Imagine a busy restaurant kitchen. Each waiter carries a plate with a word written on it—these are your input tokens. As the waiters move through the kitchen, they must call out key information to each other so every plate absorbs the context of the whole table's order (self-attention). Then they pause at a chef's station to refine each plate individually (feed-forward network). To make sure no one forgets the original plate under all the added garnish, they keep a side copy of the original order (residual connection). And to keep everyone's voice at a normal volume—not shouting over each other—there's a "volume control" (layer normalization). Finally, that control panel also ensures the whole kitchen remembers the order in which courses were placed (positional encoding). This entire orchestrated kitchen workflow—repeated again and again—is what powers the Transformer Encoder.
职业价值:理解 Transformer Encoder 是掌握 BERT、GPT 底层逻辑的基石。对工程师而言,这能让你从“调包”进阶到“改骨架”,是深耕 NLP、多模态与大模型推理的必修内功。
可视化演示
动态演示Transformer En的核心机制,通过总览/传输/转换/输出四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Transformer En:编码器输 编码器输入处理与位置编码注入 多头自注意力的逐头计算流程 核心机制可视化 Transformer En整体架构与关键组件
编码器总览
回到那间繁忙的厨房。这间厨房的目标是把一张张写着单词的“点菜单”变成一盘盘“菜品”—也就是富含上下文信息的向量。整个厨房被称为“编码器”(Encoder),它不是一间,而是一长列相同的操作台,每张操作台都叫“编码器层”(Encoder Layer)。你点多少道菜,或者句子有多长,这些操作台都并行工作——每个词的处理同时进行,不再像老式 RNN 那样必须等前一个词处理完。
每张操作台都放置着两台机器。第一台是“传话器”(多头自注意力子层),让所有单词互相打听对方的含义。比如“苹果”这个词,在“苹果发布了新手机”这句话里,它需要从“手机”那里获得“这是品牌”的信号。第二台是“调味机”(前馈网络子层),它不看别人,只单独为每个词“提味”——把词向量里的特征做一次精细的加工。
有意思的是,每台机器都被一个“防丢包装”包裹着:原版菜先走一条旁路(残差连接),和加工后的菜合并起来,再被统一“调整音量”(层归一化)。这就是传说中的“残差连接 + 归一化”包裹结构。为什么要这样?后面细说。
这座厨房最妙的地方在于:无论句子多长,所有单词都同时在操作台上被处理,这大大提升了训练速度。而“传话器”让每个词都能感知全局,又保证了像“苹果”这类多义词得到正确的上下文含义。整个编码器,就是这条流水线的无限重复——层数越多,提取的语义越抽象、越高级。
- 内层整体X 先进入多头自注意力,结果与原输入相加(残差),再做第一次层归一化。
- 外层整体归一化后的结果进入前馈网络,再与 X_1 相加(残差),最后再归一化。
输入嵌入与位置编码
厨房里有个小麻烦:如果所有“点菜单”只是被随意放在操作台上,没有编号,大厨们就分不清哪道菜先点、哪道菜后点。在 Transformer 诞生前,RNN 是让单词排队进入,天然自带顺序;但 Transformer 是同时把所有词倒进操作台,词与词的先后关系瞬间丢失了。
怎么办?解决方案是:在词向量注入位置信息。每个词先被查表转换为一个向量(词嵌入),然后加上一个“坐标向量”——这个坐标由正弦和余弦函数生成。位置越靠后的词,坐标向量的频率就越低。这就像给每张“点菜单”贴上不同颜色的序号贴纸,而且贴纸的颜色模式有规律可循。
位置编码的公式如下:
其中 pos 是词的位置(0,1,2…),2i 和 2i+1 表示向量里第偶数维和第奇数维。
为什么用正余弦而不是直接写数字 0,1,2?因为正余弦值域在 [-1,1],不会把词向量的数值冲爆;而且正余弦函数具有线性和差性质,模型可以通过它们感知两个词的相对距离。这就像厨房里贴的序号贴纸,无论单子被放到哪里,大厨看一眼贴纸就能算出两张单子之间隔了几道菜。位置编码正是 Transformer 理解语序的“秘密传感器”。
- 取位置序号
pos表示当前处理的是句子中第几个词(从 0 开始)。 - 计算频率分母
10000^{2i/d_{\text{model}}}控制着波的快慢。i 越大(越靠向向量尾部),分母越大,波的频率越低。 - 偶数维用正弦向量第 0、2、4… 维填入 sin(位置 / 频率)。
- 奇数维用余弦第 1、3、5… 维填入 cos(位置 / 频率)。
多头自注意力
现在,最精彩的环节来了——传话器(多头自注意力)。想象操作台前站着所有单词,比如 “The animal didn't cross the street because it was too tired“。句子里有个 it,它到底指 animal 还是 street?人一眼能看出是 animal,可机器怎么知道?传话器干的正是这件事:每个词都向其他词”提问“,并根据答案从其他词那里”汲取”信息。
具体机制分三步。每个词的向量被复制成三份,分别叫查询(Query)、键(Key)、值(Value)。就像厨房里,每个服务员把自己的菜(Value)展示出来,同时拿出一张写着“我在寻找什么”的标签(Query),所有菜上都贴着“我是什么”的标签(Key)。然后任何两个词之间,都拿 Query 和 Key 做“点积”,得到一个分数——分数越高,说明“我要找的”和“你提供的”越匹配,也就应该拿到越多对方 Value 的信息。
但直接点积有个问题:当向量维度很大时,分数会爆炸,导致梯度不稳定。所以需要缩放——把分数除以维度的平方根。公式如下:
说到“多头”,就是不只做一次上面的运算,而是把 Q、K、V 切分成 h 份“小组”,每份独立做注意力,最后拼接起来。这就像传话器不只一个话务员,而是有 8 个话务员同时工作——一个关注语法关系,一个关注指代关系,一个关注语义相似度,各司其职。
可以想象,8 个话务员各拿各的传话筒,有人听到代词 “it“ 就自动去问前面的名词,有人听到主语就在找谓语,还有人关注形容词和名词的搭配。8 个角度的信息像八份报告,最后由主厨汇总成一份完整的”上下文档案“。这样的词向量,才真正”听懂”了整个句子。
- 分头切分把 Q、K、V 切成 h 份(如 h=8),每份维度为 d_k = d_model / h。
- 结果拼接把 8 个头的结果拼成长向量。
- 输出投影乘以输出权重矩阵 WO,变回 d_model 维度。
残差连接与层归一化
机器不停运行,时间一长就会出问题:菜越加越多,盘子都快装不下了;有些话务员声音太大,把其他声音盖住了。在 Transformer 里,深度网络也会碰到两个麻烦:梯度消失(深层网络反向传播时梯度变小,底层参数学不动)和激活值漂移(网络太深导致各层数据分布不稳定,像音量越调越大或越来越小)。
残差连接就是给数据流开一条“高速公路”:不让信息非得经过层层“加工”才能到后面,而是直接抄近道跳过某些层去和加工后的结果汇合。它的结构非常简单:
这就像厨房里的“主厨试味”环节:每道菜在出锅前,都要往里面放一小羹最初的高汤原汁,保证菜味再丰富也不会失去“本味”。对训练来说,即使神经网络有 12 层、24 层,梯度都能顺这条小路一路传回底层,深层的网络因此能被稳定训练。
层归一化则是“音量控制器”。它的操作对象是当前层每个词的所有特征维度——将一个词向量里所有数值减去均值、除以标准差,再做一次平移缩放。公式如下:
为什么这样做有效?当厨房里每个人音量差距过大时,交流会失控。层归一化强制让每个服务员的声音都回到“标准音量”再继续干活,训练过程立刻稳定下来。而且 normalization 是逐词做的,不受句子长度影响——batch 里句子长短不一也没关系,这让 Transformer 处理变长句子的能力远胜传统神经网络。残差和归一化就像厨房里的“救火员”和“调音师”,两者齐备,深层的厨房才能持续运转不掉链子。
- 计算均值统计这个词向量所有维度的平均值。
- 计算方差每个维度与均值的差的平方再求平均。
- 归一化每个维度减去均值、除以标准差(加 ε 防止除零)。
- 缩放和平移乘以可学习参数 γ,再加上 β,让网络可以“反悔”恢复原分布。
前馈网络
传话器让词与词之间交换了信息,但这还不够。之后每张操作台还有一步“个人精修”——前馈网络(Feed-Forward Network)。它和传话器很不一样:这里的每个词向量是独立加工的,完全不与其他词交互。传话器是团队协作,前馈网络是个人练习。
前馈网络的结构是两层的全连接网络,中间夹着一个 ReLU 激活函数。最核心的特点是:先把维度从 d_model 放大 4 倍,再压缩回 d_model。好比厨房里一道菜的“提味过程”:先把原料打散、翻倍地加入各种调料充分混合(升维),再把它压缩回收成原来大小的一团精华(降维)。
公式如下:
为什么要先扩大再缩小?这是神经网络中经典的“隐层技巧”——高维空间更容易把数据特征分离开来。就像炒菜时要先猛火爆香、让香料分子充分扩散到整个空间,然后再收汁浓缩,味道才进得去。前馈网络的两个全连接层为每一个词向量都配备了一套独立的“特征精炼器”,是 Transformer 里“处理非线性关系”的主力军。
- 第一层线性变换乘以 W1 加上偏置 b1,把维度从 d_model 升到 4d_model。
- ReLU 激活把负值全部置零,正值保持不变——引入非线性,让网络有更强的拟合能力。
- 第二层线性变换乘以 W2 加上 b2,把维度从 4d_model 降回 d_model。
编码器堆叠
现在,你已经有了一台完整的“操作台”(单层编码器),但一台够吗?不够。在实际模型中,BERT-base 用了 12 层编码器,BERT-large 用了 24 层。为什么要叠这么多层?因为单层的“传话器”只能捕捉词与词的表面关联,而深层堆叠能逐层抽象,提炼出越来越高级的语义。
我们来看一条信息是如何在多层之间流动的。假设输入是 “苹果发布了新款手机”。第一层编码器中,注意力可能关注词性搭配,把 “苹果” 和 “发布” 关联起来;第二层中,“苹果” 开始和 “手机” 建立联系,意识到自己在这里是“品牌”而不是“水果”;到了更高层,“苹果、手机、发布、新款”被聚合成一个更完整的语义块——一个“主题”。
每一层接收上一层的输出,输出又喂给下一层:
最终,最顶层的每个词向量都编码了整个句子中与这个词相关的所有上下文信息。BERT 之所以强大,就是因为它拿了 12 层编码器的输出作为“通用特征”,再针对不同任务挂上不同的“小帽子”(分类头、抽取头等)做微调。编码器堆叠,就是那根不断把“词语原石”打磨成“语义宝石”的流水线。
- 初始表示第一层的输入是词嵌入与位置编码之和。
- 逐层传递每经过一层编码器,向量被更新一次。
- 层间递推第 L 层的输出 h^L 就是最终编码结果。
AI 应用场景
场景 1:BERT 句子分类 — 客服工单自动分流
阶段:特征提取(预训练编码器)
解决的问题:传统客服系统中,工单分流依赖人工填写类别或关键词规则,准确率低且无法泛化。“退款”和“退货”规则写法略有不同就能让系统判断出错。BERT 内部的 12 层编码器把工单文本编码为上下文向量,再送进一个分类层,模型自然理解“钱没到账”和“退款”是一回事。
具体案例:某电商平台每天收到 20 万条工单。工程师用 BERT-base 的编码器抽取每张工单的 [CLS] 向量,接一层 softmax 分类器,将工单自动分进“物流、退款、账号、投诉”四类。每类准确率 92% 以上,比关键词规则高出 15 个百分点。
场景 2:句向量抽取 — 语义检索系统
阶段:文本表示
解决的问题:传统的 TF-IDF 或 BM25 检索只能匹配字面关键词,搜“怎么退会员”找不到“取消自动续费”的文档。利用编码器堆叠后,每个词向量都携带全局语义,平均池化所有词的向量得到句向量,语义相近的句子在向量空间中距离很近。
具体案例:某知识库系统将企业内部的 10 万篇操作手册分别通过编码器转换成句向量,存入向量数据库。用户提问时,先编码再检索,找到的答案不仅包含相同关键词,还包含语义等价的表述。这个方案帮助企业客服答复率提升了 30%。
Python 演示
import torchimport torch.nn as nnimport math class MultiHeadAttention(nn.Module): def __init__(self, d_model, n_heads): super().__init__() self.d_model = d_model self.n_heads = n_heads self.d_k = d_model // n_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, seq_len, _ = x.shape Q = self.W_q(x).view(batch, seq_len, self.n_heads, self.d_k).transpose(1, 2) K = self.W_k(x).view(batch, seq_len, self.n_heads, self.d_k).transpose(1, 2) V = self.W_v(x).view(batch, seq_len, self.n_heads, self.d_k).transpose(1, 2) scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k) weights = torch.softmax(scores, dim=-1) output = torch.matmul(weights, V) output = output.transpose(1, 2).contiguous().view(batch, seq_len, self.d_model) return self.W_o(output) class FeedForward(nn.Module): def __init__(self, d_model, d_ff): super().__init__() self.linear1 = nn.Linear(d_model, d_ff) self.linear2 = nn.Linear(d_ff, d_model) def forward(self, x): return self.linear2(torch.relu(self.linear1(x))) class EncoderLayer(nn.Module): def __init__(self, d_model, n_heads, d_ff): super().__init__() self.attention = MultiHeadAttention(d_model, n_heads) self.ffn = FeedForward(d_model, d_ff) self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) def forward(self, x): x = self.norm1(x + self.attention(x)) x = self.norm2(x + self.ffn(x)) return x class TransformerEncoder(nn.Module): def __init__(self, vocab_size, d_model, n_heads, d_ff, num_layers, max_len): super().__init__() self.embedding = nn.Embedding(vocab_size, d_model) self.pos_encoding = self._build_pos_encoding(max_len, d_model) self.layers = nn.ModuleList([ EncoderLayer(d_model, n_heads, d_ff) for _ in range(num_layers) ]) def _build_pos_encoding(self, max_len, d_model): pe = torch.zeros(max_len, d_model) pos = 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(pos * div_term) pe[:, 1::2] = torch.cos(pos * div_term) return pe.unsqueeze(0) def forward(self, x): x = self.embedding(x) + self.pos_encoding[:, :x.size(1), :] for layer in self.layers: x = layer(x) return x if __name__ == "__main__": vocab_size = 1000 batch_size, seq_len = 2, 8 model = TransformerEncoder(vocab_size, d_model=128, n_heads=8, d_ff=512, num_layers=3, max_len=64) tokens = torch.randint(0, vocab_size, (batch_size, seq_len)) output = model(tokens) print("Output shape:", output.shape)
Q = self.W_q(x).view(...).transpose(1, 2)将输入映射到 Q 矩阵并拆分成多个头,维度变为 (batch, heads, seq_len, d_k)。scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)缩放点积注意力,计算每个词对所有词的匹配分数。weights = torch.softmax(scores, dim=-1)对最后一维归一化,得到和为 1 的注意力权重。x = self.norm1(x + self.attention(x))残差连接 + 层归一化,这是编码器层标准包裹的第一步。div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))预计算位置编码的频率分母,等价于公式中的10000^{2i/d_model}。pe[:, 0::2] = torch.sin(pos * div_term)偶数维填正弦值。
常见误区
误区残差连接好像只是加了一个旁路,网络层数浅的时候,去掉它模型差别不大。
正解这个“简单的加法”是深度 Transformer 能训练起来的关键。没有残差连接,当网络堆到 12 层以上时,反向传播的梯度会在层层相乘中趋近于零(梯度消失),底层网络完全学不到东西。就像一盏灯如果没有电线直连电源,信号会在线路里衰减到几乎为零,灯根本亮不起来。残差连接就是用一根粗电线绕过所有开关直连电源,让电流(梯度)永远有一条通畅的路。
为何常见:因为数值上只是加了一个 x,看起来过于简单,初学者容易低估其“让深层网络可训练”的核心作用。
误区都是归一化,LayerNorm 和 BatchNorm 差不多,随便用哪个都行。
正解两者归一化的维度方向完全不同。BatchNorm 在一个 batch 的同一个特征维度上做统计,受 batch 大小影响巨大;而 LayerNorm 是对单个样本的所有特征做统计,完全不受 batch 内其他样本影响。Transformer 训练时常用大 batch,但推理时可能 batch=1,BatchNorm 的统计量就会漂移,而 LayerNorm 稳稳当当。
为何常见:大多数初学者先接触 CNN 里的 BatchNorm,遇到 Transformer 自然而然代入,忽略了维度方向这个本质区别。
误区多头不就是把注意力算 8 遍然后取平均吗?
正解多头是把 Q、K、V 切分成 8 份,每份独立计算注意力,最后拼接(Concat)而不是求平均。每个头有自己的投影矩阵,能关注不同模式。求平均会削弱每个头的特异性,而拼接保留了每个头学到不同侧面的信息,再通过输出投影融合。
为何常见:从“8 个头 = 8 次计算”的直觉出发,容易把并行的结果简单理解为平均。
误区位置编码是 Transformer 独有的创新点。
正解RNN 和 LSTM 通过“按时间步逐个输入”隐式编码了位置;CNN 可以通过不同大小的卷积核在一定程度上感知局部位置信息。Transformer 是第一个因完全并行处理、丧失顺序感而必须显式设计位置编码的架构。“怎么编码位置”这个问题的答案为所有序列模型提供了新的武器。
为何常见:因为 Transformer 的论文里位置编码公式特别显眼,掩盖了其他模型对位置信息的隐式处理。
面试题
Q1. Transformer 编码器层的标准包裹顺序是什么?
- A.注意力 → 前馈 → 残差 → 归一化
- B.残差+归一化后接注意力,再残差+归一化后接前馈← 正确答案
- C.前馈 → 归一化 → 注意力 → 残差
- D.归一化 → 残差 → 注意力 → 前馈
答案:B。标准顺序是先做“注意力 + 残差 + 归一化”,再做“前馈 + 残差 + 归一化”。选项 B 准确地描述了这一步。其他选项的次序打乱了残差与归一化的先后关系,均不符合原论文公式。
Q2. 位置编码公式中,偶数维使用哪个函数?
- A.余弦函数
- B.正弦函数← 正确答案
- C.指数函数
- D.正切函数
答案:B。位置编码公式中,偶数维(2i)填入正弦值,奇数维(2i+1)填入余弦值。这是原文的定义方式,目的是让相邻维度互补、产生更丰富的编码模式。
Q3. 缩放点积注意力中乘以 1/√d_k 的目的是什么?
- A.增加模型参数
- B.防止点积过大导致 Softmax 梯度消失← 正确答案
- C.加速矩阵乘法
- D.降低计算精度
答案:B。当 d_k 较大时,点积数值会过大,Softmax 会进入饱和区,导致梯度极小。除以 √d_k 是为了把分数压回正常范围,保持梯度平稳。这与参数数量、计算速度无关。
Q4. 多头注意力中每个头可以关注哪些模式?
- A.语法关系← 正确答案
- B.指代关系← 正确答案
- C.语义相似度← 正确答案
- D.图像像素颜色
答案:ABC。多头注意力中的不同头在训练中会“自组织”关注不同的语言模式:有的头学语法依赖,有的头学代词指代,有的头学词汇相似度。D 选项与自然语言处理无关,编码器适用于文本序列。
Q5. 前馈网络中的维度变化过程是怎样的?
- A.保持不变,d_model → d_model
- B.先降维到 d_model/4,再升回
- C.先升维到 4×d_model,再压缩回 d_model← 正确答案
- D.先升到 2×d_model,再升到 4×d_model
答案:C。前馈网络第一个线性层将维度从 d_model 提升到 4×d_model,经过 ReLU 后第二个线性层压缩回 d_model。“先扩后缩”的漏斗式设计赋予模型更丰富的特征表达空间。
Q6. 残差连接主要解决深度 Transformer 的什么问题?
- A.过拟合问题
- B.梯度消失问题← 正确答案
- C.位置信息丢失问题
- D.词嵌入稀疏问题
答案:B。残差连接为梯度提供了捷径,让信息从高层直接传回底层,有效缓解链式求导带来的梯度消失。过拟合通常靠 Dropout 正则缓解,位置丢失靠位置编码,稀疏性与嵌入查表有关,均与本问题无关。
Q7. LayerNorm 的统计量基于什么计算?
- A.整个 batch 所有样本的同一维度
- B.单个样本的所有特征维度← 正确答案
- C.所有 batch 的所有维度
- D.单个样本的单个维度
答案:B。LayerNorm 对每个样本独立计算均值和方差。这与 BatchNorm 完全不同,后者在 batch 维度上统计。独立统计让 LayerNorm 不受 batch 大小影响,非常适合变长序列的 Transformer。
Q8. 关于 Transformer 编码器的描述,正确的有哪些?
- A.编码器可以并行处理输入序列← 正确答案
- B.每层包含注意力和前馈子层← 正确答案
- C.子层输出不需要残差连接
- D.层数加深可以提取更抽象的语义← 正确答案
答案:ABD。Transformer 的核心优势就是并行处理(A);每层确实由注意力和前馈两大子层构成(B);残差连接是标准配置,没有会梯度消失(C 错);叠加更深编码器能逐层抽象语义(D 正确),这正是 BERT-large 用 24 层的原因。
Q9. 在编码器堆叠中,第 l+1 层的输入是?
- A.原始词嵌入
- B.第 l 层的输入
- C.第 l 层的输出← 正确答案
- D.位置编码
答案:C。编码器堆叠是链式传递结构,第 l 层的输出 h^l 正是第 l+1 层的输入。原始词嵌入只作为第 0 层的输入,位置编码在其上叠加。层层递进,语义逐步抽象。
Q10. 关于多头注意力的信息流,以下哪些说法正确?
- A.每个头有自己的 Q、K、V 投影矩阵← 正确答案
- B.所有头的输出拼接后乘以输出投影矩阵← 正确答案
- C.头之间共享权重
- D.每个头关注不同子空间的特征← 正确答案
答案:ABD。每个头有独立的权重矩阵,才能学到不同关注模式(A 对,C 错);输出是拼接后通过 WO 投影(B 对);不同头确实对应不同表示子空间(D 对)。这些独立投影是多头机制获得丰富语义的关键。