课程版本:李宏毅《機器學習 2021》spring,Self-attention (上)(2021-03-12) 官方讲义:
self_v7.pdf,43 页 — https://speech.ee.ntu.edu.tw/~hylee/ml/ml2021-course-data/self_v7.pdf 分P:BV1ou411N7X3?p=36,28:18 来源边界(上下讲切分):讲义 p1–p25 从 "Sophisticated Input" 一路讲到单头注意力的矩阵形式与 "Attention Matrix";p26 页内标题变为 "Multi-head Self-attention"。据此按讲义章节切:P36 = p1–p25,P37 = p26–p42。metadata 只能给出两支视频的总时长(28:18 / 45:58),不能确定视频在第几页切换,所以本文的切分标注为「讲义章节切分复习版」,不声称分钟级对应。无字幕,不引用逐字台词。
1. 这一讲解决什么问题
到这一讲为止,模型吃的都是一个向量;但很多任务的输入是一组向量,而且长度会变(p2):
- 一句话:每个词一个 embedding(p3);
- 一段语音:每 25ms 一帧、每 10ms 取一帧,1 秒约 100 个向量(p4);
- 一张图:每个像素(或 patch)一个向量;
- 一张图结构:每个节点一个向量(p5–p6)。
输出有三种(p7–p9):每个向量一个标签(词性标注)、整段一个标签(情感分析)、由模型决定长度的标签序列(seq2seq,翻译)。本讲只处理前两种里"每个位置都要看上下文"的需求。
直观例子:I saw a saw。第一个 saw 是动词,第二个是名词;它们的词向量可以完全一样,区别只在上下文(p7、p10)。如果把每个位置单独送进全连接层,模型只能看到这个词自己;加一个固定窗口,窗口又盖不住整句(p10 明确问 "a window covers the whole sequence?")。self-attention 的回答是:让每个位置自己学一个权重,去加权整句所有位置。
2. 核心机制:query 找 key,再取 value
对第 \(i\) 个位置:
符号与 shape(设序列长 \(N\)、输入维 \(d\)、key/query 维 \(d_k\)、value 维 \(d_v\)):
| 符号 | 含义 | shape |
|---|---|---|
| \(A=[a_1,\dots,a_N]\) | 输入向量按列排 | \(d\times N\) |
| \(W_q,W_k\) | 投影矩阵(可学习) | \(d_k\times d\) |
| \(W_v\) | value 投影 | \(d_v\times d\) |
| \(Q,K\) | 所有位置的 query / key | \(d_k\times N\) |
| \(A'\) | 注意力权重 | \(N\times N\) |
| \(O=[b_1,\dots,b_N]\) | 输出 | \(d_v\times N\) |
矩阵形式(p21–p25):
softmax 是按行做的:\(A'\) 的第 \(i\) 行是第 \(i\) 个 query 对所有 key 的分布,\(\sum_j\alpha'_{ij}=1\)。这不是"对整个矩阵归一",也不是按列。
讲义 p15 还提了 additive attention(\(v^{\top}\tanh(W[q;k])\)),但后面全部用 dot-product——因为点积可以写成矩阵乘,GPU 上快。
玩具数值例(Python 计算,非课堂原始数据)
取 4 个 token、\(d=4\)、\(d_k=d_v=2\),人为设定小整数权重便于手算复核:
A = [[1,0,1,0],[0,1,0,1],[1,1,0,0],[0,1,1,0]]
Wq = [[1,0,0,1],[0,2,1,0]] Wk = [[1,0,2,0],[0,1,0,1]] Wv = [[1,0,0,0],[0,1,0,0]]
Q = [[1,1,2,0],[1,3,0,2]] K = [[3,2,1,0],[0,2,1,1]] V = [[1,0,1,0],[0,1,0,1]]
分数矩阵 \(Q^{\top}K\) 与逐行 softmax:
四行和分别为 \(1.0000\)(脚本输出 row_sums=[1.0,1.0,1.0,1.0])。列和却是 \([1.1226,\,2.5060,\,0.2257,\,0.1457]\),全都不是 1——这一条数字就能证明 softmax 是沿行(每个 query 自己归一),不是沿列。
输出第一列(第 1 个 token 的新表示):
因为 \(v_1=v_3=[1,0]^{\top}\)、\(v_2=v_4=[0,1]^{\top}\),两个维度上的系数正好是"奇数位权重和 / 偶数位权重和"。这个例子同时说明:输出是 value 的加权平均,权重由 query-key 相似度决定,与 value 本身的内容无关。
为什么要除以 \(\sqrt{d_k}\)【补充】
2021 讲义的矩阵式没有写 \(1/\sqrt{d_k}\),这是原始 Transformer 论文的做法,属于本节补充。理由可以用数值看清:若 \(q,k\) 各分量独立且方差为 1,点积的方差约为 \(d_k\)。用确定性伪随机数生成 4000 组向量,实测 \(q\cdot k\) 的标准差为:
| \(d_k\) | 2 | 8 | 64 | 256 |
|---|---|---|---|---|
| std\((q\cdot k)\) | 1.413 | 2.790 | 7.987 | 15.826 |
基本就是 \(\sqrt{d_k}\)。\(d_k=64\) 时分数尺度约 8,softmax 很容易被最大的那一项吃掉(接近 one-hot),梯度变得很小。除以 \(\sqrt{d_k}\) 把尺度拉回 \(O(1)\),softmax 留在有梯度的区间。
mask 放在哪【补充 / 为 P48 埋伏笔】
本讲不涉及 mask,但公式里有一个天然的位置:mask 加在 softmax 之前、分数矩阵 \(Q^{\top}K\) 上。把不允许看的位置设为 \(-\infty\),softmax 后这些位置的权重精确为 0(不是"很小")。
用同一组分数做 4×4 因果 mask(只允许 \(j\le i\)),Python 计算结果:
行和仍为 1,上三角全部是 0.0(future_weights_exact_zero=True)。
因果 mask ≠ padding mask,这是两件常被混为一谈的事:
- 因果 mask:作用在 \((i,j)\) 且 \(j>i\) 的位置,块状上三角,目的是不让第 \(i\) 步看到未来;只用于 decoder 的自回归训练。
- padding mask:作用在某些 key 的整列(一个 batch 里为了对齐而补的
<pad>),目的是不让任何 query 关注无效位置;与"未来"无关,encoder 也可能需要。
两者可以同时存在,mask 的形状和语义都不同。P48 讲 decoder 时会看到因果 mask 的实际用法。
3. 并行性与"同一层里传消息"
p19 只写了一个词:parallel。把 \(Q,K,V\) 写成矩阵乘之后,所有位置的输出是一次矩阵运算同时算出来的,不需要像 RNN 那样等前一个时间步。这也是后面 P37 拿它和 RNN 对比的伏笔(p38:self-attention 可并行,RNN 不行)。
代价在 \(A'\):它是 \(N\times N\)。序列长 1000,就是 100 万个分数;长 10000,就是 1 亿。这个 \(O(N^2)\) 是 P37 "truncated self-attention" 与各种高效 attention 的动机。
4. 伪代码
def self_attention(A, Wq, Wk, Wv, mask=None):
Q, K, V = Wq @ A, Wk @ A, Wv @ A # (dk,N), (dk,N), (dv,N)
S = Q.T @ K # (N,N) 分数
if mask is not None:
S = S + mask # 0 / -inf,加在 softmax 前
A_prime = softmax(S, axis=1) # 每一行归一
return V @ A_prime.T # (dv,N)
(这是形状级伪代码,不依赖任何框架;本文所有数值用纯 Python 乘加复算,没有安装 torch。)
5. 常见误区
- "self-attention 只能做 NLP"。讲义 p2–p6 一开始就把语音帧、图节点和词向量并列;P37 还会给图像(p33–p34)与图(p40)。输入只要是"向量集合"就能用。
- "Q、K、V 是三份不同的输入"。三者都来自同一个 \(A\),只是乘了三个不同的可学习矩阵。说"三份输入"会掩盖"同一内容在不同角色下被投影"这个关键。
- "attention 权重按列归一"。见上面的列和 \([1.12,2.51,0.23,0.15]\);按列归一会让每个 key 被所有 query 平分,语义完全不同。
- "attention 权重就是解释"。权重高只说明该位置对当前 query 的加权大,经过 value 投影和多层混合后,不一定等于因果贡献。这是【补充】的方法论提醒,讲义没有讨论。
- "不加 positional encoding 也能知道顺序"。不能。本讲的公式对输入列的置换是等变的:把 4 个 token 打乱,输出只是跟着打乱,模型完全无法区分"我打你"和"你打我"。这是 P37 要补 positional encoding 的直接原因。
6. 自测
Q1. \(N=5\)、\(d=8\)、\(d_k=d_v=4\),写出 \(Q,K,A',O\) 的 shape,并说明 \(A'\) 有多少个元素、每行满足什么。
答案
\(Q,K\) 是 \(4\times5\);\(A'\) 是 \(5\times5\),共 25 个元素;\(O\) 是 \(4\times5\)。\(A'\) 每一行是一个概率分布,元素非负且行和为 1;它一般不是对称矩阵,因为 \(q_i\cdot k_j\ne q_j\cdot k_i\)(\(W_q\ne W_k\))。
Q2. 如果把 \(W_v\) 换成单位矩阵,输出 \(b_i\) 是什么?如果再把所有 \(v_j\) 都设成同一个向量 \(c\),输出又是什么?
答案
\(W_v=I\) 时 \(b_i=\sum_j\alpha'_{ij}a_j\),即输入向量的加权平均。若所有 \(v_j=c\),则 \(b_i=c\sum_j\alpha'_{ij}=c\),与 query 无关——这说明输出依赖 value 的差异;value 全同,attention 再尖锐也传不出信息。
Q3. 为什么 mask 要加在 softmax 之前,而不是之后把权重乘 0?
答案
softmax 之前设为 \(-\infty\),归一化时分母自然排除这些位置,剩余权重仍然和为 1,且被遮位置精确为 0。若在 softmax 之后乘 0,行和会小于 1,输出尺度被缩小,且被遮位置仍参与了分母,等价于"看见但忽略",不是真正的屏蔽。
Q4. 序列长度从 500 增到 2000,\(A'\) 的元素数和注意力矩阵乘的 FLOPs 各变几倍?
答案
\(A'\) 是 \(N\times N\),元素数变 \(4^2=16\) 倍;\(Q^{\top}K\) 与 \(VA'^{\top}\) 的乘加量都与 \(N^2\) 成正比,也变 16 倍。线性投影 \(W_qA\) 只随 \(N\) 线性增长,变 4 倍——所以长序列的瓶颈在 \(N^2\) 的分数矩阵。
7. 30 秒速记
输入是一组变长向量 → 全连接看不全整句。每个位置投影出 \(q,k,v\);\(q_i\cdot k_j\) 打分,按行 softmax,再对 \(v\) 加权求和。矩阵形式 \(A'=\mathrm{softmax}(Q^{\top}K)\)、\(O=VA'^{\top}\),一次算完所以并行,代价是 \(N\times N\)。\(1/\sqrt{d_k}\) 和 mask 都是【补充】:前者控制点积尺度(std 约 \(\sqrt{d_k}\)),后者加在 softmax 之前;因果 mask 遮未来、padding mask 遮补齐列,不是一回事。
跨 P 连接:P37 从 p26 接着讲多头(不同子空间的相关性)、位置编码(补回本节丢掉的顺序),再把 self-attention 与 CNN/RNN 放在同一张谱上比较。P47/P48 的 Transformer encoder/decoder 就是把本节的 block 堆起来,并在 decoder 里把"因果 mask"真正用上;P38 的 RNN 则是"串行看上下文"的对照方案。
引用页(PDF 物理页,1 起)
p2–p6(向量集合:词、语音帧、图)、p7–p10(三种输出与窗口局限)、p11–p15(self-attention 与 q/k/v、两种打分)、p16–p18(逐位置打分、softmax、加权求和)、p19–p25(并行、矩阵形式、\(A'\) 与 \(O\))。