在之前的描述中,我们通常把机器学习模型和训练算法当作黑箱子来处理。如果你实践过前几章的一些示例,你惊奇的发现你可以优化回归系统,改进数字图像的分类器,你甚至可以零基础搭建一个垃圾邮件的分类器,但是你却对它们内部的工作流程一无所知。事实上,许多场合你都不需要知道这些黑箱子的内部有什么,干了什么。
然而,如果你对其内部的工作流程有一定了解的话,当面对一个机器学习任务时候,这些理论可以帮助你快速的找到恰当的机器学习模型,合适的训练算法,以及一个好的假设集。同时,了解黑箱子内部的构成,有助于你更好地调试参数以及更有效的误差分析。本章讨论的大部分话题对于机器学习模型的理解,构建,以及神经网络(详细参考本书的第二部分)的训练都是非常重要的。
首先我们将以一个简单的线性回归模型为例,讨论两种不同的训练方法来得到模型的最优解:
直接使用封闭方程进行求根运算,得到模型在当前训练集上的最优参数(即在训练集上使损失函数达到最小值的模型参数)
使用迭代优化方法:梯度下降(GD),在训练集上,它可以逐渐调整模型参数以获得最小的损失函数,最终,参数会收敛到和第一种方法相同的的值。同时,我们也会介绍一些梯度下降的变体形式:批量梯度下降(Batch GD)、小批量梯度下降(Mini-batch GD)、随机梯度下降(Stochastic GD),在第二部分的神经网络部分,我们会多次使用它们。
接下来,我们将研究一个更复杂的模型:多项式回归,它可以拟合非线性数据集,由于它比线性模型拥有更多的参数,于是它更容易出现模型的过拟合。因此,我们将介绍如何通过学习曲线去判断模型是否出现了过拟合,并介绍几种正则化方法以减少模型出现过拟合的风险。
最后,我们将介绍两个常用于分类的模型:Logistic回归和Softmax回归
提示
在本章中包含许多数学公式,以及一些线性代数和微积分基本概念。为了理解这些公式,你需要知道什么是向量,什么是矩阵,以及它们直接是如何转化的,以及什么是点积,什么是矩阵的逆,什么是偏导数。如果你对这些不是很熟悉的话,你可以阅读本书提供的 Jupyter 在线笔记,它包括了线性代数和微积分的入门指导。对于那些不喜欢数学的人,你也应该快速简单的浏览这些公式。希望它足以帮助你理解大多数的概念。
在第一章,我们介绍了一个简单的的生活满意度回归模型:
$$ life_satisfaction = \theta _{0} + \theta _{1} * GDP_per_capita $$
这个模型仅仅是输入量GDP_per_capita
的线性函数,$\theta _{0}$ 和 $\theta _{1}$ 是这个模型的参数,线性模型更一般化的描述指通过计算输入变量的加权和,并加上一个常数偏置项(截距项)来得到一个预测值。如公式 4-1:
公式 4-1:线性回归预测模型
$$ \hat{y} = \theta _{0} + \theta _{1}x _{1}+\theta _{2}x _{2}+\dots+\theta _{n}x _{n} $$
$\hat{y}$ 表示预测结果
$n$ 表示特征的个数
$x_{i}$ 表示第i
个特征的值
$\theta_{j}$ 表示第j
个参数(包括偏置项 $\theta _{0}$ 和特征权重值 $\theta _{1},\theta _{2},\dots,\theta _{n}$)
上述公式可以写成更为简洁的向量形式,如公式 4-2:公式 4-2:线性回归预测模型(向量形式)
$$ \hat{y} = h _{\theta} (\mathbf{x})= \theta^T \cdot \mathbf{x} $$
$\theta$ 表示模型的参数向量包括偏置项 $\theta _{0}$ 和特征权重值 $\theta _{1}$ 到 $\theta _{n}$
$\theta^T$ 表示向量$\theta$的转置(行向量变为了列向量)
$\mathbf{x}$ 为每个样本中特征值的向量形式,包括 $x {1}$ 到 $x{n}$,而且 $x_0$ 恒为 1
$\theta^T \cdot \mathbf{x}$ 表示 $\theta^T$ 和$ \mathbf{x}$ 的点积
$h_{\theta}$ 表示参数为 $\theta$ 的假设函数
怎么样去训练一个线性回归模型呢?好吧,回想一下,训练一个模型指的是设置模型的参数使得这个模型在训练集的表现较好。为此,我们首先需要找到一个衡量模型好坏的评定方法。在第二章,我们介绍到在回归模型上,最常见的评定标准是均方根误差(RMSE,详见公式 2-1)。因此,为了训练一个线性回归模型,你需要找到一个 $\theta$ 值,它使得均方根误差(标准误差)达到最小值。实践过程中,最小化均方误差比最小化均方根误差更加的简单,这两个过程会得到相同的 $\theta$,因为函数在最小值时候的自变量,同样能使函数的方根运算得到最小值。
在训练集 $\mathbf{X}$ 上使用公式 4-3 来计算线性回归假设 $h_{\theta}$ 的均方差($MSE$)。
公式 4-3:线性回归模型的 MSE 损失函数
$$ MSE (\mathbf{X},h {\theta}$) = $\frac{1}{m} \sum\limits{i=1}^m{\left(\theta^T \cdot \mathbf{x}^{(i)}-y^{(i)}\right)}^2 $$
公式中符号的含义大多数都在第二章(详见“符号”)进行了说明,不同的是:为了突出模型的参数向量 $\theta$,使用 $h_{\theta}$ 来代替 $h$。以后的使用中为了公式的简洁,使用 $MSE(\theta)$ 来代替 $MSE(\mathbf{X},h _{\theta})$。
为了找到最小化损失函数的 $\theta$ 值,可以采用公式解,换句话说,就是可以通过解正态方程直接得到最后的结果。
公式 4-4:正态方程
$$ \hat{\theta} = ({\mathbf{X}}^T\cdot\mathbf{X})^{-1}\cdot{\mathbf{X}}^T\cdot\mathbf{y} $$
$\hat{\theta}$ 指最小化损失 $\theta$ 的值
$\mathbf{y}$ 是一个向量,其包含了 $y^{(1)}$ 到 $y^{(m)}$ 的值
让我们生成一些近似线性的数据(如图 4-1)来测试一下这个方程。
import numpy as np X = 2 * np.random.rand(100, 1) y = 4 + 3 * X + np.random.randn(100, 1)
图 4-1:随机线性数据集
现在让我们使用正态方程来计算 $\hat{\theta}$,我们将使用 Numpy 的线性代数模块(np.linalg
)中的inv()
函数来计算矩阵的逆,以及dot()
方法来计算矩阵的乘法。
X_b = np.c_[np.ones((100, 1)), X] theta_best = np.linalg.inv(X_b.T.dot(X_B)).dot(X_b.T).dot(y)
我们生产数据的函数实际上是 $y = 4 + 3x_0 + 高斯噪声$。让我们看一下最后的计算结果。
>>> theta_best array([[4.21509616],[2.77011339]])
我们希望最后得到的参数为 $\theta_0=4,\theta_1=3$ 而不是 $\theta_0=3.865,\theta_1=3.139$ (译者注:我认为应该是 $\theta_0=4.2150,\theta_1=2.7701$)。这已经足够了,由于存在噪声,参数不可能达到到原始函数的值。
现在我们能够使用 $\hat{\theta}$ 来进行预测:
>>> X_new = np.array([[0],[2]]) >>> X_new_b = np.c_[np.ones((2, 1)), X_new] >>> y_predict = X_new_b.dot(theta.best) >>> y_predict array([[4.21509616],[9.75532293]])
画出这个模型的图像,如图 4-2
plt.plot(X_new,y_predict,"r-") plt.plot(X,y,"b.") plt.axis([0,2,0,15]) plt.show()
图4-2:线性回归预测
使用下面的 Scikit-Learn 代码可以达到相同的效果:
正态方程需要计算矩阵 ${\mathbf{X}}^T\cdot\mathbf{X}$ 的逆,它是一个 $n * n$ 的矩阵($n$ 是特征的个数)。这样一个矩阵求逆的运算复杂度大约在 $O(n^{2.4})$ 到 $O(n^3)$ 之间,具体值取决于计算方式。换句话说,如果你将你的特征个数翻倍的话,其计算时间大概会变为原来的 5.3($2^{2.4}$)到 8($2^3$)倍。
提示
当特征的个数较大的时候(例如:特征数量为 100000),正态方程求解将会非常慢。
有利的一面是,这个方程在训练集上对于每一个实例来说是线性的,其复杂度为 $O(m)$,因此只要有能放得下它的内存空间,它就可以对大规模数据进行训练。同时,一旦你得到了线性回归模型(通过解正态方程或者其他的算法),进行预测是非常快的。因为模型中计算复杂度对于要进行预测的实例数量和特征个数都是线性的。 换句话说,当实例个数变为原来的两倍多的时候(或特征个数变为原来的两倍多),预测时间也仅仅是原来的两倍多。
接下来,我们将介绍另一种方法去训练模型。这种方法适合在特征个数非常多,训练实例非常多,内存无法满足要求的时候使用。
梯度下降是一种非常通用的优化算法,它能够很好地解决一系列问题。梯度下降的整体思路是通过的迭代来逐渐调整参数使得损失函数达到最小值。
假设浓雾下,你迷失在了大山中,你只能感受到自己脚下的坡度。为了最快到达山底,一个最好的方法就是沿着坡度最陡的地方下山。这其实就是梯度下降所做的:它计算误差函数关于参数向量$\theta$的局部梯度,同时它沿着梯度下降的方向进行下一次迭代。当梯度值为零的时候,就达到了误差函数最小值 。
具体来说,开始时,需要选定一个随机的$\theta$(这个值称为随机初始值),然后逐渐去改进它,每一次变化一小步,每一步都试着降低损失函数(例如:均方差损失函数),直到算法收敛到一个最小值(如图:4-3)。
图 4-3:梯度下降
在梯度下降中一个重要的参数是步长,超参数学习率的值决定了步长的大小。如果学习率太小,必须经过多次迭代,算法才能收敛,这是非常耗时的(如图 4-4)。
图 4-4:学习率过小
另一方面,如果学习率太大,你将跳过最低点,到达山谷的另一面,可能下一次的值比上一次还要大。这可能使的算法是发散的,函数值变得越来越大,永远不可能找到一个好的答案(如图 4-5)。
图 4-5:学习率过大
最后,并不是所有的损失函数看起来都像一个规则的碗。它们可能是洞,山脊,高原和各种不规则的地形,使它们收敛到最小值非常的困难。 图 4-6 显示了梯度下降的两个主要挑战:如果随机初始值选在了图像的左侧,则它将收敛到局部最小值,这个值要比全局最小值要大。 如果它从右侧开始,那么跨越高原将需要很长时间,如果你早早地结束训练,你将永远到不了全局最小值。
图 4-6:梯度下降的陷阱
幸运的是线性回归模型的均方差损失函数是一个凸函数,这意味着如果你选择曲线上的任意两点,它们的连线段不会与曲线发生交叉(译者注:该线段不会与曲线有第三个交点)。这意味着这个损失函数没有局部最小值,仅仅只有一个全局最小值。同时它也是一个斜率不能突变的连续函数。这两个因素导致了一个好的结果:梯度下降可以无限接近全局最小值。(只要你训练时间足够长,同时学习率不是太大 )。
事实上,损失函数的图像呈现碗状,但是不同特征的取值范围相差较大的时,这个碗可能是细长的。图 4-7 展示了梯度下降在不同训练集上的表现。在左图中,特征 1 和特征 2 有着相同的数值尺度。在右图中,特征 1 比特征2的取值要小的多,由于特征 1 较小,因此损失函数改变时,$\theta_1$ 会有较大的变化,于是这个图像会在$\theta_1$轴方向变得细长。
图 4-7:有无特征缩放的梯度下降
正如你看到的,左面的梯度下降可以直接快速地到达最小值,然而在右面的梯度下降第一次前进的方向几乎和全局最小值的方向垂直,并且最后到达一个几乎平坦的山谷,在平坦的山谷走了很长时间。它最终会达到最小值,但它需要很长时间。
提示
当我们使用梯度下降的时候,应该确保所有的特征有着相近的尺度范围(例如:使用 Scikit Learn 的
StandardScaler
类),否则它将需要很长的时间才能够收敛。
这幅图也表明了一个事实:训练模型意味着找到一组模型参数,这组参数可以在训练集上使得损失函数最小。这是对于模型参数空间的搜索,模型的参数越多,参数空间的维度越多,找到合适的参数越困难。例如在300维的空间找到一枚针要比在三维空间里找到一枚针复杂的多。幸运的是线性回归模型的损失函数是凸函数,这个最优参数一定在碗的底部。
使用梯度下降的过程中,你需要计算每一个 $\theta_j$ 下损失函数的梯度。换句话说,你需要计算当$\theta_j$变化一点点时,损失函数改变了多少。这称为偏导数,它就像当你面对东方的时候问:"我脚下的坡度是多少?"。然后面向北方的时候问同样的问题(如果你能想象一个超过三维的宇宙,可以对所有的方向都这样做)。公式 4-5 计算关于 $\theta_j$ 的损失函数的偏导数,记为 $\frac{\partial }{\partial \theta_j}MSE(\theta)$。
公式 4-5: 损失函数的偏导数
$$ \frac{\partial }{\partial \theta_j}MSE(\theta)=\frac{2}{m} \sum\limits_{i=1}^m{\left(\theta^T \cdot \mathbf{x}^{(i)}-y^{(i)}\right)}{x_j}^{(i)} $$
为了避免单独计算每一个梯度,你也可以使用公式 4-6 来一起计算它们。梯度向量记为 $\nabla_{\theta}MSE(\theta)$,其包含了损失函数所有的偏导数(每个模型参数只出现一次)。
公式 4-6:损失函数的梯度向量
$$ \nabla_{\theta}MSE(\theta)= \left(\begin{matrix} \frac{\partial }{\partial \theta_0}MSE(\theta)\ \frac{\partial }{\partial \theta_1}MSE(\theta)\ \vdots \ \frac{\partial }{\partial \theta_n}MSE(\theta)\ \end{matrix}\right) =\frac{2}{m}{\mathbf{X}}^T\cdot{(\mathbf{X}\cdot\theta-y)} $$
提示
在这个方程中每一步计算时都包含了整个训练集 $\mathbf{X}$,这也是为什么这个算法称为批量梯度下降:每一次训练过程都使用所有的的训练数据。因此,在大数据集上,其会变得相当的慢(但是我们接下来将会介绍更快的梯度下降算法)。然而,梯度下降的运算规模和特征的数量成正比。训练一个数千数量特征的线性回归模型使用*梯度下降要比使用正态方程快的多。
一旦求得了方向是上山的梯度向量,你就可以向着相反的方向去下山。这意味着从 $\theta$ 中减去 $\nabla_{\theta}MSE(\theta)$。学习率 $\eta$ 和梯度向量的积决定了下山时每一步的大小,如公式 4-7。
公式 4-7:梯度下降步长
$$ \theta^{(next\ step)}=\theta - \eta\nabla_{\theta}MSE(\theta) $$
让我们看一下这个算法的应用:
eta = 0.1 # 学习率 n_iterations = 1000 m = 100 theta = np.random.randn(2,1) # 随机初始值 for iteration in range(n_iterations): gradients = 2/m * X_b.T.dot(X_b.dot(theta) - y) theta = theta - eta * gradiens
这不是太难,让我们看一下最后的结果 $\theta$:
>>> theta array([[4.21509616],[2.77011339]])
看!正态方程的表现非常好。完美地求出了梯度下降的参数。但是当你换一个学习率会发生什么?图 4-8 展示了使用了三个不同的学习率进行梯度下降的前 10 步运算(虚线代表起始位置)。
图 4-8:不同学习率的梯度下降
在左面的那副图中,学习率是最小的,算法几乎不能求出最后的结果,而且还会花费大量的时间。在中间的这幅图中,学习率的表现看起来不错,仅仅几次迭代后,它就收敛到了最后的结果。在右面的那副图中,学习率太大了,算法是发散的,跳过了所有的训练样本,同时每一步都离正确的结果越来越远。
为了找到一个好的学习率,你可以使用网格搜索(详见第二章)。当然,你一般会限制迭代的次数,以便网格搜索可以消除模型需要很长时间才能收敛这一个问题。
你可能想知道如何选取迭代的次数。如果它太小了,当算法停止的时候,你依然没有找到最优解。如果它太大了,算法会非常的耗时同时后来的迭代参数也不会发生改变。一个简单的解决方法是:设置一个非常大的迭代次数,但是当梯度向量变得非常小的时候,结束迭代。非常小指的是:梯度向量小于一个值 $\varepsilon$(称为容差)。这时候可以认为梯度下降几乎已经达到了最小值。
收敛速率:
当损失函数是凸函数,同时它的斜率不能突变(就像均方差损失函数那样),那么它的批量梯度下降算法固定学习率之后,它的收敛速率是 $O(\frac{1}{iterations})$。换句话说,如果你将容差 $\varepsilon$ 缩小 10 倍后(这样可以得到一个更精确的结果),这个算法的迭代次数大约会变成原来的 10 倍。