本文来源公众号“江大白”,仅用于学术分享,侵权删,干货满满。
原文链接:基于Pytorch框架,从零实现Transformer模型实战
以下文章来源于微信公众号:小喵学AI
作者:郭小喵
链接:https://mp.weixin.qq.com/s/XFniIyQcrxambld5KmXr6Q
本文仅用于学术分享,如有侵权,请联系后台作删文处理
导读
Transformer作为深度学习进入大模型时代的标志性模型,其强大的性能被广泛应用于各个领域。本文基于Pytorch框架从零开始搭建Transformer模型,不仅有详细的脚本说明,还涵盖了丰富了模型分析,希望对大家有帮助。
2017年Google在论文《Attention is All You Need》中提出了Transformer模型,并成功应用到NLP领域。该模型完全基于自注意力机制Attention mechanism实现,弥补了传统的RNN模型的不足。
本文笔者将详解使用Pytorch从零开始逐步实现Transformer模型。
0 回顾
首先我们先回顾一下Transformer原理。宏观层面,Transformer可以看成是一个黑箱操作的序列到序列(seq2seq)模型。例如,在机器翻译中,输入一种语言,经Transformer输出翻译后的另一种语言。
拆开这个黑箱,可以看到模型本质就是一个Encoders-Decoders结构。
-
每个Encoders中分别由6层Encoder组成。(所有Encoder结构完全相同,但是训练参数不同,每个参数是独立训练的,循环执行6次Encode,而不是只训练了一个Encoder然后复制5份)。
-
Decoders同理。
-
这里每个Encoders包含6层Encoder,只是论文中Nx=6,实际可以自定义。
Transformer整体架构如下图所示。
其中,
-
编码端:经过词向量层(Input Embedding)和位置编码层(Positional Encoding),得到最终输入,流经自注意力层(Multi-Head Attention)、残差和层归一化(Add&Norm)、前馈神经网络层(Feed Forward)、残差和层归一化(Add&Norm),得到编码端的输出(后续会和解码端进行交互)。
-
解码端:经过词向量层(Output Embedding)和位置编码层(Positional Encoding),得到最终输入,流经掩码自注意力层(Masked Multi-Head Attention,把当前词之后的词全部mask掉)、残差和层归一化(Add&Norm)、交互注意力层(Multi-Head Attention,把编码端的输出和解码端的信息进行交互,Q矩阵来自解码端,K、V矩阵来自编码端的输出)、残差和层归一化(Add&Norm)、前馈神经网络层(Feed Forward)、残差和层归一化(Add&Norm),得到解码端的输出。
注:编码端和解码端的输入不一定等长。
1 Encoder
下面还是以机器翻译("我是学生"->"I am a student")为例说明。
对于上图中,整个模型的输入即为"我是学生",目标是将其翻译为"I am a student",但是计算机是无法识别"我是学生"的,需要将其转化为二进制形式,再送入模型。
将中文转换为计算机可以识别的向量通常有两种方法:
-
One Hot编码:形成高维向量,对于中文来说,汉字的数量就是向量的维度,然后是哪个字就将对应位置变为1,其它位置变为0,以此来表示一句话。
-
Embedding词嵌入:通过网络进行训练或者通过一些训练好的模型将其转化成连续性的向量。
一般来说第二种方法使用较多,因为第一种有几个缺点,第一个就是每个字都是相互独立的,缺少语义联系信息,第二就是汉字数量太多,会导致生成的维度过大,占用系统内存。
1.1 输入Input
输入Inputs维度是[batch size,sequence length],经Word2Vec,转换为计算机可以识别的Input Embedding,论文中每个词对应一个512维度的向量,维度是[batch_size,sequence_length,embedding_dimmension]。batch size指的是句子数,sequence length指的是输入的句子中最长的句子的字数,embedding_dimmension是词向量长度。
如上图所示,以机器翻译("我是学生"->"I am a student")为例,首先对输入的文字进行Word Embedding处理,每个字(词)用一个连续型向量表示(这里定义的是4维向量),称为词向量。这样一个句子,也就是嵌入后的输入向量input Embedding就可以用一个矩阵表示(4*4维,序列长度为4,每个字用4维向量表示)。input Embedding加上位置信息得到编码器的输入矩阵。
「为什么需要在input Embedding加上位置信息?」 与RNN相比,RNN是一个字一个字输入,自然可以保留每个字的顺序关系信息,而Transformer使用的是自注意力机制来提取信息,一个句子中的每个字/词是并行计算,虽然处理每个字的时候考虑到了所有字对其的影响,但是并没有考虑到各个字相互之间的位置信息,也就是上下文。所以需要引入位置信息。
Transformer中使用Positional Encoding表示每个字/词的位置信息。定义如下:
这样就即可实现让自注意力机制考虑词的顺序,同时又可以输入所有的词。
1.2 输入Input代码实现
1.2.1「Word Embedding」
Word Embedding在Pytorch中通常用nn.Embedding实现。
class Embeddings(nn.Module):
"""
类的初始化
:param d_model: 词向量维度,512
:param vocab: 当前语言的词表大小
"""
def __init__(self, d_model, vocab):
super(Embeddings, self).__init__()
# 调用nn.Embedding预定义层,获得实例化词嵌入对象self.lut
self.lut = nn.Embedding(vocab, d_model)
self.d_model = d_model #表示词向量维度
def forward(self, x):
"""
Embedding层的前向传播
参数x:输入给模型的单词文本通过此表映射后的one-hot向量
x传给self.lut,得到形状为(batch_size, sequence_length, d_model)的张量,与self.d_model相乘,
以保持不同维度间的方差一致性,及在训练过程中稳定梯度
"""
return self.lut(x) * math.sqrt(self.d_model)
1.2.2「Positional Encoding」
class PositionalEncoding(nn.Module):
"""实现Positional Encoding功能"""
def __init__(self, d_model, dropout=0.1, max_len=5000):
"""
位置编码器的初始化函数
:param d_model: 词向量的维度,与输入序列的特征维度相同,512
:param dropout: 置零比率
:param max_len: 句子最大长度,5000
"""
super(PositionalEncoding, self).__init__()
# 初始化一个nn.Dropout层,设置给定的dropout比例
self.dropout = nn.Dropout(p=dropout)
# 初始化一个位置编码矩阵
# (5000,512)矩阵,保持每个位置的位置编码,一共5000个位置,每个位置用一个512维度向量来表示其位置编码
pe = torch.zeros(max_len, d_model)
# 偶数和奇数在公式上有一个共同部分,使用log函数把次方拿下来,方便计算
# position表示的是字词在句子中的索引,如max_len是128,那么索引就是从0,1,2,...,127
# 论文中d_model是512,2i符号中i从0取到255,那么2i对应取值就是0,2,4...510
# (5000) -> (5000,1)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
# 计算用于控制正余弦的系数,确保不同频率成分在d_model维空间内均匀分布
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
# 根据位置和div_term计算正弦和余弦值,分别赋值给pe的偶数列和奇数列
pe[:, 0::2] = torch.sin(position * div_term) # 从0开始到最后面,补长为2,其实代表的就是偶数位置
pe[:, 1::2] = torch.cos(position * div_term) # 从1开始到最后面,补长为2,其实代表的就是奇数位置
# 上面代码获取之后得到的pe:[max_len * d_model]
# 下面这个代码之后得到的pe形状是:[1 * max_len * d_model]
# 多增加1维,是为了适应batch_size
# (5000, 512) -> (1, 5000, 512)
pe = pe.unsqueeze(0)
# 将计算好的位置编码矩阵注册为模块缓冲区(buffer),这意味着它将成为模块的一部分并随模型保存与加载,但不会被视为模型参数参与反向传播
self.register_buffer('pe', pe)
def forward(self, x):
"""
x: [seq_len, batch_size, d_model] 经过词向量的输入
"""
x = x + self.pe[:, :x.size(1)].clone().detach() # 经过词向量的输入与位置编码相加
# Dropout层会按照设定的比例随机“丢弃”(置零)一部分位置编码与词向量相加后的元素,
# 以此引入正则化效果,防止模型过拟合
return self.dropout(x)
1.3 自注意力机制(Self Attention Mechanism)
注意力机制,顾名思义,就是我们对某件事或某个人或物的关注重点。举个生活中的例子,当我们阅读一篇文章时,并非每个词都会被同等重视,我们会更关注那些关键的、与上下文紧密相关的词语,而非