BERT实战——(1)文本分类
BERT实战——(1)文本分类
引言
我们将展示如何使用 🤗 Transformers代码库中的模型来解决文本分类任务,任务来源于GLUE Benchmark.
任务介绍
本质就是分类问题,比如对一句话的情感极性分类(正向1或负向-1或中性0):
输入:这部电影真不错! |
主要分为以下几个部分:
- 数据加载
- 数据预处理
- 微调预训练模型:使用transformer中的
Trainer
接口对预训练模型进行微调; - 超参数搜索
前期准备
安装以下两个库:
pip install datasets transformers |
数据加载
数据集介绍
我们使用的是GLUE榜单的数据集:
GLUE_TASKS = ["cola", "mnli", "mnli-mm", "mrpc", "qnli", "qqp", "rte", "sst2", "stsb", "wnli"] |
GLUE榜单包含了9个句子级别的分类任务,分别是:
分类任务 | 任务目标 |
---|---|
CoLA (Corpus of Linguistic Acceptability) | 鉴别一个句子是否语法正确. |
MNLI (Multi-Genre Natural Language Inference) | 给定一个假设,判断另一个句子与该假设的关系:entails, contradicts 或者 unrelated。 |
MRPC (Microsoft Research Paraphrase Corpus) | 判断两个句子是否互为paraphrases改写. |
QNLI (Question-answering Natural Language Inference) | 判断第2句是否包含第1句问题的答案。 |
QQP (Quora Question Pairs2) | 判断两个问句是否语义相同。 |
RTE (Recognizing Textual Entailment) | 判断一个句子是否与假设成entail关系。 |
SST-2 (Stanford Sentiment Treebank) | 判断一个句子的情感正负向. |
STS-B (Semantic Textual Similarity Benchmark) | 判断两个句子的相似性(分数为1-5分)。 |
WNLI (Winograd Natural Language Inference) | 判断一个有匿名代词的句子和一个有该代词被替换的句子是否包含。Determine if a sentence with an anonymous pronoun and a sentence with this pronoun replaced are entailed or not. |
加载数据
下面介绍两种使用🤗 Datasets库来加载数据load_dataset
的方法,主要参考官方文档:
- 加载官方库的数据;
- 加载自己的数据或来自网络的数据:
- csv格式;
- json格式;
- txt格式
- pandas.DataFrame格式。
加载官方库的数据
除了mnli-mm
以外,其他任务都可以直接通过任务名字进行加载。数据加载之后会自动缓存。
from datasets import load_dataset |
这个datasets
对象本身是一种DatasetDict
数据结构. 对于训练集、验证集和测试集,只需要使用对应的key(train,validation,test)
即可得到相应的数据。
给定一个数据切分的key(train、validation或者test)和下标即可查看数据:dataset["train"][0]
下面的函数将从数据集里随机选择几个例子进行展示:
import datasets |
加载自己的数据或来自网络的数据
csv格式
data_files为本地文件名或网络数据链接,如果没有用字典指定训练集、验证集、测试集,默认都为训练集。
from datasets import load_dataset |
json格式
情况1:json数据不包括嵌套的json,比如:
{"a": 1, "b": 2.0, "c": "foo", "d": false} |
此时可以直接加载数据:
from datasets import load_dataset |
情况2:json数据包括嵌套的json,比如:
{"version": "0.1.0", |
此时需要使用 field
参数指定哪个字段包含数据集:
from datasets import load_dataset |
txt格式
from datasets import load_dataset |
dict格式
my_dict = {'id': [0, 1, 2], |
pandas.DataFrame格式
from datasets import Dataset |
数据预处理
在将数据喂入模型之前,我们需要对数据进行预处理。之前我们已经知道了数据预处理的基本流程:
- 分词;
- 转化成对应任务输入模型的格式;
Tokenizer
用于上面两步数据预处理工作:Tokenizer
首先对输入进行tokenize,然后将tokens转化为预模型中需要对应的token ID,再转化为模型需要的输入格式。
初始化Tokenizer
使用AutoTokenizer.from_pretrained
方法根据模型文件实例化tokenizer,这样可以确保:
- 得到一个与预训练模型一一对应的tokenizer。
- 使用指定的模型checkpoint对应的tokenizer时,同时下载了模型需要的词表库vocabulary,准确来说是tokens vocabulary。
from transformers import AutoTokenizer |
注意:use_fast=True
要求tokenizer必须是transformers.PreTrainedTokenizerFast类型,以便在预处理的时候需要用到fast tokenizer的一些特殊特性(比如多线程快速tokenizer)。如果对应的模型没有fast tokenizer,去掉这个选项即可。
几乎所有模型对应的tokenizer都有对应的fast tokenizer,可以在模型tokenizer对应表里查看所有预训练模型对应的tokenizer所拥有的特点。
Tokenizer分词示例
预训练的Tokenizer通常包含了分单句和分一对句子的函数。如:
#分单句(一个batch) |
#分一对句子 |
我们之前也提到如果是自己预训练的tokenizers可以通过以下方式为tokenizers增加处理一对句子的方法:
from tokenizers.processors import TemplateProcessing |
转化成对应任务输入模型的格式
tokenizer有不同的返回取决于选择的预训练模型,tokenizer和预训练模型是一一对应的,更多信息可以在这里进行学习。
不同数据和对应的数据格式,为了预处理我们的数据,定义下面这个dict,以便分别用tokenizer处理输入是单句或句子对的情况。
task_to_keys = { |
将预处理的代码放到一个函数中:
def preprocess_function(examples): |
前面我们已经展示了tokenizer处理一个小batch的案例。dataset类直接用索引就可以取对应下标的句子1和句子2,因此上面的预处理函数既可以处理单个样本,也可以对多个样本进行处理。如果输入是多个样本,那么返回的是一个list:
preprocess_function(dataset['train'][:5]) |
接下来使用map函数对数据集datasets里面三个样本集合的所有样本进行预处理,将预处理函数prepare_train_features应用到(map)所有样本上。
encoded_dataset = dataset.map(preprocess_function, batched=True) |
返回的结果会自动被缓存,避免下次处理的时候重新计算(但是也要注意,如果输入有改动,可能会被缓存影响!)。
datasets库函数会对输入的参数进行检测,判断是否有变化,如果没有变化就使用缓存数据,如果有变化就重新处理。但如果输入参数不变,想改变输入的时候,最好清理调这个缓存(使用
load_from_cache_file=False
参数)。另外,上面使用到的batched=True
这个参数是tokenizer的特点,这会使用多线程同时并行对输入进行处理。
微调预训练模型
数据已经准备好了,我们需要下载并加载预训练模型,然后微调预训练模型。
加载预训练模型
既然是做seq2seq任务,那么需要一个能解决这个任务的模型类。我们使用AutoModelForSequenceClassification
这个类。
和tokenizer相似,from_pretrained
方法同样可以帮助下载并加载模型,同时也会对模型进行缓存,也可以填入一个包括模型相关文件的文件夹(比如自己预训练的模型),这样会从本地直接加载。理论上可以使用各种各样的transformer模型(模型面板),解决任何文本分类分类任务。
需要注意的是:STS-B是一个回归问题,MNLI是一个3分类问题:
from transformers import AutoModelForSequenceClassification, TrainingArguments, Trainer |
由于我们的任务是文本分类任务,而我们加载的是预训练语言模型,所以会提示我们加载模型的时候扔掉了一些不匹配的神经网络参数(比如:预训练语言模型的神经网络head被扔掉了,同时随机初始化了文本分类的神经网络head)。
设定训练参数
为了能够得到一个Trainer
训练工具,我们还需要训练的设定/参数 TrainingArguments
。这个训练设定包含了能够定义训练过程的所有属性。
batch_size = 16 |
定义评估方法
还有一件重要的事,我们需要选择一个合适的评价指标引导模型进行微调。
我们使用🤗 Datasets库来加载评价指标计算库load_metric
。metic是datasets.Metric
的一个实例:
from datasets import load_metric |
直接调用metric的compute
方法,传入labels
和predictions
即可得到metric的值:
import numpy as np |
每一个文本分类任务所对应的metic有所不同,一定要将metric和任务对齐,具体如下:
GLUE benchmark分类任务 | 评价指标 |
---|---|
CoLA | Matthews Correlation Coefficient |
MNLI (matched or mismatched) | Accuracy |
MRPC | Accuracy and F1 score |
QNLI | Accuracy |
QQP | Accuracy and F1 score |
RTE | Accuracy |
SST-2 | Accuracy |
STS-B | Pearson Correlation Coefficient and Spearman's_Rank_Correlation_Coefficient |
WNLI | Accuracy |
为Trainer
定义各个任务的评估方法compute_metrics
:
from datasets import load_metric |
开始训练
将数据/模型/参数传入Trainer
即可:
validation_key = "validation_mismatched" if task == "mnli-mm" else "validation_matched" if task == "mnli" else "validation" |
开始训练:
trainer.train() |
模型评估
训练完成后对模型进行评估:
trainer.evaluate() |
超参数搜索
Trainer
还支持超参搜索,使用optuna or Ray Tune代码库。
需要安装以下两个依赖:
pip install optuna |
超参搜索时,Trainer
将会返回多个训练好的模型,所以需要传入一个定义好的模型从而让Trainer
可以不断重新初始化该传入的模型:
def model_init(): |
和之前调用 Trainer
类似:
trainer = Trainer( |
调用方法hyperparameter_search
进行超参数搜索。
注意,这个过程可能很久,可以先用部分数据集进行超参搜索,再进行全量训练。 比如使用1/10的数据进行搜索(利用n_trials
设置):
best_run = trainer.hyperparameter_search(n_trials=10, direction="maximize") |
hyperparameter_search
会返回效果最好的模型相关的参数best_run:
将Trainner
设置为搜索到的最好参数best_run,再对全部数据进行训练:
for n, v in best_run.hyperparameters.items(): |
上传模型到huggingface
学习如何上传模型到🤗 Model Hub。别人也可以用你上传的模型,通过网络直接用模型名字就能直接下载上传的模型。