概括地说,线性模型就是对输入特征加权求和,再加上一个偏置项(也称为截距项)的常数,以此进行预测。
线性回归模型预测公式:
y ^ = θ 0 + θ 1 x 1 + θ 2 x 2 + ⋯ + θ n x n \hat{y}=\theta_0+\theta_1x_1+\theta_2x_2+\cdots+\theta_nx_n y^=θ0+θ1x1+θ2x2+⋯+θnxn
这可以用更为简洁的向量化形式表达:
y ^ = h θ ( X ) = θ T ⋅ X \hat{y}=h_\theta(X)=\theta^T\cdot X y^=hθ(X)=θT⋅X
训练模型就是设置模型参数直到模型最适应训练集的过程。要达到这个目的,我们首先需要知道怎么衡量模型对训练数据的拟合程度是好是差。回归模型最常见的性能指标是均方根误差(RMSE)。因此,在训练线性回归模型时,我们需要找到最小化RMSE的 θ \theta θ值。在实践中,将均方误差(MSE)最小化比最小化RMSE更为简单,二者效果相同(因为使函数最小化的值,同样也使其平方根最小)。
线性回归模型的MSE成本函数:
M S E ( X , h θ ) = 1 m ∑ i = 1 m ( θ T ⋅ X ( i ) − y ( i ) ) 2 MSE(X,h_\theta)=\frac1m\sum^m_{i=1}(\theta^T\cdot X^{(i)}-y^{(i)})^2 MSE(X,hθ)=m1i=1∑m(θT⋅X(i)−y(i))2
为了得到使成本函数最小的 θ \theta θ值,有一个闭式解方法——也就是一个直接得出结果的数学方程,即标准方程:
θ ^ = ( X T ⋅ X ) − 1 ⋅ X T ⋅ y \hat{\theta}=(X^T\cdot X)^{-1}\cdot X^T\cdot y θ^=(XT⋅X)−1⋅XT⋅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.axis([0,2,0,15])
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)
我们实际用来生成数据的函数是 y = 4 + 3 x 0 + 高 斯 噪 声 y=4+3x_0+高斯噪声 y=4+3x0+高斯噪声。来看看公式的结果:
theta_best
array([[ 4.0929349 ],
[ 3.00811997]])
我们期待的是 θ 0 = 4 , θ 1 = 3 \theta_0=4,\theta_1=3 θ0=4,θ1=3得到的是 θ 0 = 4.093 , θ 1 = 3.008 \theta_0=4.093,\theta_1=3.008 θ0=4.093,θ1=3.008。非常接近,噪声的存在使其不可能完全还原为原本的函数。
现在可以用 θ ^ \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.0929349 ],
[ 10.10917484]])
绘制模型的预测结果:
plt.plot(X,y,'b.')
plt.plot(X_new,y_predict,'r-')
plt.axis([0,2,0,15])
plt.show()
Scikit-Learn的等效代码如下所示:
from sklearn.linear_model import LinearRegression
lin_reg=LinearRegression()
lin_reg.fit(X,y)
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=False)
Scikit-Learn将偏置项(intercept_)和特征权重(coef_)分离开:
lin_reg.intercept_,lin_reg.coef_
(array([ 4.0929349]), array([[ 3.00811997]]))
预测结果也是一样的:
X_new=np.array([[0],[2]])
y_predict=lin_reg.predict(X_new)
y_predict
array([[ 4.0929349 ],
[ 10.10917484]])
标准方程求逆的矩阵 X T ⋅ X X^T\cdot X XT⋅X,是一个nXn矩阵(n是特征数量)。对这种矩阵求逆的计算复杂度通常为 O ( n 2.4 ) O(n^{2.4}) O(n2.4)到 O ( n 3 ) O(n^3) O(n3)之间(取决于计算实现)。换句话说,如果将特征数量翻倍,那么计算时间将乘以大约 2 2.4 = 5.3 2^{2.4}=5.3 22.4=5.3倍到 2 3 = 8 2^3=8 23=8倍之间。
特征数量比较大(例如100000)时,标准方程的计算将极其缓慢。
好的一面是,相对于训练集中的实例数量( O ( m ) O(m) O(m))来说,方程是线性的,所以能够有效地处理大量的训练集,只要内存足够。
同样,线性回归模型一经训练(不论是标准方程还是其他算法),预测就非常快速:因为计算复杂度相对于想要预测的实例数量和特征数量来说,都是线性的。换句话说,对两倍的实例(或者是两倍的特征数)进行预测,大概需要两倍的时间。
梯度下降是一种非常通用的优化算法,能够为大范围的问题找到最优解。梯度下降的中心思想就是迭代地调整参数从而使成本函数最小化:通过测量参数向量 θ \theta θ相关的误差函数的局部梯度,并不断沿着降低梯度的方向调整,直到梯度降为0,到达最小值!
具体来说,首先使用一个随机的 θ \theta θ值(这被称为随机初始化),然后逐步改进,每次踏出一步,每一步都尝试降低一点成本函数(如MSE),直到算法收敛出一个最小值。
梯度下降中一个重要参数是每一步的步长,这取决于超参数学习率。如果学习率太低,算法需要经过大量迭代才能收敛,这将耗费很长时间。反之,如果学习率太高,会导致算法发散,值越来越大,最后无法找到好的解决方案。
梯度下降的两个主要挑战是:如果随机初始化不好,一种可能是算法会收敛到一个局部最小值,而不是全局最小值。另一种可能是算法需要经过很长时间的迭代过程,如果我们停下得太早,将永远达不到全局最小值。
幸好,线性回归模型的MSE成本函数恰好是个凸函数,这意味着连接曲线上任意两个点的线段永远不会跟曲线相交。也就是说不存在局部最小,只有一个全局最小值。它同时也是一个连续函数,所以斜率不会产生陡峭的变化。这两件事保证的结论是:即便是乱走,梯度下降都可以趋近到全局最小值(只要等待时间足够长,学习率也不是太高)。
要实现梯度下降,我们需要计算每个模型关于参数 θ j \theta_j θj的成本函数的梯度。换言之,我们需要计算的是如果改变 θ j \theta_j θj,成本函数会改变多少。这被称为偏导数。
关于参数 θ j \theta_j θj的成本函数的偏导数:
δ δ θ j M S E ( θ ) = 2 m ∑ i = 1 m ( θ T ⋅ X ( i ) − y ( i ) ) x j ( i ) \frac{\delta}{\delta\theta_j}MSE(\theta)=\frac2m\sum^m_{i=1}(\theta^T\cdot X^{(i)}-y^{(i)})x_j^{(i)} δθjδMSE(θ)=m2i=1∑m(θT⋅X(i)−y(i))xj(i)
如果不想单独计算这些梯度,可以使用成本函数的梯度向量计算公式对其进行一次性计算。梯度向量,记作 ∇ θ M S E ( θ ) \nabla_\theta MSE(\theta) ∇θMSE(θ),包含所有成本函数(每个模型参数一个)的偏导数:
∇ θ M S E ( θ ) = 2 m X T ⋅ ( X ⋅ θ − y ) \nabla_\theta MSE(\theta)=\frac2mX^T\cdot (X\cdot \theta-y) ∇θMSE(θ)=m2XT⋅(X⋅θ−y)
公式在计算梯度下降每一步时,都是基于完整的训练集X。这就是为什么该算法会被称为批量梯度下降:每一步都使用整批训练数据。因此,面对非常庞大的训练集时,算法会变得极慢。但是,梯度下降算法随特征数量扩展的表现比较好:如果要训练的线性模型拥有几十万个特征,使用梯度下降比标准方程要快得多。
一旦有了梯度向量,用梯度向量乘以学习率 η \eta η就可以确定下坡步长的大小:
θ ( n e x t s t e p ) = θ − η ∇ θ M S E ( θ ) \theta^{(next\;step)}=\theta-\eta\nabla_\theta MSE(\theta) θ(nextstep)=θ−η∇θMSE(θ)
批量梯度下降算法的快速实现:
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
代码不是太难,看看产生的结果theta:
theta
array([[ 4.0929349 ],
[ 3.00811997]])
与标准方程的结果完全一致,梯度下降表现完美。但如果使用了其他的学习率eta,算法可能不会表现得这么完美。要找到合适的学习率,可以使用网格搜索(GridSearchCV)。但是我们可能需要限制迭代次数,这样网格搜索可以淘汰掉那些收敛耗时太长的模型。
要怎么限制迭代次数呢?如果设置太低,算法可能在离最优解还很远时就停了;但是如果设置得太高,模型达到最优解后,继续迭代参数不再变化,又会浪费时间。一个简单的办法是,在开始时设置一个非常大的迭代次数,但是当梯度向量的值变得很微小时中断算法——也就是当它的范数变得低于 ε \varepsilon ε(容差)时,因为这时梯度下降已经几乎到达了最小值。
批量梯度下降的主要问题是它要用整个训练集来计算每一步的梯度,所以训练集很大时,算法会特别慢。与之相反的极端是随机梯度下降,每一步在训练集中随机选择一个实例,并且仅基于该单个实例来计算梯度。显然,这让算法变得快多了,因为每个迭代都只需要操作少量的数据。它也可以被用来训练海量的数据集,因为每次迭代只需要在内存中运行一个实例即可。
另一方面,由于算法的随机性质,它比批量梯度下降要不规则得多。成本函数将不再是缓慢降低直到抵达最小值,而是不断上上下下,但是从整体来看,还是在慢慢下降。随着时间推移,最终会非常接近最小值,但是即使它到达了最小值,依旧还会持续反弹,永远不会停止。所以算法停下来的参数值肯定是足够好的,但不是最优的。
随机性的好处在于可以逃离局部最优,但缺点是永远定位不出最小值。要解决这个困境,有一个办法是逐步降低学习率。开始的步长比较大(这有助于快速进展和逃离局部最小值),然后越来越小,让算法尽量靠近全局最小值。这个过程叫作模拟退火,因为它类似于冶金时熔化的金属慢慢冷却的退火过程。确定每个迭代学习率的函数叫作学习计划。如果学习率降得太快,可能会陷入局部最小值,甚至是停留在走向最小值的半途中。如果学习率降得太慢,则需要太长时间才能跳到差不多最小值附近,如果提早结束训练,可能只得到一个次优的解决方案。
下面这段代码使用了一个简单的学习计划实现随机梯度下降:
n_epochs=50
t0,t1=5,50 #学习计划超参数
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*gradients
前面的批量梯度下降需要在整个训练集上迭代1000次,而这段代码只迭代了50次就得到了一个相当不错的解:
theta
array([[ 4.07034981],
[ 3.01321888]])
训练过程的前10步(虚线表示起点):
在Scikit-Learn里,用SGD执行线性回归可以使用SGDRegression类,其默认优化的成本函数是平方误差。下面这段代码从学习率0.1开始(eta0=0.1),使用默认的学习计划(跟前面的学习计划不同)运行了50轮,而且没有使用任何正则化(penalty=None):
from sklearn.linear_model import SGDRegressor
sgd_reg=SGDRegressor(n_iter=50,penalty=None,eta0=0.1)
sgd_reg.fit(X,y.ravel())
SGDRegressor(alpha=0.0001, average=False, epsilon=0.1, eta0=0.1,
fit_intercept=True, l1_ratio=0.15, learning_rate='invscaling',
loss='squared_loss', max_iter=None, n_iter=50, penalty=None,
power_t=0.25, random_state=None, shuffle=True, tol=None, verbose=0,
warm_start=False)
我们再次得到了一个跟标准方程的解非常相近的解决方案:
sgd_reg.intercept_,sgd_reg.coef_
(array([ 4.07553058]), array([ 3.00980982]))
一旦理解了批量梯度下降和随机梯度下降,小批量梯度下降算法就非常容易理解了:每一步的梯度计算,既不是基于整个训练集(如批量梯度下降)也不是基于单个实例(如随机梯度下降),而是基于一小部分随机的实例集也就是小批量。相比随机梯度下降,小批量梯度下降的主要优势在于可以从矩阵运算的硬件优化中获得显著的性能提升,特别是需要用到图形处理器时。
这个算法在参数空间层面的前进过程也不像SGD那样不稳定,特别是批量较大时。所以小批量梯度下降最终会比SGD更接近最小值一些。但是另一方面,它可能更难从局部最小值中逃脱。
如果数据比简单的直线更为复杂,该怎么办?令人意想不到的是,其实我们也可以用线性模型来拟合非线性数据。一个简单的方法就是将每个特征的幂次方添加为一个新特征,然后在这个拓展过的特征集上训练线性模型。这种方法被称为多项式回归。
首先,基于简单的二次方程制造一些非线性数据(添加随机噪声):
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,'m.')
plt.axis([-3,3,0,10])
plt.show()
显然,直线永远不可能拟合这个数据。所以我们使用Scikit-Learn的PolynomialFeatures类来对训练数据进行转换,将每个特征的平方(二次多项式)作为新特征加入训练集(这个例子中只有一个特征):
from sklearn.preprocessing import PolynomialFeatures
poly_features=PolynomialFeatures(degree=2,include_bias=False)
X_poly=poly_features.fit_transform(X)
X_poly
array([[ -1.55313385e+00, 2.41222476e+00],
[ 2.26937536e+00, 5.15006453e+00],
[ -2.89199723e+00, 8.36364798e+00],
[ 1.55085644e+00, 2.40515569e+00],
[ -2.66421494e+00, 7.09804125e+00],
[ -2.99880861e+00, 8.99285311e+00],
[ -1.70687637e+00, 2.91342695e+00],
[ 1.45709972e+00, 2.12313958e+00],
[ -1.05377654e+00, 1.11044499e+00],
[ 1.51733338e+00, 2.30230057e+00],
[ 1.70881718e+00, 2.92005615e+00],
[ -1.38765055e+00, 1.92557405e+00],
[ 7.30097359e-01, 5.33042153e-01],
[ 2.89995823e+00, 8.40975774e+00],
[ -2.85713000e+00, 8.16319184e+00],
[ -1.79953189e+00, 3.23831502e+00],
[ 2.42766083e+00, 5.89353709e+00],
[ 1.50320219e+00, 2.25961683e+00],
[ 2.72161325e+00, 7.40717866e+00],
[ 8.50566387e-01, 7.23463179e-01],
[ 1.00878308e+00, 1.01764330e+00],
[ 7.16892551e-01, 5.13934930e-01],
[ 9.18762249e-01, 8.44124071e-01],
[ 2.39297456e+00, 5.72632725e+00],
[ -2.15182429e+00, 4.63034775e+00],
[ 8.60003007e-01, 7.39605171e-01],
[ -9.66919695e-01, 9.34933697e-01],
[ -2.96605114e-01, 8.79745934e-02],
[ -2.14945589e+00, 4.62016064e+00],
[ 1.88383675e+00, 3.54884091e+00],
[ -1.00302248e+00, 1.00605410e+00],
[ -1.62588277e+00, 2.64349478e+00],
[ -7.46713312e-01, 5.57580770e-01],
[ -1.06495365e+00, 1.13412627e+00],
[ -5.62994869e-01, 3.16963223e-01],
[ 2.76755553e+00, 7.65936360e+00],
[ -2.15244570e+00, 4.63302248e+00],
[ 1.74078101e+00, 3.03031854e+00],
[ -7.81461656e-01, 6.10682319e-01],
[ 2.37770233e+00, 5.65346836e+00],
[ -4.99466218e-01, 2.49466503e-01],
[ 2.41152004e+00, 5.81542891e+00],
[ 2.15580178e+00, 4.64748131e+00],
[ -2.45726564e+00, 6.03815440e+00],
[ -5.18464605e-01, 2.68805546e-01],
[ 2.38849825e+00, 5.70492389e+00],
[ -1.54238326e+00, 2.37894611e+00],
[ -1.73766682e+00, 3.01948599e+00],
[ -1.51790177e+00, 2.30402579e+00],
[ -1.15762723e+00, 1.34010080e+00],
[ -6.34584181e-01, 4.02697083e-01],
[ 1.60671637e+00, 2.58153751e+00],
[ 9.74868991e-01, 9.50369550e-01],
[ 1.92347452e+00, 3.69975421e+00],
[ -1.68903626e+00, 2.85284348e+00],
[ -2.05249104e+00, 4.21271948e+00],
[ -2.85976339e+00, 8.17824667e+00],
[ 4.24419509e-01, 1.80131920e-01],
[ 2.37807356e+00, 5.65523387e+00],
[ 1.84128950e+00, 3.39034703e+00],
[ -1.67367133e+00, 2.80117572e+00],
[ 5.41742730e-01, 2.93485185e-01],
[ -2.48276151e-01, 6.16410473e-02],
[ 2.69516149e+00, 7.26389546e+00],
[ 2.79606067e+00, 7.81795529e+00],
[ 1.51829156e+00, 2.30520925e+00],
[ 1.16107487e+00, 1.34809485e+00],
[ -7.88319237e-01, 6.21447219e-01],
[ 2.47181195e+00, 6.10985433e+00],
[ -2.69723801e+00, 7.27509286e+00],
[ 1.67410423e-01, 2.80262499e-02],
[ 1.97463507e+00, 3.89918366e+00],
[ -1.23522556e+00, 1.52578217e+00],
[ -2.33918268e+00, 5.47177562e+00],
[ 1.23635707e+00, 1.52857881e+00],
[ 7.85800389e-01, 6.17482252e-01],
[ 8.38700179e-01, 7.03417989e-01],
[ -1.49906554e+00, 2.24719748e+00],
[ 8.79324615e-02, 7.73211779e-03],
[ 6.52166917e-01, 4.25321687e-01],
[ -1.91966465e+00, 3.68511239e+00],
[ 1.74480175e+00, 3.04433316e+00],
[ 7.44510448e-01, 5.54295808e-01],
[ -2.03029713e+00, 4.12210642e+00],
[ 4.53720792e-01, 2.05862557e-01],
[ -3.41030006e-02, 1.16301465e-03],
[ 1.43885895e+00, 2.07031507e+00],
[ -9.86554672e-01, 9.73290121e-01],
[ -3.63086200e-01, 1.31831589e-01],
[ 1.13971400e+00, 1.29894801e+00],
[ -1.27603303e+00, 1.62826028e+00],
[ -2.41495989e+00, 5.83203125e+00],
[ -3.25413767e-01, 1.05894120e-01],
[ 2.62006977e+00, 6.86476560e+00],
[ -2.70899797e+00, 7.33866999e+00],
[ -2.15429577e+00, 4.64099028e+00],
[ -1.79334054e+00, 3.21607028e+00],
[ 5.83205776e-01, 3.40128977e-01],
[ 5.53139307e-01, 3.05963093e-01],
[ 1.80854460e+00, 3.27083359e+00]])
X_poly现在包含原本的特征X和该特征的平方。现在对这个扩展的训练集匹配一个LinearRegression模型:
lin_reg=LinearRegression()
lin_reg.fit(X_poly,y)
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=False)
看下模型预测结果:
lin_reg.intercept_,lin_reg.coef_
(array([ 2.23279821]), array([[ 0.95930232, 0.42162001]]))
还算不错,模型 y ^ = 0.42 x 1 2 + 0.96 x 1 + 2.23 \hat{y}=0.42x_1^2+0.96x_1+2.23 y^=0.42x12+0.96x1+2.23,而实际上原来的函数是 y = 0.5 x 1 2 + 1.0 x 1 + 2.0 + 高 斯 噪 声 y=0.5x_1^2+1.0x_1+2.0+高斯噪声 y=0.5x12+1.0x1+2.0+高斯噪声。
图形化展示:
plt.plot(X,y,'m.')
plt.plot(X_random,y_predict,'c-',label='预测结果')
plt.rcParams['font.sans-serif']=['SimHei']
plt.xlabel('x1')
plt.ylabel('y')
plt.axis([-3,3,0,10])
plt.legend()
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)
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='训练集')
plt.plot(np.sqrt(val_errors),'b-',linewidth=3,label='验证集')
plt.legend()
plt.xlabel('训练集大小')
plt.ylabel('RMSE')
plt.show()
看看纯线性回归模型(一条直线)的学习曲线:
lin_reg=LinearRegression()
plot_learning_curves(lin_reg,X,y)
这条学习曲线是典型的模型拟合不足。两条曲线均到达高地,非常接近,而且相当高。
如果模型对训练数据拟合不足,添加更多训练示例也于事无补。我们需要使用更复杂的模型或者找到更好的特征。
现在我们再来看看在同样的数据集上,一个10项多阶式模型的学习曲线:
from sklearn.pipeline import Pipeline
polynomial_regression=Pipeline((
('poly_features',PolynomialFeatures(degree=10,include_bias=False)),
('sgd_reg',LinearRegression()),
))
plt.ylim(0,3)
plot_learning_curves(polynomial_regression,X,y)
这条学习曲线看起来跟眼前一条差不多,但是有两个非常大的区别:
改进模型过度拟合的方法之一是提供更多的训练数据,直到验证误差接近训练误差。
在统计学习和机器学习领域,一个重要的理论结果是,模型的泛化误差可以被表示为三个截然不同的误差之和:
增加模型的复杂度通常会显著提升模型的方差,减少偏差。反过来,降低模型的复杂度则会提升模型的偏差,降低方差。这就是为什么称其为权衡。