资讯动态

从RNN到Transformer:144页PPT补全序列建模认知缺口

发布时间:2026/10/18 0:16:26 来源:尧图企业网站定制
简介这份资源是面向NLP初学者、算法工程师与高校学生的Transformer系统学习资料由卡内基梅隆大学讲师Jean de Dieu Nyandwi主讲以144页PPT配合视频课程的形式帮助读者从零建立对Transformer架构的完整认知。内容从Transformer出现前的序列建模方法切入梳理多层感知机、一维卷积网络与RNN、LSTM、GRU的局限再深入注意力机制、注意力可视化、大语言模型、Transformer实现、当代架构演进与高效Transformer等核心主题并附有趣味推文与可选练习。资源包内共1个PDF文件大小约5.55MB以图文讲义形式呈现便于对照视频逐页学习与复习。目前已有495人学习下载。学完后读者能理解Transformer内部工作原理掌握其在NLP及更广领域的应用思路并借助示例与练习将知识迁移到实际复杂问题的解决中。1. 从 RNN 到 Transformer这份 144 页 PPT 到底补的是哪块认知缺口如果你最近在调一个长文本分类任务发现 LSTM 训到第 8 个 epoch 梯度就开始飘或者用 CNN 做序列标注时感受野怎么堆都覆盖不到跨句依赖那这份 144 页的 Transformers 全面讲解 PPT 加配套视频大概率能把你从“知道 Attention 公式但说不清为什么 Q 和 K 要分开”的状态里拽出来。它覆盖的是一条完整的认知链路从 MLP、1D CNN、RNN/LSTM/GRU 在序列建模上的结构性缺陷讲起再落到 Attention is All You Need 的原始架构拆解、注意力可视化、LLM 演进、实现方式和高效 Transformer 变体。适合已经跑过至少一个 NLP 任务、但对 Transformer 内部数据流还停留在“抄 nn.TransformerEncoderLayer”阶段的从业者。PPT 本身是英文的但公式和结构图密度高配合视频过一遍比自己啃论文快不少。2. 为什么 RNN 和 CNN 没走到最后序列建模的并行化与全局上下文2.1 MLP、1D CNN、RNN 各自卡在哪MLP 处理序列的方式是把整个序列展平成一个固定长度的向量直接送进全连接层。问题很直接序列顺序信息在展平那一刻就丢了而且参数量随序列长度平方级增长。PPT 里给了一张对比图输入长度从 10 涨到 100MLP 第一层权重矩阵直接从 10×hidden 变成 100×hidden显存占用肉眼可见地炸。1D CNN 做序列建模的思路是用卷积核在时间轴上滑动。它的优势是容易并行——每个时间步的卷积计算互不依赖GPU 利用率高。但卷积核的感受野是局部的要覆盖长距离依赖就得堆层数。PPT 里引了一张 Text CNN 的结构图7 层卷积堆下来感受野也就覆盖 30 个 token 左右再长就力不从心了。而且卷积核权重在整条序列上共享对位置信息的感知是隐式的不像 RNN 那样有显式的顺序状态。RNN 的设计天然适合序列每个时间步的隐藏状态 h_t 依赖 h_{t-1} 和当前输入 x_t顺序信息被编码在状态传递里。但问题也出在这——它必须串行计算t 时刻算完才能算 t1GPU 的并行能力完全用不上。PPT 里用了一个很直观的对比同样处理 512 个 token 的序列RNN 需要 512 个串行步骤而 Transformer 的 self-attention 可以一次性算完所有位置的注意力权重。另外 RNN 的梯度不稳定问题在长序列上几乎是玄学LSTM 的门控机制缓解了一部分但没根治。2.2 LSTM 和 GRU 的改进边界LSTM 引入 cell state 和三个门输入门、遗忘门、输出门来控制信息流。PPT 里用了 Christopher Olah 那篇经典博客的图cell state 那条水平线贯穿整个时间步梯度可以沿着它稳定回传。遗忘门决定丢弃多少旧信息输入门决定写入多少新信息输出门决定当前时刻暴露多少状态给下一层。这套机制在 2017 年之前确实是序列建模的默认选择机器翻译、语音识别、文本分类都在用。GRU 是 LSTM 的简化版把三个门合并成两个重置门和更新门。参数量少了约 25%训练速度更快在很多任务上效果和 LSTM 持平。PPT 里给了一个经验判断序列长度在 100 以内、数据量不大时GRU 和 LSTM 的差距可以忽略但序列超过 300 之后LSTM 的 cell state 在长距离信息保持上更稳。但 LSTM 和 GRU 都没解决两个根本问题。第一是串行计算PPT 里原话是“modern AI accelerators are designed for parallel processing”RNN 的串行特性和硬件设计方向是拧着的。第二是全局上下文推理LSTM 的状态向量理论上可以编码整条序列的信息但实际上随着时间步推移早期信息会被后续输入稀释。机器翻译里一个典型翻车场景是源句开头的主语在翻译到句尾时已经被遗忘了导致代词指代错误。2.3 Self-Attention 怎么同时解决并行和全局依赖Transformer 的核心思路是把序列里每个位置和所有其他位置做一次注意力计算这样任意两个位置之间的路径长度都是 1不存在 RNN 那种“隔了 50 个时间步梯度就衰减”的问题。具体做法是每个 token 的 embedding 分别乘三个权重矩阵 W_Q、W_K、W_V得到 Query、Key、Value 三个向量。然后用 Q 和 K 做点积算注意力分数除以 sqrt(d_k) 做缩放softmax 归一化后对 V 加权求和。import torch import torch.nn.functional as F def scaled_dot_product_attention(Q, K, V, maskNone): Q: (batch, heads, seq_len, d_k) K: (batch, heads, seq_len, d_k) V: (batch, heads, seq_len, d_v) d_k Q.size(-1) # 缩放点积防止 softmax 梯度消失 scores torch.matmul(Q, K.transpose(-2, -1)) / (d_k ** 0.5) if mask is not None: # 用 -1e9 填充被 mask 的位置softmax 后趋近于 0 scores scores.masked_fill(mask 0, -1e9) attn_weights F.softmax(scores, dim-1) output torch.matmul(attn_weights, V) return output, attn_weights这段代码里d_k ** 0.5的缩放是必须的。PPT 里专门用了一页解释当 d_k 较大时Q 和 K 的点积结果方差会变大softmax 的输入落在梯度极小的饱和区训练会卡住。除以 sqrt(d_k) 把方差拉回 1 附近梯度就正常了。mask 参数在 decoder 的自注意力里用来遮住未来位置保证预测第 t 个 token 时只能看到前 t-1 个。多头注意力的做法是把 Q、K、V 在特征维度上切成 h 份每份独立做 attention最后拼接再过一个线性层。PPT 里给了一个参数原始论文用 h8d_model512所以每个头的 d_k d_v 64。多头的好处是不同头可以关注不同类型的模式——有的头关注语法依赖有的头关注位置邻近可视化那章会展开讲。3. 把 PPT 里的架构图跑起来从位置编码到 Encoder 堆叠3.1 位置编码为什么用正弦函数而不是可学习 embeddingTransformer 没有 RNN 的循环结构也没有 CNN 的局部感受野如果不加位置信息打乱输入 token 的顺序输出完全不变。PPT 里给了两种方案可学习的位置 embedding 和正弦位置编码。原始论文选了后者原因是正弦函数可以外推到训练时没见过的序列长度而可学习 embedding 在超过最大训练长度后就没有对应参数了。import math import torch import torch.nn as nn class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len5000, dropout0.1): super().__init__() self.dropout nn.Dropout(dropout) # 预计算位置编码矩阵避免每次 forward 重复计算 pe torch.zeros(max_len, d_model) position torch.arange(0, max_len, dtypetorch.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) pe pe.unsqueeze(0) # (1, max_len, d_model) self.register_buffer(pe, pe) def forward(self, x): # x: (batch, seq_len, d_model) x x self.pe[:, :x.size(1), :] return self.dropout(x)div_term那行是频率项10000.0是原始论文里的常数。偶数维度用 sin奇数维度用 cos这样任意两个位置之间的编码可以通过线性变换互相表示模型能学到相对位置关系。register_buffer把 pe 注册成不参与梯度更新的缓冲区跟着模型一起搬到 GPU 上但不会被优化器更新。3.2 Encoder 层的残差连接和 LayerNorm 顺序原始论文的 Encoder 层结构是多头自注意力 → 残差连接 LayerNorm → 前馈网络 → 残差连接 LayerNorm。PPT 里画的是 Post-LN 结构也就是 LayerNorm 放在残差相加之后。后来很多实现改成了 Pre-LN把 LayerNorm 放在子层输入之前训练更稳定尤其是层数深的时候。class EncoderLayer(nn.Module): def __init__(self, d_model, nhead, dim_feedforward2048, dropout0.1): super().__init__() self.self_attn nn.MultiheadAttention(d_model, nhead, dropoutdropout) self.linear1 nn.Linear(d_model, dim_feedforward) self.linear2 nn.Linear(dim_feedforward, d_model) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.dropout nn.Dropout(dropout) def forward(self, src, src_maskNone): # Post-LN: 先做注意力残差相加后再 norm attn_out, _ self.self_attn(src, src, src, attn_masksrc_mask) src self.norm1(src self.dropout(attn_out)) # 前馈网络两层线性 ReLU ff_out self.linear2(self.dropout(F.relu(self.linear1(src)))) src self.norm2(src self.dropout(ff_out)) return srcdim_feedforward默认是 2048是 d_model 的 4 倍。这个比例在原始论文里定的后来有研究说 4 倍不是最优但大部分实现还是沿用。前馈网络的作用是对每个位置独立做非线性变换PPT 里强调它和注意力是互补的——注意力负责跨位置信息交换前馈网络负责单位置特征提取。3.3 用 PyTorch 搭一个最小可跑的 Transformer Encoder把上面几块拼起来就是一个可以跑通前向传播的最小 Encoder。class TransformerEncoder(nn.Module): def __init__(self, vocab_size, d_model512, nhead8, num_layers6, dim_feedforward2048, dropout0.1): super().__init__() self.embedding nn.Embedding(vocab_size, d_model) self.pos_enc PositionalEncoding(d_model, dropoutdropout) self.layers nn.ModuleList([ EncoderLayer(d_model, nhead, dim_feedforward, dropout) for _ in range(num_layers) ]) self.d_model d_model def forward(self, src, src_maskNone): # embedding 后乘 sqrt(d_model) 是原始论文的做法 x self.embedding(src) * math.sqrt(self.d_model) x self.pos_enc(x) for layer in self.layers: x layer(x, src_mask) return xembedding后乘sqrt(d_model)这个操作容易被忽略。原始论文的解释是位置编码的值域在 [-1, 1] 附近而 embedding 的初始化方差和 d_model 相关乘 sqrt(d_model) 把两者量级拉到同一水平避免位置信息被 embedding 淹没。num_layers6是 base 版本的配置big 版本用了 6 层 encoder 6 层 decoderd_model 拉到 1024。跑通前向之后可以拿一个随机输入验证输出 shapemodel TransformerEncoder(vocab_size10000) src torch.randint(0, 10000, (2, 20)) # batch2, seq_len20 out model(src) print(out.shape) # torch.Size([2, 20, 512])如果 shape 不对先检查src的维度是不是 (batch, seq_len)PyTorch 的 MultiheadAttention 默认 batch_firstFalse需要转置或者初始化时指定batch_firstTrue。这个坑在 PPT 的实现章节里没展开但实际写代码时经常翻车。4. 注意力可视化与高效 Transformer从黑匣子到可解释性4.1 注意力权重怎么提取和解读PPT 里专门有一节讲注意力可视化用的是原始论文里的例子把 encoder 最后一层的注意力权重矩阵画成热力图横轴是 Key 位置纵轴是 Query 位置颜色深浅代表注意力分数。在英德翻译任务里可以看到某些头明显学到了语法对齐关系——比如动词和宾语之间的注意力权重特别高。提取注意力权重的关键是拿到 softmax 之后的attn_weights。PyTorch 的nn.MultiheadAttention默认不返回注意力权重需要设置need_weightsTrue。# 提取单层注意力权重 attn_output, attn_weights model.layers[0].self_attn( src, src, src, need_weightsTrue ) # attn_weights: (batch, seq_len, seq_len) # 取第一个样本画热力图 import matplotlib.pyplot as plt plt.imshow(attn_weights[0].detach().numpy(), cmapviridis) plt.colorbar() plt.xlabel(Key position) plt.ylabel(Query position) plt.show()注意need_weightsTrue会稍微拖慢计算因为要额外做一次 softmax 的输出保留。生产环境里如果不需要可视化设成 False 能省一点显存。另外多头注意力的权重是每个头独立的nn.MultiheadAttention返回的是所有头平均后的权重要看单个头的模式得自己拆in_proj的输出。4.2 高效 Transformer 的几条路线PPT 最后几页扫了一遍高效 Transformer 的变体主要分三类。第一类是稀疏注意力比如 Longformer 和 BigBird把全连接的注意力矩阵改成带状或块状稀疏模式复杂度从 O(n²) 降到 O(n)。第二类是低秩近似比如 Linformer用两个线性投影把 K 和 V 的序列维度压缩到固定长度注意力计算量大幅下降。第三类是核方法比如 Performer用随机特征映射近似 softmax 核把注意力写成核函数的形式。这些变体在 PPT 里没有展开公式推导但给了适用场景的判断序列长度在 512 以内时原始 Transformer 的 O(n²) 完全够用上高效变体反而增加实现复杂度序列超过 2048 之后稀疏注意力或低秩近似才有明显收益。另外高效 Transformer 在长序列上的效果通常比原始版本差一点PPT 里原话是“there is no free lunch”省了计算量往往要在精度上还回去。4.3 一个容易忽略的细节学习率 warmup原始论文用了 warmup 策略前 4000 步学习率线性增加之后按步数的平方根倒数衰减。PPT 里提了一句但没展开实际训练时这个策略对 Transformer 的收敛很关键。没有 warmup 的话训练初期注意力权重的梯度方差很大容易发散。class WarmupScheduler: def __init__(self, optimizer, d_model, warmup_steps4000): self.optimizer optimizer self.d_model d_model self.warmup_steps warmup_steps self.step_num 0 def step(self): self.step_num 1 lr (self.d_model ** -0.5) * min( self.step_num ** -0.5, self.step_num * self.warmup_steps ** -1.5 ) for param_group in self.optimizer.param_groups: param_group[lr] lrd_model ** -0.5是基础学习率base 版本 d_model512 时约等于 0.044。warmup 阶段学习率和步数成正比之后和步数的平方根成反比。这个调度器在每个 batch 后调用一次step()不是每个 epoch。5. 避坑与排查复现 Transformer 时最容易翻车的五个点5.1 现象训练 loss 从第一个 epoch 就卡在 0.69 不动原因分类任务里类别数设错了或者标签没有做 shift。PPT 里 decoder 的输入是右移一位的如果直接拿原始标签做 teacher forcing模型在预测第 t 个位置时看到了第 t 个位置的答案loss 会异常低然后迅速过拟合。但如果是分类任务0.69 约等于 ln(2)说明模型输出接近均匀分布大概率是标签和 logits 的维度对不上。解决检查nn.CrossEntropyLoss的输入维度。logits 应该是 (batch, num_classes)标签是 (batch,) 的整数张量。如果 logits 是 (batch, seq_len, num_classes)需要先 reshape 成 (batch*seq_len, num_classes)标签对应展平。5.2 现象验证集 loss 比训练集低很多原因Dropout 在验证时没关。PyTorch 的model.eval()会把 Dropout 和 BatchNorm 切到推理模式如果忘了调用验证时 Dropout 还在随机丢神经元loss 会偏高。但反过来如果训练时 Dropout 设得太大比如 0.5训练 loss 会偏高验证 loss 反而低这是欠拟合的信号。解决训练循环里model.train()和model.eval()要成对出现。PPT 里给的 dropout 默认值是 0.1实际用的时候如果数据量小于 10 万条可以降到 0.05 或者关掉。5.3 现象多头注意力训练后所有头的权重几乎一样原因初始化时 Q、K、V 的权重矩阵用了相同的随机种子或者学习率太小导致头之间没有分化。PPT 里可视化那节展示的是训练充分后不同头关注不同模式但如果训练不充分所有头会退化成近似相同的注意力分布。解决检查in_proj_weight的初始化PyTorch 默认用 Xavier 均匀初始化一般没问题。如果头之间还是没分化可以试着把学习率调大一点或者增加训练步数。另外有些实现会在注意力权重上加正交正则强制头之间解耦但 PPT 里没提这个技巧。5.4 现象位置编码加进去之后 loss 反而变差了原因位置编码的 scale 和 embedding 不匹配。如果 embedding 初始化方差很小而位置编码的值域在 [-1, 1]加完之后位置信息会主导输入模型学不到 token 本身的语义。原始论文乘 sqrt(d_model) 就是为了解决这个问题但有些实现漏掉了这一步。解决确认 embedding 后有没有乘math.sqrt(d_model)。另外如果用的是可学习位置 embedding初始化方差要设小一点比如 0.02避免和 token embedding 打架。5.5 现象GPU 显存够但训练速度上不去原因nn.MultiheadAttention在 batch_firstFalse 时内部会做转置如果输入本身就是 batch_first 格式转置操作会额外拷贝一份显存。另外 attention mask 如果每次 forward 都重新创建也会拖慢速度。解决初始化时指定batch_firstTrue输入保持 (batch, seq_len, d_model)。mask 在 dataset 的 collate_fn 里预计算好不要在每个 batch 的 forward 里现算。PPT 里没讲这些工程细节但实际训练大模型时这些开销累积起来很可观。6. 把 PPT 当索引而不是教材我的使用习惯和一条验证路径这份 PPT 的信息密度很高144 页里塞了从 RNN 缺陷到高效 Transformer 的完整链路但它的定位更像一个索引而不是教材。我自己的用法是先花 40 分钟把视频过一遍在 PPT 上标记出三个地方——注意力公式的推导、位置编码的实现、多头注意力的拆分逻辑。然后关掉视频打开 PyTorch 从零写一遍 EncoderLayer遇到卡住的地方再翻回对应页。这样走一遍下来比单纯看 PPT 或者单纯跑开源代码理解得深。验证自己有没有真正搞懂的一个方法是把nn.MultiheadAttention替换成手写的 Q、K、V 投影加 scaled dot-product attention看输出能不能对上。如果能对上说明你对权重矩阵的 shape 变换和头拆分逻辑清楚了。对不上的话大概率是in_proj_weight的切分顺序搞错了——PyTorch 把 Q、K、V 的权重拼在一个大矩阵里切的时候是按d_model维度均分的不是按头切。# 手动实现多头注意力验证和 nn.MultiheadAttention 是否等价 def manual_mha(x, in_proj_weight, in_proj_bias, out_proj_weight, out_proj_bias, nhead): batch, seq_len, d_model x.shape d_k d_model // nhead # 一次线性投影得到 Q、K、V qkv F.linear(x, in_proj_weight, in_proj_bias) Q, K, V qkv.chunk(3, dim-1) # 拆头: (batch, seq_len, nhead, d_k) - (batch, nhead, seq_len, d_k) Q Q.view(batch, seq_len, nhead, d_k).transpose(1, 2) K K.view(batch, seq_len, nhead, d_k).transpose(1, 2) V V.view(batch, seq_len, nhead, d_k).transpose(1, 2) # 缩放点积注意力 scores torch.matmul(Q, K.transpose(-2, -1)) / (d_k ** 0.5) attn F.softmax(scores, dim-1) out torch.matmul(attn, V) # (batch, nhead, seq_len, d_k) # 拼头 out out.transpose(1, 2).contiguous().view(batch, seq_len, d_model) return F.linear(out, out_proj_weight, out_proj_bias)跑通这个验证之后再回头看 PPT 里注意力可视化那节就能看懂每个头的注意力矩阵是怎么从attn变量里取出来的。从那以后我每次复现一个新的 Transformer 变体都会先用手写版本对一遍官方实现的输出确认 shape 变换和 mask 逻辑没写错再往下加训练循环。这个习惯帮我省了不少调参的后悔药。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑