精通Transformer——为标记分类微调语言模型

210 阅读23分钟

在本章中,我们将学习如何为标记分类任务微调语言模型。诸如命名实体识别(NER)、词性标注(POS)和问答(QA)等任务将在本章中进行探讨。我们将学习如何在这些任务上微调特定的语言模型,重点将放在BERT模型上。你将学会如何使用BERT应用POS、NER和QA。你将熟悉这些任务的理论细节,例如各自的数据集及如何执行这些任务。完成本章后,你将能够使用Transformers执行任何标记分类任务。

本章中,我们将针对以下任务微调BERT:为标记分类问题(如NER)微调BERT,针对NER问题微调语言模型,并将QA问题视为开始/停止标记分类。

本章将涵盖以下主题:

  • 标记分类简介
  • 为NER任务微调语言模型
  • 使用标记分类进行问答
  • 多任务问答

技术要求

我们将使用Jupyter Notebook来运行编码练习,Python 3.6+和以下软件包需要安装:

  • sklearn
  • transformers 4.0+
  • datasets
  • seqeval

所有包含编码练习的笔记本可以在以下GitHub链接中找到:github.com/PacktPublis…

标记分类简介

标记分类任务是指对标记序列中的每个标记进行分类的任务。该任务要求模型将每个标记分类到一个类别中。词性标注(POS)和命名实体识别(NER)是这个标准中最知名的任务之一。然而,问答(QA)也是一个适合这个类别的主要自然语言处理(NLP)任务。我们将在以下部分讨论这三种任务的基础知识。需要注意的是,NER、POS和QA等任务可以从两个角度来看待:一种是用相应的类别对文本的现有上下文进行标记,另一种是文本生成。

标记分类方法将文本作为输入,并将每个相应的标记分类到一个类别中。以以下例子为例:

“I went to Berlin.”

通过基于标记分类的NER,输出应如下所示:

  • I = O
  • Went = O
  • to = O
  • Berlin = Location

然而,生成模型的解决方式有所不同;它们将输入作为文本接受,并以文本形式生成结果。例如,基于T5的NER模型(huggingface.co/dbmdz/t5-ba…)使用以下格式:

  • I went to [ Berlin | location ]

如果需要,额外的后处理步骤是必要的,以提取相应的实体。问答也是如此。在生成问答中,答案不总是完全从上下文中提取;相反,模型的狭义理解和重新措辞也会被添加到响应中。

生成问答在不仅需要从文本中提取信息时,而且对问题的附加知识和背景智慧也很重要。然而,使用生成问答时,我们不能总是确保生成的答案是正确的,因为这些词可能与原始答案不同。

在以下小节中,我们将特别关注基于标记分类的方法,这些方法仅根据基础任务对给定的输入文本进行标记。例如,这里的QA仅指使用标记分类的开卷问答。NER和POS也是如此。

理解NER

标记分类类别中的一个知名任务是NER——识别每个标记是否为实体并确定每个检测到的实体的类型。例如,一段文本可以同时包含多个实体——人名、地点、组织以及其他类型的实体。以下文本是NER的一个清晰示例:

  • George Washington is one of the presidents of the United States of America.

George Washington是一个人名,而United States of America是一个地点名。序列标注模型应对每个单词进行标记,每个标记包含有关标签的信息。BIO标签在标准NER任务中被广泛使用。

以下表格列出了标签及其描述:

image.png

在这个表格中,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 格式进行标记。像 CoNLL-2003 这样的示例数据集(www.clips.uantwerpen.be/conll2003/n…)可以以以下格式展示:

image.png

除了我们已经看到的 NER 标签之外,这个数据集中还有 POS 标签。

理解 POS 标注

POS 标注,即词性标注,是根据词在给定文本中的词性对其进行标注。举个简单的例子,在给定的文本中,识别每个词在名词、形容词、副词和动词等类别中的角色被称为 POS 标注。然而,从语言学的角度来看,词汇的角色远不止这四种。

在 POS 标签中,存在不同的变体,但 Penn Treebank POS 标签集是最知名的之一。以下截图展示了这些角色的摘要和相应描述:

image.png

用于 POS 任务的数据集的标注方式类似于图 6.2 中的示例。这些标签的标注在特定的 NLP 应用中非常有用,是许多其他方法的基础构件之一。变压器和许多高级模型能够在其复杂的架构中以某种方式理解单词之间的关系。

理解 QA

QA(问题回答)或阅读理解任务包括一组阅读理解文本及其相应的问题。一个典型的数据集是 SQUAD(斯坦福问题回答数据集)。该数据集包含维基百科文本及针对这些文本提出的问题。答案以原始维基百科文本的片段形式出现。

以下截图展示了该数据集的一个示例:

image.png

用红色突出显示的片段是答案,每个问题的重要部分用蓝色突出显示。一个好的 NLP 模型需要根据问题对文本进行分段,这种分段可以通过序列标注的形式完成。模型将段落的开始和结束标记为答案的开始和结束段落。

到目前为止,你已经了解了现代 NLP 序列标注任务的基础知识,如 QA、NER 和 POS。在下一部分,你将学习如何针对这些特定任务对 BERT 进行微调,并使用数据集库中的相关数据集。

微调语言模型以进行 NER

在本节中,我们将学习如何对 BERT 进行 NER 任务的微调。我们将首先从数据集库开始,并加载 CoNLL-2003 数据集。

数据集卡可以在 Hugging Face 上访问。以下截图展示了来自 Hugging Face 网站的模型卡:

image.png

从这个截图中可以看到,该模型已在此数据集上进行过训练,并且目前在右侧面板中列出。然而,数据集的描述,包括其大小和特性,也会提供。让我们现在深入了解这些信息:

要加载数据集,可以使用以下命令:

import datasets
conll2003 = datasets.load_dataset("conll2003")

下载进度条将会出现,下载和缓存完成后,数据集将准备好使用。以下截图显示了进度条的情况:

image.png

你可以通过以下命令轻松地再次检查数据集的训练样本:

>>> conll2003["train"][0]

以下截图显示了结果:

image.png

相应的 POS 和 NER 标签在前面的截图中显示。我们将在本例中仅使用 NER 标签。您可以使用以下命令获取数据集中可用的 NER 标签:

>>> conll2003["train"].features["ner_tags"]

结果也显示在图 6.7 中。所有的 BIO 标签都被列出,总共有九个标签:

>>> Sequence(feature=ClassLabel(num_classes=9, names=['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC'], names_file=None, id=None), length=-1, id=None)

接下来的步骤是加载 BERT 分词器:

from transformers import BertTokenizerFast
tokenizer = BertTokenizerFast.from_pretrained("bert-base-uncased")

分词器类也可以处理以空格分隔的句子。我们需要启用分词器以处理以空格分隔的句子,因为 NER 任务为每个标记提供基于标记的标签。在这个任务中,标记通常是以空格分隔的单词,而不是 Byte Pair Encoding (BPE) 或其他分词器的标记。让我们看看如何使用分词器处理以空格分隔的句子:

tokenizer(["Oh", "this", "sentence", "is", "tokenized", "and", "splitted", "by", "spaces"], is_split_into_words=True)

如您所见,只需将 is_split_into_words 设置为 True,即可解决这个问题。

在使用数据进行训练之前,需要对数据进行预处理。为此,我们必须使用以下函数并将其映射到整个数据集:

def tokenize_and_align_labels(examples):
    tokenized_inputs = tokenizer(examples["tokens"],
        truncation=True, is_split_into_words=True)
    labels = []
    for i, label in enumerate(examples["ner_tags"]):
        word_ids = tokenized_inputs.word_ids(batch_index=i)
        previous_word_idx = None
        label_ids = []
        for word_idx in word_ids:
            if word_idx is None:
                label_ids.append(-100)
            elif word_idx != previous_word_idx:
                label_ids.append(label[word_idx])
            else:
                label_ids.append(label[word_idx] if label_all_tokens else -100)
            previous_word_idx = word_idx
        labels.append(label_ids)
    tokenized_inputs["labels"] = labels
    return tokenized_inputs

这个函数将确保我们的标记和标签正确对齐。这种对齐是必要的,因为标记是以片段的形式进行标记的,但单词必须是一个整体。要测试和查看此函数的工作方式,您可以给它一个单一的样本:

q = tokenize_and_align_labels(conll2003['train'][4:5])
print(q)

结果如下所示:

>>> {'input_ids': [[101, 2762, 1005, 1055, 4387, 2000, 1996, 2647, 2586, 1005, 1055, 15651, 2837, 14121, 1062, 9328, 5804, 2056, 2006, 9317, 10390, 2323, 4965, 8351, 4168, 4017, 2013, 3032, 2060, 2084, 3725, 2127, 1996, 4045, 6040, 2001, 24509, 1012, 102]], 'token_type_ids': [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], 'labels': [[-100, 5, 0, -100, 0, 0, 0, 3, 4, 0, -100, 0, 0, 1, 2, -100, -100, 0, 0, 0, 0, 0, 0, 0, -100, -100, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, -100]]}

但这个结果不容易阅读,因此您可以运行以下代码来获取可读的版本:

for token, label in zip(tokenizer.convert_ids_to_tokens(
    q["input_ids"][0]), q["labels"][0]):
    print(f"{token:_<40} {label}")

结果如下所示:

image.png

这个函数的映射可以通过使用 datasets 库的 map 函数来完成:

tokenized_datasets = conll2003.map(tokenize_and_align_labels, batched=True)

在下一步中,我们需要加载 BERT 模型并指定相应的标签数量:

from transformers import AutoModelForTokenClassification
model = AutoModelForTokenClassification.from_pretrained(
    "bert-base-uncased", num_labels=9)

模型将被加载并准备好进行训练。在接下来的步骤中,我们必须准备训练器和训练参数:

from transformers import TrainingArguments, Trainer
args = TrainingArguments(
    "test-ner",
    evaluation_strategy = "epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    num_train_epochs=3,
    weight_decay=0.01,
)

准备数据整理器是必要的。它将在训练数据集上应用批处理操作,以减少内存使用并提高速度。您可以按如下方式进行:

from transformers import DataCollatorForTokenClassification
data_collator = DataCollatorForTokenClassification(tokenizer)

为了能够评估模型性能,Hugging Face 的 datasets 库提供了许多任务的评估指标。我们将使用 seqeval 库进行 NER 的序列评估。seqeval 是一个用于评估序列标注算法和模型的 Python 框架。首先,需要安装 seqeval 库:

pip install seqeval

然后,您可以加载评估指标:

metric = datasets.load_metric("seqeval")

通过使用以下代码,您可以轻松查看指标如何工作:

example = conll2003['train'][0]
label_list = conll2003["train"].features["ner_tags"].feature.names
labels = [label_list[i] for i in example["ner_tags"]]
metric.compute(predictions=[labels], references=[labels])

结果如下所示:

image.png

各种指标,如准确率、F1 分数、精确度和召回率,将根据样本输入进行计算。用于计算指标的函数如下:

import numpy as np

def compute_metrics(p):
    predictions, labels = p
    predictions = np.argmax(predictions, axis=2)
    true_predictions = [
        [label_list[p] for (p, l) in zip(prediction, label) if l != -100]
        for prediction, label in zip(predictions, labels)]
    true_labels = [
        [label_list[l] for (p, l) in zip(prediction, label) if l != -100]
        for prediction, label in zip(predictions, labels)
    ]
    results = metric.compute(predictions=true_predictions, references=true_labels)
    return {
        "precision": results["overall_precision"],
        "recall": results["overall_recall"],
        "f1": results["overall_f1"],
        "accuracy": results["overall_accuracy"],
    }

最后一步是创建一个训练器,并相应地进行训练:

from transformers import Trainer

trainer = Trainer(
    model,
    args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,
    tokenizer=tokenizer,
    compute_metrics=compute_metrics
)
trainer.train()

运行 trainer.train() 函数后,结果将如下所示:

image.png

在训练完成后,必须保存模型和分词器:

model.save_pretrained("ner_model")
tokenizer.save_pretrained("tokenizer")

如果您希望使用模型与 pipeline,必须读取配置文件,并根据您在 label_list 对象中使用的标签正确设置 label2idid2label

id2label = {
    str(i): label for i, label in enumerate(label_list)
}
label2id = {
    label: str(i) for i, label in enumerate(label_list)
}
import json
config = json.load(open("ner_model/config.json"))
config["id2label"] = id2label
config["label2id"] = label2id
json.dump(config, open("ner_model/config.json", "w"))

之后,您可以像以下示例一样轻松使用模型:

from transformers import pipeline
model = AutoModelForTokenClassification.from_pretrained("ner_model")
nlp = pipeline("ner", model=model, tokenizer=tokenizer)
example = "I live in Istanbul"
ner_results = nlp(example)
print(ner_results)

结果将如下所示:

[{'entity': 'B-LOC', 'score': 0.9983942, 'index': 4, 'word': 'istanbul', 'start': 10, 'end': 18}]

在特定数据集、领域或甚至语言上进行微调可以显著提升模型性能。然而,这种微调有其优缺点;数据集应广泛覆盖我们将要使用的实际领域。例如,使用给定数据(如前面的例子所示)微调的模型在医学领域的表现可能不会很好。

到目前为止,您已经学会了如何使用 BERT 进行 POS 标注。您了解了如何使用 Transformers 训练自己的 POS 标注模型,并且测试了该模型。在接下来的部分中,我们将重点讨论 QA。

使用令牌分类的问答

问答(QA)问题通常被定义为一个自然语言处理(NLP)问题,其中包含给定的文本和一个问题,并要求 AI 返回一个答案。通常,这个答案可以在原始文本中找到,但解决这个问题有不同的方法。在视觉问答(VQA)的情况下,问题涉及的是视觉实体或视觉概念,而不是文本,但问题本身以文本形式存在。

以下是一些 VQA 的示例:

image.png

使用令牌分类的问答

大多数旨在用于视觉问答(VQA)的模型都是多模态模型,这些模型能够理解视觉上下文以及问题,并生成合适的答案。然而,单模态的纯文本问答或简单问答则基于文本上下文和文本问题,并提供相应的文本答案。

SQUAD 是问答领域最著名的数据集之一。要查看 SQUAD 的示例并进行检查,可以使用以下代码:

from pprint import pprint
from datasets import load_dataset

squad = load_dataset("squad")
for item in squad["train"][1].items():
    print(item[0])
    pprint(item[1])
    print("="*20)

以下是结果:

answers
{'answer_start': [188], 'text': ['a copper statue of Christ']}
====================
Context
('Architecturally, the school has a Catholic character. Atop the Main ' "Building's gold dome is a golden statue of the Virgin Mary. Immediately in " 'front of the Main Building and facing it, is a copper statue of Christ with ' 'arms upraised with the legend "Venite Ad Me Omnes". Next to the Main ' 'Building is the Basilica of the Sacred Heart. Immediately behind the ' 'basilica is the Grotto, a Marian place of prayer and reflection. It is a ' 'replica of the grotto at Lourdes, France where the Virgin Mary reputedly ' 'appeared to Saint Bernadette Soubirous in 1858. At the end of the main drive ' '(and in a direct line that connects through 3 statues and the Gold Dome), is ' 'a simple, modern stone statue of Mary.')
====================
Id
'5733be284776f4190066117f'
====================
Question
'What is in front of the Notre Dame Main Building?'
====================
Title
'University_of_Notre_Dame'
====================

然而,SQUAD 数据集还有一个版本 2,包含更多的训练样本,并且强烈推荐使用它。为了全面了解如何为问答问题训练模型,我们将重点讨论如何使用 SQUAD 数据集进行文本问答的训练:

首先,使用以下代码加载 SQUAD 版本 2:

from datasets import load_dataset

squad = load_dataset("squad_v2")

加载 SQUAD 数据集后,可以使用以下代码查看数据集的详细信息:

squad

结果如下:

image.png

SQUAD 数据集的详细信息将如图 6.12 所示。正如你所看到的,训练样本超过 130,000 个,验证样本超过 11,000 个。

与 NER 一样,我们必须对数据进行预处理,以使其适合模型使用。为此,你首先需要加载你的分词器,这是一种预训练的分词器,前提是你正在使用预训练模型并希望对其进行微调以解决问答问题:

from transformers import AutoTokenizer

model = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model)

如你所见,我们将使用 distilBERT 模型。

在我们的 SQUAD 示例中,我们需要将多个文本输入模型,一个是问题,一个是上下文。因此,我们需要将分词器配置为将它们并排放置,并使用特殊的 [SEP] 令牌分隔它们,因为 distilBERT 是基于 BERT 的模型。

另一个问题是上下文的大小。上下文的大小可能比模型接受的输入大小更长,但我们不能将其减少到模型接受的大小。对于某些特定的 NLP 任务,我们可以截断输入,但在问答中,答案可能在被截断的部分。我们将展示一个使用文档步幅(document stride)来解决这个问题的示例。

以下是使用分词器的示例:

max_length = 384
doc_stride = 128
example = squad["train"][173]
tokenized_example = tokenizer(
    example["question"],
    example["context"],
    max_length=max_length,
    truncation="only_second",
    return_overflowing_tokens=True,
    stride=doc_stride
)

步幅(stride)是用于返回第二部分的步幅,如窗口一样,而 return_overflowing_tokens 标志则告诉模型是否应返回额外的令牌。tokenized_example 的结果不是单一的令牌化输出,而是包含两个输入 ID。以下代码片段展示了结果:

len(tokenized_example['input_ids'])
# 2

因此,你可以通过运行以下 for 循环查看完整结果:

for input_ids in tokenized_example["input_ids"][:2]:
    print(tokenizer.decode(input_ids))
    print("-"*50)

结果如下:

[CLS] beyonce got married in 2008 to whom? [SEP] on april 4, 2008, beyonce married jay z. she publicly revealed their marriage in a video montage at the listening party for her third studio album, i am... sasha fierce, in manhattan's sony club on october 22, 2008. i am... sasha fierce was released on november 18, 2008 in the united states. the album formally introduces beyonce's alter ego sasha fierce, conceived during the making of her 2003 single " crazy in love ", selling 482, 000 copies in its first week, debuting atop the billboard 200, and giving beyonce her third consecutive number - one album in the us. the album featured the number - one song " single ladies ( put a ring on it ) " and the top - five songs " if i were a boy " and " halo ". achieving the accomplishment of becoming her longest - running hot 100 single in her career, " halo "'s success in the us helped beyonce attain more top - ten singles on the list than any other woman during the 2000s. it also included the successful " sweet dreams ", and singles " diva ", " ego ", " broken - hearted girl " and " video phone ". the music video for " single ladies " has been parodied and imitated around the world, spawning the " first major dance craze " of the internet age according to the toronto star. the video has won several awards, including best video at the 2009 mtv europe music awards, the 2009 scottish mobo awards, and the 2009 bet awards. at the 2009 mtv video music awards, the video was nominated for nine awards, ultimately winning three including video of the year. its failure to win the best female video category, which went to american country pop singer taylor swift's " you belong with me ", led to kanye west interrupting the ceremony and beyonce [SEP]
--------------------------------------------------
[CLS] beyonce got married in 2008 to whom? [SEP] single ladies " has been parodied and imitated around the world, spawning the " first major dance craze " of the internet age according to the toronto star. the video has won several awards, including best video at the 2009 mtv europe music awards, the 2009 scottish mobo awards, and the 2009 bet awards. at the 2009 mtv video music awards, the video was nominated for nine awards, ultimately winning three including video of the year. its failure to win the best female video category, which went to american country pop singer taylor swift's " you belong with me ", led to kanye west interrupting the ceremony and beyonce improvising a re - presentation of swift's award during her own acceptance speech. in march 2009, beyonce embarked on the i am... world tour, her second headlining worldwide concert tour, consisting of 108 shows, grossing $ 119. 5 million. [SEP]
--------------------------------------------------

如上所示,使用 128 个令牌的窗口,第二个输入 ID 的输出再次复制了其余的上下文。

另一个问题是结束跨度(end span),数据集中没有提供,但提供了答案的开始跨度(start span)或开始字符。很容易找到答案的长度并将其加到开始跨度上,这样就会自动得到结束跨度。

现在我们了解了这个数据集的所有细节以及如何处理它们,我们可以轻松地将它们组合在一起,制作一个预处理函数(github.com/huggingface…)。该函数接受示例作为输入:

def prepare_train_features(examples):
    # tokenize examples
    tokenized_examples = tokenizer(
        examples["question" if pad_on_right else "context"],
        examples["context" if pad_on_right else "question"],
        truncation="only_second" if pad_on_right else "only_first",
        max_length=max_length,
        stride=doc_stride,
        return_overflowing_tokens=True,
        return_offsets_mapping=True,
        padding="max_length",
    )

    # map from a feature to its example
    sample_mapping = tokenized_examples.pop("overflow_to_sample_mapping")
    offset_mapping = tokenized_examples.pop("offset_mapping")
    tokenized_examples["start_positions"] = []
    tokenized_examples["end_positions"] = []

    # Impossible to answer examples should be CLS and also start and end tokens for each one should be added as well
    for i, offsets in enumerate(offset_mapping):
        input_ids = tokenized_examples["input_ids"][i]
        cls_index = input_ids.index(tokenizer.cls_token_id)
        sequence_ids = tokenized_examples.sequence_ids(i)
        sample_index = sample_mapping[i]
        answers = examples["answers"][sample_index]
        if len(answers["answer_start"]) == 0:
            tokenized_examples["start_positions"].append(cls_index)
            tokenized_examples["end_positions"].append(cls_index)
        else:
            start_char = answers["answer_start"][0]
            end_char = start_char + len(answers["text"][0])
            token_start_index = 0
            while sequence_ids[token_start_index] != (1 if pad_on_right else 0):
                token_start_index += 1
            token_end_index = len(input_ids) - 1
            while sequence_ids[token_end_index] != (1 if pad_on_right else 0):
                token_end_index -= 1
            if not (offsets[token_start_index][0] <= start_char and offsets[token_end_index][1] >= end_char):
                tokenized_examples["start_positions"].append(cls_index)
                tokenized_examples["end_positions"].append(cls_index)
            else:
                while token_start_index < len(offsets) and offsets[token_start_index][0] <= start_char:
                    token_start_index += 1
                tokenized_examples["start_positions"].append(token_start_index - 1)
                while offsets[token_end_index][1] >= end_char:
                    token_end_index -= 1
                tokenized_examples["end_positions"].append(token_end_index + 1)

    return tokenized_examples

将这个函数映射到数据集上将应用所有必要的更改:

tokenized_datasets = squad.map(prepare_train_features,
    batched=True, remove_columns=squad["train"].column_names)

就像其他示例一样,你现在可以加载一个预训练模型并进行微调:

from transformers import (
    AutoModelForQuestionAnswering, TrainingArguments, Trainer)

model = AutoModelForQuestionAnswering.from_pretrained(model)

下一步是创建训练参数:

args = TrainingArguments(
    "test-squad",
    evaluation_strategy = "epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    num_train_epochs=3,
    weight_decay=0.01,
)

如果我们不使用数据整理器(data collator),我们将提供一个默认的数据整理器给模型训练器:

from transformers import default_data_collator

data_collator = default_data_collator

现在,一切准备好进行训练器的创建:

trainer = Trainer(
    model,
    args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,
    tokenizer=tokenizer,
)

训练器可以使用 train 函数进行训练:

trainer.train()

结果将类似于以下内容:

image.png

如你所见,模型已经经过三轮训练,并报告了验证和训练的损失输出。

与其他模型一样,你可以使用以下函数轻松保存这个模型:

trainer.save_model("distilBERT_SQUAD")

如果你想使用保存的模型或任何其他经过问答训练的模型,transformers 库提供了一个易于使用和实现的管道(pipeline)功能,无需额外的工作。

通过使用这个管道功能,你可以使用任何模型。以下是使用 QA 管道的示例:

from transformers import pipeline

qa_model = pipeline('question-answering',
    model='distilbert-base-cased-distilled-squad',
    tokenizer='distilbert-base-cased')

管道只需要两个输入来使模型准备好使用:模型和分词器。然而,你还需要给它一个管道类型,这里是 QA 类型。

下一步是提供它所需的输入,即上下文和问题:

question = squad["validation"][0]["question"]
context = squad["validation"][0]["context"]

可以使用以下代码查看问题和上下文:

print("Question:")
print(question)
print("Context:")
print(context)

输出结果如下:

Question:
In what country is Normandy located?
Context:
('The Normans (Norman: Nourmands; French: Normands; Latin: Normanni) were the ' 'people who in the 10th and 11th centuries gave their name to Normandy, a ' 'region in France. They were descended from Norse ("Norman" comes from ' '"Norseman") raiders and pirates from Denmark, Iceland and Norway who, under ' 'their leader Rollo, agreed to swear fealty to King Charles III of West ' 'Francia. Through generations of assimilation and mixing with the native ' 'Frankish and Roman-Gaulish populations, their descendants would gradually ' 'merge with the Carolingian-based cultures of West Francia. The distinct ' 'cultural and ethnic identity of the Normans emerged initially in the first ' 'half of the 10th century, and it continued to evolve over the succeeding ' 'centuries.')

你可以使用以下示例来运行模型:

qa_model(question=question, context=context)

结果如下:

{'answer': 'France', 'score': 0.9889379143714905, 'start': 159, 'end': 165,}

到此为止,你已经学会了如何在所需数据集上进行训练。你还学会了如何使用管道来使用训练好的模型。在下一节中,你将学习如何将问答应用于多种任务。

问答在多任务中的应用

许多语言的本质使我们能够将许多不同的 NLP 任务融入到一个简单的范式中,即问答(QA)。例如,对于情感分类任务,我们可以使用基于 QA 的方法来解决,而不是将输入分类到不同的类别(积极、消极、中立)。我们可以这样重新定义输入:

  • 上下文: "I loved this movie!"
  • 问题: "What best describes the sentiment of this text (Positive, Negative, Neutral)?"
  • 答案: "Positive"

通过这种方式,不仅可以结合单一的 NLP 任务,还可以将许多其他 NLP 任务结合到单一的令牌分类器中。例如,可以使用不同的问题来处理不同的 NLP 任务。正如前面所述,需要一组问题和相应的答案。但在这个特定的示例中,并非所有答案都来自给定的上下文,答案也可以来自问题本身!

另一个这种方法的示例是使用 QA 进行代词解析。例如,使用以下格式可以进行代词解析:

  • 上下文: "Meysam admired Savas. He was always fascinated about his work."
  • 问题: "What does He refer to in this text?"
  • 答案: "Meysam"

如你所见,令牌分类可以用于许多任务。实际上,令牌分类可以用于比 QA 更多的任务。

总结

在这一章中,我们讨论了如何对任何令牌分类任务微调预训练模型。探讨了在 NER 和 QA 问题上微调模型的过程。详细介绍了如何使用预训练和微调模型在特定任务上进行管道处理。我们还了解了这些任务的各种预处理步骤。保存微调于特定任务的预训练模型也是本章的一个重要学习点。我们还看到如何在像 QA 这样的任务中,处理比模型输入更长的序列大小,并利用文档步幅(document stride)来更高效地使用分词器。

在下一章中,我们将讨论使用 Transformers 进行文本表示的方法。到章末,你将学习如何执行零-shot 或少-shot 学习以及语义文本聚类。