张量运算:深度学习的通用语言 本节摘要:张量(Tensor)是数据与深度学习之间的共同语言——每一张图像、每一个句子、每一个梯度都流经它。矩阵只能描述两组事物之间的成对关系,而真实数据无法塞进二维:32 张 224×224 的 RGB 图是 4 维张量 ,12 个注意力头的自注意力是 。本节从几何与存储双重视角,讲透张量的三大基石:形状(Shape)、步长(Strides)、广播(Broadcasting);给出从零实现的 类(扁平存储 + 步长索引),支持 reshape、transpose、squeeze/unsqueeze、逐元素运算与归约;演示 einsum(Einstein
本节摘要:张量(Tensor)是数据与深度学习之间的共同语言——每一张图像、每一个句子、每一个梯度都流经它。矩阵只能描述两组事物之间的成对关系,而真实数据无法塞进二维:32 张 224×224 的 RGB 图是 4 维张量
(32, 3, 224, 224),12 个注意力头的自注意力是(batch, heads, seq_len, head_dim)。本节从几何与存储双重视角,讲透张量的三大基石:形状(Shape)、步长(Strides)、广播(Broadcasting);给出从零实现的Tensor类(扁平存储 + 步长索引),支持 reshape、transpose、squeeze/unsqueeze、逐元素运算与归约;演示 einsum(Einstein 求和)这一万能张量运算符,用一行表达式写出点积、外积、矩阵乘、批量矩阵乘、注意力分数;最后把多头注意力从头到尾用 einsum 走一遍,证明掌握张量运算后,形状错误便从「玄学」变成可机械调试的小事。
对应原课程:Phase 01 · Lesson 12 ·
tensor-operations(原英文phases/01-math-foundations/12-tensor-operations/docs/en.md)。前置:第 1、2、3 节(线性代数)。
阅读完本节,你应当能够:
Tensor 类,含形状、步长、reshape、transpose 与逐元素运算。你搭了一个 Transformer,前向传播看起来很干净,一跑就报错:RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x768 and 512x768)。你盯着形状看,试着转置,现在它说 Expected 4D input (got 3D input);你加了个 unsqueeze,别的地方又崩了。
形状错误是深度学习代码里最常见的 bug。它们在概念上不难——每个算子都有一份形状契约——但它们会飞速繁殖。一个 Transformer 把几十个 reshape、transpose、broadcast 串在一起,一个轴搞错,错误就级联放大。更糟的是,有些形状错误根本不抛异常:沿错误的轴广播、在错误的轴上求和,会悄悄产出垃圾。
矩阵处理的是两组事物之间的关系。真实数据塞不进二维。你需要一个能推广到任意维数、且运算能跨维干净组合的数据结构——这就是张量。掌握它的运算,形状错误就变得轻而易举。
张量是一个具有统一数据类型的多维数组。维数称为秩(Rank/阶),每一维是一个轴(Axis),形状(Shape) 是列出各轴大小的元组。
元素总数 = 各轴大小之积。形状 (2, 3, 4) 含 2*3*4 = 24 个元素。
不同数据按惯例映射到特定形状:
PyTorch 用 NCHW(通道在前),TensorFlow 默认 NHWC(通道在后)。布局不匹配会导致隐蔽的变慢或报错。
二维数组在内存里是一段一维字节序列。步长(Strides) 告诉你沿每个轴前进一格要跳过多少元素。
行主序(C order): a b c d e f 步长 (3, 1) ← 每行连续 列主序(F order): a d b e c f 步长 (1, 2) ← 每列连续
transpose 不搬数据,只交换步长,让张量变得非连续(non-contiguous)——某行的元素在内存里不再相邻。
广播让你对不同形状的张量做运算而无需拷贝数据。规则:从右对齐,两维相等或其中一维为 1 即可兼容;维数少的在左侧补 1。
张量 A: (8, 1, 6, 1) 张量 B: (7, 1, 5) 补齐 B: (1, 7, 1, 5) 结果: (8, 7, 6, 5)
Einstein 求和给每个轴贴一个字母标签。只出现在输入、不出现在输出的轴被求和消掉;同时出现在输入和输出的轴被保留。
常用模式:i,i->(点积)、i,j->ij(外积)、ii->(迹)、ij->ji(转置)、bij,bjk->bik(批量矩阵乘)、bhtd,bhsd->bhts(注意力分数)。
一次收缩的计算量是所有索引大小(保留与求和)之积。对 bij,bjk->bik,B=32、I=128、J=64、K=128,需 32*128*64*128 = 33,554,432 次乘加。
完整源码见 phases/01-math-foundations/12-tensor-operations/code/tensors.py。
张量存一段扁平数字加形状元数据;步长告诉索引逻辑如何把多维下标映射到扁平位置。
class Tensor: def __init__(self, data, shape=None): if isinstance(data, (list, tuple)): self._data, self._shape = self._flatten_nested(data) elif isinstance(data, np.ndarray): self._data = data.flatten().tolist() self._shape = tuple(data.shape) else: self._data = [data]; self._shape = () if shape is not None: total = reduce(lambda a, b: a * b, shape, 1) if total != len(self._data): raise ValueError(f"无法把 {len(self._data)} 个元素 reshape 成 {shape}") self._shape = tuple(shape) self._strides = self._compute_strides(self._shape) @staticmethod def _compute_strides(shape): if len(shape) == 0: return () strides = [1] * len(shape) for i in range(len(shape) - 2, -1, -1): strides[i] = strides[i + 1] * shape[i + 1] return tuple(strides)
对形状 (3, 4),步长为 (4, 1)——前进一行跳 4 个元素,前进一列跳 1 个。
reshape 改形状不改元素顺序,元素总数必须不变;-1 表示该维由其他维推断。squeeze 去掉大小为 1 的轴;unsqueeze 插入一维——这是广播的关键:偏置向量 (D,) 要加到 batch (B, T, D) 上,得先 unsqueeze 成 (1, 1, D)。
transpose 交换两轴,permute 重排所有轴——这就是 NCHW 与 NHWC 互转的方法。转置或 permute 后张量在内存里非连续;PyTorch 的 view 对非连续张量会失败,要改用 reshape 或先 .contiguous()。
逐元素运算(加、乘、减)独立作用于每个元素并保持形状;归约(sum、mean、max)塌缩一或多个轴。CNN 的全局平均池化:(B, C, H, W).mean(axis=[2,3]) 得 (B, C);NLP 的序列均值池化:(B, T, D).mean(axis=1) 得 (B, D)。
activations = np.random.randn(4, 3) bias = np.array([0.1, 0.2, 0.3]) result = activations + bias # (4,3)+(3,) -> (4,3) images = np.random.randn(2, 3, 4, 4) scale = np.array([0.5, 1.0, 1.5]).reshape(1, 3, 1, 1) result = images * scale # 按通道缩放 a = np.array([1, 2, 3]).reshape(-1, 1) b = np.array([10, 20, 30, 40]).reshape(1, -1) outer = a * b # (3,4) 外积
用广播算两两距离:把 (M, 2) reshpe 成 (M, 1, 2)、(N, 2) 成 (1, N, 2),相减、平方、沿末轴求和、开根,得 (M, N)。
dot = np.einsum("i,i->", a, b) # 点积 matmul = np.einsum("ik,kj->ij", A, B) # 矩阵乘 batch_mm = np.einsum("bij,bjk->bik", batch_A, batch_B) # 批量矩阵乘
B, H, T, D = 2, 4, 8, 16 E = H * D X = np.random.randn(B, T, E) W_q = np.random.randn(E, E) * 0.02 Q = np.einsum("bte,ek->btk", X, W_q) # 投影 Q = Q.reshape(B, T, H, D).transpose(0, 2, 1, 3) # 拆头 -> (B,H,T,D) scores = np.einsum("bhtd,bhsd->bhts", Q, K) / np.sqrt(D) weights = softmax(scores, axis=-1) attn_output = np.einsum("bhts,bhsd->bhtd", weights, V) concat = attn_output.transpose(0, 2, 1, 3).reshape(B, T, E) output = np.einsum("bte,ek->btk", concat, W_o) # 输出投影
每一步都是张量运算:投影(einsum 矩阵乘)、拆头(reshape+transpose)、注意力分数(einsum 批量矩阵乘)、加权和(einsum 批量矩阵乘)、并头(transpose+reshape)、输出投影(einsum 矩阵乘)。
| 操作 | 从零(Tensor 类) | NumPy |
|---|---|---|
| 创建 | Tensor([[1,2],[3,4]]) |
np.array([[1,2],[3,4]]) |
| reshape | t.reshape((3,4)) |
a.reshape(3,4) |
| 转置 | t.transpose(0,1) |
a.T 或 a.transpose(0,1) |
| squeeze | t.squeeze(0) |
np.squeeze(a, 0) |
| 求和 | t.sum(axis=0) |
a.sum(axis=0) |
| einsum | 无 | np.einsum("ij,jk->ik", a, b) |
import torch t = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32) t.shape # torch.Size([2, 3]) t.stride() # (3, 1) t.is_contiguous() # True t.reshape(3, 2); t.unsqueeze(0); t.transpose(0, 1).contiguous() torch.einsum("ik,kj->ij", A, B)
PyTorch 额外提供自动微分、GPU 支持与优化 BLAS 内核,但形状语义完全一致。理解了从零版本,PyTorch 的形状错误就变得可读。
| 运算 | 张量形式 | einsum |
|---|---|---|
| 线性层 | Y = X @ W.T + b |
"bd,od->bo" + 偏置 |
| 注意力 QKV | Q = X @ W_q |
"btd,dh->bth" |
| 注意力分数 | Q @ K.T / sqrt(d) |
"bhtd,bhsd->bhts" |
| 注意力输出 | softmax(scores) @ V |
"bhts,bhsd->bhtd" |
| 批归一化 | (X - mu) / sigma * gamma |
逐元素 + 广播 |
| softmax | exp(x) / sum(exp(x)) |
逐元素 + 归约 |
outputs/prompt-tensor-shapes.md:一份系统化的张量形状调试提示,含每个常见算子(matmul、broadcast、cat、Linear、Conv2d、BatchNorm、softmax)的决策表与修复查找表。outputs/prompt-tensor-debugger.md:一个分步调试提示,把报错信息和你手上的张量形状贴进任何 AI 助手,即可拿回精确修复。源码见 phases/01-math-foundations/12-tensor-operations/code/tensors.py。
(2, 3, 4) 的张量,依次 reshape 成 (6, 4)、(24,),再回到 (2, 3, 4),每步打印扁平数据验证元素顺序不变。Tensor 类加 broadcast_to(shape) 方法,把大小为 1 的维扩展到目标形状;再改 _elementwise_op 使其运算前自动广播。用 (3,1) 与 (1,4) 测试得 (3,4)。einsum(subscripts, *tensors),至少支持点积 i,i->、矩阵乘 ij,jk->ik、外积 i,j->ij、转置 ij->ji。解析下标串、识别收缩索引、遍历所有索引组合,并与 np.einsum 对比。batch_size、seq_len、embed_dim、num_heads,打印多头注意力每一步的精确形状(输入、Q/K/V 投影、拆头、注意力分数、softmax 权重、加权和、并头、输出投影),并与 demo_attention_einsum() 的输出对照验证。(3,1) 表示每行连续;transpose 只换步长、不搬数据,张量变非连续。bias (D,) + (B,T,D) 自动广播。下一节,我们直面浮点数的漏的抽象——数值稳定性:为什么训练会突然 NaN,log-sum-exp 技巧如何拯救 softmax 与交叉熵,以及 bfloat16 为何在训练中胜过 float16。