news 2026/9/9 21:07:22

线性回归全解析:从最小二乘到梯度下降的优化之旅

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
线性回归全解析:从最小二乘到梯度下降的优化之旅

1. 为什么线性回归是所有机器学习入门的第一道坎

很多人第一次接触机器学习,是从"房价预测"或者"成绩预测"这类例子开始的。你有一堆数据:房子面积、卧室数量、地段评分,要预测房价;或者复习时长、历史成绩,要预测期末分数。第一次看到y = wx + b这个公式时,你可能觉得这不就是初中数学的一次函数吗?没错,线性回归就是从这里出发,但它把所有机器学习的关键环节都串了起来:模型假设、损失函数、优化方法、评估指标、过拟合与欠拟合。所以我说,线性回归是所有机器学习入门的第一道坎,迈过它,后面的逻辑回归、决策树、神经网络学起来都会顺很多。

这篇内容我打算用一篇相对完整的篇幅,把线性回归和它的优化方法讲透。不光是公式推导,还包括为什么选这种优化方法、怎么在Python里从零实现、以及你在实操中一定会遇到的那些坑。无论你是准备面试、准备期末复习,还是刚从吴恩达的课程视频里出来想动手写代码,这篇文章都适合你。

1.1 线性回归在机器学习中的位置

先明确一个概念:线性回归属于监督学习中的回归问题,目标是拟合一个连续型的目标变量。它的核心假设是:目标值y与特征x之间存在线性关系,只不过我们观察到的是带噪声的数据。我们要做的,是从数据中估计出参数wb,使模型在训练集上的预测误差尽可能小。

在监督学习的分类里,回归和分类是两条主线。回归输出连续数值,分类输出离散类别。线性回归就是回归这条线的最基础模型。整个机器学习体系里,很多模型都能追溯到线性回归:逻辑回归是线性回归加了一个 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, size=x.shape)

这里的y是真实线性关系加上高斯噪声后的观测值。我们的任务是从(x, y)的数据对中,尽量准确地估计出true_wtrue_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

这里的wx都是列向量。注意,线性回归的"线性"指的是参数线性,不是特征线性。也就是说,模型输出对参数w是线性的,但对特征x不一定是线性的。比如y = w_1 x + w_2 x^2依然是线性回归,因为它在参数上是线性的,只是把x^2当成了一个新特征。这一点很多人会混淆,所以特别提一下。理解了这点,后面的多项式回归就顺理成章了。

2.2 最小二乘的几何直觉:残差平方和与投影

线性回归最常见的损失函数是最小二乘损失,也就是残差平方和(RSS):

L(w) = sum_{i=1}^{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 正则化(Ridge):L = loss + lambda * ||w||_2^2,让权重整体变小但不会为 0。
  • L1 正则化(Lasso):L = 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, lr=0.01, epochs=100, batch_size=32): self.lr = lr self.epochs = epochs self.batch_size = batch_size self.loss_history = [] def fit(self, X, y): # 标准化 self.mean = X.mean(axis=0) self.std = X.std(axis=0) + 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:i+self.batch_size] y_batch = y[i:i+self.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(lr=0.05, epochs=200, batch_size=16).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:均方误差,单位和目标值平方一致,注意对异常值敏感。
  • RMSE:MSE 开根号,单位与目标一致,便于解释。
  • R²:决定系数,表示模型解释了多少比例的目标方差。默认值是 1 减残差平方和除以总平方和,范围通常在 0 到 1 之间,越接近 1 越好。但注意,R² 在测试集上可能为负数,说明模型比直接用均值预测还差。
print("MSE:", mean_squared_error(y, pred_sgd)) print("RMSE:", mean_squared_error(y, pred_sgd, squared=False)) 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^2x^3,然后继续用线性回归拟合。这就是多项式回归。它本质是线性回归,但特征空间更丰富,可以拟合曲线。需要注意,多项式阶数过高容易过拟合,一般用交叉验证选阶数。

另一个方向是逻辑回归:把线性输出通过 Sigmoid 函数映射到 0-1 之间,用交叉熵作为损失函数,用于分类。逻辑回归虽然名字里有回归,实际是分类算法。它的优化方法仍然可以是梯度下降。理解了线性回归的损失函数和优化方法,再看逻辑回归的梯度下降,你会发现它们俩的代码结构几乎一样,只是换了损失函数和预测表达式。

6.2 神经网络中的线性层与端到端优化

神经网络的全连接层,做的事情就是z = Wx + b,然后过一个非线性激活函数。所以一个不含隐藏层的神经网络,本质上就是线性回归。当你把多个线性层堆叠起来并插入非线性激活函数,模型表达能力才得到提升。理解这一点,你就明白为什么深度学习库里的优化器(SGD、Adam)和线性回归梯度下降是同源的:都是计算损失对参数的梯度,然后沿负梯度方向更新。区别只是网络层数多、梯度计算用反向传播而不是直接求导。

6.3 学完线性回归后,我建议你做的三件事

第一,用真实数据集完整走一遍流程:加载数据、拆分训练测试、标准化、训练、评估、画残差图。比如 UCI 的 Boston 房价数据集,虽然现在 sklearn 里移除了,但可以通过其他渠道下载。第二,把线性回归改成 Ridge 回归,用交叉验证搜一遍正则化参数lambda,体会正则化带来的稳定性变化。第三,尝试用 PyTorch 写一个简单的线性回归模型,从nn.Linear开始,用SGD优化器训练一次。这一步能帮你把深度学习框架的接口和线性回归的数学对应起来,为后面学神经网络铺路。

最后再分享一个小技巧:不管用什么模型,我都会在训练后打印一份包含训练集和测试集的指标对比。这个习惯让我好几次在模型部署前发现了过拟合问题。线性回归虽然朴素,但把它从原理到实现、从训练到评估完整跑通,你后面学任何模型都会有一种"原来如此"的踏实感。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 21:04:51

WonderTrader依赖库配置实战:从vcpkg到CMake的C++编译避坑指南

简介:在 Ubuntu 22.04 环境、gcc 11.4.0 工具链下编译 WonderTrader 的 C 开发者,可借助这份预编译的依赖库集合,跳过 Boost 等第三方依赖的下载、版本匹配与 include 路径配置等繁琐过程。压缩包采用 tgz 格式,共 2000 个文件&am…

作者头像 李华
网站建设 2026/9/9 21:03:08

Android歌词渐变:用LinearGradient实现卡拉OK式进度高亮

简介:一份Android自定义View源码,实现歌词风格的文字颜色渐变效果,面向需要为TextView添加动态渐变文字的移动开发者。项目通过GradientTextView对文本着色,以两种颜色平滑过渡,适合音乐播放器歌词、字幕强调等场景&am…

作者头像 李华
网站建设 2026/9/9 21:02:21

diagram-design:现代前端可视化表达系统核心技术解析

1. 什么是 diagram-design:不是画图工具,而是现代前端工程中的可视化表达系统 “diagram-design”这个词乍看像某个软件功能名,但实际它早已脱离单一工具范畴,演变成一套融合设计思维、前端工程能力与领域建模逻辑的 可视化表达系…

作者头像 李华