背景

此前的模型(如ByteNet, ConvS2S)使用 CNN 并行计算,但处理长距离依赖关系的能力随着位置间距的增加而减弱(线性或对数增长)。

Transformer 利用自注意力机制,使得关联任意两个位置所需的操作次数降至常数级,极大改善了处理长距离依赖的能力;引入 Multi-Head Attention 抵消因注意力位置平均化可能导致的"有效分辨率"下降问题。

Transformer 是第一个只依赖 Self-Attnetion 来实现 Encoder-Decoder 架构的模型。

模型架构

在 GPT 之前,大部分神经序列转换模型都采用 Encoder-Decoder 结构,Transformer 模型也不例外。Encoder 将输入的符号序列\((x_1,...,x_n)\)映射为一个连续序列\(z=(z_1,...,z_n)\);得到编码后的序列\(z\),Decoder 逐个元素生成输出序列\((y_1,...,y_m)\)。Decoder 每一步输出是 auto-regressive 的,将当前 step 的输出和输入拼接,作为下一个 step 的输入。

Transformer 架构由 Encoder 和 Decoder 两个部分组成:其中 Encoder 和 Decoder 都是由 N=6 个相同的层堆叠而成

Encoder

Encoder 结构由 N=6 个相同的 encoder block 堆叠而成,每一层( layer)主要有两个子层(sub-layers): 第一个子层是多头注意力机制(Multi-Head Attention),第二个是简单的位置全连接前馈网络(Positionwise Feed Forward)。

Tensor 形状变化

Encoder 的输入是待推理的句子序列X: [batch_size, seq_len, d_model]。其中:d_model为 embedding vector 的维度。

操作层级具体操作操作结果张量形状
输入层X(token index)[batch_size, seq_len]
输入层X = Embedding(X)[batch_size, seq_len, d_model]
输入层X = X + PositionalEncoding(PE)[batch_size, seq_len, d_model]
编码器层内部X_attn = MultiHeadAttention(X)[batch_size, seq_len, d_model]
编码器层内部X = X + X_attn(残差连接)[batch_size, seq_len, d_model]
编码器层内部X = LayerNorm(X)[batch_size, seq_len, d_model]
编码器层内部X_ffn = FeedForwardNetwork(X)[batch_size, seq_len, d_model]
编码器层内部X = X + X_ffn(残差连接)[batch_size, seq_len, d_model]
编码器层内部X = LayerNorm(X)[batch_size, seq_len, d_model]
编码器层X = EncoderLayer(X)[batch_size, seq_len, d_model]
编码器X = Encoder(X) = 6 × EncoderLayer(X)[batch_size, seq_len, d_model]

Add & Norm

Add & Norm 层由 Add 和 Norm 两部分组成:

  1. Add 指 \(x + \mathrm{Sublayer}(x)\),将多头注意力机制产生的新数据和最开始输入的原始数据合并在一起(采用简单加法),是一种残差连接;
  2. Norm 是 Layer Normalization,对每个样本在特征维度上进行归一化,使得其均值为0,方差为1。

Add & Norm 层计算过程用数学公式可表达为: \(\mathrm{LayerNorm}(x + \mathrm{Sublayer}(x))\)

为什么需要归一化(Normalization)?

  1. 解决梯度消失,加速收敛:网络加深时,层间输入分布易进入激活函数(如Sigmoid、Tanh)的饱和区,导致梯度变小甚至消失;归一化将输入重新中心化为均值为0、方差为1的分布,使其落在激活函数的敏感(线性)区域,从而稳定和加速训练。
  2. 满足独立同分布假设:机器学习模型有效的一个基本假设是训练数据和测试数据来自相同的分布;对网络中间层的输出进行归一化,可以稳定其分布,使后续层的学习更有效,提升模型的泛化能力。

BatchNorm 是“跨样本归一化”,而 LayerNorm 是“跨特征归一化”;CV领域多用 BatchNorm,NLP领域多用 LayerNorm。

LayerNorm 基于公式: \[ y=\frac{x-E[x]}{\sqrt{Var[x]+\epsilon}}*\gamma \] LayerNorm 在 Pytorch 中的表示:[LayerNorm

](https://docs.pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)

1
2
3
4
5
6
7
class torch.nn.LayerNorm(normalized_shape, eps=1e-05, elementwise_affine=True, bias=True, device=None, dtype=None)[source]
"""
1. normalized_shape: 要进行归一化的维度形状,可以是int(最后一维)或list/tuple(表示要归一化的形状,从最后一维开始);
2. eps: 为了数值稳定性添加到分母的小常数,防止除零错误;
3. elementwise_affine: 是否使用可学习的逐元素仿射参数(gamma和beta);
4. bias: 如果为False,则不会学习加性偏置(即beta为0,仅当elementwise_affine为True时有效)
"""

例子:

  • NLP例子:归一化维度为embedding_dim,即对每个token的embedding向量做归一化;
  • 图像例子:归一化维度为[C, H, W],即对每个通道和空间位置做归一化。

LayerNorm 层的 Pytorch 实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class LayerNorm(nn.Module):
"Construct a layernorm module"
def __init__(self, d_model, eps=1e-12):
super(LayerNorm, self).__init__()
self.gamma = nn.Parameter(torch.ones(d_model))
self.beta = nn.Parameter(torch.zeros(d_model))
self.eps = eps

def forward(self, x):
mean = x.mean(-1, keepdim=True) # '-1' means last dimension.
var = x.var(-1, keepdim=True)

out = (x - mean) / torch.sqrt(var + self.eps)
out = self.gamma * out + self.beta

return out

# NLP Example
batch, sentence_length, embedding_dim = 20, 5, 10
embedding = torch.randn(batch, sentence_length, embedding_dim)

Feed Forward

Feed Forward 层全称是 Position-wise Feed-Forward Networks,其本质是一个两层的全连接层,第一层的激活函数为 Relu;第二层不使用激活函数,计算过程用数学公式可表达为:\(FFN(x)=max(0,XW_1+b_1)W_2+b_2\).

除了使用两个全连接层来完成线性变换,另外一种方式是使用 kernal_size = 1 的两个\(1\times 1\)的卷积层,输入输出维度不变,都是 512,中间维度是 2048。 ​
PositionwiseFeedForward 层的 Pytorch 实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PositionwiseFeedForward(nn.Module):
"Implements FFN equation."
def __init__(self, d_model, d_diff, drop_prob=0.1):
super(PositionwiseFeedForward, self).__init__()
self.fc1 = nn.Linear(d_model, d_diff)
self.fc2 = nn.Linear(d_diff, d_model)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(drop_prob)

def forward(self, x):
x = self.fc1(x)
x = self.relu(x)
x = self.dropout(x)
x = self.fc2(x)

return x

实现

Encoder

Encoder类是编码器的实现:forward()函数返回的是编码之后的向量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 使用Encoder类来实现编码器,它继承了 nn.Module 类
class Encoder(nn.Module):
"Core encoder is a stack of N layers"
# Encoder的核心部分是 N 个 EncoderLayer 堆叠而成的栈

def __init__(self, layer, N):
"""
初始化方法接受两个参数,分别是:
layer: 要堆叠的编码器层,对应 EncoderLayer 类
N: 堆叠多少次,即 EncoderLayer 的数量
"""
super(Encoder, self).__init__()
# 使用clone()函数将layer克隆N份,并将这些层放在self.layers中
self.layers = clones(layer, N)
# 创建一个LayerNorm层,并赋值给self.norm,这是“Add & Norm”中的“Norm”部分
self.norm = LayerNorm(layer.size)

def forward(self, x, mask):
"Pass the input (and mask) through each layer in turn."
"""
前向传播函数接受两个参数,分别是:
x: 输入数据,即经过 Embedding 处理和添加位置编码后的输入。形状为(batch_size, seq_len,embedding_dim)
mask:掩码
"""

# 使用 EncoderLayer 对输入 x 进行逐层处理,每次都会得到一个新的 x,然后将 x 作为下一层的输入
# 此循环的过程相当于输出的 x 经过了 N 个编码器层的逐步处理
for layer in self.layers: # 遍历 self.layers 中的每一个编码器层
x = layer(x, mask) # 将 x 和 mask 传递给当前编码器层,编码器层进行运算,并将输出结果赋值给 x
return self.norm(x) # 对最终的输出 x 应用层归一化,并将结果返回
EncoderLayer

EncoderLayer类是编码器层的实现;作为编码器的组成单元, 每个 EncoderLayer 完成一次对输入的特征提取过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class EncoderLayer(nn.Module):
"Encoder is made up of self-attn and feed forward (defined below)"

def __init__(self, size, self_attn, feed_forward, dropout):
"""
初始化函数接受如下参数:
size: 对应 d_model,即 word embedding 维度的大小
self_attn: 多头自注意力模块
feed_forward: FFN 层
dropout: 置0比率
"""
super(EncoderLayer, self).__init__()
self.self_attn = self_attn
self.feed_forward = feed_forward
# 创建两个具有相同参数的 SublayerConnection 实例,一个用于自注意力,一个用于FFN
self.sublayer = clones(SublayerConnection(size, dropout), 2)
self.size = size

def forward(self, x, mask):
"Follow Figure 1 (left) for connections."
"""
前向函数的参数如下:
x: 源语句的嵌入向量或者前一个编码器的输出
mask: 掩码
"""

# 顺序运行两个函数:self_attn(),self.sublayer[0]()
# 1. 对输入x进行自注意力操作
# 2. 将自注意力结果传递给第一个 SublayerConnection 实例(SublayerConnection 实例内部做残差连接和层归一化)
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))

# 用上面计算结果来顺序运行两个函数:self.feed_forward()和self.sublayer[1]
# 1. FFN 进行运算
# 2. 将 FFN 计算结果传递给第一个 SublayerConnection 实例
return self.sublayer[1](x, self.feed_forward)
SublayerConnection

paper 的实现(post LN):\(\mathrm{LayerNorm}(x + \mathrm{Sublayer}(x))\)

SublayerConnection的实现(pre LN):\(x+\mathrm{LayerNorm}(\mathrm{Sublayer}(x))\)

1
2
3
4
5
6
7
8
9
10
11
12
13
class SublayerConnection(nn.Module):
"""
A residual connection followed by a layer norm.
Note for code simplicity the norm is first as opposed to last.
"""
def __init__(self, size, dropout):
super(SublayerConnection, self).__init__()
self.norm = LayerNorm(size)
self.dropout = nn.Dropout(dropout)

def forward(self, x, sublayer):
"Apply residual connection to any sublayer with the same size."
return x + self.dropout(sublayer(self.norm(x)))

Decoder

Decoder 组件也是由 N=6 个相同的 Decoder block 堆叠而成。Decoder block 与 Encoder block 相似,但是存在一些区别:

包含两个 Multi-Head Attention 层。

  • 第一个 Multi-Head Attention 层采用了 Masked 操作;
  • 第二个 Multi-Head Attention 层的 K, V 矩阵使用 Encoder 的编码信息矩阵 C 进行计算,而 Q 使用上一个 Decoder block 的输出计算。这样做的好处是在 Decoder 的时候,每一位单词都可以利用到 Encoder 所有单词的信息 (这些信息无需 Mask)

Tensor 形状变化

解码器的输入是一个长度变化的张量Y:[batch_size, seq_len, d_model],初始时,这个张量中,每个矩阵只有1行(即seq_len=1),即开始字符的编码。

操作步骤操作结果张量的形状
输入层Y(token index)[batch_size, seq_len]
输入层Y = embedding(Y)[batch_size, seq_len, d_model]
输入层Y = Y + PE[batch_size, seq_len, d_model]
解码器层内部Y = Masked-MHA(Y)[batch_size, seq_len, d_model]
解码器层内部Y = LayerNorm(Y + Masked-MHA(Y))[batch_size, seq_len, d_model]
解码器层内部Y = Cross-MHA(Y, M, M)[batch_size, seq_len, d_model]
解码器层内部Y = LayerNorm(Y + Cross-MHA(Y, M, M))[batch_size, seq_len, d_model]
解码器层内部Y = FFN(Y)[batch_size, seq_len, d_model]
解码器层内部Y = LayerNorm(Y + FFN(Y))[batch_size, seq_len, d_model]
解码器层Y = DecoderLayer(Y)[batch_size, seq_len, d_model]
解码器Y = Decoder(Y) = N × DecoderLayer(Y)[batch_size, seq_len, d_model]
输出层logits = Linear(Y)[batch_size, seq_len, d_voc]
输出层prob = softmax(logits)[batch_size, seq_len, d_voc]

实现

Decoder

Decoder类是解码器的实现,是 N 个解码层堆叠的栈:Decoder类根据当前翻译过的第i个单词,翻译下一个单词(i+1);在解码过程中,翻译到第i+1个单词的时候,需要通过 Mask 操作遮盖住(i+1)之后的单词。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Decoder(nn.Module):
"Generic N layer decoder with masking."

def __init__(self, layer, N):
"""
初始化函数有两个参数。layer 对应下面的 DecoderLayer,是要堆叠的解码器层;N是解码器层的个数
"""
super(Decoder, self).__init__()
self.layers = clones(layer, N) # 使用 clones() 函数克隆 N 个 DecoderLayer
self.norm = LayerNorm(layer.size) # 层归一化的实例

def forward(self, x, memory, src_mask, tgt_mask):
"""
前向传播函数有四个参数:
x: 目标数据的嵌入表示,x的形状是(batch_size, seq_len, d_model),在预测时,x 的词数会不断增加,比如第一次是(1,1,512),第二次是(1,2,512),以此类推
memory: 编码器的输出
src_mask: 源序列的掩码
tgt_mask: 目标序列的掩码
"""
# 实现多个编码层堆叠起来的效果,并完成整个前向传播过程
for layer in self.layers: # 让x逐次在每个解码器层流通,进行处理
x = layer(x, memory, src_mask, tgt_mask)
# 对多个编码层的输出结果进行层归一化并返回最终的结果
return self.norm(x)
DecoderLayer

DecoderLayer类是解码器层的实现:作为解码器的组成单元,每个解码器层根据给定的输入向目标方向进行特征提取操作,即解码。

DecoderLayerEncoderLayer的内部相似,区别在于:EncoderLayer只有一个多头自注意力模块,而DecoderLayer有两个多头自注意力模块(比EncoderLayer多了一个src_attn成员变量;self_attnsrc_attn的实现完全一样,只不过使用的Query,Key 和 Value 的输入不同)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class DecoderLayer(nn.Module): 
"Decoder is made of self-attn, src-attn, and feed forward (defined below)"

def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
super(DecoderLayer, self).__init__()
self.size = size
self.self_attn = self_attn
self.src_attn = src_attn
self.feed_forward = feed_forward
# 创建三个 SublayerConnection 类实例,分别对应 self_attn, src_attn 和 feed_forward
self.sublayer = clones(SublayerConnection(size, dropout), 3)

def forward(self, x, memory, src_mask, tgt_mask):
"""
前向传播函数有四个参数:
x: 目标数据的嵌入表示,x的形状是(batch_size, seq_len, d_model),在预测时,x的词数会不断增加,比如第一次是(1,1,512),第二次是(1,2,512),以此类推。x可能是上一层的输出或者是整个解码器的输出
memory: 编码器的输出
src_mask: 源序列的掩码
tgt_mask: 目标序列的掩码
"""
m = memory
# 第一个子层执行掩码多头自注意力计算。相当于顺序运行两个函数:self_attn()和 self.sublayer[0]()。这里的 Q、K、V 都是 x。tgt_mask 的作用是防止预测时看到未来的单词。
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))
# 第二个子层执行交叉注意力操作,此时 Q 是输入 x,K 和 V 是编码器输出的 m。src_mask 的作用是遮挡填充符号,避免无意义的计算,提升模型效果和训练速度。(此刻需要注意的是,两个注意力计算的mask参数不同,上一个是tgt_mask,此处是src_mask)
x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))
# 第三个子层是 FFN ,经过它的处理后返回结果
return self.sublayer[2](x, self.feed_forward)

参考

The Annotated Transformer

transformer论文解读

transformer 模型结构详解及实现

探秘Transformer系列之(4)--- 编码器 & 解码器

[大语言模型中的归一化技术:LayerNorm与RMSNorm的深入研究

](https://mp.weixin.qq.com/s/IN_94xagIYOqWsR7KWLivQ)

Layer Normalization

BatchNorm and LayerNorm