《机器学习实战:基于Scikit-LearnKeras和TensorFlow第2版》-学习笔记:训练模型

Posted 新四石路打卤面

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《机器学习实战:基于Scikit-LearnKeras和TensorFlow第2版》-学习笔记:训练模型相关的知识,希望对你有一定的参考价值。

第四章 训练模型

· 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 线性回归

线性模型就是对输入特征加权求和,再加上一个我们称为偏置项(也称为截距项)的常数,以此进行预测。
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) = \\frac1m\\sum^m_i=1(θ^Tx^(i)-y^(i))^2 MSE=(X,h0)=m1i=1m(θTx(i)y(i))2

4.1.1 标准方程

为了得到使成本函数最小的θ值,有一个闭式解方法——也就是一个直接得出结果的数学方程,即标准方程。

θ ′ = ( X T X ) − 1 X T y θ' = (X^TX)^-1X^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 梯度下降及其算法

梯度下降是一种非常通用的优化算法,能够为大范围的问题找到最优解。梯度下降的中心思想就是迭代地调整参数从而使成本函数最小化。

**假设你迷失在山上的浓雾之中,你能感觉到的只有你脚下路面的坡度。快速到达山脚的一个策略就是沿着最陡的方向下坡。**这就是梯度下降的做法:通过测量参数向量θ相关的误差函数的局部梯度,并不断沿着降低梯度的方向调整,直到梯度降为0,到达最小值

具体来说,首先使用一个随机的 θ θ θ值(这被称为随机初始化),然后逐步改进,每次踏出一步,每一步都尝试降低一点成本函数(如MSE),直到算法收敛出一个最小值(参见图4-3)。

梯度下降中一个重要参数是每一步的步长,这取决于超参数学习率。如果学习率太低,算法需要经过大量迭代才能收敛,这将耗费很长时间;反过来说,如果学习率太高,那你可能会越过山谷直接到达另一边,甚至有可能比之前的起点还要高。这会导致算法发散,值越来越大,最后无法找到好的解决方案。

最后,并不是所有的成本函数看起来都像一个漂亮的碗。有的可能看着像洞、山脉、高原或者各种不规则的地形,导致很难收敛到最小值。下图显示了梯度下降的两个主要挑战:如果随机初始化,算法从左侧起步,那么会收敛到一个局部最小值,而不是全局最小值。如果算法从右侧起步,那么需要经过很长时间才能越过整片高原,如果你停下得太早,将永远达不到全局最小值。

幸好,线性回归模型的MSE成本函数恰好是个凸函数,这意味着连接曲线上任意两点的线段永远不会跟曲线相交。也就是说,不存在局部最小值,只有一个全局最小值。它同时也是一个连续函数,所以斜率不会产生陡峭的变化。这两点保证的结论是:即便是乱走,梯度下降都可以趋近到全局最小值(只要等待时间足够长,学习率也不是太高)。

成本函数虽然是碗状的,但如果不同特征的尺寸差别巨大,那它可能是一个非常细长的碗。如图4-7所示的梯度下降,左边的训练集上特征1和特征2具有相同的数值规模,而右边的训练集上,特征1的值则比特征2要小得多(注:因为特征1的值较小,所以θ1需要更大的变化来影响成本函数,这就是为什么碗形会沿着θ1轴拉长。)。

正如你所见,左图的梯度下降算法直接走向最小值,可以快速到达。而在右图中,先是沿着与全局最小值方向近乎垂直的方向前进,接下来是一段几乎平坦的长长的山谷。最终还是会抵达最小值,但是这需要花费大量的时间。

应用梯度下降时,需要保证所有特征值的大小比例都差不多(比如使用Scikit-Learn的StandardScaler类),否则收敛的时间会长很多。

上图也说明,训练模型也就是搜寻使成本函数(在训练集上)最小化的参数组合。这是模型参数空间层面上的搜索:模型的参数越多,这个空间的维度就越多,搜索就越难。同样是在干草堆里寻找一根针,在一个三百维的空间里就比在一个三维空间里要棘手得多。幸运的是,线性回归模型的成本函数是凸函数,针就躺在碗底。

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次迭代,但此代码仅在训练集中遍历了

以上是关于《机器学习实战:基于Scikit-LearnKeras和TensorFlow第2版》-学习笔记:训练模型的主要内容,如果未能解决你的问题,请参考以下文章

Keras深度学习实战——基于编码器-解码器的机器翻译模型

☀️机器学习实战☀️基于 YOLO网络 的人脸识别 |(文末送机器学习书籍~)

大数据-基于Spark的机器学习-智能客户系统项目实战

机器学习实战3:基于朴素贝叶斯实现单词拼写修正器(附Python代码)

读书笔记-《机器学习实战:基于Scikit-LearnKeras和TensorFlow》 第2版(第一章)

分享《机器学习实战:基于Scikit-Learn和TensorFlow》高清中英文PDF+源代码