不用库实现注意力
目标
不借助库,亲手实现注意力。不用 numpy,也不用 torch——只用标准库(math)。
速度很慢。所以维度取得很小(d=8–64,n=4–8)。目的不是速度,而是能看清内部。
要创建的内容
在 /root/work/tf/model.py 中定义以下内容。
| 函数 | 契约 |
|---|---|
softmax(xs) |
和为 1。即使输入很大也不会溢出 |
attention(Q, K, V, mask=None) |
返回 (out, weights)。除以 √d_k |
causal_mask(n) |
mask[i][j] 为真表示 i 能看到 j |
multi_head(Q, K, V, h, mask=None) |
把最后一个维度分成 h 等份,分别做注意力后再拼接 |
pos_encoding(n, d) |
每个位置各不相同、范围在 [-1,1] 的向量 |
layer_norm(row, eps=1e-5) |
均值为 0,方差为 1 |
block(X, h, mask=None) |
X + multi_head(LN(X), ...) |
矩阵全部是 Python 列表的列表([[float]])。位置 × 维度。
确认
cd /root/work/tf
python3 -c "import model; print(model.softmax([1,2,3]))"
步骤
softmax——不会溢出attention+ 测量 √d →02-scale.txtcausal_maskmulti_head(h=1 时与attention相同)pos_encoding+ 置换实验 →05-perm.txtlayer_normblock(pre-LN + 残差)- 整理 →
08-notes.md
参考
评分器会把结果与参考实现比较,误差在 1e-6 以内。注意力是实现不同也会得出相同数字的运算,所以如果数值对不上,多半是缩放或掩码的顺序错了。
不会溢出的 softmax
在 /root/work/tf/model.py 中创建 softmax(xs)。和必须为 1,即使像 [1000, 1001, 1002] 这样的大输入,也必须在没有 inf、nan 的情况下正常工作。
mkdir -p /root/work/tf。只使用标准库(import math)。先减去最大值再 exp——exp(x - max) 在数学上是同样的值,但不会溢出。没有这一行,大值一进来就会崩。
注意力三行与 √d
创建 attention(Q, K, V, mask=None),返回 (출력, 가중치)(占位符依次为输出与权重)。分数必须除以 √d_k。然后测量除与不除时最大权重相差多少,把结果留在 02-scale.txt 中。
Q·K·V 是 [[float]](位置 × 维度)。score[i][j] = dot(Q[i], K[j]) / sqrt(len(K[j])),w[i] = softmax(score[i]),out[i] = Σ w[i][j] * V[j]。测量请用 d=64 的随机向量——不除的话,最大权重会贴近 1.0。那就是“不是加权平均,而是只选一个”的状态。
遮住未来
创建 causal_mask(n)。mask[i][j] 为真,意思是第 i 个 query 能看到 j。attention 接收这个掩码时,被遮住的位置的权重必须恰好为 0。
i >= j 就能看到。实现时要在 softmax 之前把分数设为 -inf(或非常小的值)。如果在 softmax 之后乘以 0,剩下的权重之和就不是 1 了——这是常见的 bug。
拆开再拼起来
创建 multi_head(Q, K, V, h, mask=None)。把最后一个维度分成 h 等份,分别运行注意力,再拼接起来。输出形状必须与输入相同。
每个头的维度是 d // h,缩放也必须以那个较小的维度为准。h=1 时必须得到与 attention 完全相同的结果——这是最好的自我验证。
证明注意力不知道顺序
创建 pos_encoding(n, d)。并确认把输入顺序打乱时,没有位置编码的话输出会原样跟着被打乱,加上之后则不会,把结果留在 05-perm.txt 中。
用正弦/余弦还是别的方式都可以。值在 [-1,1] 范围内,并且每个位置必须不同。证明方法:比较把 X 打乱得到的 X' 做注意力的结果,是否与把原来的结果按同样的顺序打乱相同。这就是 permutation equivariance。
LayerNorm 沿特征轴
创建 layer_norm(row, eps=1e-5)。把一个令牌(token)向量调整为均值 0、方差 1。
不是沿批次,而是在这个向量内部求均值和方差。所以不受批次大小或序列长度影响——这是与 BatchNorm 的决定性区别。为了即使输入常量向量也不会除以 0,要使用 eps。
组装块
创建 block(X, h, mask=None)。是 pre-LN + 残差结构:X + multi_head(LN(X), ...)。输出形状必须与输入相同。
先 rows = [layer_norm(r) for r in X],再运行多头,最后加上原来的 X。没有残差的话,堆得很深时梯度到不了输入。如今的模型之所以是 pre-LN 而不是 post-LN,是因为不需要预热也能训练。
用数字整理三件事
在 08-notes.md 中写至少三行。第 2 步测得的两个最大权重、第 5 步显示的性质,以及序列长度增加到 2 倍时分数矩阵变成几倍。
正文中必须包含 스케일링、순서、제곱(韩文,意为“缩放”“顺序”“平方”)。第三项是实际工作中最常碰到的——把上下文从 4k 增加到 8k,就是 4 倍。