给你三个方程、三个未知数,中学的做法是一个个消元。但如果方程有一万个呢?如果方程比未知数多、根本没有精确解呢?线性代数把”解方程”这件事换了一个看法:矩阵不是一张数字表,而是一台把向量变成向量的机器。看懂这台机器在做什么,消元、最小二乘、PCA、神经网络里的 W @ x 就都是同一件事。

前置:无。会用 numpy 更好。

矩阵乘向量:列的组合

先看一个 的方程组:

“行视角”是把每个方程画成平面上一条直线,解就是两线交点 。这在二维很直观,到了十维就画不出来了。

Strang 更喜欢”列视角”。把方程写成

问题变成:两根列向量各取多少份,加起来恰好等于右边的 ?这就是 的真正含义。 是 的列的线性组合,系数是 的分量。

import numpy as np
A = np.array([[2., -1.], [-1., 2.]])
x = np.array([1., 2.])
# 两种算法结果一样,但第二种才是"列视角"
print(A @ x)
print(x[0] * A[:, 0] + x[1] * A[:, 1])

输出:

[0. 3.]
[0. 3.]

这个看法马上带来一个问题:对所有 都能解吗?只要两列不共线,它们的组合能铺满整个平面,答案是能。如果两列共线(比如第二列是第一列的 3 倍),组合只能落在一条直线上,大多数 无解。” 的列的所有组合”有个名字,叫列空间 。 有解,当且仅当 。

矩阵乘矩阵也用同样的眼光看。 的第 列等于 乘 的第 列,也就是 的列按 第 列给的系数组合。所以 的每一列都在 里。还有一种拆法同样有用:AB = \sum_k (\text{Ak 列})(\text{Bk 行}),即若干个秩 1 矩阵之和。后面讲 SVD 时会再用到它。

顺带一提,矩阵乘法不满足交换律: 和 一般不相等,甚至形状都可能不同。从变换的角度看这很自然,“先旋转再拉伸”和”先拉伸再旋转”是两件不同的事。但它满足结合律 ,这条性质在工程上很值钱:同样的结果,括号加在不同位置,计算量可能差几个数量级。比如 是 、 是 、 是一个向量,先算 再算 只要两次矩阵乘向量,约 次运算;先算 则要约 次。反向传播从输出往输入逐层乘雅可比矩阵,就是在利用这一点。

一个矩阵也可以看成一个线性变换:它把空间里每个向量搬到新的位置,同时保持直线还是直线、原点不动。 的第一列就是基向量 被送到的位置,第二列是 被送到的位置。知道了基向量去哪,任何向量去哪都确定了,这就是为什么一个矩阵能完整描述一个线性变换。神经网络里的一层 y = W @ x,就是先做一个这样的变换,再接非线性(见 从线性回归到神经网络)。

停一下,检查理解单选

A 是 3×2 矩阵,两列线性无关。关于 Ax = b,哪个说法对?

消元:计算机怎么解方程

列视角告诉我们解是否存在,但还没告诉我们怎么求。计算机用的是高斯消元,这和中学的做法一样,只是组织得更整齐。

取

第一步用第 1 行消掉下面两行的第一列:第 2 行减去 2 倍第 1 行,第 3 行加上 1 倍第 1 行。第二步用新的第 2 行消第 3 行的第二列。最后得到上三角矩阵 ,从最后一个方程往回代就能解出所有未知数。

每一次”第 行减去 倍第 行”都是左乘一个初等矩阵。把这些乘数 收集起来,正好放在一个下三角矩阵 的对应位置,于是

这件事的好处是: 和 只依赖于 。如果要对同一个 解很多个不同的 ,只要分解一次(约 次浮点运算),之后每个 只需要两次三角回代(约 )。

import numpy as np
A = np.array([[2., 1, 1], [4, -6, 0], [-2, 7, 2]])
 
def lu(A):
    # 不做行交换的教学版;主元为 0 时会坏,正好说明为什么需要 pivoting
    n = len(A); U = A.copy(); L = np.eye(n)
    for j in range(n):
        for i in range(j + 1, n):
            L[i, j] = U[i, j] / U[j, j]
            U[i] -= L[i, j] * U[j]
    return L, U
 
L, U = lu(A)
print(L); print(U)
print(np.allclose(L @ U, A))

输出:

[[ 1.  0.  0.]
 [ 2.  1.  0.]
 [-1. -1.  1.]]
[[ 2.  1.  1.]
 [ 0. -8. -2.]
 [ 0.  0.  1.]]
True

的对角线 叫主元。这个函数在主元为 0 时会除以零;主元很小时会把舍入误差放大。实际的库(LAPACK、np.linalg.solve)会做部分主元选择:每一列挑绝对值最大的元素换到主元位置,于是分解变成 , 是置换矩阵。

主元也回答了”可不可逆”的问题:消元后 个主元全非零, 就可逆;有一列找不到非零主元,说明这一列能被前面的列组合出来, 是奇异的。行列式 就是主元的乘积(再乘上行交换带来的 ),上面这个例子是 。行列式的几何意义是变换对体积的缩放倍数: 表示单位立方体被映成体积 16 的平行六面体, 表示空间被压扁了一维。

实际计算里几乎从不显式求 。解 时用 np.linalg.solve,它内部就是 LU 分解加回代,比先求逆再乘更快也更准。

四个子空间:解长什么样

消元做到底,能把任意 矩阵化成行最简形 。数一数主元个数,就得到矩阵最重要的一个数:秩 。秩是线性无关列的最大个数,也等于线性无关行的最大个数。

看一个秩不满的例子:

第三行等于前两行之和,第二列是第一列的 2 倍,所以秩是 2。四个未知数、两个有效方程,必然有两个自由变量。

import numpy as np
A = np.array([[1., 2, 2, 2], [2, 4, 6, 8], [3, 6, 8, 10]])
print("rank:", np.linalg.matrix_rank(A))
# 零空间的一组基:SVD 里奇异值为 0 的右奇异向量
_, s, Vt = np.linalg.svd(A)
N = Vt[2:].T
print("singular values:", s.round(4))
print("A @ N ≈ 0:", np.allclose(A @ N, 0))

输出:

rank: 2
singular values: [18.4656  1.0102  0.    ]
A @ N ≈ 0: True

的所有解组成零空间 ,维数是 。 的通解是”一个特解 + 零空间里任意向量”,因为零空间里的向量被 送到 0,加上它不改变 。

Strang 把每个矩阵配上四个子空间:

子空间住在哪维数
列空间
零空间
行空间
左零空间

维数的两条等式(,)叫秩-零化度定理。更深的一层是正交:行空间和零空间在 里互相垂直,列空间和左零空间在 里互相垂直。原因直接来自定义: 说的正是 和 的每一行点积为 0。

于是 做的事可以这样描述:任何输入 拆成行空间分量和零空间分量,零空间分量被直接压成 0,行空间分量被一一对应地送到列空间。 在行空间和列空间之间是一个可逆的映射,这是理解伪逆和 SVD 的钥匙。

停一下,检查理解多选

A 是 5×3 矩阵,秩为 2。下面哪些成立?

正交与投影:没有解时找最好的解

现实中的方程组常常没有解。测了 100 个点想拟合一条直线 ,100 个方程、2 个未知数, 几乎不可能恰好落在 的列空间里。

既然 解不出来,退而求其次:在列空间里找离 最近的点 。几何上,最近点就是 往列空间做垂线的垂足,误差 必须垂直于列空间,也就是垂直于 的每一列:

这就是正规方程。投影矩阵 把任何向量投到列空间上,它满足 (投两次和投一次一样)。

拿三个点 拟合直线:

import numpy as np
t = np.array([0., 1, 2]); b = np.array([6., 0, 0])
A = np.column_stack([np.ones(3), t])
xhat = np.linalg.solve(A.T @ A, A.T @ b)
e = b - A @ xhat
print("c, d =", xhat)
print("误差 e =", e)
print("A^T e =", (A.T @ e).round(12))  # 误差与每一列正交
print("lstsq:", np.linalg.lstsq(A, b, rcond=None)[0])

输出:

c, d = [ 5. -3.]
误差 e = [ 1. -2.  1.]
A^T e = [0. 0.]
lstsq: [ 5. -3.]

最佳直线是 ,误差向量 与两列都正交。最小二乘就是一次投影,从线性回归到神经网络 里的线性回归在没有正则化时解的正是这个方程。

正规方程在数值上有缺点: 的条件数是 的平方,列接近共线时误差会被放大。更好的办法是先把 的列变成标准正交的。

标准正交向量 两两垂直、长度为 1,排成矩阵 满足 。有了它,投影公式退化成 ,最小二乘变成 ,不需要解任何方程。Gram-Schmidt 正是把任意一组无关向量变成标准正交向量的过程:每来一个新向量,减去它在已有方向上的投影,剩下的部分归一化。把这个过程记下来就是 分解, 是上三角。np.linalg.lstsq 用的是 QR 或 SVD 而不是正规方程。

正交矩阵还有一个性质:,它只旋转或反射,不拉伸。这在数值计算里很宝贵,因为误差不会被放大。

停一下,检查理解单选

最小二乘解要求误差 e = b - A x̂ 满足什么?

特征值:找到不被转向的方向

到目前为止研究的是 ,一个”静态”问题。另一类问题是”动态”的:反复乘同一个矩阵,,一百步之后会怎样?直接算 太蠢,我们需要一个看法让矩阵的幂变简单。

大多数向量被 作用后会改变方向。但有一些特殊方向, 只把它拉长或缩短:

是特征向量, 是特征值。沿着这个方向,乘 100 次 就是乘 。如果 矩阵有 个无关的特征向量,把它们排成 ,就有 ,于是 ,只需要对对角线上的数取幂。

斐波那契数列是经典例子。 写成 ,其中 。

import numpy as np
A = np.array([[1., 1], [1, 0]])
# A 对称,用 eigh 得到实特征值(升序)和正交特征向量
lam, S = np.linalg.eigh(A)
print("特征值:", lam)
# 大的特征值主导长期行为:F_k ≈ φ^k / √5
k = 30
Ak = S @ np.diag(lam**k) @ S.T  # 正交矩阵的逆就是转置
print("F_30 via eig:", round(Ak[0, 1]))
print("φ^30/√5   :", lam.max()**k / np.sqrt(5))

输出:

特征值: [-0.61803399  1.61803399]
F_30 via eig: 832040
φ^30/√5   : 832040.0000002412

两个特征值是 。 的那部分每步都在衰减,所以 很快就由黄金比例 主导。这个思路推广开来:长期行为由绝对值最大的特征值决定。这也是幂迭代的原理(PageRank 就是求一个巨大矩阵的主特征向量),也是 RNN 梯度消失/爆炸的数学根源:反复乘同一个权重矩阵,特征值大于 1 的方向指数爆炸,小于 1 的方向指数衰减(见 序列模型与注意力的起源)。

特征值怎么求? 有非零解意味着 奇异,即 。这是一个 次多项式,但这只适合手算小矩阵。数值库用的是 QR 算法这样的迭代法。两个不用算的检查:特征值之和等于迹(对角线之和),特征值之积等于行列式。斐波那契矩阵的迹是 1,行列式是 ,两个特征值 和 正好满足。

并不是所有矩阵都能对角化。 只有一个特征方向;旋转矩阵的特征值是复数。但有一类矩阵总是表现完美:

对称矩阵 的特征值都是实数,特征向量可以选成两两正交,于是 (谱定理)。如果所有特征值都为正,就叫正定矩阵,等价于对任意非零 有 。 是一个二次函数,正定说明它的图像是一个向上开口的碗,有唯一的最低点。优化里,损失函数在极小值附近的 Hessian 是正定的;Hessian 最大和最小特征值之比(条件数)越大,碗越扁,梯度下降越容易来回震荡。协方差矩阵 至少是半正定的,因为 。

停一下,检查理解单选

矩阵 A 的特征值是 0.5 和 1.2,从一个一般的初始向量开始反复做 u = A @ u,很多步之后会怎样?

SVD:每个矩阵都是旋转、拉伸、旋转

特征分解有两个限制:只对方阵有意义,而且不一定存在。数据矩阵(1000 个样本 × 50 个特征)是长方形的,我们需要一个对任何矩阵都成立的分解。

奇异值分解说:任何 矩阵都可以写成

()和 ()是正交矩阵, 是对角线上为 的长方形对角阵。几何上: 先旋转输入, 沿坐标轴拉伸, 再旋转输出。任何线性变换都把单位圆映成一个椭圆, 就是椭圆的半轴长。

它和前面所有内容都接上了。 的列是 的特征向量, 的列是 的特征向量, 是对应的特征值。非零奇异值的个数就是秩 ; 的前 列是行空间的标准正交基,后 列是零空间的基, 同理给出列空间和左零空间的基。四个子空间一次全部拿到。

按”列乘行”展开,

每一项是秩 1 矩阵,按重要性从大到小排好了。只保留前 项,得到的 是所有秩不超过 的矩阵里离 最近的(Eckart–Young 定理),误差的谱范数恰好是 。

import numpy as np
rng = np.random.default_rng(0)
# 构造一个"本质上是秩 3"的 100x80 矩阵,再加一点噪声
A = rng.normal(size=(100, 3)) @ rng.normal(size=(3, 80)) + 0.01 * rng.normal(size=(100, 80))
U, s, Vt = np.linalg.svd(A, full_matrices=False)
print("前 6 个奇异值:", s[:6].round(3))
for k in [1, 2, 3, 10]:
    Ak = U[:, :k] * s[:k] @ Vt[:k]
    rel = np.linalg.norm(A - Ak) / np.linalg.norm(A)
    print(f"k={k:2d}  相对误差={rel:.4f}  参数量={k*(100+80)} vs {100*80}")

输出:

前 6 个奇异值: [100.118  87.259  85.266   0.182   0.178   0.175]
k= 1  相对误差=0.7730  参数量=180 vs 8000
k= 2  相对误差=0.5403  参数量=360 vs 8000
k= 3  相对误差=0.0055  参数量=540 vs 8000
k=10  相对误差=0.0047  参数量=1800 vs 8000

前三个奇异值远大于其余(其余只是噪声),保留 3 项就把相对误差降到约 0.5%(已经是噪声水平,再加到 10 项也几乎不再下降),存储只要原来的 7%。这个”大部分信息集中在少数方向上”的现象在真实数据里极常见,于是有了一串应用:

  • PCA:把数据中心化后做 SVD, 的前几列就是方差最大的方向,投影上去就是降维。
  • 伪逆:(非零 取倒数),对任何矩阵给出最小范数的最小二乘解,np.linalg.pinv 就是这么算的。
  • 条件数: 衡量解方程时误差会被放大多少倍。
  • 低秩近似:LoRA 微调把权重更新限制成 两个瘦矩阵的乘积,背后的假设正是”更新矩阵是近似低秩的”(见 后训练)。
停一下,检查理解多选

关于 A = U Σ V^T,哪些说法正确?

线性代数与机器学习

回头看,机器学习里的矩阵计算几乎都落在上面几节里:

场景背后的线性代数
全连接层 y = W @ x线性变换;输出是 W 列的组合
一个 batch 的前向矩阵乘矩阵,GPU 上的 GEMM
线性回归闭式解投影与正规方程
注意力分数 所有 query 与 key 的点积表,见 Transformer 架构
PCA、embedding 降维SVD
LoRA低秩分解
优化难易Hessian 的条件数
RNN 梯度爆炸/消失反复乘矩阵,特征值决定增长

还有一个容易忽略的工程事实:矩阵乘法 乘 需要 次浮点运算,它是现代 AI 硬件优化的头号目标。为什么矩阵乘能跑满 GPU 而逐元素加法不能,是 资源核算与 Scaling Laws 里算术强度要回答的问题。

停一下,检查理解单选

一个形状为 (4096, 4096) 的权重矩阵,用秩 16 的 B @ A 来表示它的更新,参数量大约是原来的多少?

记住这三件事

  1. 是 的列的线性组合; 有解当且仅当 在列空间里,解的自由度由零空间决定。
  2. 没有精确解时,最小二乘就是把 投影到列空间,误差垂直于每一列。
  3. 特征值决定反复作用的长期行为;SVD 对任何矩阵成立,按重要性把它拆成秩 1 矩阵之和,截断即最优低秩近似。

延伸阅读