简介
Transformer模型最早是由Google于2017年在“Attention is all you need”一文中提出,在论文中该模型主要是被用于克服机器翻译任务中传统网络训练时间过长,难以较好实现并行计算的问题。后来,由于该方法在语序特征的提取效果由于传统的RNN、LSTM而被逐渐应用至各个领域,如GPT模型以及目前Google最新出品的新贵Bert模型等。相较于传统的RNN、LSTM或注意力集中机制,Transformer模型已经抛弃了以往的时序结构(这也是其为何能够很好的并行,且训练速度较快的原因),更准确的来说其实际上是一种编码机制,该编码同时包括了语义信息(Multi-Head Attention)和位置信息(Positional Encoding)。
transformer 直观认识:
1. transformer 实质上就是一个 encoder decoder 的模型结构,在翻译任务中,就是把一个源语言翻译成对应的目标语言,首先将原句子进行编码处理encoder 提取句子里的特征,然后将提取到的特征输入到 decoder 结构进行解码处理,最终输出结果目标语言。
2. 跟 seq2seq 模型的处理任务目标本质上是一致的,只不过跟 传统的循环神经网络 RNN,LSTM等最大的区别是,LSTM 的训练是迭代的、串行的,必须要等当前字处理完,才可以处理下一个字。而 Transformer 的训练时并行的,即所有字是同时训练的,这样就大大增加了计算效率。同时 transformer 引入了位置嵌入(Positional Encoding)来处理语言的位置顺序,使用 自注意力机制(Self Attention Mechanism)和全连接层进行计算。下面主要从encoder decoder两部分来记录其中的模型结构细节。
Encoder:
encoder部分核心要做的就是:把自然语言序列映射为隐藏层的数学表达式的过程。
首先上一张图:
下面按照图中的标号记录:
1. Positional Encoding
1)前面提到了 transformer 跟传统的循环神经网络不同的是它可以并行处理语言序列,但是这个也带来另一个问题就是不能像 串行的RNN,LSTM等序列模型一样带有语言顺序信息,所以google 引入了 位置嵌入的概念。
2)位置嵌入的维度跟词向量的维度一样:[max_seq_len, emb_dim ]
max_seq_len:是指定输入序列为相同的最大长度,如果输入句子不够的会进行 padding 操作
emb_dim :是指词向量和位置向量的维度
transformer 一般会以 字 为单位训练,初始化字编码的大小为:[vocab_size, emb_dim],vocab_size是字库中所有字的数量,对应到pytorch 中 的实现就是:nn.Embedding(src_vocab_size, emb_dim)
关于 nn.Embedding 的理解 可以查看我的这篇博客:nn.Embedding - 简书
3)具体的实现论文中使用的是 和 函数的线性变换来提供给模型位置信息:
上式中 指的是一句话中某个字的位置,取值范围是[0, max_seq_len] , 指的是字向量的维度序号,取值范围是 [0 , emb_dim/2 ], 指的是 emb_dim 的值
上图的公式 ,对应着 维度的一组奇数和偶数的序列的维度,例如一组,一组,分别用上面的和 函数处理,从而产生不同的周期变化,而位置嵌入在 维度上随着维度序号增大,周期变化会越来越慢,最终产生一种包含位置信息的纹理,论文原文所讲的位置嵌入的周期从 到 变化,而每一个位置在 维度都会得到不同周期的 和 函数的取值组合,从而产生独一的纹理位置信息,最终使得模型学到位置之间的依赖关系和自然语言的时序特性
可以看到图中每一行对应一个词向量的位置编码,所以第一行对应着输入序列的第一个词。每行包含512个值,每个值介于1和-1之间。20字(行)的位置编码实例,词嵌入大小为512(列)。可以看到它从中间分裂成两半。这是因为左半部分的值由一个函数(使用正弦)生成,而右半部分由另一个函数(使用余弦)生成。然后将它们拼在一起而得到每一个位置编码向量。位置编码的方式还有很多,选择这一种的优点是能够扩展到未知的序列长度(例如,当我们训练出的模型需要翻译远比训练集里的句子更长的句子时),因为这种方式总能体现出序列之间的相对位置关系。
作者写了一段代码取位置编码向量的四个维度进行可视化:
plt.figure(figsize=(15, 5))
pe = PositionalEncoding(20, 0)
y = pe.forward(Variable(torch.zeros(1, 100, 20)))
plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())
plt.legend(["dim %d"%p for p in [4,5,6,7]])
可以看出,位置编码基于不同位置添加了正弦波,对于每个维度,波的频率和偏移都有不同。也就是说对于序列中不同位置的单词,对应不同的正余弦波,可以认为他们有相对关系。
关于这个位置编码详细的可以看看知乎这篇回答:
如何理解Transformer论文中的positional encoding,和三角函数有什么关系? - 知乎
4) 输入的句子序列通过了 词嵌入之后,转换成了word embedding 得到每个字的向量,然后通过位置编码层(Positional Encoding)之后得到每个字的位置向量,将这两个向量进行直接相加,不是拼接concat,两个向量的维度一样:[batch_size, src_len, emb_dim],得到每个字的最终编码向量,然后进入下一层。
2. Self Attention Mechanism
1)关于自注意力机制的原理,首先是定义三个矩阵:,使用这三个矩阵对每个字向量进行三次线性变换,使得每个字向量衍生出三个新的向量:;接着把所有的向量拼成一个大矩阵,叫做查询矩阵,将所有的向量拼接成键矩阵记作,将所有的向量拼接成值矩阵记作。这个对应到里就是使用一个 nn.Linear()进行
2)然后自注意力机制就是把每个字向量对应的查询向量去跟每个字对应的键向量(包括自己的)做点积得到一个值,这里得到的值就可以体现两个字之间的相关程度,为什么呢?首先在数学里面的定义,向量之间的内积就是表示两个向量之间的关系,内积越大,表示两个向量越相关,所以这个地方每个向量跟其他向量的点积就可以代表跟序列中的每个词的相关程度。这就是自注意力的核心。下图以第一个词为例去做点积:
每个字向量衍生出三个向量:
然后将点积得到的结果经过一个,是的其相加为1
得到权重之后将这个权重分别乘以对应的字的值向量
最后将这些权重化后的值向量求和,得到第一个字的输出:
整个过程下面这张图可以看到:
其他字向量经过同样的操作就可以得到通过 self-attention 之后的全部输出:
3)上面是以一句话为例,依次遍历每个字向量经过 attention 操作,实际中这样效率不高,可以采取矩阵相乘的方式进行批量操作,不是计算出某一个时刻的,而是一次性计算出所有时刻的,计算过程如下:
这里的是矩阵代表一句话,每一行代表一个字的向量表示
然后将,然后除以,经过之后再乘得到输出
这种通过 query 和 key 的相似性程度来确定 value 的权重分布的方法被称为scaled dot-product attention,这里为什么要除以一个 进行缩放呢?实际上是的最后一个维度,当越大,就越大,可能会将softmax函数推入梯度极小的区域,所以引引入一个调节因子,使得内积不至于太大.
Multi-Head Attention:
论文中提出了一个多头注意力机制,实际上是一样的,就是定义多组,分别去关注不同的上下文,计算过程完全一样,只是线性变换从一组变成多组而已,如图:
然后每一组得到一个输出矩阵,多组就得到多组,最后将多组进行concat:
实际上Multi-Head-Attention 就是将 embedding 之后的按维度切割成个,分别做self-attention之后再合并在一起.
padding mask:
transformer的计算过程了解之后,里面有一个细节需要注意就是,在进行词嵌入的时候,通常会根据一个 进行补0的 padding 操作,但是在计算上面一系列相关权重的时候,是每个维度都参与计算,并且都会经过一个:
,这样的话 padding 的部分就会参与运算,这样显然不行,于是需要做一个 mask 操作,让这些无效的区域不参与运算,一般是给无效区域加一个很大的负数偏置,即:
3. 残差连接和归一化:
残差连接就是,将输入注意力层之前的字向量,与经过自注意力层之后的输出层的向量相加:
至于为什么要加这么一层呢?残差连接到底啥意思:
一般来说,深度学习的套路就是依靠误差的链式反向传播来进行参数更新,随着深度网络层数的加深,带来了一系列问题,如梯度消失,梯度爆炸,模型容易过拟合,计算资源的消耗等问题。随着网络层数的增加发生了网络退化现象,loss先是下降并趋于饱和,然后loss增加。
这些问题有一些解决方案,比如 drop_out 层用来防止过拟合,随机丢弃神经元对网络结构进行轻量化处理,一定程度上解决数据集小和网络复杂的情况下的过拟合问题。或者Rule层(y=max(0,x))主要用来防止梯度消失问题。
先看看残差网络的百度百科定义:
残差网络的特点是容易优化,并且能够通过增加相当的深度来提高准确率。其内部的残差块使用了跳跃连接,缓解了在深度神经网络中增加深度带来的梯度消失问题。
深入理解可以看原论文:《Deep Residual Learning for Image Recognition》
直观理解就是:我们可以使用一个非线性变化函数来描述一个网络的输入输出,即输入为X,输出为F(x),F通常包括了卷积,激活等操作,强行将一个输入添加到函数的输出;
为什么这么做,深度学习的套路就是依靠误差的链式反向传播来进行参数更新,链式法则求导的过程中一旦其中某一个导数很小,多次连乘后梯度可能越来越小,就是常说的梯度消散,对于深层网络,传到浅层几乎就没了。但是如果使用了残差,每一个导数就加上了一个恒等项1,此时就算原来的导数df/dx很小,这时候误差仍然能够有效的反向传播,这就是核心思想。
具体的理解可以看这篇:【模型解读】resnet中的残差连接,你确定真的看懂了? - 知乎
Layer Normalization
Layer Normalization 的作用是把神经网络中隐藏层归一为标准正态分布,也就是 独立同分布,以起到加快训练速度,加速收敛的作用。
因为神经网络的训练过程本质就是对数据分布的学习,因此训练前对输入数据进行归一化处理显得很重要。我们知道,神经网络有很多层,每经过一个隐含层,训练数据的分布会因为参数的变化而发生改变,导致网络在每次迭代中都需要拟合不同的数据分布,这样子会增加训练的复杂度以及过拟合的风险。
因此我们需要对数据进行归一化处理(均值为0,标准差为1),把数据分布强制统一在一个数据分布下,而且这一步不是一开始做的,而是在每次进行下一层之前都需要做的。也就是说,在网络的每一层输入之前增加一个当前数据归一化处理,然后再输入到下一层网路中去训练。
layer normalization 的做法是在每一个样本上计算均值和方差;用每一列的每一个元素减去这列的均值,再除以这列的标准差,从而得到归一化后的数值。图中另外一种是 batch normalization,是在batch 上计算均值和方差,可以使得数据收敛更快,但是缺点会比较明显,比如当Batch size很小的适合,BN的效果就非常不理想了。在很多情况下,Batch size大不了,因为你GPU的显存不够。
Feed Forward:
前馈神经网络,进行两次线性变换加上一个激活函数,信息流向下一个block,起到提取特征的作用,序列此时的模块可以并行的流向独立的前馈神经单元,不像self-attention 层,每个序列单元之间不是独立的,存在某种关联。
下面是整个encoder部分的结构图:
输入 x1,x2 经 self-attention 层之后变成 z1,z2,然后和输入 x1,x2 进行残差连接,经过 LayerNorm 后输出给全连接层。全连接层也有一个残差连接和一个 LayerNorm,最后再输出给下一个 Encoder(每个 Encoder Block 中的 FeedForward 层权重都是共享的),多个这样的层堆叠起来,最后才得到输出,其中每个block的内容都是一样的。
下面是关于encoder部分的总结:
1)字向量与位置编码:
2)自注意力机制:
3)self-attention 残差连接与 Layer Normalization
4)下面进行 Encoder block 结构图中的第 4 部分,也就是 FeedForward,就是两层线性映射并用激活函数激活,比如说 ReLU
5)FeedForward 残差连接与 Layer Normalization:
Decoder:
终于来到decoder了,跟encoder实际上差不多,先看看整体的图:
1. Masked Multi-Head Attention:
1)跟encoder一样,以翻译任务为例,首先会将目标句子,图中就是 Outputs 进行自编码转换成字向量,再经过位置编码(Positional Encoding),然后经过一个 decoder 部分的 self-attention,这里的自注意力机制跟encoder的操作基本一样,除了为补齐句子长度而做的padding 部分进行 mask之外,还有一个 mask,这个mask是为了防止作弊的,一般叫做 sequence mask.
2)传统 Seq2Seq 中 Decoder 使用的是 RNN 模型,因此在训练过程中输入 t 时刻的词,根据之前序列来处理句子的每一个时刻的输入,模型无法看到未来时刻的词,因为循环神经网络是时间驱动的,只有当 t 时刻运算结束了,才能看到 t+1 时刻的词。
而 Transformer Decoder 抛弃了 RNN,改为 Self-Attention,由此就产生了一个问题,在训练过程中,整个 ground truth 都暴露在 Decoder 中,这显然是不对的,我们需要对 Decoder 的输入进行一些处理,见下图:
decoder的输入同样经过attention之后产生的矩阵称为:Scaled Scores,例如输入句子:
"<start> I am fine",当我们输入 "I" 时,模型目前仅知道包括 "I" 在内之前所有字的信息,即
"<start>" 和 "I" 的信息,不应该让其知道 "I" 之后词的信息,因为做预测的时候是一个词预测完了之后再根据之前的信息,然后好预测下一个词的信息,所以如果此时模型实现就知道之后的信息,那就是作弊了呀,所以这个时候的处理是:首先生成一个下三角全 0,上三角全为负无穷的矩阵,然后将其与 Scaled Scores 相加即可,之后再做 softmax,就能将 - inf 变为 0,得到的这个矩阵即为每个字之间的权重:
2)Masked Encoder-Decoder Attention:
模型经过了Decoder Self-Attention 层之后就进入下一个模块,这也是一个 masked attention,只不过这里的是encoder的输出,是 Decoder 的 Masked Self-Attention,操作跟前面一样,直接看图就好:
在训练过程中,模型没有收敛得很好时,Decoder预测产生的词很可能不是我们想要的。这个时候如果再把错误的数据再输给Decoder,就会越跑越偏。这个时候怎么办?decoder 的时候是把上一个时刻的输出和当前时刻的输入一起传给下一个解码单元网络。
(1)在训练过程中可以使用 “teacher forcing”。因为我们知道应该预测的word是什么,那么可以给Decoder喂一个正确的结果作为输入。
(2)除了选择最高概率的词 (greedy search),还可以选择是比如 “Beam Search”,可以保留topK个预测的word。 Beam Search 方法不再是只得到一个输出放到下一步去训练了,我们可以设定一个值,拿多个值放到下一步去训练,这条路径的概率等于每一步输出的概率的乘积。
Ending part:
整个 transform 就梳理完了,用一张图直观的表示其结构就是 多个encoder 和 decoder 层的堆叠 特征提取器具:
优点:
(1)每层计算复杂度比RNN要低。
(2)可以进行并行计算。
(3)从计算一个序列长度为n的信息要经过的路径长度来看, CNN需要增加卷积层数来扩大视野,RNN需要从1到n逐个进行计算,而Self-attention只需要一步矩阵计算就可以。Self-Attention可以比RNN更好地解决长时依赖问题。当然如果计算量太大,比如序列长度N大于序列维度D这种情况,也可以用窗口限制Self-Attention的计算数量。
(4)从作者在附录中给出的例子可以看出,Self-Attention模型更可解释,Attention结果的分布表明了该模型学习到了一些语法和语义信息。