前一篇文章https://blog.csdn.net/zp563987805/article/details/104350670里我们实现了投资论坛里问题答案是否匹配的自动检测模型,但对于这类问答论坛,还有个特别重要的功能:最佳答案推荐,即基于现有的问答数据库,对于用户提出的问题推荐最匹配该问题的答案。本篇文章就该问题提出解决方案。
1、数据集
所有数据及代码下载链接:网盘链接,提取码:z9u1
原始数据集还是上篇文章中的投资知道数据集,总共58W。数据结构 title/question/reply/is_best,question大部分为空。处理方式详见 preprocess_data_touzi.ipynb。
查看数据概况:总共588159条数据,数据分布:
-
question数据分布
question为空的有: 439921 question 与 title 内容相同的有: 10542291253 question 不为空,且与title 内容不相同的有: 56985
-
分析question与title不同的5W6数据
-
question数据处理:
1)考虑title与question的长度。2)考虑title与question的文本相似度。
对于文本相似度大于一定值的取长度较长的数据,对于文本相似度较低的,互为补充考虑将question与title拼接起来作为最终的best_title。
最后只保留答案与问题相匹配的32W条数据。
对于答案推荐,一个比较合理的解决思路是:我们可以基于语句相似度来推荐和新问题最相似的原有问题的正确答案给用户,这样我们还缺少验证数据集。手动制作一份验证数据集,包括训练数据集中不存在的新问题,以及与该问题最相近的已知问题。原则上应该还要包括新问题的正确专业的答案,不过采用问题之间相似度匹配,可以不使用新问题的原答案。
2、基线模型
现在的问题就是,如何判断问题之间的语义相似度?如果我们能得到句子的向量表示,那么两个句子之间的相似度就可以采用**余弦相似度(cosine similarity)**来做计算。首先尝试两个比较简单的基线模型,分别基于ELMo和BERT。评估指标采用Mean Reciprocal Rank,这是一个推荐系统的评价指标,也就是正确答案在推荐答案中的排名的倒数平均。
ELMo基线模型
ELMo使用Bi_LSTM拼接的方式来实现BiLM获取双向语义信息,下面是具体使用方法:
-
安装PyTorch
-
安装allennlp,ELMo的具体实现
pip install allennlp -
下载 ELMoForManyLangs,提供中文支持
git clone https://github.com/HIT-SCIR/ELMoForManyLangs.git
cd ELMoForManyLangs
python setup.py install (注意ELMoForManyLangs要求python >= 3.6) -
下载简体中文版预训练ELMo模型
百度云链接 https://pan.baidu.com/s/1RNKnj6hgL-2orQ7f38CauA?errno=0&errmsg=Auth%20Login%20Sucess&&bduss=&ssnerror=0&trace。注意从百度云下载并untar模型之后,需要把config.json当中的config_path修改成elmoformanylangs当中提供的config包里的json文件路径。 -
安装北大分词工具:pip install pkuseg 。(这步不是必须,也可以用其他中文分词工具包或者按字分割)
完整的代码见ELMo_base.py,最终的ELMo的mrr指标为:0.18630804064680587,观察几个提问的推荐答案结果:
BERT基线模型
与ELMo基线模型相似,直接使用BERT来对问题进行编码,采用pooled_output来作为句子的表示。注意pooled_output是[CLS]的token表示通过一层线性层之后得到的结果。这里还需要特别指出的是,由于BERT模型一般需要被finetune,所以直接采用预训练BERT模型得到基于[CLS]的问题表达方式未必是最好的问题向量,也可以采用取平均的方式。这里代码提供了2种方式可供选择:
parser.add_argument(
"--output_type",
default="avg",
type=str,
required=False,
choices=["pooled", "avg"],
help="the type of choice output vector",
)
为了使用BERT对句子进行编码,我们使用Huggingface实现的BertModel。BertModel输出中前2个分别为(sequence_output,pooled_output),参照run_glue.py,这段代码中采用了BertForSequenceClassification,是基于BERT模型外包装了一层做序列分类任务,我们需要的是BertModel本身,而不是这个SequenceClassification的部分,使用chinese_wwm_pytorch中文整词预训练好的BERT模型。完整代码参考bert_base.py,注意不同点在于需要实现自己的DataProcessor,另外我们这里做baseline不需要train。最终BERT基线模型的mrr值:0.1910977126696094,比ELMo模型稍高,观察几个推荐答案:
3、模型搭建
模型的关键是判断句子之间的相似度,考虑训练一个同义句模型,使用Hinge Loss损失函数。第一想法是构建同义句pair与非同义句pair,同义句pair的分数一定要比非同义句pair高,max( - (同义句的分数 - 非同义句的分数 - margin), 0 ),训练模型后发现效果非常差,比不上baseline模型,且没有很好的评估方式。cosine similarity的方式应该更好,考虑使用同义句之间的cosine similarity分数与非同义句的cosine similarity分数之间做Hinge Loss,具体流程如下:
- 数据预处理,包括匹配原句子、回译句子、回答。
- 构造同义句和非同义句
- 定义模型,使用Hinge Loss进行训练
- 评估、分析模型
首先寻找同义句。这里调用有道api进行回译来得到同义句pair,即先将中文翻译成英文,在将英文翻译回中文。回译代码参照round_trip_translation.py
对于非同义句,可以简单的在其他问题中随机sample,这里采用随机sample10个非同义句并取tf-idf最大的作为非同义句,这样一定程度保证随机性和相似度较大但又不是同义句,增大训练难度。还是使用huggingface开源库,主要代码参见bert_synonymous.py。总共训练了5个epoch,每40K个iteration保存一次模型,训练结束共有10个checkpoint,模型在第五个checkpoint达到最优,验证mrr:0.19247115729927698。模型的loss曲线如下图:观察几个推荐答案:
4、结果分析
从2个基线模型的结果来看,当前BERT模型要比ELMo更适用一点,从fine-tuning后的模型mrr指标及训练loss曲线来看,我们的模型提升很有限,仅仅高了0.014左右。分析可能的原因有:
- round trip translation的翻译效果不佳,很多句子与原句的语义有较大差距。
- 数据量太小,ParaNMT的训练采用了5000万条数据,而我们的训练数据仅有32万多条。虽然用上了BERT模型也有很显著的差距。
5、模型部署
使用网页版Flask部署,在项目根目录下创建一个templates文件夹,存放FQA.html文件,另外需要一个flaskServer.py提供http服务。网盘里都有提供,展示页面如下:
6、总结
针对问答论坛里,推荐答案的应用场景,使用huggingface的BERT预训练模型搭建了最佳答案推荐系统。遗憾的是模型表现的并不很好,相比于未fine-tuning的baseline提升很少,后续优化考虑从寻找更多的数据集、更好的同义句对、模型架构及模型集成方面着手。另一方面,模型的响应速度较慢,取决于原有问题数据量,需要考虑使用缓存等工具优化。