18.06 第 24 讲:Markov 矩阵与 Fourier 级数
- 118.06 第 1 讲:方程组的行图与列图
- 218.06 第 2 讲:用矩阵做消元
- 318.06 第 3 讲:矩阵乘法的四种看法与逆矩阵
- 418.06 第 4 讲:分解成 A = LU
- 518.06 第 5 讲:转置、置换与向量空间
- 618.06 第 6 讲:列空间与零空间
- 718.06 第 7 讲:解 Ax = 0,主变量与特解
- 818.06 第 8 讲:解 Ax = b,行最简形 R
- 918.06 第 9 讲:线性无关、基与维数
- 1018.06 第 10 讲:四个基本子空间
- 1118.06 第 11 讲:矩阵空间、秩 1 矩阵与小世界图
- 1218.06 第 12 讲:图、网络与关联矩阵
- 1318.06 第 13 讲:第一次测验复习
- 1418.06 第 14 讲:正交向量与正交子空间
- 1518.06 第 15 讲:投影到子空间
- 1618.06 第 16 讲:投影矩阵与最小二乘
- 1718.06 第 17 讲:正交矩阵与 Gram-Schmidt
- 1818.06 第 18 讲:行列式的性质
- 1918.06 第 19 讲:行列式公式与代数余子式
- 2018.06 第 20 讲:Cramer 法则、逆矩阵与体积
- 2118.06 第 21 讲:特征值与特征向量
- 2218.06 第 22 讲:对角化与矩阵的幂
- 2318.06 第 23 讲:微分方程与 exp(At)
- 2418.06 第 24 讲:Markov 矩阵与 Fourier 级数本文
- 2518.06 第 24b 讲:第二次测验复习
- 2618.06 第 25 讲:对称矩阵与正定性
- 2718.06 第 26 讲:复矩阵与快速 Fourier 变换
- 2818.06 第 27 讲:正定矩阵与极小值
- 2918.06 第 28 讲:相似矩阵与 Jordan 标准形
- 3018.06 第 29 讲:奇异值分解
- 3118.06 第 30 讲:线性变换及其矩阵
- 3218.06 第 31 讲:基变换与图像压缩
- 3318.06 第 32 讲:第三次测验复习
- 3418.06 第 33 讲:左逆、右逆与伪逆
- 3518.06 第 34 讲:期末复习
这一讲其实是两件互不相干的事,共用一个工具:特征值管“乘很多次会怎样”,标准正交基管“系数怎么一步算出来”。Strang 先拿 Markov 矩阵做特征值的应用,再用后半讲把投影推到无穷维,得到 Fourier 级数。我把他的加州和麻州人口迁移连乘了 20 年,离稳态 只差 0.7523 人,每多乘一年,差距就缩成原来的 0.7 倍。后半段我用 np.trapezoid 算 Fourier 系数,取到 19 次谐波时,连续的三角波误差是 0.0103,带跳变的方波是 0.3558,差了 35 倍。
视频:Lecture 24: Markov matrices; Fourier series(OCW 页面与字幕) 教材:《Introduction to Linear Algebra》第 6 版 Appendix 8 Markov Matrices and Perron-Frobenius;Fourier 级数在第 6 版移到了网站:ila5fourierseries.pdf 前置:第 23 讲:微分方程与 exp(At) 一句话:每列和为 1 的非负矩阵必有特征值 1,它的特征向量就是稳态;用标准正交基展开,每个系数只是一次内积,Fourier 级数就是把这个想法用在函数上。
- 开头他先写下一个凭空编出来的 3 × 3 Markov 矩阵,然后追问“为什么 1 一定是特征值”。这一段( 的各列加起来是 0,所以各行线性相关)值得认真看,它是本讲唯一需要动脑的论证。
- 他顺手算出特征向量 的那几句,本篇第一节已经用代码验算过,倍速即可。
- 加州与麻州的例子是全讲的主线,他心算 、 的过程建议跟着做一遍。
- 最后一部分是标准正交基展开和 Fourier 级数。时间很赶,他只推了系数公式,没有算具体函数。我在本篇最后一节补了两个函数的数值例子。
Markov 矩阵:非负、列和为 1
他在黑板上写下的矩阵是
什么样的矩阵叫 Markov 矩阵? 两条:每个元素大于等于 0;每一列加起来等于 1。第一条保证做概率用的时候不出现负数,第二条保证“每个人都有去处”,总量不变。它的每个幂依然满足这两条,所以 也是 Markov 矩阵。
他关心的还是 ,即 。上一讲的微分方程里,稳态来自 ( 不变);幂的情形里, 的分量一步就死了,真正管稳态的是 ( 不变)。他预告了两个事实:
- 一定是特征值;
- 其余特征值的模都小于 1(他留了余地:特殊情形下可能有另一个模等于 1 的,但绝不会大于 1)。
于是 ,乘 次之后 ,后面的项全部衰减,剩下 :初始条件里沿 的那一份就是稳态。(他照例说了一遍每讲一次的良心话:这需要一组完整的特征向量。)
为什么 1 一定是特征值? 这是我觉得整讲最漂亮的一步。特征值是把对角线减掉之后能让矩阵变奇异的那些数,所以他检查 :每个对角元减 1,每一列的和从 1 变成 0。各列加起来是 0,意味着把三行各乘 1 再相加,得到零行,即 在 的左零空间里。方阵的行线性相关,矩阵就奇异,所以 1 是特征值。不用算行列式,对任何 Markov 矩阵都成立。
他接着多说了一句: 与 的特征值相同,因为 (行列式性质里的第 10 条,转置不改变行列式)。所以上面其实是在说 有特征向量 ,对应特征值 1;而 自己的特征向量是 的零空间里的另一个向量,得另外去找。
我把这些逐条交给 NumPy:
import numpy as np
def main() -> None: np.set_printoptions(precision=4, suppress=True) # Strang 黑板上凭空编出来的 Markov 矩阵,每一列是一个概率分布 A = np.array([[0.1, 0.01, 0.3], [0.2, 0.99, 0.3], [0.7, 0.0, 0.4]]) print("列和:", A.sum(axis=0)) print("A^2 的列和:", (A @ A).sum(axis=0)) print("A^2 的最小元素 >= 0:", (A @ A).min() >= 0)
# 全 1 向量在左零空间:1^T (A - I) = 0,所以 A - I 奇异 ones = np.ones(3) print("1^T (A - I) =", ones @ (A - np.eye(3))) print("det(A - I) =", f"{np.linalg.det(A - np.eye(3)):.2e}")
vals, vecs = np.linalg.eig(A) print("A 的特征值:", vals) print("A^T 的特征值:", np.linalg.eigvals(A.T))
# 他手算的特征向量 (0.6, 33, 0.7),验证它在 A - I 的零空间里 x1 = np.array([0.6, 33.0, 0.7]) print("(A - I) @ (0.6, 33, 0.7) =", (A - np.eye(3)) @ x1) k = np.argmin(np.abs(vals - 1.0)) v = vecs[:, k].real print("eig 给出的 λ = 1 的特征向量,缩放后:", v / v[0] * 0.6)
# 稳态:把特征向量缩放成和为 1 的概率分布 steady = x1 / x1.sum() print("稳态(和为 1):", steady) P = np.linalg.matrix_power(A, 100) print("A^100 的每一列:") print(P) print("每一列都接近稳态:", np.allclose(P, steady[:, None]))
if __name__ == "__main__": main()列和: [1. 1. 1.]A^2 的列和: [1. 1. 1.]A^2 的最小元素 >= 0: True1^T (A - I) = [ 0. -0. 0.]det(A - I) = -2.33e-18A 的特征值: [-0.2315 0.7215 1. ]A^T 的特征值: [-0.2315 0.7215 1. ](A - I) @ (0.6, 33, 0.7) = [-0. -0. 0.]eig 给出的 λ = 1 的特征向量,缩放后: [ 0.6 33. 0.7]稳态(和为 1): [0.0175 0.9621 0.0204]A^100 的每一列:[[0.0175 0.0175 0.0175] [0.9621 0.9621 0.9621] [0.0204 0.0204 0.0204]]每一列都接近稳态: True- 列和是 , 也是; 是零向量, 是 ,浮点意义上的 0。
- 和 的特征值都是 、、:1 在,其余两个模都小于 1。
- 他手算的特征向量 通过了检验:他说“这是线性代数史上第一次特征向量里出现 3300 这种分量”,随即发现多读了两位小数,其实只是 33。
np.linalg.eig给出的向量缩放后和他的完全一致。 - 缩放成和为 1 的概率分布,稳态是 。 的三列都收敛到它,三列相同,意味着不管从哪个状态出发,100 步之后都是同一个分布。第二个状态的自留率 0.99 太高了,所以稳态里它占了 96%。
特征向量的分量全是正的,这点他说理论保证,但没有证,我也不证(据我所知这是 Perron-Frobenius 定理的内容,教材的 Appendix 8 讲的就是它)。
加州与麻州:特征值管着人口往哪走
稳态到底有什么用? 他的例子是两个州的人口迁移:每年 10% 的加州人搬去麻州,20% 的麻州人搬去加州(他说“Red Sox 又输了”),其余的留在原地:
(我怀疑这两个比例是为了让黑板上的数好算而挑的,现实里大概没有人这么整齐地搬家。)第一列是加州人的去向(0.9 留下,0.1 搬走,合计 1),第二列是麻州人的去向。初始状态:加州 0 人,麻州 1000 人。
一步之后是 :200 人去了加州。再乘一次,加州会超过 200,麻州会低于 800,总数仍是 1000。这个矩阵的特征值不用解方程:1 肯定有;迹是 1.7,所以另一个是 0.7(验证:行列式 )。特征向量: 的零空间是 , 的零空间是 。
稳态是 的某个倍数,倍数由“总数 1000”定出:,即加州 、麻州 。再把 展开:
时第一个分量是 ,第二个是 ,对上了。下面的脚本验算这一整套,并看看闭式公式和直接连乘是不是同一回事、离稳态有多远:
import numpy as np
def main() -> None: np.set_printoptions(precision=4, suppress=True) # 加州和麻州:每年 10% 的加州人搬去麻州,20% 的麻州人搬去加州 A = np.array([[0.9, 0.2], [0.1, 0.8]]) u0 = np.array([0.0, 1000.0]) # (加州, 麻州)
u = u0.copy() for k in range(1, 4): u = A @ u print(f"第 {k} 年:", u, "总人数:", u.sum())
vals, vecs = np.linalg.eig(A) print("特征值:", vals) # 迹 1.7,行列式 0.7
# 他手算的两个特征向量:λ = 1 对应 (2, 1),λ = 0.7 对应 (-1, 1) x1 = np.array([2.0, 1.0]) x2 = np.array([-1.0, 1.0]) print("A @ x1 - x1 =", A @ x1 - x1) print("A @ x2 - 0.7 * x2 =", A @ x2 - 0.7 * x2)
# u0 = c1 x1 + c2 x2,他心算得到 c1 = 1000/3,c2 = 2000/3 S = np.column_stack([x1, x2]) c = np.linalg.solve(S, u0) print("c =", c, ",1000/3 =", 1000 / 3)
steady = c[0] * x1 print("稳态:", steady) for k in [1, 5, 10, 20, 100]: closed = c[0] * x1 + c[1] * 0.7**k * x2 direct = np.linalg.matrix_power(A, k) @ u0 gap = np.linalg.norm(direct - steady) print( f"k = {k:3d}:公式与连乘一致 {np.allclose(closed, direct)}," f"离稳态 {gap:.4f}" )
if __name__ == "__main__": main()第 1 年: [200. 800.] 总人数: 1000.0第 2 年: [340. 660.] 总人数: 1000.0第 3 年: [438. 562.] 总人数: 1000.0特征值: [1. 0.7]A @ x1 - x1 = [0. 0.]A @ x2 - 0.7 * x2 = [0. 0.]c = [333.3333 666.6667] ,1000/3 = 333.3333333333333稳态: [666.6667 333.3333]k = 1:公式与连乘一致 True,离稳态 659.9663k = 5:公式与连乘一致 True,离稳态 158.4579k = 10:公式与连乘一致 True,离稳态 26.6320k = 20:公式与连乘一致 True,离稳态 0.7523k = 100:公式与连乘一致 True,离稳态 0.0000- 前三年是 、、,每年总数都是 1000。
eig给出的特征值是 和 ,,与他心算的 、 一致。- 闭式公式在 都与
matrix_power连乘一致。 - 离稳态的距离:第 10 年是 26.6320,第 20 年是 0.7523。二者的比 ,正好是 :十年缩小到原来的 2.8% 左右(约 1/35),每年缩小 30%。第 100 年,打印到四位小数已经是 0.0000。
所以他说“波士顿的清教徒现在在哪”,答案和初始条件没有关系:只要总数是 1000,不管一开始 1000 个人全在麻州,还是全在加州,几百年之后都是 。第二个特征值 0.7 只决定走得多快,不决定走向哪里。
他还提醒了一件会让你在别的书里糊涂的事:很多工程课程喜欢用行向量,,这时 是上面这个矩阵的转置,改成“每一行和为 1”。数学上是同一回事,注意一下约定就行了。我以后在控制和概率的书里碰到“行随机矩阵”,大概就是这个意思。
标准正交基:系数只是一次内积
后半讲他说是“投影那一章的一个了不起的应用”。先回到有限维。 是 维空间的一组标准正交基,任何向量 都能写成
系数 怎么求? 一般的基要解方程组。标准正交基的好处是:两边同时和 做内积, 在 时是 0, 时是 1,其余全部消失,得到 。写成矩阵形式,,解是 ,而 是正交矩阵时 ,所以 ,第 个分量就是第 个 与 的内积。他说这时的“展开”和“投影”是一回事:投影的正规方程 里 ,直接剩下 。
我用随机正交基验证一遍:
import numpy as np
def main() -> None: np.set_printoptions(precision=4, suppress=True) rng = np.random.default_rng(1806)
# 随机正交基:对随机矩阵做 QR,取 Q 的列 Q, _ = np.linalg.qr(rng.standard_normal((5, 5))) v = rng.standard_normal(5) print("||Q^T Q - I|| =", f"{np.linalg.norm(Q.T @ Q - np.eye(5)):.2e}")
# 展开系数:x1 = q1^T v,一次点积,不用解方程 x_dot = np.array([Q[:, i] @ v for i in range(5)]) x_matrix = Q.T @ v x_solve = np.linalg.solve(Q, v) print("x =", x_matrix) print("逐个点积 = Q^T v:", np.allclose(x_dot, x_matrix)) print("Q^T v = solve(Q, v):", np.allclose(x_matrix, x_solve)) print("Q @ x 还原 v:", np.allclose(Q @ x_matrix, v))
if __name__ == "__main__": main()||Q^T Q - I|| = 5.10e-16x = [-0.1733 0.336 -2.3959 2.6273 0.7986]逐个点积 = Q^T v: TrueQ^T v = solve(Q, v): TrueQ @ x 还原 v: True- 随机矩阵做 QR 得到的 满足 ,在舍入误差内是正交的。
- 逐个做点积、一次
Q.T @ v、解方程solve(Q, v),三种做法得到同一个 ,Q @ x能还原 。
solve 要做一次 LU 分解,Q.T @ v 只是五个点积。这个差别在 里看不出来,在 Fourier 里它是决定性的:基向量有无穷多个,不能解无穷个方程,但每个系数仍然只需要一次积分。
函数也是向量:Fourier 级数
怎么把这个想法搬到函数上? 他的原话大意是:Fourier 意识到,向量 可以换成函数 ,正交向量 可以换成正交函数 ,只不过无穷多个,因为这个空间是无穷维的。展开式是
“正交”要有内积。向量的内积是 :把对应分量相乘,再相加。函数有一整条连续的 ,“对应分量相乘”就是在每个 处取 ,“相加”换成积分:
积分区间是 ,因为所有的正弦和余弦都以 为周期, 也得是。他当场检验了一对:,其余各对“用一点三角恒等式”就能得到。我用 np.trapezoid 把五个函数两两做内积,排成 Gram 矩阵:
import numpy as np
def inner(f: np.ndarray, g: np.ndarray, x: np.ndarray) -> float: """函数内积:在 [0, 2π] 上对 f(x) g(x) 做数值积分。""" return float(np.trapezoid(f * g, x))
def main() -> None: np.set_printoptions(precision=4, suppress=True) x = np.linspace(0.0, 2.0 * np.pi, 2001) basis = [ ("1", np.ones_like(x)), ("cos x", np.cos(x)), ("sin x", np.sin(x)), ("cos 2x", np.cos(2 * x)), ("sin 2x", np.sin(2 * x)), ]
# 他口算的那一个:sin x 与 cos x 的内积 print("<sin x, cos x> =", f"{inner(basis[2][1], basis[1][1], x):.2e}")
# 五个函数两两做内积:对角线是长度的平方,其余应当是 0 G = np.array([[inner(f, g, x) for _, g in basis] for _, f in basis]) print(G) print("对角线:", np.diag(G)) print("π =", np.pi, ",2π =", 2 * np.pi) off = np.abs(G - np.diag(np.diag(G))).max() print("非对角元的最大绝对值:", f"{off:.2e}")
if __name__ == "__main__": main()<sin x, cos x> = 0.00e+00[[ 6.2832 -0. 0. -0. -0. ] [-0. 3.1416 0. -0. 0. ] [ 0. 0. 3.1416 0. 0. ] [-0. -0. 0. 3.1416 -0. ] [-0. 0. 0. -0. 3.1416]]对角线: [6.2832 3.1416 3.1416 3.1416 3.1416]π = 3.141592653589793 ,2π = 6.283185307179586非对角元的最大绝对值: 4.44e-16- ,与他的黑板一致。
- Gram 矩阵除对角线外全是 0(最大的非对角元绝对值是 ),对角线是 :常数函数长度的平方是 ,其余是 。他说 ,这里印证了。
注意:这五个函数是正交的,但没有归一化,所以它们更像“长度各不相同的正交基”。求系数时要除以长度的平方,这就是 Fourier 公式里 的来历。
到底是多少? 套用有限维的做法:两边同时乘 再积分,右边只有 这一项活着,所以
同理 ,。常数项 是例外,要除以 ,所以它就是 在一个周期里的平均值,他说“那个问题很容易”。
他到这里下课了,没有算一个具体函数。我挑了两个:方波( 上为 , 上为 )和三角波 。代码里用 20001 个点做梯形积分,求出 、前 20 个 ,再看取前 次谐波的部分和离原函数有多远(用 距离 ,也就是函数空间里的“长度”):
import numpy as np
def inner(f: np.ndarray, g: np.ndarray, x: np.ndarray) -> float: """函数内积:在 [0, 2π] 上对 f(x) g(x) 做数值积分。""" return float(np.trapezoid(f * g, x))
def fourier( f: np.ndarray, x: np.ndarray, n: int) -> tuple[float, np.ndarray, np.ndarray]: """返回 a0(平均值),以及 a_k、b_k(k = 1..n)。""" a0 = inner(f, np.ones_like(x), x) / (2.0 * np.pi) a = np.array([inner(f, np.cos(k * x), x) / np.pi for k in range(1, n + 1)]) b = np.array([inner(f, np.sin(k * x), x) / np.pi for k in range(1, n + 1)]) return a0, a, b
def partial_sum( a0: float, a: np.ndarray, b: np.ndarray, x: np.ndarray, n: int) -> np.ndarray: s = np.full_like(x, a0) for k in range(1, n + 1): s += a[k - 1] * np.cos(k * x) + b[k - 1] * np.sin(k * x) return s
def main() -> None: np.set_printoptions(precision=4, suppress=True) x = np.linspace(0.0, 2.0 * np.pi, 20001) # 方波:跳变点取 0(左右极限的平均),这样 [0, 2π] 两端也对称 square = np.sign(np.round(np.sin(x), 12)) tri = np.abs(x - np.pi) # 三角波,连续但有尖角
for name, f in [("方波", square), ("三角波", tri)]: a0, a, b = fourier(f, x, 20) print(f"{name}:a0 = {a0:.4f}") print(" a1..a5 =", a[:5]) print(" b1..b5 =", b[:5]) for n in [1, 3, 9, 19]: s = partial_sum(a0, a, b, x, n) err = np.sqrt(inner((f - s) ** 2, np.ones_like(x), x)) print(f" 取到 {n:2d} 次谐波:L2 误差 = {err:.4f}")
# 方波的理论值:b_k = 4 / (π k)(k 为奇数),偶数项为 0 _, _, b = fourier(square, x, 5) print("方波 b_k 的数值:", b) print("理论 4/(πk):", [round(4 / (np.pi * k), 4) for k in (1, 3, 5)])
# 方波在跳变点附近的过冲(Gibbs 现象) a0, a, b = fourier(square, x, 19) peak = partial_sum(a0, a, b, x, 19).max() print(f"方波 19 次部分和的最大值:{peak:.4f}")
if __name__ == "__main__": main()方波:a0 = -0.0000 a1..a5 = [ 0. -0. 0. -0. 0.] b1..b5 = [ 1.2732 -0. 0.4244 0. 0.2546] 取到 1 次谐波:L2 误差 = 1.0907 取到 3 次谐波:L2 误差 = 0.7898 取到 9 次谐波:L2 误差 = 0.5032 取到 19 次谐波:L2 误差 = 0.3558三角波:a0 = 1.5708 a1..a5 = [ 1.2732 -0. 0.1415 -0. 0.0509] b1..b5 = [-0. -0. -0. -0. -0.] 取到 1 次谐波:L2 误差 = 0.2734 取到 3 次谐波:L2 误差 = 0.1090 取到 9 次谐波:L2 误差 = 0.0289 取到 19 次谐波:L2 误差 = 0.0103方波 b_k 的数值: [ 1.2732 -0. 0.4244 0. 0.2546]理论 4/(πk): [1.2732, 0.4244, 0.2546]方波 19 次部分和的最大值:1.1798- 方波是奇函数,所有 都是 0(打印出来的 是舍入噪声);偶数次的 也是 0,只剩奇数次,数值积分给出 ,与理论值 一致。
- 三角波关于 对称,所有 都是 0;平均值 ;奇数次的 是 ,即 ,衰减得比方波的 快得多。
- 取到 19 次谐波,三角波的误差是 0.0103,方波是 0.3558,相差约 35 倍。尖角还连得上,跳变连不上,高次项就得一直陪着。
- 方波 19 次部分和的最大值是 1.1798,而真值是 1。多取几项过冲不会消失,只是被挤到跳变点附近(Gibbs 现象,我是从别处看到的,这里只验证了 19 项的峰值)。
我觉得这就像一块三棱镜:一束白光进去,按频率分成一排纯色,每一种颜色是多少,就是 和 的大小。区别是棱镜分的是光,Fourier 级数分的是函数,而分的依据恰好是“正交”。
这一讲的两部分其实在谈同一个对象:一组特别好用的基。Markov 矩阵的特征向量是让 变简单的基,、 是让展开变简单的基。第 26 讲讲 Fourier 矩阵时,这两条线会撞到一起:Fourier 矩阵归一化之后是酉矩阵,也就是复数世界里的正交矩阵。
下一讲不是新内容,是第二次测验的复习,他在结尾说周一复习、周三考试。我会逐题用 NumPy 验证他讲的题,见 18.06 第 24b 讲:第二次测验复习。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!











