Eclipse Deeplearning4j GitChat课程:https://gitbook.cn/gitchat/column/5bfb6741ae0e5f436e35cd9f
Eclipse Deeplearning4j 系列博客:https://blog.csdn.net/wangongxi
Eclipse Deeplearning4j Github:https://github.com/eclipse/deeplearning4j
在之前的文章中,我们介绍过如何基于LSTM来进行情感识别的任务。从本质上来说,LSTM可以用于提取整段文本的语义信息,然后对最后一个LSTM Cell输出的结果进行业务层面的分类建模即可。在工业界的实际尝试中,尤其对于短文本,这种建模方式本身可以作为baseline甚至可以经过精心的调优达到生产环境的精度要求。但是RNN结构有其自身的一些缺点。比如,长距离依赖导致头部的信息丢失,容易在BPTT算法执行的时候发生梯度的弥散等问题。当然一些改进的RNN结构,包括上面提到的LSTM和GRU等等可以缓解此类问题,但当遇到长文本问题的时候,效果的提升就很难了。
Attention机制早期用在图像领域较多。在论文《Recurrent Models of Visual Attention》中作者将Attention机制与RNN结合用于图像分类的任务。Attention适用于图像领域其实并不意外,对于人类来说通过识别核心内容来完成一个模式识别的任务是非常常见的,也是人类所擅长的,这其实就是注意力机制的一种表现或者说应用。传统的CNN和RNN虽然可以提取一些局部特征并通过整合这些局部特征来达到整体识别的效果,但对于一些长距离关联的场景就比较难了。无论是在图像还是文本处理的过程中,长距离依赖的识别问题总是一个挑战,而attention则更适合处理这样的问题。Deeplearning4j从1.0.0-beta4版本开始支持attention机制,主要实现了用于RNN的RecurrentAttention模型、自注意力模型(Self-attention)及其变种Learned Self-attention三种。下面我们首先介绍注意力模型的基本原理,再结合NLP中经典的分类问题尝试基于attention机制来实现。
我们先来看下Attention机制的思想。对于三元组Query, Key, Value,attention机制的数学表达式可以如下:
公式中的函数符号f用来表示某种相似度计算。这可以是普通的余弦相似度、欧式距离的计算,也可以是一个完整的全连接神经网络。需要注意的一点是,Key和Value不一定代表两个不同的事物,很多时候它们可以指代同一个元素(Key=Value),比如Embedding后的词向量。从公式中我们可以看出,无论Query、Key和Value的物理意义代表什么,都是这三个元素之间直接建立函数关系,不需要依赖时空维度的太多信息。也正是因为这样,RNN中添加attention机制可以在输出和输入cell之间直接建立某种联系,而无需经过多步的前向传播,对于CNN也是类似的手段。另外必须指出的是,attention机制并非完全没有缺陷。首先它需要消耗大量的存储空间,其次在一些细节上比如相似度函数f的选择上也是值得推敲的,不能完全一概而论。但其作为提升baseline模型的优化手段,是完全可以尝试的。我们将attetion机制的基本理论用到RNN模型中可以优化诸如文本分类、机器翻译的问题。我们以Seq2Seq框架为例来解释attention在RNN建模中的使用。先看下面这张示意图。
在Seq2Seq或者Encoder-Decoder架构中,图中在编解码层各有4个RNN Cell。根据上面提到的公式,我们令Decoder层的每一个cell的输出作为Query,而Encoder层的每一个神经元作为Key(这里Key=Value)。此外,Q1神经元与Encoder层的每一个神经元之间的连线上都标有一个权重来表示相似度的大小且加和为1(通常可以用softmax函数来实现)。那么根据基础公式我们可以得到如下表达式:
我们可以设想实际的Encoder层的输入是词向量,那么对于Decoder层的第一个cell的输出,Encoder层的第一个词向量贡献最大,因为它的权重最大,换言之Docoder的第一个输出和Encoder层第一个输入有很大关系,作为Q1它的注意力集中在K1或者说V1上面。这就是一种朴素的RNN+Attention机制的使用方式。这个很好理解,比如我们需要将“I am Allen”翻译成“我 是 艾伦”,那么自然“我”这个字注意力应该集中在“I”上面,其他的关系不大了。从另一个角度说,这种机器翻译中的一种对齐机制(alignment)。当然这种alignment是通过学习训练出来的,而不是像传统的CRF和HMM需要事先编排好对齐机制。这种基本的RNN attention机制在Deeplearning4j也有实现,就是引言中提到的RecurrentAttention模型。接下来,我们看下自注意力模型机制。
自注意力机制(self-attention)主要的参考文献是论文《Attenton is all you need》。它的核心思想其实是在序列上下文本身通过attention机制来找出语义单元之间分布的规律,因此它比较适合做语言模型,这也是去年Bert模型采用它的原因。Bert以及依赖的Transformer在后面的文章中会单独介绍,这里不单独展开。我们还是回到self-attention机制的原理上。先看下面这张图。
和上面提到的RNN attention机制有些类似,我们还是会涉及到Q,K,V这三元组,但不同之处在于图中这两个序列其实是完全相同的,这里为了方便解释说明,才用了两个序列来表示。以序列中第一元素为例,它和其他元素的相似度同样用一个加权求和为1的分布来表示。那么attention的结果其实和上面公式将会是一样的。这里我们假定Q=K=V,这是一种简单处理的方式,实际上我们可以将Q,K,V分别乘以一个矩阵做一次简单的线性变换后再计算attention的值。我们来看下这种更为普遍做法的图解。
这里就是通过简单的线性变换得到三元组。原始输入X可以是Embedding向量等等。
以上这张图就是计算attention值的过程。在上面的图中,注意力权重(0.6,0.2,0.1,0.1)都是我们指定的,而这里则给出了详细的计算过程。通过对原始输入进行线性变换后的三元组,Q和K进行某种相似度计算(这里是内积)得到一个标量,除以模长后再经过softmax函数得到一个和为1的概率分布作为注意力的值。输出元素的值则是对各个V加权求和的结果,总体上可以用下图来表示。
这里我们对原始输入X分别用单个矩阵得到Q,K,V。如果我们用多个矩阵的话,就可以得到多个三元组的表示,那么这种场景就称之为多头的自注意力机制(Multi-head Self Attention)。
以上即是对attention在RNN中以及self-attention中实现原理的一个解释。可以看出attention本身的原理并不是很复杂,但通过变换三元组中的元素,可以赋予attention机制不同的使用场景。对于某一神经元的输出,通过attention机制可以将输入层神经元对它的贡献用一个概率分布来表示,我们可以理解为这是一种权重,权重值越大,则对输出神经元的影响越大,换句话说注意力的集中点就是权重较大的那个输入层神经元。Attention机制可以通过空间换时间的方式直接看到全局的信息,学习到对目标更为重要的特征并为此提权,较好的解决了长距离上下文依赖的问题。下面我们来看下Deeplearning4j中attention机制与文本分类问题中的使用。
我们首先介绍下语料的情况。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H05jzGWr-1589937262876)(…/Attention/attention-corpus-label.jpg)]
从截图中我们可以看到有两份数据,一份是以空格分隔的文本,每一行即为一条语料。另一张图则是每条语料对应的标注,我们共设计了4种label。
首先给出基于原始LSTM建模文本分类问题的实现逻辑。
MultiLayerConfiguration netconf = new NeuralNetConfiguration.Builder()
.seed(1234)
.optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
.updater(new Adam(0.001))
.list()
.layer(0, new EmbeddingLayer.Builder().nIn(VOCAB_SIZE).nOut(100).activation(Activation.IDENTITY).build())
.layer(1, new LSTM.Builder().nIn(100).nOut(100).activation(Activation.SOFTSIGN).build())
.layer(2, new RnnOutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
.activation(Activation.SOFTMAX).nIn(100).nOut(4).build())
.setInputType(InputType.recurrent(VOCAB_SIZE))
.build();
这个建模逻辑在之前的博客《Deeplearning4j 实战(6):基于LSTM的文本情感识别及其Spark实现》中是一致的(略微不同的地方在于LSTM接口的变化,可以升级Deeplearning4j版本后直接使用)。这个建模逻辑将语料中的词进行Embedding之后,通过LSTM来扫描整个序列,并且将最后一个cell的输出向量进行分类即可。那么我们来看下增加RNN attention机制后的模型。
MultiLayerConfiguration netconf = new NeuralNetConfiguration.Builder()
.seed(1234)
.optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
.updater(new Adam(0.01))
.list()
.layer(0, new EmbeddingLayer.Builder().nIn(VOCAB_SIZE).nOut(100).activation(Activation.IDENTITY).build())
.layer(1, new LSTM.Builder().nIn(100).nOut(100).activation(Activation.SOFTSIGN).build())
.layer(2, new RecurrentAttentionLayer.Builder().nIn(100).nOut(100).nHeads(2).activation(Activation.SOFTSIGN).build())
.layer(3, new RnnOutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
.activation(Activation.SOFTMAX).nIn(100).nOut(4).build())
.setInputType(InputType.recurrent(VOCAB_SIZE))
.build();
我们在LSTM层后面直接添加RnnAttentionLayer。Attention层有几个比较重要的参数,即逻辑中的nIn、nOut和nHead。nIn其实就是词向量的维度,nOut则是nIn向量经过变换后的输出。至于nHead则是是否使用multi-head的设置。我们来看下RnnAttentionLayer源码中的注释对该机制的说明。
对于RNN attention的输出输出shape在注释中做了说明。输入格式是兼容RNN的输出[batchSize, features, timesteps],而对应的输出则是[batchSize, nOut, timesteps]。
上面截图则是RnnAttentionLayer的实现。从sameDiff.nn.dotProductAttention方法中的入参可以看出,RnnAttentionLayer计算的是该层的隐藏层的数据作为Query,而每一个step的输入则是作为Key和Value(Key=Value),这和我们上面分析的结果是一致的。需要注意的是,目前的版本还不支持在同一个mini-batch中不等长的语料的训练,因此最简单的处理办法就是将mini-batch设置为1,这样就回避了这个问题。
接着,我们来看下如果添加Self-attention Layer。同RNN attention相似,我们可以直接在LSTM上一层添加一层self-attention。示例如下:
MultiLayerConfiguration netconf = new NeuralNetConfiguration.Builder()
.seed(1234)
.optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
.updater(new Adam(0.01))
.list()
.layer(0, new EmbeddingLayer.Builder().nIn(VOCAB_SIZE).nOut(100).activation(Activation.IDENTITY).build())
.layer(1, new LSTM.Builder().nIn(100).nOut(100).activation(Activation.SOFTSIGN).build())
.layer(2, new SelfAttentionLayer.Builder().nIn(100).nOut(100).nHeads(1).projectInput(false).build())
.layer(3, new RnnOutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
.activation(Activation.SOFTMAX).nIn(100).nOut(4).build())
.setInputType(InputType.recurrent(VOCAB_SIZE))
.build();
这里我们通过下面layer的声明来设置了一层自注意力机制的模型。
.layer(2, new SelfAttentionLayer.Builder().nIn(100).nOut(100).nHeads(1).projectInput(false).build())
这里同样可以设置.nHeads来设置multi-head的数量,注意如果多头数大于1,参数.projectInput需要设置为true。这里我们多头数量设置为1。我们经过每一轮就在训练集上评估下模型的效果,可以得到类似如下的控制台输出。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ei8R9TYk-1589937262879)(…/Attention/self-attention-train-eval-2.jpg)]
可以看到在前几轮尤其是第一轮loss值相对较高,评估的准确率也比较低,后来经过几轮的学习后,loss有了进一步的降低同时最终可以得到100%的准确率。可能有人会疑惑是不是增加多头的数量就一定好呢,其实是不一定的。一方面增加head的数量,会造成新增大量的训练参数,另一方面如果head数量较少,甚至不用多头机制,模型效果也已经达到上线指标,那其实就没有使用多头的必要了。当然,感兴趣的朋友可以自己设置多头的数量进行验证,相信最终的模型指标也会是相当不错,这里就不再展开了。我们来看下self-attention底层的实现。
在Deeplearning4j中,attention机制是依赖于SameDiff自动微分工具来实现的。SameDiff的一些应用可以参考我之前的博客,这里就不再展开了。我们注意截图中红框框出来的那个部分,即通过内积来计算注意力机制的接口方法。
上面这张图则是基于内积的注意力机制的实现,从方法的入参可以看出,会对Q、K、V三元组进行内积计算。而自注意力机制的Q、K、V其实都是上下文本身,也就是上面defineLayer方法中的layerInput对象,因此这也和上文的理论分析保持了一致。至于多头的实现,也就是defineLayer方法中的sameDiff.nn.multiHeadDotProductAttention方法,其实入参都是一样的,底层也依赖dotProduct计算方法,只不过就像上面原理分析中讲的那样需要根据head的数量设置对应数量的权重矩阵,Wq、Wk、Wv。
这是源码中对multi-head实现的一些注释,可以作为参考。
以上我们介绍了在LSTM中融合attention机制来实现文本分类的模型结构。我们来做下简单的分析。模型的第一层通过Embedding来实现词向量的计算,LSTM层扫描整个文本序列而后续的attention层重新分配每个cell输出的权重对一些输出做些提权一些做些降权,最后则是输出分类。这里提出一个问题,那就是如果我们不通过LSTM层,是否可以呢?其实是可以的。
我们先给出模型的结构。
MultiLayerConfiguration netconf = new NeuralNetConfiguration.Builder()
.seed(1234)
.optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
.updater(new Adam(0.01))
.list()
.layer(0, new EmbeddingLayer.Builder().nIn(VOCAB_SIZE).nOut(100).activation(Activation.IDENTITY).build())
.layer(1, new SelfAttentionLayer.Builder().nIn(100).nOut(100).nHeads(1).projectInput(false).build())
.layer(2, new RnnOutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
.activation(Activation.SOFTMAX).nIn(100).nOut(4).build())
.setInputType(InputType.recurrent(VOCAB_SIZE))
.build();
从训练过程来看,同样可以达到不错的效果,甚至每一轮评估指标的提升相较于LSTM的模型还更好些。我们尝试分析下原因。对于一些文本分类的问题来说,上下文中的若干关键词其实就可以决定这条记录的分类结果。而通过LSTM层去扫描整个序列,确实可以获取整个序列的语义,但是也可能会添加一些无用的信息。Attention机制则比较直接,分配注意力的同时,直接对分类有益的关键词赋予较高的权重,那依靠这些关键词分类的问题也就可以解决了。但添加LSTM层也并非是毫无意义的,因为每一步的扫描可以获取获取几步的语义信息,通过注意力分配同样可以将对分类有益的部分的权重提升,从而达到分类的目的。
最后我们介绍一片基于Bi-LSTM + Attention的实现的文本分类的论文。论文题目是《Attention-Based Bidirectional Long Short-Term Memory Networks for Relation Classification》。
上面截图是论文中模型实现的主要框架。整体架构和我们上面给出的模型结构是比较类似的。从图中可以看出,第一层是Embedding层,接着是LSTM层,attention之后把重新分配权重的结果做一个加权求和,最后得到的结果进行分类。下面我们就给出基于Deeplearning4j的实现方案。
MultiLayerConfiguration netconf = new NeuralNetConfiguration.Builder()
.seed(1234)
.optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
.updater(new Adam(0.001))
.list()
.layer(0, new EmbeddingLayer.Builder().nIn(VOCAB_SIZE).nOut(100).activation(Activation.IDENTITY).build())
.layer(1, new LSTM.Builder().nIn(100).nOut(100).activation(Activation.SOFTSIGN).build())
.layer(2, new SelfAttentionLayer.Builder().nIn(100).nOut(100).nHeads(2).projectInput(true).build())
.layer(3, new Subsampling1DLayer.Builder(PoolingType.SUM).kernelSize(32).stride(1).build())
.layer(4, new DenseLayer.Builder().activation(Activation.LEAKYRELU).nIn(100).nOut(50).build())
.layer(5, new OutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
.activation(Activation.SOFTMAX).nIn(50).nOut(4).build())
.setInputType(InputType.recurrent(VOCAB_SIZE))
.build();
这里做下简单的说明。前三层和上面描述的类似,不过我用的是自注意力机制。然后我们采用1D的池化层将上面的结果做加和,后面我们接了一个全连接。有兴趣的同学可以跑下数据试试。
本文介绍了Deeplearning4j中attention机制的使用。分别从attention的原理介绍到attetion在文本分类应用中的应用两个方面介绍attention机制。在文本分类的应用场景中,我们介绍了LSTM+Attention以及单使用Attention机制进行分类的模型结构。由于我们只是为了验证模型的可行性,因此对最终的模型效果以及可能的调优并没有花太多篇幅来介绍,有时间的同学可以自行尝试。需要指出的是在Deeplearning4j中LearnedSelfAttention机制我们并没有花太多篇幅介绍,它自身和Self Attention类似,因此我们可以直接用来替换Self Attention层。