如何在nlp中加入attention机制?

做句子的encoding,发现通常的encoding方法无法提取句子中的关键因素,易受无关词汇干扰。 请问如何在句子编码中加入attention机制?…
关注者
90
被浏览
25,045
登录后你可以
不限量看优质回答私信答主深度交流精彩内容一键收藏

原文首发地址:

seq2seq

seq2seq最初是google2014年在《Sequence to Sequence Learning with Neural Networks》提出的,简单的说就是一种序列到另一种序列的转换。在自然语言处理( Natural Language Processing, NLP)应用领域主要有翻译,摘要,作诗等等。

自从seq2seq提出后,关于seq2seq的变体也有很多,但大多都是基于循环神经网络(Recurrent Neural Network,RNN)进行的处理,随后逐渐引入基于attention的处理。但这些都是基于时间步处理不能够进行并发的运算,在2017年facebook在《Convolutional Sequence to Sequence Learning》中提出了基于卷积神经网络seq2seq,由于使用卷积神经网络(Convolutional Neural Networks, CNN),可以做到并行计算,无论在速度和正确率都达到了前所未有的结果。不过同年google发表的《Attention all you need》(就是transformer)吸引人的眼球,基于transformer的研究如雨后春笋似的迸发出来,刷新了nlp研究领域的各个榜单。但是transformer计算量是比较大的,参数也是比较多的,实际的生产还是根据具体业务合理选择的。

本文就先分别介绍基于RNN + attention和基于CNN+attention的seq2seq模型吧。对于transformer,涉及的内容还蛮多的,后期会单独拿出来介绍。

一、循环神经网络

1.循环神经网络(RNN)及其变体

本部分包含RNN,长短期记忆(Long short-term memory, LSTM),门控循环单元(Gate Recurrent Unit,GRU),BPTT(back-propagation through time)等内容。了解这部分内容,需要对神经网络的有一个基本的了解,需要了解深度学习网络的基本输入和输出,模型训练,反向传播等内容。

1.1 RNN

1.1.1 RNN简介

经典的神经网络如下图所示,一条数据有n个特征,j个输出。在术语上,可不用追究隐含层和隐藏层的表达,本质上都是hidden layer。

RNN主要是适用于时间序列的数据,数据的处理单元具有先后顺序的特征,例如:语音,文字等,RNN这种模型就具有一定的记忆能力,能够按时序依次处理任意长度的信息。RNN中模型的输出可以在下一个时间戳直接作为模型的输出。下面我们来看看RNN与经典的神经网络的区别,如下图:

从上图来看,RNN网络与经典的神经网络没什么太大的区别,只是隐含层不断循环。这个圈就代表着神经元的输出在下一个时间步(time step)还会返回来作为输入的一部分,内部的计算与经典的神经网络一致。RNN可以被看做是同一神经网络的对多个时间步的计算和权重更新,如:第i层神经元在t时刻的输入,除了(i-1)层神经元在该时刻的输出外,还包括其自身在(t-1)时刻的输出,对所有的时间步共享参数。按时间点将RNN展开,将得到以下的结构图:

在不同的时间点,RNN的输入都与之前的时间状态有关,t_n时刻网络的输出结果是该时刻的输入和所有历史共同作用的结果,这就达到了对时间序列建模的目的。

1.1.2 RNN相关公式

「1.变量准备」

对RNN有了基本的了解后,再看看相关的数学公式。为了更深刻的了解,可以参考下图:

其中涉及的数学符号意义如下:

  1. x^{(t)}代表在序列索引号 t 时训练样本的输入。同样的,x^{(t-1)}x^{(t+1)} 代表在序列索引号 t−1 和 t+1 时训练样本的输入
  2. h^{(t)} 代表在序列索引号 t 时模型的隐藏状态。h^{(t)}x^{(t)}h^{(t-1)} 共同决定
  3. o^{(t)} 代表在序列索引号 t 时模型的输出。o^{(t)}只由模型当前的隐藏状态h^{(t)} 决定
  4. L^{(t)} 代表在序列索引号 t 时模型的损失函数,模型整体的损失函数是所有的L^{(t)}相加和
  5. y^{(t)} 代表在序列索引号 t 时训练样本序列的真实输出
  6. U,W,V这三个矩阵就是我们的模型的线性关系参数,它在整个RNN网络中是共享的,也是模型主需要学习的参数。也正是因为是共享的,它体现了RNN的模型的“循环反馈”的思想。

「2.RNN的前向传播」

循环网络的前向传播算法非常简单,每个时间步都有一个前向传播,那么对于t时刻隐藏层计算结果如下:

h^{(t)}=\phi\left(U x^{(t)}+W h^{(t-1)}+b\right) \\

其中ϕ(.)为激活函数,一般来说会选择tanh函数,b为偏置。则 t 时刻的输出则有:

o^{(t)}=V h^{(t)}+c \\

最终模型在t时刻的预测输出为:

\hat{y}^{(t)}=\sigma\left(o^{(t)}\right) \\

其中\sigma为激活函数,激活函数通常选择softmax函数。

「3.循环神经网络的反向传播算法」

BPTT(back-propagation through time)算法是常用的训练RNN的方法,其实本质还是BP算法,只不过RNN处理时间序列数据,所以要基于时间反向传播,故叫随时间反向传播。BPTT的中心思想和BP算法相同,沿着需要优化的参数的负梯度方向不断寻找更优的点直至收敛。当然这里的BPTT和深度神经网络(Deep Neural Networks, DNN)中的BP算法也有很大的不同点,即这里所有的 U, W, V 在序列的各个位置是共享的,反向传播(Back Propagation, BP)时更新的是相同的参数。

对于RNN,由于在序列的每个位置都有损失函数,因此最终的损失L为各个时间步损失计算之和:

L=\sum_{t=1}^{n} L^{(t)} \\

因此可以得到U,V,W的偏导,其中V的比较好求,如下:

\frac{\partial L}{\partial V}=\sum_{t=1}^{n} \frac{\partial L^{(t)}}{\partial o^{(t)}} \cdot \frac{\partial o^{(t)}}{\partial V} \\

而在求WU的时候就比较的复杂。在反向传播时,在某一序列位置 t 的梯度损失由当前位置的输出对应的梯度损失和序列索引位置 t + 1 时的梯度损失两部分共同决定的。对于W在某一序列位置 t 的梯度损失需要反向传播一步步的计算。

比如以t=3时刻为例:

\begin{aligned} &\frac{\partial L^{(3)}}{\partial W}=\frac{\partial L^{(3)}}{\partial o^{(3)}} \frac{\partial o^{(3)}}{\partial h^{(3)}} \frac{\partial h^{(3)}}{\partial W}+\frac{\partial L^{(3)}}{\partial o^{(3)}} \frac{\partial o^{(3)}}{\partial h^{(3)}} \frac{\partial h^{(3)}}{\partial h^{(2)}} \frac{\partial h^{(2)}}{\partial W}+\frac{\partial L^{(3)}}{\partial o^{(3)}} \frac{\partial o^{(3)}}{\partial h^{(3)}} \frac{\partial h^{(3)}}{\partial h^{(2)}} \frac{\partial h^{(2)}}{\partial h^{(1)}} \frac{\partial h^{(1)}}{\partial W} \\ &\frac{\partial L^{(3)}}{\partial U}=\frac{\partial L^{(3)}}{\partial o^{(3)}} \frac{\partial o^{(3)}}{\partial h^{(3)}} \frac{\partial h^{(3)}}{\partial U}+\frac{\partial L^{(3)}}{\partial o^{(3)}} \frac{\partial o^{(3)}}{\partial h^{(3)}} \frac{\partial h^{(3)}}{\partial h^{(2)}} \frac{\partial h^{(2)}}{\partial U}+\frac{\partial L^{(3)}}{\partial o^{(3)}} \frac{\partial o^{(3)}}{\partial h^{(3)}} \frac{\partial h^{(3)}}{\partial h^{(2)}} \frac{\partial h^{(2)}}{\partial h^{(1)}} \frac{\partial h^{(1)}}{\partial U} \end{aligned} \\

因此,在某个时刻的对W 或是U 的偏导数,需要追溯这个时刻之前所有时刻的信息。根据上面的式子可以归纳出 L 在 t 时刻对 W U 偏导数的通式:

\begin{aligned} &\frac{\partial L^{(t)}}{\partial W}=\sum_{k=0}^{t} \frac{\partial L^{(t)}}{\partial o^{(t)}} \frac{\partial o^{(t)}}{\partial h^{(t)}}\left(\prod_{j=k+1}^{t} \frac{\partial h^{(j)}}{\partial h^{(j-1)}}\right) \frac{\partial h^{(k)}}{\partial W} \\ &\frac{\partial L^{(t)}}{\partial U}=\sum_{k=0}^{t} \frac{\partial L^{(t)}}{\partial o^{(t)}} \frac{\partial o^{(t)}}{\partial h^{(t)}}\left(\prod_{j=k+1}^{t} \frac{\partial h^{(j)}}{\partial h^{(j-1)}}\right) \frac{\partial h^{(k)}}{\partial U} \end{aligned} \\

而对于里面的乘积部分,引入激活函数,则可以表示为:

\prod_{j=k+1}^{t} \frac{\partial h^{j}}{\partial h^{j-1}}=\prod_{j=k+1}^{t} \tanh ^{\prime} \cdot W_{s} \\

或:

\prod_{j=k+1}^{t} \frac{\partial h^{j}}{\partial h^{j-1}}=\prod_{j=k+1}^{t} \text { sigmoid }^{\prime} \cdot W_{s} \\

上面的计算公式看起来很麻烦,但是在实际的使用中很多深度学习框架都很方便地解决了。

「4.梯度爆炸和梯度消失」 在RNN中,通常使用的激活函数有sigmoid,tanh,relu等。 Sigmoid 函数和 tanh 函数及其导数有以下的特点。

sigmoid 函数及其导数:


tanh 函数及其导数

Relu 函数及其导数

可以从中观察到,sigmoid 函数的导数范围是(0, 0.25], tanh 函数的导数范围是 (0, 1] ,他们的导数最大都不大于 1。因此在上面求梯度的乘积中,随着时间序列的不断深入,小数的累乘就会导致梯度越来越小直到接近于 0,这就会引起梯度消失现象。梯度消失就意味着那一层的参数再也不更新了,则模型的训练毫无意义。同理,Relu 函数一定程度上可以解决梯度消失的问题,但是容易引起梯度爆炸的问题。此外 tanh 函数的收敛速度要快于 sigmoid 函数,而且梯度消失的速度要慢于 sigmoid 函数。利用BPTT算法训练网络时容易出现梯度消失的问题,当序列很长的时候问题尤其严重,因此上面的RNN模型一般不能直接应用。而较为广泛使用的是基于门控的RNN:LSTM和GRU。

1.1.3 RNN处理的任务

根据以上的介绍可以看出,RNN可以进行一下类型的任务:

  • 一对一
  • 一对多
  • 多对一
  • 多对多(等长和不等长)

「1.一对一」

最基本的单层网络,输入是x,经过变换Wx+b和激活函数f得到输出y。

「2.一对多」

输入不是序列而输出为序列的情况,只在序列开始进行输入计算:

图示中记号的含义是:

  • 「圆圈或方块表示的是向量。」
  • 「一个箭头就表示对该向量做一次变换。如上图中h_0和x分别有一个箭头连接,就表示对h_0和x各做了一次变换。」

还有一种结构是把输入信息X作为每个阶段的输入:

下图省略了一些X的圆圈,是一个等价表示:


这种 one-to-n 的结构可以处理的问题有:

  • 从图像生成文字(image caption),此时输入的X就是图像的特征,而输出的y序列就是一段句子,就像看图说话等
  • 从类别生成语音或音乐等

「3.多对多」

最经典的RNN结构,输入、输出都是等长的序列数据。


假设输入为X=(x1, x2, x3, x4),每个x是一个单词的词向量。

为了建模序列问题,RNN引入了隐状态h(hidden state)的概念,h可以提取序列类型的数据的特征,接着再转换为输出。先从h1的计算开始看:

h2的计算和h1类似。要注意的是,在计算时,「每一步使用的参数U、W、b都是一样的,也就是说每个步骤的参数都是共享的,这是RNN的重要特点,一定要牢记。」

依次计算剩下来的(使用相同的参数U、W、b):

这里为了方便起见,只画出序列长度为4的情况,实际上,这个计算过程可以无限地持续下去。得到输出值的方法就是直接通过h进行计算:

正如之前所说,「一个箭头就表示对对应的向量做一次类似于f(Wx+b)的变换,这里的这个箭头就表示对h1进行一次变换,得到输出y1。」

剩下的输出类似进行(使用和y1同样的参数V和c):

这就是最经典的RNN结构,它的输入是x1, x2, …..xn,输出为y1, y2, …yn,也就是说,「输入和输出序列必须要是等长的」。由于这个限制的存在,经典RNN的适用范围比较小,但也有一些问题适合用经典的RNN结构建模,如:

  • 计算视频中每一帧的分类标签。因为要对每一帧进行计算,因此输入和输出序列等长。
  • 输入为字符,输出为下一个字符的概率。这就是著名的Char RNN(详细介绍请参考:The Unreasonable Effectiveness of Recurrent Neural Networks,Char RNN可以用来生成文章,诗歌,甚至是代码,非常有意思)。

还有一种不等长的多对多,这里涉及到Encoder和Decoder的内容,这种结果也叫seq2seq。原始的n-to-n的RNN要求序列等长,然而我们遇到的大部分问题序列都是不等长的,如机器翻译中,源语言和目标语言的句子往往并没有相同的长度。「为此,Encoder-Decoder结构先将输入数据编码成一个上下文语义向量c:」

「语义向量c」可以有多种表达方式,最简单的方法就是把Encoder的最后一个隐状态赋值给c,还可以对最后的隐状态做一个变换得到c,也可以对所有的隐状态做变换。

「拿到c之后,就用另一个RNN网络对其进行解码」,这部分RNN网络被称为Decoder。Decoder的RNN可以与Encoder的一样,也可以不一样。具体做法就是将c当做之前的初始状态h0输入到Decoder中:

还有一种做法是将c当做每一步的输入:

「Encoder-Decoder 应用」

由于这种Encoder-Decoder结构不限制输入和输出的序列长度,因此应用的范围非常广泛,比如:

  • 机器翻译:Encoder-Decoder的最经典应用,事实上这结构就是在机器翻译领域最先提出的。
  • 文本摘要:输入是一段文本序列,输出是这段文本序列的摘要序列。
  • 阅读理解:将输入的文章和问题分别编码,再对其进行解码得到问题的答案。
  • 语音识别:输入是语音信号序列,输出是文字序列。

「Encoder-Decoder 框架」

Encoder-Decoder 不是一个具体的模型,是一种框架。

  • Encoder:将 input序列 →转成→ 固定长度的向量
  • Decoder:将 固定长度的向量 →转成→ output序列
  • Encoder 与 Decoder 可以彼此独立使用,实际上经常一起使用

因为最早出现的机器翻译领域,最早广泛使用的转码模型是RNN。其实模型可以是 CNN /RNN /BiRNN /LSTM /GRU /…

「Encoder-Decoder 缺点」

  • 最大的局限性:编码和解码之间的唯一联系是固定长度的语义向量「c」
  • 编码要把整个序列的信息压缩进一个固定长度的语义向量「c」
  • 语义向量c无法完全表达整个序列的信息
  • 先输入的内容携带的信息,会被后输入的信息稀释掉,或者被覆盖掉
  • 输入序列越长,这样的现象越严重,这样使得在Decoder解码时一开始就没有获得足够的输入序列信息,解码效果会打折扣

因此,为了弥补基础的 Encoder-Decoder 的局限性,提出了attention机制。

「4.多对一」

要处理的问题输入是一个序列,输出是一个单独的值而不是序列,应该怎样建模呢?实际上,我们只在最后一个h上进行输出变换就可以了:

这种结构通常用来处理序列分类问题。如输入一段文字判别它所属的类别,输入一个句子判断其情感倾向,输入一段视频并判断它的类别等等。

2.长短期记忆网络LSTM

「下面介绍的公式中有[],其表示两个向量相连,*表示矩阵的乘积。」

LSTM 通过刻意的设计门限来避免长期依赖问题。核心的设计就是隐藏层神经元的设计,如下图:

下面我们就来剖析LSTM网络。图中使用的各种元素的图标如下:

  • 黄色的矩形是学习得到的神经网络层
  • 粉色的圆形表示一些运算操作,诸如加法,乘法
  • 黑色的单箭头表示向量的传输
  • 两个箭头合成一个表示向量的连接
  • 一个箭头分开表示向量的复制

LSTM 的关键就是细胞状态,水平线在图上方贯穿运行。细胞状态类似于传送带。直接在整个链上运行,只有一些少量的线性交互。信息在上面流传保持不变会很容易。如下图:

LSTM 通过精心设计的称作为“门”的结构来去除或者增加信息到细胞状态的能力。门是一种让信息选择式通过的方法。他们包含一个 sigmoid 神经网络层和一个 pointwise 乘法操作。这种门涉及的参数是需要学习的。


Sigmoid 层输出 0 到 1 之间的数值,描述每个部分有多少量可以通过。0 代表“不许任何量通过”,1 就指“允许任意量通过”!LSTM 拥有三个门来保护和控制细胞状态。下面就来进一步了解LSTM。

2.1 遗忘门

LSTM 中的第一步是决定会从细胞状态中丢弃什么信息。这个决定通过一个称为遗忘门完成。该门会读取h_{t-1}x_t ,输出一个在 0 到 1 之间的数值给每个在细胞状态C_{t-1}中,决定遗忘程度。1 表示“完全保留”,0 表示“完全舍弃”。

回想一下语言模型中基于已经看到的内容预测下一个词。在这个问题中,细胞状态可能包含当前主语的性别,因此正确的代词可以被选择出来。当我们看到新的主语,我们希望忘记旧的主语。

现在遇到两个问题:这个门怎么做到“遗忘“的呢?既然是遗忘旧的内容,为什么这个门还要接收新的x_t呢? 对于第一个问题,“遗忘“可以理解为“之前的内容记住多少“,其精髓在于只能输出(0,1)小数的sigmoid函数和粉色圆圈的乘法,LSTM网络经过学习决定让网络记住以前百分之多少的内容。对于第二个问题就更好理解,决定记住什么遗忘什么,其中新的输入就决定着sigmoid的值,也就影响着遗忘门。

2.2 输入门

拿到以往的“记忆”后,对于新的输入需要确定什么样的新信息被存放在细胞状态中。这里包含两个部分。第一,sigmoid 层称 “输入门层” 决定什么值将要更新。然后,一个 tanh 层创建一个新的候选值向量\tilde{C}_t ,会被加入到状态中。下一步,我们会将这两个信息来产生对状态的更新。

现在是更新旧细胞状态的时间了,C_{t-1} 更新为 C_t 。我们把旧状态与f_t 相乘,丢弃掉我们确定需要丢弃的信息。接着加上i_t * \tilde{C}_t 。这就是新的候选值,根据我们决定更新每个状态的程度进行变化。有了上面的理解基础输入门,输入门理解起来就简单多了。「sigmoid函数选择更新内容,tanh函数创建更新候选。」

2.3 输出门

最终,我们需要确定输出什么值。这个输出将会基于细胞的状态,但是也是一个过滤后的版本。首先,运行一个 sigmoid 层来确定细胞状态的哪个部分将输出出去。接着,我们把细胞状态通过 tanh 进行处理(得到一个在 -1 到 1 之间的值)并将它和 sigmoid 门的输出相乘,最终我们仅仅会输出我们确定输出的那部分。


这三个门虽然功能上不同,但在执行任务的操作上是相同的。他们都是使用sigmoid函数作为选择工具,tanh函数作为变换工具,这两个函数结合起来实现三个门的功能。

2.4 LSTM的前向传播

LSTM模型有两个隐藏状态 h_t , C_t ,模型参数几乎是RNN的4倍,因为现在多了 W_f, U_f, b_f, W_a, U_a,b_a, W_i, U_i,b_i,W_o, U_o,b_o 这些参数。前向传播过程在每个序列时间步处理过程为:

  1. 更新遗忘门输出:

\mathrm{f}_{\mathrm{t}}=\sigma\left(\mathrm{W}_{\mathrm{f}} \mathrm{h}_{\mathrm{t}-1}+\cup_{\mathrm{f}} x_{\mathrm{t}}+\mathrm{b}_{\mathrm{f}}\right) \\

  1. 更新输入门两部分输出:
    \mathrm{i}_{\mathrm{t}}=\sigma\left(\mathrm{W}_{\mathrm{i}} \mathrm{h}_{\mathrm{t}-1}+\mathrm{U}_{\mathrm{i}} \mathrm{x}_{\mathrm{t}}+\mathrm{b}_{\mathrm{i}}\right) ; a_{\mathrm{t}}=\tanh \left(\mathrm{W}_{\mathrm{a}} h_{\mathrm{t}-1}+U_{\mathrm{a}} \mathrm{x}_{\mathrm{t}}+\mathrm{b}_{\mathrm{a}}\right) \\
  2. 更新细胞状态:
    C_{t}=C_{t-1} f_{t}+i_{t} a_{t} \\
  3. 更新输出门输出:
    \begin{gathered} o_{t}=\sigma\left(W_{0} h_{t-1}+U_{0} x_{t}+b_{o}\right) \\ h_{t}=o_{t} \tanh \left(C_{t}\right) \end{gathered} \\
  4. 更新当前序列索引预测输出:
    \mathrm{y}_{\mathrm{t}}=\sigma\left(\mathrm{V} \mathrm{h}_{\mathrm{t}}+\mathrm{c}\right) \\

知道了前向传播,反向传播和RNN中的一样,也是借助梯度下降来训练模型。模型算法很复杂,但是当前已有的深度学习框架已经做好了封装。

2.5 总结

当然还有关于LSTM的变体网络,本文就不过多介绍了,下文只介绍一个GRU网络。RNN/LSTM是一个伟大的算法,在未来终究会成为过去式。目前最强的特征抽取器当属transformer,bert、gpt等预训练模型也是基于transformer。从序列长度问题上,transformer依靠其独特的self-attention解决了RNN长序列梯度消失的问题;从训练方式上,transformer结构上的非序列性也解决了RNN难以并行的问题,可谓从各方面吊打了RNN,称得上革命性的创举。当然还需要说明的是,这些算法模型都是建立在实际的任务基础上的,具体的使用还需要结合实际场景。效果好的模型不一定适合落地。

3.门控循环单元GRU

GRU是LSTM网络的一种效果很好的变体,它较LSTM网络的结构更加简单,而且效果也很好,因此也是当前非常流形的一种网络。GRU既然是LSTM的变体,因此也是可以解决RNN网络中的长依赖问题。

在LSTM中引入了三个门函数:输入门、遗忘门和输出门来控制输入值、记忆值和输出值。而在GRU模型中只有两个门:分别是更新门和重置门。具体结构如下图所示:

图中的z_tr_t分别表示更新门和重置门。更新门用于控制前一时刻的状态信息被带入到当前状态中的程度,更新门的值越大说明前一时刻的状态信息带入越多。重置门控制前一状态有多少信息被写入到当前的候选集 \tilde{h}_t 上,重置门越小,前一状态的信息被写入的越少。

上图的输出则有如下公式得到:

y_t = \sigma\left( W_o\cdot h_t \right) \\

概括来说,LSTM和CRU都是通过各种门函数来将重要特征保留下来,这样就保证了在long-term传播的时候也不会丢失。此外GRU相对于LSTM少了一个门函数,因此在参数的数量上也是要少于LSTM的,所以整体上GRU的训练速度要快于LSTM的。不过对于两个网络的好坏还是得看具体的应用场景。

4.注意力机制Attention

4.1 背景

注意力模型最近几年在深度学习各个领域被广泛使用,无论是图像处理、语音识别还是自然语言处理的各种不同类型的任务中,都很容易遇到注意力模型的身影。我们的主题是seq2seq模型,下面就结合seq2seq任务进行介绍和学习。

要了解seq2seq模型,在当前的研究中就不得不先谈Encoder-Decoder框架,因为目前大多数注意力模型附着在Encoder-Decoder框架下。当然,其实注意力模型可以看作一种「通用的思想」,本身并不依赖于特定框架,这点需要注意。

Encoder-Decoder框架可以看作是一种深度学习领域的研究模式,应用场景异常广泛。下图是文本处理领域里常用的Encoder-Decoder框架最抽象的一种表示。

Encoder-Decoder框架可以了解为将一种类型的输入使用Encoder编码,然后用解码器Decoder解码出另一种类型的结果。这种seq2seq类型的任务也特别多,如:文本摘要,机器翻译,文本分类等等。为了后面方便介绍这里主要使用,对于句子对<Source,Target>,目标是给定输入句子Source,期待通过Encoder-Decoder框架来生成目标句子Target。Source和Target可以是同一种语言,也可以是两种不同的语言。而Source和Target分别由各自的序列构成:

\begin{aligned} &\text { Source }=\left\langle\mathbf{x}_{1}, \mathbf{x}_{2} \ldots \mathbf{x}_{\mathrm{m}}\right\rangle \\ &\text { Target }=\left\langle\mathbf{y}_{1}, \mathbf{y}_{2} \ldots \mathbf{y}_{\mathbf{n}}\right\rangle \end{aligned} \\

Encoder顾名思义就是对输入句子Source进行编码,将输入句子通过非线性变换转化为中间语义表示C:

\mathbf{C}=\mathcal{F}\left(\mathrm{x}_{1}, \mathbf{x}_{2} \ldots \mathbf{x}_{\mathrm{m}}\right) \\

对于解码器Decoder来说,其任务是根据句子Source的中间语义表示C和之前已经生成的历史信息:y_1,\cdots,y_{i-1}来生成i时刻要生成的单词(可以理解为一个序列单元为单词)y_i,如下:

y_{i}=\mathcal{G}\left(C, y_{1}, y_{2} \ldots y_{i-1}\right) \\

可以看出,每个y_i都依次这么产生,整个系统看起来就是根据输入句子Source生成了目标句子Target。这样一说,如果Source是中文句子,Target是英文句子,那么这就是解决机器翻译问题的Encoder-Decoder框架;如果Source是一篇文章,Target是概括性的几句描述语句,那么这是文本摘要的Encoder-Decoder框架;如果Source是一句问句,Target是一句回答,那么这是问答系统或者对话机器人的Encoder-Decoder框架。由此可见,在文本处理领域,Encoder-Decoder的应用领域就相当广泛了,实际效果也还不错。

Encoder-Decoder框架不仅仅在文本领域广泛使用,否则感觉格局有点小了。在语音识别、图像处理等领域也经常使用。比如对于语音识别来说,Encoder-Decoder框架完全适用,区别无非是Encoder部分的输入是语音流,输出是对应的文本信息;而对于“图像描述”任务来说,Encoder部分的输入是一副图片,Decoder的输出是能够描述图片语义内容的一句描述语。一般而言,文本处理和语音识别的Encoder部分通常采用RNN模型,图像处理的Encoder一般采用CNN模型。万变不离其宗,就是信息论中的编码-解码问题了。

上图中的Encoder-Decoder框架是没有体现出“注意力模型”的,所以可以把它看作是注意力不集中的分心模型或注意力均匀模型。为什么说它注意力不集中呢?请观察下目标句子Target中每个单词的生成过程如下:

\begin{aligned} &\mathbf{y}_{1}=\mathbf{f}(\mathrm{C}) \\ &\mathbf{y}_{2}=\mathbf{f}\left(\mathrm{C}, \mathbf{y}_{1}\right) \\ &\mathbf{y}_{3}=\mathbf{f}\left(\mathrm{C}, \mathbf{y}_{1}, \mathbf{y}_{2}\right) \end{aligned} \\

其中f是Decoder的非线性变换函数。从这里可以看出,在生成目标句子的单词时,不论生成哪个单词,它们使用的输入句子Source的语义编码C都是一样的,没有任何区别。

而语义编码C是由句子Source的每个单词经过Encoder 编码产生的,这意味着不论是生成哪个单词,y_1,y_2还是y_3,其实句子Source中任意单词对生成某个目标单词y_i来说影响力都是相同的,这是为何说这个模型没有体现出注意力的缘由。这类似于人类看到眼前的画面,但是眼中却没有注意焦点一样。所以为模型增加注意力(Attention)的思想就出来了。

4.2 Attention

Attention多种,主要有:soft attention、hard attention、 local attention、global attention结构。soft attention和global attention结构可参考下图。


Soft Attention中是对于每个Encoder的Hidden State会match一个概 率值,而在Hard Attention会直接找一个特定的单词概率为1,而 其它对应概率为0。如下图:

local attention的结构图如下:

self attention,transformer中使用的结构,如下:

4.3 以soft attention与机器翻译了解attention

比如输入的是英文句子:Tom chase Jerry,Encoder-Decoder框架逐步生成中文单词:“汤姆”,“追逐”,“杰瑞”。在翻译“杰瑞”这个中文单词的时候,没有Attention的模型里面的每个英文单词对于翻译目标单词“杰瑞”贡献是相同的,很明显这里不太合理,显然“Jerry”对于翻译成“杰瑞”更重要,但是没有Attention的模型是无法体现这一点的,这就是为何说它没有引入注意力的原因。没有引入注意力的模型在输入句子比较短的时候问题不大,但是如果输入句子比较长,此时所有语义完全通过一个中间语义向量来表示,单词自身的信息已经消失,可想而知会丢失很多细节信息,这也是为何要引入注意力模型的重要原因。

假如我们引入Attention,应该在翻译“杰瑞”的时候,体现出英文单词对于翻译当前中文单词不同的影响程度,比如给出类似**(Tom,0.3)(Chase,0.2) (Jerry,0.5)**的一个概率分布值。每个英文单词的概率代表了翻译当前单词“杰瑞”时,注意力分配模型分配给不同英文单词的注意力大小。这对于正确翻译目标语单词肯定是有帮助的,因为引入了新的信息。

同理,目标句子中的每个单词都应该学会其对应的源语句子中单词的注意力分配概率信息。这意味着在生成每个单词的时候,原先都是相同的中间语义表示C会被替换成根据当前生成单词而不断变化的C_i。理解Attention模型的关键就是这里,即由固定的中间语义表示C换成了根据当前输出单词来调整成加入注意力模型的变化的C_i。增加了注意力模型的Encoder-Decoder框架理解起来如下图所示。

那么生成目标句子单词的过程就是下面的形式:

\begin{aligned} &y_{1}=f 1\left(C_{1}\right) \\ &y_{2}=f 1\left(C_{2}, y_{1}\right) \\ &y_{3}=f 1\left(C_{3}, y_{1}, y_{2}\right) \end{aligned} \\

而每个C_i可能对应着不同的源语句子单词的注意力分配概率分布,比如对于上面的英汉翻译来说,其对应的信息可能如下:

其中,f2函数代表Encoder对输入英文单词的某种变换函数,比如如果Encoder是用的RNN模型的话,这个f2函数的结果往往是某个时刻输入后「隐层节点」的状态值;g代表Encoder根据单词的中间表示合成整个句子中间语义表示的变换函数,一般的做法中,g函数就是对构成元素加权求和,即下列公式:

C_{i}=\sum_{j=1}^{L_{x}} a_{i j} h_{j} \\

现在符号开始多了,需要认真理清这些内容。其中:

  • L_x代表输入句子Source的长度,
  • a_{ij}代表在Target输出第i个单词时Source输入句子中第j个单词的注意力分配系数,
  • h_j是Source输入句子中第j个单词的语义编码

假设C_i下标i就是上面例子的“ 汤姆”,则有:

  • L_x=3
  • h_1=f(''Tom''), h_2=f('' Chase '' ), h_3=f('' Jerry'')分别是输入句子每个单词的语义编码
  • 对应的注意力模型权值则分别是a_{11}=0.6, a_{12}=0.2,a_{13}=0.2

所以g函数本质上就是个加权求和函数.如果形象表示的话,翻译中文单词“汤姆”的时候,数学公式对应的中间语义表示C_i的形成过程类似如下图:

这里还有一个问题:生成目标句子某个单词,比如“汤姆”的时候,如何知道Attention模型所需要的输入句子单词注意力分配概率分布值呢?就是说“汤姆”对应的输入句子Source中各个单词的概率分布:(Tom,0.6)(Chase,0.2) (Jerry,0.2) 是如何得到的呢?

为了便于说明,我们假设对非Attention模型的Encoder-Decoder框架进行细化,Encoder采用RNN模型,Decoder也采用RNN模型,这是比较常见的一种模型配置,则非Attention模型的框架转换如下图(下图中是有一定问题的,在解码时c、eos、h1共同作用生成y1的)。

那么下图可以较为便捷地说明注意力分配概率分布值的通用计算过程。

对于采用RNN的Decoder来说,在时刻i,如果要生成y_i单词,我们是可以知道Target在生成之y_i前的时刻i-1时(Decoder)隐层节点i-1时刻的输出值H_{i-1}的,而我们的目的是要计算生成y_i时输入句子中的单词 “Tom”、 “Chase”、 “Jerry” 对y_i来说的注意力分配概率分布,那么可以用Target输出句子i-1时刻的隐层节点状态H_{i-1}去一一和输入句子Source中每个单词对应的RNN隐层节点状态hj进行对比计算,即通过函数F(h_j,H_{i-1})来获得目标单词y_i和每个输入单词对应的对齐可能性.「这个F函数在不同论文里可能会采取不同的方法」,然后函数F的输出经过Softmax进行归一化就得到了符合概率分布取值区间的注意力分配概率分布数值。绝大多数Attention模型都是采取上述的计算框架来计算注意力分配概率分布信息,区别只是在F的定义上可能有所不同,通常有以下几种。


当然也可可视化地展示经过attention前后seq2seq中各个单元之间注意力情况,在英语-德语翻译系统中加入Attention机制后,Source和Target两个句子每个单词对应的注意力分配概率分布。


上述内容就是经典的Soft Attention模型的基本思想,怎么理解Attention模型的物理含义呢?一般在自然语言处理应用里会把Attention模型看作是输出Target句子中某个单词和输入Source句子每个单词的对齐模型,这是非常有道理的。目标句子生成的每个单词对应输入句子单词的概率分布可以理解为输入句子单词和这个目标生成单词的对齐概率,这在机器翻译语境下是非常直观的:传统的统计机器翻译一般在做的过程中会专门有一个短语对齐的步骤,而注意力模型其实起的是相同的作用。

4.4 Attention机制的本质思想

如果把Attention机制从上文讲述例子中的Encoder-Decoder框架中剥离,并进一步做抽象,可以更容易看懂Attention机制的本质思想。如下图:

我们可以这样来看待Attention机制:将Source中的构成元素想象成是由一系列的<Key,Value>数据对构成,此时给定Target中的某个元素Query,通过计算Query和各个Key的相似性或者相关性,得到每个Key对应Value的权重系数,然后对Value进行加权求和,即得到了最终的Attention数值。所以本质上Attention机制是对Source中元素的Value值进行加权求和,而Query和Key用来计算对应Value的权重系数。即可以将其本质思想改写为如下公式:

\text { Attention(Query, Source) }=\sum_{i=1}^{L_{x}} \text { Similarity }\left(\text { Query, Key }_{i}\right) * \text { Value }_{i} \\

其中L_x=||Source||代表,Source中长度,可以理解为语句中词语或字符的个数。上文所举的机器翻译的例子里,因为在计算Attention的过程中,Source中的Key和Value合二为一,指向的是同一个东西,也即输入句子中每个单词对应的语义编码,所以可能不容易看出这种能够体现本质思想的结构。从概念上理解,把Attention仍然理解为从大量信息中有选择地筛选出少量重要信息并聚焦到这些重要信息上,忽略大多不重要的信息,这种思路仍然成立。聚焦的过程体现在权重系数的计算上,权重越大越聚焦于其对应的Value值上,即权重代表了信息的重要性,而Value是其对应的信息。

至于Attention机制的具体计算过程,如果对目前大多数方法进行抽象的话,可以将其归纳为两个过程:第一个过程是根据Query和Key计算权重系数,第二个过程根据权重系数对Value进行加权求和。而第一个过程又可以细分为两个阶段:第一个阶段根据Query和Key计算两者的相似性或者相关性;第二个阶段对第一阶段的原始分值进行归一化处理;这样,可以将Attention的计算过程抽象为如下图三个阶段。

在第一个阶段,可以引入不同的函数和计算机制,根据Query和某个key_{i},计算两者的相似性或者相关性,最常见的方法包括:「求两者的向量点积」「求两者的向量Cosine相似性」或者通过再引「入额外的神经网络来求值,」 即如下方式:

  • 点积:Similarity(Query, Key_{i}) = Query\cdot Key_i
  • Cosine相似性:Similarity(Query, Key_{i}) = \frac{Query\cdot Key_i}{||Query||\cdot || Key_i||}
  • MLP网络:Similarity(Query, Key_{i}) = \text{MLP}(Query, Key_i)

第一阶段产生的分值根据具体产生的方法不同其数值取值范围也不一样,第二阶段引入类似SoftMax的计算方式对第一阶段的得分进行数值转换,一方面可以进行归一化,将原始计算分值整理成所有元素权重之和为1的概率分布;另一方面也可以通过SoftMax的内在机制更加突出重要元素的权重。即一般采用如下公式计算:

a_{i}=\operatorname{Softmax}\left(\operatorname{Sim}_{i}\right)=\frac{e^{\operatorname{sim}_{i}}}{\Sigma_{j=1}^{L_{x}} e^{\operatorname{sim}_{j}}} \\

第二阶段的计算结果a_i即为Value_i对应的权重系数,然后进行加权求和即可得到Attention数值:

\text { Attention (Query, Source) }=\sum_{i=1}^{L_{x}} a_{i} \cdot \text { Value }_{i} \\

通过如上三个阶段的计算,即可求出针对Query的Attention数值,目前绝大多数具体的注意力机制计算方法都符合上述的三阶段抽象计算过程。

4.5 小节

总得来说,理解注意力机制后各个环节为什么这么做,以及如何做之后,整体理解注意力机制就不是那么难了。对于transformer中使用的self attention在后面介绍transformer时再做介绍。

二、评判指标BLEU

在自然语言处理中的机器翻译任务中, BLEU非常常见, 它是用于评估「模型生成的句子(candidate)「和」实际句子(reference)「的差异的指标。它的取值范围在0.0到1.0之间, 如果两个句子」完美匹配(perfect match)」, 那么BLEU是1.0, 反之, 如果两个句子「完美不匹配(perfect mismatch)」, 那么BLEU为0.0.BLEU方法的实现是分别计算「candidate句」「reference句」「N-gram模型」, 然后统计其匹配的个数来计算得到的. 公式如下:

\mathrm{BLEU}=\mathrm{BP} \cdot \exp \left(\sum_{n=1}^{N} w_{n} \log p_{n}\right) \\

1. N-gram

BLEU 采用一种N-gram的匹配规则,原理比较简单,就是比较译文和参考译文之间n组词的相似的一个占比。例如:

原文:今天天气不错
机器译文:It is a nice day today
人工译文:Today is a nice day

如果用1-gram匹配:

可以看到机器译文一共6个词,有5个词语都命中的了参考译文,那么它1-gram的匹配度为 5/6 .

再以3-gram举例:

可以看到机器译文一共可以分为四个3-gram的词组,其中有两个可以命中参考译文,那么它3-gram的匹配度为 2/4.

依次类推,可以很容易实现一个程序来遍历计算N-gram的一个匹配度。一般来说1-gram的结果代表了文中有多少个词被单独翻译出来了,因此它反映的是这篇译文的忠实度;而计算2-gram以上时,更多时候结果反映的是译文的流畅度,值越高文章的可读性就越好。

2.召回率

召回率是评估参数中经常要使用到的,上面所说的方法比较好理解,但是没有考虑到召回率,举一个非常简单的例子说明:

原文:猫站在地上
机器译文:the the the the 
人工译文:The cat is standing on the ground

在计算1-gram的时候,the 都出现在译文中,因此匹配度为4/4 ,但是很明显 the 在人工译文中最多出现的次数只有2次。BLEU算法考虑到了这个问题,首先会计算该n-gram在译文中可能出现的最大次数:

Count_{clicp} = min(Count, Max\_Ref\_Count) \\

其中:

  • Count是N-gram在机器翻译译文中的出现次数
  • Max_Ref_Count是该N-gram在一个参考译文中最大的出现次数

取两者中的较小值。然后在把这个匹配结果除以机器翻译译文的N-gram个数。因此对于上面的例子来说,修正后的1-gram的统计结果就是2/4。

p_n的计算公式如下:

p_{n}=\frac{\sum_{c \in\{\text { Candidates }\}} \sum_{n-\operatorname{gram} \in \mathcal{C}} \text { Count }_{\text {clip }}(n \text {-gram })}{\sum_{C^{\prime} \in\{\text { Candidates }\}} \sum_{n-g r a m^{\prime} \in C^{\prime}} \text { Count }\left(n-g r a m^{\prime}\right)} \\

计算p_n就是将机器翻译的每类n-gram的Min值相加与候选译文中每类中n-gram出现的次数相加的值进行除法计算。p_n前的权重w_n通常为\frac{1}{N},各个w累加和为1.

3.惩罚因子

N-gram的匹配度可能会随着句子长度的变短而变好,因此会存在这样一个问题:一个翻译引擎只翻译出了句子中部分结果且翻译的比较准确,那么它的匹配度依然会很高。为了避免这种评分的偏向性,BLEU在最后的评分结果中引入了长度惩罚因子(Brevity Penalty)。

B P= \begin{cases}1 & \text { if } c>r \\ e^{1-r / c} & \text { if } c \leq r\end{cases} \\

其中r是一个参考翻译的词数,c是一个候选翻译的词数,BP代表译句较短惩罚值。

4.小节

有了上面的介绍,对于bleu的理解也就相对简单了。如果想看这个具体的案例的介绍可参考:一种机器翻译的评估方法-BLEU.

三、基于CNN+Attention的seq2seq模型

前面的内容也基本上把基于LSTM+Attention的seq2seq模型介绍完了.这里主要介绍一下如何使用cnn结合attention进行seq2seq.该模型如下:


1. 简介

因为RNN的链式结构,能够很好地应用于处理序列信息。但是RNN也存在着劣势:由于RNN运行时是将序列的信息逐个处理,不能实现并行操作,导致运行速度慢;除此之外传统的RNN并不能很好地处理句子中的结构化信息,或者说更复杂的关系信息。相比之下,CNN能够并行处理数据,计算更加高效。此外,CNN是层级结构,与循环网络建模的链结构相比,层次结构提供了一种较短的路径来捕获词之间远程的依赖关系,因此也可以更好地捕捉更复杂的关系。

总得来说,就是使用过滤器的卷积层提取特征。这里的过滤器只有宽度(在文本处理中embedding的维度固定了)。假如一个过滤器的宽度为3,那么这个过滤器可以一次性“看到”3个连续的token。而每个卷积层又有很多过滤器。每个卷积核就从头到尾过所有的token提取特征。下面就来看看具体是如何操作的。

注:之所以使用token来描述是因为,在不同语言或任务中中处理的单元不同。

1. Encoder

1.1 基本流程

在编码器部分中,需要将整个输入的语句压缩成一个上下文向量z。输入的语句的每个token会被转成两个向量,一个是token对应的embeding,另一个则是位置向量(这也是能够记住语句的顺序的关键)。经过编码器后得到的向量如下:

  • conved vector,每个token通过卷积层后的结果
  • combined vector,conved vector与token and position embedding进行残差连接

Encoder数据的结果就供Decoder使用。具体流程如下:

1.输入向量化表征:这里就是一个embedding layer,将token对应的embedding和位置编码按位求和得到token and position embedding, 也就是embedding vector。

2.使用一个全连接层将embedding vector转换成隐藏层需要的维度

3.接下来将隐藏层输出的结果传入N个卷积层,这些卷积层输入的结果再传入一个全连接层,将数据维度转成和embedding vector相同就得到了conved vector

4.将conved vector和embedding vector进行残差连接得到combined vector

1.2 卷积层

让人不解的是卷积层是如何计算的。下面是两个卷积层,每个卷积层有一个卷积核(蓝色)的计算过程。这里的卷积核的宽度设置为3。

卷积层处理流程如下:

1.将输入数据进行pad操作,这是因为经过卷积操作有输出的结果相对于tokens的个数减少,为了使输出结果与tokens结果保持一致;当然具体增加多少个pad可以使用公式进行计算,选择奇数个卷积核大小更方便计算两边需要增加pad的数目:\frac{filter\_size - 1 }{2}。这些卷积核是被设计的,其输出的结果在这里称为隐藏层,在图像中称为输出通道,输出的隐藏层维度是输入隐藏层的2倍。

2.为什么要做这个2倍关系呢?主要是为了使用一种特殊的激活函数——GLU(gated linear units)。这种激活函数有类似于LSTM的门限机制并且实际输出的维度是输入的一半。

3.在经过GLU激活函数后,每个token对应的输出的维度就与输入的维度相同了,再和输入的向量进行想加,然后进而进入下一个卷积层继续做更加复杂的特征提取。

这就是一个卷积层处理的过程。每一个卷积层都有自己的参数不会彼此之间共享参数。最后一个卷积层的输出会和一个全连接层连接得到conved vector和combined vector。

2. Decoder

2.1 基本流程

解码器在训练时使用语料数据进行模型训练,或对实际的数据进行预测。这种模型不同于使用RNN作为解码器的seq2seq的网络,因为这里会并行训练模型,而RNN那种时序网络则需要一个循环。

解码器与编码器类似如下图。

其处理的过程如下:

1.将训练数据的目标seq对token和position进行编码成并逐元素相加得到embedding vector,然后就如编码器中处理一样将embedding vector 使用一个全连接层处理转为hidden dim 的向量

2.在卷积层中使用编码器输出的conved vector、combined vector和hidden dim、embedding进行卷积和attention计算

3.解码器的输出是一个全连接层,这个则是用来预测下一个token。

2.2 解码器卷积层(Attention的使用)

解码器的卷积层与编码器的的卷积层类似,如下图:

其处理的主要流程如下:

1.首先对输入的pad处理。这里的pad添加是从左边开始,这不同于编码器中pad处理方法,同时保证输出的token与输入的token一致。因为需要并行化操作,不是序列化的,就需要一种方法允许卷积核处理地第i个token仅只看到序列中的前i个token。如果允许了看到i+1个token(解码器将要输出的结果),那么解码器完全可以直接拷贝这个结果,而却没有进行实际的学习。

再看看如果没有正确地从左边进行pad的结果,错误的操作如下:

卷积核在第一个位置上尝试使用序列中的第一个token去预测第二个token,那么就可以同时“看到”第二个token,后面的操作也是如此,那么这样的处理就没有多大的意义了。

2.在经过GLU之后残差连接之前,这里就会使用当前token经过编码的embedding等向量进行attention的相关的计算。其实有了encoder输出的两个结果就如同rnn输出的两个结果,拿这两个向量以及decoder输入的两个向量就方便计算了。

四、总结

基于RNN的seq2seq模型能够结合序列化数据的特征能够更加有效地提取数据特征,在相关领域也取得了不错的结果,也因为其利用序列化的特征导致其不能够进行并行操作,以致于这种类型的模型速度较慢。使用基于CNN的seq2seq模型,通过position来记录序列化特征,能够将序列化数据并行操作,这也使得序列的长度限制了改类模型的处理上限。

总体来说facebook提出的这个基于CNN的seq2seq在一些任务上效果不亚于基于RNN模型,并且还在速度上得到了比较大的提升。而在实际生产和业务场景中也是需要根据具体的任务需求合理选择。

空说不练假把式,理论总归是理论,实践才是检验真理的唯一标准。接下来将通过一个案例,分别使用基于GRU和Attention的seq2seq模型以及基于CNN和Attention的seq2seq模型进行实战。并且实际操作中还有很多小tricks。

文中参考很多网上的博文,这里一并感谢,相关连接没有在正文中直接给出,参考文章也都在Reference中给出,对文中不了解的部分可以详细阅读原文。

Reference

  1. 深度学习之从RNN到LSTM
  2. 大话循环神经网络(RNN)
  3. RNN 结构详解
  4. Understanding LSTM Networks
  5. 深度学习之GRU网络
  6. 深度学习中的注意力机制(2017版)
  7. soft attention、hard attention、 local attention结构
  8. 机器翻译评测——BLEU算法详解 (新增 在线计算BLEU分值)
  9. Bleu: a Method for Automatic Evaluation of Machine Translation
  10. Convolutional Sequence to Sequence Learning
  11. 5 - Convolutional Sequence to Sequence Learning
  12. Gated Linear Units (GLU) and Gated CNN