之前用了那么多的机器学习的模型,但是它们各自的训练算法在很大程度上还是一个黑匣子,我们对对系统内部的实现机制一无所知。所以本章就要研究下这些厉害的机器学习模型内部究竟是如何运作的呢?
线性模型就是对输入特征加权求和,再加上一个我们称为偏置项(也称为截距项)的常数,以此进行预测。
公式1-1:线性回归模型预测
y ^ \hat{y} y^是与预测值
n是特征数量
x i x_i xi是第i个特征值
θ j \theta_j θj是第j个模型参数(包括 θ 0 , θ 1 , . . . , θ n \theta_0,\theta_1,...,\theta_n θ0,θ1,...,θn)
可以用更加简洁的向量化形式表示:
公式1-2:线性回归模型预测(向量化)
θ \theta θ是模型的参数向量
θ T \theta^T θT是 θ \theta θ的转置向量(为行向量,不再是列向量)
X是实例的特征向量,包括从 x 0 x_0 x0到 x n x_n xn, x 0 x_0 x0永远为1
θ T ⋅ X \theta^T\cdot X θT⋅X是 θ T \theta^T θT和X的点积
我们该怎样训练线性回归模型呢?回想一下,训练模型就是设置模型参数直到模型最适应训练集的过程。要达到这个目的,我们首先需要知道怎么衡量模型对训练数据的拟合程度是好还是差。在第2章中,我们了解到回归模型最常见的性能指标是均方根误差(RMSE)。因此,在训练线性回归模型时,你需要找到最小化RMSE的θ值。在实践中,将均方误差(MSE)最小化比最小化RMSE更为简单,二者效果相同(因为使函数最小化的值,同样也使其平方根最小)
公式1-3:线性回归模型的MSE成本函数
为了得到使成本函数最小的θ值,有一个闭式解方法——也就是一个直接得出结果的数学方程,即标准方程
公式1-4:标准方程
θ ^ = ( X T ⋅ X ) ⋅ X T ⋅ y \hat{\theta}=(X^T\cdot X)\cdot X^T\cdot y θ^=(XT⋅X)⋅XT⋅y
以下例子随机生成一些数据来进行测试标准方程的公式:
import matplotlib.pyplot as plt
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()
# 标准方程来计算
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)
# 实际用来生成数据的函数是y=4+3x 0 +高斯噪声
print(theta_best)
print("----------------------------------------------")
# 预测
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)
print("----------------------------------------------")
plt.plot(X_new, y_predict, "r-")
plt.plot(X, y, "b.")
plt.axis([0, 2, 0, 15])
# plt.show()
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(X, y)
print(lin_reg.intercept_)
print(lin_reg.coef_)
标准方程求逆的矩阵 X T ⋅ X X^T\cdot X XT⋅X,是一个n×n矩阵(n是特征数量)。对这种矩阵求逆的计算复杂度通常为O(n2.4)到O(n 3 )之间(取决于计算实现)。
梯度下降是一种非常通用的优化算法,能够为大范围的问题找到最优解。梯度下降的中心思想就是迭代地调整参数从而使成本函数最小化。
假设你迷失在山上的浓雾之中,你能感觉到的只有你脚下路面的坡度。快速到达山脚的一个策略就是沿着最陡的方向下坡。这就是梯度下降的做法:通过测量参数向量θ相关的误差函数的局部梯度,并不断沿着降低梯度的方向调整,直到梯度降为0,到达最小值.
具体来说,首先使用一个随机的θ值(这被称为随机初始化),然后逐步改进,每次踏出一步,每一步都尝试降低一点成本函数(如MSE),直到算法收敛出一个最小值.如下图所示:
梯度下降中一个重要参数是每一步的步长,这取决于超参数学习率。如果学习率太低,算法需要经过大量迭代才能收敛,这将耗费很长时间。
学习率太低:
学习率太高:
最后,并不是所有的成本函数看起来都像一个漂亮的碗。有的可能看着像洞、像山脉、像高原或者是各种不规则的地形,导致很难收敛到最小值。
下图显示了梯度下降的两个主要挑战:如果随机初始化,算法从左侧起步,那么会收敛到一个局部最小值,而不是全局最小值。如果算法从右侧起步,那么需要经过很长时间才能越过整片高原如果你停下得太早,将永远达不到全局最小值。
要实现梯度下降,你需要计算每个模型关于参数 θ j \theta_j θj的成本函数的梯度。换言之,你需要计算的是如果改变 θ j \theta_j θj,成本函数会改变多少。
这被称为偏导数。这就好比是在问“如果我面向东,我脚下的坡度斜率是多少?”然后面向北问同样的问题(如果你想象超过三个维度的宇宙,对于其他的维度以此类推)
公式1-5:成本函数的偏导数
如果不想单独计算这些梯度,可以使用公式4-6对其进行一次性计算。梯度向量,记做: ∇ θ \nabla_\theta ∇θMSE( θ \theta θ)包含所有成本函数(每个模型
参数一个)的偏导数
公式1-6:成本函数的梯度向量
公式1-6在计算梯度下降的每一步时,都是基于完整的训练集X的。这就是为什么该算法会被称为批量梯度下降:每一
步都使用整批训练数据。因此,面对非常庞大的训练集时,算法会变得极(不过我们即将看到快得多的梯度下降法)。但是,梯度下降算法随特征数量扩展的表现比较好:如果要训练的线性模型拥有几十万个特征,使用梯度下降比标准方程要快得多。
旦有了梯度向量,哪个点向上,就朝反方向下坡。也就是从 θ \theta θ中减去 ∇ θ \nabla_\theta ∇θMSE( θ \theta θ)
这时学习率 η \eta η就发挥作用了:用梯度向量乘以 η \eta η确定下坡步长的大小
公式1-6:梯度下降步长
看例子:
import numpy as np
from sklearn.linear_model import LinearRegression
# 随机生成一些线性数据
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)
# 实际用来生成数据的函数是y=4+3x 0 +高斯噪声
print(theta_best)
print("----------------------------------------------")
lin_reg = LinearRegression()
lin_reg.fit(X, y)
print(lin_reg.intercept_)
print(lin_reg.coef_)
print("----------------------------------------------")
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 * gradients
print(theta)
print("----------------------------------------------")
批量梯度下降的主要问题是它要用整个训练集来计算每一步的梯度,所以训练集很大时,算法会特别慢。与之相反的极端是随机梯度
下降,每一步在训练集中随机选择一个实例,并且仅基于该单个实例
来计算梯度。显然,这让算法变得快多了,因为每个迭代都只需要操
作少量的数据。它也可以被用来训练海量的数据集,因为每次迭代只
需要在内存中运行一个实例即可
例子:
import numpy as np
from sklearn.linear_model import LinearRegression
# 随机生成一些线性数据
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)
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
m = len(X_b)
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)
print("----------------------------------------------")
lin_reg = LinearRegression()
lin_reg.fit(X, y)
print(lin_reg.intercept_)
print(lin_reg.coef_)
print("----------------------------------------------")
要了解的最后一个梯度下降算法叫作小批量梯度下降。一旦
理解了批量梯度下降和随机梯度下降,这个算法就非常容易理解了:
每一步的梯度计算,既不是基于整个训练集(如批量梯度下降)也不
是基于单个实例(如随机梯度下降),而是基于一小部分随机的实例
集也就是小批量。相比随机梯度下降,小批量梯度下降的主要优势在
于可以从矩阵运算的硬件优化中获得显著的性能提升,特别是需要用
到图形处理器时。
如果数据比简单的直线更为复杂,该怎么办?令人意想不到的
是,其实你也可以用线性模型来拟合非线性数据。一个简单的方法就
是将每个特征的幂次方添加为一个新特征,然后在这个拓展过的特征
集上训练线性模型。这种方法被称为多项式回归。
例子:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
# 随机生成一个二次方程
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()
# 直线永远不可能拟合这个数据
# 使用Scikit-Learn
# 的PolynomialFeatures类来对训练数据进行转换,将每个特征的平方(二次多项式)作为新特征加入训练集(这个例子中只有一个特
# 征)
poly_features = PolynomialFeatures(degree=2, include_bias=False)
X_poly = poly_features.fit_transform(X)
print(X[0])
print("----------------------------------------------")
print(X_poly[0])
print("----------------------------------------------")
# X_poly现在包含原本的特征X和该特征的平方。现在对这个扩展
# 后的训练集匹配一个LinearRegression模型
lin_reg = LinearRegression()
lin_reg.fit(X_poly, y)
print(lin_reg.intercept_)
print(lin_reg.coef_)
例子:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.pipeline import Pipeline
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)
X_new = np.linspace(-3, 3, 100).reshape(100, 1)
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.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
import numpy as np
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)
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)):
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_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.legend(loc="upper right", fontsize=14) # not shown in the book
plt.xlabel("Training set size", fontsize=14) # not shown
plt.ylabel("RMSE", fontsize=14) # not shown
# 首先,我们来看训练数据上的性能:当训练
# 集中只包括一两个实例时,模型可以完美拟合,这是为什么曲线是从
# 0开始的。但是,随着新的实例被添加进训练集中,模型不再能完美
# 拟合训练数据了,因为数据有噪声,并且根本就不是线性的。所以训
# 练集的误差一路上升,直到抵达一个高地,从这一点开始,添加新实
# 例到训练集中不再使平均误差上升或下降。然后我们再来看看验证集
# 的性能表现。当训练集实例非常少时,模型不能很好地泛化,这是为
# 什么验证集误差的值一开始非常大,随着模型经历更多的训练数据,
# 它开始学习,因此验证集误差慢慢下降。但是仅靠一条直线终归不能
# 很好地为数据建模,所以误差也停留在了一个高值,跟另一条曲线十
# 分接近。
lin_reg = LinearRegression()
plot_learning_curves(lin_reg, X, y)
plt.axis([0, 80, 0, 3]) # not shown in the book
plt.show()
如果你的模型对训练数据拟合不足,添加更多训练示例也于事无补。你需要使用更复杂的模型或者找到更好的特征。
改进模型过度拟合的方法之一是提供更多的训练数据,直到验证误差接近训练误差。
减少过度拟合的一个好办法就是对模型正则化(即约束它):它拥有的自由度越低,就越不容易过度拟合数据。比如,将多项式模型正则化的简单方法就是降低多项式的阶数。
对线性模型来说,正则化通常通过约束模型的权重来实现。
岭回归(也叫作吉洪诺夫正则化)是线性回归的正则化版:在成本函数中添加一个等于 α ∑ i = 1 n θ i 2 \alpha\sum_{i=1}^n\theta^2_i α∑i=1nθi2的正则项。这使得学习中的算法不仅需要拟合数据,同时还要让模型权重保持最小。注意,正则项只能在训练的时候添加到成本函数中,一旦训练完成,你需要使用未经正则化的性能指标来评估模型性能。
超参数 α \alpha α控制的是对模型进行正则化的程度。如果 α \alpha α=0,则岭回归就是线性模型。如果 α \alpha α非常大,那么所有的权重都将非常接近于零,结果是一条穿过数据平均值的水平线。
公式1-7:岭回归成本函数
在执行岭回归之前,必须对数据进行缩放(例如使用StandardScaler),因为它对输入特征的大小非常敏感。大多数正则化模型都是如此。
公式1-8:闭式解的岭回归
例子:
import numpy as np
from sklearn.linear_model import Ridge, SGDRegressor
np.random.seed(42)
m = 20
X = 3 * np.random.rand(m, 1)
y = 1 + 0.5 * X + np.random.randn(m, 1) / 1.5
X_new = np.linspace(0, 3, 100).reshape(100, 1)
ridge_reg = Ridge(alpha=1, solver="cholesky", random_state=42)
ridge_reg.fit(X, y)
print(ridge_reg.predict([[1.5]]))
print("----------------------------------------------")
# 使用随机梯度下降:
# 超参数penalty设置的是使用正则项的类型。设为"l2"表示希望
# SGD在成本函数中添加一个正则项,等于权重向量的l 2 范数的平方的
# 一半,即岭回归
sgd_reg = SGDRegressor(penalty="l2", random_state=42)
sgd_reg.fit(X, y.ravel())
print(sgd_reg.predict([[1.5]]))
线性回归的另一种正则化,叫作最小绝对收缩和选择算子回归(Least Absolute Shrinkage and Selection Operator Regression,简称Lasso回归,或套索回归)。与岭回归一样,它也是向成本函数增加一个正则项,但是它增加的是权重向量的 L 1 L_1 L1范数,而不是 L 2 L_2 L2范数的平方的一半.
公式1-9:Lasso回归成本函数
一些回归算法也可用于分类(反之亦
然)。逻辑回归(Logistic回归,也称为罗吉思回归)被广泛用于估算一个实例属于某个特定类别的概率。(比如这封电子邮件属于垃圾邮件的概率是少?)如果预估概率超过50%,则模型预测该实例属于该类别(称为正类,标记为“1”),反之,则预测不是(也就是负类,标记为“0”)。这样它就成了一个二元分类器。
公式1-10:逻辑回归模型概率估算
逻辑模型(也称为罗吉特),是一个sigmoid函数(即S形),记作σ(·),它的输出为一个0到1之间的数字。
公式1-11:逻辑函数
一旦逻辑回归模型估算出实例x属于正类的概率 p ^ = h θ ( x ) \hat{p} =h_\theta(x) p^=hθ(x),就可以轻松做出预测 y ^ \hat{y} y^
公式1-12:逻辑回归模型预测
y ^ = { 0 , if p ^ < 0.5 1 , if p ^ >= 0.5 \hat{y} = \begin{cases} 0, & \text{if $\hat{p}$ < 0.5} \\ 1, & \text{if $\hat{p}$ >= 0.5} \end{cases} y^={0,1,if p^ < 0.5if p^ >= 0.5
公式1-13:单个训练实例的成本函数
c ( θ ) = { − l o g ( p ^ ) , (y=1) − l o g ( 1 − p ^ ) , (y=0) c(\theta) = \begin{cases} -log(\hat{p}), & \text{(y=1)} \\ -log(1-\hat{p}), & \text{(y=0)} \end{cases} c(θ)={−log(p^),−log(1−p^),(y=1)(y=0)
公式1-14:逻辑回归成本函数(log损失函数)
坏消息是,这个函数没有已知的闭式方程(不存在一个标准方程的等价方程)来计算出最小化成本函数的θ值。而好消息是,这是个凸函数,所以通过梯度下降(或是其他任意优化算法)保证能够
找出全局最小值(只要学习率不是太高,你又能长时间等待)。
公式1-15:Logistic成本函数的偏导数