Transformer注解及PyTorch实现
原文:http://nlp.seas.harvard.edu/2018/04/03/attention.html 作者:Alexander Rush 转载自机器之心:https://www.jiqizhixin.com/articles/2018-11-06-10?from=synced&keyword=transformer
在学习的过程中,将代码及排版整理了一下,方便阅读。
"Attention is All You Need" 一文中提出的Transformer网络结构最近引起了很多人的关注。Transformer不仅能够明显地提升翻译质量,还为许多NLP任务提供了新的结构。虽然原文写得很清楚,但实际上大家普遍反映很难正确地实现。
所以我们为此文章写了篇注解文档,并给出了一行行实现的Transformer的代码。本文档删除了原文的一些章节并进行了重新排序,并在整个文章中加入了相应的注解。此外,本文档以Jupyter notebook的形式完成,本身就是直接可以运行的代码实现,总共有400行库代码,在4个GPU上每秒可以处理27,000个tokens。
想要运行此工作,首先需要安装PyTorch。这篇文档完整的notebook文件及依赖可在github 或 Google Colab 上找到。
需要注意的是,此注解文档和代码仅作为研究人员和开发者的入门版教程。这里提供的代码主要依赖OpenNMT实现,想了解更多关于此模型的其他实现版本可以查看Tensor2Tensor (tensorflow版本) 和 Sockeye(mxnet版本)
-
Alexander Rush (@harvardnlp[8] or srush@seas.harvard.edu)
准备工作
pip install http://download.pytorch.org/whl/cu80/torch-0.3.0.post4-cp36-cp36m-linux_x86_64.whl numpy matplotlib spacy torchtext seaborn
import numpy as np import torch import torch.nn as nn import torch.nn.functional as F import math, copy, time from torch.autograd import Variable import matplotlib.pyplot as plt import seaborn seaborn.set_context(context="talk") %matplotlib inline
背景
减少序列处理任务的计算量是一个很重要的问题,也是Extended Neural GPU、ByteNet和ConvS2S等网络的动机。上面提到的这些网络都以CNN为基础,并行计算所有输入和输出位置的隐藏表示。 在这些模型中,关联来自两个任意输入或输出位置的信号所需的操作数随位置间的距离增长而增长,比如ConvS2S呈线性增长,ByteNet呈现以对数形式增长,这会使学习较远距离的两个位置之间的依赖关系变得更加困难。而在Transformer中,操作次数则被减少到了常数级别。
Self-attention有时候也被称为Intra-attention,是在单个句子不同位置上做的Attention,并得到序列的一个表示。它能够很好地应用到很多任务中,包括阅读理解、摘要、文本蕴涵,以及独立于任务的句子表示。端到端的网络一般都是基于循环注意力机制而不是序列对齐循环,并且已经有证据表明在简单语言问答和语言建模任务上表现很好。
据我们所知,Transformer是第一个完全依靠Self-attention而不使用序列对齐的RNN或卷积的方式来计算输入输出表示的转换模型。
模型结构
目前大部分比较热门的神经序列转换模型都有Encoder-Decoder结构。Encoder将输入序列$ (x_1, …, x_n) $映射到一个连续表示序列 $ \mathbf{z} = (z_1, …, z_n) $。对于编码得到的$\mathbf{z}$,Decoder每次解码生成一个符号,直到生成完整的输出序列:$(y_1,…,y_m)$。对于每一步解码,模型都是自回归的,即在生成下一个符号时将先前生成的符号作为附加输入。
class EncoderDecoder(nn.Module): """ A standard Encoder-Decoder architecture. Base for this and many other models. """ 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 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) 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)
class Generator(nn.Module): "Define standard linear + softmax generation step." def __init__(self, d_model, vocab): super(Generator, self).__init__() self.proj = nn.Linear(d_model, vocab) def forward(self, x): return F.log_softmax(self.proj(x), dim=-1)
Transformer的整体结构如下图所示,在Encoder和Decoder中都使用了Self-attention, Point-wise和全连接层。Encoder和decoder的大致结构分别如下图的左半部分和右半部分所示。
Encoder和Decoder
Encoder
Encoder由N=6个相同的层组成。
def clones(module, N): "Produce N identical layers." return nn.ModuleList([copy.deepcopy(module) for _ in range(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)
我们在每两个子层之间都使用了残差连接(Residual Connection) 和归一化。
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
也就是说,每个子层的输出为 $\mathrm{LayerNorm}(x + \mathrm{Sublayer}(x))$,其中 $\mathrm{Sublayer}(x)$ 是由子层自动实现的函数。我们在每个子层的输出上使用Dropout,然后将其添加到下一子层的输入并进行归一化。
为了能方便地使用这些残差连接,模型中所有的子层和Embedding层的输出都设定成了相同的维度,即$d_{\text{model}}=512$。
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)))
每层都有两个子层组成。第一个子层实现了“多头”的 Self-attention,第二个子层则是一个简单的Position-wise的全连接前馈网络。
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 self.sublayer = clones(SublayerConnection(size, dropout), 2) self.size = size def forward(self, x, mask): "Follow Figure 1 (left) for connections." x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask)) return self.sublayer[1](x, self.feed_forward)
Decoder
Decoder也是由N=6个相同层组成。
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) 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)
除了每个编码器层中的两个子层之外,解码器还插入了第三种子层对编码器栈的输出实行“多头”的Attention。 与编码器类似,我们在每个子层两端使用残差连接进行短路,然后进行层的规范化处理。
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_fo