机器学习|模型训练
在训练不同机器学习算法模型时,遇到的各类训练算法大多对用户都是一个黑匣子,而理解它们实际怎么工作,对用户是很有帮助的;
- 快速定位到合适的模型与正确的训练算法,找到一套适当的超参数等;
- 更高效的执行错误调试、错误分析等;
- 有助于理解、构建和训练神经网络等;
训练方法
- 线性回归模型
- 闭式方程,直接计算出最拟合训练集的模型参数(使训练集上的成本函数最小化的模型参数);
- 迭代优化(GD、梯度下降、梯度下降变体、小批量梯度下降、随机梯度下降),逐渐调整模型参数直至训练集上的成本函数调至最低;
- 多项式回归模型
- 学习曲线评估过拟合情况
- 正则化技巧(降低过拟合风险)
- 分类模型
- Logistic 回归
- Softmax 回归
1. 线性回归
线性模型可以当做是对输入特征做加权求和,再加上一个偏置项(截距项)常数;
线性回归模型预测
- ,预测值;
n
,特征数量;- ,第 i 个特征值;
- ,第 j 个模型参数(包括偏差项 和特征权重 、、...、);
线性回归模型预测(向量化形式)
- ,模型的参数向量,其中包含偏差项 和特征权重 至
x
,实例的特征向量,包含从 到 , 始终等于 1;- ,向量 和 X 的点积,它相当于 ;
- ,假设函数,使用模型参数 ;
特征向量
(feature vector
),一个样本对应在样本空间中坐标轴上的坐标向量;
向量
,在机器学习中通常表示列向量,表示单一列的二维数组;
线性回归模型的 MSE 成本函数
- ,其中的 表示模型 h 是被向量 参数化的;
1. 标准方程
闭式解方法
,直接得出使成本函数最小的 值的数据方程,也称标准方程;
- ,使成本函数最小的 值;
- y,包含 到 的目标值向量;
使用线性数据测试标准方程
import numpy as np
X = 2 * np.random.rand(100, 1)
y = 4 + 3 * X + np.random.randn(100, 1)
plt.plot(X, y, "b.")
plt.xlabel("$x_1$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.axis([0, 2, 0, 15])
plt.show()
使用标准方程计算
inv()
,对矩阵求逆;dot()
,计算矩阵的内积;
>>> 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)
>>>
array([[4.21509616],
[2.77011339]])
原本的 =4,=3,这里算出的 =4.215,=2.770,已经比较接近;
使用 做预测
>>> 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)
>>> y_predict
array([[4.21509616],
[9.75532293]])
绘制模型的预测结果
plt.plot(X_new, y_predict, "r-", linewidth=2, label="Predictions")
plt.plot(X, y, "b.")
plt.xlabel("$x_1$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.legend(loc="upper left", fontsize=14)
plt.axis([0, 2, 0, 15])
plt.show()
2. 奇异值分解
回顾使用 Scikit-Learn 的 LinearRegression
>>> 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]])
intercept_
,偏差项;coef_
,特征权重;
LinearRegression 的 是基于 scipy.linalg.lstsq() 函数(最小二乘
)计算的;
- ,X 的伪逆;
>>> theta_best_svd, residuals, rank, s = np.linalg.lstsq(X_b, y, rcond=1e-6)
>>> theta_best_svd
array([[4.21509616],
[2.77011339]])
使用 np.linalg.pinv() 计算伪逆
>>> np.linalg.pinv(X_b).dot(y)
array([[4.21509616],
[2.77011339]])
奇异值分解
(Singular Value Decomposition
,SVD
),计算伪逆的标准矩阵分解技术,可将训练集矩阵 X 分解成三个矩阵 的乘积(numpy.linalg.svd() 实现);
- 的计算方式:取 并将所有小于一个阈值的值设置成 0,再将非 0 值替换成它们的倒数,最后把结果矩阵转置;
** SVD vs. 标准方程**
伪逆比标准方程更有效,可以很好的处理边缘问题,若 是不可逆的,标准方程可能无解,而伪逆总是有定义的;
3. 计算复杂度
标准方程相对特征数量 n 的计算复杂度
标准方程计算的是 的逆, 是一个 的矩阵(n 是特征数),求逆的计算复杂度通常为 至 (具体取决于实现方式);当 n 翻倍,计算复杂度将变大 =5.3 至 =8 倍;
SVD 相对特征数量 n 的计算复杂度
相对于训练集的实例数量 ,标准方程和 SVD 的计算复杂度都是线性的;
线性回归模型一经训练(不论标准方程还是其他算法),预测就非常快,计算复杂度相对于要预测实例 数量和特征数量都是线性的;
2. 梯度下降
假设你迷失在山上的浓雾之中,你能感觉到的只有你脚下路面的坡度;快速到达山脚的一个策略就是沿着最陡的方向下坡;
梯度下降算法
,通过测量参数向量 相关的误差函数的局部梯度,并不断沿着降低梯度的方向调整,直到梯度降为 0,达到最小值;
首先随机选择一个 值(随机初始化),然后逐步改进,每次踏出一步,每步参试降低一点成本函数(如 MSE),直到算法收敛为一个最小值;
学习率
,超参数,梯度下降每一步的步长;太低
,算法需要大量迭代才能收敛,耗时变得很长;太高
,导致算法发散,值越来越大,可能直接越过山谷到达另一边,甚至比之前的起点还高,无法找到最优解;
梯度下降陷阱
并非所有的成本函数都是一个碗型的,可能如图一般不规则,导致很难收敛到全局最小值;若随机初始化起点在左侧,会收敛到一个局部最小值,而非全局最小值;若随机初始化起点在右侧,则可能需要很长一段时间才能迭代到最低点,若停下得太早,可能永远无法到底全局最小值;
线性回归模型的 MSE 成本函数是一个连续的凸函数,因此不存在局部最小值,只有一个全局最小值,且斜率不会产生陡峭变化;即使乱走梯度下降也可以趋近全局最小值;
细长碗状成本函数
因不同特征的尺寸差异巨大导致的细长碗状成本函数;特征值越小(如 ),就需要更大的变化来影响成本函数;
左图的梯度下降算法直接走向最小值,可以快速到达;右图则先沿着与全局最小值方向近乎垂直的方向前进,然后验证近乎平坦的山谷走到最小值,需要花费大量时间;
应用梯度下降时,需保证所有特征值大小比例相差不多(比如使用 Scikit-Learn 的 StandardScaler),否则收敛时间会长很多;
训练模型也就是搜寻使成本函数(在训练集上)最小化的参数组合;这是模型参数空间层面上的搜索,模型的参数越多,这个空间的维度就越多,搜索就越难;同样是在干草堆里寻找一根针,在一个三百维的空间里就比在一个三维空间里要棘手得多;幸运的是,线性回归模型的成本函数是凸函数,针就躺在碗底;
1. 批量梯度下降
实现梯度下降需要计算每个模型关于参数 的成本函数的梯度,即计算关于参数 的成本函数的偏导数,计作 ;
成本函数的偏导数
一次性计算偏导数
在计算梯度下降的每一步时,都是基于完整的训练集 X 的;因此面对非常庞大的训练集时,算法会变得极慢(梯度下降算法随特征数量扩展的表现比较好,比如几十万个特征,梯度下降比标准方程或者 SVD 要快很多);
梯度下降步骤
- ,学习率,用梯度向量乘以 确定下坡步长的大小;
梯度下降的快速实现
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)
[[4.21509616]
[2.77011339]]
theta 计算结果与标准方程的计算结果相同;
计算三种学习率的梯度下降前十步
虚线表示起点;
theta_path_bgd = []
def plot_gradient_descent(theta, eta, theta_path=None):
m = len(X_b)
plt.plot(X, y, "b.")
n_iterations = 1000
for iteration in range(n_iterations):
if iteration < 10:
y_predict = X_new_b.dot(theta)
style = "b-" if iteration > 0 else "r--"
plt.plot(X_new, y_predict, style)
gradients = 2/m * X_b.T.dot(X_b.dot(theta) - y)
theta = theta - eta * gradients
if theta_path is not None:
theta_path.append(theta)
plt.xlabel("$x_1$", fontsize=18)
plt.axis([0, 2, 0, 15])
plt.title(r"$\eta = {}$".format(eta), fontsize=16)
np.random.seed(42)
theta = np.random.randn(2,1) # random initialization
plt.figure(figsize=(10,4))
plt.subplot(131); plot_gradient_descent(theta, eta=0.02)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.subplot(132); plot_gradient_descent(theta, eta=0.1, theta_path=theta_path_bgd)
plt.subplot(133); plot_gradient_descent(theta, eta=0.5)
plt.show()
左图
,学习率太低,需要太长时间找到解决方法;中图
,学习率恰好,经过几次迭代收敛出了最终解;右图
,学习率太高,算法发散,直接跳过了数据区域,且每次都离实际解决方案越来越远;
可以通过网格搜索
找到合适的学习率,但需要限制迭代次数
从而淘汰掉那些收敛耗时太长的模型;
-
限制迭代次数
的简单办法是,在开始时设置一个非常大的迭代次数,当梯度向量的值变得非常微小时中断;即当梯度向量的范数变得低于容差时,梯度下降到了几乎最小值; -
收敛速度
,若成本函数为凸函数,且斜率没有陡峭变化(如 MSE 的成本函数),则具有固定学习率的批量梯度下降最终会收敛到最佳解;若将容差缩小到原来的 1/10 以得到更精确的解,算法将不得不运行 10 倍的时间;
2. 随机梯度下降
随机梯度下降
,与使用整个训练集计算每一步的梯度,算法特别慢的批量梯度下降相对,随机梯度下降在每一步梯度计算时,在训练集随机选择一个实例计算梯度,算法很快;可用于海量数据集的训练,每次迭代只需要在内存中运行一个实例(SGD 可以作为核外算法实现);
随机梯度下降的随机性质让它比批量梯度下降要不规则得多;成本函数不再是持续下降至最下值,而是存在上下波动,但整体上 会慢慢下降,最终接近最小值(即时达到最小值依旧会反弹,该算法的参数值只能是足够好,不会是最优);
-
逃离局部最优
,成本函数非常不规则时,随机梯度下降可以跳出局部最小值,相比批量梯度下降,它更能找到全局最小值; -
模拟退火
,逐步降低学习率,开始的步长较大,快速进展和逃离局部最小值,然后越来越小,让算法尽量靠近全局最小值; -
学习率调度
,确认每个迭代学习率的函数;
学习率降得太快可能陷入局部最小值,学习率降得太慢可能需要太长时间走到最小值的附近,提前结束训练可能导致得到一个次优的解决方案;
学习率调度实现随机梯度下降
theta_path_sgd = []
m = len(X_b)
np.random.seed(42)
n_epochs = 50
t0, t1 = 5, 50 # learning schedule hyperparameters
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):
if epoch == 0 and i < 20:
y_predict = X_new_b.dot(theta)
style = "b-" if i > 0 else "r--"
plt.plot(X_new, y_predict, style)
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
theta_path_sgd.append(theta)
plt.plot(X, y, "b.")
plt.xlabel("$x_1$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.axis([0, 2, 0, 15])
plt.show()
随机选取事例,某些实例可能每个轮次被选中多次,有的实例则可能不会被选中;
IID
,独立且均匀分布
,在训练过程中对实例进行随机混洗;使用随机梯度下降时,训练实例必须独立且均匀分布,确保平均而言将参数拉向全局最优值;
>>> print(theta)
[[4.21076011]
[2.74856079]]
使用 SGDRegressor 执行线性回归
from sklearn.linear_model import SGDRegressor
sgd_reg = SGDRegressor(max_iter=1000, tol=1e-3, penalty=None, eta0=0.1)
sgd_reg.fit(X, y.ravel())
print(sgd_reg.intercept_, sgd_reg.coef_)
[4.22520079] [2.79873691]
max_iter
,最多可运行的轮次数;tol
,轮次期间损失下降小于该值将停止训练;eta0
,起始学习率;
3. 小批量梯度下降
小批量梯度下降
,相比于基于完整训练集(如批量梯度下降)和基于一个实例(如随机梯度下降)来计算梯度,小批量梯度下降在小型批量的随机实例集上计算梯度;可以通过矩阵操作的硬件优化(如 GPU)来提高性能;
小批量梯度下降比随机梯度下降在参数空间上的进展更稳定,且最终更接近最小值,但可能很难摆脱局部最小值(局部最小值影响情况下不像线性回归);而批量梯度下降最终会实际停留在最小值,只是批量梯度下降每一步需要花费很多时间;好的处理方式是使用良好的学习率调度
让随机梯度下降和小批量梯度下降尽可能达到最小值;
线性回归算法的比较
算法 | m 很大 | 核外支持 | n 很大 | 超参数 | 要求缩放 | Scikit-Learn |
---|---|---|---|---|---|---|
标准方程 | 快 | 否 | 慢 | 0 | 否 | N/A |
SVD | 快 | 否 | 慢 | 0 | 否 | LinearRegression |
批量 GD | 慢 | 否 | 快 | 2 | 是 | SGDRegressor |
随机 GD | 快 | 是 | 快 | >= 2 | 是 | SGDRegressor |
小批量 GD | 快 | 是 | 快 | >= 2 | 是 | SGDRegressor |
所有这些算法最终都具有非常相似的模型,且以完全相同的方式进行预测;
3. 多项式回归
多项式回归
,使用线性模型来拟合非线性模型;比如将每个特征的幂次方添加为一个新特征,然后在此扩展特征集上训练一个线性模型;
多项式回归示例:
在二次方程 的基础上添加一些噪声;
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)
plt.plot(X, y, "b.")
plt.xlabel("$x_1$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.axis([-3, 3, 0, 10])
plt.show()
使用 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] # 包含 X 的原始特征以及该特征的平方
array([-0.75275929, 0.56664654])
将 LinearRegression 模型拟合到该扩展训练数据中;
>>> lin_reg = LinearRegression()
>>> lin_reg.fit(X_poly, y)
>>> lin_reg.intercept_, lin_reg.coef_
(array([1.78134581]), array([[0.93366893, 0.56456263]]))
即模型估算:(与原始函数相符: )
多项式回归曲线
X_new = np.linspace(-3, 3, 100).reshape(100, 1)
X_new_poly = poly_features.transform(X_new)
y_new = lin_reg.predict(X_new_poly)
plt.plot(X, y, "b.")
plt.plot(X_new, y_new, "r-", linewidth=2, label="Predictions")
plt.xlabel("$x_1$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.legend(loc="upper left", fontsize=14)
plt.axis([-3, 3, 0, 10])
plt.show()
存在多个特征时,多项式回归能够找到特征之间的关系;PolynomialFeatures 可以将特征的所有组合添加到给定的多项式阶数(如存在两个特征 a 和 b,degress=3 的 PolynomialFeatures 不仅会添加特征 ,还会添加组合 );
PolynomialFeatures(degree=d)可以将一个包含 n 个特征的数组转换为包含 个特征的数组;因此要小心特征组合的数量爆炸;
4. 学习曲线
高阶多项式回归的拟合曲线
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
for style, width, degree in (("g-", 1, 300), ("b--", 2, 2), ("r-+", 2, 1)):
polybig_features = PolynomialFeatures(degree=degree, include_bias=False)
std_scaler = StandardScaler()
lin_reg = LinearRegression()
polynomial_regression = Pipeline([
("poly_features", polybig_features),
("std_scaler", std_scaler),
("lin_reg", lin_reg),
])
polynomial_regression.fit(X, y)
y_newbig = polynomial_regression.predict(X_new)
plt.plot(X_new, y_newbig, style, label=str(degree), linewidth=width)
plt.plot(X, y, "b.", linewidth=3)
plt.legend(loc="upper left")
plt.xlabel("$x_1$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.axis([-3, 3, 0, 10])
plt.show()
高阶多项式回归模型严重过拟合训练数据,而线性模型欠拟合,二次模型最能泛化数据集(数据就是使用二次模型生成的);
评估模型泛化性能的方法
交叉验证
- 若模型在训练数据上表现良好,但交叉验证的指标泛化性能较差,则模型过拟合;
- 若模型在训练数据和交叉验证表现都较差,则说明欠拟合;
学习曲线
:绘制模型在训练集和验证集上关于训练集大小(或训练迭代)的性能函数;在不同大小的训练子集上多次训练模型,观察期分别在训练集和验证集上的性能表现;
学习曲线函数
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, random_state=10)
train_errors, val_errors = [], []
for m in range(1, len(X_train) + 1):
model.fit(X_train[:m], y_train[:m])
y_train_predict = model.predict(X_train[:m])
y_val_predict = model.predict(X_val)
train_errors.append(mean_squared_error(y_train[:m], y_train_predict))
val_errors.append(mean_squared_error(y_val, y_val_predict))
plt.plot(np.sqrt(train_errors), "r-+", linewidth=2, label="train")
plt.plot(np.sqrt(val_errors), "b-", linewidth=3, label="val")
plt.legend(loc="upper right", fontsize=14)
plt.xlabel("Training set size", fontsize=14)
plt.ylabel("RMSE", fontsize=14)
分别截取前 1、2、... m 个实例作为训练集进行训练,绘制其训练集和测试集上的性能效果;
普通线性回归模型的学习曲线
lin_reg = LinearRegression()
plot_learning_curves(lin_reg, X, y)
plt.axis([0, 80, 0, 3])
plt.show()