Token 分类
我们将首先探讨的应用是 Token 分类。这个通用任务涵盖了所有可以表述为“给句子中的词或字贴上标签”的问题,例如:
- 实体命名识别 (NER):找出句子中的实体(如人物、地点或组织)。这可以通过为每个实体指定一个类别的标签,如果没有实体则会输出无实体的标签。
- 词性标注 (POS):将句子中的每个单词标记为对应于特定的词性(如名词、动词、形容词等)。
- 分块(chunking):找出属于同一实体的 tokens 这个任务(可以与词性标注或命名实体识别结合)可以被描述为将位于块开头的 token 赋予一个标签(通常是 “
B-
” (Begin),代表该token位于实体的开头),将位于块内的 tokens 赋予另一个标签(通常是 “I-
”(inner)代表该token位于实体的内部),将不属于任何块的 tokens 赋予第三个标签(通常是 “O
” (outer)代表该token不属于任何实体)。
当然,还有很多其他类型的 token 分类问题;这些只是几个有代表性的例子。在本节中,我们将在 NER 任务上微调模型 (BERT),然后该模型将能够生成如下预测:
你可以 在这里 。找到我们将训练并上传到 Hub 的模型,可以尝试输入一些句子测试一下模型的预测结果。
准备数据
首先,我们需要一个适合 token 分类的数据集。在本节中,我们将使用 CoNLL-2003 数据集 ,该数据集来源于路透社的新闻报道。
💡 只要你的数据集由分词文本和对应的标签组成,就能够将这里描述的数据处理过程应用到自己的数据集中。如果需要复习如何在 Dataset
中加载自定义数据集,请复习 第五章 。
CoNLL-2003 数据集
要加载 CoNLL-2003 数据集,我们可以使用🤗 Datasets 库的 load_dataset()
方法:
from datasets import load_dataset
raw_datasets = load_dataset("conll2003")
这将下载并缓存数据集,就像和我们在 第三章 加载 GLUE MRPC 数据集一样。查看 raw_datasets
对象可以让我们看到数据集中存在哪些列,以及训练集、验证集和测试集之间是如何划分的:
raw_datasets
DatasetDict({
train: Dataset({
features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
num_rows: 14041
})
validation: Dataset({
features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
num_rows: 3250
})
test: Dataset({
features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
num_rows: 3453
})
})
可以看到数据集包含了我们之前提到的三项任务的标签:命名实体识别(NER)、词性标注(POS)以及分块(chunking)。这个数据集与其他数据集的一个显著区别在于,输入文本的那一列并非以句子或整片的文本的形式存储,而是以单词列表的形式(最后一列被称为 tokens
,不过 tokens
列保存的还是单词,也就是说,这些预先分词的输入仍需要经过 tokenizer 进行子词分词处理)。
我们来看看训练集的第一个元素:
raw_datasets["train"][0]["tokens"]
['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.']
由于我们要进行命名实体识别,让我们查看一下 NER 标签:
raw_datasets["train"][0]["ner_tags"]
[3, 0, 7, 0, 0, 0, 7, 0, 0]
这些是可以直接训练的整数类别标签,当我们想要检查数据时,直接看整数的标签不是很直观。就像在文本分类中,我们可以通过查看数据集的 features
属性来找到这些整数和标签名称之间的对应关系:
ner_feature = raw_datasets["train"].features["ner_tags"]
ner_feature
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)
因此,我们得到了 ClassLabels
类型的序列。序列中元素的类型存储在 ner_feature
的 feature
中,我们可以通过查看该 feature
的 names
属性来访问标签名称的列表:
label_names = ner_feature.feature.names label_names
['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC']
我们在 第六章 深入研究 token-classification
管道时已经看到过这些标签 我们在这里进行一个快速的回顾:
O
表示这个词不对应任何实体。B-PER
/I-PER
意味着这个词对应于人名实体的开头/内部。B-ORG
/I-ORG
的意思是这个词对应于组织名称实体的开头/内部。B-LOC
/I-LOC
指的是是这个词对应于地名实体的开头/内部。B-MISC
/I-MISC
表示这个词对应一个其他实体(不属于特定类别或类别之外)的开头 / 内部。
接下来使用对这些标签名对数据集的 ner_tags
进行解码,将得到以下输出结果:
words = raw_datasets["train"][0]["tokens"]
labels = raw_datasets["train"][0]["ner_tags"]
line1 = ""
line2 = ""
for word, label in zip(words, labels):
full_label = label_names[label]
max_length = max(len(word), len(full_label))
line1 += word + " " * (max_length - len(word) + 1)
line2 += full_label + " " * (max_length - len(full_label) + 1)
print(line1)
print(line2)
'EU rejects German call to boycott British lamb .'
'B-ORG O B-MISC O O O B-MISC O O'
我们还可以查看训练集中索引为 4 的数据,它是一个同时包含 B-
和 I-
标签的例子:
'Germany \'s representative to the European Union \'s veterinary committee Werner Zwingmann said on Wednesday consumers should buy sheepmeat from countries other than Britain until the scientific advice was clearer .'
'B-LOC O O O O B-ORG I-ORG O O O B-PER I-PER O O O O O O O O O O O B-LOC O O O O O O O'
正如我们在上面的输出中所看到的,跨越两个单词的实体,如“European Union”和“Werner Zwingmann”,数据集把第一个单词标注为了 B-
标签,将第二个单词标记为了 I-
标签。
✏️ 轮到你了! 检查同一个句子的词性标注 (POS)或分块(chunking)列,查看输出的结果。
处理数据
像往常一样,我们的文本需要转换为 Token ID,然后模型才能理解它们。正如我们在 第六章 所学的那样。不过与 tokens 分类任务不同的是数据集已经完成了预分词,我们在处理的过程中不需要再次分词。幸运的是,tokenizer API 可以很容易地处理这种差异;我们只需要通过一个特殊的标志告诉 tokenizer即可。
首先,让我们创建 tokenizer
对象。如前所述,我们将使用 BERT 预训练模型,因此我们将从下载并缓存关联的 tokenizer
开始:
from transformers import AutoTokenizer
model_checkpoint = "bert-base-cased"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
你可以更换把 model_checkpoint
更换为 Hub 上你喜欢的任何其他模型,或使用本地保存的预训练模型和 tokenizer
。唯一的限制是 tokenizer 需要由 🤗 Tokenizers 库支持,并且有一个“快速”版本可用。你可以在 Huggingface 模型后端支持表 上看到所有带有快速版本的 tokenizer
架构,或者你也可以通过查看它 is_fast
属性来检测正在使用的 tokenizer
对象是否由 🤗 Tokenizers 支持:
tokenizer.is_fast
True
我们可以像往常一样使用我们的 tokenizer
对预先分词的输入进行 tokenize
,只需额外添加 is_split_into_words=True
参数:
inputs = tokenizer(raw_datasets["train"][0]["tokens"], is_split_into_words=True)
inputs.tokens()
['[CLS]', 'EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'la', '##mb', '.', '[SEP]']
如我们所见,tokenizer
在结果中添加了模型需要使用的特殊 tokens(在开头的 [CLS]
,在结尾的 [SEP]
),并且大部分单词保持不变。不过,单词 lamb
被分词为两个子词, la
和 ##mb
。这导致了输入和标签之间的不匹配:标签列表只有 9 个元素,而我们的输入现在有 12 个 tokens。解决特殊 tokens 的问题很容易(我们已经知道了每个 token
开始和结束的位置),我们需要确保的是我们将所有的标签与正确的词对齐。
幸运的是,由于我们使用的是快速 tokens 因此我们可以使用🤗 Tokenizers 超能力,这意味着我们可以轻松地将每个 token 映射到其相应的单词(如 第六章 中所学):
inputs.word_ids()
[None, 0, 1, 2, 3, 4, 5, 6, 7, 7, 8, None]
只需要一点点工作,我们就可以扩展我们的标签列表。我们将添加的使其与 token
列表相匹配。 添加的第一条规则是,特殊 tokens 的标签设置为 -100
。这是因为默认情况下, -100
会被我们的损失函数(交叉熵)忽略。然后将每个 token
的标签设置为这个 token
所在单词开头的 token
的标签,因为同一个单词一定是同一个实体的一部分。最后将单词的内部 tokens
标签中的 B-
替换为 I-
,因为该 token
不在实体的开头,B-
在每个实体中只应该出现一次:
def align_labels_with_tokens(labels, word_ids):
new_labels = []
current_word = None
for word_id in word_ids:
if word_id != current_word:
# 新单词的开始!
current_word = word_id
label = -100 if word_id is None else labels[word_id]
new_labels.append(label)
elif word_id is None:
# 特殊的token
new_labels.append(-100)
else:
# 与前一个 tokens 类型相同的单词
label = labels[word_id]
# 如果标签是 B-XXX 我们将其更改为 I-XXX
if label % 2 == 1:
label += 1
new_labels.append(label)
return new_labels
让我们在数据集的第一个元素上试一试:
labels = raw_datasets["train"][0]["ner_tags"]
word_ids = inputs.word_ids()
print(labels)
print(align_labels_with_tokens(labels, word_ids))
[3, 0, 7, 0, 0, 0, 7, 0, 0]
[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100]
正如我们所看到的,我们的函数为开头和结尾添加了两个特殊 tokens :-100
,并为切分成两个 tokens 的单词添加了一个新的 0
标签。
✏️ 轮到你了! 有些研究人员更喜欢只为每个单只分配一个标签,对该单词其他部分的 token 分配 -100
标签。这是为了避免那些分解成许多子词的长单词对损失作出过多的贡献。请按照这个思路,改变之前的函数,使标签与 inputs ID 对齐。
为了预处理我们的整个数据集,我们需要对所有输入进行 tokenize
,并使用 align_labels_with_tokens()
函数处理所有标签。为了充分利用快速 tokenizer 的优势,最好是同时对大量文本一起进行 tokenize
,所以我们需要编写一个处理一组示例的函数,并使用带有 batched=True
参数的 Dataset.map()
方法。与我们之前的示例唯一不同的是,当 tokenizer
的输入是文本列表(或示例中单词的列表的列表)时, word_ids()
函数需要根据列表的索引获取 token
的 ID,所以我们也在下面的函数中添加了这个功能:
def tokenize_and_align_labels(examples):
tokenized_inputs = tokenizer(
examples["tokens"], truncation=True, is_split_into_words=True
)
all_labels = examples["ner_tags"]
new_labels = []
for i, labels in enumerate(all_labels):
word_ids = tokenized_inputs.word_ids(i)
new_labels.append(align_labels_with_tokens(labels, word_ids))
tokenized_inputs["labels"] = new_labels
return tokenized_inputs
注意,我们还没有填充我们的输入,我们将在稍后使用数据整理器创建 batch
时进行。
我们现在可以一次性使用预处理函数处理整个数据集:
tokenized_datasets = raw_datasets.map(
tokenize_and_align_labels,
batched=True,
remove_columns=raw_datasets["train"].column_names,
)
我们已经完成了最困难的部分!现在数据已经经过了预处理,实际的训练过程将会与我们在 第三章 所做的很相似。
使用 Trainer API 微调模型
使用 Trainer
的代码会和以前一样,唯一的变化是数据如何整理成 batch
以及计算评估的分数。
整理数据
我们不能像 第三章 那样直接使用 DataCollatorWithPadding
,因为那样只会填充输入(inputs ID、注意掩码和 tokens 类型 ID)。除了输入部分,在这里我们还需要对标签也使用与输入完全相同的方式填充,以保证它与输入的大小相同。我们将使用 -100
进行填充,以便在损失计算中忽略填充的内容。
以上的这些过程可以由 DataCollatorForTokenClassification
实现。它是一个带有填充功能的数据整理器,使用时只需要传入用于预处理输入的 tokenizer
:
from transformers import DataCollatorForTokenClassification
data_collator = DataCollatorForTokenClassification(tokenizer=tokenizer)
为了在几个样本上测试这个数据整理器,我们可以先在训练集中的几个示例上调用它:
batch = data_collator([tokenized_datasets["train"][i] for i in range(2)])
batch["labels"]
tensor([[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100],
[-100, 1, 2, -100, -100, -100, -100, -100, -100, -100, -100, -100]])
将数据整理器的结果与数据集中未经处理的第一个和第二个元素的标签进行比较。
for i in range(2):
print(tokenized_datasets["train"][i]["labels"])
[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100]
[-100, 1, 2, -100]
正如我们所看到的,已经使用 -100
将第二组标签填充到了与第一组标签相同的长度。
评估指标
要让 Trainer
在每个周期计算一个指标,我们需要定义一个 compute_metrics()
函数,该函数的输入是预测值和标签的数组,并返回带有指标名称和评估结果的字典。
用于评估 Token 分类预测的经典框架是 seqeval 。要使用此指标,我们首先需要安装 seqeval 库:
!pip install seqeval
然后我们可以通过 evaluate.load()
函数加载它,就像我们在 第三章 中所做的那样:
import evaluate
metric = evaluate.load("seqeval")
该指标和常规的评测指标有些区别:它需要字符串形式的标签列表而不是整数,所以我们需要在将它们传递给指标之前将预测值和标签由数字解码为字符串。让我们先用一些测试数据看看它是如何工作的:
首先,获取第一个训练样本的标签。
labels = raw_datasets["train"][0]["ner_tags"]
labels = [label_names[i] for i in labels]
labels
['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O']
然后我们可以通过更改索引 2 处的值来为这些标签创建假的预测值来测试该指标:
predictions = labels.copy()
predictions[2] = "O"
metric.compute(predictions=[predictions], references=[labels])
请注意,该指标的输入是预测列表(不是一个)和标签列表,输出结果如下:
{'MISC': {'precision': 1.0, 'recall': 0.5, 'f1': 0.67, 'number': 2},
'ORG': {'precision': 1.0, 'recall': 1.0, 'f1': 1.0, 'number': 1},
'overall_precision': 1.0,
'overall_recall': 0.67,
'overall_f1': 0.8,
'overall_accuracy': 0.89}
结果返回很多信息!包括每个单独实体及整体的准确率、召回率和 F1 分数。在这里我们将只保留总分,但是你可以自由地调整 compute_metrics()
函数返回的所需要指标。这个函数中我们首先会先取预测 logits
的 argmax
,并将其转换为预测值。 compute_metrics()
函数首先取 logits
的 argmax
,将它们转换为预测值(通常情况下,logits 和概率的顺序是相同,所以我们不需要使用 softmax)。然后我们需要将标签和预测值都从整数转换为字符串。我们删除所有标签为 -100
的值,最后将结果传递给 metric.compute()
方法:
import numpy as np
def compute_metrics(eval_preds):
logits, labels = eval_preds
predictions = np.argmax(logits, axis=-1)
# 删除忽略的索引(特殊 tokens )并转换为标签
true_labels = [[label_names[l] for l in label if l != -100] for label in labels]
true_predictions = [
[label_names[p] for (p, l) in zip(prediction, label) if l != -100]
for prediction, label in zip(predictions, labels)
]
all_metrics = metric.compute(predictions=true_predictions, references=true_labels)
return {
"precision": all_metrics["overall_precision"],
"recall": all_metrics["overall_recall"],
"f1": all_metrics["overall_f1"],
"accuracy": all_metrics["overall_accuracy"],
}
现在已经完成了,我们下面就可以开始定义我们的 Trainer
了。接下来我们只需要一个 model
并对其微调!
定义模型
由于我们正在研究 Token 分类问题,因此我们将使用 AutoModelForTokenClassification
类。定义此模型时要记得传递我们标签的数量,最简单方法是将该数字传递给 num_labels
参数,但是如果我们想要一个就像我们在本节开头看到的那样的推理小部件,就需要用最标准的方法正确设置标签的对应关系。
最标准的方式是用两个字典 id2label
和 label2id
来设置标签,这两个字典包含从 ID 到标签的映射以及反向的映射:
id2label = {str(i): label for i, label in enumerate(label_names)}
label2id = {v: k for k, v in id2label.items()}
现在我们只需将它们传递给 AutoModelForTokenClassification.from_pretrained()
方法,它们就会被保存在模型的配置中,然后被正确地保存和上传到 Hub:
from transformers import AutoModelForTokenClassification
model = AutoModelForTokenClassification.from_pretrained(
model_checkpoint,
id2label=id2label,
label2id=label2id,
)
就像我们在 第三章 中定义 AutoModelForSequenceClassification
类一样 创建模型会发出一个警告,提示一些权重未被使用(来自预训练头部的权重)和一些其他权重被随机初始化(来自新 Token 分类头部的权重),不过不用担心,我们马上就要训练这些权重。在这之前,让我们先确认一下我们的模型是否具有正确的标签数量:
model.config.num_labels
9
⚠️ 如果你的模型的标签数量有错误,那么在后面调用 Trainer.train()
时,你会得到一个晦涩的错误(类似于“CUDA error:device-side assert triggered”)。这可能会令人烦恼,所以确保你做了这个检查,确认你的标签数量是正确。
微调模型
我们现在准备好训练我们的模型了!在定义 Trainer
之前,我们只需要做最后两件事:登录 Hugging Face 并设置我们的训练参数。如果你在 notebook 上工作,有一个方便的小工具可以帮助你:
from huggingface_hub import notebook_login
notebook_login()
登录后,我们就可以设置我们的 TrainingArguments
:
from transformers import TrainingArguments
args = TrainingArguments(
"bert-finetuned-ner",
evaluation_strategy="epoch",
save_strategy="epoch",
learning_rate=2e-5,
num_train_epochs=3,
weight_decay=0.01,
push_to_hub=True,
)
你已经对大多数内容有所了解了:我们设置了一些超参数(如学习率、训练的轮数和权重衰减),并设定 push_to_hub=True
,表示我们希望在每个训练轮次结束时保存并评估模型,然后将结果上传到模型中心。注意,你可以通过 hub_model_id
参数指定你想推送的仓库的名称(特别需要注意的是,如果你需要推送给某个组织,就必须使用这个参数)。例如,当我们将模型推送到 huggingface-course
组织 时,我们在 TrainingArguments
中添加了 hub_model_id="huggingface-course/bert-finetuned-ner"
。默认情况下,使用的仓库将保存在你的账户之内,并以你设置的输出目录命名,所以在我们的例子中,仓库的地址是 "sgugger/bert-finetuned-ner"
。
💡 如果你使用的输出路径已经存在一个同名的文件夹,那么它需要是你想推送到 hub 的仓库的克隆在本地的版本。如果不是,你将在声明 Trainer
时遇到一个错误,并需要设置一个新的路径。
最后,我们将所有内容传递给 Trainer
并启动训练:
from transformers import Trainer
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=data_collator,
compute_metrics=compute_metrics,
tokenizer=tokenizer,
)
trainer.train()
请注意,在训练过程中每次保存模型时(这里是每个 epooch),它都会在后台上传到 Hub。这样,如有必要,你将能够在另一台机器上继续你的训练。
训练完成后,我们使用 push_to_hub()
上传模型的最新版本
trainer.push_to_hub(commit_message="Training complete")
如果你想检查一下是否上传成功,这个命令会返回刚刚执行的提交的 URL:
'https://huggingface.co/sgugger/bert-finetuned-ner/commit/26ab21e5b1568f9afeccdaed2d8715f571d786ed'
同时 Trainer
还创建并上传了一张包含所有评估结果的模型卡。到此阶段,你可以在模型 Hub 上使用推理小部件来测试你的模型并与你的朋友分享。你已经成功地在一个 tokens 分类任务上微调了一个模型——恭喜你!
如果你想更深入地了解训练循环,我们现在将向你展示如何使用 🤗 Accelerate 做同样的事情。
自定义训练循环
现在我们来看看完整的训练循环,这样你就可以轻松地定制你需要的部分。它与我们在 第三章 中所做的内容很相似,但对评估部分有一些改动。
做好训练前的准备
首先我们需要为我们的数据集构建 DataLoader
。我们将 data_collator
传递给 collate_fn
参数并随机打乱训练集,但不打乱验证集:
from torch.utils.data import DataLoader
train_dataloader = DataLoader(
tokenized_datasets["train"],
shuffle=True,
collate_fn=data_collator,
batch_size=8,
)
eval_dataloader = DataLoader(
tokenized_datasets["validation"], collate_fn=data_collator, batch_size=8
)
接下来我们重新实例化我们的模型,以确保我们不是继续之前的微调,而是重新开始从 BERT 微调预训练模型:
model = AutoModelForTokenClassification.from_pretrained( model_checkpoint, id2label=id2label, label2id=label2id, )
然后我们需要一个优化器。我们将使用经典 AdamW
,它类似于 Adam
,但在权重衰减的方式上进行了改进:
from torch.optim import AdamW
optimizer = AdamW(model.parameters(), lr=2e-5)
当我们拥有了所有这些对象之后,我们就可以将它们发传递给 accelerator.prepare()
方法:
from accelerate import Accelerator
accelerator = Accelerator()
model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare(
model, optimizer, train_dataloader, eval_dataloader
)
🚨 如果你正在 TPU 上训练,你需要将上面单元格开始的所有代码移动到一个专门的训练函数中。更多详情请回顾 第三章 。
现在我们已经将我们的 train_dataloader
传递给了 accelerator.prepare()
方法,我们还可以使用 len()
来计算训练步骤的数量。请记住,我们应该在准备好 dataloader
后再使用 len()
,因为改动 dataloader
会改变训练长度的数量。这里我们将使用一个从学习率衰减到 0 的经典线性学习率调度:
from transformers import get_scheduler
num_train_epochs = 3
num_update_steps_per_epoch = len(train_dataloader)
num_training_steps = num_train_epochs * num_update_steps_per_epoch
lr_scheduler = get_scheduler(
"linear",
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=num_training_steps,
)
最后,为了将我们的模型推送到 Hub,我们需要在一个工作文件夹中创建一个 Repository
对象。如果你还没有登录的话,首先需要登录到 Hugging Face,然后根据模型 ID 来确定仓库名称(你可以将 repo_name
替换为你喜欢的名字;只需要包含你的用户名即可,你可以使用 get_full_repo_name()
函数的查看目前的 repo_name
):
from huggingface_hub import Repository, get_full_repo_name
model_name = "bert-finetuned-ner-accelerate"
repo_name = get_full_repo_name(model_name)
repo_name
'sgugger/bert-finetuned-ner-accelerate'
然后我们可以将该仓库克隆到本地文件夹中。如果本地已经存在同名的文件夹,这个本地文件夹必须是我们正在使用的仓库克隆在本地的版本:
output_dir = "bert-finetuned-ner-accelerate"
repo = Repository(output_dir, clone_from=repo_name)
我们现在可以通过调用 repo.push_to_hub()
方法上传保存在 output_dir
中的所有内容。它帮助我们在每个训练周期结束时上传中间模型。
训练循环
我们现在准备编写完整的训练循环。为了简化其评估部分,我们定义了一个 postprocess()
函数,该函数会接收模型的预测和真实的标签,并将它们转换为字符串列表,也就是我们的 metric
(评估函数)对象需要的输入格式。
def postprocess(predictions, labels):
predictions = predictions.detach().cpu().clone().numpy()
labels = labels.detach().cpu().clone().numpy()
# 删除忽略的索引(特殊 tokens )并转换为标签
true_labels = [[label_names[l] for l in label if l != -100] for label in labels]
true_predictions = [
[label_names[p] for (p, l) in zip(prediction, label) if l != -100]
for prediction, label in zip(predictions, labels)
]
return true_labels, true_predictions
然后我们可以编写训练循环。在定义一个进度条来跟踪训练的进度后,循环分为三个部分:
- 训练本身,这是经典的迭代过程,即在
train_dataloader
上进行迭代,在模型上前向传播训练数据,然后反向传递loss
和优化参数 - 评估,在获取模型在一个
batch
上的输出之后,这里有一个需要注意的地方:由于两个进程可能已将输入和标签填充到不同的形状,我们需要使用accelerator.pad_across_processes()
使预测和真实的标签在调用gather()
方法之前具有相同的形状。如果我们不这样做,评估循环将会出错或无限期卡住。最后我们将结果发送到metric.add_batch()
方法中,并在评估循环结束时调用metric.compute()
方法。 - 保存和上传,首先保存模型和
tokenizer
然后调用repo.push_to_hub()
方法。注意,我们使用参数blocking=False
来告诉 🤗Hub
库在一个异步进程中推送。这样,在训练时,这个指令在后台将模型和tokenizer
推送到hub
。
以下是完整的训练循环代码:
from tqdm.auto import tqdm
import torch
progress_bar = tqdm(range(num_training_steps))
for epoch in range(num_train_epochs):
# 训练
model.train()
for batch in train_dataloader:
outputs = model(**batch)
loss = outputs.loss
accelerator.backward(loss)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
progress_bar.update(1)
# 评估
model.eval()
for batch in eval_dataloader:
with torch.no_grad():
outputs = model(**batch)
predictions = outputs.logits.argmax(dim=-1)
labels = batch["labels"]
# 填充模型的预测和标签后才能调用 gathere()
predictions = accelerator.pad_across_processes(predictions, dim=1, pad_index=-100)
labels = accelerator.pad_across_processes(labels, dim=1, pad_index=-100)
predictions_gathered = accelerator.gather(predictions)
labels_gathered = accelerator.gather(labels)
true_predictions, true_labels = postprocess(predictions_gathered, labels_gathered)
metric.add_batch(predictions=true_predictions, references=true_labels)
results = metric.compute()
print(
f"epoch {epoch}:",
{
key: results[f"overall_{key}"]
for key in ["precision", "recall", "f1", "accuracy"]
},
)
# 保存并上传
accelerator.wait_for_everyone()
unwrapped_model = accelerator.unwrap_model(model)
unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)
if accelerator.is_main_process:
tokenizer.save_pretrained(output_dir)
repo.push_to_hub(
commit_message=f"Training in progress epoch {epoch}", blocking=False
)
如果这是你第一次看到使用 🤗 Accelerate 保存模型,让我们花点时间来了解一下这个过程中的三行代码:
accelerator.wait_for_everyone() unwrapped_model = accelerator.unwrap_model(model) unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)
第一行是不言自明的:它告诉所有的进程先等待,直到所有的进程都处于这个阶段再继续(阻塞)。这是为了确保在保存之前,我们在每个进程中都有相同的模型。
第二行代码用于获取 unwrapped_model
,它就是我们定义的基本模型。 accelerator.prepare()
方法会为了在分布式训练中工作而对模型进行了一些修改,所以它不再有 save_pretraining()
方法;使用 accelerator.unwrap_model()
方法可以撤销对模型的更改。
在第三行代码中,我们调用 save_pretraining()
,并指定 accelerator.save()
作为 save_function
而不是默认的 torch.save()
。
完成这些操作后,你应该拥有一个与 Trainer
训练出的模型结果相当类似的模型。你可以在 huggingface-course/bert-finetuned-ner-accelerate 查看我们使用这些代码训练的模型。如果你想在训练循环中测试任何调整,你可以直接通过编辑上面显示的代码来实现它们!
使用微调模型
我们已经向你展示了如何使用在模型中心微调的模型和推理小部件。在本地使用 pipeline
来使用它非常容易,你只需要指定正确的模型标签:
from transformers import pipeline
# 将此替换为你自己的 checkpoint
model_checkpoint = "huggingface-course/bert-finetuned-ner"
token_classifier = pipeline(
"token-classification", model=model_checkpoint, aggregation_strategy="simple"
)
token_classifier("My name is Sylvain and I work at Hugging Face in Brooklyn.")
[{'entity_group': 'PER', 'score': 0.9988506, 'word': 'Sylvain', 'start': 11, 'end': 18},
{'entity_group': 'ORG', 'score': 0.9647625, 'word': 'Hugging Face', 'start': 33, 'end': 45},
{'entity_group': 'LOC', 'score': 0.9986118, 'word': 'Brooklyn', 'start': 49, 'end': 57}]
太棒了!我们的模型与此管道的默认模型一样有效!
< > Update on GitHub