5. 位置编码:谁在前谁在后
这是什么:位置编码(Positional Encoding) = 给每个位置的词向量加上一行代表"我排第几"的数字,让全靠注意力平行处理的模型感知语序。
为什么用:第 2 章的嵌入矩阵只认词不认顺序——"猫追狗"和"狗追猫"查出的向量一模一样,意思却完全相反。注意力又天生"不分先后",必须专门把顺序的信息补进去。
如何用:本章手搓经典的三角函数位置编码,看每个位置的向量如何随位置滑出波纹。
编码要点:像一组指针
Transformer 用一组不同频率的正弦/余弦波给位置编号:偶数维放 sin、奇数维放 cos,且波长随维度下标递减——低维波长短,像秒针,转眼就能区分相邻位置;高维波长长,像日历,负责撑起远距离的分辨。
import numpy as np
T, d_model = 4, 6 # 句长 4 个词,向量 6 维
pos = np.arange(T)[:, None] # 位置:0, 1, 2, 3(列向量)
i = np.arange(d_model)[None, :] # 维度下标:0 ~ 5(行向量)
angle = pos / (10000 ** (2 * (i // 2) / d_model)) # 各位置各维度的相位
pe = np.where(i % 2 == 0, np.sin(angle), np.cos(angle)) # 偶维 sin、奇维 cos
print(np.round(pe, 2))
你会看到:
[[ 0. 1. 0. 1. 0. 1. ]
[ 0.84 0.54 0.05 1. 0. 1. ]
[ 0.91 -0.42 0.09 1. 0. 1. ]
[ 0.14 -0.99 0.14 0.99 0.01 1. ]]
| 代码 | 作用 |
|---|---|
pos[:, None] / i[None, :] |
广播前的形状调整:一行位置 × 一列维度,相除成 4×6 相位表 |
10000 ** (2 * (i // 2) / d_model) |
波长公式:i 越大波长越长,像是"秒针表 + 时钟表 + 日历"齐上阵 |
np.where(i % 2 == 0, sin, cos) |
偶数列放 sin、奇数列放 cos,合起来就是正负一来回摆动 |
| 输出第 1 列 | sin(位置):0、0.84、0.91、0.14,一眼看出"随位置划过弧线" |
加上词向量只一步:H = 词向量 + 位置编码。两个形状相同、按位相加,位置信息便"贴"在了每个词的内容之上。
现状一句:RoPE 已经接班
这套三角函数编码是论文原版写法;如今主流大模型多用 RoPE(旋转位置编码),把"位置"改造成对 Q/K 向量的旋转,长文本外推更稳。但思路同源:都要解决"注意力天生不知道顺序"这一点——手算一遍正弦版,再理解 RoPE 就顺了。
记忆口诀:偶维 sin 奇维 cos,波长随维慢慢长;低频秒针分近处,高频日历辨远方。
动手改
- 把
d_model改成 8,观察新增两列的波形频率变化 - 手算
pos=2、i=0这格的相位:2 / (10000 ** 0) = 2,再对照输出里的0.91——正是sin(2) ≈ 0.909 - 把
T改到 10,看第 1 列sin(位置)怎么循环起来
下一章:BERT:看懂句子的编码器,配齐同一图纸的另一半玩法 🐸