pytorch实现attention_transformer代码解读(pytorch)
内容简介:本⽂介绍Transformer的代码。本⽂内容参考了注意:这⾥并不是完全翻译这篇⽂章,⽽是根据作者⾃⼰的理解来分析和阅读其源代码。Transformer的原理在前⾯的图解部分已经分析的很详细了,因此这⾥关注的重点是代码。⽹上有很多Transformer 的源代码,也有⼀些⽐较⼤的库包含了Transformer的实现,⽐如Tensor2Tensor和OpenNMT等等。作者选择这个实现的原因是因为它是⼀个单独的ipynb⽂件,如果我们要实际使⽤⾮常简单,复制粘贴代码就⾏了。⽽Tensor2Tensor或者Ope
本⽂转载⾃: fancyerii.github.io/201 9/03/09/transformer-codes/ ,版权归原作者或者来源机构所有。
本⽂内容参考了 The Annotated Transformer 。读者可以从 这⾥ 下载代码。这篇⽂章是原始论⽂的读书笔记,它除了详细的解释论⽂的原理,还⽤代码实现了论⽂的模型。
注意:这⾥并不是完全翻译这篇⽂章,⽽是根据作者⾃⼰的理解来分析和阅读其源代码。Transformer的原理在前⾯的图解部分已经分析的很详细了,因此这⾥关注的重点是代码。⽹上有很多Transformer的源代码,也有⼀些⽐较⼤的库包含了Transformer的实现,⽐如Tensor2Tensor和OpenNMT等等。作者选择这个实现的原因是因为它是⼀个单独的ipynb⽂件,如果我们要实际使⽤⾮常简单,复制粘贴代码就⾏了。⽽Tensor2Tensor或者OpenNMT包含了太多其它的东西,做了过多的抽象。虽然代码质量和重⽤性
更好,但是对于理解论⽂来说这是不必要的,并且增加了理解的难度。
运⾏代码
这⾥的代码需要PyTorch-0.3.0(⾼版本的0.4.0+都不⾏),所以建议读者使⽤virtualenv安装。为了在Jupyter notebook⾥使⽤这个virtualenv,需要执⾏如下命令:
source /path/to/virtualenv/bin/activate
pip install ipykernel
python -m ipykernel install --ur --name=pytorch-0.3.0
买沙发jupyter notebook
点击kernel菜单->lect kernel -> pytorch-0.3.0
背景介绍
前⾯提到过RNN等模型的缺点是需要顺序计算,从⽽很难并⾏。因此出现了Extended Neural GPU、ByteNet和ConvS2S等⽹络模型。这些模型都是以CNN为基础,这⽐较容易并⾏。但是和RNN相⽐,
两直线垂直斜率关系
它较难学习到长距离的依赖关系。
本⽂的Transformer使⽤了Self-Attention机制,它在编码每⼀词的时候都能够注意(attend to)整个句⼦,从⽽可以解决长距离依赖的问题,同时计算Self-Attention可以⽤矩阵乘法⼀次计算所有的时刻,因此可以充分利⽤计算资源(CPU/GPU上的矩阵运算都是充分优化和⾼度并⾏的)。
模型结构
⽬前的主流神经序列转换(neural quence transduction)模型都是基于Encoder-Decoder结构的。所谓的序列转换模型就是把⼀个输⼊序列转换成另外⼀个输出序列,它们的长度很可能是不同的。⽐如基于神经⽹络的机器翻译,输⼊是法语句⼦,输出是英语句⼦,这就是⼀个序列转换模型。类似的包括⽂本摘要、对话等问题都可以看成序列转换问题。我们这⾥主要关注机器翻译,但是任何输⼊是⼀个序列输出是另外⼀个序列的问题都可以考虑使⽤Encoder-Decoder模型。
Encoder讲输⼊序列$(x_1,…,x_n)$映射(编码)成⼀个连续的序列$z=(z_1,…,z_n)$。⽽Decoder根据z来解码得到输出序列$y_1,
…,y_m$。Decoder是⾃回归的(auto-regressive)——它会把前⼀个时刻的输出作为当前时刻的输⼊。Encoder-Decoder结构模型的代码如下:
class EncoderDecoder(nn.Module):
"""
标准的Encoder-Decoder架构。这是很多模型的基础
"""
def __init__(lf, encoder, decoder, src_embed, tgt_embed, generator):
super(EncoderDecoder, lf).__init__()
# encoder和decoder都是构造的时候传⼊的,这样会⾮常灵活
lf.decoder = decoder
旧金山旅游攻略
# 源语⾔和⽬标语⾔的embedding
lf.src_embed = src_embed
<_embed = tgt_embed
# generator后⾯会讲到,就是根据Decoder的隐状态输出当前时刻的词
# 基本的实现就是隐状态输⼊⼀个全连接层,全连接层的输出⼤⼩是词的个数
# 然后接⼀个softmax变成概率。
中国人民解放军军事经济学院def forward(lf, src, tgt, src_mask, tgt_mask):
# ⾸先调⽤encode⽅法对输⼊进⾏编码,然后调⽤decode⽅法解码
return lf.de(src, src_mask), src_mask,
tgt, tgt_mask)
def encode(lf, src, src_mask):
# 调⽤encoder来进⾏编码,传⼊的参数embedding的src和src_mask
der(lf.src_embed(src), src_mask)
def decode(lf, memory, src_mask, tgt, tgt_mask):
# 调⽤decoder
return lf._embed(tgt), memory, src_mask, tgt_mask)
EncoderDecoder定义了⼀种通⽤的Encoder-Decoder架构,具体的Encoder、Decoder、src_embed、target_embed和generator都是构造函数传⼊的参数。这样我们做实验更换不同的组件就会更加⽅便。
class Generator(nn.Module):
# 根据Decoder的隐状态输出⼀个词
# d_model是Decoder输出的⼤⼩,vocab是词典⼤⼩
def __init__(lf, d_model, vocab):
super(Generator, lf).__init__()
lf.proj = nn.Linear(d_model, vocab)
入职宣言# 全连接再加上⼀个softmax
def forward(lf, x):
return F.log_softmax(lf.proj(x), dim=-1)
注意:Generator返回的是softmax的log值。在PyTorch⾥为了计算交叉熵损失,有两种⽅法。第⼀种⽅法是使⽤
nn.CrossEntropyLoss(),⼀种是使⽤NLLLoss()。第⼀种⽅法更加容易懂,但是在很多开源代码⾥第⼆种更常见,原因可能是它后来才有,⼤家都习惯了使⽤NLLLoss。我们先看CrossEntropyLoss,它就是计算交叉熵损失函数,⽐如:
criterion = nn.CrossEntropyLoss()
x = torch.randn(1, 5)
y = pty(1, dtype=torch.long).random_(5)
loss = criterion(x, y)
⽐如上⾯的代码,假设是5分类问题,x表⽰模型的输出logits(batch=1),⽽y是真实分类的下标(0-4)。实际的计算过程为:
。
⽐如logits是[0,1,2,3,4],真实分类是3,那么上式就是:
因此我们也可以使⽤NLLLoss()配合F.log_softmax函数(或者nn.LogSoftmax,这不是⼀个函数⽽是⼀个Module了)来实现⼀样的效果:
m = nn.LogSoftmax(dim=1)
criterion = nn.NLLLoss()
x = torch.randn(1, 5)
y = pty(1, dtype=torch.long).random_(5)
loss = criterion(m(x), y)
NLLLoss(Negative Log Likelihood Loss)是计算负log似然损失。它输⼊的x是log_softmax之后的结果(长度为5的数组),y是真实分类(0-4),输出就是x[y]。因此上⾯的代码为:
criterion(m(x), y)=m(x)[y]
Transformer模型也是遵循上⾯的架构,只不过它的Encoder是N(6)个EncoderLayer组成,每个EncoderLayer包含⼀个Self-Attention SubLayer层和⼀个全连接SubLayer层。⽽它的Decoder也是N(6)个DecoderLayer组成,每个DecoderLayer包含⼀个Self-Attention SubLayer层、Attention SubLayer层和全连接SubLayer层。如下图所⽰。
图:Transformer的结构
Encoder和Decoder Stack
前⾯说了Encoder和Decoder都是由N个相同结构的Layer堆积(stack)⽽成。因此我们⾸先定义clones函数,⽤于克隆相同的SubLayer。
def clones(module, N):
# 克隆N个完全相同的SubLayer,使⽤了copy.deepcopy
return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
这⾥使⽤了nn.ModuleList,ModuleList就像⼀个普通的 Python 的List,我们可以使⽤下标来访问它,它的好处是传⼊的ModuleList的所有Module都会注册的PyTorch⾥,这样Optimizer就能找到这⾥⾯的参数,从⽽能够⽤梯度下降更新这些参数。但是nn.ModuleList并不是Module(的⼦类),因此它没有forward等⽅法,我们通常把它放到某个Module⾥。接下来我们定义Encoder:
景甜壁纸
class Encoder(nn.Module):
"Encoder是N个EncoderLayer的stack"
def __init__(lf, layer, N):
super(Encoder, lf).__init__()
# layer是⼀个SubLayer,我们clone N个
lf.layers = clones(layer, N)
# 再加⼀个LayerNorm层
< = LayerNorm(layer.size)
def forward(lf, x, mask):
"逐层进⾏处理"
for layer in lf.layers:
x = layer(x, mask)
# 最后进⾏LayerNorm,后⾯会解释为什么最后还有⼀个LayerNorm。
(x)
Encoder就是N个SubLayer的stack,最后加上⼀个LayerNorm。我们来看LayerNorm:
class LayerNorm(nn.Module):
def __init__(lf, features, eps=1e-6):
super(LayerNorm, lf).__init__()
lf.a_2 = nn.s(features))
lf.b_2 = nn.s(features))
lf.eps = eps
def forward(lf, x):
mean = x.mean(-1, keepdim=True)
std = x.std(-1, keepdim=True)
return lf.a_2 * (x - mean) / (std + lf.eps) + lf.b_2
LayerNorm我们以前介绍过,代码也很简单,这⾥就不详细介绍了。注意Layer Normalization不是Batch Normalization。如所⽰,原始论⽂的模型为:
x -> attention(x) -> x+lf-attention(x) -> layernorm(x+lf-attention(x)) => y
y -> den(y) -> y+den(y) -> layernorm(y+den(y)) => z(输⼊下⼀层)
这⾥稍微做了⼀点修改,在lf-attention和den之后加了⼀个dropout层。另外⼀个不同⽀持就是把layernorm层放到前⾯了。这⾥的模型为:
x -> layernorm(x) -> attention(layernorm(x)) -> x + attention(layernorm(x)) => y
y -> layernorm(y) -> den(layernorm(y)) -> y+den(layernorm(y))
原始论⽂的layernorm放在最后;⽽这⾥把它放在最前⾯并且在Encoder的最后⼀层再加了⼀个layernorm。这⾥的实现和论⽂的实现基本是⼀致的,只是给最底层的输⼊x多做了⼀个layernorm,⽽原始论⽂是没有的。下⾯是Encoder的forward⽅法,这样对⽐读者可能会⽐较清楚为什么N个EncoderLayer处理完成之后还需要⼀个LayerNorm
def forward(lf, x, mask):
"逐层进⾏处理"
for layer in lf.layers:
x = layer(x, mask)
(x)
不管是Self-Attention还是全连接层,都⾸先是LayerNorm,然后是Self-Attention/Den,然后是Dropout,最好是残差连接。这⾥⾯有很多可以重⽤的代码,我们把它封装成SublayerConnection。
class SublayerConnection(nn.Module):
"""
LayerNorm + sublayer(Self-Attenion/Den) + dropout + 残差连接
为了简单,把LayerNorm放到了前⾯,这和原始论⽂稍有不同,原始论⽂LayerNorm在最后。
"""
def __init__(lf, size, dropout):
super(SublayerConnection, lf).__init__()
< = LayerNorm(size)
lf.dropout = nn.Dropout(dropout)
def forward(lf, x, sublayer):迷迭香提取物
"sublayer是传⼊的参数,参考DecoderLayer,它可以当成函数调⽤,这个函数的有⼀个输⼊参数"
return x + lf.dropout((x)))
愿望成真
这个类会构造LayerNorm和Dropout,但是Self-Attention或者Den并不在这⾥构造,还是放在了EncoderLayer⾥,在forward的时候由EncoderLayer传⼊。这样的好处是更加通⽤,⽐如Decoder也是类似的需要在Self-Attention、Attention或者Den前⾯后加上LayerNorm和Dropout以及残差连接,我们就可以复⽤代码。但是这⾥要求传⼊的sublayer可以使⽤⼀个参数来调⽤的函数(或者有
__call__)。
有了这些代码之后,EncoderLayer就很简单了:
class EncoderLayer(nn.Module):
"EncoderLayer由lf-attn和feed forward组成"
def __init__(lf, size, lf_attn, feed_forward, dropout):
super(EncoderLayer, lf).__init__()
lf.lf_attn = lf_attn
lf.feed_forward = feed_forward
lf.sublayer = clones(SublayerConnection(size, dropout), 2)
lf.size = size
def forward(lf, x, mask):
"Follow Figure 1 (left) for connections."
x = lf.sublayer[0](x, lambda x: lf.lf_attn(x, x, x, mask))
return lf.sublayer[1](x, lf.feed_forward)
为了复⽤,这⾥的lf_attn层和feed_forward层也是传⼊的参数,这⾥只构造两个SublayerConnection。forward调⽤sublayer[0] (这是SublayerConnection对象)的__call__⽅法,最终会调到它的forward⽅法,⽽这个⽅法需要两个参数,⼀个是输⼊Tensor,⼀个是⼀个callable,并且这个callable可以⽤⼀个参数来调⽤。⽽lf_attn函数需要4个参数(Query的输⼊,Key的输⼊,Value的输⼊和Mask),因此这⾥我们使⽤lambda的技巧把它变成⼀个参数x的函数(mask可以看成已知的数)。因为lambda的形参也叫x,读者可能难以理解,我们改写⼀下:
def forward(lf, x, mask):
z = lambda y: lf.lf_attn(y, y, y, mask)
x = lf.sublayer[0](x, z)
return lf.sublayer[1](x, lf.feed_forward)
lf_attn有4个参数,但是我们知道在Encoder⾥,前三个参数都是输⼊y,第四个参数是mask。这⾥
mask是已知的,因此我们可以⽤lambda的技巧它变成⼀个参数的函数z = lambda y: lf.lf_attn(y, y, y, mask),这个函数的输⼊是y。
call (x, z),然后会调⽤
lf.sublayer[0]是个callable,lf.sublayer[0] (x, z)会调⽤lf.sublayer[0]. call
SublayerConnection.forward(x, z),然后会调⽤(x)),sublayer就是传⼊的参数z,因此就是(x))。z是⼀个lambda,我们可以先简单的看成⼀个函数,显然这⾥要求函数z的输⼊是⼀个参数。理解了Encoder之后,Decoder就很简单了。