ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

线性回归实战笔记:从最小二乘到正则化,避开建模陷阱

线性回归实战笔记:从最小二乘到正则化,避开建模陷阱

1. 从一个翻车项目说起:复杂模型为什么会输给线性回归

前阵子我参与了一个销售预测的项目,数据大概是两千多条历史订单,特征三四十个。当时团队里刚流行用树模型,我觉得正好练手,就顺手把特征全塞进了一个梯度提升模型,调了两天参,训练集上拟合得近乎完美,R²能到0.95。结果放到验证集上直接垮掉,预测偏差大到业务方无法接受。

后来我被逼着回头试了一下最朴素的线性回归,反而验证集的R²稳定在0.7左右,虽然不算惊艳,但预测趋势完全对得上,业务方反而敢用了。

那次之后我就意识到一个很多人忽略的事实:线性回归不是"入门玩具",它是建模时真正该先做的基线。复杂模型之所以会输,通常不是因为参数不够多,而是因为数据量撑不起那么大的模型复杂度,再加上特征里大量噪声被复杂模型一并学了进去。线性回归结构简单,自带很强的归纳偏置,在中小数据集上往往比灵活模型更稳。

这篇内容不是什么教科书式的理论复述。我想把平时做线性回归时真正会用到的部分讲清楚,包括最小二乘怎么求、模型拟合完之后怎么看、特征进模型之前该怎么处理、以及什么时候该考虑岭回归和Lasso。你可以直接把它当成一份从零到一的落地笔记来参考,如果你是刚接触数据分析或者想把手头业务数据做成可解释的预测模型,这篇应该能帮你少走不少弯路。

2. 最小二乘求解:正规方程和梯度下降到底该选谁

2.1 先从模型形式和损失函数说起

线性回归假设目标变量 y 和特征 x₁、x₂、…、xₚ 之间存在线性关系,写成:

y = β₀ + β₁x₁ + β₂x₂ + … + βₚxₚ + ε

其中 β₀ 是截距,β₁ 到 βₚ 是各项的系数,ε 是误差项。这里说"线性"并不是说 x 之间必须严格没有曲线关系,而是说模型对 β 是线性的。所以你完全可以在特征里放入 x²、log(x)、x₁×x₂ 这类派生项,模型照样是"线性"的,只要参数之间不是相乘关系就行。

最常见的求解思路是最小二乘法,目标函数是所有样本残差平方和最小:

SSE = Σ(yᵢ − xᵢᵀβ)²

为什么用平方而不是绝对值?因为平方误差对大偏差的惩罚更强烈,而且误差平方和是凸函数,存在全局唯一最优解。缺点是对异常点敏感,如果数据里有离谱的离群值,平方会把它的影响放大好几倍,这正是后面要专门处理异常值的原因。

2.2 正规方程:小数据量下的直接答案

最小二乘有解析解,把 SSE 对 β 求导并令其等于零,可以得到正规方程:

β = (XᵀX)⁻¹Xᵀy

这里的 X 是所有样本组成的矩阵,每一行是一个样本的特征向量,开头要加一列全 1 来对应截距项。直接解这个方程,一次就能得到最优系数,不需要设置学习率,也不需要迭代。

实际使用中要注意两个问题:

一是 (XᵀX)⁻¹ 存在的前提是 X 的各列之间不存在完全共线性。如果有两个特征完全线性相关,XᵀX 就不可逆,正规方程直接报错。即使只是高度共线但没到完全一致,矩阵也可能接近奇异,求出来的系数会非常大且不稳定。

二是计算复杂度。求解 (XᵀX)⁻¹ 大约是 O(p³),p 是特征数量。如果 p 到几千甚至几万,这个直接解法的耗时和内存占用会迅速失控。但如果你的数据是几百个样本、几十个特征这种量级,正规方程就是最快最稳的方案,不需要迭代收敛,跑完就是精确解。

2.3 梯度下降:特征多了之后的正确姿势

当特征数量很大,或者样本量大到矩阵乘法都吃力时,梯度下降就更有优势。它的思路是沿着损失函数下降最快的方向不断更新系数:

β ← β − η·∇SSE(β)

其中 ∇SSE(β) = −2Xᵀ(y−Xβ),η 是学习率。

这里有个容易被新手忽略的坑:梯度的大小跟特征量纲直接相关。如果某个特征是收入(几万元级),另一个特征是年龄(几十岁),那么收入对应的梯度分量天然会大几十倍。如果不对特征做标准化,梯度下降就很容易在某个方向上震荡、在另一个方向上龟速前进,导致收敛极慢甚至发散。所以用梯度下降之前,一定要先做 Z-score 标准化或者归一化。

学习率的选择同样很关键。学习率太大,参数会在最优点附近来回跳动甚至越过;学习率太小,迭代几千步还看不到明显下降。我的经验是先用 0.01 或 0.001 试跑一小段看损失变化曲线,如果损失震荡严重就降低学习率,如果下降太慢就适当调大,并且在迭代过程中衰减学习率往往能兼顾前期的快速下降和后期的稳定收敛。

2.4 实践中怎么选

直接给结论,我的习惯是:

场景推荐方法
特征数少于几千、内存够、无严重共线性正规方程
特征数很大或样本量巨大批量梯度下降或小批量梯度下降
需要在线增量更新模型随机梯度下降
在正规方程求解时报矩阵不可逆错误先检查共线性,再考虑岭回归

除了这两种主流方式,现在很多库内部还提供了 SVD 分解或 QR 分解来求解最小二乘,数值稳定性比直接求逆更好。像 Python 的 scikit-learn 里 LinearRegression 默认就走的是 SVD 路径,所以平时我们用库函数并不会真的手动去算 (XᵀX)⁻¹,但理解背后的原理仍然重要,因为很多报错和异常结果都要回到这个层面去解释。

3. 拟合完别急着交差:R²、残差图和共线性体检

3.1 R² 的诱惑与陷阱

模型跑完,大部分人第一反应是看 R²。R² = 1 − SSE/SST,它刻画的是模型解释了多少因变量的方差。R² 越高,模型对样本的拟合越好。

但 R² 有几个容易被误读的地方。

第一,R² 高不表示模型真的有用。只要往模型里加特征,R² 就不会下降,哪怕加进去的特征纯粹是随机噪声。所以看到 0.95 的 R² 时,反而要警惕是不是过拟合了,尤其是当测试集表现远差于训练集时。

第二,R² 低也不一定代表模型失败。如果数据本身的噪声很大,比如预测个体用户的行为,R² 0.15 都可能是业务上的巨大提升。关键要看这个模型相比简单用均值预测到底好了多少,而不是死盯一个绝对值。我在做用户增长分析时就见过不少模型 R² 只有 0.2 左右,但因为变量显著性很强、方向符合业务直觉,最终照样能指导运营决策。

第三,R² 和相关系数的平方在简单线性回归里确实是一致的,但到多元回归里就不再有这么直观的含义,你没法说"模型解释了 y 的百分之多少"这个口头表述在数学上完全严格。准确的说法应该是模型预测值与实际值的相关系数的平方,或者从方差分解角度解释的方差比例。

3.2 残差分析:最该养成的好习惯

比看 R² 更重要的是看残差。残差是 yᵢ − ŷᵢ,也就是真实值和预测值之差。我每次建模后都至少画三张图。

第一张是残差对预测值的散点图。正常情况下残差应该随机分布在零轴两侧,没有明显形状。如果你看到漏斗形,也就是随着预测值增大残差也越来越散开,这就是异方差性。此时虽然系数依然是无偏估计,但标准误和置信区间不再可靠,p 值也会失真。处理办法要么对 y 做 log 变换,要么使用加权最小二乘,或者直接用稳健标准误。

第二张是残差对每个特征的散点图。如果残差和某个特征之间存在明显的曲线关系,说明这个特征可能以非线性形式影响 y。比如特征 x 和 y 其实是抛物线关系,你只放了 x 这一项进去,残差就会出现一条 U 形曲线。这时候把 x² 加进去,残差通常就能平坦下来。

第三张是 Q-Q 图,用来检查残差是否接近正态分布。虽然正态性不是线性回归系数无偏性的必要条件,但它影响置信区间和假设检验的准确性。如果尾部偏离明显,可能需要考虑对变量做变换,或者采用对分布假设不那么敏感的鲁棒回归。

3.3 多重共线性:系数的定时炸弹

多元回归里,特征之间常常存在相关性。比如预测房价时,房屋面积和房间数量通常高度相关。当两个或多个特征高度相关时,模型很难区分每个特征的独立贡献,表现就是系数的标准误变大,单个系数的符号甚至会和常识相反。

我常用 VIF(方差膨胀因子)来量化这个问题。对每个特征 Xⱼ,用其余所有特征去拟合它,得到拟合优度 Rⱼ²,然后:

VIFⱼ = 1 / (1 − Rⱼ²)

经验上 VIF 超过 10 就需要处理了,超过 5 就要警惕。处理思路无非几种:删除其中和业务含义重复度最高的变量,或者对相关特征做合并(比如把面积和房间数合成一个"户型综合指标"),或者改用岭回归把系数压缩住。

这里还要提醒一点:共线性不影响整体预测精度,所以如果你只关心预测准确度,不太在乎单个系数的解释,其实可以不用太在意 VIF。但如果你的项目是要给业务方讲清楚每个因素的贡献,共线性就是绕不过去的问题,否则你很可能得出一个"房间数量增加反而房价下降"的荒唐结论。

3.4 离群点和高杠杆点怎么对待

离群点对最小二乘的影响远超很多人想象。一个极端大的 y 值,哪怕只有一个,也可能把回归线拽偏一大截。判断离群点不能只看 y 值,还要结合 x 的位置看。如果某个样本的 x 本身也在特征空间边缘,它就是高杠杆点,对拟合结果有不成比例的影响力。

我一般会先算 Cook's Distance(库克距离),它同时考虑了残差大小和杠杆高低。对于 Cook's D 明显高于其他样本的记录,我不会直接删除,而是先确认它是真实业务记录还是数据录入错误。如果数据本身没问题,那么这个样本可能蕴含着重要信息,需要单独分析;如果是录入错误,那确实应该修正或剔除。盲目删点是一个很容易犯的错误,删完可能提升了 R²,却让模型失去了对真实边界的刻画能力。

4. 特征进模型前的处理:量纲、编码与缺失值

4.1 标准化和归一化不是可选项

前面提到,如果用梯度下降求解,特征标准化几乎是必须的。即使你用正规方程或现成库函数求解,标准化也能让系数之间变得可比——你想比较"年龄每增加一岁的影响"和"收入每增加一万元的影响",如果两个特征的量纲差距很大,不标准化的话系数数值的绝对大小完全没法直接比较。

常用做法是 Z-score:

z = (x − μ) / σ

另一种是把数据缩放到 [0,1] 区间,通常在特征本身有明确上下界时更合适。我个人的习惯是:树模型可以不怎么关心量纲,但线性模型一定先标准化。这一点看着简单,实际操作里特别容易被忽略,最后导致正则化惩罚项对不同特征产生了不均衡的作用。

4.2 类别变量的处理没有标准答案

遇到字符串类型的分类变量,最常见的做法是 one-hot 编码,也就是把有 k 个类别的变量拆成 k−1 个 0/1 变量。为什么是 k−1 而不是 k?因为如果不删掉一个参照类,就会和截距项产生完全共线性,这叫"虚拟变量陷阱"。

但 one-hot 并不总是最优解。如果类别本身有顺序关系,比如教育程度(小学、初中、高中、大学),应该用有序编码,赋成 0、1、2、3。这比平铺开一堆 0/1 变量更贴近真实业务含义,而且大大减少了参数数量。如果类别数量特别多,比如几万个,one-hot 会让特征矩阵变得非常宽,此时可以考虑目标编码或嵌入方式,但要小心目标编码带来的目标泄漏问题。

4.3 缺失值怎么填充才靠谱

缺失值处理没有万能公式,但我有个基本判断标准:你选择的填充方式不能偷偷改变数据本身的分布结构。用均值填充,会把缺失样本的预测值都拉向均值附近,变相缩小了该特征的方差;用中位数填充,对抗离群点更稳健;更复杂的做法是根据其他特征预测缺失值,比如用随机森林或 KNN 填充,适合缺失机制比较复杂的情况。

还有一类问题是"缺失本身可能是信息"。比如用户填写表单时,有职业信息的人可能只属于特定人群,缺失这个字段本身就暗示了某种人群特征。这时候可以单独构造一个"是否缺失"的 0/1 变量进模型,往往能捞到额外的解释力。我在做风控类分析时经常这么干,缺失标志本身有时比真实值还重要。

4.4 偏态特征:为什么建议做对数变换

线性回归对特征的要求并不是严格正态,而是更希望特征之间的关系近似线性。如果某个特征分布严重右偏,比如用户收入、订单金额这种东西,大量样本集中在低值区域,少数极端高值会像锚点一样勾住回归线。

对这类特征做 log(x+1) 变换是常见的做法。加 1 是为了规避 x 为 0 时取对数无意义。变换之后,极端值的跨度被压缩,特征和因变量的关系更容易呈现线性。而且 log 变换有一个很好的性质:系数可以直接解释为"x 变化 1%,y 平均变化 β/100 个单位",这种弹性解释在业务汇报里非常讨喜。

5. 线性回归不够用的时候:岭回归、Lasso 与偏差方差权衡

5.1 为什么需要正则化

当特征数量接近甚至超过样本量,或者特征之间存在严重共线性时,普通最小二乘的系数会变得非常不稳定。原因在于最小二乘纯粹追求训练集上的误差最小,没有任何对系数规模的限制,于是模型会利用噪声信息来拟合,系数值被推得极大。

正则化的思想一点也不复杂:在原有损失函数后面加上对系数大小的惩罚,逼着模型在拟合数据和保持系数简洁之间做权衡。

岭回归(Ridge)的损失是:

SSE + λ·Σβⱼ²

它惩罚的是系数的平方和,效果是让所有系数均匀地往零方向收缩,但不真正清零。Lasso 的损失是:

SSE + λ·Σ|βⱼ|

它惩罚的是系数的绝对值之和,由于 L1 范数的几何特性,解更容易落在坐标轴上,也就是一部分系数被精确压缩到零。所以 Lasso 自带特征选择功能,在高维场景下特别好用。

5.2 偏差方差权衡的一次直观理解

推荐大家在看正则化时不要一上来就背公式,而是想明白一个类比。

假设你在射箭。最小二乘模型像是一个瞄得很用力的人,总是瞄着靶心的那个训练样本附近的点去打,训练集上每支箭都离得很近,可一旦换一个数据集,他的瞄准方向就完全被噪声带走了,结果方差很大。部分的箭都掉在靶外。岭回归和 Lasso 则是给他的手加上一点束缚,不让他瞄得太用力,宁可每支箭都稳定地落在靶心中等偏一点的位置,也不要忽内忽外。

λ 就是那个束缚的力度。λ 太小,模型依然过拟合;λ 太大,模型把系数压得太狠,连真正有预测力的信号都被抹掉了,偏差变得不可接受。所以 λ 必须靠交叉验证确定。我会在训练集上把 λ 取对数值等间隔生成几十个候选值,对每个候选值做 K 折交叉验证,选验证误差最小的那个。

5.3 为什么说 ElasticNet 才是更实用的选择

如果特征之间本来存在分组相关性,比如五个特征同属一个业务维度、彼此高度相关,Lasso 的表现就不太稳定,它会在这组相关变量里随机挑一个保留,其他全清零,最终模型的选择带有随机性。这种情况更适合弹性网络(ElasticNet),它在损失里同时加上 L1 和 L2 惩罚:

SSE + λ₁·Σ|βⱼ| + λ₂·Σβⱼ²

这样既保留了 Lasso 的稀疏化能力,又通过 L2 惩罚让相关的特征可以同时被保留或同时被压缩,解更稳定。在我的经验里,遇到高维且特征分组明显的业务数据,ElasticNet 收尾的比例远高于纯 Lasso。

5.4 正则化系数怎么选择

实际项目中我不会只依赖数学公式,还会结合业务解释性来选。比如某个模型虽然交叉验证选了 λ=0.1 的 Lasso,但保留下来的变量里有一个符号方向是反常识的。这时候我会进一步调大 λ,宁可损失一点预测精度,换取模型符合业务直觉。毕竟线性模型的核心价值是可解释性,如果系数本身没法说服业务方,那预测再准也很难被采纳。

6. 一个可以抄作业的建模流程与踩坑总结

6.1 完整流程

我把实际操作中的步骤整理成一个清单,基本可以套用在大部分中小规模的回归任务上。

第一步:梳理业务口径。模型预测的目标是什么、时间窗口怎么定义、特征来自哪些数据源、训练集和验证集怎么切分才能避免泄漏。这一步往往比后面调参更影响模型最终效果。

第二步:做描述性统计。看每个特征的缺失率、均值、分位数、极值,先发现明显的脏数据。特别留意那些缺失率超过 50% 的字段,除非业务明确说明缺失是有意义的,否则我通常会先放着不放进基线模型。

第三步:特征工程。标准化数值变量,编码类别变量,对偏态严重的字段做对数变换,按业务判断构造少量交互项。不要一上来就用工具自动生成几百个交互特征,先让基线模型跑通再说。

第四步:训练基线线性回归,看训练集和验证集的 R² 差距,画残差图,计算 VIF。如果一切正常就交付;如果残差有规律,回去补特征或做变换;如果 VIF 高,考虑删减特征或者换岭回归。

第五步:尝试正则化模型。用交叉验证选 λ,对比线性回归、岭回归、Lasso 和 ElasticNet 在验证集上的表现。

第六步:解读系数,完成业务验证。和业务方逐条过一遍关键变量的符号和量级是否有常识性矛盾,确认模型结论能被业务接受。

6.2 几个我反复踩过的坑

先说数据泄漏。做时间序列预测时特别容易发生:我见过有人直接把未来月份的促销活动变量放进训练集,模型当然表现很好,因为训练集和测试集共享了那些未来信息。等到真正上线预测未来时,这个变量根本不存在,模型直接废掉。防止方法很简单:凡是时间上依赖未来的特征,一律不能用;切分数据时必须按时间顺序,不能用随机切分。

再说特征泄漏到残差里。某次做会员价值预测,特征里放了会员是否已购某一类产品,模型 R² 非常高。但细想这个特征本身可能就是结果的一部分,用它作为因变量的特征循环论证了。判断标准是:这个特征在你要预测的时点是不是已知、是不是可以被提前获取,如果回答不了,宁可不放。

还有一个很隐蔽的坑:one-hot 编码后直接做 Lasso,某些被压缩的类别会把大量信息丢失,整体预测效果反而不如不做特征选择。所以遇到类别变量特别多的场景,我会先对 less frequent categories 做合并,或者改用有序编码。

6.3 一个可复用的最小配置示例

如果你用 Python 和 scikit-learn,有一个非常简洁的 Pipeline 配置,我在多个项目里都用过这个作为起点。下面的代码只是最小演示,实际使用时需要根据数据情况补充缺失值策略和特征选择逻辑。

from sklearn.linear_model import ElasticNet from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import GridSearchCV pipeline = Pipeline([ ("scaler", StandardScaler()), ("en", ElasticNet()) ]) param_grid = { "en__alpha": [0.001, 0.01, 0.1, 1.0, 10], "en__l1_ratio": [0.1, 0.3, 0.5, 0.7, 0.9] } grid = GridSearchCV(pipeline, param_grid, cv=5, scoring="neg_mean_squared_error") grid.fit(X_train, y_train) print(grid.best_params_)

不要只把 GridSearchCV 当作调参工具,它还同时承担了模型选择的功能。每次跑完,我都习惯把每一组参数下的验证集误差画成热力图,观察最优参数附近是不是一片平坦的区域。如果最优参数所在区域非常狭窄、稍微偏离一点误差就剧烈上升,说明模型不稳定,这时候我宁可选择一片大平原上的次优参数。

6.4 我个人最后想留下的几句话

接触建模这几年,我越来越觉得线性回归不是一门"那么简单的课"。它真正考验人的地方在于:面对不可靠的数据、不整洁的特征、看不懂的业务关系时,你能不能保持清醒,通过一套诊断流程把问题定位到具体环节。复杂模型能帮你掩盖问题,线性回归却总是把问题摊开在你面前。

如果你刚开始学,建议不要急着上各种 Boosting 框架,先手撕一遍最小二乘,写个正规方程的矩阵版代码,再画上几次残差图。当你能从一张普通的残差图里读出数据背后的故事时,你对"建模"这两个字的理解就已经超过很多只会调库的人了。那时候再回去用复杂模型,你会发现自己的姿势完全不一样。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进