查看原文
其他

flyai医疗智能问答比赛小结

邱震宇 AINLP 2020-10-22

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

知乎专栏:我的ai之路





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

更新一下,将工程上传到了github。链接:
https://github.com/qiufengyuyi/flyai_QA_NLG

qiufengyuyi/flyai_QA_NLGgithub.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。这个我还没有尝试,但是其思想也是很直观的。它的公式如下:

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

提升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),备注工作/研究方向+加群目的。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存