
简介这是一份Transformer模型详解PPT围绕NLP里程碑论文《Attention is all you need》展开是面向NLP学习者、算法工程师及高校学生的优质导读课件。资源仅1个pptx文件约17.08MB内容组织系统。课件从传统Seq2Seq模型和Attention机制讲起指出RNN在并行计算上的局限进而引入Transformer的完整架构。重点部分逐一拆解编码器中自注意力层的Q、K、V计算、缩放点积、softmax加权等微观步骤以及多头注意力的原理和多头融合方式同时解释位置编码、Padding Mask、解码器中的掩码自注意力与编码器-解码器注意力层并给出训练阶段的损失函数与推理阶段自回归解码策略。整套课件兼具宏观脉络与微观细节适合配合原始论文研读也可作为组会分享或课程教学的PPT底稿。该资源目前已有3560人学习是理解Transformer原理的实用参考。1. 一份讲Transformer的PPT真正该拆的是什么很多人拿到「Transformer详解.pptx」这类材料第一反应是从那幅经典结构图开始逐层讲 encoder、decoder、多头注意力。但我见过太多组内分享这么讲完台下反馈只有一个公式都认识但回去自己写一个照样写不出来。真正卡住大家的不是那个缩放点积公式本身而是三件事Q、K、V 到底是哪来的mask 在哪些环节起作用以及训练和推理为什么行为不一样。这篇笔记就按「先立原理、再上代码、再讲训练、最后避坑」的顺序把一份讲 Transformer 的 PPT 该有的内容给你捋清楚。适合两类人一是要在组内做技术分享、需要把 Transformer 讲透讲准的工程师二是刷了很多文章但还没动手实现过的初学者。2. 先把注意力机制讲透Q、K、V 与三类 mask2.1 Q、K、V 三个矩阵到底在算什么注意力机制最常见的比喻是「查字典」query 是你要查的词key 是字典里的词条索引value 是词条内容。但工程实现里Q、K、V 并不是输入自己而是输入经过三个可学习矩阵投影出来的结果。设输入序列为 X形状是 [batch, seq_len, d_model]那么Q X W_qW_q 形状 [d_model, d_k]K X W_kW_k 形状 [d_model, d_k]V X W_vW_v 形状 [d_model, d_v]这里 d_k 和 d_v 通常相等经典配置里取 d_model / num_heads比如 d_model512、8 个头时 d_k64。为什么要做投影而不是直接用 X 自己算相似度因为直接算相似度只有一种相似性度量而投影让模型有机会在不同子空间里学习不同的匹配模式——某个头关注语法关系某个头关注指代关系某个头关注位置邻近性这种分工是训练出来的不是设计出来的。我一般会在 PPT 里放一张 2x2 的小图左边画「X 进入三个投影矩阵」右边画「一个头内部的 QK 点乘结果经过 softmax 后作用于 V」。这张图比堆公式更能让听众建立直觉。2.2 缩放点积与多头为什么要除根号 d_k注意力分数是 Q 和 K 的点积然后除以 sqrt(d_k)再 softmax。这个缩放是 Transformer 原论文里反复强调的细节当 d_k 变大时点积的方差会跟着变大softmax 的输入会落在梯度极小的饱和区训练就推不动。除以 sqrt(d_k) 把方差拉回 1 附近梯度能顺畅地回流。这个解释要在讲解时明确说出来不然听众会以为只是数值上的小技巧实际上它是训练稳定性的前提。多头注意力则是把投影后的 Q、K、V 沿 head 维度切开每个头独立算注意力最后把所有头的输出拼接再经过一个输出投影。常见实现里有两种切法。第一种是直接 reshape# x: [batch, seq_len, d_model] # num_heads 个头的 Q/K/V 一次性算完再切分 batch, seq_len, d_model x.shape head_dim d_model // num_heads q q_proj(x) # [batch, seq_len, d_model] q q.view(batch, seq_len, num_heads, head_dim) q q.transpose(1, 2) # [batch, num_heads, seq_len, head_dim]第二种是每个头单独做线性投影。两种写法计算结果等价但第一种在 PyTorch 里算子更少、显存更友好。要注意的是 transpose 之后必须用contiguous()或者干脆把后续算子都支持非连续内存否则跑 matmul 时会报错或者触发隐式拷贝。很多初学的翻车都出在这。多头的好处不是「模型更大所以更强」而是让注意力分布多样化。我见过一个直观实验把 8 个头的注意力权重可视化出来你会发现约一半的头学到了比较集中的局部依赖相邻词之间另一半学到了远程依赖句子头尾之间的指代。单头注意力只能二选一多头不用选。2.3 positional encoding 为什么绕不开自注意力对位置是置换等变的——把输入序列的顺序打乱输出也会跟着打乱但模型不会「发现」顺序变了。它不像 RNN 天然按时间步展开也不像 CNN 用卷积核感知局部位置。所以必须把位置信息显式加进输入。经典做法是正弦余弦位置编码pe torch.zeros(max_len, d_model) position torch.arange(0, max_len).unsqueeze(1).float() div_term torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) # 使用时 pe[:seq_len] 加到 embedding 上选择正弦余弦而不是可学习位置向量的理由原论文给的解释是正弦函数的线性组合可以把相对位置表达出来模型有机会外推到比训练时更长的序列。这听起来有点玄学实际工程里很多人后来换成了可学习位置向量或旋转位置编码。我的建议是做标准 Transformer 讲解时讲正弦余弦因为它是原版方案的基石做工程选型时如果你的任务序列长度变化范围很大优先考虑支持相对位置信息的编码方案外推表现通常更好。2.4 三类 mask 的适用场景这可能是整份 PPT 里最容易被讲漏的部分。Transformer 里实际有不止一种 mask它们作用位置不同、目的不同、形状也不同。第一类叫 padding mask。因为 batch 内序列要补齐到相同长度pad 位置不能参与注意力。它加在 softmax 之前把 pad 位置的分数置为极小的负数。第二类叫 look-ahead mask 或 causal mask只用在 decoder 的自注意力里保证位置 i 只能看到 i 和 i 之前的位置不能看到未来。它是个上三角为 0 的矩阵加到分数矩阵上之后未来位置被屏蔽。第三类叫 cross-attention 的 key padding mask即 decoder 在 attend encoder 输出时也要把 encoder 侧 pad 掉的 token 屏蔽掉。实际实现里这三类 mask 经常是组合使用的。有一个常见的坑很多人只在 encoder 里做了 padding maskdecoder 里只做了 look-ahead mask忘了 decoder 输入同样有 pad 位置结果 pad token 之间互相 attend训练指标还行一到生成就出乱码。后面第 5 章我会专门展开这条。3. 用几十行代码把 Transformer 块跑通从自注意力到完整 Block3.1 自注意力层的最小实现讲清楚原理后最好的验证方式是让听众跟着你写一个能跑的最小实现。下面这份代码不追求效率只追求把每个操作和上述原理对应上import torch import torch.nn as nn import math class SelfAttention(nn.Module): def __init__(self, d_model, num_heads): super().__init__() assert d_model % num_heads 0 self.d_model d_model self.num_heads num_heads self.head_dim d_model // num_heads self.q_proj nn.Linear(d_model, d_model) self.k_proj nn.Linear(d_model, d_model) self.v_proj nn.Linear(d_model, d_model) self.out_proj nn.Linear(d_model, d_model) def forward(self, x, maskNone): # x: [batch, seq_len, d_model] batch, seq_len, _ x.shape q self.q_proj(x).view(batch, seq_len, self.num_heads, self.head_dim).transpose(1, 2) k self.k_proj(x).view(batch, seq_len, self.num_heads, self.head_dim).transpose(1, 2) v self.v_proj(x).view(batch, seq_len, self.num_heads, self.head_dim).transpose(1, 2) # 现在形状都是 [batch, num_heads, seq_len, head_dim] scores torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim) # scores: [batch, num_heads, seq_len, seq_len] if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) attn torch.softmax(scores, dim-1) out torch.matmul(attn, v) # [batch, num_heads, seq_len, head_dim] out out.transpose(1, 2).contiguous().view(batch, seq_len, self.d_model) return self.out_proj(out)这里有一个关键细节所有头的 Q/K/V 用一次线性投影算完再通过 view 和 transpose 切成多头。masked_fill(mask 0, float(-inf))是通用写法无论 mask 是 padding mask 还是 causal mask只要约定好「0 代表屏蔽」就能通用。softmax 在 -inf 上会算出 0 概率不会影响梯度。参数选择上d_model512、num_heads8 是最经典的比例head_dim64。如果你只有单张消费级显卡做验证实验时可以把 d_model 降到 128、head_dim 保持 64、num_heads 降到 2就能跑起来。但要注意d_model 和 num_heads 的比例不能乱改head_dim 太小比如低于 16会让每个头的表达能力不足模型容量涨不上去。3.2 前馈网络与残差连接自注意力之后的下一步是逐位置的前馈网络。它其实就是两个全连接层夹一个激活函数PyTorch 里通常写成class FeedForward(nn.Module): def __init__(self, d_model, d_ff, dropout0.1): super().__init__() self.net nn.Sequential( nn.Linear(d_model, d_ff), nn.ReLU(), nn.Dropout(dropout), nn.Linear(d_ff, d_model), nn.Dropout(dropout), ) def forward(self, x): return self.net(x)d_ff 是前馈网络的隐藏层维度原论文用的是 2048是 d_model512 的 4 倍。这个比例后来成了经验法则前馈层宽度取模型宽度的 4 倍左右太小则非线性变换能力不足太大则参数量和过拟合风险都上来。前馈网络之所以必要是因为自注意力本质上是「按权重做加权求和」它是一个线性操作权重来自输入但数值计算是线性的。真正让 Transformer 具备非线性表达能力的是前馈网络里的 ReLU。这点在讲解时值得单独强调很多人误以为注意力是模型唯一的非线性来源。残差连接和层归一化是让 Transformer 能堆到 12 层、24 层而不崩的关键。每一层子层的输出都是x x sublayer(x)后接 LayerNorm。注意原版结构里是先加残差再 LayerNorm称为 post-norm后来很多实现采用 pre-norm先 LayerNorm 再进子层训练更稳定。讲解时提一下这两种顺序的差别即可深入展开留给训练章节。3.3 用 copy task 验证实现能学起来写完模块不代表实现正确验证最快的方式是跑一个 copy task给模型一串随机整数序列让它原样输出这串序列。这个任务简单到「只要注意力机制是对的就一定能学会」所以非常适合做实现的冒烟测试。import torch import torch.nn as nn class TinyTransformer(nn.Module): def __init__(self, vocab_size, d_model64, num_heads4, d_ff256, num_layers2): super().__init__() self.embed nn.Embedding(vocab_size, d_model) self.pos nn.Parameter(torch.randn(1, 128, d_model)) self.blocks nn.ModuleList([ nn.TransformerEncoderLayer(d_model, num_heads, d_ff, batch_firstTrue) for _ in range(num_layers) ]) self.out nn.Linear(d_model, vocab_size) def forward(self, x): seq_len x.size(1) h self.embed(x) self.pos[:, :seq_len, :] for block in self.blocks: h block(h) return self.out(h) # 训练数据源序列是随机 token目标序列是同一串 token vocab_size 32 seq_len 16 model TinyTransformer(vocab_size) optimizer torch.optim.Adam(model.parameters(), lr1e-3) loss_fn nn.CrossEntropyLoss() for step in range(2000): x torch.randint(1, vocab_size, (64, seq_len)) # 0 作为 pad 位置 logits model(x) loss loss_fn(logits.reshape(-1, vocab_size), x.reshape(-1)) optimizer.zero_grad() loss.backward() optimizer.step() if step % 200 0: print(fstep {step}: loss {loss.item():.4f})这个任务的收敛标准很直观loss 在 1000 步内应该从 3.4 左右随机猜测的交叉熵降到 1 以下。如果你发现自己实现的 attention 在这个任务上 loss 卡在 3 打头不动几乎可以断定 QKV 投影、mask、或者 reshape 里有一步写错了。这个 trick 是我自己的血泪经验与其在一开始就上真实数据集不如先用 copy task 把正确性锁死。注意上面代码里我用的是 PyTorch 内置的TransformerEncoderLayer实际复现原版时建议自己写一层 block把残差、LayerNorm、dropout 的位置控制在自己手里。内置层是优化过的黑匣子用来做快速验证可以用来理解结构不行。4. 训练与推理warmup、mask 策略与 KV cache4.1 学习率与 Adam 参数的工程设定Transformer 的训练和 CNN 有个显著差别它对学习率极其敏感。原论文用的是 Noam 方案——先线性 warmup 再按步数倒数衰减原因是训练初期模型参数还没就位如果一开始就用大学习率注意力分布会被推到错误的方向且难以回退。常见做法是前 4000 步或总步数的 1%~2%线性升温到峰值之后按 sqrt(step) 倒数衰减。在实操里我一般直接设一个经验组合Adam lr1e-3base 模型或 5e-4大模型beta10.9beta20.98epsilon1e-9。注意 epsilon 这里比 PyTorch 默认的 1e-8 更小这是原论文里的配置。如果你训练过程中频繁出现 loss 变成 nan第一个怀疑对象就是 epsilon 太大导致 Adam 的分母修正不够。一个值得写在 PPT 里的表格参数常用范围调参方向峰值学习率5e-4 ~ 3e-3模型越大峰值越低warmup 步数总步数 1%~2%数据噪声越大 warmup 越长beta20.98 ~ 0.999loss 震荡时调大epsilon1e-9 ~ 1e-8出现 NaN 时优先检查梯度裁剪max_norm1.0长序列任务必须开4.2 训练时的 label smoothing 与梯度裁剪label smoothing 是把 one-hot 标签换成软标签例如对 vocab32 的任务把正确位置的 1.0 变成 0.9其余 31 个位置平均分 0.1。它的作用是防止模型对训练集过于自信从而让注意力分布更平滑生成的文本不那么「死板」。原论文里 smoothing0.1这个值在文本生成任务里基本不用改。梯度裁剪是另一道安全网。Transformer 堆积多层后梯度范数很容易冲高特别是在训练初期。做法很简单torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)放在optimizer.step()之前。它的效果是保证梯度方向不变、但长度被限制在 max_norm 以内。我在长序列任务上见过太多 loss 突然跳高的案例最后查下来都是梯度爆炸开了裁剪之后问题消失。这个操作不应该被认为是「掩盖问题」而应该被视为稳定的训练基础设施。还有一点是初始化。PyTorch 的 Linear 层默认初始化对 Transformer 通常够用但如果你自己实现了 embedding 和输出层注意输出层和 embedding 是否要共享权重——原论文里两者共享这能省大量参数也让 embedding 学到更有意义的语义结构。实践里共享权重还有一个额外好处模型更容易在短训练步数内收敛。4.3 推理时的自回归与 KV cache训练时 decoder 可以并行——teacher forcing 让所有位置的输入同时给你loss 同时算。但推理时必须逐个 token 生成先有 token1才能预测 token2。这就引出自回归解码的概念。每次前向都从头算一遍完整序列是浪费的因为位置 1 到 i-1 的 K 和 V 在预测 token i-1 时已经算过了它们是确定的历史不需要重算。KV cache 就是把每一层已经算好的 K、V 缓存下来下次只算新 token 的 Q并复用缓存的 K、V 做注意力。def decode_one_step(model, token, past_kv, position): # token 形状 [batch, 1]past_kv 保存此前所有步的 K/V q model.q_proj(token) # 只有当前 token 的 query k model.k_proj(token) v model.v_proj(token) # 和缓存的历史 K/V 拼接 full_k torch.cat([past_kv[0], k], dim2) full_v torch.cat([past_kv[1], v], dim2) scores torch.matmul(q, full_k.transpose(-2, -1)) / math.sqrt(head_dim) # 不再需要 causal mask当前 token 只能 attend 自己和过去 attn torch.softmax(scores, dim-1) out torch.matmul(attn, full_v) return out, (full_k, full_v)设计上这个函数返回更新后的 past_kv 供下一步继续传入。注意生成阶段不再需要 look-ahead mask因为每一步的位置天然大于或等于历史位置不可能看到未来。KV cache 带来的提升不是小数目。假设生成长度 1024 的序列不缓存的话复杂度是 O(T^2) 的矩阵运算缓存后每一步只做常数长度的矩阵乘训练和推理的差距从这里体现得最直接。面试或分享时被问到「Transformer 推理为什么不能并行」「本质是生成任务的条件依赖以及 KV cache 的存在让每一步只依赖于当前输入和历史缓存」是标准答案。另外做「感知经验知识蒸馏」这类想用大模型产出监督信号、再训练一个小模型的任务时缓存的价值会更明显蒸馏通常要跑几百万条样本的大模型推理每一条的生成长度都不短把 KV cache 做对能省下成倍的推理时间。这也是这类任务真正落地时最容易被低估的计算成本来源。5. Transformer 训练避坑5 个容易翻车的细节5.1 现象loss 卡住不动或降得极慢这是初学 Transformer 时遇到最多的现象。排查看似复杂其实就三个方向学习率、mask、初始化。原因一学习率不对。Transformer 对学习率太敏感lr1e-2 在 CNN 上可能没事在 Transformer 上会直接发散。原因二padding mask 位置搞错pad token 的 embedding 参与更新等于往输入里掺了噪声。原因三embedding 没做缩放。解决方法是先跑一个 batch 看输出。我的习惯是固定随机种子取 8 条样本用lr1e-4跑 50 步如果 loss 完全不动就检查代码本身如果 loss 降了但很慢把 lr 调到 1e-3 再试。50 步内看不到下降迹象说明实现有 bug而不是参数问题。5.2 现象验证集 loss 突然飙升训练集正常下降、验证集突然爆掉最常见的元凶是 padding mask 没有传给验证阶段。很多人会在训练数据里带上 mask但验证时图省事直接 forward 而不传 maskpad 位置在 softmax 里获得了注意力权重等于把无效信息灌进了结果里。另一个常见原因是序列长度从训练短序列切换到了验证长序列位置编码外推失效。如果是正弦余弦位置编码超过训练长度后位置向量的高频分量会以不可预期的方式运作loss 表现会很怪。解决方法是要么训练时就随机截取不同长度做数据增强要么显式限制验证序列长度不超过训练时的最大长度。5.3 现象显存爆炸显存炸掉几乎都发生在注意力分数矩阵上。序列长度 512、batch 16、8 个头时scores 矩阵是16 * 8 * 512 * 512个 float约 1.34 亿个元素占 500MB 以上。序列长度一长这个矩阵是平方增长的。解决思路有三个方向。第一降低 batch size。第二使用 memory-efficient attention 的实现比如 PyTorch 里scaled_dot_product_attention的 flash 路径它不显式物化完整分数矩阵显存占用从 O(n^2) 降到 O(n)。第三如果序列真的很长考虑对注意力做窗口化、稀疏化或者改用线性注意力近似。注意第三类方案会改变模型行为不要在没有评估的情况下盲上。5.4 现象生成结果大量重复重复的原因可能是多方面的但最被忽视的一个是 label smoothing 过大和 sampling 策略不当的组合。smoothing 把真实标签的概率分摊出去模型在生成时后验概率趋于平滑加上 temperature 设置不当就容易在最高概率的几个 token 之间循环。另一种情况是自回归推理时长序列越走越偏与训练时的 teacher forcing 分布不一致即 exposure bias。缓解手段包括训练时做 scheduled sampling有一定概率喂自己的生成结果而不是 ground truth推理时用 top-p 或 top-k 采样还有就是把训练目标里对重复的惩罚显式加进去。值得一提的是一条容易被忽略的经验很多重复问题实际上是数据问题——训练语料里大量重复短语模型只是学会了数据里的模式。先把语料去重再说采样参数的事。5.5 现象多头注意力实现错误导致训练异常这属于代码正确性 bug但非常隐蔽。最常见的错误是 view 和 transpose 的顺序搞反。正确做法是先 view 成 4 维再 transpose即[batch, seq_len, num_heads, head_dim] - [batch, num_heads, seq_len, head_dim]。有人图省事先 transpose 再 view结果把 head 维和 seq 维切错了模型也能训但性能永远上不去。判断这种 bug 的好办法是前面 3.3 节的 copy task。如果 copy task 都过不了几乎就是这个问题。另一个排查手段是用一个小脚本对比你自己的实现和 PyTorch 内置nn.MultiheadAttention的输出随机输入下两种输出的最大误差应当在 1e-4 级别。查 bug 时不要靠肉眼读代码靠数值对比更高效。6. 把 Transformer 讲给别人听从一张 A4 纸到 15 分钟讲解6.1 先画三层结构图讲了这么多最后回到 PPT 本身。一份好的讲解材料不应该从公式出发而应该从图出发。我习惯准备三张图第一张是单层 block 的完整数据流从输入向量到输出向量把残差连接画成旁路第二张是展开后的多头注意力细节重点标出 Q/K/V 三条投影路径和拼接后的输出投影第三张是完整的 encoder-decoder 架构注明 cross-attention 的 Q 来自 decoder、K/V 来自 encoder。三张图层层递进比任何文字都好使。6.2 用一条链路串完整场讲解给组内分享时我最常用的开场是一句话跟到底token 先过 embedding 得到向量加上位置编码然后进入多头注意力——在这里每个 token 通过 Q 去查询所有 token 的 Ksoftmax 归一化后用权重去加权 V多个头的结果拼起来过一层线性层接下来进入前馈网络做逐位置的非线性变换这两大块各自包着残差和 LayerNorm整个 block 堆 N 层之后输出接一个线性层映射到词表大小。这条链路能把 15 分钟的讲解串起来中间随时停下来展开某一步。有人问「为什么这里要残差」——因为几十层网络不残差根本训不动有人问「为什么 attention 后面还有个 FFN」——因为注意力是线性加权真正的非线性来自 FFN。每个问题都对应到链路上一个具体环节听众不会迷失。6.3 收尾留三个值得延伸的问题如果听众里有人想继续深入我一般丢出三个延伸方向一是位置编码的改进方案从正弦编码到 RoPE 的相对位置编码二是长序列场景下的注意力近似方法三是和「感知经验知识蒸馏」这类范式结合时如何用大模型的注意力分布去监督小模型。这些可以作为下一场分享的预告比在当场展开有效得多。我自己的习惯是每次准备这类分享前都先用一张 A4 纸把完整结构图画出来画不出来就说明还有地方没懂先补课再上讲台。这套方法帮我在多次组内分享里避开了「讲完没人能复现」的尴尬。如果你正要准备这样一份 Transformer 讲解材料也希望这套拆解能帮你把内容组织得更实。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。