10个问题看透Transformer

点击上方视学算法”,选择加"星标"或“置顶css

重磅干货,第一时间送达node

做者丨张春阳@知乎(已受权)
来源丨https://zhuanlan.zhihu.com/p/347709112
编辑丨极市平台

Github 上有与文章配套的 jupyter notebook,和文章配合食用,效果更佳。python

https://github.com/BSlience/transformer-all-in-onegit

本文主要回答如下几个问题:github

  • Attention 机制是用来作什么的 ?web

  • Self-attention 是怎么从 Attention 过分过来的 ?算法

  • Attention 和 self-attention 的区别是什么 ?apache

  • Self-attention 为何能 work ?json

  • 怎么用 Pytorch 实现 self-attention ?ruby

  • Transformer 的做者对 self-attention 作了哪些 tricks ?

  • 怎么用 Pytorch/Tensorflow2.0 实如今 Transfomer 中的 self-attention ?

  • 完整的 Transformer Block 是什么样的?

  • 怎么捕获序列中的顺序信息呢 ?

  • 怎么用 Pytorch 实现一个完整的 Transformer 模型?

Attention 机制是用来作什么的 ?

Attention机制最先的提出是针对与序列模型的,出处是Bengio大神在2015年的这篇文章:

Neural Machine Translation by jointly learning to align and translate, Bengio et. al. ICLR 2015

https://arxiv.org/pdf/1409.0473.pdf

在这篇文章中其实并无常常性的提到attention(其实只有3次),这个词流行起来实际上是在后来的一些工做中,被不少work说起到。在这篇文章中,我试图在更common的视角下去理解attention的机制,而不是使用论文中在translation任务中的应用。

咱们要说attention机制实际上是借鉴了生物在观察和学习行为中的过程,也就是说咱们人来一般在观察和学习的时候,都是经过快速的获取全局的信息,创建起对于事物的须要重点观察或者学习的区域,这些须要重点关注的目标区域,就是咱们注意力的焦点。然而,就和咱们平常生活中处理事情同样,咱们没有办法同时处理全部的事情,咱们会给他们分出优先级。一样的,注意力也会有一个权重值,从而更专一的聚焦在某些关键的信息上。

咱们把attention放到不一样case里,再去看看这种注意力不一样的解释。

首先咱们看看在视觉领域,咱们应该怎么去理解attention:

当咱们去看上面这张照片的时候,咱们首先就是先去看总体,这里有车、有街道、还有不少的广告牌,不知道你们是否有感觉到,当我开始描述这些的时候,其实就是我把注意力放在了这些上面了。那当咱们想要跟深刻了解这张图片的时候,我就要把注意力放的更聚焦。好比说,我想知道这是拍的哪里,那我可能会试着去看看广告牌上的文字,看这些文字是否是能给我一些启示。

就像上面这张图同样,咱们可能会试着把注意力放到不一样的区域,那咱们就可以获得更多的关于不一样角度的信息。这些信息,正是咱们但愿在图像处理的时候但愿获得的。

咱们再来看看再天然语言处理中,attention机制表示的又是什么呢?

好比说上面的这句话,“她正在吃一个绿色的苹果”,这里咱们能够比较清楚的看到,“吃”和“苹果”有很强的联系,那咱们就但愿在处理吃这个单词的时候,可以在语义中,包含必定的苹果的信息,这样可以帮助咱们更好的理解“吃”这个动做。“绿色的”和“苹果”也是同样的,attention的机制可以帮助咱们在处理单个的token的时候,带有必定的上下文信息。这就像是一种“软性记忆”同样,帮助咱们记住上下文中包含的信息。

当咱们看一篇文章的时候,其实也是相似的。咱们从拿到一篇文章开始,首先关注的也只是一些关键性的词语,这些关键性的词语,就可以帮助咱们快速的判断文章的内容和结构。这些场景就是咱们在一些具体场景中对attention的应用。

那接下来,咱们再来看看,attention具体是怎么工做的?

假设咱们的时间序列:   ,咱们把它放到坐标轴上,就是上面这张图的样子。

这些点是咱们从总体数据中采样出来的,这里有不少的噪音(noisy),咱们想办法能不能经过一些方法,获得这些数据的更好的表示,从而可以使噪音减小。那这里面咱们可使用re-weighting的操做,让咱们的这些点都包含一些其余点的信息,使得全部的数据可以更平滑一些。咱们定义这些re-weighting的参数为   ,咱们使用这些weights就可以获得一个点的新的表示   。

Self-attention是一个序列到序列的操做:一个向量的序列做为输入,一个向量的序列做为输出。咱们把输入的序列定义为   ,而且与它相关的输出向量是   。这两个向量的维度都是   。那么对于每个点   ,咱们均可以经过一个不一样的权重值,来将它转化为一个新的序列,这个新的序列就多是咱们原始序列的一个更好的表示,这些   就是一组attention的值。

来看一个动图的例子:

以上就是attention机制。

Self-attention 是怎么从 Attention 过分过来的 ?

Self-attention就本质上是一种特殊的attention。它和attention的区别我会在下一个章节介绍,这里先来介绍下self-attention,这种应用在transformer中最重要的结构之一。

上面咱们介绍了attention机制,它可以帮咱们找到子序列和全局的attention的关系,也就是找到权重值   。self-attention对于attention的变化,其实就是寻找权重值 的过程不一样。下面,咱们来看看self-attention的运算过程。

为了可以产生输出的向量   ,self-attention实际上是对全部的输入作了一个加权平均的操做,这个公式和上面的attention是一致的。

 表明整个序列的长度,而且   个权重的相加之和等于1。值得一提的是,这里的   并非一个须要神经网络学习的参数,它是来源于   和   的之间的计算的结果(这里   的计算发生了变化)。它们之间最简单的一种计算方式,就是使用点积的方式。

 和   是一对输入和输出。对于下一个输出的向量   ,咱们有一个全新的输入序列和一个不一样的权重值。

这个点积的输出的取值范围在负无穷和正无穷之间,因此咱们要使用一个softmax把它映射到   之间,而且要确保它们对于整个序列而言的和为1。

以上这些就是self-attention最基本的操做,其余的部分咱们须要完整的Trasnformer才可以解释,这些咱们会在后面的内容中说明。

self-attention的基础操做,没有包含softmax操做

Attention 和 self-attention 的区别是什么 ?

这里有几个重要的区别,能够帮助你们更好的区分在不一样任务中的使用方法:

  1. 在神经网络中,一般来讲你会有输入层(input),应用激活函数后的输出层(output),在RNN当中你会有状态(state)。若是attention (AT) 被应用在某一层的话,它更多的是被应用在输出或者是状态层上,而当咱们使用self-attention(SA),这种注意力的机制更多的实在关注input上。
  2. Attention (AT) 常常被应用在从编码器(encoder)转换到解码器(decoder)。好比说,解码器的神经元会接受一些AT从编码层生成的输入信息。在这种状况下,AT链接的是两个不一样的组件(component),编码器和解码器。可是若是咱们用SA,它就不是关注的两个组件,它只是在关注你应用的那一个组件。那这里他就不会去关注解码器了,就好比说在Bert中,使用的状况,咱们就没有解码器。
  3. SA能够在一个模型当中被屡次的、独立的使用(好比说在Transformer中,使用了18次;在Bert当中使用12次)。可是,AT在一个模型当中常常只是被使用一次,而且起到链接两个组件的做用。
  4. SA比较擅长在一个序列当中,寻找不一样部分之间的关系。好比说,在词法分析的过程当中,可以帮助去理解不一样词之间的关系。AT却更擅长寻找两个序列之间的关系,好比说在翻译任务当中,原始的文本和翻译后的文本。这里也要注意,在翻译任务重,SA也很擅长,好比说Transformer。
  5. AT能够链接两种不一样的模态,好比说图片和文字。SA更多的是被应用在同一种模态上,可是若是必定要使用SA来作的话,也能够将不一样的模态组合成一个序列,再使用SA。
  6. 对我来讲,大部分状况,SA这种结构更加的general,在不少任务做为降维、特征表示、特征交叉等功能尝试着应用,不少时候效果都不错。

Self-attention 为何能 work ?

上面描述的方法看起来彷佛很简单,可是它为何可以work呢?为了可以创建起直观的感觉,让咱们来看看一种标准的推荐电影的方法,看看是否能获得一些启发。

假设你正在运营一家在线看电影的网站,你有一些电影和一些用户,你想要把合适的电影推荐给你的用户。你该怎么办呢?

一种方法是这样的,给你的电影手动的建立一些特征(feature),好比说这部电影关于爱情的部分有多少,关于动做的部分有多少;而后咱们再去对用户的特征进行分析,好比说用户A对于爱情电影的喜好程度有多少,对动做电影的喜好程度有多少。若是咱们按照上述方式构建了用户和电影的两个矩阵,那么它们的点积就会给你一个分数,这个分数就表明了用户对于某种电影的喜好程度。

经过上面的这种计算方式,咱们就可以获得一些score值。这些值有正数也有负数。好比说,电影是一部关于爱情的电影,而且用户也很喜欢爱情电影,那么这个分值就是一个正数;若是电影是关于爱情的,可是用户却不喜欢爱情电影,那么这个分值就会是一个负值。

还有,这个分值的大小也表示了在某个属性上,它的程度是多大:好比说某一部电影,可能它的内容中只有一点点是关于爱情的,那么它的这个值就会很小;或者说有个用户他不是很喜欢爱情电影,那么这个值的绝对值就会很大,说明他对这种电影的偏见是很大的。

显而易见,上面说的这种方法在现实中是很难实现的。咱们很难去人工标注上千万的电影的特征,和用户喜欢哪一种类型的电影的分值。

那么,咱们有没有一种方法去经过问一小部分人,经过收集他们对电影的喜爱,来经过一些算法来优化找出用户对于电影喜好程度这个模型的参数呢?固然是有的,那就是FM算法,这个不是调频多少多少兆赫的那个FM,而是Factorization Machine。这个算法就是能经过左边的这个用户-电影矩阵,找到用户对于不一样特征的喜爱程度。

上面右边的矩阵是怎么来的呢?咱们把上面的这个问题稍微的简化如下,只当作是一个和用户和物品两个维度相关的task,那其实咱们就能够经过估计两个矩阵的点乘的形式,来对原有的矩阵表示。这两个向量中表示的就分别是用户的embedding和电影的embedding。咱们反过来思考下,这种办法的核心思想就是是经过两个低维小矩阵(一个表明用户embedding矩阵,一个表明物品embedding矩阵)的点乘计算,来模拟真实用户点击或评分产生的大的协同信息稀疏矩阵,本质上是编码了用户和物品协同信息的降维模型。

当咱们想要看,某个用户对于某个电影的喜爱程度时,只须要用他们的embedding相乘,就能获得相应的score了。

虽然,咱们这里的两个embedding并无直接的告诉咱们,里面每一个维度的参数的含义是什么,可是当你按照这种方法求的最后的参数的时候,这些参数都可以描述某种有实际含义的特征上。

上面的这个过程,就和咱们使用下面这个公式来表示attention的想法是一致的。

以上这些就是self-attention中为何使用点乘的方法而且能work的缘由了。那再让咱们看一个在天然语言处理中使用self-attention的例子。为了应用self-attention,咱们给每个在词表中的单词   一个embeding向量   (这个是咱们经过一些NLP方法学习到的)。这也是咱们在序列模型中常见的embeding layer。它会把单词  转换成向量的形式

若是咱们对这些向量序列进行self-attention的处理,那么就会生成一个新的向量序列

这其中   就是全部在第一个序列中的embedding向量的加权和,权重值就是   的点积。

上文中咱们也提到了,   是咱们学习到的embedding向量,它是   这个单词向量化的表示。在大部分的场景中,   这个单词和句子中的其余单词没有很强的相关性,所以,咱们就会期待   和其余单词的点积结果应该比较小或者是一个负值。那再看  这个单词,为了可以解释这个单词,咱们但愿可以知道是谁在   ,那在这句话当中,   和   的点积就应该有一个比较大的正的值。

以上这些,就是在self-attention背后一些直觉上的含义。点积操做很好的表示了输入语句中两个向量之间的相关性。

在咱们继续下面的内容以前,很是有必要作一个小的总结。

  • 到目前为止,咱们尚未用到须要学习的参数。基础的self-attention实际上彻底取决于咱们建立的输入序列,上游的embeding layer驱动着self-attention学习对于文本语义的向量表示。
  • Self-attention看到的序列只是一个集合(set),不是一个序列,它并无顺序。若是咱们从新排列集合,输出的序列也是同样的。后面咱们要使用一些方法来缓和这种没有顺序所带来的信息的缺失。可是值得一提的是,self-attention自己是忽略序列的天然输入顺序的。

怎么用 Pytorch 实现self-attention ?

我不能实现的,也是我没有理解的。
-- 费曼

因此,咱们将一块儿从头开始写一个self-attention。咱们这里将会使用Pytorch来实现。

一个简单的实现方法就是循环全部的向量,去计算出权重和输出,可是这样的方法明显太慢了。因此咱们要作的第一件事就是怎么使用矩阵乘法的形式来表达self-attention。

咱们首先来表示输入,一个   维的由   个向量组成的序列构成的矩阵   。包含一个batch的参数   ,咱们会得倒一个维度为   的张量。

全部的点积的结果   也构成一个矩阵,咱们能够简单的使用   乘以它的转置获得。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
import torchimport torch.nn.functional as F
# assume we have some tensor x with size (b, t, k)x = ...
raw_weights = torch.bmm(x, x.transpose(1, 2))# - torch.bmm is a batched matrix multiplication. It # applies matrix multiplication over batches of # matrices.

而后咱们把权重值   转换成正值而且确保它们的和为1,咱们使用一个row-wise的softmax。

 
   
weights = F.softmax(raw_weights, dim=2)

最后,咱们计算输出的序列,咱们只须要使用权重 乘以矩阵   。这样咱们就获得了维度为 的矩阵   。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
y = torch.bmm(weights, x)以上,通过两个简单的矩阵乘法和一个softmax,咱们就获得了self-attention。

Transformer 的做者对 Self-attention 作了哪些 tricks ?

实际在Transformer的实现过程当中,做者使用了三个tricks。下面就来一个个的聊一聊这几个tricks。

1) Queries, keys and values

咱们回顾如下上面所说道的self-attention的内容,上面咱们也提到了,在这样一个模型当中,是没有使用到能够学习参数的,那咱们能不能使用一些参数,来让整个结构更加的flexable。就是因为这样的想法,诞生了query,key和value这些参数。

为了可以更清楚的说明,咱们使用图片来稍微回顾下,以前讲过的self-attention,以下图。

在整个计算的过程当中,你们会发现,咱们使用了三次向量   这个文本的表示来作计算,那在Transformer中,就是把这几个变量参数化,使用能够学习的参数来替代,这里咱们分别使用key、query和value三个可学习的向量来表示,这里记为  ,   ,  ,经过下面的计算,来获得re-weighting的向量   。

经过图形化的方法表示以下:

Linear层是一个没有bias的全链接层,其实就是一个点乘。红色的箭头表示的是反向传播的过程。经过方向传播,key,query,value就可以学习到一个合理的表示。那么这里面key,queue,value分别学习到的是什么呢?这个可能并无一个官方的解释,可是经过这三个名称的命名方式,咱们能够大体的猜想。

这种命名的方式来源于搜索领域,假设咱们有一个key-value形式的数据集,就好比说是咱们知乎的文章,key就是文章的标题,value就是咱们文章的内容,那这个搜索系统就是但愿,可以在咱们输入一个query的时候,可以惟一返回一篇最咱们最想要的文章。那在self-attention中实际上是对这个task作了一些退化的处理,咱们优化并非返回一篇文章,而是返回全部的文章value,而且使用key和query计算出来的相关权重,来找到一篇得分最高的文章。

2) 缩放点积的值(Scaling the dot product)

Softmax 函数对很是大的输入很敏感。这会使得梯度的传播出现为问题(kill the gradient),而且会致使学习的速度降低(slow down learning),甚至会致使学习的中止。那若是咱们使用   来对输入的向量作缩放,就可以防止进入到softmax的函数增加过大:

这里分母为何要使用   呢?咱们想象一下,当咱们有一个全部的值都为   的在   空间内的值。那它的欧式距离就为   。除以 其实就是在除以向量平均的增加长度。

3) Multi-head attention

最后,咱们必需要知道的是,在真实的语言环境中,每个词和不一样的词,都有不一样的关系。咱们考虑下面这个例子,  。咱们能够看到   和不一样的部分有不一样的关系。首先,   表示谁在进行   的动做,   表达被   的东西是什么,   表示谁在接受东西。咱们就能够用不一样的self-attention mechanism来补货这些不一样的关系。以下图:

若是咱们只进行single self-attention,全部的信息都会被加和到一块儿。若是是   给     ,那么咱们获得的   就是同样的了,可是其实意思应发生了改变。

因此,咱们能够经过增长多个self-attention这样的结构,来给self attention更强的辨别能力,咱们就有了更多个   的矩阵   ,那咱们把这些不一样的self-attention就叫作attention head。

对于输入   每个attention head都会生成一个向量   。咱们把这些向量进行concat操做,而且把concat的结果传递给一个全链接层,使得向量的维度从新回到k。这样咱们就获得了一个表示能力更强的向量。那应用了multi-head后的attention结构就变成了下图这样子:

有了这个结构,咱们就能够把多个multi-head attention结构堆叠起来,从而获得更增强大的能力。

Narrow and wide self-attention
一般,咱们有两种方式来实现multi-head的self-attention。默认的作法是咱们会把embedding的向量   切割成块,好比说咱们有一个256大小的embedding vector,而且咱们使用8个attention head,那么咱们会把这vector切割成8个维度大小为32的块。对于每一块,咱们生成它的queries,keys和values,它们每个的size都是32,那么也就意味着咱们矩阵   的大小都是   。
那还有一种方法是,咱们可让矩阵 的大小都是   ,而且把每个attention head都应用到所有的256维大小的向量上。第一种方法的速度会更快,而且可以更节省内存,第二种方法可以获得更好的结果(同时也花费更多的时间和内存)。这两种方法分别叫作narrow and wide self-attention。

怎么用 Pytorch/Tensorflow2.0 实如今 Transfomer 中的self-attention ?

实现Transformer中的self-attention过程,咱们一共有8个步骤:

  1. 准备输入
  2. 初始化参数
  3. 获取key,query和value
  4. 给input1计算attention score
  5. 计算softmax
  6. 给value乘上score
  7. 给value加权求和获取output1
  8. 重复步骤4-7,获取output2,output3

1 准备输入

为了简单起见,咱们使用3个输入,每一个输入都是一个4维的向量。

 
   
Input 1: [1, 0, 1, 0] Input 2: [0, 2, 0, 2]Input 3: [1, 1, 1, 1]
2 初始化参数

每个输入都有三个表示,分别为key(橙黄色)query(红色)value(紫色)。好比说,每个表示咱们但愿是一个3维的向量。因为输入是4维,因此咱们的参数矩阵为   维。

后面咱们会看到,value的维度,一样也是咱们输出的维度。

为了可以获取这些表示,每个输入(绿色)要和key,query和value相乘,在咱们例子中,咱们使用以下的方式初始化这些参数。

key的参数:

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[[0, 0, 1], [1, 1, 0], [0, 1, 0], [1, 1, 0]]query的参数:
   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[[1, 0, 1], [1, 0, 0], [0, 0, 1], [0, 1, 1]]value的参数:
 
   
[[0, 2, 0], [0, 3, 0], [1, 0, 3], [1, 1, 0]]

一般在神经网络的初始化过程当中,这些参数都是比较小的,通常会在_Gaussian,

Xavier and Kaiming distributions随机采样完成。_

3 获取key,query和value

如今咱们有了三个参数,如今就让咱们来获取实际上的key,query和value。

对于input1的key的表示为:

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[0, 0, 1][1, 0, 1, 0] x [1, 1, 0] = [0, 1, 1] [0, 1, 0] [1, 1, 0]

使用相同的参数获取input2的key的表示:

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[0, 0, 1][0, 2, 0, 2] x [1, 1, 0] = [4, 4, 0] [0, 1, 0]               [1, 1, 0]

使用参数获取input3的key的表示:

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[0, 0, 1][1, 1, 1, 1] x [1, 1, 0] = [2, 3, 1] [0, 1, 0]               [1, 1, 0]

那使用向量化的表示为:

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[0, 0, 1][1, 0, 1, 0] [1, 1, 0] [0, 1, 1][0, 2, 0, 2] x [0, 1, 0] = [4, 4, 0][1, 1, 1, 1] [1, 1, 0] [2, 3, 1]

让咱们对value作相同的事情。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[0, 2, 0][1, 0, 1, 0] [0, 3, 0] [1, 2, 3] [0, 2, 0, 2] x [1, 0, 3] = [2, 8, 0][1, 1, 1, 1] [1, 1, 0] [2, 6, 3]

query也是同样的。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[1, 0, 1][1, 0, 1, 0] [1, 0, 0] [1, 0, 2][0, 2, 0, 2] x [0, 0, 1] = [2, 2, 2][1, 1, 1, 1] [0, 1, 1] [2, 1, 3]

在咱们实际的应用中,有可能会在点乘后,加上一个bias的向量。

4 给input1计算attention score

为了获取input1的attention score,咱们使用点乘来处理全部的key和query,包括它本身的key和value。这样咱们就可以获得3个key的表示(由于咱们有3个输入),咱们就得到了3个attention score(蓝色)。

 
   
 [0, 4, 2][1, 0, 2] x [1, 4, 3] = [2, 4, 4]            [1, 0, 1]

这里咱们须要注意一下,这里咱们只有input1的例子。后面,咱们会对其余的输入的query作相同的操做。

5 计算softmax

给attention score应用softmax。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
softmax([2, 4, 4]) = [0.0, 0.5, 0.5]

6 给value乘上score

使用通过softmax后的attention score乘以它对应的value值(紫色),这样咱们就获得了3个weighted values(黄色)。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
1: 0.0 * [1, 2, 3] = [0.0, 0.0, 0.0]2: 0.5 * [2, 8, 0] = [1.0, 4.0, 0.0]3: 0.5 * [2, 6, 3] = [1.0, 3.0, 1.5]

7 给value加权求和获取output1

把全部的weighted values(黄色)进行element-wise的相加。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[0.0, 0.0, 0.0]+ [1.0, 4.0, 0.0]+ [1.0, 3.0, 1.5]-----------------= [2.0, 7.0, 1.5]

获得结果向量[2.0, 7.0, 1.5](深绿色)就是ouput1的和其余key交互的query representation。

8 重复步骤4-7,获取output2,output3

如今,咱们已经完成output1的所有计算,咱们要对input2和input3也重复的完成步骤4~7的计算。这相信你们本身是能够实现的。

实现的代码,我给你们准备了jupyter notebook,你们能够clone下面的repo,本身一步步的完成代码的调试,加深对于self-attention的理解。

https://github.com/BSlience/transformer-all-in-onegithub.com

完整的 Transformer Block 是什么样的?

Transformer 模型来源于Google发表的一篇论文 “Attention Is All You Need”,截止到我查询的时候,这篇文章已经有17000+的引用量,可见这篇文章的影响力。但愿你们能在有一些了解的基础上,可以本身读一下这篇文章。

https://arxiv.org/pdf/1706.03762.pdfarxiv.org

上面这张图片是论文原文中的图片,我把他们放在了一块儿。这几个模型分别表明了 Transformer 在翻译任务中的应用(左),Multi-Head Attention(中),self-attention(右)。在前面的文章中,咱们已经讲解过 self-attetnion(右),这里和咱们以前讲解过的稍有不一样的是多了一个粉色的方框 Mask(opt),这个是用来左Mask任务的,括号中的opt表示是一个可选项,本篇先不提,后面咱们再细说;也讲解了 Multi-Head Attention(中),多头的注意力机制;本篇文章,咱们把重点集中在最左侧的图片,来看看 Transformer 结构。

咱们来把这幅图放大来看,这个模型结构分为左右两个部分,由于原文中是用Transtormer来作翻译任务的,你们可能知道一般咱们作翻译任务的时候,都使用 Encoder-Decoder 的架构来作。这里面的左侧对应着 Encoder ,右侧就是 Decoder 。Encoder 本质的目的就是对 input 生成一种中间表示,Decoder目的就是对这种中间表示作解码,生成目标语言的ouput。你们会发现两边的结构基本上是一致的,为了着重的研究Transformer结构,咱们把视线聚焦在Encoder的部分。

你们会在图中看到,这里有个   的符号,这表示了右侧的结构能够被   次堆叠,这就像是咱们在使用神经网络的时候,能够   次堆叠 layer 同样,一般咱们把这样的一种由多个 layer 组成的模块叫作 block,这种 block 就是一种比 layer 更大规模的可复用单元。那么,接下来咱们把重点放到 Transformer Block 上。

在这样一个block中,是由几个重要的组件构成的:

  1. self-attention layer
  2. normalization layer
  3. feed forward layer
  4. another normalization layer

在这样四个组件中的两个 normalization layer 以前,使用了残差网络(Residula connections)进行了链接。实际上,这几个组件之间的顺序并无被彻底的定死,这里面最重要的事情是,要联合使用 self-attention 和 feed forward layer,而且要在它们之间增长normalization 和 residual connections。

Normaliztion 和 residual connections 是咱们常用的,帮助加快深度神经网络训练速度和准确率的 tricks。

这里咱们能够先看看使用 Pytorch 实现这样一个 block 是什么样子的。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class TransformerBlock(nn.Module): def __init__(self, k, heads): super().__init__()
self.attention = SelfAttention(k, heads=heads)
self.norm1 = nn.LayerNorm(k) self.norm2 = nn.LayerNorm(k)
self.ff = nn.Sequential( nn.Linear(k, 4 * k), nn.ReLU(), nn.Linear(4 * k, k))
def forward(self, x): attended = self.attention(x) x = self.norm1(attended + x) fedforward = self.ff(x)    return self.norm2(fedforward + x)

咱们这里主观的选择4倍输入大小做为咱们 feedforward 层的维度,这个值使用的越小就越节省内存,可是相应的表示性也会变弱;可是,最小也应该大于咱们输入的维度。

怎么捕获序列中的顺序信息呢 ?

经过使用 Transformer 咱们能够获得一个对于输入信息的 embedding vector,可是这里你们可能也会发现,咱们并无利用好序列的输入顺序。好比说   和   ,它们获得的 vector 是同样的。显然,这并非但愿看到的。因此,咱们要给模型增长捕获序列顺序的能力。咱们应该怎么作呢?

办法也很简单,咱们建立一个和输入序列等长的新序列,这个序列里包含序列中的顺序信息,咱们把这个序列和原有序列进行相加,从而获得输入到 Transformer 的序列。那应该怎样表示序列中的位置信息呢?

这里咱们有两种方法:

  • position embeddings

咱们简单的 embed 位置信息,就像咱们对待每个输入同样。好比说咱们以前对每一个单词 建立一个 vector  ,那咱们也对每个位置生成一个向量   。而后咱们使用模型的学习能力来学习到这些位置的 vector。可是这种方法会存在一个问题,那就是咱们须要在训练的过程当中让模型见过全部的须要在预测阶段使用的位置 vector,不然模型就不知道相应位置的 vector。

  • position encodings

position encoding的方法其实和 position embedding 的方法很类似,咱们都是但愿可以经过一个位置的 vector 来表示位置的信息,让模型学习到这个信息。可是,这里稍有不一样的是,encoding 的方法是由咱们选择一个 function 来生成每一个位置的 vector 的,而且让模型网络去找出该如何去理解这些 encoding vector。这样作的好处是,对于一个选择的比较好的function,网络模型可以处理那些在训练阶段没有见过的序列位置 vector(虽然这也并非说这些没见过的位置 vector 必定可以表现的很好,可是好在是咱们能够有比较直接的方法来测试他们)。这种方法也是 Transformer 的做者选择的方法,让咱们看看做者是怎么设计这个 function 的。

做者使用上面的两个 functions 来生成一个2维的矩阵常量,   表示在序列中的顺序,   表示序列中数据 vector 的维度,   表示输出的维度大小,以下图所示:

这里我给出一个使用 Pytorch 实现的 PositionEncoder 的代码:

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class PositionalEncoder(nn.Module): def __init__(self, d_model, max_seq_len = 80): super().__init__() self.d_model = d_model # 根据pos和i建立一个常量pe矩阵 pe = torch.zeros(max_seq_len, d_model) for pos in range(max_seq_len): for i in range(0, d_model, 2): pe[pos, i] = \ math.sin(pos / (10000 ** ((2 * i)/d_model))) pe[pos, i + 1] = \ math.cos(pos / (10000 ** ((2 * (i + 1))/d_model))) pe = pe.unsqueeze(0) self.register_buffer('pe', pe) def forward(self, x): # 让 embeddings vector 相对大一些 x = x * math.sqrt(self.d_model) # 增长位置常量到 embedding 中 seq_len = x.size(1) x = x + Variable(self.pe[:,:seq_len], \ requires_grad=False).cuda() return x

上面的这个模块中,咱们在数据的 embedding vector 增长了 position encoding 的信息。

让 embeddings vector 在增长 postion encoing 以前相对大一些的操做,主要是为了让position encoding 相对的小,这样会让原来的 embedding vector 中的信息在和 position encoding 的信息相加时不至于丢失掉。

怎么用 Pytorch 实现一个完整的 Transformer 模型?

  1. Tokenize
  2. Input Embedding
  3. Positional Encoder
  4. Transformer Block
  5. Encoder
  6. Decoder
  7. Transformer

1 Tokenize

首先,咱们要对输入的语句作分词,这里我使用 spacy 来完成这件事,你也能够选择你喜欢的工具来作。

 
   
class Tokenize(object):  def __init__(self, lang): self.nlp = importlib.import_module(lang).load()  def tokenizer(self, sentence): sentence = re.sub( r"[\*\"“”\n\\…\+\-\/\=\(\)‘•:\[\]\|’\!;]", " ", str(sentence)) sentence = re.sub(r"[ ]+", " ", sentence) sentence = re.sub(r"\!+", "!", sentence) sentence = re.sub(r"\,+", ",", sentence) sentence = re.sub(r"\?+", "?", sentence) sentence = sentence.lower()        return [tok.text for tok in self.nlp.tokenizer(sentence) if tok.text != " "]

2 Input Embedding

Token Embedding

给语句分词后,咱们就获得了一个个的 token,咱们以前有说过,要对这些token作向量化的表示,这里咱们使用 pytorch 中torch.nn.Embedding 让模型学习到这些向量。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class Embedding(nn.Module): def __init__(self, vocab_size, d_model): super().__init__() self.d_model = d_model self.embed = nn.Embedding(vocab_size, d_model) def forward(self, x): return self.embed(x)

Positional Encoder

前文中,咱们有说过,要把 token 在句子中的顺序也加入到模型中,让模型进行学习。这里咱们使用的是 position encodings 的方法。

 
   
class PositionalEncoder(nn.Module):
def __init__(self, d_model, max_seq_len = 80): super().__init__() self.d_model = d_model # 根据pos和i建立一个常量pe矩阵 pe = torch.zeros(max_seq_len, d_model) for pos in range(max_seq_len): for i in range(0, d_model, 2): pe[pos, i] = \ math.sin(pos / (10000 ** ((2 * i)/d_model))) pe[pos, i + 1] = \ math.cos(pos / (10000 ** ((2 * (i + 1))/d_model))) pe = pe.unsqueeze(0) self.register_buffer('pe', pe) def forward(self, x): # 让 embeddings vector 相对大一些 x = x * math.sqrt(self.d_model) # 增长位置常量到 embedding 中 seq_len = x.size(1) x = x + Variable(self.pe[:,:seq_len], \ requires_grad=False).cuda()        return x
3 Transformer Block

有了输入,咱们接下来就要开始构建 Transformer Block 了,Transformer Block 主要是有如下4个部分构成的:

  1. self-attention layer
  2. normalization layer
  3. feed forward layer
  4. another normalization layer

它们之间使用残差网络进行链接,详细在上文同一个图下有描述,这里就再也不赘述了。


Attention

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
def attention(q, k, v, d_k, mask=None, dropout=None): scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k) # mask掉那些为了padding长度增长的token,让其经过softmax计算后为0 if mask is not None: mask = mask.unsqueeze(1) scores = scores.masked_fill(mask == 0, -1e9) scores = F.softmax(scores, dim=-1) if dropout is not None: scores = dropout(scores) output = torch.matmul(scores, v) return output

这个 attention 的代码中,使用 mask 的机制,这里主要的意思是由于在去给文本作 batch化的过程当中,须要序列都是等长的,不足的部分须要 padding。可是这些 padding 的部分,咱们并不想在计算的过程当中起做用,因此使用 mask 机制,将这些值设置成一个很是大的负值,这样才能让 softmax 后的结果为0。关于 mask 机制,在 Transformer 中有 attention、encoder 和 decoder 中,有不一样的应用,我会在后面的文章中进行解释。

MultiHead Attention

多头的注意力机制,用来识别数据之间的不一样联系,前文中的第二篇也已经聊过了。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class MultiHeadAttention(nn.Module): def __init__(self, heads, d_model, dropout = 0.1): super().__init__() self.d_model = d_model self.d_k = d_model // heads self.h = heads self.q_linear = nn.Linear(d_model, d_model) self.v_linear = nn.Linear(d_model, d_model) self.k_linear = nn.Linear(d_model, d_model) self.dropout = nn.Dropout(dropout) self.out = nn.Linear(d_model, d_model) def forward(self, q, k, v, mask=None): bs = q.size(0) # perform linear operation and split into N heads k = self.k_linear(k).view(bs, -1, self.h, self.d_k) q = self.q_linear(q).view(bs, -1, self.h, self.d_k) v = self.v_linear(v).view(bs, -1, self.h, self.d_k) # transpose to get dimensions bs * N * sl * d_model k = k.transpose(1,2) q = q.transpose(1,2) v = v.transpose(1,2) # calculate attention using function we will define next scores = attention(q, k, v, self.d_k, mask, self.dropout) # concatenate heads and put through final linear layer concat = scores.transpose(1,2).contiguous()\ .view(bs, -1, self.d_model) output = self.out(concat) return output

Layer Norm

这里使用 Layer Norm 来使得梯度更加的平稳,关于为何选择 Layer Norm 而不是选择其余的方法,有篇论文对此作了一些研究,Rethinking Batch Normalization in Transformers,对这个有兴趣的能够看看这篇文章。

https://arxiv.org/pdf/2003.07845.pdfarxiv.org

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class NormLayer(nn.Module): def __init__(self, d_model, eps = 1e-6): super().__init__() self.size = d_model # 使用两个能够学习的参数来进行 normalisation self.alpha = nn.Parameter(torch.ones(self.size)) self.bias = nn.Parameter(torch.zeros(self.size)) self.eps = eps def forward(self, x): norm = self.alpha * (x - x.mean(dim=-1, keepdim=True)) \ / (x.std(dim=-1, keepdim=True) + self.eps) + self.bias return norm

Feed Forward Layer

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class FeedForward(nn.Module): def __init__(self, d_model, d_ff=2048, dropout = 0.1): super().__init__() # We set d_ff as a default to 2048 self.linear_1 = nn.Linear(d_model, d_ff) self.dropout = nn.Dropout(dropout) self.linear_2 = nn.Linear(d_ff, d_model) def forward(self, x): x = self.dropout(F.relu(self.linear_1(x))) x = self.linear_2(x)

5 Encoder

Encoder 就是将上面讲解的内容,按照下图堆叠起来,完成将源编码到中间编码的转换。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class EncoderLayer(nn.Module):
def __init__(self, d_model, heads, dropout=0.1): super().__init__() self.norm_1 = Norm(d_model) self.norm_2 = Norm(d_model) self.attn = MultiHeadAttention(heads, d_model, dropout=dropout) self.ff = FeedForward(d_model, dropout=dropout) self.dropout_1 = nn.Dropout(dropout) self.dropout_2 = nn.Dropout(dropout) def forward(self, x, mask): x2 = self.norm_1(x) x = x + self.dropout_1(self.attn(x2,x2,x2,mask)) x2 = self.norm_2(x) x = x + self.dropout_2(self.ff(x2)) return x

class Encoder(nn.Module):
def __init__(self, vocab_size, d_model, N, heads, dropout): super().__init__() self.N = N self.embed = Embedder(vocab_size, d_model) self.pe = PositionalEncoder(d_model, dropout=dropout) self.layers = get_clones(EncoderLayer(d_model, heads, dropout), N) self.norm = Norm(d_model)
def forward(self, src, mask): x = self.embed(src) x = self.pe(x) for i in range(self.N): x = self.layers[i](x, mask) return self.norm(x)

6 Decoder

Decoder部分和 Encoder 的部分很是的类似,它主要是把 Encoder 生成的中间编码,转换为目标编码。后面我会在具体的任务中,来分析它和 Encoder 的不一样。


   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class DecoderLayer(nn.Module):
def __init__(self, d_model, heads, dropout=0.1): super().__init__() self.norm_1 = Norm(d_model) self.norm_2 = Norm(d_model) self.norm_3 = Norm(d_model) self.dropout_1 = nn.Dropout(dropout) self.dropout_2 = nn.Dropout(dropout) self.dropout_3 = nn.Dropout(dropout) self.attn_1 = MultiHeadAttention(heads, d_model, dropout=dropout) self.attn_2 = MultiHeadAttention(heads, d_model, dropout=dropout) self.ff = FeedForward(d_model, dropout=dropout)
def forward(self, x, e_outputs, src_mask, trg_mask): x2 = self.norm_1(x) x = x + self.dropout_1(self.attn_1(x2, x2, x2, trg_mask)) x2 = self.norm_2(x) x = x + self.dropout_2(self.attn_2(x2, e_outputs, e_outputs, \ src_mask)) x2 = self.norm_3(x) x = x + self.dropout_3(self.ff(x2)) return x

class Decoder(nn.Module):
def __init__(self, vocab_size, d_model, N, heads, dropout): super().__init__() self.N = N self.embed = Embedder(vocab_size, d_model) self.pe = PositionalEncoder(d_model, dropout=dropout) self.layers = get_clones(DecoderLayer(d_model, heads, dropout), N) self.norm = Norm(d_model)
def forward(self, trg, e_outputs, src_mask, trg_mask): x = self.embed(trg) x = self.pe(x) for i in range(self.N): x = self.layers[i](x, e_outputs, src_mask, trg_mask) return self.norm(x)

7 Transformer

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class Transformer(nn.Module): def __init__(self, src_vocab, trg_vocab, d_model, N, heads, dropout): super().__init__() self.encoder = Encoder(src_vocab, d_model, N, heads, dropout) self.decoder = Decoder(trg_vocab, d_model, N, heads, dropout) self.out = nn.Linear(d_model, trg_vocab) def forward(self, src, trg, src_mask, trg_mask): e_outputs = self.encoder(src, src_mask) d_output = self.decoder(trg, e_outputs, src_mask, trg_mask) output = self.out(d_output) return output

以上,就是 Transformer 实现的全过程,配套着 jupyter notebook 食用, 效果更加。

https://github.com/BSlience/transformer-all-in-onegithub.com

实现了上述这些,咱们就获得了一个 Transformer 中的结构。


点个在看 paper不断!

本文分享自微信公众号 - 视学算法(visualAlgorithm)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索