用Python和NumPy手把手实现最小二乘法:从投影矩阵到直线拟合的完整代码

在数据科学和机器学习领域,线性回归是最基础也最重要的算法之一。当我们谈论线性回归时,最小二乘法(Least Squares)是绕不开的核心数学工具。本文将带你从线性代数的几何视角出发,逐步推导最小二乘法的原理,并用Python和NumPy从零实现一个完整的直线拟合解决方案。

1. 最小二乘法的几何直观

想象你面前有一张白纸,上面散落着几个点。你的任务是画一条直线,使得这些点到直线的垂直距离之和最小。这就是最小二乘法要解决的问题——找到最优的直线拟合。

从线性代数的角度看,这个问题可以转化为:

  • 将数据点的x坐标作为矩阵A的输入
  • y坐标作为向量b
  • 寻找系数向量x,使得Ax尽可能接近b

当数据点不完全共线时,方程Ax=b无解。此时最小二乘法的核心思想是:将b投影到A的列空间,得到投影p,然后求解Ax̂=p。

关键公式:

x̂ = (AᵀA)⁻¹Aᵀb

这个解x̂使得误差向量e=b-Ax̂的长度(即误差平方和)最小。

2. 投影矩阵的数学推导

要深入理解最小二乘法,我们需要先掌握投影矩阵的概念。投影矩阵P可以将任意向量b投影到矩阵A的列空间。

推导过程:

  1. 设投影p=Ax̂,其中x̂是我们要求的系数
  2. 误差e=b-p应该与A的列空间正交
  3. 因此Aᵀ(b-Ax̂)=0
  4. 整理得到正规方程:AᵀAx̂=Aᵀb
  5. 解得x̂=(AᵀA)⁻¹Aᵀb

投影矩阵P的表达式为:

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

这个矩阵有几个重要性质:

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

3. NumPy实现投影矩阵

现在让我们用NumPy来实现这个投影矩阵。假设我们有三个数据点(1,1), (2,2), (3,2),我们要找到最佳拟合直线y=kx+b。

import numpy as np

# 构造矩阵A和向量b
A = np.array([[1, 1], 
              [2, 1],
              [3, 1]])
b = np.array([1, 2, 2])

# 计算投影矩阵P
ATA = A.T @ A  # A的转置乘以A
P = A @ np.linalg.inv(ATA) @ A.T

print("投影矩阵P:")
print(P)

运行这段代码,你会看到输出的投影矩阵:

[[ 0.83333333  0.33333333 -0.16666667]
 [ 0.33333333  0.33333333  0.33333333]
 [-0.16666667  0.33333333  0.83333333]]

4. 实现最小二乘解

有了投影矩阵,我们可以计算最小二乘解x̂:

# 计算最小二乘解
x_hat = np.linalg.inv(ATA) @ A.T @ b
k, b = x_hat
print(f"拟合直线方程: y = {k:.2f}x + {b:.2f}")

输出结果应该是:

拟合直线方程: y = 0.50x + 0.67

这意味着我们的最佳拟合直线是y=0.5x+0.67。

5. 可视化结果

为了更直观地理解,让我们用matplotlib将原始数据点、拟合直线和投影点可视化:

import matplotlib.pyplot as plt

# 计算投影点p
p = P @ b

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

# 绘制拟合直线
x_line = np.linspace(0, 4, 100)
y_line = k * x_line + b
plt.plot(x_line, y_line, label=f'拟合直线: y={k:.2f}x+{b:.2f}')

# 绘制投影点
plt.scatter(A[:,0], p, color='green', label='投影点')

# 绘制误差线
for xi, yi, pi in zip(A[:,0], b, p):
    plt.plot([xi, xi], [yi, pi], 'k--', alpha=0.3)

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

这张图会显示:

  • 红色点:原始数据点
  • 绿色点:投影到拟合直线上的点
  • 黑色虚线:误差向量(原始点到投影点的距离)
  • 蓝色直线:最佳拟合直线

6. 从误差最小化角度理解

最小二乘法也可以从优化角度理解——最小化误差平方和。对于直线y=kx+b,误差平方和定义为:

E = Σ(yi - (kxi + b))²

我们可以通过求偏导并令其为零来找到最优k和b:

# 通过解正规方程验证结果
ATA = A.T @ A
ATb = A.T @ b
x_hat_verify = np.linalg.solve(ATA, ATb)  # 解ATA x = ATb

print("通过正规方程验证的解:", x_hat_verify)

这个方法应该给出与之前相同的结果,验证了我们实现的正确性。

7. 处理更一般的情况

上面的例子是二维直线拟合。最小二乘法可以推广到更高维的线性回归。例如,拟合一个二次曲线y=ax²+bx+c:

# 假设我们有数据点
x_data = np.array([1, 2, 3, 4, 5])
y_data = np.array([1.2, 1.9, 3.2, 4.1, 5.0])

# 构造设计矩阵A
A = np.column_stack([x_data**2, x_data, np.ones_like(x_data)])

# 计算最小二乘解
coefficients = np.linalg.inv(A.T @ A) @ A.T @ y_data
a, b, c = coefficients

print(f"拟合二次曲线: y = {a:.2f}x² + {b:.2f}x + {c:.2f}")

这种推广展示了最小二乘法的强大灵活性——只要模型对参数是线性的,就可以用相同的方法求解。

8. 数值稳定性的考量

在实际应用中,直接计算(AᵀA)⁻¹可能会遇到数值不稳定的问题。更稳健的方法是使用QR分解或奇异值分解(SVD)。

使用QR分解的实现:

def least_squares_qr(A, b):
    Q, R = np.linalg.qr(A)
    return np.linalg.solve(R, Q.T @ b)

x_hat_qr = least_squares_qr(A, b)
print("QR分解求解结果:", x_hat_qr)

这种方法通常数值稳定性更好,特别是当A的条件数较大时。

9. 性能优化技巧

对于大规模数据,我们可以利用一些优化技巧:

  1. 使用Cholesky分解:因为AᵀA是对称正定矩阵

    L = np.linalg.cholesky(ATA)
    y = np.linalg.solve(L, ATb)
    x_hat_chol = np.linalg.solve(L.T, y)
    
  2. 避免显式构造AᵀA:可以使用迭代方法如共轭梯度法

  3. 稀疏矩阵优化:当A是稀疏矩阵时,使用稀疏矩阵专用算法

10. 实际应用中的注意事项

在实际项目中使用最小二乘法时,需要注意以下几点:

  • 特征缩放:当特征量纲差异大时,应先进行标准化
  • 异常值处理:最小二乘法对异常值敏感,可考虑使用RANSAC或Huber回归
  • 多重共线性:当特征高度相关时,AᵀA可能接近奇异矩阵,需要正则化
  • 模型诊断:拟合后应检查残差分布,验证模型假设

11. 与现成库的对比

虽然我们实现了最小二乘法,但实际项目中可能会使用scikit-learn等库。了解底层实现有助于我们更好地使用这些工具:

from sklearn.linear_model import LinearRegression

model = LinearRegression(fit_intercept=False)
model.fit(A, b)
print("scikit-learn结果:", model.coef_)

这个结果应该与我们手动实现的结果一致,验证了我们的实现是正确的。

12. 扩展到加权最小二乘法

有时候不同数据点的可靠性不同,我们可以给每个点赋予不同的权重。加权最小二乘法的解为:

x̂ = (AᵀWA)⁻¹AᵀWb

其中W是对角权重矩阵。实现代码如下:

# 假设第二个数据点权重更高
weights = np.array([1, 2, 1])
W = np.diag(weights)

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

13. 正则化:岭回归与Lasso

为防止过拟合,可以加入正则化项。岭回归(L2正则化)的解为:

x̂ = (AᵀA + λI)⁻¹Aᵀb

实现代码:

lambda_ = 0.1  # 正则化系数
I = np.eye(A.shape[1])
x_hat_ridge = np.linalg.inv(A.T @ A + lambda_ * I) @ A.T @ b
print("岭回归解:", x_hat_ridge)

14. 非线性问题的线性化

最小二乘法也可以用于某些非线性问题,通过适当的变换将其线性化。例如,对于指数模型y=aeᵇˣ:

  1. 取对数:ln y = ln a + bx
  2. 令y' = ln y, a' = ln a
  3. 转化为线性问题y' = a' + bx
# 假设我们有指数增长数据
x_exp = np.array([0, 1, 2, 3, 4])
y_exp = np.array([1.0, 2.7, 7.4, 20.1, 54.6])

# 线性化处理
A_exp = np.column_stack([x_exp, np.ones_like(x_exp)])
b_exp = np.log(y_exp)

# 求解线性问题
coeff_exp = np.linalg.inv(A_exp.T @ A_exp) @ A_exp.T @ b_exp
b, ln_a = coeff_exp
a = np.exp(ln_a)

print(f"拟合指数曲线: y = {a:.2f}e^({b:.2f}x)")

15. 统计性质与假设检验

最小二乘估计量具有良好的统计性质(在满足高斯-马尔可夫假设下):

  • 无偏性:E[x̂] = x
  • 最小方差:在所有线性无偏估计中方差最小

我们可以计算参数的标准误差:

# 计算残差
residuals = b - A @ x_hat
sigma_squared = np.sum(residuals**2) / (len(b) - A.shape[1])

# 参数协方差矩阵
cov_matrix = sigma_squared * np.linalg.inv(ATA)
std_errors = np.sqrt(np.diag(cov_matrix))

print("参数标准误差:", std_errors)

16. 多元线性回归的实现

最小二乘法自然扩展到多元情况。假设我们要拟合y = b + a₁x₁ + a₂x₂:

# 多元回归示例
X = np.array([[1, 2], 
              [2, 3],
              [3, 1],
              [4, 4]])
y = np.array([3, 5, 4, 8])

# 添加截距项
A_multi = np.column_stack([X, np.ones(len(X))])

# 计算最小二乘解
coefficients_multi = np.linalg.inv(A_multi.T @ A_multi) @ A_multi.T @ y
a1, a2, b = coefficients_multi

print(f"多元回归方程: y = {a1:.2f}x1 + {a2:.2f}x2 + {b:.2f}")

17. 模型评估指标

拟合完成后,我们需要评估模型质量。常用指标包括:

  • R²(决定系数):表示模型解释的方差比例
  • 调整R²:考虑自变量数量的修正
  • 均方误差(MSE):平均预测误差平方
# 计算R平方
y_pred = A @ x_hat
ss_total = np.sum((b - np.mean(b))**2)
ss_residual = np.sum((b - y_pred)**2)
r_squared = 1 - (ss_residual / ss_total)

print(f"R²: {r_squared:.4f}")

18. 交叉验证与模型选择

为避免过拟合,可以使用交叉验证评估模型性能:

from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LinearRegression

model = LinearRegression()
scores = cross_val_score(model, A[:,:-1], b, cv=3, scoring='r2')
print(f"交叉验证R²: {np.mean(scores):.3f} ± {np.std(scores):.3f}")

19. 在线学习与增量更新

对于流式数据,我们可以实现在线最小二乘法,避免每次都重新计算:

class OnlineLeastSquares:
    def __init__(self, n_features):
        self.ATA = np.zeros((n_features, n_features))
        self.ATb = np.zeros(n_features)
        self.n_features = n_features
        
    def update(self, x, y):
        x = np.array(x).reshape(1, -1)
        self.ATA += x.T @ x
        self.ATb += x.T * y
        
    def get_coefficients(self):
        return np.linalg.solve(self.ATA, self.ATb)

# 使用示例
ols = OnlineLeastSquares(n_features=2)
for xi, yi in zip(A[:,:-1], b):
    ols.update(xi, yi)
print("在线学习结果:", ols.get_coefficients())

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

最小二乘法是连接线性代数与机器学习的桥梁。理解它的数学本质和实现细节,将为你学习更复杂的机器学习算法打下坚实基础。当你使用scikit-learn的LinearRegression或TensorFlow/Keras的Dense层时,背后都是最小二乘法的思想在发挥作用。

Logo

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

更多推荐