16

flyai医疗智能问答比赛小结

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MjM5ODkzMzMwMQ%3D%3D&%3Bmid=2650412252&%3Bidx=2&%3Bsn=26cd96de36925f67100ada0e002d90eb
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.

作者:邱震宇( 华泰证券股份有限公司 算法工程师)

知乎专栏:我的ai之路

前段时间参加了flyai平台的月赛——医疗智能问答比赛。 最后虽然没有达到规定要求的基础分20,但是也算是排在了第四位,基于平台的各种限制,结果倒也可以接受,因此本文就以我在比赛中的一些过程做一个小结。

更新一下,将工程上传到了github。链接:

https://github.com/qiufengyuyi/flyai_QA_NLG

qiufengyuyi/flyai_QA_NLG github.com

赛题

初见这个赛题,我以为是那种类似于阅读理解式的QA比赛,正好自己最近在工作上也在研究QA相关的业务,就参加了。没想到仔细研究了赛题,研究了给出的样例数据后,发现这是一个NLG问题。问题为给出一个医疗相关的问题,然后直接用模型生成该问题相关的解答。另外基于flyai平台本身的规则限制,我是看不到整体的训练数据的,因此最后也还是使用了传统的NLG的方式去做这个问题。

尝试的方法

下面就我尝试过的一些方法做一个总体的介绍。有些方法虽然最后的分数并不高,但是我觉得有可能是我没有训练好的原因,因此也写下来,拓宽一下思路,如果其他同学对此有自己的一些看法,欢迎交流。

Seq2Seq

首先,我使用的主体架构仍然是seq2seq,且基于的是RNN,由于数据量不多,所以没有使用transformer。关于seq2seq的内容在我之前的文章中有介绍过,可以移步tensorflow与seq2seq回顾一下,同时这篇文章还详细解析了tensorflow中的seq2seq的实现过程。本次我的实现也是基于这个,但是在此之上做了一些扩展,如下所示。

解码

在解码方面,尝试了传统的greedy-embedding,beam-search方式。前者虽然效率较高,但是效果不太好。后者效果会有提升,但是解码速度有点慢,而且容易产生重复的或者短的序列。通过学习cs224n课程中关于NLG的那节课,知道了最近两年又催生出以sampling为核心思想的一些解码方法。该思想主要是在每个timestep解码的时候,对当前生成的词库的概率分布进行采样,得到最终结果。

通过一些调研,找到了这样一篇论文The Curious Case of Neural Text Degeneration。这里介绍几个效果不错的基于采样的解码方法。我主要实现了其中的两种,下面分别介绍一下。

1、Top-K sampling。由于如果对整个词概率分布进行采样的话,容易采样到概率分布长尾中的词(虽然概率极小),这些词通常被认为是严重错误的词。有些同学可能会说,他们的概率非常小,应该会极不可能被采样到。但是随着文本序列的增长,这些词被采样到的概率反而会变大,越到文本的结尾,这些词出现的可能性反而会越高。基于这个原因,有必要对整个词的概率分布做一个truncated。因此就有了这个top-k sampling。具体做法就是先将词根据其概率进行排序,然后选取最大的K个词。K是超参数,根据实际情况来判断。我个人觉得覆盖词库中的前50%的词就可以了。具体实现上,我是继承了greedyEmbedding的方法,重写是sample方法,如下:

def sample(self, time, outputs, state, name=None):
    """sample for SampleEmbeddingHelper."""
    del time, state  # unused by sample_fn
    # Outputs are logits, we sample instead of argmax (greedy).
    if not isinstance(outputs, ops.Tensor):
      raise TypeError("Expected outputs to be a single Tensor, got: %s" %
                      type(outputs))
    topk_outputs,topk_outputs_indices = tf.nn.top_k(outputs,self._top_k,sorted=True)

    # print(topk_outputs)
    sample_id_sampler = categorical.Categorical(logits=topk_outputs)
    sample_ids = sample_id_sampler.sample(seed=self._seed)
    sample_ids = tf.expand_dims(sample_ids,-1)
    batch_list = tf.range(0,self._batch_size)
    batch_list = tf.expand_dims(batch_list,-1)
    sample_batch_ids = tf.concat([batch_list,sample_ids],axis=1)
    sample_ids_result = tf.gather_nd(topk_outputs_indices,sample_batch_ids)
    return sample_ids_result

主要是先用top_k方法选取logits最大(跟选概率最大的一个意思)的k个词及其位置。然后在抽取后的K个词建立新的概率分布,然后从这个分布中进行采样。

2、top-k方法简单有效,但是k的选取似乎不是那么直观。而另外一种top-p sampling方法就显得更加的直观了。它提出的理由跟上述一样,也是为了解决长尾分布词的问题。但是它对分布进行truncated的方式与top-k不太一样,它的思想是先将词根据概率进行排序,然后将这些词的概率逐个累加,直到概率累加和大于等于设定的阈值p就停止。最后要处理的词就是被累加概率的那些词。p虽然也是超参数,但是意义上比k直观点,它代表词的cumulated distribution。一般我会选择p为0.9,就能囊括大部分非长尾的词了。具体实现参考了github上的一位大神的代码,具体链接忘记了,下次找到再补上,代码如下:

def sample(self, time, outputs, state, name=None):
    """sample for SampleEmbeddingHelper."""
    del time, state  # unused by sample_fn
    # Outputs are logits, we sample instead of argmax (greedy).
    if not isinstance(outputs, ops.Tensor):
      raise TypeError("Expected outputs to be a single Tensor, got: %s" %
                      type(outputs))
    logits_sort = tf.sort(outputs,direction='DESCENDING')
    probs_sort = tf.nn.softmax(logits_sort)
    probs_sort_sum = tf.cumsum(probs_sort,axis=1,exclusive=True)
    logits_sort_masked = tf.where(probs_sort_sum < self._top_p,logits_sort,tf.ones_like(outputs,dtype=outputs.dtype)*1e10)
    min_logits = tf.reduce_min(logits_sort_masked,axis=1,keep_dims=True)
    sample_logits = tf.where(outputs < min_logits,tf.ones_like(outputs,dtype=outputs.dtype)*-1e10,outputs)
    sample_ids = tf.multinomial(sample_logits,num_samples=1,output_dtype=tf.int32)
    sample_ids = tf.squeeze(sample_ids,axis=1)
    return sample_ids

同样是继承的greedyEmbedding,重写了sample方法。核心就是先根据logits计算词的概率分布,然后排序,算累加和,找到累加和大于阈值p的那个词的位置,然后对于这个词之前的所有词重新建立新的概率分布再采样。

上述两个方法都对最后的结果分数有不少的提升,top-k相对于greedyEmbedding提升了1分多,而top-p相对于top-k有提升了接近1分。

另外上述的论文已经cs224n课程中都提到了一种采样方法叫 Sampling with Temperature。 这个我还没有尝试,但是其思想也是很直观的。它的公式如下:

YnEj2iU.jpg!web

简单说就是在根据logits计算概率时,在logits上除以一个温度系数。这个温度系数控制的是整个概率分布的一个缓陡程度。简单来说就是这样:

i6fiimE.jpg!web

提升t,概率P分布更加均匀平滑,因此得到的output会更加的多样化。

降低t,概率P分布更加陡峭,因此得到的output会倾向于一些概率高的word,多样性会下降。

multi-RNN

这里特指在decoder端,使用了多层的RNN。因为看过很多seq2seq的实现,似乎在decoder侧都是用的单层的RNN。可能是因为多层的RNN,与encoder建议attention计算的时候需要做特殊的处理。最后我找到了tensorflow官方repo中的GNMT实现。它的实现主要思想在于只对底层的RNN与encoder建立attention的关系,然后将最后得到的attention直接复制给上面叠加的所有层。具体代码如下:

def __call__(self, inputs, state, scope=None):
    """Run the cell with bottom layer's attention copied to all upper layers."""
    if not tf.contrib.framework.nest.is_sequence(state):
      raise ValueError(
          "Expected state to be a tuple of length %d, but received: %s"
          % (len(self.state_size), state))

    with tf.variable_scope(scope or "multi_rnn_cell"):
      new_states = []

      with tf.variable_scope("cell_0_attention"):
        attention_cell = self._cells[0]
        attention_state = state[0]
        cur_inp, new_attention_state = attention_cell(inputs, attention_state)
        new_states.append(new_attention_state)

      for i in range(1, len(self._cells)):
        with tf.variable_scope("cell_%d" % i):

          cell = self._cells[i]
          cur_state = state[i]

          if self.use_new_attention:
            cur_inp = tf.concat([cur_inp, new_attention_state.attention], -1)
          else:
            cur_inp = tf.concat([cur_inp, attention_state.attention], -1)

          cur_inp, new_state = cell(cur_inp, cur_state)
          new_states.append(new_state)

其中attention_cell就是经过attention wrapper之后的cell。之后会把产生的attention信息与上面叠层的rnn的input一起输入到rnn中进行计算。

但是在实际的实验结果中,这样操作的方式效果反而不是最优的。我只对最底层的RNN做attention的wrapper后,不做上述的复制操作。最后得到的分数反而是最优的。探究原因可能是模型的学习率和超参数并没有调整好吧。有兴趣的同学可以自己去试一下,说不定会得到跟我不一样的结果。

不work的方法

这里强调一下下面介绍的方法只是我没有实现预期的结果,不代表这个方法本身有问题。

最近bert模型这么火,我肯定是要拿过来试一下的啦。但是bert在NLG的任务中表现似乎没有那么抢眼,究其原因还是因为它的训练方式是一个自编码式的。而NLG的seq2seq,它的范式是自回归式,即解码都是从一个方向到另一个方向,并不能双向同时建模信息。我尝试将bert模型的层固定住不finetune,然后只将其作为encoder和decoder的编码器,然后再接rnn层。最后得到的效果差强人意。探究了几点原因:

1、还是需要用bert进行finetune。但是需要人为修改bert里面mask language model的mask方式。用bert做NLG其实有很多业界大牛都在尝试。最近微软放出的UniLM的代码,我计划抽时间详细看一下,据说效果不错。另外由于平台的限制,一些自回归的language model没办法去尝试,如GPT-2,XLNET等,这些如果后续有机会也可以尝试一下。

2、学习率以及其他超参数的设置。用bert进行finetune时,整个任务的学习率的设计是很重要的。我在工作中深有体会。通常不能设置太大的学习率,因为会导致bert模型本身通过预训练学习到的知识会被覆盖掉,丧失了其本身的优点。而且bert每一层所学习到的语言知识也是不同的,通常是越底层,它学习到都是一些general的语言信息,接近于word embedding。而越高层,它学习到的会是更贴近具体任务的语言信息。因此可能需要每一层的在不同时期逐个放开finetune,且学习率也可能需要分开设计。总之,这是一个很靠工程技艺的事情,目前我还在探索中。

小结

总而言之,这次比赛算是复习了一下之前关于seq2seq的内容。同时也有一些收获,研究很多的decoding的方式,也尝试用bert做一些尝试。虽然不是有尝试都成功,但是也不失为一次很好的经验收货。后续关于NLG任务,计划看一下今年ACL的best paper中专门针对训练和解码方式不匹配的问题做的研究,同时也会看一下如何更好利用目前效果不错的bert系模型来做NLG任务。

本文由作者授权AINLP原创发布于公众号平台,欢迎投稿,AI、NLP均可。 原文链接,点击"阅读原文"直达:

https://zhuanlan.zhihu.com/p/89378740

推荐阅读

AINLP年度阅读收藏清单

FlyAI算法竞赛平台初体验

BottleSum——文本摘要论文系列解读

抛开模型,探究文本自动摘要的本质——ACL2019 论文佳作研读系列

基于RASA的task-orient对话系统解析(一)

基于RASA的task-orient对话系统解析(二)——对话管理核心模块

基于RASA的task-orient对话系统解析(三)——基于rasa的会议室预定对话系统实例

征稿启示 | 稿费+GPU算力+星球嘉宾一个都不少

关于AINLP

AINLP 是一个有趣有AI的自然语言处理社区,专注于 AI、NLP、机器学习、深度学习、推荐算法等相关技术的分享,主题包括文本摘要、智能问答、聊天机器人、机器翻译、自动生成、知识图谱、预训练模型、推荐系统、计算广告、招聘信息、求职经验分享等,欢迎关注!加技术交流群请添加AINLP君微信(id:AINLP2),备注工作/研究方向+加群目的。

qIR3Abr.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK