R-Net – Machine Reading Comprehension with Self-Matching Networks

今天要分享的是一篇来自微软的轰动一时的阅读理解任务的AI网络结构R-Net,谷歌工程师Sachin Joglekar就曾经这样评价过这个结构:“人工智能的阅读能力在某些方面已经超越了人类,微软的R-Net就是达到了这一里程碑的人工智能之一。”它也是极少被应用于商业界的阅读理解网络结构,阿里就应用了这个结构去解决活动相关的具有时效性的客户服务问题。
论文下载地址:https://www.microsoft.com/en-us/research/wp-content/uploads/2017/05/r-net.pdf
参考git地址:https://github.com/NLPLearn/R-net.git

首先我们看下问题定义,对于一个段落P和问题Q,机器阅读理解的任务是根据P和Q预测答案A。SQuAD数据集的答案是段落里面的连续的一段,而MS-MARCO数据集的答案是人工生成的,所以不是段落里的一段,为了衡量它,这里用ROUGE-L和BLEU1两个指标。R-Net获得的成就就是在这两个数据集上获得了当时最好的结果。

接下来看一下网络结构,如图所示:

也是相当复杂的一个网络结构,在我看来这个网络结构真是把attention用到了极致,跟BERT有异曲同工之妙。
一层一层来看,输入层左边是Question Q=\{w_t^Q\}_{t=1}^m、右边是Passage P=\{w_t^P\}_{t=1}^n,文中用了word-level embeddings和character-level embeddings(RNN的最后一个隐层输出),主要是为了解决OOV问题。输入层的输出是两个level的embeddings的BiRNN的输出:

    \[\begin{split} u_t^Q=BiRNN_Q(u_{t-1}^Q,[e_t^Q,c_t^Q])\\ u_t^P=BiRNN_P(u_{t-1}^P,[e_t^P,c_t^P]) \end{split}\]

接下来进入Gated Attention-Based RNN,这一层主要是为了找到passage相对于question的敏感区域。它相对于普通的attention来说多了一个额外的门(gate)来确定对于问题(Q),哪一片段落(P)是比较重要的信息,结合attention机制,从而达到上述的目的。网络的输出是:

    \[v_t^P=RNN(v_{t-1}^P,[u_t^P,c_t])\]

可以参考match-LSTM来看下为什么右侧不是单独一个u_t^P,而是两个。Gate加在[u_t^P,c_t]这里:

    \[\begin{split} g_t=sigmoid(W_g[u_t^P,c_t])\\ [u_t^P,c_t]^*=g_t\odot[u_t^P,c_t] \end{split}\]

而其中的c_t=att(u^Q,[u_t^P,v_{t-1}^P]),注意这里引入了Q,公式即:

    \[\begin{split} s_j^t&=v^Ttanh(W_u^Qu_j^Q+W_u^Pu_t^P+W_v^Pv_{t-1}^P)\\ a_i^t&=exp(s_i^t)/\sum_{i=1}^mexp(s_j^t)\\ c_t&=\sum_{i=1}^ma_i^tu_i^Q \end{split}\]

下一层进入到了Self-Matching Attention层,主要是为了纵观全文,检查之前的理解是否有误,同样还是通过attention达到此目的。

    \[\begin{split} h_t^P&=BiRNN(h_{t-1}^P,[v_t^P,c_t])\\ s_j^t&=v^Ttanh(W_v^Pv_j^P+W_v^{\widetilde{P}}v_t^P)\\ a_i^t&=exp(s_i^t)/\sum_{i=1}^nexp(s_j^t)\\ c_t&=\sum_{i=1}^ma_i^tv_i^P \end{split}\]

最后一层到了输出层,输出层主要使用了pointer network,本质上它就是一个seq2seq的过程,只不过只有两级,第一级的输入是question的attention-pooling,而不是其他RNN的随机初始化(但是r^Q的初始状态是随机初始化的,因为总会有一个起始点),输出可以通过argmax获取到答案在段落中的起始地址,第二级的输入是第一级的attention-pooling,输出可以通过argmax得到终止地址。实验中还用了一个小trick来确保终止地址始终大于起始地址而且end-start不会过长。公式如下:

    \[\begin{split} s_j^t&=v^Ttanh(W_h^Ph_j^P+W_h^ah_{t-1}^a)\\ a_i^t&=exp(s_i^t)/\sum_{i=1}^nexp(s_j^t)\\ p^t&=argmax(a_1^t,...,a_n^t)\\ c_t&=\sum_{i=1}^na_i^th_i^P\\ h_t^a&=RNN(h_{t-1}^a,c_t)\\ s_j&=v^Ttanh(W_u^Qu_j^Q+W_v^QV_r^Q)\\ a_i&=exp(s_i)/\sum_{i=1}^mexp(s_j)\\ r^Q&=\sum_{i=1}^ma_iu_i^Q(=h_0^a) \end{split}\]

这里我们要预测的起始位置是p^1,终止位置是p^2
这里肯定会有很多人困惑这个V_r^Q是怎么来的,文中并没有介绍,其实它是RNN的首状态,大部分RNN的第一个输入都是这么处理的。看代码还会发现它就是一个通过tf.contrib.layers.xavier_initializer()初始化的参数,那为什么不把这个乘法写成一个参数呢,因为两个都是可学习的参数完全可以合并,我觉得是因为要和attention的输入保持一致(attention函数有两个输入)。代码如下(来自文首代码的layers.py文件):

def question_pooling(memory, units, weights, memory_len = None, scope = "question_pooling"):
    with tf.variable_scope(scope):
        shapes = memory.get_shape().as_list()
        V_r = tf.get_variable("question_param", shape = (Params.max_q_len, units), initializer = tf.contrib.layers.xavier_initializer(), dtype = tf.float32)
        inputs_ = [memory, V_r]
        attn = attention(inputs_, units, weights, memory_len = memory_len, scope = "question_attention_pooling")
        attn = tf.expand_dims(attn, -1)
        return tf.reduce_sum(attn * memory, 1)

到这里整个结构就介绍完了,按照惯例,这里贴上文章的实验结果,第一个SQuAD数据集:

第二个MS-MARCO数据集:

可以说是相当耀眼。

— 2018-12-18 14:13

Exploring the Limits of Language Modeling

今天要分享的是来自google brain的一篇2016年的论文,当时也是获得了非常好的效果。忽然发现我分享的文章几乎都是来自google,可从侧面稍微反映出google在AI领域的举足轻重的地位:)
下载地址:https://arxiv.org/pdf/1602.02410

按照惯例,先分享它取得的结果,它的single model参数个数在减少到当时最好的模型的参数的二十分之一的情况下,将perplexity从51.3降到30.0,而ensemble model将perplexity从41.0降到23.7,可以说是极大的提升,而完成这项成就的就是字符级别的CNN或者LSTM(character convolutional neural network or long-short term memory)。

论文的第二部分分享了一下相关工作和当时状况,主要包括语言模型(Language Models)、Convolutional Embedding Models、和Softmax Over Large Vocabularies三部分,主要是一些背景,感兴趣的可以根据论文介绍检索相关文献去详细了解下,这里略过,重点放在这篇文章的模型上。

图中a是一个典型的LSTM语言模型,从当前单词预测下一个单词。图中b是本文提出的模型,它不是通过当前词预测下一个词,而是分别将当前词和下一个词都先进行Char CNN产生两个向量,LSTM的输入向量就是当前词的Char CNN,输出就是下个词的Char CNN和LSTM输出的“相似性”z_w=h^Te_w(针对每个w都需要产生一个z_w,然后进行softmax:p(w)=\frac{exp(z_w)}{\sum_{w'\inV}exp(z_{w'})}),这么做的一个好处就是极大降低了参数量,不在需要维护整张vocabulary词表,只需要维护characters的词表,这里称它为CNN Softmax,还有另外一个好处就是基本不用担心OOV了。文中还有一个提升就是修改了上述“相似性”计算公式,变成:

    \[z_w=h^TCNN(chars_w)+h^TMcoor_w\]

其中corr是为了解决相似拼法的单词有较大语义差异的问题,M是将低维corr映射到高维h,对效果提升非常明显。
图c是为了提升计算性能,但效果差一些,LSTM预测目标char不是word,LSTM的输出作为sequence的decoder的输入,所以每个decoder要做softmax的数目就极大减少了,从而极大降低计算量。文中提到的小trick:In order to make the whole process reasonably efficient, we train the standard LSTM model until convergence, freeze its weights, and replace the standard word-level Softmax layer with the aforementioned character-level LSTM。

文中的第四部分针对模型做了很多参数调整,并不难理解,这里略去。强劲的实验以及实验结果也是这篇文章成功的一个重要因素,详情可见论文。

综上,本文的最大贡献在于论证了RNN的语言模型可以用于大规模数据集上,而且可以取得相当好的结果。

— 2018-12-17 20:48

BERT系列3 – transformer

首先声明一下,这里的transformer并不是变形金刚,而是BERT的基本组成单元。这一部分应该是三个系列中最枯燥无味的,不过还是有必要记一下。

论文:Attention Is All You Need(名字起得不错)
git地址1:https://github.com/tensorflow/tensor2tensor
git地址2:https://github.com/google-research/bert

Transformer是BERT的基本组成单元,在系列2中我们把它看做是一个黑盒,这里我们深入理解一下这个黑盒,模型结构图如下:

从图上可以看出,transformer主要分为两部分,encoder和decoder。从全局看,encoder有N个identical layers,每个identical layer又有两个sub-layers,每个sub-layer又有Multi-Head Attention、Add&Norm、Feed Forward、Add&Norm,decoder大体相似,只不过多了一层Multi-Head Attention、Add&Norm。到这里应该还是云里雾里,不知道它到底是啥,那我们从流程来看。

首先,模型的输入首先会经过embedding,这里它使用的是learned embeddings,并且encoder和decoder使用的是同一个learned embeddings。
其次,embeddings会经过一个positional encoding,主要是为了注入位置信息,公式如下:

    \[\begin{split} PE_{pos,2i}=sin(pos/10000^{2i/d_{model}})\\ PE_{pos,2i+1}=cos(pos/10000^{2i/d_{model}}) \end{split}\]

pos是word所在位置,对dim 1 2 3 4 5 blabla可以直接根据i算出一个向量跟输入的embedding进行element-wise的相加,encoder和decoder都是如此。
下一步就到了Multi-Head Attention,文中首先介绍了Attention函数可以看做是Q、K、V到output的映射,具体可以参考论文Key-value Attention Mechanism for Neural Machine Translation part 3.2,本质上就是将一个向量划分为两个相同维度向量进行计算,在这里可以把Q看做weight,K看做key,是向量前半部分用于计算Softmax权重,V是value,是向量后半部分用来跟权重相乘求和。公式如下:

    \[Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V\]

这里多了一个scaling factor\sqrt{d_k},文中解释了为什么要加这个因子,因为点乘会使Q、K多了一个数量级,使得softmax进入到极小梯度的地方(类似sigmoid曲线的两端),为了抵消这种影响才加了这个因子。
上文介绍的attention这里叫Scaled Dot-Product Attention,是上图Multi-Head Attention的基本元素,如下图所示:

理解起来不难,直接公式:

    \[\begin{split} MultiHead(Q,K,V)=Concat(head_1,...,head_h)W^O\\ where\ head_i=Attention(QW_i^Q,KW_i^K,VW_i^V) \end{split}\]

这里还要注意一下在decoder里面的第一层有一个mask,主要是为了使当前的decoder输入仅仅依靠左侧的输出,防止右侧的数据流入,它是个技术处理并不算一个结构特征。为了更容易理解,这里提醒一下transformer的是把所有文本一次输入给模型,并不是像RNN一个一个处理,从而获得很高的并行性。
再下一步是Add&Norm,相当于LayerNorm(x+Sublayer(x)),这里引入了residual connection,似乎它为什么有效学术界还没有定论。
在下一步是Feed Forward,公式如下:

    \[FFN(x)=max(0,xW_1+b_1)W_2+b_2\]

然后又是Add&Norm,encoder的identical layer已经完成,重复N次就是encoder了。
Decoder的所有单元都是复用encoder的单元,无需多做解释。

文中还花了一个part去解释为什么self-attention,原因有三,第一个是计算复杂度考虑,第二个是计算并行性,第三个是长依赖问题没了(最长路径)。似乎三个都是在说性能更好,当然实验结果也表现得更好,BERT非常完美地体现了,这里也贴一下论文结果:

还有一个side benefit,就是模型更容易解释。

最后强调一下,BERT里面使用的并不是上文介绍的全部,而仅仅是transformer的encoder,所以可以有较高的并行性,我觉得这也是为啥要加position embedding的原因,具体实现细节可以看BERT代码的transformer的transformer_model函数。

— 2018-12-14 17:16

BERT系列2 – pre-training

BERT:Bidirectional Encoder Representations from Transformers
论文下载地址:https://arxiv.org/abs/1810.04805

BERT如雷贯耳,刷新了很多项NLP记录,极大发挥了transfer learning的效果,google出精品!稍微感受下:它在GLUE数据集上获取7.6%的绝对提升,MultiNLI数据集上5.6%的绝对提升,SQuAD v1.1数据集上1.5%的绝对提升!可以说是万能NLP模型。接下来我们来详细看看BERT pre-training模块,fine-tuning已经在系列1中阐述,不做赘述。

Pre-training就是预训练一个模型,这个模型可以适用于很多NLP任务,譬如文本分类、NER、QA等。Google开源了它的代码,良心的是Google不仅预训练了英文模型,还预训练了中文模型,可以说是节省了我们大量财力(据说在TPU上预训练一把需要花费5w块)。我们可以将模型稍微修改一下就可以应用到很多NLP任务里面,可以说是相当方便,代码地址:https://github.com/google-research/bert

接下来我们进入主题,model architecture。如果我们不管transformer(会在系列3中阐述)是什么,把transformer当做是一个黑盒,那么它的结构就相当简单,一个多层的transformer,层数、宽度等等参数都是可以调节的,详见论文。
第一部分是模型的输入,论文进行了相当多的细节处理,如图所示:

第一个,它用了wordpiece embeddings,第二个它加入了positional embeddings,第三个它还加入了segment embeddings。论文中对后两个embeddings讲的不多,看代码可以知道positional embeddings是个位置编码,通过模型训练出来之后每个位置都会有一个跟word embedding一样维度的向量,加入到word embedding中,如下代码所示:

  if use_position_embeddings:
    assert_op = tf.assert_less_equal(seq_length, max_position_embeddings)
    with tf.control_dependencies([assert_op]):
      full_position_embeddings = tf.get_variable(
          name=position_embedding_name,
          shape=[max_position_embeddings, width],
          initializer=create_initializer(initializer_range))
      # Since the position embedding table is a learned variable, we create it
      # using a (long) sequence length `max_position_embeddings`. The actual
      # sequence length might be shorter than this, for faster training of
      # tasks that do not have long sequences.
      #   
      # So `full_position_embeddings` is effectively an embedding table
      # for position [0, 1, 2, ..., max_position_embeddings-1], and the current
      # sequence has positions [0, 1, 2, ... seq_length-1], so we can just
      # perform a slice.
      position_embeddings = tf.slice(full_position_embeddings, [0, 0], 
                                     [seq_length, -1])
      num_dims = len(output.shape.as_list())

      # Only the last two dimensions are relevant (`seq_length` and `width`), so
      # we broadcast among the first dimensions, which is typically just
      # the batch size.
      position_broadcast_shape = []
      for _ in range(num_dims - 2):
        position_broadcast_shape.append(1)
      position_broadcast_shape.extend([seq_length, width])
      position_embeddings = tf.reshape(position_embeddings,
                                       position_broadcast_shape)
      output += position_embeddings

为什么要加入positional embedding?原因就是多层transformer不像RNN没有包含未知信息。
segment embedding就更简单了,SEP分隔的第一段句子为0,第二段句子为1,还有个二维数组将这个0或者1映射到word embedding的维度,参数可以学习。代码里面还说了为什么有SEP还要segment embedding,它的解释是为了模型更容易学习到序列的概念。

  if use_token_type:
    if token_type_ids is None:
      raise ValueError("`token_type_ids` must be specified if"
                       "`use_token_type` is True.")
    token_type_table = tf.get_variable(
        name=token_type_embedding_name,
        shape=[token_type_vocab_size, width],
        initializer=create_initializer(initializer_range))
    # This vocab will be small so we always do one-hot here, since it is always
    # faster for a small vocabulary.
    flat_token_type_ids = tf.reshape(token_type_ids, [-1])
    one_hot_ids = tf.one_hot(flat_token_type_ids, depth=token_type_vocab_size)
    token_type_embeddings = tf.matmul(one_hot_ids, token_type_table)
    token_type_embeddings = tf.reshape(token_type_embeddings,
                                       [batch_size, seq_length, width])
    output += token_type_embeddings

将以上三个embeddings相加作为多层transformer的输入,整个模型就这样了。

怎么做分类?获取BERT输出第一个label做个LR。怎么做相似性计算?获取BERT输出第一个label做个LR。怎么做QA或者NER?获取所有输出对所有输出做LR。就这么简单。什么?怎么获取输出或者embedding?google给你写好啦:

  def get_pooled_output(self):
    return self.pooled_output

  def get_sequence_output(self):
    """Gets final hidden layer of encoder.

    Returns:
      float Tensor of shape [batch_size, seq_length, hidden_size] corresponding
      to the final hidden of the transformer encoder.
    """
    return self.sequence_output

  def get_all_encoder_layers(self):
    return self.all_encoder_layers

  def get_embedding_output(self):
    """Gets output of the embedding lookup (i.e., input to the transformer).

    Returns:
      float Tensor of shape [batch_size, seq_length, hidden_size] corresponding
      to the output of the embedding layer, after summing the word
      embeddings with the positional embeddings and the token type embeddings,
      then performing layer normalization. This is the input to the transformer.
    """
    return self.embedding_output

  def get_embedding_table(self):
    return self.embedding_table

当然到这里并没有结束,怎么去预训练这个模型?google又花了很多心思在这上面,这可能是这个模型能成功的很大一个原因,也给了我们不少启发。
两个无监督任务做pre-training:Masked LM和Next Sentence Prediction。
第一个任务MLM简单来说就是随机抹去15%的单词去预测这个单词是啥。细节来了,它并不是总是用[MASK]替换那些单词,替换只占了80%,剩余的10%替换成了一个随机单词,最后的10%保留了原句。Transformer并不知道预测哪个单词,所以它需要记住每个单词的上下文分布,并且被替换的单词不多,并没有破坏语义。
第二个任务就是判断句子A是否是B的前一句。这个任务对QA和NLI收益很大。

最后loss的定义就是两个task的loss求和平均。

实验结果这里就不多说了,反正就是牛!

— 2018-12-13 18:41

Hybrid Code Networks: practical and efficient end-to-end dialog control with supervised and reinforcement learning

今天要分享的是一篇端到端的会话系统,简称HCNs(Hybrid Code Networks),主要作者是来自Microsoft Research,下载链接:https://arxiv.org/abs/1702.03274。论文的主要贡献是极大降低了对训练集的大小的要求,而且可以用有监督学习或者强化学习或者两者结合去优化它,在bAbI会话数据集上获取到了state-of-the-art的效果,并且打败了两个商业系统,这就很厉害了!

Part I首先介绍了传统的任务型会话系统的弊端,各个模块的依赖性极大的提升了系统的复杂性。在我自己实现的时候感觉最大的问题就是为需要为各个模块准备不同的数据集,各个数据集之间的关联性还很强,非常难以把控,数据也非常难以收集,它不是一个简单的工程。同时它也指出了现有的端到端的方法有个一弊端,就是没有一个通用的方法去注入领域知识还有限制条件,例子就是对于结果进行排序,软件工程里面很简单就可以实现,但是在对话系统里面就需要很多的数据去学习。HCNs就很好的解决了这个问题,它是个端到端模型,它允许开发者通过software或者action templates方便注入领域知识,文中还提到它可以用很少的数据集就获得跟现有端到端系统相同的performance,靓!

Part II模型介绍,主要就是一张结构图:

捡重点来说,第一步用户会提问,第二步向量化,这里有两种方法:词袋子模型、fasttext模型(第三步),第四步就是slot filling槽位填充了,第五步将第四步获取到的实体信息送给entity tracking模块(有点像简化版DST),它能够管理这些实体,譬如将获取到的实体映射到数据表的一行去,然后(optionally)返回一个action mask,就是表征哪些会话动作当前会话轮可以做,哪些不可以做,还可以返回(optionally)一个context features用于记录哪些实体已经提取到了哪些没有。1-5步获取到的向量会被连接成一个新向量,作为RNN(LSTM/GRU)的输入,然后将输出送给一个dense layer输出给Softmax,它的维度就是系统的动作数。第十步将action mask应用上去,重新归一化(因为剔除了一些概率值,总和不为1了)。将十一步获取到的概率分布做个抉择,十二步获取一个系统action,在这里如果使用RL的话,会有个探索过程,会对结果进行一个sample输出,不能一直取最大概率值。选取的会话动作送给entity output,会将实体信息代入模板,譬如”, right?”变成”Seattle, right?”。十四步控制分支根据action决定调用api(返回富文本)或者返回文本,这一步的输入也会作为RNN的输入。

剩余部分就是一些相关工作、有监督学习评估、RL学习评估的工作,稍微贴几张结果图片:

在使用RL的时候,文章里面定义完全成功完成任务时reward为1,否则为0,轮数越多,reward越少,系数为0.95。论文最后还提到RL如何和SL(Supervised Learning)结合,当RL效果不行时切换到SL。

说了这么多当然要隆重推出HCNs的开源实现啦:https://deeppavlov.ai/。简介:DeepPavlov is built and maintained by Neural Networks and Deep Learning Lab at MIPT within iPavlov project (part of National Technology Initiative) and in partnership with Sberbank.
附:MIPT怎么样?

— 2018-12-13 08:58