最小二乘解的理解

hemol / 2024-09-19 / 原文

记录一下工作时遇到的拟合问题,将两个数据的关系建模为最小二乘的模型:

\[y = a_0 + a_1 x + a_2 x^2 + a_3 x^3 + a_4 x^4 \]

使用了python里面的numpy.linalg.lstsq函数进行拟合,以下是一个简单的示例


import numpy as np
import matplotlib.pyplot as plt

# 样本数据点
x = np.array([-10, -8, -5, -3, 0, 2, 5, 7, 9, 12])
y = np.array([1200, 800, 200, 100, 50, 80, 300, 600, 1000, 1800])

# 构建设计矩阵
X = np.vstack([x**n for n in range(5)]).T  # 包含 x 的 0 次到 4 次方

# 求解最小二乘问题
coefficients, residuals, rank, singular_values = np.linalg.lstsq(X, y, rcond=None)

# 打印系数
print("拟合多项式的系数为:", coefficients)

# 计算拟合的 y 值
y_fit = X @ coefficients

# 绘制结果
plt.scatter(x, y, color='blue', label='原始数据')
plt.plot(x, y_fit, color='red', label='拟合多项式')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.title('最小二乘法多项式拟合')
plt.show()

调库拟合感觉很抽象,没办法深入理解到底做了什么事情。
于是研究了一下拟合的时候具体做了哪些步骤:

在拟合多项式 $y = a_0 + a_1 x + a_2 x^2 + a_3 x^3 + a_4 x^4 $的系数时,我们需要解决一个线性方程组,使得预测值与实际值之间的误差平方和最小。这就是典型的最小二乘问题。


numpy.linalg.lstsq 的内部操作步骤:

  1. 构建设计矩阵 $ A $:

    • 我们将多项式的每一项视为一个特征,构建一个设计矩阵$ A $,其大小为 $ m \times n $(其中 $ m $是数据点的数量, $ n $ 是多项式系数的数量)。

    • 矩阵 $ A $ 的每一列对应于 $ x $ 的不同次幂:

      \[A = \begin{bmatrix} 1 & x_1 & x_1^2 & x_1^3 & x_1^4 \\ 1 & x_2 & x_2^2 & x_2^3 & x_2^4 \\ \vdots & \vdots & \vdots & \vdots & \vdots \\ 1 & x_m & x_m^2 & x_m^3 & x_m^4 \\ \end{bmatrix} \]

  2. 设定观测向量 $ b $:

    • 向量 $ b $ 包含了对应的 $ y $ 值:

      \[b = \begin{bmatrix} y_1 \\ y_2 \\ \vdots \\ y_m \\ \end{bmatrix} \]

  3. 定义最小二乘目标:

    • 我们的目标是找到系数向量 $ x $(即 $ a_0, a_1, a_2, a_3, a_4 $),使得残差的平方和最小:

      \[\min_x \| Ax - b \|_2^2 \]

  4. 计算矩阵的奇异值分解(SVD):

    • 内部,lstsq 函数使用 奇异值分解(Singular Value Decomposition, SVD) 对矩阵 $ A $ 进行分解:

      \[A = U \Sigma V^T \]

      • $ U $ 是 $ m \times m $ 的正交矩阵。
      • $ \Sigma $ 是 $ m \times n $ 的对角矩阵,对角线上的元素为 $ A $ 的奇异值。
      • $ V^T $ 是 $ n \times n $ 的正交矩阵的转置。
  5. 计算矩阵 $ A $ 的伪逆 $ A^+ $:

    • 基于 SVD 分解,计算 $ A $ 的 Moore-Penrose 伪逆

      \[A^+ = V \Sigma^+ U^T \]

      • $ \Sigma^+ $ 是将 $ \Sigma $ 中非零元素取倒数后转置而得。
  6. 求解最小二乘解 $ x $:

    • 使用伪逆计算解:

      \[x = A^+ b \]

      • 这给出了使得 $ | Ax - b |_2 $ 最小的系数向量 $ x $。
  7. 计算残差 $ r $:

    • 计算残差向量:

      \[r = b - Ax \]

    • 残差的平方和为:

      \[\text{residuals} = \| r \|_2^2 \]

  8. 确定矩阵 $ A $ 的秩:

    • 通过奇异值判断 $ A $ 的 有效秩(rank),这有助于了解问题的可解性和解的稳定性。
  9. 返回结果:

    • lstsq 函数返回以下内容:
      • 系数向量 $ x $:拟合多项式的系数。
      • 残差数组:残差的平方和(若方程组为超定,即方程数大于未知数)。
      • 秩 $ rank $:矩阵 $ A $ 的秩。
      • 奇异值数组:矩阵 $ A $ 的奇异值。

总结:

  • numpy.linalg.lstsq 函数通过对设计矩阵 ( A ) 进行奇异值分解,计算其伪逆,然后求解最小二乘问题,得到使预测值与实际值误差平方和最小的系数。
  • 使用 SVD 有以下优点:
    • 数值稳定性:SVD 对于病态矩阵(条件数很大)能够提供稳定的求解。
    • 处理欠定和超定方程组:即使矩阵 ( A ) 不满秩,SVD 也能找到范数最小的最小二乘解。

附加说明:

  • 为什么不是直接求解正常方程 $ A^T A x = A^T b $?

    • 直接求解可能导致数值不稳定,尤其是当 $ A^T A $ 的条件数很大时。
    • 使用 SVD 可以避免这些问题,提供更可靠的解。
  • 计算复杂度:

    • SVD 的计算复杂度较高,但对于中小规模的问题(如多项式拟合),现代计算机可以快速完成计算。

举例:

假设我们有以下数据点:

import numpy as np

# 样本数据点
x = np.array([-10, -8, -5, -3, 0, 2, 5, 7, 9, 12])
y = np.array([1200, 800, 200, 100, 50, 80, 300, 600, 1000, 1800])

# 构建设计矩阵 A
A = np.vstack([x**0, x**1, x**2, x**3, x**4]).T

# 使用最小二乘法求解
coefficients, residuals, rank, singular_values = np.linalg.lstsq(A, y, rcond=None)

print("拟合多项式的系数为:", coefficients)
print("残差的平方和为:", residuals)
print("矩阵 A 的秩为:", rank)
print("矩阵 A 的奇异值为:", singular_values)

详细步骤解读:

  1. 构建设计矩阵 $ A $:

    • 每一列对应 $ x $ 的 0 到 4 次幂,共 5 列。
    • $ A $ 的大小为 $ 10 \times 5 $。
  2. 使用 np.linalg.lstsq 求解:

    • 函数内部对 $ A $ 进行 SVD 分解。
    • 计算 $ A $ 的伪逆 $ A^+ $。
    • 求解系数向量 $ x = A^+ b $。
  3. 解的解释:

    • coefficients:拟合的多项式系数 $ [a_0, a_1, a_2, a_3, a_4] $。
    • residuals:预测值与实际 $ y $ 值之间的误差平方和。
    • rank:矩阵 $ A $ 的秩,表示特征的线性独立数量。
    • singular_values:用于判断矩阵是否存在病态或奇异情况。