Skip to content

第 13 章 正交投影与最小二乘

学习目标

  • 掌握正交向量、正交基与标准正交基
  • 掌握 Gram-Schmidt 正交化的推导与步骤
  • 推导向子空间投影的公式与法方程
  • 理解最小二乘的几何意义:残差垂直于列空间
  • 用最小二乘做线性回归,并对比闭式解与梯度下降
  • 会用 NumPy 与 PyTorch 求解

13.1 正交与正交基:垂直的基最好用

第 4 章已经知道:两个非零向量垂直(正交),当且仅当它们的点积为 0。

正交基:一组基,如果两两正交,叫正交基;如果每个向量还都是单位向量,叫标准正交基

标准正交基的好处是坐标特别好算。设 是标准正交基,要表示

两边点乘 :

因为 ,中间所有交叉项全部消失。坐标 = 直接和基向量做点积,不用解方程组。这就是为什么「正交」在数值计算里这么宝贵。

13.2 Gram-Schmidt 正交化

给一组线性无关的向量 ,怎么把它们变成正交基?Gram-Schmidt 正交化的做法是逐个处理:每处理一个新向量,就把它减去在已有方向上的投影,剩下的部分一定与已有方向垂直。

Gram-Schmidt

第 1 步:

python
import matplotlib.pyplot as plt
from matplotlib import font_manager
for f in font_manager.findSystemFonts():
    if any(k in f for k in ("NotoSansCJK", "NotoSansSC", "wqy", "SimHei", "msyh", "PingFang")):
        font_manager.fontManager.addfont(f)
plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "WenQuanYi Zen Hei", "SimHei", "Microsoft YaHei", "PingFang SC"]
plt.rcParams["axes.unicode_minus"] = False

import numpy as np

v1 = np.array([1.0, 1.0])
v2 = np.array([1.0, 2.0])
proj = (np.dot(v2, v1) / np.dot(v1, v1)) * v1
u2 = v2 - proj

fig, ax = plt.subplots(figsize=(6, 6))
ax.axhline(0, color="black", linewidth=1)
ax.axvline(0, color="black", linewidth=1)
ax.grid(True, linestyle=":", alpha=0.5)
ax.set_xlim(-1, 2)
ax.set_ylim(-0.5, 2.5)
ax.set_aspect("equal")

for v, c, label in [(v1, "tab:red", "u1 = v1"), (v2, "tab:blue", "v2"),
                    (proj, "tab:orange", "投影"), (u2, "tab:green", "u2")]:
    ax.quiver(0, 0, v[0], v[1], angles="xy", scale_units="xy", scale=1,
              color=c, width=0.018)
    ax.annotate(label, v / 2, textcoords="offset points", xytext=(8, 6), color=c, fontsize=12)

# 投影连线:v2 终点到 u2 终点(这条线垂直于 u1)
ax.plot([v2[0], u2[0]], [v2[1], u2[1]], color="gray", linestyle="--")
ax.annotate("垂直", (u2[0] + (v2[0] - u2[0]) / 2, u2[1] + (v2[1] - u2[1]) / 2),
            textcoords="offset points", xytext=(-24, -6), color="gray")
ax.set_title("u2 = v2 − proj_u1(v2) ⟂ u1")
fig.savefig("book/public/figs/fig13-gram_schmidt.png", dpi=120, bbox_inches="tight")
plt.close(fig)

第 2 步:从 里减去它在 上的投影(第 4 章的公式):

为什么 ?直接算点积:

第 3 步:从 里减去它在 两个方向上的投影:

以此类推:每个新向量减去在所有已有正交向量上的投影。

例:

验证:

python
import numpy as np

def gram_schmidt(vectors):
    """把线性无关的向量组正交化(不归一化)"""
    vs = [np.array(v, dtype=float) for v in vectors]
    us = []
    for v in vs:
        u = v.copy()
        for prev in us:
            u -= (np.dot(v, prev) / np.dot(prev, prev)) * prev
        us.append(u)
    return us

u1, u2 = gram_schmidt([(1, 1), (1, 2)])
print("u1 =", u1)
print("u2 =", u2)
print("u1 · u2 =", np.dot(u1, u2))

运行输出:

text
u1 = [1. 1.]
u2 = [-0.5  0.5]
u1 · u2 = 0.0

13.3 向子空间投影:找「最近的」向量

第 4 章投影到一条直线;现在投影到子空间。给定矩阵 (列向量张成子空间 )和向量 ,在 里找离 最近的向量 ,叫 上的正交投影

投影到子空间

因为 ,它可以写成 。正交投影的关键性质:误差向量 垂直于 ,即垂直于 的每一列 :

python
import matplotlib.pyplot as plt
from matplotlib import font_manager
for f in font_manager.findSystemFonts():
    if any(k in f for k in ("NotoSansCJK", "NotoSansSC", "wqy", "SimHei", "msyh", "PingFang")):
        font_manager.fontManager.addfont(f)
plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "WenQuanYi Zen Hei", "SimHei", "Microsoft YaHei", "PingFang SC"]
plt.rcParams["axes.unicode_minus"] = False

import numpy as np

A = np.array([[1.0, 0.0],
              [0.0, 1.0],
              [0.0, 0.0]])   # 列空间 = xy 平面
b = np.array([1.0, 2.0, 1.5])
p = A @ np.linalg.lstsq(A, b, rcond=None)[0]

fig = plt.figure(figsize=(6.5, 6))
ax = fig.add_subplot(111, projection="3d")

# 列空间平面 z=0
xx, yy = np.meshgrid(np.linspace(-0.5, 2.5, 10), np.linspace(-0.5, 2.5, 10))
ax.plot_surface(xx, yy, np.zeros_like(xx), alpha=0.25, color="tab:blue")

ax.quiver(0, 0, 0, b[0], b[1], b[2], color="tab:red", arrow_length_ratio=0.1)
ax.quiver(0, 0, 0, p[0], p[1], p[2], color="tab:green", arrow_length_ratio=0.1)
ax.plot([b[0], p[0]], [b[1], p[1]], [b[2], p[2]], color="gray", linestyle="--")

ax.text(*b, " b", color="tab:red")
ax.text(*p, " p", color="tab:green")
ax.text(p[0] / 2, p[1] / 2, p[2] / 2 + 0.8, "残差 ⟂ 平面", color="gray")
ax.set_xlim(0, 2.5)
ax.set_ylim(0, 2.5)
ax.set_zlim(0, 2)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("z")
ax.set_title("b 在列空间上的投影 p,残差垂直于列空间")
fig.savefig("book/public/figs/fig13-subspace_proj.png", dpi=120, bbox_inches="tight")
plt.close(fig)

把这 个方程竖着摞起来,就是

展开:

这叫法方程(normal equation)。当 的列线性无关时, 可逆,解出

投影矩阵:把任意 垂直投影到

13.4 最小二乘:解「无解」的方程组

线性回归遇到的情形:方程比未知数多(超定方程组),通常没有精确解。比如三个点拟合直线,就有 3 个方程、2 个未知数,一般无解。

最小二乘解:没有精确解时,退而求其次——找 使

最小。而「离 最近的 」正是 13.3 的正交投影,所以最小二乘解就是法方程的解。这就是「最小二乘 = 投影」的完整闭环:残差 垂直于列空间时,残差长度达到最小。

例:拟合直线

数据点 。把「」写成矩阵:

第一个方程 、第二个 、第三个 ——把第一个和第二个代入第三个会得到 ,无解。上法方程:

拟合直线:

线性回归

python
import matplotlib.pyplot as plt
from matplotlib import font_manager
for f in font_manager.findSystemFonts():
    if any(k in f for k in ("NotoSansCJK", "NotoSansSC", "wqy", "SimHei", "msyh", "PingFang")):
        font_manager.fontManager.addfont(f)
plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "WenQuanYi Zen Hei", "SimHei", "Microsoft YaHei", "PingFang SC"]
plt.rcParams["axes.unicode_minus"] = False

import numpy as np

xs = np.array([1.0, 2.0, 3.0])
ys = np.array([2.0, 3.0, 5.0])
A = np.column_stack([np.ones_like(xs), xs])
coef = np.linalg.lstsq(A, ys, rcond=None)[0]
a, b = coef

fig, ax = plt.subplots(figsize=(6, 4.5))
ax.scatter(xs, ys, color="tab:red", zorder=3, label="数据点")
t = np.linspace(0.5, 3.5, 100)
ax.plot(t, a + b * t, color="tab:blue", label=f"y = {a:.2f} + {b:.2f}x")

# 残差竖线
for x, y in zip(xs, ys):
    ax.plot([x, x], [y, a + b * x], color="gray", linestyle="--", linewidth=1)

ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_title("最小二乘拟合直线")
ax.legend()
ax.grid(True, linestyle=":", alpha=0.5)
fig.savefig("book/public/figs/fig13-regression.png", dpi=120, bbox_inches="tight")
plt.close(fig)
python
import numpy as np

A = np.array([[1.0, 1.0],
              [1.0, 2.0],
              [1.0, 3.0]])
b = np.array([2.0, 3.0, 5.0])

# 法方程
AtA = A.T @ A
Atb = A.T @ b
x_hat = np.linalg.solve(AtA, Atb)
print("法方程解 a, b =", x_hat)

# 库的通用最小二乘
x_lstsq, *_ = np.linalg.lstsq(A, b, rcond=None)
print("np.linalg.lstsq =", x_lstsq)

# 预测与残差
p = A @ x_hat
print("预测值:", p)
print("残差:", b - p)
print("残差平方和:", np.sum((b - p) ** 2))

运行输出:

text
法方程解 a, b = [0.33333333 1.5       ]
np.linalg.lstsq = [0.33333333 1.5       ]
预测值: [1.83333333 3.33333333 4.83333333]
残差: [ 0.16666667 -0.33333333  0.16666667]
残差平方和: 0.16666666666666688

13.5 梯度下降:让计算机自己找最小值

法方程是闭式解——一步算完。但数据量大、特征多时,算 太贵,更常用的是梯度下降:从一个初始值出发,沿损失函数的「下坡方向」反复走小步。

损失函数取残差平方和:

求偏导(链式法则):

更新规则:,其中 是学习率。用 PyTorch 的自动求导做,连手推梯度都省了:

python
import torch

X = torch.tensor([1.0, 2.0, 3.0])
y = torch.tensor([2.0, 3.0, 5.0])

# 参数 a、b,requires_grad=True 让 torch 记录计算图
a = torch.tensor(0.0, requires_grad=True)
b = torch.tensor(0.0, requires_grad=True)

lr = 0.05
for step in range(300):
    # 前向:预测与损失
    pred = a + b * X
    loss = ((pred - y) ** 2).sum()
    # 反向:自动计算梯度
    loss.backward()
    with torch.no_grad():
        a -= lr * a.grad
        b -= lr * b.grad
        a.grad.zero_()
        b.grad.zero_()

print(f"梯度下降:a = {a.item():.4f}, b = {b.item():.4f}")
print("闭式解:  a = 0.3333, b = 1.5000")

运行输出:

text
梯度下降:a = 0.3333, b = 1.5000
闭式解:  a = 0.3333, b = 1.5000

两种方法殊途同归。闭式解靠线性代数一次到位,梯度下降靠迭代逼近——后者就是神经网络训练的基本动作,你已经在亲手做「深度学习的最小二乘」了。

动手实践

  1. 验证 是标准正交基,并写出 在它下的坐标。
  2. 用 Gram-Schmidt 把 正交化,验证结果互相垂直。
  3. 对数据点 拟合直线 :写出 、法方程,求解并画图。
  4. np.linalg.lstsq 拟合一条抛物线 (数据自拟),并解释为什么有 3 个参数。
  5. 调整 13.5 的学习率(如 0.001、1.0),观察收敛速度与震荡,用文字总结学习率的作用。

常见错误

错误写法/理解原因
投影公式写成 投影方向是分母对应向量的模平方:投影到 方向要除以
认为超定方程组一定无解不一定:如果数据恰好共线(在列空间里)就有精确解;通常才是无解
最小二乘与「误差为零」混淆最小二乘最小化误差平方和,通常误差不为零,只是「尽量小」
法方程写反,解 正确是 ,乘的顺序不能乱
梯度下降不置零梯度backward() 会累加梯度,每步之后必须 grad.zero_()

章末练习

基础

  1. 判断下列向量对是否正交:;
  2. 投影到 方向,写出投影向量。
  3. np.linalg.lstsq 拟合数据 的直线

提高

  1. 用 Gram-Schmidt 把 正交化,并验证两两正交。
  2. 推导法方程:写出「残差垂直于每一列」的式子,说明为什么等价于

挑战

  1. 证明投影矩阵 满足 (投影两次等于投影一次)与
  2. 三个点 ,如果强行用「任意两点求直线」会得到 3 条不同的直线;比较它们与最小二乘直线,讨论哪个更合理。

章末自测

  1. 两个非零向量正交,当且仅当?
    • A. 点积为 1
    • B. 点积为 0
    • C. 长度相等
    • D. 平行
  2. 标准正交基中的向量满足?
    • A. 两两正交且都是单位向量
    • B. 只两两正交
    • C. 只都是单位向量
    • D. 线性相关
  3. Gram-Schmidt 第 2 步是?
    • A.
    • B.
    • C.
    • D.
  4. 法方程是?
    • A.
    • B.
    • C.
    • D.
  5. 最小二乘的几何意义是?
    • A. 残差垂直于列空间
    • B. 残差等于零
    • C. 残差平行于列空间
    • D. 残差长度最大
  6. 3 个点拟合直线 ,矩阵 的形状是?
    • A.
    • B.
    • C.
    • D.
  7. 投影矩阵 满足?
    • A.
    • B.
    • C.
    • D.
  8. 梯度下降中 loss.backward() 之后,参数梯度会?
    • A. 自动清零
    • B. 累积到已有梯度上
    • C. 消失
    • D. 变成学习率
  9. 法方程要求 可逆,条件是?
    • A. 的列线性无关
    • B. 是方阵
    • C. 可逆
    • D. 对称
  10. 闭式解与梯度下降的关系是?
    • A. 完全无关
    • B. 一个精确一步到位,一个迭代逼近,结果一致
    • C. 梯度下降更精确
    • D. 闭式解只能用于非线性