# 样条与最小二乘 · 分段的智慧与超定拟合

> 样条用一段段低次多项式拼接，天然免疫 Runge 现象，是插值主力。而当数据带噪声，你压根不该穿过每个点——那是在拟合噪声，该用最小二乘去「近似」。这一章讲清这两件事，以及「插值 vs 拟合」的根本区别。

- 作者：David（道雾轩）
- 专栏：数值计算的艺术（https://daiw.org/manual/numerical-computing.md）
- 最后更新：2026-07-14
- 原文：https://daiw.org/manual/numerical-computing/splines-lsq
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 样条与最小二乘 · 分段的智慧与超定拟合

上一章的 [Runge 现象](https://daiw.org/manual/numerical-computing/interpolation)告诉我们：一条高次多项式管全局，危险。这一章给出两个更实用的工具：**样条**（把插值做对）和**最小二乘**（处理带噪数据）。它们背后是一个关键区分——**你的数据干净吗？**

## 样条：一段段低次拼起来

**样条（spline）** 的思路：不用一条高次多项式，而是**每相邻两点之间用一小段低次多项式（通常是三次），在连接点处拼接光滑**。最常用的**三次样条**保证连接处**二阶导连续**（$C^2$），曲线肉眼看是完全光滑的。

```python
import numpy as np
from scipy.interpolate import CubicSpline

xs = np.array([0., 1., 2., 3., 4., 5.])
ys = np.array([0., 1., 0., 1., 0., 1.])
cs = CubicSpline(xs, ys)

print(cs(2.5))        # 插值:x=2.5 处的值
print(cs(2.5, 1))     # 顺便还能求导数
```

样条的好处一箩筐：**免疫 Runge**（每段都是低次，不会疯狂震荡）、**光滑**、而且**局部**——改动一个数据点，只影响它附近几段，不像高次多项式牵一发而动全身。所以工程绘图、动画路径、CAD 曲线，插值的默认选择几乎都是样条。

## 关键区分：数据干净吗

现在问一个决定性的问题：**你的数据点，是精确的，还是带噪声的？** 这直接决定你该「插值」还是「拟合」：

<Callout type="warn">
  **数据带噪声时，千万别插值——插值会精确地拟合噪声。** 想象你测了 50 个点，真实规律是一条直线 $y=2x+1$，但每个测量都有随机误差。如果你**强行让曲线穿过全部 50 个点**（插值），得到的会是一条为了迁就每一个噪声点而**上蹿下跳**的曲线——它完美「记住」了噪声，却完全没抓住「直线」这个真相。这在机器学习里叫**过拟合（overfitting）**。

  **正确做法：承认数据有噪声，用一个简单模型去「近似」它，而不是穿过它。** 目标不是「零误差」，而是「抓住趋势、忽略噪声」——这就是最小二乘。
</Callout>

## 最小二乘：近似，而非穿过

**最小二乘（least squares）** 的目标：找一个（通常低阶的）模型，让它到所有数据点的**残差平方和最小**——不要求穿过任何点，只要求整体最贴合。

$$
\min_{c}\ \sum_{i} \big(y_i - m(x_i; c)\big)^2
$$

因为数据点（50 个）远多于模型参数（直线只有 2 个），这是个**超定方程组**（方程比未知数多，无精确解），最小二乘给出「最接近」的解：

```python
rng = np.random.default_rng(0)
x = np.linspace(0, 10, 50)
y = 2*x + 1 + rng.normal(0, 2, 50)        # 真实 y=2x+1,叠加噪声

a, b = np.polyfit(x, y, 1)                # 最小二乘拟合一条直线
print(f"拟合: y = {a:.2f}x + {b:.2f}")    # y ≈ 2.0x + 1.1 —— 从噪声里还原了趋势
```

50 个乱跳的点，拟合出的直线斜率约 $2$、截距约 $1$——**准确抓住了藏在噪声底下的规律**。这正是回归 / 曲线拟合的日常。

## 怎么解：别用正规方程

最小二乘在数学上可以化成**正规方程** $A^\top A\, c = A^\top y$。但——

<Callout type="warn">
  **别直接解正规方程 $A^\top A c = A^\top y$——$A^\top A$ 的条件数是 $A$ 的平方！** 也就是说，如果 $A$ 的[条件数](https://daiw.org/manual/numerical-computing/conditioning)是 $10^6$，$A^\top A$ 就是 $10^{12}$，精度直接砍半还多。正确做法是用基于 **QR 分解或 SVD** 的求解器，它们不构造 $A^\top A$、数值上稳得多：

  ```python
  A = np.vstack([x, np.ones_like(x)]).T
  coef, *_ = np.linalg.lstsq(A, y, rcond=None)   # 内部走 SVD,稳
  ```

  这又是一个「数学等价、数值不等价」的经典案例：正规方程**理论上**对，但**数值上**是个坑；`np.linalg.lstsq`（或 `scipy.linalg.lstsq`）才是正道。
</Callout>

## 插值 vs 拟合：一张表

| | 插值（interpolation） | 拟合 / 回归（least squares） |
| --- | --- | --- |
| 是否穿过每个点 | **穿过** | 不穿过，只近似 |
| 适用数据 | **干净、精确** | **带噪声** |
| 模型自由度 | = 点数（高） | 远小于点数（低） |
| 典型工具 | 三次样条 `CubicSpline` | `np.polyfit` / `lstsq` |
| 风险 | 高次 → Runge | 阶太高 → 过拟合 |

**一句话：点是精确的、要光滑穿过，用样条插值；点带噪声、要抓趋势，用最小二乘拟合。** 分不清这个，是很多「曲线画得很怪」的根源。

## 小结与下一站

**样条**用分段低次多项式拼接（$C^2$ 光滑、免疫 Runge、局部），是插值主力。**最小二乘**处理**带噪数据**——不穿过每点（那是拟合噪声 / 过拟合），而是最小化残差平方和抓趋势；求解别用正规方程（$\kappa$ 平方），用 `lstsq`（QR/SVD）。核心是分清**插值（干净数据、穿过）vs 拟合（带噪、近似）**。

逼近这一站讲完。下一站回到微积分——但这次是它的**数值版**：导数、积分在计算机上怎么算。先看数值微分，以及它一个「天生不稳」的尴尬。👉 [数值微分：有限差分，以及它为什么不稳](https://daiw.org/manual/numerical-computing/differentiation)。

## 参考文献

- [1] Carl de Boor.《A Practical Guide to Splines》—— 样条权威专著。
- [2] Trefethen & Bau.《Numerical Linear Algebra》, lec. 11, 18-19 —— 最小二乘与 QR/SVD。
- [3] SciPy 文档.「interpolate.CubicSpline / linalg.lstsq.」https://docs.scipy.org/doc/scipy/ —— 样条与最小二乘实现。
