视频加载失败

18.06 第 15 讲:投影到子空间

4086 字
20 分钟
18.06 第 15 讲:投影到子空间
  1. 118.06 第 1 讲:方程组的行图与列图
  2. 218.06 第 2 讲:用矩阵做消元
  3. 318.06 第 3 讲:矩阵乘法的四种看法与逆矩阵
  4. 418.06 第 4 讲:分解成 A = LU
  5. 518.06 第 5 讲:转置、置换与向量空间
  6. 618.06 第 6 讲:列空间与零空间
  7. 718.06 第 7 讲:解 Ax = 0,主变量与特解
  8. 818.06 第 8 讲:解 Ax = b,行最简形 R
  9. 918.06 第 9 讲:线性无关、基与维数
  10. 1018.06 第 10 讲:四个基本子空间
  11. 1118.06 第 11 讲:矩阵空间、秩 1 矩阵与小世界图
  12. 1218.06 第 12 讲:图、网络与关联矩阵
  13. 1318.06 第 13 讲:第一次测验复习
  14. 1418.06 第 14 讲:正交向量与正交子空间
  15. 1518.06 第 15 讲:投影到子空间本文
  16. 1618.06 第 16 讲:投影矩阵与最小二乘
  17. 1718.06 第 17 讲:正交矩阵与 Gram-Schmidt
  18. 1818.06 第 18 讲:行列式的性质
  19. 1918.06 第 19 讲:行列式公式与代数余子式
  20. 2018.06 第 20 讲:Cramer 法则、逆矩阵与体积
  21. 2118.06 第 21 讲:特征值与特征向量
  22. 2218.06 第 22 讲:对角化与矩阵的幂
  23. 2318.06 第 23 讲:微分方程与 exp(At)
  24. 2418.06 第 24 讲:Markov 矩阵与 Fourier 级数
  25. 2518.06 第 24b 讲:第二次测验复习
  26. 2618.06 第 25 讲:对称矩阵与正定性
  27. 2718.06 第 26 讲:复矩阵与快速 Fourier 变换
  28. 2818.06 第 27 讲:正定矩阵与极小值
  29. 2918.06 第 28 讲:相似矩阵与 Jordan 标准形
  30. 3018.06 第 29 讲:奇异值分解
  31. 3118.06 第 30 讲:线性变换及其矩阵
  32. 3218.06 第 31 讲:基变换与图像压缩
  33. 3318.06 第 32 讲:第三次测验复习
  34. 3418.06 第 33 讲:左逆、右逆与伪逆
  35. 3518.06 第 34 讲:期末复习

这一讲的核心是一个很朴素的问题:Ax=bA\mathbf{x} = \mathbf{b} 没有解的时候,AA 的列空间里离 b\mathbf{b} 最近的点在哪里。Strang 的答案是,找到那个点 p\mathbf{p},使得误差 e=b−p\mathbf{e} = \mathbf{b} - \mathbf{p} 垂直于列空间,由此得到 ATAx^=ATbA^\mathsf{T}A\hat{\mathbf{x}} = A^\mathsf{T}\mathbf{b}。我在他的三点例子上算了一遍:投影点到 b\mathbf{b} 的距离是 0.4082,我在平面里随机撒了 10 万个点,离 b\mathbf{b} 最近的一个也有 0.4085,没有一个能赢过它。

本讲速览

视频:Lecture 15: Projections onto subspaces(OCW 页面与字幕) 教材:《Introduction to Linear Algebra》第 6 版 §4.2 Projections onto Lines and Subspaces 前置:第 14 讲:正交向量与正交子空间 一句话:投影点 p=Ax^\mathbf{p} = A\hat{\mathbf{x}} 由“误差垂直于列空间”这一个条件决定,投影矩阵是 P=A(ATA)−1ATP = A(A^\mathsf{T}A)^{-1}A^\mathsf{T},它对称,而且 P2=PP^2 = P。

配合视频
  • 他先花了一整块黑板只讲投影到一条直线上,这一段是全讲的骨架,值得认真看。他问“b\mathbf{b} 翻倍怎么样,a\mathbf{a} 翻倍怎么样”的那几分钟也有用,本篇用代码算了一遍,看完可以对照。
  • “PP 是对称的,P2=PP^2 = P”那一段要认真听。他说这两条性质合起来就是投影矩阵的全部身份。
  • 他想把 (ATA)−1(A^\mathsf{T}A)^{-1} 拆成两个逆的乘积、又自己叫停的那一段很短,本篇有一个小实验,看一眼即可。
  • 最后几分钟是三个点的拟合问题,他只列出方程,没有解。数值例子在下一讲,本篇只在最后用代码做了一次预演。

先投影到一条直线上#

最近的点在哪里? Strang 从二维开始:一条过原点的直线,方向是 a\mathbf{a};直线外有一个向量 b\mathbf{b}。直线上离 b\mathbf{b} 最近的点记作 p\mathbf{p},连接 b\mathbf{b} 与 p\mathbf{p} 的误差向量 e=b−p\mathbf{e} = \mathbf{b} - \mathbf{p} 就是垂线。他提了一句,如果用三角函数,会出现 θ\theta、sin⁡θ\sin\theta、cos⁡θ\cos\theta,公式很难看;线性代数能让这些都不出现。

条件只有两个:p\mathbf{p} 在直线上,所以 p=xa\mathbf{p} = x\mathbf{a},xx 是个待求的数;误差垂直于 a\mathbf{a},所以

aT(b−xa)=0⟹x aTa=aTb⟹x=aTbaTa\mathbf{a}^\mathsf{T}(\mathbf{b} - x\mathbf{a}) = 0 \quad\Longrightarrow\quad x\,\mathbf{a}^\mathsf{T}\mathbf{a} = \mathbf{a}^\mathsf{T}\mathbf{b} \quad\Longrightarrow\quad x = \frac{\mathbf{a}^\mathsf{T}\mathbf{b}}{\mathbf{a}^\mathsf{T}\mathbf{a}}

aTa\mathbf{a}^\mathsf{T}\mathbf{a} 是一个数(a\mathbf{a} 的长度平方),可以直接除。于是投影是

p=a aTbaTa=aaTaTa⏟P b\mathbf{p} = \mathbf{a}\,\frac{\mathbf{a}^\mathsf{T}\mathbf{b}}{\mathbf{a}^\mathsf{T}\mathbf{a}} = \underbrace{\frac{\mathbf{a}\mathbf{a}^\mathsf{T}}{\mathbf{a}^\mathsf{T}\mathbf{a}}}_{P}\,\mathbf{b}

他强调的是括号放在哪里:把数 xx 放在 a\mathbf{a} 右边,括号换个位置,b\mathbf{b} 前面站着的就是一个矩阵。分子 aaT\mathbf{a}\mathbf{a}^\mathsf{T} 是列乘行,是一个 n×nn \times n 矩阵,分母仍然是一个数。我用他的“字母”配一组数字:a=(2,1)\mathbf{a} = (2, 1),b=(1,2)\mathbf{b} = (1, 2)。

import numpy as np
def main() -> None:
np.set_printoptions(precision=4, suppress=True)
a = np.array([2.0, 1.0])
b = np.array([1.0, 2.0])
# 一维:x = aᵀb / aᵀa,p = a x,误差 e = b - p 与 a 垂直
x = (a @ b) / (a @ a)
p = a * x
e = b - p
print("x =", x, " p =", p, " e =", e)
print("aᵀe 等于 0:", np.isclose(a @ e, 0.0))
# P = a aᵀ / aᵀa 是一个 2 x 2 矩阵,不是数
P = np.outer(a, a) / (a @ a)
print("P =")
print(P)
print("P @ b 等于 p:", np.allclose(P @ b, p))
# b 翻倍,投影翻倍;a 翻倍或变号,投影不变
for name, P_new, b_new in [
("2b", P, 2 * b),
("2a", np.outer(2 * a, 2 * a) / ((2 * a) @ (2 * a)), b),
("-a", np.outer(-a, -a) / ((-a) @ (-a)), b),
]:
print(name, "的投影:", P_new @ b_new)
# P 的三条性质
print("秩:", np.linalg.matrix_rank(P))
print("P.T 等于 P:", np.allclose(P.T, P))
print("P @ P 等于 P:", np.allclose(P @ P, P))
if __name__ == "__main__":
main()
x = 0.8 p = [1.6 0.8] e = [-0.6 1.2]
aᵀe 等于 0: True
P =
[[0.8 0.4]
[0.4 0.2]]
P @ b 等于 p: True
2b 的投影: [3.2 1.6]
2a 的投影: [1.6 0.8]
-a 的投影: [1.6 0.8]
秩: 1
P.T 等于 P: True
P @ P 等于 P: True

x=0.8x = 0.8,p=(1.6,0.8)\mathbf{p} = (1.6, 0.8),误差 e=(−0.6,1.2)\mathbf{e} = (-0.6, 1.2) 与 a\mathbf{a} 的点积为零。这一步的所有判断都用 np.isclose,因为 2×(−0.6)+1×1.22 \times (-0.6) + 1 \times 1.2 在浮点数里剩下一个 10−1610^{-16} 量级的尾巴,直接打印出来很难看。

三个“问题”的答案和他在黑板上说的一致:b\mathbf{b} 变成 2b2\mathbf{b},投影从 (1.6,0.8)(1.6, 0.8) 变成 (3.2,1.6)(3.2, 1.6),也翻倍;a\mathbf{a} 变成 2a2\mathbf{a} 或 −a-\mathbf{a},投影一点没变,因为直线没变,分子分母各多出来的因子 4 和 (−1)2(-1)^2 抵消了。

PP 的性质他列了三条,我也一并验证:

  • 列空间是那条直线,所以秩是 1。它就是“一列乘一行”,也就是第 11 讲里的秩 1 矩阵。
  • PT=PP^\mathsf{T} = P:转置 aaT\mathbf{a}\mathbf{a}^\mathsf{T} 还是 aaT\mathbf{a}\mathbf{a}^\mathsf{T},分母是个数,不受影响。
  • P2=PP^2 = P:投影一次已经落在直线上,再投影原地不动。

他说“对称,且平方等于自己”这两条合起来,就是投影矩阵的全部特征。我没有去查这个说法的严格表述,但这里的 PP 两条都满足,这个证据对我够用。

下面的图把这一节和后面的例子放在一起:左边就是这一节的数,e\mathbf{e} 与直线之间那个小方块是直角。

左:b = (1, 2) 投影到过 a = (2, 1) 的直线,p = (1.6, 0.8),误差 e 与直线垂直。右:三个点 (1, 1)、(2, 2)、(3, 2) 的纵坐标 b 投影到 A 的列空间,得到直线上的三个点 p
左:b = (1, 2) 投影到过 a = (2, 1) 的直线,p = (1.6, 0.8),误差 e 与直线垂直。右:三个点 (1, 1)、(2, 2)、(3, 2) 的纵坐标 b 投影到 A 的列空间,得到直线上的三个点 p

上面这张图由下面这个脚本生成(需要 matplotlib,在当前目录写出 SVG;图中的数据与前面的代码块相同):

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
def main() -> None:
matplotlib.use("Agg")
plt.rcParams["svg.hashsalt"] = "la1806"
fig = plt.figure(figsize=(10, 4))
# 左:投影到直线(与 block1 同一组数)
ax1 = fig.add_subplot(1, 2, 1)
a = np.array([2.0, 1.0])
b = np.array([1.0, 2.0])
p = a * (a @ b) / (a @ a)
e = b - p
t = np.linspace(-0.5, 3.0, 2)
ax1.plot(t * a[0], t * a[1], color="gray", lw=1, label="line through a")
ax1.annotate(
"",
xy=a,
xytext=(0, 0),
arrowprops=dict(arrowstyle="->", color="tab:blue"),
)
ax1.annotate(
"",
xy=b,
xytext=(0, 0),
arrowprops=dict(arrowstyle="->", color="tab:red"),
)
ax1.annotate(
"",
xy=p,
xytext=(0, 0),
arrowprops=dict(arrowstyle="->", color="tab:green", lw=2),
)
ax1.plot([p[0], b[0]], [p[1], b[1]], "k--", lw=1)
ax1.text(a[0] + 0.05, a[1] - 0.15, "a = (2, 1)", color="tab:blue")
ax1.text(b[0] - 0.55, b[1] + 0.08, "b = (1, 2)", color="tab:red")
ax1.text(p[0] + 0.05, p[1] - 0.2, "p = (1.6, 0.8)", color="tab:green")
ax1.text(0.9, 1.35, "e", fontsize=12)
s = 0.12
u = a / np.linalg.norm(a)
v = e / np.linalg.norm(e)
ax1.plot(
[p[0] - s * u[0], p[0] - s * u[0] + s * v[0], p[0] + s * v[0]],
[p[1] - s * u[1], p[1] - s * u[1] + s * v[1], p[1] + s * v[1]],
color="k",
lw=0.8,
)
ax1.set_xlim(-0.3, 3.0)
ax1.set_ylim(-0.3, 2.4)
ax1.set_aspect("equal")
ax1.set_title("projection onto a line")
# 右:三个点的拟合(与 block2 同一组数)
ax2 = fig.add_subplot(1, 2, 2)
A = np.array([[1.0, 1.0], [1.0, 2.0], [1.0, 3.0]])
bb = np.array([1.0, 2.0, 2.0])
xh = np.linalg.solve(A.T @ A, A.T @ bb)
pp = A @ xh
tt = A[:, 1]
tl = np.linspace(0.5, 3.5, 2)
ax2.plot(tl, xh[0] + xh[1] * tl, color="tab:green", lw=1.5)
for ti, bi, pi in zip(tt, bb, pp):
ax2.plot([ti, ti], [bi, pi], "k--", lw=1)
ax2.plot(tt, bb, "o", color="tab:red", label="b = (1, 2, 2)")
ax2.plot(tt, pp, "s", color="tab:green", label="p = A x_hat")
ax2.legend(loc="upper left")
ax2.set_xlabel("t")
ax2.set_ylabel("height")
ax2.set_xlim(0.5, 3.5)
ax2.set_ylim(0.5, 2.8)
ax2.set_title("b projected onto the column space of A")
fig.tight_layout()
fig.savefig(
"l15-projection.svg",
metadata={"Date": None},
)
if __name__ == "__main__":
main()

投影到一个子空间上#

直线换成平面,该怎么办? 他先说为什么要关心投影:Ax=bA\mathbf{x} = \mathbf{b} 的方程比未知数多,多半无解。AxA\mathbf{x} 永远落在 AA 的列空间里,而 b\mathbf{b} 大概率不在,所以退一步,解一个能解的问题:把 b\mathbf{b} 换成列空间里最近的那个向量 p\mathbf{p},解 Ax^=pA\hat{\mathbf{x}} = \mathbf{p}。x^\hat{\mathbf{x}} 头上那顶小帽子,是用来提醒自己它不是那个不存在的 x\mathbf{x}。

平面怎么描述?给两个线性无关的向量 a1\mathbf{a}_1、a2\mathbf{a}_2 当基,它们不必垂直,但必须无关。把它们作为列拼成 AA,这个平面就是 AA 的列空间,之后的公式都用 AA 写出来,两列还是 nn 列都不用改。

投影 p\mathbf{p} 是列的组合,p=Ax^\mathbf{p} = A\hat{\mathbf{x}},问题变成找 x^\hat{\mathbf{x}}。条件仍然是误差垂直:e=b−Ax^\mathbf{e} = \mathbf{b} - A\hat{\mathbf{x}} 垂直于平面,所以垂直于 a1\mathbf{a}_1,也垂直于 a2\mathbf{a}_2:

a1T(b−Ax^)=0a2T(b−Ax^)=0⟹AT(b−Ax^)=0\begin{aligned} \mathbf{a}_1^\mathsf{T}(\mathbf{b} - A\hat{\mathbf{x}}) &= 0 \\ \mathbf{a}_2^\mathsf{T}(\mathbf{b} - A\hat{\mathbf{x}}) &= 0 \end{aligned} \qquad\Longrightarrow\qquad A^\mathsf{T}(\mathbf{b} - A\hat{\mathbf{x}}) = \mathbf{0}

两个方程叠起来,恰好就是 ATA^\mathsf{T}(第一行是 a1T\mathbf{a}_1^\mathsf{T},第二行是 a2T\mathbf{a}_2^\mathsf{T})乘以误差等于零。一条直线时 AA 只有一列,这个方程退回到上一节的 aTe=0\mathbf{a}^\mathsf{T}\mathbf{e} = 0。

他接着做了一件我觉得很漂亮的事:把这个方程放回第 14 讲的四个子空间里。ATe=0A^\mathsf{T}\mathbf{e} = \mathbf{0} 说的是 e\mathbf{e} 在 ATA^\mathsf{T} 的零空间里,也就是左零空间,它和列空间正交。他在黑板上说,推导出来的方程恰好说出了他想要的几何,这很舒服。

整理方程,得到三个要记的公式:

ATAx^=ATb,x^=(ATA)−1ATb,p=Ax^,P=A(ATA)−1ATA^\mathsf{T}A\hat{\mathbf{x}} = A^\mathsf{T}\mathbf{b},\qquad \hat{\mathbf{x}} = (A^\mathsf{T}A)^{-1}A^\mathsf{T}\mathbf{b},\qquad \mathbf{p} = A\hat{\mathbf{x}},\qquad P = A(A^\mathsf{T}A)^{-1}A^\mathsf{T}

对照一维:aTa\mathbf{a}^\mathsf{T}\mathbf{a} 是个数,可以直接除;ATAA^\mathsf{T}A 是矩阵,只能写成逆。公式的形状没变。

下面我用他最后一个例子里的矩阵。三个点 (1,1)(1, 1)、(2,2)(2, 2)、(3,2)(3, 2),想让直线 b=C+Dtb = C + Dt 穿过它们,方程是

[111213][CD]=[122]\begin{bmatrix} 1 & 1 \\ 1 & 2 \\ 1 & 3 \end{bmatrix} \begin{bmatrix} C \\ D \end{bmatrix} = \begin{bmatrix} 1 \\ 2 \\ 2 \end{bmatrix}

三个方程两个未知数,无解。AA 的两列张成三维空间里的一个平面,b=(1,2,2)\mathbf{b} = (1, 2, 2) 在平面外,正好是用来练习投影的材料:

import numpy as np
def main() -> None:
np.set_printoptions(precision=4, suppress=True)
# 三个点 (1, 1)、(2, 2)、(3, 2),想解 C + D t = b 但解不了
A = np.array([[1.0, 1.0], [1.0, 2.0], [1.0, 3.0]])
b = np.array([1.0, 2.0, 2.0])
# 正规方程 AᵀA x = Aᵀb
x_hat = np.linalg.solve(A.T @ A, A.T @ b)
p = A @ x_hat
e = b - p
print("x_hat =", x_hat)
print("p =", p)
print("e =", e)
print("Aᵀe 等于 0:", np.allclose(A.T @ e, 0.0))
# 投影矩阵 P = A (AᵀA)⁻¹ Aᵀ
P = A @ np.linalg.inv(A.T @ A) @ A.T
print("P =")
print(P)
print("P @ b 等于 p:", np.allclose(P @ b, p))
print("秩:", np.linalg.matrix_rank(P), ",迹:", round(np.trace(P), 6))
print("P.T 等于 P:", np.allclose(P.T, P))
print("P @ P 等于 P:", np.allclose(P @ P, P))
print("P @ p 等于 p:", np.allclose(P @ p, p))
# e 在 Aᵀ 的零空间里:Aᵀ 是 2 x 3,零空间由最后一个右奇异向量张成
_, _, Vt = np.linalg.svd(A.T)
n = Vt[-1]
print("零空间基向量 n =", n)
print("e 平行于 n:", np.allclose(np.cross(e, n), 0))
# 与 lstsq 对照
x_lstsq, *_ = np.linalg.lstsq(A, b, rcond=None)
print("lstsq 的解一致:", np.allclose(x_hat, x_lstsq))
if __name__ == "__main__":
main()
x_hat = [0.6667 0.5 ]
p = [1.1667 1.6667 2.1667]
e = [-0.1667 0.3333 -0.1667]
Aᵀe 等于 0: True
P =
[[ 0.8333 0.3333 -0.1667]
[ 0.3333 0.3333 0.3333]
[-0.1667 0.3333 0.8333]]
P @ b 等于 p: True
秩: 2 ,迹: 2.0
P.T 等于 P: True
P @ P 等于 P: True
P @ p 等于 p: True
零空间基向量 n = [ 0.4082 -0.8165 0.4082]
e 平行于 n: True
lstsq 的解一致: True

读一下这份输出:

  • x^=(2/3,1/2)\hat{\mathbf{x}} = (2/3, 1/2),投影点 p=(7/6,5/3,13/6)\mathbf{p} = (7/6, 5/3, 13/6),误差 e=(−1/6,1/3,−1/6)\mathbf{e} = (-1/6, 1/3, -1/6)。ATe=0A^\mathsf{T}\mathbf{e} = \mathbf{0} 成立。
  • PP 是 3×33 \times 3 的,秩是 2(列空间是平面,二维),PT=PP^\mathsf{T} = P,P2=PP^2 = P 都成立。多补了一行 Pp=pP\mathbf{p} = \mathbf{p}:投影点已经在平面上,再投影不动。
  • 迹是 2.0,等于秩。我没有在字幕里看到他说这件事,这是我顺手打印的,下一节的随机矩阵里它也成立,我猜这是投影矩阵的一般性质(据我所知,投影矩阵的特征值只有 0 和 1,迹就是 1 的个数,也就是秩)。
  • e\mathbf{e} 的方向和 ATA^\mathsf{T} 的零空间基向量 n\mathbf{n} 平行,确认了 e\mathbf{e} 在左零空间里,和他在黑板上的话对得上。
  • np.linalg.lstsq 给出了同一个 x^\hat{\mathbf{x}}。它做的是同一件事,只是据我所知它内部用的是 SVD,不去解 ATAA^\mathsf{T}A(ATAA^\mathsf{T}A 的条件数是 AA 的平方,数值上不划算)。

x^=(2/3,1/2)\hat{\mathbf{x}} = (2/3, 1/2) 就是这三个点的最佳直线 b=23+12tb = \frac23 + \frac12 t 的系数。Strang 说下一讲才算数值例子,我这里提前把结果打印出来了(图右半边画的就是它):三个红点是 b\mathbf{b},绿方块是 p\mathbf{p},虚线是 e\mathbf{e} 的三个分量。

公式里藏着的几个问题#

为什么不能把逆拆开? 他在黑板上写完 P=A(ATA)−1ATP = A(A^\mathsf{T}A)^{-1}A^\mathsf{T} 之后,立刻动了一个念头:乘积的逆等于逆的乘积反过来,那 (ATA)−1=A−1(AT)−1(A^\mathsf{T}A)^{-1} = A^{-1}(A^\mathsf{T})^{-1},代进去全部抵消,P=IP = I。他的原话大意是,这件事要上录像了,我写在这里,你们别太认真看。(当场写了错的,然后自己叫停,我觉得这是整门课里很可爱的一段。)

错在哪里:AA 是 3×23 \times 2 的,不是方阵,没有逆。而 ATAA^\mathsf{T}A 是 2×22 \times 2 的方阵,有逆。反过来,如果 AA 本身是可逆方阵,列空间是整个空间,投影到整个空间当然是单位矩阵,这时拆开是合法的。这两件事我都让 NumPy 验证了。

另外两件事也一起验证:我在平面里随机撒点,看没有任何一个点离 b\mathbf{b} 更近;我又换一个 50×550 \times 5 的随机矩阵,看对称和幂等不是三阶小矩阵的巧合。

import numpy as np
def main() -> None:
np.set_printoptions(precision=4, suppress=True)
rng = np.random.default_rng(1806)
A = np.array([[1.0, 1.0], [1.0, 2.0], [1.0, 3.0]])
b = np.array([1.0, 2.0, 2.0])
P = A @ np.linalg.inv(A.T @ A) @ A.T
p = P @ b
dist = np.linalg.norm(b - p)
# 1. 平面里随机取 100000 个点,没有一个比 p 离 b 更近
coeffs = rng.standard_normal((100_000, 2)) * 2.0
others = coeffs @ A.T
dists = np.linalg.norm(others - b, axis=1)
print(f"p 到 b 的距离:{dist:.4f}")
print(f"随机点到 b 的最小距离:{dists.min():.4f}")
print("p 确实最近:", bool(np.all(dists >= dist - 1e-12)))
# 2. 想把 (AᵀA)⁻¹ 拆成 A⁻¹ (Aᵀ)⁻¹,A 不是方阵,拆不了
try:
np.linalg.inv(A)
except np.linalg.LinAlgError as err:
print("inv(A) 失败:", err)
# 3. A 是可逆方阵时,列空间是整个空间,P 就是单位矩阵
S = np.array([[2.0, -1.0, 0.0], [-1.0, 2.0, -1.0], [0.0, -1.0, 2.0]])
P_full = S @ np.linalg.inv(S.T @ S) @ S.T
print("S 可逆时 P 等于 I:", np.allclose(P_full, np.eye(3)))
# 4. 更高维:50 x 5 的随机 A,P 仍然是对称幂等,迹等于秩
M = rng.standard_normal((50, 5))
Q = M @ np.linalg.inv(M.T @ M) @ M.T
print("‖Q - Qᵀ‖ =", f"{np.linalg.norm(Q - Q.T):.2e}")
print("‖Q @ Q - Q‖ =", f"{np.linalg.norm(Q @ Q - Q):.2e}")
print("迹 =", round(float(np.trace(Q)), 6))
if __name__ == "__main__":
main()
p 到 b 的距离:0.4082
随机点到 b 的最小距离:0.4085
p 确实最近: True
inv(A) 失败: Last 2 dimensions of the array must be square
S 可逆时 P 等于 I: True
‖Q - Qᵀ‖ = 3.76e-16
‖Q @ Q - Q‖ = 6.52e-16
迹 = 5.0

四行输出,四个结论:

  • 平面上 10 万个随机点,最近的离 b\mathbf{b} 是 0.4085,投影点是 0.4082,投影点赢。差距只有 0.0003,是因为随机点已经撒得够密,恰好有一个落在投影点附近(这当然不说明投影“只比随机略好”,要是只撒 100 个点,差距会大得多)。
  • np.linalg.inv(A) 直接抛出 LinAlgError,原话是“最后两个维度必须是方阵”。
  • 可逆方阵 SS 的 PP 等于 II,和他说的一致。
  • 50×550 \times 5 的随机矩阵上,∥PT−P∥\|P^\mathsf{T} - P\| 是 3.76×10−163.76 \times 10^{-16},∥P2−P∥\|P^2 - P\| 是 6.52×10−166.52 \times 10^{-16},都在双精度的舍入误差量级,迹恰好是 5,等于列数,也等于秩。

为什么随机点永远赢不了? 这不是运气,是勾股定理。平面里任取一点 q\mathbf{q},b−q=e+(p−q)\mathbf{b} - \mathbf{q} = \mathbf{e} + (\mathbf{p} - \mathbf{q}),其中 e\mathbf{e} 垂直于平面,p−q\mathbf{p} - \mathbf{q} 在平面里,两者垂直,所以 ∥b−q∥2=∥e∥2+∥p−q∥2≥∥e∥2\|\mathbf{b} - \mathbf{q}\|^2 = \|\mathbf{e}\|^2 + \|\mathbf{p} - \mathbf{q}\|^2 \ge \|\mathbf{e}\|^2,等号只在 q=p\mathbf{q} = \mathbf{p} 时成立。用上面的数手算一遍:∥b∥2=1+4+4=9\|\mathbf{b}\|^2 = 1 + 4 + 4 = 9,∥p∥2=53/6\|\mathbf{p}\|^2 = 53/6,∥e∥2=1/6\|\mathbf{e}\|^2 = 1/6,两者之和恰好是 9,同样是勾股,1/61/6 开方就是 0.4082。这也解释了为什么一维、二维、nn 维的推导可以用同一个条件:只要误差垂直,剩下的都是勾股。

顺便一提一个类比:这像太阳在头顶正上方时的影子。影子是物体投到地面上的点,光线垂直于地面,这就是“误差垂直于子空间”。影子一旦落在地上,再来一次,它还是在原地,这就是 P2=PP^2 = P。(我只用这一个类比,再讲下去就要开始解释太阳的位置了。)

什么时候 ATAA^\mathsf{T}A 可逆#

公式里的逆什么时候存在? 他没有在这一讲停下来讨论,我顺手补一句。这个例子里 AA 的两列线性无关,ATA=[36614]A^\mathsf{T}A = \begin{bmatrix} 3 & 6 \\ 6 & 14 \end{bmatrix} 可逆,一切顺利。如果两列线性相关,ATAA^\mathsf{T}A 就奇异,x^\hat{\mathbf{x}} 不唯一。“ATAA^\mathsf{T}A 可逆当且仅当 AA 的列线性无关”这个结论在第 14 讲已经备好,这里是它第一次真正派上用场。

下一讲他说会把今天只写了字母的这些东西算成数,从三个点的拟合接着做下去,见 18.06 第 16 讲:投影矩阵与最小二乘。我猜他也不会再写“eight a’s in a row”那样的矩阵了。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
18.06 第 15 讲:投影到子空间
https://bfmhno3.github.io/posts/linear-algebra-15-projections/
作者
bfmhno3
发布于
2026-10-18
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
18.06 第 16 讲:投影矩阵与最小二乘
NoteMIT 18.06 第 16 讲的配套笔记:我用 Python 3.13 和 NumPy 2.4 把 (1, 1)、(2, 2)、(3, 2) 三个点的直线拟合解了一遍,正规方程给出 C = 2/3、D = 1/2,误差向量 (-1/6, 1/3, -1/6) 的平方长度只有 0.1667,而在 t = 0 处多加一个离群点,斜率就从 0.5 翻成 -1.1。
2
18.06 第 14 讲:正交向量与正交子空间
NoteMIT 18.06 第 14 讲的配套笔记:我用 Python 3.13 和 NumPy 2.4 在一个随机的 4 × 6、秩 3 的矩阵上检查行空间与零空间正交,两组基的点积最大只有 1.0e-16,又在 1000 个随机矩阵上验证了 AᵀA 与 A 的秩始终相同。
3
18.06 第 10 讲:四个基本子空间
NoteMIT 18.06 第 10 讲的配套笔记:我用 Python 3.13 和 NumPy 2.4 对 Strang 的 3 × 4 矩阵分别用消元和 SVD 求出四个子空间的基,维数是 2、2、2、1,再用 5 个随机造的不同形状矩阵验证 r、n - r、r、m - r 全部成立。
4
18.06 第 6 讲:列空间与零空间
NoteMIT 18.06 第 6 讲的配套笔记:我用 Python 3.13 和 NumPy 2.4 对 Strang 的 4 × 3 矩阵用最小二乘残差判断右端项在不在列空间,再用 SVD 求零空间,1000 个随机右端项里没有一个落在列空间里,而 Ax = b 的全部解是一条到原点距离 0.8165 的直线。
5
18.06 第 5 讲:转置、置换与向量空间
NoteMIT 18.06 第 5 讲的配套笔记:我用 Python 3.13 和 NumPy 2.4 数出 4 × 4 置换矩阵共 24 个、手写了带换行的 PA = LU,又在 1e-20 的小主元上看到不换行的解误差是 1.0,换行之后降到 0.0。
随机文章随机推荐

评论区

Profile Image of the Author
bfmhno3
Hello, I'm bfmhno3.
公告
本站曾使用 Jekyll + Minimal Mistakes 主题,现已迁移至 Astro + Firefly。博客内容未变,但文章链接有所调整,请自行查找需要的文章。
分类
标签
最新动态
站点统计
文章
123
分类
2
标签
239
总字数
753,253
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录