线性回归 - 岭回归(五)

276 阅读13分钟

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

代码位置如下:

岭回归

岭回归解决多重共线性问题

在线性模型之中,除了线性回归之外,最知名的就是岭回归与Lasso了。这两个算法非常神秘,他们的原理和应用都不像其他算法那样高调,学习资料也很少。这可能是因为这两个算法不是为了提升模型表现,而是为了修复漏洞而设计的(实际上,我们使用岭回归或者Lasso,模型的效果往往会下降一些,因为我们删除了一小部分信息),因此在结果为上的机器学习领域颇有些被冷落的意味。这一节我们就来了解一下岭回归。

岭回归,又称为吉洪诺夫正则化(Tikhonov regularization)。通常来说,大部分的机器学习教材会使用代数的形式来展现岭回归的原理,这个原理和逻辑回归及支持向量机非常相似,都是将求解的过程转化为一个带条件的最优化问题,然后用最小二乘法求解。然而,岭回归可以做到的事其实可以用矩阵非常简单地表达出来。

岭回归在多元线性回归的损失函数上加上了正则项,表达为ww系数的L2范式(即系数ww的平方项)乘以α\alpha正则化系数。如果你们看其他教材中的代数推导,正则化系数会写作λ\lambda,用以和Lasso区别,不过在sklearn中由于是两个不同的算法,因此正则项系数都使用α\alpha来代表。岭回归的损失函数的完整表达式写作:

image.png

这个操作看起来简单,其实带来了巨大的变化。在线性回归中我们通过在损失函数上对ww求导来求解极值,在这里虽然加上了正则项,我们依然使用最小二乘法来求解。假设我们的特征矩阵结构为(m,n),系数的结构是(1,n),则可以有:

image.png

现在,只要(XTX+αI)(X^TX + \alpha I)存在逆矩阵,我们就可以求解出ww。一个矩阵存在逆矩阵的充分必要条件是这个矩阵的行列式不为0。假设原本的特征矩阵中存在共线性,则我们的方阵XTXX^TX就会不满秩(存在全为零的行):

image.png

此时方阵XTXX^TX就是没有逆的,最小二乘法就无法使用。然而,加上αI\alpha I了之后,我们的矩阵就大不一样了

image.png

最后得到的这个行列式还是一个梯形行列式,然而它的已经不存在全0行或者全0列了,除非: (1) 等于0,或者 (2) 原本的矩阵XTXX^TX中存在对角线上元素为α-\alpha,其他元素都为0的行或者列 否则矩阵(XTX+αI)(X^TX + \alpha I)永远都是满秩。在sklearn中, α\alpha的值我们可以自由控制,因此我们可以让它不为0,以避免第一种情况。而第二种情况,如果我们发现某个α\alpha的取值下模型无法求解,那我们只需要换一个α\alpha的取值就好了,也可以顺利避免。也就是说,矩阵的逆是永远存在的!有利这个保障,我们的就可以写作:

image.png

如此,正则化系数α\alpha就非常爽快地避免了”精确相关关系“带来的影响,至少最小二乘法在α\alpha存在的情况下是一定可以使用了。对于存在”高度相关关系“的矩阵,我们也可以通过调大α\alpha,来让(XTX+αI)(X^TX + \alpha I)矩阵的行列式变大,从而让逆矩阵变小,以此控制参数向量ww的偏移。当α\alpha越大,模型越不容易受到共线性的影响。

image.png

如此,多重共线性就被控制住了:最小二乘法一定有解,并且这个解可以通过α\alpha来进行调节,以确保不会偏离太多。当然了, α\alpha挤占了ww中由原始的特征矩阵贡献的空间,因此α\alpha如果太大,也会导致ww的估计出现较大的偏移,无法正确拟合数据的真实面貌。我们在使用中,需要找出α\alpha让模型效果变好的最佳取值。

linear_model.Ridge

在sklearn中,岭回归由线性模型库中的Ridge类来调用:

class sklearn.linear_model.Ridge (alpha=1.0, fit_intercept=True, normalize=False, copy_X=True, max_iter=None,tol=0.001, solver=’auto’, random_state=None)

和线性回归相比,岭回归的参数稍微多了那么一点点,但是真正核心的参数就是我们的正则项的系数,其他的参数是当我们希望使用最小二乘法之外的求解方法求解岭回归的时候才需要的,通常我们完全不会去触碰这些参数。所以大家只需要了解的用法就可以了。

之前我们在加利佛尼亚房屋价值数据集上使用线性回归,得出的结果大概是训练集上的拟合程度是60%,测试集上的拟合程度也是60%左右,那这个很低的拟合程度是不是由多重共线性造成的呢?在统计学中,我们会通过VIF或者各种检验来判断数据是否存在共线性,然而在机器学习中,我们可以使用模型来判断——如果一个数据集在岭回归中使用各种正则化参数取值下模型表现没有明显上升(比如出现持平或者下降),则说明数据没有多重共线性,顶多是特征之间有一些相关性。反之,如果一个数据集在岭回归的各种正则化参数取值下表现出明显的上升趋势,则说明数据存在多重共线性。

接下来,我们就在加利佛尼亚房屋价值数据集上来验证一下这个说法:

  • 导包,探索数据
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()

image.png

  • 划分数据
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])
    
Xtrain.head()
  • 建模
#使用岭回归来进行建模
reg = Ridge(alpha=1).fit(Xtrain,Ytrain)
reg.score(Xtest,Ytest) #加利佛尼亚房屋价值数据集中应该不是共线性问题
# 0.6043610352312279
  • 岭回归线性回归相比较
from sklearn.model_selection import cross_val_score
# 交叉验证下,与线性回归相比,岭回归的结果如何变化?
alpharange = np.arange(1,1001,100)
ridge, lr = [], []
for alpha in alpharange:
    reg = Ridge(alpha=alpha)
    linear = LinearRegression()
    regs = cross_val_score(reg,X,y,cv=5,scoring = "r2").mean()
    linears = cross_val_score(linear,X,y,cv=5,scoring = "r2").mean()
    ridge.append(regs)
    lr.append(linears)
plt.plot(alpharange,ridge,color="red",label="Ridge")
plt.plot(alpharange,lr,color="orange",label="LR")
plt.title("Mean")
plt.legend()
plt.show()

image.png

可以看出,加利佛尼亚数据集上,岭回归的结果轻微上升,随后骤降。可以说,加利佛尼亚房屋价值数据集带有很轻微的一部分共线性,这种共线性被正则化参数消除后,模型的效果提升了一点点,但是对于整个模型而言是杯水车薪。在过了控制多重共线性的点后,模型的效果飞速下降,显然是正则化的程度太重,挤占了参数本来的估计空间。从这个结果可以看出,加利佛尼亚数据集的核心问题不在于多重共线性,岭回归不能够提升模型表现。

#使用岭回归来进行建模
reg = Ridge(alpha=0).fit(Xtrain,Ytrain)
reg.score(Xtest,Ytest) #加利佛尼亚房屋价值数据集中应该不是共线性问题

#细化一下学习曲线
alpharange = np.arange(1,201,10)
ridge, lr = [], []
for alpha in alpharange:
    reg = Ridge(alpha=alpha)
    linear = LinearRegression()
    regs = cross_val_score(reg,X,y,cv=5,scoring = "r2").mean()
    linears = cross_val_score(linear,X,y,cv=5,scoring = "r2").mean()
    ridge.append(regs)
    lr.append(linears)
plt.plot(alpharange,ridge,color="red",label="Ridge")
plt.plot(alpharange,lr,color="orange",label="LR")
plt.title("Mean")
plt.legend()
plt.show()

image.png

  • 在正则化参数逐渐增大的过程中,我们可以观察一下模型的方差如何变化
#模型方差如何变化?
alpharange = np.arange(1,1001,100)
ridge, lr = [], []
for alpha in alpharange:
    reg = Ridge(alpha=alpha)
    linear = LinearRegression()
    varR = cross_val_score(reg,X,y,cv=5,scoring="r2").var()
    varLR = cross_val_score(linear,X,y,cv=5,scoring="r2").var()
    ridge.append(varR)
    lr.append(varLR)
plt.plot(alpharange,ridge,color="red",label="Ridge")
plt.plot(alpharange,lr,color="orange",label="LR")
plt.title("Variance")
plt.legend()
plt.show()

image.png

可以发现,模型的方差上升快速,不过方差的值本身很小,其变化不超过上升部分的1/3,因此只要噪声的状况维持恒定,模型的泛化误差可能还是一定程度上降低了的。虽然岭回归和Lasso不是设计来提升模型表现,而是专注于解决多重共线性问题的,但当在一定范围内变动的时候,消除多重共线性也许能够一定程度上提高模型的泛化能力。

但是泛化能力毕竟没有直接衡量的指标,因此我们往往只能够通过观察模型的准确性指标和方差来大致评判模型的泛化能力是否提高。来看看多重共线性更为明显一些的情况:

岭回归在波士顿房价数据集上的应用
  • 导包,探索数据
from sklearn.datasets import load_boston
from sklearn.model_selection import cross_val_score

X = load_boston().data
y = load_boston().target

Xtrain,Xtest,Ytrain,Ytest = TTS(X,y,test_size=0.3,random_state=420)
X.shape
# (506, 13)
  • 查看方差变化
#先查看方差的变化
alpharange = np.arange(1,1001,100)
ridge, lr = [], []
for alpha in alpharange:
    reg = Ridge(alpha=alpha)
    linear = LinearRegression()
    varR = cross_val_score(reg,X,y,cv=5,scoring="r2").var()
    varLR = cross_val_score(linear,X,y,cv=5,scoring="r2").var()
    ridge.append(varR)
    lr.append(varLR)
plt.plot(alpharange,ridge,color="red",label="Ridge")
plt.plot(alpharange,lr,color="orange",label="LR")
plt.title("Variance")
plt.legend()
plt.show()

image.png

  • 查看R2R^2的变化
#查看R2的变化
alpharange = np.arange(1,1001,100)
ridge, lr = [], []
for alpha in alpharange:
    reg = Ridge(alpha=alpha)
    linear = LinearRegression()
    regs = cross_val_score(reg,X,y,cv=5,scoring = "r2").mean()
    linears = cross_val_score(linear,X,y,cv=5,scoring = "r2").mean()
    ridge.append(regs)
    lr.append(linears)
plt.plot(alpharange,ridge,color="red",label="Ridge")
plt.plot(alpharange,lr,color="orange",label="LR")
plt.title("Mean")
plt.legend()
plt.show()

image.png

  • 细化学习曲线
#细化学习曲线
alpharange = np.arange(100,300,10)
ridge, lr = [], []
for alpha in alpharange:
    reg = Ridge(alpha=alpha)
    #linear = LinearRegression()
    regs = cross_val_score(reg,X,y,cv=5,scoring = "r2").mean()
    #linears = cross_val_score(linear,X,y,cv=5,scoring = "r2").mean()
    ridge.append(regs)
    lr.append(linears)
plt.plot(alpharange,ridge,color="red",label="Ridge")
#plt.plot(alpharange,lr,color="orange",label="LR")
plt.title("Mean")
plt.legend()
plt.show()

image.png

可以发现,比起加利佛尼亚房屋价值数据集,波士顿房价数据集的方差降低明显,偏差也降低明显,可见使用岭回归还是起到了一定的作用,模型的泛化能力是有可能会上升的。

遗憾的是,没有人会希望自己获取的数据中存在多重共线性,因此发布到scikit-learn或者kaggle上的数据基本都经过一定的多重共线性的处理的,要找出绝对具有多重共线性的数据非常困难,也就无法给大家展示岭回归在实际数据中大显身手的模样。我们也许可以找出具有一些相关性的数据,但是大家如果去尝试就会发现,基本上如果我们使用岭回归或者Lasso,那模型的效果都是会降低的,很难升高,这恐怕也是岭回归和Lasso一定程度上被机器学习领域冷遇的原因。

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

既然要选择α\alpha的范围,我们就不可避免地要进行最优参数的选择。在各种机器学习教材中,总是教导使用岭迹图来判断正则项参数的最佳取值。传统的岭迹图长这样,形似一个开口的喇叭图(根据横坐标的正负,喇叭有可能朝右或者朝左):

image.png

这一个以正则化参数为横坐标,线性模型求解的系数为纵坐标的图像,其中每一条彩色的线都是一个系数ww。其目标是建立正则化参数与系数ww之间的直接关系,以此来观察正则化参数的变化如何影响了系数ww的拟合。岭迹图认为,线条交叉越多,则说明特征之间的多重共线性越高。我们应该选择系数较为平稳的喇叭口所对应α\alpha的取值作为最佳的正则化参数的取值。绘制岭迹图的方法非常简单,代码如下:

import numpy as np
import matplotlib.pyplot as plt
from sklearn import linear_model
#创造10*10的希尔伯特矩阵
X = 1. / (np.arange(1, 11) + np.arange(0, 10)[:, np.newaxis])
y = np.ones(10)
#计算横坐标
n_alphas = 200
alphas = np.logspace(-10, -2, n_alphas)
#建模,获取每一个正则化取值下的系数组合
coefs = []
for a in alphas:
    ridge = linear_model.Ridge(alpha=a, fit_intercept=False)
    ridge.fit(X, y)
    coefs.append(ridge.coef_)
    
#绘图展示结果
ax = plt.gca()
ax.plot(alphas, coefs)
ax.set_xscale('log')
ax.set_xlim(ax.get_xlim()[::-1]) #将横坐标逆转
plt.xlabel('正则化参数alpha')
plt.ylabel('系数w')
plt.title('岭回归下的岭迹图')
plt.axis('tight')
plt.show()

image.png

其中涉及到的希尔伯特矩阵长这样:

image.png

然而,我非常不建议大家使用岭迹图来作为寻找最佳参数的标准。 有这样的两个理由:

  • 岭迹图的很多细节,很难以解释。比如为什么多重共线性存在会使得线与线之间有很多交点?当很大了之后看上去所有的系数都很接近于0,难道不是那时候线之间的交点最多吗?
  • 岭迹图的评判标准,非常模糊。哪里才是最佳的喇叭口?哪里才是所谓的系数开始变得”平稳“的时候?一千个读者一千个哈姆雷特的画像?未免也太不严谨了。

所以我们应该使用交叉验证来选择最佳的正则化系数。在sklearn中,我们有带交叉验证的岭回归可以使用,我们来看一看:

class sklearn.linear_model.RidgeCV (alphas=(0.1, 1.0, 10.0), fit_intercept=True, normalize=False, scoring=None,cv=None, gcv_mode=None, store_cv_values=False)

可以看到,这个类于普通的岭回归类Ridge非常相似,不过在输入正则化系数的时候我们可以传入元祖作为正则化系数的备选,非常类似于我们在画学习曲线前设定的for i in 的列表对象。来看RidgeCV的重要参数,属性和接口:

重要参数含义
alphas需要测试的正则化参数的取值的元祖
scoring用来进行交叉验证的模型评估指标,默认是,可自行调整
store_cv_values是否保存每次交叉验证的结果,默认False
cv交叉验证的模式,默认是None,表示默认进行留一交叉验证
可以输入Kfold对象和StratifiedKFold对象来进行交叉验证
注意,仅仅当为None时,每次交叉验证的结果才可以被保存下来
当cv有值存在(不是None)时,store_cv_values无法被设定为True
重要属性含义
alpha_查看交叉验证选中的alpha
cv_values_调用所有交叉验证的结果,只有当store_cv_values=True的时候才能够调用,因此返回的
结构是(n_samples, n_alphas)
重要接口含义
score调用Ridge类不进行交叉验证的情况下返回的R平方

这个类的使用也非常容易,依然使用我们之前建立的加利佛尼亚房屋价值数据集:

  • 导包,数据探索
import numpy as np
import pandas as pd
from sklearn.linear_model import RidgeCV, LinearRegression
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 = ["住户收入中位数","房屋使用年代中位数","平均房间数目"
           ,"平均卧室数目","街区人口","平均入住率","街区的纬度","街区的经度"]
  • 建模
Ridge_ = RidgeCV(alphas=np.arange(1,1001,100)
                 #,scoring="neg_mean_squared_error"
                 ,store_cv_values=True
                 #,cv=5
                ).fit(X, y)
  • 结果展示
#无关交叉验证的岭回归结果
Ridge_.score(X,y)
# 0.6060251767338417

#调用所有交叉验证的结果
Ridge_.cv_values_
"""
array([[0.1557472 , 0.16301246, 0.16892723, ..., 0.18881663, 0.19182353,
        0.19466385],
       [0.15334566, 0.13922075, 0.12849014, ..., 0.09744906, 0.09344092,
        0.08981868],
       [0.02429857, 0.03043271, 0.03543001, ..., 0.04971514, 0.05126165,
        0.05253834],
       ...,
       [0.56545783, 0.5454654 , 0.52655917, ..., 0.44532597, 0.43130136,
        0.41790336],
       [0.27883123, 0.2692305 , 0.25944481, ..., 0.21328675, 0.20497018,
        0.19698274],
       [0.14313527, 0.13967826, 0.13511341, ..., 0.1078647 , 0.10251737,
        0.0973334 ]])
"""

#进行平均后可以查看每个正则化系数取值下的交叉验证结果
Ridge_.cv_values_.mean(axis=0)
"""
array([0.52823795, 0.52787439, 0.52807763, 0.52855759, 0.52917958,
       0.52987689, 0.53061486, 0.53137481, 0.53214638, 0.53292369])
"""

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