完整项目
文本分类(三)专栏主要是对Github优秀文本分类项目的解析,该文本分类项目,主要基于预训练语言模型,包括bert、bert + CNN/RNN/RCNN/DPCNN、ERNIE等,使用PyTorch实现。
本博客还讲解了一种预训练语言模型的通用方法,即使用transformers库,可以将本项目扩展为使用任意的预训练语言模型(包括:albert、xlnet、roberta,t5,gpt等,以及他们与各种深度学习模型的结合)。
目录
1. 项目特点
2. 数据集
3. 项目组织结构
4. transformers库
5. 使用方式
相比于文本分类(一)、(二),它主要有以下几个不同:
1)提供了一种不同的数据预处理方式。文本分类(一)中我们使用的是THUCNews完整数据集,每条数据都是完整的新闻,属于篇章分类;文本分类(二)、(三),我们使用的是THUCNews的一个子集,每条数据都是从新闻中抽取的标题,属于标题(短文本)分类。之前我们是提前把数据预处理好,存储为数组或tensor的格式,训练时再从文件中加载,适合数据量比较大的情况;现在我们预处理和训练同时进行,将数据预处理完接着进行训练,不需要存储为中间文件,适合数据量比较小的情况。
2)数据生成器:当数据比较大时,没办法一次性把数据全部加载到内存或显存中,此时我们可以使用数据生成器。训练时,不是把全部数据都加载到内存或显存中,而是用到哪一部分数据(某个batch),就用数据生成器生成该部分数据,只把这部分数据加载到内存或显存中,避免溢出。在文本分类(一)中,我们把预处理好的数据想封装为Dataset对象,再使用DataLoader进行加载(PyTorch内置);在文本分类(二)、(三)中,我们自定义数据生成/迭代器。
3)词典/字典构造方式:在文本分类(一)、(二)中我们需要首先构建词典或字典,再把文本中的词或字转变为词典或字典中的索引。在文本分类(三)中使用基于Bert的模型时,不需要手动构建词典或字典,需要下载所使用的Bert模型对应的词表,用Bert内置的分词工具对输入文本进行处理,内部会自动把文本转换为词表中的索引。
4)文本分类(二)中主要是基于character-level,文本以字为间隔进行分割,当然也提供了word-level的版本;文本分类(一)中主要基于word-level,文本以词为间隔进行分割(需要使用一些分词工具,如jieba)。文本分类(三)中Bert系列模型都是character-level,文本都是以字为间隔进行分割(汉语)。
5)命令行工具:文本分类(一)把项目所有相关的超参数都集中在一个文件中,若要修改配置,可以利用fire工具在命令行进行覆盖;文本分类(二)、(三)使用的是argparse工具,每个模块都有一个对应的配置类(包含该模块的超参数)和一个对应的模型定义类(超参定义和模型定义在一个文件中)。
6)使用模型:文本分类(一)和文本分类(二)所使用的模型差不多,都是一些基于深度学习的模型。不同在于,文本分类(二)的FastText模型增加了bi-gram、tri-gram特征,且增加了Transformer模型(利用transformer encoder进行分类)。文本分类(三)主要是基于预训练语言模型,如Bert和ERNIE,在对输入经过encoder编码后,取[CLS] token(输入序列最前面需要添加特定的[CLS] token表示序列开始)对应的最后一层编码向量(隐状态),再接全连接层进行分类;以及预训练语言模型和深度学习模型的结合,如Bert + CNN/RNN/RCNN/DPCNN,即取Bert最后一层所有的编码向量作为后续深度学习模型的输入,再进行分类。
上述是三个项目主要的不同,当然还有一些编码风格和一些具体细节的差异,之后的几篇博客,我会详细介绍。多解析一些相关项目,不仅可以加深我们对该领域的理解,还可以掌握一些不同的编码风格,可以让我们在编程时根据不同的情况有更多选择、更灵活,而且可以更容易的看懂别人优秀的开源代码。
在THUCNews数据集中抽取20w新闻标题,文本长度在20-30之间,一共10个类别,每个类别2万条。
类别:财经、房产、股票、教育、科技、社会、时政、体育、游戏、娱乐。
数据集划分:训练集18w(每个类别18,000条),验证集和测试集各1w(每个类别1000条)。
处理为.txt文件,格式如下:文本
(数据已处理好,可以直接使用)
1)ERNIE_pretain:包含了ERNIE模型的结构和预训练参数以及相关超参数配置文件以及ERNIE模型的词表文件百度网盘下载链接。
2)models:各个模型的定义以及各自超参数定义
3)pretrained_models/bert-base-chinese:包含bert模型的超参数配置文件config.json,bert模型结构及预训练参数文件pytorch_model.bin,以及词表文件vocab.txt(三者都可以提前下载,后续会详细说明)
4)THUCnews/data:存储训练集、验证集、测试集(处理成txt格式,每条数据一行,格式:文本
5)run.py:程序入口
6)train_eval.py:定义训练、验证、测试函数
7)utils.py:定义数据预处理和加载的函数
使用该项目需要安装transformers库,它极大的简化了在Pytorch中调用预训练语言模型(bert、albert、xlnet等)的流程,使我们可以轻易地将预训练语言模型和其他深度学习模型相结合,并像训练其他普通深度学习模型那样对其进行训练。该库的官方Github:https://github.com/huggingface/transformers.
transformers库的代码主要包含四部分(以下的xx可以替换为任意的预训练语言模型bert、gpt、xlnet、albert、t5、roberta等等,可以在官方Github上查看所有的预训练语言模型):
所有预训练模型的建模脚本(基于PyTorch),如modeling_bert.py、modeling_albert.py、modeling_xlnet.py等,对于分类任务来说,我们只需要关注其中的两个类,如modeling_bert.py中有BertModel类和BertForSequenceClassification类、modeling_albert.py中有AlbertModel类和AlbertForSequenceClassification类等,其他预训练模型一样都有两个和文本分类相关的类。
这两个类的区别是:
1)XXModel类产生的是相关预训练语言模型的输出,及encoder的输出(预训练语言模型可以看作是一个encoder),它后面可以接各种不同的下游任务(我们可以对其输出进行各种不同的自定义设置,文本分类任务只是其中一个),对于文本分类任务,可以把[cls]token对应的最后一层的编码向量再接全连接层进行分类。也可以基于最后一层所有的编码向量,后面接CNN、RNN等深度学习模型,并且可以自定义损失函数、优化器等,像训练普通深度学习模型一样对其训练(本项目采取的就是这种方式)。
2)XXForSequenceClassification类,其实就是把第一种情况([cls]token对应的最后一层的编码向量再接全连接层进行分类)进行了封装,整体作为一个分类模型,单纯提供分类服务,并且类内部定义了损失函数。接下来只需要自定义训练过程。(个人推荐使用XXModel类,更灵活,流程和训练普通深度学习模型几乎没什么差别)。当然,你也可以把后面的几种情况如基于最后一层所有的编码向量,后面接CNN、RNN等深度学习模型,进行封装,写一个XX_cnnForSequenceClassification或XX_lstmForSequenceClassification类放在modeling_xx.py中。只能供分类服务。
你可能注意到,还有一类建模脚本,modeling_tf__xx.py,这些是基于tensorflow的,我们暂时不关注。
所有预训练模型的配置文件,包含各个预训练模型的超参数配置。在建模脚本中建模之前,需要初始化模型配置。
所有预训练模型对应的切分工具,可以对输入文本进行切分,并且基于下载的词表(character-level),将文本转换为id序列。
optimization.py 定义了预训练语言模型的优化器(和普通深度学习模型有所差别)。
convert_xx_tf_checkpoing_to_pytorch.py定义了把保存为tf checkpoint格式的预训练参数转换为pytorch中格式的方法。
transformers库的使用:
1)首先导入相关的类:
Bert:
from transformers import BertModel, BertTokenizer,BertForSequenceClassification
Albert:
from transformers import AlbertModel, AlbertTokenizer,AlbertForSequenceClassification
2)定义模型:
Bert:
第一种方式(本项目所使用方式):
self.bert = BertModel.from_pretrained(config.bert_path)
self.bert = BertModel.from_pretrained('bert-base-chinese')
第二种方式:
self.bert1 = BertForSequenceClassification.from_pretrained(config.bert_path)
self.bert1 = BertForSequenceClassification.from_pretrained('Bert-base-chinese')
Albert:
第一种方式(本项目所使用方式):
self.albert = AlbertModel.from_pretrained(config.albert_path)
self.albert = AlbertModel.from_pretrained('albert-large-v2')
第二种方式:
self.albert1 = AlbertForSequenceClassification.from_pretrained(config.albert_path)
self.albert1 = AlbertForSequenceClassification.from_pretrained('albert-large-v2')
对于bert来说,config.bert_path的格式不是固定的(只要该路径下包含所选用模型版本对应的那三个文件即可),建立采用下面的路径命名方式,采用两级目录:config.bert_path='./pretrained_models/bert-base-chinese'(对于中文文本分类我们使用的是bert模型的bert-base-chinese版本,第一级目录命名为pretrained_models,我们可能使用多个不同版本的预训练模型,统一放在该目录下,第二级目录命名为所用预训练模型的版本名称),在该路径下中需要存储我们提前下好的三个文件:pytorch_model.bin,config.json,vocab.txt。至于如何下载相应的文件,可以分别进入modeling_bert.py、 tokenization_bert.py以及configuration_bert.py中进行查看,内部以字典的形式存储了各个文件的路径(作者把所有文件保存在了亚马逊云上),如下所示:
modeling_bert.py:
tokenization_bert.py
configuration_bert.py
拿到对应版本的三个文件的路径后,下载下来,保存在上述路径中,调用模型时,传入config.bert_path路径即可。注意下载下来的文件需要更名,不加任何前缀,词表文件为vocab.txt、模型文件pytorch_model.bin、配置文件config.json。当然,还有一种方式,注意到三个文件的键名都是一样的,即对应的预训练语言模型版本,可以在调用模型时,直接传入该键名(bert-base-chinese)(我没有试过,估计可能是基于键名,拿到对应的文件云链接,再进行使用,不如提前下好这种方式快,因此推荐第一种方式,提前下好存到指定位置)。
对于albert来说,config.albert_path的格式也不是固定的,和之前一样,建议采用这个命名方式:config.albert_path='./pretrained_models/albert-large-v2'(albert-large-v2是所使用的albert模型的版本号),在该路径下中需要存储我们提前下好的三个文件:pytorch_model.bin,config.json,vocab.txt。和bert的做法一样,分别进入modeling_albert.py、 tokenization_albert.py以及configuration_albert.py中进行查看,在三个字典中分别找到albert-large-v2对应的文件下载链接,将对应文件下载下来,并进行更名,不加任何前缀,然后调用模型传入config.albert_path路径即可。也可以直接传入键名'albert-large-v2'
3)使用模型
Bert:
第一种方式(本项目使用的方式):
outputs = self.bert(input_ids=context, attention_mask=mask, token_type_ids=None)
encoder_outputs,text_cls = outputs[0],outputs[1]
input_ids:就是把输入文本作切分后,基于下载的词表,转换为索引,得到的索引序列,句子头部要添加[CLS] token。
attention_mask:需要把一个batch中的数据填充为一个长度,方便并行计算。要对填充部分进行区分,即填充部分对应0,非填充部分对应1,得到一个和input_ids一样长的序列,值为0,1.(bert的多头注意力计算时,需要把填充部分token对应的点积结果加mask,置为很小的数)。不设置该参数的话,默认全为1,即都是非填充的。
token_type_ids:文本分类任务用不到,对于问答这种任务,输入是句子对,需要区分输入序列中哪些token属于句子A,哪些属于句子B。属于句子A的token对应0,属于句子B的token对应1,得到一个和input_ids一样长的序列,值为0,1.两个句子间用[sep] token分隔.不设置该参数的话,默认全为0,即所有token都属于句子A(只有一个输入句子,如文本分类这种任务)。
text_cls:[CLS] token对应的最后一层编码向量。(batch,hidden_size)
encoder_outputs:encoder最后一层所有的编码向量。(batch,seq_len,hidden_size)
第二种方式:
outputs = self.bert1(input_ids=context, attention_mask=mask, token_type_ids=None,labels=labels)
loss,logits = outputs[0],outputs[1]
input_ids:就是把输入文本作切分后,基于下载的词表,转换为索引,得到的索引序列,句子头部要添加[CLS] token。
attention_mask:需要把一个batch中的数据填充为一个长度,方便并行计算。要对填充部分进行区分,即填充部分对应0,非填充部分对应1,得到一个和input_ids一样长的序列,值为0,1.(bert的多头注意力计算时,需要把填充部分token对应的点积结果加mask,置为很小的数)。不设置该参数的话,默认全为1,即都是非填充的。
token_type_ids:文本分类任务用不到,对于问答这种任务,输入是句子对,需要区分输入序列中哪些token属于句子A,哪些属于句子B。属于句子A的token对应0,属于句子B的token对应1,得到一个和input_ids一样长的序列,值为0,1.两个句子间用[sep] token分隔.不设置该参数的话,默认全为0,即所有token都属于句子A(只有一个输入句子,如文本分类这种任务)。
labels:输入文本对应的标签(0~num_classes-1)。
loss:计算的损失。当num_classes>1时,计算的是分类损失(交叉熵损失);当num_classes=1时,计算的是回归损失(均方损失)
logits:模型的输出。
初始化切分工具,self.bert_path为下载的那三个文件所在的路径。
self.tokenizer = BertTokenizer.from_pretrained(self.bert_path)
self.tokenizer = BertTokenizer.from_pretrained('Bert-base-chinese')
注意预处理的时候,除了input_ids,别忘了构建 attention_mask;对于句子对任务还要构建token_type_ids。
from transformers.optimization import BertAdam
需要使用预训练模型专用的优化器。其他训练流程和训练普通深度学习模型没什么差别。
上述主要以bert为例进行讲解,其他所有的预训练语言模型的使用都是类似地(可能稍有不同,如Roberta没有token_type_ids参数,使用时可以查一下文档),可以很方便的进行迁移。
其实,除了transformers库,还有一个pytorch_pretrained_bert库,不过transformers库包含所有的预训练语言模型,而pytorch_pretrained_bert库仅包含几个预训练语言模型。所以推荐使用transformers库。二者的原理差不多,使用方式略有不同,如BertModel的参数和返回值有些不同,预训练模型的存储路径要求不同等,可以具体查文档。不再赘述,但还是推荐transformers库,更强大,模型更多。
1)下载好预训练语言模型相关的文件(配置文件.json、模型文件.bin,词表文件vocab.txt),存储在相应的位置。就可以在程序中使用transformers库中的方法加载使用了。
2)训练并测试: