ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Transformer 课件详解:从 Self-Attention 到 Padding Mask 的工程实践

2026/9/23 1:13:42 拓冰建站 浏览量
Transformer 课件详解:从 Self-Attention 到 Padding Mask 的工程实践 简介这份PPT课件面向自然语言处理初学者与算法工程师系统讲解Transformer模型及其论文《Attention is all you need》的核心思想。内容从传统Seq2Seq与RNN的并行计算瓶颈切入引出Attention机制再深入剖析Transformer的编码器-解码器架构涵盖多头自注意力、前馈网络、残差连接与层归一化等关键组件。课件还逐步拆解工作流程包括词嵌入、位置编码、Q/K/V生成、缩放点积注意力、Padding Mask以及训练阶段的损失优化与推理阶段的自回归解码策略。资源包为单个pptx文件大小约17.08MB结构清晰、图文并茂适合课堂讲授或自学参考。目前已有3548人学习能帮助读者建立从宏观架构到微观计算的完整认知并为后续理解BERT、GPT等预训练模型打下基础。1. 从 RNN 的串行瓶颈说起这份 Transformer 课件到底解决什么问题如果你做过 NMT神经机器翻译任务大概率被 RNN 系 Seq2Seq 的训练速度折磨过一个长度为 50 的句子编码器必须按时间步一步步算GPU 利用率常年趴在 20% 以下。这份《Transformer 详解.pptx》正是从这个问题切入的——它先摆出传统 Encoder-Decoder Attention 的 Seq2Seq 结构再指出 RNN 及其变体无法并行计算的硬伤然后引出 2017 年那篇《Attention Is All You Need》给出的答案把循环结构整个拿掉只留注意力。课件覆盖的链路相当完整论文导读、Seq2Seq 任务示例、Encoder/Decoder 的 Block 堆叠结构、Self-Attention 的六步微观计算、Multi-Head 的拆分与拼接、Padding Mask 的-inf处理一直到训练细节和推理阶段的解码策略。它适合两类人一类是刚看完论文但被 Q/K/V 矩阵绕晕、想找一份能对着图推公式的入门者另一类是要手写 Transformer 或排查 Attention 实现 bug 的工程师。下面按「结构 → 计算 → 掩码 → 训练推理」的顺序把它拆开讲。2. Encoder-Decoder 的 Block 堆叠与残差 LayerNorm 结构2.1 编码器和解码器的层数差异课件里反复强调一个容易记混的点编码器和解码器都由 N 个 block 堆叠但每个 block 的层数不一样。编码器 block 只有两层——Multi-Head Self-Attention 和 Feed Forward每层后面都跟 Add残差连接 NormLayerNorm。解码器 block 有三层多出来的那层是 Masked Multi-Head Attention夹在 Self-Attention 和 Feed Forward 之间而且中间还有一层 Co-Attention编码器-解码器注意力用来让解码器关注编码器的输出。数据流向也要记清Block1 到 BlockN-1 的输出直接喂给下一个 Block只有 BlockN 的输出才交给解码器各层编码器侧或后续 Linear 层解码器侧。这个「最后一层才对外输出」的设计是后面做特征提取时选层的依据。2.2 残差连接与 LayerNorm 的摆放顺序课件把每个子层写成Sublayer(x) x再 Norm 的形式也就是 Post-LN。这个顺序直接影响训练稳定性Post-LN 在深层网络里容易出现梯度问题后来很多实现改成 Pre-LN先 Norm 再进子层。如果你要复现课件里的结构按 Post-LN 写如果训练时 loss 震荡厉害可以试试换成 Pre-LN。用 PyTorch 描述一个编码器 block 的结构大致是这样import torch.nn as nn class EncoderBlock(nn.Module): def __init__(self, d_model512, n_heads8, d_ff2048, dropout0.1): super().__init__() # 多头自注意力d_model 必须能被 n_heads 整除 self.self_attn nn.MultiheadAttention(d_model, n_heads, dropoutdropout) self.ffn nn.Sequential( nn.Linear(d_model, d_ff), # 先升维到 2048 nn.ReLU(), nn.Linear(d_ff, d_model), # 再降回 512 ) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.dropout nn.Dropout(dropout) def forward(self, x, maskNone): # Post-LN先算子层加残差再归一化 attn_out, _ self.self_attn(x, x, x, attn_maskmask) x self.norm1(x self.dropout(attn_out)) ffn_out self.ffn(x) x self.norm2(x self.dropout(ffn_out)) return xd_model是词向量和隐层维度课件里取 512n_heads是头数取 8所以每个头的维度是 64d_ff是前馈网络的中间维度取 2048通常是d_model的 4 倍。attn_mask就是后面要讲的 Padding Mask。注意nn.MultiheadAttention默认输入形状是(seq_len, batch, d_model)和课件里[batch_size, max_sequence_length, embedding_dimension]的写法是转置关系实际写代码时别搞反。2.3 为什么是「N 个 block」而不是一层堆到底单层 Self-Attention 只能建模一次全局依赖堆叠 N 层后浅层捕捉局部语法关系深层捕捉长距离语义关系。课件里 N 取 6这是原论文的配置。层数不是越多越好层数增加会带来参数量和显存压力而且没有残差和 LayerNorm 的话深层网络根本训不动。这也是为什么每个子层后面都必须挂 Add Norm而不是可选组件。3. Self-Attention 六步计算与 Multi-Head 的拆分拼接3.1 从 Q/K/V 生成到加权求和课件把 Self-Attention 拆成六步这是整份材料里最值得反复看的部分。以「The animal didnt cross the street because it was too tired」为例模型要判断it指代的是 animal 还是 street靠的就是注意力权重。第一步每个词的 embedding 分别乘以三个权重矩阵 WQ、WK、WV得到查询向量 Q、键向量 K、值向量 V。第二步用当前词的 Q 和所有候选词的 K 做点积得到相关性分数。第三步除以sqrt(d_k)防止点积过大导致 softmax 梯度消失。第四步softmax 归一化成权重。第五步用权重对每个 V 加权。第六步求和得到该位置的输出。用矩阵形式一次算完整个序列import torch import torch.nn.functional as F import math def scaled_dot_product_attention(Q, K, V, maskNone): # Q/K/V 形状: (batch, n_heads, seq_len, d_k) d_k Q.size(-1) scores torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k) if mask is not None: # mask 中为 0 的位置填 -infsoftmax 后趋近于 0 scores scores.masked_fill(mask 0, float(-inf)) weights F.softmax(scores, dim-1) return torch.matmul(weights, V), weightsd_k是每个头的维度等于d_model / n_heads。math.sqrt(d_k)这个缩放因子是原论文的关键细节少了它当d_k较大时点积结果方差会变大softmax 输出会退化成接近 one-hot梯度几乎消失。masked_fill配合-inf是标准做法因为softmax(-inf) 0正好把无效位置屏蔽掉。3.2 Multi-Head 为什么要拆成多个头课件用 One-Head、Two-Heads、All-Heads 三张图对比说明多头不是简单重复。把 512 维拆成 8 个 64 维的子空间每个头独立做一次 Self-Attention相当于让模型从 8 个不同的表示子空间去关注输入。有的头可能关注语法主谓关系有的头关注指代关系最后把 8 个头的输出拼接回 512 维再过一层线性变换。配置项单头多头8 头每头维度 d_k51264计算量1 次大矩阵乘8 次小矩阵乘总量相当表达能力单一关注模式多子空间并行关注参数量WQ/WK/WV 各 512×512各 512×512拼接后等价关键结论多头并不会显著增加参数量因为每个头的维度缩小了总维度不变。它的收益在于表达能力的多样性而不是计算量的堆叠。3.3 位置编码怎么补回顺序信息Self-Attention 本身是置换不变的——把输入序列打乱输出只是跟着打乱模型感知不到顺序。所以课件专门用两页讲位置编码。原论文用正弦余弦函数import numpy as np def positional_encoding(seq_len, d_model): pe np.zeros((seq_len, d_model)) for pos in range(seq_len): for i in range(0, d_model, 2): # 偶数维用 sin奇数维用 cos频率随维度变化 pe[pos, i] np.sin(pos / (10000 ** (i / d_model))) pe[pos, i 1] np.cos(pos / (10000 ** (i / d_model))) return pepos是位置索引i是维度索引10000是底数。这种编码的好处是任意位置的编码都能用线性关系表示相对位置而且能外推到训练时没见过的更长序列。常见做法是把位置编码直接加到词嵌入上而不是拼接这样维度不用变。4. Padding Mask 与训练推理阶段的工程细节4.1 Padding Mask 的 -inf 处理课件里给了一个很具体的例子一个 batch 里有四句话长度分别是 2、5、3、5max_sequence_length取 5短句后面补 0。补出来的 0 如果参与注意力计算会污染真实位置的权重所以必须用 mask 屏蔽。def create_padding_mask(seq, pad_token0): # seq 形状: (batch, seq_len)pad 位置为 1 表示需要屏蔽 mask (seq ! pad_token).unsqueeze(1).unsqueeze(2) # (batch, 1, 1, seq_len) return mask # 1 保留0 屏蔽这个 mask 广播到(batch, n_heads, seq_len, seq_len)后配合前面masked_fill(mask 0, -inf)就能让 softmax 在 padding 位置输出 0。解码器侧还要额外加一个上三角的因果 mask防止当前位置看到未来词保证自回归性。两个 mask 通常用逻辑与合并。提示mask 的布尔方向很容易写反。记住「True/1 表示保留False/0 表示屏蔽」masked_fill里填的是mask 0的位置写反了模型会完全学不到东西loss 卡在常数不降。4.2 训练阶段的损失与优化配置课件在训练细节部分提到用交叉熵损失和反向传播更新参数。实际工程里还有几个关键点学习率用 warmup 策略先线性升高再按步数平方根衰减标签平滑label smoothing设 0.1缓解过拟合dropout 在 embedding、注意力权重、FFN 里都要加。优化器用 Adambeta10.9、beta20.98、eps1e-9这些是原论文的配置。import torch.optim as optim # 原论文的 Adam 配置beta2 比默认的 0.999 小 optimizer optim.Adam(model.parameters(), lr1.0, betas(0.9, 0.98), eps1e-9) def lr_lambda(step): d_model, warmup 512, 4000 # 前 warmup 步线性升温之后按 step 的 -0.5 次方衰减 return (d_model ** -0.5) * min(step ** -0.5, step * warmup ** -1.5) scheduler optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)warmup设 4000 步是原论文的值小数据集可以调小。lr初始设 1.0 是因为lr_lambda会把它缩放到合理范围别直接拿 1.0 去训。4.3 推理阶段的解码策略推理时模型自回归逐词生成每生成一个词就拼到已生成序列末尾作为下一步输入。课件提到的解码策略包括贪心解码和束搜索beam search。贪心每步取概率最大的词快但容易陷入局部最优束搜索保留 top-k 条候选路径beam_size一般取 4 到 10翻译质量更好但计算量成倍增加。def greedy_decode(model, src, max_len50, start_token1, end_token2): memory model.encode(src) # 编码器输出供 Co-Attention 使用 ys torch.full((1, 1), start_token, dtypetorch.long) for _ in range(max_len): out model.decode(ys, memory) # 解码器带因果 mask next_token out[:, -1, :].argmax(dim-1, keepdimTrue) ys torch.cat([ys, next_token], dim1) if next_token.item() end_token: break return ysmemory是编码器最后一层的输出解码器每层都会对它做 Co-Attention。start_token和end_token是句首句尾标记具体 id 取决于词表。循环里每次只取最后一个时间步的 logits因为前面的位置已经生成过了。5. 用注意力权重做可解释性排查与手写验证课件里那张it指代 animal 的注意力热力图其实是个很实用的调试手段。当你怀疑模型没学到东西时把某一层的注意力权重打印出来看分布比盯着 loss 曲线有效得多。下面这段代码把权重可视化前的数据准备好# 复用第 3 章的 scaled_dot_product_attention返回 weights _, attn_weights scaled_dot_product_attention(Q, K, V, mask) # attn_weights 形状: (batch, n_heads, seq_len, seq_len) avg_weights attn_weights.mean(dim1) # 对 8 个头取平均得到 (batch, seq_len, seq_len) # 取第一个样本看第 i 行第 j 列表示位置 i 对位置 j 的关注强度 sample avg_weights[0] print(sample[3]) # 打印第 4 个词对所有词的注意力分布对头取平均只是入门做法更细的排查是逐头看如果所有头的权重分布几乎一样说明多头退化了可能是初始化或维度拆分出了问题。正常情况下不同头应该呈现不同的关注模式。验证手写实现是否正确有个低成本方法用极小的配置d_model8、n_heads2、seq_len4跑一遍手动算一遍 Q/K/V 和 softmax对比数值。另一个方法是把位置编码关掉看模型在「打乱词序」的任务上是否退化——如果关掉位置编码后性能没变化说明你的位置信息根本没生效。注意注意力权重高不等于因果贡献大它只是相关性的一种近似。做可解释性分析时最好配合梯度类方法交叉验证别只凭热力图下结论。最后给一个排查清单loss 不降先查 mask 方向和学习率 warmup输出重复词查因果 mask 是否漏加长句效果差查位置编码外推多头无差异查维度拆分和初始化。这几条覆盖了手写 Transformer 时八成以上的坑。本文还有配套的精品资源点击获取