机器学习笔记之正则化的线性回归的岭回归与Lasso回归

2021-01-21 06:15:36 浏览数 (1)

0x00 概述

正则化是用来防止过拟合的方法。在最开始学习机器学习的课程时,只是觉得这个方法就像某种魔法一样非常神奇的改变了模型的参数。

但是一直也无法对其基本原理有一个透彻、直观的理解。直到最近再次接触到这个概念,经过一番苦思冥想后终于有了我自己的理解。

0x01 正则化(Regularization )

使用多项式回归,如果多项式最高次项比较大,模型就容易出现过拟合。

正则化是一种常见的防止过拟合的方法,一般原理是在代价函数后面加上一个对参数的约束项,这个约束项被叫做正则化项(regularizer)。

在线性回归模型中,通常有两种不同的正则化项:

代码语言:javascript复制
# 加上所有参数(不包括θ0)的绝对值之和,即L1范数,此时叫做Lasso回归;

# 加上所有参数(不包括θ0)的平方和,即L2范数的平方,此时叫做岭回归.

看过不少关于正则化原理的解释,但是都没有获得一个比较直观的理解。

下面用代价函数的图像以及正则化项的图像来帮助解释正则化之所以起作用的原因。

1.1 代价函数的图像

为了可视化,选择直线方程进行优化。

假设一个直线方程以及代价函数如下:

该方程只有一个特征x,两个参数θ0和θ1;

该代价函数为均方误差函数(MSE),其中m表示样本量;

为了保持简单,只取一个样本点(1,1)(1,1)代入上面的代价函数方程中,可得

该式是一个二元一次方程,可以在3维空间中作图(下面利用网站GeoGebra画出该方程的图像):

图0-1,代入样本点(1,1)(1,1)后的代价函数MSE的图像

由于多个样本点的代价函数是所有样本点代价函数之和,且不同的样本点只是相当于改变了代价函数中两个变量的参数(此时θ0和θ1是变量,样本点的取值是参数)。因此多样本的代价函数MSE的图像只会在图0-1上发生缩放和平移,而不会发生过大的形变。

对于坐标轴,表示如下:

代码语言:javascript复制
# 使用J轴表示蓝色轴线,上方为正向;
# 使用θ1表示红色轴线,左边为正向;
# 使用θ0表示绿色轴线,指向屏幕外的方向为正向.

此时的函数图像相当于一条抛物线沿着平面J=0上直线θ0=−θ1平移后形成的图像。

1.2 正则化项的图像

这里使用L1范数作为正则化项,加上正则化项之后MSE代价函数变成:

上式中λ是正则化项的参数,为了简化取λ=1。

由于正则化项中始终不包含截距项θ0,此时的L1范数相当于参数θ1的绝对值,函数图像如下:

图0-2,L2正则化项的图像

此时的函数图像相当于一张对折后,半张开的纸。纸的折痕与平面J=0上θ0轴重叠。

1.3 代价函数与正则化项图像的叠加

直接将这两个图像放在一起的样子:

图0-3,同时显示代价函数与正则化项的图像

将两个方程相加之后,即

做图可以得到下面的图像:

图0-4,加入正则化项之后代价函数的图像

此时的图像,就像是一个圆锥体被捏扁了之后,立在坐标原点上。观察添加正则化项前后的图像,我们会发现:

代码语言:javascript复制
# 加上正则化项之后,此时损失函数就分成了两部分:第1项为原来的MSE函数,第2项为正则化项,最终的结果是这两部分的线性组合;

# 在第1项的值非常小但在第2项的值非常大的区域,这些值会受到正则化项的巨大影响,从而使得这些区域的值变的与正则化项近似:例如原来的损失函数沿θ0=−θ1,J轴方向上的值始终为0,但是加入正则化项J=|θ1|后,该直线上原来为0的点,都变成了θ1的绝对值。这就像加权平均值一样,哪一项的权重越大,对最终结果产生的影响也越大;

# 如果想象一种非常极端的情况:在参数的整个定义域上,第2项的取值都远远大于第一项的取值,那么最终的损失函数几乎100%都会由第2项决定,也就是整个代价函数的图像会非常类似于J=|θ1|(图0-2)而不是原来的MSE函数的图像(图0-1)。这时候就相当于λ的取值过大的情况,最终的全局最优解将会是坐标原点,这就是为什么在这种情况下最终得到的解全都为0.

0x01 岭回归

岭回归与多项式回归唯一的不同在于代价函数上的差别。岭回归的代价函数如下:

为了方便计算导数,通常也写成下面的形式:

上式中的ww是长度为n的向量,不包括截距项的系数θ0;θ是长度为n 1的向量,包括截距项的系数θ0;m为样本数;n为特征数.

岭回归的代价函数仍然是一个凸函数,因此可以利用梯度等于0的方式求得全局最优解(正规方程):

上述正规方程与一般线性回归的正规方程相比,多了一项λIλI,其中II表示单位矩阵。假如XTXXTX是一个奇异矩阵(不满秩),添加这一项后可以保证该项可逆。

由于单位矩阵的形状是对角线上为1其他地方都为0,看起来像一条山岭,因此而得名。

除了上述正规方程之外,还可以使用梯度下降的方式求解(求梯度的过程可以参考一般线性回归,3.2.2节)。这里采用式子1−21−2来求导:

因为式子1−2中和式第二项不包含θ0,因此求梯度后,上式第二项中的w本来也不包含θ0。为了计算方便,添加θ0=0到w

因此在梯度下降的过程中,参数的更新可以表示成下面的公式:

其中α为学习率,λ为正则化项的参数

1.1 数据以及相关函数

代码语言:javascript复制
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
from sklearn.metrics import mean_squared_error

data = np.array([[ -2.95507616,  10.94533252],
       [ -0.44226119,   2.96705822],
       [ -2.13294087,   6.57336839],
       [  1.84990823,   5.44244467],
       [  0.35139795,   2.83533936],
       [ -1.77443098,   5.6800407 ],
       [ -1.8657203 ,   6.34470814],
       [  1.61526823,   4.77833358],
       [ -2.38043687,   8.51887713],
       [ -1.40513866,   4.18262786]])
m = data.shape[0]  # 样本大小
X = data[:, 0].reshape(-1, 1)  # 将array转换成矩阵
y = data[:, 1].reshape(-1, 1)

继续使用多项式回归中的数据。

1.2 岭回归的手动实现

有了上面的理论基础,就可以自己实现岭回归了,下面是Python代码:

代码语言:javascript复制
# 代价函数
def L_theta(theta, X_x0, y, lamb):
    """
    lamb: lambda, the parameter of regularization
    theta: (n 1)·1 matrix, contains the parameter of x0=1
    X_x0: m·(n 1) matrix, plus x0
    """
    h = np.dot(X_x0, theta)  # np.dot 表示矩阵乘法
    theta_without_t0 = theta[1:]
    L_theta = 0.5 * mean_squared_error(h, y)   0.5 * lamb * np.sum(np.square(theta_without_t0))
    return L_theta

# 梯度下降
def GD(lamb, X_x0, theta, y, alpha):
    """
    lamb: lambda, the parameter of regularization
    alpha: learning rate
    X_x0: m·(n 1), plus x0
    theta: (n 1)·1 matrix, contains the parameter of x0=1
    """
    for i in range(T):
        h = np.dot(X_x0, theta)
        theta_with_t0_0 = np.r_[np.zeros([1, 1]), theta[1:]]  # set theta[0] = 0
        theta -= (alpha * 1/m * np.dot(X_x0.T, h - y)   lamb*(theta_with_t0_0))  # add the gradient of regularization term
        if iP000==0:
            print(L_theta(theta, X_x0, y, lamb))
    return theta

T = 1200000  # 迭代次数
degree = 11
theta = np.ones((degree   1, 1))  # 参数的初始化,degree = 11,一个12个参数
alpha = 0.0000000006 # 学习率
# alpha = 0.003  # 学习率
lamb = 0.0001
# lamb = 0
poly_features_d = PolynomialFeatures(degree=degree, include_bias=False)
X_poly_d = poly_features_d.fit_transform(X)
X_x0 = np.c_[np.ones((m, 1)), X_poly_d]  # ADD X0 = 1 to each instance
theta = GD(lamb=lamb, X_x0=X_x0, theta=theta, y=y, alpha=alpha)

上面第10行对应公式1−21−2,第24行对应公式1−31−3。

由于自由度比较大,此时利用梯度下降的方法训练模型比较困难,学习率稍微大一点就会出现出现损失函数的值越过最低点不断增长的情况。下面是训练结束后的参数以及代价函数值:

代码语言:javascript复制
[[  1.00078848e 00]
 [ -1.03862735e-05]
 [  3.85144400e-05]
 [ -3.77233288e-05]
 [  1.28959318e-04]
 [ -1.42449160e-04]
 [  4.42760996e-04]
 [ -5.11518471e-04]
 [  1.42533716e-03]
 [ -1.40265037e-03]
 [  3.13638870e-03]
 [  1.21862016e-03]]
3.59934190413

从上面的结果看,截距项的参数最大,高阶项的参数都比较小。下面是比较原始数据和训练出来的模型之间的关系:

代码语言:javascript复制
X_plot = np.linspace(-2.99, 1.9, 1000).reshape(-1, 1)
poly_features_d_with_bias = PolynomialFeatures(degree=degree, include_bias=True)
X_plot_poly = poly_features_d_with_bias.fit_transform(X_plot)
y_plot = np.dot(X_plot_poly, theta)
plt.plot(X_plot, y_plot, 'r-')
plt.plot(X, y, 'b.')
plt.xlabel('x')
plt.ylabel('y')
plt.show()

图1-1,手动实现岭回归的效果

图中模型与原始数据的匹配度不是太好,但是过拟合的情况极大的改善了,模型变的更简单了。

1.3 正规方程

下面使用正规方程求解:

其中λ=10

代码语言:javascript复制
theta2 = np.linalg.inv(np.dot(X_x0.T, X_x0)   10*np.identity(X_x0.shape[1])).dot(X_x0.T).dot(y)
print(theta2)
print(L_theta(theta2, X_x0, y, lamb))

X_plot = np.linspace(-3, 2, 1000).reshape(-1, 1)
poly_features_d_with_bias = PolynomialFeatures(degree=degree, include_bias=True)
X_plot_poly = poly_features_d_with_bias.fit_transform(X_plot)
y_plot = np.dot(X_plot_poly, theta2)
plt.plot(X_plot, y_plot, 'r-')
plt.plot(X, y, 'b.')
plt.xlabel('x')
plt.ylabel('y')
plt.show()

参数即代价函数的值:

代码语言:javascript复制
[[ 0.56502653]
 [-0.12459546]
 [ 0.26772443]
 [-0.15642405]
 [ 0.29249514]
 [-0.10084392]
 [ 0.22791769]
 [ 0.1648667 ]
 [-0.05686718]
 [-0.03906615]
 [-0.00111673]
 [ 0.00101724]]
0.604428719639

从参数来看,截距项的系数减小了,1-7阶都有比较大的参数都比较大,后面更高阶项的参数越来越小,下面是函数图像:

图1-2,使用正规方程求解

从图中可以看到,虽然模型的自由度没变,还是11,但是过拟合的程度得到了改善

1.4 使用scikit-learn

scikit-learn中有专门计算岭回归的函数,而且效果要比上面的方法好。使用scikit-learn中的岭回归,只需要输入以下参数:

  • alpha: 上面公式中的λλ,正则化项的系数;
  • solver: 求解方法;
  • X: 训练样本;
  • y: 训练样本的标签.
代码语言:javascript复制
from sklearn.linear_model import Ridge

# 代价函数
def L_theta_new(intercept, coef, X, y, lamb):
    """
    lamb: lambda, the parameter of regularization
    theta: (n 1)·1 matrix, contains the parameter of x0=1
    X_x0: m·(n 1) matrix, plus x0
    """
    h = np.dot(X, coef)   intercept  # np.dot 表示矩阵乘法
    L_theta = 0.5 * mean_squared_error(h, y)   0.5 * lamb * np.sum(np.square(coef))
    return L_theta

lamb = 10
ridge_reg = Ridge(alpha=lamb, solver="cholesky")
ridge_reg.fit(X_poly_d, y)
print(ridge_reg.intercept_, ridge_reg.coef_)
print(L_theta_new(intercept=ridge_reg.intercept_, coef=ridge_reg.coef_.T, X=X_poly_d, y=y, lamb=lamb))

X_plot = np.linspace(-3, 2, 1000).reshape(-1, 1)
X_plot_poly = poly_features_d.fit_transform(X_plot)
h = np.dot(X_plot_poly, ridge_reg.coef_.T)   ridge_reg.intercept_
plt.plot(X_plot, h, 'r-')
plt.plot(X, y, 'b.')
plt.show()

训练结束后得到的参数为(分别表示截距,特征的系数;代价函数的值):

代码语言:javascript复制
[ 3.03698398] [[ -2.95619849e-02   6.09137803e-02  -4.93919290e-02   1.10593684e-01
   -4.65660197e-02   1.06387336e-01   5.14340826e-02  -2.29460359e-02
   -1.12705709e-02  -1.73925386e-05   2.79198986e-04]]
0.213877232488

图1-3,使用scikit-learn训练岭回归

经过与前面两种方法得到的结果比较,这里得到的曲线更加平滑,不仅降低了过拟合的风险,代价函数的值也非常低。

0x02 Lasso回归

Lasso回归于岭回归非常相似,它们的差别在于使用了不同的正则化项。最终都实现了约束参数从而防止过拟合的效果。但是Lasso之所以重要,还有另一个原因是:Lasso能够将一些作用比较小的特征的参数训练为0,从而获得稀疏解。也就是说用这种方法,在训练模型的过程中实现了降维(特征筛选)的目的。

Lasso回归的代价函数为:

上式中的w是长度为n的向量,不包括截距项的系数θ0, θθ是长度为n 1的向量,包括截距项的系数θ0,m为样本数,n为特征数.

||w||1|表示参数ww的l1范数,也是一种表示距离的函数。加入w表示3维空间中的一个点(x,y,z),那么||w||1=|x| |y| |z|,即各个方向上的绝对值(长度)之和。

式子2−12−1的梯度为:

2.1 Lasso的实现

直接使用scikit-learn中的函数:

可以参考官方文档,http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.Lasso.html

下面模型中的参数alpha就是公式(2-1)中的参数λ,是正则化项的系数,可以取大于0的任意值。alpha的值越大,对模型中参数的惩罚力度越大,因此会有更多的参数被训练为0(只对线性相关的参数起作用),模型也就变得更加简单了。

代码语言:javascript复制
from sklearn.linear_model import Lasso

lamb = 0.025
lasso_reg = Lasso(alpha=lamb)
lasso_reg.fit(X_poly_d, y)
print(lasso_reg.intercept_, lasso_reg.coef_)
print(L_theta_new(intercept=lasso_reg.intercept_, coef=lasso_reg.coef_.T, X=X_poly_d, y=y, lamb=lamb))

X_plot = np.linspace(-3, 2, 1000).reshape(-1, 1)
X_plot_poly = poly_features_d.fit_transform(X_plot)
h = np.dot(X_plot_poly, lasso_reg.coef_.T)   lasso_reg.intercept_
plt.plot(X_plot, h, 'r-')
plt.plot(X, y, 'b.')
plt.show()

最终获得的参数以及代价函数的值为:

其中计算代价函数值的函数"L_theta_new"需要修改其中的"L_theta"为"L_theta = 0.5 * mean_squared_error(h, y) lamb * np.sum(np.abs(coef))"

代码语言:javascript复制
[ 2.86435179] [ -0.00000000e 00   5.29099723e-01  -3.61182017e-02   9.75614738e-02
   1.61971116e-03  -3.42711766e-03   2.78782527e-04  -1.63421713e-04
  -5.64291215e-06  -1.38933655e-05   1.02036898e-06]
0.0451291096773

从结果可以看到,截距项的值最大,一次项的系数为0,二次项的系数是剩下的所有项中值最大的,也比较符合数据的真实来源。这里也可以看出来,更高阶的项虽然系数都非常小但不为0,这是因为这些项之间的关系是非线性的,无法用线性组合互相表示。

图2-1,Lasso回归得到的图像

图2-1是目前在degree=11degree=11的情况下,得到的最好模型。

0x03 弹性网络( Elastic Net)

弹性网络是结合了岭回归和Lasso回归,由两者加权平均所得。据介绍这种方法在特征数大于训练集样本数或有些特征之间高度相关时比Lasso更加稳定。

其代价函数为:

使用scikit-learn的实现:

代码语言:javascript复制
from sklearn.linear_model import ElasticNet

# 代价函数
def L_theta_ee(intercept, coef, X, y, lamb, r):
    """
    lamb: lambda, the parameter of regularization
    theta: (n 1)·1 matrix, contains the parameter of x0=1
    X_x0: m·(n 1) matrix, plus x0
    """
    h = np.dot(X, coef)   intercept  # np.dot 表示矩阵乘法
    L_theta = 0.5 * mean_squared_error(h, y)   r * lamb * np.sum(np.abs(coef))   0.5 * (1-r) * lamb * np.sum(np.square(coef))
    return L_theta

elastic_net = ElasticNet(alpha=0.5, l1_ratio=0.8)
elastic_net.fit(X_poly_d, y)
print(elastic_net.intercept_, elastic_net.coef_)
print(L_theta_ee(intercept=elastic_net.intercept_, coef=elastic_net.coef_.T, X=X_poly_d, y=y, lamb=0.1, r=0.8))

X_plot = np.linspace(-3, 2, 1000).reshape(-1, 1)
X_plot_poly = poly_features_d.fit_transform(X_plot)
h = np.dot(X_plot_poly, elastic_net.coef_.T)   elastic_net.intercept_
plt.plot(X_plot, h, 'r-')
plt.plot(X, y, 'b.')
plt.show()

得到的结果为:

代码语言:javascript复制
[ 3.31466833] [ -0.00000000e 00   0.00000000e 00  -0.00000000e 00   1.99874040e-01
  -1.21830209e-02   2.58040545e-04   3.01117857e-03  -8.54952421e-04
   4.35227606e-05  -2.84995639e-06  -8.36248799e-06]
0.0807738447192

该方法中得到了,更多的0,当然这也跟参数的设置有关。

图3-1,使用elastic-net得到的结果

0x04 正则化项的使用以及L1与L2的比较

根据吴恩达老师的机器学习公开课,建议使用下面的步骤来确定λ的值:

代码语言:javascript复制
'''
1. 创建一个λ值的列表,例如λ∈0,0.01,0.02,0.04,0.08,0.16,0.32,0.64,1.28,2.56,5.12,10.24;
2. 创建不同degree的模型(或改变其他变量);
3. 遍历不同的模型和不同的λ值;
4. 使用学习到的参数θ(包含正则化项)计算验证集上的误差(计算误差时不包含正则化项),JCV(θ);
5. 选择在验证集上误差最小的参数组合(degree和λ);
6. 使用选出来的参数和λ在测试集上测试,计算Jtest(θ).
'''

下面通过一张图像来比较一下岭回归和Lasso回归:

图4-1,Lasso与岭回归的比较(俯瞰图)

上图中,左上方表示L1(图中菱形图案)和代价函数(图中深色椭圆环);左下方表示L2(椭圆形线圈)和代价函数(图中深色椭圆环)。同一条线上(或同一个环上),表示对应的函数值相同;图案中心分别表示L1,L2范数以及代价函数的最小值位置。

右边表示代价函数加上对应的正则化项之后的图像。添加正则化项之后,会影响原来的代价函数的最小值的位置,以及梯度下降时的路线(如果参数调整合适的话,最小值应该在距离原来代价函数最小值附近且与正则化项的图像相交,因为此时这两项在相互约束的情况下都取到最小值,它们的和也最小)。右上图,显示了Lasso回归中参数的变化情况,最终停留在了θ2=0这条线上;右下方的取值由于受到了L2范数的约束,也产生了位移。

当正则化项的权重非常大的时候,会产生左侧黄色点标识的路线,最终所有参数都为0,但是趋近原点的方式不同。这是因为对于范数来说,原点是它们的最小值点。

修改记录:

7/14, 2020: 经评论区@小鸡快跑learning指出,岭回归中加在代价函数后面的各项平方和是l2范数的平方,已更正

0x05 参考

http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.Ridge.html

Géron A. Hands-on machine learning with Scikit-Learn and TensorFlow: concepts, tools, and techniques to build intelligent systems[M]. " O'Reilly Media, Inc.", 2017. github

https://www.coursera.org/learn/machine-learning

edx: UCSanDiegoX - DSE220x Machine Learning Fundamentals

0 人点赞