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)的函数。我们要找它的最小值。怎么做?
回想一下高中数学:函数在极值点处的导数为零。对多元函数来说,就是偏导数为零。
具体步骤:
- 对每个参数求偏导数
- 令所有偏导数等于零
- 解这个方程组
拿线性回归举例。对 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 知识体系总览
下面这张图把本章的核心逻辑串起来了。你一看就明白:
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 代替手动求逆;从二次曲面开始练手