Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(二)(1)https://developer.aliyun.com/article/1482409
练习
- 尝试为 MNIST 数据集构建一个分类器,在测试集上实现超过 97%的准确率。提示:
KNeighborsClassifier
对这个任务效果很好;您只需要找到好的超参数值(尝试在weights
和n_neighbors
超参数上进行网格搜索)。 - 编写一个函数,可以将 MNIST 图像向任何方向(左、右、上或下)移动一个像素。然后,对于训练集中的每个图像,创建四个移位副本(每个方向一个)并将它们添加到训练集中。最后,在这个扩展的训练集上训练您最好的模型,并在测试集上测量其准确率。您应该观察到您的模型现在表现得更好了!这种人为扩展训练集的技术称为数据增强或训练集扩展。
- 解决泰坦尼克号数据集。一个很好的开始地方是Kaggle。或者,您可以从https://homl.info/titanic.tgz下载数据并解压缩这个 tarball,就像您在第二章中为房屋数据所做的那样。这将给您两个 CSV 文件,train.csv和test.csv,您可以使用
pandas.read_csv()
加载。目标是训练一个分类器,可以根据其他列预测Survived
列。 - 构建一个垃圾邮件分类器(一个更具挑战性的练习):
- 从Apache SpamAssassin 的公共数据集下载垃圾邮件和正常邮件的示例。
- 解压数据集并熟悉数据格式。
- 将数据分割为训练集和测试集。
- 编写一个数据准备流水线,将每封电子邮件转换为特征向量。您的准备流水线应该将一封电子邮件转换为一个(稀疏)向量,指示每个可能单词的存在或不存在。例如,如果所有电子邮件只包含四个单词,“Hello”、“how”、“are”、“you”,那么电子邮件“Hello you Hello Hello you”将被转换为向量[1, 0, 0, 1](表示[“Hello”存在,“how”不存在,“are”不存在,“you”存在]),或者如果您更喜欢计算每个单词出现的次数,则为[3, 0, 0, 2]。
您可能希望在准备流水线中添加超参数,以控制是否剥离电子邮件头部,将每封电子邮件转换为小写,删除标点符号,用“URL”替换所有 URL,用“NUMBER”替换所有数字,甚至执行词干提取(即修剪单词结尾;有 Python 库可用于执行此操作)。 - 最后,尝试几种分类器,看看是否可以构建一个既具有高召回率又具有高精度的垃圾邮件分类器。
这些练习的解决方案可以在本章笔记本的末尾找到,网址为https://homl.info/colab3。
¹ 默认情况下,Scikit-Learn 会将下载的数据集缓存到名为scikit_learn_data的目录中,该目录位于您的主目录中。
² fetch_openml()
返回的数据集并不总是被洗牌或分割。
³ 在某些情况下,洗牌可能不是一个好主意——例如,如果您正在处理时间序列数据(如股票市场价格或天气状况)。我们将在第十五章中探讨这个问题。
⁴ Scikit-Learn 分类器总是具有decision_function()
方法或predict_proba()
方法,有时两者都有。
⁵ Scikit-Learn 提供了一些其他平均选项和多标签分类器指标;更多细节请参阅文档。
⁶ 您可以使用scipy.ndimage.interpolation
模块中的shift()
函数。例如,shift(image, [2, 1], cval=0)
将图像向下移动两个像素,向右移动一个像素。
第四章:训练模型
到目前为止,我们大多将机器学习模型及其训练算法视为黑匣子。如果您在之前章节的一些练习中有所了解,您可能会对不知道底层原理的情况下能做多少事情感到惊讶:您优化了一个回归系统,改进了一个数字图像分类器,甚至从头开始构建了一个垃圾邮件分类器,所有这些都是在不知道它们实际如何工作的情况下完成的。实际上,在许多情况下,您并不真正需要知道实现细节。
然而,对事物如何运作有一个良好的理解可以帮助您快速找到适当的模型、正确的训练算法以及适合您任务的一组良好的超参数。了解底层原理还将帮助您更有效地调试问题并执行错误分析。最后,本章讨论的大多数主题将对理解、构建和训练神经网络(本书的第二部分中讨论)至关重要。
在本章中,我们将首先看一下线性回归模型,这是最简单的模型之一。我们将讨论两种非常不同的训练方法:
- 使用一个“封闭形式”方程¹直接计算最适合训练集的模型参数(即最小化训练集上成本函数的模型参数)。
- 使用一种称为梯度下降(GD)的迭代优化方法,逐渐调整模型参数以最小化训练集上的成本函数,最终收敛到与第一种方法相同的参数集。我们将看一下几种梯度下降的变体,当我们研究神经网络时会一再使用:批量 GD、小批量 GD 和随机 GD。
接下来我们将看一下多项式回归,这是一个可以拟合非线性数据集的更复杂模型。由于这个模型比线性回归有更多的参数,所以更容易过拟合训练数据。我们将探讨如何通过学习曲线检测是否存在这种情况,然后我们将看一下几种正则化技术,可以减少过拟合训练集的风险。
最后,我们将研究另外两种常用于分类任务的模型:逻辑回归和 softmax 回归。
警告
本章将包含相当多的数学方程,使用线性代数和微积分的基本概念。要理解这些方程,您需要知道向量和矩阵是什么;如何转置、相乘和求逆;以及什么是偏导数。如果您对这些概念不熟悉,请查看在线补充材料中作为 Jupyter 笔记本提供的线性代数和微积分入门教程。对于那些真正对数学过敏的人,您仍然应该阅读本章,并简单跳过方程;希望文本足以帮助您理解大部分概念。
线性回归
在第一章中,我们看了一个关于生活满意度的简单回归模型:
life_satisfaction = θ[0] + θ[1] × GDP_per_capita
该模型只是输入特征GDP_per_capita
的线性函数。θ[0]和θ[1]是模型的参数。
更一般地,线性模型通过简单地计算输入特征的加权和加上一个称为偏置项(也称为截距项)的常数来进行预测,如方程 4-1 所示。
方程 4-1。线性回归模型预测
y ^ = θ 0 + θ 1 x 1 + θ 2 x 2 + ⋯ + θ n x n
在这个方程中:
- ŷ是预测值。
- n是特征数量。
- x[i]是第i个特征值。
- θ[j]是第j个模型参数,包括偏置项θ[0]和特征权重θ[1]、θ[2]、⋯、θ[n]。
这可以用矢量化形式更简洁地表示,如方程 4-2 所示。
方程 4-2. 线性回归模型预测(矢量化形式)
y^=hθ(x)=θ·x
在这个方程中:
- h[θ]是假设函数,使用模型参数θ。
- θ是模型的参数向量,包括偏置项θ[0]和特征权重θ[1]到θ[n]。
- x是实例的特征向量,包含x[0]到x[n],其中x[0]始终等于 1。
- θ · x是向量θ和x的点积,等于θ[0]x[0] + θ[1]x[1] + θ[2]x[2] + … + θ[n]x[n]。
注意
在机器学习中,向量通常表示为列向量,这是具有单列的二维数组。如果θ和x是列向量,那么预测值为y^=θ⊺x,其中θ⊺是θ的转置(行向量而不是列向量),θ⊺x是θ⊺和x的矩阵乘法。当然,这是相同的预测,只是现在表示为单元格矩阵而不是标量值。在本书中,我将使用这种表示法,以避免在点积和矩阵乘法之间切换。
好的,这就是线性回归模型,但我们如何训练它呢?嗯,回想一下,训练模型意味着设置其参数,使模型最好地适应训练集。为此,我们首先需要一个衡量模型与训练数据拟合程度的指标。在第二章中,我们看到回归模型最常见的性能指标是均方根误差(方程 2-1)。因此,要训练线性回归模型,我们需要找到最小化 RMSE 的θ的值。在实践中,最小化均方误差(MSE)比最小化 RMSE 更简单,并且会导致相同的结果(因为最小化正函数的值也会最小化其平方根)。
警告
在训练期间,学习算法通常会优化不同的损失函数,而不是用于评估最终模型的性能指标。这通常是因为该函数更容易优化和/或因为在训练期间仅需要额外的项(例如,用于正则化)。一个好的性能指标应尽可能接近最终的业务目标。一个好的训练损失易于优化,并且与指标强相关。例如,分类器通常使用成本函数进行训练,如对数损失(稍后在本章中将看到),但使用精度/召回率进行评估。对数损失易于最小化,这样做通常会提高精度/召回率。
线性回归假设h[θ]在训练集X上的 MSE 是使用方程 4-3 计算的。
方程 4-3. 线性回归模型的 MSE 成本函数
MSE ( X , h θ ) = 1 m ∑ i=1 m (θ ⊺ x (i) -y (i) ) 2
大多数这些符号在第二章中已经介绍过(参见“符号”)。唯一的区别是我们写h[θ]而不是只写h,以明确模型是由向量θ参数化的。为了简化符号,我们将只写 MSE(θ)而不是 MSE(X, h[θ])。
正规方程
为了找到最小化 MSE 的θ的值,存在一个闭式解——换句话说,一个直接给出结果的数学方程。这被称为正规方程(方程 4-4)。
方程 4-4. 正规方程
θ ^ = (X ⊺ X) -1 X ⊺ y
在这个方程中:
- θ^是最小化成本函数的θ的值。
- y是包含y((1))到*y*((m))的目标值向量。
让我们生成一些看起来线性的数据来测试这个方程(图 4-1):
import numpy as np np.random.seed(42) # to make this code example reproducible m = 100 # number of instances X = 2 * np.random.rand(m, 1) # column vector y = 4 + 3 * X + np.random.randn(m, 1) # column vector
图 4-1. 随机生成的线性数据集
现在让我们使用正规方程计算θ^。我们将使用 NumPy 的线性代数模块(np.linalg
)中的inv()
函数计算矩阵的逆,以及矩阵乘法的dot()
方法:
from sklearn.preprocessing import add_dummy_feature X_b = add_dummy_feature(X) # add x0 = 1 to each instance theta_best = np.linalg.inv(X_b.T @ X_b) @ X_b.T @ y
注意
@
运算符执行矩阵乘法。如果A
和B
是 NumPy 数组,则A @ B
等同于np.matmul(A, B)
。许多其他库,如 TensorFlow、PyTorch 和 JAX,也支持@
运算符。但是,不能在纯 Python 数组(即列表的列表)上使用@
。
我们用来生成数据的函数是y = 4 + 3x[1] + 高斯噪声。让我们看看方程找到了什么:
>>> theta_best array([[4.21509616], [2.77011339]])
我们希望θ[0] = 4 和θ[1] = 3,而不是θ[0] = 4.215 和θ[1] = 2.770。足够接近,但噪声使得无法恢复原始函数的确切参数。数据集越小且噪声越大,问题就越困难。
现在我们可以使用θ^进行预测:
>>> X_new = np.array([[0], [2]]) >>> X_new_b = add_dummy_feature(X_new) # add x0 = 1 to each instance >>> y_predict = X_new_b @ theta_best >>> y_predict array([[4.21509616], [9.75532293]])
让我们绘制这个模型的预测(图 4-2):
import matplotlib.pyplot as plt plt.plot(X_new, y_predict, "r-", label="Predictions") plt.plot(X, y, "b.") [...] # beautify the figure: add labels, axis, grid, and legend plt.show()
图 4-2. 线性回归模型预测
使用 Scikit-Learn 执行线性回归相对简单:
>>> from sklearn.linear_model import LinearRegression >>> lin_reg = LinearRegression() >>> lin_reg.fit(X, y) >>> lin_reg.intercept_, lin_reg.coef_ (array([4.21509616]), array([[2.77011339]])) >>> lin_reg.predict(X_new) array([[4.21509616], [9.75532293]])
请注意,Scikit-Learn 将偏置项(intercept_
)与特征权重(coef_
)分开。LinearRegression
类基于scipy.linalg.lstsq()
函数(名称代表“最小二乘法”),您可以直接调用该函数:
>>> theta_best_svd, residuals, rank, s = np.linalg.lstsq(X_b, y, rcond=1e-6) >>> theta_best_svd array([[4.21509616], [2.77011339]])
这个函数计算θ^=X+y,其中X+是X的伪逆(具体来说,是 Moore-Penrose 逆)。您可以使用np.linalg.pinv()
直接计算伪逆:
>>> np.linalg.pinv(X_b) @ y array([[4.21509616], [2.77011339]])
伪逆本身是使用称为奇异值分解(SVD)的标准矩阵分解技术计算的,可以将训练集矩阵X分解为三个矩阵U Σ V^⊺的矩阵乘法(参见numpy.linalg.svd()
)。伪逆计算为X+=VΣ+U⊺。为了计算矩阵Σ+,算法取Σ并将小于一个微小阈值的所有值设为零,然后用它们的倒数替换所有非零值,最后转置结果矩阵。这种方法比计算正规方程更有效,而且可以很好地处理边缘情况:实际上,如果矩阵X^⊺X不可逆(即奇异),例如如果m<n或者某些特征是冗余的,那么正规方程可能无法工作,但伪逆总是被定义的。
计算复杂度
正规方程计算X⊺**X**的逆,这是一个(*n*+1)×(*n*+1)矩阵(其中*n*是特征数)。求解这样一个矩阵的*计算复杂度*通常约为*O*(*n*(2.4))到O(n³),取决于实现。换句话说,如果特征数翻倍,计算时间大约会乘以 2^(2.4)=5.3 到 2³=8。
Scikit-Learn 的LinearRegression
类使用的 SVD 方法大约是O(n²)。如果特征数量翻倍,计算时间大约会乘以 4。
警告
当特征数量增多时(例如 100,000),正规方程和 SVD 方法都变得非常慢。积极的一面是,它们都与训练集中实例数量线性相关(它们是O(m)),因此它们可以有效地处理大型训练集,只要它们可以放入内存。
此外,一旦训练好线性回归模型(使用正规方程或任何其他算法),预测速度非常快:计算复杂度与您要进行预测的实例数量和特征数量成正比。换句话说,对两倍实例(或两倍特征)进行预测将花费大约两倍的时间。
现在我们将看一种非常不同的训练线性回归模型的方法,这种方法更适用于特征数量较多或训练实例太多无法放入内存的情况。
梯度下降
梯度下降是一种通用的优化算法,能够找到各种问题的最优解。梯度下降的一般思想是迭代地调整参数,以最小化成本函数。
假设你在浓雾中的山中迷失了方向,只能感受到脚下的坡度。快速到达山谷底部的一个好策略是沿着最陡的坡度方向下坡。这正是梯度下降所做的:它测量了关于参数向量θ的误差函数的局部梯度,并沿着下降梯度的方向前进。一旦梯度为零,你就到达了一个最小值!
在实践中,您首先用随机值填充θ(这称为随机初始化)。然后逐渐改进它,每次尝试减少成本函数(例如 MSE)一点点,直到算法收敛到最小值(参见图 4-3)。
图 4-3。在这个梯度下降的描述中,模型参数被随机初始化,并不断调整以最小化成本函数;学习步长大小与成本函数的斜率成比例,因此随着成本接近最小值,步长逐渐变小
梯度下降中的一个重要参数是步长的大小,由学习率超参数确定。如果学习率太小,那么算法将需要经过许多迭代才能收敛,这将花费很长时间(参见图 4-4)。
图 4-4。学习率太小
另一方面,如果学习率太高,您可能会跳过山谷,最终停在另一侧,甚至可能比之前更高。这可能导致算法发散,产生越来越大的值,无法找到一个好的解决方案(参见图 4-5)。
图 4-5。学习率太高
此外,并非所有成本函数都像漂亮的、规则的碗一样。可能会有洞、脊、高原和各种不规则的地形,使得收敛到最小值变得困难。图 4-6 展示了梯度下降的两个主要挑战。如果随机初始化将算法开始于左侧,则它将收敛到局部最小值,这不如全局最小值好。如果它从右侧开始,则穿过高原将需要很长时间。如果您停得太早,您将永远无法达到全局最小值。
图 4-6。梯度下降的陷阱
幸运的是,线性回归模型的 MSE 成本函数恰好是一个凸函数,这意味着如果您选择曲线上的任意两点,连接它们的线段永远不会低于曲线。这意味着没有局部最小值,只有一个全局最小值。它还是一个连续函数,斜率永远不会突然改变。这两个事实有一个重要的结果:梯度下降保证可以无限接近全局最小值(如果等待足够长的时间且学习率不太高)。
虽然成本函数的形状像一个碗,但如果特征具有非常不同的比例,它可能是一个延长的碗。图 4-7 展示了在特征 1 和 2 具有相同比例的训练集上的梯度下降(左侧),以及在特征 1 的值远小于特征 2 的训练集上的梯度下降(右侧)。
图 4-7。特征缩放的梯度下降(左)和不缩放的梯度下降(右)
正如您所看到的,左侧的梯度下降算法直接朝向最小值,因此快速到达,而右侧首先朝向几乎与全局最小值方向正交的方向,最终沿着几乎平坦的山谷长途跋涉。它最终会到达最小值,但需要很长时间。
警告
在使用梯度下降时,您应确保所有特征具有相似的比例(例如,使用 Scikit-Learn 的StandardScaler
类),否则收敛所需的时间将更长。
这个图表还说明了训练模型意味着寻找一组模型参数的组合,使得成本函数(在训练集上)最小化。这是在模型的参数空间中进行的搜索。模型的参数越多,空间的维度就越多,搜索就越困难:在一个 300 维的草堆中搜索一根针比在 3 维空间中要困难得多。幸运的是,由于线性回归的情况下成本函数是凸的,所以这根针就在碗底。
批量梯度下降
要实现梯度下降,您需要计算成本函数相对于每个模型参数θ[j]的梯度。换句话说,您需要计算如果您稍微改变θ[j],成本函数将如何变化。这被称为偏导数。这就像问,“如果我面向东,脚下的山坡有多陡?”然后面向北问同样的问题(如果您可以想象一个超过三维的宇宙,那么其他维度也是如此)。方程 4-5 计算了关于参数θ[j]的 MSE 的偏导数,表示为∂ MSE(θ) / ∂θ[j]。
方程 4-5. 成本函数的偏导数
∂ ∂θ j MSE ( θ ) = 2 m ∑ i=1 m ( θ ⊺ x (i) - y (i) ) x j (i)
与单独计算这些偏导数不同,您可以使用方程 4-6 一次性计算它们。梯度向量,表示为∇[θ]MSE(θ),包含成本函数的所有偏导数(每个模型参数一个)。
方程 4-6. 成本函数的梯度向量
∇ θ MSE ( θ ) = ∂ ∂θ 0 MSE ( θ ) ∂ ∂θ 1 MSE ( θ ) ⋮ ∂ ∂θ n MSE ( θ ) = 2 m X ⊺ ( X θ - y )
警告
请注意,这个公式涉及对整个训练集X进行计算,每次梯度下降步骤都要进行!这就是为什么该算法被称为批量梯度下降:它在每一步使用整个批量的训练数据(实际上,全梯度下降可能是一个更好的名称)。因此,在非常大的训练集上,它非常慢(我们很快将看到一些更快的梯度下降算法)。然而,梯度下降随着特征数量的增加而扩展得很好;当特征数量达到数十万时,使用梯度下降训练线性回归模型比使用正规方程或 SVD 分解要快得多。
一旦有了指向上坡的梯度向量,只需朝相反方向前进以下坡。这意味着从θ中减去∇[θ]MSE(θ)。这就是学习率η发挥作用的地方:⁴将梯度向量乘以η来确定下坡步长的大小(方程 4-7)。
方程 4-7. 梯度下降步骤
θ(下一步)=θ-η∇θMSE(θ)
让我们快速实现这个算法:
eta = 0.1 # learning rate n_epochs = 1000 m = len(X_b) # number of instances np.random.seed(42) theta = np.random.randn(2, 1) # randomly initialized model parameters for epoch in range(n_epochs): gradients = 2 / m * X_b.T @ (X_b @ theta - y) theta = theta - eta * gradients
这并不难!每次对训练集的迭代称为epoch。让我们看看得到的theta
:
>>> theta array([[4.21509616], [2.77011339]])
嘿,这正是正规方程找到的!梯度下降完美地工作了。但是如果您使用了不同的学习率(eta
)会怎样呢?图 4-8 显示了使用三种不同学习率的梯度下降的前 20 步。每个图中底部的线代表随机起始点,然后每个迭代由越来越深的线表示。
图 4-8. 不同学习率的梯度下降
在左侧,学习率太低:算法最终会达到解,但需要很长时间。在中间,学习率看起来相当不错:在几个迭代中,它已经收敛到解。在右侧,学习率太高:算法发散,跳来跳去,实际上每一步都离解越来越远。
要找到一个好的学习率,可以使用网格搜索(参见第二章)。然而,您可能希望限制迭代次数,以便网格搜索可以消除收敛时间过长的模型。
您可能想知道如何设置迭代次数。如果太低,当算法停止时,您仍然离最优解很远;但如果太高,您将浪费时间,因为模型参数不再改变。一个简单的解决方案是设置一个非常大的迭代次数,但在梯度向量变得微小时中断算法——也就是说,当其范数小于一个微小数ϵ(称为容差)时——因为这表示梯度下降已经(几乎)达到了最小值。
随机梯度下降
批量梯度下降的主要问题在于,它在每一步使用整个训练集来计算梯度,这使得在训练集很大时非常缓慢。相反,随机梯度下降 在每一步选择训练集中的一个随机实例,并仅基于该单个实例计算梯度。显然,一次只处理一个实例使得算法更快,因为每次迭代时需要操作的数据量很少。这也使得在庞大的训练集上进行训练成为可能,因为每次迭代只需要一个实例在内存中(随机梯度下降可以作为一种离线算法实现;参见第一章)。
另一方面,由于其随机(即随机)性质,这种算法比批量梯度下降不规则得多:成本函数不会温和地减少直到达到最小值,而是会上下波动,仅平均减少。随着时间的推移,它最终会非常接近最小值,但一旦到达那里,它将继续上下波动,永远不会稳定下来(参见图 4-9)。一旦算法停止,最终的参数值将是不错的,但不是最优的。
图 4-9。使用随机梯度下降,每个训练步骤比使用批量梯度下降快得多,但也更不规则。
当成本函数非常不规则时(如图 4-6 中所示),这实际上可以帮助算法跳出局部最小值,因此随机梯度下降比批量梯度下降更有可能找到全局最小值。
因此,随机性有助于摆脱局部最优解,但也不好,因为这意味着算法永远无法稳定在最小值处。解决这一困境的一个方法是逐渐降低学习率。步骤开始很大(有助于快速取得进展并摆脱局部最小值),然后变得越来越小,允许算法在全局最小值处稳定下来。这个过程类似于模拟退火,这是一种受金属冶炼过程启发的算法,其中熔化的金属被慢慢冷却。确定每次迭代学习率的函数称为学习计划。如果学习率降低得太快,您可能会陷入局部最小值,甚至最终冻结在最小值的一半。如果学习率降低得太慢,您可能会在最小值周围跳来跳去很长时间,并且如果您在训练过早停止,最终会得到一个次优解。
此代码使用简单的学习计划实现随机梯度下降:
n_epochs = 50 t0, t1 = 5, 50 # learning schedule hyperparameters def learning_schedule(t): return t0 / (t + t1) np.random.seed(42) theta = np.random.randn(2, 1) # random initialization for epoch in range(n_epochs): for iteration in range(m): random_index = np.random.randint(m) xi = X_b[random_index : random_index + 1] yi = y[random_index : random_index + 1] gradients = 2 * xi.T @ (xi @ theta - yi) # for SGD, do not divide by m eta = learning_schedule(epoch * m + iteration) theta = theta - eta * gradients
按照惯例,我们按照m次迭代的轮次进行迭代;每一轮称为epoch,如前所述。虽然批量梯度下降代码通过整个训练集迭代了 1,000 次,但这段代码只通过训练集迭代了 50 次,并达到了一个相当不错的解决方案:
>>> theta array([[4.21076011], [2.74856079]])
图 4-10 显示了训练的前 20 步(请注意步骤的不规则性)。
请注意,由于实例是随机选择的,一些实例可能在每个 epoch 中被多次选择,而其他实例可能根本不被选择。如果您想确保算法在每个 epoch 中通过每个实例,另一种方法是对训练集进行洗牌(确保同时洗牌输入特征和标签),然后逐个实例地进行,然后再次洗牌,依此类推。然而,这种方法更复杂,通常不会改善结果。
图 4-10。随机梯度下降的前 20 步
警告
在使用随机梯度下降时,训练实例必须是独立同分布的(IID),以确保参数平均被拉向全局最优解。确保这一点的一个简单方法是在训练期间对实例进行洗牌(例如,随机选择每个实例,或在每个 epoch 开始时对训练集进行洗牌)。如果不对实例进行洗牌,例如,如果实例按标签排序,则 SGD 将从优化一个标签开始,然后是下一个标签,依此类推,并且不会接近全局最小值。
要使用 Scikit-Learn 进行随机梯度下降线性回归,您可以使用SGDRegressor
类,默认情况下优化 MSE 成本函数。以下代码最多运行 1,000 个时代(max_iter
)或在 100 个时代内损失下降不到 10^(–5)(tol
)时停止(n_iter_no_change
)。它以学习率 0.01(eta0
)开始,使用默认学习计划(与我们使用的不同)。最后,它不使用任何正则化(penalty=None
;稍后会详细介绍):
from sklearn.linear_model import SGDRegressor sgd_reg = SGDRegressor(max_iter=1000, tol=1e-5, penalty=None, eta0=0.01, n_iter_no_change=100, random_state=42) sgd_reg.fit(X, y.ravel()) # y.ravel() because fit() expects 1D targets
再次,您会发现解决方案与正规方程返回的解非常接近:
>>> sgd_reg.intercept_, sgd_reg.coef_ (array([4.21278812]), array([2.77270267]))
提示
所有 Scikit-Learn 估计器都可以使用fit()
方法进行训练,但有些估计器还有一个partial_fit()
方法,您可以调用它来对一个或多个实例运行一轮训练(它会忽略max_iter
或tol
等超参数)。反复调用partial_fit()
会逐渐训练模型。当您需要更多控制训练过程时,这是很有用的。其他模型则有一个warm_start
超参数(有些模型两者都有):如果您设置warm_start=True
,在已训练的模型上调用fit()
方法不会重置模型;它将继续训练在哪里停止,遵守max_iter
和tol
等超参数。请注意,fit()
会重置学习计划使用的迭代计数器,而partial_fit()
不会。
小批量梯度下降
我们将要看的最后一个梯度下降算法称为小批量梯度下降。一旦您了解了批量梯度下降和随机梯度下降,这就很简单了:在每一步中,小批量梯度下降不是基于完整训练集(批量梯度下降)或仅基于一个实例(随机梯度下降)计算梯度,而是在称为小批量的小随机实例集上计算梯度。小批量梯度下降相对于随机梯度下降的主要优势在于,您可以通过硬件优化矩阵运算获得性能提升,尤其是在使用 GPU 时。
该算法在参数空间中的进展比随机梯度下降更加稳定,尤其是在使用相当大的小批量时。因此,小批量梯度下降最终会比随机梯度下降更接近最小值,但它可能更难逃离局部最小值(在存在局部最小值的问题中,不同于具有 MSE 成本函数的线性回归)。图 4-11 显示了训练过程中三种梯度下降算法在参数空间中的路径。它们最终都接近最小值,但批量梯度下降的路径实际上停在最小值处,而随机梯度下降和小批量梯度下降则继续移动。但是,请不要忘记,批量梯度下降需要很长时间才能完成每一步,如果您使用良好的学习计划,随机梯度下降和小批量梯度下降也会达到最小值。
图 4-11. 参数空间中的梯度下降路径
表 4-1 比较了迄今为止我们讨论过的线性回归算法(请回忆m是训练实例的数量,n是特征的数量)。
表 4-1. 线性回归算法比较
算法 | 大 m | 支持离线 | 大 n | 超参数 | 需要缩放 | Scikit-Learn |
正规方程 | 快 | 否 | 慢 | 0 | 否 | N/A |
SVD | 快 | 否 | 慢 | 0 | 否 | LinearRegression |
批量梯度下降 | 慢 | 否 | 快 | 2 | 是 | N/A |
随机梯度下降 | 快 | 是 | 快 | ≥2 | 是 | SGDRegressor |
小批量梯度下降 | 快 | 是 | 快 | ≥2 | 是 | N/A |
训练后几乎没有区别:所有这些算法最终得到非常相似的模型,并以完全相同的方式进行预测。
多项式回归
如果你的数据比一条直线更复杂怎么办?令人惊讶的是,你可以使用线性模型来拟合非线性数据。一个简单的方法是将每个特征的幂作为新特征添加,然后在这个扩展的特征集上训练线性模型。这种技术称为多项式回归。
让我们看一个例子。首先,我们将生成一些非线性数据(参见图 4-12),基于一个简单的二次方程——即形式为y = ax² + bx + c的方程——再加上一些噪声:
np.random.seed(42) m = 100 X = 6 * np.random.rand(m, 1) - 3 y = 0.5 * X ** 2 + X + 2 + np.random.randn(m, 1)
图 4-12。生成的非线性和嘈杂数据集
显然,一条直线永远无法正确拟合这些数据。因此,让我们使用 Scikit-Learn 的PolynomialFeatures
类来转换我们的训练数据,将训练集中每个特征的平方(二次多项式)作为新特征添加到训练数据中(在这种情况下只有一个特征):
>>> from sklearn.preprocessing import PolynomialFeatures >>> poly_features = PolynomialFeatures(degree=2, include_bias=False) >>> X_poly = poly_features.fit_transform(X) >>> X[0] array([-0.75275929]) >>> X_poly[0] array([-0.75275929, 0.56664654])
X_poly
现在包含了X
的原始特征以及该特征的平方。现在我们可以将LinearRegression
模型拟合到这个扩展的训练数据上(图 4-13):
>>> lin_reg = LinearRegression() >>> lin_reg.fit(X_poly, y) >>> lin_reg.intercept_, lin_reg.coef_ (array([1.78134581]), array([[0.93366893, 0.56456263]]))
图 4-13。多项式回归模型预测
不错:模型估计y ^ = 0.56 x 1 2 + 0.93 x 1 + 1.78,而实际上原始函数是y = 0.5 x 1 2 + 1.0 x 1 + 2.0 + 高斯噪声。
请注意,当存在多个特征时,多项式回归能够找到特征之间的关系,这是普通线性回归模型无法做到的。这是因为PolynomialFeatures
还会添加给定次数的所有特征组合。例如,如果有两个特征a和b,PolynomialFeatures
的degree=3
不仅会添加特征a²、a³、b²和b³,还会添加组合ab、a²b和ab²。
警告
PolynomialFeatures(degree=*d*)
将包含n个特征的数组转换为包含(n + d)! / d!n!个特征的数组,其中n!是n的阶乘,等于 1 × 2 × 3 × ⋯ × n。注意特征数量的组合爆炸!
学习曲线
如果进行高次多项式回归,你很可能会比普通线性回归更好地拟合训练数据。例如,图 4-14 将一个 300 次多项式模型应用于前面的训练数据,并将结果与纯线性模型和二次模型(二次多项式)进行比较。请注意,300 次多项式模型在训练实例周围摆动以尽可能接近训练实例。
图 4-14。高次多项式回归
这个高次多项式回归模型严重过拟合了训练数据,而线性模型则欠拟合了。在这种情况下,最能泛化的模型是二次模型,这是有道理的,因为数据是使用二次模型生成的。但通常你不会知道是什么函数生成了数据,那么你如何决定模型应该有多复杂呢?你如何判断你的模型是过拟合还是欠拟合了数据?
在第二章中,您使用交叉验证来估计模型的泛化性能。如果模型在训练数据上表现良好,但根据交叉验证指标泛化能力差,那么您的模型是过拟合的。如果两者表现都不好,那么它是拟合不足的。这是判断模型过于简单或过于复杂的一种方法。
另一种方法是查看学习曲线,这是模型的训练误差和验证误差作为训练迭代的函数的图表:只需在训练集和验证集上定期评估模型,并绘制结果。如果模型无法进行增量训练(即,如果它不支持partial_fit()
或warm_start
),那么您必须在逐渐扩大的训练集子集上多次训练它。
Scikit-Learn 有一个有用的learning_curve()
函数来帮助解决这个问题:它使用交叉验证来训练和评估模型。默认情况下,它会在不断增长的训练集子集上重新训练模型,但如果模型支持增量学习,您可以在调用learning_curve()
时设置exploit_incremental_learning=True
,它将逐步训练模型。该函数返回评估模型的训练集大小,以及每个大小和每个交叉验证折叠的训练和验证分数。让我们使用这个函数来查看普通线性回归模型的学习曲线(参见图 4-15):
from sklearn.model_selection import learning_curve train_sizes, train_scores, valid_scores = learning_curve( LinearRegression(), X, y, train_sizes=np.linspace(0.01, 1.0, 40), cv=5, scoring="neg_root_mean_squared_error") train_errors = -train_scores.mean(axis=1) valid_errors = -valid_scores.mean(axis=1) plt.plot(train_sizes, train_errors, "r-+", linewidth=2, label="train") plt.plot(train_sizes, valid_errors, "b-", linewidth=3, label="valid") [...] # beautify the figure: add labels, axis, grid, and legend plt.show()
图 4-15. 学习曲线
这个模型拟合不足。为了了解原因,首先让我们看看训练误差。当训练集中只有一个或两个实例时,模型可以完美拟合它们,这就是曲线从零开始的原因。但随着新实例被添加到训练集中,模型无法完美拟合训练数据,因为数据存在噪声,而且根本不是线性的。因此,训练数据的误差会上升,直到达到一个平台,在这一点上,向训练集添加新实例不会使平均误差变得更好或更糟。现在让我们看看验证误差。当模型在非常少的训练实例上训练时,它无法正确泛化,这就是为什么验证误差最初相当大的原因。然后,随着模型展示更多的训练示例,它学习,因此验证误差慢慢下降。然而,再次,一条直线无法很好地对数据建模,因此误差最终会达到一个接近另一条曲线的平台。
这些学习曲线是典型的拟合不足模型。两条曲线都达到了一个平台;它们接近且相当高。
提示
如果您的模型对训练数据拟合不足,增加更多的训练样本将无济于事。您需要使用更好的模型或提出更好的特征。
现在让我们看看相同数据上 10 次多项式模型的学习曲线(参见图 4-16):
from sklearn.pipeline import make_pipeline polynomial_regression = make_pipeline( PolynomialFeatures(degree=10, include_bias=False), LinearRegression()) train_sizes, train_scores, valid_scores = learning_curve( polynomial_regression, X, y, train_sizes=np.linspace(0.01, 1.0, 40), cv=5, scoring="neg_root_mean_squared_error") [...] # same as earlier
图 4-16. 10 次多项式模型的学习曲线
这些学习曲线看起来有点像之前的曲线,但有两个非常重要的区别:
- 训练数据上的误差比以前低得多。
- 曲线之间存在差距。这意味着模型在训练数据上的表现明显优于验证数据,这是过拟合模型的标志。然而,如果您使用更大的训练集,这两条曲线将继续接近。
提示
改进过拟合模型的一种方法是提供更多的训练数据,直到验证误差达到训练误差。
正则化线性模型
正如您在第一章和第二章中看到的,减少过拟合的一个好方法是对模型进行正则化(即,约束它):它的自由度越少,过拟合数据的难度就越大。对多项式模型进行正则化的一种简单方法是减少多项式次数。
对于线性模型,通常通过约束模型的权重来实现正则化。我们现在将看一下岭回归、套索回归和弹性网络回归,它们实现了三种不同的约束权重的方式。
岭回归
岭回归(也称为Tikhonov 正则化)是线性回归的正则化版本:一个等于αm∑i=1nθi2的正则化项被添加到 MSE 中。这迫使学习算法不仅拟合数据,还要尽量保持模型权重尽可能小。请注意,正则化项应该只在训练期间添加到成本函数中。一旦模型训练完成,您希望使用未经正则化的 MSE(或 RMSE)来评估模型的性能。
超参数α控制着您希望对模型进行多少正则化。如果α=0,则岭回归就是线性回归。如果α非常大,则所有权重最终都非常接近零,结果是一条通过数据均值的平坦线。方程 4-8 呈现了岭回归成本函数。⁷
方程 4-8。岭回归成本函数
J(θ)=MSE(θ)+αm∑i=1nθi2
请注意,偏置项θ[0]不被正则化(总和从i=1 开始,而不是 0)。如果我们将w定义为特征权重的向量(θ[1]到θ[n]),则正则化项等于α(∥ w ∥[2])² / m,其中∥ w ∥[2]表示权重向量的ℓ[2]范数。⁸ 对于批量梯度下降,只需将 2αw / m添加到对应于特征权重的 MSE 梯度向量的部分,而不要将任何内容添加到偏置项的梯度(参见方程 4-6)。
警告
在执行岭回归之前,重要的是对数据进行缩放(例如,使用StandardScaler
),因为它对输入特征的规模敏感。这对大多数正则化模型都是正确的。
图 4-17 显示了在一些非常嘈杂的线性数据上使用不同α值训练的几个岭模型。在左侧,使用普通的岭模型,导致线性预测。在右侧,首先使用PolynomialFeatures(degree=10)
扩展数据,然后使用StandardScaler
进行缩放,最后将岭模型应用于生成的特征:这是带有岭正则化的多项式回归。请注意,增加α会导致更平缓(即,更不极端,更合理)的预测,从而减少模型的方差但增加其偏差。
图 4-17。线性(左)和多项式(右)模型,都具有不同级别的岭正则化
与线性回归一样,我们可以通过计算闭式方程或执行梯度下降来执行岭回归。优缺点是相同的。方程 4-9 显示了闭式解,其中A是(n + 1) × (n + 1) 单位矩阵,⁹除了左上角的单元格为 0,对应于偏置项。
方程 4-9. 岭回归闭式解
θ ^ = (X ⊺ X+αA) -1 X ⊺ y
以下是如何使用 Scikit-Learn 执行岭回归的闭式解(一种方程 4-9 的变体,使用 André-Louis Cholesky 的矩阵分解技术):
>>> from sklearn.linear_model import Ridge >>> ridge_reg = Ridge(alpha=0.1, solver="cholesky") >>> ridge_reg.fit(X, y) >>> ridge_reg.predict([[1.5]]) array([[1.55325833]])
使用随机梯度下降:¹⁰
>>> sgd_reg = SGDRegressor(penalty="l2", alpha=0.1 / m, tol=None, ... max_iter=1000, eta0=0.01, random_state=42) ... >>> sgd_reg.fit(X, y.ravel()) # y.ravel() because fit() expects 1D targets >>> sgd_reg.predict([[1.5]]) array([1.55302613])
penalty
超参数设置要使用的正则化项的类型。指定"l2"
表示您希望 SGD 将正则化项添加到 MSE 成本函数中,等于alpha
乘以权重向量的ℓ[2]范数的平方。这就像岭回归一样,只是在这种情况下没有除以m;这就是为什么我们传递alpha=0.1 / m
,以获得与Ridge(alpha=0.1)
相同的结果。
提示
RidgeCV
类也执行岭回归,但它会自动使用交叉验证调整超参数。它大致相当于使用GridSearchCV
,但它针对岭回归进行了优化,并且运行快得多。其他几个估计器(主要是线性的)也有高效的 CV 变体,如LassoCV
和ElasticNetCV
。
Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(二)(3)https://developer.aliyun.com/article/1482412#slide-79