最近在重温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的打印,可以看看在训练过程中是否出现过拟合情况。
小白第一次写博客,不足之处请多多包涵,如有错误或认识不全的地方,欢迎各位大神批评指正