简介:这份文档面向机器学习初学者与需要巩固回归算法基础的开发者,聚焦岭回归这一经典线性模型,帮助读者理解多重共线性与过拟合问题的解决思路。内容从岭回归的基本概念讲起,对比普通最小二乘法的差异,并给出损失函数与解析解的数学推导,再通过Python代码演示模型构建、参数调节与权重计算过程。资源包共1个docx文件,约27KB,以图文与代码片段结合的方式组织,涵盖数据预处理、特征缩放、数据集分割及模型评估等完整流程,便于边学边练。文档中提供了基于sklearn的Ridge实现示例,也展示了用numpy手动求解权重向量的方法,读者可据此掌握正则化参数λ对模型复杂度的影响,并学会用均方误差与R²评估预测性能。目前已有117人学习,适合希望系统入门AI算法、提升回归建模能力的读者参考。
1. 岭回归到底解决什么问题:从一次房价预测翻车说起
去年帮朋友做一个二手房估价的小工具,特征里同时放了「建筑面积」「套内面积」「公摊面积」三个字段。用普通最小二乘跑出来,建筑面积的系数是正的,套内面积的系数居然是负的——面积越大价格越低,这显然违背常识。模型在训练集上 R² 有 0.89,换一批测试数据直接掉到 0.6。这就是典型的多重共线性:特征之间高度相关,OLS 求逆时矩阵接近奇异,权重估计被放大到失真。岭回归(Ridge Regression)就是冲着这个问题来的,它在损失函数里加了一个 L2 正则项 λ∑w²,等价于在 XᵀX 对角线加上 λI 再求逆,把病态矩阵拉回可逆区间。这份《人工智能和机器学习之回归算法:岭回归:使用 Python 实现岭回归》文档,从数学原理、sklearn 实现到房价案例、Lasso/ElasticNet 对比都覆盖了,适合刚学完线性回归、想搞明白正则化到底在干什么的人,也适合手头有共线性数据、需要一套能直接跑的 Python 代码的从业者。
2. 岭回归的数学底子:解析解、λ 与标准化
2.1 从损失函数到闭式解
岭回归的损失函数是在残差平方和后面加一项惩罚:
Loss = Σ(yᵢ − ŷᵢ)² + λΣwⱼ²
对 w 求导令其为零,得到闭式解:
w = (XᵀX + λI)⁻¹Xᵀy
关键就在那个 λI。当特征之间存在共线性时,XᵀX 的行列式接近 0,直接求逆会数值爆炸;加上 λI 之后,对角线被抬高,最小特征值从接近 0 变成至少 λ,矩阵条件数大幅下降。λ 越大,权重被压得越狠,模型越保守;λ 趋近 0 时退化成 OLS,λ 趋近无穷时所有权重趋近 0。文档里给的 numpy 手算版本很直白,我把它整理成可直接跑的脚本:
import numpy as np np.random.seed(0) X = np.random.rand(100, 10) y = np.random.rand(100) # 添加偏置项,对应截距 X = np.c_[np.ones(X.shape[0]), X] lambda_ = 1.0 # 正则化参数,对应 sklearn 的 alpha # 闭式解:w = (X^T X + λI)^(-1) X^T y # 注意:偏置项通常不参与正则化,这里为演示统一处理 w = np.linalg.inv(X.T @ X + lambda_ * np.eye(X.shape[1])) @ X.T @ y print("Weights:", w)这段代码里np.eye(X.shape[1])生成的是 (11, 11) 的单位阵,因为加了偏置列后特征维度变成 11。lambda_就是 sklearn 里的alpha,两者是同一个东西,只是叫法不同。实际工程中我不会用np.linalg.inv直接求逆,而是用np.linalg.solve解线性方程组,数值稳定性更好:
w = np.linalg.solve(X.T @ X + lambda_ * np.eye(X.shape[1]), X.T @ y)2.2 为什么标准化不是可选项
岭回归的惩罚项 λ∑w² 对所有系数一视同仁。如果特征量纲差异大——比如一个特征是面积(几十到几百),另一个是房间数(1 到 5)——那么面积对应的系数自然会被压得很小,而房间数的系数相对较大,惩罚的力度就不公平了。结果就是:量纲大的特征被过度惩罚,量纲小的特征惩罚不足。所以做岭回归之前,标准化是必须的,不是可选项。常见做法是用StandardScaler做 Z-score 标准化:
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 注意:测试集用训练集的均值和方差这里有个容易翻车的点:fit_transform只能在训练集上调用,测试集必须用transform,否则测试集的统计量泄漏到训练过程里,评估结果会虚高。我见过有人图省事对整个数据集做fit_transform再划分,交叉验证的分数漂亮得不像话,上线就崩。
2.3 λ 的物理含义与量级直觉
λ 控制的是「拟合精度」和「权重幅度」之间的权衡。λ 太小,正则化形同虚设,共线性问题照旧;λ 太大,所有权重被压向零,模型欠拟合,预测值趋近于目标均值。实践中 λ 的候选范围通常取对数刻度,比如[0.001, 0.01, 0.1, 1, 10, 100],用交叉验证挑。文档里用GridSearchCV做 5 折交叉验证选 α,这是标准做法。需要提醒的是:Ridge的alpha参数和RidgeCV的alphas数组含义一致,但RidgeCV内部用的是高效的留一交叉验证(LOO),小数据集上比GridSearchCV快很多。
3. 用 scikit-learn 跑通完整流程:从 CSV 到预测
3.1 数据清洗与异常值处理
文档里给的流程是:加载 CSV → 检查缺失值 → 均值填充 → Z-score 删除异常值。这套流程在中小规模结构化数据上够用,但有几个细节值得展开。均值填充适合数值型特征且缺失比例低(比如低于 5%)的情况;如果缺失比例高,均值填充会引入偏差,考虑用中位数或模型填充。Z-score 删除异常值的阈值设为 3,意味着剔除偏离均值超过 3 个标准差的样本,这个阈值在正态分布假设下大约剔除 0.3% 的数据,比较保守。如果数据本身厚尾,可以放宽到 3.5 或改用 IQR 方法。
import pandas as pd import numpy as np from scipy import stats data = pd.read_csv('data.csv') # 检查缺失值 print(data.isnull().sum()) # 均值填充 data.fillna(data.mean(), inplace=True) # Z-score 删除异常值,阈值 3 data = data[(np.abs(stats.zscore(data)) < 3).all(axis=1)]stats.zscore(data)会对每一列计算 Z 分数,.all(axis=1)要求所有列的 Z 分数都小于 3 才保留该行。注意zscore默认按列计算,如果数据里有非数值列会报错,需要先做类型筛选。
3.2 特征缩放与数据集划分的顺序
顺序很重要:先划分训练集和测试集,再分别做标准化。文档里的代码是先标准化整个data_scaled再划分,这在演示里没问题,但严格来说有数据泄漏风险。正确的顺序是:
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X = data.drop('target', axis=1).values y = data['target'].values X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test)random_state=42保证每次划分结果一致,方便复现。test_size=0.2是 80/20 划分,样本量小于 1000 时可以考虑 70/30 或做交叉验证。
3.3 模型训练、调参与评估
创建Ridge模型、拟合、用GridSearchCV调 α、评估 MSE 和 R²,这套流程文档里写得很完整。我把关键代码串起来,并补上几个实用参数:
from sklearn.linear_model import Ridge from sklearn.model_selection import GridSearchCV from sklearn.metrics import mean_squared_error, r2_score # 参数网格,对数刻度 param_grid = {'alpha': [0.001, 0.01, 0.1, 1.0, 10.0, 100.0]} ridge = Ridge() grid_search = GridSearchCV( ridge, param_grid, cv=5, # 5 折交叉验证 scoring='neg_mean_squared_error', # 优化目标 n_jobs=-1 # 并行加速 ) grid_search.fit(X_train, y_train) best_alpha = grid_search.best_params_['alpha'] print(f"最佳 alpha: {best_alpha}") # 用最佳参数重新训练 ridge_best = Ridge(alpha=best_alpha) ridge_best.fit(X_train, y_train) y_pred = ridge_best.predict(X_test) mse = mean_squared_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) print(f"MSE: {mse:.4f}, R²: {r2:.4f}")scoring='neg_mean_squared_error'是因为 sklearn 的交叉验证默认「分数越大越好」,而 MSE 是越小越好,所以取负值。n_jobs=-1用满所有 CPU 核心,参数网格大的时候能省不少时间。Ridge还有一个solver参数,默认是'auto',小数据选'cholesky',大数据或稀疏矩阵选'sag'或'saga'。如果特征数远大于样本数,'sag'比闭式解快得多。
3.4 结果可视化与残差检查
文档里画了真实值 vs 预测值的散点图,这是最直观的评估方式。理想情况下点应该沿 45 度线分布。我一般还会补一张残差图:
import matplotlib.pyplot as plt residuals = y_test - y_pred fig, axes = plt.subplots(1, 2, figsize=(12, 5)) axes[0].scatter(y_test, y_pred, alpha=0.6) axes[0].plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--') axes[0].set_xlabel('真实值') axes[0].set_ylabel('预测值') axes[0].set_title('预测 vs 真实') axes[1].scatter(y_pred, residuals, alpha=0.6) axes[1].axhline(y=0, color='r', linestyle='--') axes[1].set_xlabel('预测值') axes[1].set_ylabel('残差') axes[1].set_title('残差分布') plt.tight_layout() plt.show()残差图如果呈现喇叭形(异方差)或曲线趋势(非线性),说明线性假设不成立,需要考虑多项式特征或换模型。残差围绕 0 均匀分布才是正常状态。
4. 避坑与排查:岭回归落地时最容易翻车的五件事
4.1 忘记标准化导致系数不可解释
现象:模型跑出来某个特征的系数特别大,另一个特别小,和业务直觉不符。原因:特征量纲差异大,惩罚项对不同特征的力度不一致。解决:训练前对所有数值特征做StandardScaler,并且保存 scaler 对象用于线上推理。如果业务上需要解释系数,标准化后的系数反映的是「特征每变化一个标准差,目标变化多少」,比原始系数更有可比性。
4.2 把 alpha 设成 0 或极小值
现象:岭回归的结果和 OLS 几乎一样,共线性问题没解决。原因:alpha=0时岭回归退化为 OLS,正则化项完全失效。有些人从alpha=0.0001开始搜,搜出来的最优值还是很小,等于没正则化。解决:参数网格从0.001起步,覆盖到100甚至1000。如果最优 alpha 落在网格边界,扩大搜索范围。另外可以用RidgeCV配合alphas=np.logspace(-3, 3, 50)做更细的搜索。
4.3 对偏置项也做了正则化
现象:模型预测值整体偏移,均值对不上。原因:闭式解里如果对偏置项 b 也加了 λ 惩罚,b 会被压向 0,导致预测值整体偏移。解决:sklearn 的Ridge默认fit_intercept=True,内部对偏置项不做正则化,所以用 sklearn 不用担心。但手写闭式解时要注意:np.eye的第一个对角元素应该设为 0,或者先把数据中心化再拟合。
4.4 交叉验证时数据泄漏
现象:交叉验证分数很高,测试集分数差很多。原因:在划分训练测试集之前做了标准化或缺失值填充,测试集的统计量泄漏到了训练过程。解决:用Pipeline把标准化和模型串起来,交叉验证时 sklearn 会自动在每个折内独立做标准化:
from sklearn.pipeline import Pipeline pipe = Pipeline([ ('scaler', StandardScaler()), ('ridge', Ridge()) ]) param_grid = {'ridge__alpha': [0.001, 0.01, 0.1, 1.0, 10.0]} grid = GridSearchCV(pipe, param_grid, cv=5) grid.fit(X_train, y_train)注意参数名要加前缀ridge__alpha,因为alpha是 Pipeline 中ridge这一步的参数。
4.5 高维数据下用闭式解导致内存爆炸
现象:特征数上万时,Ridge拟合报内存错误或跑得极慢。原因:闭式解需要计算 (XᵀX + λI)⁻¹,XᵀX 是 p×p 矩阵,p 上万时求逆的复杂度是 O(p³)。解决:改用随机梯度下降类求解器:
ridge = Ridge(alpha=1.0, solver='sag', max_iter=1000, tol=1e-4)sag和saga的复杂度与样本数线性相关,适合大样本高维场景。max_iter和tol控制收敛条件,如果收敛警告频繁出现,调大max_iter或放宽tol。
5. 进阶用法:Lasso/ElasticNet 对比与高维场景验证
5.1 三种正则化的选择逻辑
岭回归用 L2 惩罚,系数被压缩但不为零,适合「所有特征都有用但存在共线性」的场景。Lasso 用 L1 惩罚,能把不重要的特征系数压到恰好为零,自带特征选择,适合「特征多但真正有用的少」的场景。ElasticNet 是两者的加权组合,l1_ratio控制 L1 的占比:l1_ratio=1退化成 Lasso,l1_ratio=0退化成岭回归。文档里给了三者的代码示例,我补一个对比表格:
| 方法 | 正则项 | 系数是否稀疏 | 适合场景 | sklearn 类 |
|---|---|---|---|---|
| OLS | 无 | 否 | 特征少、无共线性 | LinearRegression |
| Ridge | L2 | 否 | 特征多、有共线性 | Ridge |
| Lasso | L1 | 是 | 特征多、稀疏解 | Lasso |
| ElasticNet | L1+L2 | 是 | 特征多、有共线性且需选择 | ElasticNet |
选择逻辑:先看特征之间有没有共线性(算相关矩阵或 VIF),有共线性优先岭回归;再看是否需要特征选择,需要就上 Lasso 或 ElasticNet。如果拿不准,ElasticNet 的l1_ratio用交叉验证一起调,通常不会比单独用 Lasso 或 Ridge 差太多。
5.2 高维小样本下的验证方法
文档里提到「100 个样本、1000 个特征」的高维场景,岭回归能防止过拟合。但高维场景下评估模型不能只看一次训练测试划分,因为样本量小,划分的随机性对结果影响很大。我一般用嵌套交叉验证:
from sklearn.model_selection import cross_val_score, KFold from sklearn.linear_model import RidgeCV import numpy as np # 生成高维数据 np.random.seed(42) X_high = np.random.randn(100, 1000) y_high = np.random.randn(100) # RidgeCV 内置 LOO 交叉验证,适合小样本 alphas = np.logspace(-3, 3, 50) ridge_cv = RidgeCV(alphas=alphas, cv=5) ridge_cv.fit(X_high, y_high) print(f"最佳 alpha: {ridge_cv.alpha_:.4f}") print(f"训练集 R²: {ridge_cv.score(X_high, y_high):.4f}") # 外层交叉验证评估泛化性能 outer_scores = cross_val_score( RidgeCV(alphas=alphas), X_high, y_high, cv=KFold(n_splits=5, shuffle=True, random_state=42), scoring='r2' ) print(f"外层 CV R²: {outer_scores.mean():.4f} ± {outer_scores.std():.4f}")RidgeCV的alpha_属性返回选中的最佳 alpha,带下划线后缀是 sklearn 的约定,表示从数据中学到的属性。外层交叉验证的分数才是对泛化性能的无偏估计,内层选 alpha 的过程不参与外层评估。如果外层 R² 的方差很大(标准差超过 0.1),说明样本量太小,结果不稳定,需要收集更多数据或做特征降维。
5.3 一个我常用的验证习惯
每次跑完岭回归,我会强制做三件事:第一,打印前 10 个最大系数的特征名和系数值,确认方向符合业务直觉;第二,画残差图,确认没有明显的异方差或非线性模式;第三,用RidgeCV和GridSearchCV各跑一遍,如果两者选出的 alpha 差异超过一个数量级,说明数据本身对正则化强度不敏感,这时候我会倾向于选更大的 alpha,宁可保守一点。这个习惯帮我避免了好几次「交叉验证分数漂亮但上线就崩」的情况。从那以后我每次做线性模型都强制走一遍标准化检查、残差检查和双路调参,希望帮到你。
本文还有配套的精品资源,点击获取