Bert中文文本多分类与传统BOW+tfidf+LR中文文本多分类对比

       最近在重温bert,对bert的中文文本多分类的效果很好奇,并将其与传统的非pre-train模型进行对比,除此之外,由于选用的是12层的base版的bert,还从第0层开始到12层,对每一层的输出进行了校验和测试。想看看每一层的transformer对bert分类效果的影响。此外,还取用了12层的element-wise的平均值进行bert结果的评估,结论以及操作方式如下。

       一 、 数据集的选取与制作

      这里选取的是cnew的中文文本分类数据集,链接:https://pan.baidu.com/s/1ZDez64S9cnzNnucIOrPapQ 密码:vutp,包含"体育、科技、娱乐、家居、时政、财经、房产、游戏、时尚、教育"十个类别,其中train文件50000条,val5000条,test10000条。但是这个数据集有一点不好,所有的数据都是按类别排序好了的,所以考虑时间和数据集的排序问题,在实际制作数据集的时候,随机shuffle了一定数量的数据。具体如下 train 中随机选取了1000条,val随机选取了400条,test随机选取了200条。单个样本如下图所示(shuffle后的),可以看出,文本是“标签+\t+内容”的形式

  二.  bert_baseline版本制作

      关于bert的中文多分类问题,很多的博客有相关的详细说明,这里仅仅提及一下自己做的流程。以及一些遇到的坑。

     1. 首先下载bert源码,https://github.com/google-research/bert,然后下载预训练好的模型BERT-Base, Chinese,这里我选用的是bert的base版的中文预训练模型,即有12层transformer,隐层数量是768层,每层transformer的mutl-head attention是12个头。

      2. 有了预训练好的模型,我们做的仅仅是fine-tuning就好,在bert的代码中,有两个fine-tuning的代码,我们的是多分类问题,选择run_classifier.py这python文件。

       在run_classifier.py中会根据输入参数中的--task_name来加载对应的processor类,而在processor类里面会有相应的数据预处理的函数,需自己根据数据集重写一个特定的processor这里附上自己的processor

添加一个cnews的processor,一个procesor需要重写父类的四个方法,分别是

2. 1 precosser的实现

注意在get_test_examples函数中,返回了test用例的所有labels,这个labels作为评估准确率的groundtruth使用。说不定有更好的方法,大神们仁者见仁。

这里遇到了一个坑,当时为了省事,直接将train的数据集的label经过set返回,作为get_labels的结果,这样做的弊端是,set内的元素排序是不固定的,而在训练和测试的时候,会分别调用一次get_labels,导致两次的标签不一样,结果出入很大。

2.2 bert模型对输入数据的预处理

这里仅仅记录文本从example到送入模型前的feature的流程,模型对数据的进一步处理会在模型的介绍中详细解说,以get_train_examples为例

        get_train_examples的实现,最终会返回一组InputExample结构,这里我们用的是单文本,根据论文单文本的处理(不是句子对)如下图所示:

     1. 对于每个词,这个词的输入表达由三个部分组成,token信息,segment信息,和position信息,如果这个词时分词的话,会用##表示(对于英文适用,中文的话不知道是什么样子=_=)比如playing =play ,##ing

     2. 句子(包括pair对)的最大长度为512个词

     3. 每一句的第一个词[cls],是特殊分类的embedding向量,这个向量对应的输出,可以看成是整个句子的某种聚合表达,可以用来做分类任务,说人话的话,就是每个句子的第一个词的输出词向量是用来做分类任务的。

     4. 每个句子用[sep]来表示结尾(分开他们),并且会用一个已经学习过的句子A,embedding这个句子(待处理的句子)的每一个token。

 在bert的代码中,convert_single_exmple负责对单个输入的example进行转换,单个example就是单个的InputExample

在convert_single_example函数中

1. 对输入的单句,先进行tokenizer.tokenize,直接生成单个的中文汉字,然后限制一下句子的长度,不能长于max_length(传入的参数)-2,因为一个是[cls]一个是[sep]

2. 每个句子会有一个segment_ids代表论文中的segment_embedding,具体是如果输入的是句子对,第一个句子的每个词用0表示,第二个句子的每个词用1表示,我们这是单句,所以 每个词的segment_ids都是0

3. 将单句的tokernize之后的中文汉字,在通过查询vocab表转成表中的id,这个表就是 vocab.txt 中的序号

4. label直接转成label_list中的id序号

5. 最后一个句子变成一个feature,里面包含这个句子每个词的id,这个句子的mask (因为句子要补齐到最大长度,mask为1的地方是实词,0的地方是padding),segment_id, label_id

  

至此是对文本进行了分词转成id的预处理,后面会把这个feature原封不动的送到bert模型中,进行embedding操作,这里为了文章的连贯性,先不对bert模型进行剖析

附: bert模型的输入数据,

3. 模型train和teset的参数

      --可以直接参考谷歌的参数配置,记得在测试时的参数的 --init_checkpoint是trian的输出目录即可

4. 模型的输出以及准确率转换

     test结果的输出就是每个句子属于每个类别的softmax,只要求个最大值就好

 output_predict_file = os.path.join(FLAGS.output_dir, "test_results.tsv")
    with tf.gfile.GFile(output_predict_file, "w") as writer:
      num_written_lines = 0
      tf.logging.info("***** Predict results *****")
      #write the labels_list to the rcv headline
      output_line ="\t".join(str(class_name)+str(i) for i,class_name in enumerate(label_list)) \
                   + "\t"+"test_ground_truth"+"\t"+"prediction"'\n'
      writer.write(output_line)
      predict_list = []
      for (i, prediction) in enumerate(result):
        probabilities = prediction["probabilities"]
        maxindex = probabilities.argmax()
        predict_list.append(maxindex)
        if i >= num_actual_predict_examples:
          break
        output_line = "\t".join(
            str(class_probability)
            for class_probability in probabilities) +"\t"+ "test"+str(label_list.index(test_labels[i]))\
                      +"\t"+ "predict"+ str(maxindex)+"\n"
        writer.write(output_line)
        num_written_lines += 1
      differ = 0
      for i in range(len(test_labels)):
        if predict_list[i] != label_list.index(test_labels[i]):
            differ += 1
      accuracy = 1 - float(differ)/len(test_labels)
      output_line = "accuracy is" + str(accuracy)
      writer.write(output_line)
      print("test_accuracy is",accuracy)
    assert num_written_lines == num_actual_predict_examples

结果样图

后面的accuracy

至此的baseline版本结束。可以看出,没有修改任何的参数下,测试集上的准确率有0.915

三.  用bert的transformer的每一层进行预测,输出结果,并且用每一层的element-wise的mean进行预测结果

3.1 修改点

 这里先说明修改点,在create_model里面,会从bert模型中获取output_layer层,然后用这一层做分类模型的fine-tuning(貌似加了一个偏置项),此外这里的probabilities就是test的结果。

对照与论文的

而在transformer_encoder模型中,有一个参数可以选择是否保留所有的层数,然后模型会自动选取最后一层,然后在对最后一层的第一个词进行维度上的处理。

所以只需要对这里的层数进行修改,就可以获得不同层的输出,求12层的平均的话也类似。

3.2 实验结果

附上实验结果x轴是层数,也就是选取哪一层的第一个[cls]作为分类的标准。y轴是accuracy,就是用分类正确的个数除以整体的个数,从结果可以看出,在这个数据集上,加上两层的transformer机制就可以使得准确率大幅提升,从第一层的0.1到第二层的0.8(test集上),而后面层的提升效果没有前面的显著。最后三层的eval和test的准确率分别是 [0.96,0.9675,0.96],[0.94,0.935,0.935]差别不是很大。此外,用12层的平均值进行预测和训练,预测值和平均值分别是0.95和0.92,与最后一层相差了1个百分点。也许是前面的几层的结果将整体的效果拉低了。

四、用COW+tfidf+LR进行无pre-train的中文文本分类预测

4.1 工具选择

      开始使用的是jieba进行分词去停用词,gensim构建字典,把每个句子当成bow模型,在进行tfidf转换。打算用scikilearn的lr包进行分类,但是发现gensim和scikilearn对数据存储的格式描述不一致。如果用gensim将句子转成tfidf,然后用scikilearn进行分类需要转换数据结构,后面为了方便,全套改成了scikilearn的。主要是调用sklearn.feature_extraction.text.TfidfVectorizer这个接口,官方文档有详细的说明,这里我是先用jieba进行了分词在去停用词的处理,在调用这个接口。也可以直接调用这个接口的tokenizer参数和stop_words参数,一步搞定。

4.2 参数设置

主要是LR的参数选择

 

4.3 结果

首先是分词和去停用词之后的样子

然后是校验集上的准确率是0.925 测试集上的准确率是0.895,和bert的最后一层的0.96和0.94比起来还是有差距的。

下图是我打印的BOW+TFIDF+LR测试集的分类结果,一共是200个样本,由于是随机抽样分布不是那么均匀,解读第一行举个例子,体育一共有17个样本,有16个分对,1个分错。

五。总结

本次实验的评价指标仅仅用了准确率一个指标,即分对的样本数除以总样本数。后面有空会补上其他的评估指标。从准确率上来看,bert较于传统的BOW+tfidf+LR模型 有着一定的优势。单单就bert模型来说,1层到2层的准确率提升最快,后面几层提升幅度没有前两层高,此外可以考虑添加l校验集oss的打印,可以看看在训练过程中是否出现过拟合情况。

小白第一次写博客,不足之处请多多包涵,如有错误或认识不全的地方,欢迎各位大神批评指正

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值