LLM 課程文件
詞元分類
並獲得增強的文件體驗
開始使用
Token 分類
我們將探索的第一個應用是 token 分類。這項通用任務涵蓋了任何可以表述為“為句子中的每個 token 歸因一個標籤”的問題,例如:
- 命名實體識別 (NER):在句子中找到實體(如人名、地點或組織)。這可以透過為每個 token 分配一個標籤來表述,其中每個實體有一個類別,另外一個類別用於“非實體”。
- 詞性標註 (POS):將句子中的每個詞標記為對應的特定詞性(如名詞、動詞、形容詞等)。
- 組塊化 (Chunking):找到屬於同一實體的 token。這項任務(可以與 POS 或 NER 結合使用)可以表述為:為組塊開頭的任何 token 分配一個標籤(通常是
B-
),為組塊內部的 token 分配另一個標籤(通常是I-
),以及為不屬於任何組塊的 token 分配第三個標籤(通常是O
)。
當然,還有許多其他型別的 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
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 和組塊化。與其他資料集的一個巨大區別是,輸入文字不以句子或文件的形式呈現,而是以詞語列表的形式(最後一列稱為 tokens
,但它包含的詞語是經過預分詞的輸入,仍需要透過分詞器進行子詞分詞)。
讓我們看看訓練集的第一個元素:
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)
所以這一列包含的元素是 ClassLabel
序列。序列元素的型別在 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
表示該詞對應於一個*雜項*實體的開頭/內部。
現在,解碼我們之前看到的標籤會得到:
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'
對於一個混合了 B-
和 I-
標籤的例子,在訓練集索引 4 的元素上,相同的程式碼會給我們:
'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 或分塊標籤的相同兩個句子。
處理資料
像往常一樣,我們的文字需要轉換為 token ID,模型才能理解它們。正如我們在第六章中看到的,token 分類任務的一個重要區別是,我們有預分詞的輸入。幸運的是,tokenizer API 可以很輕鬆地處理這個問題;我們只需要用一個特殊標誌來警告 tokenizer
。
首先,讓我們建立 tokenizer
物件。正如我們之前所說,我們將使用 BERT 預訓練模型,所以我們將首先下載並快取相關的 tokenizer:
from transformers import AutoTokenizer
model_checkpoint = "bert-base-cased"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
你可以用Hub上的任何其他模型替換 model_checkpoint
,或者用你儲存了預訓練模型和 tokenizer 的本地資料夾替換。唯一的限制是 tokenizer 需要由 🤗 Tokenizers 庫支援,因此有一個“fast”版本可用。你可以在這張大表中看到所有帶有 fast 版本的架構,並且要檢查你正在使用的 tokenizer
物件是否確實由 🤗 Tokenizers 支援,你可以檢視它的 is_fast
屬性:
tokenizer.is_fast
True
要對預分詞的輸入進行分詞,我們可以像往常一樣使用我們的 tokenizer
,只需新增 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]']
正如我們所看到的,分詞器添加了模型使用的特殊標記(開頭是 [CLS]
,結尾是 [SEP]
),並且大部分單詞保持不變。然而,單詞 lamb
被分詞為兩個子詞:la
和 ##mb
。這導致了我們的輸入和標籤之間存在不匹配:標籤列表只有 9 個元素,而我們的輸入現在有 12 個標記。處理特殊標記很容易(我們知道它們在開頭和結尾),但我們還需要確保將所有標籤與正確的單詞對齊。
幸運的是,由於我們使用的是快速分詞器,我們可以使用 🤗 Tokenizers 的超能力,這意味著我們可以輕鬆地將每個 token 對映到其對應的單詞(如第六章中所述):
inputs.word_ids()
[None, 0, 1, 2, 3, 4, 5, 6, 7, 7, 8, None]
稍作修改,我們就可以擴充套件標籤列表以匹配 token。我們將應用的第一條規則是特殊 token 的標籤為 -100
。這是因為預設情況下,-100
是我們稍後將使用的損失函式(交叉熵)中忽略的索引。然後,每個 token 都獲得與其所在單詞開頭的 token 相同的標籤,因為它們屬於同一個實體。對於單詞內部但不在開頭的 token,我們將 B-
替換為 I-
(因為該 token 不以實體開頭)。
def align_labels_with_tokens(labels, word_ids):
new_labels = []
current_word = None
for word_id in word_ids:
if word_id != current_word:
# Start of a new 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:
# Special token
new_labels.append(-100)
else:
# Same word as previous token
label = labels[word_id]
# If the label is B-XXX we change it to 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]
正如我們所看到的,我們的函式在開頭和結尾添加了兩個特殊 token 的 -100
,併為我們被拆分為兩個 token 的單詞添加了一個新的 0
。
✏️ 輪到你了! 有些研究人員更喜歡每個詞只分配一個標籤,並將 -100
分配給給定詞中的其他子 token。這是為了避免那些被拆分成許多子 token 的長詞對損失函式造成過大的影響。請按照此規則更改前面的函式,以將標籤與輸入 ID 對齊。
為了預處理我們的整個資料集,我們需要對所有輸入進行分詞,並對所有標籤應用 align_labels_with_tokens()
。為了利用我們快速分詞器的速度,最好同時對大量文字進行分詞,所以我們將編寫一個函式來處理示例列表,並使用帶有 batched=True
選項的 Dataset.map()
方法。與我們之前的示例唯一不同的是,當分詞器的輸入是文字列表(或者在我們的例子中是詞語列表的列表)時,word_ids()
函式需要獲取我們想要其詞語 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
請注意,我們尚未填充輸入;我們將在稍後使用資料收集器建立批次時進行填充。
現在我們可以一次性地對資料集的其他拆分應用所有這些預處理:
tokenized_datasets = raw_datasets.map(
tokenize_and_align_labels,
batched=True,
remove_columns=raw_datasets["train"].column_names,
)
最困難的部分已經完成了!現在資料已經預處理完畢,實際的訓練將與我們在第三章中所做的非常相似。
使用 Trainer API 微調模型
使用 Trainer
的實際程式碼將與之前相同;唯一的更改是資料如何整理成批次以及度量計算函式。
資料整理
我們不能像第三章那樣簡單地使用 DataCollatorWithPadding
,因為它只填充輸入(輸入 ID、注意力掩碼和 token 型別 ID)。在這裡,我們的標籤應該以與輸入完全相同的方式進行填充,以便它們保持相同的大小,使用 -100
作為值,這樣相應的預測在損失計算中就會被忽略。
這一切都由一個DataCollatorForTokenClassification
完成。與 DataCollatorWithPadding
一樣,它也接受用於預處理輸入的 tokenizer
:
from transformers import DataCollatorForTokenClassification
data_collator = DataCollatorForTokenClassification(tokenizer=tokenizer)
要在幾個樣本上進行測試,我們只需在來自我們 token 化的訓練集的一些示例列表上呼叫它:
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
在每個 epoch 計算一個度量,我們需要定義一個 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()
函式以返回您希望報告的所有指標。
這個 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)
# Remove ignored index (special tokens) and convert to labels
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 到標籤以及從標籤到 ID 的對映:
id2label = {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 並定義我們的訓練引數。如果你在筆記本中工作,有一個便捷函式可以幫助你完成此操作:
from huggingface_hub import notebook_login
notebook_login()
這將顯示一個可以輸入您的 Hugging Face 登入憑據的小部件。
如果您不在筆記本中工作,只需在終端中輸入以下行
huggingface-cli 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,
)
您以前見過其中的大部分:我們設定了一些超引數(如學習率、訓練 epoch 數量和權重衰減),並指定 push_to_hub=True
以表明我們希望儲存模型並在每個 epoch 結束時對其進行評估,並且我們希望將結果上傳到模型 Hub。請注意,您可以使用 hub_model_id
引數指定要推送到的儲存庫名稱(特別是,您將不得不使用此引數推送到組織)。例如,當我們將模型推送到huggingface-course
組織時,我們向 TrainingArguments
添加了 hub_model_id="huggingface-course/bert-finetuned-ner"
。預設情況下,使用的儲存庫將在您的名稱空間中,並以您設定的輸出目錄命名,因此在我們的例子中將是 "sgugger/bert-finetuned-ner"
。
💡 如果您使用的輸出目錄已經存在,它需要是您要推送到的儲存庫的本地克隆。如果不是,您在定義 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,
processing_class=tokenizer,
)
trainer.train()
請注意,在訓練過程中,每次模型儲存時(此處是每個 epoch),它都會在後臺上傳到 Hub。這樣,如有必要,您就可以在另一臺機器上恢復訓練。
訓練完成後,我們使用 push_to_hub()
方法確保上傳模型的最新版本:
trainer.push_to_hub(commit_message="Training complete")
此命令返回剛剛提交的 URL,如果您想檢查它:
'https://huggingface.co/sgugger/bert-finetuned-ner/commit/26ab21e5b1568f9afeccdaed2d8715f571d786ed'
訓練器還會起草一份包含所有評估結果的模型卡並將其上傳。在此階段,您可以使用模型中心上的推理小部件來測試您的模型並與您的朋友分享。您已成功微調了 token 分類任務模型——恭喜!
如果您想更深入地瞭解訓練迴圈,我們現在將向您展示如何使用 🤗 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()
,我們可以使用它的長度來計算訓練步驟的數量。請記住,我們應該總是在準備完資料載入器之後再執行此操作,因為該方法會更改其長度。我們使用經典的從學習率到 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()
函式所做的工作):
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
中的任何內容。這將幫助我們在每個 epoch 結束時上傳中間模型。
訓練迴圈
我們現在準備編寫完整的訓練迴圈。為了簡化其評估部分,我們定義了 postprocess()
函式,該函式接受預測和標籤,並將其轉換為字串列表,正如我們的 metric
物件所期望的那樣:
def postprocess(predictions, labels):
predictions = predictions.detach().cpu().clone().numpy()
labels = labels.detach().cpu().clone().numpy()
# Remove ignored index (special tokens) and convert to labels
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
的經典迭代,模型前向傳播,然後反向傳播和最佳化器步進。 - 評估部分,在獲取模型在批次上的輸出後,有一個新穎之處:由於兩個程序可能將輸入和標籤填充成不同的形狀,我們需要使用
accelerator.pad_across_processes()
來使預測和標籤具有相同的形狀,然後才能呼叫gather()
方法。如果我們不這樣做,評估將要麼出錯,要麼永遠掛起。然後我們將結果傳送到metric.add_batch()
,並在評估迴圈結束後呼叫metric.compute()
。 - 儲存和上傳,我們首先儲存模型和 tokenizer,然後呼叫
repo.push_to_hub()
。請注意,我們使用引數blocking=False
來告訴 🤗 Hub 庫以非同步程序推送。這樣,訓練正常進行,並且此(長時間)指令在後臺執行。
這是訓練迴圈的完整程式碼:
from tqdm.auto import tqdm
import torch
progress_bar = tqdm(range(num_training_steps))
for epoch in range(num_train_epochs):
# Training
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)
# Evaluation
model.eval()
for batch in eval_dataloader:
with torch.no_grad():
outputs = model(**batch)
predictions = outputs.logits.argmax(dim=-1)
labels = batch["labels"]
# Necessary to pad predictions and labels for being gathered
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"]
},
)
# Save and upload
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_pretrained()
方法;accelerator.unwrap_model()
方法會撤銷該步驟。最後,我們呼叫 save_pretrained()
但告訴該方法使用 accelerator.save()
而不是 torch.save()
。
完成此操作後,您應該會得到一個與使用 Trainer
訓練的模型產生非常相似結果的模型。您可以在huggingface-course/bert-finetuned-ner-accelerate檢視我們使用此程式碼訓練的模型。如果您想測試對訓練迴圈的任何調整,可以直接透過編輯上面顯示的程式碼來實現!
使用微調模型
我們已經向您展示瞭如何在模型 Hub 上使用我們微調的模型進行推理。要在 pipeline
中本地使用它,您只需指定正確的模型識別符號:
from transformers import pipeline
# Replace this with your own 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}]
太棒了!我們的模型與此管道的預設模型一樣出色!
< > 在 GitHub 上更新