机器学习算法2-线性回归-归一化与正则化

Posted by Hilda on August 10, 2025

1.归一化(Normalization)

image-20250809163256507

梯度下降优化的效率受到不同特征维度(如\(x_1\) 和 \(x_2\))数值大小差异的影响。

在这种情况下,梯度下降的路径会非常曲折,需要在宽阔的维度上迈小步,在狭窄的维度上迈大步,导致收敛速度非常慢。

当对特征进行归一化处理后,它们的取值范围变得相似,损失函数的等高线会变得接近圆形,梯度下降可以沿着更直线的路径,高效地向中心收敛,大大提高了收敛速度。

归一化的目的是将不同特征的数值范围或数量级统一化,使得梯度下降在不同维度上的优化步伐能够协调一致,从而加快模型的收敛速度。

比如下面的例子就是未归一化之前的情况:

image-20250809164605245

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import numpy as np
import matplotlib.pyplot as plt

# 设置 Matplotlib 字体以支持中文和负号
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 1. 构造一个平缓、可控的非归一化损失函数
# 这是一个拉长的二次函数,其等高线是又扁又长的椭圆
# 我们用一个相对较小的 a 值和一个较大的 b 值来模拟
def cost_function_unnormalized(theta1, theta2):
    return 10 * theta1**2 + theta2**2

# 2. 绘制未归一化损失函数的等高线图
# 调整 theta1 和 theta2 的范围,使等高线能够被完全捕捉
# theta1 的范围较小,theta2 的范围较大,与损失函数系数相反,才能得到拉长的等高线
theta1_range = np.linspace(-3, 3, 200)
theta2_range = np.linspace(-6, 6, 200)

T1, T2 = np.meshgrid(theta1_range, theta2_range)
J_unnormalized = cost_function_unnormalized(T1, T2)

# 3. 绘制等高线图
plt.figure(figsize=(10, 8))

# levels参数设置得更直观,直接指定一系列值
levels = np.array([2, 5, 10, 20, 50, 100])
plt.contour(T1, T2, J_unnormalized, levels=levels, cmap='viridis')

# 4. 绘制梯度下降轨迹
# 模拟一个合适的初始点和学习率,以展示典型的“之”字形路径
start_theta = np.array([-2.5, 5.0])
learning_rate = 0.05 # 学习率要足够小,避免发散
n_iterations = 20
theta_history = [start_theta]

for _ in range(n_iterations):
    # 损失函数对 theta1 的导数:20 * theta1
    grad_theta1 = 20 * theta_history[-1][0]
    # 损失函数对 theta2 的导数:2 * theta2
    grad_theta2 = 2 * theta_history[-1][1]
    
    new_theta1 = theta_history[-1][0] - learning_rate * grad_theta1
    new_theta2 = theta_history[-1][1] - learning_rate * grad_theta2
    theta_history.append(np.array([new_theta1, new_theta2]))

theta_history = np.array(theta_history)
plt.plot(theta_history[:, 0], theta_history[:, 1], 'r-o', markersize=5, label='梯度下降轨迹')
plt.plot(theta_history[0, 0], theta_history[0, 1], 'go', markersize=10, label='初始点')
plt.plot(0, 0, 'y*', markersize=15, label='最优解')

# 5. 添加标题、标签和图例
plt.title('未归一化数据的损失函数等高线', fontsize=18)
plt.xlabel(r'$\theta_1$', fontsize=14)
plt.ylabel(r'$\theta_2$', fontsize=14)
plt.grid(True)
plt.axis('equal') # 保证x,y轴比例一致,让椭圆看起来更真实
plt.legend()
plt.show()

特别注意:通常不需要对标签(y)进行归一化,但是也不是绝对的,取决于你的模型和任务。

标签值通常具有明确的物理意义。例如,预测房价(万元)、预测气温(摄氏度)或预测销售额(元)。对这些值进行归一化,会使其失去原有的意义,预测结果也需要反归一化才能被理解和使用。

在大多数线性回归、深度学习等任务中,我们只对特征(X)进行归一化

1.1归一化的两种方法

最大最小值归一化 (Min-Max Normalization)

将原始数据线性变换,使其结果映射到 \([0,1]\) 之间。可以使用 sklearn.preprocessing.MinMaxScaler 完成

image-20250809163345076

  • 优点: 保证所有特征的数值都落在 \([0,1]\) 的固定区间内,非常直观。
  • 缺点: 对异常值(离群点)非常敏感。如果数据中有一个异常值,比如“马云的财富”,会导致 \(X_{max}\) 变得非常大,使得其他正常的数值归一化后都非常接近0,失去了数据的区分度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import numpy as np
from sklearn.preprocessing import MinMaxScaler, StandardScaler
from joblib import dump, load

# 1. 创建一个具有不同数量级特征的模拟数据集
# 特征1: 面积,取值范围 0-2000
x1 = np.random.randint(1, 2000, size=(10, 1))
# 特征2: 卧室数,取值范围 1-5
x2 = np.random.randint(1, 5, size=(10, 1))
# 合并成一个数据集
x = np.hstack([x1, x2])

print('--- 原始数据 ---')
print(x)

# 2. 最大最小值归一化 (Min-Max Normalization)

# 自定义实现
x_minmax_custom = (x - x.min(axis=0)) / (x.max(axis=0) - x.min(axis=0))
print('\n--- 最大最小值归一化后的数据 (自定义实现) ---')
print(x_minmax_custom)
print('各特征的最小值:', np.min(x_minmax_custom, axis=0))
print('各特征的最大值:', np.max(x_minmax_custom, axis=0))

# 使用sklearn实现
minmax_scaler = MinMaxScaler()
x_minmax_sklearn = minmax_scaler.fit_transform(x)
print('\n--- 最大最小值归一化后的数据 (sklearn实现) ---')
print(x_minmax_sklearn)
print('各特征的最小值:', np.min(x_minmax_sklearn, axis=0))
print('各特征的最大值:', np.max(x_minmax_sklearn, axis=0))

image-20250809164746018

0-1均值标准化 (Z-score Normalization)

对原始数据进行标准化,使其均值为0,标准差为1。处理后的数据符合标准正态分布。可以使用 sklearn.preprocessing.StandardScaler 完成

image-20250809163459480

  • 优点:
    • 不受异常值影响。因为标准差和均值的计算考虑了所有样本,异常值的影响被稀释了。
    • 在梯度下降中,标准化的处理使得梯度更容易沿正确的路径收敛。
  • 缺点: 标准化后的数据不一定落在 \([0,1]\) 的固定区间内。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 3. 0-1均值标准化 (Z-score Normalization)

# 自定义实现
x_zscore_custom = (x - x.mean(axis=0)) / x.std(axis=0)
print('\n--- 0-1均值标准化后的数据 (自定义实现) ---')
print(x_zscore_custom)
print('各特征的均值:', np.mean(x_zscore_custom, axis=0))
print('各特征的标准差:', np.std(x_zscore_custom, axis=0))

# 使用sklearn实现
standard_scaler = StandardScaler()
x_zscore_sklearn = standard_scaler.fit_transform(x)
print('\n--- 0-1均值标准化后的数据 (sklearn实现) ---')
print(x_zscore_sklearn)
print('各特征的均值:', np.mean(x_zscore_sklearn, axis=0))
print('各特征的标准差:', np.std(x_zscore_sklearn, axis=0))

1.2技巧:Scaler的持久化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 4. 演示Scaler的持久化和使用
# 在实际项目中,StandardScaler对象需要被保存,以便对新数据进行相同的处理
# 以StandardScaler为例,进行持久化
dump(standard_scaler, 'standard_scaler.joblib')
print('\n已将StandardScaler对象持久化到文件:standard_scaler.joblib')

# 模拟加载模型并处理新数据
loaded_scaler = load('standard_scaler.joblib')

# 模拟一个新到来的数据
new_data = np.array([[1500, 3], [500, 1]])
print('\n--- 新到来的数据 ---')
print(new_data)

# 使用加载的scaler对新数据进行转换
new_data_transformed = loaded_scaler.transform(new_data)
print('\n--- 经过StandardScaler处理后的新数据 ---')
print(new_data_transformed)

# 观察:转换后的数据均值接近0,标准差接近1,与之前训练集处理后的结果一致

image-20250809164931425


1.3总结

  • 在应用归一化时,应使用训练集的统计量(均值、标准差、最大值、最小值)来处理训练集、验证集和测试集
  • 为了在模型上线后处理新数据,需要将训练集计算得到的 scaler 对象持久化保存
  • MinMaxScalerStandardScalerfit 方法用于计算统计量,transform 方法用于应用转换,fit_transform 则是两者的结合。
归一化方法 适用场景 不适用场景 备注
Min-Max归一化 1. 数据范围已知且固定 (如图像像素)
2. 数据稀疏
1. 存在异常值
2. 数据分布范围不确定
对异常值敏感,是其最大的局限性。
Z-score标准化 1. 大多数机器学习算法
2. 数据近似正态分布
3. 存在异常值
数据需要保持在固定区间时 (如 [0, 1]) 更通用、更稳健,是机器学习中的首选

image-20250809165219356

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import numpy as np
import matplotlib.pyplot as plt

# 设置 Matplotlib 字体以支持中文和负号
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 1. 定义未归一化和归一化后的损失函数
# 这是一个二次函数,其等高线是椭圆,我们通过调整系数来模拟归一化前后的情况
def cost_function_unnormalized(theta1, theta2):
    return 10 * theta1**2 + theta2**2

def cost_function_normalized(theta1, theta2):
    # 假设归一化后,两个特征的尺度相近,所以系数也相近
    return theta1**2 + theta2**2

# 2. 模拟梯度下降过程
def run_gradient_descent(cost_func, start_theta, learning_rate, n_iterations):
    """
    运行梯度下降并返回参数轨迹。
    """
    theta_history = [start_theta]
    for _ in range(n_iterations):
        # 梯度计算(根据不同的损失函数)
        theta1, theta2 = theta_history[-1]
        
        if cost_func == cost_function_unnormalized:
            grad_theta1 = 20 * theta1
            grad_theta2 = 2 * theta2
        else: # cost_func == cost_function_normalized
            grad_theta1 = 2 * theta1
            grad_theta2 = 2 * theta2
            
        new_theta1 = theta1 - learning_rate * grad_theta1
        new_theta2 = theta2 - learning_rate * grad_theta2
        
        theta_history.append(np.array([new_theta1, new_theta2]))
        
    return np.array(theta_history)

# --- 参数设置 ---
# 未归一化
start_theta_unnorm = np.array([-2.5, 5.0])
learning_rate_unnorm = 0.05
n_iterations_unnorm = 20
theta_history_unnorm = run_gradient_descent(cost_function_unnormalized, start_theta_unnorm, learning_rate_unnorm, n_iterations_unnorm)

# 归一化
# 注意:归一化后,由于等高线更圆,我们可以使用更大的学习率来加快收敛
start_theta_norm = np.array([-2.5, 5.0])
learning_rate_norm = 0.3
n_iterations_norm = 20
theta_history_norm = run_gradient_descent(cost_function_normalized, start_theta_norm, learning_rate_norm, n_iterations_norm)


# 3. 可视化对比
fig, axes = plt.subplots(1, 2, figsize=(16, 8))

# --- 绘制未归一化等高线和轨迹 ---
ax = axes[0]
theta1_range = np.linspace(-3, 3, 200)
theta2_range = np.linspace(-6, 6, 200)
T1, T2 = np.meshgrid(theta1_range, theta2_range)
J_unnorm = cost_function_unnormalized(T1, T2)

levels = np.array([2, 5, 10, 20, 50, 100])
ax.contour(T1, T2, J_unnorm, levels=levels, cmap='viridis')
ax.plot(theta_history_unnorm[:, 0], theta_history_unnorm[:, 1], 'r-o', markersize=5, label='梯度下降轨迹')
ax.plot(theta_history_unnorm[0, 0], theta_history_unnorm[0, 1], 'go', markersize=10, label='初始点')
ax.plot(0, 0, 'y*', markersize=15, label='最优解')

ax.set_title(f'未归一化:之字形收敛\n学习率={learning_rate_unnorm}', fontsize=16)
ax.set_xlabel(r'$\theta_1$', fontsize=14)
ax.set_ylabel(r'$\theta_2$', fontsize=14)
ax.grid(True)
ax.axis('equal')
ax.legend()


# --- 绘制归一化后等高线和轨迹 ---
ax = axes[1]
theta1_range = np.linspace(-6, 6, 200)
theta2_range = np.linspace(-6, 6, 200)
T1, T2 = np.meshgrid(theta1_range, theta2_range)
J_norm = cost_function_normalized(T1, T2)

levels = np.array([2, 5, 10, 20, 50, 100])
ax.contour(T1, T2, J_norm, levels=levels, cmap='viridis')
ax.plot(theta_history_norm[:, 0], theta_history_norm[:, 1], 'r-o', markersize=5, label='梯度下降轨迹')
ax.plot(theta_history_norm[0, 0], theta_history_norm[0, 1], 'go', markersize=10, label='初始点')
ax.plot(0, 0, 'y*', markersize=15, label='最优解')

ax.set_title(f'归一化后:直线收敛\n学习率={learning_rate_norm}', fontsize=16)
ax.set_xlabel(r'$\theta_1$', fontsize=14)
ax.set_ylabel(r'$\theta_2$', fontsize=14)
ax.grid(True)
ax.axis('equal')
ax.legend()

plt.tight_layout()
plt.show()

2.正则化 (Regularization)

2.1过拟合与欠拟合 (Underfit & Overfit)

image-20250810135103765

欠拟合 (Underfit):模型过于简单,无法捕捉数据中的基本规律。(学习不足,没学好)

  • 效果: 训练集和测试集的准确率都很低,都没有达到理想水平。

过拟合 (Overfit):模型过于复杂,过度学习了训练数据中的噪声和细节。

  • 效果: 训练集的准确率很高,但测试集的准确率反而降低。

刚好拟合 (Just Right):模型复杂度适中,既能捕捉数据中的主要规律,又不会过度学习噪声。

  • 效果: 训练集和测试集的准确率都达到较高水平。

机器学习的目标就是找到一个刚好拟合的模型。正则化是解决过拟合问题的关键技术之一。

2.2正则化

正则化的目的是通过在损失函数中添加一个惩罚项,来限制模型参数的复杂度,从而避免过拟合。

损失函数: \(J=J_0+正则化项\)

  • \(J_0\) 是原始的损失函数(如均方误差MSE)。
  • 正则化项是用来惩罚模型复杂度的项。模型参数(权重 w)越大,惩罚项的值就越大,总损失 J 也就越大。

image-20250810155900617

L1正则化(也称为 Lasso 回归):

  • 正则化项: \(L_1=\alpha \sum_{i=1}^n∣w_i∣\)。
  • L1 项是所有参数绝对值之和,\(\alpha\) 是正则化系数,控制惩罚项的强度。

L2正则化(也称为 Ridge 回归):

  • 正则化项: \(L_2=\alpha \sum_{i=1}^n w_i^2\)。
  • L2 项是所有参数平方和。

L1和L2正则化通过调节模型参数来增加模型的鲁棒性(Robustness),使模型在面对未见过的数据时,也能有更好的泛化能力。

L1正则化 (Lasso): 惩罚项为参数绝对值之和,其几何等高线是菱形。它倾向于将不重要的参数权重压缩为0,因此具有稀疏性,可用于特征选择

L2正则化 (Ridge): 惩罚项为参数平方和,其几何等高线是圆形。它将参数压缩到接近0,但通常不会完全变为0。

L1 和 L2 正则项惩罚项可以加到任何算法的损失函数上面去提高计算出来模型的泛化能力

image-20250810143213042

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import numpy as np
import matplotlib.pyplot as plt

# 设置 Matplotlib 字体以支持中文和负号
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 1. 定义数据和损失函数
# 模拟两个参数 w1 和 w2
w1_range = np.linspace(-2, 2, 400)
w2_range = np.linspace(-2, 2, 400)
W1, W2 = np.meshgrid(w1_range, w2_range)

# 原始损失函数 (J0),其等高线是椭圆形
J0 = 0.8 * (W1 - 0.5)**2 + 0.3 * (W2 - 1.2)**2 
# 最优解在 (w1=0.5, w2=1.2)

# L1和L2正则化项
alpha = 0.5
L1_norm = alpha * (np.abs(W1) + np.abs(W2))
L2_norm = alpha * (W1**2 + W2**2)

# 2. 可视化 L1 正则化
fig, axes = plt.subplots(1, 2, figsize=(16, 8))

# --- L1 可视化 ---
ax = axes[0]
ax.set_title('L1正则化 (Lasso)', fontsize=18)
ax.set_xlabel(r'$w_1$', fontsize=14)
ax.set_ylabel(r'$w_2$', fontsize=14)
ax.set_aspect('equal', adjustable='box')
ax.grid(True, linestyle='--', alpha=0.6)

# 绘制原始损失函数的等高线 (椭圆)
ax.contour(W1, W2, J0, levels=np.linspace(0.1, 5, 20), cmap='viridis')
# 绘制L1惩罚项的等高线 (菱形)
ax.contour(W1, W2, L1_norm, levels=[0.5, 1.0, 1.5, 2.0], colors='red', linestyles='dashed', linewidths=2)

# 标记最优解
ax.plot(0.5, 1.2, 'y*', markersize=15, label='无正则化最优解')
# 模拟L1正则化最优解(在角点)
ax.plot(0, 0.9, 'g*', markersize=15, label='L1正则化最优解')
ax.legend()


# --- L2 可视化 ---
ax = axes[1]
ax.set_title('L2正则化 (Ridge)', fontsize=18)
ax.set_xlabel(r'$w_1$', fontsize=14)
ax.set_ylabel(r'$w_2$', fontsize=14)
ax.set_aspect('equal', adjustable='box')
ax.grid(True, linestyle='--', alpha=0.6)

# 绘制原始损失函数的等高线 (椭圆)
ax.contour(W1, W2, J0, levels=np.linspace(0.1, 5, 20), cmap='viridis')
# 绘制L2惩罚项的等高线 (圆形)
ax.contour(W1, W2, L2_norm, levels=[0.5, 1.0, 1.5, 2.0], colors='red', linestyles='dashed', linewidths=2)

# 标记最优解
ax.plot(0.5, 1.2, 'y*', markersize=15, label='无正则化最优解')
# 模拟L2正则化最优解(在非坐标轴位置)
ax.plot(0.2, 0.8, 'g*', markersize=15, label='L2正则化最优解')
ax.legend()

plt.tight_layout()
plt.show()

2.3Lasso回归

image-20250810160234113

α 是L1正则化项前面的系数,它的作用是控制正则化惩罚的强度。

因为 惩罚项公式是 \(L1=α∑∣w_i∣\),所以L1等高线的方程是 \(∑∣w_i∣=\frac{C}{\alpha}\)(其中 C 是常数)。

当 α 越小,分母 α 变小,\(\frac{C}{\alpha}\)变大,因此等高线的半径(菱形的大小)会变大。表示对参数 w 的惩罚越弱,模型更倾向于拟合训练数据,参数可以取较大的值。

当 α 越大,分母 α 变大,\(\frac{C}{\alpha}\) 变小,因此等高线的半径(菱形的大小)会变小。表示对参数 w 的惩罚越强,模型越倾向于将参数压缩到0。

  • α 越小,L1的等高线(菱形)越大。这时,L1的菱形等高线与原始损失函数(椭圆形)等高线相交的位置离原点更远,最优解的参数值会比较大,类似于没有正则化的情况,容易导致过拟合
  • α 越大,L1的等高线(菱形)越小。这时,L1的等高线被压缩得非常小,甚至小到只在原点附近与损失函数的等高线相交。

当 \(\alpha\)大到一定程度时,相交点就会位于L1等高线的“角”上,从而导致其中一个参数为0。这正是L1正则化实现特征选择的机制。

当 \(\alpha\) 很大时,L1的等高线非常小,与损失函数等高线相交的点会非常接近原点,导致非零参数的值也变得很小,模型参数的整体大小都被有效地控制住了,从而避免过拟合。

【更新规则】

回顾梯度下降法的通用参数更新规则:image-20250810161941029

image-20250810162003497

现在计算L1正则化后的损失函数 J 的梯度。

L1正则化后的总损失函数 J 被分解为两部分:原始的线性回归损失函数\(J_0\),即均方误差(MSE);L1正则化项,即参数绝对值之和 \(α∑∣w_i∣\)。

根据导数的线性性质,总损失 J 的梯度也等于两部分梯度的和:\(\frac{∂J}{∂θ_j}=\frac{∂J_0}{∂θ_j}+\frac{∂L_1}{∂θ_j}\)

image-20250810162414196

其中\(sgn(w_j)\)是一个符号函数:image-20250810162516686

最终得到了L1正则化参数更新的最终公式:

image-20250810162550344

这个公式与标准线性回归的更新公式相比,多了一个惩罚项:\(−ηα∗sgn(w_j)\)。

这个额外的惩罚项是L1正则化的核心,它的作用是将参数向0拉近

image-20250810163215583

这种“每次都强制减去或加上一个固定值 \(ηα\)”的特性,使得L1正则化能够将不重要的参数值直接变为0,从而实现特征选择

有些书本将L1正则化系数用 \(λ\) 表示,这只是符号上的不同,其含义和作用与 \(α\) 完全相同。

注:下面公式是sklearn官方给出的公式(矩阵形式)

image-20250810192214745

2.4Ridge回归

image-20250810185922540

\(J_0\)是原始的线性回归损失函数,通常采用均方误差(MSE)。我们的目标是最小化这个损失,以使模型的预测值 \(h_θ(x^{(i)})\) 尽可能接近真实值 \(y^{(i)}\)。

\(L_2\)是L2正则化项,也称为L2范数。它等于所有模型参数 \(w_i\) 的平方和,再乘以一个正则化系数\(α\)。

\(J\)这是最终的、带有L2正则化的总损失函数。通过最小化 \(J\),我们不仅要使模型拟合数据(最小化 \(J_0\)),还要让模型参数尽可能小(最小化 \(L_2\))。

推导L2正则化下的参数更新公式:

总损失 \(J\) 的梯度被分解为两部分:原始损失的梯度\(\frac{∂J_0}{∂θ_j}\) 和L2正则化项的梯度\(\frac{∂L_2}{∂θ_j}\)。

image-20250810190310160

将这两个梯度项代入通用梯度下降公式 \(θ_j^{n+1}=θ_j^n−η\frac{∂J}{∂θ_j}\),得到最终的更新公式:

image-20250810191331455

也可以提取公因式:

image-20250810191455200

注:这里的 α 实际上包含了原来的 2α。(系数2吸收进 α了,应该本来是乘以1−2ηα)

每次更新时,参数 \(θ_j^n\) 都会先乘以一个小于1的因子 (\(1−ηα\))

这个因子会使参数 \(θ_j^n\) 绝对值减小,因此L2正则化被称为权重衰减(weight decay)

然后再减去原始梯度项,继续拟合数据。

L2正则化在每次迭代中,都强制性地减小了参数的绝对值,从而防止参数变得过大,实现了防止过拟合的效果,增加了模型的鲁棒性。

由于L2惩罚项的等高线是平滑的圆形,它与椭圆形等高线相切相交时,相交点极少会正好落在坐标轴上。因此,L2正则化不具备将参数直接压缩为0的稀疏性,无法用于特征选择。

注:下面公式是sklearn官方给出的公式(矩阵形式)

image-20250810192142660

2.5sklearn的岭回归与Lasso回归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import numpy as np
from sklearn.linear_model import Ridge, Lasso, SGDRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

def run_ridge_regression(n_features, alpha_list):
    """
    运行Ridge回归,并对比不同alpha值的影响。
    """
    print(f"--- 运行 Ridge 回归,特征数={n_features} ---")

    # 1. 创建数据集X,y
    n_samples = 100
    w_true = np.random.randint(1, 10, size=(n_features, 1))
    b_true = np.random.randint(1, 10, size=1)
    X = 2 * np.random.rand(n_samples, n_features)
    y = X.dot(w_true) + b_true + np.random.randn(n_samples, 1)

    print('真实系数是:', w_true.ravel())
    print('真实截距是:', b_true)

    for alpha in alpha_list:
        # 使用Pipeline先标准化再训练,更规范
        ridge_pipeline = Pipeline([
            ('scaler', StandardScaler()),
            ('ridge', Ridge(alpha=alpha, solver='sag', random_state=42))
        ])
        ridge_pipeline.fit(X, y)
        
        # 从Pipeline中获取模型参数
        ridge_model = ridge_pipeline.named_steps['ridge']
        coef = ridge_model.coef_.ravel()
        intercept = ridge_model.intercept_
        
        print(f"\n--- Ridge (alpha={alpha}) ---")
        print('求解得到的系数是:', coef)
        print('求解得到的截距是:', intercept)
        print(f'系数L2范数之和: {np.sum(coef**2):.2f}')
    
    print("--- Ridge 回归结束 ---\n")

def run_lasso_regression(n_features, alpha_list):
    """
    运行Lasso回归,并对比不同alpha值的影响。
    """
    print(f"--- 运行 Lasso 回归,特征数={n_features} ---")

    # 1. 创建数据集X,y
    n_samples = 100
    w_true = np.random.randint(1, 10, size=(n_features, 1))
    b_true = np.random.randint(1, 10, size=1)
    X = 2 * np.random.rand(n_samples, n_features)
    y = X.dot(w_true) + b_true + np.random.randn(n_samples, 1)

    print('真实系数是:', w_true.ravel())
    print('真实截距是:', b_true)
    
    for alpha in alpha_list:
        lasso_pipeline = Pipeline([
            ('scaler', StandardScaler()),
            ('lasso', Lasso(alpha=alpha, random_state=42))
        ])
        lasso_pipeline.fit(X, y)
        
        lasso_model = lasso_pipeline.named_steps['lasso']
        coef = lasso_model.coef_.ravel()
        intercept = lasso_model.intercept_
        
        print(f"\n--- Lasso (alpha={alpha}) ---")
        print('求解得到的系数是:', coef)
        print('求解得到的截距是:', intercept)
        print(f'非零系数数量: {np.sum(coef != 0)}')
        
    print("--- Lasso 回归结束 ---\n")


# 运行示例
run_ridge_regression(n_features=5, alpha_list=[0.1, 1.0, 10.0])
run_lasso_regression(n_features=20, alpha_list=[0.01, 0.1, 1.0])

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
--- 运行 Ridge 回归特征数=5 ---
真实系数是: [7 1 4 4 5]
真实截距是: [7]

--- Ridge (alpha=0.1) ---
求解得到的系数是: [3.88325326 0.485939   2.2188067  2.15102884 2.95363648]
求解得到的截距是: [28.36730483]
系数L2范数之和: 33.59

--- Ridge (alpha=1.0) ---
求解得到的系数是: [3.84864021 0.48524549 2.19999549 2.13536089 2.92595118]
求解得到的截距是: [28.36730483]
系数L2范数之和: 33.01

--- Ridge (alpha=10.0) ---
求解得到的系数是: [3.53466527 0.47483545 2.02865551 1.99106369 2.67527307]
求解得到的截距是: [28.36730483]
系数L2范数之和: 27.96
--- Ridge 回归结束 ---

--- 运行 Lasso 回归特征数=20 ---
真实系数是: [3 6 1 7 1 9 1 1 7 2 5 8 8 3 7 3 6 6 3 7]
真实截距是: [3]

--- Lasso (alpha=0.01) ---
求解得到的系数是: [1.71805221 3.62045461 0.61881976 3.79011797 0.58778622 5.50322165
 0.64129722 0.70482784 3.9746928  1.25201778 2.83025796 4.78596247
 4.53422295 1.73996259 3.80902682 1.7225133  3.46246065 3.39341479
 1.78950567 3.95358098]
求解得到的截距是: [96.45147968]
非零系数数量: 20

--- Lasso (alpha=0.1) ---
求解得到的系数是: [1.60734194 3.44013429 0.47984614 3.6577235  0.51285813 5.37722892
 0.61678585 0.55192351 3.70265358 1.08265346 2.85744022 4.69986216
 4.40579606 1.57520922 3.62848919 1.53596559 3.35472812 3.10676125
 1.65924678 3.80519759]
求解得到的截距是: [96.45147968]
非零系数数量: 20

--- Lasso (alpha=1.0) ---
求解得到的系数是: [0.60863589 1.77887291 0.         2.35838296 0.         4.14171351
 0.50245925 0.         1.37531228 0.         2.93709894 3.75374493
 3.13428597 0.04363814 1.85255855 0.         2.37450251 0.51110751
 0.72201241 2.41528787]
求解得到的截距是: [96.45147968]
非零系数数量: 15
--- Lasso 回归结束 ---