线性回归全解析:从最小二乘到梯度下降的优化之旅
1. 为什么线性回归是所有机器学习入门的第一道坎很多人第一次接触机器学习是从房价预测或者成绩预测这类例子开始的。你有一堆数据房子面积、卧室数量、地段评分要预测房价或者复习时长、历史成绩要预测期末分数。第一次看到y wx b这个公式时你可能觉得这不就是初中数学的一次函数吗没错线性回归就是从这里出发但它把所有机器学习的关键环节都串了起来模型假设、损失函数、优化方法、评估指标、过拟合与欠拟合。所以我说线性回归是所有机器学习入门的第一道坎迈过它后面的逻辑回归、决策树、神经网络学起来都会顺很多。这篇内容我打算用一篇相对完整的篇幅把线性回归和它的优化方法讲透。不光是公式推导还包括为什么选这种优化方法、怎么在Python里从零实现、以及你在实操中一定会遇到的那些坑。无论你是准备面试、准备期末复习还是刚从吴恩达的课程视频里出来想动手写代码这篇文章都适合你。1.1 线性回归在机器学习中的位置先明确一个概念线性回归属于监督学习中的回归问题目标是拟合一个连续型的目标变量。它的核心假设是目标值y与特征x之间存在线性关系只不过我们观察到的是带噪声的数据。我们要做的是从数据中估计出参数w和b使模型在训练集上的预测误差尽可能小。在监督学习的分类里回归和分类是两条主线。回归输出连续数值分类输出离散类别。线性回归就是回归这条线的最基础模型。整个机器学习体系里很多模型都能追溯到线性回归逻辑回归是线性回归加了一个 Sigmoid 变换支持向量机的线性核本质也在学一个线性决策边界神经网络的全连接层其实就是一层线性变换加激活函数。可以说线性回归学好后面很多东西都是它的变体或组合。1.2 一个最小可运行的例子直观理解为了让后面所有讨论都有落点我在这里先给一个极简例子。假设只有一维特征x比如学习时长目标y比如考试分数数据生成函数是import numpy as np rng np.random.default_rng(42) x np.linspace(0, 10, 100) true_w 2.5 true_b 5.0 y true_w * x true_b rng.normal(0, 2, sizex.shape)这里的y是真实线性关系加上高斯噪声后的观测值。我们的任务是从(x, y)的数据对中尽量准确地估计出true_w和true_b。后面所有优化方法的讨论都会围绕如何找回这两个参数展开。这个例子够小但麻雀虽小五脏俱全它包含了线性回归的所有关键要素。2. 线性回归模型背后的数学直觉与前提条件有人会觉得线性回归太简单不值得深究。但恰恰是简单模型能把优化的本质暴露出来。理解线性回归的数学直觉比背公式重要得多。2.1 从一元到多元参数线性才是关键一元线性回归就是一条直线y wx b。到了多元场景模型写成y w_1 x_1 w_2 x_2 ... w_n x_n b如果引入x_0 1可以统一写成向量形式y w^T x这里的w和x都是列向量。注意线性回归的线性指的是参数线性不是特征线性。也就是说模型输出对参数w是线性的但对特征x不一定是线性的。比如y w_1 x w_2 x^2依然是线性回归因为它在参数上是线性的只是把x^2当成了一个新特征。这一点很多人会混淆所以特别提一下。理解了这点后面的多项式回归就顺理成章了。2.2 最小二乘的几何直觉残差平方和与投影线性回归最常见的损失函数是最小二乘损失也就是残差平方和RSSL(w) sum_{i1}^{m} (y_i - w^T x_i)^2为什么是平方而不是绝对值两个原因一是平方函数处处可导方便求梯度二是它惩罚大误差的速度远大于小误差相当于让模型更在意那些偏差大的样本。从几何上看最小二乘解实际上是在做高维空间中的向量投影。我把所有样本特征按行堆叠成矩阵XX的列向量张成一个线性空间预测值Xw就是y在这个空间上的投影。最小化残差平方和就是找到投影点使投影点到y的距离最短。所以正规方程的解w (X^T X)^{-1} X^T y本质上就是一个投影矩阵作用在y上。2.3 四个经典假设与它们被违反时的后果线性回归的经典教科书会强调四个假设。虽然在实际工程中很难完全满足但了解它们是理解模型局限性的关键。第一线性关系。特征和目标之间是线性关系如果真实关系是曲线线性回归会欠拟合残差图会出现明显模式。第二误差独立同分布且均值为零。如果误差存在自相关比如时间序列数据系数估计的标准误会被低估。第三同方差性即误差方差一致。如果方差不恒定就是异方差最小二乘估计虽然仍无偏但不再是有效估计。第四无多重共线性即特征之间不能高度相关。如果两个特征几乎一样X^T X会接近奇异参数估计变得极不稳定。实操中我更愿意把假设当成诊断工具。训练完模型后画一下残差 vs 预测值的散点图如果残差随机分布在零线附近说明模型基本合理如果出现漏斗形或曲线形就该考虑做特征变换或者换更复杂的模型。3. 优化方法选型最小二乘、梯度下降与正规方程怎么选线性回归的优化方法大致分三类正规方程Normal Equation、梯度下降Gradient Descent以及基于矩阵分解的 QR/SVD 方法。这里重点讲前两类因为它们最能体现机器学习优化的核心思想。3.1 正规方程一场矩阵运算解决最优解对于线性回归的平方损失可以直接通过求导令梯度为零得到闭式解w (X^T X)^{-1} X^T y这就是正规方程。它不需要迭代一次矩阵运算就能得到最优解。但问题也出在矩阵运算上X^T X的逆需要计算量复杂度近似O(n^3)其中n是特征数量。当特征数量在 1000 以内时这个开销通常可以接受但当特征数量上万甚至更多时计算和存储X^T X就不太现实了。另一个问题是X^T X可能不可逆。原因包括特征之间存在完全共线性、样本数小于特征数。这时正规方程会报错或者得到数值不稳定的结果。所以正规方程适合小规模、特征相关度不高的场景。在 sklearn 的LinearRegression内部默认使用基于 SVD 的求解器数值稳定性比直接求逆好很多这也是我建议你尽量用现成库的原因。3.2 梯度下降家族批量、随机与小批量的博弈梯度下降的思路非常直观从一个初始点出发沿着损失函数下降最快的方向负梯度方向走一步然后重复直到收敛。用生活化的比喻就像你在山里浓雾中想走到谷底每一步只能判断当前最陡的下坡方向然后迈出一步。学习率就是步子大小。最朴素的是批量梯度下降BGD每一步用所有样本计算梯度更新公式w w - lr * (1/m) * X^T (Xw - y)它的优点是方向稳定缺点是大数据量下每步计算太慢。随机梯度下降SGD则每一步只用一个样本计算梯度更新极快但方向噪声大损失曲线会上下震荡。小批量梯度下降Mini-batch GD是两者的折中每次用一小批样本比如 32、64、128 个计算梯度既降低了方差又能利用矩阵运算的并行加速。现在深度学习里用的就是小批量梯度下降它可以说是所有优化器的基石。选择哪种方法取决于数据规模。特征数几千、样本数几十万以内我建议直接用量小二乘类方法特征或样本规模很大、需要在线学习或者要扩展到神经网络时小批量梯度下降是更稳的选择。3.3 维度灾难下的正则化优化目标当特征维度很高或者存在多重共线性时普通最小二乘的权重会变得很大模型方差很高。这时需要在损失函数里加上正则项限制权重大小。最常见两种L2 正则化RidgeL loss lambda * ||w||_2^2让权重整体变小但不会为 0。L1 正则化LassoL loss lambda * ||w||_1会让一部分权重变成 0起到特征选择作用。正则化的本质是给优化目标增加一个约束相当于在平坦的损失面上增加了一个偏好。从优化的角度看L1 正则导致目标函数在零点不可导因此不能用简单梯度下降直接求解通常用坐标下降或近端梯度法L2 正则让目标函数强凸梯度下降收敛更稳定。这也是为什么在机器学习算法库里Ridge 和 Lasso 通常有各自专门的求解器。4. 用Python从零实现线性回归并对比sklearn理论讲多了容易飘直接手写代码才知道细节有多重要。下面我用 NumPy 从零实现正规方程和小批量梯度下降再和 sklearn 的结果对比。4.1 NumPy手写正规方程十行代码理解核心先写一个最简单的正规方程实现。为了处理偏置b我们给特征矩阵加一列全 1。import numpy as np class LinearRegressionNormal: def fit(self, X, y): # 添加偏置列 X np.column_stack([np.ones(X.shape[0]), X]) # 正规方程 w (X^T X)^(-1) X^T y XtX X.T X self.w np.linalg.pinv(XtX) X.T y return self def predict(self, X): X np.column_stack([np.ones(X.shape[0]), X]) return X self.w这里我用的是pinv伪逆而不是inv。虽然在这个小例子里两者结果一样但伪逆在X^T X接近奇异时更稳定。如果你去看 sklearn 源码会发现它内部也用了更稳定的 SVD 分解。这个细节就是工程和课本的区别。4.2 手写小批量梯度下降跟踪损失变化接下来实现小批量梯度下降。核心有三步随机打乱数据、按 batch 计算梯度、更新权重。为了优化效果更好我会对特征做标准化。class LinearRegressionSGD: def __init__(self, lr0.01, epochs100, batch_size32): self.lr lr self.epochs epochs self.batch_size batch_size self.loss_history [] def fit(self, X, y): # 标准化 self.mean X.mean(axis0) self.std X.std(axis0) 1e-8 X (X - self.mean) / self.std X np.column_stack([np.ones(X.shape[0]), X]) m, n X.shape self.w np.zeros(n) for epoch in range(self.epochs): # 随机打乱 indices np.random.permutation(m) X X[indices] y y[indices] for i in range(0, m, self.batch_size): X_batch X[i:iself.batch_size] y_batch y[i:iself.batch_size] grad (1 / self.batch_size) * X_batch.T (X_batch self.w - y_batch) self.w - self.lr * grad # 记录每个 epoch 结束时的全量损失 pred X self.w loss np.mean((pred - y) ** 2) self.loss_history.append(loss) return self def predict(self, X): X (X - self.mean) / self.std X np.column_stack([np.ones(X.shape[0]), X]) return X self.w注意这里标准化时的均值/方差必须用训练集拟合不能在predict里重新计算否则会造成数据泄露。这个坑我在初学阶段踩过后面你测试集上的结果就会虚高。4.3 与sklearn结果对比及评估指标解读用之前生成的数据做对比from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score X x.reshape(-1, 1) model_sk LinearRegression().fit(X, y) pred_sk model_sk.predict(X) model_np LinearRegressionNormal().fit(X, y) pred_np model_np.predict(X) model_sgd LinearRegressionSGD(lr0.05, epochs200, batch_size16).fit(X, y) pred_sgd model_sgd.predict(X) print(sklearn w:, model_sk.coef_, b:, model_sk.intercept_) print(normal w:, model_np.w[1:], b:, model_np.w[0]) print(sgd w:, model_sgd.w[1:], b:, model_sgd.w[0])由于数据量小三种方法得到的参数都很接近。评估回归模型时最常用三个指标MSE均方误差单位和目标值平方一致注意对异常值敏感。RMSEMSE 开根号单位与目标一致便于解释。R²决定系数表示模型解释了多少比例的目标方差。默认值是 1 减残差平方和除以总平方和范围通常在 0 到 1 之间越接近 1 越好。但注意R² 在测试集上可能为负数说明模型比直接用均值预测还差。print(MSE:, mean_squared_error(y, pred_sgd)) print(RMSE:, mean_squared_error(y, pred_sgd, squaredFalse)) print(R2:, r2_score(y, pred_sgd))在这份仿真数据上R² 通常会很高因为真实关系确实线性。但换成真实数据R² 能到 0.8 以上就算相当不错了。5. 实操中的经典坑特征缩放、学习率与过拟合调试线性回归代码看着简单实际跑起来总会有各种意外。下面这些问题是我在教学和项目里反复遇到的也是期末复习或面试时常被考的点。5.1 特征缩放为什么标准化能让梯度下降跑得更稳梯度下降对特征尺度非常敏感。如果一个特征是年龄0-100另一个特征是收入0-100000那么权重更新时梯度会被大尺度特征主导导致收敛路径呈锯齿状。标准化z-score或归一化min-max能把这些特征统一到相近的尺度。正规方程不需要特征缩放因为它一步到位但使用梯度下降时我强烈建议先标准化。用代码感受一下如果不标准化同一份数据用梯度下降可能要 10000 步才收敛标准化后200 步就能达到很低的损失。我习惯把标准化封装在模型类里避免每次预测前忘记处理。5.2 学习率调试损失曲线是你最好的仪表盘学习率是最难调的 hyperparameter 之一。太大损失震荡甚至发散太小收敛极慢。怎么判断画损失曲线。如果训练过程中 loss 在下降后开始增大多半是学习率太大如果 loss 平稳但下降很慢可以调大学习率如果 loss 一开始就变成 NaN那是学习率爆了。我常用的学习率调试策略是从1e-2开始跑 50 个 epoch观察 loss 曲线。若震荡就降到1e-3若下降太慢就升到1e-1。另一个技巧是学习率衰减随着 epoch 增加逐步降低学习率让初期大步快速下降后期小步精细收敛。实现也简单比如每 50 轮将学习率乘以 0.9。5.3 评估陷阱训练集上的R²不是一切很多人刚学的时候把训练集上的损失当作模型好坏的标准。这是大忌。线上实际效果好不好要看模型在没见过的数据上的表现。正确做法是用训练集拟合参数再在验证集或测试集上评估指标。如果训练集 R² 很高但测试集 R² 很低就是过拟合。线性回归在高维小样本场景很容易过拟合比如特征数比样本数还多这时模型会记住训练数据的噪声。处理过拟合的办法有几种一是增加数据量二是做特征筛选删除无关特征三是加正则化Ridge 或 Lasso四是做交叉验证比如 K-Fold用多次划分的平均评估代替单次划分。线性回归虽然简单但训练集评估乐观这个坑在深度学习里照样存在所以从线性回归开始就养成正确评估的习惯非常值。6. 从线性回归走向更广的机器学习地图线性回归不是终点但它是理解更复杂模型的起点。这里我梳理几条常见的扩展路径帮你把知识连成网络。6.1 多项式与逻辑回归线性假设的两个自然延伸当数据关系不是直线时可以把特征做多项式变换比如x^2、x^3然后继续用线性回归拟合。这就是多项式回归。它本质是线性回归但特征空间更丰富可以拟合曲线。需要注意多项式阶数过高容易过拟合一般用交叉验证选阶数。另一个方向是逻辑回归把线性输出通过 Sigmoid 函数映射到 0-1 之间用交叉熵作为损失函数用于分类。逻辑回归虽然名字里有回归实际是分类算法。它的优化方法仍然可以是梯度下降。理解了线性回归的损失函数和优化方法再看逻辑回归的梯度下降你会发现它们俩的代码结构几乎一样只是换了损失函数和预测表达式。6.2 神经网络中的线性层与端到端优化神经网络的全连接层做的事情就是z Wx b然后过一个非线性激活函数。所以一个不含隐藏层的神经网络本质上就是线性回归。当你把多个线性层堆叠起来并插入非线性激活函数模型表达能力才得到提升。理解这一点你就明白为什么深度学习库里的优化器SGD、Adam和线性回归梯度下降是同源的都是计算损失对参数的梯度然后沿负梯度方向更新。区别只是网络层数多、梯度计算用反向传播而不是直接求导。6.3 学完线性回归后我建议你做的三件事第一用真实数据集完整走一遍流程加载数据、拆分训练测试、标准化、训练、评估、画残差图。比如 UCI 的 Boston 房价数据集虽然现在 sklearn 里移除了但可以通过其他渠道下载。第二把线性回归改成 Ridge 回归用交叉验证搜一遍正则化参数lambda体会正则化带来的稳定性变化。第三尝试用 PyTorch 写一个简单的线性回归模型从nn.Linear开始用SGD优化器训练一次。这一步能帮你把深度学习框架的接口和线性回归的数学对应起来为后面学神经网络铺路。最后再分享一个小技巧不管用什么模型我都会在训练后打印一份包含训练集和测试集的指标对比。这个习惯让我好几次在模型部署前发现了过拟合问题。线性回归虽然朴素但把它从原理到实现、从训练到评估完整跑通你后面学任何模型都会有一种原来如此的踏实感。