☰
线性回归实战拆解:从最小二乘到梯度下降全流程解析
2026/10/15 4:57:36 网站建设 项目流程

线性回归是我觉得整个机器学习里最值得认真手写一遍的模型。你别看它形式简单,好像就是 y = wx + b 这条直线,可一旦你拿一份真实数据把流程完整跑通——从数据清洗、特征标准化,到最小二乘与梯度下降两种解法,再到评估指标和正则化调参——你会发现后面学逻辑回归、树模型甚至神经网络时,很多概念都是在这里第一次埋下的。这篇文章就按我自己做项目时的真实顺序,把线性回归从建模思路到代码实现完整拆一遍。适合两类人:一类是刚看完理论、想动手跑第一份数据的同学;另一类是用过第三方库调包,但始终没搞明白 fit 背后到底发生了什么的人。

1. 线性回归是在解决什么问题:建模思路先理清

1.1 从业务问题到数学表达

先想清楚一件事:线性回归处理的是"预测连续数值"的问题。比如某电商团队想知道,投放预算、广告曝光量、受众规模这几个变量是怎么影响成交额的。团队手里有过去几个月的投放记录,每一条记录都是一组特征 X 对应一个真实成交额 y。目标很明确:学到一个函数,输入新的特征组合,输出一个尽可能接近真实值的成交额预测。

写成数学形式就是:

y = w1·x1 + w2·x2 + w3·x3 + b

这里的 w 是每个特征的权重,b 是截距。为了后面代码好写,通常习惯把 b 合并进 w,也就是在特征矩阵左边补一列全 1,统一写成矩阵形式 y ≈ Xw。权重 w 的含义非常直白:w1 是正数,就说明投放预算每增加一个单位,成交额平均增加 w1 个单位。这份解释能力是很多复杂模型给不了的。

为什么不少任务宁愿先用线性模型?第一是可解释性,算出来的系数可以直接拿回去和运营同学对业务;第二是它天然就是个很好的基线。一个复杂模型如果效果还不如线性回归,通常说明特征工程或数据本身有问题,而不是模型不够强。我处理新数据的习惯永远是:先线性回归打底,再谈要不要上更复杂的模型。

1.2 损失函数与拟合的本质

拟合,说白了就是让预测值和真实值的差距尽量小。每个样本的差距叫残差:e = y - ŷ。把所有残差综合起来,最常用的指标是均方误差(MSE):

J(w) = (1/n) Σ (y_i - w·x_i)²

这里的 Σ 表示对所有样本求和。为什么选平方而不是绝对值?不只是因为平方方便求导。平方意味着大残差会被放大,模型会更努力去压低那些偏离很远的点;同时,在误差服从正态分布的假设下,平方损失恰好对应最大似然估计,是有统计理论撑腰的选择。副作用也很明显:它对异常值极度敏感,这一点到数据清洗部分我会再强调。

有了损失函数,训练模型就变成了最优化问题:找一组 w,让 J(w) 最小。好消息是,MSE 对 w 是凸函数,不存在多个局部极小值,有且只有一个全局最小值。求解有两条路:一条是解析地求出闭式解,也就是最小二乘法;另一条是迭代逼近,也就是梯度下降法。两条路我都建议亲手写一遍,先别急着调库。

2. 动手前的数据准备:这一步决定后面 80% 的体验

2.1 数据探索与清洗

很多人拿到数据,第一件事就是直接塞进 fit(),这是最容易翻车的姿势。线性回归对数据质量极其敏感,尤其是异常值和缺失值。我一般先用 pandas 做一轮快速体检:

import pandas as pd import numpy as np df = pd.read_csv("广告投放数据.csv") print(df.shape) print(df.info()) print(df.describe()) print(df.corr())

shape 告诉你有多少样本、多少特征;info() 能看出有没有空值、每一列是什么类型;describe() 给出均值、标准差、最大最小值,特征尺度差异一眼就能发现;corr() 是特征之间以及特征与目标之间的相关性矩阵,后面判断共线性会频繁用到。

拿到结果重点看两件事。第一是缺失值,量少直接均值或中位数填充,量多要思考背后的原因,删列还是删行都行,但不能不处理。第二是异常值。我之前处理一份曝光量数据时,大部分值在几万到几十万区间,结果某几次投放活动产生了上千万的曝光,那几条记录直接把拟合线整个拽歪。处理异常值最常用的办法是 IQR 封顶:把 25% 分位到 75% 分位之外的极端值压到边界值上,这样既保留样本,又不让极端点主导损失函数。

2.2 特征标准化与数据集划分

用梯度下降求解时,特征尺度不一样会非常难受。假设一个特征是"投放预算",量级在十万,另一个特征是"是否节假日",只有 0 和 1,两个方向的梯度差异巨大,更新路径会来回震荡,收敛极慢。解决办法是标准化,把每个特征变成均值为 0、标准差为 1 的分布:

z = (x - mean) / std

这里有个关键细节:标准化必须在训练集上计算均值和标准差,再用同一组参数去转换测试集,绝不能把全量数据一起 fit。否则测试集的信息就提前泄露给了模型,后面评估分数会虚高,上线就露馅。这是新手最容易忽略的坑。

from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X = df[["投放预算", "曝光量", "受众规模"]].values y = df["成交额"].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)

随机种子固定为 42,是为了保证每次跑出来的数据划分一致。后面做模型对比时,误差差异应该来自模型本身,而不是随机划分带来的噪声。

3. 两种主流实现:从零手写与框架调用

3.1 最小二乘法:闭式解的实现

最小二乘的核心思想很直接:想让 J(w) 最小,就对 w 求导并令导数等于零。矩阵形式推导下来,会得到一个非常干净的表达式:

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

公式看着简单,真正用 numpy 实现时有几处细节要注意。先给特征矩阵补一列全 1 作为截距项,然后计算 XᵀX,再求逆、乘 Xᵀy。代码如下:

def fit_ols(X, y): Xb = np.hstack([np.ones((X.shape[0], 1)), X]) return np.linalg.inv(Xb.T @ Xb) @ Xb.T @ y w_ols = fit_ols(X_train, y_train) print(w_ols)

实际工程里,我不会直接用 np.linalg.inv 去硬算逆矩阵,数值上更稳的是 np.linalg.solve 直接解线性方程组,或者用 np.linalg.lstsq。inv 方案在 XᵀX 接近奇异时数值误差很大,表现就是系数出现夸张的巨大正负值,模型完全没法解释。

闭式解的局限性也很明显。计算复杂度大致是特征维度的三次方,特征一旦到几千上万,求逆会变得很慢甚至内存爆掉。而且当特征之间存在强共线性时,XᵀX 不可逆,解不出来。所以它适合特征数量不多、数据量适中的场景;大数据和高维特征下,迭代方法才是主力。

3.2 梯度下降法:迭代逼近的工程实现

梯度下降的思路和闭式解完全不同:从一个初始 w 出发,沿着损失函数下降最快的方向不断更新参数:

w = w - learning_rate * grad

对 MSE 求梯度,结果非常漂亮:

grad = (2/n) · Xᵀ(Xw - y)

这个式子可以这么理解:先算当前预测和真实值的残差,再让每个特征与残差做内积,得到该特征对损失贡献的方向。实现如下:

def gradient_descent(X, y, lr=0.1, epochs=500): Xb = np.hstack([np.ones((X.shape[0], 1)), X]) n = len(y) w = np.zeros(Xb.shape[1]) loss_history = [] for epoch in range(epochs): y_pred = Xb @ w grad = (2 / n) * Xb.T @ (y_pred - y) w = w - lr * grad loss = np.mean((y_pred - y) ** 2) loss_history.append(loss) return w, loss_history

跑完之后,把 loss_history 画成曲线,应当平滑下降并最终趋于水平。如果 loss 直接变成 NaN 或者不降反升,基本就是学习率太大,从 0.1 降到 0.01 或 0.001 再试。如果下降得像蜗牛,可能是学习率太小,也可能是前面说的特征没标准化。

数据量很大的时候,全量梯度计算成本太高,可以改用小批量梯度下降,每次随机抽一批样本算梯度。batch size 取 32、64、128 都是常见选择,它兼顾了稳定性和速度,也是后面深度学习里真正在用的套路。手写版和框架版的核心区别就在这里:手写版帮你理解每一步在算什么,框架版帮你把每一步跑得更快更稳。

对比项最小二乘(闭式解)梯度下降(迭代)
思路直接求导令梯度为 0沿负梯度反复更新
计算成本随特征维度三次方增长取决于迭代次数和数据量
大数据集不友好,内存压力大可配小批量,几乎无上限
共线性场景XᵀX 奇异时直接失效同样受影响,可配合正则化
全局最优精确解析解凸函数下可收敛到全局最优

4. 用第三方库快速实战:全流程与指标解读

4.1 完整建模流程演示

手写过一遍之后,再回到第三方库就踏实多了。用开源库 sklearn 的 LinearRegression,几行就能完成同样的任务:

from sklearn.linear_model import LinearRegression model = LinearRegression() model.fit(X_train, y_train) print("截距:", model.intercept_) print("系数:", model.coef_) y_pred = model.predict(X_test)

你会发现,这里输出的系数和手写最小二乘几乎一模一样,差异只在浮点计算的精度级别。fit 这个动作背后,本质上就是在解前面的最优化问题。理解了底层之后,换任何框架都不会慌——它再怎么封装,求的还是那一组 w。

这套流程里有个小细节:LinearRegression 默认自带截距项,不需要你在数据里手动补一列 1。但某些业务场景明确要求拟合线必须过原点,可以设置 fit_intercept=False。这个开关平时很少动,但调参和面试时经常被问到,知道它存在就不会一头雾水。

4.2 评估指标:不止是看 R²

模型训练完,评估才是真正花心思的地方。评测必须只在测试集上进行,训练集误差再低也可能是模型把数据背下来了。常用指标我列成了一张表:

指标公式一句话理解
MSE(1/n) Σ(y - ŷ)²误差平方的平均,会放大特大误差
RMSEsqrt(MSE)回到原始单位,方便向业务解释
MAE(1/n) Σy - ŷ
R²1 - SS_res / SS_tot相比均值基线,模型解释了多少方差

计算直接调用现成函数:

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score print("MSE:", mean_squared_error(y_test, y_pred)) print("RMSE:", np.sqrt(mean_squared_error(y_test, y_pred))) print("MAE:", mean_absolute_error(y_test, y_pred)) print("R²:", r2_score(y_test, y_pred))

我自己的习惯是 RMSE 和 R² 一起看。RMSE 带原始单位,能直接和业务对口径,比如"预测成交额平均偏差 1.2 万";R² 说明模型的相对解释力。R² 到 0.95 别急着高兴,先查是不是过拟合;R² 出现负值也别慌,这说明模型比"直接用平均值当预测"还差,通常是特征没选对,或者线性假设本身不成立。

5. 过拟合、欠拟合与正则化:线性回归的另一面

5.1 如何判断模型处于哪种状态

线性回归看着简单,同样躲不开偏差与方差的权衡。判断方法就是对比训练误差和测试误差:

现象训练误差测试误差结论
模型太简单高高欠拟合
模型记住噪声低高过拟合
两者都低且接近低低健康状态

欠拟合的时候,加正则化反而雪上加霜。正确做法是增加有效特征、做特征多项式扩展,或者直接换更灵活的模型。过拟合的时候,优先考虑三类手段:增加训练数据、减少特征数量、加正则化惩罚。三招里面加数据最有效但成本最高;减特征需要业务判断;正则化是成本最低、见效最快的通用方案。

5.2 岭回归与 Lasso 的实战选择

正则化是在原损失函数后面加一个惩罚项,让系数不要长得太放肆。岭回归加了 L2 惩罚,系数整体被压缩但不会变成零,尤其擅长处理特征共线性的情况。Lasso 加了 L1 惩罚,会把一部分不重要特征的系数直接压成零,天然做特征选择。两者都通过 alpha 控制惩罚强度,alpha 越大,压缩越狠。

选择 alpha 用交叉验证最稳,sklearn 提供了现成的 CV 类:

from sklearn.linear_model import RidgeCV, LassoCV ridge = RidgeCV(alphas=[0.01, 0.1, 1, 10], cv=5) ridge.fit(X_train, y_train) lasso = LassoCV(alphas=[0.01, 0.1, 1, 10], cv=5) lasso.fit(X_train, y_train)

cv=5 表示把训练集切成 5 份,轮流拿 4 份训练、1 份验证,最终平均误差最小的 alpha 会被选中。实际项目里,如果特征多但业务上认为真正起作用的只有少数几个,我倾向先用 Lasso 看哪些系数被压成 0,再用剩下的特征去做报告;如果特征是高度相关的连续变量,就用 Ridge,它保住所有特征,只是统一收缩。

6. 常见问题与排查技巧实录

6.1 损失不下降、结果异常时怎么排查

手写模型和框架模型都会遇到各种怪问题,我把踩过的坑整理成速查表:

现象常见原因检查手段
loss 为 NaN 或爆炸学习率过大调小 lr,从 0.001 开始
loss 下降极慢未做特征标准化检查特征量纲差异
loss 震荡不收敛学习率偏高或数据有异常值逐轮打印 loss 曲线
系数异常巨大XᵀX 接近奇异或共线性看 corr 矩阵,改用岭回归
代码报维度不匹配忘补截距列或矩阵方向反打印 X 和 w 的 shape

排查时最有效的习惯是每轮迭代都记录 loss,画出来看趋势。曲线观察比只看一个最终数值信息量大得多:平滑下降说明一切正常;突然跳升说明学习率太大;下降到一半变平但值还很高,说明模型容量不足,不是优化的问题。

6.2 特征共线性、结果波动与数据泄漏

特征共线性的典型表现是:单看每个特征和目标的相关系数都不低,但模型系数忽正忽负,换个数据划分系数就大变。原因在于强相关特征之间可以互相替代,最优解不唯一。判断标准除了相关系数,还可以算 VIF(方差膨胀因子),超过 10 就该警惕。处理办法很简单:删掉冗余特征,或者直接用岭回归。

结果波动也是高频问题。单次随机划分数据,模型表现可能忽好忽坏,数据量越小越明显。对策是固定 random_state 做对比实验,或者用 k 折交叉验证评估整体水平。还有一个隐蔽的坑是数据泄漏:标准化时把测试集的均值和标准差混进训练集,或者时间序列数据随机打乱导致未来信息混入训练。这类问题不会报错,只会让测试集分数虚高,等上线后直接崩掉。

最后说一点个人体会:手写一遍线性回归,收益远大于把公式背下来。每次拿到全新数据,我都会先用线性回归打底,把它的系数和 RMSE 当作一切复杂模型的起点基准。模型再花哨,如果连线性基线都跑不过,多半是数据或特征出了问题,而不是模型不够强。你也动手跑一遍吧,把该踩的坑踩完,后面学任何模型都会顺很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询