近年来,Transformer 架构及其衍生模型,如 BERT,在自然语言处理(NLP)领域引领了技术革新。作为许多大型语言模型的基础,Transformer 为提升 NLP 任务的准确性奠定了重要基础,并推动语言技术广泛应用于各行业。本系列内容将循序渐进,从了解 Transformer 架构开始,逐步深入到 BERT 细节及语言模型预训练实践。
文章目录
1 Transformer概述
Transformer架构支撑着无数现代自然语言处理、计算机视觉、自动语音识别、时间序列建模及其他应用。在这篇文章中,我们将从整体上了解Transformer架构,以便在本课程后续部分讨论BERT和GPT架构的实现细节。
在2017年末,大量的序列到序列(seq2seq
)问题,包括翻译,都是通过卷积神经网络或循环神经网络解决的。当时所有模型的共同点是,它们都大量利用了注意力机制,以便处理长序列。Transformer架构在 Attention is All You Need! 论文中提出,展示了一种新方法,仅使用注意力机制也能达到类似性能,完全去除了卷积或循环机制的需要。
Transformer成为了随后开发的许多模型的基础,这些模型广泛应用于自然语言处理(NLP)任务,同时也推动了计算机视觉等领域的新进展。
由于Transformer是BERT等模型的基础,因此理解其内部工作原理非常重要。
Transformer是一个强大的序列到序列(seq2seq
)模型,是在2017/2018年之前广泛使用的循环神经网络(RNN)或卷积神经网络(CNN)的替代方案。这是因为Transformer解决了RNN和CNN的一个重大缺陷,即它们的计算开销。在原始论文中,作者仅用八块GPU训练了3.5天就达到了当时的最先进水平,而替代模型通常需要更长的训练时间。这种计算要求的大幅降低使得研究社区可以使用更大规模的数据集和模型,进而促成了如GPT、BERT等架构的成功。
大多数序列到序列模型由编码器和解码器组成。编码器将输入序列映射到某种中间表示(即n维向量)。然后这个抽象向量被输入到解码器中,生成输出序列。图2以翻译任务为例,展示了一个编码器-解码器架构。
Transformer架构同样由编码器和解码器组成,但与传统RNN或CNN架构不同,Transformer完全依赖于自注意力机制,直接建模句子中所有单词之间的关系。
与循环或卷积序列到序列模型相比,Transformer由相对简单的机制构建(后续讨论)。
- 分词器(
Tokenizer
) - 嵌入层(
Embedding layers
) - 位置编码(
Positional Encoding
) - 多头注意力(
Multi-Head Attention
)和掩码多头注意力(Masked Multi-Head Attention
) - 以及一些基础组件,如前馈神经网络(
Feed Forward Layers
)
多头注意力机制和前馈神经网络共同形成了一个Transformer层,可以重复多次,使我们能够根据需求扩展或缩小模型规模。原始Transformer模型由编码器和解码器各6个相同的层组成。
在进入BERT架构之前,我们将简要讨论上述组件。
2 分词器
在 Transformer 架构中,分词器 (Tokenizer
)位于神经网络之外,处于整个流程的基础层。
分词器负责将原始文本转换为最初的数字表示,即由子词单元 (subword components
) 组成的 token 序列。它是架构中非常重要的一部分,因为它让模型能够灵活地处理人类语言的复杂性。
子词单元指的是介于完整单词和单个字符之间的小片段。
也就是说,一个单词如果太长、太少见,就可以被拆成几个更常见的子词单元。子词单元既不是单个字母,也不是完整单词,而是一个折中的单位。比如英文单词
unhappiness
可以被分成子词单元:“un” + “happi” + “ness”。这些小单元本身有一定含义,而且出现在语料库中的频率相对较高。这样即使模型没见过完整的"unhappiness",它也可能见过"un"、“happy”、“ness”,所以依然能理解大致意思。
例如,在黏着语 (agglutinative languages
) 中,分词器可以将复杂单词拆分成更易管理的组成部分;还能处理原始语料中未出现的新词、外来词或特殊字符,同时尽可能保持文本表示的紧凑性,使序列尽量短小。
针对每种语言,存在大量不同的分词器,它们在设计和细节上差异很大。大多数基于 Transformer 的架构使用经过训练的分词器,目标是最大限度地缩短典型输入序列的长度。像 WordPiece (BERT 使用的分词器) 和 SentencePiece (T5、RoBERTa 使用的分词器) 这样的分词器还有多个不同的变体,以适应不同的语言、特定领域 (例如医疗语料库) 或其他架构细节,例如 token 数量不同、是否区分大小写等。
不过,目前关于分词器选择对模型行为影响的研究文献仍然相对较少。因此,大多数情况下,人们仍沿用原始模型实现中默认的分词器。
尽管如此,重新训练分词器并将其替换到模型中,相对来说是比较容易的。
- 下一篇文章中,我们将更详细地探讨 BERT 模型中的分词器选项,以及具体的实现细节。
3 嵌入
经过分词后的文本接下来由嵌入机制处理。嵌入是通过一种算法将原始数据转换为神经网络可以接受的数字表示。这种数字表示通常被称为文本嵌入。
文本嵌入通常被设计成具有附加属性,以帮助机器学习算法处理文本的复杂性,并使文本表示更紧凑(更少稀疏)。例如,Word2Vec
、GloVe
、或fastText
等算法/库,生成能捕捉单词语义意义的嵌入,使得相关单词在嵌入空间中更接近,而无关单词则距离更远。
与此不同的是,Transformer架构通常使用非常简单、可学习的嵌入,因为神经网络本身足够强大,能够自行学习这些语义结构。因此,Transformer中的嵌入层实际上只是一个将原始token
映射到目标数字表示的矩阵(矩阵大小为词汇表大小 ×
d
m
o
d
e
l
\mathbf{d_{model}}
dmodel)。这个嵌入矩阵在端到端优化过程中被训练。值得注意的是,在原始Transformer实现中,这个矩阵在输入嵌入、输出嵌入以及Softmax操作之前的线性层之间共享权重(见上面的架构图右上角)。
- 词汇表大小:指的是分词器将所有可能出现的 token (子词单元)列成一个固定列表,这个列表的总长度
- d m o d e l \mathbf{d_{model}} dmodel:每个 token 的向量表示的维度大小。
词汇表大小 和
d
m
o
d
e
l
\mathbf{d_{model}}
dmodel 都是可以调整的架构超参数。原始Transformer模型使用
d
m
o
d
e
l
=
512
\mathbf{d_{model} = 512}
dmodel=512,我们后续的代码使用的是Transformer-big
模型,即
d
m
o
d
e
l
=
1024
\mathbf{d_{model} = 1024}
dmodel=1024。
4 位置编码
4.1 正余弦位置编码
语言模型需要利用单词在句子中的顺序特性。由于Transformer模型中没有循环或卷积单元,因此使用位置编码(Positional Encodings
, PEs
)来考虑输入序列中单词的顺序。位置编码的维度与嵌入向量(即dmodel)相同,因此可以将两者相加(见上面的架构图)。这使得模型能够理解输入文本中每个单词的位置。
在原始的Transformer的论文中,作者采用了不同频率的正弦和余弦函数来进行位置编码:
P
E
(
p
o
s
,
2
i
)
=
sin
(
p
o
s
10000
2
i
/
d
model
)
P
E
(
p
o
s
,
2
i
+
1
)
=
cos
(
p
o
s
10000
2
i
/
d
model
)
PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right) \\ PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right)
PE(pos,2i)=sin(100002i/dmodelpos)PE(pos,2i+1)=cos(100002i/dmodelpos)
其中,
pos
\text{pos}
pos 是单词的位置,
i
i
i 是维度,范围是
[
0
,
d
model
/
2
)
[0, d_{\text{model}}/2)
[0,dmodel/2)。让我们通过一个例子来解释上面的公式:
假设 d model = 4 d_{\text{model}} = 4 dmodel=4。这意味着,输入序列中位置 pos ∈ [ 0 , L − 1 ] \text{pos} \in [0, L-1] pos∈[0,L−1] 的单词 w w w 会用一个4维的嵌入向量 e w e_w ew 来表示。设 i ∈ [ 0 , 2 ) i \in [0, 2) i∈[0,2),那么,对于嵌入向量中偶数索引的位置,我们使用公式:
sin ( pos 10000 2 i / d model ) \sin\left(\frac{\text{pos}}{10000^{2i/d_{\text{model}}}}\right) sin(100002i/dmodelpos)
对于奇数索引的位置,我们使用公式:
cos ( pos 10000 2 i / d model ) \cos\left(\frac{\text{pos}}{10000^{2i/d_{\text{model}}}}\right) cos(100002i/dmodelpos)
我们将嵌入向量的索引记为 k k k,其中 k ∈ [ 0 , 2 i ) k \in [0, 2i) k∈[0,2i)。假设输入句子的第一个位置是 pos = 0 \text{pos} = 0 pos=0,嵌入向量的第一个索引是 k = 0 k = 0 k=0。那么,第一个维度( k = 0 k = 0 k=0)的位置编码(PE)是:
sin ( 0 10000 0 / 4 ) \sin\left(\frac{0}{10000^{0/4}}\right) sin(100000/40)
第二个维度( k = 1 k = 1 k=1)的位置编码是:
cos ( 0 10000 0 / 4 ) \cos\left(\frac{0}{10000^{0/4}}\right) cos(100000/40)
对于第三个维度( k = 2 k = 2 k=2)和第四个维度( k = 3 k = 3 k=3),分别是:
sin ( 0 10000 2 / 4 ) \sin\left(\frac{0}{10000^{2/4}}\right) sin(100002/40)
和
cos ( 0 10000 2 / 4 ) \cos\left(\frac{0}{10000^{2/4}}\right) cos(100002/40)
因此,我们可以写出输入序列第一个单词的位置编码:
PE ( pos = 0 ) = [ sin ( 0 10000 0 / 4 ) , cos ( 0 10000 0 / 4 ) , sin ( 0 10000 2 / 4 ) , cos ( 0 10000 2 / 4 ) ] \text{PE}(\text{pos} = 0) = \left[\sin\left(\frac{0}{10000^{0/4}}\right), \cos\left(\frac{0}{10000^{0/4}}\right), \sin\left(\frac{0}{10000^{2/4}}\right), \cos\left(\frac{0}{10000^{2/4}}\right)\right] PE(pos=0)=[sin(100000/40),cos(100000/40),sin(100002/40),cos(100002/40)]
简单化后可以得到:
PE ( pos = 0 ) = [ sin ( 0 / 10000 0 ) , cos ( 0 / 10000 0 ) , sin ( 0 / 100 ) , cos ( 0 / 100 ) ] = [ 0 , 1 , 0 , 1 ] \text{PE}(\text{pos} = 0) = \left[\sin\left(0/10000^0\right), \cos\left(0/10000^0\right), \sin\left(0/100\right), \cos\left(0/100\right)\right] = [0, 1, 0, 1] PE(pos=0)=[sin(0/100000),cos(0/100000),sin(0/100),cos(0/100)]=[0,1,0,1]
下一步是将这个向量加到嵌入向量 e w e_w ew 上,得到新的向量 e w ′ e'_w ew′:
e w ′ = PE ( pos = 0 ) + e w e'_w = \text{PE}(\text{pos}=0) + e_w ew′=PE(pos=0)+ew
然后,对于输入序列中的每个单词(位置 pos = 1, 2, …, L-1),我们依次计算对应的 e w ′ e'_w ew′。
1.为什么要同时用sin和cos
这是为了编码方向信息和保留位置信息的唯一性。
- sin是奇函数,cos是偶函数,它们组合后能区分相对位置的前后方向
- sin和cos是线性无关函数,两者组合在一起后,对任意位置pos编码是唯一的,并且保留了平移不变性的结构
- 有助于模型在训练过程中通过线性变换轻松提取位置差异信息
此外,有研究表明加sin和cos比单用sin(或cos)效果更稳定。
·
2.平移不变性?
模型可以识别两个token的相对位置关系,而不是只记住它们的绝对位置。
假设你有两个句子:“the cat sat"和"a dog slept”。
我们希望模型能理解:"cat"和"sat"的相对位置(比如相差1个token)和"dog"和"slept"的相对位置(也相差1个token)
即使它们的起始位置不同,只要相对位置相同,它们的语义结构就相似。我们希望模型能学习到这类结构性信息,这就是平移不变性。
如果位置编码不具备这种特性,那么模型只能死记硬背"cat"出现在第2个位置、"sat"出现在第3个位置,而不能泛化到其它场景。
4.2 位置编码的选择
后续我们将使用的英伟达NeMo
库中的位置编码。
- 注意:我们这里选择的位置编码方式是随机的。但幸运的是,目前关于位置编码对模型性能影响的研究越来越多,比如 RETHINKING POSITIONAL ENCODING IN LANGUAGE PRE-TRAINING。
以下是NeMo中FixedPositionalEncoding
的实现,更多内容见transformer_modules.py:
import math
import numpy as np
import torch
# 定义隐藏层大小
hidden_size=512
# 定义最大序列长度
max_sequence_length=768
# 初始化位置编码矩阵
pos_enc = torch.zeros(max_sequence_length, hidden_size)
# 创建位置索引
position = torch.arange(0.0, max_sequence_length).unsqueeze(1)
# 计算正弦余弦函数的系数
coef = -math.log(10000.0) / hidden_size
# 生成除数项
div_term = torch.exp(coef * torch.arange(0.0, hidden_size, 2))
# 计算偶数索引位置的正弦值
pos_enc[:, 0::2] = torch.sin(position * div_term)
# 计算奇数索引位置的余弦值
pos_enc[:, 1::2] = torch.cos(position * div_term)
# 归一化处理
pos_enc.div_(math.sqrt(hidden_size))
# 将位置编码矩阵转换为numpy数组
T=pos_enc.cpu().detach().numpy()
# 画出位置编码曲线
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
# 设置画布大小
plt.figure(figsize=(50,20))
# 绘制位置编码图
plt.plot(T)
# 显示图形
plt.show()
输出如下:
5 Transformer编码器
在Transformer论文中,编码器和解码器各由 N = 6 \mathbf{N = 6} N=6个相同的层组成,总共12层。每个编码器层有两个子层:第一个是多头自注意力机制;第二个是简单的位置逐点全连接前馈神经网络。
编码器的作用是将源句子编码为隐藏状态向量;解码器则使用这些隐藏状态的最终表示来预测目标语言的字符。下面是单个编码器块(六个中的一个)的示意图:
(1)输入部分
输入的是每个单词(如 “tennis”、“ball”)的嵌入向量。在送入编码器之前,每个嵌入向量加上了位置编码,这样模型才能感知单词的顺序。
(2)自注意力机制(Self-Attention
)
加了位置编码后的向量首先送入 Self-Attention 模块,这里的 “Self” 意味着同一组输入内部的单词互相计算注意力权重,比如 “tennis” 在计算自己的表示时,也会参考 “ball” 的信息。
(3)加和归一化(Add & Normalize)
Self-Attention 的输出会与原始输入做残差连接(Residual Connection
),然后再经过归一化(Layer Normalization
)处理。
(4)前馈神经网络(Feed Forward)
前馈神经网络是位置逐点(Position-wise
)的全连接神经网络,对每个位置的向量独立处理,不涉及序列中其他位置。
(5)再一次加和归一化(Add & Normalize)
Feed Forward 的输出再次进行残差连接与归一化处理。
(6)输出部分
这个单层编码器块输出的结果会作为输入送入下一个编码器块,整个编码器由6个这样的块串联组成。
6 注意力机制
Transformer模型的核心是注意力机制。在深度学习中,注意力机制借鉴了人类在感知环境时,对不同信息片段进行选择性关注的方式。我们通常不会扫描视野中的所有内容,而是根据场景的上下文关注重要的特征。同样地,在语言处理上,我们也会根据上下文更加关注某些重要单词。
注意力机制在处理输入序列时,在每个步骤中决定哪些部分是重要的。可以将注意力机制看作是重要性权重的向量。在下图的例子中,“ball”对“tennis”和“playing”有强注意力联系,而“tennis”和“dog”的联系则较弱。
6.1 自注意力(Self-Attention)
Transformer中的注意力机制由三个组成部分构成:查询(Query
)、键(Key
)和值(Value
)。每个部分都有一个关联的权重矩阵,并在优化过程中进行训练。
计算自注意力的第一步是,从每个输入的嵌入向量生成三个向量:Query
向量、Key
向量和Value
向量,这通过分别乘以不同的权重矩阵实现。
注意力函数定义为:
Attention
(
Q
,
K
,
V
)
=
softmax
(
Q
K
T
d
k
)
V
\text{Attention}(Q, K, V) = \text{softmax}\left( \frac{QK^T}{\sqrt{d_k}} \right) V
Attention(Q,K,V)=softmax(dkQKT)V
让我们看看如何计算Q、K、V矩阵。Transformer将输入的编码表示视为键值对(K, V)
,它们都具有
d
k
d_k
dk维。
步骤如下:
- 对每个单词,生成一个查询向量 q i q_i qi、一个键向量 k i k_i ki和一个值向量 v i v_i vi。这些向量是通过将单词的嵌入向量分别与训练中学习到的三个矩阵相乘得到的。
- 计算每个单词的自注意力分数,例如第一个单词的得分是 q 1 ⋅ k 1 q_1 \cdot k_1 q1⋅k1,第二个是 q 1 ⋅ k 2 q_1 \cdot k_2 q1⋅k2。
- 将分数除以
d
k
\sqrt{d_k}
dk。
-
d
k
d_k
dk 是每个头(
head
)里key向量的维度,论文中取了64
-
d
k
d_k
dk 是每个头(
- 将结果通过SoftMax归一化。
- 将每个值向量乘以相应的SoftMax分数。
- 加总加权后的值向量,得到当前位置的自注意力输出。
想象你在开班会,听大家讲话,然后决定要专注听谁的意见。
- Q Q Q(Query 查询向量) 是你自己的兴趣点:你关心什么。
- K K K(Key 键向量) 是每个人的发言主题标签:别人说的重点是什么。
- V V V(Value 值向量) 是每个人真正说的话内容。
1.生成 Q、K、V
每个人发言前,你(模型)根据他们的特点,给他们标上了兴趣点 (Q)、主题标签 (K)、内容 (V)。2.计算打分 q i ⋅ k j q_i \cdot k_j qi⋅kj
你拿着自己的兴趣点 (Q),去和每个人的主题标签 (K)做对比,看谁说的话更符合你的关注点。
内积(点积)越大,表示你对这个人的话越感兴趣。3.分数除以 d k \sqrt{d_k} dk
因为不同人发言的信息量 (维度)可能很大,直接比较分数的话可能导致差距过大。
除以 d k \sqrt{d_k} dk 就像是把打分标准统一调整一下,防止某些分数特别大,导致Softmax过于偏激。4.Softmax归一化
你根据打分,把大家的得分变成一个注意力分布,归一成0到1之间,且总和是1。
这个步骤意味着:你决定把多少注意力分配给每个人。5.每个人的V乘以权重
把每个人实际说的话(V)乘上你分给他的注意力权重。6.加总加权后的V
把听到的重要内容加权平均,得到你最终综合理解的信息。这个结果,就是当前位置(单词)的自注意力输出!
自注意力矩阵计算
下图说明了自注意力矩阵计算的方式(可与上面的公式进行对比)。首先,我们将嵌入向量打包成矩阵X。X的每一行包含输入序列中每个单词的嵌入值,即X = [x1, x2, …, xn],其中每个xi是单词i的嵌入向量。
Q矩阵通过X乘以 W i Q ∈ R d model × d k W^Q_i \in \mathbb{R}^{d_{\text{model}} \times d_k} WiQ∈Rdmodel×dk得到,K矩阵通过X乘以 W i K W^K_i WiK得到,V矩阵通过X乘以 W i V W^V_i WiV得到。
- b表示把原本是 d model d_{\text{model}} dmodel 维的输入向量(比如 512 维),映射成更小的 d k d_k dk 维(比如 64 维)向量。
X 是输入矩阵,包含了所有单词的嵌入向量(比如"tennis"、“ball”……每个词对应一个向量)。
但是原始嵌入向量不能直接拿去做注意力计算,因为注意力机制需要专门设计的 查询(Q)、键(K)、值(V) 向量。所以我们需要给 X 分别做三次线性变换(矩阵乘法),得到三个不同的矩阵:Q、K、V。
正如上面的公式所示,最终的值向量会根据查询和键的点积结果进行重新缩放。通过训练,这种机制可以学习如何抑制Value向量的某些部分,同时加强其他部分,使模型能够专注于文本的某些重要部分。在下一篇文章中,我们将尝试通过训练好的BERT模型来可视化这一注意力机制。
6.2 多头注意力(Multi-Head Attention)
两个头比一个头好吗?那八个头呢? 自注意力机制的一个改进版本叫做“多头注意力”,它允许模型在不同的位置或子空间上进行关注。
1.不同的位置
在注意力机制中,“位置”指的是输入序列中每个单词所在的位置。
例如句子:“The cat sat on the mat.” 中,每个单词(The, cat, sat, on, the, mat)都有各自的位置。多头注意力允许模型在不同单词之间建立不同的关注关系。比如第一个头可能专注在 “the” 和 “cat” 的联系,第二个头则关注 “sat” 和 “mat” 的联系。每个头可以在不同位置上分配不同的注意力权重。
2.不同的子空间
“子空间”指的是特征空间(
feature space
)中不同方向或维度的组合。原始输入向量(如512维)包含了词义、语法、情感等混合信息。多头注意力中,每个头通过不同的线性变换(不同的 W Q , W K , W V W^Q, W^K, W^V WQ,WK,WV),将输入映射到不同的子特征空间。这样,每个头可以在不同子空间中学习不同的注意力模式,例如:
- 有的头关注名词之间的关系,
- 有的头关注动词与主语的关系,
- 有的头捕捉情感线索。
简单理解就是:每个头的“观察角度”不同。
在Transformer架构中,有h=8个并行的注意力层或头。这意味着有8个自注意力版本同时运行。
MultiHead ( Q , K , V ) = [ head 1 ; … ; head h ] W O where head i = Attention ( Q W i Q , K W i K , V W i V ) \text{MultiHead}(Q, K, V) = [\text{head}_1; \ldots; \text{head}_h] W^O \\ \text{where head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V) MultiHead(Q,K,V)=[head1;…;headh]WOwhere headi=Attention(QWiQ,KWiK,VWiV)
W i Q W_i^Q WiQ、 W i K W_i^K WiK、 W i V W_i^V WiV 和 W O W^O WO 是需要学习的参数矩阵。其中:
W
i
Q
∈
R
d
model
×
d
k
,
W
i
K
∈
R
d
model
×
d
k
,
W
i
V
∈
R
d
model
×
d
v
,
W
O
∈
R
h
d
v
×
d
model
W_i^Q \in \mathbb{R}^{d_{\text{model}} \times d_k}, \quad W_i^K \in \mathbb{R}^{d_{\text{model}} \times d_k}, \quad W_i^V \in \mathbb{R}^{d_{\text{model}} \times d_v}, \quad W^O \in \mathbb{R}^{hd_v \times d_{\text{model}}}
WiQ∈Rdmodel×dk,WiK∈Rdmodel×dk,WiV∈Rdmodel×dv,WO∈Rhdv×dmodel
多头注意力本质上就是并行地重复多次注意力计算。如果我们用不同的权重矩阵,执行上面介绍的自注意力计算
h
=
8
h = 8
h=8 次,就会得到 8 个不同的
Z
Z
Z 矩阵。
我们可以看到,单个注意力头有一个简单的结构:它对输入的查询、键和值应用一个独特的线性变换,计算每个查询和键之间的注意力分数,然后用这个分数对值加权并求和。多头注意力模块则是并行地应用多个这样的子模块,连接它们的输出后,再应用一个统一的线性变换。
注意,每个子层周围都使用了残差连接,并在之后进行层归一化。用数学表达就是,每个子层的输出为:
LayerNorm ( x + Sublayer ( x ) ) \text{LayerNorm}(x + \text{Sublayer}(x)) LayerNorm(x+Sublayer(x))
其中 Sublayer ( x ) \text{Sublayer}(x) Sublayer(x) 是子层本身实现的函数。
添加残差连接 X ′ X' X′ 到多头注意力层输出并应用层归一化的步骤如下:
1.计算输入矩阵的位置信息嵌入(Positional Embeddings)。
X
′
=
P
E
(
X
)
+
X
X' = PE(X) + X
X′=PE(X)+X
其中 X ′ ∈ R n input × d model X' \in \mathbb{R}^{n_{\text{input}} \times d_{\text{model}}} X′∈Rninput×dmodel。
2.执行多头注意力层,生成输出矩阵
Z
1
,
1
E
Z^E_{1,1}
Z1,1E。
M
u
l
t
i
H
e
a
d
(
Q
,
K
,
V
)
=
Concat
(
head
1
,
.
.
.
,
head
h
)
W
O
MultiHead(Q, K, V) = \text{Concat}(\text{head}_1, ..., \text{head}_h) W^O
MultiHead(Q,K,V)=Concat(head1,...,headh)WO
3.使用残差连接并应用层归一化,得到
Z
1
,
2
E
∈
R
n
input
×
d
model
Z^E_{1,2} \in \mathbb{R}^{n_{\text{input}} \times d_{\text{model}}}
Z1,2E∈Rninput×dmodel。
Z
1
,
2
E
=
LayerNorm
(
X
′
+
Z
1
,
1
E
)
Z^E_{1,2} = \text{LayerNorm}(X' + Z^E_{1,1})
Z1,2E=LayerNorm(X′+Z1,1E)
总结一下,在前面Transformer概述的架构图左边部分的流程如下:
# 步骤1:嵌入向量 + 位置编码
Step1_out = Embedding512 + PositionEncoding512
# 步骤2:多头注意力输出 + 残差连接 + 层归一化
Step2_out = layer_normalization(multihead_attention(Step1_out) + Step1_out)
# 步骤3:前馈神经网络输出 + 残差连接 + 层归一化
Step3_out = layer_normalization(FFN(Step2_out) + Step2_out)
# 编码器最终输出
out_enc = Step3_out
7 Transformer解码器
解码器的工作方式与编码器类似,但它是从左到右逐词生成输出。解码器不仅关注之前生成的单词,还关注编码器输出的最终表示(输入句子经过编码器处理后的向量表示)。
- 解码器在生成目标语言单词时,一方面看自己已经生成的内容,另一方面也要参考源语言的信息,才能准确地继续生成。
在编码阶段结束后,进入解码阶段。解码器的结构与编码器非常相似。除了每个编码器层中的两个子层外,解码器还插入了第三个子层,该子层执行对编码器输出的多头注意力机制。与编码器一样,解码器的每个子层周围都有残差连接,并且都进行层归一化。
7.1 掩码多头注意力(Masked Multi-Head Attention)
你可能注意到了在Transformer概述的架构图中,解码器中有一个Masked Multi-Head Attention
(掩码多头注意力)层。解码器中的自注意力层允许每个位置关注到解码器中该位置及之前的所有位置。为了保持自回归性质,我们需要阻止信息向左流动。
掩码的作用是将未来单词的信息屏蔽掉。通过这样做,可以确保模型在生成当前位置的单词时,只能依赖于之前已生成的单词,而不能看到未来的单词。简单来说,掩码机制是将源单词之后的单词的相似度置零,只考虑之前单词的相似度。
想象你在写一句话,比如:“I am going to the …”。你还没有写完,但需要决定下一个单词是什么(比如 park)。但这个时候,你只能基于你已经写出的单词(I am going to the)来做判断,不能提前偷看到答案!
而在Transformer解码器中,如果不特别处理,模型是可以直接看到未来所有单词的(即训练阶段模型是可以拿到完整的目标句子)。
如果什么都不管,让模型自由看完整目标,它就会直接利用未来的信息,相当于作弊。为了模拟实际推理的情况(只能一步步生成),必须加 Mask。
8 可视化
下面的动画展示了Transformer在机器翻译中的应用过程。Transformer首先为每个单词生成初始表示(嵌入),用空心圆表示。然后通过自注意力机制,聚合其他单词的信息,为每个单词生成新的表示,用实心球表示。这一步骤会对所有单词并行进行多次,逐步生成新的表示。
9 总结
Transformer通过去除循环神经网络(RNN),革新了神经机器翻译(NMT)领域。注意力机制是Transformer架构的核心,支持模型灵活地捕捉输入序列中的重要信息。Transformer编码器默认由六层堆叠的编码器块组成,每个编码器块内部包含两个子层:自注意力机制和前馈神经网络(FFN)。嵌入算法用于将单词转换成向量表示,而由于缺少RNN结构,模型需要引入位置编码来保留序列顺序信息。多头注意力机制允许模型在不同的子空间中并行关注不同特征,从而丰富表示能力。
在解码器部分,结构包含三个子层,分别是掩码自注意力、编码器-解码器注意力和前馈网络。解码器不仅关注自己已生成的单词,还要参考编码器输出的最终表示,同时使用掩码多头注意力机制,防止在生成过程中提前看到未来单词的信息,保持自回归生成的特性。