☰
95页PPT拆解Transformer:从注意力公式到GPT、ViT完整链路
2026/10/17 19:00:44 网站建设 项目流程

简介:这份PPT面向深度学习入门与进阶学习者,系统讲解Transformer从论文原理到NLP、CV两大领域演进的完整脉络,帮助读者建立对注意力机制与主流大模型的整体认知。内容共95页,涵盖Transformer背景、整体架构、输入部分、编码器、解码器、输出部分及其他细节,并延伸至GPT-1与BERT、GPT-2、GPT-3、InstructGPT与ChatGPT,以及ViT、CLIP与DALL·E-1、Diffusion Model与DALL·E-2等代表性工作,兼顾机器翻译、分类、分割、检测等任务场景。资源包为1个pptx文件,约17.48MB,结构清晰、图文并茂,适合课堂讲授或自学翻阅。目前已有2515人学习下载,可作为理解Transformer核心机制、梳理大模型发展路线与跨模态应用的参考材料。

1. 95 页 PPT 拆解 Transformer:从注意力公式到 GPT、ViT 的完整链路

如果你正在找一份能把 Transformer 从公式推导到 NLP、CV 两条落地路线讲透的课件,这份 95 页的 PPT 值得放进你的学习清单。它没有停留在“Attention is all you need”这句口号上,而是把输入嵌入、位置编码、多头注意力、编码器堆叠、解码器掩码、输出层这些模块逐个拆开,再顺着 GPT-1 到 ChatGPT、ViT 到 Diffusion Model 的脉络往下讲。适合刚学完深度学习基础、想系统啃 Transformer 的算法工程师,也适合需要一份现成课件做内部分享的从业者。我翻完之后最大的感受是:它把很多教程里一笔带过的“为什么除以根号 dk”“位置编码为什么用 sin/cos”都给了推导和数值例子,这对排查模型不收敛、位置信息丢失这类玄学问题很有帮助。

2. Transformer 输入部分:词嵌入与位置编码的工程细节

2.1 从“我爱你”到 (10, 512) 张量:嵌入层到底做了什么

PPT 里用中文“我爱你”翻译成英文“I love you”作为贯穿例子,这个选法很聪明,因为句子短、词汇少,每一步的张量形状都能手算验证。假设中文词汇表大小是 20000,那么“我”对应 37,“爱”对应 4201,“你”对应 197,输入变成一个长度为 10 的数组,不足的位置补 0。这一步在工程上对应的是 tokenizer 的 vocab 映射,常见做法是用 BPE 或 WordPiece 先切子词,再查表转 ID。

接下来进入嵌入层。PPT 里明确说嵌入层就是一个单层 MLP,把每个 token ID 映射成 512 维向量。这里有个容易翻车的点:很多人以为嵌入层是“查表”,所以不需要初始化,实际上查表操作等价于一个 one-hot 乘以权重矩阵,权重依然要初始化。我一般会用正态分布初始化,std 取 0.02 左右,太大容易在深层堆叠时梯度爆炸,太小则不同 token 的向量区分度不够。

import torch import torch.nn as nn class TokenEmbedding(nn.Module): def __init__(self, vocab_size, d_model): super().__init__() # 等价于 one-hot 查表,但计算上更高效 self.embedding = nn.Embedding(vocab_size, d_model) # 常见初始化:std=0.02,避免深层堆叠时方差累积 nn.init.normal_(self.embedding.weight, mean=0.0, std=0.02) def forward(self, x): # x: (batch, seq_len) 的 token ID return self.embedding(x) # 输出 (batch, seq_len, d_model)

这段代码里vocab_size对应 PPT 里的 20000,d_model对应 512。nn.Embedding的 forward 接收的是整数张量,输出浮点向量。注意padding_idx参数如果设了 0,那么填充位置的梯度会被屏蔽,这在变长序列训练时能省掉不少无效计算。

2.2 位置编码:为什么 sin/cos 能表达绝对位置

PPT 里用黄色高亮标出了位置编码的加法操作,并给出公式:偶数维度用 sin(pos/10000^(2i/d_model)),奇数维度用 cos。它强调这是“绝对位置信息”,但没展开的是:这种编码方式能让模型通过线性变换学到相对位置关系。具体来说,位置 pos+k 的编码可以表示为 pos 编码的线性函数,这让模型在推理时即使遇到训练没见过的长度,也能外推一部分。

工程上,位置编码有两种实现方式:一种是像 PPT 里那样预计算好一个 (max_len, d_model) 的矩阵,训练时直接切片相加;另一种是把位置编码设为可学习参数。前者不增加参数量,后者更灵活但需要更多数据。我一般会在数据量小于百万级时用预计算 sin/cos,大于百万级时尝试可学习位置编码,但后者在长文本外推时容易崩。

import math def positional_encoding(max_len, d_model): pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) 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) return pe # (max_len, d_model)

div_term那行是数值稳定写法,等价于 1/10000^(2i/d_model),但避免了直接算幂次时的溢出。pe[:, 0::2]和pe[:, 1::2]分别填偶数和奇数维度。实际使用时,把pe[:seq_len]加到嵌入输出上即可。注意如果用了 padding,填充位置也会被加上位置编码,但后续注意力掩码会把这些位置屏蔽掉,所以不影响结果。

3. 编码器与多头注意力:从 q/k/v 到梯度稳定的完整推导

3.1 单头注意力:q、k、v 的物理含义与计算流程

PPT 用“The animal didn't cross the street because it was too tired”这个经典例子解释自注意力:模型要知道“it”指代“animal”,就需要让“it”的查询向量 q 和“animal”的键向量 k 做点积,得到一个高分,从而在加权求和时给“animal”的值向量 v 更大权重。这个解释很直观,但落到代码上,关键是把 q、k、v 的维度搞清楚。

假设输入 x 的形状是 (10, 512),经过三个线性层分别得到 q、k、v,每个的形状是 (10, 64)。这里的 64 是单头维度,512/8=64,8 是头数。PPT 里明确写了“除以根号 dk 的原因:为了梯度稳定”。具体来说,q 和 k 的点积方差会随 dk 增大而增大,如果不缩放,softmax 的输入会落在饱和区,梯度接近 0。除以根号 64 等于 8,正好把方差拉回 1 附近。

import torch.nn.functional as F def scaled_dot_product_attention(q, k, v, mask=None): # q: (batch, 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: scores = scores.masked_fill(mask == 0, -1e9) attn = F.softmax(scores, dim=-1) return torch.matmul(attn, v), attn

mask在解码器里是下三角矩阵,防止当前位置看到未来 token。-1e9是常见做法,比-inf更安全,避免 NaN。返回的attn可以拿来做可视化,检查模型是否真的把注意力放到了正确位置。

3.2 多头注意力:为什么要拆成 8 个头

PPT 里把多头注意力画成多个单头并行,最后拼接再线性变换。很多人会问:既然单头也能算注意力,为什么还要多头?我的理解是,多头相当于让模型在多个子空间里分别学习不同的关注模式。比如一个头可能关注语法依赖,另一个头关注语义相似度。如果只用一个头,这些模式会互相干扰,平均下来反而学不好。

工程上,多头注意力的实现通常是把 d_model 拆成 heads × d_k,然后通过 reshape 和 transpose 把 heads 维度提出来,并行计算后再合并。这样比循环每个头快得多。注意d_model必须能被heads整除,否则 reshape 会报错。我见过有人设 d_model=512、heads=7,结果调试了半天才发现是整除问题。

class MultiHeadAttention(nn.Module): def __init__(self, d_model, heads): super().__init__() assert d_model % heads == 0 self.d_k = d_model // heads self.heads = heads self.w_q = nn.Linear(d_model, d_model) self.w_k = nn.Linear(d_model, d_model) self.w_v = nn.Linear(d_model, d_model) self.w_o = nn.Linear(d_model, d_model) def forward(self, q, k, v, mask=None): batch = q.size(0) # 线性变换后拆头:(batch, seq_len, d_model) -> (batch, heads, seq_len, d_k) q = self.w_q(q).view(batch, -1, self.heads, self.d_k).transpose(1, 2) k = self.w_k(k).view(batch, -1, self.heads, self.d_k).transpose(1, 2) v = self.w_v(v).view(batch, -1, self.heads, self.d_k).transpose(1, 2) out, _ = scaled_dot_product_attention(q, k, v, mask) # 合并头:(batch, heads, seq_len, d_k) -> (batch, seq_len, d_model) out = out.transpose(1, 2).contiguous().view(batch, -1, self.heads * self.d_k) return self.w_o(out)

transpose(1, 2)把 heads 维度换到 seq_len 前面,这样scaled_dot_product_attention里的矩阵乘法会自动对每个头并行。contiguous()是因为 transpose 后内存不连续,view 会报错。这个坑我踩过不止一次,血泪经验是:只要看到 transpose 后面跟 view,先加 contiguous 再说。

3.3 前馈网络与残差连接:编码器模块的完整堆叠

PPT 里编码器模块的结构是:多头自注意力 → 残差 + LayerNorm → 前馈网络 → 残差 + LayerNorm。前馈网络通常是两个线性层,中间夹一个 ReLU,隐藏层维度是 d_model 的 4 倍。这个 4 倍不是随便定的,原论文里试过 1 倍、2 倍、4 倍,4 倍在翻译任务上最好。但到了 CV 或大语言模型,这个比例会变,比如 ViT 用 4 倍,GPT-3 用 4 倍,有些高效变体会用 2 倍甚至 1 倍来省显存。

残差连接的作用是缓解深层网络的梯度消失。LayerNorm 放在残差之后还是之前,有两种流派:Post-LN 和 Pre-LN。原论文用 Post-LN,但后来大家发现 Pre-LN 训练更稳定,不需要 warmup 也能收敛。PPT 里没有展开这个细节,但如果你要复现,建议先用 Pre-LN,等模型稳定后再试 Post-LN。

class EncoderLayer(nn.Module): def __init__(self, d_model, heads, d_ff=2048, dropout=0.1): super().__init__() self.attn = MultiHeadAttention(d_model, heads) self.ffn = nn.Sequential( nn.Linear(d_model, d_ff), nn.ReLU(), nn.Linear(d_ff, d_model) ) self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) self.dropout = nn.Dropout(dropout) def forward(self, x, mask=None): # Pre-LN:先归一化再进子层 attn_out = self.attn(self.norm1(x), self.norm1(x), self.norm1(x), mask) x = x + self.dropout(attn_out) ffn_out = self.ffn(self.norm2(x)) x = x + self.dropout(ffn_out) return x

d_ff=2048对应 d_model=512 的 4 倍。dropout=0.1是常见默认值,数据量小的时候可以调到 0.2 或 0.3。注意 Pre-LN 里norm1(x)被用了三次,实际实现时可以只算一次存下来,省点计算。

4. 解码器与输出层:掩码注意力和自回归生成的实现

4.1 解码器模块:三个子层与两种注意力

PPT 里解码器模块比编码器多了一个子层:编码器-解码器注意力。这个子层的 q 来自解码器上一层的输出,k 和 v 来自编码器的最终输出。这样做的目的是让解码器在生成每个词时,能动态地从编码器输出的源序列里提取相关信息。比如翻译“我爱你”时,生成“love”的时候,模型会重点关注编码器里“爱”对应的位置。

解码器的自注意力层需要加掩码,防止当前位置看到未来 token。这个掩码是一个上三角为 0、下三角为 1 的矩阵,形状是 (seq_len, seq_len)。实际实现时,通常用torch.tril生成下三角矩阵,再扩展成 (batch, heads, seq_len, seq_len) 的形状。

def causal_mask(seq_len): # 下三角为 1,上三角为 0 mask = torch.tril(torch.ones(seq_len, seq_len)).bool() return mask # (seq_len, seq_len)

在scaled_dot_product_attention里,mask == 0的位置会被填成-1e9,softmax 后权重接近 0。注意编码器-解码器注意力不需要因果掩码,因为编码器输出是完整的源序列,解码器可以看全部。

4.2 输出层:从 512 维向量到词表概率

解码器最后一层的输出形状是 (batch, seq_len, 512),经过一个线性层映射到词表大小,比如 20000,得到 (batch, seq_len, 20000) 的 logits,再经过 softmax 得到每个位置的概率分布。训练时用交叉熵损失,推理时用贪心搜索或束搜索。

PPT 里没有展开束搜索的细节,但工程上束搜索的beam_size是个关键参数。设 1 就是贪心,设 5 到 10 通常能提升生成质量,但计算量线性增长。我一般会在验证集上试 3、5、8 三个值,选 BLEU 或 ROUGE 最高的。

class Transformer(nn.Module): def __init__(self, vocab_size, d_model=512, heads=8, num_layers=6): super().__init__() self.embedding = TokenEmbedding(vocab_size, d_model) self.pos_enc = positional_encoding(5000, d_model) self.encoder_layers = nn.ModuleList([EncoderLayer(d_model, heads) for _ in range(num_layers)]) self.decoder_layers = nn.ModuleList([DecoderLayer(d_model, heads) for _ in range(num_layers)]) self.fc_out = nn.Linear(d_model, vocab_size) def forward(self, src, tgt, src_mask=None, tgt_mask=None): src_emb = self.embedding(src) + self.pos_enc[:src.size(1)].to(src.device) tgt_emb = self.embedding(tgt) + self.pos_enc[:tgt.size(1)].to(tgt.device) for layer in self.encoder_layers: src_emb = layer(src_emb, src_mask) for layer in self.decoder_layers: tgt_emb = layer(tgt_emb, src_emb, tgt_mask, src_mask) return self.fc_out(tgt_emb) # (batch, tgt_len, vocab_size)

num_layers=6是原论文的默认值,但实际任务里 3 到 12 层都常见。层数越多,参数量越大,训练越慢,但表达能力越强。我一般会从 3 层开始调,等 pipeline 跑通了再加深。

5. 从 NLP 到 CV:GPT、ViT、Diffusion 的演进路线与避坑

5.1 GPT 系列与 BERT:预训练范式的两条路

PPT 后半部分把 GPT-1、GPT-2、GPT-3、InstructGPT、ChatGPT 串成一条线,核心讲的是“预训练 + 微调”到“预训练 + 提示”的转变。GPT 系列用解码器架构,做自回归语言建模;BERT 用编码器架构,做掩码语言建模。两者的区别在于:GPT 只能看左边,适合生成;BERT 能看两边,适合理解。

工程上,GPT 的注意力掩码是下三角,BERT 没有掩码。如果你要复现 BERT,注意[CLS]和[SEP]的位置编码要正确,否则下游任务会掉点。我见过有人把[CLS]放在句尾,结果分类任务准确率直接掉 10 个点。

5.2 ViT 与 Diffusion:Transformer 在 CV 领域的落地

ViT 的做法是把图像切成 16×16 的 patch,每个 patch 展平后经过线性层映射成 token,再加上位置编码,送进标准 Transformer 编码器。PPT 里提到 CLIP 和 DALL-E,前者用对比学习对齐图像和文本,后者用 Transformer 做图像生成。Diffusion Model 则是另一条路线,用 U-Net 做去噪,但 DALL-E-2 把 U-Net 换成了 Transformer。

ViT 的坑在于:它需要大量数据预训练,否则不如 CNN。如果你只有几万张图,直接训 ViT 大概率过拟合。常见做法是用预训练权重微调,或者用 DeiT 的蒸馏方法,让小模型也能训。

5.3 避坑与常见问题排查

现象:训练 loss 不降,一直卡在 10 左右。原因:学习率太大或 warmup 不够。Transformer 对学习率敏感,原论文用了 4000 步 warmup。 解决:加 warmup,峰值学习率设 1e-4 到 3e-4,用 Adam 的 beta2=0.98。

现象:推理时生成的句子重复。原因:解码器掩码写错,或者束搜索的重复惩罚没设。 解决:检查causal_mask是否下三角,束搜索加repetition_penalty=1.2。

现象:显存溢出,batch 只能设 1。原因:注意力矩阵是 O(seq_len^2) 的,长序列直接爆。 解决:用梯度累积模拟大 batch,或者换 FlashAttention 这类高效实现。

现象:位置编码加错了维度。原因:pe[:seq_len]的形状是 (seq_len, d_model),但嵌入输出是 (batch, seq_len, d_model),广播时可能对不上。 解决:用pe[:seq_len].unsqueeze(0)显式加 batch 维度。

现象:多头注意力 reshape 报错。原因:d_model 不能被 heads 整除,或者 transpose 后没加 contiguous。 解决:检查整除关系,transpose 后加.contiguous()。

6. 用这份 PPT 做内部分享:一个可复用的讲解节奏

如果你打算拿这份 95 页 PPT 做内部分享,我建议不要按页码从头讲到尾,而是按“输入 → 编码器 → 解码器 → 输出 → 演进”五段来切。每段控制在 15 到 20 分钟,中间留 5 分钟提问。第一段讲输入部分时,重点演示“我爱你”到 (10, 512) 的张量变化,让听众手算一遍位置编码的前几个值,这样后面讲注意力时他们才有体感。

第二段讲编码器时,我会把 q、k、v 的计算过程用 Excel 或 NumPy 现场跑一遍,特别是除以根号 dk 前后的 softmax 分布对比。这个对比很直观:不缩放时 softmax 输出接近 one-hot,梯度几乎为 0;缩放后分布更平滑,梯度能传回去。第三段讲解码器时,重点演示掩码矩阵的生成和可视化,让听众看到上三角被屏蔽的效果。

第四段讲输出层时,可以现场跑一个贪心搜索和束搜索的对比,用同一个输入看生成结果差异。第五段讲 GPT、ViT、Diffusion 时,不要展开太多细节,而是画一条时间线,标出每个节点的关键创新。最后留一个动手环节:让听众用 PyTorch 写一个单头注意力,输入随机张量,验证输出形状和 softmax 权重之和为 1。

我自己的习惯是,每次讲 Transformer 之前,都会把这份 PPT 里的公式和代码对照着重跑一遍,确保没有版本兼容问题。从那以后我每次做技术分享,都强制走一遍“手算 + 代码验证 + 可视化”三步,避免纸上谈兵。希望这份拆解能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询