1. 从“差不多”到“刚刚好”:为什么我们需要曲线拟合?

做数据分析、信号处理或者搞工程建模的朋友,肯定都遇到过这种场景:你手里有一堆实验数据点,横七竖八地散落在坐标图上,像一群迷路的蚂蚁。你心里清楚,这些数据背后应该藏着某种规律,比如一个线性关系、一个指数衰减,或者一个更复杂的函数。你的任务,就是找到一根“线”,能最好地穿过这些点,或者至少,能最贴切地描述它们背后的趋势。这根“线”,就是我们要的“拟合曲线”。

这个过程,就叫 曲线拟合 。它绝不是简单地画一根看起来顺眼的线,而是一个有严格数学定义的优化过程。那么,问题来了:什么叫“最好”?什么叫“最贴切”?对于同一组数据,我画一根高一点的线,你说离某些点远了;我画一根低一点的线,他又说离另一些点远了。公说公有理,婆说婆有理,我们需要一个客观、统一的评判标准。

这个标准,就是 最小二乘原理 。它可以说是数据拟合领域里最经典、最基础,也几乎是应用最广泛的方法。它的核心思想非常直观,甚至有点“朴素”:我找的那条曲线,应该让所有数据点到这条曲线的 垂直距离的平方和 最小。

为什么是“平方和”,而不是直接的距离和?这里有两个关键原因。第一,距离有正有负,直接相加可能会相互抵消,比如一个点在曲线上方(正距离),一个点在下方(负距离),一加和可能变成0,这显然不能反映整体的偏离程度。第二,使用平方,可以放大那些偏离较远的点的影响,让拟合曲线对“异常点”更敏感,同时也让整个数学问题变得“友好”——平方函数是光滑可导的,这为我们后续用微积分工具求解极值点铺平了道路。

所以,最小二乘拟合,本质上就是在解决一个最优化问题:在某一类候选曲线(比如所有一次函数 y = ax + b )中,寻找一组特定的参数( a b ),使得根据这组参数计算出的预测值,与所有实际观测值之间的误差平方和,达到全局最小。这个思想,由大名鼎鼎的高斯和勒让德在两百多年前分别独立提出并应用于天体轨道计算,至今仍是工程和科学研究的基石。

2. 最小二乘的数学内核:误差、模型与目标函数

要真正理解最小二乘,我们不能只停留在“让距离平方和最小”这个口号上,必须拆开看看它的数学骨架。这个过程,也是我们建立任何定量分析模型的标准思路。

2.1 定义误差:观测值与预测值的差距

假设我们有一组观测数据,共有 n 个点。第 i 个点的坐标是 (x_i, y_i) 。这里 x_i 是自变量(比如时间、温度、压力), y_i 是因变量(比如位移、电阻、产量),是我们实际测量得到的值。

现在,我们猜测 y x 之间存在某种函数关系,我们用一个带参数的函数 f(x; θ) 来表示它。这里的 θ 代表了一组待确定的参数。例如,对于线性模型, f(x; a, b) = a * x + b ,那么参数 θ 就是 [a, b]

对于每一个数据点 (x_i, y_i) ,我们用模型预测出的 y 值是 ŷ_i = f(x_i; θ) 。那么,这个点的 误差 (或称残差) e_i 就定义为: e_i = y_i - ŷ_i = y_i - f(x_i; θ) 这个误差 e_i 可正可负,代表了模型在该点预测的偏差。

2.2 构建目标函数:误差平方和(SSE)

如果只有一个误差,我们很容易判断模型好坏。但现在有 n 个误差, e_1, e_2, ..., e_n 。我们需要一个单一的标量来整体衡量模型在所有数据点上的表现。最小二乘法选择的衡量标准就是 误差平方和 (Sum of Squared Errors, SSE): S(θ) = Σ_{i=1}^{n} e_i^2 = Σ_{i=1}^{n} [y_i - f(x_i; θ)]^2 这个 S(θ) 就是我们的 目标函数 。它依赖于模型参数 θ 。不同的参数 θ 会给出不同的预测值 ŷ_i ,从而计算出不同的 S(θ)

2.3 优化求解:寻找最优参数

最小二乘拟合的目标,就是找到一组参数 θ* ,使得目标函数 S(θ) 的值达到最小: θ* = argmin_{θ} S(θ) 这是一个标准的无约束优化问题。对于许多常见的模型(特别是线性模型),我们可以通过数学方法直接求出这个最优解的解析表达式。

为什么这个方法如此强大? 从统计学的视角看,最小二乘估计在满足一系列理想假设(如误差独立、同方差、均值为零)时,是所有无偏估计中方差最小的,即 最佳线性无偏估计 。从计算的角度看,平方项导致目标函数是凸函数(对于线性模型),这意味着通常只有一个全局最小值,我们可以稳定地求解。从感性的角度看,它惩罚大的误差比惩罚小的误差更严厉,这符合我们“不希望模型在任何一点上偏离太远”的直觉。

3. 线性最小二乘:手把手推导与几何意义

线性拟合是最简单也最常用的情况。我们的模型是 y = a * x + b 。此时,参数 θ = [a, b] ,目标函数为: S(a, b) = Σ (y_i - (a*x_i + b))^2

我们的任务是找到 a b ,最小化 S(a, b) 。根据微积分,函数在极值点处对各个自变量的偏导数应为零。这引出了著名的 正规方程组

3.1 正规方程组的推导

分别对 a b 求偏导,并令其等于0:

  1. ∂S/∂a = -2 * Σ [x_i * (y_i - a*x_i - b)] = 0
  2. ∂S/∂b = -2 * Σ (y_i - a*x_i - b) = 0

整理后得到正规方程组:

a * Σ x_i^2 + b * Σ x_i = Σ (x_i * y_i)
a * Σ x_i + b * n = Σ y_i

这是一个关于 a b 的二元一次方程组。解这个方程组,就得到了最小二乘估计值:

a = (n * Σ(x_i*y_i) - Σx_i * Σy_i) / (n * Σ(x_i^2) - (Σx_i)^2)
b = (Σy_i * Σ(x_i^2) - Σx_i * Σ(x_i*y_i)) / (n * Σ(x_i^2) - (Σx_i)^2)

或者,用均值的概念表示会更简洁。令 x̄ = (Σx_i)/n , ȳ = (Σy_i)/n ,则:

a = Σ[(x_i - x̄)(y_i - ȳ)] / Σ[(x_i - x̄)^2]
b = ȳ - a * x̄

这个形式揭示了 a 实际上是 x y 的协方差除以 x 的方差,非常直观。

3.2 线性最小二乘的几何透视

我们可以从线性代数的角度,获得更深刻的理解。把 n 个数据点的 y 值写成一个列向量 Y = [y_1, y_2, ..., y_n]^T 。 我们的线性模型 ŷ_i = a*x_i + b 可以改写为: Ŷ = a * X + b * 1 其中 X = [x_1, x_2, ..., x_n]^T 1 是一个全为1的 n 维列向量。

令矩阵 A = [X, 1] ,参数向量 β = [a, b]^T 。那么模型可以写成矩阵形式: Ŷ = A β 我们的目标是最小化真实向量 Y 与预测向量 Ŷ 之间的欧氏距离的平方,即 ||Y - Aβ||^2

在几何上, 代表的是由矩阵 A 的列向量(即 X 1 )所张成的 列空间 中的一个向量。 Y 是空间中的一个点。最小二乘解 β* 所对应的 Ŷ* = Aβ* ,正是 Y 在这个列空间上的 正交投影

注意 :这个几何解释是理解最小二乘,乃至更广义线性模型的关键。它意味着我们寻找的拟合直线,其预测值向量 Ŷ ,是真实数据向量 Y 在由“自变量”和“常数项”所构成平面上的“影子”。误差向量 e = Y - Ŷ 垂直于这个平面。这也解释了为什么正规方程 (A^T A) β = A^T Y 成立——它本质上是要求误差向量与列空间的所有基向量(即 A 的列)都正交。

4. 超越线性:非线性最小二乘与模型线性化

现实世界的关系远非总是线性的。可能是指数增长 y = a * e^{bx} ,可能是幂律关系 y = a * x^b ,也可能是多项式 y = a_0 + a_1*x + a_2*x^2 + ... 。这些都属于 非线性最小二乘 问题,因为待估参数 θ 与预测值 f(x; θ) 之间的关系是非线性的。

4.1 多项式拟合:披着非线性外衣的线性问题

多项式拟合 y = Σ_{j=0}^{m} a_j * x^j 是一个特例。虽然 y 关于 x 是非线性的,但关于参数 a_j 却是线性的!我们可以令 φ_j(x) = x^j ,那么模型就变成了 y = a_0*φ_0(x) + a_1*φ_1(x) + ... + a_m*φ_m(x) 。这被称为 线性于参数 的模型。

对于这类模型,我们完全可以套用线性最小二乘的框架。只需将设计矩阵 A 中的列,从 [X, 1] 扩展为 [1, X, X.^2, ..., X.^m] ,其中 X.^k 表示对向量 X 的每个元素求 k 次幂。然后,求解正规方程 (A^T A) β = A^T Y (其中 β = [a_0, a_1, ..., a_m]^T )即可。在 MATLAB 或 Python (NumPy) 中,这通常只需一两行代码。

实操心得:多项式阶数 m 的选择 这里有一个经典的陷阱: 过拟合 。阶数 m 越高,曲线越“柔软”,能更精确地穿过每一个数据点(甚至让 SSE 降为 0),但这样的曲线往往震荡剧烈,失去了揭示底层规律的能力,对新数据的预测能力极差。我个人的经验法则是:

  1. 先可视化 :画出散点图,观察大致趋势。线性?二次?饱和增长?
  2. 从低阶开始 :优先尝试 m=1 (线性), m=2 (二次)。很多时候简单的模型更稳健。
  3. 交叉验证 :如果有足够数据,将数据分为训练集和测试集。用训练集拟合不同阶数的模型,在测试集上计算误差。选择测试集误差最小的模型。
  4. 观察系数 :如果高阶项的系数绝对值非常小,或者其置信区间包含0,通常可以考虑去掉该项。
  5. 一个实用警告 :尽量不要让多项式阶数 m 超过数据点数量 n 的十分之一,对于 m 接近 n 的情况,结果基本是灾难性的。

4.2 真正的非线性拟合:迭代优化与线性化技巧

对于像 y = a * e^{bx} y = a / (b + x) 这类参数非线性的模型,目标函数 S(θ) 关于 θ 是非线性的,可能有很多局部极小值。我们无法直接解出像正规方程那样的解析解,必须借助 迭代优化算法 ,例如:

  • 高斯-牛顿法 :一种利用目标函数近似为二次型的迭代方法,收敛速度快,但需要计算雅可比矩阵,且对初始值敏感。
  • 列文伯格-马夸尔特法 :高斯-牛顿法的改进版,通过引入阻尼因子,在梯度下降和高斯-牛顿法之间自适应切换,更稳定、更常用。SciPy 和 MATLAB 中的 lsqcurvefit curve_fit 等函数默认或常用此算法。
  • 信任域反射法 :另一种稳健的迭代算法。

实操中的关键:参数初始值 对于非线性拟合,提供一个好的参数初始猜测 θ0 至关重要,它直接决定了算法能否收敛到全局最优,以及收敛的速度。我常用的策略是:

  1. 物理意义法 :如果参数有物理意义(如衰减率、饱和值),根据数据范围和经验给出粗略估计。
  2. 线性化法 :这是最实用的技巧之一。以指数模型 y = a * e^{bx} 为例,两边取自然对数: ln(y) = ln(a) + b*x 。令 Y' = ln(y) A = ln(a) , 则模型变为 Y' = A + b*x , 这是一个关于 A b 的线性模型!我们可以先用线性最小二乘拟合 (x, ln(y)) 的数据,得到 A b 的初始估计,然后反推出 a = exp(A) 。这个方法对于幂律、饱和增长等许多可线性化模型都有效。
  3. 网格搜索法 :如果参数范围大致可知,可以在一个粗糙的网格上计算 S(θ) ,选择使 S(θ) 最小的点作为初始值。

5. 实战避坑:从MATLAB到Python的完整流程与陷阱

理论懂了,不实践等于零。我们以 MATLAB 和 Python (SciPy) 为例,走一遍完整的拟合流程,并指出每一步可能遇到的坑。

5.1 数据准备与可视化:第一步就错了,后面全白费

拿到数据,千万别急着 fit curve_fit 。第一步永远是 可视化

% MATLAB 示例
% 假设已有数据向量 x_data 和 y_data
figure;
scatter(x_data, y_data, 50, 'filled', 'DisplayName', '原始数据');
xlabel('自变量 X');
ylabel('因变量 Y');
title('数据散点图');
grid on; legend;
% 仔细观察点的分布:是线性?有弯曲?有异常点?
# Python (Matplotlib) 示例
import matplotlib.pyplot as plt
import numpy as np

# 假设已有数据数组 x_data 和 y_data
plt.figure(figsize=(8, 6))
plt.scatter(x_data, y_data, s=50, label='原始数据', alpha=0.7)
plt.xlabel('自变量 X')
plt.ylabel('因变量 Y')
plt.title('数据散点图')
plt.grid(True, linestyle='--', alpha=0.5)
plt.legend()
plt.show()

这一步的陷阱:

  • 异常值 :图中是否有一两个点离群索居?它们会严重扭曲最小二乘的结果,因为平方项放大了大误差的影响。需要判断是测量错误(应剔除)还是重要现象(需用稳健回归等方法)。
  • 数据密度不均 :如果数据在某个 x 区间非常密集,在另一个区间非常稀疏,最小二乘拟合的结果会被密集区域“主导”。需要考虑是否进行数据加权或变换。
  • 异方差性 :误差的方差是否随着 x 变化?如果数据在 x 较大时波动范围明显变大,这就违反了“同方差”假设,标准最小二乘的统计性质会变差。

5.2 模型选择与拟合执行

根据可视化结果选择模型。假设我们决定用二次多项式 y = p0 + p1*x + p2*x^2 拟合。

% MATLAB 多项式拟合 (二次)
p = polyfit(x_data, y_data, 2); % p 是系数向量,从高次到低次: p(1)*x^2 + p(2)*x + p(3)
% 生成拟合曲线上的点
x_fit = linspace(min(x_data), max(x_data), 100);
y_fit = polyval(p, x_fit);
% 画图对比
hold on;
plot(x_fit, y_fit, 'r-', 'LineWidth', 2, 'DisplayName', '二次多项式拟合');
hold off;
# Python 多项式拟合 (使用 NumPy)
import numpy as np
p = np.polyfit(x_data, y_data, deg=2) # p 是系数向量,从高次到低次: p[0]*x^2 + p[1]*x + p[2]
# 生成拟合曲线上的点
x_fit = np.linspace(np.min(x_data), np.max(x_data), 100)
y_fit = np.polyval(p, x_fit)
# 画图对比 (接续之前的画图代码)
plt.plot(x_fit, y_fit, 'r-', linewidth=2, label='二次多项式拟合')
plt.legend()
plt.show()

对于非线性模型,例如指数衰减 y = a * exp(-b*x) + c

% MATLAB 非线性拟合 (使用 fit 函数或 lsqcurvefit)
% 定义模型函数句柄
modelfun = @(p, x) p(1) * exp(-p(2)*x) + p(3);
% 初始猜测值 p0 = [a_guess, b_guess, c_guess]
p0 = [max(y_data), 0.1, min(y_data)];
% 使用 lsqcurvefit
options = optimoptions('lsqcurvefit', 'Display', 'iter'); % 显示迭代过程
[p_opt, resnorm] = lsqcurvefit(modelfun, p0, x_data, y_data, [], [], options);
% p_opt 是最优参数
y_fit_nl = modelfun(p_opt, x_fit);
# Python 非线性拟合 (使用 SciPy)
from scipy.optimize import curve_fit
import numpy as np

# 定义模型函数
def exp_decay(x, a, b, c):
    return a * np.exp(-b * x) + c

# 初始猜测值 p0 = [a_guess, b_guess, c_guess]
p0 = [np.max(y_data), 0.1, np.min(y_data)]
# 执行拟合
p_opt, p_cov = curve_fit(exp_decay, x_data, y_data, p0=p0)
# p_opt 是最优参数, p_cov 是参数的协方差矩阵,可用于计算标准差
y_fit_nl = exp_decay(x_fit, *p_opt)

这一步的陷阱:

  • 初始值导致收敛到局部最优 :对于非线性拟合,如果结果不合理(如预测曲线完全偏离数据),首先怀疑初始值。尝试不同的 p0 ,或使用前面提到的线性化技巧获取初始值。
  • 参数物理意义与约束 :有时参数应有物理范围(如衰减率 b > 0 )。 curve_fit lsqcurvefit 都支持设置参数的上下界 ( bounds )。
  • 算法不收敛 :可能模型过于复杂,或数据噪声太大。可以尝试简化模型,增加最大迭代次数 ( maxfev ),或调整优化算法参数。

5.3 结果评估与诊断:拟合好≠模型好

拟合出一条曲线只是开始,评估其质量才是重点。绝不能只看“拟合曲线和散点图看起来挺近”。

1. 残差分析: 这是诊断模型缺陷的最有力工具。残差 e_i = y_i - ŷ_i 应该随机分布在0附近,不应有任何明显的模式。

# Python 残差图
y_pred = exp_decay(x_data, *p_opt) # 或用 polyval 计算预测值
residuals = y_data - y_pred

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# 残差 vs. 预测值
axes[0].scatter(y_pred, residuals, alpha=0.7)
axes[0].axhline(y=0, color='r', linestyle='--')
axes[0].set_xlabel('预测值 ŷ')
axes[0].set_ylabel('残差')
axes[0].set_title('残差 vs. 预测值')
axes[0].grid(True, alpha=0.3)

# 残差 vs. 自变量
axes[1].scatter(x_data, residuals, alpha=0.7)
axes[1].axhline(y=0, color='r', linestyle='--')
axes[1].set_xlabel('自变量 X')
axes[1].set_ylabel('残差')
axes[1].set_title('残差 vs. 自变量')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

理想的残差图 :点随机、均匀地分布在水平线 y=0 两侧,无明显趋势、漏斗形或弯曲。 有问题的残差图

  • 漏斗形 :残差随预测值增大而发散,提示 异方差 ,需要考虑加权最小二乘或对 y 做变换(如取对数)。
  • U型或倒U型曲线 :残差与预测值/自变量呈系统性弯曲,提示 模型缺失了重要项 (如该用二次但用了线性)。
  • 周期性波动 :残差呈现周期性,提示数据可能存在未被模型捕捉的周期成分。

2. 量化指标:

  • R-squared (决定系数) :表示模型解释的数据变异性的比例。越接近1越好。但注意,对于非线性模型,其定义和解释与线性模型不同,需谨慎使用。
  • 调整后的 R-squared :考虑了模型复杂度(参数个数),用于比较不同复杂度模型。
  • 均方根误差 RMSE = sqrt(SSE / n) 。它与 y 有相同量纲,可以直观理解为“平均预测误差有多大”。
  • 参数的标准误与置信区间 :从协方差矩阵 p_cov 可以计算参数的标准差,进而评估参数估计的精度。如果某个参数的置信区间包含0,意味着该参数可能不显著(对于线性模型)。

3. 过拟合检验(针对多项式或复杂模型): 将数据随机分为训练集(如70%)和测试集(30%)。用训练集拟合模型,然后计算模型在 测试集 上的 RMSE。如果训练集 RMSE 远小于测试集 RMSE,说明模型过拟合了训练数据的噪声,泛化能力差。

6. 系统辨识中的应用:从数据到动态模型

在控制工程和系统辨识领域,最小二乘法是构建动态系统数学模型(如差分方程、传递函数)的基石。这里的“曲线”变成了系统输入输出数据在时间序列上展现的动态关系。

假设我们有一个离散时间系统,我们认为其输出 y(k) 与过去若干时刻的输入 u(k-1), u(k-2)... 和自身过去的输出 y(k-1), y(k-2)... 有关,这可以用一个线性差分方程描述(自回归外生模型): y(k) + a1*y(k-1) + ... + ana*y(k-na) = b1*u(k-1) + ... + bnb*u(k-nb) + e(k) 其中 e(k) 是白噪声。

我们可以将其重写为: y(k) = -a1*y(k-1) - ... - ana*y(k-na) + b1*u(k-1) + ... + bnb*u(k-nb) + e(k) = φ(k)^T * θ + e(k) 其中, φ(k) = [-y(k-1), ..., -y(k-na), u(k-1), ..., u(k-nb)]^T 回归向量 θ = [a1, ..., ana, b1, ..., bnb]^T 是待辨识的 参数向量

对于从 k=1 k=N N 组观测数据,我们可以构建: Y = [y(1), y(2), ..., y(N)]^T Φ = [φ(1)^T; φ(2)^T; ...; φ(N)^T] (这是一个 N x (na+nb) 的矩阵) E = [e(1), e(2), ..., e(N)]^T 于是系统方程可以写成矩阵形式: Y = Φθ + E

这正是一个标准的线性最小二乘问题!目标是最小化误差平方和 E^T E 。其最小二乘解为: θ_LS = (Φ^T Φ)^{-1} Φ^T Y 这个公式与线性回归的正规方程解在形式上完全一致。通过采集系统的输入输出数据,构造出 Φ Y ,我们就可以利用这个公式一次性估计出模型的所有参数 θ

在MATLAB中的系统辨识工具箱 Python的 SciPy 或 SysIdentPy 库 中,都有现成的函数来实现这个过程。例如,在较新版本的MATLAB中,可以使用 arx 函数或 tfest 函数;在Python中,可以自定义构建 Φ 矩阵并使用 np.linalg.lstsq 求解。

系统辨识中的特殊考量:

  1. 持续激励 :输入信号 u(k) 需要足够“丰富”(如包含多种频率成分的伪随机二进制序列),才能激励出系统的所有动态模式,使矩阵 Φ^T Φ 可逆(满秩)。
  2. 模型阶次选择 na nb 的选择至关重要。阶次太低,模型欠拟合,无法捕捉系统动态;阶次太高,模型过拟合,会去拟合噪声。通常结合残差检验(残差是否像白噪声)和信息准则(如AIC、BIC)来综合确定。
  3. 闭环辨识 :如果数据来自闭环控制系统,直接使用上述最小二乘可能会产生有偏估计,需要采用其他方法,如辅助变量法、预报误差法等。

从静态的曲线拟合到动态的系统辨识,最小二乘原理提供了一套统一、强大的框架,将我们面对杂乱数据时的直觉——“找一条最贴近所有点的线”——转化为了严谨的数学优化问题。理解其背后的误差定义、目标函数构建和优化求解过程,不仅能让你在调用 polyfit curve_fit 时心里有底,更能让你在面对更复杂的建模任务时,知道如何将问题“翻译”成最小二乘能够解决的形式。这,或许就是这个古老原理历经两百年而不衰的魅力所在。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐