精通 Transformers(二)(2)https://developer.aliyun.com/article/1510691
用自定义数据集对多类分类进行 BERT 微调
在本节中,我们将对土耳其 BERT,即BERTurk,进行多类分类下游任务的微调,其自定义数据集包含从土耳其报纸编制的七个类别。我们将从获取数据集开始。或者,您可以在本书的 GitHub 存储库中找到它,或者从www.kaggle.com/savasy/ttc4900 获取它:
- 首先,在 Python 笔记本中运行以下代码以获取数据:
!wget https://raw.githubusercontent.com/savasy/TurkishTextClassification/master/TTC4900.csv
- 从加载数据开始:
import pandas as pd data= pd.read_csv("TTC4900.csv") data=data.sample(frac=1.0, random_state=42)
- 让我们用
id2label和label2id组织 ID 和标签,以使模型弄清楚哪个 ID 指代哪个标签。我们还将NUM_LABELS的数量传递给模型,以指定 BERT 模型顶部薄分类头层的大小:
labels=["teknoloji","ekonomi","saglik","siyaset","kultur","spor","dunya"] NUM_LABELS= len(labels) id2label={i:l for i,l in enumerate(labels)} label2id={l:i for i,l in enumerate(labels)} data["labels"]=data.category.map(lambda x: label2id[x.strip()]) data.head()
- 输出如下:
图 5.5 – 文本分类数据集 – TTC 4900 - 让我们使用 pandas 对象计算并绘制类别数量:
data.category.value_counts().plot(kind='pie')
- 如下图所示,数据集的类别已经被相当分配:
图 5.6 – 类别分布 - 以下执行实例化一个序列分类模型,带有标签数量(
7)、标签 ID 映射和一个土耳其 BERT 模型(dbmdz/bert-base-turkish-uncased),即 BERTurk。要检查这一点,请执行以下操作:
>>> model
- 输出将是模型的摘要,太长了,无法在此处显示。相反,让我们通过以下代码关注最后一层:
(classifier): Linear(in_features=768, out_features=7, bias=True)
- 你可能已经注意到我们没有选择
DistilBert,因为没有预先训练好的uncasedDistilBert适用于土耳其语:
from transformers import BertTokenizerFast tokenizer = BertTokenizerFast.from_pre-trained("dbmdz/bert-base-turkish-uncased", max_length=512) from transformers import BertForSequenceClassification model = BertForSequenceClassification.from_pre-trained("dbmdz/bert-base-turkish-uncased", num_labels=NUM_LABELS, id2label=id2label, label2id=label2id) model.to(device)
- 现在,让我们准备训练(%50)、验证(%25)和测试(%25)数据集,如下所示:
SIZE= data.shape[0] ## sentences train_texts= list(data.text[:SIZE//2]) val_texts= list(data.text[SIZE//2:(3*SIZE)//4 ]) test_texts= list(data.text[(3*SIZE)//4:]) ## labels train_labels= list(data.labels[:SIZE//2]) val_labels= list(data.labels[SIZE//2:(3*SIZE)//4]) test_labels= list(data.labels[(3*SIZE)//4:]) ## check the size len(train_texts), len(val_texts), len(test_texts) (2450, 1225, 1225)
- 以下代码将三个数据集的句子进行标记化,并将它们的标记转换为整数(
input_ids),然后将它们输入 BERT 模型:
train_encodings = tokenizer(train_texts, truncation=True, padding=True) val_encodings = tokenizer(val_texts, truncation=True, padding=True) test_encodings = tokenizer(test_texts, truncation=True, padding=True)
- 我们已经实现了
MyDataset类(请参阅第 14 页)。该类继承自抽象的Dataset类,通过重写__getitem__和__len__()方法来使用任何数据加载器返回数据集的项目和大小,分别预期返回:
train_dataset = MyDataset(train_encodings, train_labels) val_dataset = MyDataset(val_encodings, val_labels) test_dataset = MyDataset(test_encodings, test_labels)
- 由于我们有一个相对较小的数据集,我们将保持批处理大小为
16。请注意,TrainingArguments的其他参数几乎与之前的情感分析实验相同:
from transformers import TrainingArguments, Trainer training_args = TrainingArguments( output_dir='./TTC4900Model', do_train=True, do_eval=True, num_train_epochs=3, per_device_train_batch_size=16, per_device_eval_batch_size=32, warmup_steps=100, weight_decay=0.01, logging_strategy='steps', logging_dir='./multi-class-logs', logging_steps=50, evaluation_strategy="steps", eval_steps=50, save_strategy="epoch", fp16=True, load_best_model_at_end=True )
- 情感分析和文本分类是相同评估指标的对象;即宏平均的宏平均 F1、精度和召回率。因此,我们不会重新定义
compute_metric()函数。以下是实例化Trainer对象的代码:
trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=val_dataset, compute_metrics= compute_metrics )
- 最后,让我们开始训练过程:
trainer.train()
- 输出如下:
图 5.7 – 文本分类的 Trainer 类的输出 - 要检查训练好的模型,我们必须在三个数据集拆分上评估微调的模型,如下所示。我们的最佳模型是在步骤 300 微调的,损失为 0.28012:
q=[trainer.evaluate(eval_dataset=data) for data in [train_dataset, val_dataset, test_dataset]] pd.DataFrame(q, index=["train","val","test"]).iloc[:,:5]
- 输出如下:
图 5.8 – 文本分类模型在训练/验证/测试数据集上的性能
分类准确率约为 92.6,而 F1 宏平均约为 92.5。在文献中,许多方法都在这个土耳其基准数据集上进行了测试。它们大多采用 TF-IDF 和线性分类器、word2vec 嵌入,或基于 LSTM 的分类器,最好的 F1 也达到了 90.0。与这些方法相比,除了 transformer,微调的 BERT 模型表现更佳。 - 与任何其他实验一样,我们可以通过 TensorBoard 跟踪实验:
%load_ext tensorboard %tensorboard --logdir multi-class-logs/
- 让我们设计一个运行推理模型的函数。如果你想看到真实标签而不是 ID,你可以使用我们模型的
config对象,如下面的predict函数所示:
def predict(text): inputs = tokenizer(text, padding=True, truncation=True, max_length=512, return_tensors="pt").to("cuda") outputs = model(**inputs) probs = outputs[0].softmax(1) return probs, probs.argmax(),model.config.id2label[probs.argmax().item()]
- 现在,我们准备调用
predict函数进行文本分类推理。以下代码对一个关于足球队的句子进行分类:
text = "Fenerbahçeli futbolcular kısa paslarla hazırlık çalışması yaptılar" predict(text) (tensor([[5.6183e-04, 4.9046e-04, 5.1385e-04, 9.9414e-04, 3.4417e-04, 9.9669e-01, 4.0617e-04]], device='cuda:0', grad_fn=<SoftmaxBackward>), tensor(5, device='cuda:0'), 'spor')
- 正如我们所看到的,该模型正确地预测了句子为体育(
spor)。现在,是时候保存模型并使用from_pre-trained()函数重新加载它了。以下是代码:
model_path = "turkish-text-classification-model" trainer.save_model(model_path) tokenizer.save_pre-trained(model_path)
- 现在,我们可以重新加载已保存的模型,并借助
pipeline类进行推理:
model_path = "turkish-text-classification-model" from transformers import pipeline, BertForSequenceClassification, BertTokenizerFast model = BertForSequenceClassification.from_pre-trained(model_path) tokenizer= BertTokenizerFast.from_pre-trained(model_path) nlp= pipeline("sentiment-analysis", model=model, tokenizer=tokenizer)
- 您可能已经注意到任务的名称是
sentiment-analysis。这个术语可能令人困惑,但这个参数实际上会返回最终的TextClassificationPipeline。让我们运行 pipeline:
>>> nlp("Sinemada hangi filmler oynuyor bugün") [{'label': 'kultur', 'score': 0.9930670261383057}] >>> nlp("Dolar ve Euro bugün yurtiçi piyasalarda yükseldi") [{'label': 'ekonomi', 'score': 0.9927696585655212}] >>> nlp("Bayern Münih ile Barcelona bugün karşı karşıya geliyor. Maçı İngiliz hakem James Watts yönetecek!") [{'label': 'spor', 'score': 0.9975664019584656}]
- 这就是我们的模型!它已经成功预测了。
到目前为止,我们已经实现了两个单句任务;即情感分析和多类分类。在下一节,我们将学习如何处理句对输入,以及如何使用 BERT 设计回归模型。
为句对回归微调 BERT 模型
回归模型被认为是用于分类的,但最后一层只包含一个单元。这不是通过 softmax logistic 回归进行处理,而是进行了归一化。为了指定模型并在顶部放置单单元头层,我们可以直接通过BERT.from_pre-trained()方法传递num_labels=1参数,或者通过Config对象传递此信息。最初,这需要从预训练模型的config对象中复制,如下所示:
from transformers import DistilBertConfig, DistilBertTokenizerFast, DistilBertForSequenceClassification model_path='distilbert-base-uncased' config = DistilBertConfig.from_pre-trained(model_path, num_labels=1) tokenizer = DistilBertTokenizerFast.from_pre-trained(model_path) model = \ DistilBertForSequenceClassification.from_pre-trained(model_path, config=config)
嗯,我们的预训练模型由于num_labels=1参数具有单单元头层。现在,我们准备用我们的数据集对模型进行微调。在这里,我们将使用语义文本相似性基准(STS-B),它是从各种内容中引用的句对集合,如新闻标题等。每对句子都被注释了从 1 到 5 的相似度分数。我们的任务是微调 BERT 模型以预测这些分数。我们将遵循文献,使用皮尔逊/斯皮尔曼相关系数评估模型。让我们开始吧:
- 以下代码加载了数据。原始数据被分成了三部分。然而,测试分组没有标签,因此我们可以将验证数据分为两部分,如下所示:
import datasets from datasets import load_dataset stsb_train= load_dataset('glue','stsb', split="train") stsb_validation = load_dataset('glue','stsb', split="validation") stsb_validation=stsb_validation.shuffle(seed=42) stsb_val= datasets.Dataset.from_dict(stsb_validation[:750]) stsb_test= datasets.Dataset.from_dict(stsb_validation[750:])
- 让我们通过 pandas 将
stsb_train训练数据整理整齐:
pd.DataFrame(stsb_train)
- 训练数据如下所示:
图 5.9 – STS-B 训练数据集 - 运行以下代码以检查三个集合的形状:
stsb_train.shape, stsb_val.shape, stsb_test.shape ((5749, 4), (750, 4), (750, 4))
- 运行以下代码对数据集进行分词:
enc_train = stsb_train.map(lambda e: tokenizer( e['sentence1'],e['sentence2'], padding=True, truncation=True), batched=True, batch_size=1000) enc_val = stsb_val.map(lambda e: tokenizer( e['sentence1'],e['sentence2'], padding=True, truncation=True), batched=True, batch_size=1000) enc_test = stsb_test.map(lambda e: tokenizer( e['sentence1'],e['sentence2'], padding=True, truncation=True), batched=True, batch_size=1000)
- 分词器使用
[SEP]分隔符合并两个句子,并为句对生成单个input_ids和一个attention_mask,如下所示:
pd.DataFrame(enc_train)
- 输出如下:
from transformers import TrainingArguments, Trainer training_args = TrainingArguments( output_dir='./stsb-model', do_train=True, do_eval=True, num_train_epochs=3, per_device_train_batch_size=32, per_device_eval_batch_size=64, warmup_steps=100, weight_decay=0.01, logging_strategy='steps', logging_dir='./logs', logging_steps=50, evaluation_strategy="steps", save_strategy="epoch", fp16=True, load_best_model_at_end=True )
- 当前回归任务与以前的分类任务之间的另一个重要区别是
compute_metrics的设计。在这里,我们的评估指标将基于皮尔逊相关系数和斯皮尔曼秩相关,遵循文献中提供的通用做法。我们还提供了均方误差(MSE)、均方根误差(RMSE)和平均绝对误差(MAE)等常用的度量标准,特别是对于回归模型:
import numpy as np from scipy.stats import pearsonr from scipy.stats import spearmanr def compute_metrics(pred): preds = np.squeeze(pred.predictions) return {"MSE": ((preds - pred.label_ids) ** 2).mean().item(), "RMSE": (np.sqrt (( (preds - pred.label_ids) ** 2).mean())).item(), "MAE": (np.abs(preds - pred.label_ids)).mean().item(), "Pearson" : pearsonr(preds,pred.label_ids)[0], "Spearman's Rank":spearmanr(preds,pred.label_ids)[0] }
- 现在,让我们实例化
Trainer对象:
trainer = Trainer( model=model, args=training_args, train_dataset=enc_train, eval_dataset=enc_val, compute_metrics=compute_metrics, tokenizer=tokenizer )
- 运行训练,像这样:
train_result = trainer.train()
- 输出如下:
图 5.11 – 文本回归的训练结果 - 计算的最佳验证损失为
0.544973,在步骤450。让我们在该步骤评估最佳检查点模型,如下所示:
q=[trainer.evaluate(eval_dataset=data) for data in [enc_train, enc_val, enc_test]] pd.DataFrame(q, index=["train","val","test"]).iloc[:,:5]
- 输出如下:
图 5.12 – 训练/验证/测试数据集上的回归性能
在测试数据集上,皮尔逊和斯皮尔曼相关分数分别约为 87.54 和 87.28。我们没有得到 SoTA 结果,但基于 GLUE Benchmark 排行榜,我们得到了一个可比较的 STS-B 任务结果。请查看排行榜! - 现在我们准备好进行推断模型了。让我们来看以下两个意思相同的句子,并将它们传递给模型:
s1,s2="A plane is taking off.","An air plane is taking off." encoding = tokenizer(s1,s2, return_tensors='pt', padding=True, truncation=True, max_length=512) input_ids = encoding['input_ids'].to(device) attention_mask = encoding['attention_mask'].to(device) outputs = model(input_ids, attention_mask=attention_mask) outputs.logits.item() OUTPUT: 4.033723831176758
- 以下代码使用了负面句对,这意味着句子在语义上是不同的:
s1,s2="The men are playing soccer.","A man is riding a motorcycle." encoding = tokenizer("hey how are you there","hey how are you", return_tensors='pt', padding=True, truncation=True, max_length=512) input_ids = encoding['input_ids'].to(device) attention_mask = encoding['attention_mask'].to(device) outputs = model(input_ids, attention_mask=attention_mask) outputs.logits.item() OUTPUT: 2.3579328060150146
- 最后,我们将保存模型,如下所示:
model_path = "sentence-pair-regression-model" trainer.save_model(model_path) tokenizer.save_pre-trained(model_path)
干得好!我们可以祝贺自己,因为我们成功完成了三项任务:情感分析、多类别分类和句对回归。
利用 run_glue.py 对模型进行微调
我们目前已经使用原生 PyTorch 和 Trainer 类从头设计了一个微调架构。HuggingFace 社区还提供了另一个强大的脚本,称为 run_glue.py,用于 GLUE 基准测试和 GLUE 类似的分类下游任务。这个脚本可以为我们处理和组织整个训练/验证过程。如果你想进行快速原型设计,应该使用这个脚本。它可以微调 HuggingFace hub 上的任何预训练模型。我们也可以用任何格式的自己的数据来提供给它。
请转到以下链接获取脚本并了解更多信息:github.com/huggingface/transformers/tree/master/examples。
该脚本可以执行九种不同的 GLUE 任务。通过该脚本,我们可以做到目前为止我们使用Trainer类所做的一切。任务名称可以是以下 GLUE 任务之一:cola、sst2、mrpc、stsb、qqp、mnli、qnli、rte或wnli。
以下是微调模型的脚本方案:
export TASK_NAME= "My-Task-Name" python run_glue.py \ --model_name_or_path bert-base-cased \ --task_name $TASK_NAME \ --do_train \ --do_eval \ --max_seq_length 128 \ --per_device_train_batch_size 32 \ --learning_rate 2e-5 \ --num_train_epochs 3 \ --output_dir /tmp/$TASK_NAME/
社区提供了另一个名为run_glue_no_trainer.py的脚本。与原始脚本的主要区别在于,这个无 Trainer 的脚本给了我们更多改变优化器选项或添加任何自定义的机会。
总结
在本章中,我们讨论了如何针对任何文本分类的下游任务对预训练模型进行微调。我们使用情感分析、多类别分类和句子对分类(具体而言,句子对回归)对模型进行了微调。我们使用了一个著名的 IMDb 数据集和我们自己的自定义数据集来训练模型。虽然我们利用了Trainer类来处理训练和微调过程的复杂性,但我们学会了如何使用原生库从头开始训练,以了解transformers库中的前向传播和反向传播。总而言之,我们讨论并进行了使用 Trainer 进行微调单句分类、使用原生 PyTorch 进行情感分类、单句多类别分类以及微调句子对回归。
在下一章中,我们将学习如何针对任何标记分类的下游任务(如词性标注或命名实体识别)对预训练模型进行微调。
第六章:为标记分类对语言模型进行微调
在本章中,我们将学习为标记分类对语言模型进行微调。本章探讨了诸如命名实体识别(NER)、词性(POS)标注和问答(QA)等任务。我们将学习如何将特定语言模型微调用于此类任务。我们将更多地关注 BERT,而不是其他语言模型。您将学习如何使用 BERT 应用 POS、NER 和 QA。您将熟悉这些任务的理论细节,如它们各自的数据集以及如何执行它们。完成本章后,您将能够使用 Transformers 执行任何标记分类。
在本章中,我们将为以下任务微调 BERT:为 NER 和 POS 等标记分类问题微调 BERT,为 NER 问题微调语言模型,并将 QA 问题视为起始/终止标记分类。
本章将涵盖以下主题:
- 介绍标记分类
- 为 NER 进行语言模型微调
- 使用标记分类进行问答
技术要求
我们将使用 Jupyter Notebook 运行我们的编码练习,并且需要安装 Python 3.6+ 和以下软件包:
sklearntransformers 4.0+数据集seqeval
所有带有编码练习的笔记本都将在以下 GitHub 链接中提供:github.com/PacktPublishing/Mastering-Transformers/tree/main/CH06。
查看以下链接以查看实际代码视频:bit.ly/2UGMQP2
介绍标记分类
将标记序列中的每个标记分类的任务称为标记分类。该任务要求特定模型能够将每个标记分类到一个类别中。POS 和 NER 是这一标准中最知名的两个任务。然而,QA 也是另一个属于这一类别的重要 NLP 任务。我们将在以下章节讨论这三个任务的基础知识。
理解 NER
在标记分类类别中一个著名的任务是 NER - 将每个标记识别为实体或非实体,并识别每个检测到的实体的类型。例如,文本可以同时包含多个实体 - 人名、地名、组织名和其他类型的实体。以下文本是 NER 的明显示例:
乔治·华盛顿是美利坚合众国的总统之一。
乔治·华盛顿是一个人名,而美利坚合众国是一个地名。序列标注模型应该能够以标签的形式标记每个单词,每个标签都包含有关该标签的信息。BIO 的标签是标准 NER 任务中通用的标签。
以下表格是标签及其描述的列表:
表 1 – BIOS 标签及其描述表
从这个表格可以看出,B 表示标记的开始,I 表示标记的内部,而 O 则表示实体的外部。这就是为什么这种类型的标注被称为 BIO。例如,前面显示的句子可以使用 BIO 进行标注:
[B-PER|George] [I-PER|Washington] [O|is] [O|one] [O|the] [O|presidents] [O|of] [B-LOC|United] [I-LOC|States] [I-LOC|of] [I-LOC|America] [O|.]
因此,序列必须以 BIO 格式进行标记。一个样本数据集可以使用如下格式:
图 6.1 – CONLL2003 数据集
除了我们见过的 NER 标签外,该数据集还包含了 POS 标签
理解 POS 标记
POS 标记,或语法标记,是根据给定文本中的各自词的词性对其进行标注。举个简单的例子,在给定文本中,识别每个词的角色,如名词、形容词、副词和动词都被认为是词性标注。然而,从语言学角度来看,除了这四种角色外还有很多其他角色。
在 POS 标签的情况下,有各种变化,但是宾州树库的 POS 标签集是最著名的之一。下面的截图显示了这些角色的摘要和相应的描述:
图 6.2 – 宾州树库 POS 标签
POS 任务的数据集如 图 6.1 所示进行了标注。
这些标签的标注在特定的 NLP 应用中非常有用,是许多其他方法的基石之一。Transformers和许多先进模型在其复杂的结构中某种程度上能理解单词之间的关系。
理解 QA
QA 或阅读理解任务包括一组阅读理解文本,并相应地提出问题。这个范围内的示例数据集包括 SQUAD 或 斯坦福问答数据集。该数据集由维基百科文本和关于它们提出的问题组成。答案以原始维基百科文本的片段形式给出。
以下截图显示了这个数据集的一个示例:
图 6.3 – SQUAD 数据集示例
突出显示的红色部分是答案,每个问题的重要部分用蓝色突出显示。要求一个良好的 NLP 模型按照问题对文本进行分割,这种分割可以通过序列标注的形式进行。模型会将答案的开始和结束部分标记为答案的起始和结束部分。
到目前为止,你已经学会了现代 NLP 序列标注任务的基础知识,如 QA、NER 和 POS。在接下来的部分,你将学习如何对这些特定任务进行 BERT 微调,并使用 datasets 库中相关的数据集。
精通 Transformers(二)(4)https://developer.aliyun.com/article/1510694