31

seq2seq之双向解码

 4 years ago
source link: https://www.tuicool.com/articles/2qmQRb6
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在文章 《玩转Keras之seq2seq自动生成标题》 中我们已经基本探讨过seq2seq,并且给出了参考的Keras实现。

本文则将这个seq2seq再往前推一步,引入双向的解码机制,它在一定程度上能提高生成文本的质量(尤其是生成较长文本时)。本文所介绍的双向解码机制参考自 《Synchronous Bidirectional Neural Machine Translation》 ,最后也是用Keras复现的。

研究过seq2seq的读者都知道,常见的seq2seq的解码过程是从左往右逐字(词)生成的,即根据encoder的结果先生成第一个字;然后根据encoder的结果以及已经生成的第一个字,来去生成第二个字;再根据encoder的结果和前两个字,来生成第三个词;依此类推。总的来说,就是在建模如下概率分解

\begin{equation}p(Y|X)=p(y_1|X)p(y_2|X,y_1)p(y_3|X,y_1,y_2)\cdots\label{eq:p}\end{equation}

当然,也可以从右往左生成,也就是先生成倒数第一个字,再生成倒数第二个字、倒数第三个字,等等。问题是,不管从哪个方向生成,都会有方向性倾斜的问题。比如,从左往右生成的话,前几个字的生成准确率肯定会比后几个字要高,反之亦然。在 《Synchronous Bidirectional Neural Machine Translation》 给出了如下的在机器翻译任务上的统计结果:

$$\begin{array}{|c|c|c|}

\hline

\text{Model} & \text{The first 4 tokens} & \text{The last 4 tokens}\\

\hline

\text{L2R} & 40.21\% & 35.10\%\\

\hline

\text{R2L} & 35.67\% & 39.47\%\\

\hline

\end{array}$$

$\text{L2R}$和$\text{R2L}$分别是指从左往右和从右往左的解码生成。从表中我们可以看到,如果从左往右解码,那么前四个token的准确率有40%左右,但是最后4个token的准确率只有35%;反过来也差不多。这就反映了解码的不对称性。

为了消除这种不对称性, 《Synchronous Bidirectional Neural Machine Translation》 提出了一个双向解码机制,它维护两个方向的解码器,然后通过Attention来进一步对齐生成。

虽然本文参考自 《Synchronous Bidirectional Neural Machine Translation》 ,但我没有完全精读原文,我只是凭自己的直觉粗读了原文,大致理解了原理之后自己实现的模型,所以并不保证跟原文完全一致。此外,这篇论文并不是第一篇做双向解码生成的论文,但它是我看到的双向解码的第一篇论文,所以我就只实现了它,并没有跟其他相关论文进行对比。

既然叫双向“解码”,那么改动就只是在decoder那里,而不涉及到encoder,所以下面的介绍中也只侧重描述decoder部分。还有,要注意的是双向解码只是一个策略,而下面只是一种参考实现,并不是标准的、唯一的,这就好比我们说的seq2seq也只是序列到序列生成模型的泛指,具体encoder和decoder怎么设计,有很多可调整的地方。

首先,给出一个简单的示意动图,来演示双向解码机制的设计和交互过程:

Your browser does not support video

如图所示,双向解码基本上可以看成是两个不同方向的解码模块共存,为了便于描述,我们将上方称为L2R模块,而下方称为R2L模块。开始情况下,大家都输入一个起始标记(上图中的S),然后L2R模块负责预测第一个字,而R2L模块负责预测最后一个字;接着,将第一个字(以及历史信息)传入到L2R模块中,来预测第二个字,为了预测第二个字,除了用到L2R模块本身的编码外, 还用到R2L模块已有的编码结果 ;反之,将最后一个字(以及历史信息)传入到R2L模块, 再加上L2R模块已有的编码信息 ,来预测倒数第二个字;依此类推,直到出现了结束标记(上图中的E)。

换句话说, 每个模块预测每一个字时,除了用到模块内部的信息外,还用到另一模块已经编码好的信息序列 ,而这个“用”是通过Attention来实现的。用公式来说,假设当前情况下L2R模块要预测第$n$个字,以及R2L模块要预测倒数第$n$个字。假设经过若干层编码后,得到的R2L向量序列(对应图中左上方的第二行)为:

\begin{equation}H^{(l2r)}=\left[h_1^{(l2r)},h_2^{(l2r)},\dots,h_n^{(l2r)}\right]\end{equation}

而R2L的向量序列(对应图中左下方的倒数第二行)为:

\begin{equation}H^{(r2l)}=\left[h_1^{(r2l)},h_2^{(r2l)},\dots,h_n^{(r2l)}\right]\end{equation}

如果是单向解码的话,我们会用$h_n^{(l2r)}$作为特征来预测第$n$个字,或者用$h_n^{(r2l)}$作为特征来预测倒数第$n$个字。

在双向解码机制下,我们以$h_n^{(l2r)}$为query,然后以$H^{(r2l)}$为key和value来做一个Attention,用Attention的输出作为特征来预测第$n$个字,这样在预测第$n$个字的时候,就可以提前“感知”到后面的字了;同样地,我们以$h_n^{(r2l)}$为query,然后以$H^{(l2r)}$为key和value来做一个Attention,用Attention的输出作为特征来预测倒数第$n$个字,这样在预测倒数第$n$个字的时候,就可以提前“感知”到前面的字了。上面示意图中,上面两层和下面两层之间的交互,就是指Attention。在下面的代码中,用到的是最普通的乘性Attention(参考 《〈Attention is All You Need〉浅读(简介+代码)》 )。

上面就是双向解码的基本原理和做法。可以感觉到,这样一来,seq2seq的decoder也变得对称起来了,这是一个很漂亮的特点。当然,为了完全实现这个模型,还需要思考一些问题:1、怎么训练?2、怎么预测?

跟普通的seq2seq一样,基本的训练方案就是用所谓的Teacher-Forcing的方式来进行训练,即L2R方向在预测第$n$个字的时候,假设前$n-1$个字都是准确知道的,而R2L方向在预测倒数第$n$个字的时候,假设倒数第$n-1,n-2,\dots,1$个字都是准确知道的。最终的loss是两个方向的逐字交叉熵的平均。

不过这样的训练方案实在是无可奈何之举,后面我们会分析它信息泄漏的弊端。

现在讨论预测过程。

如果是常规的单向解码的seq2seq,我们会使用beam search(束搜索)的算法,给出概率尽可能大的序列。所谓beam search,指的是依次逐字解码,每次只保留概率最大的topk条“临时路径”,直到出现结束标记为止。

到了双向解码这里,情况变得复杂了一些。我们依然用beam search的思路,但是同时缓存两个方向的topk结果,也就是说,L2R和R2L两个方向各存topk条临时路径。此外,由于双向解码时,L2R的解码是要参考R2L已有的解码结果的,所以当我们要预测下一个字时,除了要枚举概率最高的topk个字、枚举topk条L2R的临时路径外,还要枚举topk条R2L的临时路径,所以一定要计算topk 3 那么多个组合,而简单起见,我对每种“字 - L2R临时路径”的R2L临时路径做了平均,使得的分数变回topk 2 个,作为每种“字 - L2R临时路径”的得分,在从这topk 2 个组合中,选出分数最高的topk个。用R2L这边的解码,则要进行反向相反的相同处理。最后,如果L2R和R2L两个方向都解码出了完成的句子,那么就选择概率(得分)最高的那个。

这样的整个过程,我们称之为“双向束搜索(双向beam search)”。如果读者自己比较熟悉单向的beam search,甚至自己都写过beam search的话,上述过程其实不难理解(看看代码就更容易懂了),它算是单向beam search自然延伸。当然,如果对beam search本身不了解的话,看上述搜索的过程应该是云里雾里的。所以要弄清楚原理的读者,应该要从常规的单向beam search出发,先把它弄懂了,然后再看上述解码过程的描述,最后再看看下面给出的参考代码,就容易弄懂了。

下面是笔者给出了双向解码的参考实现,整体还是跟之前的 《玩转Keras之seq2seq自动生成标题》 一致,只是解码端从双向换成单向了:

https://github.com/bojone/seq2seq/blob/master/seq2seq_bidecoder.py

注: 测试环境还是跟之前差不多,大概是Python 2.7 + Keras 2.2.4 + Tensorflow 1.8。用Python 3.x或者其他环境的朋友,如果你们能自己改,那就做相应的改动,如果你们自己不会改,那也请你们别来问我了,我实在没有空也没有义务帮你们跑通每一个环境。本文只讨论seq2seq技术相关的内容可否?

在这个实现里,我觉得有必要解释一下起始标记和结束标记的事情。在之前的单向解码的例子中,笔者是用2作为起始标记,用3作为结束标记。到了双向解码这里,一个很自然的问题就是:L2R和R2L两个方向是不是应该要用两套起始和结束标记呢?

其实这个应该没有什么标准答案,我觉得不管是共用一套还是维护两套起止标记,结果可能都差不多。至于我在上面的参考代码中,使用的方案有点另类,但我认为比较符合直觉,具体是:依然是只用一套,但是在L2R方向中,用2作为起始标记、3作为结束标记,而在R2L方向中,用3作为起始标记、2作为结束标记。

最后,我们进一步思考一下这种双向解码方案。尽管将解码过程对称化是一个很漂亮的特点,但也不代表它完全没有问题了,将它思考得更深入一些,有助于我们更好地理解和使用它。

1、改进生成的原因

一个有意思的问题是:看上去双向解码确实能提高句子首尾的生成质量,但会不会同时降低中间部分的生成质量?

当然,理论上这是有可能的,但实际测试时不是很严重。一方面,seq2seq架构的信息编码和解码能力还是很强的,所以不会轻易损失信息;另一方面,我们自己去评估一个句子的质量的时候,往往会重点关注首尾部分,如果首尾部分都很合理,而中间部分不至于太糟糕的话,那么我们都认为它是一个合理的句子;反过来,如果首或尾不合理的话,我们会觉得这个句子很糟糕。这样一来,把句子首尾的生成质量提高了,整体的生成质量也就提高了。

2、对应不上概率模型

对于单向解码,我们有清晰的概率解释,即在估计条件概率$p(Y|X)$(也就是$\eqref{eq:p}$)。但是在双向解码的时候,我们发现压根儿不知道怎么对应上一个概率模型,换句话说,我们感觉我们是在算概率,感觉效果也有了,却不知道真正算得是啥,因为条件概率的条件依赖完全已经被打乱了。

当然,如果真的有实效的话,理论美感差点也无妨,我说的这一点只是理论审美的追求,大家见仁见智就好。

3、信息提前泄漏

所谓信息泄漏,指的是本来作为预测目标的标签被用来做输入了,从而导致训练阶段的loss虚低(或者准确率虚高)。

由于在双向解码中,L2R端的解码要去读取R2L端已有的向量序列,而在训练阶段,为了预测R2L端的第$n$个字,是需要传入前$n-1$个字的,这样一来,越解码到后面,信息泄漏就越严重。如下图所示:

iqqUZfA.png!web

信息泄漏示意图。训练阶段,当L2R端在预测“你”的时候,事实上用到了传入到R2L端的“你”标签;反之,R2L端预测“北”字的时候,同样存在这个问题,即用到了L2R的“北”字标签

信息泄漏的一个表观现象是: 训练到后期,双向解码中L2R和R2L两个方向的交叉熵之和,比单独训练单向解码模型时的单个交叉熵还要小,这并不是因为双向解码带来多大的拟合提升,而正是信息泄漏的体现。

既然训练过程中把信息泄漏了,那为什么这样的模型还有用呢?我想,大概的原因在文章一开头的表格中就给出了。还是刚才的例子,L2R端在预测最后一个字“你”的时候,会用到了R2L端所有的已知信息;而R2L端是从右往左逐字解码的,按照文章一开头的表格的统计数据,我们不难想象到,对于R2L端来说,倒数第一个字的预测准确率应该是最高的。这样一来,假设R2L的倒数第一个字真的能以很高的准确率预测成功的话,那信息泄漏也变成不泄漏了———因为信息泄漏是因为我们人为地传入了标签,但如果预测的结果本身就跟标签一致,那泄漏也不再是泄漏了。

本文介绍并实现了一种seq2seq的双向解码机制,它将整个解码过程对称化了,从而在一定程度上使得生成质量更高了。个人认为这种改进的尝试还是有一定的价值的,尤其是对于追求形式美的读者来说。所以就将其介绍一番。

除此之外,文章也分析了这种双向解码可能存在的问题,给出了笔者自己的看法。敬请各位读者多多交流直角~

转载到请包括本文地址: https://kexue.fm/archives/6877

更详细的转载事宜请参考: 《科学空间FAQ》

如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。

如果您觉得本文还不错,欢迎/本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK