蚂蚁金融NLP竞赛——文本语义相似度赛题总结

7,318 阅读15分钟
原文链接: blog.csdn.net

1前言

许久没有更新博客了,主要是忙于考试周和最近参加的一个蚂蚁金融的NLP比赛——文本语义相似度赛题。话不多说,直奔主题。本篇博客主要记录的是自己入门NLP以来第一次参加NLP性质的赛题的详细解题过程。接下来将分成三个部分进行叙述:赛题描述、解题思路及相关代码实现、赛题总结.
项目代码地址及说明

2赛题描述

说明:问题相似度计算,即给定客服里用户描述的两句话,用算法来判断是否表示了相同的语义。若语义相同则判断为1,不相同则为0.

1 “花呗如何还款” –“花呗怎么还款”:同义问句
2 “花呗如何还款” – “我怎么还我的花被呢”:同义问句
3 “花呗分期后逾期了如何还款”– “花呗分期后逾期了哪里还款”:非同义问句

需要思考和解决的问题: 1.句子对中存在错别字、同义词、词序变换等问题。2.两句话很类似,仅仅有一处细微的差别,最后的语义却不同。
赛题的评价指标:
赛题评分以F1-score为准,得分相同时,参照accuracy排序。对于F1的计算,需要明白以下几个概念,True Positive(TP)意思表示做出同义的判定,而且判定是正确的,TP的数值表示正确的同义判定的个数; False Positive(FP)意思表示做出同义的判定,但是判定是错误的,数值表示错误的同义判定的个数;True Negative(TN)意思表示做出不是同义的判定,而且判定是正确的。数值表示正确的不同义判定个数;False Negative(FN)意思表示做出不是同义的判定,而且判定是错误的。数值表示错误的不同义判定个数。
计算公式:

精准率(做出为同义的判定中,事实上为同义的比例):precision rate = TP / (TP + FP)
召回率(事实上为同义的语句对中,有多少比例的同义对被预测出来了):
recall rate = TP / (TP + FN)
准确率:accuracy = (TP + TN) / (TP + FP + TN + FN)
F1-score = 2 * precision rate * recall rate / (precision rate + recall rate)

3解题思路及相关代码实现

3.1赛题整体分析

分析:本赛题中需要设计一个算法计算两个语句的语义相似度,最后表示是否为同义和非同义。对于要设计出一个算法,首先,需要先明确算法的输入和输出。很容易的判断出算法最终的输出是0或1,而算法的输入则是需要思考的地方,如果考虑以神经网络进行建模的话,需要通过表示学习方法将输入的语句用向量表示(词向量),如果以机器学习的分类方法进行建模的话,则需要对输入的语句进行特征的抽取,特征的选择,和特征的表示。输入语句的词向量表示需要思考的问题——训练词向量的语料库和训练词向量的模型方法的选择。

3.2训练数据的分析

整个赛题相关的数据下载地址,密码为:5ig5。

1.数据label包含两类1和0,属于经典的二分类问题,数据集中类别1和类别0的比例大致为4.5:1,属于分类不平衡的现象(这点也是后面需要解决的问题也是赛题成绩提高的重要点)。
2.需要判断的每队语句队存在着一定的错别字,如:“花呗”写成“花被”,“登录”写成“登陆等,语句中也包含相应的同义词如:”为啥”:”为什么”,虽然这些人可以立马判断出,但是算法却是需要解决的问题点。
3.样本数据不仅类别分配不平衡,其中正例(同义)的语句队数据量偏少,模型比较难学习出同义语句队的特征。
4.中文句子中存有未登录词的现象,经过测试像蚂蚁花呗,蚂蚁借呗…等新词经jieba分词是区分不出来的。

3.3整体思路

本次赛题最终的评价指标F1为0.6238,赛题的评价指标F1提升过程0.29——>0.45->0.53->0.5925->0.6159->0.6238。本次赛题纯属是第一次参加NLP的比赛,一步一步慢慢积累解题技能的过程。最终的解题思路为:

最终采用机器学习的方法对本赛题的分类问题进行建模,对机器学习输入主要是两个方面:抽取的NLP统计特征和经过深度模型训练输出的语句的相似度。最终的分类模型采用Stacking的集成学习方式进行训练。

3.4具体实现过程和步骤

Step1 前期外部数据整理工作
根据前期训练数据的分析工作,需要使用到的外部数据有,主要部分是人工寻找的:

1.用于语句队的分词的停用词表——stop_words.txt
2.用于jieba分词的新词的自定义字典——dict_all.txt
3.用于纠错及部分同义词替换规则的文件——spelling_corrections.json
4.用于语义相似度的深度学习模型的预训练的词向量(知乎问答语料库训练的300维度)——sgns.zhihu.bigram
5.用于计算两个语句中的疑问词的相似度的疑问词相似的规则文件——doubt_words.txt

Step2 文本预处理工作
该部分主要的目的是:为最终算法的输入提供一个相对较正确的表示,主要的工作是:将寻找的规则文件的词语对在语句中进行替换,去除语句中的停用词,将语句进行分词处理,并根据预训练的词向量的矩阵,构建出词汇表的词向量矩阵embedding_matrix.其中构建预训练的词向量使用的语料库为整个训练集10447对个语句组成,训练方法是Word2Vec中的CBOW方式,negative sampling技巧。另外一种预训练的词向量是外部网上资源知乎问答语料库经过Word2vec训练的。词向量(词的分布表示)主要是为了作为神经网络的Embeding层的输入。
详细的过程为:

    # step 1 加载原数据
    jieba.load_userdict(project.aux_dir + dict_path)
    data_local_df = pd.read_csv(project.data_dir + train_all, sep='\t', header=None,names=["index", "s1", "s2", "label"])
    data_test_df = pd.read_csv(project.data_dir + test_all, sep='\t', header=None, names=["index", "s1", "s2", "label"])
    data_all_df = pd.read_csv(project.data_dir + train_data_all, sep='\t', header=None, names=["index", "s1", "s2", "label"])
    # 预训练中文字符向量
    pre_train_char_w2v()
    # 训练集中的语句的文本处理,去除停用词,根据规则表替换相应的词,使用jieba对语句进行分词处理
    preprocessing(data_local_df,'train_0.6_seg')
    preprocessing(data_test_df,'test_0.4_seg')
    preprocessing(data_all_df,'data_all_seg')

    # 保存label
    project.save(project.features_dir + 'y_0.4_test.pickle', data_test_df['label'].tolist())
    project.save(project.features_dir + 'y_0.6_train.pickle', data_local_df['label'].tolist())
    project.save(project.features_dir + 'y_train.pickle', data_all_df['label'].tolist())

    # step 2预训练中文词组向量
    pre_train_w2v()

    # step 3记录训练集中的词汇表中的词对应的词向量表示
    process_save_embedding_wv('train_all_w2v_embedding_matrix.pickle',type=2,isStore_ids=True)
    # process_save_embedding_wv('zhihu_w2v_embedding_matrix.pickle',type=2,isStore_ids=False)
    # process_save_embedding_wv('zhihu_w2v_embedding_matrix.pickle',type=3,isStore_ids=False)

    # step 4 char wordembedding
    process_save_char_embedding_wv(isStore_ids=True)

Step3 文本特征抽取工作
描述:赛题的重点部分,赛题成绩的好坏主要取决于特征的选取工作。由于不像很多大佬一样一开始便能设计出针对语句对进行语义相似度判断的神经网络模型,此部分是一点一点调研和摸索出来的。因为最终的分类模型是通过机器学习的方法进行建立,所以需要以多维度的特征作为计算语句对语义相似度算法的输入。最终我们选取的特征包括两个方面,一方面是计算语句对语义相似度的神经网络的输出作为特征之一,另一方面是统计语句对的NLP数据的特征。
a.两个语句的长度上的差距.
b.两个语句的编辑距离.
c.两个语句的n-gram相似性的特征.
d.两个语句的词的统计特征.包括相同词的个数,不同词的个数,Jaccard相似度。
e.两个语句中的疑问词的相似度.主要是根据疑问词相似度规则文件进行计算。
f.两个语句是否同时包括蚂蚁花呗和蚂蚁借呗相关的主题.,观测到训练数据中的语句对基本全都是关于蚂蚁花呗和蚂蚁借呗相关的问题。
g.两个语句的词向量组合的相似度.主要是根据全部训练集做语料库使用word2vec训练的词向量计算的两个语句的词向量的组合相似度。
h.两个语句神经网络编码的曼哈顿距离相似度和余弦相似度主要是根据两个语句的预训练词向量输入经过LSTM进行编码计算出两个语句的语义向量的曼哈顿距离和余弦相似度作为最后的机器学习的分类模型特征之一。
i.两个语句的神经网络编码的match vector形式计算的相似度.
j.两个语句的神经网络编码的改进的Compare-Aggregate模型的相似度
神经网络模型说明
此处相似度特征表现最好的神经网络模型是改进的Compare-Aggregate模型,其次是两个语句神经网络编码的曼哈顿距离相似度和余弦相似度的模型。所以接下来对这两个神经网络模型做详细的介绍。接下来将详细介绍这三种神经网络模型,这也是自己在赛题中的使用模型的尝试改进和理解性的构建模型的过程。
模型一:基于LSTM进行语义编码的曼哈顿距离相似度和余弦相似度的模型
图1
提交结果后的平台测试的F1结果为0.456,该模型主要是简单对输入的语句序列进行LSTM的编码获取得到句子的语义表示向量,再经过语义向量的曼哈顿计算和余弦距离计算,最后依据这两项训练出模型参数。
具体实现为:

class ManDist(Layer):
    def call(self, inputs, **kwargs):
        """This is where the layer's logic lives."""
        self.res  = K.exp(- K.sum(K.abs(inputs[0]-inputs[1]),axis = 1,keepdims = True))
        return self.res
class ConsDist(Layer):
    def call(self, inputs, **kwargs):
        self.res = K.sum(inputs[0] * inputs[1],axis=1,keepdims=True)/(K.sum(inputs[0]**2,axis=1,keepdims=True) * K.sum(inputs[1]**2,axis=1,keepdims=True))
        return self.res

模型二:基于LSTM进行语义编码的match vector形式计算的相似度的模型
该模型是在模型一的基础上进行了改进,提交结果后的平台测试的F1结果为0.53.改进的地方是在使用LSTM进行对语句词向量编码后将两个语句的语义编码向量进行点乘和减法计算后再进行接下来的相似度计算。提升的原因是:语句的语义向量除了LSTM编码形成的语义向量还附加了两个语句的语义向量的点乘和减法形成的语义交互。
具体实现为:

    def call(self, inputs, **kwargs):
        encode_s1 = inputs[0]
        encode_s2 = inputs[1]
        sentence_differerce = encode_s1 - encode_s2
        sentece_product = encode_s1 * encode_s2
        self.match_vector = K.concatenate([encode_s1,sentence_differerce,sentece_product,encode_s2],1)
        return self.match_vector

模型三:改进的Compare-Aggregate的模型
模型三做出的改进:第一部分,该模型是基于论文《A Decomposable Attention Model for Natural Language Inference》中的Attention机制,在语句中的词级别上添加了注意力机制,让经过LSTM编码后的语义向量在语句中的词上更有了重心,主要体现在语句中各个词的词向量经过attention机制后的权重分配不一样了。此外,还借助了该论文中的Compare部分,将attention机制表示的语句词向量与双向LSTM语义编码的向量进行Concatenate连接。第二部分,模型三除了以两个语句的词组级别的向量做为输入外,还增加了两个语句的字级别的向量作为输入。增加字符级的输入,主要是为了解决out-of-vocabulary词组级别的问题。第三部分,在语句的词向量的语义向量上添加了基于CNN的交互式处理思想,这种方式考虑了句子之间的所有交互属性。
根据模型三提交结果后的平台测试的F1结果提升到0.59.
图2
最终的代码实现

    # 提取深度学习特征
    extract_feature_siamese_lstm_manDist()
    extract_feature_siamese_lstm_attention()
    extract_feature_siamese_lstm_dssm()
    # 提取NLP特征
    extract_sentece_length_diff()
    extract_edit_distance()
    extract_ngram()
    extract_sentence_diff_same()
    extract_doubt_sim()
    extract_sentence_exist_topic()
    extract_word_embedding_sim()

每种抽取方法对应一个函数,抽取特征的步骤:
step1 定义抽取特征的方法名
step2 载入需要抽取特征的数据
step3 定义抽取特征的方法(如果是深度学习需要调用./final_demo/train_model.py文件中构建模型的方法)
step4 逐行读取数据,并进行特征的抽取,保存到相应的np.array数组中
step5 特征的保存
调用方法:
project.save_features(feature_train, feature_test, col_names, feature_name)
参数说明:训练集的抽取出的特征feature_train,测试集的抽取特征feature_test,抽取特征的方法的多列特征的列名col_names,抽取特征的方式名feature_name

Step4 Stacking分类模型的建立工作
通过上述的特征提取方法将会提取出19个特征,通过对提取出的特征使用机器学习建立分类模型。一开始选择使用了基本的sklearn中的LogisticRegression分类方法。后来提出使用集成学习中的Stacking模式将Sklearn中的多个分类学习方法进行融合,最终提交结果后的平台测试的F1结果提升到0.61.
图3
本次赛题使用的两层Stacking方式,选用了GussianNBClassifier、RandomForestClassifier、LogisticRegression、DecisionTreeClassifier四个基分类器作为第一层Stacking基模型。第二层Stacking选用的是RandomForestClassifier分类器进行训练的。具体实现的方式为:

    # stacking 第一层模型训练,分别使用基分类器对训练集X_train进行5折交叉验证,在使用训练的模型预测X_test取均值。作为第二层Stacking模型的输入。
    gnb_cls = GussianNBClassifier()
    gnb_oop_train,gnb_oofp_val = gnb_cls.get_model_out(,y_train,X_test)

    rf_cls = RFClassifer()
    rf_oop_train, rf_oofp_val = rf_cls.get_model_out(X_train, y_train, X_test)

    lg_cls = LogisicClassifier()
    lg_oop_train, lg_oofp_val = lg_cls.get_model_out(X_train, y_train, X_test)

    dt_cls = DecisionClassifier()
    dt_oop_train, dt_oofp_val = dt_cls.get_model_out(X_train, y_train, X_test)
     # 构造第二层Stacking模型的输入
    input_train = [gnb_oop_train,rf_oop_train,lg_oop_train,dt_oop_train]
    input_test = [gnb_oofp_val,rf_oofp_val,lg_oofp_val,dt_oofp_val]

    stacked_train = np.concatenate([data.reshape(-1,1) for data in input_train],axis=1)
    stacked_test = np.concatenate([data.reshape(-1,1) for data in input_test],axis=1)

    # stacking 第二层模型训练
    second_model = DecisionTreeClassifier(max_depth=3,class_weight={0: 1, 1: 4})
    second_model.fit(stacked_train,y_train)

Step5 整个项目的运行

1.选出抽取特征的方法名组合成需要特征输入list
2.加载多个抽取方法抽取出的特征数据和训练集的label数据
调用方法:project.load_feature_lists(feature_names_list)
参数说明:feature_names_list为你的抽取特征的方法名组合成list
返回参数说明:你的训练集经过多种抽取方法组合成的多个列,你的测试集经过多种抽取方法组合成的多个列,每种抽取方法对应的列的index。
注意:一种抽取方法可能对应着多种特征列
3.构建最终的分类模型,交叉验证的方式

Step6 前期训练数据的分析出现的问题的处理
1.对于样本分类不平衡的问题,通过最终的样本类别权重调整实现样本不平衡问题的解决,在Sklearn中的分类模型用设置class_weight参数即可,查看Sklearn的原理设置class_weight就是改变sample_weight。
2.对于正例的语句对样本数量少的问题,通过将正例的样本语句对进行顺序调换,形成新的正例样本对。
通过解决上述两个问题,最终提交结果后的平台测试的F1结果提升到0.6238.

4赛后总结

1.首先需要自己定义算法的赛题,首先需要考虑好算法的输入的形式和输出的展示。对于算法的输入,需要考虑好对输入要做什么预处理,以什么形式输入算法中进行计算,也就是怎么表示输入。对于算法的输出,这是重点,它决定算法的结构选型。
2.其次对于数据的分析,设计算法的结构之前,一定需要对给出的赛题数据进行统计方面,算法方面的分析,对潜在的可能需要解决的问题,或者存在的统计特征进行记录下来,这很可能是后期赛题成绩提高的关键。
3.设计算法的结构的时候,如果完全没有想法,可以查询相关的论文,从论文中提出的方法模型进行改进这是一种很好的方法。针对前期提出的潜在问题有针对性的查询解决该些问题的论文。所以说平时有空多阅读自己领域相关论文还是很有好处的。
4.训练模型时,要尽量打印出每次算法迭代过程中的val_loss,val_f1,val_acc,val_precision,val_recall.学会观察这些数据,根据经验来处理和分析出现打印的结果的原因。这很可能是查看模型是否拟合训练数据或者过拟合数据的基础。
5.就是细节问题,最后成绩的提高除了比拼算法架构外,还比拼处理问题的细节,比如说词向量的训练的过程,中文预处理的过程,Embeding 矩阵是否当做参数进行微调的过程。

5参考文献及链接

1.深度学习中汉语字向量和词向量结合方式探究
2.A Compare-Aggregate Model for Matching Text Sequences
3.A Decomposable Attention Model for Natural Language Inference
4.Text Matching as Image Recognition
5.sentence pair model 总结
5.gensim中的Word2Vec的使用
6.集成学习总结 & Stacking方法详解