资讯动态

Transformer前置概念详解:从RNN到自注意力与位置编码

发布时间:2026/8/30 5:36:14 来源:尧图企业网站定制
Transformer 的论文你多半翻过但真正卡住的地方往往不是框架图而是公式背后那些前置概念。比如你记住了 Attention 的公式是softmax(QK^T / sqrt(d_k))V但 Q、K、V 到底从哪来为什么除以sqrt(d_k)为什么还需要位置编码这些如果不补后面去看 transformer 源码、手撕 transformer或者去追 vision transformer、swin transformer 的改进都会卡在同一批问题上。这篇文章就把 Transformer 的前置概念按顺序完整复习一遍序列建模与 RNN、词嵌入、注意力机制、自注意力与 QKV、位置编码、残差连接、LayerNorm、前馈网络以及训练阶段的 Softmax、交叉熵、Adam 和 warmup。最后用一套完整的前向流程把 Transformer 架构及其工作原理串起来。与其说这是一篇教程不如说是一份自查清单。适合论文能看懂大意的初学者、准备面试的算法岗候选人以及被源码里的张量形状劝退的读者。文中所有代码示例都不依赖 GPU普通 Python 环境就能跑建议看到对应章节直接复制运行效果比只看笔记好得多。1. 前置概念复习目标速览先把整份复习内容拆成一张表后续章节按这个顺序展开章节核心概念在 Transformer 中的作用复习重点序列建模与 RNNRNN / LSTM / CNN理解 Transformer 要替代的基线循环过程、长距离依赖、并行瓶颈词嵌入分布式表示 / BPE文本如何变成可计算的向量token - embedding - (batch, seq_len, d_model)注意力机制相关性加权求和Transformer 的基本操作单元query / key / value 的含义自注意力与 QKV缩放点积注意力 / 多头Encoder 和 Decoder 的核心模块线性投影产生 QKV、注意力权重位置编码正弦位置编码 / 可学习位置编码给无顺序感知的自注意力补上顺序信息公式、相对位置特性、外推能力工程组件残差连接 / LayerNorm / FFN保证深层网络训练稳定编码层的前向流程训练技巧Softmax / 交叉熵 / Adam / Warmup让 Transformer 真正训练起来损失函数、优化器、学习率策略这张表对应了很多人在学习 transformer 架构时最容易漏掉的部分。下面逐项展开。2. 序列建模与 RNNTransformer 想解决什么2.1 序列建模的目标自然语言、语音、时序信号都属于变长序列。序列建模要解决的核心问题是给定一串输入x1, x2, ..., xt模型需要学习它们之间的依赖关系。举一个最简单的例子句子“我昨天看到一只猫它非常可爱”里的“它”指代的是“猫”模型要正确生成或理解这个指代关系就必须记住前文的信息并且知道“它”与“猫”之间有关系。Transformer 出现之前处理这类问题的主力是 RNN 以及在它基础上改进的 LSTM、GRU。要理解 Transformer 的价值必须先理解 RNN 的处理方式和瓶颈在哪。2.2 RNN 的核心公式RNN 的基本思想是引入一个隐状态h_t把上一步的信息带到现在。前向计算可以写成h_t tanh(x_t W_ih h_{t-1} W_hh b_h)其中x_t是当前时刻的输入h_{t-1}是上一时刻的隐状态W_ih和W_hh是两个可学习权重矩阵tanh是激活函数。用 Python 写一个最简版本import numpy as np np.random.seed(0) # 当前输入和上一步隐状态 x_t np.array([0.2, 0.4, 0.6]) # 假设输入维度是 3 h_prev np.zeros(4) # 假设隐状态维度是 4 # 随机初始化权重 W_ih np.random.randn(3, 4) W_hh np.random.randn(4, 4) b_h np.zeros(4) h_t np.tanh(x_t W_ih h_prev W_hh b_h) print(h_t)这套机制的本质是把序列信息压缩进隐状态然后用h_t作为当前时刻的“记忆”。如果输入很长那么靠前时刻的信息要经过很多次非线性变换才能到达后面很容易衰减或爆炸。2.3 RNN 的瓶颈长距离依赖与串行计算RNN 有两个非常头疼的问题。第一个是梯度消失和梯度爆炸。由于每个时间步都要乘以相同或相似的权重矩阵误差信号在反向传播时会出现指数级衰减或放大。LSTM 和 GRU 用门控机制缓解了这个问题但并没有完全解决尤其是超过一定长度后长距离依赖仍然难以稳定建模。第二个问题是必须串行计算。h_t依赖h_{t-1}所以没办法像计算两个矩阵乘法那样一次把整段序列同时算完。对 GPU 来说串行意味着利用率很低训练速度慢。CNN 可以并行计算但感受野有限要覆盖长距离依赖需要堆很多层或者用膨胀卷积。这引出了一个看起来很自然的问题能不能设计一种结构既能并行又能直接建模全序列依赖2.4 为什么最后是 TransformerTransformer 的答案是自注意力。让每个 token 直接和序列里所有 token 计算相关性再把信息聚合起来。这样任意两个位置之间只需要一次交互不依赖中间传递梯度路径变短长距离依赖问题也得到缓解。同时自注意力计算本质上是矩阵乘法整个序列可以一次算完GPU 并行效率很高。后面所有关于 transformer 模型详解、手撕 transformer、以及 vision transformer、swin transformer 等改进都建立在这个基础上。理解这一点就理解了为什么最后是 Transformer 成为主流结构。3. 词嵌入文本怎么变成向量3.1 从 One-Hot 到分布式表示模型不能直接处理文字第一步要把文本转成数字。最朴素的做法是 One-Hot假设词表大小是V每个词用一个长度V的向量表示对应位置为 1其余为 0。One-Hot 有两个明显问题维度巨大训练和存储成本高任意两个词向量都是正交的语义相近的词之间没有距离关系。比如“猫”和“猫咪”在向量空间里的关系和“猫”与“汽车”的关系没有任何区别。更好的方案是分布式表示把每个词映射到一个低维稠密向量比如 128 维、512 维让语义相近的词在向量空间中距离更近。3.2 Word2Vec 与共现统计Word2Vec 是学习分布式表示的经典方法核心思想来自分布假说一个词的含义由它经常出现的上下文决定。Word2Vec 有两个主要变体CBOW用上下文预测当前词。Skip-gram用当前词预测上下文。训练完成后嵌入矩阵中每一行就是一个词的向量。后面出现的 GloVe 利用全局共现统计信息FastText 引入子词信息本质上都是为了让词向量表达更丰富。3.3 Tokenization 与 BPE现代 Transformer 模型很少直接对整词做嵌入而是先做子词切分。常用的算法是 BPEByte Pair Encoding。它把词表控制在一个合理范围内同时能处理未登录词。例如“transformer”可能被切成[transform, er]这样即使遇到没有见过的词也能用已有子词拼出来。GPT 系列用的 BPEBERT 用的 WordPiece思路类似。3.4 词嵌入在 Transformer 里的形状在 Transformer 里输入先转成 token id再查嵌入表得到形状为(batch_size, seq_len, d_model)的向量。其中d_model是模型隐藏维度BERT base 是 768也有的模型用 512 或 1024。在 PyTorch 里一个最简单的嵌入层长这样import torch from torch.nn import Embedding # 假设词表大小 10000嵌入维度 512 embed Embedding(num_embeddings10000, embedding_dim512) # token id 输入形状: (batch_size, seq_len) tokens torch.tensor([[2, 15, 289, 44]]) embeddings embed(tokens) print(embeddings.shape) # torch.Size([1, 4, 512])看到这个形状变化后面的很多结构就都围绕同一个问题展开如何在这个三维张量上做各种变换同时保持每个 token 对应的位置不动。4. 注意力机制先理解“加权求和”4.1 为什么要注意力注意力机制最早在 seq2seq 机器翻译任务中被引入。当时的问题是Encoder 把整个输入句子压缩成一个固定向量Decoder 在生成每个词时都只能从这个固定向量里取信息句子越长信息丢失越严重。注意力机制改变了这种做法Decoder 在生成每个词时都回去看一遍 Encoder 的每个输出并且根据当前生成位置动态决定应该重点看哪些词。这个动态决定的过程就是“注意力”。4.2 注意力的通用公式注意力可以非常简洁地概括为三步有一个 query 和一个 key 集合。计算 query 与每个 key 的相关性得到权重。用权重对 value 做加权求和得到输出。写成公式就是Attention(query, key, value) sum_i(weight_i * value_i)其中weight_i softmax(score(query, key_i))这里的本质不是“注意什么神奇的东西”而是对一组向量做加权平均权重来自 query 与 key 的匹配程度。4.3 加性注意力 vs 点积注意力历史上常用的打分函数有两种加性注意力常见于 Bahdanau 等人的工作用一层全连接网络计算打分。点积注意力直接计算 query 与 key 的点积更简单矩阵运算更高效。Transformer 使用的是缩放点积注意力。因为点积结果会随维度增大而变大如果不做缩放Softmax 的梯度会变得很小所以除以sqrt(d_k)做归一化。Attention(Q, K, V) softmax(QK^T / sqrt(d_k)) V4.4 一次最小规模的手算验证为了看清这个过程构造一个极小的例子。假设只有 2 个 tokend_k 2import numpy as np # 两个 token 的 query 和 key Q np.array([[0.5, 0.1], [0.2, 0.8]]) K np.array([[0.3, 0.7], [0.6, 0.4]]) V np.array([[1.0, 0.0], [0.0, 1.0]]) d_k Q.shape[1] scores Q K.T / np.sqrt(d_k) weights np.exp(scores - scores.max(axis-1, keepdimsTrue)) weights weights / weights.sum(axis-1, keepdimsTrue) output weights V print(scores:, scores, sep\n) print(weights:, weights, sep\n) print(output:, output, sep\n)运行之后会看到多行输出scores 是两两 token 的相关性分数weights 是归一化后的概率output 是对 V 的加权求和结果。这个流程理解清楚后自注意力就只是把它扩展到大矩阵上。5. 自注意力与 QKV手撕 Transformer 的重要前置5.1 从注意力到自注意力普通注意力里query 来自 Decoderkey 和 value 来自 Encoder。自注意力则不同query、key、value 都来自同一个输入序列。这意味着每个 token 都去和序列里的所有 token 计算相关性包括它自己。自注意力让每个位置能直接捕获全局信息并且不依赖循环结构。5.2 Q、K、V 从哪来很多初学者卡在这里注意力公式里写着 Q、K、V但原始输入不是 QKV它们是怎么产生的答案非常简单用三个可学习的线性变换矩阵对输入做投影。Q X W_Q K X W_K V X W_V其中X的形状是(batch_size, seq_len, d_model)W_Q、W_K、W_V的形状是(d_model, d_k)或(d_model, d_v)。经过投影后Q 和 K 的每一行仍然对应一个 token但被映射到适合做相关性比较的空间里。这也是为什么代码里经常能看到类似nn.Linear(d_model, d_k)的层。5.3 缩放点积注意力公式自注意力的计算过程可以写成QK^T - scores scores / sqrt(d_k) - scaled_scores softmax(scaled_scores) - attention_weights attention_weights V - output矩阵形状变化如下Q:(batch, seq_len, d_k)K:(batch, seq_len, d_k)V:(batch, seq_len, d_v)Q K^T:(batch, seq_len, seq_len)最后attention_weights V:(batch, seq_len, d_v)除以sqrt(d_k)的原因是当d_k较大时点积结果方差会变大Softmax 会变得很尖锐导致梯度极小。缩放之后分布更平滑。5.4 多头注意力多头注意力就是把上面这个过程做多次而不是只做一次。做法是把d_model维度的 Q、K、V 拆成多组每组独立计算注意力最后拼回去再经过一个线性层投影。通俗地说单头注意力只能捕获一种相关性模式多头注意力可以让不同头分别关注不同的关系比如一个头关注指代关系另一个头关注语法结构。这也是 transformer 性能更强的重要原因。5.5 NumPy 实现一个最小自注意力下面给出一个不依赖深度学习框架的最小自注意力实现重点在于看清形状变化import numpy as np def scaled_dot_product_attention(Q, K, V): d_k Q.shape[-1] scores Q K.transpose(-2, -1) / np.sqrt(d_k) weights np.exp(scores - scores.max(axis-1, keepdimsTrue)) weights weights / weights.sum(axis-1, keepdimsTrue) output weights V return output, weights # 模拟一个 batch 为 1、序列长度 3、隐藏维度 8 的输入 np.random.seed(42) X np.random.randn(1, 3, 8) d_model 8 d_k 8 W_Q np.random.randn(d_model, d_k) W_K np.random.randn(d_model, d_k) W_V np.random.randn(d_model, d_k) Q X W_Q K X W_K V X W_V print(Q:, Q.shape) # (1, 3, 8) print(K:, K.shape) # (1, 3, 8) print(V:, V.shape) # (1, 3, 8) output, weights scaled_dot_product_attention(Q, K, V) print(output:, output.shape) # (1, 3, 8) print(weights:, weights.shape) # (1, 3, 3)手撕 Transformer 时这个函数就是最核心的模块之一。理解了它后面看编码层、解码层、BERT、GPT、ViT都会顺很多。6. 位置编码让模型知道顺序6.1 自注意力对顺序不敏感的问题自注意力虽然强大但它有一个天然缺陷完全不关心 token 的顺序。如果把“猫追老鼠”和“老鼠追猫”分别转成 token再输入自注意力模型在没有额外信息的情况下会得到完全一样的表示。因为在注意力计算里序列是被当成一个集合来处理的。语言中词序非常重要所以必须给模型注入位置信息这就是位置编码存在的意义。6.2 正弦位置编码公式原始 Transformer 使用正弦位置编码公式如下PE(pos, 2i) sin(pos / 10000^(2i / d_model)) PE(pos, 2i1) cos(pos / 10000^(2i / d_model))这里的pos是 token 在序列中的位置i是嵌入维度的下标。也就是说位置编码向量的偶数和奇数维度分别使用正弦和余弦函数。用代码生成一组位置编码import numpy as np def sinusoidal_positional_encoding(max_len, d_model): pe np.zeros((max_len, d_model)) for pos in range(max_len): for i in range(0, d_model, 2): angle pos / np.power(10000, 2 * i / d_model) pe[pos, i] np.sin(angle) if i 1 d_model: pe[pos, i 1] np.cos(angle) return pe pe sinusoidal_positional_encoding(10, 512) print(pe.shape) # (10, 512)在实际 Transformer 里位置编码会加到词嵌入上X_final X_embedding position_encoding这样每个 token 的向量里既包含语义信息又包含位置信息。6.3 为什么用正弦和余弦选择正弦和余弦而不是直接给每个位置一个随机向量主要是两个原因。第一相对位置可以方便地表示出来。对于固定的偏移量kPE(pos k)可以用PE(pos)的线性变换近似表达这对模型学习相邻位置关系有帮助。第二不同维度使用不同的波长可以让模型同时关注短距离和长距离的依赖关系。不过需要说明的是现在很多实际模型会直接使用可学习的位置嵌入而不是固定的正弦编码。效果上看两者差别并不大正弦编码的主要优势在于不需要训练额外参数并且有一定外推能力。6.4 可学习位置编码与 RoPE可学习位置编码的做法是初始化一个和输入序列长度相同的位置嵌入表然后随模型一起训练。GPT、BERT 早期版本就是这样的思路。近年来的很多 transformer 架构进一步采用了旋转位置编码 RoPE。它把位置信息通过旋转矩阵加到 query 和 key 上能够同时表达绝对位置和相对位置在长文本和推理类任务里表现更好。看 transformer 源码时如果遇到rotate_half这类函数通常就是在实现 RoPE。7. 残差连接、LayerNorm 与前馈网络7.1 残差连接Transformer 每个子层后面都套了一层残差连接。做法很简单output LayerNorm(x sublayer(x))先对输入做自注意力或前馈网络变换再把原始输入加回去最后做 LayerNorm。残差连接的意义是让梯度可以绕过当前子层直接回传避免深层网络出现梯度消失。如果没有残差连接几十层 Transformer 很难稳定训练。7.2 LayerNorm 与 BatchNormTransformer 使用 LayerNorm 而不是 BatchNorm这个细节很容易被忽略但很重要。BatchNorm 对一个 batch 的同一维度做归一化依赖 batch 大小如果 batch 很小统计量会不稳定。而且处理变长序列时不同样本 pad 的规则也会对统计量造成干扰。LayerNorm 是对单个样本的所有特征维度做归一化不依赖 batch 大小更适合序列模型。公式如下LayerNorm(x) (x - mean(x)) / sqrt(var(x) eps) * gamma beta其中gamma和beta是可学习参数。用 PyTorch 实现时import torch x torch.randn(2, 5, 8) # (batch, seq_len, d_model) ln torch.nn.LayerNorm(8) y ln(x) print(y.shape) # torch.Size([2, 5, 8])这里的LayerNorm(8)指的是对最后一维做归一化也就是对每个 token 的 8 维向量做归一化。7.3 逐位置前馈网络自注意力负责聚合序列信息前馈网络负责对每个位置的信息做非线性变换。它的结构很简单就是两层全连接加激活函数FFN(x) max(0, x W1 b1) W2 b2原始 Transformer 用的是 ReLU后续很多模型换成了 GELU。中间维度通常比d_model大很多一般是 4 倍比如d_model512时中间层是2048。这个过程也被称为 Position-wise FFN意思是同一个前馈网络对序列中的每个 token 独立计算参数共享。7.4 编码层完整前向流程一个 Transformer Encoder Block 的前向流程可以写成下面的伪代码def encoder_block(x, self_attention, ffn, dropout): # 1. 自注意力子层 attn_out self_attention(x, x, x) x layer_norm(x dropout(attn_out)) # 2. 前馈网络子层 ffn_out ffn(x) x layer_norm(x dropout(ffn_out)) return x注意这里的顺序是 Post-LN也就是先残差后 LayerNorm。现在很多主流实现也采用 Pre-LN即先 LayerNorm 再进入子层训练更稳定。两种方式对应不同的代码写法读源码时需要注意区分。维度变化可以用下面的表总结阶段输入形状输出形状说明词嵌入(batch, seq_len)(batch, seq_len, d_model)token id 查表加位置编码(batch, seq_len, d_model)(batch, seq_len, d_model)逐位置相加多头自注意力(batch, seq_len, d_model)(batch, seq_len, d_model)形状不变信息交互前馈网络(batch, seq_len, d_model)(batch, seq_len, d_model)中间升维再降维全连接输出(batch, seq_len, d_model)(batch, seq_len, vocab_size)预测下一个 token 概率这个表几乎就是 transformer 架构及其工作原理的主干。8. 训练 Transformer 的前置概念8.1 Softmax 与交叉熵Transformer 在输出层会接一个 Softmax把 logits 转成概率分布然后和真实标签计算交叉熵损失。Softmax 公式softmax(z)_i exp(z_i) / sum_j exp(z_j)交叉熵公式Loss -sum_i y_i * log(p_i)其中y_i是真实标签的概率分布p_i是模型预测分布。语言模型通常一次只预测下一个 token所以真实标签可以看成 one-hot。在代码里如果使用 PyTorchCrossEntropyLoss会自己把 Softmax 和交叉熵合并计算不需要手动加 Softmax。8.2 Adam 优化器Transformer 训练几乎所有场景都用 Adam 优化器。它结合了动量和自适应学习率对大多数参数都能给出比较稳定的更新。Transformer 参数量大梯度尺度变化也很剧烈Adam 的优势在于每个参数有独立的学习率调整在训练早期更容易稳定。更常用的是 AdamW在 Adam 基础上解耦了权重衰减。8.3 Warmup 学习率预热Warmup 是训练 Transformer 时经常用到的学习率策略。它的特点是在训练初期学习率从很小的值线性上升到一个峰值然后再按平方根衰减。为什么要这样做Transformer 层数深初始化阶段的输出分布不太稳定。如果一开始就用较大学习率容易造成训练震荡甚至发散。先用小学习率让模型逐步稳定再进入正常学习区间效果通常更好。8.4 Dropout、标签平滑与梯度裁剪Dropout 是 Transformer 里最常用的正则化手段会加在自注意力的输出、残差连接和 FFN 中。很多开源实现里都能看到dropout0.1或dropout0.2这样的参数。标签平滑是另一种常见的正则化手段。它把真实标签的 one-hot 分布部分概率分给其他类别避免模型过于自信。原始 Transformer 训练时已经使用了 label smoothing。梯度裁剪则用于处理梯度爆炸尤其是训练较长序列时。方法很简单当梯度的全局范数超过阈值时等比缩放所有梯度。这个技巧在训练 RNN 时代就很常用Transformer 训练中同样有效。9. Transformer 架构串联与复习自查9.1 Encoder 侧Encoder 的输入是源序列经过词嵌入和位置编码后进入若干个相同的 Encoder Block。每个 Block 内部只有两种子层多头自注意力。逐位置前馈网络。每个子层都带残差连接和 LayerNorm。最后的输出是对源序列的高层表示每个位置都融合了全序列的信息。9.2 Decoder 侧Decoder 的结构要复杂一些每个 Block 内部有三种子层Masked 多头自注意力。多头交叉注意力。逐位置前馈网络。Masked 自注意力保证生成时只能看到当前位置之前的 token不能偷看未来。交叉注意力的 query 来自 Decoderkey 和 value 来自 Encoder 最后一层的输出也就是让解码器去编码器的信息里检索相关内容。9.3 Masked Self-AttentionMasked Self-Attention 的实现方式很简单在计算注意力得分时把未来位置的得分替换成负无穷大这样 Softmax 之后这些位置的权重就是 0。例如序列长度为 4生成第 2 个位置时位置 3 和位置 4 的未来信息都要被屏蔽。这个 mask 矩阵是上三角矩阵对角线及以下为 0以上为负无穷。9.4 Cross-AttentionCross-Attention 是连接 Encoder 和 Decoder 的桥梁。在机器翻译场景里Encoder 负责理解源语言Decoder 负责生成目标语言Cross-Attention 让每个目标语言 token 去关注源语言中最相关的部分。它和自注意力的唯一区别是 Q、K、V 的来源不同Q 来自 Decoder 当前层的输出。K 和 V 来自 Encoder 最后一层的输出。如果只看公式Cross-Attention 和普通注意力没有任何区别。9.5 输入到输出的形状流转把整条链路串起来看源文本 - token ids - embedding - 加位置编码 - N 个 Encoder Block - Encoder 输出 目标文本 - token ids - embedding - 加位置编码 - N 个 Decoder Block - 输出层 - Softmax - 概率形状流转如下位置形状说明输入 token(batch, seq_len)离散 idEmbedding 后(batch, seq_len, d_model)密集向量Encoder 输出(batch, seq_len, d_model)语义表示Decoder 输入(batch, tgt_len, d_model)目标序列最终 logits

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

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

免费获取报价