1. 从“差不多”到“刚刚好”:理解最小二乘法的灵魂

做数据分析、搞工程建模,甚至是处理实验数据,我们总会遇到一个绕不开的问题:手里有一堆散乱的数据点,怎么才能找到一条最合适的曲线来描述它们背后的规律?你可能会说,画条线穿过去,让点尽量靠近这条线不就行了?这话没错,但“尽量靠近”这个词太模糊了。是让所有点到线的垂直距离之和最小?还是水平距离?或者别的什么距离?不同的定义,会得到完全不同的“最佳”曲线。而 最小二乘法 ,就是给“最佳拟合”这个模糊概念一个清晰、普适且数学上极其优美的定义:它追求的是所有数据点到拟合曲线的 垂直距离的平方和 达到最小。这个看似简单的准则,自高斯和勒让德时代提出以来,已经成为了科学和工程领域数据建模的基石。今天,我们就抛开复杂的公式推导,从根子上聊聊这个“二乘”到底乘了什么,以及我们该如何用好它。

2. 核心思想拆解:为什么是“平方”和“最小”?

2.1 误差的度量:从绝对值到平方的跃迁

当我们用一条曲线 $y = f(x)$ 去拟合数据点 $(x_i, y_i)$ 时,每个点都会产生一个误差 $e_i = y_i - f(x_i)$。最直观的想法是让所有误差的总和 $\sum |e_i|$ 最小,即最小一乘法。这很符合直觉,但它在数学上有个麻烦:绝对值函数在零点不可导。不可导意味着我们很难用那些强大的微积分工具去寻找最优解,计算会变得复杂。

最小二乘法巧妙地选择了误差的平方 $e_i^2$ 作为度量。平方运算有两大好处:

  1. 可导性 :平方函数处处可导,这使得我们可以通过求导数等于零(即求极值点)这个标准方法来寻找最优拟合参数。这是其计算可行性的核心。
  2. 放大大误差 :平方运算会放大较大的误差。如果一个点的误差是2,另一个是10,在绝对值和中它们贡献12;但在平方和中,2²=4,10²=100,大误差的权重被显著放大。这意味着最小二乘拟合会 更倾向于惩罚那些偏离很远的点 ,从而迫使拟合曲线不能为了照顾多数点而过分忽视少数异常点,拟合结果通常对整体趋势的捕捉更稳健。

注意 :这种对大误差的敏感性是一把双刃剑。它使得拟合对 异常值 非常敏感。一个明显的错误数据点(野值)可能会把整个拟合线“拉偏”。因此,在实际应用最小二乘法前,数据清洗和异常值检测至关重要。

2.2 “最小”的几何与代数意义

从几何角度看,我们是在所有可能的曲线构成的“空间”里,寻找一条到给定数据点“距离”最近的曲线。这里“距离”被定义为所有数据点在y轴方向上的偏差的平方和。最小二乘解就是这个空间中的“垂足”。

从代数角度看,当我们把拟合模型(比如线性模型 $y = ax + b$)代入所有数据点,会得到一个方程组。通常数据点数量多于待求参数(方程数多于未知数),这是一个 超定方程组 ,一般无精确解。最小二乘法的目标就是寻找一组参数 $(a, b)$,使得方程组左右两边的差异(即残差)的平方和最小,从而求出一个在最小平方误差意义下的 最优近似解

3. 从直线到曲线:拟合模型的构建

3.1 线性最小二乘:一切的起点

我们以最经典的线性拟合 $y = ax + b$ 为例,透彻理解整个过程。设有n个数据点 $(x_i, y_i)$,目标是找到 $a$ 和 $b$,使损失函数 $L = \sum_{i=1}^{n} [y_i - (a x_i + b)]^2$ 最小。

这是一个关于 $a$ 和 $b$ 的二元二次函数求极小值问题。我们分别对 $a$ 和 $b$ 求偏导数,并令其等于零:

$$ \begin{aligned} \frac{\partial L}{\partial a} &= -2 \sum_{i=1}^{n} [y_i - (a x_i + b)] x_i = 0 \ \frac{\partial L}{\partial b} &= -2 \sum_{i=1}^{n} [y_i - (a x_i + b)] = 0 \end{aligned} $$

整理后得到著名的 正规方程组

$$ \begin{aligned} (\sum x_i^2) a + (\sum x_i) b &= \sum x_i y_i \ (\sum x_i) a + n b &= \sum y_i \end{aligned} $$

解这个二元一次方程组,即可得到 $a$ 和 $b$ 的解析解:

$$ \begin{aligned} a &= \frac{n \sum x_i y_i - \sum x_i \sum y_i}{n \sum x_i^2 - (\sum x_i)^2} \ b &= \frac{\sum y_i \sum x_i^2 - \sum x_i \sum x_i y_i}{n \sum x_i^2 - (\sum x_i)^2} \end{aligned} $$

实操心得 :自己推导并编程实现一次这个公式,比调用十次 np.polyfit lm() 函数理解得更深刻。你会注意到分母 $n \sum x_i^2 - (\sum x_i)^2$,这其实就是 $n$ 倍 $x$ 的方差。当 $x$ 值全部相同时,方差为零,分母为零,公式失效——这对应着所有数据点垂直排列,显然无法确定一条有斜率的直线。

3.2 非线性关系的线性化:化曲为直的智慧

很多物理、生物、经济现象并非线性关系,但我们可以通过变量替换,将其转化为线性模型处理。

  • 多项式拟合 :拟合 $y = a_0 + a_1 x + a_2 x^2 + ... + a_m x^m$。只需令 $X_1 = x, X_2 = x^2, ..., X_m = x^m$,原方程就变成了关于新变量 $X_1, X_2, ..., X_m$ 的 多元线性模型 ,可以直接套用多元线性最小二乘法。
  • 指数拟合 :拟合 $y = A e^{Bx}$。两边取自然对数:$\ln y = \ln A + Bx$。令 $Y' = \ln y, A' = \ln A$,则变为 $Y' = A' + Bx$,成为线性模型。
  • 幂律拟合 :拟合 $y = A x^B$。两边取对数:$\ln y = \ln A + B \ln x$。令 $Y' = \ln y, X' = \ln x, A' = \ln A$,则变为 $Y' = A' + B X'$。

注意事项 :经过线性化变换后,我们是在最小化变换后变量(如 $\ln y$)的误差平方和,而不是原始变量 $y$ 的。这会导致拟合优度是针对变换后的数据而言的。有时这没问题,但如果你需要最小化原始数据的误差,就需要使用下一节的非线性最小二乘。

3.3 非线性最小二乘:迭代寻优的战场

当模型无法通过变换转为线性形式时,如 $y = a e^{-bx} + c$,我们就进入了非线性最小二乘的领域。此时损失函数 $L = \sum [y_i - f(x_i; \theta)]^2$ 是关于参数向量 $\theta$ 的复杂非线性函数,没有解析解。

核心思路是 迭代优化

  1. 初始化 :给参数 $\theta$ 一个初始猜测值。
  2. 线性近似 :在当前参数值 $\theta_k$ 处,对模型函数 $f(x;\theta)$ 进行一阶泰勒展开,将其近似为一个线性模型。
  3. 求解增量 :对这个线性近似模型使用线性最小二乘,计算出一个参数增量 $\Delta \theta$。
  4. 更新参数 :$\theta_{k+1} = \theta_k + \Delta \theta$。
  5. 判断收敛 :如果参数变化或误差下降足够小,则停止;否则回到第2步。

最经典的算法是 高斯-牛顿法 列文伯格-马夸尔特法 。LM算法是GN法的改进,更鲁棒,能处理初始值不佳的情况,可以说是非线性拟合的实际标准算法。

实操心得 :使用非线性最小二乘时, 初始值的选择至关重要 。一个糟糕的初始值可能导致算法收敛到局部最优解,甚至发散。通常需要基于物理意义或通过线性化模型先得到一个粗略估计作为初始值。在代码中(如SciPy的 curve_fit 或 MATLAB的 lsqcurvefit ),务必关注返回的协方差矩阵,它反映了参数估计的不确定性。

4. 实操演练:以MATLAB/Python为例

理论说得再多,不如动手做一遍。我们用一个简单的例子,分别用MATLAB和Python实现线性与非线性拟合,并解读关键输出。

4.1 线性拟合实战

假设我们测量了弹簧在不同负重下的伸长量,数据如下: 负重 (x): [1, 2, 3, 4, 5, 6] (kg) 伸长 (y): [2.1, 3.8, 6.1, 7.8, 10.2, 11.9] (cm)

根据胡克定律,这应该是一个线性关系 $y = kx + b$。

Python (NumPy/SciPy) 实现:

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

x = np.array([1, 2, 3, 4, 5, 6])
y = np.array([2.1, 3.8, 6.1, 7.8, 10.2, 11.9])

# 方法1:使用np.polyfit进行一阶多项式拟合
k, b = np.polyfit(x, y, 1)
print(f"斜率 k (劲度系数倒数) = {k:.4f}")
print(f"截距 b (初始长度?) = {b:.4f}")

# 方法2:使用stats.linregress,它提供更多统计信息
slope, intercept, r_value, p_value, std_err = stats.linregress(x, y)
print(f"斜率: {slope:.4f}, 截距: {intercept:.4f}")
print(f"相关系数 R: {r_value:.4f}, R^2: {r_value**2:.4f}")
print(f"斜率的标准误: {std_err:.4f}")

# 绘图
y_fit = k * x + b
plt.scatter(x, y, label='原始数据')
plt.plot(x, y_fit, 'r-', label=f'拟合直线: y = {k:.2f}x + {b:.2f}')
plt.xlabel('负重 (kg)')
plt.ylabel('伸长量 (cm)')
plt.legend()
plt.grid(True)
plt.show()

关键输出解读

  • k 约为 1.98,这可能是弹簧劲度系数 $K$ 的倒数(因为 $F=K\Delta x$,这里 $F=mg\approx 10x$,所以 $K \approx 10/k \approx 5.05 N/cm$)。需要根据你的单位理解其物理意义。
  • b 约为 0.13,理论上应为0(无负重时无伸长),这里的微小正值可能是测量系统误差或弹簧初始状态所致。
  • R^2 (决定系数)非常接近1(如0.999),说明线性模型解释度极高。
  • std_err 是斜率估计的标准误差,可用于计算置信区间。

MATLAB 实现:

x = [1, 2, 3, 4, 5, 6];
y = [2.1, 3.8, 6.1, 7.8, 10.2, 11.9];

% 使用 polyfit
p = polyfit(x, y, 1); % p(1)是斜率,p(2)是截距
k = p(1);
b = p(2);
fprintf('斜率 k = %.4f\n', k);
fprintf('截距 b = %.4f\n', b);

% 计算拟合值和R^2
y_fit = polyval(p, x);
SS_res = sum((y - y_fit).^2);
SS_tot = sum((y - mean(y)).^2);
R2 = 1 - SS_res / SS_tot;
fprintf('R^2 = %.4f\n', R2);

% 使用 fitlm 获取更详细的统计模型(需要Statistics and Machine Learning Toolbox)
% mdl = fitlm(x, y);
% disp(mdl)

4.2 非线性拟合实战:药物浓度衰减

假设某药物在体内的浓度随时间呈指数衰减:$C(t) = C_0 e^{-kt}$。我们测得一组数据: 时间 t: [0.5, 1, 2, 3, 4, 6, 8] (小时) 浓度 C: [8.2, 5.7, 3.1, 1.8, 1.1, 0.4, 0.2] (mg/L)

Python (SciPy) 实现:

import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit

# 定义模型函数
def model_func(t, C0, k):
    return C0 * np.exp(-k * t)

t_data = np.array([0.5, 1, 2, 3, 4, 6, 8])
C_data = np.array([8.2, 5.7, 3.1, 1.8, 1.1, 0.4, 0.2])

# 提供初始猜测值至关重要!这里从数据粗略估计:t=0时C约10,半衰期约1.5小时 => k ~ ln2/1.5
initial_guess = [10, 0.46]

# 进行非线性最小二乘拟合
params_opt, params_cov = curve_fit(model_func, t_data, C_data, p0=initial_guess)
C0_opt, k_opt = params_opt
perr = np.sqrt(np.diag(params_cov)) # 计算参数的标准误差

print(f"拟合参数: C0 = {C0_opt:.2f} ± {perr[0]:.2f} mg/L")
print(f"拟合参数: k = {k_opt:.3f} ± {perr[1]:.3f} /小时")
print(f"药物半衰期 t_{1/2} = {np.log(2)/k_opt:.2f} 小时")

# 绘图
t_fine = np.linspace(0, 9, 100)
C_fit = model_func(t_fine, C0_opt, k_opt)

plt.scatter(t_data, C_data, label='实测浓度')
plt.plot(t_fine, C_fit, 'r-', label=f'拟合曲线: C(t)={C0_opt:.1f}*exp(-{k_opt:.3f}t)')
plt.xlabel('时间 (小时)')
plt.ylabel('药物浓度 (mg/L)')
plt.legend()
plt.grid(True)
plt.show()

关键操作解析

  1. curve_fit 函数的核心是迭代优化(默认使用LM算法)。
  2. p0=initial_guess 提供了初始值 [C0, k] 。尝试去掉它或用 [1, 1] 试试,可能会收敛到错误解或失败。
  3. params_cov 是参数的协方差矩阵,其对角线元素的平方根 perr 给出了各个参数的 标准误差 ,这是评估参数估计精度的关键指标。
  4. 我们从速率常数 k 计算了半衰期 $t_{1/2} = \ln 2 / k$。

MATLAB 实现:

t = [0.5, 1, 2, 3, 4, 6, 8];
C = [8.2, 5.7, 3.1, 1.8, 1.1, 0.4, 0.2];

% 定义模型函数句柄
model = @(p, t) p(1) * exp(-p(2) * t);

% 初始猜测值
initialGuess = [10, 0.46];

% 使用 lsqcurvefit 进行非线性拟合
options = optimoptions('lsqcurvefit', 'Display', 'iter'); % 显示迭代过程
[params_opt, resnorm, residual, exitflag, output, lambda, jacobian] = ...
    lsqcurvefit(model, initialGuess, t, C);

C0_opt = params_opt(1);
k_opt = params_opt(2);

% 计算参数置信区间(需要Statistics and Machine Learning Toolbox)
% ci = nlparci(params_opt, residual, 'jacobian', jacobian);

fprintf('拟合参数: C0 = %.2f mg/L\n', C0_opt);
fprintf('拟合参数: k = %.3f /小时\n', k_opt);
fprintf('半衰期 = %.2f 小时\n', log(2)/k_opt);

% 绘图
t_fine = linspace(0, 9, 100);
C_fit = model(params_opt, t_fine);
plot(t, C, 'o', t_fine, C_fit, '-');
xlabel('时间 (小时)'); ylabel('浓度 (mg/L)');
legend('数据', '拟合曲线');
grid on;

5. 系统辨识中的应用:动态模型的参数估计

在控制工程和系统辨识领域,最小二乘法是辨识系统模型参数的核心工具。其思想是:将一个动态系统(如差分方程描述的系统)的输出表示成关于过去输入输出数据和待估参数的线性形式,从而将动态参数估计问题转化为静态的线性最小二乘问题。

考虑一个简单的单输入单输出系统,可以用如下自回归外生模型描述: $$y(k) + a_1 y(k-1) + ... + a_{na} y(k-na) = b_1 u(k-1) + ... + b_{nb} u(k-nb) + e(k)$$ 其中 $u$ 是输入,$y$ 是输出,$e$ 是噪声,$k$ 是时间步,$na, nb$ 是模型阶次。

将其改写为: $$y(k) = [-y(k-1), ..., -y(k-na), u(k-1), ..., u(k-nb)] \cdot [a_1, ..., a_{na}, b_1, ..., b_{nb}]^T + e(k)$$

对于从 $k=1$ 到 $N$ 的所有数据,我们可以构建矩阵方程: $$\mathbf{Y} = \mathbf{\Phi} \mathbf{\theta} + \mathbf{E}$$ 其中:

  • $\mathbf{Y} = [y(1), y(2), ..., y(N)]^T$ 是输出向量。
  • $\mathbf{\Phi}$ 是回归矩阵,每一行由对应时刻的过去输入输出数据组成。
  • $\mathbf{\theta} = [a_1, ..., a_{na}, b_1, ..., b_{nb}]^T$ 是待估参数向量。
  • $\mathbf{E}$ 是误差向量。

此时,最小二乘估计 $\hat{\mathbf{\theta}}$ 就是使 $|\mathbf{Y} - \mathbf{\Phi}\mathbf{\theta}|^2$ 最小的解,其解析解为: $$\hat{\mathbf{\theta}} = (\mathbf{\Phi}^T \mathbf{\Phi})^{-1} \mathbf{\Phi}^T \mathbf{Y}$$ 这就是 批处理最小二乘 。对于时变系统,还有递推最小二乘等在线算法。

注意事项 :在系统辨识中,一个关键前提是噪声 $e(k)$ 是白噪声。如果噪声是有色的(即与过去的输入输出相关),普通最小二乘估计将是有偏的。此时需要用到 广义最小二乘法 辅助变量法 等更高级的方法。此外,输入信号 $u(k)$ 需要具有持续激励性,才能保证 $(\mathbf{\Phi}^T \mathbf{\Phi})$ 矩阵可逆,即参数可辨识。

6. 常见陷阱、问题排查与高级考量

即使理解了原理和步骤,在实际应用中仍会踩坑。下面是一些典型问题及应对策略。

6.1 过拟合与欠拟合:模型的复杂度选择

这是拟合中的核心矛盾。

  • 欠拟合 :模型过于简单(如用直线拟合明显弯曲的数据),无法捕捉数据中的趋势。表现为训练误差和未来预测误差都很大。
  • 过拟合 :模型过于复杂(如用高阶多项式拟合带噪声的数据),不仅拟合了趋势,还“拟合”了噪声。表现为训练误差极小,但预测新数据时误差很大,泛化能力差。

如何判断与选择?

  1. 可视化 :始终绘制拟合曲线与原始数据点的对比图。观察曲线是否平滑地穿过数据点聚集区,还是剧烈波动以穿过每一个点。
  2. 交叉验证 :将数据分为训练集和测试集。用训练集拟合模型,在测试集上评估误差。如果训练误差远小于测试误差,很可能过拟合了。
  3. 信息准则 :对于参数化模型,可以使用AIC或BIC准则。它们在拟合优度(残差平方和)的基础上,增加了对参数数量的惩罚,倾向于选择更简洁的模型。
  4. 正则化 :当模型复杂度必须较高时(如多项式阶数高),可以使用 岭回归 LASSO 。它们在损失函数中加入参数向量的L2或L1范数作为惩罚项,强制参数值变小甚至为零,从而抑制过拟合。

6.2 病态问题与数值稳定性

当求解正规方程 $(\mathbf{X}^T\mathbf{X})\mathbf{\theta} = \mathbf{X}^T\mathbf{Y}$ 时,如果矩阵 $\mathbf{X}^T\mathbf{X}$ 接近奇异(条件数很大),其逆矩阵对数据中的微小扰动会极其敏感,导致参数估计结果极不稳定。这在以下情况容易出现:

  • 特征量纲差异大 :例如,一个特征范围是[0, 1],另一个是[10000, 100000]。
  • 特征高度相关 :例如,在多项式拟合中,$x$ 和 $x^2$ 高度相关。

解决方案:

  1. 数据标准化/归一化 :将每个特征减去其均值,除以其标准差。这是最常用且有效的方法。
  2. 使用更稳定的算法 :避免直接计算 $(\mathbf{X}^T\mathbf{X})^{-1}$。使用 QR分解 奇异值分解 来求解最小二乘问题。像 numpy.linalg.lstsq 和 MATLAB的 \ 运算符内部都使用了SVD这类稳定算法。
  3. 增加正则化 :岭回归 $(X^TX + \lambda I)^{-1}X^TY$ 通过引入一个小常数 $\lambda$ 改善矩阵的条件数,使其可逆且稳定。

6.3 加权最小二乘:当误差并不平等时

标准最小二乘假设所有数据点的误差方差相同(同方差)。但现实中,不同数据点的测量精度可能不同。例如,某些点由精密仪器测得,误差小;另一些点由粗略方法测得,误差大。此时,我们应该给高精度数据点更大的权重。

加权最小二乘的损失函数变为: $$L_w = \sum_{i=1}^{n} w_i [y_i - f(x_i)]^2$$ 其中 $w_i$ 是权重,通常与测量误差方差 $\sigma_i^2$ 成反比,即 $w_i = 1/\sigma_i^2$。其解为: $$\hat{\mathbf{\theta}} = (\mathbf{X}^T \mathbf{W} \mathbf{X})^{-1} \mathbf{X}^T \mathbf{W} \mathbf{Y}$$ 其中 $\mathbf{W}$ 是以 $w_i$ 为对角元素的对角矩阵。

6.4 鲁棒回归:对抗异常值的铠甲

如前所述,最小二乘对异常值敏感。当数据中存在少量但严重的异常点时,鲁棒回归方法能提供更可靠的拟合。其核心思想是降低大残差数据点的权重。

  • Huber损失 :在残差较小时使用平方损失,较大时使用线性损失,平滑过渡。
  • Tukey双权重损失 :当残差超过某个阈值时,权重降为零,完全忽略该点。
  • RANSAC :一种随机采样一致性算法。它反复随机选取一个子集进行拟合,然后计算有多少点符合这个模型(即残差小于阈值),最后选择共识集最大的模型。对包含大量外点的数据非常有效。

在Python中, sklearn.linear_model 提供了 RANSACRegressor HuberRegressor 。在MATLAB中, robustfit 函数提供了多种鲁棒拟合选项。

7. 评估拟合质量:不止看R²

得到一个拟合模型后,如何判断它好不好?

  1. 残差分析 :这是最强大的诊断工具。绘制残差 $e_i = y_i - \hat{y}_i$ 随自变量 $x_i$ 或拟合值 $\hat{y}_i$ 变化的散点图。
    • 理想情况 :残差随机、均匀地分布在0线上下,无明显模式。
    • 出现趋势 :如果残差呈现曲线趋势(如先正后负再正),说明模型可能漏掉了非线性成分。
    • 漏斗形状 :残差范围随 $x$ 增大而增大,说明可能存在异方差性,考虑加权最小二乘或对y做变换(如取对数)。
  2. 决定系数 R² :$R^2 = 1 - \frac{SS_{res}}{SS_{tot}}$,表示模型解释的数据变异比例。越接近1越好。但要注意,增加模型参数(复杂度)总会使R²增加,即使增加的是无意义的变量。因此更推荐看 调整后的R² ,它惩罚了参数数量。
  3. 参数置信区间 :通过协方差矩阵计算出的参数标准误,可以构建参数的置信区间(如95%置信区间)。如果区间包含0,则该参数可能不显著。
  4. 预测区间 :对于新的 $x_0$,我们不仅可以给出拟合值 $\hat{y}_0$,还可以给出其预测区间。这个区间比置信区间宽,因为它包含了单个观测值的随机误差。

最后,记住一句老生常谈但无比正确的话: 所有模型都是错的,但有些是有用的 。最小二乘法为我们提供了一个强大的工具来找到那个“有用”的模型,但模型的最终选择,必须结合物理背景、工程常识和对数据的深入洞察。它始于数学,但绝不止于数学。

Logo

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

更多推荐