Transformer细节思考

今天研究一下Transformer的一些细节,总结一下。

参考:

Transformer原理和实现-从入门到精通

绝对干货!NLP预训练模型:从transformer到albert

Transformer图解

Transformer使用self-attention和position-encoding替代原始的RNN。

图片

每一层encoder

图片

每一层decoder

图片

Transformer具体结构

图片

加入Tensor

输入的句子是一个词(ID)的序列,我们首先通过Embedding把它变成一个连续稠密的向量,如下图所示。

图片

Embedding之后的序列会输入Encoder,首先经过Self-Attention层然后再经过全连接层,如下图所示。

图片

我们在计算𝑧𝑖时需要依赖所有时刻的输入𝑥1,…,𝑥𝑛,不过我们可以用矩阵运算一下子把所有的𝑧𝑖计算出来。而全连接网络的计算则完全是独立的,计算i时刻的输出只需要输入𝑧𝑖就足够了,因此很容易并行计算。下图更加明确的表达了这一点(注意全连接层每一个时刻的参数共享)。

图片

Self-Attention

对于输入的每一个向量(第一层是词的Embedding,其它层是前一层的输出),我们首先需要生成3个新的向量Q、K和V,分别代表查询(Query)向量、Key向量和Value向量。Q表示为了编码当前词,需要去注意(attend to)其它(其实也包括它自己)的词,我们需要有一个查询向量。而Key向量可以认为是这个词的关键的用于被检索的信息,而Value向量是真正的内容。

对于普通的Attention机制,其计算过程如下:

图片

每个向量的Key和Value向量都是它本身,而Q是当前隐状态ℎ𝑡,计算energy的时候我们计算Q(ℎ𝑡)和Key(ℎ¯𝑗)。然后用softmax变成概率,最后把所有的ℎ¯𝑗加权平均得到context向量。

而Transformer使用的self-attention的计算,以t1时刻为例,如下图:

图片

Self-Attention里的Query不是隐状态,并且来自当前输入向量本身,因此叫作Self-Attention。另外Key和Value都不是输入向量,而是输入向量做了一下线性变换。当然理论上这个线性变换矩阵可以是Identity矩阵,也就是使得Key=Value=输入向量。因此可以认为普通的Attention是这里的特例。这样做的好处是模型可以根据数据从输入向量中提取最适合作为Key(可以看成一种索引)和Value的部分。类似的,Query也是对输入向量做一下线性变换,它让系统可以根据任务学习出最适合的Query,从而可以注意到(attend to)特定的内容。

具体的计算过程:比如图中的输入是两个词”thinking”和”machines”,我们对它们进行Embedding(这是第一层,如果是后面的层,直接输入就是向量了),得到向量𝑥1,𝑥2。接着我们用3个矩阵分别对它们进行变换,得到向量𝑞1,𝑘1,𝑣1和𝑞2,𝑘2,𝑣2。比如𝑞1=𝑥1𝑊𝑄。图中𝑥1的shape是1x4,𝑊𝑄是4x3,得到的𝑞1是1x3。其它的计算也是类似的,为了能够使得Key和Query可以内积,我们要求𝑊𝐾和𝑊𝑄的shape是一样的,但是并不要求𝑊𝑉和它们一定一样(虽然实际论文实现是一样的)。

每个时刻t都计算出𝑄𝑡,𝐾𝑡,𝑉𝑡之后,我们就可以来计算Self-Attention了。以第一个时刻为例,我们首先计算𝑞1和𝑘1,𝑘2的内积,得到score。接下来使用softmax把得分变成概率,注意这里把得分除以8, 即sqrt(𝑑𝑘)之后再计算的softmax,根据论文的说法,这样计算梯度时会更加稳定(stable)。接下来用softmax得到的概率对所有时刻的V求加权平均,这样就可以认为得到的向量根据Self-Attention的概率综合考虑了所有时刻的输入信息

Self-Attention的矩阵运算

第一步还是计算Q、K和V,不过不是计算某个时刻的𝑞𝑡,𝑘𝑡,𝑣𝑡了,而是一次计算所有时刻的Q、K和V。计算过程如下图所示。这里的输入是一个矩阵,矩阵的第i行表示第i个时刻的输入𝑥𝑖。

图片

接下来就是计算Q和K得到score,然后除以sqrt(dk),然后再softmax,最后加权平均得到输出。全过程如下图所示。

图片

Multi-Head-Attention

图片

对于输入矩阵(time_step, num_input),每一组Q、K和V都可以得到一个输出矩阵Z(time_step, num_features):

图片

但是后面的全连接网络需要的输入是一个矩阵而不是多个矩阵,因此我们可以把多个head输出的Z按照第二个维度拼接起来,但是这样的特征有一些多,因此Transformer又用了一个线性变换(矩阵𝑊𝑂)对它进行了压缩:

图片

整体过程:

图片

可以Q、K、V在维度上比词嵌入向量更低。他们的维度是64,而词嵌入和编码器的输入/输出向量的维度是512. 但实际上不强求维度更小,这只是一种基于架构上的选择,它可以使多头注意力(multiheaded attention)的大部分计算保持不变。

使用self-attention的好处

比如我们要翻译如下句子”The animal didn’t cross the street because it was too tired”(这个动物无法穿越马路,因为它太累了)。这里的it到底指代什么呢,是animal还是street?要知道具体的指代,我们需要在理解it的时候同时关注所有的单词,重点是animal、street和tired,然后根据知识(常识)我们知道只有animal才能tired,而street是不能tired的。Self-Attention用Encoder在编码一个词的时候会考虑句子中所有其它的词,从而确定怎么编码当前词。如果把tired换成narrow,那么it就指代的是street了。

而LSTM(即使是双向的)是无法实现上面的逻辑的。为什么呢?比如前向的LSTM,我们在编码it的时候根本没有看到后面是tired还是narrow,所有它无法把it编码成哪个词。而后向的LSTM呢?当然它看到了tired,但是到it的时候它还没有看到animal和street这两个单词,当然就更无法编码it的内容了。

当然多层的LSTM理论上是可以编码这个语义的,它需要下层的LSTM同时编码了animal和street以及tired三个词的语义,然后由更高层的LSTM来把it编码成animal的语义。但是这样模型更加复杂。

但如果使用multo-head的attention,在编码it的时候有一个Attention Head(后面会讲到)注意到了Animal,因此编码后的it有Animal的语义。

位置编码

位置编码有很多方法,其中需要考虑的一个重要因素就是需要它编码的是相对位置的关系。比如两个句子:”北京到上海的机票”和”你好,我们要一张北京到上海的机票”。显然加入位置编码之后,两个北京的向量是不同的了,两个上海的向量也是不同的了,但是我们期望Query(北京1)Key(上海1)却是等于Query(北京2)Key(上海2)的。具体的编码算法我们在代码部分再介绍。位置编码加入后的模型如下图所示:

图片

Layer Normalization

几乎所有的归一化方法都能起到平滑损失平面的作用,LN也一样。前面我们介绍过Batch Normalization,这个技巧能够让模型收敛的更快。但是Batch Normalization有一个问题——它需要一个minibatch的数据,而且这个minibatch不能太小(比如1)。另外一个问题就是它不能用于RNN,因为同样一个节点在不同时刻的分布是明显不同的。

假设我们的输入是一个minibatch的数据,我们再假设每一个数据都是一个向量,则输入是一个矩阵,每一行是一个训练数据,每一列都是一个特征。BatchNorm是对每个特征进行Normalization,而LayerNorm是对每个样本的不同特征进行Normalization,因此LayerNorm的输入可以是一行(一个样本)。

如下图所示,输入是(3,6)的矩阵,minibatch的大小是3,每个样本有6个特征。BatchNorm会对6个特征维度分别计算出6个均值和方差,然后用这两个均值和方差来分别对6个特征进行Normalization。而LayerNorm是分别对3个样本的6个特征求均值和方差,因此可以得到3个均值和方差,然后用这3个均值和方差对3个样本来做Normalization。

图片

BatchNorm看起来比较直观,我们在数据预处理也经常会把输入Normalize成均值为0,方差为1的数据,只不过它引入了可以学习的参数使得模型可以更加需要重新缓慢(不能剧烈)的调整均值和方差。而LayerNorm似乎有效奇怪,比如第一个特征是年龄,第二个特征是身高,把一个人的这两个特征求均值和方差似乎没有什么意义。论文里有一些讨论,都比较抽象。当然把身高和年龄平均并没有什么意义,但是对于其它层的特征,我们通过平均”期望”它们的取值范围大体一致,也可能使得神经网络调整参数更加容易,如果这两个特征实在有很大的差异,模型也可以学习出合适的参数让它来把取值范围缩放到更合适的区间。

LN原理详解

在Transformer中,LN被一笔带过,但却是不可或缺的一部分。每个子层的输出值为$\text { Layer } N o r m(x+\text { Sublayer }(x))$,这在网络结构图上非常明显(Norm即LN)。基本上所有的规范化技术,都可以概括为如下的公式:

  • 调整前:$h_{i}=f\left(a_{i}\right)$
  • 调整后:$h_{i}^{\prime}=f\left(\frac{g_{i}}{\sigma_{i}}\left(a_{i}-u_{i}\right)+b_{i}\right)$

对于隐层中某个节点的输出为对激活值 a_i 进行非线性变换 f() 后的h_i ,先使用均值$u$和方差$\sigma_{i}$对 $a_i$ 进行分布调整。如果将其理解成正态分布,就是把“高瘦”和“矮胖”的都调整回正常体型(深粉色),把偏离x=0的拉回中间来(淡紫色)。

图片

这样做的第一个好处(平移)是,可以让激活值落入f()的梯度敏感区间(红色虚线的中间段)。梯度更新幅度变大,模型训练加快。第二个好处是,可以将每一次迭代的数据调整为相同分布(相当于“白化”),消除极端值,提升训练稳定性。

然而,在梯度敏感区内,隐层的输出接近于“线性”,模型表达能力会大幅度下降。引入 gain 因子g_i和 bias 因子b_i,为规范化后的分布再加入一点“个性”。需要注意的是, g_i和 b_i作为模型参数训练得到,$u_i$和$\sigma_{i}$在限定的数据范围内统计得到。BN 和 LN 的差别就在这里,前者在某一个 Batch 内统计某特定神经元节点的输出分布(跨样本),后者在某一次迭代更新中统计同一层内的所有神经元节点的输出分布(同一样本下)。

那么,为什么要舍弃 BN 改用 LN 呢?朴素版的 BN 是为 CNN 任务提出的,需要较大的 BatchSize 来保证统计量的可靠性,并在训练阶段记录全局的$u$和$\sigma$供预测任务使用。对于天然变长的 RNN 任务,需要对每个神经元进行在每个时序的状态进行统计。这不仅把原本非常简单的 BN 流程变复杂,更导致偏长的序列位置统计量不足。相比之下,LN 的使用限制就小很多,不需要在预测中使用训练阶段的统计量,即使 BatchSize = 1 也毫无影响。

个人理解,对于 CNN 图像类任务,每个卷积核可以看做特定的特征抽取器,对其输出做统计是有理可循的;对于 RNN 序列类任务,统计特定时序每个隐层的输出,毫无道理可言——序列中的绝对位置并没有什么显著的相关性。相反,同一样本同一时序同一层内,不同神经元节点处理的是相同的输入,在它们的输出间做统计合理得多。

从上面的分析可以看出,Normalization 通常被放在非线性化函数之前。以 GRU 为例,来看看 LN 是怎么设置的:

  • $\left(\begin{array}{l}
    \mathbf{z}_{t} \\
    \mathbf{r}_{t}
    \end{array}\right)=\mathbf{W}_{h} \mathbf{h}_{t-1}+\mathbf{W}_{x} \mathbf{x}_{t}$
  • $\hat{\mathbf{h}}_{t}=\tanh \left(\mathbf{W}_{\mathbf{x}_{t}}+\sigma\left(\mathbf{r}_{t}\right) \odot\left(\mathbf{U} \mathbf{h}_{t-1}\right) \mathbf{h}\right.$
  • $\mathbf{h}_{t}=\left(1-\sigma\left(\mathbf{z}_{t}\right)\right) \mathbf{h}_{t-1}+\sigma\left(\mathbf{z}_{t}\right) \hat{\mathbf{h}}_{\mathbf{t}}$

可以看到,总体的原则是在“非线性之前单独处理各个矩阵”。对于 Transformer,主要的非线性部分在 FFN(ReLU) 和 Self-Attention(Softmax) 的内部,已经没有了显式的循环,但这些逐个叠加的同构子层像极了 GRU 和 LSTM 等 RNN 单元。信息的流动由沿着时序变成了穿过子层,把 LN 设置在每个子层的输出位置,意义上已经不再是“落入sigmoid 的梯度敏感空间来加速训练”了,个人认为更重要的是前文提到的“白化”—— 让每个词的向量化数值更加均衡,以消除极端情况对模型的影响,获得更稳定的深层网络结构 —— 就像这些词从 Embdding 层出来时候那样,彼此只有信息的不同,没有能量的多少。在和之前的 TWWT 实验一样的配置中,删除了全部的 LN 层后模型不再收敛。LN 正如 LSTM 中的tanh,它为模型提供非线性以增强表达能力,同时将输出限制在一定范围内。 因此,对于 Transformer 来说,LN 的效果已经不是“有多好“的范畴了,而是“不能没有”。

MLP中的LN

设H是一层中隐层节点的数量,l是MLP的层数,我们可以计算LN的归一化统计量$u$和$\sigma$:

  • $\mu^{l}=\frac{1}{H} \sum_{i=1}^{H} a_{i}^{l}$
  • $\sigma^{l}=\sqrt{\frac{1}{H} \sum_{i=1}^{H}\left(a_{i}^{l}-\mu^{l}\right)^{2}}$

注意上面统计量的计算是和样本数量没有关系的,它的数量只取决于隐层节点的数量,所以只要隐层节点的数量足够多,我们就能保证LN的归一化统计量足够具有代表性。通过$u^l$和$\sigma^l$可以得到归一化后的值$\hat{\mathbf{a}}^{l}=\frac{\mathbf{a}^{l}-\mu^{l}}{\sqrt{\left(\sigma^{l}\right)^{2}+\epsilon}}$(其中$\epsilon$是一个很小的小数,防止除0)。

在LN中我们也需要一组参数来保证归一化操作不会破坏之前的信息,在LN中这组参数叫做增益(gain)g和偏置(bias) b(等同于BN中的$\gamma$和)$\beta$。假设激活函数为f,最终LN的输出为:$\mathbf{h}^{l}=f\left(\mathbf{g}^{l} \odot \hat{\mathbf{a}}^{l}+\mathbf{b}^{l}\right)$。合并公式并忽略参数l可以得到:$\mathbf{h}=f\left(\frac{\mathbf{g}}{\sqrt{\sigma^{2}+\epsilon}} \odot(\mathbf{a}-\mu)+\mathbf{b}\right)$

RNN中的LN

在RNN中,我们可以非常简单的在每个时间片中使用LN,而且在任何时间片我们都能保证归一化统计量统计的是H个节点的信息。对于RNN时刻 t 时的节点,其输入是 t-1 时刻的隐层状态 h^{t-1}和 t 时刻的输入数据x_t,可以表示为:$\mathbf{a}^{t}=W_{h h} h^{t-1}+W_{x h} \mathbf{x}^{t}$。

接着我们便可以在$\mathbf{a}^{t}$上采取归一化过程:

  • $\mu^{t}=\frac{1}{H} \sum_{i=1}^{H} a_{i}^{t}$
  • $\sigma^{t}=\sqrt{\frac{1}{H} \sum_{i=1}^{H}\left(a_{i}^{t}-\mu^{t}\right)^{2}}$
  • $\mathbf{h}^{t}=f\left(\frac{\mathbf{g}}{\sqrt{\left(\sigma^{t}\right)^{2}+\epsilon}} \odot\left(\mathbf{a}^{t}-\mu^{t}\right)+\mathbf{b}\right)$

LSTM中的LN

LSTM的计算公式为:

  • $\left(\begin{array}{c}
    \mathbf{f}_{t} \\
    \mathbf{i}_{t} \\
    \mathbf{o}_{t} \\
    \mathbf{g}_{t}
    \end{array}\right)=\mathbf{W}_{h} \mathbf{h}_{t-1}+\mathbf{W}_{x} \mathbf{x}_{t}+b$
  • $\mathbf{c}_{t}=\sigma\left(\mathbf{f}_{t}\right) \odot \mathbf{c}_{t-1}+\sigma\left(\mathbf{i}_{t}\right) \odot \tanh \left(\mathbf{g}_{t}\right)$
  • $\mathbf{h}_{t}=\sigma\left(\mathbf{o}_{t}\right) \odot \tanh \left(\mathbf{c}_{t}\right)$

使用了LN后的计算公式为:

  • $\left(\begin{array}{c}
    \mathbf{f}_{t} \\
    \mathbf{i}_{t} \\
    \mathbf{o}_{t} \\
    g_{t}
    \end{array}\right)=\quad L N\left(\mathbf{W}_{h} \mathbf{h}_{t-1} ; \boldsymbol{\alpha}_{1}, \boldsymbol{\beta}_{1}\right)+L N\left(\mathbf{W}_{x} \mathbf{x}_{t} ; \boldsymbol{\alpha}_{2}, \boldsymbol{\beta}_{2}\right)+b$
  • $\mathbf{c}_{t}=\sigma\left(\mathbf{f}_{t}\right) \odot \mathbf{c}_{t-1}+\sigma\left(\mathbf{i}_{t}\right) \odot \tanh \left(\mathbf{g}_{t}\right)$
  • $\mathbf{h}_{t}=\sigma\left(\mathbf{o}_{t}\right) \odot \tanh \left(L N\left(\mathbf{c}_{t} ; \boldsymbol{\alpha}_{3}, \boldsymbol{\beta}_{3}\right)\right)$

对照实验

这里我们设置了一组对照试验来对比普通网络,BN以及LN在MLP和RNN上的表现。

MLP上的归一化

这里使用的是MNIST数据集,但是归一化操作只添加到了后面的MLP部分。Keras官方源码中没有LN的实现,我们可以通过pip install keras-layer-normalization进行安装,使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from keras_layer_normalization import LayerNormalization
# 构建LN CNN网络
model_ln = Sequential()
model_ln.add(Conv2D(input_shape = (28,28,1), filters=6, kernel_size=(5,5), padding='valid', activation='tanh'))
model_ln.add(MaxPool2D(pool_size=(2,2), strides=2))
model_ln.add(Conv2D(input_shape=(14,14,6), filters=16, kernel_size=(5,5), padding='valid', activation='tanh'))
model_ln.add(MaxPool2D(pool_size=(2,2), strides=2))
model_ln.add(Flatten())
model_ln.add(Dense(120, activation='tanh'))
model_ln.add(LayerNormalization()) # 添加LN运算
model_ln.add(Dense(84, activation='tanh'))
model_ln.add(LayerNormalization())
model_ln.add(Dense(10, activation='softmax'))

另外两个对照试验也使用了这个网络结构,不同点在于归一化部分。图3左侧是batchsize=128时得到的收敛曲线,从中我们可以看出BN和LN均能取得加速收敛的效果,且BN的效果要优于LN。图3右侧是batchsize=8是得到的收敛曲线,这时BN反而会减慢收敛速度,验证了我们上面的结论,对比之下LN要轻微的优于无归一化的网络,说明了LN在小尺度批量上的有效性:
图片图片

LSTM上的归一化

另外一组对照实验是基于imdb的二分类任务,使用了glove作为词嵌入。这里设置了无LN的LSTM和带LN的LSTM的作为对照试验,网络结构如下面代码:

1
2
3
4
5
6
from lstm_ln import LSTM_LN
model_ln = Sequential()
model_ln.add(Embedding(max_features,100))
model_ln.add(LSTM_LN(128))
model_ln.add(Dense(1, activation='sigmoid'))
model_ln.summary()

LN对于RNN系列动态网络的收敛加速上的效果是略有帮助的。LN的有点主要体现在两个方面:

  • LN得到的模型更稳定;
  • LN有正则化的作用,得到的模型更不容易过拟合。

残差连接

每个Self-Attention层都会加一个残差连接,然后是一个LayerNorm层,如下图所示:

图片

下图展示了更多细节:输入𝑥1、𝑥2经self-attention层之后变成𝑧1、𝑧2,然后和残差连接的输入𝑥1、𝑥2加起来,然后经过LayerNorm层输出给全连接层。全连接层也是有一个残差连接和一个LayerNorm层,最后再输出给上一层:

图片

这样做的好处不言而喻,避免了梯度消失(求导时多了一个常数项)。

Decoder层

Decoder和Encoder是类似的,如下图所示,区别在于它多了一个Encoder-Decoder Attention层,这个层的输入除了来自Self-Attention之外还有Encoder最后一层的所有时刻的输出。Encoder-Decoder Attention层的Query来自下一层,而Key和Value则来自Encoder的输出。

图片

在这个框架下,解码器实际上可看成一个神经网络语言模型,预测的时候,target中的每一个单词是逐个生成的,当前词的生成依赖两方面:一是Encoder的输出,二是target的前面的单词。例如,在生成第一个单词是,不仅依赖于Encoder的输出,还依赖于起始标志[CLS];生成第二个单词是,不仅依赖Encoder的输出,还依赖起始标志和第一个单词…依此类推。这其实是说,在翻译当前词的时候,是看不到后面的要翻译的词。由上可以看出,这里的mask是动态的。

Transformer细节讨论

Scaled Dot-product Attention

图片

首先, Q 与 K 进行了一个点积操作,这个其实就是我在Attention讲到的 score 操作。然后,有一个 Scale 操作,其实就是为了防止结果过大,除以一个尺度标度 sqrt(dk)(注:主要因为dk比较大时,点乘注意力的表现变差,认为是由于点乘后的值过大,导致softmax函数趋近于边缘,梯度较小), 其中dk是Q中一个向量的维度。再然后,经过一个Mask操作; Mask操作是将需要隐藏的数据设置为负无穷,这样经过后面的 Softmax 后就接近于 0,这样这些数据就不会对后续结果产生影响了。最后经过一个 Softmax 层, 然后计算 Attention Value。其公式:$\text {Attention}(Q, K, V)=\operatorname{softmax}\left(\frac{Q K^{T}}{\sqrt{d_{k}}}\right) V$

这里解释一下 Scaled Dot-product Attention 在本文中的应用,也是称为 Self-Attention 的原因所在,这里的Q,K, V 都是一样的,意思就是说,这里是句子对句子自己进行Attention 来查找句子中词之间的关系,这是一件很厉害的事情,回想LSTM是怎么做的,再比较 Self-Attention, 直观的感觉,Self-Attention更能把握住词与词的语义特征,而LSTM对长依赖的句子,往往毫无办法,表征极差。

常用的attention主要有“Add-相加”和“Mul-相乘”两种:

  • $\operatorname{score}\left(h_{j}, s_{i}\right)=[A d d]$
  • $\operatorname{score}\left(h_{j}, s_{i}\right)=\left\langle W_{1} h_{j}, W_{2} s_{i}>\quad[M u l]\right.$

矩阵加法的计算更简单,但是外面套着tanh和 v,相当于一个完整的隐层。在整体计算复杂度上两者接近,但考虑到矩阵乘法已经有了非常成熟的加速算法,Transformer采用了Mul形式。

在模型效果上,《Massive Exploration of Neural Machine Translation Architectures》对不同Attention-Dimension (d_k) 下的Add和Mul进行了对比,如下图:

图片

可以看到,在$d_k$较小的时候,Add和Mul相差不大;随着$d_k$增大,Add明显超越了Mul。Transformer 设置 d_k=64虽然不在表格的范围内,但是可以推测Add仍略优于Mul。作者认为,$d_k$的增大将点积结果推向了softmax函数的梯度平缓区,影响了训练的稳定性。原话是这么说的:

We suspect that for large values of dk, the dot products grow large in magnitude, pushing the softmax function into regions where it has extremely small gradients.

因此,Transformer 为“dot-product attention”加了一个前缀“scaled”,即引入一个温度因子(temperature)$\sqrt{d_{k}}$,中文全称“缩放的点积注意力网络”:$\text {Attention}(Q, K, V)=\operatorname{softmax}\left(Q K^{T} / \sqrt{d_{k}}\right) V$

等等,Add也离不开softmax,怎么没有这个问题呢?首先,左侧v的导数就是 tanh的输出,后者本身就是 [-1,1]之间的,一定不会超限。然后,右侧 W_1的导数就是隐层h_j ,必然是sigmoid或者其他激活函数的输出,值也不会太大。

回到Mul。左侧W_1的导数来自于$h_{j}\left(W_{2} s_{i}\right)$。如果s_i分布在(0,1),那么$W_{2} s_{i}$就会扩展到(0, d_k) ,即点积可能会造成一个非常大的梯度值。

MultiHeadAttention

Transformer 中使用 Multi-head Attention要注意以下几点:

首先, 在Encoder与Decoder中的黑色框中,采用的都是是 Self-Attention ,Q,K,V 同源。

其次,需要注意的是只有在Decoder中的Muti-head中才有 Mask 操作,而在Encoder中却没有,这是因为我们在预测第t个词时,需要将 t 时刻及以后的词遮住,只对前面的词进行 self-attention。

最后, 在黄色框中, 采用的是传统的Attention思路,Q 来自Decoder层, 而 K, V来自Encoder的输出 。

最后的Linear与Softmax

一般都会在最后一层加一个前馈神经网络来增加泛化能力,最后用一个 softmax 来进行预测。

线性层的参数个数为d_model vocab_size, 一般来说,vocab_size会比较大,拿20000为例,那么只这层的参数就有51220000个,约为10的8次方,非常惊人。而在词向量那一层,同样也是这个数值,所以,一种比较好的做法是将这两个全连接层的参数共享,会节省不少内存,而且效果也不会差。

注意:在Encoder的输入embedding、Decoder的输入embedding后,增加了1个scale因子,而生成下一词汇的linear transformation前却未添加scale因子。

Position Embedding

在下图中,每一行对应一个词向量的位置编码,所以第一行对应着输入序列的第一个词。每行包含512个值,每个值介于1和-1之间。我们已经对它们进行了颜色编码,所以图案是可见的。20字(行)的位置编码实例,词嵌入大小为512(列)。你可以看到它从中间分裂成两半。这是因为左半部分的值由一个函数(使用正弦)生成,而右半部分由另一个函数(使用余弦)生成。然后将它们拼在一起而得到每一个位置编码向量。这种编码的优点是能够扩展到未知的序列长度(例如,当我们训练出的模型需要翻译远比训练集里的句子更长的句子时)。

  • $P E_{(p o s, 2 i)}=\sin \left(p o s / 10000^{2 i / d_{\text {mod }}}\right)$
  • $P E_{(p o s, 2 i+1)}=\cos \left(p o s / 10000^{2 i / d_{\text {model }}}\right)$

图片

首先,可以证明出每个位置都能获得唯一的编码。其次,i决定了频率的大小,不同的i可以看成是不同的频率空间中的编码,是相互正交的,通过改变i的值,就能得到多维度的编码,类似于词向量的维度。这里2i<=512($d_{model}$), 一共512维。想象一下,当2i大于$d_{model}$时会出现什么情况,这时sin函数的周期会变得非常大,函数值会非常接近于0,这显然不是我们希望看到的,因为这样和词向量就不在一个量级了,位置编码的作用被削弱了。另外,值得注意的是,位置编码是不参与训练的,而词向量是参与训练的。作者通过实验发现,位置编码参与训练与否对最终的结果并无影响。

其次,之所以对奇偶位置分别编码,论文中是这样解释的:

We chose this function because we hypothesized it would allow the model to easily learn to attend by relative positions, since for any fixed offset k, PEpos+k can be represented as a linear function of PEpos.

相隔 k 个词的两个位置 pos 和 pos+k 的位置编码是由 k 的位置编码定义的一个线性变换,推导公式如下:

  • $P E(p o s+k, 2 i)=P E(p o s, 2 i) P E(k, 2 i+1)+P E(p o s, 2 i+1) P E(k, 2 i)$
  • $P E(p o s+k, 2 i+1)=P E(p o s, 2 i+1) P E(k, 2 i+1)-P E(p o s, 2 i) P E(k, 2 i)$

或者下面的推导更清晰一些:

  • $\begin{aligned}
    P E_{(p o s+k, 2 i)} &=\sin \left((p o s+k) / 10000^{2 i / d_{\text {model}}}\right) \\
    &=\sin \left(p o s / 10000^{2 i / d_{\text {model}}}\right) \cos \left(k / 10000^{2 i / d_{\text {model}}}\right)+\cos \left(p o s / 10000^{2 i / d_{\text {model}}}\right) \sin \left(k / 10000^{2 i / d_{\text {model}}}\right) \\
    &=\cos \left(k / 10000^{2 i / d_{\text {model}}}\right) P E_{(p o s, 2 i)}+\sin \left(k / 10000^{2 i / d_{\text {model}}}\right) P E_{(p o s, 2 i+1)}
    \end{aligned}$
  • $\begin{aligned}
    P E_{(p o s+k, 2 i+1)} &=\cos \left((p o s+k) / 10000^{2 i / d_{\text {model}}}\right) \\
    &=\cos \left(\text {pos} / 10000^{2 i / d_{\text {model}}}\right) \cos \left(k / 10000^{2 i / d_{\text {model}}}\right)-\sin \left(p o s / 10000^{2 i / d_{\text {model}}}\right) \sin \left(k / 10000^{2 i / d_{\text {model}}}\right) \\
    &=\cos \left(k / 10000^{2 i / d_{\text {model}}}\right) P E_{(p o s, 2 i+1)}-\sin \left(k / 10000^{2 i / d_{\text {model}}}\right) P E_{(\text {pos}, 2 i)}
    \end{aligned}$

前馈网络

每个encoderLayer中,多头attention后会接一个前馈网络。这个前馈网络其实是两个全连接层,进行了如下操作:$FFN(x)=max(0,xW1+b1)W2+b2$

1
2
3
4
self.w_1 = nn.Linear(d_model, d_ff)
# self.w_1 = nn.Conv1d(in_features=d_model, out_features=d_ff, kenerl_size=1)
self.w_2 = nn.Linear(d_ff, d_model)
# self.w_2 = nn.Conv1d(in_features=d_ff, out_features=d_model, kenerl_size=1)

这两层的作用等价于两个 kenerl_size=1的一维卷积操作。
FFN 相当于将每个位置的Attention结果映射到一个更大维度的特征空间,然后使用ReLU引入非线性进行筛选,最后恢复回原始维度。需要说明的是,在抛弃了 LSTM 结构后,FFN 中的 ReLU成为了一个主要的能提供非线性变换的单元。

Weight Tying

论文的3.4小节Embeddings and Softmax,有这样一句话:

In our model, we share the same weight matrix between the two embedding layers and the pre-softmax linear transformation, similar to [29].

《Using the Output Embedding to Improve Language Models》这篇文章主要介绍的是RNNLM中的Weight Tying技术,不仅能压缩LM的大小,还能显著改善PPL表现。作者为了证明算法的普适性,在NMT任务上也进行了扩展实验。在seq2seq模型中,decoder可以近似地看作RNNLM,它必不可少地有一个embedding矩阵$U \in R^{C \times H}$和一个pre-softmax矩阵$V \in R^{C \times H}$,来完成词表大小C到隐层大小H的尺度转换:$h_{i n}=U^{T} C, \ldots, h_{p r e_{-} s o f t m a x}=V h_{o u t}$

。Weight Tying在操作上非常简单,即令U=V 。在OPEN-NMT的Pytorch实现版本中,仅仅一行代码:

1
2
if model_opt.share_decoder_embeddings:
generator[0].weight = decoder.embeddings.word_lut.weight

Transformer在英法和英德上,混用了源语言和目标语言的词表,因此使用了升级版的TWWT(Three way weight tying),把encoder的embedding层权重,也加入共享:

1
2
if model_opt.share_embeddings:
tgt_emb.word_lut.weight = src_emb.word_lut.weight

虽然weight共享了,但是embedding和pre-softmax仍然是两个不同的层,因为bias是彼此独立的。
在我个人的理解中,one-hot向量和对U的操作是“指定抽取”,即取出某个单词的向量行;pre-softmax对U的操作是“逐个点积”,对隐层的输出,依次计算其和每个单词向量行的变换结果。虽然具体的操作不同,但在本质上,U和V都是对任一的单词进行向量化表示(H列),然后按词表序stack起来(C行)。因此,两个权重矩阵在语义上是相通的。

也是由于上面两种操作方式的不同,U在反向传播中不如V训练得充分。将两者绑定在一起,缓和了这一问题,可以训练得到质量更高的新矩阵。另外,由于词表大小C通常比模型隐层大小H高出一个数量级,Weight Tying 可以显著减小模型的参数量。这个数据在论文中是28%~51%,我使用Transformer-base在非共享词表的英中方向上进行测试,模型参数量从 131,636,930 减小到 102,177,474,足足的22%。模型更小,收敛更快更容易。

下图是知乎上网友用Transformer-base模型跑的实验结果,绿色是Weight Tying版本,蓝色是基础版本。X轴为step,Y轴为Appromix BLEU。可以看到,两者的收敛速度接近,曲线平稳后绿色明显优于蓝色,峰值相差0.17。

图片

Label Smoothing

Label Smoothing是一种正则化技术,核心功能就是防止过拟合,它的全称是 Label Smoothing Regularization(LSR),是 2015 年的经典论文《Rethinking the Inception Architecture for Computer Vision》的一个副产品。作者认为 LSR 可以避免模型 too confident。关于这一点论文里没有详细的解释,不过对于 NMT 任务来说,鼓励模型产生多样化的译文确实会帮助提升 BLEU 值,毕竟标准译文并不是唯一的。

最著名的正则化技术,dropout,通过随机抹掉一些节点来削弱彼此之间的依赖,通常设置在网络内部隐层。作为 dropout 的配合策略,LSR 考虑的是 softmax 层。

假设目标类别为y,任意类别为,ground-truth 分布为q(k),模型预测分布为p(k)。显然,当k=y时,q(k)=1。当k!=y时,q(k)=0。LSR 为了让模型的输出不要过于贴合单点分布,选择在 gound-truth 中加入噪声。即削弱y的概率,并整体叠加一个独立于训练样例的均匀分布u(k):$q^{\prime}(k)=(1-\epsilon) q(k)+\epsilon u(k)=(1-\epsilon) q(k)+\epsilon / K$。其中K是 softmax 的类别数。拆开来写可以看得清楚一点:

  • $q^{\prime}(k)=1-\epsilon+\epsilon / K, \quad k=y$
  • $q^{\prime}(k)=\epsilon / K, \quad k \neq y$

所有类别的概率和仍然是归一的。说白了就是把最高点砍掉一点,多出来的概率平均分给所有人。调整之后,交叉熵(损失函数)也随之变化:$H\left(q^{\prime}, p\right)=(1-\epsilon) H(q, p)+\epsilon H(u \cdot p)$。对于两个完全一致的分布,其交叉熵为0。LSR 可以看作是在优化目标中加入了正则项 H(u, p),在模型输出偏离均匀分布时施以惩罚。

Warmup & Noam学习率更新

warmup

论文提出了一个全新的学习率更新公式:$\text {lrate}=d_{\text {model}}^{-0.5} \cdot \min \left(\text {step_num}^{-0.5}, \text {step_num} \cdot \text {warmup_steps}^{-1.5}\right)$

如果把min去掉的话,就变成一个以warmup_steps为分界点的分段函数。在该点之后,$\text { lrate }=d_{\text {model }}^{-0.5} \cdot \text { step_num }^{-0.5}$,是 decay 的部分。常用方法有指数衰减(exponential)、分段常数衰减(piecewise-constant)、反时限衰减(inverse-time)等等。Transformer 采用了负幂的形式,衰减速度先快后慢。

在该点之前,$\text { lrate }=d_{\text {model}}^{-0.5} \cdot \text { step_num } \cdot \text { warmup_steps}^{-1.5}$,是 warmup 的部分。Transformer 采用了线性函数的形式,warmup_steps 越大,斜率越小。

图片

画在图上明显很多。Transformer 的学习率更新公式叫作“noam”,它将 warmup 和 decay 两个部分组合在一起,总体趋势是先增加后减小。

warmup为什么有效? 可参考https://www.zhihu.com/question/338066667https://zhuanlan.zhihu.com/p/45410279

weight-decay

指数衰减

指数衰减学习率是先使用较大的学习率来快速得到一个较优的解,然后随着迭代的继续,逐步减小学习率,使得模型在训练后期更加稳定。Transfomer使用的是指数衰减。指数衰减学习率的公式:$\text { decayed_learning_rate = learning_rate\cdotdecay_rate }^{(\text {global_step} / \text {decay_steps})}$

global_step是计数器,从0计数到训练的迭代次数,learning_rate是初始化的学习率,decayed_learning_rate是随着 global_step递增而衰减。显然,当global_step为初值0时, 有下面等式:$\text { decayed_learning_rate = learning_rate }$。

decay_steps用来控制衰减速度,如果decay_steps大一些, global_step/decay_steps就会增长缓慢一些。从而指数衰减学习率decayed_learning_rate就会衰减得慢一否则学习率很快就会衰减为趋近于0。

具体代码可参考:https://zhuanlan.zhihu.com/p/29421235

分段常数衰减

参考:[https://www.jianshu.com/p/125fe2ab085b](https://www.jianshu.com/p/125fe2ab085b)

自然指数衰减

它与指数衰减方式相似,不同的在于它的衰减底数是e,故而其收敛的速度更快,一般用于相对比较容易训练的网络,便于较快的收敛,其更新规则如下:$\text { decayed_learning_rate = learning_rate } * e^{\frac{-d e c a y_{r a t e}}{g l b b a_{s t e p}}}$

下图为为分段常数衰减、指数衰减、自然指数衰减三种方式的对比图,红色的即为分段常数衰减图,阶梯型曲线。蓝色线为指数衰减图,绿色即为自然指数衰减图,很明可以看到自然指数衰减方式下的学习率衰减程度要大于一般指数衰减方式,有助于更快的收敛。

图片

多项式衰减

应用多项式衰减的方式进行更新学习率,这里会给定初始学习率和最低学习率取值,然后将会按照给定的衰减方式将学习率从初始值衰减到最低值,其更新规则如下式所示:

  • $\text { global_step }=\min (\text {global_step, decay_steps})$
  • $\begin{array}{c}
    \text { decayed_learning_rate }=(\text {learning_rate}-\text {end_learning_rate}) *\left(1-\frac{\text {global_step}}{\text {decay_steps}}\right)^{\text {powe}} \\
    +\text {end_learning_rate}
    \end{array}$

需要注意的是,有两个机制,降到最低学习率后,到训练结束可以一直使用最低学习率进行更新,另一个是再次将学习率调高,使用 decay_steps 的倍数,取第一个大于 global_steps 的结果,它是用来防止神经网络在训练的后期由于学习率过小而导致的网络一直在某个局部最小值附近震荡,这样可以通过在后期增大学习率跳出局部极小值。

如下图所示,红色线代表学习率降低至最低后,一直保持学习率不变进行更新,绿色线代表学习率衰减到最低后,又会再次循环往复的升高降低。

图片

余弦衰减

余弦衰减就是采用余弦的相关方式进行学习率的衰减,衰减图和余弦函数相似。其更新机制如下式所示:

  • $\text { global_step }=\min (\text {global_step, decay_steps})$
  • $\text {cosine_decay}=0.5 \left(1+\cos \left(\pi \frac{\text {global_step}}{\text {decay_steps}}\right)\right)$
  • $\text { decayed }=(1-\alpha) * \text { cosine_decay }+\alpha$
  • $\text { decayed_learning_rate = learning_rate * decayed }$

如下图所示,红色即为标准的余弦衰减曲线,学习率从初始值下降到最低学习率后保持不变。蓝色的线是线性余弦衰减方式曲线,它是学习率从初始学习率以线性的方式下降到最低学习率值。绿色噪声线性余弦衰减方式:

图片

Masking

pad mask

通常也是编码端的mask

图片

sequence mask

图片

解码端的mask要同时考虑pad mask和sequece mask

图片

Word Piece

原理

现在基本性能好一些的NLP模型,例如OpenAI GPT,google的BERT,在数据预处理的时候都会有WordPiece的过程。WordPiece字面理解是把word拆成piece一片一片,其实就是这个意思。

WordPiece的一种主要的实现方式叫做BPE(Byte-Pair Encoding)双字节编码。

BPE的过程可以理解为把一个单词再拆分,使得我们的此表会变得精简,并且寓意更加清晰。

比如”loved”,”loving”,”loves”这三个单词。其实本身的语义都是“爱”的意思,但是如果我们以单词为单位,那它们就算不一样的词,在英语中不同后缀的词非常的多,就会使得词表变的很大,训练速度变慢,训练的效果也不是太好。

BPE算法通过训练,能够把上面的3个单词拆分成”lov”,”ed”,”ing”,”es”几部分,这样可以把词的本身的意思和时态分开,有效的减少了词表的数量。

BPE算法

参考:https://plmsmile.github.io/2017/10/19/subword-units/

BPE的大概训练过程:首先将词分成一个一个的字符,然后在词的范围内统计字符对出现的次数,每次将次数最多的字符对保存起来,直到循环次数结束。

我们模拟一下BPE算法。

  • 我们原始词表如下:{‘l o w e r ‘: 2, ‘n e w e s t ‘: 6, ‘w i d e s t ‘: 3, ‘l o w ‘: 5}
  • 其中的key是词表的单词拆分层字母,再加代表结尾,value代表词出现的频率。
  • 下面我们每一步在整张词表中找出频率最高相邻序列,并把它合并,依次循环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
原始词表 {'l o w e r </w>': 2, 'n e w e s t </w>': 6, 'w i d e s t </w>': 3, 'l o w </w>': 5}
出现最频繁的序列 ('s', 't') 9
合并最频繁的序列后的词表 {'n e w e st </w>': 6, 'l o w e r </w>': 2, 'w i d e st </w>': 3, 'l o w </w>': 5}
出现最频繁的序列 ('e', 'st') 9
合并最频繁的序列后的词表 {'l o w e r </w>': 2, 'l o w </w>': 5, 'w i d est </w>': 3, 'n e w est </w>': 6}
出现最频繁的序列 ('est', '</w>') 9
合并最频繁的序列后的词表 {'w i d est</w>': 3, 'l o w e r </w>': 2, 'n e w est</w>': 6, 'l o w </w>': 5}
出现最频繁的序列 ('l', 'o') 7
合并最频繁的序列后的词表 {'w i d est</w>': 3, 'lo w e r </w>': 2, 'n e w est</w>': 6, 'lo w </w>': 5}
出现最频繁的序列 ('lo', 'w') 7
合并最频繁的序列后的词表 {'w i d est</w>': 3, 'low e r </w>': 2, 'n e w est</w>': 6, 'low </w>': 5}
出现最频繁的序列 ('n', 'e') 6
合并最频繁的序列后的词表 {'w i d est</w>': 3, 'low e r </w>': 2, 'ne w est</w>': 6, 'low </w>': 5}
出现最频繁的序列 ('w', 'est</w>') 6
合并最频繁的序列后的词表 {'w i d est</w>': 3, 'low e r </w>': 2, 'ne west</w>': 6, 'low </w>': 5}
出现最频繁的序列 ('ne', 'west</w>') 6
合并最频繁的序列后的词表 {'w i d est</w>': 3, 'low e r </w>': 2, 'newest</w>': 6, 'low </w>': 5}
出现最频繁的序列 ('low', '</w>') 5
合并最频繁的序列后的词表 {'w i d est</w>': 3, 'low e r </w>': 2, 'newest</w>': 6, 'low</w>': 5}
出现最频繁的序列 ('i', 'd') 3
合并最频繁的序列后的词表 {'w id est</w>': 3, 'newest</w>': 6, 'low</w>': 5, 'low e r </w>': 2}

这样我们通过BPE得到了更加合适的词表了,这个词表可能会出现一些不是单词的组合,但是这个本身是有意义的一种形式,加速NLP的学习,提升不同词之间的语义的区分度。

join BPE

为目标语言和原语言一起使用BPE,即联合两种语言的词典去做BPE。提高了源语言和目标语言的分割一致性。训练中一般concat两种语言。

机器翻译时,编码与解码共享wordpiece model,可以处理翻译时目标语言和当前语言直接拷贝的词情况。

阻止训练发散的方法

降低学习速率、增加warmup_steps以及引入梯度截断。

Dropout使用

dropout设置在word embedding与position embedding相加之后,以及add+layernorm之前。

Checkpoint average

指将训练一段时间的存储的checkpoint(文中选择为5个或20个),将这些checkpoint的所有模型的参数求平均。

Xavier 初始化

在官方 tensor2tensor 代码中,可以看到关于 weight 参数初始化的时候,采用了一种叫作xavier_uniform 的方法。也可以简称为 Xavier(读作 [ˈzeɪvjər]) 或者 Glorot,来自于作者 Xavier Glorot 在 2010 年和 Bengio 大神一起发表的《Understanding the difficulty of training deep feedforward neural networks》。

Xavier 是一种均匀初始化。其基线是朴素版本,即对于包含 n_i 个输入单元的网络层 layer_i ,其 weight 的初始值均匀采样自$\left[-1 / \sqrt{\left.\left.\left(n_{i}\right), 1 / \sqrt{(} n_{i}\right)\right]}\right.$。作者发现,在深层神经网络中,朴素版本的均匀初始化和激活函数(sigmoid 与 tanh)配合得不好。具体说来,就是不仅收敛得慢,训练平稳后的模型效果还差。如果使用预训练的模型来初始化网络,则能够明显改善训练过程。因此,作者认为,更换初始化方式是可取的。

图片

首先来看看 sigmoid。它的线性区在 0.5 附近,偏向 1 或 0 表示则表示饱和。在理想情况下,激活值应当落在表达能力最强的非线性区间,远离线性区与饱和区。然而,在实际训练中,输出层 layer-4 在很长一段时间内都接近下饱和(黑);其他层由上往下离线性区越来越近(蓝绿红)。虽然从 epoch-100 开始慢慢正常了,但这显然拖慢了收敛过程。

图片

然后是正负对称的 tanh。随着训练进行,自下而上(红绿蓝黑青),各层慢慢地都落入了饱和区。明明验证集的效果还达不到预期,模型却“训不动”了。

那么,我们需要什么样的激活值分布呢?一个标准的第 i 层可以分为线性$s^{i}=z^{i} W^{i}+b^{i}$和非线性$z^{i+1}=f\left(s^{i}\right)$两部分。在深层网络中,激活值的方差会自下而上逐层累积, 链式推导可得:$\operatorname{Var}\left[z^{i}\right]=\operatorname{Var}[x] \prod_{j=0}^{i-1} n_{j} \operatorname{Var}\left[W^{j}\right]$

对于反向传播,假定输出落在激活函数的线性区,即$f^{\prime}\left(s_{k}^{i}\right) \approx 1$。同样应用链式法则,各参数的梯度为(d 为总层数):

  • $\operatorname{Var}\left[\frac{\partial \text { cost }}{s^{i}}\right]=\operatorname{Var}\left[\frac{\partial \text { cost }}{s^{d}}\right] \prod_{j=i}^{d} n_{j+1} \operatorname{Var}\left[W^{j}\right]$
  • $\operatorname{Var}\left[\frac{\partial \operatorname{cost}}{w^{i}}\right]=\operatorname{Var}\left[\frac{\partial \operatorname{cost}}{s^{i}}\right] \operatorname{Var}\left[z^{i}\right]$

现在要回到正题上了。一个稳定的深层网络,要求$\operatorname{Var}\left[z^{i}\right]$和$\operatorname{Var}\left[\partial \operatorname{cost} / s^{i}\right]$在各层保持一致,这样可以让激活值(forward)始终远离饱和区,并且梯度(backward)不会消失或爆炸。根据上面的公式,可以得到约束目标:$\forall i, n_{i} \operatorname{Var}\left[W^{i}\right]=1=n^{i+1} \operatorname{Var}\left[W^{i}\right]$

于方差是和输入单元的个数有关的,所以正向和反向分别使用了$n_i$和$n_{i+1}$。这两个等式显然不能同时成立,只好折中一下,让$\operatorname{Var}\left[W^{i}\right]=2 /\left(n_{i}+n_{i+1}\right)$。补充一条基础知识,对于均匀分布U[a,b] ,其方差为$(b-a)^{2} / 12$。现在开始解方程,让 a=b 并且$(b-a)^{2} / 12=2 /\left(n_{i}+n_{i+1}\right)$,得到 Xavier 均匀分布的最终形式:$U\left[-\sqrt{\frac{6}{n_{i}+n_{i+1}}}, \quad \sqrt{\frac{6}{n_{i}+n_{i+1}}}\right]$

Transformer有哪些缺点

  • 评价不同模型需要考虑的是:
    • 句法特征提取能力
    • 语义特征提取能力:Transformer >> 原生CNN == 原生RNN
    • 长距离特征捕获能力:Transformer > 原生RNN >> 原生CNN
    • 任务综合特征抽取能力:Transformer最强
    • 并行计算能力及运行效率:Transformer = 原生CNN >> 原生RNN
  • 超长文本:Transformer-XL
  • 计算简化:ALBert