一、参考资料
The Illustrated Transformer 图解Transformer(完整版) Attention Is All You Need: The Core Idea of the Transformer transformer 总结(超详细-初版) Transformer各层网络结构详解!面试必备!(附代码实现) 大语言模型核心技术-Transformer 详解
论文:Attention Is All You Need
二、Transformer相关介绍
1. RNN的缺点
RNN由于其顺序结构,训练速度常常受到限制。
RNN系列的模型,并行计算能力很差 。RNN并行计算的问题就出在这里,因为 T 时刻的计算依赖 T-1 时刻的隐层计算结果,而 T-1 时刻的计算依赖 T-2 时刻的隐层计算结果,如此下去就形成了所谓的序列依赖关系。
2. seq2seq的缺点
seq2seq最大的问题在于,将Encoder端的所有信息压缩到一个固定长度的向量中 ,并将其作为Decoder端首个隐藏状态的输入,来预测Decoder端第一个单词(token)的隐藏状态。在输入序列比较长的时候,这样做显然会损失Encoder端的很多信息,而且这样一股脑的把该固定向量送入Decoder端,Decoder端不能够关注到其想要关注的信息。
3. Transformer的优势和缺点
3.1 优势
表征能力 。Transformer让源序列和目标序列“自关联”起来,源序列和目标序列自身的embedding所蕴含的信息更加丰富,而且后续的FFN(前馈神经网络)层也增强了模型的表达能力。
特征提取能力 。Transformer的特征提取能力 比RNN系列的模型要好。具体实验对比可以参考:放弃幻想,全面拥抱Transformer:自然语言处理三大特征抽取器(CNN/RNN/TF)比较 。
并行计算能力 。Transformer并行计算的能力 是远远超过seq2seq系列的模型。
语义结构 。Transformer擅长捕捉文本和图像中的语义结构,它们比其他技术更好地捕捉文本甚至图像中的语义结构。Transformer的泛化能力和Diffusion Model(扩散模型)的细节保持能力的结合,提供了一种生成细粒度的高度详细图像的能力,同时保留图像中的语义结构。
泛化能力 。在视觉应用中,Transformer表现出泛化 和自适应 的优势,使其适合通用学习 。
3.2 缺点
Transformer需要大量的数据 ,并且在许多视觉领域也面临着性能 方面的问题。
4. Transformer输入输出维度变换
Transformer的整体结构,大致可分为:Input、Encoder、Decoder、Output。
输入 :假设输入序列长度为T,则Encoder输入的维度为 [batch_size, T]
,经过embedding层、position encoding等流程后,生成 [batch_size, T, D]
的数据,D表示模型隐层维度;
Encoder :假设输入序列长度为T,则Encoder输入的维度为[batch_size, T]
,经过embedding层、position encoding等流程后,生成[batch_size, T, D]的数据,D表示模型隐层维度;
Decoder :Decoder的输入也经过类似的变换得到 [batch_size, T', D]
,T’是Decoder输入长度。之后会进入多个相同结果的模块,每个模块包括:
Self Multi-head Attention
,表示Decoder序列上的元素内部做Attention,和Encoder是一样的;
Add&Norm
;
Cross Multi-head Attention
,是Decoder每个位置和Encoder各个位置进行Attention,类似于传统的seq2seq中的Attention,用来进行Decoder和Encoder的对齐;
Add&Norm
;
Feed Forward
;
Add&Norm
。
5. Scaled Dot-Product Attention
通过 query 和 key 的相似性程度来确定 value 的权重分布的方法,被称为 Scaled Dot-Product Attention
。
Transformer为什么采用Scaled Dot-Product Attention
?
目前常见的注意力机制有:加性注意力(additive attention)与 点乘注意力(dot-product attention) 。两者在理论复杂度相近,但由于矩阵乘法的代码已经比较优化,所以点乘注意力在实际中,不论是计算还是存储上都更有效率。
点乘注意力增加了1个缩放因子(
1
d
k
\frac1{\sqrt{d_k}}
d k
1 )。增加的原因是:当
d
k
\sqrt{d_k}
d k
较小时,两种注意力机制的表现情况类似;而
d
k
\sqrt{d_k}
d k
增大时,点乘注意力的表现变差,认为是由于点乘后的值过大,导致softmax函数趋近于边缘,梯度较小。
6. 机器翻译
我们以一个法语翻译成英语的例子,来讲解Transformer进行机器翻译的的过程:
6.1 Step 1
首先,输入句子的Embedding(词嵌入)会被传递到编码器块 ,该块具有多头注意力机制。将词嵌入和权重矩阵计算出的Q,K和V值输入到这个模块,然后生成一个可以传递到前馈神经网络的矩阵。在Transformer的论文中,这个编码块会被重复N次,一般N的值为6。
6.2 Step 2
然后,编码器的输出会被输入到解码器块 。解码器的任务是输出英文翻译。解码器块的每一步都会输入已经生成的翻译的前几个单词。刚开始时,翻译会以一个开始句子的标记开始。这个标记被输入到多头注意力块,并用于计算这个多头注意力块的Q,K和V。这个块的输出会用于生成下一个多头注意力块的Q矩阵,而编码器的输出会用于生成K和V 。
6.3 Step 3
解码器块的输出被输入到前馈神经网络 ,该网络的任务是预测句子中的下一个单词。
7. Transformer在CV领域的应用
2020 年 5 月,Facebook AI 实验室推出Detection Transformer(DETR) ,用于目标检测和全景分割。这是第一个将 Transformer 成功整合为检测 pipeline 中心构建块的目标检测框架, 在大型目标上的检测性能要优于 Faster R-CNN。
2020 年 10 月,谷歌提出了Vision Transformer (ViT),可以直接利用 Transformer 对图像进行分类,而不需要卷积网络。ViT 模型取得了与当前最优卷积网络相媲美的结果,但其训练所需的计算资源大大减少。
三、Transformer结构
全网最强ViT (Vision Transformer)原理及代码解析
Transformer的结构和Attention模型一样,采用 encoer-decoder 架构。但其结构相比于Attention更加复杂,论文中encoder层由6个encoder堆叠在一起,decoder层也一样。
0. 总体结构
Transformer是由一堆encoder和decoder形成的,Encoder和Decoder均由多头注意力层和全连接前馈网络 组成,网络的高层结构如下:
Encoder 由N个编码器块(Encoder Block)串联组成,每个编码器块包含:
一个多头注意力 (Multi-Head Attention)层;
一个前馈全连接神经网络 (Feed Forward Neural Network);
Decoder 也由N个解码器块(Decoder Block)串联组成,每个解码器块包含:
一个多头注意力层;
一个对Encoder输出的多头注意力层;
一个前馈全连接神经网络;
1. Transformer 输入
Transformer 中单词的输入表示 x 由 Embedding(词嵌入)
和 Positional Encoding位置编码
相加得到。
1.1 Embedding
Embedding 原理的详细介绍,请查阅博客:【Transformer系列】深入浅出理解Embedding(词嵌入)
Embedding 有很多种方式可以获取,例如可以采用 Word2Vec、Glove 等算法预训练得到,也可以在 Transformer 中训练得到。
1.2 Positional Embedding
Positional Embedding 原理的详细介绍,请查阅博客:【Transformer系列】深入浅出理解Positional Encoding位置编码
Transformer 中除了 Embedding(词嵌入)
,还需要使用 Positional Embedding
表示单词出现在句子中的位置。因为 Transformer 不采用 RNN 的结构,而是使用全局信息 ,不能利用单词的顺序信息,而这部分信息对于 NLP 来说非常重要。所以 Transformer 中使用 Positional Embedding
保存单词在序列中的相对或绝对位置。
2. Encoder编码器
2.0 Encoder结构
Encoder 结构由
N
=
6
\text{N} = 6
N = 6 个相同的 encoder block
堆叠而成,Encoder block
由 Multi-Head Attention
、Add&Norm
、Feed Forward
、Add & Norm
层组成。每个 encoder block
的输入矩阵和输出矩阵维度是一样的,每一层( layer)主要有两个子层(sub-layers):
第一个子层是多头注意力机制 (Multi-Head Attention);
第二个是简单的位置全连接前馈网络 (Positionwise Feed Forward)。
2.1 Encoder编码过程
首先,模型需要对输入的数据进行一个embedding(词嵌入)操作,也可以理解为类似w2c的操作,enmbedding结束之后,输入到encoder层,self-attention处理完数据后把数据送给前馈神经网络,前馈神经网络的计算可以并行,得到的输出会输入到下一个encoder。
2.2 Embedding
Embedding原理的详细介绍,请查阅博客:【Transformer系列】深入浅出理解Embedding(词嵌入)
2.3 Positional Encoding
Positional Encoding原理的详细介绍,请查阅博客:【Transformer系列】深入浅出理解Positional Encoding位置编码
对输入进行位置编码,以便在翻译中考虑单词在句子中的位置。使用一组正弦和余弦方程来实现。
Attention中缺少一种解释输入序列中单词顺序的方法,它跟序列模型(RNN)还不一样。为了处理这个问题,Transformer对输入进行位置编码,以便在翻译中考虑单词在句子中的位置。具体来说,Transformer给encoder层和decoder层的输入添加了一个额外的向量Positional Encoding,维度和embedding的维度一样,这个向量采用了一种很独特的方法来让模型学习到这个值,这个向量能决定当前词的位置,或者说在一个句子中不同的词之间的距离。这个位置向量的具体计算方法有很多种,Transformer论文中使用一组正弦和余弦方程来实现,计算方法如下:
P
E
(
p
o
s
,
2
i
)
=
s
i
n
(
p
o
s
1000
0
2
i
d
m
o
d
e
l
)
PE(pos,2i)=sin(\frac{pos}{10000^{\frac{2i}{d_{model}}}})
PE ( p os , 2 i ) = s in ( 1000 0 d m o d e l 2 i p os )
P
E
(
p
o
s
,
2
i
+
1
)
=
c
o
s
(
p
o
s
1000
0
2
i
d
m
o
d
e
l
)
PE(pos,2i+1)=cos(\frac{pos}{10000^{\frac{2i}{d_{model}}}})
PE ( p os , 2 i + 1 ) = cos ( 1000 0 d m o d e l 2 i p os )
其中pos是指当前词在句子中的位置,i是指向量中每个值的index,可以看出,在偶数位置,使用正弦编码,在奇数位置,使用余弦编码 。
最后把这个Positional Encoding与embedding的值相加,作为输入送到下一层。
2.4 Self-Attention
Self-Attention原理的详细介绍,请查阅博客:【Transformer系列】深入浅出理解Attention和Self-Attention机制
2.5 Multi-Headed Attention
Multi-Headed Attention原理的详细介绍,请查阅博客:【Transformer系列】深入浅出理解Attention和Self-Attention机制
Multi-Headed Attention
是在Self-Attention
基础上改进的,也就是在产生q,k,v的时候,对q,k,v进行了切分,分别分成了num_heads份,对每一份分别进行self-attention的操作,最后再拼接起来,这样在一定程度上进行了参数隔离 。
Multi-Headed Attention
不仅仅只初始化一组Q、K、V的矩阵,而是初始化多组,Transformer是使用了8组 ,所以最后得到的结果是8个矩阵。
Multi-head Attention
的示意图如下:
2.6 Add & Norm模块
在Transformer中,每一个子层(self-attetion,Feed Forward Neural Network)之后都会接一个残缺模块,并且有一个Layer normalization。 Add & Norm
层由 Add
和 Norm
两部分组成。这里的 Add
指 X + MultiHeadAttention(X),是一种残差连接。Norm
是 Layer Normalization
。Add & Norm
层计算过程用数学公式可表达为:
Layer Norm
(
X
+
MultiHeadAttention
(
X
)
)
\text{Layer Norm}(X+\text{MultiHeadAttention}(X))
Layer Norm ( X + MultiHeadAttention ( X ))
其中,Add代表Residual Connection残差连接,是为了解决多层神经网络训练困难的问题。通过将前一层的信息无差的传递到下一层,可以有效的仅关注差异部分 ,这一方法在图像处理结果如ResNet等中常常用到。
Layer Norm 是一种常用的神经网络归一化技术,可以使得模型训练更加稳定,收敛更快。它的主要作用是对每个样本在特征维度上进行归一化,减少了不同特征之间的依赖关系,提高了模型的泛化能力,其原理可参考论文:Layer Normalization 。Layer Norm 层的计算可视化如下图所示:
2.7 Feed Forward
Feed Forward层全称 Feed Forward Neural Network
(简称FFN),即前馈神经网络,其本质是一个两层的全连接层,第一层的激活函数为 Relu,第二层不使用激活函数,计算过程用数学公式可表达为:
FFN
(
X
)
=
max
(
0
,
X
W
1
+
b
1
)
W
2
+
b
2
\operatorname{FFN}(X)=\max(0,XW_1+b_1)W_2+b_2
FFN ( X ) = max ( 0 , X W 1 + b 1 ) W 2 + b 2
除了使用两个全连接层来完成线性变换,另外一种方式是使用 kernal_size = 1 的两个
1
×
1
1\times 1
1 × 1 卷积层,输入输出维度不变,都是 512,中间维度是 2048。
Feed Forward没法输入 8 个矩阵,这该怎么办呢?所以我们需要一种方式,把 8 个矩阵降为 1 个。首先,我们把 8 个矩阵连在一起,这样会得到一个大矩阵,再随机初始化一个权重矩阵,并与这个组合好的大矩阵相乘,得到一个最终的矩阵。
3. Decoder编码器
根据上面的总体结构图可以看出,Decoder的结构与Encoder结构大同小异,先添加一个Positional Encoding位置向量,再接一个masked mutil-head attetion,这里的mask是Transformer一个很关键的技术,本章节对其进行详细介绍。其余的层结构与Encoder一样,请参考Encoder层结构。
3.1 Decoder结构
3.2 masked mutil-head attetion
mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果 。Transformer 模型里面涉及两种 mask,分别是 padding mask
和 sequence mask
。其中,padding mask
在所有的 scaled dot-product attention
里面都需要用到,而 sequence mask
只有在 decoder 的 self-attention
里面用到。
通过 query 和 key 的相似性程度来确定 value 的权重分布的方法,被称为 scaled dot-product attention
。
3.2.1 padding mask
什么是 padding mask 呢?因为每个批次输入序列长度是不一样的,也就是说,我们要对输入序列进行对齐。具体来说,就是给较短的序列后面填充 0。如果输入的序列太长,则是截取左边的内容,把多余的直接舍弃。对于太长的输入序列,这些填充的位置,是没什么意义的,attention机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。
具体的做法是,把这些位置的值加上一个非常大的负数(负无穷) ,这样经过 softmax,这些位置的概率就会接近0。
padding mask
实际上是一个张量,每个值都是一个Boolean,值为 false 的地方就是我们要进行处理的地方。
3.2.2 Sequence mask
sequence mask
是为了使得 decoder 不能看见未来的信息。也就是对于一个序列,在 time_step 为 t 的时刻,解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此需要想一个办法,把 t 之后的信息给隐藏起来。
那么具体怎么做呢?也很简单:产生一个上三角矩阵,上三角的值全为0。把这个矩阵作用在每一个序列上,就可以达到我们的目的 。
3.3 Output层
当decoder层全部执行完毕后,怎么把得到的向量映射为我们需要的词呢,很简单,只需要在结尾再添加一个全连接层 和 softmax层 。假如词典有1w个词,那最终softmax会输入1w个词的概率,概率值最大的对应的词就是最终的结果。
4. 动态流程图
编码器阶段,编码器通过处理输入序列开启工作。顶端编码器的输出之后会转化为一个包含向量K(键向量)和V(值向量)的注意力向量集 ,这是并行化操作 。这些向量将被每个解码器用于自身的“编码-解码注意力层”,而这些层可以帮助解码器关注输入序列的相关性(重要程度):
在完成编码阶段后,则开始解码阶段。解码阶段的每个步骤都会输出一个输出序列的元素(例如,English翻译成German)。
接下来的步骤,重复这个过程,直到到达一个特殊的终止符号,它表示Transformer的解码器已经完成了它的输出。每个步骤的输出在下一个time_step(时间步)被提供给底端解码器,这些解码器会输出它们的解码结果 。
5. Transformer总结
Transformer 与 RNN 不同,可以较好地并行训练 。
Transformer 本身是不能利用单词的顺序信息的,因此需要在输入中添加 Positional Encoding
位置编码,否则 Transformer 就是一个词袋模型了。
Transformer 的重点是 Self-Attention 结构,其中用到的 Q, K, V矩阵通过线性变换得到。
Transformer 中 Multi-Head Attention 中有多个 Self-Attention,可以捕获单词之间多种维度上的相关系数 Attention Score
。
四、相关经验
A TensorFlow Implementation of Attention Is All You Need Transformer解析与tensorflow代码解读 熬了一晚上,我从零实现了Transformer模型,把代码讲给你听 零基础理解为什么是Transformer?什么是Transformer?(深入浅出 通俗理解Transformer及其pytorch源码)
1. 开源项目
Transformers NLP_ability ML-NLP
2. Self-Attention代码实现
class ScaleDotProductAttention ( nn. Module) :
def __init__ ( self, ) :
super ( ScaleDotProductAttention, self) . __init__( )
self. softmax = nn. Softmax( dim = - 1 )
def forward ( self, Q, K, V, mask= None ) :
K_T = K. transpose( - 1 , - 2 ) # 计算矩阵 K 的转置
d_k = Q. size( - 1 )
# 1, 计算 Q, K^T 矩阵的点积,再除以 sqrt(d_k) 得到注意力分数矩阵
scores = torch. matmul( Q, K_T) / math. sqrt( d_k)
# 2, 如果有掩码,则将注意力分数矩阵中对应掩码位置的值设为负无穷大
if mask is not None :
scores = scores. masked_fill( mask == 0 , - 1e9 )
# 3, 对注意力分数矩阵按照最后一个维度进行 softmax 操作,得到注意力权重矩阵,值范围为 [0, 1]
attn_weights = self. softmax( scores)
# 4, 将注意力权重矩阵乘以 V,得到最终的输出矩阵
output = torch. matmul( attn_weights, V)
return output, attn_weights
# 创建 Q、K、V 三个张量
Q = torch. randn( 5 , 10 , 64 ) # (batch_size, sequence_length, d_k)
K = torch. randn( 5 , 10 , 64 ) # (batch_size, sequence_length, d_k)
V = torch. randn( 5 , 10 , 64 ) # (batch_size, sequence_length, d_k)
# 创建 ScaleDotProductAttention 层
attention = ScaleDotProductAttention( )
# 将 Q、K、V 三个张量传递给 ScaleDotProductAttention 层进行计算
output, attn_weights = attention( Q, K, V)
# 打印输出矩阵和注意力权重矩阵的形状
print ( f"ScaleDotProductAttention output shape: { output. shape} " ) # torch.Size([5, 10, 64])
print ( f"attn_weights shape: { attn_weights. shape} " ) # torch.Size([5, 10, 10])
3. Multi-head Attention代码实现
class MultiHeadAttention ( nn. Module) :
"""Multi-Head Attention Layer
Args:
d_model: Dimensions of the input embedding vector, equal to input and output dimensions of each head
n_head: number of heads, which is also the number of parallel attention layers
"""
def __init__ ( self, d_model, n_head) :
super ( MultiHeadAttention, self) . __init__( )
self. n_head = n_head
self. attention = ScaleDotProductAttention( )
self. w_q = nn. Linear( d_model, d_model) # Q 线性变换层
self. w_k = nn. Linear( d_model, d_model) # K 线性变换层
self. w_v = nn. Linear( d_model, d_model) # V 线性变换层
self. fc = nn. Linear( d_model, d_model) # 输出线性变换层
def forward ( self, q, k, v, mask= None ) :
# 1. dot product with weight matrices
q, k, v = self. w_q( q) , self. w_k( k) , self. w_v( v) # size is [batch_size, seq_len, d_model]
# 2, split by number of heads(n_head) # size is [batch_size, n_head, seq_len, d_model//n_head]
q, k, v = self. split( q) , self. split( k) , self. split( v)
# 3, compute attention
sa_output, attn_weights = self. attention( q, k, v, mask)
# 4, concat attention and linear transformation
concat_tensor = self. concat( sa_output)
mha_output = self. fc( concat_tensor)
return mha_output
def split ( self, tensor) :
"""
split tensor by number of head(n_head)
:param tensor: [batch_size, seq_len, d_model]
:return: [batch_size, n_head, seq_len, d_model//n_head], 输出矩阵是四维的,第二个维度是 head 维度
# 将 Q、K、V 通过 reshape 函数拆分为 n_head 个头
batch_size, seq_len, _ = q.shape
q = q.reshape(batch_size, seq_len, n_head, d_model // n_head)
k = k.reshape(batch_size, seq_len, n_head, d_model // n_head)
v = v.reshape(batch_size, seq_len, n_head, d_model // n_head)
"""
batch_size, seq_len, d_model = tensor. size( )
d_tensor = d_model // self. n_head
split_tensor = tensor. view( batch_size, seq_len, self. n_head, d_tensor) . transpose( 1 , 2 )
# it is similar with group convolution (split by number of heads)
return split_tensor
def concat ( self, sa_output) :
""" merge multiple heads back together
Args:
sa_output: [batch_size, n_head, seq_len, d_tensor]
return: [batch_size, seq_len, d_model]
"""
batch_size, n_head, seq_len, d_tensor = sa_output. size( )
d_model = n_head * d_tensor
concat_tensor = sa_output. transpose( 1 , 2 ) . contiguous( ) . view( batch_size, seq_len, d_model)
return concat_tensor
4. Add & Norm代码实现
class LayerNorm ( nn. 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)
# 1,Activate nn.LayerNorm module
layer_norm1 = nn. LayerNorm( embedding_dim)
pytorch_ln_out = layer_norm1( embedding)
# 2,Activate my nn.LayerNorm module
layer_norm2 = LayerNorm( embedding_dim)
my_ln_out = layer_norm2( embedding)
# 比较结果
print ( torch. allclose( pytorch_ln_out, my_ln_out, rtol= 0.1 , atol= 0.01 ) ) # 输出 True
5. Feed Forward代码实现
PositionwiseFeedForward 层的 Pytorch 实现代码如下所示:
class PositionwiseFeedForward ( nn. Module) :
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
6. Encoder代码实现
基于前面 Multi-Head Attention
, Feed Forward
, Add & Norm
的内容我们可以完整的实现 Encoder 结构。
class EncoderLayer ( nn. Module) :
def __init__ ( self, d_model, ffn_hidden, n_head, drop_prob= 0.1 ) :
super ( EncoderLayer, self) . __init__( )
self. mha = MultiHeadAttention( d_model, n_head)
self. ffn = PositionwiseFeedForward( d_model, ffn_hidden)
self. ln1 = LayerNorm( d_model)
self. ln2 = LayerNorm( d_model)
self. dropout1 = nn. Dropout( drop_prob)
self. dropout2 = nn. Dropout( drop_prob)
def forward ( self, x, mask= None ) :
x_residual1 = x
# 1, compute multi-head attention
x = self. mha( q= x, k= x, v= x, mask= mask)
# 2, add residual connection and apply layer norm
x = self. ln1( x_residual1 + self. dropout1( x) )
x_residual2 = x
# 3, compute position-wise feed forward
x = self. ffn( x)
# 4, add residual connection and apply layer norm
x = self. ln2( x_residual2 + self. dropout2( x) )
return x
class Encoder ( nn. Module) :
def __init__ ( self, enc_voc_size, seq_len, d_model, ffn_hidden, n_head, n_layers, drop_prob= 0.1 , device= 'cpu' ) :
super ( ) . __init__( )
self. emb = TransformerEmbedding( vocab_size = enc_voc_size,
max_len = seq_len,
d_model = d_model,
drop_prob = drop_prob,
device= device)
self. layers = nn. ModuleList( [ EncoderLayer( d_model, ffn_hidden, n_head, drop_prob)
for _ in range ( n_layers) ] )
def forward ( self, x, mask= None ) :
x = self. emb( x)
for layer in self. layers:
x = layer( x, mask)
return x
7. Decoder代码实现
Decoder 组件的代码实现如下所示:
class DecoderLayer ( nn. Module) :
def __init__ ( self, d_model, ffn_hidden, n_head, drop_prob) :
super ( DecoderLayer, self) . __init__( )
self. mha1 = MultiHeadAttention( d_model, n_head)
self. ln1 = LayerNorm( d_model)
self. dropout1 = nn. Dropout( p= drop_prob)
self. mha2 = MultiHeadAttention( d_model, n_head)
self. ln2 = LayerNorm( d_model)
self. dropout2 = nn. Dropout( p= drop_prob)
self. ffn = PositionwiseFeedForward( d_model, ffn_hidden)
self. ln3 = LayerNorm( d_model)
self. dropout3 = nn. Dropout( p= drop_prob)
def forward ( self, dec_out, enc_out, trg_mask, src_mask) :
x_residual1 = dec_out
# 1, compute multi-head attention
x = self. mha1( q= dec_out, k= dec_out, v= dec_out, mask= trg_mask)
# 2, add residual connection and apply layer norm
x = self. ln1( x_residual1 + self. dropout1( x) )
if enc_out is not None :
# 3, compute encoder - decoder attention
x_residual2 = x
x = self. mha2( q= x, k= enc_out, v= enc_out, mask= src_mask)
# 4, add residual connection and apply layer norm
x = self. ln2( x_residual2 + self. dropout2( x) )
# 5. positionwise feed forward network
x_residual3 = x
x = self. ffn( x)
# 6, add residual connection and apply layer norm
x = self. ln3( x_residual3 + self. dropout3( x) )
return x
class Decoder ( nn. Module) :
def __init__ ( self, dec_voc_size, max_len, d_model, ffn_hidden, n_head, n_layers, drop_prob, device) :
super ( ) . __init__( )
self. emb = TransformerEmbedding( d_model= d_model,
drop_prob= drop_prob,
max_len= max_len,
vocab_size= dec_voc_size,
device= device)
self. layers = nn. ModuleList( [ DecoderLayer( d_model= d_model,
ffn_hidden= ffn_hidden,
n_head= n_head,
drop_prob= drop_prob)
for _ in range ( n_layers) ] )
self. linear = nn. Linear( d_model, dec_voc_size)
def forward ( self, trg, src, trg_mask, src_mask) :
trg = self. emb( trg)
for layer in self. layers:
trg = layer( trg, src, trg_mask, src_mask)
# pass to LM head
output = self. linear( trg)
return output
8. Transformer代码实现
大语言模型核心技术-Transformer 详解
基于前面实现的 Encoder 和 Decoder 组件,我们可以实现 Transformer 模型的完整代码,如下所示:
import torch
from torch import nn
from models. model. decoder import Decoder
from models. model. encoder import Encoder
class Transformer ( nn. Module) :
def __init__ ( self, src_pad_idx, trg_pad_idx, trg_sos_idx, enc_voc_size, dec_voc_size, d_model, n_head, max_len,
ffn_hidden, n_layers, drop_prob, device) :
super ( ) . __init__( )
self. src_pad_idx = src_pad_idx
self. trg_pad_idx = trg_pad_idx
self. trg_sos_idx = trg_sos_idx
self. device = device
self. encoder = Encoder( d_model= d_model,
n_head= n_head,
max_len= max_len,
ffn_hidden= ffn_hidden,
enc_voc_size= enc_voc_size,
drop_prob= drop_prob,
n_layers= n_layers,
device= device)
self. decoder = Decoder( d_model= d_model,
n_head= n_head,
max_len= max_len,
ffn_hidden= ffn_hidden,
dec_voc_size= dec_voc_size,
drop_prob= drop_prob,
n_layers= n_layers,
device= device)
def forward ( self, src, trg) :
src_mask = self. make_pad_mask( src, src, self. src_pad_idx, self. src_pad_idx)
src_trg_mask = self. make_pad_mask( trg, src, self. trg_pad_idx, self. src_pad_idx)
trg_mask = self. make_pad_mask( trg, trg, self. trg_pad_idx, self. trg_pad_idx) * \
self. make_no_peak_mask( trg, trg)
enc_src = self. encoder( src, src_mask)
output = self. decoder( trg, enc_src, trg_mask, src_trg_mask)
return output
def make_pad_mask ( self, q, k, q_pad_idx, k_pad_idx) :
len_q, len_k = q. size( 1 ) , k. size( 1 )
# batch_size x 1 x 1 x len_k
k = k. ne( k_pad_idx) . unsqueeze( 1 ) . unsqueeze( 2 )
# batch_size x 1 x len_q x len_k
k = k. repeat( 1 , 1 , len_q, 1 )
# batch_size x 1 x len_q x 1
q = q. ne( q_pad_idx) . unsqueeze( 1 ) . unsqueeze( 3 )
# batch_size x 1 x len_q x len_k
q = q. repeat( 1 , 1 , 1 , len_k)
mask = k & q
return mask
def make_no_peak_mask ( self, q, k) :
len_q, len_k = q. size( 1 ) , k. size( 1 )
# len_q x len_k
mask = torch. tril( torch. ones( len_q, len_k) ) . type ( torch. BoolTensor) . to( self. device)
return mask
五、FAQ
答案解析(1)—史上最全Transformer面试题:灵魂20问帮你彻底搞定Transformer 关于Transformer的若干问题整理记录 Transformer的细节与技巧
1. 为什么需要Multi-head Attention?
为什么Transformer 需要进行 Multi-head Attention?
原论文中说到进行Multi-head Attention的原因是将模型分为多个头,形成多个子空间 ,可以让模型去关注不同方面(从不同角度)的信息,最后再将各个方面的信息综合起来。其实直观上也可以想到,如果自己设计这样的一个模型,必然也不会只做一次attention,多次attention综合的结果至少能够起到增强模型 的作用,也可以类比CNN中同时使用多个卷积核 的作用。直观上讲,多头注意力保证了Transformer可以注意到不同子空间的信息,有助于网络捕捉到更丰富的特征/信息 。
2. 为什么Q和K使用不同的权重矩阵?
transformer中为什么使用不同的K 和 Q, 为什么不能使用同一个值?
简单理解,使用Q/K/V不同权重矩阵,可以保证在不同空间 进行投影,增强表达能力 ,提高泛化能力 。
3. 计算Attention的时候为何选择点乘积而不是加法?
为了计算更快 。矩阵加法的计算量确实简单,但作为一个整体计算Attention时,相当于一个隐层,整体计算量和点乘积相似。在效果上来说,两个的效果与
d
k
d_k
d k 向量维度相关,
d
k
d_k
d k 维度越大,加法的效果越显著。
4. 进行softmax之前需要对attention进行scaled?
请查看【Scaled Dot-Product Attention】章节。