ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Transformer前置概念复习:词嵌入、注意力与位置编码核心解析

Transformer前置概念复习:词嵌入、注意力与位置编码核心解析 学习 Transformer 时很多人直接把注意力集中在多头注意力、编码器解码器结构这些核心模块上结果读源码时被形状推导、归一化、残差连接和位置编码反复打断。Transformer 的前置概念不是可跳过的基础而是真正理解架构的钥匙。下面围绕词嵌入、RNN/LSTM、注意力机制、自注意力、位置编码、层归一化和残差连接这些前置概念展开复习帮助读者在动手写 Transformer 代码之前把必要的知识缺口补上。如果只停留在“看懂了结构图”的程度很容易在实现时卡住。网络上的 Transformer 详解、手撕 Transformer、架构学习笔记非常多但大多数默认读者已经知道什么是 token embedding、为什么需要位置编码、LayerNorm 和 BatchNorm 有什么区别。这些前置知识没有补齐后面读源码就变成查单词而不是理解设计。本文按一条主线复习从序列建模问题出发逐步走到多头注意力每一块都给出可运行的最小示例并指出常见理解误区。1. 先从问题出发Transformer 到底在解决什么问题1.1 序列建模任务通常长什么样在自然语言处理、语音识别、时间序列预测等领域输入数据都是一个序列。一句中文、一段语音信号、一组股票价格都可以看成按时间顺序排列的元素。序列建模要完成的核心任务是从这样的序列中学习规律并输出结果。序列的每个元素之间并不是独立的。句子里的“银行”是“河岸”还是“金融机构”取决于前后词。股票今天的价格受过去一段时间走势影响。因此模型必须有能力把序列中的上下文信息融合到一起。从数据形态上看序列建模任务通常分为几类序列到类别例如情感分类、文本分类。序列到序列例如机器翻译、语音识别。序列到固定长度向量例如句子 embedding。多序列交互例如问答、对话。无论哪种任务都绕不开一个核心问题如何让模型看到“整个序列的上下文”而不是只看当前一个词。1.2 传统模型为什么处理不好长序列在 Transformer 出现之前循环神经网络RNN及其变体是最常用的序列模型。RNN 的核心思路是逐步读取输入并把之前的信息保存在隐藏状态中。这种方式在短序列上表现不错但存在三个明显问题难以并行。每个时间步的隐藏状态依赖前一个时间步GPU 的优势无法发挥。长距离依赖容易丢失。信息经过多步传递后会出现梯度消失或梯度爆炸。顺序建模的假设并不一定符合所有任务。某些任务的输出依赖“全局信息”而不是严格从左到右的因果过程。Transformer 的贡献在于它把“位置”和“内容”分开处理用自注意力机制在一次计算中捕获所有位置之间的关系。这样既解决了并行问题也缓解了长距离依赖问题。正因为 Transformer 的设计完全不同于 RNN学习它的前置概念时不能只背结构图而要理解它每解决一个问题对应的旧方案是什么、旧方案为什么不够好。1.3 前置概念到底是哪几块把 Transformer 拆开看核心组件包括 Embedding、多头自注意力、前馈网络、位置编码、残差连接和层归一化。每一项都有独立的前置知识Embedding 对应如何把离散符号表示成连续向量。注意力机制对应如何让模型筛选输入信息。自注意力对应如何让序列内元素互相交换信息。位置编码对应如何在无序集合中保留位置信息。LayerNorm 和残差连接对应如何让深层网络稳定训练。前馈网络对应如何在注意力之外增加非线性变换。如果这些概念中某一块有缺口阅读 Transformer 源码时就会卡住。下文按照这个顺序逐块复习并在每块给出最小示例和易错点。2. 从符号到向量词嵌入是模型理解语言的第一步2.1 为什么不能直接把单词当作数字一个最朴素的想法是给每个词一个整数编号比如“苹果”1“香蕉”2。但整数编号有两个问题1 和 2 之间存在大小关系模型会误认为“香蕉”大于“苹果”。任意两个词的编号没有语义关系“猫”和“狗”的距离不反映它们同为宠物的关系。因此需要把每个词映射到一个固定维度的稠密向量。这个向量称为词嵌入Word Embedding它是模型对语言语义进行表示的基础。在 Transformer 中输入序列首先经过 embedding 层每个词由一个可学习的向量替代这个向量的维度通常是 512 或 768。实际项目里也有用 128、256 的取决于模型大小和任务复杂度。2.2 词嵌入的直觉语义相似性决定向量距离训练好的词嵌入空间中语义相近的词向量距离更近。例如“猫”和“狗”的向量距离要小于“猫”和“汽车”的距离。更经典的例子是类比关系king - man woman ≈ queen。这说明向量从数据中学到了部分语义规律。词嵌入有两种来源随机初始化跟着主任务一起训练。使用预训练向量如 Word2Vec、GloVe或直接用 BERT 等模型的 Token Embedding。在 Transformer 结构里通常采用随机初始化 任务训练的方式因为 Transformer 更大端到端训练的效果通常也能学好表示。使用预训练向量的场景常见于小数据集或者希望加速收敛的项目。2.3 代码演示用 PyTorch 创建词嵌入层下面用 PyTorch 创建一个词嵌入层说明维度变化的规律。import torch import torch.nn as nn # vocab_size: 词表大小 # d_model: 嵌入维度Transformer 中常见取 512 embedding nn.Embedding(num_embeddings10000, embedding_dim512) # 输入形状: (batch_size, seq_len) # 这里假设 batch_size2, seq_len5 input_ids torch.tensor([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]], dtypetorch.long) output embedding(input_ids) print(output.shape) # 期望输出: torch.Size([2, 5, 512])这段代码中input_ids是经过 tokenization 和词表映射后的整数序列。embedding 层接收整数索引输出对应的稠密向量。最终形状中batch_size和seq_len都保留最后一个维度是嵌入维度。这里要注意一个常见坑nn.Embedding的输入必须是整数类型传入 float 会直接报错。另一个坑是num_embeddings必须大于数据中出现的最大索引否则运行时会越界报错。3. 循环神经网络的成就与天花板RNN、LSTM、GRU3.1 RNN 如何建模序列循环神经网络的核心思想是把隐藏状态当作一种“记忆”。每读取一个新输入就把当前输入和上一个隐藏状态压缩成新的隐藏状态。在时间步 tRNN 的计算可以写成h_t tanh(W_ih * x_t W_hh * h_{t-1} b)其中 x_t 是当前输入h_{t-1} 是上一个时间步的隐藏状态W_ih 和 W_hh 是权重矩阵。由于权重在所有时间步共享RNN 的参数数量与序列长度无关。RNN 的问题在于信息在传递过程中会不断被非线性变换“压缩”。如果序列很长较早的信息会逐渐被新输入覆盖模型实际上记不住最开始的内容。3.2 LSTM 通过门控缓解梯度问题长短时记忆网络LSTM引入了门控机制用遗忘门、输入门和输出门控制信息的写入和丢弃。它的记忆单元 C_t 可以长期保存信息不被每次输入完全覆盖。LSTM 的核心公式通常写成遗忘门f_t sigmoid(W_f * [h_{t-1}, x_t] b_f)输入门i_t sigmoid(W_i * [h_{t-1}, x_t] b_i)候选记忆C_tild_t tanh(W_C * [h_{t-1}, x_t] b_C)记忆更新C_t f_t * C_{t-1} i_t * C_tild_t输出门o_t sigmoid(W_o * [h_{t-1}, x_t] b_o)输出h_t o_t * tanh(C_t)相比普通 RNNLSTM 多了一条记忆单元的“高速公路”梯度可以沿着 C_t f_t * C_{t-1} ... 这条路径反向传播。遗忘门接近 1 时信息更容易跨时间步保留。GRU 则进一步简化了门控结构把遗忘门和输入门合并为更新门。3.3 为什么仍不能满足长序列需求即使有了门控机制LSTM 和 GRU 仍然有三个限制必须按时间步顺序计算无法并行。训练效率受序列长度限制非常长的序列仍然可能损失早期信息。结构复杂度高调参成本大。Transformer 没有完全抛弃 RNN 的思想而是用注意力机制替代了逐步传递信息的方案。注意力机制可以直接建立任意两个位置之间的关联不需要经过中间步骤“传话”。这一步看起来不难但它改变了序列建模的底层逻辑。理解 RNN 和 Transformer 的关系是学习前置概念时最容易出现误区的地方Transformer 不是 RNN 的简单改进而是把“顺序递推”替换成了“全局关联 位置编码”。4. 注意力机制从全局对齐到信息筛选4.1 机器翻译中的对齐问题注意力机制最早在机器翻译任务中被广泛使用。翻译句子时每个目标语言单词并不是只由源语言中的某一个单词决定而是与源语言中的多个单词相关只是相关程度不同。早期的 Seq2Seq 模型只把编码器的最终隐藏状态作为上下文向量。句子很长时最终状态很难包含完整信息。注意力机制的做法是解码到每个时间步时根据当前状态对编码器的所有隐藏状态计算权重再把所有状态加权求和得到上下文。这样模型每一步都能“回看”源语言的所有位置。4.2 注意力计算的三要素Query、Key、Value现代注意力机制通常用 Query、Key、Value 来描述。这个表述最初来自检索系统但很容易理解Query 是你关心的问题或当前需求。Key 是候选信息的索引。Value 是候选信息本身。计算步骤可以拆成四步对每个候选位置计算 Query 和 Key 的相似度得分。归一化得分常用 softmax得到概率分布。把每个候选位置的 Value 乘上对应概率。将所有加权后的 Value 求和得到注意力输出。相似度得分有多种计算方式Transformer 中使用的是缩放点积注意力Attention(Q, K, V) softmax(Q * K^T / sqrt(d_k)) * V除以 sqrt(d_k) 是为了防止点积过大导致 softmax 进入饱和区。d_k 是每个注意力头的 Key 的维度实际计算中通常等于 d_model / num_heads。4.3 一个最小注意力实现下面用 PyTorch 实现一个最简版注意力计算便于理解形状变化。import torch import torch.nn.functional as F def attention(query, key, value): # query: (batch, seq_len_q, d_k) # key: (batch, seq_len_k, d_k) # value: (batch, seq_len_k, d_v) d_k query.size(-1) scores torch.matmul(query, key.transpose(-2, -1)) / (d_k ** 0.5) # scores: (batch, seq_len_q, seq_len_k) weights F.softmax(scores, dim-1) out torch.matmul(weights, value) return out, weights # 模拟一个小例子 batch_size, seq_len, d_k 2, 4, 8 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) out, weights attention(q, k, v) print(out.shape) # 期望输出: torch.Size([2, 4, 8]) print(weights.shape) # 期望输出: torch.Size([2, 4, 4])这里的weights形状是(batch, seq_len_q, seq_len_k)表示每个查询位置对每个键位置的注意力权重。实际代码中建议加上 dropout 和 mask这里为了展示核心逻辑省略了这些细节。5. 自注意力Transformer 的革命性选择5.1 自注意力与注意力机制的区别普通注意力机制中Query 来自解码器Key 和 Value 来自编码器。这是一种“跨序列”的信息交互。而自注意力Self-Attention中Query、Key、Value 全部来自同一个序列自身。自注意力的作用是让序列中的每个元素都能看到其他所有元素并根据与它们的关系更新自己的表示。比如句子“小明喜欢他的新手机”“他”指代谁需要结合整个句子的信息才能判断。在 Transformer 中每个 token 的输入向量会被三个不同的线性变换分别映射成 Query、Key、Value。这个映射过程引入了可学习的参数让模型学习“什么问题是重要问题”、“什么信息是关键信息”。5.2 自注意力为何能并行计算自注意力的计算可以用矩阵乘法一次完成。输入矩阵 X 形状为 (seq_len, d_model)经过三个权重矩阵得到 Q、K、V然后执行 Q * K^T / sqrt(d_k) 和 softmax最后乘 V。整个过程没有时间步之间的循环依赖GPU 可以高效并行。这带来一个对比优势指标RNN/LSTM自注意力时间复杂度O(n) 步递推O(1) 步矩阵运算但注意力权重是 O(n^2)长距离依赖需要多步传递一步直接建立关联并行性低高顺序信息天然隐含需要额外位置编码这里的 O(n^2) 是注意力权重矩阵的空间和时间开销n 是序列长度。这也是为什么长序列 Transformer 需要稀疏注意力、局部注意力等变体。5.3 多头注意力的意义简单自注意力只能学习一种“视角”的关联。多头注意力把 Query、Key、Value 分成多组每组用不同的线性映射然后分别计算注意力最后拼接起来。每个头可以关注不同关系。例如一个头关注语法关系另一个头关注指代关系另一个头关注语义相似性。这在实现上很直接# 假设 d_model512, num_heads8 # 每个头的维度 d_k d_model // num_heads 64 # 把 Q、K、V 分成 8 组分头计算最后拼接一个常见实现方式是使用torch.nn.MultiheadAttention也可以自己实现。自己实现时要注意必须在拼接后再经过输出投影层否则多头的拼接结果不是最终输出。还要留意一个坑d_model必须能被num_heads整除。常见设置是 512/8、768/12、1024/16。6. 位置编码没有顺序感的模型如何理解词序6.1 自注意力本身对顺序不敏感自注意力计算时如果把序列中的 token 顺序打乱Query 和 Key 的点积结果不会改变。因为点积只关心向量内容不关心它们的位置。但是在语言中“我打你”和“你打我”含义完全不同。Transformer 的解决办法是向输入向量中加入位置编码Positional Encoding让模型知道每个 token 在序列中的位置。位置编码可以理解为给每个位置分配一个向量叠加到词嵌入向量上从而让相同 token 在不同位置产生不同表示。位置编码需要满足几个基本要求不同位置要有不同的编码。编码值要在一个合理范围内不能淹没词嵌入信息。模型最好能泛化到训练时没见过的序列长度。6.2 正弦余弦位置编码的原理原始 Transformer 论文使用正弦余弦函数生成位置编码PE(pos, 2i) sin(pos / 10000^(2i / d_model)) PE(pos, 2i1) cos(pos / 10000^(2i / d_model))其中 pos 是位置下标i 是维度下标d_model 是模型维度。这个公式的本质是不同维度使用不同频率的正弦波位置在编码空间中占据不同相位。优点在于不需要训练参数。可以外推长度理论上任意位置都能计算。由于正弦函数性质模型可以通过相对位置关系进行推断。6.3 位置编码的实现与验证下面用 NumPy 生成一个位置编码矩阵并验证形状。import numpy as np def positional_encoding(max_len, d_model): pe np.zeros((max_len, d_model)) position np.arange(max_len).reshape(-1, 1) div_term np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model)) pe[:, 0::2] np.sin(position * div_term) pe[:, 1::2] np.cos(position * div_term) return pe pe positional_encoding(50, 64) print(pe.shape) # 期望输出: (50, 64)这段代码用对数间隔的方式构造div_term与原始公式中的10000^(2i/d_model)等价但数值上更稳定。这里有一个易错点有些实现会在偶数维度用 sin奇数维度用 cos但必须保证position * div_term的维度与切片维度对齐。如果d_model是奇数0::2和1::2长度不同需要特殊处理。Transformer 中d_model通常为偶数所以不会触发这个问题。在实际代码中位置编码通常与 token embedding 相加而不是拼接。相加意味着模型需要把语义信息和位置信息融合到同一向量空间。这一点经常被误解需要留意。7. 让网络训练更深残差连接、层归一化与前馈网络7.1 残差连接解决什么深层神经网络在训练中容易出现退化问题网络越深训练误差反而可能越大。残差连接通过在子层输出上加上输入让网络至少保留恒等映射的能力。Transformer 的每个子层结构都是output LayerNorm(x Sublayer(x))Sublayer 可以是多头注意力或前馈网络。这个结构在 Transformer 论文中称为 Post-LN因为 LayerNorm 放在残差相加之后。残差连接另一个作用是缓解梯度消失。反向传播时梯度可以沿着恒等分支直接传到前面的层路径更短、更稳定。7.2 层归一化与批归一化的区别归一化的目的是把每层激活值拉到稳定范围内减少训练过程中分布变化的影响。Transformer 使用层归一化LayerNorm而不是批归一化BatchNorm。两者的核心区别在于统计维度归一化方法统计范围适用场景BatchNorm同一个通道内的 batch 数据适合 CNN依赖 batch 大小LayerNorm每条样本内部的所有特征适合 NLP 和 Transformer与 batch 无关LayerNorm 对每条样本独立计算均值和方差因此 batch size 变化、batch 大小为 1、甚至推理阶段都能稳定工作。这在序列模型中非常关键因为每个 batch 内的 token 长度可能不同。7.3 Feed-Forward Network 的作用多头注意力之后Transformer 还包含一个前馈网络FFN通常由两个线性变换和一个激活函数组成FFN(x) max(0, xW1 b1)W2 b2激活函数在原始论文中是 ReLU后来很多模型使用 GELU。FFN 的作用是在注意力输出的基础上增加非线性变换增强模型的表达能力。注意力机制本质上是对输入信息的“加权组合”属于线性操作。如果只堆叠多头注意力模型的表达能力会有限。FFN 的加入让每个 token 的表示在固定的参数下进行非线性映射类似在每层加入独立计算。在实际代码中FFN 内部维度通常比 d_model 大不少。Transformer base 模型里d_model512FFN 内部维度是 2048也就是放大约 4 倍。这个参数会影响模型参数量和显存占用。8. 常见概念混淆与排查路径8.1 最容易混淆的概念学习前置概念时有几个地方经常被混淆概念容易混淆点正确理解注意力与自注意力注意力是跨序列交互自注意力是序列内部交互自注意力是注意力机制的一种特例token embedding 与位置编码拼接还是相加原始 Transformer 使用相加Post-LN 与 Pre-LNLayerNorm 放在残差内还是残差外原始论文是 Post-LN实践中有 Pre-LN 变体LayerNorm 与 BatchNorm归一化维度不同LayerNorm 按样本内部归一化多头注意力并行头与分头结果不拼接多头后要拼接并投影8.2 学习 Transformer 时常见的理解误区第一个误区是认为位置编码可以解决所有顺序问题。位置编码只提供位置信息模型能否真正利用它取决于训练任务和数据量。在实际部署中如果训练序列长度远小于推理序列长度位置编码的外推能力仍然可能不足。第二个误区是以为多头注意力一定会提升效果。多头数量的增加会带来参数和计算开销如果任务简单、数据量少过多的头反而容易过拟合。实际调参时可以先用默认配置再根据验证集表现调整。第三个误区是把代码跑通等于理解原理。Transformer 源码中很多细节是工程优化比如 mask 的处理、缓存机制、推理时的 KV Cache。如果只读代码容易把注意力机制的数学本质和工程实现混在一起。8.3 推荐的自查清单与学习路径在学习完前置概念之后可以用下面的清单检查自己是否掌握能否解释为什么 Embedding 输入必须是整数索引。能否比较 RNN 和自注意力的时间复杂度和并行性。能否写出 Q、K、V 的形状并说明 d_model、num_heads、d_k 的关系。能否解释为什么位置编码使用正余弦函数而非随机初始化。能否说明 LayerNorm 与 BatchNorm 的区别。能否在 NumPy 或 PyTorch 中实现一个最小自注意力模块。能否解释残差连接对深层网络训练的作用。如果某个问题回答不上来建议回到对应小节重新复习再进入 Transformer 源码阅读。再给出一个学习路径上的建议顺序先用情感分类或文本分类这类小任务熟悉 Embedding。手工实现一个简单的自注意力层调试形状。阅读 PyTorch 的MultiheadAttention源码对照公式。使用一个预训练 Transformer 做 fine-tune 或推理观察输出。再学习 BERT、GPT、Swin Transformer 等具体架构。这样从头到尾从基础到应用前置概念就不会成为学习 Transformer 的拦路虎。不过到这里也补充说明一下Transformer 的变体非常多例如用于视觉任务的 Vision Transformer、Swin Transformer用于时间序列预测的 TCN Transformer 组合方案以及各类涨点技巧。这些发展方向虽然各异但底层都需要先理解上面讲解的这套前置概念。基础扎实后接触新变体时只需要关注它在哪个环节做了改变。在实际项目落地时前置概念复习同样重要。在线推理服务的文本长度波动可能很大需要理解位置编码的外推限制和注意力矩阵的显存开销。调优时如果只提高 num_heads 而不观察显存与延迟很可能让接口响应明显变慢。因此学习阶段跑通代码生产阶段关注指标是两个不同层次的问题。
返回列表