线性回归:从最小二乘到生产级实践

标签: machinelearning, datascience, python, tutorial

线性回归是大多数人接触的第一个算法,也是大多数人从未深入研究的算法。它也是在更复杂的算法失效后,仍然会在生产环境中被保留的模型,因为它快速、稳定且可解释。

本文不是一个“调用 .fit() 并读取分数”的教程。我们将涵盖数学原理、统计假设、诊断、正则化、评估、生产注意事项,以及区分初学者与工程师的面试问题。

为什么线性回归值得再次审视

线性回归是理解几乎所有其他监督模型的基础:

  • 逻辑回归是带 sigmoid 的线性回归。
  • Ridge 和 Lasso 是带约束权重的线性回归。
  • 神经网络是带非线性激活的堆叠线性变换。
  • 树模型的判断基准是:“我能击败线性模型吗?”

更重要的是,线性回归在许多业务问题中仍然是正确答案。当需要向监管机构、客户或财务团队解释预测时,一个带有可解释系数的简洁线性模型胜过黑箱模型。

数学原理:最小二乘与正规方程

给定特征 X 和目标 y,线性模型假设:

y = X * beta + epsilon

Enter fullscreen mode Exit fullscreen mode

目标是最小化残差平方和:

L(beta) = ||y - X*beta||^2

Enter fullscreen mode Exit fullscreen mode

beta 求导并设为零,得到正规方程

beta = (X^T * X)^(-1) * X^T * y

Enter fullscreen mode Exit fullscreen mode

在实践中,使用伪逆(pinv)而非逆矩阵,因为当特征共线性时,X^T X 可能奇异或数值不稳定。

import numpy as np

def normal_equation(X, y):
    Xb = np.c_[np.ones(X.shape[0]), X]  # 添加截距
    beta = np.linalg.pinv(Xb.T @ Xb) @ Xb.T @ y
    return beta

Enter fullscreen mode Exit fullscreen mode

最小二乘的三种等效视角

1. 几何视角

预测值 X * betayX 列空间上的投影。最小二乘在该子空间中找到最近点。这也是残差与拟合值正交的原因。

2. 最大似然视角

假设:

epsilon ~ N(0, sigma^2)

Enter fullscreen mode Exit fullscreen mode

则最大化对数似然等价于最小化平方误差和。这种联系解释了为什么残差正态性对置信区间很重要,即使在较弱假设下 OLS 系数仍然一致。

3. 优化视角

对于大数据集,计算 (X^T X)^(-1) 成本高昂。梯度下降迭代求解同一目标:

def gradient_descent(X, y, lr=0.01, epochs=500):
    Xb = np.c_[np.ones(X.shape[0]), X]
    n, d = Xb.shape
    beta = np.zeros(d)
    for _ in range(epochs):
        grad = (2 / n) * Xb.T @ (Xb @ beta - y)
        beta -= lr * grad
    return beta

Enter fullscreen mode Exit fullscreen mode

正规方程:精确,适合中小数据集。梯度下降:可扩展,但需要特征缩放和学习率调优。

必须检查的假设

拟合后 R 平方很高的模型并不自动可靠。这些假设使系数可解释:

假设 含义 检查方法
线性 特征与目标呈线性关系 残差-拟合值图
独立性 误差不相关 Durbin-Watson 统计量
同方差性 误差方差恒定 Breusch-Pagan 检验、残差图
正态性 误差呈正态分布 Q-Q 图、Jarque-Bera 检验
无多重共线性 特征不高度相关 VIF、相关矩阵

完整诊断示例

import statsmodels.api as sm
from statsmodels.stats.stattools import durbin_watson
from statsmodels.stats.diagnostic import het_breuschpagan
from statsmodels.stats.outliers_influence import variance_inflation_factor
from scipy import stats
import pandas as pd

X_const = sm.add_constant(X_train)
model = sm.OLS(y_train, X_const).fit()
resid = model.resid

print("Durbin-Watson:", durbin_watson(resid))
# 接近 2 表示无自相关;接近 0 或 4 是警告。
    lm, lm_pvalue, fvalue, f_pvalue = het_breuschpagan(resid, X_const)
print("Breusch-Pagan p-value:", f_pvalue)
# p > 0.05:无强异方差证据。
    print("Jarque-Bera p-value:", stats.jarque_bera(resid).pvalue)
# p > 0.05:无强非正态证据。
    vif = pd.DataFrame({
    "feature": X_train.columns,
    "VIF": [variance_inflation_factor(X_train.values, i)
            for i in range(X_train.shape[1])],
})
print(vif)
# VIF > 10 通常视为多重共线性警告。

Enter fullscreen mode Exit fullscreen mode

正则化:偏差-方差权衡

普通最小二乘最小化训练误差,当特征较多或共线性时容易过拟合。正则化添加惩罚:

Ridge:       L = MSE + alpha * sum(beta_i^2)
Lasso:       L = MSE + alpha * sum(|beta_i|)
ElasticNet:  L = MSE + alpha * (r * L1 + (1 - r) * L2)

Enter fullscreen mode Exit fullscreen mode

模型 惩罚 效果 适用场景
Ridge L2 收缩权重,保留所有特征 特征高度相关
Lasso L1 将部分权重精确收缩为零 需要特征选择
ElasticNet L1 + L2 稀疏且对相关特征组稳定 两种需求兼顾

关键细节:正则化前务必标准化特征,否则惩罚会不公平地压缩量纲大的特征。

from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet

models = {
    "OLS": LinearRegression(),
    "Ridge(alpha=1)": Ridge(alpha=1.0),
    "Lasso(alpha=0.01)": Lasso(alpha=0.01),
    "ElasticNet(alpha=0.01)": ElasticNet(alpha=0.01, l1_ratio=0.5),
}

for name, model in models.items():
    pipe = make_pipeline(StandardScaler(), model)
    pipe.fit(X_train, y_train)
    y_pred = pipe.predict(X_test)
    print(f"{name:20s} RMSE={mean_squared_error(y_test, y_pred, squared=False):.4f} "
          f"R2={r2_score(y_test, y_pred):.4f}")

Enter fullscreen mode Exit fullscreen mode

评估指标

不要仅报告 R 平方。每个指标讲述不同的故事:

指标 公式含义 适用场景
MAE 平均绝对误差 不希望对误差平方,异常值影响较小
RMSE 均方根误差 大误差尤其有害
MAPE 平均绝对百分比误差 业务需要百分比解释
方差解释比例 比较模型质量
调整 R² 按特征数量惩罚后的 R² 比较不同特征集的模型

始终与均值基线对比。如果模型仅略优于“预测平均值”,说明特征贡献不大。

baseline_pred = np.full_like(y_test, y_train.mean())
print("Baseline RMSE:", mean_squared_error(y_test, baseline_pred, squared=False))

Enter fullscreen mode Exit fullscreen mode

完整端到端示例

import numpy as np
import pandas as pd
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

data = fetch_california_housing(as_frame=True)
X = data.frame.drop(columns=["MedHouseVal"])
y = data.frame["MedHouseVal"]

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 1. 手写正规方程
beta = normal_equation(X_train.values, y_train.values)
X_test_b = np.c_[np.ones(X_test.shape[0]), X_test.values]
y_pred_manual = X_test_b @ beta

# 2. sklearn 基线
from sklearn.linear_model import LinearRegression
lr = LinearRegression().fit(X_train, y_train)
y_pred_sklearn = lr.predict(X_test)

for name, y_pred in [("manual", y_pred_manual), ("sklearn", y_pred_sklearn)]:
    print(f"{name}: RMSE={mean_squared_error(y_test, y_pred, squared=False):.4f} "
          f"MAE={mean_absolute_error(y_test, y_pred):.4f} "
          f"R2={r2_score(y_test, y_pred):.4f}")

Enter fullscreen mode Exit fullscreen mode

手写解与 sklearn 解应产生几乎相同的结果。若不相同,则梯度下降未收敛或特征未缩放。

生产检查清单

  1. 特征缩放:梯度下降和正则化模型必需。
  2. 交叉验证:切勿在测试集上调优 alpha。
  3. 数据泄露:在缩放、编码或插补前先划分。
  4. 监控:部署后跟踪特征分布与预测漂移。
  5. 可解释性:记录系数、特征重要性与 SHAP 值以备审计。
  6. 简单优先:在尝试 XGBoost 前先用线性回归。

常见错误

1. 相关不等于因果

高系数不意味着改变特征会改变目标。遗漏变量和反向因果始终可能。

2. 外推

线性模型在训练范围外危险。训练于 300 平米以下的房屋模型,无法预测 3000 平米房屋价格。

3. 高 R 平方不等于好预测

R 平方衡量样本内拟合。模型可在训练集上 R 平方很高,在未见数据上 RMSE 却很差。

4. 忽略残差

若残差有模式,说明模型缺失结构。不要盲目添加更多线性特征来“修复”非线性模式。

5. 正则化时使用原始特征

未标准化时,Ridge 和 Lasso 会对不同量纲的特征施加不平等惩罚。

面试常见问题

  1. 正规方程与梯度下降的区别?
  2. 为什么在正态误差假设下 OLS 等价于最大似然?
  3. X^T X 奇异时会发生什么?Ridge 如何帮助?
  4. 如何检测并处理多重共线性?
  5. 为什么使用调整 R 平方而非 R 平方?
  6. 残差-拟合值图能告诉我们什么?
  7. 何时优先选择 Lasso 而非 Ridge?
  8. 为什么 L1 或 L2 正则化前必须标准化?
  9. 异方差性的后果?
  10. 如何在回归流水线中检测数据泄露?

结论

线性回归看似简单,却包含机器学习的核心思想:目标函数、优化、统计假设、正则化、评估与部署。若能深入理解这一模型,其后的所有算法都将更容易学习。

从数学入手,验证假设,牢记生产关注点。那个“过于简单”的模型往往在生产环境中存活最久。


觉得有用?关注我获取更多实用 AI 与数据工程文章。
没 ;。l