4. 最小二乘法入门:从线性回归到曲面拟合

说实话,很多人一听到「最小二乘法」就觉得头大。我当年刚接触时也一样,觉得这名字太数学了。但后来做项目多了才发现——这玩意儿说白了就是「找一条线,让所有点到这条线的距离平方和最小」。就这么简单。

今天咱们就把这个核心思想掰开揉碎,从最简单的线性回归一路聊到曲面拟合。你跟着走一遍,保证能上手。

4.1 从一条直线说起:线性回归

先看个最直观的场景。假设你有一堆数据点,横坐标是广告投放金额,纵坐标是销售额。你想知道它们之间是什么关系。

画个图看看——这些点大致沿着一条直线分布。那问题就来了:怎么找到这条「最佳」的直线?

核心思路: 直线方程是 y = ax + b。我们要找到一组 (a, b),让所有数据点到这条直线的「垂直距离平方和」最小。

为什么是「平方」?我刚开始也纳闷。后来在实际项目中踩过坑才明白:如果不平方,正负误差会互相抵消,你算出来的结果可能完全不对。平方之后,所有误差都是正的,累加起来才能真实反映拟合的好坏。

具体来说,对于每个数据点 (xᵢ, yᵢ),直线预测的值是 ŷᵢ = axᵢ + b。误差就是 yᵢ - ŷᵢ。把所有误差平方加起来:

S = Σ (yᵢ - (axᵢ + b))²

我们要找的就是让 S 最小的 a 和 b。这就是最小二乘法的本质。

4.2 误差函数:到底在最小化什么?

上面那个 S,就是误差函数(也叫损失函数、目标函数)。不同的问题,误差函数的定义会不一样,但核心思想一致——衡量「预测值」和「真实值」之间的差距。

问题类型 模型形式 误差函数
线性回归 y = ax + b Σ(yᵢ - (axᵢ + b))²
多项式拟合 y = a₀ + a₁x + a₂x² + ... Σ(yᵢ - (a₀ + a₁xᵢ + a₂xᵢ² + ...))²
曲面拟合 z = a₀ + a₁x + a₂y + a₃x² + ... Σ(zᵢ - (a₀ + a₁xᵢ + a₂yᵢ + ...))²

我的经验: 误差函数定义得好不好,直接决定拟合效果。我在做传感器标定时,一开始没考虑噪声分布,结果拟合出来的曲面在边缘处完全跑偏。后来在误差函数里加了权重项,才把问题解决。

4.3 最小化原理:怎么找到最优参数?

误差函数 S 是关于参数(比如 a 和 b)的函数。我们要找它的最小值。怎么做?

回想一下高中数学:函数在极值点处的导数为零。对多元函数来说,就是偏导数为零。

具体步骤:

  1. 对每个参数求偏导数
  2. 令所有偏导数等于零
  3. 解这个方程组

拿线性回归举例。对 a 和 b 分别求偏导:

∂S/∂a = -2 Σ xᵢ (yᵢ - (axᵢ + b)) = 0
∂S/∂b = -2 Σ (yᵢ - (axᵢ + b)) = 0

整理一下,就得到正规方程组:

a Σ xᵢ² + b Σ xᵢ = Σ xᵢ yᵢ
a Σ xᵢ + b n = Σ yᵢ

解这个方程组,a 和 b 就出来了。整个过程不需要迭代,一步到位——这就是最小二乘法最吸引人的地方。

注意: 正规方程组要求矩阵可逆。如果数据点太少,或者特征之间高度相关,矩阵可能奇异。我曾经在分析多变量数据时遇到过这个问题,最后用正则化才解决。

4.4 从直线到曲面:维度升级

理解了线性回归,曲面拟合就是水到渠成的事。说白了,就是把 x 换成 (x, y),把直线换成曲面。

一个二次多项式曲面可以写成:

z = a₀ + a₁x + a₂y + a₃x² + a₄xy + a₅y²

这里有 6 个未知参数。误差函数变成:

S = Σ (zᵢ - (a₀ + a₁xᵢ + a₂yᵢ + a₃xᵢ² + a₄xᵢyᵢ + a₅yᵢ²))²

然后对每个 aⱼ 求偏导,令其为零,解方程组。流程完全一样,只是计算量大了不少。

我建议你从二次曲面开始练手。次数太高容易过拟合,次数太低又拟合不够。二次是个不错的平衡点。

4.5 知识体系总览

下面这张图把本章的核心逻辑串起来了。你一看就明白:

最小二乘法知识体系 最小二乘法 线性回归 多项式拟合 曲面拟合 y = ax + b 正规方程组 y = Σ aᵢxⁱ 次数选择 z = f(x, y) 偏导方程组 核心:误差平方和最小化 → 偏导为零 → 解方程组

4.6 实战:用代码跑一遍

光说不练假把式。咱们用 Python 写个最小二乘法拟合二次曲面的例子。你直接复制就能跑:

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# 生成模拟数据
np.random.seed(42)
x = np.random.uniform(-3, 3, 50)
y = np.random.uniform(-3, 3, 50)
z = 2 + 1.5*x - 0.8*y + 0.3*x**2 + 0.5*x*y - 0.2*y**2 + np.random.normal(0, 0.5, 50)

# 构建设计矩阵
A = np.column_stack([
    np.ones_like(x),   # 常数项
    x,                 # x
    y,                 # y
    x**2,              # x²
    x*y,               # xy
    y**2               # y²
])

# 最小二乘法求解:AᵀA·θ = Aᵀz
theta = np.linalg.inv(A.T @ A) @ A.T @ z

print("拟合参数:", theta)
# 输出示例: [2.12, 1.48, -0.76, 0.28, 0.52, -0.18]

避坑指南: 我曾经直接用 np.linalg.inv 求逆,结果数据量一大就报错。后来改用 np.linalg.lstsq,既稳定又高效。建议你养成习惯,用最小二乘求解器而不是手动求逆。

4.7 误差函数可视化:看看最小化过程

为了让你更直观地理解「最小化」到底在干什么,咱们画个误差函数的等高线图。以线性回归的两个参数 a 和 b 为例:

# 生成误差函数在参数空间中的值
a_vals = np.linspace(-2, 4, 100)
b_vals = np.linspace(-1, 3, 100)
A_grid, B_grid = np.meshgrid(a_vals, b_vals)
S_grid = np.zeros_like(A_grid)

for i in range(len(a_vals)):
    for j in range(len(b_vals)):
        pred = A_grid[j,i] * x + B_grid[j,i]
        S_grid[j,i] = np.sum((z - pred)**2)

plt.contour(A_grid, B_grid, S_grid, levels=20)
plt.scatter(theta[0], theta[1], color='red', s=100)
plt.xlabel('a')
plt.ylabel('b')
plt.title('误差函数等高线 & 最优解')
plt.show()

你会看到,误差函数在参数空间中是一个「碗」状曲面。碗底就是最优解。最小二乘法就是直接跳到碗底,一步到位。

4.8 本章小结

  • 最小二乘法的本质:让预测值与真实值的误差平方和最小
  • 求解方法:对每个参数求偏导 → 令偏导为零 → 解正规方程组
  • 从线性到曲面:核心流程完全一样,只是参数数量和方程复杂度增加
  • 实用建议:用 np.linalg.lstsq 代替手动求逆;从二次曲面开始练手