第四章 训练模型
· Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow, 2nd Edition, by Aurélien Géron (O’Reilly). Copyright 2019 Aurélien Géron, 978-1-492-03264-9. · 环境:Anaconda(Python 3.8) Pycharm · 学习时间:2022.04.16
到目前为止,我们已经讨论了不同的机器学习模型,但他们自己的训练算法在很大程度上仍然是一个黑匣子。回顾前几章的一些案例,你可能会感到非常惊讶。当你对系统内部一无所知时,你已经实现了这么多:优化回归系统,改进数字图片分类器,从零开始构建垃圾邮件分类器。你不知道他们实际上是如何工作的。的确,在很多情况下,你不需要知道实施细节。
然而,很好地理解系统是如何工作的也很有帮助。对于您的任务,它有助于快速定位正确的模型、正确的训练算法和适当的超参数。不仅如此,以后还可以更有效地进行错误调试和错误分析。最后,本章讨论的大部分主题对理解、构建和训练神经网络(本书的第二部分)至关重要。
本章将从最简单的模型之一线性回归模型,介绍两种非常不同的训练模型:
- 最拟合训练集的模型参数(即最小化训练集成本函数的模型参数)通过闭式方程直接计算;
- 采用迭代优化的方法,即梯度下降(GD),逐步调整模型参数,直到培训集中的成本函数最小化,最终趋同于第一种方法计算的模型参数。我们还研究了几个梯度下降的变体,包括批量梯度下降、小批量梯度下降和随机梯度下降。当我们进入神经网络学习的第二部分时,我们经常使用这些变体。
然后我们将进入多项式回归讨论,这是一个更复杂的模型,更适合非线性数据集。由于该模型的参数大于线性模型,因此更容易导致过拟合训练数据,我们将使用学习曲线来确定这种情况是否发生。然后,介绍一些正则技能,以降低过拟合训练数据的风险。
最后,我们将学习两种常用于分类任务的模型:Logistic回归和Softmax回归。
本章将出现许多需要线性代数和微积分的数学公式。要理解这些方程,你需要知道什么是向量和矩阵,如何转移向量和矩阵,什么是点积,逆矩阵,偏导数。如果您不熟悉这些概念,请先在线补充材料Jupyter notebook,入门学习线性代数和微积分。对于极度讨厌数学的读者来说,他们仍然需要学习这一章,但他们可以跳过这些数学公式。我希望文本足以让你理解大多数概念。
文章目录
- 第四章 训练模型
-
- 4.1 线性回归
-
- 4.1.1 标准方程
- 4.1.2 计算复杂度
- 4.2 梯度下降及其算法
-
- 4.2.1 批量梯度下降(Batch Gradient Descent,BGD)
- 4.2.2 随机梯度下降(Stochastic Gradient Descent,SGD)
- 4.2.3 小批量梯度下降(Mini-Batch Gradient Descent)
- 4.3 多项式回归
- 4.4 学习曲线
-
- 权衡偏差/方差
- 4.5 线性模型的正则化
-
- 4.5.1 岭回归
- 4.5.2 Lasso回归
- 4.5.3 弹性网络
- 4.5.4 提前停止
- 4.6 逻辑回归
-
- 4.6.1 估计概率
- 4.6.2 培训和成本函数
- 4.6.3 决策边界(鸢尾数据集)
- 4.6.4 Softmax回归
- 4.7 练习题
-
- 问题
- 答案
4.1 线性回归
线性模型是对输入特性的加权求和,再加上一个常数,我们称之为偏置项(也称为截距项)进行预测。 y = θ 0 θ 1 x 1 θ 2 x 2 … … θ n x n y = θ_0 θ_1x_1 θ_2x_2 …… θ_nx_n y=θ0 θ1x1+θ2x2+……+θnxn 线性回归模型预测(向量化形式): y = h θ ( x ) = θ ⋅ x y = h_θ(x) = θ·x y=hθ(x)=θ⋅x。
在机器学习中,向量通常表示为列向量,是有单一列的二维数组。如果 θ θ θ和 x x x为列向量,则预测为$y = θ^Tx , 其 中 ,其中 ,其中θT$为$θ$(行向量而不是列向量)的转置,且$θTx 为 θ 为θ 为θT 和 和 和x$的矩阵乘积。
这就是线性回归模型,我们该怎样训练线性回归模型呢?回想一下,训练模型就是设置模型参数直到模型最拟合训练集的过程。为此,我们首先需要知道怎么测量模型对训练数据的拟合程度是好还是差。在第2章中,我们了解到回归模型最常见的性能指标是均方根误差(RMSE)。因此,在训练线性回归模型时,你需要找到最小化RMSE的 θ θ θ值。在实践中,将均方误差(MSE)最小化比最小化RMSE更为简单,二者效果相同(因为使函数最小化的值,同样也使其平方根最小)。
线性回归模型的MSE成本函数: M S E = ( X , h 0 ) = 1 m ∑ i = 1 m ( θ T x ( i ) − y ( i ) ) 2 MSE = (X, h_0) = \frac{1}{m}\sum^m_{i=1}(θ^Tx^{(i)}-y^{(i)})^2 MSE=(X,h0)=m1∑i=1m(θTx(i)−y(i))2。
4.1.1 标准方程
为了得到使成本函数最小的θ值,有一个闭式解方法——也就是一个直接得出结果的数学方程,即标准方程。
θ ′ = ( X T X ) − 1 X T y θ' = (X^TX)^{-1}X^Ty θ′=(XTX)−1XTy,我们生成一些线性数据来测试这个公式:
import numpy as np
import matplotlib.pyplot as plt
# 随机生成数据
X = 2 * np.random.rand(100, 1)
y = 4 + 3 * X + np.random.randn(100, 1)
X_b = np.c_[np.ones((100, 1)), X] # add x0 = 1 to each instance
theta_best = np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y) # dot()方法计算矩阵内积
print(theta_best)
# 输出:期待的是θ0=4,θ1=3得到的是θ0=3.6,θ1=3.2。非常接近,噪声的存在使其不可能完全还原为原本的函数
# 根据参数做出预测
X_new = np.array([[0], [2]])
X_new_b = np.c_[np.ones((2, 1)), X_new] # add x0 = 1 to each instance
y_predict = X_new_b.dot(theta_best)
print(y_predict)
# 绘制模型的预测结果
plt.plot(X_new, y_predict, "r-")
plt.plot(X, y, "b.")
plt.axis([0, 2, 0, 15])
plt.show()
使用Scikit-Learn执行线性回归很简单:
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression() # 实例化线性模型
lin_reg.fit(X, y) # 训练模型
print(lin_reg.intercept_, lin_reg.coef_) # 输出模型训练后的参数
print(lin_reg.predict(X_new)) # 进行预测
# LinearRegression类基于scipy.linalg.lstsq()函数(名称代表“最小二乘”),你可以直接调用它:
theta_best_svd, residuals, rank, s = np.linalg.lstsq(X_b, y, rcond=1e-6)
print(theta_best_svd) # 输出最优的参数
# LinearRegression()模型的参数和scipy.linalg.lstsq()函数的参数输出是一致的
scipy.linalg.lstsq()
函数计算 θ ′ = X + y θ' = X^+y θ′=X+y ,其中 X + X^+ X+是 X X X的伪逆。伪逆本身是使用被称为**奇异值分解(Singular Value Decomposition,SVD)**的标准矩阵分解技术来计算的,可以将训练集矩阵 X X X分解为三个矩阵 U Σ V T UΣV^T UΣVT的乘积。为了计算矩阵 Σ + Σ^+ Σ+,该算法取 Σ Σ Σ并将所有小于一个小阈值的值设置为零,然后将所有非零值替换成它们的倒数,最后把结果矩阵转置。再加上它可以很好地处理边缘情况这种,方法比计算标准方程更有效。
4.1.2 计算复杂度
标准方程计算XT X的逆,XT X是一个(n+1)×(n+1)的矩阵(n是特征数量)。对这种矩阵求逆的计算复杂度通常为O(n2.4)到O(n3)之间,取决于具体实现。换句话说,如果将特征数量翻倍,那么计算时间将乘以大约22.4=5.3倍到23=8倍之间。
Scikit-Learn的LinearRegression类使用的SVD方法的复杂度约为O(n2)。如果你将特征数量加倍,那计算时间大约是原来的4倍。
特征数量比较大(例如100 000)时,标准方程和SVD的计算将极其缓慢。好的一面是,相对于训练集中的实例数量(O(m))来说,两个都是线性的,所以能够有效地处理大量的训练集,只要内存足够。
同样,线性回归模型一经训练(不论是标准方程还是其他算法),预测就非常快速:因为计算复杂度相对于想要预测的实例数量和特征数量来说都是线性的。换句话说,对两倍的实例(或者是两倍的特征数)进行预测,大概需要两倍的时间。
现在,我们再看几个截然不同的线性回归模型的训练方法,这些方法更适合特征数或者训练实例数量大到内存无法满足要求的场景。
4.2 梯度下降及其算法
梯度下降是一种非常通用的,能够为大范围的问题找到最优解。梯度下降的中心思想就是迭代地调整参数从而使成本函数最小化。
**假设你迷失在山上的浓雾之中,你能感觉到的只有你脚下路面的坡度。快速到达山脚的一个策略就是沿着最陡的方向下坡。**这就是梯度下降的做法:!
具体来说,首先使用一个随机的 θ θ θ值(这被称为随机初始化),然后逐步改进,每次踏出一步,每一步都尝试降低一点成本函数(如MSE),直到算法收敛出一个最小值(参见图4-3)。
梯度下降中一个重要参数是每一步的步长,这取决于超参数学习率。如果学习率太低,算法需要经过大量迭代才能收敛,这将耗费很长时间;反过来说,如果学习率太高,那你可能会越过山谷直接到达另一边,甚至有可能比之前的起点还要高。这会导致算法发散,值越来越大,最后无法找到好的解决方案。
最后,并不是所有的成本函数看起来都像一个漂亮的碗。有的可能看着像洞、山脉、高原或者各种不规则的地形,导致很难收敛到最小值。下图显示了梯度下降的两个主要挑战:如果随机初始化,算法从左侧起步,那么会收敛到一个局部最小值,而不是全局最小值。如果算法从右侧起步,那么需要经过很长时间才能越过整片高原,如果你停下得太早,将永远达不到全局最小值。
幸好,线性回归模型的MSE成本函数恰好是个凸函数,这意味着连接曲线上任意两点的线段永远不会跟曲线相交。也就是说,不存在局部最小值,只有一个全局最小值。它同时也是一个连续函数,所以斜率不会产生陡峭的变化。这两点保证的结论是:即便是乱走,梯度下降都可以趋近到全局最小值(只要等待时间足够长,学习率也不是太高)。
成本函数虽然是碗状的,但如果不同特征的尺寸差别巨大,那它可能是一个非常细长的碗。如图4-7所示的梯度下降,左边的训练集上特征1和特征2具有相同的数值规模,而右边的训练集上,特征1的值则比特征2要小得多(注:因为特征1的值较小,所以θ1需要更大的变化来影响成本函数,这就是为什么碗形会沿着θ1轴拉长。)。
正如你所见,左图的梯度下降算法直接走向最小值,可以快速到达。而在右图中,先是沿着与全局最小值方向近乎垂直的方向前进,接下来是一段几乎平坦的长长的山谷。最终还是会抵达最小值,但是这需要花费大量的时间。
应用梯度下降时,需要保证所有特征值的大小比例都差不多(比如使用Scikit-Learn的类),否则收敛的时间会长很多。
上图也说明,训练模型也就是搜寻使成本函数(在训练集上)最小化的参数组合。这是模型参数空间层面上的搜索:模型的参数越多,这个空间的维度就越多,搜索就越难。同样是在干草堆里寻找一根针,在一个三百维的空间里就比在一个三维空间里要棘手得多。幸运的是,线性回归模型的成本函数是凸函数,针就躺在碗底。
4.2.1 批量梯度下降(Batch Gradient Descent,BGD)
要实现梯度下降,你需要计算每个模型关于参数 θ j θ_j θj的成本函数的梯度。换言之,你需要计算的是如果改变 θ j θ_j θj,成本函数会改变多少。这被称为偏导数。
如果不想单独计算这些偏导数,可以使用公式对其进行一次性计算。梯度向量记作 ▽ θ M S E ( θ ) ▽θMSE(θ) ▽θMSE(θ),包含所有成本函数(每个模型参数一个)的偏导数。
请注意,在计算梯度下降的每一步时,都是基于完整的训练集X的。这就是为什么该算法会被称为批量梯度下降:每一步都使用整批训练数据(实际上,全梯度下降可能是个更好的名字)。因此,面对非常庞大的训练集时,算法会变得极慢(不过我们即将看到快得多的梯度下降算法)。但是,梯度下降算法随特征数量扩展的表现比较好。如果要训练的线性模型拥有几十万个特征,使用梯度下降比标准方程或者SVD要快得多。
一旦有了梯度向量,哪个点向上,就朝反方向下坡。也就是从 θ θ θ中减去 ▽ θ M S E ( θ ) ▽θMSE(θ) ▽θMSE(θ)。这时学习率 η η η就发挥作用了:用梯度向量乘以 η η η确定下坡步长的大小。
让我们看一下该算法的快速实现:
eta = 0.1 # learning rate
n_iterations = 1000
m = 100
theta = np.random.randn(2, 1) # random initialization
for iteration in range(n_iterations):
gradients = 2/m * X_b.T.dot(X_b.dot(theta) - y)
theta = theta - eta * gradients
print(theta)
下图展现了分别使用三种不同的学习率时,梯度下降的前十步(虚线表示起点)。
要找到合适的学习率,可以使用⭐⭐(见第2章)。但是你可能需要限制迭代次数,这样网格搜索可以淘汰掉那些收敛耗时太长的模型。
你可能会问,要怎么限制迭代次数呢?如果设置太低,算法可能在离最优解还很远时就停了。但是如果设置得太高,模型达到最优解后,继续迭代则参数不再变化,又会浪费时间。一个简单的办法是在开始时设置一个非常大的迭代次数,但是当梯度向量的值变得很微小时中断算法——也就是当它的范数变得低于(称为容差)时,因为这时梯度下降已经(几乎)到达了最小值。
收敛速度:
成本函数为凸函数,并且斜率没有陡峭的变化时(如MSE成本函数),具有固定学习率的批量梯度下降最终会收敛到最佳解,但是你需要等待一段时间:它可以进行O(1/∈)次迭代以在∈的范围内达到最佳值,具体取决于成本函数的形状。换句话说,如果将容差缩小为原来的1/10(以得到更精确的解),算法将不得不运行10倍的时间。
4.2.2 随机梯度下降(Stochastic Gradient Descent,SGD)
批量梯度下降的主要问题是它要用整个训练集来计算每一步的梯度,所以训练集很大时,算法会特别慢。与之相反的极端是随机梯度下降,每一步在训练集中随机选择一个实例,并且仅基于该单个实例来计算梯度。显然,这让算法变得快多了,因为每次迭代都只需要操作少量的数据。它也可以被用来训练海量的数据集,因为每次迭代只需要在内存中运行一个实例即可(SGD可以作为核外算法实现,见第1章)。
另一方面,由于算法的随机性质,它比批量梯度下降要不规则得多。成本函数将不再是缓缓降低直到抵达最小值,而是不断上上下下,但是从整体来看,还是在慢慢下降。随着时间的推移,最终会非常接近最小值,但是即使它到达了最小值,依旧还会持续反弹,永远不会停止(见图4-9)。所以算法停下来的参数值肯定是足够好的,但不是最优的。
当成本函数非常不规则时(见图4-6),随机梯度下降其实可以帮助算法跳出局部最小值,所以相比批量梯度下降,它对找到全局最小值更有优势。
因此,随机性的好处在于可以逃离局部最优,但缺点是永远定位不出最小值。要解决这个困境,有一个办法是逐步降低学习率。开始的步长比较大(这有助于快速进展和逃离局部最小值),然后越来越小,让算法尽量靠近全局最小值。这个过程叫作,因为它类似于冶金时熔化的金属慢慢冷却的退火过程。确定每个迭代学习率的函数叫作学习率调度。如果学习率降得太快,可能会陷入局部最小值,甚至是停留在走向最小值的半途中。如果学习率降得太慢,你需要太长时间才能跳到差不多最小值附近,如果提早结束训练,可能只得到一个次优的解决方案。
下面这段代码使用了一个简单的学习率调度实现随机梯度下降:
def learning_schedule(t):
return t0 / (t + t1)
theta = np.random.randn(2, 1) # random initialization
for epoch in range(n_epochs):
for i 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.dot(xi.dot(theta) - yi)
eta = learning_schedule(epoch * m + i)
theta = theta - eta * gradients
print(theta)
按照惯例,我们进行m个回合的迭代。每个回合称为一个轮次。虽然批量梯度下降代码在整个训练集中进行了1000次迭代,但此代码仅在训练集中遍历了50次,并达到了一个很好的解决方案:
使用随机梯度下降时,训练实例必须独立且均匀分布(IID),以确保平均而言将参数拉向全局最优值。确保这一点的一种简单方法是在训练过程中对实例进行随机混洗(例如,随机选择每个实例,或者在每个轮次开始时随机混洗训练集)。如果不对实例进行混洗(例如,如果实例按标签排序),那么SGD将首先针对一个标签进行优化,然后针对下一个标签进行优化,以此类推,并且它不会接近全局最小值。
要使用带有Scikit-Learn的随机梯度下降执行线性回归,可以使用,该类默认优化平方误差成本函数。以下代码最多可运行100000个轮次,或者直到一个轮次期间损失下降小于0.001为止(max_iter=1000,tol=1e-3)。它使用默认的学习调度(与前一个学习调度不同)以0.1(eta0=0.1)的学习率开始。最后,它不使用任何正则化(penalty=None,稍后将对此进行详细介绍):
from sklearn.linear_model import SGDRegressor
sgd_reg = SGDRegressor(max_iter=100000, tol=1e-3, penalty=None, eta0=0.1) # 实例化SGDRegressor的类
sgd_reg.fit(X, y.ravel()) # 训练模型
print(sgd_reg.intercept_, sgd_reg.coef_) # 输出模型的参数
4.2.3 小批量梯度下降(Mini-Batch Gradient Descent)
我们要研究的最后一个梯度下降算法称为小批量梯度下降。只要你了解了批量和随机梯度下降,就很容易理解它:在每一步中,不是根据完整的训练集(如批量梯度下降)或仅基于一个实例(如随机梯度下降)来计算梯度,小批量梯度下降在称为小型批量的随机实例集上计算梯度。小批量梯度下降优于随机梯度下降的主要优点是,你可以通过矩阵操作的硬件优化来提高性能,特别是在使用GPU时。
与随机梯度下降相比,该算法在参数空间上的进展更稳定,尤其是在相当大的小批次中。结果,小批量梯度下降最终将比随机梯度下降走得更接近最小值,但它可能很难摆脱局部最小值(在受局部最小值影响的情况下,不像线性回归)。下图显示了训练期间参数空间中三种梯度下降算法所采用的路径。它们最终都接近最小值,但是批量梯度下降的路径实际上是在最小值处停止,而随机梯度下降和小批量梯度下降都继续走动。但是,不要忘记批量梯度下降每步需要花费很多时间,如果你使用良好的学习率调度,随机梯度下降和小批量梯度下降也会达到最小值。
让我们比较到目前为止讨论的线性回归算法(回想一下, m m m是训练实例的数量, n n n是特征的数量)。
训练后几乎没有区别:所有这些算法最终都具有非常相似的模型,并且以完全相同的方式进行预测。
4.3 多项式回归
如果你的数据比直线更复杂怎么办?令人惊讶的是,你可以使用线性模型来拟合非线性数据。一个简单的方法就是将每个特征的幂次方添加为一个新特征,然后在此扩展特征集上训练一个线性模型。这种技术称为多项式回归。
让我们看一个示例。
首先,让我们基于一个简单的二次方程式(注:二次方程的形式为y=ax2+bx+c。)(加上一些噪声)生成一些非线性数据:
m = 100
X = 6 * np.random.rand(m, 1) - 3
y = 0.5 * X**2 + X + 2 + np.random.randn(m, 1)
显然,一条直线永远无法正确地拟合此数据。因此,让我们使用Scikit-Learn的PolynomialFeatures类来转换训练数据,将训练集中每个特征的平方(二次多项式)添加为新特征(在这种情况下,只有一个特征):
# 将每一个特征的平方都变成一个新的特征
from sklearn.preprocessing import PolynomialFeatures
poly_features = PolynomialFeat