线性代数 入门

读懂 Transformer 所需的线性代数,全部搭在同一张图上:一个带网格的平面,和一个你能亲手挪动的箭头。向量、作为变换的矩阵、点积、投影、特征向量与 SVD —— 页面上每一个数字都由旁边那张图算出来,所以你可以把它拖到任何状态,它依然成立。

01

向量是一个位置

两个数,有顺序。把它们读成坐标而不是读成列表,整个学科都建立在这一步之上。

地址、RGB 颜色、一对 GPS 坐标 —— 都是靠顺序承载含义的列表。(255, 0, 0) 是红色,(0, 255, 0) 是绿色;(37.78, −122.42) 是旧金山,两个数一交换就落到南极附近的大洋里了。向量就是这个想法再往前走一步:把这些数读成坐标,列表就变成了一个位置。拖动箭头的尖端,看那两个数跟着走:

v = [3.0, 4.0]
v = [3.0, 4.0]

注意,这里没有任何东西被存了两遍。箭头就是那一对数画出来的样子;按惯例它的尾巴钉在原点,所以尖端和读数是同一件事的两种看法。第一个槽位是水平方向的步长,第二个是垂直方向的 —— 换一下顺序,你就到了完全不同的地方。

这张图还白送了长度。勾股定理早就画在网格里了:两个分量是直角三角形的两条直角边,箭头是斜边。把高度拉高,看 ‖v‖ 怎么涨:

‖v‖ = 5.00

因为两条边先平方再相加,最长的那个分量占绝对主导:在初始高度上,竖边贡献了总数 25 里的 16,箭头正好长 5。把它压到,就只剩下那条水平的直角边。这就是 ‖v‖ = √(Σ vᵢ²)—— 欧几里得范数,也叫 L2 范数 —— 推广到任意多个分量时,一个符号都不用改。

长度和方向可以干净地拆开。把向量除以它自己的长度,剩下的就是同一条射线上的单位向量:只留方向,扔掉大小。转动角度,看 v 和 v̂始终待在过原点的同一条直线上:

53°

边转边看读数:v̂ 的两个分量正好是角度的余弦和正弦,而且永远跑不出 [−1, 1]。几乎所有检索管线的第一步都是归一化,因为正是它让长度差十倍的两篇文档变得可比。

乘上一个数 —— 一个标量 —— 只会让箭头沿那条射线滑动,从不离开它。把标量推到零以下,箭头就穿过原点翻到另一边:

s = 1.0

一个非零向量的所有倍数都落在同一条直线上,所以单个向量张成了一个一维空间。在 s = 0 处箭头消失了:零向量有长度但没有方向,这也是为什么每一个要除以长度的公式,都必须对长度为零的情况给个说法。

加法就是走路。把 v 的尾巴接到 w 的尖端,走到哪儿,哪儿就是它们的和。拖动 v,看平行四边形怎么合上:

v = [−1.0, 2.0]
v = [−1.0, 2.0]

这个平行四边形就是“顺序无关”的证明:先走 w 再走 v,和反过来走,终点是同一个角。按槽位看无非是 [3, 1] + [−1, 2] = [2, 3] —— 而这张图正是它在 768 维里依然显然的原因。

它们真正生活的地方就是高维。GPT-2 的一个 token 是 768 维向量, Llama 3 70B 用 8192 维。高维不只是“多几个数”这么简单:在那里随手画两个方向,几乎总是接近垂直。把维数拉高,看成对夹角构成的整团样本怎样坍缩到90° 上:

2 维 · 中位数 88°

在 d = 2 时夹角铺满整个区间;到 时它们全挤在离垂直只有几度的地方,而且宽度按 1/√d 收缩。这叫测度集中,也正是余弦相似度能当信号用的根本原因:在一个“默认关系就是 90° 不相关”的空间里,一对量出 30° 的向量,是真的在说事情。

02

矩阵对网格做了什么

四个数,整个平面同时移动。这四个数只说了两个箭头落在哪里,其余全是被带过去的。

矩阵是一个搬动整个平面的函数,而且它只用一种搬法:让网格线保持笔直、平行、间距均匀。这个约束强到什么程度?强到你只需要说清两个箭头去了哪里:指向 (1, 0) 的î,和指向 (0, 1) 的 ĵ。拖动 î,看整张格子跟着走:

第 1 列 = [1.0, 0.0]
第 1 列 = [1.0, 0.0]

注意你真正在拖的东西:矩阵的第一列。括号里那四个数,就是两个落点并排写在一起 —— 第一列是 î 去的地方,第二列是 ĵ 去的地方,除此之外哪里都没有再存别的东西。

列一定,每个点就都定了。任何向量都是 v₁î + v₂ĵ,所以它的像必然是 v₁(新 î) + v₂(新 ĵ) —— 乘开来正好就是那套"行乘列"的算术,只不过是从图里看出来的,不是背下来的。拉动滑块,看 v 骑在弯曲的网格上一起走:

已施加 0%

注意 v 从没离开过自己那一格。它出发时是沿两个旧基向量各走一步,到达时是沿两个新基向量各走一步:Mv = [2·1 + 1·1, 0.5·1 + 1.5·1] = [3, 2],正是读数里那一对(把变换再看)。算术和图说的是同一件事。

“笔直、平行、间距均匀”有个名字:线性,写出来就是 M(u + w) = Mu + Mw 和 M(su) = s(Mu)。大多数函数都不满足它 —— 平方不满足,阈值化也不满足。施加变换,看那个平行四边形是怎么活下来的:

已施加 0%

因为加法能活下来,你可以先变换再相加,也可以先相加再变换,落点一样。正是这条性质让一个权重矩阵可以一次性作用在一整批 token 向量上。它也解释了为什么偏置必须加在矩阵外面:常数平移恰恰是线性映射做不到的事,因为那要把整张网格从原点挪开。

四个数里有两条信息就够说清变换制造或毁掉了多少面积。单位正方形变成一个平行四边形,它的有向面积就是行列式。把滑块往右推,直到面积变成零:

det = 1.00

在 处行列式为零,平面被压扁成一条线:第二列正好变成第一列的两倍,两个箭头张不出任何面积,输入里一整个方向从此映射到同一个点。再往前,行列式变成负数 —— 变换把平面翻了个面,ĵ 从 î 的逆时针一侧跑到了顺时针一侧。负行列式是定向翻转,不是错误。

有一族变换只改方向,别的什么都不动。旋转把第一列和第二列都放在单位圆上,相差四分之一圈:[cos θ, sin θ] 和 [−sin θ, cos θ]。转动它,看行列式怎么死死钉在 1 上:

0°

因为两列都保持单位长度、保持垂直,每一个长度和每一个夹角都原封不动地活了下来。这就是正交矩阵,QᵀQ = I 是同一句话的符号写法。正交矩阵是那种你可以连乘一千次而不会让任何东西膨胀或衰减的矩阵 —— 所以凡是以数值稳定为要害的地方,都能看见它们。

变换用乘法复合,而顺序本身就是答案的一部分。AB 的意思是先施加 B —— 这个记法要从右往左读,人人都在这儿栽过。走完两步,再交换顺序,看落点怎样离开另一种顺序给出的位置:

已施加 0%

矩阵乘法不可交换,图已经说明了原因:把旋转过的方块再错切,和把错切过的方块再旋转,形状不一样。这是模型代码里最常见的静默 bug —— 矩阵是方阵时,x @ W 和 W @ x 都能通过类型检查,都会算出有限的数字,只有损失曲线会抱怨 —— 两个答案的差别,正如上面后另一种顺序的差别。

没有压扁任何东西的变换,是可以撤销的。M⁻¹就是把每个点送回去的那个变换,所以 M⁻¹M = I。单步走一遍:先施加 M,再撤销它:

未变换

网格分毫不差地回来了,而这只有在 det(M) ≠ 0 时才可能 —— 出去的路上什么都没丢。行列式为零时根本不存在逆矩阵:两个不同的输入早就落到了同一个输出上,没有哪个函数能把一个点送回两个地方。 “奇异矩阵没有逆”和“平面被压扁了”,是同一句话。

03

点积衡量的是“合拍程度”

两个向量进去,一个数出来。这个数关心的不是大小,而是两者朝同一方向的程度。

定义是心算就能做的算术:对应槽位相乘再全部加起来,v · w = v₁w₁ + v₂w₂ + …。而这个定义恰恰藏起了它唯一重要的性质。拖动 v 绕着 w 转,盯着那个数看:

v = [1.2, 2.4]
v = [1.2, 2.4]

注意符号在哪儿翻转。v 和 w 朝同一侧时它是正的,恰好成直角时是零,倒向另一侧后变负。它衡量的是合拍程度,再乘上两者的长度。

几何上的读法是一道影子。从 v 的尖端向 w 所在的直线作垂线,垂足落在‖v‖ cos θ 处,点积就是这个长度乘以 ‖w‖。把夹角收小,看影子怎么变长:

30°

因为 v · w = ‖v‖ ‖w‖ cos θ,你拿来计算的定义和你脑中想象的定义是同一个数,而把它们缝在一起的正是那道影子 —— 逐槽相乘的形式是硬件跑的那个,余弦的形式是你推理用的那个。两者谁都不是谁的近似。

有一个角度比其他所有角度都重要。把 v 扫过直角,看那个数在路上穿过零点:

20°

恰好 时点积是零,不管两者多长。这就是正交的定义,也是后面两节赖以成立的检验:两个方向携带独立信息,当且仅当它们的点积为零。

让向量和自己做点积,夹角是零,余弦是 1,剩下的就只有长度的平方。拖动 v,看以它为边的正方形:

‖v‖² = 13.00
‖v‖² = 13.00

于是 ‖v‖ = √(v · v),范数从来就不是另一个概念:它就是两个参数相同的点积 —— 也就是你刚拖过的那个正方形。这也是 L2 成为机器学习默认范数的原因 —— 它是内积白送给你的那个,而且它的导数是干净的 2v。

把两者的长度都除掉,剩下的就是纯粹的合拍程度:cos θ = v·w / (‖v‖‖w‖),无论向量多大,它都落在 [−1, 1] 里。拖动那个短箭头,看余弦如何对你把它拉多长完全无动于衷:

[1.2, 0.4]
[1.2, 0.4]

盯住单位圆上那两个点。它们是两个箭头除掉长度之后的落点,而余弦只认识它们。这就是检索系统用余弦而不是裸点积排序的原因:不除长度,一篇长文档会仅仅因为长而压过一篇真正相关的。

代价才是它无处不在的理由:d 次乘法加 d − 1 次加法,一趟扫完,没有分支,完美向量化。拉高宽度,从曲线上读出这个数:

2 维

在 上,一个注意力分数要 1535 次浮点运算 —— 单看不算什么。但注意力要为每一个有序的 token 对各算一个,所以在 1024 token 的上下文里,那是每层每头 1048576 次点积。大家抱怨的那个平方复杂度,就是这张图跑了这么多遍。

04

矩阵乘法就是一摞点积

乘积的每一个格子都是一次点积。现代模型里所有昂贵的东西,都来自格子的数量。

矩阵乘法没有引入任何新东西:AB 的第 (i, j) 个格子,就是 A 的第 i 行与 B 的第 j 列做点积。这也正是形状必须在中间对上的原因。把内维滑离 4,看乘积怎样直接不存在:

B 的行数 = 4

注意这是形状层面的失败,不是数值层面的:在 时,乘积那一块根本没有可以装的答案,所以所有框架都会在这里抛错,而不是算出一堆垃圾。这是整个技术栈里最友善的错误,也是把 (3×4)(4×2) → 3×2 记成“内侧一对相消”的理由。

形状对上以后,乘积一次填一个格子。单步走过这 6 个格子,看每个格子吃掉的是A 的哪一行和 B 的哪一列:

第 1 个 / 共 6 个

每个格子要 4 次乘法和 3 次加法,一共 6 个格子:3×4 乘 4×2 正好 42 次运算。一般地,M×K 乘 K×N 恰好是 MN(2K − 1),大家都把它约成 2MNK,因为在真实宽度下少掉的那个 MN 相比之下可以忽略。这个计数跟数值本身毫无关系 —— 矩阵乘法没有快速路径也没有提前退出,而这恰恰是可以为它专门造硬件的原因。

真实负载从不一次只推一行。把 N 个 token 向量摞成一个矩阵,一次乘法就把它们全都乘上同一份权重。把批量拉大,看只有输入块和输出块在长:

1 个 token

因为权重只读一次却用了 N 次,每取一字节权重所做的工作量随 N 上升 —— 这就是批处理让加速器变快的全部原因。在 N = 1 时,你要搬 768² 个权重(bf16 下 1.2 MB)来做 120 万次乘加:每字节一次浮点运算,算术单元只能干等内存。到 时,同样这些字节干了 32 倍的活。

另一个轴更糟,因为加宽模型会让权重块的两条边同时变长。下面的方块是按比例画的 —— 把模型加宽,看权重按平方增长,而 token 数原地不动:

宽度 64

从 到 ,宽度是 10.7 倍,算术量是 114 倍:那一次投影在 2048 个 token 上从 2.4 GFLOP 涨到 275 GFLOP,在 H100 的 989 TFLOP/s 稠密 BF16 上是 0.28 毫秒(NVIDIA 数据手册,2023)。而每层每个注意力块里有 4 次这样的投影,之后前馈网络还会把账单再翻一倍。

还有一个动作出现在每一份注意力实现里:转置。Aᵀ 装的是同一批数,只是下标交换了 —— 但底下的字节一个都没动。翻转它,看读取顺序变成了什么样:

已翻转 0%

因为数组是一行接一行存的,沿着列往下读,每一步跨的是整整一行的宽度,而不是一个元素。真实矩阵一行 768 个浮点数时,这个跨度是 3072 字节,于是每个元素都落在自己的 64 字节缓存行上,取回来的 16 个浮点数里有 15 个被扔掉。算术完全一样,墙上时钟并不一样 —— 所以各种库会把转置融进乘法里,而不是真的把它算出来。

05

投影就是留下合得上的那部分

把一个向量拆成沿某方向的部分和不沿它的部分。最小二乘、注意力、PCA,全是这一招。

模型对向量做的事,几乎都能归结成一句话:留下它沿某个方向的那部分,其余扔掉。拖动 v,看它怎样裂成沿直线的那部分和垂直于它的那部分:

v = [1.0, 2.6]
v = [1.0, 2.6]

注意这两块永远能加回成 v,而且垂直的那块永远和直线成直角。写成公式是 p = (v·a / a·a) a—— 点积挑出份量,方向提供其余。在初始位置上 p = [1.84, 0.92],r = [−0.84, 1.68]。

垂足不只是直线上某一个点,它是最近的那个 —— 而这句话是可以亲手检验的,不必照单全收。让点沿直线滑动,看到 v 的距离怎么变:

t = 0.00

滑过最低点时盯住右边那个数。(v − q) · a恰好在距离最小处为零,别处都不为零:最小化平方距离和让残差正交,是同一个条件。正是这个等价关系,把微积分问题变成了线性代数问题。

最小二乘就是这张图多加几行。7 个点、1 条直线、7 条残差 —— 最好的那条线,就是残差向量与这条直线能触及的一切都正交的那条。转动斜率,看总和怎么变:

斜率 = 0.00

水平那条线留下 Σr² = 7.046;最小二乘斜率是 0.575,滑块能到达的最近位置 留下 0.837。论证就是你刚才亲手驱动的那一个:AᵀA x̂ = Aᵀb说的正是“残差与 A 的每一列正交”,这也是正规方程不再像是凭空变出来的原因。

误差对每个参数都是二次的,所以它构成的曲面只有一个底,再无其他。上下平移直线,看那条抛物线穿过它的最小值:

截距 = −1.20

最低点落在处;因为这条曲线是抛物线,在它上面做梯度下降不可能卡住,直接求解也才成为可能。这是平方误差独有的性质:换成绝对值误差,底部会变成一道没有导数的折痕 —— 所以 L1 回归需要的是另一套算法,而不是另一个常数。

投影唯一做不到的事是记住。垂直于该方向的那条直线上的每一个向量,都落到同一个垂足,所以这个映射不可逆。让向量沿那条线滑动,看左边那个数怎样纹丝不动:

偏移 = 0.0

这一组向量就是零空间,它的存在正是“秩亏”从内部摸上去的手感。当设计矩阵里有两列互为倍数时,拟合会有一整条同样好的解,AᵀA 奇异,而求解器要么抛错 —— 要么更糟,悄悄返回浮点噪声碰巧偏爱的那一个。

06

矩阵不去动的那几个方向

几乎所有向量从矩阵里出来时都换了方向。少数没换的那几个,才是矩阵真正在做的事。

把前两节那个变换拿来,让一个向量穿过它。绝大多数时候,Av 指的方向和 v 不一样。转动方向,去找那些让两个箭头叠在一起的角度:

70°
70°

恰好有两个,在 和 ,而在那里读数会把 λ 交给你: 2.50 和 1.00。特征向量就是矩阵只会缩放、不会转动的方向,它的特征值就是那个缩放倍数 —— Av = λv,一整个矩阵在这条线上塌缩成了一个数。

从网格看,这两个方向就是弯曲怎么也移不开的两条直线。施加变换,看特征线怎样守住自己的角度,而它们之间的一切都在摆动:

已施加 0%

这正是 A = PDP⁻¹ 想说的事:把任意向量改写到特征向量这套基上,矩阵就变成了乘两个互不干涉的数。整份收益就在这里 —— 也是 Aᵏ 只要对每个特征值做一次乘方、而不必做 k 次矩阵乘法的原因。

它还预言了反复作用会发生什么。把任意初始向量一遍遍乘上 A,沿最大特征值那个分量会指数式地压过其余分量。单步走过 8 次作用,看方向怎样安定下来:

作用 0 次

从离主特征向量 80.5° 出发,8 步后只差 0.075°:误差每次按 λ₂/λ₁ = 0.4 收缩。这就是幂迭代 —— PageRank 当年真的是这么算的 —— 也是同一套算术,让 λ₁ > 1 的循环网络爆炸、让 λ₁ < 1 的循环网络遗忘。

并不是每个矩阵都有这样的方向。旋转把每个向量都转过同样的角度,一个都活不下来。扫过整整一圈,看那个数怎样始终够不到零:

0° · 从未共线

这个角在哪儿都是 40.0°,因为真正的特征向量必须是旋转固定住的方向,而它一个都没有。它的特征值是复数,而这两句话说的是同一件事。所以“是不是每个矩阵都能对角化”,在实数域上答案是否定的 —— 而像 [[1,1],[0,1]] 这样的亏损矩阵,在复数域上也照样不能。

有一族矩阵永远规矩。当 A = Aᵀ 时,无论元素取什么值,特征向量都保证是实的、而且两两垂直。把非对角元推到任何地方,看那个直角怎样活下来:

b = c = 0.60

那个直角就是谱定理,也是协方差矩阵、Gram 矩阵和 AᵀA —— 个个天生对称 —— 成为所有算法首选对象的原因。它同时也是通向下一节的桥:即使 A 不是方阵,AᵀA 依然是对称的。

07

任何矩阵都是:旋转、拉伸、再旋转

奇异值分解把特征值那套的两个前提都丢掉了,却几乎保住了全部收益。

特征向量需要方阵,而且不保证存在。SVD 两样都不需要,它的图景只有一句话:任何矩阵都把单位圆送成一个椭圆。施加变换,看那个圆怎样张开:

已施加 0%

两条半轴就是奇异值,σ₁ = 2.558 和 σ₂ = 0.977:这个矩阵能把任何东西拉伸的最大与最小倍数。对任意 v 都有 ‖Mv‖ ≤ σ₁‖v‖,只有沿长轴才取等号 —— 所以 σ₁ 就是算子范数,不是它的比喻。

反过来读,这个椭圆就给出了分解:圆是靠三步走到这儿的 —— 旋转输入的坐标架、沿坐标轴缩放、再旋转结果。逐步走一遍:

圆

于是 M = UΣVᵀ,其中 U、V 正交,Σ 对角且非负 —— 而且每个矩阵都有这样一份分解,长方形也好、奇异也好、秩亏也好。注意那两个箭头:它们在圆上出发时垂直,每一步之后仍然垂直,因为只有 Σ 会改变长度,而它是沿自己的坐标轴改变的。

因为 Σ 是排好序的,小的那头可以扔掉。只保留前 k 个奇异方向,得到的就是最好的秩 k 逼近。把 k 拉高,看形状怎样回来:

秩 0

时椭圆已经塌成一条线段,矩阵有 35.7% 不见了; 时精确无误。这个“最好”是 Eckart–Young 定理,不是经验法则:无论按 Frobenius 范数还是算子范数,都没有更接近的秩 1 矩阵。它就是 PCA、潜在语义分析和一切低秩适配器背后的压缩论证。

两条轴的比值有名字,也有账单。把短轴压向零,看条件数怎样失控:

σ₂ = 1.000

κ = σ₁/σ₂ 是输入里的相对误差走到输出时可能被放大的倍数。它才是决定一次求解值不值得信的那个数 —— 而 det ≠ 0对此什么也没说,因为一个矩阵完全可以行列式为 1、条件数为 10⁸。把 σ₂ 压到 ,κ 就到了 160。

代价是用位数计的。κ 每涨十倍,答案就被吃掉一位十进制有效数字,而一种格式统共就那么多位。抬高条件数,看位数怎样消失:

κ = 10^0

float32 大约有 7.2 位十进制有效数字,所以在 时只剩 1.2 位,答案不过是带小数点的噪声。float64 的 16.0 位挺过同一次求解还余 10.0 位。这就是为什么线性求解器会报告条件数估计,以及为什么“它没抛异常”并不能证明答案有任何意义。

同样的截断论证,放到模型规模上跑,就是低秩微调之所以有效的原因。 4096 见方的权重矩阵是 1680 万个参数;一个秩 r 的更新只是两块薄板。把秩拉高,读出它占的比例:

秩 1

时这份更新是 65536 个参数 —— 占该层的 0.39% —— 而它能表达任何“第 8 个之后的奇异值都很小”的改动。这是一个关于更新形状的赌注,而不是关于它大小的赌注,也正是 LoRA 下的那一注。

同一个定理最后再读一遍。去中心化数据矩阵的奇异方向,就是数据真正铺开的方向,这就是 PCA。转动那条直线,看投影后那些点的散布:

0°
0°

散布在 32° 处达到峰值 2.089,而总量是 2.280 —— 一个方向就承载了 91.6% 的方差,所以这团点几乎是一维的。保留协方差的头几条轴就保住了散布;这就是一句话版的 PCA,也是上面那个秩截断换了身衣服。

08

四种不声不响的失败

形状错了会抛异常,这四种不会。

顺序。方阵下 x @ W 和 W @ x 都能过类型检查,只有损失曲线会抱怨。条件数。行列式远离零对 κ 什么也没说。归一化。裸点积排序偏爱长向量。内存顺序。转置在下标上免费,在缓存行上昂贵。