两条样本,预测完全正确,均方误差却不是零。先别换 optimizer,看一眼 shape:预测可能是 [2,1],标签却是 [2]。广播(broadcasting)让这段代码顺利运行,也悄悄换了你要计算的问题。
这章不按函数名背 API,而是逐个回答:谁和谁算、沿哪个轴合并、结果还剩哪些轴。
广播先对齐末尾,不理解你的 batch
Broadcasting 从最右边对齐各轴,对应长度相等或其中一个为 1 才能匹配;缺少的前导轴按长度 1 处理。
prediction [B, 1]
target [B] → treated as [1, B]
result [B, B]
Each prediction is compared with every target,
not just the target from the same sample.
import torch
prediction = torch.tensor([[1.0], [3.0]])
target = torch.tensor([1.0, 3.0])
pairwise_error = (prediction - target).square()
assert pairwise_error.tolist() == [[0.0, 4.0], [4.0, 0.0]]
assert pairwise_error.mean().item() == 2.0
matched_error = (prediction.squeeze(-1) - target).square()
assert matched_error.mean().item() == 0.0
有时候 [B,B] 正是我们想要的,例如对比学习(contrastive learning)里所有 query 与 candidate 的相似度;回归里却通常不是。广播只检查尺寸,不检查样本含义。
长度为 0 的轴要特别看:[0] 与 [1] 可以得到 [0],不是机械地取最大长度。原地操作也不能把左侧存储扩成更大的形状。规则见 Broadcasting semantics。
省了输入复制,不一定省结果内存
expand 可以用零 stride 表示重复读取,不真的复制这些输入值;repeat 会复制。但两个小输入广播成很大的结果,结果本身仍可能占据大量内存。
比如 [B,1,D] - [1,N,D] 会产生 [B,N,D]。算两两距离时,如果只要平方距离,可以改用 ||q||² + ||k||² - 2qkᵀ 的矩阵写法,避免显式保存三维差值;但两个相近的大数相减会产生相消误差,它不一定在所有 dtype 下都更准确。
多个逻辑元素可能指向 expand 的同一存储位置,因此别直接往扩展视图(view)里写值;需要可修改副本时先 clone。反向传播时,广播出去的多条路径会把梯度累加回原来的元素:例如把 [D] 的 bias 加到 [B,D] 上,bias 的梯度就是上游梯度沿 batch 轴求和。
reduction:分母是什么,剩下哪些轴
对 [B,T,D],mean(dim=-1) 得到每个 token 的特征均值,shape 为 [B,T];加上 keepdim=True 得到 [B,T,1],方便沿特征轴减回去。
| 操作 | 返回内容 | 需要想清楚 |
|---|---|---|
sum / mean / prod |
归约后的值 | 不写 dim 时通常归约全部元素 |
max(dim=...) / min(dim=...) |
值与索引 | 只要值可用 amax / amin |
argmax / argmin |
索引 | 不是对应值,也不是可微的选择规则 |
topk |
values 与 indices | 相同分数的索引顺序不保证稳定 |
var / std |
方差(variance) / 标准差 | correction 决定分母,默认是 1 |
median / quantile |
中位数 / 分位数 | 偶数个元素时,median 返回较小的中间值 |
归一化中常用总体方差 correction=0。只有一个元素却使用 correction=1,就没有足够自由度;空 tensor 的均值也没有定义。不能看到 NaN 就先加一个 epsilon,而不问统计量本来该是什么。
samples = torch.tensor([2.0, 6.0])
assert samples.var(correction=0).item() == 4.0
assert samples.var(correction=1).item() == 8.0
assert samples.median().item() == 2.0
assert samples.quantile(0.5).item() == 4.0
token_loss = torch.tensor([[1.0, 3.0, 99.0], [2.0, 99.0, 99.0]])
valid = torch.tensor([[True, True, False], [True, False, False]])
token_average = token_loss[valid].mean()
sequence_average = torch.stack([row[mask].mean() for row, mask in zip(token_loss, valid)]).mean()
assert token_average.item() == 2.0
assert sequence_average.item() == 2.0
上例两个均值恰好相同,不说明它们等价。把第二条唯一的有效 loss 从 2 改为 8,token 平均变成 4,逐序列平均变成 5:前者按 token 加权,后者每条序列等权。训练目标选择不同,梯度也会不同。空有效集合应明确跳过或报错,不能默默制造 NaN。
矩阵乘法:最后两个轴负责什么
* 是逐元素乘法,可以广播;@ / matmul 才是矩阵乘积。对于矩阵,[M,K] @ [K,N] → [M,N],中间的 K 被求和消掉。
| 入口 | 典型输入 | 区别 |
|---|---|---|
dot / vdot |
两个一维向量 | 复数时 vdot 对第一个输入取共轭 |
mv |
矩阵与向量 | [M,K] × [K] → [M] |
mm |
两个二维矩阵 | 不广播 batch 轴 |
bmm |
两个三维张量 | batch 数相同,不做 batch 广播 |
matmul / @ |
向量、矩阵、批量矩阵 | 最后两个轴相乘,前导 batch 轴可广播 |
addmm |
偏置与两个矩阵 | beta * input + alpha * (left @ right) |
baddbmm / addbmm |
偏置与批量矩阵 | 前者保留 batch;后者把 batch 乘积求和 |
queries = torch.arange(24, dtype=torch.float32).reshape(2, 3, 4)
keys = torch.arange(40, dtype=torch.float32).reshape(2, 5, 4)
scores = queries @ keys.transpose(-2, -1)
assert scores.shape == (2, 3, 5)
torch.testing.assert_close(scores, torch.bmm(queries, keys.transpose(1, 2)))
torch.testing.assert_close(scores, torch.einsum("bqd,bkd->bqk", queries, keys))
offset = torch.zeros(3, 5)
summed = torch.addbmm(offset, queries, keys.transpose(1, 2))
torch.testing.assert_close(summed, scores.sum(dim=0))
这里每个 batch 有 3 个 query、5 个 key、4 个特征;输出是每个 query 对每个 key 的分数。einsum 把这件事写成带字母的轴约定,便于核对,不保证比 matmul 更快。addbmm会消掉 batch 轴,不能把它当作带 bias 的普通 bmm。
常用的矩阵构造也有实际用途:eye 是单位矩阵,diag 可从向量构造对角矩阵或从二维矩阵取对角线;批量取对角可看 diagonal。triu / tril 只保留上 / 下三角,diagonal=1 的上三角常用于标记“未来位置”。t() 面向不超过二维的输入,批量转置用 transpose(-2,-1),别拿整个高维 .T 当成只交换最后两个轴。
每行取一个目标值:gather
三分类任务中,每条样本的正确类别不同。index_select 会给所有行取同一组列;我们需要的是每行各取一个。
logits = torch.tensor([[3.0, 1.0, -2.0], [0.0, 2.0, 4.0]])
labels = torch.tensor([0, 2], dtype=torch.long)
log_probs = logits.log_softmax(dim=-1)
selected = log_probs.gather(1, labels[:, None]).squeeze(1)
loss = -selected.mean()
torch.testing.assert_close(loss, torch.nn.functional.cross_entropy(logits, labels))
gather 的 index 与输入维数一致,这里是 [B,1];它不替你广播 index。我们只是把标签加一个轴,选完再去掉。类别数来自任务定义,不要每个 batch 都用 labels.unique() 的数量重新决定输出层大小。
数值函数:选对写法比多留小数重要
exp、log、sqrt 等是逐元素运算;logsumexp 则计算“指数和的对数”,不是“更精确的普通求和”。大 logits 先 exp 可能溢出,先 softmax 再 log 也可能碰到零概率。
large = torch.tensor([1000.0, 999.0])
assert torch.isinf(large.exp()).all()
stable = torch.logsumexp(large, dim=0)
torch.testing.assert_close(stable, torch.tensor(1000.3132617))
assert torch.isfinite(large.log_softmax(dim=0)).all()
assert torch.exp(torch.tensor([1, 2])).is_floating_point()
assert torch.round(torch.tensor([0.5, 1.5, 2.5])).tolist() == [0.0, 2.0, 2.0]
round 遇到正中间时取偶数;ceil 向正无穷取整,floor 向负无穷取整。log1p 在输入接近零时,比先算 1 + value 再取 log 更稳。log 的实数输入应为正,sqrt 的实数输入应非负,异常值要追到来源。
距离也要写清定义:linalg.vector_norm(vector, ord=2) 是欧氏长度,ord=1 是绝对值和;dist(left,right,p=2) 是两个张量之差的欧氏距离。它们不等于归一化后的 cosine similarity。
浮点数的结合顺序会影响结果。equal 不是近似比较,也不替你检查 dtype;allclose 使用绝对与相对容差,shape 还可能广播。测试模型时更适合明确检查 shape,再用 torch.testing.assert_close 检查值和类型。不要把“多打印几位”和“提高计算精度”混为一谈。
上述基础规则对照 PyTorch 数值精度说明、归约与数学 API及 CrossEntropyLoss。本章只算小 tensor,没有声称复现大模型训练或性能。
本页术语 3
- 对比学习 contrastive learning
- 拉近正样本对、推开负样本对来学习表示
- 视图 view
- 同一个对象的一种侧面表示;用户可以同时有画像视图和历史视图
- 方差 variance
- 估计量的波动幅度;重要性加权最容易在这里爆炸