手推最小二乘法:从散点图到回归公式的完整推导

📅 2026/6/17 5:36:16
手推最小二乘法:从散点图到回归公式的完整推导
1. 这不是公式默写而是亲手推导出那条直线——从散点图到数学直觉的完整旅程你有没有盯着一张散点图发过呆那些密密麻麻的点像一群没有队形的鸟看似杂乱无章却总在暗示某种秩序。线性回归要做的就是从这片混沌里亲手拉出一根最“诚实”的直线——它不强行穿过每一个点也不随意摆弄姿态而是用数学的尺子量出所有点到它的平均距离最短的那一条。这根线就是我们常说的回归线而它的斜率和截距不是凭感觉画出来的是被最小二乘法这个“数学裁判”严格裁定出来的。我第一次真正理解它不是在课本上看到那个漂亮的 y ax b 公式而是在 Excel 里手动拖动一条线看着下方的“误差平方和”数字不断跳动、变小、再变小直到它停在一个再也无法降低的谷底——那一刻公式活了。这篇文章就是带你重走一遍这条从视觉直觉到代数推导、再到数值验证的完整路径。它不假设你精通微积分但要求你愿意拿起笔在草稿纸上跟着算几步它不回避求导和偏导这些词但会告诉你为什么非得用它们而不是别的工具它更不会只给你一个黑箱函数调用完就结束。如果你正被“为什么截距 a 的公式长那样”、“为什么非得用平方而不是绝对值”、“手算三个点就能验证公式吗”这类问题卡住那你来对地方了。这是一份给实践者的推导笔记不是给考试者的速记口诀。2. 核心思路拆解为什么是“最小二乘”而不是“最小距离”或“最小绝对值”2.1 目标函数的诞生我们到底在“最小化”什么线性回归的终极目标是找到一条直线 y ax b让它能最好地“代表”我们手头的所有数据点 (xᵢ, yᵢ)。这里的“最好”必须量化。一个最朴素的想法是让每个点到直线的垂直距离之和最小。这听起来很公平对吧但数学上点到直线的垂直距离公式是 |axᵢ b - yᵢ| / √(a² 1)分母里带着 a这会让整个优化问题变得极其复杂求导后方程非线性没有解析解。我们想要的是一个能“一锤定音”算出 a 和 b 的公式而不是一个需要反复试错的数值游戏。所以我们必须简化这个距离的定义。于是统计学家们做了一个关键且精妙的妥协他们不看真正的几何垂直距离而是看纵轴方向上的偏差也就是 yᵢ - (axᵢ b)。这个值叫“残差”residual它代表了模型预测值 (axᵢ b) 和真实观测值 yᵢ 之间的差距。这个选择有坚实的现实基础在绝大多数应用场景中x 是我们能精确控制或测量的自变量比如实验中的温度、投入的广告费而 y 是我们试图预测的因变量比如反应速率、销售额其测量本身就带有随机误差。因此我们默认 x 是“干净”的所有不确定性都集中在 y 上。所以衡量拟合好坏自然就聚焦在 y 方向的误差上。提示这个“纵轴偏差”的假设是线性回归模型成立的基石之一。如果 x 本身也存在巨大测量误差那么普通最小二乘法OLS就不再是最优选择你需要转向“主成分回归”或“误差变量模型”Errors-in-Variables Model那是另一个故事了。2.2 为什么是“平方”而不是“绝对值”或“四次方”有了残差 eᵢ yᵢ - (axᵢ b)下一步就是把所有 eᵢ “加起来”。但直接相加不行因为正负残差会相互抵消。比如一个点高估了 5另一个点低估了 5总和是 0但这显然不代表拟合得好。所以我们需要一个能放大误差、且永远为正的度量。第一个想到的可能是绝对值∑|eᵢ|。这确实能避免正负抵消而且计算直观。但它有一个致命的数学缺陷绝对值函数在 eᵢ 0 处不可导。这意味着当我们想用微积分这个最强大的优化武器去寻找最优的 a 和 b 时会在残差为零的点上“卡壳”找不到一个平滑的下降路径。整个优化过程会变得笨拙需要借助更复杂的算法如线性规划失去了我们追求“解析解”的初衷。而平方和 ∑eᵢ² 就完美避开了这个问题。函数 f(e) e² 在整个实数域上都是光滑可导的它的导数是 2e清晰明了。更重要的是平方操作天然地惩罚大误差。一个 10 的误差其平方是 100而两个 5 的误差其平方和是 25 25 50。这符合我们的直觉一个巨大的错误比几个中等错误更不可接受。它迫使模型去“照顾”那些离群的点让整体的拟合更加稳健虽然有时也会被异常值带偏这是它的另一面。至于四次方 ∑eᵢ⁴它惩罚大误差的力度更强但同样会带来计算复杂度的上升并且对异常值过于敏感反而可能牺牲掉大部分数据点的拟合精度。平方是在数学优雅性、计算可行性和实际解释性之间找到的一个近乎完美的平衡点。2.3 最小二乘法的几何本质在高维空间里找一个“投影”如果你熟悉线性代数最小二乘法还有一个极其优美的几何解释。我们可以把所有的观测值 y 看作一个 n 维向量y [y₁, y₂, ..., yₙ]ᵀ。而所有可能的预测值 (axᵢ b) 的集合则构成了一个由两个向量张成的二维平面一个是全 1 向量1 [1, 1, ..., 1]ᵀ对应截距 b另一个是特征向量x [x₁, x₂, ..., xₙ]ᵀ对应斜率 a。任何一条直线的预测结果都可以表示为这个平面上的一个向量ŷ ax b1。那么寻找最优的 a 和 b就等价于在这个二维平面上找到一个向量ŷ使得它与真实向量y之间的欧氏距离 ||y-ŷ|| 最小。而在线性代数中一个向量到一个子空间的最短距离就是该向量在这个子空间上的正交投影。也就是说最优的ŷ必须满足 (y-ŷ) ⊥ 平面即 (y-ŷ) 与平面内的任意向量都正交。特别地它必须与1和x都正交(y- ax- b1) ·1 0(y- ax- b1) ·x 0展开这两个点积得到的正是我们后面将要推导的正规方程组。这个视角彻底揭示了最小二乘法的本质它不是在瞎猜而是在一个由数据定义的几何空间里进行一次精准的“影子投射”。我第一次在黑板上画出这个三维示意图时才真正感受到数学的震撼力——那些抽象的公式原来都有如此具象的空间意义。3. 核心细节解析与实操要点从定义到公式的每一步推导3.1 定义损失函数并写出其显式表达式让我们把上面的思路变成一个可以动手计算的数学对象。我们定义损失函数Loss Function或目标函数Objective FunctionJ(a, b) 为所有残差的平方和J(a, b) ∑ᵢ₌₁ⁿ eᵢ² ∑ᵢ₌₁ⁿ [yᵢ - (axᵢ b)]²这里i 从 1 到 n代表我们有 n 个数据点。我们的目标就是找到使 J(a, b) 取得最小值的 a 和 b。为了推导方便我们先把括号里的平方展开[yᵢ - (axᵢ b)]² yᵢ² - 2yᵢ(axᵢ b) (axᵢ b)² yᵢ² - 2ayᵢxᵢ - 2byᵢ a²xᵢ² 2abxᵢ b²现在对 i 求和。注意a 和 b 是我们要找的常数而 xᵢ 和 yᵢ 是已知的数据。所以我们可以把求和符号分配进去J(a, b) ∑yᵢ² - 2a∑yᵢxᵢ - 2b∑yᵢ a²∑xᵢ² 2ab∑xᵢ nb²这里∑ 表示对 i 从 1 到 n 求和。我们引入一些简写符号让公式更清爽Sₓₓ ∑xᵢ² x 的平方和Sₓᵧ ∑xᵢyᵢ x 与 y 的交叉和Sᵧ ∑yᵢ y 的和Sₓ ∑xᵢ x 的和Sᵧᵧ ∑yᵢ² y 的平方和那么损失函数就变成了J(a, b) Sᵧᵧ - 2aSₓᵧ - 2bSᵧ a²Sₓₓ 2abSₓ nb²这是一个关于两个变量 a 和 b 的二次函数。它的图像是一个开口向上的“抛物面”其最低点就是我们要找的全局最优解。3.2 利用微积分求极值对 a 和 b 分别求偏导并令其为零对于一个可导的多元函数其极值点必然满足所有一阶偏导数为零。这就是我们求解的关键。首先对 a 求偏导 ∂J/∂a。把 b 当作常数对 a 求导∂J/∂a -2Sₓᵧ 2aSₓₓ 2bSₓ令其等于零-2Sₓᵧ 2aSₓₓ 2bSₓ 0 aSₓₓ bSₓ Sₓᵧ …… (方程1)接着对 b 求偏导 ∂J/∂b。把 a 当作常数对 b 求导∂J/∂b -2Sᵧ 2aSₓ 2nb令其等于零-2Sᵧ 2aSₓ 2nb 0 aSₓ bn Sᵧ …… (方程2)恭喜你我们得到了著名的正规方程组Normal Equations。它是一个包含两个未知数 a 和 b 的二元一次方程组。只要 Sₓₓ 和 n 不为零这在实际数据中几乎总是成立的这个方程组就有唯一解。3.3 解方程组推导出斜率 a 和截距 b 的最终公式现在我们来亲手解这个方程组。从方程2出发我们可以很容易地解出 baSₓ bn Sᵧ bn Sᵧ - aSₓ b (Sᵧ - aSₓ) / n b ȳ - a x̄其中ȳ Sᵧ / n 是 y 的均值x̄ Sₓ / n 是 x 的均值。这个形式非常优美它告诉我们最优的回归直线必然经过点 (x̄, ȳ)即数据的“重心”。这是一个非常重要的几何性质也是我们后续验证计算是否正确的快速方法。现在把 b ȳ - a x̄ 代入方程1aSₓₓ (ȳ - a x̄) Sₓ Sₓᵧ aSₓₓ ȳSₓ - a x̄ Sₓ Sₓᵧ a(Sₓₓ - x̄ Sₓ) Sₓᵧ - ȳSₓ注意到 Sₓ n x̄所以 x̄ Sₓ n x̄²。而 Sₓₓ - n x̄² 正是 x 的离差平方和Sum of Squares for X通常记作 SSₓ。同理Sₓᵧ - ȳSₓ Sₓᵧ - n x̄ ȳ这是 x 与 y 的离差交叉和Sum of Cross Products记作 SPₓᵧ。所以我们得到a SPₓᵧ / SSₓ展开写出来就是a (∑(xᵢ - x̄)(yᵢ - ȳ)) / (∑(xᵢ - x̄)²)这就是斜率 a 的标准公式。它清晰地表明a 是 y 关于 x 的“协变”程度分子与 x 自身的“变异”程度分母的比值。最后把 a 代回 b ȳ - a x̄就得到了截距 b 的公式b ȳ - a x̄3.4 手动验算用三个点见证公式的诞生理论再好不如亲手算一遍。我们取一个最简单的例子三个点 (1, 1), (2, 2), (3, 2)。第一步计算均值。 x̄ (1 2 3) / 3 2ȳ (1 2 2) / 3 5/3 ≈ 1.6667第二步计算离差平方和 SSₓ。 (1-2)² (2-2)² (3-2)² 1 0 1 2第三步计算离差交叉和 SPₓᵧ。 (1-2)(1-5/3) (2-2)(2-5/3) (3-2)(2-5/3) (-1)(-2/3) (0)(1/3) (1)(1/3) 2/3 0 1/3 1第四步计算斜率 a。 a SPₓᵧ / SSₓ 1 / 2 0.5第五步计算截距 b。 b ȳ - a x̄ 5/3 - 0.5 * 2 5/3 - 1 2/3 ≈ 0.6667所以回归直线是 y 0.5x 0.6667。现在我们来验证一下。把三个 x 值代入x1: y0.5*1 0.6667 1.1667残差 1 - 1.1667 -0.1667x2: y0.5*2 0.6667 1.6667残差 2 - 1.6667 0.3333x3: y0.5*3 0.6667 2.1667残差 2 - 2.1667 -0.1667平方和 (-0.1667)² (0.3333)² (-0.1667)² ≈ 0.0278 0.1111 0.0278 0.1667如果我们随便选一条线比如 y x它的平方和是 (0)² (0)² (-1)² 1远大于 0.1667。这证明了我们的公式确实找到了一个更优的解。这个手动验算的过程是建立数学直觉最有效的方式。我建议你拿出纸笔用自己手头的一组小数据重复一遍你会对公式产生一种前所未有的信任感。4. 实操过程与核心环节实现从纸面公式到代码落地的完整闭环4.1 Python 实现从零开始不依赖任何机器学习库理解了公式下一步就是把它变成可运行的代码。下面是一个完全“裸写”的 Python 函数它只依赖numpy进行基础的数组运算没有任何sklearn或statsmodels的调用。这能让你看清每一行代码背后对应的数学步骤。import numpy as np def linear_regression_manual(x, y): 手动实现线性回归返回斜率a和截距b Parameters: x (array-like): 自变量一维数组 y (array-like): 因变量一维数组 Returns: tuple: (a, b) 斜率和截距 # 转换为numpy数组确保可以进行向量化运算 x np.array(x) y np.array(y) # 1. 计算均值 x_mean np.mean(x) y_mean np.mean(y) # 2. 计算离差交叉和 SP_xy sum((x_i - x_mean) * (y_i - y_mean)) # 使用numpy的向量化操作避免显式循环效率更高 sp_xy np.sum((x - x_mean) * (y - y_mean)) # 3. 计算离差平方和 SS_x sum((x_i - x_mean) ** 2) ss_x np.sum((x - x_mean) ** 2) # 4. 计算斜率 a SP_xy / SS_x a sp_xy / ss_x # 5. 计算截距 b y_mean - a * x_mean b y_mean - a * x_mean return a, b # 示例使用我们之前的手动验算数据 x_data [1, 2, 3] y_data [1, 2, 2] a_calc, b_calc linear_regression_manual(x_data, y_data) print(f计算得到的斜率 a: {a_calc:.4f}) print(f计算得到的截距 b: {b_calc:.4f}) # 输出a: 0.5000, b: 0.6667这段代码的每一行都严格对应着我们前面推导的数学步骤。np.sum((x - x_mean) * (y - y_mean))就是 SPₓᵧ 的代码实现np.sum((x - x_mean) ** 2)就是 SSₓ。这种“所见即所得”的代码是调试和教学的利器。当你发现结果不对时可以逐行打印中间变量如x_mean,sp_xy,ss_x立刻定位是哪一步出了问题。4.2 与成熟库的对比验证确保你的“轮子”造得靠谱自己造的轮子必须和工业级的轮子跑在同一条赛道上才能放心使用。我们用scikit-learn来做一个权威验证。from sklearn.linear_model import LinearRegression # 创建sklearn模型 model LinearRegression() # 注意sklearn要求x是二维数组形状为(n_samples, n_features) # 我们的数据是一维的所以需要reshape(-1, 1) x_reshaped np.array(x_data).reshape(-1, 1) model.fit(x_reshaped, y_data) # 获取结果 a_sklearn model.coef_[0] b_sklearn model.intercept_ print(fsklearn得到的斜率 a: {a_sklearn:.4f}) print(fsklearn得到的截距 b: {b_sklearn:.4f}) # 输出a: 0.5000, b: 0.6667结果完全一致这说明我们的手动实现是正确无误的。这种对比验证是工程实践中不可或缺的一环。我曾经在一个项目中因为一个微小的索引错误导致手动计算的截距总是差一个小数点花了整整半天才通过和statsmodels的summary()输出对比揪出了那个 bug。所以永远不要相信自己的第一版代码一定要有“金标准”来校验。4.3 可视化让回归线“活”在散点图上公式和数字是冰冷的而图形是温暖的。我们将用matplotlib把回归线画在散点图上亲眼见证它的“拟合”效果。import matplotlib.pyplot as plt # 生成用于绘图的平滑x值以便画出一条连续的直线 x_plot np.linspace(min(x_data), max(x_data), 100) y_plot a_calc * x_plot b_calc # 创建图形 plt.figure(figsize(8, 6)) plt.scatter(x_data, y_data, colorblue, label原始数据点, s50, zorder5) plt.plot(x_plot, y_plot, colorred, labelf回归线: y {a_calc:.2f}x {b_calc:.2f}, linewidth2) plt.xlabel(X) plt.ylabel(Y) plt.title(线性回归可视化) plt.legend() plt.grid(True, alpha0.3) plt.show()这张图会清晰地展示出那条红色的直线是如何“居中”地穿过蓝色的数据点云的。你可以直观地看到它确实没有强行穿过任何一个点但又巧妙地平衡了所有点的上下分布。这种视觉反馈是任何数字都无法替代的。我习惯在每次建模后都画这样一张图它就像一个“健康检查”如果直线看起来明显歪斜或偏离数据中心那一定是哪里出错了。4.4 核心参数的物理/业务意义解读别只当它是数学符号很多初学者把 a 和 b 当作纯粹的数学输出这是巨大的浪费。它们承载着丰富的现实意义。斜率 a它代表了 x 每增加一个单位y 的平均变化量。在我们的身高-年龄例子中a 6.5 意味着孩子每长大一岁平均身高会增长 6.5 厘米。这是一个极具业务价值的洞见它可以直接指导决策比如为不同年龄段的孩子设计不同尺寸的校服。截距 b它代表了当 x 0 时y 的预测值。但要注意这个解释只有在 x 0 具有现实意义时才成立。比如在广告投入-销售额模型中b 可以解释为“不花一分钱广告费时预计的自然销售额”。但如果 x 是“员工工龄”那么 x 0刚入职时的销售额可能和 b 的数值并不完全等同因为新员工的销售模式可能完全不同。所以对 b 的解读必须结合具体的业务场景切忌生搬硬套。注意在某些情况下为了赋予截距更合理的解释我们会对 x 进行中心化处理即用 x - x̄ 替代原始 x。这样新的截距 b 就直接等于 ȳ即 y 的均值其含义就变得无比清晰和稳健。5. 常见问题与排查技巧实录那些只有踩过坑才知道的真相5.1 问题我的斜率 a 算出来是负数但业务上明明应该是正相关的哪里错了这是新手最常见的困惑之一。请先深呼吸然后按以下顺序排查检查数据输入顺序这是最高频的错误确认你的x数组确实是自变量原因y数组是因变量结果。我曾见过一个同事把“销售额”当成了 x“广告费”当成了 y结果算出负相关还写了一页报告分析“为什么花钱越多卖得越少”闹了个大笑话。用print(x[:3], y[:3])快速确认前几行数据是否符合你的预期。检查数据范围和量纲如果 x 和 y 的数值范围相差极大比如 x 是 0.001, 0.002, ...而 y 是 1000, 2000, ...浮点数计算可能会引入微小的数值误差导致符号错误。此时对 x 和 y 进行标准化减均值除标准差后再计算结果会更稳定。检查是否存在强离群点一个极端的离群点足以把整条回归线“拽”向错误的方向。画出散点图是发现这个问题最快的方法。如果图上有一个点孤零零地远离主数据云先把它暂时剔除再重新计算看看 a 的符号是否恢复正常。如果是那就需要深入分析这个离群点是数据录入错误还是一个值得单独研究的特殊现象。5.2 问题SSₓ计算出来是 0导致除零错误我的数据怎么了SSₓ ∑(xᵢ - x̄)² 0意味着所有 xᵢ 的值都完全相等。这在现实中意味着你的自变量根本没有变化比如你收集了 100 个样本但所有样本的“温度”都是 25°C。在这种情况下讨论“温度对反应速率的影响”本身就是无意义的因为温度这个因素根本没动。模型无法从静止的数据中学习到任何关系。解决方案立即停止建模回到数据源头检查数据采集过程是否出了问题。如果确认数据无误例如你真的只在一个固定温度下做了实验那么你需要引入其他有变化的变量比如催化剂浓度、反应时间作为新的 x或者承认当前数据不足以支持线性回归分析。5.3 问题手动计算和sklearn结果不一致差了小数点后几位是精度问题吗这通常是由于sklearn内部使用了更稳定的数值算法如基于 QR 分解的求解器而我们的手动计算直接用了sum在数据量极大或数值范围极广时累积误差会显现。这不是你的公式错了而是计算方式的差异。应对策略对于绝大多数中小规模数据n 10000这种微小差异完全可以忽略不影响业务决策。如果你追求极致的数值稳定性可以将手动计算升级为使用numpy.linalg.lstsq它内部就采用了 QR 分解# 更稳定的求解方式 X_matrix np.column_stack((x, np.ones(len(x)))) # 构造设计矩阵 [x, 1] coeffs, residuals, rank, s np.linalg.lstsq(X_matrix, y, rcondNone) a_stable, b_stable coeffs[0], coeffs[1]5.4 问题回归线看起来“太陡”或“太平”和我的直觉不符是模型有问题吗回归线的陡峭程度完全由数据本身的变异程度决定。一个常见的误解是认为“陡峭”就代表关系强“平缓”就代表关系弱。其实不然。斜率 a 的大小同时取决于 x 和 y 的单位和量纲。比如用“米”和“秒”计算速度a 是 5换成“厘米”和“毫秒”a 就会变成 500000。所以孤立地看 a 的数值大小没有意义。真正衡量关系强度的是相关系数 r它的取值范围是 [-1, 1]完全不受量纲影响。r 的绝对值越接近 1线性关系越强。r SPₓᵧ / √(SSₓ * SSᵧ)其中 SSᵧ 是 y 的离差平方和。因此当你觉得斜率“怪”时应该立刻计算r。如果|r|很小比如 0.3那说明即使斜率不为零x 和 y 之间的线性关联也非常微弱这时回归线的形态本身就不该成为关注焦点而应该去探索是否存在非线性关系或者是否有其他更重要的变量被忽略了。6. 实战心得与经验总结一个十年从业者的真实体会在我用线性回归解决过上百个实际问题之后有一些心得是任何教科书都不会写的但却是保证项目成功的关键。第一永远先画图再建模。这句话我跟团队新人说了不下一百遍。在敲下第一行import numpy之前必须先用plt.scatter(x, y)看一眼。这张图能告诉你一切数据是线性的吗有离群点吗x 的分布是均匀的吗y 有明显的异方差误差随 x 增大而增大吗有一次一个同事跳过了这一步直接跑模型得出 R² 0.95 的“完美”结果。但当我把图调出来时发现数据呈现完美的抛物线形状而他的直线只是碰巧在中间一段拟合得不错。R² 高只是因为数据本身“胖”而不是模型好。从那以后我们的建模流程强制加入了一条红线没有散点图不准提交模型。第二截距 b 的业务解读比斜率 a 更重要也更难。斜率 a 告诉你“怎么变”而截距 b 告诉你“起点在哪”。在金融风控模型中b 代表了“当所有风险因子都为 0 时客户的基准违约概率”。这个数字直接决定了整个评分卡的基线水平。我见过太多模型a 算得无比精准但 b 被随意设为 0 或一个经验值导致整个模型的预测在低风险客户群体上系统性地偏高或偏低。所以我会花至少一半的时间和业务方一起反复推敲 b 的合理取值范围并用历史数据进行回溯测试。第三线性回归不是万能的但它是所有复杂模型的“锚点”。当你面对一个全新的、复杂的业务问题时不要一上来就祭出 XGBoost 或神经网络。先用最简单的线性回归跑一遍记录下它的 RMSE均方根误差和 R²。这个数字就是你后续所有复杂模型的“及格线”。如果一个花了三天调参的深度学习模型其 RMSE 只比线性回归低了 0.5%那它带来的额外复杂度和维护成本几乎肯定是不值得的。线性回归是你在数据科学世界里的罗盘它不一定能带你到达终点但能确保你始终朝着正确的方向前进。最后我想分享一个我自己的小技巧。每当我需要向非技术背景的老板或客户解释线性回归时我从来不说“最小二乘法”或“正规方程”。我会拿起一支笔在白纸上画三个点然后说“看我们想用一条直线来概括这三个点。最公平的办法就是让这条线到三个点的‘总距离’最短。但因为我们不能让上面的点和下面的点互相抵消所以我们把每个距离都‘平方’一下再加起来。然后我们用数学的方法算出能让这个‘总距离’最小的那条线。它一定经过这三个点的‘中心’就像一个跷跷板的支点。” 用生活化的语言把数学的严谨翻译成可感知的逻辑这才是沟通的真谛。