Python 机器学习示例第四版(二)

原文:annas-archive.org/md5/143aadf706a620d20916160319321b2e

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章:使用逻辑回归预测在线广告点击率

在上一章中,我们使用基于树的算法预测了广告点击率。在本章中,我们将继续探索解决数十亿美元问题的旅程。我们将重点学习一种非常(可能是最)可扩展的分类模型——逻辑回归。我们将探讨逻辑函数是什么,如何训练逻辑回归模型,如何为模型添加正则化,以及适用于非常大数据集的逻辑回归变种。除了在分类中的应用外,我们还将讨论如何使用逻辑回归和随机森林模型来选择重要特征。你不会感到无聊,因为我们将有很多从零开始的实现,使用 scikit-learn 和 TensorFlow。

在本章中,我们将讨论以下主题:

  • 将分类特征转换为数值型特征——独热编码和原始编码

  • 使用逻辑回归对数据进行分类

  • 训练逻辑回归模型

  • 使用在线学习训练大规模数据集

  • 处理多类分类问题

  • 使用 TensorFlow 实现逻辑回归

将分类特征转换为数值型特征——独热编码和原始编码

第三章使用基于树的算法预测在线广告点击率中,我提到过独热编码如何将分类特征转换为数值特征,以便在 scikit-learn 和 TensorFlow 的树算法中使用。如果我们使用独热编码将分类特征转换为数值特征,就不会将算法的选择局限于能够处理分类特征的基于树的算法。

我们能想到的最简单的解决方案是,将具有 k 个可能值的分类特征映射到一个数值特征,数值范围从 1 到 k。例如,[Tech, Fashion, Fashion, Sports, Tech, Tech, Sports] 变成 [1, 2, 2, 3, 1, 1, 3]。然而,这将引入顺序特性,例如 Sports 大于 Tech,以及距离特性,例如 Sports 距离 FashionTech 更近。

相反,独热编码将分类特征转换为 k 个二进制特征。每个二进制特征表示是否存在相应的可能值。因此,前面的示例变成了以下内容:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_01.png

图 4.1:使用独热编码将用户兴趣转换为数值特征

之前,我们使用了来自 scikit-learn 的 OneHotEncoder 将字符串矩阵转换为二进制矩阵,但在这里,让我们来看看另一个模块 DictVectorizer,它也提供了高效的转换。它将字典对象(分类特征:值)转换为独热编码向量。

例如,看看以下代码,它对包含分类特征的字典列表执行独热编码:

>>> from sklearn.feature_extraction import DictVectorizer
>>> X_dict = [{'interest': 'tech', 'occupation': 'professional'},
...           {'interest': 'fashion', 'occupation': 'student'},
...           {'interest': 'fashion','occupation':'professional'},
...           {'interest': 'sports', 'occupation': 'student'},
...           {'interest': 'tech', 'occupation': 'student'},
...           {'interest': 'tech', 'occupation': 'retired'},
...           {'interest': 'sports','occupation': 'professional'}]
>>> dict_one_hot_encoder = DictVectorizer(sparse=False)
>>> X_encoded = dict_one_hot_encoder.fit_transform(X_dict)
>>> print(X_encoded)
[[ 0\.  0\. 1\. 1\.  0\. 0.]
 [ 1\.  0\. 0\. 0\.  0\. 1.]
 [ 1\.  0\. 0\. 1\.  0\. 0.]
 [ 0\.  1\. 0\. 0\.  0\. 1.]
 [ 0\.  0\. 1\. 0\.  0\. 1.]
 [ 0\.  0\. 1\. 0\.  1\. 0.]
 [ 0\.  1\. 0\. 1\.  0\. 0.]] 

我们也可以通过执行以下操作查看映射:

>>> print(dict_one_hot_encoder.vocabulary_)
{'interest=fashion': 0, 'interest=sports': 1,
'occupation=professional': 3, 'interest=tech': 2,
'occupation=retired': 4, 'occupation=student': 5} 

当处理新数据时,我们可以通过以下方式进行转换:

>>> new_dict = [{'interest': 'sports', 'occupation': 'retired'}]
>>> new_encoded = dict_one_hot_encoder.transform(new_dict)
>>> print(new_encoded)
[[ 0\. 1\. 0\. 0\. 1\. 0.]] 

我们可以像这样将编码后的特征逆向转换回原始特征:

>>> print(dict_one_hot_encoder.inverse_transform(new_encoded))
[{'interest=sports': 1.0, 'occupation=retired': 1.0}] 

需要注意的一点是,如果在新数据中遇到一个新的(训练数据中未出现过的)类别,它应该被忽略(否则,编码器会抱怨未见过的类别值)。DictVectorizer会隐式处理这个问题(而OneHotEncoder需要指定ignore参数):

>>> new_dict = [{'interest': 'unknown_interest',
               'occupation': 'retired'},
...             {'interest': 'tech', 'occupation':
               'unseen_occupation'}]
>>> new_encoded = dict_one_hot_encoder.transform(new_dict)
>>> print(new_encoded)
[[ 0\.  0\. 0\. 0\.  1\. 0.]
 [ 0\.  0\. 1\. 0\.  0\. 0.]] 

有时,我们更倾向于将具有k个可能值的类别特征转换为一个数值特征,取值范围从1k。这就是顺序编码,我们进行顺序编码是为了在学习中利用顺序或排名知识;例如,大、中、小分别变为 3、2 和 1;好和坏分别变为 1 和 0,而独热编码无法保留这样的有用信息。我们可以通过使用pandas轻松实现顺序编码,例如:

>>> import pandas as pd
>>> df = pd.DataFrame({'score': ['low',
...                              'high',
...                              'medium',
...                              'medium',
...                              'low']})
>>> print(df)
    score
0     low
1    high
2  medium
3  medium
4     low
>>> mapping = {'low':1, 'medium':2, 'high':3}
>>> df['score'] = df['score'].replace(mapping)
>>> print(df)
   score
0      1
1      3
2      2
3      2
4      1 

我们根据定义的映射将字符串特征转换为顺序值。

最佳实践

处理由独热编码导致的高维度可能是具有挑战性的。这可能会增加计算复杂性或导致过拟合。以下是一些在使用独热编码时处理高维度的策略:

  • 特征选择:这可以减少独热编码特征的数量,同时保留最有信息量的特征。

  • 降维:它将高维特征空间转换为低维表示。

  • 特征聚合:与其为每个类别单独进行独热编码,不如考虑将具有相似特征的类别进行聚合。例如,将罕见类别归为“其他”类别。

我们已经讨论了如何将类别特征转换为数值特征。接下来,我们将讨论逻辑回归,这是一种只接受数值特征的分类器。

使用逻辑回归进行数据分类

在上一章中,我们仅使用了来自 4000 万样本中的前 30 万个样本来训练基于树的模型。之所以这样做,是因为在大数据集上训练树模型的计算代价和时间开销非常大。由于我们不再局限于直接接受类别特征的算法(这要感谢独热编码),我们应该转向一种适合大数据集的高可扩展性算法。如前所述,逻辑回归是最具可扩展性的分类算法之一,甚至可能是最具可扩展性的。

开始使用逻辑函数

在深入了解算法之前,我们先介绍一下逻辑函数(更常称为sigmoid 函数),它是该算法的核心。它基本上将输入映射到 0 到 1 之间的输出值,其定义如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_001.png

我们定义逻辑函数如下:

>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> def sigmoid(input):
...     return 1.0 / (1 + np.exp(-input)) 

接下来,我们可视化输入变量在-88之间的变化,结果如下:

>>> z = np.linspace(-8, 8, 1000)
>>> y = sigmoid(z)
>>> plt.plot(z, y)
>>> plt.axhline(y=0, ls='dotted', color='k')
>>> plt.axhline(y=0.5, ls='dotted', color='k')
>>> plt.axhline(y=1, ls='dotted', color='k')
>>> plt.yticks([0.0, 0.25, 0.5, 0.75, 1.0])
>>> plt.xlabel('z')
>>> plt.ylabel('y(z)')
>>> plt.show() 

请参考下方截图查看结果:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_02.png

图 4.2:逻辑函数

在 S 形曲线中,所有输入都会被转换到 0 到 1 的范围内。对于正输入,较大的值使得输出接近 1;对于负输入,较小的值使得输出接近 0;当输入为 0 时,输出是中点,即 0.5。

从逻辑函数跳转到逻辑回归

现在你对逻辑函数有了一些了解,映射到源自它的算法就容易多了。在逻辑回归中,函数输入z是特征的加权和。给定一个具有n个特征的数据样本x,其中 x[1], x[2], …, x[n](x代表特征向量,x = (x[1], x[2], …, x[n])),以及模型的权重(也叫系数ww表示向量(w[1], w*[2], …, w[n])),z表示如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_002.png

这里,T是转置操作符。

有时,模型会带有一个截距(也叫偏置),w[0],它表示固有的偏差或基线概率。在这种情况下,前述的线性关系变为:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_003.png

至于输出y(z)的范围在 0 到 1 之间,在算法中,它表示目标为1或正类的概率:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_004.png

因此,逻辑回归是一种概率分类器,类似于朴素贝叶斯分类器。

逻辑回归模型,或者更具体地说,其权重向量w,是通过训练数据学习得到的,目的是使得正样本尽可能接近1,负样本尽可能接近 0。在数学上,权重的训练目标是最小化定义为均方误差MSE)的成本函数,该函数衡量真实值与预测值之间差值的平方的平均值。给定m个训练样本:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_005.png

这里,y^((i))的值要么是1(正类),要么是0(负类),而需要优化的权重所对应的成本函数J(w)表示如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_006.png

然而,前述的成本函数是非凸的,这意味着在寻找最优的w时,会发现许多局部(次优)最优解,且该函数不会收敛到全局最优解。

函数和非凸函数的示例分别如图所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_03.png

图 4.3:凸函数和非凸函数的示例

在凸函数示例中,只有一个全局最优解,而在非凸函数示例中,有两个最优解。

更多关于凸函数和非凸函数的信息,请查看 web.stanford.edu/class/ee364a/lectures/functions.pdf

为了克服这个问题,在实践中,我们使用导致凸优化问题的成本函数,该函数定义如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_007.png

我们可以更详细地查看单个训练样本的成本:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_008.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_009.png

当真实值 y^((i)) = 1 时,如果模型完全自信地正确预测(正类的概率为 100%),样本成本 j0;当预测概率 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_010.png 下降时,成本 j 会增加。如果模型错误地预测正类没有任何机会,则成本无限高。我们可以通过以下方式进行可视化:

>>> y_hat = np.linspace(0.001, 0.999, 1000)
>>> cost = -np.log(y_hat)
>>> plt.plot(y_hat, cost)
>>> plt.xlabel('Prediction')
>>> plt.ylabel('Cost')
>>> plt.xlim(0, 1)
>>> plt.ylim(0, 7)
>>> plt.show() 

请参考以下图表查看最终结果:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_04.png

图 4.4:当 y=1 时逻辑回归的成本函数

相反,当真实值 y^((i)) = 0 时,如果模型完全自信地正确预测(正类的概率为 0,或负类的概率为 100%),样本成本 j0;当预测概率 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_010.png 增加时,成本 j 会增加。当模型错误地预测没有负类的可能性时,成本将变得无限高。我们可以使用以下代码进行可视化:

>>> y_hat = np.linspace(0.001, 0.999, 1000)
>>> cost = -np.log(1 - y_hat)
>>> plt.plot(y_hat, cost)
>>> plt.xlabel('Prediction')
>>> plt.ylabel('Cost')
>>> plt.xlim(0, 1)
>>> plt.ylim(0, 7)
>>> plt.show() 

下图是结果输出:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_05.png

图 4.5:当 y=0 时逻辑回归的成本函数

最小化这个替代的成本函数实际上等同于最小化基于 MSE 的成本函数。选择它而不是 MSE 版本的优点包括:

  • 它是凸的,因此可以找到最优的模型权重

  • 预测的对数和如下所示,它简化了关于权重的导数计算,稍后我们将讨论这一点:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_012.png

或者:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_013.png

  • 由于对数函数,以下成本函数也被称为对数损失,简称log loss

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_014.png

现在成本函数已经准备好,我们如何训练逻辑回归模型以最小化成本函数?让我们在下一节中看看。

训练逻辑回归模型

现在,问题如下:我们如何获得最优的 w 使得 J(w) 最小化?我们可以通过梯度下降法来实现。

使用梯度下降法训练逻辑回归模型

梯度下降(也叫最速下降法)是一种通过一阶迭代优化最小化损失函数的方法。在每次迭代中,模型参数会根据目标函数在当前点的负导数移动一个小步长。这意味着待优化的点会迭代地沿着目标函数的最小值方向向下移动。我们刚才提到的步长比例称为学习率,也叫步长。它可以用一个数学方程总结如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_015.png

在这里,左边的 w 是学习一步后的权重向量,右边的 w 是学习前的权重,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_016.png 是学习率,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_017.png 是一阶导数,梯度。

为了使用梯度下降训练逻辑回归模型,我们从成本函数 J(w) 对 w 的导数开始。这可能需要一些微积分的知识,但不用担心,我们将一步步讲解:

  1. 我们首先计算 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_018.pngw 的导数。我们在这里以 j-th 权重 w[j] 为例(注意 z=w^Tx,为简便起见我们省略了 ^((i))):

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_019.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_020.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_021.png

  1. 然后,我们按如下方式计算样本成本 J(w) 的导数:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_022.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_023.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_024.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_025.png

  1. 最后,我们按以下方式计算 m 个样本的整体成本:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_026.png

  1. 然后,我们将其推广到 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_027.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_028.png

  1. 结合前面的推导,权重可以如下更新:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_029.png

在这里,w 在每次迭代中都会更新。

  1. 经过大量迭代后,学习到的参数 w 将用于通过以下方程对新样本 x’ 进行分类:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_030.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_031.png

默认情况下,决策阈值为 0.5,但它可以是其他值。例如,在避免假阴性时,比如预测火灾发生(正类)时的警报,决策阈值可以低于 0.5,如 0.3,具体取决于我们的警觉性以及我们希望多主动地防止正类事件的发生。另一方面,当假阳性类是需要避免的情况时,例如在质量保证中预测产品成功率(正类),决策阈值可以大于 0.5,如 0.7,或者低于 0.5,具体取决于你设定的标准。

通过对基于梯度下降的训练和预测过程的深入理解,我们现在将从头实现逻辑回归算法:

  1. 我们首先定义计算预测值的函数!,使用当前的权重:

    >>> def compute_prediction(X, weights):
    ...     """
    ...     Compute the prediction y_hat based on current weights
    ...     """
    ...     z = np.dot(X, weights)
    ...     return sigmoid(z) 
    
  2. 通过这种方式,我们可以继续以梯度下降的方式更新权重函数,步骤如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_033.png

看一下以下代码:

>>> def update_weights_gd(X_train, y_train, weights,
                                           learning_rate):
...     """
...     Update weights by one step
...     """
...     predictions = compute_prediction(X_train, weights)
...     weights_delta = np.dot(X_train.T, y_train - predictions)
...     m = y_train.shape[0]
...     weights += learning_rate / float(m) * weights_delta
...     return weights 
  1. 然后,实现计算成本 J(w) 的函数:

    >>> def compute_cost(X, y, weights):
    ...     """
    ...     Compute the cost J(w)
    ...     """
    ...     predictions = compute_prediction(X, weights)
    ...     cost = np.mean(-y * np.log(predictions)
                          - (1 - y) * np.log(1 - predictions))
    ...     return cost 
    
  2. 接下来,我们通过执行以下代码将所有这些函数连接到模型训练函数中:

    • 在每次迭代中更新weights向量

    • 100次迭代(可以是其他值)打印当前的成本,以确保cost在减少,并且一切都在正确的轨道上。

它们在以下函数中实现:

>>> def train_logistic_regression(X_train, y_train, max_iter,
                                  learning_rate, fit_intercept=False):
...     """ Train a logistic regression model
...     Args:
...         X_train, y_train (numpy.ndarray, training data set)
...         max_iter (int, number of iterations)
...         learning_rate (float)
...         fit_intercept (bool, with an intercept w0 or not)
...     Returns:
...         numpy.ndarray, learned weights
...     """
...     if fit_intercept:
...         intercept = np.ones((X_train.shape[0], 1))
...         X_train = np.hstack((intercept, X_train))
...     weights = np.zeros(X_train.shape[1])
...     for iteration in range(max_iter):
...         weights = update_weights_gd(X_train, y_train,
                                       weights, learning_rate)
...         # Check the cost for every 100 (for example)      
             iterations
...         if iteration % 100 == 0:
...             print(compute_cost(X_train, y_train, weights))
...     return weights 
  1. 最后,我们使用训练好的模型预测新输入的结果,方法如下:

    >>> def predict(X, weights):
    ...     if X.shape[1] == weights.shape[0] - 1:
    ...         intercept = np.ones((X.shape[0], 1))
    ...         X = np.hstack((intercept, X))
    ...     return compute_prediction(X, weights) 
    

实现逻辑回归非常简单,就像你刚刚看到的那样。现在让我们通过一个玩具示例来进一步研究:

>>> X_train = np.array([[6, 7],
...                     [2, 4],
...                     [3, 6],
...                     [4, 7],
...                     [1, 6],
...                     [5, 2],
...                     [2, 0],
...                     [6, 3],
...                     [4, 1],
...                     [7, 2]])
>>> y_train = np.array([0,
...                     0,
...                     0,
...                     0,
...                     0,
...                     1,
...                     1,
...                     1,
...                     1,
...                     1]) 

我们训练一个逻辑回归模型,进行1000次迭代,学习率为0.1,基于包含截距的权重:

>>> weights = train_logistic_regression(X_train, y_train,
             max_iter=1000, learning_rate=0.1, fit_intercept=True)
0.574404237166
0.0344602233925
0.0182655727085
0.012493458388
0.00951532913855
0.00769338806065
0.00646209433351
0.00557351184683
0.00490163225453
0.00437556774067 

成本的下降意味着模型随着时间的推移在优化。我们可以通过以下方式检查模型在新样本上的表现:

>>> X_test = np.array([[6, 1],
...                    [1, 3],
...                    [3, 1],
...                    [4, 5]])
>>> predictions = predict(X_test, weights)
>>> print(predictions)
array([ 0.9999478 , 0.00743991, 0.9808652 , 0.02080847]) 

为了可视化这一点,使用0.5作为分类决策阈值执行以下代码:

>>> plt.scatter(X_train[:5,0], X_train[:5,1], c='b', marker='x')
>>> plt.scatter(X_train[5:,0], X_train[5:,1], c='k', marker='.')
>>> for i, prediction in enumerate(predictions):
        marker = 'X' if prediction < 0.5 else 'o'
        c = 'b' if prediction < 0.5 else 'k'
        plt.scatter(X_test[i,0], X_test[i,1], c=c, marker=marker) 

蓝色填充的交叉表示从类 0 预测的测试样本,而黑色填充的圆点表示从类 1 预测的测试样本:

>>> plt.show() 

请参考以下截图查看结果:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_06.png

图 4.6:玩具示例的训练集和测试集

我们训练的模型能正确预测新样本的类别(填充的交叉和填充的圆点)。

使用梯度下降进行逻辑回归预测广告点击率

现在,我们将在我们的点击预测项目中部署我们刚刚开发的算法。

我们将从仅使用 10,000 个训练样本开始(你很快就会明白为什么我们不从 270,000 开始,就像在上一章中那样):

>>> import pandas as pd
>>> n_rows = 300000
>>> df = pd.read_csv("train.csv", nrows=n_rows)
>>> X = df.drop(['click', 'id', 'hour', 'device_id', 'device_ip'],
                                                     axis=1).values
>>> Y = df['click'].values
>>> n_train = 10000
>>> X_train = X[:n_train]
>>> Y_train = Y[:n_train]
>>> X_test = X[n_train:]
>>> Y_test = Y[n_train:]
>>> from sklearn.preprocessing import OneHotEncoder
>>> enc = OneHotEncoder(handle_unknown='ignore')
>>> X_train_enc = enc.fit_transform(X_train)
>>> X_test_enc = enc.transform(X_test) 

我们在10000次迭代中训练逻辑回归模型,学习率为0.01,并带有偏差:

>>> import timeit
>>> start_time = timeit.default_timer()
>>> weights = train_logistic_regression(X_train_enc.toarray(),
              Y_train, max_iter=10000, learning_rate=0.01,
              fit_intercept=True)
0.6820019456743648
0.4608619713011896
0.4503715555130051
…
…
…
0.41485094023829017
0.41477416506724385
0.41469802145452467
>>> print(f"--- {(timeit.default_timer() - start_time :.3f} seconds ---")
--- 183.840 seconds --- 

优化模型花费了 184 秒。训练后的模型在测试集上的表现如下:

>>> pred = predict(X_test_enc.toarray(), weights)
>>> from sklearn.metrics import roc_auc_score
>>> print(f'Training samples: {n_train}, AUC on testing set: {roc_auc_score(Y_test, pred):.3f}')
Training samples: 10000, AUC on testing set: 0.703 

现在,让我们使用 100,000 个训练样本(n_train = 100000)并重复相同的过程。它将需要超过一个小时——这比拟合 10 倍大小数据集的时间多 22 倍。正如我在本章开始时提到的,逻辑回归分类器在处理大型数据集时表现良好。但我们的测试结果似乎与此相矛盾。我们如何有效地处理更大的训练数据集,不仅是 100,000 个样本,而是数百万个样本?让我们在下一节中看看更高效的训练逻辑回归模型的方法。

使用随机梯度下降(SGD)训练逻辑回归模型

在基于梯度下降的逻辑回归模型中,所有训练样本都用于每次迭代中更新权重。因此,如果训练样本数量很大,整个训练过程将变得非常耗时和计算昂贵,正如您在我们的最后一个例子中所见到的。

幸运的是,一个小小的调整就可以使逻辑回归适用于大型数据集。每次权重更新,只消耗一个训练样本,而不是整个训练集。模型根据单个训练样本计算的误差前进一步。一旦所有样本都被使用,一次迭代就完成了。这种进阶版本的梯度下降被称为SGD。用公式表达,对于每次迭代,我们执行以下操作:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_034.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_035.png

SGD 通常比梯度下降收敛速度快得多,后者通常需要大量迭代次数。

要实现基于 SGD 的逻辑回归,我们只需要稍微修改update_weights_gd函数:

>>> def update_weights_sgd(X_train, y_train, weights,
                                           learning_rate):
...     """ One weight update iteration: moving weights by one
            step based on each individual sample
...     Args:
...     X_train, y_train (numpy.ndarray, training data set)
...     weights (numpy.ndarray)
...     learning_rate (float)
...     Returns:
...     numpy.ndarray, updated weights
...     """
...     for X_each, y_each in zip(X_train, y_train):
...         prediction = compute_prediction(X_each, weights)
...         weights_delta = X_each.T * (y_each - prediction)
...         weights += learning_rate * weights_delta
...     return weights 

train_logistic_regression函数中,应用了 SGD:

>>> def train_logistic_regression_sgd(X_train, y_train, max_iter,
                              learning_rate, fit_intercept=False):
...     """ Train a logistic regression model via SGD
...     Args:
...     X_train, y_train (numpy.ndarray, training data set)
...     max_iter (int, number of iterations)
...     learning_rate (float)
...     fit_intercept (bool, with an intercept w0 or not)
...     Returns:
...     numpy.ndarray, learned weights
...     """
...     if fit_intercept:
...         intercept = np.ones((X_train.shape[0], 1))
...         X_train = np.hstack((intercept, X_train))
...     weights = np.zeros(X_train.shape[1])
...     for iteration in range(max_iter):
...         weights = update_weights_sgd(X_train, y_train, weights,
                                                     learning_rate)
...         # Check the cost for every 2 (for example) iterations
...         if iteration % 2 == 0:
...             print(compute_cost(X_train, y_train, weights))
...     return weights 

现在,让我们看看 SGD 有多强大。我们将使用 10 万个训练样本,选择10作为迭代次数,0.01作为学习率,并打印出每两次迭代的当前成本:

>>> start_time = timeit.default_timer()
>>> weights = train_logistic_regression_sgd(X_train_enc.toarray(),
        Y_train, max_iter=10, learning_rate=0.01, fit_intercept=True)
0.4127864859625796
0.4078504597223988
0.40545733114863264
0.403811787845451
0.4025431351250833
>>> print(f"--- {(timeit.default_timer() - start_time)}.3fs seconds ---")
--- 25.122 seconds ---
>>> pred = predict(X_test_enc.toarray(), weights)
>>> print(f'Training samples: {n_train}, AUC on testing set: {roc_auc_score(Y_test, pred):.3f}')
Training samples: 100000, AUC on testing set: 0.732 

训练过程仅用时 25 秒完成!

成功从头开始实现基于 SGD 的逻辑回归算法后,我们使用 scikit-learn 的SGDClassifier模块来实现它:

>>> from sklearn.linear_model import SGDClassifier
>>> sgd_lr = SGDClassifier(loss='log_loss', penalty=None,
             fit_intercept=True, max_iter=20,
             learning_rate='constant', eta0=0.01) 

在这里,'``log_loss'作为loss参数表明成本函数是对数损失,penalty是减少过拟合的正则化项,我们将在下一节中进一步讨论,max_iter是迭代次数,另外两个参数意味着学习率是0.01,在训练过程中保持不变。应注意,默认的learning_rate'optimal',随着更新的进行,学习率会稍微降低。这对于在大型数据集上找到最优解是有益的。

现在,训练模型并测试它:

>>> sgd_lr.fit(X_train_enc.toarray(), Y_train)
>>> pred = sgd_lr.predict_proba(X_test_enc.toarray())[:, 1]
>>> print(f'Training samples: {n_train}, AUC on testing set: {roc_auc_score(Y_test, pred):.3f}')
Training samples: 100000, AUC on testing set: 0.732 

快速简单!

训练带有正则化的逻辑回归模型

正如我在前一节简要提到的,逻辑回归的SGDClassifier中的penalty参数与模型的正则化有关。正则化有两种基本形式,L1(也称为Lasso)和L2(也称为Ridge)。无论哪种方式,正则化都是原始成本函数之外的一个额外项:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_036.png

这里,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_037.png是乘以正则化项的常数,q可以是12,代表 L1 或 L2 正则化,其中以下适用:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_038.png

训练一个逻辑回归模型是减少以权重 w 为函数的成本的过程。如果到达某个点时,某些权重(例如 w[i]、w[j] 和 w[k])非常大,整个成本将由这些大权重决定。在这种情况下,学习到的模型可能只是记住了训练集,无法很好地推广到未见过的数据。正则化项的引入是为了惩罚大权重,因为权重现在成为了最小化成本的一部分。

正则化通过消除过拟合来起到作用。最后,参数 α 提供了对数损失和泛化之间的权衡。如果 α 太小,它不能压缩大的权重,模型可能会遭遇高方差或过拟合;另一方面,如果 α 太大,模型可能会过度泛化,无法很好地拟合数据集,表现出欠拟合的症状。α 是调优的一个重要参数,用于获得最佳的带正则化的逻辑回归模型。

在选择 L1 和 L2 形式时,通常的经验法则是看是否预期进行 特征选择。在 机器学习 (ML) 分类中,特征选择是选择一组重要特征以用于构建更好的模型的过程。在实践中,数据集中的并非每个特征都包含有助于区分样本的信息;有些特征是冗余的或无关的,因此可以在损失较小的情况下丢弃。

在逻辑回归分类器中,特征选择只能通过 L1 正则化来实现。为了理解这一点,我们假设有两个权重向量,w[1]= (1, 0) 和 w[2]= (0.5, 0.5);假设它们产生相同的对数损失,两个权重向量的 L1 和 L2 正则化项如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_039.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_040.png

两个向量的 L1 项是相等的,而 w[2] 的 L2 项小于 w[1] 的 L2 项。这表明,L2 正则化相比 L1 正则化对由极大和极小权重组成的权重施加了更多的惩罚。换句话说,L2 正则化偏向于所有权重的相对较小值,避免任何权重出现极大或极小的值,而 L1 正则化允许某些权重具有显著较小的值,某些则具有显著较大的值。只有使用 L1 正则化,某些权重才能被压缩到接近或完全为 0,这使得特征选择成为可能。

在 scikit-learn 中,正则化类型可以通过 penalty 参数指定,选项包括 none(无正则化)、"l1""l2""elasticnet"(L1 和 L2 的混合),而乘数 α 可以通过 alpha 参数指定。

使用 L1 正则化进行特征选择

我们在此讨论通过 L1 正则化进行特征选择。

初始化一个带有 L1 正则化的 SGD 逻辑回归模型,并基于 10,000 个样本训练模型:

>>> sgd_lr_l1 = SGDClassifier(loss='log_loss',
                          penalty='l1',
                          alpha=0.0001,
                          fit_intercept=True,
                          max_iter=10,
                          learning_rate='constant',
                          eta0=0.01,
                          random_state=42)
>>> sgd_lr_l1.fit(X_train_enc.toarray(), Y_train) 

使用训练好的模型,我们可以获得其系数的绝对值:

>>> coef_abs = np.abs(sgd_lr_l1.coef_)
>>> print(coef_abs)
[[0\. 0.16654682 0\. ... 0\. 0\. 0.12803394]] 

底部的 10 个系数及其值如下所示:

>>> print(np.sort(coef_abs)[0][:10])
[0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0.]
>>> bottom_10 = np.argsort(coef_abs)[0][:10] 

我们可以通过以下代码查看这 10 个特征:

>>> feature_names = enc.get_feature_names_out()
>>> print('10 least important features are:\n', feature_names[bottom_10])
10 least important features are:
 ['x0_1001' 'x8_84c2f017' 'x8_84ace234'  'x8_84a9d4ba' 'x8_84915a27'
'x8_8441e1f3' 'x8_840161a0' 'x8_83fbdb80' 'x8_83fb63cd' 'x8_83ed0b87'] 

它们分别是来自 X_train 中第 0 列(即 C1 列)的 1001,来自第 8 列(即 device_model 列)的 84c2f017,依此类推。

同样,前 10 个系数及其值可以通过以下方式获得:

>>> print(np.sort(coef_abs)[0][-10:])
[0.67912376 0.70885933 0.75157162 0.81783177 0.94672827 1.00864062
 1.08152137 1.130848   1.14859459 1.37750805]
>>> top_10 = np.argsort(coef_abs)[0][-10:]
>>> print('10 most important features are:\n', feature_names[top_10])
10 most important features are:
 ['x4_28905ebd' 'x3_7687a86e' 'x18_61' 'x18_15' 'x5_5e3f096f' 'x5_9c13b419' 'x2_763a42b5' 'x3_27e3c518' 'x2_d9750ee7' 'x5_1779deee'] 

它们分别是来自 X_train 中第 4 列(即 site_category)的 28905ebd,来自第 3 列(即 site_domain)的 7687a86e,依此类推。

在本节中,你已经了解了如何使用 L1 正则化的逻辑回归进行特征选择,在该方法中,不重要特征的权重被压缩到接近 0 或者完全为 0。除了 L1 正则化的逻辑回归,随机森林是另一种常用的特征选择技术。我们将在下一节进一步探讨。

使用随机森林进行特征选择

总结一下,随机森林是对一组独立决策树的袋装方法。每棵树在每个节点寻找最佳分割点时都会考虑特征的随机子集。在决策树中,只有那些重要的特征(以及它们的分割值)被用来构建树节点。考虑到整个森林:一个特征在树节点中使用得越频繁,它的重要性就越大。

换句话说,我们可以根据特征在所有树的节点中出现的频率对特征重要性进行排名,并选择最重要的特征。

在 scikit-learn 中训练好的 RandomForestClassifier 模块包含一个名为 feature_importances_ 的属性,表示特征重要性,计算方式为特征在树节点中出现的比例。同样,我们将在一个包含 100,000 个广告点击样本的数据集上使用随机森林进行特征选择:

>>> from sklearn.ensemble import RandomForestClassifier
>>> random_forest = RandomForestClassifier(n_estimators=100,
                 criterion='gini', min_samples_split=30, n_jobs=-1)
>>> random_forest.fit(X_train_enc.toarray(), Y_train) 

在拟合随机森林模型后,我们可以通过以下方式获取特征重要性分数:

>>> feature_imp = random_forest.feature_importances_
>>> print(feature_imp)
[1.22776093e-05 1.42544940e-03 8.11601536e-04 ... 7.51812083e-04 8.79340746e-04 8.49537255e-03] 

看一下底部 10 个特征分数以及相应的 10 个最不重要特征:

>>> feature_names = enc.get_feature_names()
>>> print(np.sort(feature_imp)[:10])
[0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0.]
>>> bottom_10 = np.argsort(feature_imp)[:10]
>>> print('10 least important features are:\n', feature_names[bottom_10])
10 least important features are:
 ['x5_f0222e42' 'x8_7d196936' 'x2_ba8f6070' 'x2_300ede9d' 'x5_72c55d0b' 'x2_4390d4c5' 'x5_69e5a5ec' 'x8_023a5294' 'x11_15541' 'x6_2022d54e'] 

现在,看看前 10 个特征分数以及相应的 10 个最重要特征:

>>> print(np.sort(feature_imp)[-10:])
[0.00849437 0.00849537 0.00872154 0.01010324 0.0109653  0.01099363 0.01319093 0.01471638 0.01802233 0.01889752]
>>> top_10 = np.argsort(feature_imp)[-10:]
>>> print('10 most important features are:\n', feature_names[top_10])
10 most important features are:
 ['x3_7687a86e' 'x18_157' 'x17_-1' 'x14_1993' 'x8_8a4875bd' 'x2_d9750ee7' 'x3_98572c79' 'x16_1063' 'x15_2' 'x18_33'] 

本节中,我们讲解了如何使用随机森林进行特征选择。我们使用随机森林对广告点击数据进行了特征排名。你能否使用前 10 或 20 个特征来构建另一个用于广告点击预测的逻辑回归模型?

在大数据集上进行在线学习训练

到目前为止,我们的模型已在不超过 300,000 个样本上进行了训练。如果超过这个数字,内存可能会被超载,因为它需要存储过多数据,程序也可能会崩溃。在本节中,我们将探讨如何使用 在线学习 在大规模数据集上进行训练。

SGD 从梯度下降演变而来,通过依次使用单个训练样本逐步更新模型,而不是一次性使用完整的训练集。我们可以通过在线学习技术进一步扩展 SGD。在在线学习中,训练所需的新数据是按顺序或实时提供的,而不像离线学习环境中那样一次性提供。每次加载并预处理一小块数据进行训练,从而释放用于存储整个大数据集的内存。除了更好的计算可行性外,在线学习还因其对实时生成新数据并需要更新模型的适应性而被使用。例如,股票价格预测模型通过及时的市场数据以在线学习方式进行更新;点击率预测模型需要包括反映用户最新行为和兴趣的最新数据;垃圾邮件检测器必须对不断变化的垃圾邮件发送者做出反应,考虑动态生成的新特征。

以前通过先前的数据集训练的现有模型,现在可以仅基于最新可用的数据集进行更新,而不是像在离线学习中那样,基于之前和当前的数据集一起从头开始重建模型:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_07.png

图 4.7:在线学习与离线学习

在前面的例子中,在线学习允许模型继续使用新到达的数据进行训练。然而,在离线学习中,我们必须使用新到达的数据和旧数据一起重新训练整个模型。

scikit-learn 中的 SGDClassifier 模块通过 partial_fit 方法实现在线学习(而 fit 方法用于离线学习,正如你所见)。我们将使用 1,000,000 个样本来训练模型,其中每次输入 100,000 个样本,以模拟在线学习环境。此外,我们还将用另外 100,000 个样本对训练好的模型进行测试,如下所示:

>>> n_rows = 100000 * 11
>>> df = pd.read_csv("train.csv", nrows=n_rows)
>>> X = df.drop(['click', 'id', 'hour', 'device_id', 'device_ip'],
                                                      axis=1).values
>>> Y = df['click'].values
>>> n_train = 100000 * 10
>>> X_train = X[:n_train]
>>> Y_train = Y[:n_train]
>>> X_test = X[n_train:]
>>> Y_test = Y[n_train:] 

如下所示,将编码器应用于整个训练集:

>>> enc = OneHotEncoder(handle_unknown='ignore')
>>> enc.fit(X_train) 

初始化一个 SGD 逻辑回归模型,并将迭代次数设置为 1,以便部分拟合模型并启用在线学习:

>>> sgd_lr_online = SGDClassifier(loss='log_loss',
                              penalty=None,
                              fit_intercept=True,
                              max_iter=1,
                              learning_rate='constant',
                              eta0=0.01,
                              random_state=42) 

对每 100000 个样本进行循环,并部分拟合模型:

>>> start_time = timeit.default_timer()
>>> for i in range(10):
...     x_train = X_train[i*100000:(i+1)*100000]
...     y_train = Y_train[i*100000:(i+1)*100000]
...     x_train_enc = enc.transform(x_train)
...     sgd_lr_online.partial_fit(x_train_enc.toarray(), y_train,
                                                    classes=[0, 1]) 

同样,我们使用 partial_fit 方法进行在线学习。此外,我们还指定了 classes 参数,这是在线学习中必需的:

>>> print(f"--- {(timeit.default_timer() - start_time):.3f} seconds ---")
--- 87.399s seconds --- 

将训练好的模型应用于测试集,即接下来的 100,000 个样本,如下所示:

>>> x_test_enc = enc.transform(X_test)
>>> pred = sgd_lr_online.predict_proba(x_test_enc.toarray())[:, 1]
>>> print(f'Training samples: {n_train * 10}, AUC on testing set: {roc_auc_score(Y_test, pred):.3f}')
Training samples: 10000000, AUC on testing set: 0.762 

在在线学习中,仅用 100 万个样本进行训练只需 87 秒,并且能得到更好的准确度。

到目前为止,我们一直在使用逻辑回归进行二分类。我们可以用它处理多分类问题吗?可以。不过,我们确实需要做一些小的调整。让我们在下一节中看看。

处理多类别分类

另一个值得注意的事情是逻辑回归算法如何处理多类分类。尽管在多类情况下我们与 scikit-learn 分类器的交互方式与二分类时相同,但了解逻辑回归在多类分类中的工作原理是很有用的。

对于超过两个类别的逻辑回归也被称为多项式逻辑回归,后来更常被称为softmax 回归。正如你在二分类情况下看到的,模型由一个权重向量 w 表示,目标属于 1 或正类的概率如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_041.png

K 类的情况下,模型由 K 个权重向量 w[1]、w[2]、…、w[K] 表示,目标属于类 k 的概率如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_042.png

请查看以下项:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_043.png

上述项规范化了以下概率(k1K),使其总和为 1

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_044.png

二分类情况下的代价函数表示如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_045.png

类似地,多类情况下的代价函数变为如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_046.png

在这里,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_047.png 函数只有在 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_048.png 为真时才为 1,否则为 0。

在定义了代价函数之后,我们以与在二分类情况下推导步长 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_050.png 相同的方式,得到 j 权重向量的步长 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_049.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_051.png

类似地,所有 K 个权重向量在每次迭代中都会被更新。经过足够的迭代后,学习到的权重向量 w[1]、w[2]、…、w[K] 将用于通过以下方程对新的样本 x’ 进行分类:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_04_052.png

为了更好地理解,我们来用一个经典数据集进行实验——手写数字分类:

>>> from sklearn import datasets
>>> digits = datasets.load_digits()
>>> n_samples = len(digits.images) 

由于图像数据存储在 8*8 矩阵中,我们需要将其展开,方法如下:

>>> X = digits.images.reshape((n_samples, -1))
>>> Y = digits.target 

然后我们将数据分割如下:

>>> from sklearn.model_selection import train_test_split
>>> X_train, X_test, Y_train, Y_test = train_test_split(X, Y,
                                    test_size=0.2, random_state=42) 

然后,我们结合网格搜索和交叉验证来找到最佳的多类逻辑回归模型,如下所示:

>>> from sklearn.model_selection import GridSearchCV
>>> parameters = {'penalty': ['l2', None],
...               'alpha': [1e-07, 1e-06, 1e-05, 1e-04],
...               'eta0': [0.01, 0.1, 1, 10]}
>>> sgd_lr = SGDClassifier(loss='log_loss',
                       learning_rate='constant',
                       fit_intercept=True,
                       max_iter=50,
                       random_state=42)
>>> grid_search = GridSearchCV(sgd_lr, parameters,
                               n_jobs=-1, cv=5)
>>> grid_search.fit(X_train, Y_train)
>>> print(grid_search.best_params_)
{'alpha': 1e-05, 'eta0': 0.01, 'penalty': 'l2' } 

我们首先定义要为模型调优的超参数网格。在用一些固定参数初始化分类器后,我们设置网格搜索交叉验证。我们在训练集上训练并找到最佳的超参数组合。

为了使用最佳模型进行预测,我们应用以下方法:

>>> sgd_lr_best = grid_search.best_estimator_
>>> accuracy = sgd_lr_best.score(X_test, Y_test)
>>> print(f'The accuracy on testing set is: {accuracy*100:.1f}%')
The accuracy on testing set is: 94.7% 

它看起来与前一个例子差别不大,因为 SGDClassifier 在内部处理了多类情况。你可以自行计算混淆矩阵作为练习。观察模型在各个类上的表现会很有意思。

下一部分将是一个奖励部分,我们将使用 TensorFlow 实现逻辑回归,并以点击预测作为例子。

使用 TensorFlow 实现逻辑回归

我们将使用 TensorFlow 实现逻辑回归,再次以点击预测为示例。我们使用前 100,000 个样本中的 90%进行训练,剩余的 10%用于测试,并假设 X_train_encY_trainX_test_encY_test 包含正确的数据:

  1. 首先,我们导入 TensorFlow,将 X_train_encX_test_enc 转换为 NumPy 数组,并将 X_train_encY_trainX_test_encY_test 转换为 float32

    >>> import tensorflow as tf
    >>> X_train_enc = enc.fit_transform(X_train).toarray().astype('float32')
    >>> X_test_enc = enc.transform(X_test).toarray().astype('float32')
    >>> Y_train = Y_train.astype('float32')
    >>> Y_test = Y_test.astype('float32') 
    

在 TensorFlow 中,通常使用 NumPy 数组形式的数据进行操作。此外,TensorFlow 默认使用 float32 进行计算,以提高计算效率。

  1. 我们使用 tf.data 模块对数据进行洗牌和分批:

    >>> batch_size = 1000
    >>> train_data = tf.data.Dataset.from_tensor_slices((X_train_enc, Y_train))
    >>> train_data = train_data.repeat().shuffle(5000).batch(batch_size).prefetch(1) 
    

对于每次权重更新,仅消耗一个批次的样本,而不是单个样本或整个训练集。模型根据批次样本计算的误差进行一步更新。在本例中,批次大小为 1,000。

tf.data 提供了一套高效加载和预处理机器学习数据的工具和实用程序。它旨在处理大规模数据集,并支持高效的数据管道构建,用于训练和评估。

  1. 然后,我们定义逻辑回归模型的权重和偏差:

    >>> n_features = X_train_enc.shape[1]
    >>> W = tf.Variable(tf.zeros([n_features, 1]))
    >>> b = tf.Variable(tf.zeros([1])) 
    
  2. 然后,我们创建一个梯度下降优化器,通过最小化损失来寻找最佳系数。我们使用 Adam(Adam:一种随机优化方法,Kingma,D. P.,和 Ba,J.(2014))作为我们的优化器,它是一种改进的梯度下降方法,具有自适应学习率(起始学习率为0.001):

    >>> learning_rate = 0.001
    >>> optimizer = tf.optimizers.Adam(learning_rate) 
    
  3. 我们定义优化过程,在该过程中计算当前的预测值和成本,并根据计算的梯度更新模型系数:

    >>> def run_optimization(x, y):
    ...     with tf.GradientTape() as tape:
    ...         logits = tf.add(tf.matmul(x, W), b)[:, 0]
    ...         loss = tf.reduce_mean(
                         tf.nn.sigmoid_cross_entropy_with_logits(
                                             labels=y, logits=logits))
            # Update the parameters with respect to the gradient calculations
    ...     gradients = tape.gradient(loss, [W, b])
    ...     optimizer.apply_gradients(zip(gradients, [W, b])) 
    

在这里,tf.GradientTape 允许我们跟踪 TensorFlow 计算,并计算相对于给定变量的梯度。

  1. 我们运行训练 5,000 步(每步使用一个批次的随机样本):

    >>> training_steps = 5000
    >>> for step, (batch_x, batch_y) in
                  enumerate(train_data.take(training_steps), 1):
    ...     run_optimization(batch_x, batch_y)
    ...     if step % 500 == 0:
    ...         logits = tf.add(tf.matmul(batch_x, W), b)[:, 0]
    ...         loss = tf.reduce_mean(
                           tf.nn.sigmoid_cross_entropy_with_logits(
                                 labels=batch_y, logits=logits))
    ...         print("step: %i, loss: %f" % (step, loss))
    step: 500, loss: 0.448672
    step: 1000, loss: 0.389186
    step: 1500, loss: 0.413012
    step: 2000, loss: 0.445663
    step: 2500, loss: 0.361000
    step: 3000, loss: 0.417154
    step: 3500, loss: 0.359435
    step: 4000, loss: 0.393363
    step: 4500, loss: 0.402097
    step: 5000, loss: 0.376734 
    

每 500 步,我们计算并打印当前的成本,以检查训练性能。如你所见,训练损失总体上在减少。

  1. 模型训练完成后,我们使用它对测试集进行预测,并报告 AUC 指标:

    >>> logits = tf.add(tf.matmul(X_test_enc, W), b)[:, 0]
    >>> pred = tf.nn.sigmoid(logits)
    >>> auc_metric = tf.keras.metrics.AUC()
    >>> auc_metric.update_state(Y_test, pred)
    >>> print(f'AUC on testing set: {auc_metric.result().numpy():.3f}')
    AUC on testing set: 0.736 
    

我们能够通过基于 TensorFlow 的逻辑回归模型实现 AUC 值 0.736。你还可以调整学习率、训练步骤数和其他超参数,以获得更好的性能。这将在本章末尾作为一个有趣的练习。

最佳实践

批次大小的选择在 SGD 中可能会显著影响训练过程和模型的性能。以下是选择批次大小的一些最佳实践:

  • 考虑计算资源:较大的批次大小需要更多的内存和计算资源,而较小的批次大小可能导致收敛速度较慢。选择一个适合硬件内存限制并最大化计算效率的批次大小。

  • 经验测试:尝试不同的批量大小,并在验证数据集上评估模型性能。选择在收敛速度和模型性能之间取得最佳平衡的批量大小。

  • 批量大小与学习率:批量大小的选择可能与学习率相互作用。较大的批量大小可能需要较高的学习率来防止收敛过慢,而较小的批量大小可能从较小的学习率中受益,以避免不稳定。

  • 考虑数据的性质:数据的性质也会影响批量大小的选择。例如,在样本高度相关或存在时间依赖性的任务中(例如,时间序列数据),较小的批量大小可能更有效。

你可能会好奇我们是如何在包含 4000 万样本的整个数据集上高效地训练模型的。你将使用如Spark (spark.apache.org/) 和 PySpark 模块等工具来扩展我们的解决方案。

小结

在本章中,我们继续进行在线广告点击率预测项目。这一次,我们通过使用独热编码技术克服了分类特征的挑战。随后,我们转向了新的分类算法——逻辑回归,因为它对大数据集具有较高的可扩展性。关于逻辑回归算法的深入讨论从介绍逻辑函数开始,进而引出了算法本身的机制。接着,我们讲解了如何使用梯度下降法训练逻辑回归模型。

在手动实现逻辑回归分类器并在我们的点击率数据集上进行测试之后,你学习了如何使用更加先进的方式训练逻辑回归模型,采用了 SGD,并相应地调整了我们的算法。我们还练习了如何使用基于 SGD 的 scikit-learn 逻辑回归分类器,并将其应用于我们的项目。

接着,我们继续解决使用逻辑回归时可能遇到的问题,包括 L1 和 L2 正则化用于消除过拟合、大规模数据集的在线学习技术以及处理多分类场景。你还学习了如何使用 TensorFlow 实现逻辑回归。最后,本章以将随机森林模型应用于特征选择作为替代 L1 正则化逻辑回归的方法结束。

回顾我们的学习历程,自第二章《使用朴素贝叶斯构建电影推荐引擎》开始,我们就一直在处理分类问题。现在我们已经覆盖了机器学习中所有强大且流行的分类模型。接下来,我们将进入回归问题的解决,回归是监督学习中与分类并列的任务。你将学习回归模型,包括线性回归和回归决策树。

练习题

  1. 在基于逻辑回归的点击率预测项目中,你能否调整penaltyeta0alpha等超参数,来优化SGDClassifier模型的表现?你能达到的最高测试 AUC 是多少?

  2. 你能否尝试在在线学习解决方案中使用更多的训练样本,比如 1000 万个样本?

  3. 在基于 TensorFlow 的解决方案中,你能否调整学习率、训练步数以及其他超参数,以获得更好的性能?

加入我们书籍的 Discord 空间

加入我们社区的 Discord 空间,与作者和其他读者一起讨论:

packt.link/yuxi

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/QR_Code1878468721786989681.png

第五章:使用回归算法预测股票价格

在上一章中,我们使用逻辑回归预测了广告点击。在本章中,我们将解决一个人人都感兴趣的问题——预测股票价格。通过智能投资致富——谁不感兴趣呢?股市波动和股价预测一直以来都是金融、交易甚至技术公司积极研究的课题。使用机器学习技术预测股票价格的各种方法已被开发出来。在此,我们将专注于学习几种流行的回归算法,包括线性回归、回归树和回归森林以及支持向量回归,利用它们来解决这个价值数十亿(甚至数万亿)美元的问题。

在本章中,我们将涵盖以下主题:

  • 什么是回归分析?

  • 挖掘股票价格数据

  • 开始进行特征工程

  • 使用线性回归进行估算

  • 使用决策树回归进行估算

  • 实现回归森林

  • 评估回归性能

  • 使用三种回归算法预测股票价格

什么是回归分析?

回归分析是机器学习中监督学习的主要类型之一。在回归分析中,训练集包含观测值(也称为特征)及其相关的连续目标数值。回归的过程包括两个阶段:

  • 第一阶段是探索观测值和目标之间的关系。这是训练阶段。

  • 第二阶段是利用第一阶段的模式生成未来观测的目标。这是预测阶段。

整个过程如下图所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_01.png

图 5.1:回归中的训练和预测阶段

回归分析和分类的主要区别在于回归分析的输出值是连续的,而分类的输出值是离散的。这导致这两种监督学习方法在应用领域上有所不同。分类主要用于确定所需的成员资格或特征,正如您在前几章中看到的那样,例如电子邮件是否为垃圾邮件,新闻组主题或广告点击率。相反,回归分析主要涉及估算结果或预测响应。

使用线性回归估算连续目标的示例如下,我们试图拟合一条直线以适应一组二维数据点:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_02.png

图 5.2:线性回归示例

典型的机器学习回归问题包括以下内容:

  • 根据位置、面积以及卧室和浴室数量预测房价

  • 根据系统进程和内存信息估算功耗

  • 预测零售需求

  • 预测股票价格

我在本节中讲解了回归分析,并将在下一节简要介绍它在股市和交易中的应用。

挖掘股票价格数据

在本章中,我们将作为股票量化分析师/研究员,探讨如何使用几种典型的机器学习回归算法预测股票价格。我们从对股市和股票价格的简要概述开始。

股市和股票价格的简要概述

公司股票代表对公司所有权的认定。每一股股票代表公司资产和收益的某个比例,具体比例依据总股数而定。股票可以在股东和其他各方之间通过股票交易所和组织进行交易。主要的股票交易所包括纽约证券交易所、纳斯达克、伦敦证券交易所集团和香港证券交易所。股票交易价格的波动基本上是由供求法则决定的。

一般来说,投资者希望以低价买入,高价卖出。这听起来很简单,但实施起来非常具有挑战性,因为很难准确预测股票价格是会上涨还是下跌。主要有两种研究方向试图理解导致价格变化的因素和条件,甚至预测未来的股票价格,基本面分析技术分析

  • 基本面分析:这一流派关注影响公司价值和经营的基础因素,包括从宏观角度来看整体经济和行业的情况,从微观角度来看公司的财务状况、管理层和竞争对手。

  • 技术分析:相反,这一领域通过对过去交易活动的统计研究来预测未来的价格走势,包括价格波动、交易量和市场数据。利用机器学习技术预测价格现在已经成为技术分析中的一个重要话题。

许多量化交易公司使用机器学习来增强自动化和算法交易。

理论上,我们可以应用回归技术来预测某一特定股票的价格。然而,很难确保我们选取的股票适合用于学习——其价格应该遵循一些可学习的模式,且不能受到前所未有的事件或不规则情况的影响。因此,我们将在这里重点关注一个最流行的股票指数,以更好地说明和概括我们的价格回归方法。

首先让我们来了解什么是股票指数。股票指数是衡量整体股市一部分价值的统计指标。一个指数包含了几个股票,这些股票足够多样化,能够代表整个市场的一部分。此外,指数的价格通常是通过选定股票价格的加权平均计算得出的。

纳斯达克综合指数是全球历史最悠久、最常被关注的指数之一。它包括所有在纳斯达克交易所上市的股票,涵盖了广泛的行业。纳斯达克主要列出技术公司股票,包括苹果、亚马逊、微软和谷歌(字母表)等已建立的大公司,以及新兴的成长型公司。

你可以在雅虎财经查看它的每日价格和表现,网址是finance.yahoo.com/quote/%5EIXIC/history?p=%5EIXIC。例如:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_03.png

图 5.3:雅虎财经每日价格和表现的截图

在每个交易日,股票价格会发生变化并实时记录。五个展示价格在一个单位时间(通常为一天,但也可以是一个星期或一个月)内波动的数值是关键交易指标,具体如下:

  • 开盘:某个交易日的起始价格

  • 收盘:当天的最终价格

  • 最高:当天股票交易的最高价格

  • 最低:当天股票交易的最低价格

  • 交易量:当天市场闭盘前的总交易股数

我们将重点关注纳斯达克,并利用其历史价格和表现来预测未来的价格。在接下来的部分中,我们将探讨如何开发价格预测模型,特别是回归模型,并研究哪些可以作为指标或预测特征。

开始进行特征工程

当谈到机器学习算法时,通常第一个问题是可用的特征是什么,或者预测变量是什么。

用于预测纳斯达克未来价格的驱动因素,包括历史和当前的开盘价格以及历史表现(最高最低,和交易量)。请注意,不应包括当前或当天的表现(最高最低,和交易量),因为我们根本无法预见股票在当天交易中达到的最高和最低价格,或者在市场闭盘前的交易总量。

仅用前面提到的四个指标来预测收盘价似乎不太可行,并可能导致欠拟合。因此,我们需要考虑如何生成更多的特征,以提高预测能力。在机器学习中,特征工程是通过创建特征来提高机器学习算法性能的过程。特征工程在机器学习中至关重要,通常是我们在解决实际问题时花费最多精力的地方。

特征工程通常需要足够的领域知识,并且可能非常困难且耗时。实际上,用于解决机器学习问题的特征通常不是直接可用的,而是需要专门设计和构建的。

在做出投资决策时,投资者通常会查看一段时间内的历史价格,而不仅仅是前一天的价格。因此,在我们的股票价格预测案例中,我们可以计算过去一周(五个交易日)、过去一个月和过去一年的平均收盘价,作为三个新特征。我们也可以自定义时间窗口大小,例如过去一个季度或过去六个月。除了这三个平均价格特征,我们还可以通过计算三个不同时间框架内每对平均价格之间的比率,生成与价格趋势相关的新特征,例如过去一周和过去一年的平均价格比率。

除了价格,成交量是投资者分析的另一个重要因素。类似地,我们可以通过计算不同时间框架内的平均成交量以及每对平均值之间的比率来生成新的基于成交量的特征。

除了时间窗口中的历史平均值,投资者还会非常关注股票的波动性。波动性描述的是在一定时间内某只股票或指数价格变化的程度。从统计学的角度来看,它基本上是收盘价的标准差。我们可以通过计算特定时间框架内的收盘价标准差以及成交量的标准差,轻松生成新的特征集。类似地,可以将每对标准差值之间的比率也包含在我们生成的特征池中。

最后但同样重要的是,回报是投资者密切关注的一个重要金融指标。回报是某股票/指数在特定期间内收盘价的涨跌幅。例如,日回报和年回报是我们常听到的金融术语。

它们的计算方式如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_001.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_002.png

这里,price[i] 是第 i 天的价格,price[i][-1] 是前一天的价格。周回报和月回报可以以类似方式计算。基于日回报,我们可以生成某一特定天数的移动平均值。

例如,给定过去一周的每日回报,return[i:i-1]、return[i-1:i-2]、return[i-2:i-3]、return[i-3:i-4] 和 return[i-4:i-5],我们可以按如下方式计算该周的移动平均值:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_003.png

总结来说,我们可以通过应用特征工程技术生成以下预测变量:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_04.png

图 5.4:生成的特征 (1)

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_05.png

图 5.5:生成的特征 (2)

最终,我们能够总共生成 31 组特征,以及以下六个原始特征:

  • OpenPrice[i]:该特征表示开盘价

  • OpenPrice[i-1]:该特征表示前一日的开盘价

  • ClosePrice[i-1]:该特征表示前一日的收盘价

  • HighPrice[i-1]:该特征表示过去一天的最高价格

  • LowPrice[i-1]:该特征表示过去一天的最低价格

  • Volume[i-1]:该特征表示过去一天的成交量

获取数据并生成特征

为了方便参考,我们将在这里实现生成特征的代码,而不是在后续部分。我们将首先获取项目所需的数据集。

在整个项目中,我们将从 Yahoo Finance 获取股票指数价格和表现数据。例如,在历史数据finance.yahoo.com/quote/%5EIXIC/history?p=%5EIXIC页面上,我们可以将Time Period更改为Dec 01, 2005 – Dec10, 2005,在Show中选择Historical Prices,在Frequency中选择Daily(或者直接打开此链接:finance.yahoo.com/quote/%5EIXIC/history?period1=1133395200&period2=1134172800&interval=1d&filter=history&frequency=1d&includeAdjustedClose=true),然后点击Apply按钮。点击Download data按钮下载数据并将文件命名为20051201_20051210.csv

我们可以如下加载刚才下载的数据:

>>> mydata = pd.read_csv('20051201_20051210.csv', index_col='Date')
>>> mydata
               Open         High         Low          Close
Date
2005-12-01 2244.850098  2269.389893	2244.709961	2267.169922	
2005-12-02 2266.169922	 2273.610107	2261.129883	2273.370117
2005-12-05 2269.070068	 2269.479980	2250.840088	2257.639893
2005-12-06 2267.760010	 2278.159912	2259.370117	2260.760010
2005-12-07 2263.290039	 2264.909912	2244.620117	2252.010010
2005-12-08 2254.800049	 2261.610107	2233.739990	2246.459961
2005-12-09 2247.280029	 2258.669922	2241.030029	2256.729980
              Adj Close  Volume    
Date
2005-12-01 2267.169922	  2010420000
2005-12-02 2273.370117	  1758510000
2005-12-05 2257.639893	  1659920000
2005-12-06 2260.760010	  1788200000
2005-12-07 2252.010010	  1733530000
2005-12-08 2246.459961	  1908360000
2005-12-09 2256.729980	  1658570000 

注意,输出的是一个 pandas DataFrame 对象。Date列是索引列,其余列是相应的财务变量。在接下来的代码行中,您将看到 pandas 如何在关系型(或表格型)数据上简化数据分析和转换的强大功能。

首先,我们通过一个子函数实现特征生成,该子函数直接从原始的六个特征中创建特征,如下所示:

>>> def add_original_feature(df, df_new):
...     df_new['open'] = df['Open']
...     df_new['open_1'] = df['Open'].shift(1)
...     df_new['close_1'] = df['Close'].shift(1)
...     df_new['high_1'] = df['High'].shift(1)
...     df_new['low_1'] = df['Low'].shift(1)
...     df_new['volume_1'] = df['Volume'].shift(1) 

然后,我们开发了一个生成六个与平均收盘价相关特征的子函数:

>>> def add_avg_price(df, df_new):
...     df_new['avg_price_5'] =
                     df['Close'].rolling(5).mean().shift(1)
...     df_new['avg_price_30'] =
                     df['Close'].rolling(21).mean().shift(1)
...     df_new['avg_price_365'] =
                     df['Close'].rolling(252).mean().shift(1)
...     df_new['ratio_avg_price_5_30'] =
                 df_new['avg_price_5'] / df_new['avg_price_30']
...     df_new['ratio_avg_price_5_365'] =
                 df_new['avg_price_5'] / df_new['avg_price_365']
...     df_new['ratio_avg_price_30_365'] =
                df_new['avg_price_30'] / df_new['avg_price_365'] 

同样,生成与平均成交量相关的六个特征的子函数如下:

>>> def add_avg_volume(df, df_new):
...     df_new['avg_volume_5'] =
                  df['Volume'].rolling(5).mean().shift(1)
...     df_new['avg_volume_30'] = 
                  df['Volume'].rolling(21).mean().shift(1)
...     df_new['avg_volume_365'] =
                      df['Volume'].rolling(252).mean().shift(1)
...     df_new['ratio_avg_volume_5_30'] =
                df_new['avg_volume_5'] / df_new['avg_volume_30']
...     df_new['ratio_avg_volume_5_365'] =
               df_new['avg_volume_5'] / df_new['avg_volume_365']
...     df_new['ratio_avg_volume_30_365'] =
               df_new['avg_volume_30'] / df_new['avg_volume_365'] 

至于标准差,我们为与价格相关的特征开发了以下子函数:

>>> def add_std_price(df, df_new):
...     df_new['std_price_5'] =
               df['Close'].rolling(5).std().shift(1)
...     df_new['std_price_30'] =
               df['Close'].rolling(21).std().shift(1)
...     df_new['std_price_365'] =
               df['Close'].rolling(252).std().shift(1)
...     df_new['ratio_std_price_5_30'] =
               df_new['std_price_5'] / df_new['std_price_30']
...     df_new['ratio_std_price_5_365'] =
               df_new['std_price_5'] / df_new['std_price_365']
...     df_new['ratio_std_price_30_365'] =
               df_new['std_price_30'] / df_new['std_price_365'] 

同样,生成六个基于成交量的标准差特征的子函数如下:

>>> def add_std_volume(df, df_new):
...     df_new['std_volume_5'] =
                 df['Volume'].rolling(5).std().shift(1)
...     df_new['std_volume_30'] =
                 df['Volume'].rolling(21).std().shift(1)
...     df_new['std_volume_365'] =
                 df['Volume'].rolling(252).std().shift(1)
...     df_new['ratio_std_volume_5_30'] =
                df_new['std_volume_5'] / df_new['std_volume_30']
...     df_new['ratio_std_volume_5_365'] =
                df_new['std_volume_5'] / df_new['std_volume_365']
...     df_new['ratio_std_volume_30_365'] =
               df_new['std_volume_30'] / df_new['std_volume_365'] 

使用以下子函数生成七个基于回报的特征:

>>> def add_return_feature(df, df_new):
...     df_new['return_1'] = ((df['Close'] - df['Close'].shift(1))  
                               / df['Close'].shift(1)).shift(1)
...     df_new['return_5'] = ((df['Close'] - df['Close'].shift(5))
                               / df['Close'].shift(5)).shift(1)
...     df_new['return_30'] = ((df['Close'] -
           df['Close'].shift(21)) / df['Close'].shift(21)).shift(1)
...     df_new['return_365'] = ((df['Close'] -
         df['Close'].shift(252)) / df['Close'].shift(252)).shift(1)
...     df_new['moving_avg_5'] =
                    df_new['return_1'].rolling(5).mean().shift(1)
...     df_new['moving_avg_30'] =
                    df_new['return_1'].rolling(21).mean().shift(1)
...     df_new['moving_avg_365'] =
                   df_new['return_1'].rolling(252).mean().shift(1) 

最后,我们将所有前面的子函数汇总成主要的特征生成函数:

>>> def generate_features(df):
...     """
...     Generate features for a stock/index based on historical price and performance
...     @param df: dataframe with columns "Open", "Close", "High", "Low", "Volume", "Adj Close"
...     @return: dataframe, data set with new features
...     """
...     df_new = pd.DataFrame()
...     # 6 original features
...     add_original_feature(df, df_new)
...     # 31 generated features
...     add_avg_price(df, df_new)
...     add_avg_volume(df, df_new)
...     add_std_price(df, df_new)
...     add_std_volume(df, df_new)
...     add_return_feature(df, df_new)
...     # the target
...     df_new['close'] = df['Close']
...     df_new = df_new.dropna(axis=0)
...     return df_new 

注意,这里的窗口大小是521252,而不是730365,分别代表每周、每月和每年的窗口。这是因为一年有 252 个(四舍五入后的)交易日,一个月有 21 个交易日,一周有 5 个交易日。

我们可以将这种特征工程策略应用于从 1990 年到 2023 年上半年查询的 NASDAQ 综合指数数据,如下所示(或直接从此页面下载: finance.yahoo.com/quote/%5EIXIC/history?period1=631152000&period2=1688083200&interval=1d&filter=history&frequency=1d&includeAdjustedClose=true):

>>> data_raw = pd.read_csv('19900101_20230630.csv', index_col='Date')
>>> data = generate_features(data_raw) 

看一下带有新特征的数据是什么样子的:

>>> print(data.round(decimals=3).head(5)) 

前面的命令行生成了以下输出:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_06.png

图 5.6:DataFrame 前五行的打印输出

既然所有特征和驱动因素已经准备就绪,我们现在将专注于回归算法,它们基于这些预测特征来估计连续的目标变量。

使用线性回归进行估计

第一个想到的回归模型是 线性回归。这是否意味着用线性函数拟合数据点,正如其名字所暗示的那样?让我们来探索一下。

线性回归是如何工作的?

简单来说,线性回归尝试用一条直线(在二维空间中)或一个平面(三维空间中)拟合尽可能多的数据点。它探索观察值与目标之间的线性关系,这种关系用线性方程或加权求和函数表示。给定一个数据样本 x,其中包含 n 个特征,x[1],x[2],…,x[n](x 表示特征向量,x = (x[1], x[2], …, x[n])),以及线性回归模型的权重(也叫做 系数ww 表示一个向量 (w[1],w[2],…,w[n])),目标 y 表达如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_004.png

此外,有时线性回归模型会带有截距项(也叫偏差),w[0],所以之前的线性关系变为:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_005.png

看起来熟悉吗?你在第四章《使用逻辑回归预测在线广告点击率》中学到的 逻辑回归 算法,实际上是在线性回归的基础上加上了逻辑变换,它将连续的加权和映射到 0(负类)或 1(正类)。同样,线性回归模型,或特别是它的权重向量 w,是从训练数据中学习的,目标是最小化定义为 均方误差 (MSE) 的估计误差,它衡量真值和预测值之间差异的平方平均值。给定 m 个训练样本,(x((1)),*y*((1))),(x((2)),*y*((2))),…,(x((i)),*y*((i)))…,(x((m)),*y*((m))),损失函数 J(w) 关于待优化权重的表达式如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_006.png

这里,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_007.png 是预测结果。

同样,我们可以通过梯度下降法得到最优的 w,使得 J(w) 最小化。以下是导出的梯度,即一阶导数!

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_009.png

结合梯度和学习率!,权重向量 w 可以在每一步中按如下方式更新:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_011.png

在经过大量迭代后,学习到的 w 用于预测一个新样本 x’,如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_012.png

在了解了线性回归背后的数学理论后,我们将在下一部分从头实现它。

从头实现线性回归

现在你已经对基于梯度下降的线性回归有了透彻的了解,我们将从头实现它。

我们首先定义计算预测的函数,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_013.png,并使用当前的权重:

>>> def compute_prediction(X, weights):
...     """
...     Compute the prediction y_hat based on current weights
...     """
...     return np.dot(X, weights) 

然后,我们继续用梯度下降的方式更新权重 w,如下所示:

>>> def update_weights_gd(X_train, y_train, weights,
learning_rate):
...     predictions = compute_prediction(X_train, weights)
...     weights_delta = np.dot(X_train.T, y_train - predictions)
...     m = y_train.shape[0]
...     weights += learning_rate / float(m) * weights_delta
...     return weights 

接下来,我们还添加计算损失 J(w) 的函数:

>>> def compute_loss(X, y, weights):
...     """
...     Compute the loss J(w)
...     """
...     predictions = compute_prediction(X, weights)
...     return np.mean((predictions - y) ** 2 / 2.0) 

现在,将所有函数与模型训练函数结合在一起,执行以下任务:

  1. 在每次迭代中更新权重向量

  2. 500 次(或者可以是任意数字)迭代时输出当前的成本,以确保成本在下降,且一切都在正确的轨道上:

让我们通过执行以下命令来看看是如何做到的:

>>> def train_linear_regression(X_train, y_train, max_iter, learning_rate, fit_intercept=False, display_loss=500):
...     """
...     Train a linear regression model with gradient descent, and return trained model
...     """
...     if fit_intercept:
...         intercept = np.ones((X_train.shape[0], 1))
...         X_train = np.hstack((intercept, X_train))
...     weights = np.zeros(X_train.shape[1])
...     for iteration in range(max_iter):
...         weights = update_weights_gd(X_train, y_train,
                                       weights, learning_rate)
...         # Check the cost for every 500 (by default) iterations
...         if iteration % 500 == 0:
...             print(compute_loss(X_train, y_train, weights))
...     return weights 

最后,使用训练好的模型预测新输入值的结果,如下所示:

>>> def predict(X, weights):
...     if X.shape[1] == weights.shape[0] - 1:
...         intercept = np.ones((X.shape[0], 1))
...         X = np.hstack((intercept, X))
...     return compute_prediction(X, weights) 

实现线性回归与实现逻辑回归非常相似,正如你刚刚所看到的。让我们通过一个小示例来进一步检验:

>>> X_train = np.array([[6], [2], [3], [4], [1],
                        [5], [2], [6], [4], [7]])
>>> y_train = np.array([5.5, 1.6, 2.2, 3.7, 0.8,
                        5.2, 1.5, 5.3, 4.4, 6.8]) 

使用 100 次迭代训练线性回归模型,学习率为 0.01,基于包含截距的权重:

>>> weights = train_linear_regression(X_train, y_train,
            max_iter=100, learning_rate=0.01, fit_intercept=True) 

检查模型在新样本上的表现,如下所示:

>>> X_test = np.array([[1.3], [3.5], [5.2], [2.8]])
>>> predictions = predict(X_test, weights)
>>> import matplotlib.pyplot as plt
>>> plt.scatter(X_train[:, 0], y_train, marker='o', c='b')
>>> plt.scatter(X_test[:, 0], predictions, marker='*', c='k')
>>> plt.xlabel('x')
>>> plt.ylabel('y')
>>> plt.show() 

请参考以下截图查看结果:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_07.png

图 5.7:在玩具数据集上进行线性回归

我们训练的模型正确地预测了新样本(由星号表示)。

让我们在另一个数据集上试试,来自 scikit-learn 的糖尿病数据集:

>>> from sklearn import datasets
>>> diabetes = datasets.load_diabetes()
>>> print(diabetes.data.shape)
(442, 10)
>>> num_test = 30
>>> X_train = diabetes.data[:-num_test, :]
>>> y_train = diabetes.target[:-num_test] 

使用 5000 次迭代训练线性回归模型,学习率为 1,基于包含截距的权重(每 500 次迭代显示一次损失):

>>> weights = train_linear_regression(X_train, y_train,
              max_iter=5000, learning_rate=1, fit_intercept=True)
2960.1229915
1539.55080927
1487.02495658
1480.27644342
1479.01567047
1478.57496091
1478.29639883
1478.06282572
1477.84756968
1477.64304737
>>> X_test = diabetes.data[-num_test:, :]
>>> y_test = diabetes.target[-num_test:]
>>> predictions = predict(X_test, weights)
>>> print(predictions)
[ 232.22305668 123.87481969 166.12805033 170.23901231
  228.12868839 154.95746522 101.09058779 87.33631249
  143.68332296 190.29353122 198.00676871 149.63039042
   169.56066651 109.01983998 161.98477191 133.00870377
   260.1831988 101.52551082 115.76677836 120.7338523
   219.62602446 62.21227353 136.29989073 122.27908721
   55.14492975 191.50339388 105.685612 126.25915035
   208.99755875 47.66517424]
>>> print(y_test)
[ 261\. 113\. 131\. 174\. 257\. 55\. 84\. 42\. 146\. 212\. 233.
  91\. 111\. 152\. 120\. 67\. 310\. 94\. 183\. 66\. 173\. 72.
  49\. 64\. 48\. 178\. 104\. 132\. 220\. 57.] 

该估计与真实值非常接近。

接下来,让我们利用 scikit-learn 来实现线性回归。

使用 scikit-learn 实现线性回归

到目前为止,我们已经在权重优化中使用了梯度下降法,但与逻辑回归一样,线性回归也可以使用随机梯度下降SGD)。为了使用它,我们只需用我们在第四章《使用逻辑回归预测在线广告点击率》中创建的 update_weights_sgd 函数替换 update_weights_gd 函数。

我们也可以直接使用 scikit-learn 中基于 SGD 的回归算法 SGDRegressor

>>> from sklearn.linear_model import SGDRegressor
>>> regressor = SGDRegressor(loss='squared_error',
                         penalty='l2',
                         alpha=0.0001,
                         learning_rate='constant',
                         eta0=0.2,
                         max_iter=100,
                         random_state=42) 

这里,'squared_error'作为loss参数表示成本函数为均方误差(MSE);penalty是正则化项,可以是Nonel1l2,类似于第四章中的SGDClassifier使用逻辑回归预测在线广告点击率,用于减少过拟合;max_iter是迭代次数;其余两个参数表示学习率为0.2,并在最多100次训练迭代过程中保持不变。训练模型并输出糖尿病数据集测试集上的预测结果,过程如下:

>>> regressor.fit(X_train, y_train)
>>> predictions = regressor.predict(X_test)
>>> print(predictions)
[213.10213626 108.68382244 152.18820636 153.81308148 208.42650616 137.24771808  88.91487772  73.83269079 131.35148348 173.65164632 178.16029669 135.26642772 152.92346973  89.39394334 149.98088897 117.62875063 241.90665387  86.59992328 101.90393228 105.13958969 202.13586812  50.60429115 121.43542595 106.34058448  41.11664041 172.53683431  95.43229463 112.59395222 187.40792     36.1586737 ] 

你也可以使用 TensorFlow 实现线性回归。让我们在下一节中看看。

使用 TensorFlow 实现线性回归

首先,我们导入 TensorFlow 并构建模型:

>>> import tensorflow as tf
>>> layer0 = tf.keras.layers.Dense(units=1,
                      input_shape=[X_train.shape[1]])
>>> model = tf.keras.Sequential(layer0) 

它使用一个线性层(或者你可以把它看作一个线性函数)将输入的X_train.shape[1]维度与输出的1维度连接。

接下来,我们指定损失函数为 MSE,并使用学习率为1的梯度下降优化器Adam

>>> model.compile(loss='mean_squared_error',
             optimizer=tf.keras.optimizers.Adam(1)) 

现在,我们将在糖尿病数据集上训练模型 100 次,过程如下:

>>> model.fit(X_train, y_train, epochs=100, verbose=True)
Epoch 1/100
412/412 [==============================] - 1s 2ms/sample - loss: 27612.9129
Epoch 2/100
412/412 [==============================] - 0s 44us/sample - loss: 23802.3043
Epoch 3/100
412/412 [==============================] - 0s 47us/sample - loss: 20383.9426
Epoch 4/100
412/412 [==============================] - 0s 51us/sample - loss: 17426.2599
Epoch 5/100
412/412 [==============================] - 0s 44us/sample - loss: 14857.0057
……
Epoch 96/100
412/412 [==============================] - 0s 55us/sample - loss: 2971.6798
Epoch 97/100
412/412 [==============================] - 0s 44us/sample - loss: 2970.8919
Epoch 98/100
412/412 [==============================] - 0s 52us/sample - loss: 2970.7903
Epoch 99/100
412/412 [==============================] - 0s 47us/sample - loss: 2969.7266
Epoch 100/100
412/412 [==============================] - 0s 46us/sample - loss: 2970.4180 

这还会打印出每次迭代的损失。最后,我们使用训练好的模型进行预测:

>>> predictions = model.predict(X_test)[:, 0]
>>> print(predictions)
[231.52155  124.17711  166.71492  171.3975   227.70126  152.02522
 103.01532   91.79277  151.07457  190.01042  190.60373  152.52274
 168.92166  106.18033  167.02473  133.37477  259.24756  101.51256
 119.43106  120.893005 219.37921   64.873634 138.43217  123.665634
  56.33039  189.27441  108.67446  129.12535  205.06857   47.99469 ] 

接下来,你将学习的回归算法是决策树回归。

使用决策树回归进行估算

决策树回归也称为回归树。通过将回归树与它的兄弟——分类树进行对比,理解回归树就变得容易了,而分类树你已经非常熟悉了。在本节中,我们将深入探讨使用决策树算法进行回归任务。

从分类树到回归树的过渡

在分类中,决策树通过递归二叉分裂构建,每个节点分裂成左子树和右子树。在每个分区中,它贪婪地搜索最重要的特征组合及其值作为最优分割点。分割的质量通过两个子节点标签的加权纯度来衡量,具体是通过基尼不纯度或信息增益。

在回归中,树的构建过程几乎与分类树相同,只有两个不同之处,因为目标值变为连续值:

  • 分割点的质量现在通过两个子节点的加权 MSE 来衡量;子节点的 MSE 等同于所有目标值的方差,加权 MSE 越小,分割效果越好。

  • 平均值作为终端节点的目标值,成为叶子值,而不是分类树中标签的多数值

为确保你理解回归树,我们将通过一个小型房价估算示例,使用房屋类型卧室数量

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_08.png

图 5.8:房价玩具数据集

我们首先定义用于计算的 MSE 和加权 MSE 函数:

>>> def mse(targets):
...     # When the set is empty
...     if targets.size == 0:
...         return 0
...     return np.var(targets) 

然后,我们定义了节点分割后的加权 MSE:

>>> def weighted_mse(groups):
...     total = sum(len(group) for group in groups)
...     weighted_sum = 0.0
...     for group in groups:
...         weighted_sum += len(group) / float(total) * mse(group)
...     return weighted_sum 

通过执行以下命令来测试:

>>> print(f'{mse(np.array([1, 2, 3])):.4f}')
0.6667
>>> print(f'{weighted_mse([np.array([1, 2, 3]), np.array([1, 2])]):.4f}')
0.5000 

为了构建房价回归树,我们首先列举所有可能的特征和值对,并计算相应的 MSE:

MSE(type, semi) = weighted_mse([[600, 400, 700], [700, 800]]) = 10333
MSE(bedroom, 2) = weighted_mse([[700, 400], [600, 800, 700]]) = 13000
MSE(bedroom, 3) = weighted_mse([[600, 800], [700, 400, 700]]) = 16000
MSE(bedroom, 4) = weighted_mse([[700], [600, 700, 800, 400]]) = 17500 

最低的 MSE 是通过type, semi这一对得到的,因此根节点由这个分割点构成。此分割的结果如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_09.png

图 5.9:使用(type=semi)进行分割

如果我们对一个单层回归树感到满意,我们可以通过将两个分支都指定为叶子节点来停止,并且该值为包含样本的目标值的平均值。或者,我们可以继续向下构建第二层,从右分支开始(左分支无法进一步分裂):

MSE(bedroom, 2) = weighted_mse([[], [600, 400, 700]]) = 15556
MSE(bedroom, 3) = weighted_mse([[400], [600, 700]]) = 1667
MSE(bedroom, 4) = weighted_mse([[400, 600], [700]]) = 6667 

通过指定bedroom, 3这一对(是否至少有三间卧室)作为第二个分割点,它具有最低的 MSE,我们的树如下图所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_10.png

图 5.10:使用(bedroom>=3)进行分割

我们可以通过将平均值分配给两个叶子节点来完成树的构建。

实现决策树回归

现在,既然你已经清楚了回归树构建的过程,是时候开始编写代码了。

我们将在本节中定义的节点分割工具函数与我们在第三章《基于树算法预测在线广告点击率》中使用的完全相同,它基于特征和值对,将节点中的样本分割为左分支和右分支:

>>> def split_node(X, y, index, value):
...     x_index = X[:, index]
...     # if this feature is numerical
...     if type(X[0, index]) in [int, float]:
...         mask = x_index >= value
...     # if this feature is categorical
...     else:
...         mask = x_index == value
...     # split into left and right child
...     left = [X[~mask, :], y[~mask]]
...     right = [X[mask, :], y[mask]]
...     return left, right 

接下来,我们定义贪心搜索函数,尝试所有可能的分割,并返回具有最小加权均方误差(MSE)的分割:

>>> def get_best_split(X, y):
...     """
...     Obtain the best splitting point and resulting children for the data set X, y
...     @return: {index: index of the feature, value: feature value, children: left and right children}
...     """
...     best_index, best_value, best_score, children =
                                     None, None, 1e10, None
...     for index in range(len(X[0])):
...         for value in np.sort(np.unique(X[:, index])):
...             groups = split_node(X, y, index, value)
...             impurity = weighted_mse(
                                [groups[0][1], groups[1][1]])
...             if impurity < best_score:
...                 best_index, best_value, best_score, children
                                   = index, value, impurity, groups
...     return {'index': best_index, 'value': best_value,
                'children': children} 

前述的选择和分割过程会在每个后续的子节点中递归发生。当满足停止标准时,节点处的过程停止,样本的平均值targets将被分配给该终端节点:

>>> def get_leaf(targets):
...     # Obtain the leaf as the mean of the targets
...     return np.mean(targets) 

最后,这是一个递归函数split,它将所有内容连接在一起。它检查是否满足停止条件,如果满足,则分配叶子节点,否则继续分割:

>>> def split(node, max_depth, min_size, depth):
...     """
...     Split children of a node to construct new nodes or assign them terminals
...     @param node: dict, with children info
...     @param max_depth: maximal depth of the tree
...     @param min_size: minimal samples required to further split a child
...     @param depth: current depth of the node
...     """
...     left, right = node['children']
...     del (node['children'])
...     if left[1].size == 0:
...         node['right'] = get_leaf(right[1])
...         return
...     if right[1].size == 0:
...         node['left'] = get_leaf(left[1])
...         return
...     # Check if the current depth exceeds the maximal depth
...     if depth >= max_depth:
...         node['left'], node['right'] = get_leaf(
                             left[1]), get_leaf(right[1])
...         return
...     # Check if the left child has enough samples
...     if left[1].size <= min_size:
...         node['left'] = get_leaf(left[1])
...     else:
...         # It has enough samples, we further split it
...         result = get_best_split(left[0], left[1])
...         result_left, result_right = result['children']
...         if result_left[1].size == 0:
...             node['left'] = get_leaf(result_right[1])
...         elif result_right[1].size == 0:
...             node['left'] = get_leaf(result_left[1])
...         else:
...             node['left'] = result
...             split(node['left'], max_depth, min_size, depth + 1)
...     # Check if the right child has enough samples
...     if right[1].size <= min_size:
...         node['right'] = get_leaf(right[1])
...     else:
...         # It has enough samples, we further split it
...         result = get_best_split(right[0], right[1])
...         result_left, result_right = result['children']
...         if result_left[1].size == 0:
...             node['right'] = get_leaf(result_right[1])
...         elif result_right[1].size == 0:
...             node['right'] = get_leaf(result_left[1])
...         else:
...             node['right'] = result
...             split(node['right'], max_depth, min_size,
                       depth + 1) 

回归树构建的入口点如下:

>>> def train_tree(X_train, y_train, max_depth, min_size):
...     root = get_best_split(X_train, y_train)
...     split(root, max_depth, min_size, 1)
...     return root 

现在,让我们通过手动计算的示例来测试一下:

>>> X_train = np.array([['semi', 3],
...                     ['detached', 2],
...                     ['detached', 3],
...                     ['semi', 2],
...                     ['semi', 4]], dtype=object)
>>> y_train = np.array([600, 700, 800, 400, 700])
>>> tree = train_tree(X_train, y_train, 2, 2) 

为了验证训练得到的树与我们手动构建的树相同,我们编写了一个显示树的函数:

>>> CONDITION = {'numerical': {'yes': '>=', 'no': '<'},
...              'categorical': {'yes': 'is', 'no': 'is not'}}
>>> def visualize_tree(node, depth=0):
...     if isinstance(node, dict):
...         if type(node['value']) in [int, float]:
...             condition = CONDITION['numerical']
...         else:
...             condition = CONDITION['categorical']
...         print('{}|- X{} {} {}'.format(depth * ' ',
                  node['index'] + 1, condition['no'],
                  node['value']))
...         if 'left' in node:
...             visualize_tree(node['left'], depth + 1)
...         print('{}|- X{} {} {}'.format(depth * ' ',
                 node['index'] + 1, condition['yes'],
                 node['value']))
...         if 'right' in node:
...             visualize_tree(node['right'], depth + 1)
...     else:
...         print('{}[{}]'.format(depth * ' ', node))
>>> visualize_tree(tree)
|- X1 is not detached
  |- X2 < 3
    [400.0]
  |- X2 >= 3
    [650.0]
|- X1 is detached
  [750.0] 

现在,通过从零实现回归树,你已经对它有了更好的理解,我们可以直接使用 scikit-learn 中的DecisionTreeRegressor包(scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeRegressor.html)。让我们将它应用于一个预测加利福尼亚房价的示例。数据集包含作为目标变量的房屋中位数价格、收入中位数、住房中位年龄、总房间数、总卧室数、人口、住户数、纬度和经度作为特征。它来自 StatLib 库(www.dcc.fc.up.pt/~ltorgo/Regression/cal_housing.html),可以通过sklearn.datasets.fetch_california_housing函数直接加载,代码如下:

>>> housing = datasets.fetch_california_housing() 

我们将最后 10 个样本用于测试,其他样本用于训练DecisionTreeRegressor决策树,代码如下:

>>> num_test = 10 # the last 10 samples as testing set
>>> X_train = housing.data[:-num_test, :]
>>> y_train = housing.target[:-num_test]
>>> X_test = housing.data[-num_test:, :]
>>> y_test = housing.target[-num_test:]
>>> from sklearn.tree import DecisionTreeRegressor
>>> regressor = DecisionTreeRegressor(max_depth=10,
                                      min_samples_split=3,
                                      random_state=42)
>>> regressor.fit(X_train, y_train) 

然后,我们将训练好的决策树应用到测试集上:

>>> predictions = regressor.predict(X_test)
>>> print(predictions)
[1.29568298 1.29568298 1.29568298 1.11946842 1.29568298 0.66193704 0.82554167 0.8546936  0.8546936  0.8546936 ] 

与真实值比较预测结果,如下所示:

>>> print(y_test)
[1.12  1.072 1.156 0.983 1.168 0.781 0.771 0.923 0.847 0.894] 

我们看到预测结果非常准确。

在本节中,我们实现了回归树。有没有回归树的集成版本呢?让我们接下来看看。

实现回归森林

第三章基于树的算法预测在线广告点击率中,我们探讨了随机森林作为一种集成学习方法,通过将多个决策树结合起来,分别训练并在每棵树的节点中随机子抽样训练特征。在分类中,随机森林通过对所有树的决策进行多数投票来做出最终决定。在回归中,随机森林回归模型(也叫做回归森林)将所有决策树的回归结果平均后作为最终决策。

在这里,我们将使用 scikit-learn 中的回归森林包RandomForestRegressor,并将其应用到加利福尼亚房价预测的示例中:

>>> from sklearn.ensemble import RandomForestRegressor
>>> regressor = RandomForestRegressor(n_estimators=100,
                                  max_depth=10,
                                  min_samples_split=3,
                                  random_state=42)
>>> regressor.fit(X_train, y_train)
>>> predictions = regressor.predict(X_test)
>>> print(predictions)
[1.31785493 1.29359614 1.24146512 1.06039979 1.24015576 0.7915538 0.90307069 0.83535894 0.8956997  0.91264529] 

你已经学习了三种回归算法。那么,我们应该如何评估回归性能呢?让我们在接下来的部分中找出答案。

评估回归性能

到目前为止,我们已经深入讨论了三种流行的回归算法,并通过使用几个著名的库从零实现了它们。我们需要通过以下指标来评估模型在测试集上的表现,而不是仅仅通过输出预测值来判断模型的好坏,这些指标能为我们提供更深入的见解:

  • 如我所提到的,MSE(均方误差)衡量的是对应于期望值的平方损失。有时,MSE 会取平方根,以便将该值转换回目标变量的原始尺度。这就得到了均方根误差RMSE)。此外,RMSE 的一个优点是对大误差的惩罚更为严厉,因为我们首先计算的是误差的平方。

  • 相反,平均绝对误差MAE)衡量的是绝对损失。它使用与目标变量相同的尺度,并让我们了解预测值与实际值的接近程度。

对于 MSE 和 MAE 而言,值越小,回归模型越好。

  • R²(读作r 平方)表示回归模型拟合的好坏。它是回归模型能够解释的因变量变化的比例。其值范围从01,表示从无拟合到完美预测。R²有一个变种叫做调整后的R²,它会根据模型中的特征数与数据点数的比值进行调整。

让我们在一个线性回归模型上计算这三个指标,使用 scikit-learn 中的相应函数:

  1. 我们将再次使用糖尿病数据集,并通过网格搜索技术对线性回归模型的参数进行微调:

    >>> diabetes = datasets.load_diabetes()
    >>> num_test = 30 # the last 30 samples as testing set
    >>> X_train = diabetes.data[:-num_test, :]
    >>> y_train = diabetes.target[:-num_test]
    >>> X_test = diabetes.data[-num_test:, :]
    >>> y_test = diabetes.target[-num_test:]
    >>> param_grid = {
    ...     "alpha": [1e-07, 1e-06, 1e-05],
    ...     "penalty": [None, "l2"],
    ...     "eta0": [0.03, 0.05, 0.1],
    ...     "max_iter": [500, 1000]
    ... }
    >>> from sklearn.model_selection import GridSearchCV
    >>> regressor = SGDRegressor(loss='squared_error',
                                 learning_rate='constant',
                                 random_state=42)
    >>> grid_search = GridSearchCV(regressor, param_grid, cv=3) 
    
  2. 我们获得了最优的参数集:

    >>> grid_search.fit(X_train, y_train)
    >>> print(grid_search.best_params_)
    {'alpha': 1e-07, 'eta0': 0.05, 'max_iter': 500, 'penalty': None}
    >>> regressor_best = grid_search.best_estimator_ 
    
  3. 我们用最优模型预测测试集:

    >>> predictions = regressor_best.predict(X_test) 
    
  4. 我们基于 MSE、MAE 和 R²指标评估测试集的性能:

    >>> from sklearn.metrics import mean_squared_error,
        mean_absolute_error, r2_score
    >>> print(mean_squared_error(y_test, predictions))
    1933.3953304460413
    >>> print(mean_absolute_error(y_test, predictions))
    35.48299900764652
    >>> print(r2_score(y_test, predictions))
    0.6247444629690868 
    

现在你已经了解了三种(或者四种,可以这么说)常用且强大的回归算法和性能评估指标,让我们利用它们来解决股票价格预测问题。

使用三种回归算法预测股票价格

以下是预测股票价格的步骤:

  1. 之前,我们基于 1990 年到 2023 年上半年的数据生成了特征,现在我们将继续使用 1990 年到 2022 年的数据构建训练集,使用 2023 年上半年的数据构建测试集:

    >>> data_raw = pd.read_csv('19900101_20230630.csv', index_col='Date')
    >>> data = generate_features(data_raw)
    >>> start_train = '1990-01-01'
    >>> end_train = '2022-12-31'
    >>> start_test = '2023-01-01'
    >>> end_test = '2023-06-30'
    >>> data_train = data.loc[start_train:end_train]
    >>> X_train = data_train.drop('close', axis=1).values
    >>> y_train = data_train['close'].values
    >>> print(X_train.shape)
    (8061, 37)
    >>> print(y_train.shape)
    (8061,) 
    

dataframe数据中的所有字段除了'close'都是特征列,'close'是目标列。我们有8,061个训练样本,每个样本是37维的。我们还拥有124个测试样本:

>>> data_train = data.loc[start_train:end_train]
>>> X_train = data_train.drop('close', axis=1).values
>>> y_train = data_train['close'].values
>>> print(X_test.shape)
(124, 37) 

最佳实践

时间序列数据通常表现出时间依赖性,即某个时间点的值受前一个时间点值的影响。忽略这些依赖性可能导致模型性能差。我们需要使用训练集和测试集划分来评估模型,确保测试集的数据来自训练集之后的时间段,以模拟现实中的预测场景。

  1. 我们将首先尝试基于 SGD 的线性回归。在训练模型之前,你应该意识到基于 SGD 的算法对特征尺度差异很大的数据非常敏感;例如,在我们的案例中,open特征的平均值大约是 3,777,而moving_avg_365特征的平均值大约是 0.00052。因此,我们需要将特征归一化为相同或可比的尺度。我们通过移除均值并使用StandardScaler将数据重新缩放到单位方差来实现这一点:

    >>> from sklearn.preprocessing import StandardScaler
    >>> scaler = StandardScaler() 
    
  2. 我们使用scaler对两个数据集进行缩放,scaler由训练集教得:

    >>> X_scaled_train = scaler.fit_transform(X_train)
    >>> X_scaled_test = scaler.transform(X_test) 
    
  3. 现在,我们可以通过搜索具有最佳参数集的基于 SGD 的线性回归。我们指定l2正则化和5000次最大迭代,并调整正则化项乘数alpha和初始学习率eta0

    >>> param_grid = {
    ...     "alpha": [1e-4, 3e-4, 1e-3],
    ...     "eta0": [0.01, 0.03, 0.1],
    ... }
    >>> lr = SGDRegressor(penalty='l2', max_iter=5000, random_state=42) 
    
  4. 对于交叉验证,我们需要确保每次拆分中的训练数据在相应的测试数据之前,从而保持时间序列的时间顺序。在这里,我们使用 scikit-learn 的TimeSeriesSplit方法:

    >>> from sklearn.model_selection import TimeSeriesSplit
    >>> tscv = TimeSeriesSplit(n_splits=3)
    >>> grid_search = GridSearchCV(lr, param_grid, cv=tscv, scoring='r2')
    >>> grid_search.fit(X_scaled_train, y_train) 
    

在这里,我们创建了一个 3 折时间序列特定的交叉验证器,并在网格搜索中使用它。

  1. 选择最佳的线性回归模型并对测试样本进行预测:

    >>> print(grid_search.best_params_)
    {'alpha': 0.0001, 'eta0': 0.1}
    >>> lr_best = grid_search.best_estimator_
    >>> predictions_lr = lr_best.predict(X_scaled_test) 
    
  2. 通过 R²测量预测性能:

    >>> print(f'R²: {r2_score(y_test, predictions_lr):.3f}'): 0.959 
    

我们通过精调的线性回归模型实现了0.959的 R²。

最佳实践

使用时间序列数据时,由于时间模式的复杂性,可能会存在过拟合的风险。如果没有正确地进行正则化,模型可能会捕捉到噪声而不是实际的模式。我们需要应用正则化技术,如 L1 或 L2 正则化,以防止过拟合。此外,在进行超参数调优的交叉验证时,考虑使用时间序列特定的交叉验证方法来评估模型性能,同时保持时间顺序。

  1. 类似地,让我们尝试一个决策树。我们调整树的最大深度max_depth;进一步分割节点所需的最小样本数min_samples_split;以及形成叶节点所需的最小样本数min_samples_leaf,如下所示:

    >>> param_grid = {
    ...     'max_depth': [20, 30, 50],
    ...     'min_samples_split': [2, 5, 10],
    ...     'min_samples_leaf': [1, 3, 5]
    ... }
    >>> dt = DecisionTreeRegressor(random_state=42)
    >>> grid_search = GridSearchCV(dt, param_grid, cv=tscv,
                                   scoring='r2', n_jobs=-1)
    >>> grid_search.fit(X_train, y_train) 
    

请注意,这可能需要一些时间;因此,我们使用所有可用的 CPU 核心进行训练。

  1. 选择最佳的回归森林模型并对测试样本进行预测:

    >>> print(grid_search.best_params_)
    {'max_depth': 30, 'min_samples_leaf': 3, 'min_samples_split': 2}
    >>> dt_best = grid_search.best_estimator_
    >>> predictions_dt = dt_best.predict(X_test) 
    
  2. 如下所示测量预测性能:

    >>> print(f'R²: {r2_score(y_test, predictions_rf):.3f}'): 0.912 
    

通过调整过的决策树,获得了0.912的 R²。

  1. 最后,我们尝试了一个随机森林。我们指定 30 棵决策树进行集成,并调整每棵树使用的相同超参数集,如下所示:

    >>> param_grid = {
    ...     'max_depth': [20, 30, 50],
    ...     'min_samples_split': [2, 5, 10],
    ...     'min_samples_leaf': [1, 3, 5]
    ... }
    >>> rf = RandomForestRegressor(n_estimators=30, n_jobs=-1, random_state=42)
    >>> grid_search = GridSearchCV(rf, param_grid, cv=tscv,
                                   scoring='r2', n_jobs=-1)
    >>> grid_search.fit(X_train, y_train) 
    

请注意,这可能需要一些时间;因此,我们使用所有可用的 CPU 核心进行训练(通过n_jobs=-1表示)。

  1. 选择最佳的回归森林模型并对测试样本进行预测:

    >>> print(grid_search.best_params_)
    {'max_depth': 30, 'min_samples_leaf': 1, 'min_samples_split': 5}
    >>> rf_best = grid_search.best_estimator_
    >>> predictions_rf = rf_best.predict(X_test) 
    
  2. 如下所示测量预测性能:

    >>> print(f'R²: {r2_score(y_test, predictions_rf):.3f}'): 0.937 
    

通过调整过的森林回归器,获得了0.937的 R²。

  1. 我们还绘制了三种算法生成的预测值,并与真实值进行对比:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_05_11.png

图 5.11:使用三种算法的预测值与真实值的对比

可视化是通过以下代码生成的:

>>> plt.rc('xtick', labelsize=10)
>>> plt.rc('ytick', labelsize=10)
>>> plt.plot(data_test.index, y_test, c='k')
>>> plt.plot(data_test.index, predictions_lr, c='b')
>>> plt.plot(data_test.index, predictions_dt, c='g')
>>> plt.plot(data_test.index, predictions_rf, c='r')
>>> plt.xticks(range(0, 130, 10), rotation=60)
>>> plt.xlabel('Date', fontsize=10)
>>> plt.ylabel('Close price', fontsize=10)
>>> plt.legend(['Truth', 'Linear regression', 'Decision tree', 'Random forest'], fontsize=10)
>>> plt.show() 

在本节中,我们使用三种回归算法分别构建了一个股票预测器。总体来看,线性回归优于其他两种算法。

股市以其剧烈波动而闻名。与本章中更为稳定的系统或明确的项目不同,股票价格是波动的,受到难以量化的复杂因素的影响。此外,甚至是最复杂的模型也难以捕捉其行为。因此,在现实世界中准确预测股市一直是一个众所周知的难题。这使得探索不同机器学习模型的能力成为一项引人入胜的挑战。

摘要

本章中,我们使用机器学习回归技术进行了股票(具体来说是股票指数)价格预测项目。回归估计一个连续的目标变量,而分类则估计离散的输出。

我们从简要介绍股市及其影响交易价格的因素开始。接着,我们深入讨论了三种流行的回归算法:线性回归、回归树和回归森林。我们涵盖了它们的定义、原理及从零开始的实现,使用了包括 scikit-learn 和 TensorFlow 在内的几个流行框架,并应用于玩具数据集。你还学习了用于评估回归模型的指标。最后,我们将本章所学应用于解决股票价格预测问题。

在接下来的章节中,我们将继续进行股票价格预测项目,但这次我们将使用强大的神经网络。我们将看看它们是否能够超越本章中通过三种回归模型取得的成果。

练习

  1. 如前所述,你能否向我们的股票预测系统添加更多信号,比如其他主要指数的表现?这样做是否能提升预测效果?

  2. 尝试将这三种回归模型进行集成,例如通过对预测结果进行平均,看看你是否能取得更好的表现。

加入我们书籍的 Discord 空间

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/yuxi

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/QR_Code187846872178698968.png

第六章:使用人工神经网络预测股票价格

继续上章的股票价格预测项目,在本章中,我们将深入介绍神经网络模型。我们将从构建最简单的神经网络开始,并通过向其中添加更多计算单元来深入探讨。我们将介绍神经网络的构建模块和其他重要概念,包括激活函数、前向传播和反向传播。我们还将使用 scikit-learn、TensorFlow 和 PyTorch 从头开始实现神经网络。我们将关注如何在不发生过拟合的情况下高效地使用神经网络进行学习,采用 Dropout 和早停技术。最后,我们将训练一个神经网络来预测股票价格,看看它是否能超越我们在上一章中使用的三种回归算法的结果。

本章将涉及以下内容:

  • 解密神经网络

  • 构建神经网络

  • 选择正确的激活函数

  • 防止神经网络过拟合

  • 使用神经网络预测股票价格

解密神经网络

这可能是媒体中最常提到的模型之一,人工神经网络ANNs);更常见的叫法是神经网络。有趣的是,神经网络曾被大众(错误地)认为等同于机器学习或人工智能。

人工神经网络(ANN)只是机器学习中众多算法之一,而机器学习是人工智能的一个分支。它是实现通用人工智能AGI)的途径之一,AGI 是一种假设中的人工智能类型,能够像人类一样思考、学习和解决问题。

尽管如此,它仍然是最重要的机器学习模型之一,并且随着深度学习DL)革命的推进,神经网络也在快速发展。

首先,让我们理解神经网络是如何工作的。

从单层神经网络开始

我们从解释网络中的不同层开始,然后介绍激活函数,最后介绍使用反向传播训练网络。

神经网络中的层

一个简单的神经网络由三层组成——输入层隐藏层输出层,如下图所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_01.png

图 6.1:一个简单的浅层神经网络

节点(也称为单元)的概念集合,模拟生物大脑中的神经元。输入层表示输入特征x,每个节点是一个预测特征,x。输出层表示目标变量。

在二分类问题中,输出层只包含一个节点,其值表示正类的概率。在多分类问题中,输出层由n个节点组成,其中n是可能的类别数,每个节点的值表示预测该类别的概率。在回归问题中,输出层只包含一个节点,其值表示预测结果。

隐藏层可以视为从前一层提取的潜在信息的组合。可以有多个隐藏层。使用具有两个或更多隐藏层的神经网络进行学习称为深度学习。在本章中,我们将首先聚焦于一个隐藏层。

两个相邻的层通过概念性边(类似于生物大脑中的突触)连接,这些边传递来自一个神经元的信号到下一个层中的另一个神经元。通过模型的权重W进行参数化。例如,前图中的W((1))连接输入层和隐藏层,*W*((2))连接隐藏层和输出层。

在标准神经网络中,数据仅从输入层传递到输出层,通过一个或多个隐藏层。因此,这种网络被称为前馈神经网络。基本上,逻辑回归是一个没有隐藏层的前馈神经网络,输出层直接与输入层连接。添加隐藏层在输入层和输出层之间引入了非线性。这使得神经网络能够更好地学习输入数据与目标之间的潜在关系。

激活函数

激活函数是应用于神经网络中每个神经元输出的数学操作。它根据神经元接收到的输入来决定该神经元是否应被激活(即,其输出值是否应传播到下一层)。

假设输入xn维的,隐藏层由H个隐藏单元组成。连接输入层和隐藏层的权重矩阵W^((1))的大小是nH,其中每一列,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_001.png,表示与第h个隐藏单元相关的输入系数。隐藏层的输出(也称为激活)可以用以下数学公式表示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_002.png

这里,f(z) 是一个激活函数。顾名思义,激活函数检查每个神经元的激活程度,模拟我们大脑的工作方式。它们的主要目的是为神经元的输出引入非线性,使得网络能够学习并执行输入与输出之间的复杂映射。典型的激活函数包括逻辑函数(在神经网络中更常被称为sigmoid函数)和tanh函数,后者被认为是逻辑函数的重新缩放版本,以及ReLURectified Linear Unit的简称),它在深度学习中经常使用:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_003.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_004.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_005.png

我们绘制这三种激活函数如下:

  • 逻辑sigmoid)函数,其中输出值的范围为(0, 1)

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_02.png

图 6.2:逻辑函数

可视化通过以下代码生成:

>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> def sigmoid(z):
        return 1.0 / (1 + np.exp(-z))
>>> z = np.linspace(-8, 8, 1000)
>>> y = sigmoid(z)
>>> plt.plot(z, y)
>>> plt.xlabel('z')
>>> plt.ylabel('y(z)')
>>> plt.title('logistic')
>>> plt.grid()
>>> plt.show() 
  • tanh函数图像,其中输出值的范围为(-1, 1)

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_03.png

图 6.3:tanh 函数

可视化通过以下代码生成:

>>> def tanh(z):
        return (np.exp(z) - np.exp(-z)) / (np.exp(z) + np.exp(-z))
>>> z = np.linspace(-8, 8, 1000)
>>> y = tanh(z)
>>> plt.plot(z, y)
>>> plt.xlabel('z')
>>> plt.ylabel('y(z)')
>>> plt.title('tanh')
>>> plt.grid()
>>> plt.show() 
  • ReLU函数图像,其中输出值的范围为(0, +inf)

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_04.png

图 6.4:ReLU 函数

可视化通过以下代码生成:

>>> relu(z):
        return np.maximum(np.zeros_like(z), z)
>>> z = np.linspace(-8, 8, 1000)
>>> y = relu(z)
>>> plt.plot(z, y)
>>> plt.xlabel('z')
>>> plt.ylabel('y(z)')
>>> plt.title('relu')
>>> plt.grid()
>>> plt.show() 

至于输出层,假设有一个输出单元(回归或二分类),且连接隐层和输出层的权重矩阵W^((2))的大小为H × 1。在回归问题中,输出可以通过以下数学公式表示(为保持一致性,我这里使用a^((3))而非y):

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_006.png

普适逼近定理是理解神经网络如何实现学习的关键概念。该定理指出,具有单个隐层且隐层包含有限数量神经元的前馈神经网络,能够逼近任何连续函数,并达到任意精度,只要隐层的神经元数量足够大。在训练过程中,神经网络通过调整其参数(权重)来学习逼近目标函数。通常,这是通过优化算法实现的,例如梯度下降法,算法通过迭代更新参数,以最小化预测输出与真实目标之间的差异。让我们在下一节中详细了解这个过程。

反向传播

那么,我们如何获得模型的最优权重,*W = {W(1), W(2)}*呢?与逻辑回归类似,我们可以通过梯度下降法学习所有权重,目标是最小化均方误差MSE)代价函数或其他损失函数,J(W)。不同之处在于,梯度!是通过反向传播计算的。每次通过网络的前向传播后,都会执行反向传播来调整模型的参数。

正如“back”这个词在名称中所暗示的那样,梯度的计算是从后往前进行的:首先计算最后一层的梯度,然后计算第一层的梯度。至于“propagation”,它意味着在计算一个层的梯度时,部分计算结果会在计算前一层梯度时被重复使用。误差信息是层层传播的,而不是单独计算的。

在单层网络中,反向传播的详细步骤如下:

  1. 我们从输入层到输出层遍历整个网络,并计算隐藏层的输出值,a((2)),以及输出层的输出值,*a*((3))。这是前向传播步骤。

  2. 对于最后一层,我们计算代价函数相对于输出层输入的导数:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_008.png

  1. 对于隐藏层,我们计算代价函数相对于隐藏层输入的导数:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_009.png

  1. 我们通过应用链式法则计算梯度:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_010.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_011.png

  1. 我们用计算出的梯度和学习率更新权重!

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_013.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_014.png

这里,m是样本的数量。

我们通过以下步骤反复更新所有权重,使用最新的权重直到代价函数收敛或模型完成足够的迭代。

链式法则是微积分中的一个基本概念,它允许你找到复合函数的导数。你可以在斯坦福大学的数学课程中了解更多相关内容(mathematics.stanford.edu/events/chain-rule-calculus),或者在麻省理工学院的微分数学课程第 6 模块,微分应用中学习(ocw.mit.edu/courses/18-03sc-differential-equations-fall-2011/)。

这可能一开始不容易理解,因此在接下来的章节中,我们将从零开始实现它,这将帮助你更好地理解神经网络。

向神经网络添加更多层:深度学习(DL)

在实际应用中,神经网络通常有多个隐藏层。这就是深度学习(DL)得名的原因——使用具有“堆叠”隐藏层的神经网络进行学习。一个深度学习模型的示例如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_05.png

图 6.5:一个深度神经网络

在多层隐藏层的堆栈中,一个隐藏层的输入是其前一个层的输出,正如你在图 6.5中看到的那样。特征(信号)是从每个隐藏层提取的。来自不同层的特征表示来自不同层次的模式。超越浅层神经网络(通常只有一个隐藏层),一个具有正确网络架构和参数的深度学习模型(通常有两个或更多隐藏层)能够更好地从数据中学习复杂的非线性关系。

让我们看一些深度学习的典型应用,让你更有动力开始即将到来的深度学习项目。

计算机视觉被广泛认为是深度学习取得巨大突破的领域。你将在第十一章《使用卷积神经网络对服装图像进行分类》和第十四章《使用 CLIP 构建图像搜索引擎:一种多模态方法》中学到更多内容。现在,以下是计算机视觉中的一些常见应用:

  • 图像识别,如人脸识别和手写数字识别。手写数字识别以及常见的评估数据集 MNIST 已成为深度学习中的“Hello, World!”项目。

  • 基于图像的搜索引擎在其图像分类和图像相似度编码组件中大量使用深度学习技术。

  • 机器视觉,这是自动驾驶汽车的关键部分,能够感知摄像头视图并做出实时决策。

  • 从黑白照片恢复颜色以及艺术风格转移,巧妙地融合两种不同风格的图像。谷歌艺术与文化(artsandculture.google.com/)中的人工艺术作品令人印象深刻。

  • 基于文本描述的逼真图像生成。这在创建视觉故事内容和帮助营销广告创作中有应用。

自然语言处理NLP)是另一个你可以看到深度学习在其现代解决方案中占主导地位的领域。你将在第十二章《使用递归神经网络进行序列预测》和第十三章《利用 Transformer 模型推进语言理解与生成》中学到更多内容。但现在让我们快速看一些示例:

  • 机器翻译,深度学习极大地提高了其准确性和流畅性,例如基于句子的谷歌神经机器翻译GNMT)系统。

  • 文本生成通过学习句子和段落中单词之间复杂的关系,利用深度神经网络再现文本。如果你能在 J. K. 罗琳或莎士比亚的作品上充分训练模型,你就能成为一位虚拟的 J. K. 罗琳或莎士比亚。

  • 图像描述(也称为图像到文本)利用深度神经网络来检测和识别图像中的物体,并用易于理解的句子“描述”这些物体。它结合了计算机视觉和自然语言处理(NLP)领域的最新突破。示例可以在cs.stanford.edu/people/karpathy/deepimagesent/generationdemo/找到(由斯坦福大学的 Andrej Karpathy 开发)。

  • 在其他常见的自然语言处理任务中,如情感分析和信息检索与提取,深度学习模型已经取得了最先进的性能。

  • 人工智能生成内容AIGC)是近年来的一个重大突破。它使用深度学习技术来创建或辅助创作各种类型的内容,如文章、产品描述、音乐、图像和视频。

类似于浅层网络,我们通过梯度下降法在深度神经网络中学习所有的权重,目标是最小化 MSE 损失函数,J(W)。梯度,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_007.png,是通过反向传播计算的。不同之处在于,我们反向传播的不止一层隐藏层。在接下来的章节中,我们将从浅层网络开始,逐步实现深度神经网络。

构建神经网络

本实操部分将从实现一个浅层网络开始,随后使用 scikit-learn 构建一个包含两层的深度网络。然后,我们将用 TensorFlow 和 PyTorch 实现一个深度网络。

从零开始实现神经网络

为了演示激活函数的工作原理,我们将在这个示例中使用 sigmoid 作为激活函数。

我们首先定义sigmoid函数及其导数函数:

>>> def sigmoid_derivative(z):
...     return sigmoid(z) * (1.0 - sigmoid(z)) 

如果你有兴趣验证它,你可以自行推导导数。

然后我们定义训练函数,该函数接受训练数据集、隐藏层单元数(我们将仅使用一个隐藏层作为示例)和迭代次数:

>>> def train(X, y, n_hidden, learning_rate, n_iter):
...     m, n_input = X.shape
...     W1 = np.random.randn(n_input, n_hidden)
...     b1 = np.zeros((1, n_hidden))
...     W2 = np.random.randn(n_hidden, 1)
...     b2 = np.zeros((1, 1))
...     for i in range(1, n_iter+1):
...         Z2 = np.matmul(X, W1) + b1
...         A2 = sigmoid(Z2)
...         Z3 = np.matmul(A2, W2) + b2
...         A3 = Z3
...
...         dZ3 = A3 - y
...         dW2 = np.matmul(A2.T, dZ3)
...         db2 = np.sum(dZ3, axis=0, keepdims=True)
...
...         dZ2 = np.matmul(dZ3, W2.T) * sigmoid_derivative(Z2)
...         dW1 = np.matmul(X.T, dZ2)
...         db1 = np.sum(dZ2, axis=0)
...
...         W2 = W2 - learning_rate * dW2 / m
...         b2 = b2 - learning_rate * db2 / m
...         W1 = W1 - learning_rate * dW1 / m
...         b1 = b1 - learning_rate * db1 / m
...
...         if i % 100 == 0:
...             cost = np.mean((y - A3) ** 2)
...             print('Iteration %i, training loss: %f' %
                                                  (i, cost))
...     model = {'W1': W1, 'b1': b1, 'W2': W2, 'b2': b2}
...     return model 

请注意,除了权重 W 外,我们还使用偏置 b。在训练之前,我们首先随机初始化权重和偏置。在每次迭代中,我们将网络的所有层馈送上最新的权重和偏置,然后使用反向传播算法计算梯度,最后用得到的梯度更新权重和偏置。为了检查训练性能,我们每 100 次迭代输出一次损失和 MSE。

为了测试模型,我们将再次使用加利福尼亚房价作为示例数据集。提醒一下,使用梯度下降时通常建议进行数据归一化。因此,我们将通过去除均值并缩放到单位方差来标准化输入数据:

>>> from sklearn import datasets
>>> housing = datasets.fetch_california_housing()
>>> num_test = 10 # the last 10 samples as testing set
>>> from sklearn import preprocessing
>>> scaler = preprocessing.StandardScaler()
>>> X_train = housing.data[:-num_test, :]
>>> X_train = scaler.fit_transform(X_train)
>>> y_train = housing.target[:-num_test].reshape(-1, 1)
>>> X_test = housing.data[-num_test:, :]
>>> X_test = scaler.transform(X_test)
>>> y_test = housing.target[-num_test:] 

使用缩放后的数据集,我们现在可以训练一个包含20个隐藏单元、学习率为0.1,并进行2000次迭代的单层神经网络:

>>> n_hidden = 20
>>> learning_rate = 0.1
>>> n_iter = 2000
>>> model = train(X_train, y_train, n_hidden, learning_rate, n_iter)
Iteration 100, training loss: 0.557636
Iteration 200, training loss: 0.519375
Iteration 300, training loss: 0.501025
Iteration 400, training loss: 0.487536
Iteration 500, training loss: 0.476553
Iteration 600, training loss: 0.467207
Iteration 700, training loss: 0.459076
Iteration 800, training loss: 0.451934
Iteration 900, training loss: 0.445621
Iteration 1000, training loss: 0.440013
Iteration 1100, training loss: 0.435024
Iteration 1200, training loss: 0.430558
Iteration 1300, training loss: 0.426541
Iteration 1400, training loss: 0.422920
Iteration 1500, training loss: 0.419653
Iteration 1600, training loss: 0.416706
Iteration 1700, training loss: 0.414049
Iteration 1800, training loss: 0.411657
Iteration 1900, training loss: 0.409502
Iteration 2000, training loss: 0.407555 

然后,我们定义一个预测函数,该函数将接受一个模型并生成回归结果:

>>> def predict(x, model):
...     W1 = model['W1']
...     b1 = model['b1']
...     W2 = model['W2']
...     b2 = model['b2']
...     A2 = sigmoid(np.matmul(x, W1) + b1)
...     A3 = np.matmul(A2, W2) + b2
...     return A3 

最后,我们将训练好的模型应用到测试集上:

>>> predictions = predict(X_test, model) 

打印出预测结果及其真实值以进行对比:

>>> print(predictions[:, 0])
[1.11805681 1.1387508  1.06071523 0.81930286 1.21311999 0.6199933 0.92885765 0.81967297 0.90882797 0.87857088]
>>> print(y_test)
[1.12  1.072 1.156 0.983 1.168 0.781 0.771 0.923 0.847 0.894] 

在成功构建了一个从零开始的神经网络模型之后,我们将开始使用 scikit-learn 实现它。

使用 scikit-learn 实现神经网络

我们将使用MLPRegressor类(MLP代表多层感知器,是神经网络的别名)来实现神经网络:

>>> from sklearn.neural_network import MLPRegressor
>>> nn_scikit = MLPRegressor(hidden_layer_sizes=(16, 8),
                         activation='relu',
                         solver='adam',
                         learning_rate_init=0.001,
                         random_state=42,
                         max_iter=2000) 

hidden_layer_sizes超参数表示隐藏神经元的数量。在这个例子中,网络包含两个隐藏层,分别有16个节点和8个节点。使用 ReLU 激活函数。

Adam 优化器是随机梯度下降算法的替代品。它基于训练数据自适应地更新梯度。有关 Adam 的更多信息,请查看arxiv.org/abs/1412.6980中的论文。

我们在训练集上拟合神经网络模型,并在测试数据上进行预测:

>>> nn_scikit.fit(X_train, y_train.ravel())
>>> predictions = nn_scikit.predict(X_test)
>>> print(predictions)
[1.19968791 1.2725324  1.30448323 0.88688675 1.18623612 0.72605956 0.87409406 0.85671201 0.93423154 0.94196305] 

然后,我们计算预测的 MSE:

>>> from sklearn.metrics import mean_squared_error
>>> print(mean_squared_error(y_test, predictions))
0.010613171947751738 

我们已经使用 scikit-learn 实现了神经网络。接下来,我们将在下一部分使用 TensorFlow 实现神经网络。

使用 TensorFlow 实现神经网络

在 TensorFlow 2.x 中,通过 Keras 模块(keras.io/)启动一个深度神经网络模型非常简单。让我们按照以下步骤使用 TensorFlow 实现神经网络:

  1. 首先,我们导入必要的模块并设置随机种子,推荐使用随机种子以确保模型的可重复性:

    >>> import tensorflow as tf
    >>> from tensorflow import keras
    >>> tf.random.set_seed(42) 
    
  2. 接下来,我们通过将一系列层实例传递给构造函数来创建一个 Keras 顺序模型,其中包括两个完全连接的隐藏层,分别包含16个节点和8个节点。同时,使用 ReLU 激活函数:

    >>> model = keras.Sequential([
    ...     keras.layers.Dense(units=16, activation='relu'),
    ...     keras.layers.Dense(units=8, activation='relu'),
    ...     keras.layers.Dense(units=1)
    ... ]) 
    
  3. 我们通过使用 Adam 优化器,学习率为0.01,并将 MSE 作为学习目标来编译模型:

    >>> model.compile(loss='mean_squared_error',
    ...               optimizer=tf.keras.optimizers.Adam(0.01)) 
    
  4. 在定义模型之后,我们现在开始在训练集上进行训练:

    >>> model.fit(X_train, y_train, epochs=300)
    Train on 496 samples
    Epoch 1/300
    645/645 [==============================] - 1s 1ms/step - loss: 0.6494
    Epoch 2/300
    645/645 [==============================] - 1s 1ms/step - loss: 0.3827
    Epoch 3/300
    645/645 [==============================] - 1s 1ms/step - loss: 0.3700
    ……
    ……
    Epoch 298/300
    645/645 [==============================] - 1s 1ms/step - loss: 0.2724
    Epoch 299/300
    645/645 [==============================] - 1s 1ms/step - loss: 0.2735
    Epoch 300/300
    645/645 [==============================] - 1s 1ms/step - loss: 0.2730
    1/1 [==============================] - 0s 82ms/step 
    

我们使用300次迭代来拟合模型。在每次迭代中,都会显示训练损失(MSE)。

  1. 最后,我们使用训练好的模型对测试案例进行预测,并打印出预测结果及其 MSE:

    >>> predictions = model.predict(X_test)[:, 0]
    >>> print(predictions)
    [1.2387774  1.2480505  1.229521   0.8988129  1.1932802  0.75052583 0.75052583 0.88086814 0.9921638  0.9107932 ]
    >>> print(mean_squared_error(y_test, predictions))
    0.008271122735361234 
    

如你所见,我们在 TensorFlow Keras API 中逐层添加神经网络模型。我们从第一个隐藏层(16 个节点)开始,然后是第二个隐藏层(8 个节点),最后是输出层(1 个单元,目标变量)。这与构建乐高积木非常相似。

在工业界,神经网络通常使用 PyTorch 实现。让我们在下一部分看看如何实现。

使用 PyTorch 实现神经网络

我们现在将按照以下步骤使用 PyTorch 实现神经网络:

  1. 首先,我们导入必要的模块并设置随机种子,推荐使用随机种子以确保模型的可重复性:

    >>> import torch
    >>> import torch.nn as nn
    >>> torch.manual_seed(42) 
    
  2. 接下来,我们通过将一个包含两个全连接隐藏层(分别有 16 个节点和 8 个节点)实例的层列表传递给构造函数,创建一个 torch.nn Sequential 模型。每个全连接层中都使用了 ReLU 激活函数:

    >>> model = nn.Sequential(nn.Linear(X_train.shape[1], 16),
                          nn.ReLU(),
                          nn.Linear(16, 8),
                          nn.ReLU(),
                          nn.Linear(8, 1)) 
    
  3. 我们初始化一个学习率为0.01、目标为 MSE 的 Adam 优化器:

    >>> loss_function = nn.MSELoss()
    >>> optimizer = torch.optim.Adam(model.parameters(), lr=0.01) 
    
  4. 定义完模型后,我们需要在使用它训练 PyTorch 模型之前,从输入的 NumPy 数组中创建张量对象:

    >>> X_train_torch = torch.from_numpy(X_train.astype(np.float32))
    >>> y_train_torch = torch.from_numpy(y_train.astype(np.float32)) 
    
  5. 现在我们可以在与 PyTorch 兼容的训练集上训练模型。我们首先定义一个训练函数,在每个周期内调用它,如下所示:

    >>> def train_step(model, X_train, y_train, loss_function, optimizer):
            pred_train = model(X_train)
            loss = loss_function(pred_train, y_train)
    
            model.zero_grad()
            loss.backward()
            optimizer.step()
            return loss.item() 
    
  6. 我们用500次迭代来训练模型。在每 100 次迭代中,显示训练损失(MSE)如下:

    >>> for epoch in range(500):
            loss = train_step(model, X_train_torch, y_train_torch,
                              loss_function, optimizer)      
            if epoch % 100 == 0:
                print(f"Epoch {epoch} - loss: {loss}")
    Epoch 0 - loss: 4.908532619476318
    Epoch 100 - loss: 0.5002815127372742
    Epoch 200 - loss: 0.40820521116256714
    Epoch 300 - loss: 0.3870624303817749
    Epoch 400 - loss: 0.3720889091491699 
    
  7. 最后,我们使用训练好的模型预测测试样本并输出预测值及其 MSE:

    >>> X_test_torch = torch.from_numpy(X_test.astype(np.float32))
    >>> predictions = model(X_test_torch).detach().numpy()[:, 0]
    >>> print(predictions)
    [1.171479  1.130001  1.1055213 0.8627995 1.0910968 0.6725116 0.8869568 0.8009699 0.8529027 0.8760005]
    >>> print(mean_squared_error(y_test, predictions))
    0.006939044434639928 
    

事实证明,使用 PyTorch 开发神经网络模型就像搭建乐高一样简单。

PyTorch 和 TensorFlow 都是流行的深度学习框架,它们的流行程度可能会因应用领域、研究社区、行业采纳和个人偏好等不同因素而有所不同。然而,截至 2023 年,PyTorch 的普及度更高,整体用户基础更大,根据 Papers With Code(paperswithcode.com/trends)和 Google Trends(trends.google.com/trends/explore?geo=US&q=tensorflow,pytorch&hl=en)的数据。因此,接下来我们将专注于本书中的 PyTorch 实现。

接下来,我们将讨论如何选择合适的激活函数。

选择合适的激活函数

到目前为止,我们在实现中使用了 ReLU 和 sigmoid 激活函数。你可能会想知道如何为你的神经网络选择合适的激活函数。接下来将给出详细的建议,告诉你什么时候选择特定的激活函数:

  • Linearf(z) = z。你可以将其解释为没有激活函数。我们通常在回归网络的输出层中使用它,因为我们不需要对输出进行任何转换。

  • Sigmoid(逻辑)将层的输出转换为 0 和 1 之间的范围。你可以将其理解为输出预测的概率。因此,我们通常在二分类网络的输出层中使用它。除此之外,有时我们也会在隐藏层中使用它。然而,需要注意的是,sigmoid 函数是单调的,但其导数并非单调。因此,神经网络可能会陷入一个次优解。

  • Softmax:正如在第四章《使用逻辑回归预测在线广告点击率》中提到的,softmax 是一种广义的逻辑函数,用于多类别分类。因此,我们在多类别分类网络的输出层中使用它。

  • Tanh 是一种比 sigmoid 更好的激活函数,具有更强的梯度。正如本章早些时候的图表所示,tanh 函数的导数比 sigmoid 函数的导数更陡峭。它的取值范围是 -11。在隐藏层中,常常使用 tanh 函数。

  • ReLU 可能是当今使用最频繁的激活函数。它是前馈网络中隐藏层的“默认”激活函数。它的取值范围是从 0 到无穷大,且该函数及其导数都是单调的。与 tanh 相比,ReLU 有几个优势。首先是稀疏性,意味着在任何给定时刻,只有一部分神经元被激活。这有助于减少训练和推理的计算成本,因为只需计算较少的神经元。ReLU 还可以缓解梯度消失问题,即在反向传播过程中,梯度变得非常小,导致学习变慢或停滞。ReLU 对于正输入不饱和,允许梯度在训练过程中更自由地流动。ReLU 的一个缺点是无法适当地映射输入的负部分,所有负输入都会被转换为 0。为了解决 ReLU 中的“负值消失”问题,Leaky ReLU 被发明出来,它在负部分引入了一个小的斜率。当 z < 0 时,f(z) = az,其中 a 通常是一个小值,如 0.01

总结一下,ReLU 通常用于隐藏层激活函数。如果 ReLU 效果不好,可以尝试 Leaky ReLU。Sigmoid 和 tanh 可以用于隐藏层,但不建议在层数很多的深度网络中使用。在输出层,回归网络使用线性激活(或没有激活);二分类网络使用 sigmoid;多分类问题则使用 softmax。

选择合适的激活函数非常重要,同样避免神经网络中的过拟合也是如此。接下来我们看看如何做到这一点。

防止神经网络中过拟合

神经网络之所以强大,是因为它能够从数据中提取层次特征,只要有合适的架构(合适的隐藏层数和隐藏节点数)。它提供了极大的灵活性,并且可以拟合复杂的数据集。然而,如果网络在学习过程中没有足够的控制,这一优势就会变成劣势。具体来说,如果网络仅仅擅长拟合训练集而不能推广到未见过的数据,就可能导致过拟合。因此,防止过拟合对于神经网络模型的成功至关重要。

目前主要有三种方法可以对神经网络进行限制:L1/L2 正则化、dropout 和提前停止。在第四章《使用逻辑回归预测在线广告点击率》中我们练习了第一种方法,接下来将在本节讨论另外两种方法。

Dropout

丢弃意味着在神经网络的学习阶段忽略某些隐藏节点。这些隐藏节点是根据指定的概率随机选择的。在训练过程的前向传播中,随机选择的节点会暂时不用于计算损失;在反向传播中,随机选择的节点也不会暂时更新。

在下图中,我们选择了网络中的三个节点,在训练过程中忽略它们:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_06.png

图 6.6:在神经网络中需要忽略的三个节点

请记住,常规层的节点与前一层和下一层的节点是完全连接的。如果网络规模过大,模型会记住个别节点对之间的相互依赖,导致过拟合。丢弃通过在每次迭代中暂时停用某些节点来打破这种依赖关系。因此,它有效地减少了过拟合,并且不会同时扰乱学习过程。

在每次迭代中随机选择的节点比例也称为丢弃率。在实践中,我们通常将丢弃率设置为不超过 50%。如果丢弃率过高,会过度影响模型的学习能力,减慢训练速度,并降低模型从数据中提取有用模式的能力。

最佳实践

确定丢弃率需要通过经验进行实验,测试不同丢弃率对模型性能的影响。以下是一个典型的方法:

  1. 从一个较低的丢弃率(例如0.10.2)开始,并在你的数据集上训练模型。监控模型在验证集上的性能指标。

  2. 逐渐以小幅度(例如0.1)增加丢弃率,并每次重新训练模型。在每次训练后监控性能指标。

  3. 评估在不同丢弃率下获得的性能。注意过拟合问题,因为过高的丢弃率可能会影响模型性能;如果丢弃率过低,模型可能无法有效防止过拟合。

在 PyTorch 中,我们使用torch.nn.Dropout对象将丢弃添加到层中。以下是一个示例:

>>> model_with_dropout = nn.Sequential(nn.Linear(X_train.shape[1], 16),
                                   nn.ReLU(),
                                   nn.Dropout(0.1),
                                   nn.Linear(16, 8),
                                   nn.ReLU(),
                                   nn.Linear(8, 1)) 

在前面的示例中,训练过程中在每次迭代时随机忽略了来自第一隐藏层的 10%节点。

请记住,丢弃只应发生在训练阶段。在预测阶段,所有节点应该重新完全连接。因此,在评估模型或使用训练好的模型进行预测之前,我们需要通过.eval()方法将模型切换到评估模式,以禁用丢弃。我们可以在下面的加利福尼亚住房示例中看到:

  1. 首先,我们使用 Adam 优化器和学习率0.01,以 MSE 为学习目标来编译模型(带丢弃):

    >>> optimizer = torch.optim.Adam(model_with_dropout.parameters(), lr=0.01) 
    
  2. 接下来,我们可以训练模型(带丢弃)1,000次迭代:

    >>> for epoch in range(1000):
            loss = train_step(model_with_dropout, X_train_torch, y_train_torch,
                              loss_function, optimizer)
            if epoch % 100 == 0:
                print(f"Epoch {epoch} - loss: {loss}")
    Epoch 0 - loss: 4.921249866485596
    Epoch 100 - loss: 0.5313398838043213
    Epoch 200 - loss: 0.4458008408546448
    Epoch 300 - loss: 0.4264270067214966
    Epoch 400 - loss: 0.4085545539855957
    Epoch 500 - loss: 0.3640516400337219
    Epoch 600 - loss: 0.35677382349967957
    Epoch 700 - loss: 0.35208994150161743
    Epoch 800 - loss: 0.34980857372283936
    Epoch 900 - loss: 0.3431631028652191 
    

每 100 次迭代,显示一次训练损失(MSE)。

  1. 最后,我们使用训练好的模型(带有 dropout)来预测测试案例并输出均方误差(MSE):

    >>> model_with_dropout.eval()
    >>> predictions = model_with_dropout (X_test_torch).detach().numpy()[:, 0]
    >>> print(mean_squared_error(y_test, predictions))
     0.005699420832357341 
    

如前所述,别忘了在评估带有 dropout 的模型之前运行model_with_dropout.eval()。否则,dropout 层将继续随机关闭神经元,导致对相同数据进行多次评估时结果不一致。

提前停止

顾名思义,使用提前停止训练的网络将在模型性能在一定次数的迭代内没有改进时停止训练。模型性能是通过与训练集不同的验证集来评估的,目的是衡量其泛化能力。在训练过程中,如果经过若干次(比如 50 次)迭代后性能下降,说明模型发生了过拟合,无法再很好地进行泛化。因此,在这种情况下,提前停止学习有助于防止过拟合。通常,我们会通过验证集来评估模型。如果验证集上的指标在超过n个训练周期内没有改善,我们就会停止训练过程。

我们还将演示如何在 PyTorch 中应用提前停止,并使用加利福尼亚房价示例:

  1. 首先,我们像之前一样重新创建模型和优化器:

    >>> model = nn.Sequential(nn.Linear(X_train.shape[1], 16),
                          nn.ReLU(),
                          nn.Linear(16, 8),
                          nn.ReLU(),
                          nn.Linear(8, 1))
    >>> optimizer = torch.optim.Adam(model.parameters(), lr=0.01) 
    
  2. 接下来,我们定义提前停止标准,即测试损失在100个周期内没有改进:

    >>> patience = 100
    >>> epochs_no_improve = 0
    >>> best_test_loss = float('inf') 
    
  3. 现在我们采用提前停止,并将模型训练至最多500个迭代周期:

    >>> import copy
    >>> best_model = model
    >>> for epoch in range(500):
            loss = train_step(model, X_train_torch, y_train_torch,
                              loss_function, optimizer)      
            predictions = model(X_test_torch).detach().numpy()[:, 0]
            test_loss = mean_squared_error(y_test, predictions)
            if test_loss > best_test_loss:
                epochs_no_improve += 1
                if epochs_no_improve > patience:
                    print(f"Early stopped at epoch {epoch}")
                    break
            else:
                epochs_no_improve = 0
                best_test_loss = test_loss
                best_model = copy.deepcopy(model)
    Early stopped at epoch 224 
    

在每一步训练后,我们计算测试损失并将其与之前记录下来的最佳损失进行比较。如果测试损失有所改善,我们使用copy模块保存当前模型,并重置epochs_no_improve计数器。然而,如果测试损失在 100 次连续迭代中没有改善,我们就会停止训练过程,因为已经达到了容忍阈值(patience)。在我们的例子中,训练在第224个周期后停止。

  1. 最后,我们使用先前记录下来的最佳模型来预测测试案例,并输出预测结果及其均方误差(MSE):

    >>> predictions = best_model(X_test_torch).detach().numpy()[:, 0]
    >>> print(mean_squared_error(y_test, predictions))
    0.005459465255681108 
    

这比我们在传统方法中得到的0.0069要好,也比使用 dropout 防止过拟合得到的0.0057要好。

虽然通用逼近定理保证神经网络能够表示任何函数,但它并不保证良好的泛化性能。如果模型的容量相对于数据分布的复杂度过大,就可能发生过拟合。因此,通过正则化和提前停止等技术来控制模型的容量,对于确保学习到的函数能够很好地泛化到未见过的数据至关重要。

现在你已经了解了神经网络及其实现,接下来我们将利用它们来解决股票价格预测问题。

使用神经网络预测股票价格

在本节中,我们将使用 PyTorch 构建股票预测器。我们将从特征生成和数据准备开始,然后构建网络并进行训练。之后,我们将微调网络以提升股票预测器的性能。

训练一个简单的神经网络

我们通过以下步骤准备数据并训练一个简单的神经网络:

  1. 我们加载股票数据,生成特征,并标记 generate_features 函数,这个函数我们在 第五章使用回归算法预测股票价格 中开发过:

    >>> data_raw = pd.read_csv('19900101_20230630.csv', index_col='Date')
    >>> data = generate_features(data_raw) 
    
  2. 我们使用 1990 到 2022 年的数据构建训练集,并使用 2023 年上半年的数据构建测试集:

    >>> start_train = '1990-01-01'
    >>> end_train = '2022-12-31'
    >>> start_test = '2023-01-01'
    >>> end_test = '2023-06-30'
    >>> data_train = data.loc[start_train:end_train]
    >>> X_train = data_train.drop('close', axis=1).values
    >>> y_train = data_train['close'].values
    >>> data_test = data.loc[start_test:end_test]
    >>> X_test = data_test.drop('close', axis=1).values
    >>> y_test = data_test['close'].values 
    
  3. 我们需要将特征标准化到相同或可比较的尺度。我们通过去除均值并缩放到单位方差来实现这一点:

    >>> from sklearn.preprocessing import StandardScaler
    >>> scaler = StandardScaler() 
    

我们使用训练集教授的缩放器对两个数据集进行重新缩放:

>>> X_scaled_train = scaler.fit_transform(X_train)
>>> X_scaled_test = scaler.transform(X_test) 
  1. 接下来,我们需要从输入的 NumPy 数组中创建张量对象,然后使用它们来训练 PyTorch 模型:

    >>> X_train_torch = torch.from_numpy(X_scaled_train.astype(np.float32))
    >>> X_test_torch = torch.from_numpy(X_scaled_test.astype(np.float32))
    >>> y_train = y_train.reshape(y_train.shape[0], 1)
    >>> y_train_torch = torch.from_numpy(y_train.astype(np.float32)) 
    
  2. 我们现在使用 torch.nn 模块构建一个神经网络:

    >>> torch.manual_seed(42)
    >>> model = nn.Sequential(nn.Linear(X_train.shape[1], 32),
                              nn.ReLU(),
                              nn.Linear(32, 1)) 
    

我们开始时的网络有一个包含 32 个节点的隐藏层,后面接着一个 ReLU 函数。

  1. 我们通过使用 Adam 作为优化器,学习率为 0.3,MSE 作为学习目标来编译模型:

    >>> loss_function = nn.MSELoss()
    >>> optimizer = torch.optim.Adam(model.parameters(), lr=0.3) 
    
  2. 定义模型后,我们进行 1,000 次迭代的训练:

    >>> for epoch in range(1000):
            loss = train_step(model, X_train_torch, y_train_torch,
                              loss_function, optimizer)
            if epoch % 100 == 0:
                print(f"Epoch {epoch} - loss: {loss}")
    Epoch 0 - loss: 24823446.0
    Epoch 100 - loss: 189974.171875
    Epoch 200 - loss: 52102.01171875
    Epoch 300 - loss: 17849.333984375
    Epoch 400 - loss: 8928.6689453125
    Epoch 500 - loss: 6497.75927734375
    Epoch 600 - loss: 5670.634765625
    Epoch 700 - loss: 5265.48828125
    Epoch 800 - loss: 5017.7021484375
    Epoch 900 - loss: 4834.28466796875 
    
  3. 最后,我们使用训练好的模型来预测测试数据并显示指标:

    >>> predictions = model(X_test_torch).detach().numpy()[:, 0]
    >>> from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
    >>> print(f'MSE: {mean_squared_error(y_test, predictions):.3f}')
    MSE: 30051.643
    >>> print(f'MAE: {mean_absolute_error(y_test, predictions):.3f}')
    MAE: 137.096
    >>> print(f'R²: {r2_score(y_test, predictions):.3f}'): 0.954 
    

我们通过一个简单的神经网络模型达到了 R² 为 0.954

微调神经网络

我们能做得更好吗?当然可以,我们还没有微调超参数。我们将在 PyTorch 中通过以下步骤进行模型微调:

  1. TensorBoard 提供了在模型训练和评估过程中记录各种指标和可视化的功能。你可以与 PyTorch 一起使用 TensorBoard 来跟踪和可视化诸如损失、准确率、梯度和模型架构等指标。我们依赖于 PyTorch utils 中的 tensorboard 模块,所以我们首先导入它:

    >>> from torch.utils.tensorboard import SummaryWriter 
    
  2. 我们希望调整隐藏层中隐节点的数量(这里,我们使用的是一个隐藏层),训练迭代次数和学习率。我们选择了以下超参数值来进行实验:

    >>> hparams_config = {
            "hidden_size": [16, 32],
            "epochs": [1000, 3000],
            "lr": [0.1, 0.3],
        } 
    

在这里,我们尝试了两个隐层节点数选项,1632;我们使用了两个迭代次数选项,3001000;还使用了两个学习率选项,0.10.3

  1. 在初始化优化的超参数后,我们将对每个超参数组合进行迭代,并通过调用帮助函数 train_validate_model 来训练和验证模型,具体如下:

    >>> def train_validate_model(hidden_size, epochs, lr):
            model = nn.Sequential(nn.Linear(X_train.shape[1], hidden_size),
                                      nn.ReLU(),
                                      nn.Linear(hidden_size, 1))
            optimizer = torch.optim.Adam(model.parameters(), lr=lr)
            # Create the TensorBoard writer
            writer_path = f"runs/{experiment_num}/{hidden_size}/{epochs}/{lr}"
            writer = SummaryWriter(log_dir=writer_path)
            for epoch in range(epochs):
                loss = train_step(model, X_train_torch, y_train_torch, loss_function, 
                                  optimizer)
                predictions = model(X_test_torch).detach().numpy()[:, 0]
                test_mse = mean_squared_error(y_test, predictions)
                writer.add_scalar(
                    tag="train loss",
                    scalar_value=loss,
                    global_step=epoch,
                )
                writer.add_scalar(
                    tag="test loss",
                    scalar_value=test_mse,
                    global_step=epoch,
                )
            test_r2 = r2_score(y_test, predictions)
            print(f'R²: {test_r2:.3f}\n')
            # Add the hyperparameters and metrics to TensorBoard
            writer.add_hparams(
                {
                    "hidden_size": hidden_size,
                    "epochs": epochs,
                    "lr": lr,
                },
                {
                    "test MSE": test_mse,
                    "test R²": test_r2,
                },
            ) 
    

在每个超参数组合中,我们根据给定的超参数(包括隐藏节点的数量、学习率和训练迭代次数)构建并拟合一个神经网络模型。这里与之前做的没有太大区别。但在训练模型时,我们还会通过 add_scalar 方法更新 TensorBoard,记录超参数和指标,包括训练损失和测试损失。

TensorBoard 编写器对象非常直观。它为训练和验证过程中的模型图和指标提供可视化。

最后,我们计算并显示测试集预测的 R²。我们还使用 add_hparams 方法记录测试集的 MSE 和 R²,并附上给定的超参数组合。

  1. 现在我们通过迭代八种超参数组合来微调神经网络:

    >>> torch.manual_seed(42)
    >>> experiment_num = 0
    >>> for hidden_size in hparams_config["hidden_size"]:
            for epochs in hparams_config["epochs"]:
                for lr in hparams_config["lr"]:
                    experiment_num += 1
                    print(f"Experiment {experiment_num}: hidden_size = {hidden_size}, 
                            epochs = {epochs}, lr = {lr}")
                    train_validate_model(hidden_size, epochs, lr) 
    

你将看到以下输出:

Experiment 1: hidden_size = 16, epochs = 1000, lr = 0.1: 0.771
Experiment 2: hidden_size = 16, epochs = 1000, lr = 0.3: 0.952
Experiment 3: hidden_size = 16, epochs = 3000, lr = 0.1: 0.969
Experiment 4: hidden_size = 16, epochs = 3000, lr = 0.3: 0.977
Experiment 5: hidden_size = 32, epochs = 1000, lr = 0.1: 0.877
Experiment 6: hidden_size = 32, epochs = 1000, lr = 0.3: 0.957
Experiment 7: hidden_size = 32, epochs = 3000, lr = 0.1: 0.970
Experiment 8: hidden_size = 32, epochs = 3000, lr = 0.3: 0.959 

使用超参数组合 (hidden_size=16, epochs=3000, learning_rate=0.3) 的实验 4 表现最佳,我们获得了 R² 为 0.977

最佳实践

神经网络的超参数调优可以显著影响模型性能。以下是一些神经网络超参数调优的最佳实践:

  • 定义搜索空间:确定需要调整的超参数及其范围。常见的超参数包括学习率、批量大小、隐藏层数量、每层神经元数量、激活函数、丢弃率等。

  • 使用交叉验证:这有助于防止过拟合,并提供更稳健的模型性能估计。

  • 监控性能指标:在训练和验证过程中,跟踪相关的指标,如损失、准确率、精确度、召回率、均方误差(MSE)、R² 等。

  • 早停法:在训练过程中监控验证损失,当验证损失持续增加,而训练损失减少时,停止训练。

  • 正则化:使用 L1 和 L2 正则化以及丢弃法等正则化技术,防止过拟合并提高模型的泛化性能。

  • 尝试不同的架构:尝试不同的网络架构,包括层数、每层神经元数量和激活函数。可以尝试深度与浅层网络、宽网络与窄网络。

  • 使用并行化:如果计算资源允许,可以将超参数搜索过程并行化,以加速实验过程。可以使用像 TensorFlow 的 tf.distribute.Strategy 或 PyTorch 的 torch.nn.DataParallel 这样的工具,将训练分布到多个 GPU 或机器上。

  1. 你会注意到,在这些实验开始后,会创建一个新的文件夹 runs。它包含每次实验的训练和验证性能。在 8 次实验完成后,是时候启动 TensorBoard 了。我们使用以下命令:

    tensorboard --host 0.0.0.0 --logdir runs 
    

启动后,你会看到美观的仪表板,访问地址为 http://localhost:6006/。你可以在这里看到预期结果的截图:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_07.png

图 6.7:TensorBoard 截图

火车和测试损失的时间序列提供了有价值的见解。它们让我们能够评估训练进度,并识别过拟合或欠拟合的迹象。过拟合可以通过观察训练损失随时间下降,而测试损失保持停滞或上升来识别。另一方面,欠拟合则表现为训练损失和测试损失值较高,表明模型未能充分拟合训练数据。

  1. 接下来,我们点击 HPARAMS 标签查看超参数日志。你可以看到所有的超参数组合及其对应的指标(MSE 和 R²)以表格形式展示,如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_08.png

图 6.8:TensorBoard 超参数调优的截图

再次可以看到,实验 4 的表现最好。

  1. 最后,我们使用最优模型进行预测:

    >>> hidden_size = 16
    >>> epochs = 3000
    >>> lr = 0.3
    >>> best_model = nn.Sequential(nn.Linear(X_train.shape[1], hidden_size),
                               nn.ReLU(),
                               nn.Linear(hidden_size, 1))
    >>> optimizer = torch.optim.Adam(best_model.parameters(), lr=lr)
    >>> for epoch in range(epochs):
        train_step(best_model, X_train_torch, y_train_torch, loss_function,
                   optimizer
    >>> predictions = best_model(X_test_torch).detach().numpy()[:, 0] 
    
  2. 如下所示,绘制预测值与真实值的对比图:

    >>> import matplotlib.pyplot as plt
    >>> plt.rc('xtick', labelsize=10)
    >>> plt.rc('ytick', labelsize=10)
    >>> plt.plot(data_test.index, y_test, c='k')
    >>> plt.plot(data_test.index, predictions, c='b')
    >>> plt.xticks(range(0, 130, 10), rotation=60)
    >>> plt.xlabel('Date' , fontsize=10)
    >>> plt.ylabel('Close price' , fontsize=10)
    >>> plt.legend(['Truth', 'Neural network'] , fontsize=10)
    >>> plt.show() 
    

请参考以下截图以查看结果:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_06_09.png

图 6.9:股票价格的预测与真实值对比

微调后的神经网络在预测股票价格方面表现良好。

在本节中,我们通过微调超参数进一步改进了神经网络股票预测器。你可以自由地增加更多的隐藏层,或应用丢弃法(dropout)或早停法(early stopping),看看是否能获得更好的结果。

摘要

在这一章中,我们再次进行了股票预测项目的工作,不过这次是使用神经网络。我们从详细解释神经网络开始,包括基本组件(层、激活函数、前向传播和反向传播),并过渡到深度学习(DL)。接着,我们使用 scikit-learn、TensorFlow 和 PyTorch 从头开始进行实现。我们还学习了避免过拟合的方法,如丢弃法和早停法。最后,我们将本章所学应用于解决股票价格预测问题。

在下一章中,我们将探索自然语言处理(NLP)技术和无监督学习。

练习

  1. 如前所述,能否在神经网络股票预测器中使用更多的隐藏层并重新调整模型?你能得到更好的结果吗?

  2. 在第一个练习之后,能否应用丢弃法和/或早停法,看看能否打破当前最佳的 R² 值 0.977

加入我们书籍的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/yuxi

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/QR_Code187846872178698968.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值