此次的文本溯源项目我们以n-gram为核心思想,构建候选句子对的评测标准。
用TF-IDF和词袋模型的的思想来预筛选候选句子对,大大提升算法效率。
最后用了两种切词方式的模型融合和规则后处理(提升很小)。
Final-Leaderboard Ranking First
原文发布于个人博客(好望角),那里有更好的阅读体验。
2018.07.24更新
经过组委会测试算法的高效性和原创性,我们最终获得了SMP-ETSE测评的第一名。
最终获奖名单
评测任务介绍
测评代码开源
序
以前没有参加过NLP类型的比赛和测评,每天看论文想idea的日子有些许的枯燥和单调。
就和一个同学趁着期末考试复习期间(实力作死)的空闲时间,抽出时间玩了玩这个比赛。
SMP 2018 测评地址
题目分析
本次的文本溯源题目和同期的另外两个比赛其实很相似。
蚂蚁金服计算句子相似度
拍拍贷识别相似的问题句子
但文本溯源又和另外两个题有着本质的不同,因为主办方放出的数据并没有任何标签。很明显,我们需要一个无监督的算法来找到句子之间的潜在语义关系,红的发紫的深度学习在此没有用武之地。
恐怕这也是为什么这个测评的参加人数如此之少的主要原因吧。
- 验证集
- 待溯源句子1000
- 候选句子约10W
- 测试集
- 待溯源句子4000
- 候选句子约500W
思路一
根据我们之前的一些经验,探讨两个句子之前的相似性的时候,n-gram就是一个简单,并且行之有效的方法。这也是我们最早的想法。
思路二
另一个想法,传统的NLP parsing技术在理论上会比简单的n-gram 方法更好的分析句子结构,从而帮助计算机理解句子语义,找到对应的句子。
但是,整个代码构建工程量大,实现难度较高。
我们的当然要从简单的第一种思路入手尝试。(也是我们最后采用的方案)
预处理
按照思路一构建一个简单的baseline,在没有调参和仔细预处理之前,在验证集上取得了0.8737 的成绩,这也给了我们继续这一方法的信心。
随后我们反过头来仔细进行预处理。
- 处理影响分词效果的杂乱字符
- 直接删除
- 将他们替换为空格
- 对于数据进行全半角格式转换(计算机并不认为相同字符的全半角格式是一样的字符)
符号预处理之后,在验证集获得了0.9008的表现 ,
然后又轻微调参(根据不等式,我们知道,P、R相等的时候,F值表现会是最好的),验证集表现上涨到了0.9087。
切词
在预处理方面,切词是最令人头疼的第一个地方。
我们有了很多开源的切词工具,最终选了用了thulac和jieba两种。
我们发现这两种切词方法的本身都有一定的局限性,但是又有一定的互补性。
虽然用两种切词工具在时间上会对花费很多,但是为了更好的算法效果,我们采用两种切词方式(后来想想好像有点亏,这样的处理大约只能带来一个点的提升,但花费了大量的时间)。
python 代码运行效率的低效众所周知,对于测试集500W句子的数据量来说,可谓是十分头疼。
解决方式也很简单,直接调用两种切词方法的c++接口就好了。就可以体验飞一般的速度。
值得注意的是,thulac 的切词包在大数据量的情况下,会有崩溃的情况,原因未知。我们的处理方式是将500W数据切分成了四份,调用四次thulac的c++接口(多了三次的model载入时间),切词后再将所有数据合并起来。
TF-IDF
n-gram 方法固然简单高效,但也很容易想到一个缺陷。我们不应该对于所有的gram“一视同仁”!
所以我们建立了TF-IDF,根据每个词语在文档中的几个句子中出现,设立TF。
并且设立反向的IDF,为了不同频次的词语反向加权。
这使我们的验证集F值表现达到了0.9356。
优化目标
对于深度学习,大家都知道。当我们的loss和最后的评价指标越相近的时候,模型的训练效果也往往是更好的。
一个简单的n-gram 通常是仅仅考虑精确率,我们为什么不进一步的考虑召回率呢?甚至直接对于F值进行优化?
这里有一篇ACL2014的文章作为参考
这样的优化操作之后,我们的验证集表现达到了0.9430
倒排索引(词袋模型)
以上主要是在算法精度上的优化,并且没有可以的优化算法效率。导致我们在每一次对齐的时候都要对10W数据做遍历比较……
为了使算法快点出结果,我们用了多进程的方法,但这显然不是长久之计。
经过观察发现。10W句子中的绝大多数句子,算法评价两个句子的相关性,都极低,对于我们的溯源任务造不成任何干扰,那么如何去掉他们呢?
不难想到,我们的算法基于n-gram,而对齐表现差的句子显然和目标句子重合的n-gram非常少。那么,我们为什么不讲这样的句子直接过滤掉呢?可以减少句子对的评价次数几百倍,大大提升算法运行效率!
具体的,我们首先用one gram对句子进行过滤(候选句子中连一个词语都没有出现在目标句子中,全部去掉不考虑);进一步的,还可以对 two gram 设定一定的阈值。让我们在算法精度和运行速度上进行权衡。
最终,我们算法经过此处理后。在10W验证集上的表现,从单进程500min缩短为了40s 以下,还没有损失精度!
模型融合 & 后处理
以上提到了两种切词方式,以及两种预处理方式。为了取得更好的算法效果。我们从它们排列组合后的四种方法中挑取了两个互补性较强的model进行模型融合。
验证集达到了0.9549的数值水准。
然后我们肉眼观察一些阈值附近的“疑似”对齐错误的句子。建立几个规则性的后处理操作。
验证集达到了0.96036的数值水准。
最终测试集结果
-
硬件环境
- Intel(R) Xeon(R) CPU E5-2697 v4 @ 2.30GHz
- 内存 188 GB
- Linux version 3.10.0-514.el7.x86_64 ,
- gcc version 4.8.5 20150623 (Red Hat 4.8.5-11)
- Python 3.4.5 ,numpy
-
运行时间参考
- 预处理时间共 1267秒 (21.1分钟)
- 符号处理 & 编码格式转换:218秒
- THULAC分词(c++版本):272秒
- jieba分词(c++版本):112秒
- 计算TF-IDF 164秒
- 建立倒排索引表 501秒
- 核心算法:115秒 (1.9分钟)
- 预处理时间共 1267秒 (21.1分钟)
-
最后数值表现
- F1-Measure : 0.801258108905052
- Precision: 0.7133356667833392
- Recall : 0.9139013452914798
- Ranking: First
尾声
虽然时间紧迫,经验不足。在比赛中和队友都有一些失误,但最终侥幸排名第一。
-
做的不够好的地方:
- 预处理不够精细
- 切词处理没有去增加一个字典
- 懒得去寻找同义词源,或者训练一个词语级别的翻译模型。理论上可以进一步提高算法表现。
- 由于留给测试集出结果的时间只有24h,事先准备的代码不够充分。
- 在测试集阈值设定的时候陷入了思维误区,算法最终的F值结果损失精度百分之三以上。
-
做得比较好的地方:
- 基本的n-gram 思路简单而高效。
- TF-IDF使算法的评价方式更加合理。
- 倒排索引是使算法高效,简洁。
- 艰苦卓绝的后处理和肉眼调参……
最后:感谢队友这些天的付出,也感谢努力的自己。