线性回归 - Lasso(六)

667 阅读12分钟

根据菜菜的课程进行整理,方便记忆理解

代码位置如下:

Lasso

Lasso与多重共线性

除了岭回归之外,最常被人们提到还有模型Lasso。Lasso全称最小绝对收缩和选择算子(least absolute shrinkage and selection operator),由于这个名字过于复杂所以简称为Lasso。和岭回归一样,Lasso是被创造来作用于多重共线性问题的算法,不过Lasso使用的是系数ww的L1范式(L1范式则是系数的绝对值)乘以正则化系数α\alpha,所以Lasso的损失函数表达式为:

image.png

许多博客和机器学习教材会说,Lasso与岭回归非常相似,都是利用正则项来对原本的损失函数形成一个惩罚,以此来防止多重共线性。这种说法不是非常严谨,我们来看看Lasso的数学过程。当我们使用最小二乘法来求解Lasso中的参数,我们依然对损失函数进行求导:

image.png

大家可能已经注意到了,现在问题又回到了要求XTXX^TX的逆必须存在。在岭回归中,我们通过正则化系数α\alpha能够向方阵XTXX^TX加上一个单位矩阵,以此来防止方阵XTXX^TX的行列式为0,而现在L1范式所带的正则项α\alpha在求导之后并不带有ww这个项,因此它无法对XTXX^TX造成任何影响。也就是说,Lasso无法解决特征之间”精确相关“的问题。当我们使用最小二乘法求解线性回归时,如果线性回归无解或者报除零错误,换Lasso不能解决任何问题

岭回归 vs Lasso

岭回归可以解决特征间的精确相关关系导致的最小二乘法无法使用的问题,而Lasso不行

幸运的是,在现实中我们其实会比较少遇到“精确相关”的多重共线性问题,大部分多重共线性问题应该是**“高度相关“**,而如果我们假设方阵XTXX^TX的逆是一定存在的,那我们可以有:

image.png

通过增大α\alpha,我们可以为ww的计算增加一个负项,从而限制参数估计中ww的大小,而防止多重共线性引起的参数ww被估计过大导致模型失准的问题。Lasso不是从根本上解决多重共线性问题,而是限制多重共线性带来的影响。何况,这还是在我们假设所有的系数都为正的情况下,假设系数ww无法为正,则很有可能我们需要将我们的正则项参数α\alpha设定为负,因此α\alpha可以取负数,并且负数越大,对共线性的限制也越大。

所有这些让Lasso成为了一个神奇的算法,尽管它是为了限制多重共线性被创造出来的,然而世人其实并不使用它来抑制多重共线性,反而接受了它在其他方面的优势。我们在讲解逻辑回归时曾提到过,L1和L2正则化一个核心差异就是他们对系数的影响:两个正则化都会压缩系数的大小,对标签贡献更少的特征的系数会更小,也会更容易被压缩。不过,L2正则化只会将系数压缩到尽量接近0,但L1正则化主导稀疏性,因此会将系数压缩到0。这个性质,让Lasso成为了线性模型中的特征选择工具首选,接下来,我们就来看看如何使用Lasso来选择特征。

Lasso的核心作用:特征选择

class sklearn.linear_model.Lasso (alpha=1.0, fit_intercept=True, normalize=False, precompute=False, copy_X=True, max_iter=1000, tol=0.0001, warm_start=False, positive=False, random_state=None, selection=’cyclic’)

sklearn中我们使用类Lasso来调用lasso回归,众多参数中我们需要比较在意的就是参数α\alpha,正则化系数。另外需要注意的就是参数positive。当这个参数为"True"的时候,是我们要求Lasso回归出的系数必须为正数,以此来保证我们的一定以增大来控制正则化的程度。

需要注意的是,在sklearn中我们的Lasso使用的损失函数是:

image.png

其中12nsmaple\frac{1}{2n_{smaple}}只是作为系数存在,用来消除我们对损失函数求导后多出来的那个2的(求解ww时所带的1/2),然后对整体的RSS求了一个平均而已,无论时从损失函数的意义来看还是从Lasso的性质和功能来看,这个变化没有造成任何影响,只不过计算上会更加简便一些。

接下来,我们就来看看lasso如何做特征选择:

  • 导包,探索数据
import numpy as np
import pandas as pd
from sklearn.linear_model import Ridge, LinearRegression, Lasso
from sklearn.model_selection import train_test_split as TTS
from sklearn.datasets import fetch_california_housing as fch
import matplotlib.pyplot as plt

housevalue = fch()

X = pd.DataFrame(housevalue.data)
y = housevalue.target
X.columns = ["住户收入中位数","房屋使用年代中位数","平均房间数目"
            ,"平均卧室数目","街区人口","平均入住率","街区的纬度","街区的经度"]

X.head()
  • 数据划分
Xtrain,Xtest,Ytrain,Ytest = TTS(X,y,test_size=0.3,random_state=420)
for i in [Xtrain,Xtest]:
    i.index = range(i.shape[0])
  • 模型拟合
#线性回归进行拟合
reg = LinearRegression().fit(Xtrain,Ytrain)
(reg.coef_*100).tolist()

"""
[43.73589305968394,
 1.0211268294493865,
 -10.780721617317587,
 62.643382753637525,
 5.2161253533133944e-05,
 -0.33485096463337294,
 -41.30959378947716,
 -42.62109536208476]
"""

#岭回归进行拟合
Ridge_ = Ridge(alpha=0).fit(Xtrain,Ytrain)
(Ridge_.coef_*100).tolist()

"""
[43.735893059684074,
 1.0211268294494085,
 -10.780721617317722,
 62.643382753638,
 5.216125353267978e-05,
 -0.3348509646333579,
 -41.30959378947707,
 -42.621095362084645]
"""

#Lasso进行拟合
lasso_ = Lasso(alpha=0).fit(Xtrain,Ytrain)
(lasso_.coef_*100).tolist()
"""
[43.73589305968409,
 1.0211268294494118,
 -10.780721617317672,
 62.64338275363768,
 5.216125353272806e-05,
 -0.33485096463335756,
 -41.30959378947691,
 -42.621095362084475]
"""

#Lasso进行拟合,alpha=0.1
lasso_ = Lasso(alpha=0.1).fit(Xtrain,Ytrain)
(lasso_.coef_*100).tolist()

"""
[39.08851438329682,
 1.6054695654279867,
 -0.0,
 0.0,
 0.002377701483909135,
 -0.3050186895638111,
 -10.771509301655513,
 -9.29434447795805]
"""

#岭回归进行拟合,alpha=0.1
Ridge_ = Ridge(alpha=0.1).fit(Xtrain,Ytrain)
(Ridge_.coef_*100).tolist()
"""
[43.734534807869565,
 1.0211508518425625,
 -10.778109335481897,
 62.62978997580455,
 5.225552031935978e-05,
 -0.33484783635443643,
 -41.30937006538783,
 -42.62068050768482]
"""

#加大正则项系数,观察模型的系数发生了什么变化
Ridge_ = Ridge(alpha=10**10).fit(Xtrain,Ytrain)
(Ridge_.coef_*100).tolist()

"""
[0.00021838533330206374,
 0.00021344956264503437,
 6.213673042878622e-05,
 -3.828084920732733e-06,
 -0.0014984087286952841,
 -4.175243714653837e-05,
 -5.295061194474961e-05,
 -1.3268982521957727e-05]
"""

# Lasso中alpha=10**4
lasso_ = Lasso(alpha=10**4).fit(Xtrain,Ytrain)
(lasso_.coef_*100).tolist()
# [0.0, 0.0, 0.0, -0.0, -0.0, -0.0, -0.0, -0.0]

#看来10**4对于Lasso来说是一个过于大的取值
lasso_ = Lasso(alpha=1).fit(Xtrain,Ytrain)
(lasso_.coef_*100).tolist()
"""
[14.581141247629395,
 0.6209347344423866,
 0.0,
 -0.0,
 -0.00028065986329010227,
 -0.0,
 -0.0,
 -0.0]
"""
  • 系数进行绘图
#将系数进行绘图
plt.plot(range(1,9),(reg.coef_*100).tolist(),color="red",label="LR")
plt.plot(range(1,9),(Ridge_.coef_*100).tolist(),color="orange",label="Ridge")
plt.plot(range(1,9),(lasso_.coef_*100).tolist(),color="k",label="Lasso")
plt.plot(range(1,9),[0]*8,color="grey",linestyle="--")
plt.xlabel('w') #横坐标是每一个特征所对应的系数
plt.legend()
plt.show()

image.png

可以看到,岭回归没有报出错误,但Lasso就不一样了,虽然依然对系数进行了计算,但是报出了整整三个红条:

image.png

这三条分别是这样的内容:

  1. 正则化系数为0,这样算法不可收敛!如果你想让正则化系数为0,请使用线性回归吧
  2. 没有正则项的坐标下降法可能会导致意外的结果,不鼓励这样做!
  3. 目标函数没有收敛,你也许想要增加迭代次数,使用一个非常小的alpha来拟合模型可能会造成精确度问题!看到这三条内容,大家可能会比较懵——怎么出现了坐标下降?这是由于sklearn中的Lasso类不是使用最小二乘法来进行求解,而是使用坐标下降。考虑一下,Lasso既然不能够从根本解决多重共线性引起的最小二乘法无法使用的问题,那我们为什么要坚持最小二乘法呢?明明有其他更快更好的求解方法,比如坐标下降就很好呀。下面两篇论文解释了了scikit-learn坐标下降求解器中使用的迭代方式,以及用于收敛控制的对偶间隙计算方式,感兴趣的大家可以进行阅读。

有了坐标下降,就有迭代和收敛的问题,因此sklearn不推荐我们使用0这样的正则化系数。如果我们的确希望取到0,那我们可以使用一个比较很小的数,比如0.01,或者10 * e3e^{-3}这样的值

比起岭回归,Lasso所带的L1正则项对于系数的惩罚要重得多,并且它会将系数压缩至0,因此可以被用来做特征选择。也因此,我们往往让Lasso的正则化系数在很小的空间中变动,以此来寻找最佳的正则化系数。

选取最佳的正则化参数取值

class sklearn.linear_model.LassoCV (eps=0.001, n_alphas=100, alphas=None, fit_intercept=True, normalize=False, precompute=’auto’, max_iter=1000, tol=0.0001, copy_X=True, cv=’warn’, verbose=False, n_jobs=None, positive=False, random_state=None, selection=’cyclic’)

使用交叉验证的Lasso类的参数看起来与岭回归略有不同,这是由于Lasso对于alpha的取值更加敏感的性质决定的。之前提到过,由于Lasso对正则化系数的变动过于敏感,因此我们往往让在很小的空间中变动。这个小空间小到超乎人们的想象(不是0.01到0.02之间这样的空间,这个空间对lasso而言还是太大了),因此我们设定了一个重要概念“正则化路径”,用来设定正则化系数的变动:

正则化路径 regularization path

假设我们的特征矩阵中有个特征,则我们就有特征向量。对于每一个的取值,我们都可以得出一组对应这个特征向量的参数向量,其中包含了n+1个参数,分别是。这些参数可以被看作是一个n+1维空间中的一个点(想想我们在主成分分析和奇异值分解中讲解的n维空间)。对于不同的取值,我们就将得到许多个在n+1维空间中的点,所有的这些点形成的序列,就被我们称之为是正则化路径。

我们把形成这个正则化路径的α\alpha的最小值除以α\alpha的最大值得到的量a.mina.max\frac{a.min}{a.max}称为正则化路径的长度(length of the path)。在sklearn中,我们可以通过规定正则化路径的长度(即限制α\alpha的最小值和最大值之间的比例),以及路径中α\alpha的个数,来让sklearn为我们自动生成α\alpha的取值,这就避免了我们需要自己生成非常非常小的α\alpha的取值列表来让交叉验证类使用,类LassoCV自己就可以计算了。

和岭回归的交叉验证类相似,除了进行交叉验证之外,LassoCV也会单独建立模型。它会先找出最佳的正则化参数,然后在这个参数下按照模型评估指标进行建模。需要注意的是,LassoCV的模型评估指标选用的是均方误差,而岭回归的模型评估指标是可以自己设定的,并且默认是R2R^2

参数含义
eps正则化路径的长度,默认0.001
n_alphas正则化路径中的个数,默认100
alphas需要测试的正则化参数的取值的元祖,默认None。当不输入的时候,自动使用eps和n_alphas
来自动生成带入交叉验证的正则化参数
cv交叉验证的次数,默认3折交叉验证,将在0.22版本中改为5折交叉验证
属性含义
alpha_调用交叉验证选出来的最佳正则化参数
alphas_使用正则化路径的长度和路径中的个数来自动生成的,用来进行交叉验证的正则化参数
mse_path返回所以交叉验证的结果细节
coef_调用最佳正则化参数下建立的模型的系数

来看看将这些参数和属性付诸实践的代码:

from sklearn.linear_model import LassoCV

#自己建立Lasso进行alpha选择的范围
alpharange = np.logspace(-10, -2, 200,base=10)

#其实是形成10为底的指数函数
#10**(-10)到10**(-2)次方
alpharange.shape
# (200,)

lasso_ = LassoCV(alphas=alpharange #自行输入的alpha的取值范围
               ,cv=5 #交叉验证的折数
               ).fit(Xtrain, Ytrain)

#查看被选择出来的最佳正则化系数
lasso_.alpha_
# 0.0020729217795953697

#调用所有交叉验证的结果
lasso_.mse_path_
"""
array([[0.52454913, 0.49856261, 0.55984312, 0.50526576, 0.55262557],
      [0.52361933, 0.49748809, 0.55887637, 0.50429373, 0.55283734],
      [0.52281927, 0.49655113, 0.55803797, 0.5034594 , 0.55320522],
      [0.52213811, 0.49574741, 0.55731858, 0.50274517, 0.55367515],
      [0.52155715, 0.49505688, 0.55669995, 0.50213252, 0.55421553],
      [0.52106069, 0.49446226, 0.55616707, 0.50160604, 0.55480104],
      [0.5206358 , 0.49394903, 0.55570702, 0.50115266, 0.55541214],
      [0.52027135, 0.49350539, 0.55530895, 0.50076146, 0.55603333],
      [0.51995825, 0.49312085, 0.5549639 , 0.50042318, 0.55665306]])
"""

lasso_.mse_path_.shape # 返回每个alpha下的五折交叉验证结果
# (200, 5)

lasso_.mse_path_.mean(axis=1) #有注意到在岭回归中我们的轴向是axis=0吗?
#在岭回归当中,我们是留一验证,因此我们的交叉验证结果返回的是,每一个样本在每个alpha下的交叉验证结果
#因此我们要求每个alpha下的交叉验证均值,就是axis=0,跨行求均值
#而在这里,我们返回的是,每一个alpha取值下,每一折交叉验证的结果
#因此我们要求每个alpha下的交叉验证均值,就是axis=1,跨列求均值
"""
array([0.52816924, 0.52742297, 0.5268146 , 0.52632488, 0.52593241,
      0.52561942, 0.52537133, 0.5251761 , 0.52502385, 0.52490641,
      0.52481712, 0.52475046, 0.52470198, 0.52466795, 0.52464541,
      0.52463188, 0.5246254 , 0.52462436, 0.52462744, 0.52463361,
      0.52464201, 0.52465199, 0.52466301, 0.52467466, 0.5246866 ,
      0.5246986 , 0.52471046, 0.52472203, 0.5247332 , 0.52474392,
      0.52475413, 0.52476379, 0.52477291, 0.52478147, 0.52478949])
"""

#最佳正则化系数下获得的模型的系数结果
lasso_.coef_
"""
array([ 4.29867301e-01,  1.03623683e-02, -9.32648616e-02,  5.51755252e-01,
       1.14732262e-06, -3.31941716e-03, -4.10451223e-01, -4.22410330e-01])
"""

lasso_.score(Xtest,Ytest)
# 0.6038982670571437

#与线性回归相比如何?
reg = LinearRegression().fit(Xtrain,Ytrain)
reg.score(Xtest,Ytest)

#使用lassoCV自带的正则化路径长度和路径中的alpha个数来自动建立alpha选择的范围
ls_ = LassoCV(eps=0.00001
             ,n_alphas=300
             ,cv=5
               ).fit(Xtrain, Ytrain)

ls_.alpha_
# 0.0020954551690628535

ls_.alphas_ #查看所有自动生成的alpha取值
"""
array([2.94059737e+01, 2.82952253e+01, 2.72264331e+01, 2.61980122e+01,
      2.52084378e+01, 2.42562424e+01, 2.33400142e+01, 2.24583946e+01,
      2.16100763e+01, 2.07938014e+01, 2.00083596e+01, 1.92525862e+01,
      1.85253605e+01, 1.78256042e+01, 1.71522798e+01, 1.65043887e+01,
      1.58809704e+01, 1.52811004e+01, 1.47038891e+01, 1.41484809e+01,
      1.36140520e+01, 1.30998100e+01, 1.26049924e+01, 1.21288655e+01,
      1.16707233e+01, 1.12298864e+01, 1.08057012e+01, 1.03975388e+01,
      1.00047937e+01, 9.62688384e+00, 9.26324869e+00, 8.91334908e+00])
"""

ls_.alphas_.shape
# (300,)

ls_.score(Xtest,Ytest)
# 0.60389154238192

ls_.coef_
"""
array([ 4.29785372e-01,  1.03639989e-02, -9.31060823e-02,  5.50940621e-01,
       1.15407943e-06, -3.31909776e-03, -4.10423420e-01, -4.22369926e-01])
"""

到这里,岭回归和Lasso的核心作用就为大家讲解完毕了。Lasso作为线性回归家族中在改良上走得最远的算法,还有许多领域等待我们去探讨。比如说,在现实中,我们不仅可以适用交叉验证来选择最佳正则化系数,我们也可以使用BIC( 贝叶斯信息准则)或者AIC(Akaike informationcriterion,艾凯克信息准则)来做模型选择。同时,我们可以不使用坐标下降法,还可以使用最小角度回归来对lasso进行计算。

当然了,这些方法下做的模型选择和模型计算,其实在模型效果上表现和普通的Lasso没有太大的区别,不过他们都在各个方面对原有的Lasso做了一些相应的改进(比如说提升了本来就已经很快的计算速度,增加了模型选择的维度,因为均方误差作为损失函数只考虑了偏差,不考虑方差的存在)。除了解决多重共线性这个核心问题之外,线性模型还有更重要的事情要做:提升模型表现。这才是机器学习最核心的需求,而Lasso和岭回归不是为此而设计的。