目标变量通常有很多影响因素,通过各类影响因素构建对目标变量的回归模型,能够实现对目标的预测。但根据稀疏性的假设,即使影响一个变量的因素有很多,其关键因素永远只会是少数。在这种情况下,还用传统的线性回归方法来处理的话,效果可能并不理想。针对这种情况,下面介绍两种线性回归模型的拓展模型—— LASSO 回归和 Ridge 回归。
一、LASSO回归
1. 原理推导
LASSO(the least absolute shrinkage and election operator)回归模型,可译为最小绝对收缩和选择算子,已知线性回归模型的最优参数估计表达式为:
ω∧✶ = (XTX)-1XTy【式 1】
具体解析见:【监督学习单模型—线性模型—线性回归】
假设训练样本量为 m ,样本特征数为 n ,按照惯例,有 m > n ,即样本量大于特征数。当 m > n 时,若 rank(X) = n ,即 X 为满秩矩阵,则 XTX 是可逆矩阵,式 1 是可以直接求解的,但如果 m < n ,即特征数大于样本量时,rank(X) < n ,矩阵 X 不满秩,XTX 不可逆,这时候式 1 中的参数 ω∧✶ 是不可估计的。
对于这个问题,LASSO 回归给出的做法是在线性回归的损失函数后面加一个 1-范数项,也叫正则化项,即:L(ω) = (y-ωX)2 + λ||ω||1【式 2】 ,其中 |ω||1 即为矩阵的 1-范数,λ 为 1-范数项的系数。
范数(norm)的概念:在数学分析中,范数可视为一种长度或者距离概念的函数。针对向量或者矩阵而言,常用的范数包括 0-范数、1-范数、2-范数和 p-范数等。矩阵的 0-范数为矩阵中非零元素的个数,矩阵的 1-范数可定义为矩阵中所有元素的绝对值之和,而矩阵的 2-范数是指矩阵中各元素的平方和再求均方根的结果。
从机器学习的角度来看,式 2 相当于给最初的线性回归损失函数添加了一个 L1 正则化项,λ 也叫正则化系数。从防止模型过拟合的角度而言,正则化项相当于对目标参数施加了一个惩罚项,使得模型不能过于复杂。在优化过程中,正则化项的存在能够使那些不重要的特征系数逐渐为零,从而保留关键特征,使得模型简化。所以,式 2 等价于:arg min (y-ωX)2【式 3】s.t. Σ|ωij| < s【式 4】,其中式 3 即为线性回归目标函数,式 4 为其约束条件,即权重系数矩阵所有元素绝对值之和小于一个指定常数 s ,s 取值越小,特征参数中被压缩到零的特征就会越多。
下图是 LASSO 回归的参数估计图:
如上图所示,横纵坐标分别为两个回归参数 ω1 和 ω2 ,菱形线框表示 LASSO 回归的 L1 正则化约束:|ω1| + |ω2| ≤ s ,椭圆形区域为回归参数的求解空间。可以看到,LASSO 回归的参数解空间与纵坐标轴相交,此时意味着参数 ω1 被压缩为 0 。
如何针对式 2 :L(ω) = (y-ωX)2 + λ||ω||1 的 LASSO 回归目标函数进行参数优化?即如何求 LASSO 回归的最优解问题。L1 正则化项的存在使得式 2 是连续不可导的函数,直接使用梯度下降法无法进行寻优,一种替代的 LASSO 回归寻优方法称为坐标下降法(coordinate descent method)。坐标下降法是一种迭代算法,相较于梯度下降法通过损失函数的负梯度来确定下降方向,坐标下降法是在当前坐标轴上搜索损失函数的最小值,无需计算函数梯度。
以二维空间为例,假设 LASSO 回归损失函数为凸函数 L(x,y) ,给定初始点 x0 ,可以找到使得 L(y) 达到最小的 y1 ,然后固定 y1 ,再找到使得 L(x) 达到最小的 x2 。这样反复迭代之后,根据凸函数的性质,一定能够找到使得 L(x,y) 最小的点 (xk,yk) 。坐标下降法的寻优过程如下图所示:
2. 代码实现
数据集:葡萄酒质量(类型:txt 文件)
-
输入变量:物理化学性质(例如,pH 值、酒精含量、酸度)。
-
输出变量:感官评分(质量),这是有序的类别。
“fixed acidity”;“volatile acidity”;“citric acid”;“residual sugar”;“chlorides”;“free sulfur dioxide”;“total sulfur dioxide”;“density”;“pH”;“sulphates”;“alcohol”;“quality” |
---|
7.4;0.7;0;1.9;0.076;11;34;0.9978;3.51;0.56;9.4;5 |
7.8;0.88;0;2.6;0.098;25;67;0.9968;3.2;0.68;9.8;5 |
7.8;0.76;0.04;2.3;0.092;15;54;0.997;3.26;0.65;9.8;5 |
11.2;0.28;0.56;1.9;0.075;17;60;0.998;3.16;0.58;9.8;6 |
7.4;0.7;0;1.9;0.076;11;34;0.9978;3.51;0.56;9.4;5 |
7.4;0.66;0;1.8;0.075;13;40;0.9978;3.51;0.56;9.4;5 |
7.9;0.6;0.06;1.6;0.069;15;59;0.9964;3.3;0.46;9.4;5 |
7.3;0.65;0;1.2;0.065;15;21;0.9946;3.39;0.47;10;7 |
7.8;0.58;0.02;2;0.073;9;18;0.9968;3.36;0.57;9.5;7 |
7.5;0.5;0.36;6.1;0.071;17;102;0.9978;3.35;0.8;10.5;5 |
6.7;0.58;0.08;1.8;0.097;15;65;0.9959;3.28;0.54;9.2;5 |
7.5;0.5;0.36;6.1;0.071;17;102;0.9978;3.35;0.8;10.5;5 |
1)基于 Numpy 的代码实现
【代码实现】:
import numpy as np# 定义符号函数
def sign(x):if x > 0:return 1elif x < 0:return -1else:return 0
# 利用Numpy对符号函数进行向量化
vec_sign = np.vectorize(sign)# 定义LASSO回归损失函数
def l1_loss(X, y, w, b, alpha):# 训练样本数num_train = X.shape[0]# 训练特征数# num_feature = X.shape[1]# 回归模型预测输出y_hat = np.dot(X, w) + b# L1损失函数loss = np.sum((y_hat - y) ** 2) / num_train + np.sum(alpha * abs(w))# 基于向量化符号函数的参数梯度计算dw = np.dot(X.T, (y_hat - y)) / num_train + alpha * vec_sign(w)db = np.sum((y_hat - y)) / num_trainreturn y_hat, loss, dw, db# 初始化模型参数
def initialize_params(dims):# 将权重向量初始化为零向量w = np.zeros((dims, 1))# 将偏置参数初始化为0b = 0return w, b# 定义LASSO回归模型的训练过程
def lasso_train(X, y, learning_rate=0.01, epochs=1000):# 记录训练损失的空列表loss_his = []# 初始化模型参数w, b = initialize_params(X.shape[1])# 迭代训练for i in range(1, epochs):# 计算当前迭代的预测值、损失和梯度alpha = 0.1y_hat, loss, dw, db = l1_loss(X, y, w, b, alpha)# 基于梯度下降法的参数更新w += -learning_rate * dwb += -learning_rate * db# 记录当前迭代的损失loss_his.append(loss)# 每50次迭代打印当前损失信息# if i % 50 == 0:# print('epoch %d loss %f' % (i, loss))# 将迭代优化后的参数保存到字典中params = {'w':w, 'b':b}return loss_his, params# 导入数据集 (12,12)
data = np.genfromtxt('D:\PycharmProjects\wine_lasso_quality.txt', delimiter=';')
# 选择特征与标签
num_row = data.shape[0]
num_column = data.shape[1]
x = data[1:, 0:num_column-1]
y = data[1:, num_column-1].reshape(-1, 1)
# 加一列
X = np.column_stack((np.ones((num_row-1, 1)), x))
# 划分数据集,使用30%的数据作为测试集
offset = int(X.shape[0] * 0.7)
# 训练集 (8,12) & (8,1)
X_train, y_train = X[:offset], y[:offset]
# 测试集 (4,12) & (4,1)
X_test, y_test = X[offset:], y[offset:]
# 执行训练示例
learning_rate = 0.01
epochs = 100
loss_list, params = lasso_train(X_train, y_train, learning_rate, epochs)
# 获取训练参数
print('训练参数:\n', params)
# 将测试集应用在模型上
y_predict = np.dot(X_test, params['w']) + params['b']
print('测试集训练标签:\n', y_predict)
【结果展示】:
2)基于 Sklearn 的模型实现
【代码实现】:
import numpy as np
from sklearn import linear_model# 导入数据集 (12,12)
data = np.genfromtxt('D:\PycharmProjects\wine_lasso_quality.txt', delimiter=';')
# 选择特征与标签
num_row = data.shape[0]
num_column = data.shape[1]
x = data[1:, 0:num_column-1]
y = data[1:, num_column-1].reshape(-1, 1)
# 加一列
X = np.column_stack((np.ones((num_row-1, 1)), x))
# 划分数据集,使用30%的数据作为测试集
offset = int(X.shape[0] * 0.7)
# 训练集 (8,12) & (8,1)
X_train, y_train = X[:offset], y[:offset]
# 测试集 (4,12) & (4,1)
X_test, y_test = X[offset:], y[offset:]# 创建LASSO回归模型实例
sk_LASSO = linear_model.Lasso(alpha=0.1)
# 对训练集进行拟合
sk_LASSO.fit(X_train, y_train)
# 打印模型相关系数
print('sklearn LASSO intercept:(截距)', sk_LASSO.intercept_)
print('\nsklearn LASSO coefficients:(系数)\n', sk_LASSO.coef_)
print('\nsklearn LASSO number of iterations(迭代次数):', sk_LASSO.n_iter_)
【结果展示】:
从上述结果可以看出,LASSO 回归模型使得大量特征的参数被压缩为 0 。
二、Ridge回归
1. 原理推导
类似于 LASSO 回归模型,Ridge 回归(岭回归)是一种使用 2-范数作为惩罚项改造线性回归损失函数的模型。此时损失函数如式 5 所示:
L(ω) = (y-ωX)2 + λ||ω||2【式 5】
其中 ||ω||2 = λ · Σi=1n ωi2 ,也叫 L2 正则化项,采用 2-范数进行正则化的原理是最小化参数矩阵的每个元素,使其无限接近 0 但又不像 L1 那样等于 0 。
为什么参数矩阵中的每个元素变得很小,就能防止过拟合?下面以深度神经网络为例来说明。在 L2 正则化中,如果正则化系数 λ 取值较大,参数矩阵 ω 中的每个元素都会变小,线性计算,激活函数在此时相对呈线性状态,这样就会降低深度神经网络的复杂性,因而可以防止过拟合,所以式 5 等价于:arg min (y-ωX)2(s.t. Σωij2 < s)【式 6】。
式 6 的第一个公式即为线性回归目标函数,括号里的公式为其约束条件,即权重系数矩阵所有元素平方之和小于指定常数 s 。相应地,式 1 可改写为:ω∧✶ = (XTX + λI)-1XTy【式 7】。
从式 7 可以看到,通过给 XTX 加上一个单位矩阵使其变成非奇异矩阵并可以进行求逆运算,从而求解 Ridge 回归。下图是 Ridge 回归的参数估计图:
如上图所示,横纵坐标分别为两个回归参数 ω1 和 ω2 ,圆形区域表示 Ridge 回归的 L2 正则化约束:ω12 + ω22 ≤ s ,椭圆形区域为回归参数的求解空间。由此可见,LASSO 回归的参数解空间与纵坐标轴相交,而 Ridge 回归参数只是接近 0 但不等于 0 。
Ridge 回归参数求解要比 LASSO 回归相对容易一些,一方面,我们可以直接基于式 7 的矩阵运算进行求解,另一方面,也可以按照线性回归的梯度下降优化方式进行迭代计算。
2. 代码实现
1)基于 Numpy 的代码实现
【代码实现】:
import numpy as np# 定义Ridge回归损失函数
def l2_loss(X, y, w, b, alpha):# 训练样本数num_train = X.shape[0]# 训练特征数# num_feature = X.shape[1]# 回归模型预测输出y_hat = np.dot(X, w) + b# L2损失函数loss = np.sum((y_hat - y) ** 2) / num_train + alpha * (np.sum(np.square(w)))# 参数梯度计算dw = np.dot(X.T, (y_hat - y)) / num_train + 2 * alpha * wdb = np.sum((y_hat - y)) / num_trainreturn y_hat, loss, dw, db# 初始化模型参数
def initialize_params(dims):# 将权重向量初始化为零向量w = np.zeros((dims, 1))# 将偏置参数初始化为0b = 0return w, b# 定义Ridge回归模型的训练过程
def ridge_train(X, y, learning_rate=0.01, epochs=1000):# 记录训练损失的空列表loss_his = []# 初始化模型参数w, b = initialize_params(X.shape[1])# 迭代训练for i in range(1, epochs):# 计算当前迭代的预测值、损失和梯度alpha = 0.1y_hat, loss, dw, db = l2_loss(X, y, w, b, alpha)# 基于梯度下降法的参数更新w += -learning_rate * dwb += -learning_rate * db# 记录当前迭代的损失loss_his.append(loss)# 每50次迭代打印当前损失信息# if i % 50 == 0:# print('epoch %d loss %f' % (i, loss))# 将迭代优化后的参数保存到字典中params = {'w':w, 'b':b}return loss_his, params# 导入数据集 (12,12)
data = np.genfromtxt('D:\PycharmProjects\wine_lasso_quality.txt', delimiter=';')
# 选择特征与标签
num_row = data.shape[0]
num_column = data.shape[1]
x = data[1:, 0:num_column-1]
y = data[1:, num_column-1].reshape(-1, 1)
# 加一列
X = np.column_stack((np.ones((num_row-1, 1)), x))
# 划分数据集,使用30%的数据作为测试集
offset = int(X.shape[0] * 0.7)
# 训练集 (8,12) & (8,1)
X_train, y_train = X[:offset], y[:offset]
# 测试集 (4,12) & (4,1)
X_test, y_test = X[offset:], y[offset:]
# 执行训练示例
learning_rate = 0.01
epochs = 100
loss_list, params = ridge_train(X_train, y_train, learning_rate, epochs)
# 获取训练参数
print('训练参数:\n', params)
# 将测试集应用在模型上
y_predict = np.dot(X_test, params['w']) + params['b']
print('测试集训练标签:\n', y_predict)
【结果展示】:
2)基于 Sklearn 的模型实现
【代码实现】:
import numpy as np
from sklearn.linear_model import Ridge# 导入数据集 (12,12)
data = np.genfromtxt('D:\PycharmProjects\wine_lasso_quality.txt', delimiter=';')
# 选择特征与标签
num_row = data.shape[0]
num_column = data.shape[1]
x = data[1:, 0:num_column-1]
y = data[1:, num_column-1].reshape(-1, 1)
# 加一列
X = np.column_stack((np.ones((num_row-1, 1)), x))
# 划分数据集,使用30%的数据作为测试集
offset = int(X.shape[0] * 0.7)
# 训练集 (8,12) & (8,1)
X_train, y_train = X[:offset], y[:offset]
# 测试集 (4,12) & (4,1)
X_test, y_test = X[offset:], y[offset:]# 创建Ridge回归模型实例
clf = Ridge(alpha=0.1)
# 对训练集进行拟合
clf.fit(X_train, y_train)
# 打印模型相关系数
print('sklearn LASSO intercept:(截距)', clf.intercept_)
print('\nsklearn LASSO coefficients:(系数)\n', clf.coef_)
【结果展示】:
可以看到,Ridge 回归参数大多比较接近 0 ,但都不等于 0 ,这也正是 Ridge 回归的一个特征。
从数学角度来看,LASSO 回归和 Ridge 回归都是在 XTX 为不可逆矩阵的情况下,求解回归参数的一种 “妥协” 性的方法。通过给常规的平方损失函数添加 L1 正则化项和 L2 正则化项,使得回归问题有可行解,虽然这种解是一种有偏估计。
但从业务可解释性的角度来看,影响一个变量的因素有很多,但关键因素永远只会是少数。当影响一个变量的因素有很多时(特征数可能会大于样本量),用传统的线性回归方法来处理可能效果会不太理想。LASSO 回归和 Ridge 回归通过对损失函数施加正则化项的方式,使得回归建模过程中大量不重要的特征系数被压缩为 0 或者接近 0 ,从而找出对目标变量有较强影响的关键特征。