金融人工智能:用Python实现AI量化交易
上QQ阅读APP看书,第一时间看更新

这是计算机程序第一次在全尺寸围棋棋盘上击败人类职业棋手,之前人们普遍认为这一壮举至少还需要十年时间。

——David Silver 等,2016 年

本章介绍人工智能(artificial intelligence,AI)领域的一般概念、思想和定义,以便于读者后续的阅读。此外,本章还为不同类型的主流学习算法提供了可以直接使用的示例。1.1 节从广阔的“算法”视角,对人工智能语境下的数据类型、学习类型和问题类型进行分类。本章还会介绍无监督学习和强化学习的相关示例。从 1.2 节开始,我们将直接进入神经网络的世界。神经网络不仅是本书后几章内容的核心基础,而且已被证明是人工智能领域目前最强大的算法之一。1.3 节会讨论数据量和数据多样性在人工智能领域中的重要性。

本节介绍与本书相关的人工智能基本概念,还会讨论与人工智能这一术语相关的数据类型、学习类型、问题类型和方法类型。Alpaydin(2016)为本节中简略覆盖的各个主题提供了非正式的介绍,以及许多示例。

数据通常有两个主要组成部分。

特征数据

  特征数据(或称输入数据)是输入到算法中的数据。举例来说,在金融领域,特征数据可以是潜在债务人的收入和储蓄。

标签数据

  标签数据(或称输出数据)是需要学习的算法输出,比如可以利用监督学习算法来学习如何通过特征数据计算出给定的标签数据。在金融领域,标签数据可以是一个潜在借款方债务人的信誉度。

学习算法主要有以下 3 类。

监督学习

  监督学习(supervised learning,SL)从包含特征值(输入值)和标签值(输出值)的样本数据集中进行学习。1.2 节会介绍此类算法的示例,比如普通最小二乘(OLS)回归和神经网络。监督学习的目的是学习输入值和输出值之间的关系。在金融领域,可以训练此类算法来预测潜在债务人是否信誉良好。就本书而言,监督学习是最重要的算法类型。

无监督学习

  无监督学习(unsupervised learning,UL)从仅包含特征值(输入值)的样本数据集中进行学习,目的通常是在数据中发现结构信息。这类算法通常基于给定的参数设置,对输入数据集进行学习。聚类算法就属于无监督学习算法。在金融领域,无监督学习算法可以用来对股票进行分组。

强化学习

  强化学习(reinforcement learning,RL)通过从反复试验中不断试错来学习,并根据收到的奖励和惩罚来更新最佳行动策略。强化学习算法可以用于需要连续采取行动并且立即获得奖励的环境,比如用于计算机游戏中。

后文会详细讨论监督学习,这里将采用一些简明示例来说明无监督学习和强化学习。

01. 无监督学习

简而言之,均值()聚类算法将 个观测值分为 个类别,每个观测值归属于距离最近的类均值(中心)所属的类别。以下 Python 代码会生成用来聚类的样本数据。图 1-1 对聚类的样本数据进行了可视化展示,可以看出,此处使用的 scikit-learn 包中的 KMeans 算法完美地识别了数据中的聚类类别,图中的点按照算法学到的所属类别进行了着色。1

In [1]: import numpy as np
import pandas as pd
from pylab import plt, mpl
plt.style.use('seaborn')
mpl.rcParams['savefig.dpi'] = 300
mpl.rcParams['font.family'] = 'serif'
np.set_printoptions(precision=4, suppress=True)
In [2]: from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
In [3]: x, y = make_blobs(n_samples=100, centers=4,
random_state=500, cluster_std=1.25)  ➊
In [4]: model = KMeans(n_clusters=4, random_state=0)  ➋
In [5]: model.fit(x)  ➌
Out[5]: KMeans(n_clusters=4, random_state=0)
In [6]: y_ = model.predict(x)  ➍
In [7]: y_  ➎
Out[7]: array([3, 3, 1, 2, 1, 1, 3, 2, 1, 2, 2, 3, 2, 0, 0, 3, 2, 0, 2, 0, 0, 3,
1, 2, 1, 1, 0, 0, 1, 3, 2, 1, 1, 0, 1, 3, 1, 3, 2, 2, 2, 1, 0, 0,
3, 1, 2, 0, 2, 0, 3, 0, 1, 0, 1, 3, 1, 2, 0, 3, 1, 0, 3, 2, 3, 0,
1, 1, 1, 2, 3, 1, 2, 0, 2, 3, 2, 0, 2, 2, 1, 3, 1, 3, 2, 2, 3, 2,
0, 0, 0, 3, 3, 3, 3, 0, 3, 1, 0, 0], dtype=int32)
In [8]: plt.figure(figsize=(10, 6))
plt.scatter(x[:, 0], x[:, 1], c=y_, cmap='coolwarm');

➊ 生成聚类用的样本数据集。

➋ 初始化 KMeans 模型对象,并指定类别数量。

➌ 用样本数据训练模型。

➍ 用训练好的模型生成预测结果。

➎ 预测结果为数字 0~3,每一个数字代表一个类别。

图 1-1:无监督聚类学习

一旦训练了如 KMeans 之类的算法,它就可以预测新样本(之前未见过的样本)所属的类别。假设我们在描述银行潜在债务人和实际债务人的特征数据集上训练这种算法,它可以通过生成两个类别来了解潜在债务人的信誉度,将新的潜在债务人归类为两个类别之一:“信誉良好”与“信誉不佳”。

 

02. 强化学习

下面的示例基于《抛硬币》游戏,在该游戏中,硬币有 80%的时间正面朝上,有 20%的时间背面朝上。这个抛硬币游戏偏重于强调学习算法相较于随机基线算法的性能改进。在基线算法中,算法在正面和背面中随机下注,平均每 100 次下注可以赢 50 次,即总奖励值为 50。

In [9]: ssp = [1, 1, 1, 1, 0]  ➊
In [10]: asp = [1, 0]  ➋
In [11]: def epoch():
tr = 0
for _ in range(100):
a = np.random.choice(asp)  ➌
s = np.random.choice(ssp)  ➍
if a == s:
tr += 1  ➎
return tr
In [12]: rl = np.array([epoch() for _ in range(15)])  ➏
rl
Out[12]: array([53, 55, 50, 48, 46, 41, 51, 49, 50, 52, 46, 47, 43, 51, 52])
In [13]: rl.mean()  ➐
Out[13]: 48.93333333333333

➊ 定义状态空间(1= 正面,0= 背面)。

➋ 定义动作空间(1= 押注正面,0= 押注背面)。

➌ 从动作空间中随机选取一个动作。

➍ 从状态空间中随机选取一个状态。

➎ 如果押注正确,则奖励值加 1。

➏ 执行多轮实验,每轮实验押注 100 次。

➐ 计算每轮实验的奖励值。

强化学习尝试从采取行动后观察到的结果中学习,通常基于奖励。为简单起见,以下学习算法仅追踪每个回合中观察到的状态,并将观察到的状态附加到动作空间 list 对象中。通过这种方式,算法可以学习游戏中的正反面出现的概率偏差,虽然不能学得与游戏设置完全一致,但也能比较好地进行。通过从更新后的动作空间中随机采样,可以自然而然地反映出游戏中的正反面出现的概率偏差,也即正面出现的概率偏高。随着采样次数的不断增加,最终平均大约 80%的时间会选择正面。平均总奖励约为 65,这反映了学习算法相较于随机基线算法的性能改进。

In [14]: ssp = [1, 1, 1, 1, 0]
In [15]: def epoch():
tr = 0
asp = [0, 1] ➊
for _ in range(100):
a = np.random.choice(asp)
s = np.random.choice(ssp)
if a == s:
tr += 1
asp.append(s) ➋
return tr
In [16]: rl = np.array([epoch() for _ in range(15)])
rl
Out[16]: array([64, 65, 77, 65, 54, 64, 71, 64, 57, 62, 69, 63, 61, 66, 75])
In [17]: rl.mean()
Out[17]: 65.13333333333334

➊ 每一轮游戏开始前重置动作空间。

➋ 将观察到的状态加入动作空间。

1 有关详细信息,请参阅 sklearn.cluster.KMeans 和 VanderPlas(2017)的第 5 章。

根据标签数据的类型和需要解决的问题,有以下两种比较重要的学习任务。

估计

  估计(也被称为近似或者回归)是指标签数据是(连续的)实数值的情况,在计算机中通常表示为浮点数。

分类

  分类是指标签数据由有限数量的分组或者类别组成的情况,这些类别通常由离散值(正自然数)表示,在计算机中表示为整数。

后续章节将分别提供两种任务的示例。

最后再介绍 3 个主要术语以及它们之间的差别,以便在后续章节中使用。

人工智能

  人工智能除了涵盖之前定义的所有类型的学习算法,还包括更多类型,比如专家系统。

机器学习

  机器学习(machine learning,ML)泛指基于一个算法和评估指标从数据集中学习数据之间关系和其他信息的一类学科。例如,在给定需要估计输出的标签值以及基于算法的预测值时,一个成功的评估指标可能是均方误差(MSE)。机器学习是人工智能的子集。

深度学习

  深度学习(deep learning,DL)包含所有基于神经网络的算法。深度在这里表示神经网络具有多个隐藏层。深度学习是机器学习的子集,因此也是人工智能的子集。

事实证明,深度学习适用的问题领域非常广泛。它适用于估计任务和分类任务,以及强化学习任务。在许多情况下,基于深度学习的方法比其他算法(比如逻辑回归或支持向量机等核方法)性能更好。2 这也是本书主要关注深度学习的原因。书中使用的深度学习方法包括密集神经网络(dense neural network,DNN)、循环神经网络(recurrent neural netwok,RNN)和卷积神经网络(convolutional neural network,CNN)。在后续章节中,尤其是第三部分,我们将针对深度学习的细节进行更多的介绍。

2 有关详细信息,请参阅 VanderPlas(2017)的第 5 章。

前文对人工智能算法进行了一般性的概述,本节将就神经网络展开讨论。我们这里用一个简单的示例说明,与 OLS 回归等传统的统计方法相比,神经网络的不同之处。该示例从一个数学函数开始,先使用线性回归的方式进行估计(或函数逼近),然后再应用神经网络方式进行估计。这里采用的是一种监督学习方法,其中的任务是根据特征数据估计标签数据。同时,本节还会演示如何在分类问题中使用神经网络。

假定数学函数如下:

这个函数将输入值 转换为输出值 ,或者将一系列输入值 转换为一系列输出值 。以下 Python 代码将这个数学函数实现为 Python 函数,并创建了一组输入值和输出值。图 1-2 绘制了输出值与输入值的关系图。

In [18]: def f(x):
return 2 * x ** 2 - x ** 3 / 3  ➊
In [19]: x = np.linspace(-2, 4, 25)  ➋
x  ➋
Out[19]: array([-2.  , -1.75, -1.5 , -1.25, -1.  , -0.75, -0.5 , -0.25,  0.  ,
0.25,  0.5 ,  0.75,  1.  ,  1.25,  1.5 ,  1.75,  2.  ,  2.25,
2.5 ,  2.75,  3.  ,  3.25,  3.5 ,  3.75,  4.  ])
In [20]: y = f(x)  ➌
y  ➌
Out[20]: array([10.6667,  7.9115,  5.625 ,  3.776 ,  2.3333,  1.2656,  0.5417,
0.1302,  0.    ,  0.1198,  0.4583,  0.9844,  1.6667,  2.474 ,
3.375 ,  4.3385,  5.3333,  6.3281,  7.2917,  8.1927,  9.    ,
9.6823, 10.2083, 10.5469, 10.6667])
In [21]: plt.figure(figsize=(10, 6))
plt.plot(x, y, 'ro');

➊ 数学函数的 Python 函数形式。

➋ 输入值。

➌ 输出值。

图 1-2:输出值与输入值的关系图

在示例函数的数学形式表达中,函数排在第一位,输入数据排在第二位,输出数据排在第三位。而在统计学习中的表达顺序则是不同的。假设同样是给定上述输入值 和输出值 ,也即样本(数据),在统计回归中将问题定义为找到一个函数,尽可能地逼近输入值(也称为自变量)和输出值(也称为因变量)之间的函数关系。

这里以一个简单的 OLS 线性回归为例,假定输入值和输出值之间的函数关系是线性的,需要求解的问题是为以下线性方程式找到最优参数

对于给定的输入值 和输出值 ,最优解将使得实际输出值与近似输出值之间的 MSE 最小:

对于简单的线性回归,可以通过公式计算出最优参数 的解析解,如以下公式所示,变量上的横线代表样本均值:

以下 Python 代码通过线性估计(逼近)输出值来计算最优参数值,同时在样本数据上绘制线性回归线,如图 1-3 所示。通过较高的 MSE 值可以看出,线性回归方法在逼近函数关系时效果不佳。

In [22]: beta = np.cov(x, y, ddof=0)[0, 1] / np.var(x)  ➊
beta  ➊
Out[22]: 1.0541666666666667
In [23]: alpha = y.mean() - beta * x.mean()  ➋
alpha  ➋
Out[23]: 3.8625000000000003
In [24]: y_ = alpha + beta * x  ➌
In [25]: MSE = ((y - y_) ** 2).mean()  ➍
MSE ➍
Out[25]: 10.721953125
In [26]: plt.figure(figsize=(10, 6))
plt.plot(x, y, 'ro', label='sample data')
plt.plot(x, y_, lw=3.0, label='linear regression')
plt.legend();

➊ 计算 的最优解。

➋ 计算 的最优解。

➌ 计算估计输出值。

➍ 根据估计输出值计算 MSE。

图 1-3:样本数据和线性回归线3

3 为了便于读者理解,本书大部分图片中的英文翻译成了中文。——译者注

如何才能改进(降低)MSE 值,甚至将其降低至 0,以达到“完美估计”?当然,OLS 回归不限于用来近似简单的线性关系。除了常数项和一次项,高次项也可以作为回归的基函数被加入进来。为了证明这一点,可以将图 1-4 所示的回归结果与用来创建该图的代码进行对比。使用二次项和三次项作为基函数所带来的改进是显而易见的,同时我们可以通过计算 MSE 值来从数值上验证这一点。对于不超过三次项的基函数,都可以通过包含二次项和三次项的 OLS 回归进行完美逼近并完美恢复函数关系。

In [27]: plt.figure(figsize=(10, 6))
plt.plot(x, y, 'ro', label='sample data')
for deg in [1, 2, 3]:
reg = np.polyfit(x, y, deg=deg)  ➊
y_ = np.polyval(reg, x)  ➋
MSE = ((y - y_) ** 2).mean()  ➌
print(f'deg={deg} | MSE={MSE:.5f}')
plt.plot(x, np.polyval(reg, x), label=f'deg={deg}')
plt.legend();
deg=1 | MSE=10.72195
deg=2 | MSE=2.31258
deg=3 | MSE=0.00000
In [28]: reg  ➍
Out[28]: array([-0.3333,  2.    ,  0.    , -0.    ])

➊ 回归训练。

➋ 回归预测。

➌ 计算 MSE。

➍ 最优参数值。

图 1-4:样本数据和 OLS 回归线

我们可以利用对于所逼近函数具体形式的了解,在回归中添加更多的基函数来达到“完美逼近”,也就是说,通过 OLS 回归可以分别恢复原始函数中二次项和三次项系数的精确值。

然而,并不是所有关系都是简单的线性关系或者高阶线性关系,这时就需要借助神经网络(neural network,NN)等方法来进行建模。无须赘述,神经网络可以在不需要知道函数关系具体形式的条件下近似各种函数关系。

01.scikit-learn

以下 Python 代码使用了 scikit-learn 库中的 MLPRegressor 类,该类可用 DNN 进行回归估计。DNN 有时也被称为多层感知器(multi-layer perceptron,MLP)。4 如图 1-5 中的 MSE 所示,结果并不完美,但是对一个配置简单的模型来说,效果已经非常不错了。

In [29]: from sklearn.neural_network import MLPRegressor
In [30]: model = MLPRegressor(hidden_layer_sizes=3 * [256],
learning_rate_init=0.03,
max_iter=5000)  ➊
In [31]: model.fit(x.reshape(-1, 1), y)  ➋
Out[31]: MLPRegressor(hidden_layer_sizes=[256, 256, 256], learning_rate_init=0.03,
max_iter=5000)
In [32]: y_ = model.predict(x.reshape(-1, 1))  ➌
In [33]: MSE = ((y - y_) ** 2).mean()
MSE
Out[33]: 0.021662355744355866
In [34]: plt.figure(figsize=(10, 6))
plt.plot(x, y, 'ro', label='sample data')
plt.plot(x, y_, lw=3.0, label='dnn estimation')
plt.legend();

➊ 实例化 MLPRegressor 对象。

➋ 拟合或学习步骤。

➌ 预测步骤。

图 1-5:样本数据和基于神经网络的估计

仅对图 1-4 和图 1-5 中的结果进行观察,你可能会认为两种方法差异不大。但是,需要强调一个根本的区别:尽管 OLS 回归方法(如简单线性回归所示)基于确定性的计算逻辑是基于给定的数值和参数进行计算,但神经网络方法依赖于有一定随机性的增量学习。神经网络首先随机初始化一组参数(神经网络中的权重),然后根据神经网络输出值与样本输出值之间的差异对参数逐步进行调整。这种方法使得我们可以通过增量的方式逐步重新训练(更新)神经网络。

 

02.Keras

下一个示例使用了 Keras 深度学习软件包中的序列模型 Sequential5 对该模型每轮拟合或训练进行 100 次迭代,共重复 5 轮。每轮训练之后,我们将更新并绘制由神经网络预测的近似值。图 1-6 显示了随着每一轮训练的近似值的准确率逐渐提高,MSE 值逐渐降低。与之前的模型相似,最终结果并不完美,但是鉴于模型的简单性,它还是不错的。

In [35]: import tensorflow as tf
tf.random.set_seed(100)
In [36]: from keras.layers import Dense
from keras.models import Sequential
Using TensorFlow backend.
In [37]: model = Sequential()  ➊
model.add(Dense(256, activation='relu', input_dim=1))  ➋
model.add(Dense(1, activation='linear'))  ➌
model.compile(loss='mse', optimizer='rmsprop')  ➍
In [38]: ((y - y_) ** 2).mean()
Out[38]: 0.021662355744355866
In [39]: plt.figure(figsize=(10, 6))
plt.plot(x, y, 'ro', label='sample data')
for _ in range(1, 6):
model.fit(x, y, epochs=100, verbose=False)  ➎
y_ =  model.predict(x)  ➏
MSE = ((y - y_.flatten()) ** 2).mean()  ➐
print(f'round={_} | MSE={MSE:.5f}')
plt.plot(x, y_, '--', label=f'round={_}')  ➑
plt.legend();
round=1 | MSE=3.09714
round=2 | MSE=0.75603
round=3 | MSE=0.22814
round=4 | MSE=0.11861
round=5 | MSE=0.09029

➊ 实例化 Sequential 模型对象。

➋ 添加采用整流线性单元(ReLU)激活函数的全连接层作为隐藏层。

➌ 添加线性激活的输出层。

➍ 编译模型对象。

➎ 迭代训练神经网络指定次数。

➏ 预测近似值。

➐ 计算当前的 MSE。

➑ 绘制当前的近似结果。

图 1-6:样本数据和多轮训练后得到的估计值

粗略地讲,神经网络提供了与 OLS 回归几乎一样完美的估计性能,那么为什么还要使用神经网络呢?本书稍后会提供更全面的解答,但这里我们可以通过一个不同的示例得到些许提示。

假设我们的数据不是通过预定义好的数学函数生成的,而是随机产生的特征和标签。当然,该示例仅用于说明,并不具有解释性。

以下代码会生成随机样本数据集,并创建不同的多次项 OLS 回归进行逼近。从图 1-7 显示的结果可以看出,即使是最高次项的 OLS 回归,逼近结果仍然不是很好,MSE 值相对较高。

In [40]: np.random.seed(0)
x = np.linspace(-1, 1)
y = np.random.random(len(x)) * 2 - 1
In [41]: plt.figure(figsize=(10, 6))
plt.plot(x, y, 'ro', label='sample data')
for deg in [1, 5, 9, 11, 13, 15]:
reg = np.polyfit(x, y, deg=deg)
y_ = np.polyval(reg, x)
MSE = ((y - y_) ** 2).mean()
print(f'deg={deg:2d} | MSE={MSE:.5f}')
plt.plot(x, np.polyval(reg, x), label=f'deg={deg}')
plt.legend();
deg= 1 | MSE=0.28153
deg= 5 | MSE=0.27331
deg= 9 | MSE=0.25442
deg=11 | MSE=0.23458
deg=13 | MSE=0.22989
deg=15 | MSE=0.21672

图 1-7:随机样本数据和 OLS 回归线

不出所料,OLS 回归的效果并不理想。OLS 回归假设我们可以通过有限个(基于多项式的)基函数的组合来逼近目标函数。由于样本数据集是随机生成的,因此在这种情况下,OLS 回归效果不佳。

神经网络表现如何呢?它使用起来仍然像前面的示例一样简单,逼近的结果如图 1-8 所示。

图 1-8:随机样本数据和神经网络估计

尽管预测结果并不完美,但很明显,在根据随机特征值估计随机标签值时,神经网络的性能优于 OLS 回归。然而,神经网络架构有近 200 000 个可训练的参数(权重),这提供了相对较高的灵活性,尤其是与 OLS 回归(最多使用 15+1 个参数)相比。

In [42]: model = Sequential()
model.add(Dense(256, activation='relu', input_dim=1))
for _ in range(3):
model.add(Dense(256, activation='relu'))  ➊
model.add(Dense(1, activation='linear'))
model.compile(loss='mse', optimizer='rmsprop')
In [43]: model.summary()  ➋
Model: "sequential_2"
_________________________________________________________________
Layer (type)                  Output Shape             Param #
=================================================================
dense_3 (Dense)               (None, 256)              512
_________________________________________________________________
dense_4 (Dense)               (None, 256)              65792
_________________________________________________________________
dense_5 (Dense)               (None, 256)              65792
_________________________________________________________________
dense_6 (Dense)               (None, 256)              65792
_________________________________________________________________
dense_7 (Dense)               (None, 1)                 257
=================================================================
Total params: 198,145
Trainable params: 198,145
Non-trainable params: 0
_________________________________________________________________
In [44]: %%time
plt.figure(figsize=(10, 6))
plt.plot(x, y, 'ro', label='sample data')
for _ in range(1, 8):
model.fit(x, y, epochs=500, verbose=False)
y_ =  model.predict(x)
MSE = ((y - y_.flatten()) ** 2).mean()
print(f'round={_} | MSE={MSE:.5f}')
plt.plot(x, y_, '--', label=f'round={_}')
plt.legend();
round=1 | MSE=0.13560
round=2 | MSE=0.08337
round=3 | MSE=0.06281
round=4 | MSE=0.04419
round=5 | MSE=0.03329
round=6 | MSE=0.07676
round=7 | MSE=0.00431
CPU times: user 30.4 s, sys: 4.7 s, total: 35.1 s
Wall time: 13.6 s

➊ 添加多个隐藏层。

➋ 显示神经网络架构以及可训练参数的数量。

4 有关详细信息,请参阅 sklearn.neural_network.MLPRegressor。如需了解更多背景信息,请参阅 Goodfellow 等(2016)的第 6 章。

5 有关详细信息,请参阅 Chollet(2017)的第 3 章。

神经网络也可以很容易地用于分类任务。考虑以下基于 Keras 实现神经网络分类的 Python 代码。二元特征数据和二元标签数据是随机生成的。建模方面的主要调整是将输出层的激活函数从 linear 更改为 sigmoid,后面的章节会对此作更详细的介绍。虽然分类效果并不完美,但是也达到了很高的准确率。图 1-9 显示了准确率(表示为正确结果与所有标签值之间的关系)如何随着训练轮数而变化。准确率一开始很低,然后逐步提高,尽管不一定每一步都会有所提高。

In [45]: f = 5
n = 10
In [46]: np.random.seed(100)
In [47]: x = np.random.randint(0, 2, (n, f))  ➊
x  ➊
Out[47]: array([[0, 0, 1, 1, 1],
[1, 0, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 1, 0, 0, 1],
[0, 1, 0, 0, 0],
[1, 1, 1, 0, 0],
[1, 0, 0, 1, 1],
[1, 1, 1, 0, 0],
[1, 1, 1, 1, 1],
[1, 1, 1, 0, 1]])
In [48]: y = np.random.randint(0, 2, n)  ➋
y  ➋
Out[48]: array([1, 1, 0, 0, 1, 1, 0, 1, 0, 1])
In [49]: model = Sequential()
model.add(Dense(256, activation='relu', input_dim=f))
model.add(Dense(1, activation='sigmoid'))  ➌
model.compile(loss='binary_crossentropy', optimizer='rmsprop',
metrics=['acc'])  ➍
In [50]: model.fit(x, y, epochs=50, verbose=False)
Out[50]: <keras.callbacks.callbacks.History at 0x7fde09dd1cd0>
In [51]: y_ = np.where(model.predict(x).flatten() > 0.5, 1, 0)
y_
Out[51]: array([1, 1, 0, 0, 0, 1, 0, 1, 0, 1], dtype=int32)
In [52]: y == y_  ➎
Out[52]: array([ True,  True,  True,  True, False,  True,  True,  True,  True,
True])
In [53]: res = pd.DataFrame(model.history.history)  ➏
In [54]: res.plot(figsize=(10, 6));  ➏

➊ 创建随机特征数据。

➋ 创建随机标签数据。

➌ 定义输出层的激活函数为 sigmoid

➍ 定义损失函数为 binary_crossentropy6

6 损失函数用来计算和衡量神经网络(或其他机器学习算法)的预测错误。二元交叉熵(binary crossentropy)适用于二元分类问题的损失函数,而 MSE 则适用于估计问题。

➎ 将预测值与标签数据进行比较。

➏ 绘制每轮训练的损失函数和准确率值。

图 1-9:分类准确率及损失与训练轮数的关系

本节中的示例说明了与 OLS 回归相比,神经网络的一些基本特征。

问题无关性

  在给定一组特征值的情况下,神经网络方法的性能与需要估计或者分类的具体标签值是无关的。而统计方法(比如 OLS 回归)可能对较小的一组问题表现良好,对其他问题则表现不太好或根本没有效果。

增量学习

  给定一个用来度量成功的目标,神经网络中的最佳权重是基于随机初始化和增量改进而逐步学习得到的。这些增量改进是在考虑预测值和样本标签值之间的差异后,通过神经网络反向传播权重更新来实现的。

通用函数逼近器

  有严格的数学定理表明神经网络(即使只有一个隐藏层)几乎可以逼近任何函数。7

7 参阅 Kratsios(2019)。

这些特点或许可以解释为什么本书将神经网络放在所用算法的核心位置,第 2 章会在这方面进行更多的讨论。

 神经网络

神经网络擅长学习输入数据和输出数据之间的关系。它们可以应用于许多问题类型,比如在存在复杂关系的情况下进行估计,或者用于传统的统计方法并不适合的分类问题。

上一节末尾的例子表明神经网络能够很好地解决分类问题。具有一个隐藏层的神经网络在给定的数据集或样本内达到了高度的准确率。然而,神经网络的预测能力如何呢?这在很大程度上取决于可用于训练神经网络的数据量和种类。另一个基于更大数据集的数值示例将说明这一点。

现在考虑一个与之前分类示例中使用的随机样本类似的数据集,但其具有更多特征和更多样本。人工智能中使用的大多数算法是关于模式识别的。在下面的 Python 代码中,二值化特征的数量定义了算法可以学习的可能的模式的数量。鉴于标签数据也是二值化的,算法会尝试了解在给定某种模式的情况下(比如特征为 [0, 0, 1, 1, 1, 1, 0, 0, 0, 0]),标签是 0 的可能性更大,还是 1 的可能性更大。因为所有数字都是以相等的概率随机选择的,所以除了观察到无论在什么(随机)模式下,标签 01 的可能性都相等这一事实之外,没有太多可学习的。因此,基线预测算法在大约 50% 的时间内应该是准确的,无论它呈现什么(随机)模式。

In [55]: f = 10
n = 250
In [56]: np.random.seed(100)
In [57]: x = np.random.randint(0, 2, (n, f))  ➊
x[:4]  ➊
Out[57]: array([[0, 0, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 1, 0, 0, 1],
[0, 1, 0, 0, 0, 1, 1, 1, 0, 0],
[1, 0, 0, 1, 1, 1, 1, 1, 0, 0]])
In [58]: y = np.random.randint(0, 2, n)  ➋
y[:4]  ➋
Out[58]: array([0, 1, 0, 0])
In [59]: 2 ** f  ➌
Out[59]: 1024

➊ 特征数据。

➋ 标签数据。

➌ 模式数量。

为了简化后续某些操作和分析,原始数据被放入一个 pandas DataFrame 对象中。

In [60]: fcols = [f'f{_}' for _ in range(f)]  ➊
fcols  ➊
Out[60]: ['f0', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9']
In [61]: data = pd.DataFrame(x, columns=fcols)  ➋
data['l'] = y ➌
In [62]: data.info()  ➍
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 250 entries, 0 to 249
Data columns (total 11 columns):
#   Column  Non-Null Count  Dtype
---  ------  --------------  -----
0   f0      250 non-null    int64
1   f1      250 non-null    int64
2   f2      250 non-null    int64
3   f3      250 non-null    int64
4   f4      250 non-null    int64
5   f5      250 non-null    int64
6   f6      250 non-null    int64
7   f7      250 non-null    int64
8   f8      250 non-null    int64
9   f9      250 non-null    int64
10  l       250 non-null    int64
dtypes: int64(11)
memory usage: 21.6 KB

➊ 定义特征数据的列名。

➋ 将特征数据放入 DataFrame 对象中。

➌ 将标签数据放入同一个 DataFrame 对象中。

➍ 显示数据集的元信息。

根据以下 Python 代码的执行结果,可以确定两个主要问题:首先,并非所有模式都在样本数据集中;其次,每个观察到的模式样本量太小。即使不深入挖掘,也可以很明显地知道没有分类算法可以真正以有意义的方式了解所有可能的模式。

In [63]: grouped = data.groupby(list(data.columns))  ➊
In [64]: freq = grouped['l'].size().unstack(fill_value=0)  ➋
In [65]: freq['sum'] = freq[0] + freq[1]  ➌
In [66]: freq.head(10)  ➍
Out[66]: l                              0  1  sum
f0 f1 f2 f3 f4 f5 f6 f7 f8 f9
0  0  0  0  0  0  0  1  1  1   0  1    1
1  0  1  0   1  1    2
1   0  1    1
1  0  0  0  0   1  0    1
1   0  1    1
1  1  1   0  1    1
1  0  0  0   0  1    1
1  0   0  1    1
1  0  0  0  1  1   1  0    1
1  1  0  0   1  0    1
In [67]: freq['sum'].describe().astype(int)  ➎
Out[67]: count    227
mean       1
std        0
min        1
25%        1
50%        1
75%        1
max        2
Name: sum, dtype: int64

➊ 按列对数据进行分组。

➋ 取消标签列分组数据的折叠。

➌ 将 01 的频率相加。

➍ 显示给定特定模式的 01 的频率。

➎ 提供频率总和的统计信息。

以下 Python 代码使用了来自 scikit-learn8MLPClassifier 模型,该模型是在完整数据集上训练的。神经网络对于给定数据集中关系的刻画能力如何呢?正如样本内准确率得分所示,这种能力非常高,接近 100%,这在很大程度上是由相对较小的数据集以及相对较高的神经网络容量所导致的结果。

8 有关详细信息,请参阅 sklearn.neural_network.MLPClassifier

In [68]: from sklearn.neural_network import MLPClassifier
from sklearn.metrics import accuracy_score
In [69]: model = MLPClassifier(hidden_layer_sizes=[128, 128, 128],
max_iter=1000, random_state=100)
In [70]: model.fit(data[fcols], data['l'])
Out[70]: MLPClassifier(hidden_layer_sizes=[128, 128, 128], max_iter=1000,
random_state=100)
In [71]: accuracy_score(data['l'], model.predict(data[fcols]))
Out[71]: 0.952

但是训练好的神经网络的预测能力如何呢?为此,可以将给定的数据集拆分为训练数据集和测试数据集。模型仅在训练数据集上进行训练,然后就其对测试数据集的预测能力进行测试。和以前一样,训练好的神经网络在样本内(训练数据集)的准确率非常高。然而,它比测试数据集上的基线算法差 10 个百分点以上。

In [72]: split = int(len(data) * 0.7)  ➊
In [73]: train = data[:split]  ➊
test = data[split:]  ➊
In [74]: model.fit(train[fcols], train['l'])  ➋
Out[74]: MLPClassifier(hidden_layer_sizes=[128, 128, 128], max_iter=1000,
random_state=100)
In [75]: accuracy_score(train['l'], model.predict(train[fcols]))  ➌
Out[75]: 0.9714285714285714
In [76]: accuracy_score(test['l'], model.predict(test[fcols]))  ➍
Out[76]: 0.38666666666666666

➊ 将数据拆分为训练数据集和测试数据集。

➋ 仅在训练数据集上训练模型。

➌ 报告样本内(训练数据集)的准确率。

➍ 报告样本外(测试数据集)的准确率。

粗略地说,由于两个已知的问题,仅在小数据集上训练的神经网络会学习到错误的关系。这些问题对于样本内的关系学习影响不大。相反,一般来说,数据集越小,越容易学习样本内关系。然而,当使用训练好的神经网络进行样本外预测时,这些问题会带来麻烦。

幸运的是,通常有一种明确的方法可以解决这种情况下的问题,即创建更大的数据集(或者说更多的数据)。面对现实世界的问题,理论上确实可以这样做。然而,从实际的角度来看,我们并不总是能够获得这种更大的数据集,而且它们通常也不那么容易生成。但是,基于本节的示例,确实可以通过代码轻松创建更大的数据集。

以下 Python 代码显著增加了初始样本数据集中的样本数量。结果表明,训练好的神经网络的预测准确率提高了 10 个百分点以上,达到约 50% 的水平,这也是我们基于标签数据的生成逻辑所预期的。它现在的性能与一个标准的基线算法相符合。

In [77]: factor = 50
In [78]: big = pd.DataFrame(np.random.randint(0, 2, (factor * n, f)),
columns=fcols)
In [79]: big['l'] = np.random.randint(0, 2, factor * n)
In [80]: train = big[:split]
test = big[split:]
In [81]: model.fit(train[fcols], train['l'])
Out[81]: MLPClassifier(hidden_layer_sizes=[128, 128, 128], max_iter=1000,
random_state=100)
In [82]: accuracy_score(train['l'], model.predict(train[fcols]))  ➊
Out[82]: 0.9657142857142857
In [83]: accuracy_score(test['l'], model.predict(test[fcols]))  ➋
Out[83]: 0.5043407707910751

➊ 样本内的预测准确率(训练数据集)。

➋ 样本外的预测准确率(测试数据集)。

如接下来的代码所示,我们可以通过对可用数据的快速分析,来解释为什么预测准确率有所提高。首先,因为数据量足够大,所以所有可能的模式现在都会出现在数据集中。其次,所有模式在数据集中的平均频率都在 10 以上。换句话说,对每一种可能的模式,神经网络都可以观测和学习多次。这使得神经网络可以“学习”到,标签 01 对于所有模式的可能性是相等的。当然,这是一种相当复杂的学习方式,但它很好地说明了一个事实:在基于神经网络的设置下,相对较小的数据集可能是不够的。

In [84]: grouped = big.groupby(list(data.columns))
In [85]: freq = grouped['l'].size().unstack(fill_value=0)
In [86]: freq['sum'] = freq[0] + freq[1]  ➊
In [87]: freq.head(6)
Out[87]: l                              0  1  sum
f0 f1 f2 f3 f4 f5 f6 f7 f8 f9
0  0  0  0  0  0  0  0  0  0  10  9   19
1   5  4    9
1  0   2  5    7
1   6  6   12
1  0  0   9  8   17
1   7  4   11
In [88]: freq['sum'].describe().astype(int)  ➋
Out[88]: count    1024
mean       12
std         3
min         2
25%        10
50%        12
75%        15
max        26
Name: sum, dtype: int64

➊ 将 01 的频率相加。

➋ 显示相加后的汇总统计信息。

 数据量和多样性

在基于神经网络的预测任务中,用于训练神经网络的可用数据的数据量和多样性对其预测性能起决定性作用。本节中的假设示例表明,相较于在数据量较大且具有多样性的数据集上训练的神经网络,在数据量较小且较为单一的数据集上训练的相同神经网络,其表现会差 10 个百分点以上。考虑到人工智能从业者和公司经常为小到 1/10 个百分点的改进而奋斗,10 个百分点可以被认为是巨大的差异了。

更大的数据集和大数据集有什么区别?十多年来,大数据一词已被用于表示许多事物。就本书的目的而言,我们可以将大数据定义为在数量、种类和速度方面足够丰富的大数据集,它可以对人工智能算法进行适当的训练,从而使得与基线算法相比,人工智能算法在预测任务上表现得更好。

之前使用的较大的数据集在实际应用中仍然很小。但是,它足以完成指定的目标。训练人工智能算法所需要的数据集的数量和种类主要由特征及标签数据的结构和特点所决定。

在这种情况下,假设零售银行会使用基于神经网络的信用评分分类方法。给定内部数据,负责的数据科学家设计了 25 个分类特征,每个特征都可以采用 8 个不同的值,由此产生的模式数量是天文数字。

In [89]: 8 ** 25
Out[89]: 37778931862957161709568

很明显,没有一个数据集可以让神经网络观测到每一种模式。9 幸运的是,在实践中,对神经网络来说没有必要根据常规用户、违约用户和被拒绝用户的数据分别了解用户的信誉度。一般来说,也没有必要对每个潜在债务人的信誉度做出“好”的预测。

9 即使有这样的数据集,当前的计算技术也不足以基于此类数据集对神经网络进行建模和训练。基于此背景,第 2 章会讨论硬件对人工智能的重要性。

这是由于多种原因造成的。这里仅举几个例子。首先是有效特征空间。并非每种模式都是实际存在的,有些模式可能根本不存在,或者完全不实际。其次是特征重要性。并非所有特征都是同等重要的,我们可以据此减少有效特征的数量,从而减少可能的模式数量。最后是特征冗余值。例如,编号为 7 的特征取值为 45 时可能根本没有区别,从而进一步减少了相关模式的数量。

在本书中,人工智能包含能够从数据中学习诸如关系、规则、概率的方法,技术,算法等。本书重点关注监督学习算法,比如用于估计和分类的算法。在算法方面,核心是神经网络和深度学习方法。

本书的核心主题是将神经网络应用于金融领域的一个核心问题:对未来市场走势的预测。更具体地说,问题可能是预测股票指数或货币对的汇率变动方向。对未来市场方向的预测(目标水平或价格是上涨还是下跌)可以轻松转换为分类问题。

在深入探讨核心主题之前,第 2 章首先讨论与超级智能技术奇点相关的一些主题。该讨论将为后续侧重于金融和人工智能在金融领域的应用的章节提供有用的背景知识。