第 32 课:正则化:Ridge、Lasso 与 ElasticNet
学习目标
- 理解正则化的目的及其在防止过拟合中的作用
- 掌握 Ridge (L2)、Lasso (L1) 和 ElasticNet 正则化的基本原理
- 学会使用 Scikit-learn 实现三种正则化回归模型
- 理解正则化强度参数 (alpha) 对模型的影响
- 能够根据实际问题选择合适的正则化方法
核心概念
为什么需要正则化?
在上一课的多项式回归中,我们看到当模型过于复杂时(例如,高次多项式),模型可能会完美拟合训练数据,但在新数据上表现很差——这就是过拟合。正则化通过给损失函数增加一个惩罚项,限制模型参数的大小,从而控制模型的复杂度,防止过拟合。
三种正则化方法
1. Ridge 回归 (L2 正则化)
- 在普通线性回归的损失函数上增加系数平方和的惩罚
- 损失函数:$L = \sum(y_{\text{真实}} - y_{\text{预测}})^2 + \alpha \sum \beta_j^2$
- 效果:将系数压缩到接近0,但不会变成0
- 适用场景:当特征间存在多重共线性,或我们希望保留所有特征时
2. Lasso 回归 (L1 正则化)
- 在损失函数上增加系数绝对值之和的惩罚
- 损失函数:$L = \sum(y_{\text{真实}} - y_{\text{预测}})^2 + \alpha \sum |\beta_j|$
- 效果:能够将某些系数压缩到完全为0,实现特征选择
- 适用场景:当特征很多且希望进行特征选择时
3. ElasticNet 回归
- 结合了 L1 和 L2 正则化
- 损失函数:$L = \sum(y_{\text{真实}} - y_{\text{预测}})^2 + \alpha \rho \sum |\beta_j| + \alpha(1-\rho) \sum \beta_j^2$
- 效果:平衡特征选择和系数收缩
- 适用场景:特征间高度相关且需要特征选择时
关键参数
- alpha (λ):正则化强度。alpha 越大,正则化越强,模型越简单
- l1_ratio (ρ):ElasticNet 专用,控制 L1 和 L2 的比例
代码示例
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet
from sklearn.metrics import mean_squared_error, r2_score
# 1. 生成非线性数据
np.random.seed(42)
X = np.sort(5 * np.random.rand(100, 1), axis=0)
y = np.sin(X).ravel() + 0.1 * np.random.randn(100)
# 添加噪声,创建过拟合场景
y[::5] += 3 * (0.5 - np.random.rand(20))
# 2. 创建多项式特征 (模拟过拟合场景)
degree = 10
poly = PolynomialFeatures(degree=degree, include_bias=False)
X_poly = poly.fit_transform(X)
# 3. 划分训练测试集
X_train, X_test, y_train, y_test = train_test_split(
X_poly, y, test_size=0.3, random_state=42
)
# 4. 标准化特征(正则化前必须做!)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 5. 训练不同模型
models = {
"Linear Regression": LinearRegression(),
"Ridge (α=0.1)": Ridge(alpha=0.1),
"Lasso (α=0.1)": Lasso(alpha=0.1, max_iter=10000),
"ElasticNet (α=0.1, ρ=0.5)": ElasticNet(alpha=0.1, l1_ratio=0.5, max_iter=10000)
}
results = {}
for name, model in models.items():
# 训练模型
model.fit(X_train_scaled, y_train)
# 预测
y_train_pred = model.predict(X_train_scaled)
y_test_pred = model.predict(X_test_scaled)
# 计算性能指标
train_mse = mean_squared_error(y_train, y_train_pred)
test_mse = mean_squared_error(y_test, y_test_pred)
train_r2 = r2_score(y_train, y_train_pred)
test_r2 = r2_score(y_test, y_test_pred)
results[name] = {
"train_mse": train_mse,
"test_mse": test_mse,
"train_r2": train_r2,
"test_r2": test_r2,
"coefficients": model.coef_
}
print(f"{name}:")
print(f" 训练 MSE: {train_mse:.4f}, R²: {train_r2:.4f}")
print(f" 测试 MSE: {test_mse:.4f}, R²: {test_r2:.4f}")
print(f" 非零系数数量: {np.sum(model.coef_ != 0)}/{len(model.coef_)}\n")
# 6. 可视化系数大小比较
plt.figure(figsize=(12, 6))
# 创建子图
plt.subplot(1, 2, 1)
for name, result in results.items():
if name != "Linear Regression":
plt.plot(result["coefficients"], 'o-', label=name, alpha=0.7)
plt.axhline(y=0, color='k', linestyle='--', alpha=0.3)
plt.xlabel("特征索引")
plt.ylabel("系数大小")
plt.title("不同正则化方法的系数比较")
plt.legend()
plt.grid(True, alpha=0.3)
# 可视化不同alpha值的影响
plt.subplot(1, 2, 2)
alphas = [0.001, 0.01, 0.1, 1, 10]
ridge_coefs = []
lasso_coefs = []
for alpha in alphas:
# Ridge
ridge = Ridge(alpha=alpha)
ridge.fit(X_train_scaled, y_train)
ridge_coefs.append(ridge.coef_)
# Lasso
lasso = Lasso(alpha=alpha, max_iter=10000)
lasso.fit(X_train_scaled, y_train)
lasso_coefs.append(lasso.coef_)
# 绘制系数路径
plt.plot(alphas, [np.sum(np.abs(coef)) for coef in ridge_coefs], 'b-o', label='Ridge (L1范数)')
plt.plot(alphas, [np.sum(np.abs(coef)) for coef in lasso_coefs], 'r-s', label='Lasso (L1范数)')
plt.xscale('log')
plt.xlabel("正则化强度 α (对数尺度)")
plt.ylabel("系数绝对值之和")
plt.title("正则化强度对系数的影响")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 7. 使用网格搜索找到最佳参数
from sklearn.model_selection import GridSearchCV
# 对ElasticNet进行网格搜索
param_grid = {
'alpha': [0.001, 0.01, 0.1, 1, 10],
'l1_ratio': [0.1, 0.3, 0.5, 0.7, 0.9]
}
elasticnet = ElasticNet(max_iter=10000, random_state=42)
grid_search = GridSearchCV(
elasticnet,
param_grid,
cv=5,
scoring='neg_mean_squared_error',
return_train_score=True
)
grid_search.fit(X_train_scaled, y_train)
print("最佳参数:", grid_search.best_params_)
print("最佳验证集 MSE:", -grid_search.best_score_)
# 使用最佳参数重新训练
best_model = grid_search.best_estimator_
y_test_pred = best_model.predict(X_test_scaled)
test_mse = mean_squared_error(y_test, y_test_pred)
test_r2 = r2_score(y_test, y_test_pred)
print(f"最佳模型在测试集上的表现 - MSE: {test_mse:.4f}, R²: {test_r2:.4f}")
实践练习
练习 1:基础 Ridge 回归
使用给定的波士顿房价数据(或使用 sklearn.datasets.load_boston),完成以下任务:
- 将数据集分为训练集和测试集
- 对数据进行标准化
- 使用 Ridge 回归(alpha=1.0)训练模型
- 计算并输出训练集和测试集的 MSE 和 R² 分数
预期输出示例:
训练集 MSE: 21.34, R²: 0.74
测试集 MSE: 24.56, R²: 0.71
练习 2:比较不同正则化方法
扩展练习 1,添加 Lasso 和 ElasticNet 回归:
- 训练 Lasso (alpha=0.5) 和 ElasticNet (alpha=0.5, l1_ratio=0.5)
- 比较三个模型的性能
- 分析哪些特征被 Lasso 完全排除(系数为0)
预期输出:
- 三个模型的性能对比表
- Lasso 选择的特征列表
练习 3:超参数调优(高级)
使用交叉验证为 ElasticNet 找到最佳参数:
- 设置参数网格:alpha ∈ [0.01, 0.1, 1, 10],l1_ratio ∈ [0.1, 0.5, 0.9]
- 使用 GridSearchCV 进行 5 折交叉验证
- 输出最佳参数组合及其在测试集上的性能
预期输出:
最佳参数: {'alpha': 0.1, 'l1_ratio': 0.9}
最佳测试 MSE: 18.92
常见错误
-
忘记标准化数据
- 正则化对特征的尺度敏感,必须先进行标准化
- 错误示例:直接使用原始数据训练 Ridge/Lasso
-
对正则化参数理解错误
- alpha 越大,正则化越强,模型越简单
- 错误:认为 alpha 越大模型越复杂
-
误解 Lasso 的特征选择
- Lasso 会将系数压缩到0,但并非总是有用的特征选择
- 错误:将所有系数为0的特征视为"无用特征"
-
忽略 ElasticNet 的 l1_ratio 参数
- l1_ratio=0 时等同于 Ridge,l1_ratio=1 时等同于 Lasso
- 错误:固定使用 l1_ratio=0.5,不尝试其他值
-
过度依赖默认参数
- 不同的 alpha 值可能导致完全不同的结果
- 错误:总是使用 alpha=1.0,不进行参数调优
-
在未标准化的数据上解释系数
- 标准化后的系数大小不能直接解释特征重要性
- 错误:直接比较标准化后系数的绝对值来判断特征重要性
小结
- 正则化的核心目的:通过惩罚模型复杂度,防止过拟合,提高泛化能力
- 三种正则化方法的特点:
- Ridge (L2):缩小所有系数,但不进行特征选择
- Lasso (L1):进行特征选择,将不重要特征的系数压缩为0
- ElasticNet:结合两者优点,适用于高度相关特征
- 关键参数:
alpha:控制正则化强度,需要通过交叉验证调优l1_ratio:ElasticNet 专用,控制 L1 和 L2 的平衡
- 实践要点:
- 正则化前必须对数据进行标准化
- 使用交叉验证选择最佳正则化参数
- 根据问题特点选择正则化方法:
- 特征间存在共线性 → Ridge
- 特征很多且需要特征选择 → Lasso
- 特征高度相关且需要特征选择 → ElasticNet
正则化是机器学习中控制模型复杂度的重要工具,正确使用可以显著提高模型的泛化性能。在下一课中,我们将学习如何使用评估指标来量化模型的性能。
练习编辑器
rust
Loading...