前言
最近在搞一个多标签分类的项目,发现多标签分类问题中的多标签难点可以转化为序列生成问题(如下图,引自论文《Ensemble Application of Convolutional and Recurrent Neural Networks for Multi-label Text Categorization》[1]),论文中思想讲的很透彻,图也一目了然,但是RNN的具体实现上还是要自己搞清楚,因此这个思考过程整个从最简单的RNN到seq2seq都梳理了一个遍,特此记录。
为了清楚透彻,下面围绕上面这张图来展开。
RNN
先将上图简化为基本的RNN结构:
一目了然,~是输入序列,~是输出序列(有时我们只用到最后一个时序的输出,比如后面将要提到的 编码器-解码器 框架中的编码器),h0~h3之间的横向箭头就是状态的传递。
Simple-RNN
Simple-RNN非常简单,就是把t时刻的输出作为t时刻的状态传递给下一个时刻t+1的状态输入,一句话就是时序间传递的状态就是上一时序的输出。
LSTM
Simple-RNN虽然简单,但有一个问题就是梯度消失,发生在序列长度较长时,具体分析不在此展开来讲了,介绍LSTM的基本都有涉及,而LSTM就是为解决这个问题而提出的,采用门机制(好复杂的样子),门机制其实可以简单理解为:把两个相邻时序和之间传递的状态分为两部分(不一定是连续的),一部分保留上个时序传递的状态(即的部分取值),另一部分是当前时序计算出的状态(即原本RNN做的那样)。门就是控制状态向量s中的哪些维被保留,哪些维被更新,一个简单体现思想但不太准确举例如下图。那么状态中哪些被保留,哪些被更新呢?由门来选,所以可以形象地理解为一些门关了新计算出的就过不去,从而保留了旧的。那么门是怎么选的呢,答案是通过对每个维度乘一个0~1之间的小数来实现开闭,可见门就是一个与状态s维度相同的向量,门向量的取值则通过学习得来(看作网络参数)。说了半天,总之一句话,LSTM解决了Simple-RNN里的梯度消失问题,更好就完了。
序列生成
之前都是从RNN网络结构的角度来展开说明,现在我们从任务的角度来看RNN,看一看RNN的应用模式——序列生成。
序列生成问题就是给定一个序列,生成另一个序列。比如机器翻译、文本生成。为了引出本篇的核心,考虑这样的文本生成任务:给出一个词,生成一个序列。思路很简单:给出的词作为RNN的t=0时刻的输入,预测得到输出,再把输出作为下一时刻的输入,如此循环地进行预测,直到到达指定长度或者预测出指定的结束符。如下图(是不是跟最开始论文中的图越来越像了?)。
思路很清晰,但问题来了,实现不好操作啊!想一下在训练时,训练数据X往往都是预定好的直接输入网络,而现在的情况是后续的输入要从输出中得到,这就需要更接近底层的设置,是现有的一些封装好的RNN(比如Keras里的LSTM/GRU之类)所不能实现的,虽然话是这样说,但其实Keras或TensorFlow还是可以实现的,虽然在Keras中就不能简单的通过定义一个LSTM layer来实现,但可以通过定义RNN(rnn_cell, ...) layer然后自定义rnn_cell相应操作来处理。(所谓cell就是每个时序的计算单元,就是图中的h0/h1/h2/h3方框,注意它们本质是同一个!看起来是多个只是按时间展开的图便于理解)。当然也可以通过Tensorflow中的tf.nn.raw_rnn来实现,具体可以看我另一篇博文。
条件生成框架
现在我们来对序列生成的输入上做一下手脚,我们不再输入单纯的词了,而是把词向量和一个有意义的向量c拼接起来,从而使得输入中包含更多的信息,这个向量c就被称为条件上下文向量。说到这里关于为什么要拼接这样一个向量可能还是不太清楚明白,下面就用下图解释一下(再向论文原图迈近一步,哎呀,已经到了)。
图中的就可以看作是条件上下文,它在每个时序都被输入了cell。Text feature vector很好地解释了这样做的目的:我们可以从一段长文本中提取出特征,用这个特征来帮助我们后续网络的预测。怎么帮助呢?下面就回到最开始提到的多标签分类问题。
序列生成 解决 多标签分类
先考虑单标签分类问题,很简单嘛,根据特征X来预测分类就OK了,那么是不是可以看作是下图呢?
看到这里可能就有点明白了,如果多进行几次分类呢?如下图。
这里为什么要多重复几次分类呢?通过重复,制造了这样一种机会:对于类别特征近似的类别,在第a次分类的结果 不同于 第b次分类的结果,当然仅凭当前这种形式还达不到预期效果,因为相同的输入基本会得到相同的输出(这里说基本是因为h0 h1 h2 h3参数权重不同,对于近似的类别可能会输出不同,但这种差异不可控,所以不能达到预期)。那么我们怎么来动摇,使得在h0预测倾向相近类别A,在h1预测倾向相近类别B呢?显然再加一个输入来动摇就可以了,看下图(左)。
这张图跟论文中的图差别在哪里?就是动摇和预测输出之间未建立联系。想一下建立联系会带来什么?对比左右两图,建立联系意味着预测的标签之间将会有所关联,这种关联可以是相近标签之间的“近义”。举个实际的例子,比如有两个相近的标签分别是校园安全和校园暴力(一般校园暴力往往也会跟校园安全挂钩),当仅用h0进行单次预测时,y1既想预测为校园安全,也想预测为校园暴力,两者在概率上互相压制,导致无论预测哪个置信度都不高;当采用上面右图的方式时,由于y1在y2的前面,训练数据中都是先将y1预测为校园安全,到y2预测时,由于y1向其传达了已预测为校园安全的信息,所以y2将不再左右不定而是倾向于预测为校园暴力。
说白了,Text feature vector定下基本基调(把置信度小的筛掉),RNN时序之间传播的信息学习标签之间的关系(用于给左右不定的标签左右加码)。
到这里,多标签问题好像解决了,到此可以结束了,但好像图还跟论文原图不太一样啊,再看一下。
这一对比又发现一些了不得的事情,这一切要从不同点说起。
论文中给出的图的不同之处在于,cell之间的状态传递(cell之间的横向箭头),而上面的结构已经达到目标了,难道这样在时序之间再加一种状态传递会提升效果?不得而知,但我的猜想是有可能的:RNN中某一时刻的输出是由上一时刻的状态和当前时刻的输入经过一定的变换确定的,即,这说明状态中包含了变换前的信息,这可能是有用的(假如O()导致了标签间有效信息丢失)。
在猜测这种重复是否有效之后,也应该考虑一下能不能去掉一个,就用一个呢?再仔细看看只用一个的图,突然又发现一个不得了的事情,那就是Simple-RNN!看下图对比:
左右两图都是只保留了一种时序间信息传递,一个是y一个是s,那么要是y=s呢?这不就是Simple-RNN嘛!为什么要这么想呢,是因为如果仅保留一个,那直接使用Simple-RNN就避免了之前的实现难题——把上一个时刻的输出作为下一个时刻的输入的动态难题。因为Simple-RNN中向后一传递的状态s正是前一时刻的输出,在图中意味着左图中的折虚线箭头和右图中的横向箭头是等价的。(不知道论文中是不是就是使用Simple-RNN实现的,如果是的话可能图就不太准确了,也有可能虚线就代表着只是表示信息传递,而不是网络中实际的连接,whatever,解决问题最关键)。
seq2seq
在之前说明了条件生成框架,通过结合多标签分类的具体实例来对RNN序列生成问题加深认识。下面再进一步就到了seq2seq了,seq2seq是 序列到序列的条件生成 的简称,也被称为 编码器-解码器(encoder-decoder) 框架。它只不过是指定了条件生成框架中上下文条件向量c的来源——通过编码器得到。什么是编码器?可以说编码器就是一个RNN,它接收一个序列,输出一个向量作为条件上下文向量c,它把序列编码成一个向量,因此称为编码器。至于解码器,就是条件生成框架中的主体部分。如下图。
其实广义来讲,编码器不一定是RNN,比如论文中的文本特征向量Text feature vector就是由CNN提取出来的,所以编码器是CNN。同样,解码器也不一定是CNN。换句话说,编码器-解码器 框架应该是涵盖 seq2seq的。
结语
本文只简要介绍了从RNN到seq2seq的主要思想,其中在序列生成问题上较深入地进行分析,但还有未提到的问题,比如序列生成中的teacher-forcing训练模式,它也是使模型学习到先后词之间关系的一种训练方式,与本文提到的训练模式不同的是,它的训练中的Y就是X在时间上后移一位,相对于本文中的训练模式更为直接,但也因此存在非gold序列的问题,其具体细节这里就不展开讲了,有兴趣的可以查找相关资料。
参考文献
[1] G. Chen, D. Ye, Z. Xing, J. Chen and E. Cambria, "Ensemble application of convolutional and recurrent neural networks for multi-label text categorization," 2017 International Joint Conference on Neural Networks (IJCNN), 2017, pp. 2377-2383, doi: 10.1109/IJCNN.2017.7966144.