第 13 章 正交投影与最小二乘
学习目标
- 掌握正交向量、正交基与标准正交基
- 掌握 Gram-Schmidt 正交化的推导与步骤
- 推导向子空间投影的公式与法方程
- 理解最小二乘的几何意义:残差垂直于列空间
- 用最小二乘做线性回归,并对比闭式解与梯度下降
- 会用 NumPy 与 PyTorch 求解
13.1 正交与正交基:垂直的基最好用
第 4 章已经知道:两个非零向量垂直(正交),当且仅当它们的点积为 0。
正交基:一组基,如果两两正交,叫正交基;如果每个向量还都是单位向量,叫标准正交基。
标准正交基的好处是坐标特别好算。设 是标准正交基,要表示
两边点乘 :
因为 、,中间所有交叉项全部消失。坐标 = 直接和基向量做点积,不用解方程组。这就是为什么「正交」在数值计算里这么宝贵。
13.2 Gram-Schmidt 正交化
给一组线性无关的向量 ,怎么把它们变成正交基?Gram-Schmidt 正交化的做法是逐个处理:每处理一个新向量,就把它减去在已有方向上的投影,剩下的部分一定与已有方向垂直。

第 1 步:。
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 步:从 里减去它在 、 两个方向上的投影:
以此类推:每个新向量减去在所有已有正交向量上的投影。
例:、。
验证: ✓
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))运行输出:
u1 = [1. 1.]
u2 = [-0.5 0.5]
u1 · u2 = 0.013.3 向子空间投影:找「最近的」向量
第 4 章投影到一条直线;现在投影到子空间。给定矩阵 (列向量张成子空间 )和向量 ,在 里找离 最近的向量 ,叫 在 上的正交投影。

因为 ,它可以写成 。正交投影的关键性质:误差向量 垂直于 ,即垂直于 的每一列 :
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 的正交投影,所以最小二乘解就是法方程的解。这就是「最小二乘 = 投影」的完整闭环:残差 垂直于列空间时,残差长度达到最小。
例:拟合直线
数据点 、、。把「」写成矩阵:
第一个方程 、第二个 、第三个 ——把第一个和第二个代入第三个会得到 ,无解。上法方程:
拟合直线:。

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)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))运行输出:
法方程解 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.1666666666666668813.5 梯度下降:让计算机自己找最小值
法方程是闭式解——一步算完。但数据量大、特征多时,算 太贵,更常用的是梯度下降:从一个初始值出发,沿损失函数的「下坡方向」反复走小步。
损失函数取残差平方和:
对 、 求偏导(链式法则):
更新规则:,其中 是学习率。用 PyTorch 的自动求导做,连手推梯度都省了:
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")运行输出:
梯度下降:a = 0.3333, b = 1.5000
闭式解: a = 0.3333, b = 1.5000两种方法殊途同归。闭式解靠线性代数一次到位,梯度下降靠迭代逼近——后者就是神经网络训练的基本动作,你已经在亲手做「深度学习的最小二乘」了。
动手实践
- 验证 是标准正交基,并写出 在它下的坐标。
- 用 Gram-Schmidt 把 、 正交化,验证结果互相垂直。
- 对数据点 、、 拟合直线 :写出 、、法方程,求解并画图。
- 用
np.linalg.lstsq拟合一条抛物线 (数据自拟),并解释为什么有 3 个参数。 - 调整 13.5 的学习率(如 0.001、1.0),观察收敛速度与震荡,用文字总结学习率的作用。
常见错误
| 错误写法/理解 | 原因 |
|---|---|
| 投影公式写成 | 投影方向是分母对应向量的模平方:投影到 方向要除以 |
| 认为超定方程组一定无解 | 不一定:如果数据恰好共线(在列空间里)就有精确解;通常才是无解 |
| 最小二乘与「误差为零」混淆 | 最小二乘最小化误差平方和,通常误差不为零,只是「尽量小」 |
| 法方程写反,解 | 正确是 ,乘的顺序不能乱 |
| 梯度下降不置零梯度 | backward() 会累加梯度,每步之后必须 grad.zero_() |
章末练习
基础
- 判断下列向量对是否正交: 与 ; 与 。
- 把 投影到 方向,写出投影向量。
- 用
np.linalg.lstsq拟合数据 、、 的直线 。
提高
- 用 Gram-Schmidt 把 、、 正交化,并验证两两正交。
- 推导法方程:写出「残差垂直于每一列」的式子,说明为什么等价于 。
挑战
- 证明投影矩阵 满足 (投影两次等于投影一次)与 。
- 三个点 、、,如果强行用「任意两点求直线」会得到 3 条不同的直线;比较它们与最小二乘直线,讨论哪个更合理。
章末自测
- 两个非零向量正交,当且仅当?
- A. 点积为 1
- B. 点积为 0
- C. 长度相等
- D. 平行
- 标准正交基中的向量满足?
- A. 两两正交且都是单位向量
- B. 只两两正交
- C. 只都是单位向量
- D. 线性相关
- Gram-Schmidt 第 2 步是?
- A.
- B.
- C.
- D.
- 法方程是?
- A.
- B.
- C.
- D.
- 最小二乘的几何意义是?
- A. 残差垂直于列空间
- B. 残差等于零
- C. 残差平行于列空间
- D. 残差长度最大
- 3 个点拟合直线 ,矩阵 的形状是?
- A.
- B.
- C.
- D.
- 投影矩阵 满足?
- A.
- B.
- C.
- D.
- 梯度下降中
loss.backward()之后,参数梯度会?- A. 自动清零
- B. 累积到已有梯度上
- C. 消失
- D. 变成学习率
- 法方程要求 可逆,条件是?
- A. 的列线性无关
- B. 是方阵
- C. 可逆
- D. 对称
- 闭式解与梯度下降的关系是?
- A. 完全无关
- B. 一个精确一步到位,一个迭代逼近,结果一致
- C. 梯度下降更精确
- D. 闭式解只能用于非线性
