查看原文
其他

【源头活水】Transformer 的一些说明

“问渠那得清如许,为有源头活水来”,通过前沿领域知识的学习,从其他研究领域得到启发,对研究问题的本质有更清晰的认识和理解,是自我提高的不竭源泉。为此,我们特别精选论文阅读笔记,开辟“源头活水”专栏,帮助你广泛而深入的阅读科研文献,敬请关注。

作者:知乎—打水小书童

地址:https://www.zhihu.com/people/da-shui-xiao-shu-tong

我想每一个了解注意力或者Transformer机制的,都读过这篇谷歌的著名论文《attention is all you need》[1] N次了,并且在实现的时候细细的梳理过其中的各种细节,对其了如指掌。但对于许多的初学菜鸡来说,能够快速的掌握这种网络结构仍存在一定的障碍和困难。所以,在这里我会给出关于它的一个详细说明,尽管在网络上也有很多这样的相似文章(具体可以看文末的参考)。


01

总体介绍
Transformer的构想与之前Gehring的论文《Convolutional Sequence to Sequence Learning》[2]是一脉相承的。与通常的NLP模型一样,该卷积神经网络包含一个编码器(模块)和一个解码器(模块)。其中,编码模块是用来将句子信息转变为各种可供计算与学习的特征信息,而解码模块是将学习到的特征再转化为相应的句子。在NLP模型中中通过编码器与解码器之间的点乘来实现注意力机制。或者准确的说,注意力是通过解码器特征空间的输出(包括文本信息特征空间与位置信息特征空间)与编码器最后一个输出层(往往为隐藏层)进行点乘计算得到。
图1. Convolutional sequence to sequence learning network 结构示意图
在论文《Convolutional Sequence to Sequence Learning》[2]中,作者提出了一个多步的注意力机制“Multi-step Attention”,在每个解码层内都计算各自对应的注意力。这种多步的注意力机制与图像处理领域的 FPN[3] 网络十分相似,随着网络层数的加深,每个卷积核所提取的信息范围也逐步扩大。举个例子来说,提取25个单位元素长度的句子,若使用的卷积核宽度为5,需要6层深的网络即可进行覆盖。每一层所计算的注意力可以用来提取相应的关键有效信息,再输入下一层进行处理。在NLP领域中,图像就变成了段落与句子,随着层数的加深逐步提取到整个句子的语义。
CV领域中FPN[3]结构网络处理的是经典的图像识别问题。这类问题不仅需要识别图像中的对象名称,还需要标定其所在图像中的准确位置,这与NLP所要解决的问题有几分相似。FPN卷积网络通过自顶向下的方式获取图像的全局或者局部信息。其结构形如一个金字塔,较高处的网络层在空间信息上较为粗糙但是在语义上较强(即更为抽象的特征)。通过将其较高处的语义特征向下采样并与底层细致的空间信息结合来获得更为全面准确的特征信息,由此识别图像中的对象和位置。在NLP中,我们也需要全局的语义信息与具体句法、词法和语法结构。
图2. FPN(Featrue Pyramid Networks)中自顶向下网络结构


02

Transformer 网络结构分析
本节将详细的说明Transformer网络结构和其中的数据流计算过程。我将主要介绍其中的几个关键概念,包括:编码器解码器堆栈(Encoder and Decoder Stacks), 多头注意力机制(Multi-head attention) 和位置编码器(Position Encodion)。相关的介绍还可以参考 Jay Alammar的博客[4]和大师兄专栏文章的相应中文翻译版[5]

编码器解码器

目前的自然语言处理模型通常都以编码器解码器结构作为基础。首先编码器将句子的文本符号  转化为连续的特征向量  。开始时,符号的向量表示往往是one-hot 的形式(向量的维数为所有单词的数量,只有对应的索引为1,其余都为0)。这种数据类型十分稀疏,即便使用很高的学习率,依然不能得到良好的学习效果。如果将其转化为低维的连续向量可以极大的降低稀疏性,获得较好的易于学习的特征。这种低维的连续向量空间被称为word Embeddings[6][7],如下图中分红色框的Input embedding 和 Output Embedding。
图 3. Transformer结构图《attention is all you need》
论文中编码器(Encoder)的每一层维数都是  ,所有编码器的共同点是,它们接收一个向量列表,word Embedding只作为编码器的第一层,在随后的编码器层中,它将直接是编码器的输出。这个向量列表的大小是一个我们需要设置的超参数——它通常是我们数据集最长句子的长度。这样可以简化模型结构,获得更好的应用效果。
一个Transformer模型中有多个编码器(Encoder)与解码器(Decoder)模块:
编码器(Encoder):编码器由六个相同的编码层模块叠加而成。每个编码层模块还有两个子网络结构。第一个子网络层是多头自注意力层multi-head self-attention(图中浅红色方块),第二个子神经网络层是一个简单的全连接前馈层Feed Forward(图中蓝色方块)。然后,这两个子网络层还应用了残差连接(residual connection[8])和层参数正则化(layer normalization[9])这两种常用方式来获得更好的训练效果,如下图中Add&Norm 模块所示。
解码器(Deocder):解码器同样由六个相同的解码层模块叠加而成,除了与编码器中相同的两个子网络层(multi-head attention 多头注意力层和Feed Forward 全连接前馈层),解码器增加了第三个子网络层,该层也是 multi-head attention 层,该层提取了编码器隐藏层的信息。同样的,残差连接(residual connection[8])和层参数正则化(layer normalization[9])应用在每个子网络层中,如下图所示。
图 4.Transformer详细结构图

多头注意力与Query、Key、Value

与卷积网络中的多步注意力不同,在Transformer中使用多头注意力的结构,如图 6 所示。在多头注意力层中每个注意力头采用不同的线性映射,这里我们使用了8个这样的注意力头,其对应的8组线性映射权重矩阵(  ,  ,  )可以在学习中得到。而每个线性映射使用相同的Query、Key和Value作为输入,对应的输出(  、  、  )维数分别为  、  和  。在实际使用中,8个注意力头可以并行处理,每个生成  维度的输出向量。之后将它们拼接其起来形成一个  维矩阵,再通过一个大的线性映射  ,最终生成  的输出向量,如下图 7 中所示。
图 6. 多头注意力(h 是注意力头数的数量)
图 7. 详细的多头注意力计算
图中的Q、K、V并没有明确的含义,仅仅作为一种指代。在刚开始时,我在这里产生了一定的困扰,纠结于Q、K、V的意义以及它们具体代表什么。在自注意力网络层,Q、K、V都来自于相同的地方,称为 multi-head self-attention。在编码解码注意力层(Encoder-Decoder attention layer)即解码器中的第二层,Q和K分别来自于编码器层,而V来自于解码器层,称为 multi-head attention 。在每个解码层中,自注意力的Q和K都来自于编码器的最顶层(即最后一层),如上图 4 中所示,V来自于上一层的解码器的输出。
上图 6 是原论文‘attention is all you need’[1]中的多头注意力结构示意图,下图7是详细的多头注意力结构示意图,参考了Jay Alammar's的博客‘Illustrated Transformer’主要说明了其‘多头’的实现方式。可以发现最终输出的是一个  矩阵(图中深橘色的矩阵),与word Embeddings 中的输入是相同了,保证了其数据结构的一致性,简化了网络结构。
图 8. Q,K,V计算
接下来我们使用编码器中的第一个自注意力层的计算作为一个例子来进一步说明transformer中注意力的实现方式,如上图 8 所示[4]。对于每个单词我们首先通过 word Embeddings 生成 Query、Key 和 Valvue(关于word Embeddings 你可以参考论文Distributed representations of words and phrases and their compositionality[6]和Glove: Global vectors for word representation[7],或者你可以将其简单理解为单词转化为一种向量表示)。这三个向量通过训练好的三个权重矩阵(  、  、  )与 word Embeddings 相乘得到,浅紫色为Q 、浅黄色为K 、浅蓝色为V 。具体计算公式如下:
公式中,  是一个尺寸系数,该系数是用来消除数据维数增大带来的影响。对于过大的  ,矩阵元素的乘积值会非常大。过大矩阵过大的元素值将 函数的输入位置推移到了相应导数值很小的区域,这会导致该处网络的梯度非常小,使得神经网络变得非常难以训练,这种现象就是梯度消失。
transformer的多头机制是该模型的一大亮点,作者认为通过不同的映射生成多组的Q、K、V值非常有好处,且增加很小的模型实际计算量。在图 7 中,共有  个注意力头,分别为Head0, Head1, Head2, ..., Head7。每个注意力头生成对应的一个注意力矩阵Z0, Z1, Z2, ..., Z7。这8个矩阵拼接在一起后形成一个大矩阵,通过权重矩阵  映射为最终的输出值 Z(图中橘红色) ,上述计算流程的公式为:
其中,Q、K、V的线性映射矩阵分别为:  、  、  和  ,在图7中,这些矩阵具体为:  、  、  和  。
除了multi-head self-attentionmulti-head self-attention,在解码器中还有一种 Masked multi-head attention 即带掩码的多头注意力。它的作用机制基于这样一个事实,即我们在预测句子中 i 位置的单词时,并不知道i+1及之后单词是什么,只能结合已经预测出的 i 位置之前单词的信息。所以在训练时,我们需要将 i 位置之后的单词‘遮掩’住。在具体实现时,往往通过一个上三角矩阵,如下图 8 所示。
图中是将一个法语句子翻译为英语句子。法语句子是‘je suis étudiant’对应翻译后的英语句子是‘I am a student’。当我们生成第一个单词‘I’的时候,我们没有关于目标句子的任何信息,第一个单词完全由原法语句子学习到的特征数据来获得,所以我们将整个目标句子遮掩起来。掩码通常为‘Inf’‘-Inf’和‘0’,由此可以产生无效的空信息。当我们生成第二个单词‘am’,我们已经获得了第一个单词‘I’的信息,但仍不知知道‘I’后面的句子信息,因此我们将‘am a student’遮掩起来。当我们生成最后一个单词‘student’时,我们只需将目标句子中的‘student’遮掩起来即可。因为掩码机制可以产生空信息,因此我们还可以用它来处理非固定长度句子的问题。
图 8. 进行masked multi-head attention中的掩码效果示意图
多头注意力机制在transfomer模型中取得了优秀的效果,一般认为多头注意力机制能够使模型获得不同位置上不同的子空间特征表示。它提高了注意力层的性能主要表现在以下两个方面:
1. 它扩展了模型注意不同位置信息的能力。因为在卷积多步注意力机制中,只有最高一层能够获得最大范围的全局信息,但多头注意力机制能够在第一层就获得足够大范围的特征信息。
2. 它使得注意力层获得了多种的子空间特征表示,提升了模型的学习能力。可以看到,在使用多头注意力计算时,我们通过训练到的不同线性映射矩阵( 、  、 )可以获得多组 Query、Key、Value数据,本文图中使用了 8 个这样的注意力头,因此可以获得8组不同的Query、Key、Value数据。这些线性映射矩阵是随机初始化的,因此在训练后,不同线性映射矩阵将word Embeddings 中的特征数据映射到不同的子空间中。

位置编码

句子中单词的顺序对句子的意思同样有着重要的影响。循环神经网络,例如RNN和LSTM,它们本身有着顺序结构,能够获得句子中单词的排列信息。但是transformer模型并没有这样的顺序结构,为了获得句子的顺序信息,作者采用了Position embedding 位置编码的方法。Position embeddingword embedding 具有相同的维数,所以它们两个可以直接相加作为模型的输入。位置编码有多种方法,在Transformer中使用的是不同频率的三角函数 sin 和 cos 来对位置进行编码,具体计算公式如下:
其中,pos表示位置,i 表示维度。
关于位置编码的描述介绍以及相应的图像解释网上已经有很多了,具体可以参考Alexander Rush的博客《The annotated transformer》[10]以及Amirhossein Kazemnejad的博客《Transformer architecture: The positional encoding》[11]。这里我将给出上述位置编码计算公式的一个简单说明。举个例子来说,在一个句子中有两个单词相距为 k ,其中,第一个单词位置为 i ,第二个单词的位置则为 i+k 。
对于初学者来说,在代码的具体实现中有一个困惑的地方。在实际代码的位置编码矩阵中,编码函数 sin 和 cos 的值被拼接在了一起 ,如下图所示。因此,在公式的左边,2i 与 2i+1 更像是位置编码空间中的一个维数索引(图中的 X 轴)。在公式的右边,i 对应 sin 和 cos 函数的频率,不同的维数 i 对应不同频率的三角函数。在下图中[11],每一行代表一个单词(token)的位置编码,每个编码值在 -1与 1 之间。可以看到 sin 与 cos 函数的值交织到了一起。
图 9. 位置编码的可视化图像
图 10. 位置编码计算
这里我们通过位置编码的计算公式来进一步说明位置编码 pos 与 pos+k 的线性关系:
写作矩阵形式为:
因为单词的距离 k 是一个确定的值,所以上式中的  矩阵也是相对的固定值。可以看出  和  存在线性映射的关系,可以被后续的multi-head的权重矩阵学习到。


03

Pytorch 源码分析
这里我们使用Anaconda的Spyder作为IDE以及pytorch作为代码框架,来对源码进行一些简单的说明。对于Transformer的源码,目前网上已经有了一个很好的来自哈佛大学的教程《The Annotated Transformer》[10]。因此我们在此基础上补充一些细节,以下的代码均来自该教程。读者可以直接copy文中的代码,本文的代码中添加了大量的注释,也可以用来参考。
# model = make_model(V, V, N=2) def make_model(src_vocab, tgt_vocab, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1): "Helper: Construct a model from hyperparameters." c = copy.deepcopy attn = MultiHeadedAttention(h, d_model) ff = PositionwiseFeedForward(d_model, d_ff, dropout) position = PositionalEncoding(d_model, dropout) model = EncoderDecoder( Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N), Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N), nn.Sequential(Embeddings(d_model, src_vocab), c(position)), nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)), Generator(d_model, tgt_vocab)) # This was important from their code. # Initialize parameters with Glorot / fan_avg. for p in model.parameters(): if p.dim() > 1: nn.init.xavier_uniform(p) return model
make_model() 函数是Transformer 模型的构建函数。EncoderDecoder() 是其主函数,定义了 编码器解码器为主体的 NLP 模型结构。MultiHeadedAttention()PositionwiseFeedForward()PositionalEncoding()函数分别对应图 3 和 图 4 中的各个子模块,模型包括了以下5个设置的超参数:
  1. N=6 是图3和图4中 编码器层和解码器层模块的数量;
  2. d_model=512 是模型所处理数据的维度;
  3. d_ff=2048 是前馈全连接层的维度;
  4. h=8 是多头注意力层中注意力头的数量;
  5. dropout=0.1 是一种训练方式的超参数,用来提高模型的健壮性,防止过拟合[12]
在make_model 函数中,deepcopy()copy()存在一定的区别,可以参考GeeksforGeeks 的专栏《copy in Python (Deep Copy and Shallow Copy)》[13]。简单来说,deepcopy()函数开辟一个新内存并将其整个复制过来,修改复制过来的对象并不会影响原先的对象。而copy()函数仅复制了地址,修改新的复制对象仍会影响原先的对象。
nn.sequential()函数定义了一种顺序容器,容器中模块的顺序与实际模型中数据处理的顺序一致,因此并不需要在各个模块之间重新添加forward()函数。它已经嵌入其中,因此要注意各个模块数据输入输出维数的对应,这点与nn.ModuleList()函数不同。
nn.init.xavier_uniform() 是模型参数的初始化函数,具体可以参考pytorch官方文档和Xavier Glorot and Yoshua Bengio 的论文《Understanding the difficulty of training deep feedforward neural networks》[14]
class EncoderDecoder(nn.Module):"""A standard Encoder-Decoder architecture. Base for this and many other models."""#nn.Sequential#A sequential container.#Modules will be added to it in the order they are passed in the constructor. #Alternatively, an ordered dict of modules can also be passed in.

# model = EncoderDecoder( # Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N), # Decoder(DecoderLayer(d_model, c(attn), c(attn), # c(ff), dropout), N), # nn.Sequential(Embeddings(d_model, src_vocab), c(position)), # nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)), # Generator(d_model, tgt_vocab)) def __init__(self, encoder, decoder, src_embed, tgt_embed, generator): super(EncoderDecoder, self).__init__() self.encoder = encoder self.decoder = decoder self.src_embed = src_embed self.tgt_embed = tgt_embed self.generator = generator
def encode(self, src, src_mask): return self.encoder(self.src_embed(src), src_mask)
def decode(self, memory, src_mask, tgt, tgt_mask): return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)
def forward(self, src, tgt, src_mask, tgt_mask): "Take in and process masked src and target sequences." return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)
EncoderDecoder 类定义了模型的编码器层和解码器的整体结构,在Transformer的模型代码中我们可以看到,sef.encoder=encoder=Encoder(Encoder(EncoderLayer(d_ model, c(attn), c(ff), dropout), N)。其中 ,N=6 确定了编码器层的数量。关于编码器层EncoderLayer()解码器层DeocderLayer()的具体实现可以参考后面的相应代码。
值得注意的一点是,在EncoderDecoder 的 forward()函数中返回的是
self.decoder(encode(src, src_ mask), src_ mask,tgt, tgt_ mask)encode()函数的输出刚好对应于 decoder() 的第一个输入,即函数
decode (self, memory, src_ mask, tgt, tgt_ mask)
的参数 memory ,但在deocde() 的 return 函数
self.decoder(self.tgt_embed(tgt), memory, src_ mask,tgt_ mask)
中, 该输入变成了第二个参数,而第一个参数由target sentence 提供,结合了目标句子与编码器中源句子的信息。
#Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N)class Encoder(nn.Module): "Core encoder is a stack of N layers" def __init__(self, layer, N): super(Encoder, self).__init__() self.layers = clones(layer, N) self.norm = LayerNorm(layer.size) def forward(self, x, mask): "Pass the input (and mask) through each layer in turn." for layer in self.layers: x = layer(x, mask) return self.norm(x)
# attn = MultiHeadedAttention(h, d_model)# ff = PositionwiseFeedForward(d_model, d_ff, dropout)
#Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N) # EncoderLayer(d_model, c(attn), c(ff), dropout) 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): super(EncoderLayer, self).__init__() self.self_attn = self_attn self.feed_forward = feed_forward # sublayer is residual connection self.sublayer = clones(SublayerConnection(size, dropout), 2) self.size = size
def forward(self, x, mask): "Follow Figure 1 (left) for connections." #SublayerConnection #forward: #def forward(self, x, sublayer): # "Apply residual connection to any sublayer with the same size." # return x + self.dropout(sublayer(self.norm(x))) x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask)) return self.sublayer[1](x, self.feed_forward)  class Decoder(nn.Module): "Generic N layer decoder with masking." def __init__(self, layer, N): super(Decoder, self).__init__() self.layers = clones(layer, N) self.norm = LayerNorm(layer.size)# decode(self.encode(src, src_mask), src_mask,tgt, tgt_mask) # decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)# def forward(self, x, memory, src_mask, tgt_mask): for layer in self.layers: x = layer(x, memory, src_mask, tgt_mask) return self.norm(x) 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 self.sublayer = clones(SublayerConnection(size, dropout), 3) def forward(self, x, memory, src_mask, tgt_mask): "Follow Figure 1 (right) for connections." m = memory x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask)) x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask)) return self.sublayer[2](x, self.feed_forward) def subsequent_mask(size): "Mask out subsequent positions." attn_shape = (1, size, size) subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8') return torch.from_numpy(subsequent_mask) == 0
编码器与解码器各自对应两个类,Encoder 类主要定义模型中解码器的网络结构,代码中定义了 6 个编码器层。EncoderLayer Encoder 中初始化,定义了编码器层的具体结构,包括注意力层、前馈全连接层、残差网络和层参数正则化。可以参考上文中的图 3 和图 4 进一步理解。
其中,lambda 函数是一种更为简洁的函数定义方式,可以提高代码的gentle程度。。。在python中被广泛采用,具体可以参考GeeksforGeeks. Python lambda functions [15]
x = self.sublayer[0](x,lambda x: self.selfattn(x, x, x, mask)) 相当于顺序运行了两个函数:self.sublayer[0]() 和 self_attn()。
函数 subsequent_ mask() 是图 8 中掩码机制的具体实现代码。
class PositionwiseFeedForward(nn.Module): "Implements FFN equation." def __init__(self, d_model, d_ff, dropout=0.1): super(PositionwiseFeedForward, self).__init__() self.w_1 = nn.Linear(d_model, d_ff) self.w_2 = nn.Linear(d_ff, d_model) self.dropout = nn.Dropout(dropout)
def forward(self, x): return self.w_2(self.dropout(F.relu(self.w_1(x)))) class Embeddings(nn.Module): def __init__(self, vocab, d_model): super(Embeddings, self).__init__() self.lut = nn.Embedding(vocab, d_model) self.d_model = d_model
def forward(self, x): return self.lut(x) * math.sqrt(self.d_model)
class PositionalEncoding(nn.Module): "Implement the PE function." def __init__(self, d_model, dropout, max_len=5000): super(PositionalEncoding, self).__init__() self.dropout = nn.Dropout(p=dropout) # Compute the positional encodings once in log space. pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) pe = pe.unsqueeze(0) self.register_buffer('pe', pe) def forward(self, x): x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False) return self.dropout(x)
PositionwiseEocoding 类是位置编码的实现,但是与论文中位置编码的计算方式略有不同。代码
div_ term = torch.exp(torch.arange(0, d_ model, 2) *-(math.log(10000.0) / d_ model))
对应的公式略微转换后,便是原论文中的公式:
def attention(query, key, value, mask=None, dropout=None):"Compute 'Scaled Dot Product Attention'"d_k = query.size(-1)scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)#query.size()=torch.Size([30, 8, 10, 64])#key.transpose(-2, -1).size()=torch.Size([30, 8, 64, 10])
#\at the end of line Line Continuationif mask is not None: scores = scores.masked_fill(mask == 0, -1e9)p_attn = F.softmax(scores, dim = -1)if dropout is not None: p_attn = dropout(p_attn)return torch.matmul(p_attn, value), p_attn

# attn = MultiHeadedAttention(h, d_model)# h=8# d_model=512class MultiHeadedAttention(nn.Module): def __init__(self, h, d_model, dropout=0.1): "Take in model size and number of heads." super(MultiHeadedAttention, self).__init__() assert d_model % h == 0 # We assume d_v always equals d_k self.d_k = d_model // h# /Integer division //float division self.h = h self.linears = clones(nn.Linear(d_model, d_model), 4) self.attn = None self.dropout = nn.Dropout(p=dropout) def forward(self, query, key, value, mask=None): "Implements Figure 2" if mask is not None: # Same mask applied to all h heads. mask = mask.unsqueeze(1) nbatches = query.size(0) # 1) Do all the linear projections in batch from d_model => hxd_k query, key, value = \ [l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2) for l, x in zip(self.linears, (query, key, value))] #zip(self.linears, (query, key, value))is put (self.linears[0],self.linears[1], #self.linears[2])and (query, key, value) together then traverse #view(nbatches, -1, self.h, self.d_k) #here, the multi-head is h feature extraction linear matrices stitched together, as #{head1,head2,head3,head4,head5,head6,head7,head8} #each matrix's size is 64*512 #concatenting together is 512*512 # 2) Apply attention on all the projected vectors in batch. x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout) # 3) "Concat" using a view and apply a final linear. x = x.transpose(1, 2).contiguous() \ .view(nbatches, -1, self.h * self.d_k) return self.linears[-1](x)
该处的代码是 attention 机制的实现,可以参考上文的图7与图8。略有不同的是,在图8 中,h=8 个注意力头是分别独自计算的。而在上面的代码中(类MultiHeadAttention),8 个注意力头是拼接在一起的形成一个大的线性映射矩阵同时进行计算。每个head 的大小是  ,拼接后的大小为  ,其代码为:
query, key, value = / [l(x).view(nbatches, -1,self.h,self.d_k).transpose(1, 2) for l, x in zip(self.linears, (query, key, value))]:
其中,zip() 函数内置了一个迭代器,在for循环内,相当于分别运行了三次:
self.linears[0](query)
self.linears[1](key)
self.linears[2](value)
而在 类MultiHeadAttention 初始化时,定义了四个这样的线性映射,self.linears = clones(nn.Linear(d_model, d_model),4)
最后一个self.linear() 作用是将计算得到的 8 个注意力  拼接在一起后再进行一次线性映射,作为注意力层的输出(在 forward() 函数的 return 中)
类attention 实现了注意力的具体计算公式,可以参考上文中的注意力计算公式:
class LayerNorm(nn.Module): "Construct a layernorm module (See citation for details)." def __init__(self, features, eps=1e-6): super(LayerNorm, self).__init__() self.a_2 = nn.Parameter(torch.ones(features)) self.b_2 = nn.Parameter(torch.zeros(features)) self.eps = eps
def forward(self, x): mean = x.mean(-1, keepdim=True) std = x.std(-1, keepdim=True) return self.a_2 * (x - mean) / (std + self.eps) + self.b_2 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)))
类 LayerNorm 和 类 SublayerConnection 分别对应的是层参数正则化[9]与残差网络连接[8],它们两种优化网络的方式在每层网络参数传递时均被使用。
可以看到transformer的代码是非常简洁的,这也为它在不同AI 领域的运用提供了方便。事实上,当你实现一个transformer网络的时候,最多的工作量往往在前期的数据处理中,被称做dirty work。你需要一点耐心来提取清洗数据和调整数据的结构与类型。
最后,我在实现The annotated transformer [10]的代码时,遇到最常见的bug就是数据格式错误,通过调整数据格式后(data.long()),就可以排除掉这个bug。总的说来,原教程的代码基本是ok的,以下是一个简单的例子:
target.data=target.data.long() print(target.data.type()) print(30*'_')
本人水平实在有限,欢迎大家指正

参考:

[1]Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez, Lukasz Kaiser, and Illia Polosukhin. Attention is all you need. arXiv preprint arXiv:1706.03762, 2017.

[2]Jonas Gehring, Michael Auli, David Grangier, Denis Yarats, and Yann N Dauphin. Convolutional sequence to sequence learning. In International Conference on Machine Learning, pages 1243–1252. PMLR, 2017.

[3]Tsung-Yi Lin, Piotr Dollár, Ross Girshick, Kaiming He, Bharath Hariharan, and Serge Belongie. Feature pyramid networks for object detection. In Proceedings of the IEEE conference on computer vision and pattern recognition, pages 2117–2125, 2017.

[4]Jay Alammar. The illustrated transformer. https://jalammar.github.io/illustrated-transformer

[5]Da ShiXiong. The illustrated transformer(chinese).  https://zhuanlan.zhihu.com/p/48508221

[6]Tomas Mikolov, Ilya Sutskever, Kai Chen, Greg Corrado, and Jeffrey Dean. Distributed representations of words and phrases and their compositionality. arXiv preprint arXiv:1310.4546, 2013.

[7]Jeffrey Pennington, Richard Socher, and Christopher D Manning. Glove: Global vectors for word representation. In Proceedings of the 2014 conference on empirical methods in natural language processing (EMNLP), pages 1532– 1543, 2014.

[8]Kaiming He, Xiangyu Zhang, Shaoqing Ren, and Jian Sun. Deep residual learning for image recognition. In Proceedings of the IEEE conference on computer vision and pattern recognition, pages 770–778, 2016.

[9]Jimmy Lei Ba, Jamie Ryan Kiros, and Geoffrey E Hinton. Layer normalization. arXiv preprint arXiv:1607.06450, 2016.

[10]Alexander Rush. The annotated transformer. http://nlp.seas.harvard.edu/2018/04/03/attention.html

[11]Amirhossein Kazemnejad. Transformer architecture: The positional encoding. https://kazemnejad.com/blog/transformer_architecture_positional_encoding

[12]Nitish Srivastava, Geoffrey Hinton, Alex Krizhevsky, Ilya Sutskever, and Ruslan Salakhutdinov. Dropout: a simple way to prevent neural networks from overfitting. The journal of machine learning research, 15(1):1929–1958, 2014.

[13]GeeksforGeeks. copy and deep copy in python. https://www.geeksforgeeks.org/copy-python-deep-copy-shallow-copy/

[14]Xavier Glorot and Yoshua Bengio. Understanding the difficulty of training deep feedforward neural networks. In Proceedings of the thirteenth international conference on artificial intelligence and statistics, pages 249–256. JMLR Workshop and Conference Proceedings, 2010.

[15]GeeksforGeeks. Python lambda functions. https://www.geeksforgeeks.org/python-lambda-anonymous-functions-filter-map-reduce/



本文目的在于学术交流,并不代表本公众号赞同其观点或对其内容真实性负责,版权归原作者所有,如有侵权请告知删除。


“源头活水”历史文章


更多源头活水专栏文章,

请点击文章底部“阅读原文”查看



分享、在看,给个三连击呗!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存