B站视频讲解php
Transformer是谷歌大脑在2017年末发表的论文attention is all you need中所提出的seq2seq模型。如今已经取得了大范围的应用和扩展,而BERT就是从Transformer中衍生出来的预训练语言模型html
这篇文章分为如下几个部分python
Transformer和LSTM的最大区别,就是LSTM的训练是迭代的、串行的,必需要等当前字处理完,才能够处理下一个字。而Transformer的训练时并行的,即全部字是同时训练的,这样就大大增长了计算效率。Transformer使用了位置嵌入(Positional Encoding)来理解语言的顺序,使用自注意力机制(Self Attention Mechanism)和全链接层进行计算,这些后面会讲到git
Transformer模型主要分为两大部分,分别是Encoder和Decoder。Encoder负责把输入(语言序列)隐射成隐藏层(下图中第2步用九宫格表明的部分),而后解码器再把隐藏层映射为天然语言序列。例以下图机器翻译的例子(Decoder输出的时候,是经过N层Decoder Layer才输出一个token,并非经过一层Decoder Layer就输出一个token)github
本篇文章大部份内容在于解释Encoder部分,即把天然语言序列映射为隐藏层的数学表达的过程。理解了Encoder的结构,再理解Decoder就很简单了markdown
上图为Transformer Encoder Block结构图,注意:下面的内容标题编号分别对应着图中1,2,3,4个方框的序号网络
因为Transformer模型没有循环神经网络的迭代操做, 因此咱们必须提供每一个字的位置信息给Transformer,这样它才能识别出语言中的顺序关系app
如今定义一个位置嵌入的概念,也就是Positional Encoding,位置嵌入的维度为[max_sequence_length, embedding_dimension]
, 位置嵌入的维度与词向量的维度是相同的,都是embedding_dimension
。max_sequence_length
属于超参数,指的是限定每一个句子最长由多少个词构成ide
注意,咱们通常以字为单位训练Transformer模型。首先初始化字编码的大小为[vocab_size, embedding_dimension]
,vocab_size
为字库中全部字的数量,embedding_dimension
为字向量的维度,对应到PyTorch中,其实就是nn.Embedding(vocab_size, embedding_dimension)
svg
论文中使用了sin和cos函数的线性变换来提供给模型位置信息:
上式中 指的是一句话中某个字的位置,取值范围是 , 指的是字向量的维度序号,取值范围是 , 指的是embedding_dimension的值
上面有 和 一组公式,也就是对应着embedding_dimension维度的一组奇数和偶数的序号的维度,例如0,1一组,2,3一组,分别用上面的 和 函数作处理,从而产生不一样的周期性变化,而位置嵌入在embedding_dimension维度上随着维度序号增大,周期变化会愈来愈慢,最终产生一种包含位置信息的纹理,就像论文原文中第六页讲的,位置嵌入函数的周期从 到 变化,而每个位置在embedding_dimension维度上都会获得不一样周期的 和 函数的取值组合,从而产生独一的纹理位置信息,最终使得模型学到位置之间的依赖关系和天然语言的时序特性
若是不理解这里为什么这么设计,能够看这篇文章Transformer中的Positional Encoding
下面画一下位置嵌入,纵向观察,可见随着embedding_dimension序号增大,位置嵌入函数的周期变化愈来愈平缓
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import math
def get_positional_encoding(max_seq_len, embed_dim):
# 初始化一个positional encoding
# embed_dim: 字嵌入的维度
# max_seq_len: 最大的序列长度
positional_encoding = np.array([
[pos / np.power(10000, 2 * i / embed_dim) for i in range(embed_dim)]
if pos != 0 else np.zeros(embed_dim) for pos in range(max_seq_len)])
positional_encoding[1:, 0::2] = np.sin(positional_encoding[1:, 0::2]) # dim 2i 偶数
positional_encoding[1:, 1::2] = np.cos(positional_encoding[1:, 1::2]) # dim 2i+1 奇数
return positional_encoding
positional_encoding = get_positional_encoding(max_seq_len=100, embed_dim=16)
plt.figure(figsize=(10,10))
sns.heatmap(positional_encoding)
plt.title("Sinusoidal Function")
plt.xlabel("hidden dimension")
plt.ylabel("sequence length")
复制代码
plt.figure(figsize=(8, 5))
plt.plot(positional_encoding[1:, 1], label="dimension 1")
plt.plot(positional_encoding[1:, 2], label="dimension 2")
plt.plot(positional_encoding[1:, 3], label="dimension 3")
plt.legend()
plt.xlabel("Sequence length")
plt.ylabel("Period of Positional Encoding")
复制代码
对于输入的句子 ,经过WordEmbedding获得该句子中每一个字的字向量,同时经过Positional Encoding获得全部字的位置向量,将其相加(维度相同,能够直接相加),获得该字真正的向量表示。第 个字的向量记做
接着咱们定义三个矩阵 ,使用这三个矩阵分别对全部的字向量进行三次线性变换,因而全部的字向量又衍生出三个新的向量 。咱们将全部的 向量拼成一个大矩阵,记做查询矩阵 ,将全部的 向量拼成一个大矩阵,记做键矩阵 ,将全部的 向量拼成一个大矩阵,记做值矩阵 (见下图)
为了得到第一个字的注意力权重,咱们须要用第一个字的查询向量 乘以键矩阵K(见下图)
[0, 4, 2]
[1, 0, 2] x [1, 4, 3] = [2, 4, 4]
[1, 0, 1]
复制代码
以后还须要将获得的值通过softmax,使得它们的和为1(见下图)
softmax([2, 4, 4]) = [0.0, 0.5, 0.5]
复制代码
有了权重以后,将权重其分别乘以对应字的值向量 (见下图)
0.0 * [1, 2, 3] = [0.0, 0.0, 0.0]
0.5 * [2, 8, 0] = [1.0, 4.0, 0.0]
0.5 * [2, 6, 3] = [1.0, 3.0, 1.5]
复制代码
最后将这些权重化后的值向量求和,获得第一个字的输出(见下图)
[0.0, 0.0, 0.0]
+ [1.0, 4.0, 0.0]
+ [1.0, 3.0, 1.5]
-----------------
= [2.0, 7.0, 1.5]
复制代码
对其它的输入向量也执行相同的操做,便可获得经过self-attention后的全部输出
上面介绍的方法须要一个循环遍历全部的字 ,咱们能够把上面的向量计算变成矩阵的形式,从而一次计算出全部时刻的输出
第一步就不是计算某个时刻的 了,而是一次计算全部时刻的 和 。计算过程以下图所示,这里的输入是一个矩阵 ,矩阵第 行为第 个词的向量表示
接下来将 和 相乘,而后除以 (这是论文中提到的一个trick),通过softmax之后再乘以 获得输出
这篇论文还提出了Multi-Head Attention的概念。其实很简单,前面定义的一组 可让一个词attend to相关的词,咱们能够定义多组 ,让它们分别关注不一样的上下文。计算 的过程仍是同样,只不过线性变换的矩阵从一组 变成了多组 , ,…以下图所示
对于输入矩阵 ,每一组 、 和 均可以获得一个输出矩阵 。以下图所示
上面Self Attention的计算过程当中,咱们一般使用mini-batch来计算,也就是一次计算多句话,即
的维度是[batch_size, sequence_length]
,sequence_length是句长,而一个mini-batch是由多个不等长的句子组成的,咱们须要按照这个mini-batch中最大的句长对剩余的句子进行补齐,通常用0进行填充,这个过程叫作padding
但这时在进行softmax就会产生问题。回顾softmax函数 , 是1,是有值的,这样的话softmax中被padding的部分就参与了运算,至关于让无效的部分参与了运算,这可能会产生很大的隐患。所以须要作一个mask操做,让这些无效的区域不参与运算,通常是给无效区域加一个很大的负数偏置,即
咱们在上一步获得了通过self-attention加权以后输出,也就是 ,而后把他们加起来作残差链接
Layer Normalization的做用是把神经网络中隐藏层归一为标准正态分布,也就是 独立同分布,以起到加快训练速度,加速收敛的做用
上式以矩阵的列(column)为单位求均值;
上式以矩阵的列(column)为单位求方差
而后用每一列的每个元素减去这列的均值,再除以这列的标准差,从而获得归一化后的数值,加 是为了防止分母为0
下图展现了更多细节:输入 经self-attention层以后变成 ,而后和输入 进行残差链接,通过LayerNorm后输出给全链接层。全链接层也有一个残差链接和一个LayerNorm,最后再输出给下一个Encoder(每一个Encoder Block中的FeedForward层权重都是共享的)
通过上面3个步骤,咱们已经基本了解了Encoder的主要构成部分,下面咱们用公式把一个Encoder block的计算过程整理一下:
1). 字向量与位置编码
2). 自注意力机制
3). self-attention残差链接与Layer Normalization
4). 下面进行Encoder block结构图中的第4部分,也就是FeedForward,其实就是两层线性映射并用激活函数激活,好比说
5). FeedForward残差链接与Layer Normalization
其中
咱们先从HighLevel的角度观察一下Decoder结构,从下到上依次是:
和Encoder同样,上面三个部分的每个部分,都有一个残差链接,后接一个 Layer Normalization。Decoder的中间部件并不复杂,大部分在前面Encoder里咱们已经介绍过了,可是Decoder因为其特殊的功能,所以在训练时会涉及到一些细节
具体来讲,传统Seq2Seq中Decoder使用的是RNN模型,所以在训练过程当中输入 时刻的词,模型不管如何也看不到将来时刻的词,由于循环神经网络是时间驱动的,只有当 时刻运算结束了,才能看到 时刻的词。而Transformer Decoder抛弃了RNN,改成Self-Attention,由此就产生了一个问题,在训练过程当中,整个ground truth都暴露在Decoder中,这显然是不对的,咱们须要对Decoder的输入进行一些处理,该处理被称为Mask
举个例子,Decoder的ground truth为"<start> I am fine",咱们将这个句子输入到Decoder中,通过WordEmbedding和Positional Encoding以后,将获得的矩阵作三次线性变换( )。而后进行self-attention操做,首先经过 获得Scaled Scores,接下来很是关键,咱们要对Scaled Scores进行Mask,举个例子,当咱们输入"I"时,模型目前仅知道包括"I"在内以前全部字的信息,即"<start>"和"I"的信息,不该该让其知道"I"以后词的信息。道理很简单,咱们作预测的时候是按照顺序一个字一个字的预测,怎么能这个字都没预测完,就已经知道后面字的信息了呢?Mask很是简单,首先生成一个下三角全0,上三角全为负无穷的矩阵,而后将其与Scaled Scores相加便可
以后再作softmax,就能将-inf变为0,获得的这个矩阵即为每一个字之间的权重
Multi-Head Self-Attention无非就是并行的对上述步骤多作几回,前面Encoder也介绍了,这里就很少赘述了
其实这一部分的计算流程和前面Masked Self-Attention很类似,结构也一摸同样,惟一不一样的是这里的 为Encoder的输出, 为Decoder中Masked Self-Attention的输出
到此为止,Transformer中95%的内容已经介绍完了,咱们用一张图展现其完整结构。不得不说,Transformer设计的十分巧夺天工
下面有几个问题,是我从网上找的,感受看完以后能对Transformer有一个更深的理解
原论文中说到进行Multi-head Attention的缘由是将模型分为多个头,造成多个子空间,可让模型去关注不一样方面的信息,最后再将各个方面的信息综合起来。其实直观上也能够想到,若是本身设计这样的一个模型,必然也不会只作一次attention,屡次attention综合的结果至少可以起到加强模型的做用,也能够类比CNN中同时使用多个卷积核的做用,直观上讲,多头的注意力有助于网络捕捉到更丰富的特征/信息
这里用代替这个词略显不稳当,seq2seq虽已老,但始终仍是有其用武之地,seq2seq最大的问题在于将Encoder端的全部信息压缩到一个固定长度的向量中,并将其做为Decoder端首个隐藏状态的输入,来预测Decoder端第一个单词(token)的隐藏状态。在输入序列比较长的时候,这样作显然会损失Encoder端的不少信息,并且这样一股脑的把该固定向量送入Decoder端,Decoder端不可以关注到其想要关注的信息。Transformer不但对seq2seq模型这两点缺点有了实质性的改进(多头交互式attention模块),并且还引入了self-attention模块,让源序列和目标序列首先“自关联”起来,这样的话,源序列和目标序列自身的embedding表示所蕴含的信息更加丰富,并且后续的FFN层也加强了模型的表达能力,而且Transformer并行计算的能力远远超过了seq2seq系列模型