Press "Enter" to skip to content

玩转seq2seq之生成标题

转载自 玩转Keras之seq2seq自动生成标题 ,作者:苏剑林

 

自入坑nlp以来,搞了很久的分类和序列标注任务,我都还没有真正跑过NLP与深度学习结合的经典之作–seq2seq。这几天一直都这研究这个任务,决定通过学习代码并实践一番seq2seq。

 

seq2seq可以做的事情非常多,我选择的是比较简单的根据文章内容生成标题 $(中文\mid英文)$,也可以理解为自动摘要的一种。选择这个任务主要是因为“文章-标题”这样的预料比较好找,能快速实验一下。

 

seq2seq简介

 

所谓seq2seq,就是指一般的序列到序列的转换任务,比如机器翻译、自动文摘等等,这种任务的特点是输入序列和输出序列是不对齐的,如果对齐的话,那幺我们称之为序列标注,这就比seq2seq简单多了。所以尽管序列标注任务也可以理解为序列到序列的转换,但我们在谈到seq2seq时,一般不包含序列标注。

 

要自己实现seq2seq,关键是搞懂seq2seq的原理和架构,一旦弄清楚了,其实不管哪个框架实现起来都不复杂。谷歌提供了一个seq2seq的全家桶,这里面的功能十分强大,很多比如 Beam Search 这些实现起来需要弯弯绕绕写一大段,很麻烦的事情,直接调用这个接口,一句话就能用,省时省力。

 

基本结构

 

假如原句子为$X=(a,b,c,d,e,f)$,目标输出为$Y=(P,Q,R,S,T)$,那幺一个基本的seq2seq就如下图所示:

 

 

尽管整个图的线条比较多,可能有点眼花,但其实结构很简单。左边是对输入的encoder,它负责把输入(可能是变长的)编码成一个固定大小的向量,这个可选择的模型就很多了,用GRU、LSTM等RNN结构或者CNN+Pooling、Google的纯Attention都可以,这个固定大小的向量,理论上包含了句子的全部信息。

 

而decoder负责将刚才我们编码出来的向量解码为我们期望的输出。与encoder不同,我们在图上强调decoder是“单向递归”的,因为解码过程是递归进行的,具体流程为:

 

1、 所有输出端,都以一个通用的

标记开头,以

标记结尾,这两个标记也视为一个词/字;

 

2、 将

输入decoder,然后得到隐藏层向量,将这个向量与encoder的输出混合,然后送入一个分类器,分类器的结果应当输出$P$

 

3 、将$P$输入decoder,得到新的隐藏层向量,再次与encoder的输出混合,送入分类器,分类器应输出$Q$;

 

4、依次递归,直至分类器的结果输出

 

这就是一个基本的seq2seq模型的解码过程,在解码的过程中,将每步的解码结果送入到下一步中,直到输出

位置。

 

训练过程

 

事实上,上图也表明了一般的seq2seq的训练过程。由于训练的时候我们有标注数据对,因此我们能提前预知decoder每一步的输入和输出,因此整个结果实际上是“输入$\color{blue}X$和$\color{blue}{Y_{[:-1]}}$,预测$\color{blue}{Y_{[1:]}}$,即将目标$\color{blue}{Y}$错开一位来训练。这种训练方式,称之为$\color{red}{Teacher-Forcing}$。使用了Teacher-Forcing,不管模型上一时刻的实际输出的是什幺,哪怕输出错了,下一时间片总是上一时间片的期望输出。

 

而decoder同样可以用GRU、LSTM或者CNN等结构,但注意再次强调这种“预知未来”的特性仅在训练中才有可能,在预测阶段是不存在的,因此decoder在执行每一步时,不能提前使用后面步的输入。所以,如果使用RNN结构,一般都只使用单向RNN,如果使用CNN或者纯Attention,那幺需要把后面的部分给mask掉,(对 于卷积来说),就是在卷积核上乘以一个0/1矩阵,使得卷积只能读取当前位置及其“左边”的输入,对于纯Attention来说也类似,不过是对query的序列进行mask处理。

 

在解码时,手动去写在decoder rnn 的每一个时间步,先把上一个时间步的输出向量映射到词表上,再找到概率最大的词,再用embedding矩阵映射成向量成为这一时刻的输入,还要判断这个序列是否结束了,结束了还要拿“_PAD”作为输入。

 

敏感的读者可能会觉察到,这种训练方案是“局部”的,事实上不够端到端。比如当我们预测$\color{blue}{R}$时是假设$\color{blue}{Q}$已知的,即$\color{blue}{Q}$在前一步被成功预测,但这是不能直接得到保证的。一般前面某一步的预测出错,那幺可能导致连锁反应,后面各步的训练和预测都没有意义了。

 

beam search

 

前面已经多次提到了解码过程,但还不完整。事实上,对seq2seq来说,我们是在建模

 

显然在解码时,我们希望能找到最大概率的$\color{blue}{Y}$,那幺怎幺做呢?

 

如果在第一步$\color{blue}{p(Y_1\mid X)}$时,直接选择最大概率的那个(我们期望是目标是 $\color{blue}{P}$),然后代入第二步$\color{blue}{p(Y_2\mid X,Y_1)} $,再次选择最大概率的$\color{blue}{Y_2}$,依次类推,每一步都选择当前最大概率的输出,那幺就称为贪心搜索,是一种最低成本的解码方案。但是要注意,这种方案得到的结果未必是最优的,假如第一步我们选择了概率不是最大的$\color{blue}{Y_1}$,代入第二步时也许会得到非常大的条件概率$\color{blue}{p(Y_2\mid X,Y_1)}$,从而两者的乘积会超过逐位取最大的算法。

 

然而,如果真的要枚举所有路径最优,那幺计算量是大到难以接受(这不是一个马尔可夫过程,动态规划也用不了)。因此,seq2seq使用了一个折中的方法:$\color{red}{beam}$ $\color{red}{search}$。

 

这种算法类似动态规划,但即使在能用动态规划的问题下,它还比动态规划要简单,它的思想是:在每步计算时,只保留当前最优的$top_k$个候选结果。比如取$top_k=3$,那幺第一步时,我们只保留使得$p(Y_1\mid X)$ 最大的前3个$Y_1$ ,然后分别代入$p(Y_2\mid X,Y_1)$,然后各取前三个$Y_2$,这样一来我们就有$3^2=9$个组合了,这时我们计算每一种组合的总概率,然后还是只保留前三个,依次递归,直到出现了第一个

。显然,它本质上还属于贪心搜索的范畴,只不过贪心的过程中保留了更多的可能性,普通的贪心搜索相当于$top_k=1$。

 

seq2seq提升

 

前面所示的seq2seq模型是标准的,但它把整个输入编码为一个固定大小的向量,然后用这个向量解码,这意味着这个向量理论上能包含原来输入的所有信息,会对encoder和decoder有更高的要求,尤其在机器翻译等信息不变的任务上。因为这种模型相当于让我们“看了一遍中文后直接写出对应的英文翻译”那样,要求有更强的记忆能力和解码能力,事实上普通人完全不必这样,我们还会反复翻看对比原文,这导致了下面两个技巧。

 

Attention

 

Attention目前基本上已经是seq2seq模型的标配模块了,它的思想就是:每一步解码时,不仅仅要结合encoder编码出来的固定大小的向量(通读全文),还要往回查阅原来的每一个字词(精读局部),两者配合来决定当前步的输出。

 

 

先验知识

 

回到用seq2seq生成文章标题这个任务上,模型可以做些简化,并且可以引入一些先验知识。比如,由于输入语言和输出语言都是中文,因此encoder和decoder的embedding层可以共享参数(也就是用一套词向量)。这样使得参数数量大幅减少了。

 

此外,还有一个有用的先验知识:标题中的大部分字词都在文章中出现过(注:仅仅是出现过, 并不一定是连续出现,更不能说标题包含在文章中,不然就成为一个普通的序列标注问题了)}。这样一来,我们可以用文章中的词作为一个先验分布,加到解码过程的分类模型中,使得模型在解码输出时更倾向选用文章中已有的字词。 具体来说,在每一步预测时,我们得到总向量$\color{blue}{x}$(如前面所述,它应该是decoder当前的隐层向量、encoder的编码向量、当前decoder和encoder的Attention编码三者的拼接),然后介入全连接层,最终得到一个大小$\color{blue}{\mid V \mid}$的向量$y=(y_1,y_2,…y_{V})$,其中$\color{blue}{\mid V \mid}$是词表的个数,$\color{blue}{y}$经过softmax后,得到原本的概率

 

这就是原始的分类方案。引入先验分布的方案是,对于每篇文章,我们得到一个大小为$\mid V\mid$的0/1向量$X=(x_1,…,x_{V})$,其中$x_i=1$意味这该词在文章中出现过,否则$x_i=0$。将这样的一个0/1向量经过一个缩放平移层得到:

 

其中$s,t$为训练参数,然后将这个向量与原来的$y$取平均后才做softmax

 

经实验,这个先验分布的引入,有助于加快收敛,生成更稳定的、质量更优的标题。

 

keras参考(中文标题生成)

 

基于上面的描述,我收集了80多万篇新闻的语料,来试图训练一个自动标题的模型。简单起见,我选择了以字为基本单位,并且引入了4个额外标记,分别代表mask、unk、start、end。而encoder我使用了双层双向LSTM,decoder使用了双层单向LSTM。具体细节可以参考源码:

 

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

 

tensorflow参考(英文标题生成)

 

Word Embedding

 

使用预训练的glove向量来初始化词向量

 

Encoder

 

使用stack_birdirection_dynamic_rnn来编码

 

Decoder

 

使用BasicDecoder来训练,BeamSearch来推测

 

Attention

 

使用了BahdanauAttention来约束权重

 

以下是一些例子

 

"general motors corp. said wednesday its us sales fell ##.# percent in december and four percent in #### with the biggest losses coming from passenger car sales ."
> Model output: gm us sales down # percent in december
> Actual title: gm december sales fall # percent
"japanese share prices rose #.## percent thursday to <unk> highest closing high for more than five years as fresh gains on wall street fanned upbeat investor sentiment , dealers said ."
> Model output:  tokyo shares close # percent higher
> Actual title: tokyo shares close up # percent

 

参考

 

Tensorflow中的Seq2Seq全家桶

 

tensorflow实现

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注