尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Transformer前置概念详解:位置编码、注意力机制与残差连接

Transformer前置概念详解:位置编码、注意力机制与残差连接 很多人在学 Transformer 的时候都有过类似的经历翻了几篇“手撕 Transformer”的教程打开源码一看满屏都是Q、K、V、mask、layer_norm、residual每个符号都眼熟但串起来就不知道在干什么。还有人直接去啃原版论文《Attention Is All You Need》结果第一页的架构图就劝退了。说实话这个现象很普遍不是因为你数学差也不是因为不会写代码而是因为Transformer 把一堆前置概念叠在了一起。如果把 Transformer 比作一栋楼注意力机制是承重墙位置编码是水电管路残差连接和归一化是消防系统。你单看哪一块都能看懂但它们为什么要以这种顺序组合在一起才是真正的难点。所以我才想写这篇文章做一次系统的“Transformer 前置概念复习”把输入表示、注意力机制、多头设计、残差结构这些基础概念一次性讲透。这篇文章不追求把 Transformer 的所有细节都铺开而是帮你把那些绕不开的基础概念补齐。读完你能明白三件事为什么 Transformer 需要位置编码为什么 RNN 不需要。Q、K、V三个矩阵到底在算什么为什么要缩放点积。残差连接和 LayerNorm 在 Transformer 里不是“附加组件”而是决定模型能不能训练下去的关键。文章会带三个完整的 PyTorch 示例代码建议你边读边跑遇到问题可以直接对着第九节的排查表定位。1. 这篇文章真正要解决的问题先说实话Transformer 的学习曲线不是线性上升的它像一个台阶。你在学 RNN、LSTM 的时候感觉知识是连续积累的。但到了 Transformer所有抽象概念同时涌进来词嵌入和位置编码为什么要相加而不是拼接。Q、K、V为什么是三个矩阵而不是两个。Softmax 之前为什么要除以根号d_k。每个子层后面为什么要接残差连接和归一化。这些概念单个看都不难但它们组合在一起恰恰是初学者最容易崩溃的地方。这篇文章的目标读者不是那种已经能改 Vision Transformer 源码的人而是正准备学习 Transformer或者刚接触 Transformer 两周以内的入门者。在传统 NLP 项目中用过 RNN、CNN想搞清楚“为什么现在是 Transformer”。正在准备面试需要系统梳理注意力机制、多头机制、位置编码这些高频考点。准备手写 Attention 源码但需要先把前置概念理清楚的人。我在这篇文章里刻意没有展开完整 Transformer 的编码器-解码器结构因为那是下一步的事。先把地基打好后面看源码才不会一头雾水。整篇文章的核心判断是Transformer 的真正难点不在某个算法公式而在概念之间的依赖关系。每个概念单独拿出来都不难难的是它们如何协同工作。2. 从词嵌入到输入表示2.1 什么是 Embedding在自然语言处理任务里我们第一步要把文字变成数字。最简单的方式就是 One-Hot 编码假设词表里有 10000 个词每个词用一个 10000 维的向量表示该词对应位置为 1其他位置为 0。但 One-Hot 有两个问题向量太稀疏10000 维里只有 1 个位置是 1存储和计算都很浪费。任意两个词的 one-hot 向量都是正交的无法体现象征语义的相似性。比如“猫”和“狗”都是宠物但它们在向量空间里没有距离关系。Embedding 层解决的就是这个问题。它本质上是一个可学习的查找表模型用来把词索引映射到一个低维的稠密向量比如 512 维。在这个向量空间里语义相近的词它们的向量距离也更近。在 PyTorch 里这就是一行代码的事import torch import torch.nn as nn # 词表大小 10000嵌入维度 512 embedding nn.Embedding(num_embeddings10000, embedding_dim512) # 模拟一个 batch 为 2、序列长度为 10 的输入 input_ids torch.randint(0, 10000, (2, 10)) embedded embedding(input_ids) print(embedded.shape) # torch.Size([2, 10, 512])注意这里的输出形状是(batch, seq_len, embed_dim)。Transformer 后续处理的核心就是这种三维张量搞清楚这个形状后面看代码就会顺很多。2.2 为什么 Transformer 需要位置编码这是复习前置概念时最容易卡住的地方。RNN 处理序列时是逐个读取的先看第一个词再看第二个词然后再看第三个词。这种循环结构天然携带了顺序信息模型“知道”第二个词在第一个词之后。而 Transformer 是并行计算的它在处理输入序列时所有词一次性输入进去。在注意力计算中模型对序列中任何两个位置的依赖权重是通过点积计算出来的跟距离没有关系。这就带来一个问题在不加任何处理的情况下把句子序列顺序打乱Transformer 计算出的结果完全一样。也就是“我爱你”和“你爱我”在模型眼里没有区别。所以必须额外注入位置信息。这就是位置编码存在的意义。Transformer 论文里使用了正弦位置编码它的公式是PE(pos, 2i) sin(pos / 10000^(2i/d_model)) PE(pos, 2i1) cos(pos / 10000^(2i/d_model))其中pos是词在句子里的位置i是维度下标d_model是嵌入维度。用正弦和余弦组合是为了让模型能学习相对位置关系。比如位置 5 和位置 6 的位置编码向量可以通过某种线性变换联系起来。在 Transformer 中位置编码不是拼接在词嵌入后面而是直接相加。这是因为位置编码的维度和词嵌入维度一致相加后依然保留完整的语义信息和位置信息。2.3 代码实现正弦位置编码下面是 PyTorch 中实现正弦位置编码的完整代码建议直接跑一下看看输出形状import torch import math def positional_encoding(seq_len, d_model): 生成正弦位置编码 Args: seq_len: 序列长度 d_model: 嵌入维度 Returns: pe: (seq_len, d_model) 的位置编码矩阵 pe torch.zeros(seq_len, d_model) position torch.arange(0, seq_len, dtypetorch.float).unsqueeze(1) # 这里用 exp 计算 10000^(2i/d_model) 的对数形式保证计算稳定性 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 # 测试序列长度 50嵌入维度 512 pe positional_encoding(50, 512) print(pe.shape) # torch.Size([50, 512]) # 和词嵌入相加 embedded torch.randn(2, 50, 512) # 模拟一个 batch 的词嵌入 input_with_pos embedded pe.unsqueeze(0) print(input_with_pos.shape) # torch.Size([2, 50, 512])这段代码里最需要留意的是div_term的计算方式。原论文用的是10000^(2i/d_model)直接写次方在数值上可能溢出所以代码里转成了exp和log的复合形式。这也是很多开源实现的标准写法。另一种常见做法是让位置编码也作为可学习参数比如 BERT 中使用的是可学习的位置嵌入。从实际效果看可学习式在某些任务上略好但正弦编码的优点是能够外推到训练时没见过的序列长度。这两种方式没有绝对优劣理解它们都只是“给模型补充顺序信息”的手段即可。3. 注意力机制它到底在解决什么问题3.1 注意力不是 Transformer 发明的很多人以为注意力机制是从 Transformer 才出现的实际上这是个误区。注意力机制最早可以追溯到神经机器翻译在 Seq2Seq 模型里为了缓解长句子翻译效果下降的问题Bahdanau 等人在 2014 年提出了注意力机制。当时的做法是这样的解码器生成每个词时不再只依赖编码器最后一个时间步的隐藏状态而是对编码器所有时间步的隐藏状态做加权求和。这个权重就是“注意力权重”它表示解码器当前生成某个词时应该重点关注源语言句子里的哪些词。用一个翻译例子来说明。把英文句子 “I love China” 翻译成中文“我爱中国”。解码器在生成“我”这个词时注意力权重会集中分配在 “I” 上生成“爱”时权重会集中在 “love” 上。这就是注意力机制最朴素的思想在生成每个输出时动态地去“看”输入序列的不同部分。3.2 自注意力机制Self-AttentionTransformer 里的注意力不是编码器到解码器的注意力而是自注意力。简单说就是输入序列自己跟自己算注意力。自注意力做的事情可以这样理解处理一个句子时每一个词都要和句子里的所有词包括它自己建立一个“关系权重”。比如处理“因为下雨所以我没去公园”这句话模型在理解“我”这个词时需要重点关注“下雨”和“没去”从而知道“我”为什么没去。这种全局依赖关系正是 RNN 和 CNN 很难处理的。RNN 处理长句子时信息从前往后传递距离很远的词之间要经历很多时间步早期的信息容易衰减CNN 只能通过堆叠层数来扩大感受野但每层看到的范围仍然是局部的。Transformer 的自注意力让任意两个位置的词直接建立连接计算复杂度是O(n^2)其中n是序列长度。这在短序列上很高效但在长序列上成本很高所以后来才出现了各种改进方法比如稀疏注意力、滑动窗口注意力等。这些可以留到后续深入了解前置复习阶段先知道这个复杂度特性即可。4. Query、Key、Value注意力公式追根溯源4.1 从检索系统中借鉴的思想Q、K、V这三个字母对初学者来说很劝退但它们的含义其实可以用一个很常见的场景来理解检索。想象你在用手机备忘录搜索关键词。你输入的内容是Query查询备忘录里每篇笔记的标题是Key键也就是被检索的对象。系统会计算 Query 和每个 Key 的相关度用相关度作为权重对每篇笔记的内容Value值做加权求和。最后你得到的结果就是所有笔记内容按相关度混合后的信息。这套流程搬到 Transformer 里就变成了每个词都生成三个向量——Query、Key、Value。Query 表示“我想查什么”Key 表示“我能提供什么”Value 表示“我实际的内容是什么”。要怎么生成这三个向量呢把输入向量分别乘以三个可学习的权重矩阵W_Q、W_K、W_V。这三个矩阵是模型在训练过程中学出来的。用代码表示就是import torch import torch.nn as nn d_model 512 d_k 64 W_Q nn.Linear(d_model, d_k) W_K nn.Linear(d_model, d_k) W_V nn.Linear(d_model, d_k) x torch.randn(2, 10, d_model) # (batch, seq_len, d_model) Q W_Q(x) # (batch, seq_len, d_k) K W_K(x) # (batch, seq_len, d_k) V W_V(x) # (batch, seq_len, d_k)注意这里的d_k是 Query、Key、Value 的维度。在 Transformer 原论文里d_model 512多头数量h 8所以每个头的维度是d_k d_model / h 64。4.2 缩放点积注意力的数学原理有了Q、K、V之后接下来的操作就是核心的缩放点积注意力公式Attention(Q, K, V) softmax(Q * K^T / sqrt(d_k)) * V分开看每一步计算Q和K^T的点积。点积表示两个向量的相似程度结果是一个形状为(seq_len, seq_len)的矩阵第i行第j列表示“第 i 个词应该对第 j 个词投入多少关注”。除以sqrt(d_k)这一步叫缩放。为什么要缩放因为当d_k很大时点积结果的值域会变大导致 Softmax 进入饱和区域梯度变得极小影响训练。缩放后能让点积结果的方差保持在合理的范围内。对每一行做 Softmax得到归一化的注意力权重所有权重之和为 1。把这个权重矩阵乘以V得到加权求和之后的输出。为什么除以的是sqrt(d_k)而不是别的一个比较直观的解释是如果 Query 和 Key 中的每个元素都是独立分布的随机变量均值为 0、方差为 1那么它们的点积结果的均值为 0方差为d_k。为了让方差回到 1 附近的量级就需要除以sqrt(d_k)。4.3 代码实现缩放点积注意力手动实现一个缩放点积注意力会帮助你把这个公式刻进脑子里import torch import torch.nn.functional as F import math def scaled_dot_product_attention(Q, K, V, maskNone): 缩放点积注意力 Args: Q: (batch, ..., seq_len_q, d_k) K: (batch, ..., seq_len_k, d_k) V: (batch, ..., seq_len_k, d_v) mask: 可选的注意力掩码 Returns: output: (batch, ..., seq_len_q, d_v) attention_weights: (batch, ..., seq_len_q, seq_len_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) attention_weights F.softmax(scores, dim-1) output torch.matmul(attention_weights, V) return output, attention_weights # 测试 batch_size 2 seq_len 10 d_k 64 Q torch.randn(batch_size, seq_len, d_k) K torch.randn(batch_size, seq_len, d_k) V torch.randn(batch_size, seq_len, d_k) output, attn_weights scaled_dot_product_attention(Q, K, V) print(output.shape) # torch.Size([2, 10, 64]) print(attn_weights.shape) # torch.Size([2, 10, 10]) print(attn_weights[0]) # 每一行的和为 1这里的mask参数在 Transformer 解码器中非常关键。在解码过程中你预测第t个词时不应该看到第t个词之后的内容。所以解码器里会用一个上三角掩码把未来位置遮挡住把那些位置的分数设为很小的负数代码里是-1e9这样 Softmax 之后它们的权重就会非常接近 0。5. 多头注意力不只看一遍5.1 单头注意力的局限如果只用一组Q、K、V做注意力模型对词与词之间关系的建模是单一的。但语言中的关系是复杂的“苹果”和“水果”是类别关系。“苹果”和“红色的”是属性关系。“苹果”和“吃”是动作关系。如果只有一组注意力权重模型必须在所有这些关系之间做一个折中。这就像你判断一个人只看一个角度肯定不全面。多头注意力的思路很简单与其只用一组Q、K、V不如把d_model维度的输入拆成h个子空间每个子空间独立做一次注意力最后再拼起来。这样每个头可以从不同角度建模关系。5.2 多头注意力的计算流程具体流程是把输入向量x分别通过W_Q、W_K、W_V映射成Q、K、V形状为(batch, seq_len, d_model)。把最后一个维度拆成h个头每个头的维度是d_k d_model / h。在代码实现上通常用 reshape 操作把(batch, seq_len, d_model)变成(batch, h, seq_len, d_k)。对每一个头独立计算缩放点积注意力。把所有头的输出拼接起来恢复成(batch, seq_len, d_model)。通过输出投影矩阵W_O做一次线性变换让不同头的信息充分融合。需要注意的是多个头不是各自独立地随机初始化然后互不交流而是在最后通过W_O融合。这种设计既允许每个头专注不同的关系又保证最终输出能整合所有的信息。5.3 头数怎么选Transformer 论文里用的是 8 个头每个头维度 64。BERT base 也是 12 层、12 个头。头数太小模型学习关系模式的空间不够头数太大每个头的维度太小单个头能表达的信息有限。实际使用中8 到 16 是比较常见的范围。有一个常见误解多头注意力是“把模型分成了多个小模型”。更准确的说法是它是在高维空间里并行地做多种关系探测。每个头不是独立的模型它们只是同一个注意力层的不同投影子空间。6. 残差连接、LayerNorm 与前馈网络6.1 Transformer 一层的完整组成一个标准的 Transformer 编码器层在自注意力之后还做了两件重要的事残差连接。层归一化LayerNorm。然后是前馈网络Feed-Forward NetworkFFN。这里的结构可以用一个很短的公式表达output LayerNorm(x MultiHeadAttention(x)) output LayerNorm(output FeedForward(output))x 是子层的输入。注意力输出先和输入 x 相加再做归一化然后再进入前馈网络。前馈网络部分也做了一次残差连接。6.2 残差连接为什么必不可少残差连接是 ResNet 提出后广泛使用的技术它的核心操作就是让数据走一条“捷径”x F(x)其中F(x)是子层内部的变换结果。如果没有残差连接梯度需要穿过所有层的非线性变换太深容易导致梯度消失或梯度爆炸模型很难训练。有了残差连接梯度多了一条直接回传的高速通道可以让网络轻松建到几十层甚至上百层。对 Transformer 来说它保证了 12 层、24 层这样深度的模型依然可以被稳定优化。残差连接还有一个直观理解每一层是在输入的基础上“做增量调整”而不是彻底改写输出。这让模型在训练初期更容易保持稳定。6.3 LayerNorm 与 BatchNorm 的区别归一化Normalization是让数据分布保持稳定的手段。BatchNorm 是最常见的一种归一化方法但它在 NLP 任务里效果有限因为序列数据长度不固定batch 维度的统计量波动很大。Transformer 使用的是 LayerNorm。它的区别在于BatchNorm 在一个 batch 内、对每个特征维度做归一化。它统计的是同一个 batch 内所有样本的某一维度的均值和方差。LayerNorm 对每个样本单独做归一化。它统计的是单个样本内部所有特征的均值和方差。用一个表格对比对比维度BatchNormLayerNorm归一化方向按特征维度跨样本归一化按样本内部所有特征归一化受 batch 大小影响是batch 太小效果差否每个样本独立计算适合变长序列不适合适合Transformer 中是否使用不使用使用LayerNorm 在 Transformer 中的具体做法是对给定样本的所有特征维度也就是d_model维计算均值和方差再通过两个可学习的缩放和平移参数进行仿射变换。这样做的好处是让进入每个子层的数据分布保持稳定有效提升训练稳定性。6.4 前馈网络在做什么在自注意力之后Transformer 的每个位置还会经过一个前馈网络。它是一个两层的全连接网络通常中间维度会比d_model大很多。在论文里d_model 512前馈网络的中间层维度是 2048也就是做了 4 倍扩展。前馈网络做的事情可以这样理解自注意力是做“信息交互”让不同位置的词互相传递信息。前馈网络则是每个位置独立做“特征变换”把交互后得到的特征映射到更复杂、更高维的空间。这两个模块形成了一种分工注意力管关系前馈网络管特征。前馈网络用代码表示就是import torch import torch.nn as nn ffn nn.Sequential( nn.Linear(512, 2048), nn.ReLU(), nn.Linear(2048, 512) ) x torch.randn(2, 10, 512) output ffn(x) print(output.shape) # torch.Size([2, 10, 512])在 Transformer 的后续变体中有些模型把 ReLU 换成了 GELU有些则会加入 Dropout。实际使用中可以根据任务需求调整。7. RNN、CNN 与 Transformer为什么最后是 Transformer7.1 RNN 的困境RNN 按时间步逐字处理输入天然建模顺序依赖。但它的根本问题是无法并行计算。当前时间步的输出依赖前一个时间步所以训练一个很长的序列要一串时间步全部跑完速度非常慢。长距离依赖问题也很严重。信息从第 1 个词传到第 50 个词中间经过 49 次变换早期的信息很难保留。LSTM 和 GRU 通过门控机制缓解了这个问题但并没有从根本上改变。7.2 CNN 的局限CNN 的优势是并行计算卷积操作可以同时处理多个区域。但卷积核的感受野是局部的一个卷积核只能看到窗口内的几个词。要捕捉长距离依赖只能不停堆叠层数网络变得很深参数也大幅增加。7.3 Transformer 的破局Transformer 同时解决了两个问题并行计算。所有词同时输入不需要等待前一个时间步的计算。长距离依赖。自注意力让任意两个位置直接建立连接一步到位。这种架构优势的结果是Transformer 可以用更大的数据量、更多的计算资源训练从而在规模上取得突破。这也是后来 GPT 系列、BERT 系列成功的基础。它们的共同逻辑是“大力出奇迹”——Transformer 提供了一个可以高效吃进海量数据的模型结构。对比维度RNNCNNTransformer并行计算不支持支持支持长距离依赖弱弱强顺序信息建模天然支持不天然支持需要位置编码可扩展性差一般强7.4 Transformer 的视觉扩展Transformer 不只在 NLP 领域有效。ViTVision Transformer把图像分割成固定大小的 patch每个 patch 当作序列中的一个“词”直接套用 Transformer 结构做图像分类。Swin Transformer 则在 ViT 的基础上引入了层次化结构和滑动窗口让模型在视觉任务上表现更稳定。这些研究方向本质上都是在复用本文复习的这些前置概念理解了基础后续看到这些变体就不会觉得陌生。8. 前置概念综合实践搭建一个最小 Transformer Block现在把上述概念组合成一个完整的 PyTorch 模块。这不是完整的 Transformer 模型而是包含输入嵌入、位置编码、多头注意力、残差连接、LayerNorm、前馈网络的最小组合。跑通它前置概念就算真正过关了。8.1 完整代码import torch import torch.nn as nn import math class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len5000): super().__init__() 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) return x self.pe[:, :x.size(1)] class MultiHeadAttention(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.d_k d_model // num_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, x): batch_size, seq_len, _ x.size() Q self.W_Q(x).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2) K self.W_K(x).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2) V self.W_V(x).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2) scores torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k) attn_weights torch.softmax(scores, dim-1) context torch.matmul(attn_weights, V) context context.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model) output self.W_O(context) return output class FeedForward(nn.Module): def __init__(self, d_model, d_ff2048): super().__init__() self.linear1 nn.Linear(d_model, d_ff) self.linear2 nn.Linear(d_ff, d_model) self.relu nn.ReLU() def forward(self, x): return self.linear2(self.relu(self.linear1(x))) class TransformerBlock(nn.Module): def __init__(self, d_model, num_heads, d_ff2048, dropout0.1): super().__init__() self.attention MultiHeadAttention(d_model, num_heads) self.norm1 nn.LayerNorm(d_model) self.ffn FeedForward(d_model, d_ff) self.norm2 nn.LayerNorm(d_model) self.dropout nn.Dropout(dropout) def forward(self, x): # 子层 1多头注意力 残差 LayerNorm attn_output self.attention(x) x self.norm1(x self.dropout(attn_output)) # 子层 2前馈网络 残差 LayerNorm ffn_output self.ffn(x) x self.norm2(x self.dropout(ffn_output)) return x class MiniTransformer(nn.Module): def __init__(self, vocab_size, d_model512, num_heads8, num_layers6, d_ff2048, max_len5000): super().__init__() self.embedding nn.Embedding(vocab_size, d_model) self.position_encoding PositionalEncoding(d_model, max_len) self.layers nn.ModuleList([ TransformerBlock(d_model, num_heads, d_ff) for _ in range(num_layers) ]) self.dropout nn.Dropout(0.1) def forward(self, input_ids): x self.embedding(input_ids) x self.position_encoding(x) x self.dropout(x) for layer in self.layers: x layer(x) return x # 测试 if __name__ __main__: model MiniTransformer(vocab_size10000, d_model512, num_heads8, num_layers6) input_ids torch.randint(0, 10000, (2, 20)) output model(input_ids) print(output.shape) # torch.Size([2, 20, 512])8.2 代码关键点说明MultiHeadAttention中的维度变换是这个模块最容易写错的地方。建议对照代码手动推一遍输入x的形状是(batch, seq_len, d_model)。经过W_Q之后仍然是(batch, seq_len, d_model)。view(batch_size, seq_len, num_heads, d_k)把最后一个维度拆成两个维度。transpose(1, 2)把头的维度换到第 1 维得到(batch, num_heads, seq_len, d_k)。这样做的好处是后续的torch.matmul可以一次对 batch 里所有样本、所有头独立计算注意力。如果你之前在这里绕晕过一定要自己在纸上画一遍维度变化。运行这段代码最终输出形状是(2, 20, 512)。序列长度 20 保留住了每个 token 经过 6 层 Transformer Block 后依然映射到 512 维向量这说明模型没有在维度上出错。接下来就可以在这个输出之上接分类头、对比学习头或者做序列标注。9. 常见误区与排查方法在自己动手实践 Transformer 前置概念时有几个高频问题几乎每个人都会遇到。下表总结了最常见的现象、可能原因和解决方案。问题现象可能原因排查方式解决方案训练 loss 不下降学习率过大或过小打印日志看 loss 变化趋势可以先用 1e-4 级别的学习率再用学习率预热模型输出 NaN注意力分数数值溢出检查点积是否除以了 sqrt(d_k)确认缩放参数正确并检查数据中是否有异常值训练速度极慢序列长度过长注意力复杂度 O(n^2)统计序列长度分布限制最大长度或换用稀疏注意力变体位置编码没生效忘记把位置编码和词嵌入相加检查 forward 中是否使用位置编码模块确认x pe操作执行多头注意力维度错误view 和 transpose 顺序写反打印中间张量形状参考 8.2 节逐步核对维度解码时看到未来信息没有添加掩码检查注意力 forward 中 mask 参数构造上三角矩阵掩码并传入模型表达能力不够d_model 或 num_heads 设置太小观察训练集 loss 是否也很高适当增大 d_model但注意算力开销9.1 重点补充排查顺序如果模型训练不收敛第一步不是调参数而是用一个小数据集尝试过拟合。具体做法是只取几十条样本把模型在训练集上反复跑几十轮。如果损失降到很低说明模型具有正常的学习能力问题出在数据或训练策略上如果已经过拟合不了说明代码实现本身有问题需要检查前向传播和反向传播的链路。9.2 自测题目清单可以试着回答以下问题如果都能答出来说明前置概念基本掌握为什么 Transformer 不直接拼接位置向量而是相加缩放点积注意力里为什么要除以sqrt(d_k)LayerNorm 和 BatchNorm 的本质区别是什么多头注意力的“多”体现在哪个维度残差连接解决的核心问题是什么为什么 RNN 不能并行而 Transformer 可以10. 从概念复习到完整 Transformer前置概念复习到这里就完整了。但需要说明的是本文还没有涉及完整的 Transformer 编码器-解码器架构比如交叉注意力Cross-Attention、编码器和解码器之间的交互、训练时的 Teacher Forcing、推理时的自回归生成流程。这些内容建立在前置概念之上是下一步要攻克的重点。10.1 推荐的下一步路径如果你学到这里觉得概念已经通了建议按以下顺序继续深入阅读原版论文《Attention Is All You Need》。看架构图对应本文讲的每个模块逐段对照理解。阅读 Hugging Face Transformers 库中BertModel或GPT2Model的源码。找开源实现对照论文看哪些地方做了简化或改动。找一个具体任务动手做。比如用 PyTorch 实现一个简单的文本分类模型把本文的 MiniTransformer 接上分类头跑通一个完整流程。实践项目还有很多方向可以探索比如时间序列预测、图像分类等场景。10.2 学习资源建议动手实践时优先选择那些自带数据集的教程。手写一遍多头注意力和位置编码比刷十篇源码解析有用得多。如果看源码容易卡住建议先把 PyTorch 的维度操作熟练起来——Transformer 的源码本质上就是各种 view、transpose、permute 的组合。最后强调一点很多同学一上来就盯着“Transformer 改进”“涨点”这些话题这其实偏离了方向。把本文的基础概念扎实掌握之后再看所谓的“改进模型”你就能一眼看出它到底改在哪一环——是改了位置编码还是改了注意力计算方式还是改了归一化层。那时候学习 Transformer 才真正进入了快车道。建议先把本文收藏在写代码遇到维度搞不清的时候再回来看看。
返回列表