用Python和NumPy手把手实现最小二乘法:从投影算子到线性回归实战

在数据科学和机器学习领域,线性回归是最基础也最重要的算法之一。但你是否曾好奇过,为什么最小二乘法能够找到最优拟合直线?背后的数学原理是什么?本文将带你从投影算子的几何视角出发,用Python和NumPy一步步实现最小二乘法,并应用于实际的线性回归问题。

1. 投影算子:最小二乘法的几何核心

想象一下,你站在一个黑暗的房间里,手电筒的光束照射在墙上形成一个光斑。这个光斑就是你的手在墙上的"投影"。在数学中,投影算子扮演着类似的角色——它将高维空间中的向量"投射"到低维子空间上。

投影算子最关键的数学性质是幂等性:无论投影多少次,结果都不会改变。用公式表示就是P²=P。这个性质保证了我们不会因为重复计算而得到不同的结果。

在Python中,我们可以用NumPy来验证这个性质:

import numpy as np

# 定义一个简单的投影矩阵:将向量投影到x轴上
P = np.array([[1, 0], 
              [0, 0]])

# 验证幂等性
print("P²是否等于P?", np.allclose(P @ P, P))  # 输出True

投影算子分为两类:

  • 正交投影:投影方向与子空间垂直(如正午阳光直射地面)
  • 斜投影:投影方向与子空间不垂直(如夕阳斜照)

最小二乘法使用的正是正交投影,因为它能保证我们找到的拟合直线与数据点的垂直距离(误差)最小。

2. 构建投影矩阵:从理论到代码实现

要构建一个投影矩阵,我们需要先理解它的数学表达式。对于一个矩阵A,它的投影矩阵P_A可以表示为:

P_A = A(AᵀA)⁻¹Aᵀ

这个公式看起来复杂,但我们可以分步实现它:

def compute_projection_matrix(A):
    """计算矩阵A的投影矩阵"""
    # 计算A的转置
    A_transpose = A.T
    
    # 计算AᵀA
    A_transpose_A = A_transpose @ A
    
    # 计算(AᵀA)的逆矩阵
    # 注意:实际应用中应该添加对奇异矩阵的检查
    inv_A_transpose_A = np.linalg.inv(A_transpose_A)
    
    # 计算完整的投影矩阵
    P = A @ inv_A_transpose_A @ A_transpose
    
    return P

让我们用一个简单的例子测试这个函数:

# 定义一个简单的矩阵A
A = np.array([[1, 1],
              [1, 2],
              [1, 3]])

# 计算投影矩阵
P = compute_projection_matrix(A)
print("投影矩阵P:\n", P)

# 验证幂等性
print("P是否满足幂等性?", np.allclose(P @ P, P))

注意:在实际应用中,直接计算逆矩阵可能会遇到数值不稳定的问题。更稳健的做法是使用奇异值分解(SVD)或QR分解等数值方法。

3. 最小二乘法实战:拟合最佳直线

现在,我们有了投影矩阵这个强大工具,可以开始解决实际问题了。假设我们有以下数据点,想要找到最佳拟合直线:

# 样本数据
x = np.array([1, 2, 3, 4, 5])  # 输入特征
y = np.array([2, 3, 5, 7, 8])  # 目标值

# 可视化数据点
import matplotlib.pyplot as plt
plt.scatter(x, y, color='red', label='Data points')
plt.xlabel('x')
plt.ylabel('y')
plt.title('原始数据点')
plt.legend()
plt.show()

3.1 构造设计矩阵A

在简单线性回归中,我们假设y = mx + b。为了使用矩阵运算,我们需要构造设计矩阵A:

# 构造设计矩阵A
# 第一列全1对应截距b,第二列是x值对应斜率m
A = np.column_stack([np.ones_like(x), x])
print("设计矩阵A:\n", A)

3.2 计算最优参数

根据最小二乘法理论,最优参数可以通过以下公式计算:

θ = (AᵀA)⁻¹Aᵀy

其中θ = [b, m]ᵀ

# 计算最优参数
A_transpose = A.T
theta = np.linalg.inv(A_transpose @ A) @ A_transpose @ y
b, m = theta

print(f"拟合直线方程: y = {m:.2f}x + {b:.2f}")

3.3 可视化拟合结果

# 生成拟合直线的点
x_fit = np.linspace(0, 6, 100)
y_fit = m * x_fit + b

# 绘制结果
plt.scatter(x, y, color='red', label='Data points')
plt.plot(x_fit, y_fit, label=f'拟合直线: y = {m:.2f}x + {b:.2f}')
plt.xlabel('x')
plt.ylabel('y')
plt.title('最小二乘法线性拟合')
plt.legend()
plt.show()

4. 与scikit-learn的对比验证

为了验证我们的实现是否正确,我们可以与scikit-learn的LinearRegression进行对比:

from sklearn.linear_model import LinearRegression

# 使用scikit-learn进行拟合
model = LinearRegression()
model.fit(x.reshape(-1, 1), y)

# 比较结果
print("\n对比结果:")
print(f"我们的实现 - 斜率: {m:.6f}, 截距: {b:.6f}")
print(f"scikit-learn - 斜率: {model.coef_[0]:.6f}, 截距: {model.intercept_:.6f}")
print("差异是否显著:", 
      np.allclose([m, b], [model.coef_[0], model.intercept_]))

5. 深入理解:投影视角下的最小二乘法

为什么投影算子能解决最小二乘问题?关键在于几何解释:

  1. 设计矩阵A的列空间定义了所有可能的拟合直线
  2. 观测值y通常不在这个列空间中
  3. 我们需要找到列空间中最接近y的点,这就是y在列空间上的投影
  4. 这个投影点对应的参数θ就是最优解

数学上,我们是在最小化残差的平方和:

min ||Aθ - y||²

通过投影算子,我们可以直接找到这个最小化问题的解。

# 计算投影后的y值
y_proj = A @ theta

# 可视化投影过程
fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')

# 为简单起见,我们只展示前三个数据点
for i in range(3):
    ax.plot([x[i], x[i]], [y[i], y_proj[i]], [0, 0], 'r--')
    ax.scatter(x[i], y[i], 0, c='r', marker='o')
    ax.scatter(x[i], y_proj[i], 0, c='b', marker='^')

ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title('最小二乘法的投影视角')
plt.show()

6. 处理实际问题:添加正则化项

在实际应用中,我们经常会遇到设计矩阵A条件数较差的情况(比如特征高度相关)。这时可以通过添加L2正则化(岭回归)来稳定解:

def ridge_regression(A, y, alpha=0.1):
    """带L2正则化的最小二乘法"""
    A_transpose = A.T
    I = np.eye(A.shape[1])  # 单位矩阵
    theta = np.linalg.inv(A_transpose @ A + alpha * I) @ A_transpose @ y
    return theta

# 尝试使用岭回归
theta_ridge = ridge_regression(A, y)
b_ridge, m_ridge = theta_ridge

print("\n岭回归结果:")
print(f"斜率: {m_ridge:.2f}, 截距: {b_ridge:.2f}")

7. 性能优化:使用QR分解

对于大规模问题,直接计算逆矩阵效率低下。我们可以使用QR分解来提高计算效率和数值稳定性:

def lstsq_qr(A, y):
    """使用QR分解求解最小二乘问题"""
    Q, R = np.linalg.qr(A)
    theta = np.linalg.solve(R, Q.T @ y)
    return theta

# 使用QR分解求解
theta_qr = lstsq_qr(A, y)
b_qr, m_qr = theta_qr

print("\nQR分解求解结果:")
print(f"斜率: {m_qr:.6f}, 截距: {b_qr:.6f}")
print("与传统方法差异:", np.allclose(theta, theta_qr))

8. 多元线性回归扩展

最小二乘法同样适用于多元线性回归。假设我们现在有二维特征:

# 生成样本数据
np.random.seed(42)
x1 = np.random.rand(20) * 10
x2 = np.random.rand(20) * 5
y = 3 * x1 + 2 * x2 + np.random.randn(20)  # 添加噪声

# 构造设计矩阵
A_multi = np.column_stack([np.ones_like(x1), x1, x2])

# 求解多元回归参数
theta_multi = np.linalg.inv(A_multi.T @ A_multi) @ A_multi.T @ y
print("\n多元回归系数:", theta_multi)

9. 模型评估:R²分数计算

一个好的拟合需要量化评估。R²分数衡量了模型解释数据变异的能力:

def r2_score(y_true, y_pred):
    """计算R²分数"""
    ss_res = np.sum((y_true - y_pred) ** 2)
    ss_tot = np.sum((y_true - np.mean(y_true)) ** 2)
    return 1 - (ss_res / ss_tot)

# 计算简单线性回归的R²
y_pred = A @ theta
print("\n模型R²分数:", r2_score(y, y_pred))

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

在实际项目中应用最小二乘法时,有几个关键点需要考虑:

  1. 特征缩放:当特征量纲差异大时,应先进行标准化

    from sklearn.preprocessing import StandardScaler
    
    scaler = StandardScaler()
    x_scaled = scaler.fit_transform(x.reshape(-1, 1))
    
  2. 异常值处理:最小二乘法对异常值敏感,可使用Huber损失等鲁棒方法

  3. 多重共线性检查:当特征高度相关时,考虑使用正则化或主成分分析

  4. 模型假设验证:检查残差是否满足正态性、同方差性等假设

# 残差分析示例
residuals = y - y_pred
plt.scatter(y_pred, residuals)
plt.axhline(y=0, color='r', linestyle='--')
plt.xlabel('预测值')
plt.ylabel('残差')
plt.title('残差图')
plt.show()

从投影算子的视角理解最小二乘法,不仅让我们看到了优美的数学结构,更重要的是掌握了解决实际问题的强大工具。通过NumPy的实现,我们验证了理论的正确性,也获得了灵活应用的能力。

Logo

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

更多推荐