训练模型
在之前的描述中,我们通常把机器学习模型和训练算法当作黑箱子来处理。
然而,如果你对其内部的工作流程有一定了解的话,当面对一个机器学习任务时候,这些理论可以帮助你快速的找到恰当的机器学习模型,合适的训练算法,以及一个好的假设集。同时,了解黑箱子内部的构成,有助于你更好地调试参数以及更有效的误差分析。本章讨论的大部分话题对于机器学习模型的理解,构建,以及神经网络(详细参考本书的第二部分)的训练都是非常重要的。
首先我们将以一个简单的线性回归模型为例,讨论两种不同的训练方法来得到模型的最优解:
直接使用封闭方程进行求根运算,得到模型在当前训练集上的最优参数(即在训练集上使损失函数达到最小值的模型参数)
使用迭代优化方法:梯度下降(GD),在训练集上,它可以逐渐调整模型参数以获得最小的损失函数,最终,参数会收敛到和第一种方法相同的的值。同时,我们也会介绍一些梯度下降的变体形式:批量梯度下降(Batch GD)、小批量梯度下降(Mini-batch GD)、随机梯度下降(Stochastic GD),在第二部分的神经网络部分,我们会多次使用它们。
接下来,我们将研究一个更复杂的模型:多项式回归,它可以拟合非线性数据集,由于它比线性模型拥有更多的参数,于是它更容易出现模型的过拟合。因此,我们将介绍如何通过学习曲线去判断模型是否出现了过拟合,并介绍几种正则化方法以减少模型出现过拟合的风险。
最后,我们将介绍两个常用于分类的模型:Logistic回归和Softmax回归。
1.线性回归
The Normal Equation
多元回归中,参数的求解公式是:
让我们生成一些近似线性的数据来测试一下这个方程。
import numpy as np
np.random.seed(42)
X = 2 * np.random.rand(100, 1) #返回大小为[100, 1]的服从标准正态分布的随机样本值
y = 4 + 3 * X + np.random.randn(100, 1)
import matplotlib.pyplot as plt
plt.scatter(X,y)
plt.show()
现在让我们使用正态方程来计算
θ
^
\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)
让我们看一下最后结果
theta_best
[[4.21509616]
[2.77011339]]
因为存在随机噪声,预测值和真实值有一定差距
现在我们能够使用 θ ^ \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)
print(y_predict)
[[4.21509616]
[9.75532293]]
画出这个模型的图像
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)
使用下面的 Scikit-Learn 代码可以达到相同的效果:
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(X,y)
lin_reg.intercept_, lin_reg.coef_ #coef_存放回归系数,intercept_则存放截距
lin_reg.predict(X_new)
(array([4.21509616]),array([2.77011339]))
array([[4.21509616],[9.75532293]])
计算复杂度
正态方程需要计算矩阵 X T ⋅ X {\mathbf{X}}^T\cdot\mathbf{X} XT⋅X 的逆,它是一个 n * n 的矩阵(n 是特征的个数)。这样一个矩阵求逆的运算复杂度大约在 O ( n 2.4 ) O(n^{2.4}) O(n2.4) 到 O ( n 3 ) O(n^{3}) O(n3)之间,具体值取决于计算方式。换句话说,如果你将你的特征个数翻倍的话,其计算时间大概会变为原来的 5.3( 2 2.4 2^{2.4} 22.4)到 8( 2 3 2^{3} 23)倍。
提示:
当特征的个数较大的时候(例如:特征数量为 100000),正态方程求解将会非常慢。
接下来,我们将介绍另一种方法去训练模型。这种方法适合在特征个数非常多,训练实例非常多,内存无法满足要求的时候使用。
梯度下降
梯度下降是一种非常通用的优化算法,它能够很好地解决一系列问题。梯度下降的整体思路是通过的迭代来逐渐调整参数使得损失函数达到最小值。
假设浓雾下,你迷失在了大山中,你只能感受到自己脚下的坡度。为了最快到达山底,一个最好的方法就是沿着坡度最陡的地方下山。这其实就是梯度下降所做的:它计算误差函数关于参数向量 θ \theta θ的局部梯度,同时它沿着梯度下降的方向进行下一次迭代。当梯度值为零的时候,就达到了误差函数最小值 。
具体来说,开始时,需要选定一个随机的\theta(这个值称为随机初始值),然后逐渐去改进它,每一次变化一小步,每一步都试着降低损失函数(例如:均方差损失函数),直到算法收敛到一个最小值,如下图。
线性回归模型的均方差损失函数是一个凸函数,这意味着如果你选择曲线上的任意两点,它们的连线段不会与曲线发生交叉(译者注:该线段不会与曲线有第三个交点)。这意味着这个损失函数没有局部最小值,仅仅只有一个全局最小值。同时它也是一个斜率不能突变的连续函数。这两个因素导致了一个好的结果:梯度下降可以无限接近全局最小值。(只要你训练时间足够长,同时学习率不是太大 )。
事实上,损失函数的图像呈现碗状,但是不同特征的取值范围相差较大的时,这个碗可能是细长的。下图展示了梯度下降在不同训练集上的表现。在左图中,特征 1 和特征 2 有着相同的数值尺度。在右图中,特征 1 比特征2的取值要小的多,由于特征 1 较小,因此损失函数改变时,
θ
1
\theta_1
θ1 会有较大的变化,于是这个图像会在
θ
1
\theta_1
θ1 轴方向变得细长。
批量梯度下降
批量梯度下降SGD:每更新一个参数时,需要所有的训练样本。
以线性回归为例:
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
array([[4.21509616],[2.77011339]])
优点:
(1) 一次迭代是对所有样本进行计算,此时利用矩阵进行操作,实现了并行。
(2) 由全数据集确定的方向能够更好地代表样本总体,从而更准确地朝向极值所在的方向。当目标函数为凸函数时,BGD一定能够得到全局最优。
缺点:
当样本数目 m 很大时,每迭代一步都需要对所有样本计算,训练过程会很慢。
随机梯度下降
随机梯度下降是每次迭代使用一个样本来对参数进行更新。
由于每一次迭代,只需要在内存中有一个实例,这使随机梯度算法可以在大规模训练集上使用。
虽然随机性可以很好的跳过局部最优值,但同时它却不能达到最小值。解决这个难题的一个办法是逐渐降低学习率。 开始时,走的每一步较大(这有助于快速前进同时跳过局部最小值),然后变得越来越小,从而使算法到达全局最小值。 决定每次迭代的学习率的函数称为learning schedule
。
n_epochs = 50
t0, t1 = 5, 50 #learning_schedule的超参数
def learning_schedule(t):
return t0 / (t + t1)
theta = np.random.randn(2,1)
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 * gradiens
优点:
由于不是在全部训练数据上的损失函数,而是在每轮迭代中,随机优化某一条训练数据上的损失函数,这样每一轮参数的更新速度大大加快。
缺点:
(1) 准确度下降。由于即使在目标函数为强凸函数的情况下,SGD仍旧无法做到线性收敛。
(2) 可能会收敛到局部最优,由于单个样本并不能代表全体样本的趋势。
(3) 不易于并行实现。
通过使用 Scikit-Learn 完成线性回归的随机梯度下降,你需要使用SGDRegressor类,这个类默认优化的是均方差损失函数。下面的代码迭代了 50 代,其学习率
η
\eta
η为0.1(eta0=0.1),使用默认的learning schedule
(与前面的不一样),同时也没有添加任何正则项(penalty = None):
from sklearn.linear_model import SGDRegressor
sgd_reg = SGDRregressor(n_iter=50, penalty=None, eta0=0.1)
sgd_reg.fit(X,y.ravel())
sgd_reg.intercept_, sgd_reg.coef_
(array([4.18380366]),array([2.74205299]))
小批量梯度下降
小批量梯度下降每次迭代 使用 ** batch_size** 个样本来对参数进行更新。
在迭代的每一步,批量梯度使用整个训练集,随机梯度时候用仅仅一个实例,在小批量梯度下降中,它则使用一个随机的小型实例集。
小批量梯度下降在参数空间上的表现比随机梯度下降要好的多,尤其在有大量的小型实例集时。作为结果,小批量梯度下降会比随机梯度更靠近最小值。但是,另一方面,它有可能陷在局部最小值中(在遇到局部最小值问题的情况下,和我们之前看到的线性回归不一样)。
优点:
(1) 通过矩阵运算,每次在一个batch上优化神经网络参数并不会比单个数据慢太多。
(2) 每次使用一个batch可以大大减小收敛所需要的迭代次数,同时可以使收敛到的结果更加接近梯度下降的效果。(比如上例中的30W,设置batch_size=100时,需要迭代3000次,远小于SGD的30W次)
(3) 可实现并行化。
缺点:
(1) batch_size的不当选择可能会带来一些问题。
2.多项式回归
如果你的数据实际上比简单的直线更复杂呢? 令人惊讶的是,你依然可以使用线性模型来拟合非线性数据。 一个简单的方法是对每个特征进行加权后作为新的特征,然后训练一个线性模型在这个扩展的特征集。 这种方法称为多项式回归。
首先,我们根据一个简单的二次方程(并加上一些噪声,如图 4-12)来生成一些非线性数据:
m = 100
X = 6 * np.random.rand(m, 1) - 3
y = 0.5 * X**2 + X + 2 + np.random.randn(m, 1)
很清楚的看出,直线不能恰当的拟合这些数据。于是,我们使用 Scikit-Learning 的PolynomialFeatures类进行训练数据集的转换,让训练集中每个特征的平方(2 次多项式)作为新特征(在这种情况下,仅存在一个特征):
from sklearn.preprocessing import PolynomialFeatures
poly_features = PolynomialFeatures(degree=2,include_bias=False)
X_poly = poly_features.fit_transform(X)
学习曲线
如果你使用一个高阶的多项式回归,你可能发现它的拟合程度要比普通的线性回归要好的多。例如,图 4-14 使用一个 300 阶的多项式模型去拟合之前的数据集,并同简单线性回归、2 阶的多项式回归进行比较。注意 300 阶的多项式模型如何摆动以尽可能接近训练实例。
m = 100
X = 6 * np.random.rand(m, 1) - 3
y = 0.5 * X**2 + X + 2 + np.random.randn(m, 1)
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
def plot_learning_curves(model, X, y):
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2)
train_errors, val_errors = [], []
for m in range(1, len(X_train)):
model.fit(X_train[:m], y_train[:m]) #用m个样本进行训练 m的范围是[1, len(X_train))
y_train_predict = model.predict(X_train[:m])
y_val_predict = model.predict(X_val)
train_errors.append(mean_squared_error(y_train_predict, y_train[:m]))
val_errors.append(mean_squared_error(y_val_predict, y_val))
plt.plot(np.sqrt(train_errors), "r-+", linewidth=2, label="train")
plt.plot(np.sqrt(val_errors), "b-", linewidth=3, label="val")
plt.show()
lin_reg = LinearRegression()
plot_learning_curves(lin_reg, X, y)
这幅图值得我们深究。首先,我们观察训练集的表现:当训练集只有一两个样本的时候,模型能够非常好的拟合它们,这也是为什么曲线是从零开始的原因。但是当加入了一些新的样本的时候,训练集上的拟合程度变得难以接受,出现这种情况有两个原因,一是因为数据中含有噪声,另一个是数据根本不是线性的。因此随着数据规模的增大,误差也会一直增大,直到达到高原地带并趋于稳定,在之后,继续加入新的样本,模型的平均误差不会变得更好或者更差。我们继续来看模型在验证集上的表现,当以非常少的样本去训练时,模型不能恰当的泛化,也就是为什么验证误差一开始是非常大的。当训练样本变多的到时候,模型学习的东西变多,验证误差开始缓慢的下降。但是一条直线不可能很好的拟合这些数据,因此最后误差会到达在一个高原地带并趋于稳定,最后和训练集的曲线非常接近。
上面的曲线表现了一个典型的欠拟合模型,两条曲线都到达高原地带并趋于稳定,并且最后两条曲线非常接近,同时误差值非常大。
如果你的模型在训练集上是欠拟合的,添加更多的样本是没用的。你需要使用一个更复杂的模型或者找到更好的特征。
3.线性模型的正则化
岭(Ridge)回归
Lasso 回归
弹性网络(ElasticNet)
早期停止法(Early Stopping)
7.逻辑回归
概率估计
训练和损失函数
决策边界
8.Softmax 回归
9.参考链接
批量梯度下降(BGD)、随机梯度下降(SGD)以及小批量梯度下降(MBGD)的理解
sklearn 的 PolynomialFeatures 的用法