【NLP】(task2)图解attention+transformer(代码讲解)

学习心得

  • 本次学习先根据seq2seq任务特点,初始用的RNN,但为了解决RNN在两个相关性较大的时刻距离较远时,会产生较大的信息损失问题,即使引入LSTM能部分解决这种长距离依赖问题(治标不治本),所以提出transformer,补阅读Transformer 详解
  • 台大佬说的:看代码要分清自己的需求,这个代码是不用全部背诵或者默出来。你只需要看到transformer内部的各种变化,有印象就可了。实际用的时候不需要的,除非你需要改核心结构。搞清楚输入和输出以及核心部件的构造(这部分最好能默出来)。第三部分的将PyTorch实现Transformer库,相比于官方的版本,手写的这个少了较多的判定语句。

问题总结:
(1)为什么Transformer中加入了positional embedding?
答:初始的self-attention的这个layer的每一个input,对于sequence中的最前还是最后的东东,缺少了重要的位置信息。如在做磁性标记POS tagging时,动词很少出现在句首,所哟某一个词汇如果放在句首的话,那么它是动词的可能性很低。所以在一开始paper《Attention Is All You Need》中将位置的信息塞进去(如下图所示),每一个位置设定一个 vector(positional vector,用 e i e^i ei 来表示),上标 i 代表是位置,每一个不同的位置。
在这里插入图片描述
(2)Transformer中的残差网络结构(residual connecttion)作用是什么?
答:在transformer里面加了了一个设计——不只是输出这个vector,还要把这个vector加上它的input得到新的ouput。BN其实已经解决了梯度消失问题,resnet残差链接更多解决的是深层网络训练时候退化的问题,更多参考https://zhuanlan.zhihu.com/p/268308900
在这里插入图片描述
(3)Transformer中的softmax计算为什么需要除以 d k d_k dk?
答:如果我们计算句子中第一个位置单词的 Attention Score(注意力分数),那么第一个分数就是 q1 和 k1 的内积,第二个分数就是 q1 和 k2 的点积。以此类推。而每个Attention Score(注意力分数)除以 ( d k e y ) \sqrt(d_{key}) ( dkey) d k e y d_{key} dkey是 Key 向量的长度),当然也可以除以其他数,除以一个数是为了在反向传播时,求取梯度更加稳定( ps:在车万翔老师的书《自然语言处理》中说的是:避免因为向量维度d过大导致点积结果过大)。

(4)为什么 num_heads 的值需要能够被 embed_dim 整除
这是为了把词的隐向量长度平分到每一组,这样多组注意力也能够放到一个矩阵里,从而并行计算多头注意力。例如,8 组注意力可以得到 8 组 Z 矩阵,然后把这些矩阵拼接起来,得到最终的输出。如果最终输出的每个词的向量维度是 512,那么每组注意力的向量维度应该是512 ÷8=64。如果不能够整除,那么这些向量的长度就无法平均分配。

(5)Transformer中attention score计算时候如何mask掉padding位置?

文章目录

第1部分:Attention

一、seq2seq模型

NLP常用于生成任务的seq2seq结构。在李宏毅机器学习课程(【李宏毅深度学习CP12】Transformer(part1))中也提到,Seq2seq在各领域的应用效果挺好,如聊天机器人、QA问答系统、语法分析、目标检测等,在机器翻译、文本摘要、图像描述生成方面很出色。

谷歌翻译在 2016 年年末开始使用Seq2seq模型。有2篇开创性的论文(需要阅读):
(1)Sutskever等2014年发表的Sequence to Sequence Learning
with Neural Networks

(2)Cho等2014年发表的Learning Phrase Representations using RNN Encoder–Decoder
for Statistical Machine Translation
都对这些模型进行了解释。

一个序列到序列(seq2seq)模型,接收的输入是一个(单词、字母、图像特征)序列,输出是另外一个序列。一个训练好的模型如下图所示(将鼠标放在图上,图就会动起来):

在这里插入图片描述
动态图:seq2seq

在神经机器翻译中,一个序列是指一连串的单词。类似地,输出也是一连串单词。
在这里插入图片描述
动态图:translation

二、进一步理解细节

seq2seq模型是由编码器(Encoder)和解码器(Decoder)组成的。其中,编码器会处理输入序列中的每个元素,把这些信息转换为一个向量(称为上下文(context))。当我们处理完整个输入序列后,编码器把上下文(context)发送给解码器,解码器开始逐项生成输出序列中的元素。

在这里插入图片描述
动态图:encoder-decoder

在这里插入图片描述
动态图:encoder-decoder

这种机制,同样适用于机器翻译。在机器翻译任务中,上下文(context)是一个向量(基本上是一个数字数组)。

2.1 初探RNN

编码器和解码器在Transformer出现之前一般采用的是循环神经网络RNN。 关于RNN可以参考Luis Serrano写的一篇关于循环神经网络

在这里插入图片描述
图:上下文context对应图里中间一个浮点数向量。在下文中,我们会可视化这些向量,使用更明亮的色彩来表示更高的值,如上图右边所示

你可以在编写seq2seq模型的时候设置上下文向量的长度。这个长度是基于编码器 RNN 的隐藏层神经元的数量。上图展示了长度为 4 的向量,但在实际应用中,上下文向量的长度可能是 256,512 或者 1024。

根据设计,RNN 在每个时间步接受 2 个输入:

  • 输入序列中的一个元素(在解码器的例子中,输入是指句子中的一个单词,最终被转化成一个向量)
  • 一个 hidden state(隐藏层状态,也对应一个向量)

2.2 word embedding

如何把每个单词都转化为一个向量呢?我们使用一类称为 “word embedding” 的方法。这类方法把单词转换到一个向量空间,这种表示能够捕捉大量单词之间的语义信息(例如,king - man + woman = queen例子来源)。
在这里插入图片描述
图:我们在处理单词之前,需要把他们转换为向量。这个转换是使用 word embedding 算法来完成的。我们可以使用预训练好的 embeddings,或者在我们的数据集上训练自己的 embedding。通常 embedding 向量大小是 200 或者 300,为了简单起见,我们这里展示的向量长度是4。上图左边每个单词对应中间一个4维的向量。

2.3 RNN机制及其可视化

介绍完了单词向量/张量的基础知识,让我们回顾一下 RNN 的机制,并可视化这些 RNN 模型:
在这里插入图片描述

动态图:RNN 在第 2 个时间步,采用第 1 个时间步的 hidden state(隐藏层状态) 和第 2 个时间步的输入向量,来得到输出。在下文,我们会使用类似这种动画,来描述神经机器翻译模型里的所有向量。

在下面的可视化图形中,编码器和解码器在每个时间步处理输入,并得到输出。由于编码器和解码器都是 RNN,RNN 会根据当前时间步的输入,和前一个时间步的 hidden state(隐藏层状态),更新当前时间步的 hidden state(隐藏层状态)。

让我们看下编码器的 hidden state(隐藏层状态)。
注意,最后一个 hidden state(隐藏层状态)实际上是我们传给解码器的上下文(context)。
在这里插入图片描述
动态图:编码器相关

解码器也持有 hidden state(隐藏层状态),而且也需要把 hidden state(隐藏层状态)从一个时间步传递到下一个时间步。我们没有在上图中可视化解码器的 hidden state,是因为这个过程和解码器是类似的,我们现在关注的是 RNN 的主要处理过程。
现在让我们用另一种方式来可视化序列到序列(seq2seq)模型。下面的动画会让我们更加容易理解模型。这种方法称为展开视图。其中,我们不只是显示一个解码器,而是在时间上展开,每个时间步都显示一个解码器。通过这种方式,我们可以看到每个时间步的输入和输出。
在这里插入图片描述
动态图:解决码器相关

三、Attention 讲解

事实证明,上下文context向量是这类模型的瓶颈。这使得模型在处理长文本时面临非常大的挑战。
2014发布的Neural Machine Translation by Jointly Learning to Align and Translate
2015年发布的Effective Approaches to Attention-based Neural Machine Translation
这 2 篇论文提出并改进了一种叫做注意力attetion的技术,它极大地提高了机器翻译的质量。注意力使得模型可以根据需要,关注到输入序列的相关部分。
在这里插入图片描述
图:在第 7 个时间步,注意力机制使得解码器在产生英语翻译之前,可以将注意力集中在 “student” 这个词(在法语里,是 “student” 的意思)。这种从输入序列放大相关信号的能力,使得注意力模型,比没有注意力的模型,产生更好的结果。

让我们继续从高层次来理解注意力模型。一个注意力模型不同于经典的序列到序列(seq2seq)模型,主要体现在 2 个方面:

首先,编码器会把更多的数据传递给解码器。编码器把所有时间步的 hidden state(隐藏层状态)传递给解码器,而不是只传递最后一个 hidden state(隐藏层状态):
在这里插入图片描述
动态图: 更多的信息传递给decoder

第二,注意力模型的解码器在产生输出之前,做了一个额外的处理。为了把注意力集中在与该时间步相关的输入部分。解码器做了如下的处理:

  1. 查看所有接收到的编码器的 hidden state(隐藏层状态)。其中,编码器中每个 hidden state(隐藏层状态)都对应到输入句子中一个单词。
  2. 给每个 hidden state(隐藏层状态)一个分数(我们先忽略这个分数的计算过程)。
  3. 将每个 hidden state(隐藏层状态)乘以经过 softmax 的对应的分数,从而,高分对应的 hidden state(隐藏层状态)会被放大,而低分对应的 hidden state(隐藏层状态)会被缩小。

在这里插入图片描述
动态图:解决码器attention

这个加权平均的步骤是在解码器的每个时间步做的。
现在,让我们把所有内容都融合到下面的图中,来看看注意力模型的整个过程:

  1. 注意力模型的解码器 RNN 的输入包括:一个embedding 向量,和一个初始化好的解码器 hidden state(隐藏层状态)。
  2. RNN 处理上述的 2 个输入,产生一个输出和一个新的 hidden state(隐藏层状态 h4 向量),其中输出会被忽略。
  3. 注意力的步骤:我们使用编码器的 hidden state(隐藏层状态)和 h4 向量来计算这个时间步的上下文向量(C4)。
  4. 我们把 h4 和 C4 拼接起来,得到一个向量。
  5. 我们把这个向量输入一个前馈神经网络(这个网络是和整个模型一起训练的)。
  6. 前馈神经网络的输出的输出表示这个时间步输出的单词。
  7. 在下一个时间步重复这个步骤。

在这里插入图片描述
动态图:attention过程

可视化注意力

下图,我们使用另一种方式来可视化注意力,看看在每个解码的时间步中关注输入句子的哪些部分:
在这里插入图片描述
动态图:attention关注的词

注意:注意力模型不是无意识地把输出的第一个单词对应到输入的第一个单词。实际上,它从训练阶段学习到了如何在两种语言中对应单词的关系(在我们的例子中,是法语和英语)。下图展示了注意力机制的准确程度(图片来自于上面提到的论文):
在这里插入图片描述
附:基于 TensorFlow 的 神经机器翻译 (seq2seq) 指南

第2部分:transformer

在这里插入图片描述

一、RNN和Transformer的不同

下文翻译自illustrated-transformer文章从输入开始,一步一步演示了数据在 Transformer 中的流动过程。下文最后是代码实现,一个基本的 Self Attention 以及 multi-head attention 的矩阵运算。Transformer 依赖于 Self Attention 的知识。

2017 年,Google 提出了 Transformer 模型,用 Self Attention 的结构,取代了以往 NLP 任务中的 RNN 网络结构,在 WMT 2014 Englishto-German 和 WMT 2014 English-to-French两个机器翻译任务上都取得了当时 SOTA 的效果。

(1)在 RNN 中,每一个 time step 的计算都依赖于上一个 time step 的输出,这就使得所有的 time step 必须串行化,无法并行计算,如下图所示。
在这里插入图片描述
(2)transformer模型的其中一个优点,就是使得模型训练过程能够并行计算。在Transformer 中,所有 time step 的数据,都是经过 Self Attention 计算,使得整个运算过程可以并行化计算
在这里插入图片描述

二、从整体宏观来理解 Transformer

首先,我们将整个模型视为黑盒。在机器翻译任务中,接收一种语言的句子作为输入,然后将其翻译成其他语言输出。
在这里插入图片描述
图:input-output

中间部分的 Transformer 可以拆分为 2 部分:左边是编码部分(encoding component),右边是解码部分(decoding component)。
在这里插入图片描述
图:encoder-decoder

其中编码部分是多层的编码器(Encoder)组成(Transformer 的论文中使用了 6 层编码器,这里的层数 6 并不是固定的,你也可以根据实验效果来修改层数)。同理,解码部分也是由多层的解码器(Decoder)组成(论文里也使用了 6 层的解码器)。
在这里插入图片描述
图:翻译例子

encoder由多层编码器组成,每层编码器在结构上都是一样的,但不同层编码器的权重参数是不同的。每层编码器里面,主要由以下两部分组成

  • Self-Attention Layer
  • Feed Forward Neural Network(前馈神经网络,缩写为 FFNN)

在这里插入图片描述
图:单层transformer encoder

输入编码器的文本数据,首先会经过一个 Self Attention 层,这个层处理一个词的时候,不仅会使用这个词本身的信息,也会使用句子中其他词的信息(你可以类比为:当我们翻译一个词的时候,不仅会只关注当前的词,也会关注这个词的上下文的其他词的信息)。本文后面将会详细介绍 Self Attention 的内部结构。

接下来,Self Attention 层的输出会经过前馈神经网络。
同理,解码器也具有这两层,但是这两层中间还插入了一个 Encoder-Decoder Attention 层——这个层能帮助解码器聚焦于输入句子的相关部分(类似于 seq2seq 模型 中的 Attention)。
在这里插入图片描述

三、从细节来理解 Transformer

下面看输入的张量数据在 Transformer 中运算最终得到输出的过程。

3.1 Transformer 的输入

和通常的 NLP 任务一样,我们首先会使用词嵌入算法(embedding algorithm),将每个词转换为一个词向量。实际中向量一般是 256 或者 512 维。为了简化起见,这里将每个词的转换为一个 4 维的词向量。

那么整个输入的句子是一个向量列表,其中有 3 个词向量。在实际中,每个句子的长度不一样,我们会取一个适当的值,作为向量列表的长度。如果一个句子达不到这个长度,那么就填充全为 0 的词向量;如果句子超出这个长度,则做截断。(句子长度是一个超参数,通常是训练集中的句子的最大长度,你可以尝试不同长度的效果。)

在这里插入图片描述
图:个词向量

3.3 Encoder(编码器)

编码器(Encoder)接收的输入都是一个向量列表,输出也是大小同样的向量列表,然后接着输入下一个编码器。第一 个/层 编码器的输入是词向量而后面的编码器的输入是上一个编码器的输出。下面,我们来看这个向量列表在编码器里面是如何流动的。

在这里插入图片描述
图:输入encoder
每个单词转换成一个向量之后,进入self-attention层,每个位置的单词得到新向量,然后再输入FFN神经网络。
下面再看一个2个单词的例子:
在这里插入图片描述
图:一层传一层
每个位置的词都经过 Self Attention 层,得到的每个输出向量都单独经过前馈神经网络层,每个向量经过的前馈神经网络都是一样的

3.3 Self-Attention 整体理解

假设我们想要翻译的句子是:

The animal didn't cross the street because it was too tired

这个句子中的 it 是一个指代词,那么 it 指的是什么呢?它是指 animal 还是street?这个问题对人来说,是很简单的,但是对算法来说并不是那么容易。当模型在处理(翻译)it 的时候,Self Attention机制能够让模型把it和animal关联起来

同理,当模型处理句子中的每个词时, S e l f Self Self A t t e n t i o n Attention Attention机制使得模型不仅能够关注这个位置的词,而且能够关注句子中其他位置的词,作为辅助线索,进而可以更好地编码当前位置的词。

如果你熟悉 RNN,回忆一下:RNN 在处理一个词时,会考虑前面传过来的hidden state,而 h i d d e n hidden hidden s t a t e state state就包含了前面的词的信息。而 Transformer 使用Self Attention机制,会把其他单词的理解融入处理当前的单词
在这里插入图片描述
图:一个词和其他词的attention
如上图可视化图所示,当我们在第五层编码器中(编码部分中的最后一层编码器)编码“it”时,有一部分注意力集中在“The animal”上(这两个单词在图中额颜色更深),并且把这两个词的信息融合到了"it"这个单词中。

四、Self-Attention 的细节

4.1 计算Query 向量,Key 向量,Value 向量

下面我们先看下如何使用向量来计算 Self Attention,然后再看下如何使用矩阵来实现 Self Attention。(矩阵运算的方式,使得 Self Attention 的计算能够并行化,这也是 Self Attention 最终的实现方式)。

计算 Self Attention 的第 1 步是:对输入编码器的每个词向量,都创建 3 个向量,分别是:Query 向量,Key 向量,Value 向量。这 3 个向量是词向量分别和 3 个矩阵相乘得到的,而这个3个矩阵是我们要学习的参数

注意,这 3 个新得到的向量一般比原来的词向量的长度更小。假设这 3 个向量的长度是 d k e y d_{key} dkey,而原始的词向量或者最终输出的向量的长度是 512(这 3 个向量的长度,和最终输出的向量长度,是有倍数关系的)。关于 Multi-head Attention,后面会给出实际代码。这里为了简化,假设只有一个 head 的 Self-Attention。

在这里插入图片描述

图:Q,K,V

上图中,有两个词向量:Thinking 的词向量 x1 和 Machines 的词向量 x2。以 x1 为例,X1 乘以 WQ 得到 q1(在在上图就是1×4的 x 1 x1 x1矩阵和 4×3 的WQ矩阵相乘),q1 就是 X1 对应的 Query 向量。同理,X1 乘以 WK 得到 k1,k1 是 X1 对应的 Key 向量;X1 乘以 WV 得到 v1,v1 是 X1 对应的 Value 向量。

Query 向量,Key 向量,Value 向量是什么含义呢?

其实它们就是 3 个向量,给它们加上一个名称,可以让我们更好地理解 Self-Attention 的计算过程和逻辑含义。继续往下读,你会知道 attention 是如何计算出来的,Query 向量,Key 向量,Value 向量又分别扮演了什么角色。

4.2 计算 Attention Score(注意力分数)

第 2 步,是计算 Attention Score(注意力分数)。假设我们现在计算第一个词 Thinking 的 Attention Score(注意力分数),需要根据 Thinking 这个词,对句子中的其他每个词都计算一个分数。这些分数决定了我们在编码Thinking这个词时,需要对句子中其他位置的每个词放置多少的注意力。

这些分数,是通过计算 T h i n k i n g Thinking Thinking” 对应的 Query 向量和其他位置的每个词的 Key 向量的点积,而得到的。如果我们计算句子中第一个位置单词的 Attention Score(注意力分数),那么第一个分数就是 q1 和 k1 的内积,第二个分数就是 q1 和 k2 的点积。

在这里插入图片描述

图:Thinking计算

第 3 步就是把每个分数除以 ( d k e y ) \sqrt(d_{key}) ( dkey) d k e y d_{key} dkey是 Key 向量的长度)。你也可以除以其他数,除以一个数是为了在反向传播时,求取梯度更加稳定

第 4 步,接着把这些分数经过一个 Softmax 层,Softmax可以将分数归一化,这样使得分数都是正数并且加起来等于 1。

在这里插入图片描述
图:Thinking计算

这些分数决定了在编码当前位置(这里的例子是第一个位置)的词时,对所有位置的词分别有多少的注意力。很明显,在上图的例子中,当前位置(这里的例子是第一个位置)的词会有最高的分数,但有时,关注到其他位置上相关的词也很有用。

第 5 步,得到每个位置的分数后,将每个分数分别与每个 Value 向量相乘。这种做法背后的直觉理解就是:对于分数高的位置,相乘后的值就越大,我们把更多的注意力放到了它们身上;对于分数低的位置,相乘后的值就越小,这些位置的词可能是相关性不大的,这样我们就忽略了这些位置的词。

第 6 步是把上一步得到的向量相加,就得到了 Self Attention 层在这个位置(这里的例子是第一个位置)的输出。

在这里插入图片描述

图:Think计算

上面这张图,包含了 Self Attention 的全过程,最终得到的当前位置(这里的例子是第一个位置)的向量会输入到前馈神经网络。但这样每次只能计算一个位置的输出向量,在实际的代码实现中,Self Attention 的计算过程是使用矩阵来实现的,这样可以加速计算,一次就得到所有位置的输出向量。

五、使用矩阵计算 Self-Attention

第一步是计算 Query,Key,Value 的矩阵。首先,我们把所有词向量放到一个矩阵 X 中,然后分别和3 个权重矩阵 W Q , W K W V W^Q, W^K W^V WQ,WKWV 相乘,得到 Q,K,V 矩阵。

在这里插入图片描述

图:QKV矩阵乘法

矩阵 X 中的每一行,表示句子中的每一个词的词向量,长度是 512。Q,K,V 矩阵中的每一行表示 Query 向量,Key 向量,Value 向量,向量长度是 64。接着,由于我们使用了矩阵来计算,我们可以把上面的第 2 步到第 6 步压缩为一步,直接得到 Self Attention 的输出。
在这里插入图片描述

六、多头注意力机制(multi-head attention)

Transformer 的论文通过增加多头注意力机制(一组注意力称为一个 attention head),进一步完善了 Self Attention 层。这种机制从如下两个方面增强了 attention 层的能力:

  • 它扩展了模型关注不同位置的能力。在上面的例子中,第一个位置的输出 z1 包含了句子中其他每个位置的很小一部分信息,但 z1 可能主要是由第一个位置的信息决定的。当我们翻译句子:The animal didn’t cross the street because it was too tired时,我们想让机器知道其中的it指代的是什么。这时,多头注意力机制会有帮助。
  • 多头注意力机制赋予 attention 层多个“子表示空间”。下面我们会看到,多头注意力机制会有多组 W Q , W K , W V W^Q, W^K ,W^V WQ,WK,WV 的权重矩阵(在 Transformer 的论文中,使用了 8 组注意力(attention heads)。因此,接下来我也是用 8 组注意力头 (attention heads))。每一组注意力的 的权重矩阵都是随机初始化的。经过训练之后,每一组注意力 W Q , W K W V W^Q, W^K W^V WQ,WKWV 可以看作是把输入的向量映射到一个”子表示空间“。

在这里插入图片描述

图:多头注意力机制

在多头注意力机制中,我们为每组注意力维护单独的 WQ, WK, WV 权重矩阵。将输入 X 和每组注意力的WQ, WK, WV 相乘,得到 8 组 Q, K, V 矩阵。
接着,我们把每组 K, Q, V 计算得到每组的 Z 矩阵,就得到 8 个 Z 矩阵。

在这里插入图片描述

图:8 个 Z 矩阵
接下来就有点麻烦了,因为前馈神经网络层接收的是 1 个矩阵(其中每行的向量表示一个词),而不是 8 个矩阵。所以我们需要一种方法,把 8 个矩阵整合为一个矩阵。
怎么才能做到呢?我们把矩阵拼接起来,然后和另一个权重矩阵 W Q W^Q WQ相乘

在这里插入图片描述

图:整合矩阵

  1. 把 8 个矩阵 {Z0,Z1…,Z7} 拼接起来
  2. 把拼接后的矩阵和 WO 权重矩阵相乘
  3. 得到最终的矩阵 Z,这个矩阵包含了所有 attention heads(注意力头) 的信息。这个矩阵会输入到 FFNN (Feed Forward Neural Network)层。

这就是多头注意力的全部内容。在上面的讲解中,出现了相当多的矩阵,可以参考下图的全部过程总结图:
在这里插入图片描述

既然我们已经谈到了多头注意力,现在让我们重新回顾之前的翻译例子,看下当我们编码单词it时,不同的 attention heads (注意力头)关注的是什么部分。

在这里插入图片描述
图:it的attention

The animal didn't cross the street because it was too tired

当我们编码单词"it"时,其中一个 attention head (注意力头)最关注的是"the animal",另外一个 attention head 关注的是"tired"。因此在某种意义上,"it"在模型中的表示,融合了"animal"和"word"的部分表达。然而,当我们把所有 attention heads(注意力头) 都在图上画出来时,多头注意力又变得难以解释了。

在这里插入图片描述

图:所有注意力heads

七、代码实现矩阵计算 Attention

下面我们是用代码来演示,如何使用矩阵计算 attention。首先使用 PyTorch 库提供的函数实现,然后自己再实现。PyTorch 提供了 MultiheadAttention 来实现 attention 的计算。

torch.nn.MultiheadAttention(embed_dim, num_heads, dropout=0.0, bias=True, add_bias_kv=False, add_zero_attn=False, kdim=None, vdim=None)

参数说明如下:

  • embed_dim:最终输出的 K、Q、V 矩阵的维度,这个维度需要和词向量的维度一样

  • num_heads:设置多头注意力的数量。如果设置为 1,那么只使用一组注意力。如果设置为其他数值,那么 - - num_heads 的值需要能够被 embed_dim 整除

  • dropout:这个 dropout 加在 attention score 后面

为什么 num_heads 的值需要能够被 embed_dim 整除
这是为了把词的隐向量长度平分到每一组,这样多组注意力也能够放到一个矩阵里,从而并行计算多头注意力。例如,我们前面说到,8 组注意力可以得到 8 组 Z 矩阵,然后把这些矩阵拼接起来,得到最终的输出。如果最终输出的每个词的向量维度是 512,那么每组注意力的向量维度应该是512 ÷8=64。如果不能够整除,那么这些向量的长度就无法平均分配。

7.1 使用矩阵实现多组注意力的并行计算(用pytorch自带的MultiheadAttention函数)

在这里插入图片描述
上图来自李宏毅深度学习课程ppt的多头注意力机制。
利用上面提的pytorch中的定义 MultiheadAttention 的对象后,调用时传入的参数如下。

forward(query, key, value, key_padding_mask=None, need_weights=True, attn_mask=None)
  • query:对应于 Key 矩阵,形状是 (L,N,E) 。其中 L 是输出序列长度,N 是 batch size,E 是词向量的维度

  • key:对应于 Key 矩阵,形状是 (S,N,E) 。其中 S 是输入序列长度,N 是 batch size,E 是词向量的维度

  • value:对应于 Value 矩阵,形状是 (S,N,E) 。其中 S 是输入序列长度,N 是 batch size,E 是词向量的维度

  • key_padding_mask:如果提供了这个参数,那么计算 attention score 时,忽略 Key 矩阵中某些 padding 元素,不参与计算 attention(PS:这里是因为在transformer中的Decoder部分,其Multi-Head Attention这个Block上面还加了一个Masked,关于这部分可以复习李宏毅课程笔记——【李宏毅深度学习CP13】Transformer(part2)的第一部分的“带Masked的MHA”)。形状是 (N,S)。其中 N 是 batch size,S 是输入序列长度。

    • 如果 key_padding_mask 是 ByteTensor,那么非 0 元素对应的位置会被忽略
    • 如果 key_padding_mask 是 BoolTensor,那么 True 对应的位置会被忽略
  • attn_mask:计算输出时,忽略某些位置。形状可以是 2D (L,S),或者 3D (N∗numheads,L,S)。其中 L 是输出序列长度,S 是输入序列长度,N 是 batch size。

    • 如果 attn_mask 是 ByteTensor,那么非 0 元素对应的位置会被忽略
    • 如果 attn_mask 是 BoolTensor,那么 True 对应的位置会被忽略

注意:在前面的讲解中,我们的 K、Q、V 矩阵的序列长度都是一样的。但是在实际中,K、V 矩阵的序列长度是一样的,而 Q 矩阵的序列长度可以不一样。这种情况发生在:在解码器部分的Encoder-Decoder Attention层中,Q 矩阵是来自解码器Decoder下层,而 K、V 矩阵则是来自编码器Encoder的输出

在这里插入图片描述
动态图:encoder-decoder动态图

在完成了编码(encoding)阶段之后,我们开始解码(decoding)阶段。解码(decoding)阶段的每一个时间步都输出一个翻译后的单词(这里的例子是英语翻译)。输出是:

  • attn_output:形状是 (L,N,E)
  • attn_output_weights:形状是 (N,L,S)
    代码示例如下:
import torch
from torch import nn
## nn.MultiheadAttention 输入第0维为length
# batch_size 为 64,有 12 个词,每个词的 Query 向量是 300 维
query = torch.rand(12,64,300)
# batch_size 为 64,有 10 个词,每个词的 Key 向量是 300 维
key = torch.rand(10,64,300)
# batch_size 为 64,有 10 个词,每个词的 Value 向量是 300 维
value= torch.rand(10,64,300)

embed_dim = 300
num_heads = 10
# 输出是 (attn_output, attn_output_weights)
multihead_attn = nn.MultiheadAttention(embed_dim, num_heads)
attn_output = multihead_attn(query, key, value)[0]
# output: torch.Size([12, 64, 300])
# batch_size 为 64,有 12 个词,每个词的向量是 300 维
print(attn_output.shape)
# 打印出torch.Size([12, 64, 300])

注意:embed_dim是最终输出的 K、Q、V 矩阵的维度,这个维度需要和词向量的维度一样;多头注意力的数量num_heads值需要能够被 embed_dim 整除。

7.2 手动实现计算 Attention

在 PyTorch 提供的 MultiheadAttention 中,第 1 维是句子长度,第 2 维是 batch size。这里我们的代码实现中,第 1 维是 batch size,第 2 维是句子长度。代码里也包括:如何用矩阵实现多组注意力的并行计算。
可以先回忆self-attention的计算过程(I为输入input,O是self-attention这个layer的输出,其中唯一需要学习的参数只有Wq、Wk、Wv(是未知的,要通过训练资料确定值),而其他参数都是人为设定好的):
在这里插入图片描述

import torch
from torch import nn

class MultiheadAttention(nn.Module):
    # n_heads:多头注意力的数量
    # hid_dim:每个词输出的向量维度
    def __init__(self, hid_dim, n_heads, dropout):
        super(MultiheadAttention, self).__init__()
        self.hid_dim = hid_dim
        self.n_heads = n_heads

        # 强制 hid_dim 必须整除 h
        assert hid_dim % n_heads == 0
        # 定义 W_q 矩阵
        self.w_q = nn.Linear(hid_dim, hid_dim)
        # 定义 W_k 矩阵
        self.w_k = nn.Linear(hid_dim, hid_dim)
        # 定义 W_v 矩阵
        self.w_v = nn.Linear(hid_dim, hid_dim)
        self.fc = nn.Linear(hid_dim, hid_dim)
        self.do = nn.Dropout(dropout)
        # 缩放
        self.scale = torch.sqrt(torch.FloatTensor([hid_dim // n_heads]))

    def forward(self, query, key, value, mask=None):
        # K: [64,10,300], batch_size 为 64,有 12 个词,每个词的 Query 向量是 300 维
        # V: [64,10,300], batch_size 为 64,有 10 个词,每个词的 Query 向量是 300 维
        # Q: [64,12,300], batch_size 为 64,有 10 个词,每个词的 Query 向量是 300 维
        bsz = query.shape[0]
        Q = self.w_q(query)
        K = self.w_k(key)
        V = self.w_v(value)
        # 这里把 K Q V 矩阵拆分为多组注意力,变成了一个 4 维的矩阵
        # 最后一维就是是用 self.hid_dim // self.n_heads 来得到的,表示每组注意力的向量长度, 每个 head 的向量长度是:300/6=50
        # 64 表示 batch size,6 表示有 6组注意力,10 表示有 10 词,50 表示每组注意力的词的向量长度
        # K: [64,10,300] 拆分多组注意力 -> [64,10,6,50] 转置得到 -> [64,6,10,50]
        # V: [64,10,300] 拆分多组注意力 -> [64,10,6,50] 转置得到 -> [64,6,10,50]
        # Q: [64,12,300] 拆分多组注意力 -> [64,12,6,50] 转置得到 -> [64,6,12,50]
        # 转置是为了把注意力的数量 6 放到前面,把 10 和 50 放到后面,方便下面计算
        Q = Q.view(bsz, -1, self.n_heads, self.hid_dim //
                   self.n_heads).permute(0, 2, 1, 3)
        K = K.view(bsz, -1, self.n_heads, self.hid_dim //
                   self.n_heads).permute(0, 2, 1, 3)
        V = V.view(bsz, -1, self.n_heads, self.hid_dim //
                   self.n_heads).permute(0, 2, 1, 3)

        # 第 1 步:Q 乘以 K的转置,除以scale
        # [64,6,12,50] * [64,6,50,10] = [64,6,12,10]
        # attention:[64,6,12,10]
        attention = torch.matmul(Q, K.permute(0, 1, 3, 2)) / self.scale

        # 把 mask 不为空,那么就把 mask 为 0 的位置的 attention 分数设置为 -1e10
        if mask is not None:
            attention = attention.masked_fill(mask == 0, -1e10)

        # 第 2 步:计算上一步结果的 softmax,再经过 dropout,得到 attention。
        # 注意,这里是对最后一维做 softmax,也就是在输入序列的维度做 softmax
        # attention: [64,6,12,10]
        attention = self.do(torch.softmax(attention, dim=-1))

        # 第三步,attention结果与V相乘,得到多头注意力的结果
        # [64,6,12,10] * [64,6,10,50] = [64,6,12,50]
        # x: [64,6,12,50]
        x = torch.matmul(attention, V)

        # 因为 query 有 12 个词,所以把 12 放到前面,把 5 和 60 放到后面,方便下面拼接多组的结果
        # x: [64,6,12,50] 转置-> [64,12,6,50]
        x = x.permute(0, 2, 1, 3).contiguous()
        # 这里的矩阵转换就是:把多组注意力的结果拼接起来
        # 最终结果就是 [64,12,300]
        # x: [64,12,6,50] -> [64,12,300]
        x = x.view(bsz, -1, self.n_heads * (self.hid_dim // self.n_heads))
        x = self.fc(x)
        return x


# batch_size 为 64,有 12 个词,每个词的 Query 向量是 300 维
query = torch.rand(64, 12, 300)
# batch_size 为 64,有 12 个词,每个词的 Key 向量是 300 维
key = torch.rand(64, 10, 300)
# batch_size 为 64,有 10 个词,每个词的 Value 向量是 300 维
value = torch.rand(64, 10, 300)
attention = MultiheadAttention(hid_dim=300, n_heads=6, dropout=0.1)
output = attention(query, key, value)
## output: torch.Size([64, 12, 300])
print(output.shape)
# 打印出torch.Size([64, 12, 300])

7.3 关键代码

其中用矩阵实现多头注意力的关键代码如下所示, K、Q、V 矩阵拆分为多组注意力,变成了一个 4 维的矩阵。

		# 这里把 K Q V 矩阵拆分为多组注意力,变成了一个 4 维的矩阵
        # 最后一维就是是用 self.hid_dim // self.n_heads 来得到的,表示每组注意力的向量长度, 每个 head 的向量长度是:300/6=50
        # 64 表示 batch size,6 表示有 6组注意力,10 表示有 10 个词,50 表示每组注意力的词的向量长度
        # K: [64,10,300] 拆分多组注意力 -> [64,10,6,50] 转置得到 -> [64,6,10,50]
        # V: [64,10,300] 拆分多组注意力 -> [64,10,6,50] 转置得到 -> [64,6,10,50]
        # Q: [64,12,300] 拆分多组注意力 -> [64,12,6,50] 转置得到 -> [64,6,12,50]
        # 转置是为了把注意力的数量 6 放到前面,把 10 和 50 放到后面,方便下面计算
        Q = Q.view(bsz, -1, self.n_heads, self.hid_dim //
                   self.n_heads).permute(0, 2, 1, 3)
        K = K.view(bsz, -1, self.n_heads, self.hid_dim //
                   self.n_heads).permute(0, 2, 1, 3)
        V = V.view(bsz, -1, self.n_heads, self.hid_dim //
                   self.n_heads).permute(0, 2, 1, 3)

经过 attention 计算得到 x 的形状是 `[64,12,6,50]`,64 表示 batch size,6 表示有 6组注意力,10 表示有 10 个词,50 表示每组注意力的词的向量长度。把这个矩阵转换为 `[64,12,300]`的矩阵,就是相当于把多组注意力的结果拼接起来。

这里的矩阵转换就是:把多组注意力的结果拼接起来,最终结果就是 [64,12,300],x: [64,12,6,50] -> [64,12,300]
x = x.view(bsz, -1, self.n_heads * (self.hid_dim // self.n_heads))

八、其他部分

8.1 使用位置编码来表示序列的顺序

到目前为止,我们阐述的模型中缺失了一个东西,那就是表示序列中单词顺序的方法。

为了解决这个问题,Transformer 模型对每个输入的向量都添加了一个向量。这些向量遵循模型学习到的特定模式,有助于确定每个单词的位置,或者句子中不同单词之间的距离。这种做法背后的直觉是:将这些表示位置的向量添加到词向量中,得到了新的向量,这些新向量映射到 Q/K/V,然后计算点积得到 attention 时,可以提供有意义的信息。

在这里插入图片描述

图:位置编码

为了让模型了解单词的顺序,我们添加了带有位置编码的向量–这些向量的值遵循特定的模式。
如果我们假设词向量的维度是 4,那么带有位置编码的向量可能如下所示:

在这里插入图片描述
图:位置编码

上图为带有位置编码的向量长度为 4 的例子。
那么带有位置编码的向量到底遵循什么模式?

在下图中,每一行表示一个带有位置编码的向量。所以,第一行对应于序列中第一个单词的位置编码向量。每一行都包含 512 个值,每个值的范围在 -1 和 1 之间。我对这些向量进行了涂色可视化,你可以从中看到向量遵循的模式。
在这里插入图片描述
图:位置编码图示

这是一个真实的例子,包含了 20 个词,每个词向量的维度是 512。你可以看到,它看起来像从中间一分为二。这是因为左半部分的值是由 sine 函数产生的,而右半部分的值是由 cosine 函数产生的,然后将他们拼接起来,得到每个位置编码向量。

你可以在get_timing_signal_1d()上查看生成位置编码的代码。这种方法来自于Tranformer2Transformer 的实现。

而论文中的方法和上面图中的稍有不同,它不是直接拼接两个向量,而是将两个向量交织在一起。如下图所示。

在这里插入图片描述
图:位置编码交织

此为生成位置编码的公式,在 Transformer 论文的 3.5 节中有详细说明。

这不是唯一一种生成位置编码的方法。但这种方法的优点是:可以扩展到未知的序列长度。例如:当我们的模型需要翻译一个句子,而这个句子的长度大于训练集中所有句子的长度,这时,这种位置编码的方法也可以生成一样长的位置编码向量。

8.2 残差连接

在我们继续讲解之前,编码器结构中有一个需要注意的细节是:编码器的每个子层(Self Attention 层和 FFNN)都有一个残差连接和层标准化(layer-normalization)。

在这里插入图片描述
图:残差连接
将 Self-Attention 层的层标准化(layer-normalization)和向量都进行可视化,如下所示:

在这里插入图片描述

图:标准化

在解码器的子层里面也有层标准化(layer-normalization)。假设一个 Transformer 是由 2 层编码器和两层解码器组成的,如下图所示。

在这里插入图片描述
图:2层示意图

8.3 Decoder和Cross Attention

现在我们已经介绍了解码器Decoder中的大部分概念,我们也基本知道了解码器的原理。现在让我们来看下, 编码器和解码器是如何协同工作的。

上面说了,编码器一般有多层,第一个编码器的输入是一个序列,最后一个编码器输出是一组注意力向量 K 和 V。这些注意力向量将会输入到每个解码器的Encoder-Decoder Attention层,这有助于解码器把注意力集中中输入序列的合适位置。

在完成了编码(encoding)阶段之后,我们开始解码(decoding)阶段。解码(decoding )阶段的每一个时间步都输出一个翻译后的单词(这里的例子是英语翻译)。

接下来会重复这个过程,直到输出一个结束符,Transformer 就完成了所有的输出。每一步的输出都会在下一个时间步输入到下面的第一个解码器。Decoder 就像 Encoder 那样,从下往上一层一层地输出结果。正对如编码器的输入所做的处理,我们把解码器的输入向量,也加上位置编码向量,来指示每个词的位置。

在这里插入图片描述
动态图:decoder动态图

解码器中的 Self Attention 层,和编码器中的 Self Attention 层不太一样:在解码器里,Self Attention 层只允许关注到输出序列中早于当前位置之前的单词。具体做法是:在 Self Attention 分数经过 Softmax 层之前,屏蔽当前位置之后的那些位置

Encoder-Decoder Attention层的原理和多头注意力(multiheaded Self Attention)机制类似,不同之处是:Encoder-Decoder Attention层(也即cross attention)是使用Encoder前一层的输出来构造 Query 矩阵,而 Key 矩阵和 Value 矩阵来自于解码器最终的输出(如下图所示,图来自李宏毅深度学习课程ppt)。
在这里插入图片描述

8.4 最后的线性层和 Softmax 层

Decoder 最终的输出是一个向量,其中每个元素是浮点数。我们怎么把这个向量转换为单词呢?这是由 Softmax 层后面的线性层来完成的。线性层就是一个普通的全连接神经网络,可以把解码器输出的向量,映射到一个更长的向量,这个向量称为 logits 向量
在这里插入图片描述
现在假设我们的模型有 10000 个英语单词(模型的输出词汇表),这些单词是从训练集中学到的。因此 logits 向量有 10000 个数字,每个数表示一个单词的分数。我们就是这样去理解线性层的输出。然后,Softmax 层会把这些分数转换为概率(把所有的分数转换为正数,并且加起来等于 1)。然后选择最高概率的那个数字对应的词,就是这个时间步的输出单词。
在这里插入图片描述
图:线性层

在上图中,最下面的向量,就是编码器的输出,这个向量输入到线性层和 Softmax 层,最终得到输出的词。

九、Transformer 的训练过程

现在我们已经了解了 Transformer 的前向传播过程。而在训练过程中,模型会经过上面讲的所有前向传播的步骤。但是,当我们在一个标注好的数据集上训练这个模型的时候,我们可以对比模型的输出和真实的标签。

为了可视化这个对比,让我们假设输出词汇表只包含 6 个单词(“a”, “am”, “i”, “thanks”, “student”, and “<eos>”(“<eos>”表示句子末尾))。

在这里插入图片描述

图:6个词

我们模型的输出词汇表,是在训练之前的数据预处理阶段构造的。当我们确定了输出词汇表,我们可以用向量来表示词汇表中的每个单词。这个表示方法也称为 one-hot encoding。例如,我们可以把单词 “am” 用下面的向量来表示:
在这里插入图片描述

图:am向量

介绍了训练过程,我们接着讨论模型的损失函数,这我们在训练时需要优化的目标,通过优化这个目标来得到一个训练好的、非常精确的模型。

(1)损失函数

用一个简单的例子来说明训练过程,比如:把“merci”翻译为“thanks”。
这意味着我们希望模型最终输出的概率分布,会指向单词 ”thanks“(在“thanks”这个词的概率最高)。但模型还没训练好,它输出的概率分布可能和我们希望的概率分布相差甚远。

在这里插入图片描述
图:概率分布

由于模型的参数都是随机初始化的。模型在每个词输出的概率都是随机的。我们可以把这个概率和正确的输出概率做对比,然后使用反向传播来调整模型的权重,使得输出的概率分布更加接近震数输出。

(2)要怎么比较两个概率分布呢?

可以简单地用一个概率分布减去另一个概率分布。关于更多细节,可以查看交叉熵(cross-entropy)]和KL 散度(Kullback–Leibler divergence)的相关概念。

有个大佬的视频真的娓娓道来(链接在下方),讲解了“信息量”、“比特”、“熵”、“KL散度”等概念和联系,极大似然估计和交叉熵的关系,更更加深入理解交叉熵作为损失函数的含义。
https://www.bilibili.com/video/BV15V411W7VB?p=1&share_medium=android&share_plat=android&share_session_id=11be65af-5446-4760-a2d1-088f2616470f&share_source=WEIXIN&share_tag=s_i&timestamp=1628701950&unique_k=wVk4Jd

但上面的例子是经过简化的,因为我们的句子只有一个单词。在实际中,我们使用的句子不只有一个单词。例如–输入是:“je suis étudiant” ,输出是:“i am a student”。这意味着,我们的模型需要输出多个概率分布,满足如下条件:

  • 每个概率分布都是一个向量,长度是 vocab_size(我们的例子中,向量长度是 6,但实际中更可能是 30000 或者 50000)
  • 第一个概率分布中,最高概率对应的单词是 “i”
  • 第二个概率分布中,最高概率对应的单词是 “am”
  • 以此类推,直到第 5 个概率分布中,最高概率对应的单词是 “”,表示没有下一个单词了

在这里插入图片描述

图:概率分布

我们用例子中的句子训练模型,希望产生图中所示的概率分布
我们的模型在一个足够大的数据集上,经过足够长时间的训练后,希望输出的概率分布如下图所示:

在这里插入图片描述

图:训练后概率分布

希望经过训练,模型会输出我们希望的正确翻译。当然,如果你要翻译的句子是训练集中的一部分,那输出的结果并不能说明什么。我们希望的是模型在没见过的句子上也能够准确翻译。需要注意的是:概率分布向量中,每个位置都会有一点概率,即使这个位置不是输出对应的单词–这是 Softmax 中一个很有用的特性,有助于帮助训练过程。

现在,由于模型每个时间步只产生一个输出,我们可以认为:模型是从概率分布中选择概率最大的词,并且丢弃其他词。这种方法叫做贪婪解码(greedy decoding)。另一种方法是每个时间步保留两个最高概率的输出词,然后在下一个时间步,重复执行这个过程:假设第一个位置概率最高的两个输出的词是”I“和”a“,这两个词都保留,然后根据第一个词计算第二个位置的词的概率分布,再取出 2 个概率最高的词,对于第二个位置和第三个位置,我们也重复这个过程。这种方法称为集束搜索(beam search),在我们的例子中,beam_size 的值是 2(含义是:在所有时间步,我们保留两个最高概率),top_beams 的值也是 2(表示我们最终会返回两个翻译的结果)。beam_sizetop_beams 都是你可以在实验中尝试的超参数。

十、更多参考

  • 阅读 Transformer 的论文:
    《Attention Is All You Need》
    链接地址:https://arxiv.org/abs/1706.03762
  • 阅读Transformer 的博客文章:
    《Transformer: A Novel Neural Network Architecture for Language Understanding》
    链接地址:https://ai.googleblog.com/2017/08/transformer-novel-neural-network.html
    阅读《Tensor2Tensor announcement》
  • 链接地址:https://ai.googleblog.com/2017/06/accelerating-deep-learning-research.html
  • 观看视频 【Łukasz Kaiser’s talk】来理解模型和其中的细节
    链接地址:https://www.youtube.com/watch?v=rBCqOTEfxvg
    运行这份代码:【Jupyter Notebook provided as part of the Tensor2Tensor repo】
  • 链接地址:https://colab.research.google.com/github/tensorflow/tensor2tensor/blob/master/tensor2tensor/notebooks/hello_t2t.ipynb。
  • 查看这个项目:【Tensor2Tensor repo】
    链接地址:https://github.com/tensorflow/tensor2tensor

第3部分:pytorch手写transformer(详解)

基于pytorch来实现一个Transformer
在这里插入图片描述
图:Transformer结构图

一、词嵌入

如上图所示,Transformer图里左边的是Encoder,右边是Decoder部分。Encoder输入源语言序列,Decoder里面输入需要被翻译的语言文本(在训练时)。一个文本常有许多序列组成,常见操作为将序列进行一些预处理(如词切分等)变成列表,一个序列的列表的元素通常为词表中不可切分的最小词,整个文本就是一个大列表,元素为一个一个由序列组成的列表。如一个序列经过切分后变为[“am”, “##ro”, “##zi”, “meets”, “his”, “father”],接下来按照它们在词表中对应的索引进行转换,假设结果如[23, 94, 13, 41, 27, 96]。假如整个文本一共100个句子,那么就有100个列表为它的元素,因为每个序列的长度不一,需要设定最大长度,这里不妨设为128,那么将整个文本转换为数组之后,形状即为100 x 128,这就对应着batch_size和seq_length。

输入之后,紧接着进行词嵌入处理,词嵌入就是将每一个词用预先训练好的向量进行映射。

词嵌入在torch里基于torch.nn.Embedding实现,实例化时需要设置的参数为词表的大小和被映射的向量的维度比如embed = nn.Embedding(10,8)。向量的维度通俗来说就是向量里面有多少个数。注意,第一个参数是词表的大小,如果你目前最多有8个词,通常填写10(多一个位置留给unk和pad),你后面万一进入与这8个词不同的词就映射到unk上,序列padding的部分就映射到pad上。

假如我们打算映射到8维(num_features或者embed_dim),那么,整个文本的形状变为100 x 128 x 8。接下来举个小例子解释一下:假设我们词表一共有10个词(算上unk和pad),文本里有2个句子,每个句子有4个词,我们想要把每个词映射到8维的向量。于是2,4,8对应于batch_size, seq_length, embed_dim(如果batch在第一维的话)。

另外,一般深度学习任务只改变num_features,所以讲维度一般是针对最后特征所在的维度。

所有需要的包的导入:

import torch
import torch.nn as nn
from torch.nn.parameter import Parameter
from torch.nn.init import xavier_uniform_
from torch.nn.init import constant_
from torch.nn.init import xavier_normal_
import torch.nn.functional as F
from typing import Optional, Tuple, Any
from typing import List, Optional, Tuple
import math
import warnings
X = torch.zeros((2,4),dtype=torch.long)
embed = nn.Embedding(10,8)
print(embed(X).shape)
# 打印出torch.Size([2, 4, 8])

二、位置编码

词嵌入之后紧接着就是位置编码,位置编码用以区分不同词以及同词不同特征之间的关系。代码中需要注意:X_只是初始化的矩阵,并不是输入进来的;完成位置编码之后会加一个dropout。另外,位置编码是最后加上去的,因此输入输出形状不变。

Tensor = torch.Tensor
def positional_encoding(X, num_features, dropout_p=0.1, max_len=512) -> Tensor:
    r'''
        给输入加入位置编码
    参数:
        - num_features: 输入进来的维度
        - dropout_p: dropout的概率,当其为非零时执行dropout
        - max_len: 句子的最大长度,默认512
    
    形状:
        - 输入: [batch_size, seq_length, num_features]
        - 输出: [batch_size, seq_length, num_features]

    例子:
        >>> X = torch.randn((2,4,10))
        >>> X = positional_encoding(X, 10)
        >>> print(X.shape)
        >>> torch.Size([2, 4, 10])
    '''

    dropout = nn.Dropout(dropout_p)
    P = torch.zeros((1,max_len,num_features))
    X_ = torch.arange(max_len,dtype=torch.float32).reshape(-1,1) / torch.pow(
        10000,
        torch.arange(0,num_features,2,dtype=torch.float32) /num_features)
    P[:,:,0::2] = torch.sin(X_)
    P[:,:,1::2] = torch.cos(X_)
    X = X + P[:,:X.shape[1],:].to(X.device)
    return dropout(X)
# 位置编码例子
X = torch.randn((2,4,10))
X = positional_encoding(X, 10)
print(X.shape)
# 打印出torch.Size([2, 4, 10])

三、多头注意力机制

3.1 拆开看多头注意力机制

多头注意力类主要成分是:参数初始化、multi_head_attention_forward

A.初始化参数
if self._qkv_same_embed_dim is False:
    # 初始化前后形状维持不变
    # (seq_length x embed_dim) x (embed_dim x embed_dim) ==> (seq_length x embed_dim)
    self.q_proj_weight = Parameter(torch.empty((embed_dim, embed_dim)))
    self.k_proj_weight = Parameter(torch.empty((embed_dim, self.kdim)))
    self.v_proj_weight = Parameter(torch.empty((embed_dim, self.vdim)))
    self.register_parameter('in_proj_weight', None)
else:
    self.in_proj_weight = Parameter(torch.empty((3 * embed_dim, embed_dim)))
    self.register_parameter('q_proj_weight', None)
    self.register_parameter('k_proj_weight', None)
    self.register_parameter('v_proj_weight', None)

if bias:
    self.in_proj_bias = Parameter(torch.empty(3 * embed_dim))
else:
    self.register_parameter('in_proj_bias', None)
# 后期会将所有头的注意力拼接在一起然后乘上权重矩阵输出
# out_proj是为了后期准备的
self.out_proj = nn.Linear(embed_dim, embed_dim, bias=bias)
self._reset_parameters()

torch.empty是按照所给的形状形成对应的tensor,特点是填充的值还未初始化,类比torch.randn(标准正态分布),这就是一种初始化的方式。在PyTorch中,变量类型是tensor的话是无法修改值的,而Parameter()函数可以看作为一种类型转变函数,将不可改值的tensor转换为可训练可修改的模型参数,即与model.parameters绑定在一起,register_parameter的意思是是否将这个参数放到model.parameters,None的意思是没有这个参数。

这里有个if判断,用以判断q,k,v的最后一维是否一致,若一致,则一个大的权重矩阵全部乘然后分割出来,若不是,则各初始化各的,其实初始化是不会改变原来的形状的(如,见注释)。

(1)最后有一个_reset_parameters()函数,这个是用来初始化参数数值的。xavier_uniform意思是从连续型均匀分布里面随机取样出值来作为初始化的值,xavier_normal_取样的分布是正态分布。正因为初始化值在训练神经网络的时候很重要,所以才需要这两个函数。

(2)constant_意思是用所给值来填充输入的向量。

(3)在PyTorch的源码里,似乎projection代表是一种线性变换的意思,in_proj_bias的意思就是一开始的线性变换的偏置

def _reset_parameters(self):
    if self._qkv_same_embed_dim:
        xavier_uniform_(self.in_proj_weight)
    else:
        xavier_uniform_(self.q_proj_weight)
        xavier_uniform_(self.k_proj_weight)
        xavier_uniform_(self.v_proj_weight)
    if self.in_proj_bias is not None:
        constant_(self.in_proj_bias, 0.)
        constant_(self.out_proj.bias, 0.)

B.multi_head_attention_forward

这个函数如下代码所示,主要分成3个部分:

  • query, key, value通过_in_projection_packed变换得到q,k,v
  • 遮挡机制
  • 点积注意力
import torch
Tensor = torch.Tensor
def multi_head_attention_forward(
    query: Tensor,
    key: Tensor,
    value: Tensor,
    num_heads: int,
    in_proj_weight: Tensor,
    in_proj_bias: Optional[Tensor],
    dropout_p: float,
    out_proj_weight: Tensor,
    out_proj_bias: Optional[Tensor],
    training: bool = True,
    key_padding_mask: Optional[Tensor] = None,
    need_weights: bool = True,
    attn_mask: Optional[Tensor] = None,
    use_seperate_proj_weight = None,
    q_proj_weight: Optional[Tensor] = None,
    k_proj_weight: Optional[Tensor] = None,
    v_proj_weight: Optional[Tensor] = None,
) -> Tuple[Tensor, Optional[Tensor]]:
    r'''
    形状:
        输入:
        - query:`(L, N, E)`
        - key: `(S, N, E)`
        - value: `(S, N, E)`
        - key_padding_mask: `(N, S)`
        - attn_mask: `(L, S)` or `(N * num_heads, L, S)`
        输出:
        - attn_output:`(L, N, E)`
        - attn_output_weights:`(N, L, S)`
    '''
    tgt_len, bsz, embed_dim = query.shape
    src_len, _, _ = key.shape
    head_dim = embed_dim // num_heads
    q, k, v = _in_projection_packed(query, key, value, in_proj_weight, in_proj_bias)

    if attn_mask is not None:
        if attn_mask.dtype == torch.uint8:
            warnings.warn("Byte tensor for attn_mask in nn.MultiheadAttention is deprecated. Use bool tensor instead.")
            attn_mask = attn_mask.to(torch.bool)
        else:
            assert attn_mask.is_floating_point() or attn_mask.dtype == torch.bool, \
                f"Only float, byte, and bool types are supported for attn_mask, not {attn_mask.dtype}"

        if attn_mask.dim() == 2:
            correct_2d_size = (tgt_len, src_len)
            if attn_mask.shape != correct_2d_size:
                raise RuntimeError(f"The shape of the 2D attn_mask is {attn_mask.shape}, but should be {correct_2d_size}.")
            attn_mask = attn_mask.unsqueeze(0)
        elif attn_mask.dim() == 3:
            correct_3d_size = (bsz * num_heads, tgt_len, src_len)
            if attn_mask.shape != correct_3d_size:
                raise RuntimeError(f"The shape of the 3D attn_mask is {attn_mask.shape}, but should be {correct_3d_size}.")
        else:
            raise RuntimeError(f"attn_mask's dimension {attn_mask.dim()} is not supported")

    if key_padding_mask is not None and key_padding_mask.dtype == torch.uint8:
        warnings.warn("Byte tensor for key_padding_mask in nn.MultiheadAttention is deprecated. Use bool tensor instead.")
        key_padding_mask = key_padding_mask.to(torch.bool)
    
    # reshape q,k,v将Batch放在第一维以适合点积注意力
    # 同时为多头机制,将不同的头拼在一起组成一层
    q = q.contiguous().view(tgt_len, bsz * num_heads, head_dim).transpose(0, 1)
    k = k.contiguous().view(-1, bsz * num_heads, head_dim).transpose(0, 1)
    v = v.contiguous().view(-1, bsz * num_heads, head_dim).transpose(0, 1)
    if key_padding_mask is not None:
        assert key_padding_mask.shape == (bsz, src_len), \
            f"expecting key_padding_mask shape of {(bsz, src_len)}, but got {key_padding_mask.shape}"
        key_padding_mask = key_padding_mask.view(bsz, 1, 1, src_len).   \
            expand(-1, num_heads, -1, -1).reshape(bsz * num_heads, 1, src_len)
        if attn_mask is None:
            attn_mask = key_padding_mask
        elif attn_mask.dtype == torch.bool:
            attn_mask = attn_mask.logical_or(key_padding_mask)
        else:
            attn_mask = attn_mask.masked_fill(key_padding_mask, float("-inf"))
    # 若attn_mask值是布尔值,则将mask转换为float
    if attn_mask is not None and attn_mask.dtype == torch.bool:
        new_attn_mask = torch.zeros_like(attn_mask, dtype=torch.float)
        new_attn_mask.masked_fill_(attn_mask, float("-inf"))
        attn_mask = new_attn_mask

    # 若training为True时才应用dropout
    if not training:
        dropout_p = 0.0
    attn_output, attn_output_weights = _scaled_dot_product_attention(q, k, v, attn_mask, dropout_p)
    attn_output = attn_output.transpose(0, 1).contiguous().view(tgt_len, bsz, embed_dim)
    attn_output = nn.functional.linear(attn_output, out_proj_weight, out_proj_bias)
    if need_weights:
        # average attention weights over heads
        attn_output_weights = attn_output_weights.view(bsz, num_heads, tgt_len, src_len)
        return attn_output, attn_output_weights.sum(dim=1) / num_heads
    else:
        return attn_output, None
(1)query, key, value通过_in_projection_packed变换得到q,k,v
q, k, v = _in_projection_packed(query, key, value, in_proj_weight, in_proj_bias)

对于nn.functional.linear函数,其实就是一个线性变换,与nn.Linear不同的是,前者可以提供权重矩阵和偏置,执行,而后者是可以自由决定输出的维度。

def _in_projection_packed(
    q: Tensor,
    k: Tensor,
    v: Tensor,
    w: Tensor,
    b: Optional[Tensor] = None,
) -> List[Tensor]:
    r"""
    用一个大的权重参数矩阵进行线性变换

    参数:
        q, k, v: 对自注意来说,三者都是src;对于seq2seq模型,k和v是一致的tensor。
                 但它们的最后一维(num_features或者叫做embed_dim)都必须保持一致。
        w: 用以线性变换的大矩阵,按照q,k,v的顺序压在一个tensor里面。
        b: 用以线性变换的偏置,按照q,k,v的顺序压在一个tensor里面。

    形状:
        输入:
        - q: shape:`(..., E)`,E是词嵌入的维度(下面出现的E均为此意)。
        - k: shape:`(..., E)`
        - v: shape:`(..., E)`
        - w: shape:`(E * 3, E)`
        - b: shape:`E * 3` 

        输出:
        - 输出列表 :`[q', k', v']`,q,k,v经过线性变换前后的形状都一致。
    """
    E = q.size(-1)
    # 若为自注意,则q = k = v = src,因此它们的引用变量都是src
    # 即k is v和q is k结果均为True
    # 若为seq2seq,k = v,因而k is v的结果是True
    if k is v:
        if q is k:
            return F.linear(q, w, b).chunk(3, dim=-1)
        else:
            # seq2seq模型
            w_q, w_kv = w.split([E, E * 2])
            if b is None:
                b_q = b_kv = None
            else:
                b_q, b_kv = b.split([E, E * 2])
            return (F.linear(q, w_q, b_q),) + F.linear(k, w_kv, b_kv).chunk(2, dim=-1)
    else:
        w_q, w_k, w_v = w.chunk(3)
        if b is None:
            b_q = b_k = b_v = None
        else:
            b_q, b_k, b_v = b.chunk(3)
        return F.linear(q, w_q, b_q), F.linear(k, w_k, b_k), F.linear(v, w_v, b_v)

# q, k, v = _in_projection_packed(query, key, value, in_proj_weight, in_proj_bias)
(2)遮挡机制

对于attn_mask来说,若为2D,形状如(L, S),L和S分别代表着目标语言和源语言序列长度,若为3D,形状如(N * num_heads, L, S),N代表着batch_size,num_heads代表注意力头的数目。若为attn_mask的dtype为ByteTensor,非0的位置会被忽略不做注意力;若为BoolTensor,True对应的位置会被忽略;若为数值,则会直接加到attn_weights。

因为在decoder解码的时候,只能看该位置和它之前的,如果看后面就犯规了,所以需要attn_mask遮挡住。

下面函数直接复制PyTorch的,意思是确保不同维度的mask形状正确以及不同类型的转换

if attn_mask is not None:
    if attn_mask.dtype == torch.uint8:
        warnings.warn("Byte tensor for attn_mask in nn.MultiheadAttention is deprecated. Use bool tensor instead.")
        attn_mask = attn_mask.to(torch.bool)
    else:
        assert attn_mask.is_floating_point() or attn_mask.dtype == torch.bool, \
            f"Only float, byte, and bool types are supported for attn_mask, not {attn_mask.dtype}"
    # 对不同维度的形状判定
    if attn_mask.dim() == 2:
        correct_2d_size = (tgt_len, src_len)
        if attn_mask.shape != correct_2d_size:
            raise RuntimeError(f"The shape of the 2D attn_mask is {attn_mask.shape}, but should be {correct_2d_size}.")
            attn_mask = attn_mask.unsqueeze(0)
    elif attn_mask.dim() == 3:
        correct_3d_size = (bsz * num_heads, tgt_len, src_len)
        if attn_mask.shape != correct_3d_size:
            raise RuntimeError(f"The shape of the 3D attn_mask is {attn_mask.shape}, but should be {correct_3d_size}.")
    else:
        raise RuntimeError(f"attn_mask's dimension {attn_mask.dim()} is not supported")

attn_mask不同的是,key_padding_mask是用来遮挡住key里面的值,详细来说应该是<PAD>,被忽略的情况与attn_mask一致

# 将key_padding_mask值改为布尔值
if key_padding_mask is not None and key_padding_mask.dtype == torch.uint8:
    warnings.warn("Byte tensor for key_padding_mask in nn.MultiheadAttention is deprecated. Use bool tensor instead.")
    key_padding_mask = key_padding_mask.to(torch.bool)

先介绍两个小函数:
(1)logical_or,输入两个tensor,并对这两个tensor里的值做逻辑或运算,只有当两个值均为0的时候才为False,其他时候均为True
(2)另一个是masked_fill,输入是一个mask,和用以填充的值。mask由1,0组成,0的位置值维持不变,1的位置用新值填充。

a = torch.tensor([0,1,10,0],dtype=torch.int8)
b = torch.tensor([4,0,1,0],dtype=torch.int8)
print(torch.logical_or(a,b))
# tensor([ True,  True,  True, False])
r = torch.tensor([[0,0,0,0],[0,0,0,0]])
mask = torch.tensor([[1,1,1,1],[0,0,0,0]])
print(r.masked_fill(mask,1))
# tensor([[1, 1, 1, 1],
#         [0, 0, 0, 0]])

其实attn_mask和key_padding_mask有些时候对象是一致的,所以有时候可以合起来看。-inf做softmax之后值为0,即被忽略。

if key_padding_mask is not None:
    assert key_padding_mask.shape == (bsz, src_len), \
        f"expecting key_padding_mask shape of {(bsz, src_len)}, but got {key_padding_mask.shape}"
    key_padding_mask = key_padding_mask.view(bsz, 1, 1, src_len).   \
        expand(-1, num_heads, -1, -1).reshape(bsz * num_heads, 1, src_len)
    # 若attn_mask为空,直接用key_padding_mask
    if attn_mask is None:
        attn_mask = key_padding_mask
    elif attn_mask.dtype == torch.bool:
        attn_mask = attn_mask.logical_or(key_padding_mask)
    else:
        attn_mask = attn_mask.masked_fill(key_padding_mask, float("-inf"))

# 若attn_mask值是布尔值,则将mask转换为float
if attn_mask is not None and attn_mask.dtype == torch.bool:
    new_attn_mask = torch.zeros_like(attn_mask, dtype=torch.float)
    new_attn_mask.masked_fill_(attn_mask, float("-inf"))
    attn_mask = new_attn_mask

(3)点积注意力
from typing import Optional, Tuple, Any
def _scaled_dot_product_attention(
    q: Tensor,
    k: Tensor,
    v: Tensor,
    attn_mask: Optional[Tensor] = None,
    dropout_p: float = 0.0,
) -> Tuple[Tensor, Tensor]:
    r'''
    在query, key, value上计算点积注意力,若有注意力遮盖则使用,并且应用一个概率为dropout_p的dropout

    参数:
        - q: shape:`(B, Nt, E)` B代表batch size, Nt是目标语言序列长度,E是嵌入后的特征维度
        - key: shape:`(B, Ns, E)` Ns是源语言序列长度
        - value: shape:`(B, Ns, E)`与key形状一样
        - attn_mask: 要么是3D的tensor,形状为:`(B, Nt, Ns)`或者2D的tensor,形状如:`(Nt, Ns)`

        - Output: attention values: shape:`(B, Nt, E)`,与q的形状一致;attention weights: shape:`(B, Nt, Ns)`
    
    例子:
        >>> q = torch.randn((2,3,6))
        >>> k = torch.randn((2,4,6))
        >>> v = torch.randn((2,4,6))
        >>> out = scaled_dot_product_attention(q, k, v)
        >>> out[0].shape, out[1].shape
        >>> torch.Size([2, 3, 6]) torch.Size([2, 3, 4])
    '''
    B, Nt, E = q.shape
    q = q / math.sqrt(E)
    # (B, Nt, E) x (B, E, Ns) -> (B, Nt, Ns)
    attn = torch.bmm(q, k.transpose(-2,-1))
    if attn_mask is not None:
        attn += attn_mask 
    # attn意味着目标序列的每个词对源语言序列做注意力
    attn = F.softmax(attn, dim=-1)
    if dropout_p:
        attn = F.dropout(attn, p=dropout_p)
    # (B, Nt, Ns) x (B, Ns, E) -> (B, Nt, E)
    output = torch.bmm(attn, v)
    return output, attn 

3.2 完整的多头注意力机制-MultiheadAttention

class MultiheadAttention(nn.Module):
    r'''
    参数:
        embed_dim: 词嵌入的维度
        num_heads: 平行头的数量
        batch_first: 若`True`,则为(batch, seq, feture),若为`False`,则为(seq, batch, feature)
    
    例子:
        >>> multihead_attn = MultiheadAttention(embed_dim, num_heads)
        >>> attn_output, attn_output_weights = multihead_attn(query, key, value)
    '''
    def __init__(self, embed_dim, num_heads, dropout=0., bias=True,
                 kdim=None, vdim=None, batch_first=False) -> None:
        # factory_kwargs = {'device': device, 'dtype': dtype}
        super(MultiheadAttention, self).__init__()
        self.embed_dim = embed_dim
        self.kdim = kdim if kdim is not None else embed_dim
        self.vdim = vdim if vdim is not None else embed_dim
        self._qkv_same_embed_dim = self.kdim == embed_dim and self.vdim == embed_dim

        self.num_heads = num_heads
        self.dropout = dropout
        self.batch_first = batch_first
        self.head_dim = embed_dim // num_heads
        assert self.head_dim * num_heads == self.embed_dim, "embed_dim must be divisible by num_heads"

        if self._qkv_same_embed_dim is False:
            self.q_proj_weight = Parameter(torch.empty((embed_dim, embed_dim)))
            self.k_proj_weight = Parameter(torch.empty((embed_dim, self.kdim)))
            self.v_proj_weight = Parameter(torch.empty((embed_dim, self.vdim)))
            self.register_parameter('in_proj_weight', None)
        else:
            self.in_proj_weight = Parameter(torch.empty((3 * embed_dim, embed_dim)))
            self.register_parameter('q_proj_weight', None)
            self.register_parameter('k_proj_weight', None)
            self.register_parameter('v_proj_weight', None)

        if bias:
            self.in_proj_bias = Parameter(torch.empty(3 * embed_dim))
        else:
            self.register_parameter('in_proj_bias', None)
        self.out_proj = nn.Linear(embed_dim, embed_dim, bias=bias)

        self._reset_parameters()

    def _reset_parameters(self):
        if self._qkv_same_embed_dim:
            xavier_uniform_(self.in_proj_weight)
        else:
            xavier_uniform_(self.q_proj_weight)
            xavier_uniform_(self.k_proj_weight)
            xavier_uniform_(self.v_proj_weight)

        if self.in_proj_bias is not None:
            constant_(self.in_proj_bias, 0.)
            constant_(self.out_proj.bias, 0.)



    def forward(self, query: Tensor, key: Tensor, value: Tensor, key_padding_mask: Optional[Tensor] = None,
                need_weights: bool = True, attn_mask: Optional[Tensor] = None) -> Tuple[Tensor, Optional[Tensor]]:
        if self.batch_first:
            query, key, value = [x.transpose(1, 0) for x in (query, key, value)]

        if not self._qkv_same_embed_dim:
            attn_output, attn_output_weights = multi_head_attention_forward(
                query, key, value, self.num_heads,
                self.in_proj_weight, self.in_proj_bias,
                self.dropout, self.out_proj.weight, self.out_proj.bias,
                training=self.training,
                key_padding_mask=key_padding_mask, need_weights=need_weights,
                attn_mask=attn_mask, use_separate_proj_weight=True,
                q_proj_weight=self.q_proj_weight, k_proj_weight=self.k_proj_weight,
                v_proj_weight=self.v_proj_weight)
        else:
            attn_output, attn_output_weights = multi_head_attention_forward(
                query, key, value, self.num_heads,
                self.in_proj_weight, self.in_proj_bias,
                self.dropout, self.out_proj.weight, self.out_proj.bias,
                training=self.training,
                key_padding_mask=key_padding_mask, need_weights=need_weights,
                attn_mask=attn_mask)
        if self.batch_first:
            return attn_output.transpose(1, 0), attn_output_weights
        else:
            return attn_output, attn_output_weights

接下来可以实践一下,并且把位置编码加起来,可以发现加入位置编码和进行多头注意力的前后形状都是不会变的

# 因为batch_first为False,所以src的shape:`(seq, batch, embed_dim)`
src = torch.randn((2,4,100))
src = positional_encoding(src,100,0.1)
print(src.shape)
multihead_attn = MultiheadAttention(100, 4, 0.1)
attn_output, attn_output_weights = multihead_attn(src,src,src)
print(attn_output.shape, attn_output_weights.shape)

# 打印出torch.Size([2, 4, 100])
# 打印出torch.Size([2, 4, 100]) torch.Size([4, 2, 2])

四、Transformer的EncoderLayer

  • Encoder Layer
    在这里插入图片描述
class TransformerEncoderLayer(nn.Module):
    r'''
    参数:
        d_model: 词嵌入的维度(必备)
        nhead: 多头注意力中平行头的数目(必备)
        dim_feedforward: 全连接层的神经元的数目,又称经过此层输入的维度(Default = 2048)
        dropout: dropout的概率(Default = 0.1)
        activation: 两个线性层中间的激活函数,默认relu或gelu
        lay_norm_eps: layer normalization中的微小量,防止分母为0(Default = 1e-5)
        batch_first: 若`True`,则为(batch, seq, feture),若为`False`,则为(seq, batch, feature)(Default:False)

    例子:
        >>> encoder_layer = TransformerEncoderLayer(d_model=512, nhead=8)
        >>> src = torch.randn((32, 10, 512))
        >>> out = encoder_layer(src)
    '''

    def __init__(self, d_model, nhead, dim_feedforward=2048, dropout=0.1, activation=F.relu,
                 layer_norm_eps=1e-5, batch_first=False) -> None:
        super(TransformerEncoderLayer, self).__init__()
        self.self_attn = MultiheadAttention(d_model, nhead, dropout=dropout, batch_first=batch_first)
        self.linear1 = nn.Linear(d_model, dim_feedforward)
        self.dropout = nn.Dropout(dropout)
        self.linear2 = nn.Linear(dim_feedforward, d_model)

        self.norm1 = nn.LayerNorm(d_model, eps=layer_norm_eps)
        self.norm2 = nn.LayerNorm(d_model, eps=layer_norm_eps)
        self.dropout1 = nn.Dropout(dropout)
        self.dropout2 = nn.Dropout(dropout)
        self.activation = activation        


    def forward(self, src: Tensor, src_mask: Optional[Tensor] = None, src_key_padding_mask: Optional[Tensor] = None) -> Tensor:
        src = positional_encoding(src, src.shape[-1])
        src2 = self.self_attn(src, src, src, attn_mask=src_mask, 
        key_padding_mask=src_key_padding_mask)[0]
        src = src + self.dropout1(src2)
        src = self.norm1(src)
        src2 = self.linear2(self.dropout(self.activation(self.linear1(src))))
        src = src + self.dropout(src2)
        src = self.norm2(src)
        return src
# 用小例子看一下
encoder_layer = TransformerEncoderLayer(d_model=512, nhead=8)
src = torch.randn((32, 10, 512))
out = encoder_layer(src)
print(out.shape)
# torch.Size([32, 10, 512])

五、Transformer layer组成Encoder

class TransformerEncoder(nn.Module):
    r'''
    参数:
        encoder_layer(必备)
        num_layers: encoder_layer的层数(必备)
        norm: 归一化的选择(可选)
    
    例子:
        >>> encoder_layer = TransformerEncoderLayer(d_model=512, nhead=8)
        >>> transformer_encoder = TransformerEncoder(encoder_layer, num_layers=6)
        >>> src = torch.randn((10, 32, 512))
        >>> out = transformer_encoder(src)
    '''

    def __init__(self, encoder_layer, num_layers, norm=None):
        super(TransformerEncoder, self).__init__()
        self.layer = encoder_layer
        self.num_layers = num_layers
        self.norm = norm
    
    def forward(self, src: Tensor, mask: Optional[Tensor] = None, src_key_padding_mask: Optional[Tensor] = None) -> Tensor:
        output = positional_encoding(src, src.shape[-1])
        for _ in range(self.num_layers):
            output = self.layer(output, src_mask=mask, src_key_padding_mask=src_key_padding_mask)
        
        if self.norm is not None:
            output = self.norm(output)
        
        return output
# 例子
encoder_layer = TransformerEncoderLayer(d_model=512, nhead=8)
transformer_encoder = TransformerEncoder(encoder_layer, num_layers=6)
src = torch.randn((10, 32, 512))
out = transformer_encoder(src)
print(out.shape)
# torch.Size([10, 32, 512])

六、Decoder Layer:

class TransformerDecoderLayer(nn.Module):
    r'''
    参数:
        d_model: 词嵌入的维度(必备)
        nhead: 多头注意力中平行头的数目(必备)
        dim_feedforward: 全连接层的神经元的数目,又称经过此层输入的维度(Default = 2048)
        dropout: dropout的概率(Default = 0.1)
        activation: 两个线性层中间的激活函数,默认relu或gelu
        lay_norm_eps: layer normalization中的微小量,防止分母为0(Default = 1e-5)
        batch_first: 若`True`,则为(batch, seq, feture),若为`False`,则为(seq, batch, feature)(Default:False)
    
    例子:
        >>> decoder_layer = TransformerDecoderLayer(d_model=512, nhead=8)
        >>> memory = torch.randn((10, 32, 512))
        >>> tgt = torch.randn((20, 32, 512))
        >>> out = decoder_layer(tgt, memory)
    '''
    def __init__(self, d_model, nhead, dim_feedforward=2048, dropout=0.1, activation=F.relu,
                 layer_norm_eps=1e-5, batch_first=False) -> None:
        super(TransformerDecoderLayer, self).__init__()
        self.self_attn = MultiheadAttention(d_model, nhead, dropout=dropout, batch_first=batch_first)
        self.multihead_attn = MultiheadAttention(d_model, nhead, dropout=dropout, batch_first=batch_first)

        self.linear1 = nn.Linear(d_model, dim_feedforward)
        self.dropout = nn.Dropout(dropout)
        self.linear2 = nn.Linear(dim_feedforward, d_model)

        self.norm1 = nn.LayerNorm(d_model, eps=layer_norm_eps)
        self.norm2 = nn.LayerNorm(d_model, eps=layer_norm_eps)
        self.norm3 = nn.LayerNorm(d_model, eps=layer_norm_eps)
        self.dropout1 = nn.Dropout(dropout)
        self.dropout2 = nn.Dropout(dropout)
        self.dropout3 = nn.Dropout(dropout)

        self.activation = activation

    def forward(self, tgt: Tensor, memory: Tensor, tgt_mask: Optional[Tensor] = None, 
                memory_mask: Optional[Tensor] = None,tgt_key_padding_mask: Optional[Tensor] = None, memory_key_padding_mask: Optional[Tensor] = None) -> Tensor:
        r'''
        参数:
            tgt: 目标语言序列(必备)
            memory: 从最后一个encoder_layer跑出的句子(必备)
            tgt_mask: 目标语言序列的mask(可选)
            memory_mask(可选)
            tgt_key_padding_mask(可选)
            memory_key_padding_mask(可选)
        '''
        tgt2 = self.self_attn(tgt, tgt, tgt, attn_mask=tgt_mask,
                              key_padding_mask=tgt_key_padding_mask)[0]
        tgt = tgt + self.dropout1(tgt2)
        tgt = self.norm1(tgt)
        tgt2 = self.multihead_attn(tgt, memory, memory, attn_mask=memory_mask,
                                   key_padding_mask=memory_key_padding_mask)[0]
        tgt = tgt + self.dropout2(tgt2)
        tgt = self.norm2(tgt)
        tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt))))
        tgt = tgt + self.dropout3(tgt2)
        tgt = self.norm3(tgt)
        return tgt
# 可爱的小例子
decoder_layer = nn.TransformerDecoderLayer(d_model=512, nhead=8)
memory = torch.randn((10, 32, 512))
tgt = torch.randn((20, 32, 512))
out = decoder_layer(tgt, memory)
print(out.shape)
# torch.Size([20, 32, 512])

Decoder代码

class TransformerDecoder(nn.Module):
    r'''
    参数:
        decoder_layer(必备)
        num_layers: decoder_layer的层数(必备)
        norm: 归一化选择
    
    例子:
        >>> decoder_layer =TransformerDecoderLayer(d_model=512, nhead=8)
        >>> transformer_decoder = TransformerDecoder(decoder_layer, num_layers=6)
        >>> memory = torch.rand(10, 32, 512)
        >>> tgt = torch.rand(20, 32, 512)
        >>> out = transformer_decoder(tgt, memory)
    '''
    def __init__(self, decoder_layer, num_layers, norm=None):
        super(TransformerDecoder, self).__init__()
        self.layer = decoder_layer
        self.num_layers = num_layers
        self.norm = norm
    
    def forward(self, tgt: Tensor, memory: Tensor, tgt_mask: Optional[Tensor] = None,
                memory_mask: Optional[Tensor] = None, tgt_key_padding_mask: Optional[Tensor] = None,
                memory_key_padding_mask: Optional[Tensor] = None) -> Tensor:
        output = tgt
        for _ in range(self.num_layers):
            output = self.layer(output, memory, tgt_mask=tgt_mask,
                         memory_mask=memory_mask,
                         tgt_key_padding_mask=tgt_key_padding_mask,
                         memory_key_padding_mask=memory_key_padding_mask)
        if self.norm is not None:
            output = self.norm(output)

        return output
# 可爱的小例子
decoder_layer =TransformerDecoderLayer(d_model=512, nhead=8)
transformer_decoder = TransformerDecoder(decoder_layer, num_layers=6)
memory = torch.rand(10, 32, 512)
tgt = torch.rand(20, 32, 512)
out = transformer_decoder(tgt, memory)
print(out.shape)
# torch.Size([20, 32, 512])

总结:其实经过位置编码,多头注意力,Encoder Layer和Decoder Layer形状不会变的,而Encoder和Decoder分别与src和tgt形状一致

七、Transformer类

class Transformer(nn.Module):
    r'''
    参数:
        d_model: 词嵌入的维度(必备)(Default=512)
        nhead: 多头注意力中平行头的数目(必备)(Default=8)
        num_encoder_layers:编码层层数(Default=8)
        num_decoder_layers:解码层层数(Default=8)
        dim_feedforward: 全连接层的神经元的数目,又称经过此层输入的维度(Default = 2048)
        dropout: dropout的概率(Default = 0.1)
        activation: 两个线性层中间的激活函数,默认relu或gelu
        custom_encoder: 自定义encoder(Default=None)
        custom_decoder: 自定义decoder(Default=None)
        lay_norm_eps: layer normalization中的微小量,防止分母为0(Default = 1e-5)
        batch_first: 若`True`,则为(batch, seq, feture),若为`False`,则为(seq, batch, feature)(Default:False)
    
    例子:
        >>> transformer_model = Transformer(nhead=16, num_encoder_layers=12)
        >>> src = torch.rand((10, 32, 512))
        >>> tgt = torch.rand((20, 32, 512))
        >>> out = transformer_model(src, tgt)
    '''
    def __init__(self, d_model: int = 512, nhead: int = 8, num_encoder_layers: int = 6,
                 num_decoder_layers: int = 6, dim_feedforward: int = 2048, dropout: float = 0.1,
                 activation = F.relu, custom_encoder: Optional[Any] = None, custom_decoder: Optional[Any] = None,
                 layer_norm_eps: float = 1e-5, batch_first: bool = False) -> None:
        super(Transformer, self).__init__()
        if custom_encoder is not None:
            self.encoder = custom_encoder
        else:
            encoder_layer = TransformerEncoderLayer(d_model, nhead, dim_feedforward, dropout,
                                                    activation, layer_norm_eps, batch_first)
            encoder_norm = nn.LayerNorm(d_model, eps=layer_norm_eps)
            self.encoder = TransformerEncoder(encoder_layer, num_encoder_layers)

        if custom_decoder is not None:
            self.decoder = custom_decoder
        else:
            decoder_layer = TransformerDecoderLayer(d_model, nhead, dim_feedforward, dropout,
                                                    activation, layer_norm_eps, batch_first)
            decoder_norm = nn.LayerNorm(d_model, eps=layer_norm_eps)
            self.decoder = TransformerDecoder(decoder_layer, num_decoder_layers, decoder_norm)

        self._reset_parameters()

        self.d_model = d_model
        self.nhead = nhead

        self.batch_first = batch_first

    def forward(self, src: Tensor, tgt: Tensor, src_mask: Optional[Tensor] = None, tgt_mask: Optional[Tensor] = None,
                memory_mask: Optional[Tensor] = None, src_key_padding_mask: Optional[Tensor] = None,
                tgt_key_padding_mask: Optional[Tensor] = None, memory_key_padding_mask: Optional[Tensor] = None) -> Tensor:
        r'''
        参数:
            src: 源语言序列(送入Encoder)(必备)
            tgt: 目标语言序列(送入Decoder)(必备)
            src_mask: (可选)
            tgt_mask: (可选)
            memory_mask: (可选)
            src_key_padding_mask: (可选)
            tgt_key_padding_mask: (可选)
            memory_key_padding_mask: (可选)
        
        形状:
            - src: shape:`(S, N, E)`, `(N, S, E)` if batch_first.
            - tgt: shape:`(T, N, E)`, `(N, T, E)` if batch_first.
            - src_mask: shape:`(S, S)`.
            - tgt_mask: shape:`(T, T)`.
            - memory_mask: shape:`(T, S)`.
            - src_key_padding_mask: shape:`(N, S)`.
            - tgt_key_padding_mask: shape:`(N, T)`.
            - memory_key_padding_mask: shape:`(N, S)`.

            [src/tgt/memory]_mask确保有些位置不被看到,如做decode的时候,只能看该位置及其以前的,而不能看后面的。
            若为ByteTensor,非0的位置会被忽略不做注意力;若为BoolTensor,True对应的位置会被忽略;
            若为数值,则会直接加到attn_weights

            [src/tgt/memory]_key_padding_mask 使得key里面的某些元素不参与attention计算,三种情况同上

            - output: shape:`(T, N, E)`, `(N, T, E)` if batch_first.

        注意:
            src和tgt的最后一维需要等于d_model,batch的那一维需要相等
            
        例子:
            >>> output = transformer_model(src, tgt, src_mask=src_mask, tgt_mask=tgt_mask)
        '''
        memory = self.encoder(src, mask=src_mask, src_key_padding_mask=src_key_padding_mask)
        output = self.decoder(tgt, memory, tgt_mask=tgt_mask, memory_mask=memory_mask,
                              tgt_key_padding_mask=tgt_key_padding_mask,
                              memory_key_padding_mask=memory_key_padding_mask)
        return output
        
    def generate_square_subsequent_mask(self, sz: int) -> Tensor:
        r'''产生关于序列的mask,被遮住的区域赋值`-inf`,未被遮住的区域赋值为`0`'''
        mask = (torch.triu(torch.ones(sz, sz)) == 1).transpose(0, 1)
        mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
        return mask

    def _reset_parameters(self):
        r'''用正态分布初始化参数'''
        for p in self.parameters():
            if p.dim() > 1:
                xavier_uniform_(p)
# 小例子
transformer_model = Transformer(nhead=16, num_encoder_layers=12)
src = torch.rand((10, 32, 512))
tgt = torch.rand((20, 32, 512))
out = transformer_model(src, tgt)
print(out.shape)
# torch.Size([20, 32, 512])

Reference

(1)datawhale notebook
(2)基于 TensorFlow 的 神经机器翻译 (seq2seq) :https://github.com/tensorflow/nmt
(3)哈尔滨工业大学张贤同学翻译https://twitter.com/JayAlammar(经原作者 @JayAlammmar 授权)撰写
(4)http://jalammar.github.io/illustrated-transformer/
(5)datawhale台运鹏transformer代码
(6)jupter notebookPytorch编写完整的Transformer
(7)http://mantchs.com/2019/09/26/NLP/Transformer/
(8)https://relph1119.github.io/my-team-learning/#/transformers_nlp28
(9)https://www.bilibili.com/video/BV1Jk4y1m7Gc?from=search&seid=5210525891523790434

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山顶夕景

小哥哥给我买个零食可好

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值