0.3 标量、向量、矩阵、张量:按轴理解形状
本节位置:模块 0 · 准备 | 前置:0.2 | 预计:30 分钟
一句话:张量就是「用多个下标索引的数组」;形状是每个下标能取多少个值。
0. 场景引入
晚上 8 点,手机相机的「夜景模式」正在合成一张照片。它一次连拍 12 帧 4000×3000 的 RAW,每帧三个通道,fp16 存放。
算一下:;再加一块同样大小的输出缓冲,就是 1.73 GB。而相机进程从系统拿到的内存预算只有 1.5 GB。
决定:12 帧塞进一个 [12,3,3000,4000] 的张量一次算完(快,但 1.73 GB 超预算),还是切成分块逐块处理(省内存,但边界要处理)?算错就是相机进程被系统杀掉——用户按下快门,看到闪退,这张照片永远没了。上一次的妥协是把分辨率降到 2000×1500,照片糊得用户投诉;这一次产品要求全分辨率输出。
更麻烦的是,帧数、分辨率、通道数、dtype 这四个数字里任何一个变了,预算就全变:上周把帧数从 8 提到 12,内存直接多了一半。
要做出这个决定,你得能看着一个形状说出三件事:每个轴是什么、一共占多少字节、哪个操作会悄悄复制一份。
读完本节,你应该能回答三个问题:
[B, T, D]与[B, H, T, d_k]里,每个轴分别代表什么?reshape与transpose的区别,为什么一个几乎免费、另一个不一定?- 为什么 batch 轴总放在最左边?
1. 直觉
先玩一个「数轴」游戏。一个数组有几个轴?看它需要几个下标才能定位一个数:
| 对象 | 例子 | 需要几个下标 | 形状 |
|---|---|---|---|
| 标量 | 0 个 | [] | |
| 向量 | 1 个:x[i] | [3] | |
| 矩阵 | 2 个:A[i,j] | [2,2] | |
| 三维张量 | 一批矩阵 | 3 个:T[i,j,k] | [2,2,2] |
💡 一句话直觉:轴 = 一种「分类维度」;形状 = 每种分类各有几项。
比如一批句子:[B, T, D]。
- 轴 0(
B):第几个句子; - 轴 1(
T):句子里第几个 token; - 轴 2(
D):这个 token 的向量有多少维。
x[2, 7, 100] 就是「第 3 个句子的第 8 个 token 的第 101 个特征」。
1.1 它在现实世界里对应什么
张量像一栋立体仓库:每个轴是一种分类方式(第几栋楼、第几层、第几个货位),形状是每种分类的格数,索引是「报出三个编号」。reshape 相当于换一张货位编号表——货物一件不动,只是编号规则变了;transpose 相当于改变「按哪一层优先取货」的读法,若新读法与内存里的摆放顺序不一致,叉车就得真跑一趟(复制数据)。
量级:一张 4000×3000 的 fp16 三通道图是 ;12 帧就是 864 MB。一个 [1,32,4096,4096] 的 fp16 注意力矩阵是 1 GiB(§8 复算)。张量本身不贵,同时存在的副本才贵。
这个类比在哪里失效?仓库取货有物理距离,而张量的「距离」只是地址算术;真正决定快慢的是连续内存。reshape 常常只改一个整数;transpose 之后若内存不连续,下一次 matmul 会先把它拷贝成连续块——这一步不写在代码里,却可能吃掉一半时间。
2. 定义
2.1 张量
一个 阶张量(rank- tensor,这里的 rank 指轴的个数,不是矩阵的秩)是
它的形状(shape)是元组 ,元素个数是 。
- :标量;
- :向量;
- :矩阵;
- :高阶张量。
张量分析(tensor calculus)的成套规则——协变指标、缩并与协变导数——由里奇与列维-奇维塔在 1900 年系统化。那时它服务于微分几何与广义相对论;今天深度学习借用的只是「多下标数组 + 每个轴独立索引」这一层,不涉及坐标变换。
⚠️ 注意:PyTorch 里
tensor.dim()返回轴的个数;线性代数里 是矩阵的秩。两个「rank」不是一回事(第 2.6 节)。
2.2 索引、切片、轴
- 索引:
x[i]取第 个元素(代码从 0 开始)。 - 切片:
x[:, 0, :]固定第 1 轴为 0,保留其余轴。 - 轴(axis):运算沿哪个方向进行。
sum(axis=1)表示「把轴 1 压掉」。 - 转置 / 换轴(permute):重排轴的顺序,元素不变。
2.3 变形(reshape)
reshape 把形状从 改成 ,只要元素总数不变:
它不改变内存中的元素顺序,只是换一种「怎么索引」的解释。因此 reshape 之后,哪个元素落到哪个位置,完全由「行优先展开」决定。
2.4 广播(broadcasting)
两个形状做逐元素运算时,NumPy/PyTorch 从最右边的轴开始对齐:
- 维度相等 → 可以;
- 其中一个为 1 → 把 1 拉伸成对方长度;
- 缺失的轴 → 在左边补 1;
- 否则报错。
例:[B,T,D] + [D] → [B,T,D],因为 [D] 被看作 [1,1,D] 后沿前两个轴复制。
这套「从最右轴对齐」的规则不是框架的随意选择:Harris 等(2020)描述的 NumPy 数组模型给出了 axis、reshape 与 broadcasting 的正式语义,PyTorch 基本沿用。
3. 为什么这样定义
如果每个样本都单独存成矩阵,代码里就要写循环:
for b in range(B):
for t in range(T):
h[b][t] = W @ x[b][t] + bias
GPU 最讨厌这种小循环。把 batch 和序列塞进额外两个轴,一次 matmul 就能并行算完 个向量。多出来的轴不是为了数学,而是为了并行。
同样,多头注意力把 拆成 ,用轴表示「第几个头」,就能让所有头同时计算。轴是并行维度的载体。
3.1 反事实:不用「形状 + 轴」,只存一段扁平内存
假设张量定义成「一段连续内存 + 你脑子里记住的偏移公式」。同一个三阶数组,行优先与列优先给出不同地址;取形状 ,找元素 :
差 1 个元素。没有报错、没有越界,你只是读到了隔壁那个像素。
荒谬之处一眼可见:把这套偏移用在 attention 的 causal mask 上,错位一格意味着每个 token 都能看到下一个 token——训练 loss 异常低(模型在抄答案),推理时却完全不会生成。用在夜景降噪上,则会把两个不同位置的像素做平均,出现「鬼影」:静止物体边缘有一圈重影,而画面里没有任何一个数看起来是错的。
形状与轴的作用,就是把这套偏移公式从人脑搬进类型系统:框架替你算地址、检查越界、决定 transpose 要不要真的搬数据。
4. 性质与推导
4.1 元素总数守恒
reshape 前后必须相等。例:[2,3,4] 有 24 个元素,可以 reshape 成 [6,4]、[2,12]、[24],但不能变成 [5,5]。
4.2 批量矩阵乘法
对 A: [B, m, n] 与 B_mat: [n, p],A @ B_mat 的结果是 [B, m, p],定义为对每个 :
即矩阵乘法只作用在最后两维,前面的轴是「批量」。这就是深度学习框架的默认语义。
4.3 拆头与合头
设 X: [B, T, D], 个头,。
X.reshape(B, T, H, d_k) # [B, T, H, d_k]
.transpose(1, 2) # [B, H, T, d_k]
拆头不是数学变换,只是把隐藏维度 重新解释为 个长度为 的子空间;换轴是为了让 matmul 在最后两维( 与 )上计算注意力。这套「拆头 → 打分 → 合头」的流程来自多头注意力(Vaswani 等, 2017),[B, H, T, d_k] 也就成了事实上的标准形状。
4.4 广播的形状推导
[B, H, T, d_k] * [T, T]:
- 右对齐:
d_kvsT→ 不等且都不为 1 → 报错; - 正确写法是
[B,H,T,T] @ [B,H,T,d_k],或先把 mask 扩成[1,1,T,T]。
5. 例子
5.1 手算:形状与索引
设 X 形状 [2, 3, 4](B=2, T=3, D=4)。
X[1, 2, 3]:第 2 个样本、第 3 个 token、第 4 维;X[:, 0, :]:所有样本的第 1 个 token,形状[2, 4];X.sum(axis=1):对 token 求和,形状[2, 4];X.transpose(0, 2, 1):形状[2, 4, 3];X.reshape(2, 12):形状[2, 12],元素顺序不变。
5.2 场景:一次多头注意力的形状旅程
| 步骤 | 操作 | 形状 |
|---|---|---|
| 输入隐藏状态 | — | [B, T, D] |
| 线性投影 | X @ Wq | [B, T, D] |
| 拆头 | reshape + transpose | [B, H, T, d_k] |
| 打分 | Q @ K.transpose(-2,-1) | [B, H, T, T] |
| 缩放 + mask + softmax | 逐元素 / 沿最后轴 | [B, H, T, T] |
| 加权 value | P @ V | [B, H, T, d_k] |
| 合头 | transpose + reshape | [B, T, D] |
| 输出投影 | @ Wo | [B, T, D] |
验算元素数:[B,H,T,d_k] 与 [B,T,D] 的元素数相同,因为 。合头之所以能成功,靠的就是这个守恒。
5.3 场景:KV Cache 的形状
推理时缓存历史 key/value:[B, H, T_cache, d_k]。每生成一个新 token,只追加一列(沿 轴),不必重算历史。显存占用约为
系数 2 是 K 和 V 各一份。这个公式在第 11.7 节会展开。
5.4 场景闭环:12 帧夜景与 1.5 GB 预算
输入:12 帧 [1,3,3000,4000] fp16,单帧 72 MB,共 864 MB;输出缓冲同样大小,合计 1.73 GB,超出 1.5 GB 预算。
计算:按 512×512 分块, 向上取整得 8 列、 得 6 行,共 48 块。单块 [12,3,512,512] 只要
48 块顺序处理,峰值内存从 1.73 GB 降到约 20 MB(输入块 18.9 MB + 输出块 1.6 MB)。
输出:合成一张 4000×3000 的图,写回相册。
成本:省下的是内存,付出的是时间与重复计算。每块四周各多算 32 像素做融合,面积从 变成 ,计算量多约 ;按每块 40 ms 估算,48 块约 1.9 s——夜景模式那个「处理中」的转圈就是这么来的。
失败条件:
- 轴顺序错:把
[B,C,H,W]当[B,H,W,C]索引,通道被当空间、空间被当通道,输出是色彩错乱的马赛克,而所有形状检查都通过。兜底:函数签名里写明布局,或用带名字的轴(einsum标bchw)。 - 配准漂移:12 帧手持拍摄有位移,对齐错 2 像素,合成后边缘发虚。兜底:先全局对齐(特征点/光流),再分块细化。
- 接缝亮暗:块与块各自估曝光,拼出网格状明暗。兜底:重叠区加权融合,全局增益只估一次。
6. 常见误区
-
误区:张量的「阶」和矩阵的「秩」是一回事。 纠正:阶(rank/ndim)是轴的个数;矩阵的秩是列空间的维数(第 2.6 节)。一个
[2,3]矩阵阶为 2,秩可能是 1。 -
误区:
reshape会重排元素。 纠正:reshape只改解释方式,不移动数据;transpose/permute才改变轴的顺序。两者组合使用时,顺序很容易错,务必用形状和小例子验证。 -
误区:广播是「自动对齐所有维度」。 纠正:广播从最右轴对齐,且只有「相等」或「其中一个为 1」才合法。
[B,T,D] + [B,D]会失败,因为右对齐后T对B不匹配。 -
误区:batch 轴也是「特征」。 纠正:batch 轴只是并行打包,不参与特征语义;对 batch 求和/平均是统计操作,不是模型的一部分。
7. 概念网络
- 前置:0.2 数学语言 —— 指标与求和。
- 后续:1.1 向量 —— 一阶张量的几何意义。
- 后续:3.3 矩阵乘法 —— 批量矩阵乘法的语义。
- 后续:11.3 Attention —— 形状链的完整应用。
- 关联:12.1 einsum —— 用指标统一描述所有轴运算。
8. 通往大模型:把 [B,T,D] 刻进脑子
大模型里 90% 的形状都是这四个字母的变体:
[B, T, D]:隐藏状态、embedding 输出、FFN 中间(4D);[B, H, T, d_k]:每个头的 Q/K/V;[B, H, T, T]:注意力权重(显存杀手,随 增长);[V, D]:embedding 表, 是词表大小;[D, 4D]、[4D, D]:FFN 的两个权重;[D, r]、[r, D]:LoRA 的低秩矩阵,。
可检验结论:对 ,注意力矩阵 [1,32,4096,4096] 用 fp16 存储需要
单层、单头矩阵就要 1 GiB,这就是长上下文必须先解决注意力复杂度或做分块的原因:FlashAttention 用分块与在线 softmax,始终不把完整矩阵写进显存。
如果这一步算错,用户会看到什么:把 [B,H,T,d_k] 和 [B,T,H,d_k] 混用,每个头拿到的是别的 token 的特征,输出「局部通顺、整体错位」——句子前半段正常,后半段开始把主语和宾语对调。若 mask 的 [T,T] 在广播到 [B,H,T,T] 时轴对错,padding 会被当成内容,模型在句尾不停吐填充符号。最麻烦的是形状对得上、语义对不上:框架不报错,只有人读输出才发现。
import numpy as np
B, T, D, H = 1, 4096, 4096, 32
d_k = D // H
print("attention 矩阵元素数:", B * H * T * T)
print("fp16 显存 GiB:", B * H * T * T * 2 / 1024**3)
# 1.0 GiB
9. 练习
- 概念:
[2, 3, 4]与[6, 4]的元素数相同,它们能互相 reshape 吗?[4, 6]呢? - 计算:
A: [8, 5, 3],B: [3, 7],A @ B的形状是什么?如果改成A: [8, 5, 3],B: [8, 3, 7]呢? - 场景:解释
[B, T, D]经过reshape(B, T, H, D//H).transpose(1, 2)后,为什么元素总数不变,以及每个轴现在代表什么。 - 开放:为什么框架不把 batch 轴放在最后,而是放在最前?提示:考虑内存布局与广播。
参考答案
[2,3,4] ↔ [6,4]可以(24 个元素);[4,6]也可以 reshape,但元素顺序不同,语义会变,需谨慎。[8,5,3] @ [3,7]→[8,5,7];[8,5,3] @ [8,3,7]是批量矩阵乘,→[8,5,7](逐 batch)。- 元素数 不变;轴变为
[batch, head, token, 每头维度]。 - 最左轴是「最外层循环」,与行优先内存布局一致;广播也从右对齐,把 batch 放最左最不容易误伤特征轴。
10. 一页速记
- 阶 = 轴的个数;形状 = 每个轴的长度;元素数 = 各轴长度之积。
- 索引定位元素,切片保留部分轴,
sum(axis=k)压掉第 轴。 reshape只改解释,transpose/permute改轴顺序;两者别混。- 批量矩阵乘只作用在最后两维;广播从最右轴对齐。
- 大模型四件套:
[B,T,D]、[B,H,T,d_k]、[B,H,T,T]、[V,D]。 - 物理意义 / 现实对应:一栋立体仓库——轴是分类方式,索引是货位编号;
reshape换编号表,transpose可能要真的搬货。
11. 参考文献与致谢
这一页的知识不是凭空来的。下面列出本页直接借用的原始工作与权威整理;正文中的人名、定理名已链接到对应条目。链接优先指向 DOI、arXiv 或可长期访问的原文存档。
- Vaswani 等(2017):Attention Is All You Need。多头注意力把 拆成 ;本页 §4.3 的拆头/合头与
[B,H,T,d_k]形状链都由这篇论文固定下来。arXiv:1706.03762 - Harris 等(2020):Array programming with NumPy。本页 §2.2–2.4 的 axis、reshape、broadcasting 语义,以这篇论文对 NumPy 数组模型的描述为准。Nature 585
- Dao 等(2022):FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness。用分块与在线 softmax 避免显式写出
[B,H,T,T]矩阵,正是本页 §8 那个 1 GiB 估算要绕开的对象。arXiv:2205.14135 - 里奇与列维-奇维塔(Ricci-Curbastro & Levi-Civita, 1900):Méthodes de calcul différentiel absolu et leurs applications。把「多个下标、每个轴独立变换」的张量运算系统化为绝对微分学;本页「张量 = 多下标数组」的数学前身。Math. Ann. 54
下一篇 → M01 向量与几何
随堂测 · 4 题
源文件:00-准备/03-标量向量矩阵张量.md