用Python+NumPy手把手实现最小二乘法:从拟合直线到理解投影矩阵

在数据科学和机器学习领域,最小二乘法是一个基础但极其重要的概念。它不仅是线性回归的核心算法,更是理解许多高级机器学习模型的基础。本文将通过Python和NumPy库,带你从零开始实现最小二乘法,并通过可视化手段深入理解其背后的线性代数原理。

1. 最小二乘法基础概念

最小二乘法(Least Squares, LS)的核心思想是通过最小化误差的平方和来寻找数据的最佳函数匹配。当我们有一组数据点,希望用一条直线来拟合这些点时,最小二乘法能给出最优的斜率和截距。

为什么需要最小二乘法?

  • 在实际测量中,数据往往带有噪声
  • 当方程数量多于未知数时,通常无精确解
  • 最小二乘解能给出最接近所有数据点的直线

考虑三个数据点(1,1)、(2,2)、(3,2),我们可以建立方程组:

import numpy as np

A = np.array([[1, 1], [2, 1], [3, 1]])
b = np.array([1, 2, 2])

这个方程组是超定的(方程数多于未知数),没有精确解。最小二乘法就是要找到参数x使得Ax尽可能接近b。

2. 投影矩阵的数学原理

最小二乘法的核心在于投影矩阵。给定矩阵A,其投影矩阵P定义为:

P = A(AᵀA)⁻¹Aᵀ

这个矩阵的神奇之处在于,它能将任何向量b投影到A的列空间上。投影后的向量p=Pb就是A列空间中最接近b的点。

投影矩阵的性质:

  • 对称性:P = Pᵀ
  • 幂等性:P² = P
  • 秩等于A的秩

在Python中,我们可以这样计算投影矩阵:

def projection_matrix(A):
    return A @ np.linalg.inv(A.T @ A) @ A.T

P = projection_matrix(A)
print("投影矩阵P:\n", P)

3. 实现最小二乘解

有了投影矩阵,我们可以通过两种等价的方式得到最小二乘解:

方法一:直接解正规方程

AᵀAx̂ = Aᵀb

对应的Python实现:

x_hat = np.linalg.inv(A.T @ A) @ A.T @ b
print("最小二乘解:", x_hat)

方法二:通过投影

先计算投影p=Pb,然后解Ax̂=p:

p = P @ b
x_hat = np.linalg.lstsq(A, p, rcond=None)[0]

这两种方法在数学上是等价的,但数值计算上第一种更直接高效。

4. 验证正交性

最小二乘法的关键性质是误差向量e=b-Ax̂与A的列空间正交。我们可以用点积来验证:

e = b - A @ x_hat
print("A的转置与e的点积:", A.T @ e)

理论上这个结果应该非常接近零向量,验证了我们的解确实满足正交性条件。

5. 可视化理解

为了更直观地理解,我们可以用matplotlib将整个过程可视化:

import matplotlib.pyplot as plt

# 原始数据点
plt.scatter(A[:,0], b, color='red', label='原始数据')

# 拟合直线
x_line = np.linspace(0, 4, 100)
y_line = x_hat[0]*x_line + x_hat[1]
plt.plot(x_line, y_line, label='拟合直线')

# 投影点
p = A @ x_hat
plt.scatter(A[:,0], p, color='green', marker='x', label='投影点')

# 误差线
for i in range(len(b)):
    plt.plot([A[i,0], A[i,0]], [b[i], p[i]], 'k--')

plt.legend()
plt.xlabel('x')
plt.ylabel('y')
plt.title('最小二乘法直线拟合')
plt.grid(True)
plt.show()

这张图会显示:

  1. 原始数据点(红色)
  2. 拟合的直线
  3. 投影到直线上的点(绿色×)
  4. 误差向量(黑色虚线)

6. 实际应用案例

最小二乘法在实际中有广泛应用。例如,考虑一个简单的房价预测模型:

# 房屋面积和价格数据
area = np.array([50, 60, 70, 80, 90])  # 平方米
price = np.array([200, 230, 250, 280, 310])  # 万元

# 构建A矩阵和b向量
A = np.vstack([area, np.ones(len(area))]).T
b = price

# 计算最小二乘解
x_hat = np.linalg.inv(A.T @ A) @ A.T @ b

print(f"房价模型: 价格 = {x_hat[0]:.2f} × 面积 + {x_hat[1]:.2f}")

这个模型可以帮助我们根据房屋面积预测价格,展示了最小二乘法在实际问题中的应用价值。

7. 数值稳定性考虑

在实际计算中,直接求逆矩阵可能带来数值不稳定性。更稳健的方法是使用QR分解或SVD:

使用QR分解:

Q, R = np.linalg.qr(A)
x_hat = np.linalg.inv(R) @ Q.T @ b

使用numpy内置的最小二乘函数:

x_hat = np.linalg.lstsq(A, b, rcond=None)[0]

这些方法在数值上更稳定,特别是当A的条件数较大时。

8. 扩展到多项式拟合

最小二乘法不仅可以拟合直线,还可以拟合更高阶的多项式。例如,二次拟合:

# 生成二次数据
x = np.array([1, 2, 3, 4, 5])
y = np.array([1.2, 3.9, 8.1, 15.2, 23.5])

# 构建A矩阵:包含x², x, 1项
A = np.vstack([x**2, x, np.ones(len(x))]).T

# 求解
x_hat = np.linalg.lstsq(A, y, rcond=None)[0]

print(f"二次模型: y = {x_hat[0]:.2f}x² + {x_hat[1]:.2f}x + {x_hat[2]:.2f}")

这种扩展性展示了最小二乘法的强大灵活性。

9. 理解AᵀA的性质

最小二乘法中,AᵀA矩阵扮演着核心角色。它的性质决定了我们能否得到唯一解:

  • AᵀA总是对称的
  • 当A列满秩时,AᵀA可逆
  • AᵀA的条件数是A条件数的平方

我们可以检查矩阵的条件数:

cond_A = np.linalg.cond(A)
cond_ATA = np.linalg.cond(A.T @ A)

print(f"A的条件数: {cond_A:.2f}")
print(f"AᵀA的条件数: {cond_ATA:.2f}")

理解这些性质有助于我们在实际问题中选择合适的求解方法。

10. 常见问题与调试技巧

在实现最小二乘法时,可能会遇到以下问题:

问题1:矩阵不可逆

  • 检查A是否列满秩
  • 考虑使用正则化(岭回归)

问题2:数值不稳定

  • 使用QR分解代替直接求逆
  • 对数据进行标准化

问题3:拟合效果差

  • 检查数据是否有异常值
  • 考虑使用更复杂的模型(如多项式拟合)
# 检查矩阵秩
print("A的秩:", np.linalg.matrix_rank(A))
print("AᵀA的秩:", np.linalg.matrix_rank(A.T @ A))

11. 性能优化技巧

对于大规模数据,我们可以采用一些优化策略:

使用广播机制加速计算:

# 快速计算误差平方和
def compute_sse(A, b, x):
    return np.sum((b - A @ x)**2)

利用稀疏矩阵(当A很大且稀疏时):

from scipy.sparse import csr_matrix
from scipy.sparse.linalg import lsqr

A_sparse = csr_matrix(A)
result = lsqr(A_sparse, b)
x_hat = result[0]

12. 从线性代数到机器学习

最小二乘法是连接线性代数和机器学习的重要桥梁。在机器学习中:

  • 线性回归就是最小二乘法
  • 正则化最小二乘对应岭回归和Lasso
  • 核方法可以看作是在特征空间中的最小二乘

理解最小二乘法的原理,为学习更复杂的机器学习模型奠定了坚实基础。

Logo

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

更多推荐