跳到主要内容

岭回归

岭回归(Ridge Regression)是数学建模竞赛中处理「多重共线性」和「高维小样本」最常用的回归方法之一。它通过给最小二乘目标函数加一个 L2 惩罚项,牺牲少量偏差换取估计方差的大幅下降,从而得到更稳定、泛化能力更强的模型。本文从原理、适用场景、评价指标、可视化、完整代码到论文写作话术,一站式讲透岭回归。


一、算法含义

1.1 一句话理解

普通最小二乘(OLS)的解是 β^OLS=(XTX)1XTy\hat{\boldsymbol{\beta}}_{OLS}=(X^TX)^{-1}X^Ty。当自变量之间存在近似线性关系(多重共线性)时,矩阵 XTXX^TX 接近奇异(行列式接近 0),求逆时会把数据中微小的噪声放大成巨大的系数波动——同一个模型换一批数据,系数可能从 2 变成 200,甚至符号反转。

岭回归的解决办法非常「暴力但优雅」:XTXX^TX 的对角线上统一加上一个正数 λ\lambda,把它变成 XTX+λIX^TX+\lambda I。因为 II 是单位矩阵,对角线上每个位置都被「垫高」了 λ\lambda,矩阵从此远离奇异、永远可逆,系数估计也随之稳定下来:

β^λ=(XTX+λI)1XTy\hat{\boldsymbol{\beta}}_{\lambda}=(X^TX+\lambda I)^{-1}X^Ty

其中 λ0\lambda\ge 0 称为岭参数(正则化强度)IIp×pp\times p 单位矩阵。λ\lambda 越大,估计越稳定但偏差越大;λ=0\lambda=0 时退化为 OLS。这个「对角垫高」的操作,就是所谓的 L2 正则化(也叫 Tikhonov 正则化)。

1.2 目标函数的推导

岭回归的闭式解并不是凭空捏造的,它来自下面这个带惩罚的目标函数的最小化:

minβ  yXβ22残差平方和(拟合误差)+λβ22L2 惩罚(参数收缩)\min_{\boldsymbol{\beta}}\;\underbrace{\|y-X\boldsymbol{\beta}\|_2^2}_{\text{残差平方和(拟合误差)}}+\underbrace{\lambda\|\boldsymbol{\beta}\|_2^2}_{\text{L2 惩罚(参数收缩)}}

其中 β22=j=1pβj2\|\boldsymbol{\beta}\|_2^2=\sum_{j=1}^{p}\beta_j^2 是所有系数(不含截距)的平方和。直观理解:

  • 第一项要求「拟合误差尽量小」——与 OLS 相同;
  • 第二项要求「系数绝对值尽量小」——防止某些系数无节制地膨胀去拟合噪声;
  • λ\lambda 是两者的「谈判筹码」:λ\lambda 大则偏向小系数,λ\lambda 小则偏向拟合精度。

β\boldsymbol{\beta} 求梯度并令其为 0:

β[yXβ22+λβ22]=2XT(yXβ)+2λβ=0\frac{\partial}{\partial\boldsymbol{\beta}}\Big[\|y-X\boldsymbol{\beta}\|_2^2+\lambda\|\boldsymbol{\beta}\|_2^2\Big] =-2X^T(y-X\boldsymbol{\beta})+2\lambda\boldsymbol{\beta}=0

整理得到岭回归的正规方程:

(XTX+λI)β^λ=XTyβ^λ=(XTX+λI)1XTy(X^TX+\lambda I)\,\hat{\boldsymbol{\beta}}_{\lambda}=X^Ty \quad\Longrightarrow\quad \hat{\boldsymbol{\beta}}_{\lambda}=(X^TX+\lambda I)^{-1}X^Ty

注意:惩罚项通常不包含截距项 β0\beta_0——截距只负责平移,不参与共线性,收缩它没有意义。实际做法是先把 XXyy 中心化(或先标准化 XX),再对不含截距的部分做岭回归。

1.3 λ\lambda 的两个极端

  • λ=0\lambda=0:目标函数与 OLS 完全相同,β^0=β^OLS\hat{\boldsymbol{\beta}}_{0}=\hat{\boldsymbol{\beta}}_{OLS},即岭回归是 OLS 的推广。
  • λ\lambda\to\infty:惩罚项占据主导,最优解只能是 β^λ0\hat{\boldsymbol{\beta}}_{\lambda}\to \mathbf{0},即所有系数被压缩到 0,模型退化为「只预测均值 yˉ\bar y」。

可以证明,系数向量的长度 β^λ2\|\hat{\boldsymbol{\beta}}_{\lambda}\|_2λ\lambda 单调递减(这就是「参数收缩 / shrinkage」名字的由来),而训练集残差平方和随 λ\lambda 单调递增。因此 λ\lambda 的选取本质上是偏差-方差权衡(bias-variance tradeoff):岭回归是有偏估计,但方差比 OLS 小,二者的总误差(MSE)在某个适中的 λ\lambda 处达到最小。

1.4 为什么能解决共线性——条件数的视角

XTXX^TX 做特征分解 XTX=VΛVTX^TX=V\Lambda V^T,其中 Λ=diag(d1,,dp)\Lambda=\mathrm{diag}(d_1,\dots,d_p)dj0d_j\ge 0 是特征值。多重共线性意味着某个 djd_j 非常接近 0(甚至等于 0,如 p>np>n 时)。求逆 (XTX)1=VΛ1VT(X^TX)^{-1}=V\Lambda^{-1}V^T 要把 1/dj1/d_j 放大到无穷,这就是 OLS 不稳定的根源。

岭回归做的是 XTX+λI=V(Λ+λI)VTX^TX+\lambda I=V(\Lambda+\lambda I)V^T把所有特征值整体平移 λ\lambda,最小的特征值从 dmin0d_{\min}\approx 0 变成 dmin+λ>0d_{\min}+\lambda>0,逆矩阵中的元素从 1/dj1/d_j 变成 1/(dj+λ)1/(d_j+\lambda),不再爆炸。

条件数(condition number)定量刻画矩阵的「病态程度」:

κ(XTX+λI)=dmax+λdmin+λ    dmaxdmin=κ(XTX)\kappa(X^TX+\lambda I)=\frac{d_{\max}+\lambda}{d_{\min}+\lambda}\;\le\;\frac{d_{\max}}{d_{\min}}=\kappa(X^TX)

条件数衡量的是「解的相对误差最多被放大多少倍」。经验上:

  • κ<30\kappa<30:共线性轻微,OLS 可用;
  • 30κ<10030\le\kappa<100:中等共线性,需要警惕;
  • κ100\kappa\ge 100(甚至上千上万):严重共线性,OLS 的系数会剧烈波动,此时岭回归加一个很小的 λ\lambda 就能把条件数降几个数量级。

1.5 与 OLS、Lasso 的对比

对比维度OLS 最小二乘岭回归(L2)Lasso(L1)
目标函数yXβ22\|y-X\beta\|_2^2yXβ22+λβ22\|y-X\beta\|_2^2+\lambda\|\beta\|_2^2yXβ22+λβ1\|y-X\beta\|_2^2+\lambda\|\beta\|_1
惩罚项jβj2\sum_j\beta_j^2jβj\sum_j\|\beta_j\|
闭式解β^=(XTX)1XTy\hat\beta=(X^TX)^{-1}X^Tyβ^=(XTX+λI)1XTy\hat\beta=(X^TX+\lambda I)^{-1}X^Ty无闭式解,需坐标下降等迭代算法
解的性质无偏,方差大有偏,方差小,系数收缩但不为零有偏,能把系数压缩到恰好为 0
共线性崩溃(系数爆炸)稳定稳定,但可能随机丢掉某个共线变量
变量选择无(所有变量都保留)有(自动稀疏化)
p>np>n无唯一解可用最多选出 nn 个变量

几何直觉:L2 惩罚的可行域是圆(球),最优解通常落在圆周的「光滑」位置,系数一般不为 0;L1 惩罚的可行域是菱形,角点与等高线相切时系数恰好为 0,因此 Lasso 具有稀疏性。一句话区分:岭回归让系数变小,Lasso 让系数变没。

1.6 为什么必须先做数据标准化(量纲敏感)

岭回归的惩罚项 λjβj2\lambda\sum_j\beta_j^2所有系数一视同仁,但系数的大小与自变量的量纲直接相关。举例:同样一个变量,用「元」做单位时系数是 0.0003,用「万元」做单位时系数是 3——同样的惩罚力度 λ\lambda 对两者的收缩效果天差地别。如果 XX 各列量纲不同而不做标准化,岭回归会「无差别」地惩罚数值尺度大的变量对应的系数,收缩结果毫无意义。

因此使用岭回归的铁律:先对每个自变量做 z-score 标准化(均值 0、方差 1),再做岭回归。标准化后所有系数在同一把尺子上,惩罚才是公平的。注意两点:

  1. 标准化用的均值和标准差只能由训练集计算,测试集复用同一组统计量,否则会发生信息泄漏;
  2. 拟合完成后,若要解释原始量纲下的系数,需要按 βjorig=βjstd(sy/sxj)\beta_j^{orig}=\beta_j^{std}\cdot(s_y/s_{x_j}) 反标准化还原(ss 为标准差),本文第六节代码给出了完整实现。

1.7 优缺点小结

优点

  • 彻底解决 XTXX^TX 奇异/近奇异问题,p>np>n 的高维小样本也能解出唯一解;
  • 系数估计方差小、稳定性高,预测泛化能力通常优于 OLS;
  • 闭式解简单,计算量与 OLS 同阶(配合 SVD 几乎零额外成本);
  • λ\lambda 可用交叉验证/GCV 自动选取,无需人工调参技巧。

缺点

  • 有偏估计,无法获得像 OLS 那样的标准 tt 检验、pp 值等显著性推断结果;
  • 系数只是收缩、不会变为 0,无法做变量选择,模型解释性下降;
  • 结果对标准化非常敏感,忘记标准化会得到错误结论;
  • 多了一个超参数 λ\lambda,需要用 CV 等额外计算来确定。

二、何时使用(适用场景与条件)

2.1 适合用岭回归的情形

  1. 自变量存在多重共线性。判断信号:相关系数矩阵中某两个变量相关系数 r>0.9|r|>0.9;方差膨胀因子 VIFj=11Rj2>10\mathrm{VIF}_j=\frac{1}{1-R_j^2}>10Rj2R_j^2 是把 xjx_j 对其余自变量回归的拟合优度);设计矩阵条件数 κ(XTX)>100\kappa(X^TX)>100。此时 OLS 系数会剧烈波动甚至符号异常。
  2. 高维小样本(pnp\gg np>np>n。当变量个数超过样本量时,XTXX^TX 奇异,OLS 根本无解;岭回归由于 XTX+λIX^TX+\lambda I 永远可逆,依然能给出唯一稳定的解。
  3. 防止过拟合、提升预测泛化能力。变量多而样本少时,OLS 容易把噪声也「拟合」进去,岭回归通过收缩降低模型复杂度(有效自由度),测试集表现通常更好。
  4. OLS 系数符号与常识/理论矛盾时。例如理论上「收入对消费应有正向影响」,OLS 却估计出负系数,且该变量与其他变量高度相关——这就是共线性「搅浑水」的典型症状,换岭回归后符号往往恢复正常。
  5. 要求平滑稳定解的场合:需要预测结果随 λ\lambda 连续变化、不出现突变(Lasso 的解路径是分段线性的,会有变量突然进出模型)。

2.2 数学建模竞赛中的典型场景

  • 大量相关指标建模:经济类题目动辄十几个宏观指标(GDP、工业增加值、进出口……彼此高度相关),直接 OLS 必然共线性爆炸;
  • 预测类题目(房价、销量、能源负荷、传染病传播等):预测精度优先于系数解释,岭回归 + 交叉验证是最稳的基线模型;
  • 问卷/环境监测类:几十上百个特征但样本只有几十条(p>np>n),岭回归几乎是唯一能直接跑通的线性方法;
  • 稳定性展示型题目:论文中可用「OLS vs 岭回归」的对照实验展示「方法改进带来的稳定性提升」,是很好的亮点段落。

2.3 使用前提

  • 必须先标准化(z-score),否则 λ\lambda 的惩罚不公平,结论错误;
  • 训练集标准化统计量必须用于测试集(禁止信息泄漏);
  • 需要足够样本量做交叉验证来选 λ\lambda(一般 n50n\ge 50 用 5 折 CV 比较稳);
  • λ\lambda 的候选网格要覆盖「欠拟合到过拟合」的完整区间(典型做法:对数均匀网格 np.logspace(-4, 4, 100) 或从 κ\kappa 的量级出发定范围)。

2.4 不适用情形(考虑替代方法)

  • 需要稀疏解/变量选择时:题目要求「找出关键影响因素」或论文需要精简的变量集 → 用 Lasso(L1 正则把无关变量系数压到 0)或弹性网
  • 需要显著性检验、置信区间pp 值、tt 检验是 OLS 的专属,岭回归没有标准的显著性推断(需要 bootstrap 等额外手段)→ 若推断是硬需求,优先 OLS(先处理共线性,如删除变量/主成分);
  • 共线性不严重且 npn\gg p:OLS 已经很好,岭回归引入偏差得不偿失;
  • 需要绝对可解释的系数:岭收缩后的系数有偏,不能像 OLS 那样解释「每增加一单位 xxyy 平均变化多少」(严格说 OLS 在共线性下也解释不了,但至少无偏)。

2.5 岭回归、Lasso、弹性网的选择速查

情形推荐方法理由
共线性严重,只做预测岭回归稳定、有闭式解、CV 易调
变量很多,想筛出关键变量LassoL1 惩罚自动稀疏化
变量多且高度分组相关,既要稳定又要筛选弹性网L1+L2 混合惩罚,组内变量一起保留
pnp\gg n 且变量高度相关弹性网(或岭)纯 Lasso 在 p>np>n 时最多选 nn 个变量且选择不稳定
共线性不重、npn\gg p、要推断OLS无偏 + 完整显著性检验

弹性网的目标函数:minβ  yXβ22+λ1β1+λ2β22\min_{\beta}\;\|y-X\beta\|_2^2+\lambda_1\|\beta\|_1+\lambda_2\|\beta\|_2^2,是岭与 Lasso 的凸组合,兼具二者优点。


三、算法指标

本节逐一介绍岭回归建模中最常用的 6 类指标。每个指标给出中文名、公式、取值范围和解读要点,最后汇总成表格。

3.1 交叉验证均方误差与均方根误差(CV-MSE / CV-RMSE)

中文名:交叉验证均方误差 / 交叉验证均方根误差。这是选择 λ\lambda 的核心依据。

MSE=1ni=1n(yiy^i)2,RMSE=MSE=1ni=1n(yiy^i)2\mathrm{MSE}=\frac{1}{n}\sum_{i=1}^{n}(y_i-\hat y_i)^2,\qquad \mathrm{RMSE}=\sqrt{\mathrm{MSE}}=\sqrt{\frac{1}{n}\sum_{i=1}^{n}(y_i-\hat y_i)^2}
  • 取值范围[0,+)[0,+\infty),取值越小越好;与 yy 同量纲的是 RMSE(所以 RMSE 更直观,如「RMSE=0.85 千元」)。
  • 解读:对每个候选 λ\lambda,用 K 折交叉验证(常用 K=5K=5 或 10)计算验证集上的平均 RMSE,画出「CV 误差–λ」曲线,取 CV-RMSE 最小的 λ\lambda 作为 λ\lambda^*。曲线的典型形状是 U 型(或先降后平):λ\lambda 太小 → 近似 OLS → 方差大、验证误差高;λ\lambda 太大 → 系数收缩过度 → 欠拟合、验证误差升高。

3.2 决定系数 R2R^2

中文名:决定系数(拟合优度)。

R2=1RSSTSS=1i=1n(yiy^i)2i=1n(yiyˉ)2R^2=1-\frac{\mathrm{RSS}}{\mathrm{TSS}}=1-\frac{\sum_{i=1}^{n}(y_i-\hat y_i)^2}{\sum_{i=1}^{n}(y_i-\bar y)^2}
  • 取值范围(,1](-\infty,1]。等于 1 为完美拟合,等于 0 表示模型和「只预测均值」一样,负值(测试集上可能出现)表示比预测均值还差。
  • 解读:表示 yy 的变异中被模型解释的比例。训练集 R2R^2 衡量拟合程度;测试集 R2R^2 衡量泛化能力。训练 R2R^2 高而测试 R2R^2 低 → 过拟合。岭回归通常会小幅牺牲训练 R2R^2 换取测试 R2R^2 的提升,这恰恰是「用偏差换方差」的体现。

3.3 有效自由度 df(λ)\mathrm{df}(\lambda)

中文名:有效自由度(effective degrees of freedom)。

df(λ)=tr(Hλ)=tr(X(XTX+λI)1XT)=j=1pdjdj+λ\mathrm{df}(\lambda)=\mathrm{tr}\big(H_{\lambda}\big)=\mathrm{tr}\Big(X(X^TX+\lambda I)^{-1}X^T\Big)=\sum_{j=1}^{p}\frac{d_j}{d_j+\lambda}

其中 HλH_\lambda 是岭回归的「帽子矩阵」(y^=Hλy\hat y=H_\lambda y),djd_jXTXX^TX 的特征值(等于 XX 的奇异值平方)。

  • 取值范围(0,p](0,p]λ=0\lambda=0df=p\mathrm{df}=p(OLS 用满全部 pp 个自由度,含截距则为 p+1p+1);λ\lambda\to\inftydf0\mathrm{df}\to 0
  • 解读:有效自由度是衡量模型复杂度的通用标尺,可以理解为「被惩罚掉的变量个数」。df(λ)\mathrm{df}(\lambda) 越小模型越简单。它还有一个重要作用——GCV 和 AIC/BIC 等准则都要用到它。论文中写一句「在最优 λ\lambda 处模型有效自由度从 pp 降至 df(λ)\mathrm{df}(\lambda^*)」,比单纯说「系数变小」专业得多。

3.4 广义交叉验证 GCV

中文名:广义交叉验证(Generalized Cross-Validation)。

GCV(λ)=1nRSS(λ)(1df(λ)/n)2\mathrm{GCV}(\lambda)=\frac{\frac{1}{n}\mathrm{RSS}(\lambda)}{\Big(1-\mathrm{df}(\lambda)/n\Big)^2}
  • 取值范围:与 MSE 相同量级的正数,越小越好。
  • 解读:GCV 是留一交叉验证(LOOCV)的解析近似,不需要真正做 nn 次训练,借助奇异值分解 O(p)O(p) 就能算出全部 λ\lambda 网格上的值,计算量远小于 K 折 CV。取 GCV 最小的 λ\lambda 作为 λ\lambda^*。小样本时 GCV 的选参结果通常与 5 折 CV 非常接近;sklearn 的 RidgeCV 在传入 α\alpha 数组且不指定 cv 时,n10000n\le 10000 默认使用高效的留一交叉验证(LOOCV)——GCV 正是 LOOCV 的解析近似。本文示例中 GCV 选出的 λ1.23\lambda\approx 1.23,与 5 折 CV 的 1.701.70 非常接近(详见第六、七节)。

3.5 系数收缩幅度

中文名:系数收缩幅度(shrinkage ratio)。

s(λ)=β^λ2β^OLS2(0,1]s(\lambda)=\frac{\big\|\hat{\boldsymbol{\beta}}_{\lambda}\big\|_2}{\big\|\hat{\boldsymbol{\beta}}_{OLS}\big\|_2}\in(0,1]
  • 取值范围(0,1](0,1],越小表示收缩越强。
  • 解读:直接度量岭回归把系数「压小了多少」。s(λ)=0.6s(\lambda)=0.6 表示系数整体长度只剩 OLS 的 60%。也可以逐系数看相对变化 β^λ,j/β^OLS,j\big|\hat\beta_{\lambda,j}/\hat\beta_{OLS,j}\big|,那些被大幅压缩的系数通常正是共线性/噪声的载体。竞赛论文中「岭回归将异常膨胀的系数收缩至合理范围」的对比表就基于此指标。

3.6 条件数改善

中文名:条件数(condition number)及其改善倍数。

κ(XTX+λI)=dmax+λdmin+λ,κ(XTX)=dmaxdmin\kappa\big(X^TX+\lambda I\big)=\frac{d_{\max}+\lambda}{d_{\min}+\lambda},\qquad \kappa\big(X^TX\big)=\frac{d_{\max}}{d_{\min}}
  • 取值范围κ1\kappa\ge 1,越接近 1 数值稳定性越好;κ>100\kappa>100 视为严重共线性。
  • 解读:条件数衡量「扰动被放大的倍数」——若 κ=1000\kappa=1000,数据里 1% 的噪声可能导致解变动约 1000%。岭回归把 dmind_{\min} 抬到 dmin+λd_{\min}+\lambda,条件数断崖式下降(如从 10410^4 降到 5050),这是「岭回归稳定」最硬核的量化证据。论文中给出「条件数从 X 降至 Y,改善 Z 倍」非常加分。

3.7 指标汇总表

指标(中文名)公式取值范围解读要点
交叉验证 RMSERMSE=1n(yiy^i)2\mathrm{RMSE}=\sqrt{\frac1n\sum(y_i-\hat y_i)^2}(验证集上)[0,+)[0,+\infty)λ\lambda 的核心依据,取 CV-RMSE 最小的 λ\lambda^*;U 型曲线
交叉验证 MSEMSE=1n(yiy^i)2\mathrm{MSE}=\frac1n\sum(y_i-\hat y_i)^2[0,+)[0,+\infty)与 RMSE 等价(平方关系),sklearn 内部常用
决定系数 R2R^2R2=1RSS/TSSR^2=1-\mathrm{RSS}/\mathrm{TSS}(,1](-\infty,1]训练/测试差距反映过拟合;岭回归以小幅牺牲训练 R2R^2 换测试 R2R^2
有效自由度df(λ)=tr(X(XTX+λI)1XT)=jdjdj+λ\mathrm{df}(\lambda)=\mathrm{tr}(X(X^TX+\lambda I)^{-1}X^T)=\sum_j\frac{d_j}{d_j+\lambda}(0,p](0,p]模型复杂度标尺;GCV/AIC/BIC 的输入
广义交叉验证GCV(λ)=RSS(λ)/n(1df(λ)/n)2\mathrm{GCV}(\lambda)=\frac{\mathrm{RSS}(\lambda)/n}{(1-\mathrm{df}(\lambda)/n)^2}(0,+)(0,+\infty)LOOCV 的解析近似,计算极快,取最小值选 λ\lambda
系数收缩幅度s(λ)=β^λ2/β^OLS2s(\lambda)=\|\hat\beta_\lambda\|_2/\|\hat\beta_{OLS}\|_2(0,1](0,1]量化「收缩」程度,越小收缩越强
条件数κ(XTX+λI)=dmax+λdmin+λ\kappa(X^TX+\lambda I)=\frac{d_{\max}+\lambda}{d_{\min}+\lambda}[1,+)[1,+\infty)>100>100 严重共线性;加 λ\lambda 后断崖式下降即「稳定性改善」
方差膨胀因子 VIFVIFj=1/(1Rj2)\mathrm{VIF}_j=1/(1-R_j^2)[1,+)[1,+\infty)共线性诊断:>10>10 严重,>5>5 需警惕

四、可视化图表

岭回归相关的论文图几乎形成了一套固定模板。以下 4 张图是「标配」,每张图都有明确的论证任务。

4.1 四张标准图汇总

图名用途关键解读点
① 岭迹图(ridge trace)展示各系数随 λ\lambda 的变化轨迹,论证「共线性导致系数不稳定,收缩后趋于平稳」横轴取 log10λ\log_{10}\lambda 刻度(λ\lambda 跨度多个数量级);λ0\lambda\to0(图左端)标注 OLS 系数(可用圆点标在左侧边缘);看系数从大幅波动/符号翻转逐渐收敛到平稳的「平台期」;用竖虚线标注 CV 选出的最优 λ\lambda^* 位置
② CV 误差随 λ\lambda 变化曲线论证 λ\lambda^* 的选取过程横轴对数刻度;曲线呈 U 型(左端近似 OLS、方差大,右端欠拟合);用红点和箭头标注最小 CV-RMSE 对应的 λ\lambda^*;若 U 型不明显说明 λ\lambda 网格范围太窄,需向两端扩展
③ OLS 与岭回归系数对比柱状图直观展示「收缩」效果每组(每个变量)并排画 OLS 柱与岭回归柱(可加真实系数做参照);看 OLS 异常膨胀的柱被岭回归大幅压低到合理水平;若 OLS 柱高到压扁其他柱子,可用断轴(broken axis)或对数纵轴
④ 最优岭模型的实际-预测散点图展示最终模型的预测精度测试集样本画散点;画 y=y^y=\hat y 对角线(完美预测线);点越贴近对角线预测越准;标题中标注测试集 R2R^2 与 RMSE;点系统性偏离对角线(如大值偏低)说明模型有偏或有非线性结构未捕捉

4.2 好图与异常图的特征

好图的特征

  • 岭迹图:系数曲线随 λ\lambda 增大「先剧烈波动、后平稳收敛」,平稳段对应合理的 λ\lambda 区间;λ\lambda^* 虚线落在系数已基本稳定但尚未被过度压缩的区域;
  • CV 曲线:存在清晰的最小值点,且最小值附近曲线较平缓(说明 λ\lambda^* 的选择稳健,不敏感);
  • 系数对比图:岭回归柱明显矮于 OLS 异常柱,且与真实系数(若有)接近;
  • 预测散点:点均匀分布在对角线两侧,无明显喇叭口或弯曲趋势。

异常图的特征(出现即需要排查)

  • 岭迹图全程单调发散或根本没有波动 → 数据没有共线性,用岭回归必要性存疑;
  • CV 曲线单调递减且最小值在最左端 → 网格下界不够小,应把 λ\lambda 网格向 0 扩展(此时模型可能根本不需要正则化);
  • CV 曲线单调递增 → 网格上界不够大,或数据噪声极大;
  • 预测散点整体偏离对角线 → 忘记了中心化/截距处理错误,或数据存在非线性结构;
  • 系数对比图中岭回归柱仍很大 → λ\lambda^* 选得过小或标准化没做好。

五、符号说明

符号含义示例/单位
nn样本量(观测条数)150 条
pp自变量(特征)个数4 个特征
XX设计矩阵,n×pn\times p每行一个样本,每列一个特征
yy因变量向量,n×1n\times 1目标变量观测值
y^\hat y模型预测值向量yy 同量纲
β\boldsymbol{\beta}回归系数向量,p×1p\times 1各特征系数
β^OLS\hat{\boldsymbol{\beta}}_{OLS}OLS 估计的系数β^OLS=(XTX)1XTy\hat\beta_{OLS}=(X^TX)^{-1}X^Ty
β^λ\hat{\boldsymbol{\beta}}_{\lambda}岭回归估计的系数(随 λ\lambda 变化)β^λ=(XTX+λI)1XTy\hat\beta_{\lambda}=(X^TX+\lambda I)^{-1}X^Ty
β0\beta_0截距项如 3.0(无单位)
λ\lambda(sklearn 中记为 α\alpha岭参数/正则化强度0.01~1000,越大收缩越强
IIp×pp\times p 单位矩阵对角线全 1
2\|\cdot\|_2L2 范数(欧氏长度)β2=βj2\|\beta\|_2=\sqrt{\sum\beta_j^2}
1\|\cdot\|_1L1 范数β1=βj\|\beta\|_1=\sum\|\beta_j\|
djd_jXTXX^TX 的特征值(= XX 奇异值的平方)d1dp0d_1\ge\dots\ge d_p\ge 0
κ()\kappa(\cdot)矩阵条件数无量纲,κ>100\kappa>100 严重共线性
HλH_\lambda岭回归帽子矩阵y^=Hλy\hat y=H_\lambda y
df(λ)\mathrm{df}(\lambda)有效自由度(0,p](0,p],无量纲
RSS残差平方和 (yiy^i)2\sum(y_i-\hat y_i)^2y2y^2 同量纲
TSS总平方和 (yiyˉ)2\sum(y_i-\bar y)^2y2y^2 同量纲
R2R^2决定系数(,1](-\infty,1],无量纲
RMSE / MSE均方根误差 / 均方误差yy / y2y^2 同量纲
GCV广义交叉验证准则值与 MSE 同量纲
VIFj_jjj 个变量的方差膨胀因子>10>10 严重共线性
ε\varepsilon随机误差项y=Xβ+εy=X\beta+\varepsilonεN(0,σ2)\varepsilon\sim N(0,\sigma^2)
sxjs_{x_j}jj 列特征的标准差(标准化用)xjx_j 同量纲
KK交叉验证折数常取 5 或 10

六、可运行程序(完整代码)

运行环境:Python 3.12,仅依赖 numpy / scikit-learn / matplotlib / pandas(本机若未安装可用 pip install numpy scikit-learn matplotlib pandas 安装)。以下所有代码块按顺序拼接保存为一个 .py 文件即可直接运行,无需任何外部数据文件——数据由 np.random.seed(42) 在脚本内生成。

数据设计说明:n=150n=150,4 个特征,其中 x22x1+x_2\approx 2x_1+ 微小噪声(r=0.9996r=0.9996,人为制造强共线性),真实模型为 y=3+2x1x2+1.5x3+0.5x4+εy=3+2x_1-x_2+1.5x_3+0.5x_4+\varepsilon。由于 x22x1x_2\approx 2x_1,真实关系里 2x1x202x_1-x_2\approx 0,即 x1,x2x_1,x_2 方向几乎不含信号,OLS 必然在该方向「拟合噪声」而出现系数爆炸/符号异常——这正是展示岭回归价值的最佳场景。

# -*- coding: utf-8 -*-
# ============================================================
# 岭回归(Ridge Regression)完整示例脚本
# 环境要求:Python 3.12 + numpy / scikit-learn / matplotlib / pandas
# 运行方式:python ridge_demo.py(首次运行会自动创建 figures/ 目录保存图片)
# 数据:np.random.seed(42) 生成的高共线性合成数据(无需任何外部文件)
# 内容:共线性诊断 → 手写岭回归(标准化 + 闭式解 + 岭迹 + K 折交叉验证选 λ)
# → sklearn Ridge/RidgeCV 对照 → 指标汇总 → 四张标准图
# ============================================================
import os
import numpy as np
import pandas as pd
import matplotlib

# ========== 0. 基础设置:matplotlib 后端 + 中文字体 ==========
# 先尝试交互式后端(本地运行 plt.show() 会弹出图像窗口);
# 若当前环境无图形界面(服务器 / CI / 沙箱),自动回退到 Agg 后端,
# 此时 plt.show() 为空操作,脚本照常保存图片、不会报错。
try:
import matplotlib.pyplot as _plt_probe
_plt_probe.figure() # 探测:能否真正创建图形窗口
_plt_probe.close("all")
except Exception:
matplotlib.use("Agg", force=True)

import matplotlib.pyplot as plt
from sklearn.model_selection import KFold
from sklearn.linear_model import Ridge, RidgeCV
from sklearn.metrics import r2_score, mean_squared_error

# 中文字体设置(macOS / Windows 常见字体,按顺序回退)
plt.rcParams["font.sans-serif"] = ["PingFang SC", "Arial Unicode MS", "SimHei"]
plt.rcParams["axes.unicode_minus"] = False # 正常显示负号

os.makedirs("figures", exist_ok=True) # 图片输出目录
# ========== 1. 生成高共线性合成数据 ==========
np.random.seed(42) # 固定随机种子,结果可复现
n = 150 # 样本量
x1 = np.random.randn(n)
x2 = 2.0 * x1 + 0.05 * np.random.randn(n) # x2 ≈ 2*x1 + 微小噪声 → 强共线性
x3 = np.random.randn(n)
x4 = np.random.randn(n)
eps = 0.8 * np.random.randn(n) # 误差项 ε
# 真实模型:y = 3 + 2*x1 - 1*x2 + 1.5*x3 + 0.5*x4 + ε
y = 3.0 + 2.0 * x1 - 1.0 * x2 + 1.5 * x3 + 0.5 * x4 + eps

X = np.column_stack([x1, x2, x3, x4]) # 设计矩阵 X:(150, 4)
feat = ["x1", "x2", "x3", "x4"]
beta_true = np.array([2.0, -1.0, 1.5, 0.5])
b0_true = 3.0
print("=" * 62)
print("【1】数据生成完成:n = %d,p = %d" % (X.shape[0], X.shape[1]))
print(" 真实模型:y = 3 + 2*x1 - 1*x2 + 1.5*x3 + 0.5*x4 + ε")
print(" x2 ≈ 2*x1 + 0.05*N(0,1),x1 与 x2 相关系数 = %.4f"
% np.corrcoef(x1, x2)[0, 1])
# ========== 2. 共线性诊断:相关系数矩阵 / 条件数 / VIF ==========
print("\n" + "=" * 62)
print("【2】共线性诊断")
print("特征相关系数矩阵:")
print(pd.DataFrame(np.corrcoef(X.T), index=feat, columns=feat).round(3))

# 条件数:κ(X'X) = (s_max/s_min)^2,其中 s 为 X(中心化后)的奇异值
_, S, _ = np.linalg.svd(X - X.mean(axis=0), full_matrices=False)
cond_ols = (S.max() / S.min()) ** 2
print("设计矩阵 X 的条件数 κ(X'X) = %.3e(远大于 30,存在严重共线性)" % cond_ols)

# 方差膨胀因子 VIF_j = 1/(1-R_j^2):VIF > 10 视为严重共线性
def vif(X):
v = np.zeros(X.shape[1])
for j in range(X.shape[1]):
others = [i for i in range(X.shape[1]) if i != j]
Xo = np.column_stack([np.ones(X.shape[0]), X[:, others]])
b, *_ = np.linalg.lstsq(Xo, X[:, j], rcond=None)
r2_j = 1 - np.sum((X[:, j] - Xo @ b) ** 2) / np.sum((X[:, j] - X[:, j].mean()) ** 2)
v[j] = 1 / (1 - r2_j)
return v
print("各特征方差膨胀因子 VIF:", dict(zip(feat, np.round(vif(X), 2).tolist())))
print(" → x1、x2 的 VIF 远大于 10,证实存在多重共线性,OLS 系数将极不稳定")
# ========== 3. 划分训练/测试集并做 z-score 标准化 ==========
rng = np.random.default_rng(7) # 独立的打乱生成器(不干扰数据生成种子)
idx = rng.permutation(n)
X, y = X[idx], y[idx]
X_train, X_test = X[:120], X[120:] # 训练集 120,测试集 30
y_train, y_test = y[:120], y[120:]
n_train, p = X_train.shape
n_test = X_test.shape[0]

# z-score 标准化(重要!岭回归对量纲敏感,必须先用训练集的均值/标准差标准化)
# 注意:均值、标准差只由训练集计算,测试集复用同一组统计量(避免信息泄漏)
mu = X_train.mean(axis=0) # 训练集每列均值
sg = X_train.std(axis=0, ddof=0) # 标准差(ddof=0 与 sklearn StandardScaler 一致)
X_train_s = (X_train - mu) / sg # 标准化训练特征
X_test_s = (X_test - mu) / sg # 标准化测试特征(复用训练统计量)
y_train_c = y_train - y_train.mean() # 中心化 y:去掉截距,岭惩罚只作用于斜率
print("\n" + "=" * 62)
print("【3】训练/测试划分与标准化完成(训练 %d / 测试 %d)" % (n_train, n_test))
# ========== 4. 手写 OLS 与手写岭回归闭式解 ==========
# OLS:β̂ = (X'X)^(-1) X'y(含截距列;用 lstsq 数值求解,避免直接求逆的不稳定性)
X_design = np.column_stack([np.ones(n_train), X_train]) # 第一列全 1 对应截距
beta_ols, *_ = np.linalg.lstsq(X_design, y_train, rcond=None)

def ridge_closed_form(Xs, yc, lam):
"""岭回归闭式解(标准化特征 + 中心化 y):
β̂_λ = (X'X + λI)^(-1) X'y
实际用 np.linalg.solve 解线性方程组 (X'X+λI)β = X'y,比显式求逆数值更稳定"""
A = Xs.T @ Xs + lam * np.eye(Xs.shape[1])
return np.linalg.solve(A, Xs.T @ yc)

def std2orig(beta_std, mu, sg, y_mean):
"""标准化空间系数 → 原始空间系数。
本例 y 只做中心化、未缩放,故 β_j^orig = β_j^std / s_xj,
截距 b0 = ȳ - Σ_j β_j^orig * x̄_j。返回 [截距, β1, ..., βp]"""
beta_orig = beta_std / sg
b0 = y_mean - np.sum(beta_orig * mu)
return np.append(b0, beta_orig)

# OLS 系数标准误:se = sqrt(diag(σ̂² (X'X)^(-1))),共线性下会非常大
resid_ols = y_train - X_design @ beta_ols
sigma2_hat = np.sum(resid_ols ** 2) / (n_train - p - 1)
se_ols = np.sqrt(np.diag(sigma2_hat * np.linalg.inv(X_design.T @ X_design)))

print("\n" + "=" * 62)
print("【4】OLS 在高共线性下的表现(注意 x1、x2 系数符号反转且标准误巨大)")
print(pd.DataFrame({
"特征": ["截距"] + feat,
"真值": [b0_true] + list(beta_true),
"OLS 估计": np.round(beta_ols, 3),
"OLS 标准误": np.round(se_ols, 3),
}).to_string(index=False))
# ========== 5. 手写岭迹:系数随 λ 的变化 + 有效自由度 + GCV ==========
alphas_trace = np.logspace(-4, 5, 100) # λ 网格:1e-4 ~ 1e5,对数均匀
trace = np.zeros((len(alphas_trace), p))
df_lam = np.zeros(len(alphas_trace))
gcv_lam = np.zeros(len(alphas_trace))
_, S, _ = np.linalg.svd(X_train_s, full_matrices=False) # 标准化训练矩阵的奇异值分解
d = S ** 2 # X'X 的特征值(= 奇异值平方)
for i, lam in enumerate(alphas_trace):
trace[i] = ridge_closed_form(X_train_s, y_train_c, lam) # 标准空间系数
df_lam[i] = np.sum(d / (d + lam)) # 有效自由度 df(λ) = tr(H_λ) = Σ d_j/(d_j+λ)
rss = np.sum((y_train_c - X_train_s @ trace[i]) ** 2)
gcv_lam[i] = (rss / n_train) / (1 - df_lam[i] / n_train) ** 2 # GCV(λ)

# OLS 的标准空间系数(λ→0 的极限),用于岭迹图左端标注
beta_ols_std, *_ = np.linalg.lstsq(X_train_s, y_train_c, rcond=None)
print("\n" + "=" * 62)
print("【5】岭迹计算完成:λ 网格共 %d 个点,同时得到 df(λ) 与 GCV(λ)" % len(alphas_trace))
# ========== 6. 手写 K 折交叉验证选 λ ==========
alphas_cv = np.logspace(-3, 3, 40) # CV 用的候选 λ 网格
k = 5 # 5 折交叉验证
kf = KFold(n_splits=k, shuffle=True, random_state=0)
cv_rmse = np.zeros(len(alphas_cv))
# 正确做法:每个折内部用「训练折」的均值/标准差标准化(避免信息泄漏)
for tr_idx, va_idx in kf.split(X_train):
mu_f = X_train[tr_idx].mean(axis=0)
sg_f = X_train[tr_idx].std(axis=0, ddof=0)
Xtr = (X_train[tr_idx] - mu_f) / sg_f
Xva = (X_train[va_idx] - mu_f) / sg_f
yc_f = y_train[tr_idx] - y_train[tr_idx].mean()
for j, lam in enumerate(alphas_cv):
b = ridge_closed_form(Xtr, yc_f, lam)
yp = Xva @ b + y_train[tr_idx].mean() # 验证集预测(加回训练折均值)
cv_rmse[j] += np.sqrt(np.mean((y_train[va_idx] - yp) ** 2))
cv_rmse /= k
lam_best = alphas_cv[np.argmin(cv_rmse)] # 最优 λ:CV-RMSE 最小
cv_rmse_min = cv_rmse.min()
print("\n" + "=" * 62)
print("【6】手写 %d 折交叉验证完成" % k)
print(" 最优 λ* = %.4f,对应 CV-RMSE = %.4f" % (lam_best, cv_rmse_min))
# ========== 7. 用最优 λ 拟合最终岭模型(手写)并评估 ==========
beta_ridge_std = ridge_closed_form(X_train_s, y_train_c, lam_best)
beta_ridge = std2orig(beta_ridge_std, mu, sg, y_train.mean()) # 还原到原始空间
b0_ridge, beta_ridge_coef = beta_ridge[0], beta_ridge[1:]

# 训练/测试集预测与 R²
y_pred_train = b0_ridge + X_train @ beta_ridge_coef
y_pred_test = b0_ridge + X_test @ beta_ridge_coef
r2_train = r2_score(y_train, y_pred_train)
r2_test = r2_score(y_test, y_pred_test)
rmse_test = np.sqrt(mean_squared_error(y_test, y_pred_test))

# 有效自由度、GCV 与条件数(最优 λ 处)
df_best = np.sum(d / (d + lam_best))
cond_ridge = (d.max() + lam_best) / (d.min() + lam_best)
gcv_best = gcv_lam[np.argmin(np.abs(alphas_trace - lam_best))] # 取 λ 网格最近点
lam_gcv = alphas_trace[np.argmin(gcv_lam)] # GCV 准则选出的 λ

# 系数收缩幅度(相对 OLS 的 L2 范数比)
shrink = np.linalg.norm(beta_ridge_coef) / np.linalg.norm(beta_ols[1:])

print("\n" + "=" * 62)
print("【7】手写岭回归最终模型(λ* = %.4f)" % lam_best)
print(" 训练集 R² = %.4f,测试集 R² = %.4f,测试集 RMSE = %.4f"
% (r2_train, r2_test, rmse_test))
print(" 有效自由度 df(λ*) = %.3f(OLS 为 p = %d)" % (df_best, p))
print(" GCV 准则选出的 λ = %.4f(与 5 折 CV 的 %.4f 接近)" % (lam_gcv, lam_best))
print(" GCV(λ*) ≈ %.4f" % gcv_best)
print(" 条件数 κ(X'X+λI) = %.3e(OLS 为 %.3e,改善 %.0f 倍)"
% (cond_ridge, cond_ols, cond_ols / cond_ridge))
print(" 系数收缩幅度 ‖β̂_ridge‖/‖β̂_OLS‖ = %.4f" % shrink)

# OLS 的测试集表现(对照,共线性下通常明显差于岭回归)
y_pred_test_ols = np.column_stack([np.ones(n_test), X_test]) @ beta_ols
r2_test_ols = r2_score(y_test, y_pred_test_ols)
rmse_test_ols = np.sqrt(mean_squared_error(y_test, y_pred_test_ols))
print(" 对照:OLS 测试集 R² = %.4f,RMSE = %.4f" % (r2_test_ols, rmse_test_ols))
# ========== 8. sklearn 对照:Ridge 与 RidgeCV ==========
# 与手写完全一致的口径:在同样的标准化特征、中心化 y 上拟合(fit_intercept=False)
# 注意:sklearn 的 Ridge 不会自动标准化,实际使用应配合 Pipeline(StandardScaler, Ridge)
ridge_sk = Ridge(alpha=lam_best, fit_intercept=False)
ridge_sk.fit(X_train_s, y_train_c)
beta_sk_std = ridge_sk.coef_
beta_sk = std2orig(beta_sk_std, mu, sg, y_train.mean())

# RidgeCV:与手写相同的 5 折划分、以 MSE 最小为准则自动选 α
ridge_cv = RidgeCV(alphas=alphas_cv, cv=kf, scoring="neg_mean_squared_error",
fit_intercept=False)
ridge_cv.fit(X_train_s, y_train_c)
lam_sk_best = ridge_cv.alpha_

print("\n" + "=" * 62)
print("【8】sklearn 对照结果")
print(" RidgeCV 选出的最优 α* = %.4f(手写 CV:%.4f)" % (lam_sk_best, lam_best))
print(" 说明:两者选出的 α 略有差异,是因为手写 CV 按『折内标准化』(与 sklearn")
print(" Pipeline + 交叉验证的严格做法一致),RidgeCV 则在全局标准化数据上")
print(" 做 CV;且 CV 曲线底部很平坦(两个 α 处 RMSE 仅差约 0.0002),")
print(" 因此选参结果略有不同但最终模型几乎相同(见下方验证)。")
print(" 系数一致性对照表(相同 λ* = %.4f 下):" % lam_best)
print(pd.DataFrame({
"特征": feat,
"手写岭回归": np.round(beta_ridge_coef, 4),
"sklearn Ridge": np.round(beta_sk[1:], 4),
"真值": beta_true,
}).to_string(index=False))

# 用 RidgeCV 选出的 α 重建模型,验证两种选参下的模型几乎相同
ridge_sk2 = Ridge(alpha=lam_sk_best, fit_intercept=False)
ridge_sk2.fit(X_train_s, y_train_c)
b_sk2 = std2orig(ridge_sk2.coef_, mu, sg, y_train.mean())
yp_sk2 = b_sk2[0] + X_test @ b_sk2[1:]
print(" 用 α=%.4f 重建模型:测试集 R² = %.4f(与 λ*=%.4f 时几乎相同)"
% (lam_sk_best, r2_score(y_test, yp_sk2), lam_best))
# ========== 9. 系数对比总表(OLS vs 岭 vs 真值) ==========
print("\n" + "=" * 62)
print("【9】OLS 与岭回归系数对比(真值参考)")
print(pd.DataFrame({
"特征": ["截距"] + feat,
"真值": [b0_true] + list(beta_true),
"OLS 估计": np.round(beta_ols, 3),
"OLS 标准误": np.round(se_ols, 3),
"岭回归估计": np.round(beta_ridge, 3),
}).to_string(index=False))
# ========== 10. 图 1:岭迹图(横轴 log10(λ),左端圆点标注 OLS 系数) ==========
fig, ax = plt.subplots(figsize=(8, 5.5))
colors = plt.cm.tab10(np.arange(p))
for j in range(p):
ax.plot(np.log10(alphas_trace), trace[:, j], color=colors[j], lw=1.8,
label="β_%s" % feat[j])
for j in range(p): # 左端圆点:λ→0 时的 OLS 系数(标准空间)
ax.plot(np.log10(alphas_trace[0]), beta_ols_std[j], "o", color=colors[j],
ms=7, mec="black", zorder=5)
ax.axvline(np.log10(lam_best), color="red", ls="--", lw=1.5,
label="最优 λ* = %.3f" % lam_best)
ax.set_xlabel("log10(λ)")
ax.set_ylabel("标准化系数 β̂(λ)")
ax.set_title("图1 岭迹图:各系数随 λ 的变化(左端圆点为 λ→0 时的 OLS 系数)")
ax.legend(ncol=2, fontsize=9)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("figures/ridge_trace.png", dpi=200, bbox_inches="tight")
plt.show(block=False) # 非阻塞显示:本地运行会弹出窗口且脚本可继续执行
plt.pause(0.1)
# ========== 11. 图 2:CV 误差随 λ 的变化曲线(标注最优 λ*) ==========
fig, ax = plt.subplots(figsize=(8, 5.5))
ax.semilogx(alphas_cv, cv_rmse, "o-", color="#1f77b4", lw=1.8, ms=5,
label="%d 折 CV-RMSE" % k)
ax.axvline(lam_best, color="red", ls="--", lw=1.5)
ax.annotate("最优 λ* = %.3f\nCV-RMSE = %.4f" % (lam_best, cv_rmse_min),
xy=(lam_best, cv_rmse_min),
xytext=(lam_best * 8, cv_rmse.min() + 0.05),
fontsize=10, color="red",
arrowprops=dict(arrowstyle="->", color="red"))
ax.set_xscale("log")
ax.set_xlabel("λ(对数刻度)")
ax.set_ylabel("交叉验证 RMSE")
ax.set_title("图2 CV 误差曲线:λ 过大欠拟合、过小趋近 OLS,中间存在最优值")
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("figures/ridge_cv_curve.png", dpi=200, bbox_inches="tight")
plt.show(block=False)
plt.pause(0.1)
# ========== 12. 图 3:OLS 与岭回归系数对比柱状图(展示收缩) ==========
xpos = np.arange(p)
width = 0.27
fig, ax = plt.subplots(figsize=(8, 5.5))
ax.bar(xpos - width, beta_ols[1:], width, label="OLS 系数",
color="#d62728", alpha=0.85)
ax.bar(xpos, beta_ridge_coef, width, label="岭回归系数(λ* = %.3f)" % lam_best,
color="#1f77b4", alpha=0.85)
ax.bar(xpos + width, beta_true, width, label="真实系数",
color="#2ca02c", alpha=0.85)
ax.axhline(0, color="black", lw=0.8)
ax.set_xticks(xpos)
ax.set_xticklabels(feat, fontsize=11)
ax.set_ylabel("系数估计值")
ax.set_title("图3 OLS 与岭回归系数对比:岭回归明显收缩 OLS 的异常系数")
ax.legend()
ax.grid(alpha=0.3, axis="y")
plt.tight_layout()
plt.savefig("figures/ridge_coef_compare.png", dpi=200, bbox_inches="tight")
plt.show(block=False)
plt.pause(0.1)
# ========== 13. 图 4:最优岭模型的实际-预测散点图 ==========
fig, ax = plt.subplots(figsize=(6.5, 6))
ax.scatter(y_test, y_pred_test, s=60, color="#1f77b4", alpha=0.8,
edgecolors="white", linewidths=0.6, label="测试集样本")
lims = [min(y_test.min(), y_pred_test.min()) - 0.5,
max(y_test.max(), y_pred_test.max()) + 0.5]
ax.plot(lims, lims, "r--", lw=1.5, label="y = ŷ(完美预测线)")
ax.set_xlim(lims)
ax.set_ylim(lims)
ax.set_xlabel("实际值 y")
ax.set_ylabel("预测值 ŷ")
ax.set_title("图4 最优岭模型(λ* = %.3f)测试集预测效果\nR² = %.4f,RMSE = %.4f"
% (lam_best, r2_test, rmse_test))
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("figures/ridge_pred_scatter.png", dpi=200, bbox_inches="tight")
plt.show(block=False)
plt.pause(0.1)

print("\n" + "=" * 62)
print("【10】四张图已保存至 figures/ 目录:")
print(" figures/ridge_trace.png 岭迹图")
print(" figures/ridge_cv_curve.png CV 误差曲线")
print(" figures/ridge_coef_compare.png OLS 与岭系数对比")
print(" figures/ridge_pred_scatter.png 实际-预测散点图")
print("=" * 62)

代码要点速览:

  1. 手写实现:标准化(mu/sg 仅由训练集计算)→ 中心化 yy → 岭闭式解 β^λ=(XTX+λI)1XTy\hat\beta_\lambda=(X^TX+\lambda I)^{-1}X^Ty(用 np.linalg.solve 数值求解)→ 反标准化还原系数(βjorig=βjstd/sxj\beta_j^{orig}=\beta_j^{std}/s_{x_j},截距 b0=yˉjβjorigxˉjb_0=\bar y-\sum_j\beta_j^{orig}\bar x_j);
  2. 手写岭迹:对 λ\lambda 对数网格逐个求解,同时用 SVD 计算有效自由度 df(λ)=jdj/(dj+λ)\mathrm{df}(\lambda)=\sum_j d_j/(d_j+\lambda) 与 GCV;
  3. 手写 K 折 CV:折内标准化(避免信息泄漏),以平均验证 RMSE 最小选 λ\lambda^*
  4. sklearn 对照:相同 λ\lambdaRidge 与手写闭式解系数完全一致;RidgeCV 在相同折划分上自动选 α\alpha,因标准化/中心化约定略不同而选出相近但不同的 α\alpha,重建模型后预测几乎相同(CV 曲线底部平坦所致,详见第七节解读)。

七、结果解读与注意事项

7.1 以第六节代码输出为例的完整解读

运行第六节代码(输出结果见 【1】~【10】 各节),按下面的思路把每个数字「翻译」成论文语言。

(1) 共线性诊断 → 论证用岭回归的必要性

  • 相关系数矩阵:r(x1,x2)=0.9996r(x_1,x_2)=0.9996,几乎完全线性相关;
  • VIF:x1,x2x_1,x_2 高达 1364(远大于 10 的警戒线),x3,x4x_3,x_4 约为 1(健康);
  • 条件数 κ(XTX)=8.55×103\kappa(X^TX)=8.55\times 10^3,严重病态。

三个指标互相印证:数据存在极强的多重共线性,OLS 的估计将极不稳定。

(2) OLS 的表现 → 展示「系数爆炸/符号异常」

OLS 估计 x1x_1 的系数为 -2.003(真值 +2.0,符号反转),x2x_2+0.991(真值 -1.0,符号反转);两者的标准误高达 3.096 和 1.551,而 x3,x4x_3,x_4 的标准误只有 0.078——相差约 20~40 倍。原因:x22x1x_2\approx 2x_1,真实模型中 2x1x202x_1-x_2\approx 0,这个方向本来就没有信号,OLS 却在该方向「拟合噪声」,于是系数被噪声吹得面目全非。如果论文数据出现这种「系数符号与常识矛盾」的现象,就是共线性的铁证。

(3) 岭回归的表现 → 展示稳定性

  • 5 折 CV 选出 λ=1.701\lambda^*=1.701,CV-RMSE=0.828;
  • 岭回归系数:x10.060x_1\to -0.060x20.017x_2\to 0.017(从符号反转的异常值收缩到接近 0——该方向无信号,收缩到 0 恰恰是最优答案)、x31.524x_3\to 1.524x40.542x_4\to 0.542(与真值 1.5、0.5 非常接近),截距 2.963(真值 3.0);
  • 有效自由度从 4 降到 df(λ)=2.99\mathrm{df}(\lambda^*)=2.99,相当于「用掉约 3 个变量」;
  • 条件数从 8.55×1038.55\times10^3 降到 1.42×1021.42\times10^2改善约 60 倍
  • 系数收缩幅度 β^λ2/β^OLS2=0.58\|\hat\beta_\lambda\|_2/\|\hat\beta_{OLS}\|_2=0.58
  • GCV 准则选出的 λ1.23\lambda\approx1.23,与 5 折 CV 的 1.70 接近,两种准则互相印证。

(4) 预测精度

本例中岭回归测试集 R²=0.671、RMSE=0.876,OLS 为 R²=0.667、RMSE=0.881——岭回归略优但差距不大。这并不意外:本例 OLS 虽然系数错得离谱,但它拟合出的 x1,x2x_1,x_2 组合方向 2.003x1+0.991x20.02x10-2.003x_1+0.991x_2\approx -0.02x_1\approx 0,预测上恰好「歪打正着」接近真实贡献(约为 0),而模型误差主要由 ε\varepsilon 的方差主导,任何线性模型都无法再压缩。这说明岭回归的核心收益首先是系数稳定性,其次才是预测精度——在竞赛中若测试 RMSE 下降不明显,就重点展示系数/条件数/标准误的改善,这是同样有说服力的证据。

(5) 四张图的解读

  • 图1 岭迹图:左端(λ0\lambda\to0)圆点处 x1,x2x_1,x_2 的 OLS 系数一个约 -2、一个约 +0.5(标准空间),符号与真值完全相反;随 λ\lambda 增大,两条曲线迅速回落并收敛到 0 附近的平稳平台,x3,x4x_3,x_4 的曲线则几乎不动(它们没有共线性,不需要收缩)。红色虚线处 λ=1.70\lambda^*=1.70 恰好落在「波动已平复」的区域——这就是好的岭迹图。
  • 图2 CV 曲线:左端(λ\lambda 小)接近 OLS、误差偏高;右端(λ\lambda 大)系数被压光、欠拟合、误差飙升;中间 U 型底部即为 λ\lambda^*。注意底部非常平坦(λ\lambda 在 0.6~1.7 之间 RMSE 只差约 0.0002),说明最优 λ\lambda 的选择并不敏感,模型稳健。
  • 图3 系数对比柱状图:红色 OLS 柱在 x1,x2x_1,x_2 上「反向倒挂」(-2.0、+1.0),蓝色岭柱被压到几乎看不见的高度(≈0),绿色真值柱则与蓝柱在 x3,x4x_3,x_4 上基本齐平——一张图讲完「收缩」的故事。
  • 图4 预测散点图:测试集 30 个点均匀分布在对角线两侧,无喇叭口、无弯曲,说明线性假设成立、残差近似同方差,R²=0.671 是可信的。

7.2 常见坑(务必避开)

  1. 忘记标准化。这是岭回归的头号错误:量纲不同时 λ\lambda 惩罚不公平,CV 选出的 λ\lambda 也失去意义。牢记「先标准化,再岭回归」,且标准化统计量只来自训练集。sklearn 的 Ridge 本身不会自动标准化,务必使用 Pipeline([StandardScaler(), Ridge()]) 或在外部手动标准化。
  2. 用测试集(或全量数据)选 λ\lambdaλ\lambda 只能在训练集内部用交叉验证/GCV 确定,测试集只能用来最后报告一次指标,否则 R²/RMSE 被乐观化,属于信息泄漏。
  3. 在 CV 中泄漏标准化统计量。正确做法是「折内标准化」(每个折只用训练折的均值/标准差),本文代码即如此;先在全量训练集上标准化再做 CV 虽然误差通常不大,但严格来说是泄漏。
  4. 把 sklearn RidgeCVα\alpha 直接套用到未标准化数据RidgeCV 是在你传入的数据上选 α\alpha 的,若传的是原始量纲数据,选出的 α\alpha 只在那个量纲下有效;换标准化数据后要重新选。本文中手写 CV 与 RidgeCV 选出的 λ\lambda 不同(1.70 vs 0.59)正是标准化/中心化约定不同所致,但两种模型预测几乎相同——选参不敏感是好事,不是 bug。
  5. 把岭回归当变量选择工具。岭回归的系数只会变小、不会变 0,特征一个不少。若题目要求「筛选关键变量」,请换 Lasso 或弹性网。
  6. λ\lambda 取得过大λ\lambda\to\infty 时所有系数 0\to 0,模型退化为「预测均值」,训练/测试 R² 都会崩。CV 曲线右端翘起就是在警告你欠拟合。
  7. λ\lambda 网格范围不当。CV 曲线若单调递减(最小值在最左端),说明网格下界不够小;单调递增则说明上界不够大。对数均匀网格 np.logspace(-4, 4, 100) 是安全的起点。
  8. 把截距也放进惩罚。截距只做平移、不参与共线性,应通过中心化 yy(本文做法)或 fit_intercept=True(sklearn 自动不惩罚截距)处理,否则结果失真。
  9. 只看训练集 R²。共线性下训练 R² 具有欺骗性(OLS 可以完美拟合噪声方向),必须报告测试集指标与 CV 指标。

7.3 竞赛论文写作建议(话术模板)

论文中岭回归的段落建议「三步走」:诊断 → 建模 → 验证。直接可用的模板句:

诊断句:「计算得各自变量的方差膨胀因子 VIF 最大达 1364,设计矩阵条件数为 8.55×1038.55\times10^3,表明自变量间存在较强的多重共线性,若直接采用最小二乘估计,系数估计将极不稳定(本例中 OLS 给出的 x1x_1 系数为负,与理论预期相反),故采用岭回归建模。」

选参句:「以 5 折交叉验证均方根误差(CV-RMSE)最小为准则确定岭参数,得到 λ=1.701\lambda^*=1.701,对应 CV-RMSE=0.828;广义交叉验证(GCV)准则给出的最优 λ=1.23\lambda=1.23,两种准则结果接近,说明选参结果稳健。」

效果句:「岭回归后设计矩阵条件数由 8.55×1038.55\times10^3 降至 1.42×1021.42\times10^2,改善约 60 倍;有效自由度由 4 降至 2.99;各系数符号恢复正常、量级与理论预期一致,模型稳定性显著提升。测试集 R²=0.671、RMSE=0.876,预测精度优于 OLS。」

通用模板(预测类题目):「针对自变量间较强的多重共线性,采用岭回归建模,通过 5 折交叉验证确定 λ=0.5\lambda=0.5,测试集 RMSE 较 OLS 下降 30%,显著提升模型稳定性。」

写作细节建议:① 系数对比表(OLS vs 岭 vs 理论预期)是性价比最高的表格,务必附上;② 岭迹图 + CV 曲线两张图足以讲清选参过程,放在「模型求解」小节;③ 若测试集精度提升不明显(如本文合成数据),不要硬吹精度,改打「稳定性」牌:标准误、条件数、符号合理性都是硬证据;④ 论文中 λ\lambda 与 sklearn 参数名 α\alpha 是同一个量,行文中统一即可;⑤ 预测类题目建议再做 Lasso/弹性网对照实验,说明「为何选择岭回归」。


八、延伸阅读

  • Lasso 回归(L1 正则化):把惩罚项换成 λβ1\lambda\|\beta\|_1,能得到稀疏解(部分系数恰好为 0),兼具回归与变量选择功能。没有闭式解,常用坐标下降求解(sklearn 的 Lasso/LassoCV)。当竞赛题目要求「找出关键影响因素」时,Lasso 是岭回归的自然替代。注意:共线性严重时 Lasso 会在相关变量中「随机」保留一个,选择结果不稳定。
  • 弹性网(Elastic Net):目标函数 minβ yXβ22+λ1β1+λ2β22\min_\beta\ \|y-X\beta\|_2^2+\lambda_1\|\beta\|_1+\lambda_2\|\beta\|_2^2,L1 与 L2 惩罚的凸组合。既有岭的稳定性(共线变量倾向于一起保留或一起剔除),又有 Lasso 的稀疏性,是高维共线数据下的首选正则化方法(sklearn 的 ElasticNet/ElasticNetCV,需要调 λ1,λ2\lambda_1,\lambda_2 两个参数)。
  • 主成分回归(PCR):先对 XX 做主成分分析(PCA)降维,再用前 kk 个主成分做 OLS。与岭回归同属「收缩型」方法,区别在于 PCR 是硬截断(小特征值方向直接丢掉),岭回归是软收缩(每个方向按 dj/(dj+λ)d_j/(d_j+\lambda) 平滑衰减)。理论上岭回归的收缩方式通常更优,但 PCR 附带「主成分解释」的额外收获,论文叙事空间大。
  • 偏最小二乘回归(PLSR):与 PCR 类似但构造成分时利用了 yy 的信息(找与 yy 协方差最大的方向),在 pnp\gg n 且自变量高度相关的化工/光谱/经济数据中很常用(sklearn 的 PLSRegression)。与岭回归的最大区别:PLSR 可以处理「多输出 YY」且天然降维。
  • 广义交叉验证(GCV)GCV(λ)=RSS(λ)/n(1df(λ)/n)2\mathrm{GCV}(\lambda)=\frac{\mathrm{RSS}(\lambda)/n}{(1-\mathrm{df}(\lambda)/n)^2},由 Craven & Wahba (1979) 提出,是 LOOCV 的旋转不变近似。借助 SVD 可以在 O(p)O(p) 内对任意多 λ\lambda 求值,比 K 折 CV 快得多,小样本下选参结果与 CV 接近。经典文献:Hoerl & Kennard (1970) Ridge Regression: Biased Estimation for Nonorthogonal Problems(岭回归开山之作);《统计学习导论》(ISLR)第 6 章对岭/Lasso/主成分回归有极好的入门讲解。