LLM 課程文件
微調掩碼語言模型
並獲得增強的文件體驗
開始使用
微調掩碼語言模型
對於涉及 Transformer 模型的許多 NLP 應用程式,您可以簡單地從 Hugging Face Hub 中獲取一個預訓練模型,並直接在您的資料上針對手頭的任務進行微調。只要用於預訓練的語料庫與用於微調的語料庫沒有太大差異,遷移學習通常會產生良好的結果。
然而,在某些情況下,您需要先在您的資料上微調語言模型,然後才能訓練特定任務的頭。例如,如果您的資料集包含法律合同或科學文章,像 BERT 這樣的普通 Transformer 模型通常會將語料庫中的領域特定詞彙視為稀有標記,並且最終的效能可能不盡如人意。透過在領域內資料上微調語言模型,您可以提高許多下游任務的效能,這意味著您通常只需要執行此步驟一次!
這種在領域內資料上微調預訓練語言模型的過程通常被稱為*領域適應*。它在 2018 年被 ULMFiT 推廣開來,ULMFiT 是首批真正實現 NLP 遷移學習的神經架構之一(基於 LSTM)。下圖展示了 ULMFiT 進行領域適應的一個例子;在本節中,我們將做類似的事情,但使用 Transformer 而不是 LSTM!
在本節結束時,您將在 Hub 上擁有一個掩碼語言模型,它可以像下面這樣自動補全句子
讓我們深入瞭解!
🙋 如果“掩碼語言建模”和“預訓練模型”這些術語對您來說不熟悉,請檢視第 1 章,其中我們解釋了所有這些核心概念,並附有影片!
選擇用於掩碼語言建模的預訓練模型
首先,讓我們選擇一個合適的預訓練模型用於掩碼語言建模。如下圖所示,您可以透過在 Hugging Face Hub 上應用“填充掩碼”過濾器來查詢候選模型列表

儘管 BERT 和 RoBERTa 系列模型下載量最大,但我們將使用一個名為 DistilBERT 的模型,它可以在下游效能損失很小甚至沒有損失的情況下更快地進行訓練。該模型採用一種特殊技術進行訓練,稱為 知識蒸餾,其中一個大型的“教師模型”(如 BERT)用於指導引數少得多的“學生模型”的訓練。知識蒸餾的詳細解釋會使我們偏離本節主題太遠,但如果您感興趣,可以在 自然語言處理與 Transformer(俗稱 Transformer 教科書)中閱讀所有相關內容。
我們現在開始使用 AutoModelForMaskedLM
類下載 DistilBERT。
from transformers import AutoModelForMaskedLM
model_checkpoint = "distilbert-base-uncased"
model = AutoModelForMaskedLM.from_pretrained(model_checkpoint)
我們可以透過呼叫 num_parameters()
方法檢視該模型有多少個引數。
distilbert_num_parameters = model.num_parameters() / 1_000_000
print(f"'>>> DistilBERT number of parameters: {round(distilbert_num_parameters)}M'")
print(f"'>>> BERT number of parameters: 110M'")
'>>> DistilBERT number of parameters: 67M'
'>>> BERT number of parameters: 110M'
DistilBERT 大約有 6700 萬個引數,比 BERT 基礎模型小兩倍左右,這大致意味著訓練速度提高了兩倍——不錯!現在讓我們看看這個模型預測的一小段文字中最可能的補全是什麼。
text = "This is a great [MASK]."
作為人類,我們可以想象 [MASK]
標記有許多可能性,例如“day”、“ride”或“painting”。對於預訓練模型,預測取決於模型訓練所用的語料庫,因為它會學習拾取資料中存在的統計模式。與 BERT 一樣,DistilBERT 是在 英語維基百科 和 BookCorpus 資料集上預訓練的,因此我們期望 [MASK]
的預測能夠反映這些領域。為了預測掩碼,我們需要 DistilBERT 的分詞器為模型生成輸入,所以我們也要從 Hub 下載它。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
有了分詞器和模型,我們現在可以將文字示例傳遞給模型,提取 logits,並打印出前 5 個候選詞。
import torch
inputs = tokenizer(text, return_tensors="pt")
token_logits = model(**inputs).logits
# Find the location of [MASK] and extract its logits
mask_token_index = torch.where(inputs["input_ids"] == tokenizer.mask_token_id)[1]
mask_token_logits = token_logits[0, mask_token_index, :]
# Pick the [MASK] candidates with the highest logits
top_5_tokens = torch.topk(mask_token_logits, 5, dim=1).indices[0].tolist()
for token in top_5_tokens:
print(f"'>>> {text.replace(tokenizer.mask_token, tokenizer.decode([token]))}'")
'>>> This is a great deal.'
'>>> This is a great success.'
'>>> This is a great adventure.'
'>>> This is a great idea.'
'>>> This is a great feat.'
從輸出中我們可以看出,模型的預測都是日常用語,考慮到其基於英語維基百科,這或許並不令人意外。現在,讓我們看看如何將這個領域改變成一個更小眾的領域——高度兩極分化的電影評論!
資料集
為了展示領域適應,我們將使用著名的 大型電影評論資料集(簡稱 IMDb),這是一個電影評論語料庫,常用於情感分析模型的基準測試。透過在此語料庫上微調 DistilBERT,我們期望語言模型能夠將其詞彙從預訓練的維基百科事實資料調整為更主觀的電影評論元素。我們可以使用 🤗 Datasets 的 load_dataset()
函式從 Hugging Face Hub 獲取資料
from datasets import load_dataset
imdb_dataset = load_dataset("imdb")
imdb_dataset
DatasetDict({
train: Dataset({
features: ['text', 'label'],
num_rows: 25000
})
test: Dataset({
features: ['text', 'label'],
num_rows: 25000
})
unsupervised: Dataset({
features: ['text', 'label'],
num_rows: 50000
})
})
我們可以看到,`train` 和 `test` 分割各包含 25,000 條評論,而 `unsupervised` 分割包含 50,000 條未標註評論。讓我們檢視一些樣本,瞭解我們正在處理的文字型別。正如我們在課程前幾章中所做的那樣,我們將連結 `Dataset.shuffle()` 和 `Dataset.select()` 函式以建立隨機樣本。
sample = imdb_dataset["train"].shuffle(seed=42).select(range(3))
for row in sample:
print(f"\n'>>> Review: {row['text']}'")
print(f"'>>> Label: {row['label']}'")
'>>> Review: This is your typical Priyadarshan movie--a bunch of loony characters out on some silly mission. His signature climax has the entire cast of the film coming together and fighting each other in some crazy moshpit over hidden money. Whether it is a winning lottery ticket in Malamaal Weekly, black money in Hera Pheri, "kodokoo" in Phir Hera Pheri, etc., etc., the director is becoming ridiculously predictable. Don\'t get me wrong; as clichéd and preposterous his movies may be, I usually end up enjoying the comedy. However, in most his previous movies there has actually been some good humor, (Hungama and Hera Pheri being noteworthy ones). Now, the hilarity of his films is fading as he is using the same formula over and over again.<br /><br />Songs are good. Tanushree Datta looks awesome. Rajpal Yadav is irritating, and Tusshar is not a whole lot better. Kunal Khemu is OK, and Sharman Joshi is the best.'
'>>> Label: 0'
'>>> Review: Okay, the story makes no sense, the characters lack any dimensionally, the best dialogue is ad-libs about the low quality of movie, the cinematography is dismal, and only editing saves a bit of the muddle, but Sam" Peckinpah directed the film. Somehow, his direction is not enough. For those who appreciate Peckinpah and his great work, this movie is a disappointment. Even a great cast cannot redeem the time the viewer wastes with this minimal effort.<br /><br />The proper response to the movie is the contempt that the director San Peckinpah, James Caan, Robert Duvall, Burt Young, Bo Hopkins, Arthur Hill, and even Gig Young bring to their work. Watch the great Peckinpah films. Skip this mess.'
'>>> Label: 0'
'>>> Review: I saw this movie at the theaters when I was about 6 or 7 years old. I loved it then, and have recently come to own a VHS version. <br /><br />My 4 and 6 year old children love this movie and have been asking again and again to watch it. <br /><br />I have enjoyed watching it again too. Though I have to admit it is not as good on a little TV.<br /><br />I do not have older children so I do not know what they would think of it. <br /><br />The songs are very cute. My daughter keeps singing them over and over.<br /><br />Hope this helps.'
'>>> Label: 1'
是的,這些確實是電影評論,如果你年齡足夠大,甚至可能理解最後一條評論中關於擁有 VHS 版本的內容😜!儘管我們不需要這些標籤進行語言建模,但我們已經可以看到 0
表示負面評論,而 1
對應正面評論。
✏️ **試一試!** 建立一個 unsupervised
分割的隨機樣本,並驗證其標籤既不是 0
也不是 1
。同時,你還可以檢查 train
和 test
分割中的標籤是否確實是 0
或 1
— 這是每個 NLP 從業者在開始新專案時都應該執行的一個有用的健全性檢查!
現在我們已經快速瀏覽了資料,接下來我們深入探討如何為掩碼語言建模準備資料。正如我們將看到的,與第三章中的序列分類任務相比,還需要一些額外的步驟。讓我們開始吧!
資料預處理
對於自迴歸和掩碼語言建模,一個常見的預處理步驟是連線所有示例,然後將整個語料庫分成大小相等的塊。這與我們通常只對單個示例進行分詞的方法大相徑庭。為什麼要將所有內容連線起來?原因在於,如果單個示例太長,它們可能會被截斷,從而導致丟失對語言建模任務可能有用的資訊!
所以,首先,我們將像往常一樣對語料庫進行分詞,但**不**在分詞器中設定 truncation=True
選項。如果可用,我們還會獲取詞 ID(如果使用快速分詞器,如第 6 章所述,詞 ID 將可用),因為我們稍後將需要它們來進行全詞掩碼。我們將此包裝在一個簡單的函式中,同時我們還將刪除 text
和 label
列,因為我們不再需要它們。
def tokenize_function(examples):
result = tokenizer(examples["text"])
if tokenizer.is_fast:
result["word_ids"] = [result.word_ids(i) for i in range(len(result["input_ids"]))]
return result
# Use batched=True to activate fast multithreading!
tokenized_datasets = imdb_dataset.map(
tokenize_function, batched=True, remove_columns=["text", "label"]
)
tokenized_datasets
DatasetDict({
train: Dataset({
features: ['attention_mask', 'input_ids', 'word_ids'],
num_rows: 25000
})
test: Dataset({
features: ['attention_mask', 'input_ids', 'word_ids'],
num_rows: 25000
})
unsupervised: Dataset({
features: ['attention_mask', 'input_ids', 'word_ids'],
num_rows: 50000
})
})
由於 DistilBERT 是一個 BERT 類模型,我們可以看到編碼後的文字由我們在其他章節中見過的 input_ids
和 attention_mask
,以及我們新增的 word_ids
組成。
現在我們已經對電影評論進行了分詞,下一步是將它們全部組合在一起,並將結果分成若干塊。但是這些塊應該有多大呢?這最終將由您可用的 GPU 記憶體量決定,但一個好的起點是檢視模型的最大上下文大小。這可以透過檢查分詞器的 model_max_length
屬性來推斷。
tokenizer.model_max_length
512
此值派生自與檢查點關聯的 *tokenizer_config.json* 檔案;在此示例中,我們可以看到上下文大小為 512 個標記,與 BERT 相同。
✏️ **試一試!** 某些 Transformer 模型,例如 BigBird 和 Longformer,其上下文長度比 BERT 和其他早期 Transformer 模型長得多。例項化其中一個檢查點的分詞器,並驗證 model_max_length
與其模型卡上引用的值一致。
所以,為了在 Google Colab 等平臺上執行我們的實驗,我們將選擇一個稍微小一點的尺寸,以便能夠適應記憶體。
chunk_size = 128
請注意,在實際場景中,使用較小的塊大小可能會帶來不利影響,因此您應該使用與您將模型應用到的用例相對應的塊大小。
現在到了有趣的部分。為了展示連線如何工作,讓我們從我們分詞後的訓練集中取出幾條評論,並打印出每條評論的詞元數量。
# Slicing produces a list of lists for each feature
tokenized_samples = tokenized_datasets["train"][:3]
for idx, sample in enumerate(tokenized_samples["input_ids"]):
print(f"'>>> Review {idx} length: {len(sample)}'")
'>>> Review 0 length: 200'
'>>> Review 1 length: 559'
'>>> Review 2 length: 192'
然後,我們可以透過一個簡單的字典理解來連線所有這些示例,如下所示:
concatenated_examples = {
k: sum(tokenized_samples[k], []) for k in tokenized_samples.keys()
}
total_length = len(concatenated_examples["input_ids"])
print(f"'>>> Concatenated reviews length: {total_length}'")
'>>> Concatenated reviews length: 951'
太棒了,總長度檢查無誤——現在我們把連線後的評論分成 chunk_size
大小的塊。為此,我們遍歷 concatenated_examples
中的特徵,並使用列表推導式建立每個特徵的切片。結果是一個字典,其中包含每個特徵的塊。
chunks = {
k: [t[i : i + chunk_size] for i in range(0, total_length, chunk_size)]
for k, t in concatenated_examples.items()
}
for chunk in chunks["input_ids"]:
print(f"'>>> Chunk length: {len(chunk)}'")
'>>> Chunk length: 128'
'>>> Chunk length: 128'
'>>> Chunk length: 128'
'>>> Chunk length: 128'
'>>> Chunk length: 128'
'>>> Chunk length: 128'
'>>> Chunk length: 128'
'>>> Chunk length: 55'
如您在此示例中看到的,最後一個塊通常會小於最大塊大小。處理這種情況有兩種主要策略:
- 如果最後一個塊小於
chunk_size
,則丟棄它。 - 填充最後一個塊,直到其長度等於
chunk_size
。
我們這裡將採用第一種方法,所以讓我們將上述所有邏輯封裝在一個函式中,以便將其應用於我們分詞後的資料集。
def group_texts(examples):
# Concatenate all texts
concatenated_examples = {k: sum(examples[k], []) for k in examples.keys()}
# Compute length of concatenated texts
total_length = len(concatenated_examples[list(examples.keys())[0]])
# We drop the last chunk if it's smaller than chunk_size
total_length = (total_length // chunk_size) * chunk_size
# Split by chunks of max_len
result = {
k: [t[i : i + chunk_size] for i in range(0, total_length, chunk_size)]
for k, t in concatenated_examples.items()
}
# Create a new labels column
result["labels"] = result["input_ids"].copy()
return result
請注意,在 `group_texts()` 的最後一步中,我們建立了一個新的 `labels` 列,它是 `input_ids` 列的副本。正如我們很快會看到的,這是因為在掩碼語言建模中,目標是預測輸入批次中隨機掩碼的詞元,透過建立 `labels` 列,我們為語言模型提供了學習的真實標籤。
現在,讓我們使用我們值得信賴的 Dataset.map()
函式將 group_texts()
應用於我們分詞後的資料集。
lm_datasets = tokenized_datasets.map(group_texts, batched=True)
lm_datasets
DatasetDict({
train: Dataset({
features: ['attention_mask', 'input_ids', 'labels', 'word_ids'],
num_rows: 61289
})
test: Dataset({
features: ['attention_mask', 'input_ids', 'labels', 'word_ids'],
num_rows: 59905
})
unsupervised: Dataset({
features: ['attention_mask', 'input_ids', 'labels', 'word_ids'],
num_rows: 122963
})
})
您可以看到,對文字進行分組然後分塊後,生成的示例比我們原始的 25,000 個 train
和 test
分割示例多得多。這是因為我們現在擁有涉及*連續詞元*的示例,這些示例跨越了原始語料庫中的多個示例。您可以透過檢視其中一個塊中的特殊 [SEP]
和 [CLS]
詞元來明確看到這一點。
tokenizer.decode(lm_datasets["train"][1]["input_ids"])
".... at.......... high. a classic line : inspector : i'm here to sack one of your teachers. student : welcome to bromwell high. i expect that many adults of my age think that bromwell high is far fetched. what a pity that it isn't! [SEP] [CLS] homelessness ( or houselessness as george carlin stated ) has been an issue for years but never a plan to help those on the street that were once considered human who did everything from going to school, work, or vote for the matter. most people think of the homeless"
在這個例子中,您可以看到兩篇重疊的電影評論,一篇關於高中電影,另一篇關於無家可歸。我們再來看看掩碼語言建模的標籤是什麼樣子的。
tokenizer.decode(lm_datasets["train"][1]["labels"])
".... at.......... high. a classic line : inspector : i'm here to sack one of your teachers. student : welcome to bromwell high. i expect that many adults of my age think that bromwell high is far fetched. what a pity that it isn't! [SEP] [CLS] homelessness ( or houselessness as george carlin stated ) has been an issue for years but never a plan to help those on the street that were once considered human who did everything from going to school, work, or vote for the matter. most people think of the homeless"
正如我們上面 `group_texts()` 函式所預期的那樣,這看起來與解碼後的 `input_ids` 完全相同——但我們的模型如何才能學到東西呢?我們缺少一個關鍵步驟:在輸入中隨機位置插入 `[MASK]` 詞元!讓我們看看如何在使用特殊資料校對器進行微調時動態地做到這一點。
使用 Trainer API 微調 DistilBERT
微調掩碼語言模型與微調序列分類模型幾乎相同,就像我們在第三章中做的那樣。唯一的區別是我們需要一個特殊的資料整理器,它可以隨機掩碼每批文字中的一些標記。幸運的是,🤗 Transformers 準備了專門的 DataCollatorForLanguageModeling
用於此任務。我們只需要將它傳遞給分詞器和一個 mlm_probability
引數,該引數指定要掩碼的標記的比例。我們將選擇 15%,這是 BERT 使用的量,也是文獻中的常見選擇。
from transformers import DataCollatorForLanguageModeling
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm_probability=0.15)
為了瞭解隨機掩碼的工作原理,讓我們向資料校對器提供一些示例。由於它期望一個 dict
列表,其中每個 dict
表示一個連續文字塊,我們首先在將批次提供給校對器之前遍歷資料集。我們刪除此資料校對器的 "word_ids"
鍵,因為它不需要它。
samples = [lm_datasets["train"][i] for i in range(2)]
for sample in samples:
_ = sample.pop("word_ids")
for chunk in data_collator(samples)["input_ids"]:
print(f"\n'>>> {tokenizer.decode(chunk)}'")
'>>> [CLS] bromwell [MASK] is a cartoon comedy. it ran at the same [MASK] as some other [MASK] about school life, [MASK] as " teachers ". [MASK] [MASK] [MASK] in the teaching [MASK] lead [MASK] to believe that bromwell high\'[MASK] satire is much closer to reality than is " teachers ". the scramble [MASK] [MASK] financially, the [MASK]ful students whogn [MASK] right through [MASK] pathetic teachers\'pomp, the pettiness of the whole situation, distinction remind me of the schools i knew and their students. when i saw [MASK] episode in [MASK] a student repeatedly tried to burn down the school, [MASK] immediately recalled. [MASK]...'
'>>> .... at.. [MASK]... [MASK]... high. a classic line plucked inspector : i\'[MASK] here to [MASK] one of your [MASK]. student : welcome to bromwell [MASK]. i expect that many adults of my age think that [MASK]mwell [MASK] is [MASK] fetched. what a pity that it isn\'t! [SEP] [CLS] [MASK]ness ( or [MASK]lessness as george 宇in stated )公 been an issue for years but never [MASK] plan to help those on the street that were once considered human [MASK] did everything from going to school, [MASK], [MASK] vote for the matter. most people think [MASK] the homeless'
太棒了,成功了!我們可以看到 [MASK]
標記已被隨機插入到文字中的各個位置。這些將是我們的模型在訓練期間必須預測的標記——而且資料校對器的好處在於它會在每個批次中隨機化 [MASK]
的插入!
✏️ **試一試!** 多次執行上面的程式碼片段,親眼看看隨機掩碼的發生!還可以將 tokenizer.decode()
方法替換為 tokenizer.convert_ids_to_tokens()
,以檢視有時給定單詞的單個標記被掩碼,而其他標記沒有被掩碼。
隨機掩碼的一個副作用是,當使用 Trainer
時,我們的評估指標將不確定,因為我們對訓練集和測試集使用相同的資料校對器。我們稍後會看到,當我們使用 🤗 Accelerate 進行微調時,如何利用自定義評估迴圈的靈活性來凍結隨機性。
在訓練掩碼語言模型時,一種可以使用的技術是整體掩碼詞語,而不僅僅是單個標記。這種方法稱為*全詞掩碼*。如果我們要使用全詞掩碼,我們將需要自己構建一個數據校對器。資料校對器只是一個函式,它接受一個樣本列表並將它們轉換為一個批次,所以我們現在就來做!我們將使用之前計算的詞 ID 來建立一個詞索引與相應標記之間的對映,然後隨機決定要掩碼哪些詞,並將該掩碼應用於輸入。請注意,除了對應於掩碼詞的標籤外,所有標籤均為 -100
。
import collections
import numpy as np
from transformers import default_data_collator
wwm_probability = 0.2
def whole_word_masking_data_collator(features):
for feature in features:
word_ids = feature.pop("word_ids")
# Create a map between words and corresponding token indices
mapping = collections.defaultdict(list)
current_word_index = -1
current_word = None
for idx, word_id in enumerate(word_ids):
if word_id is not None:
if word_id != current_word:
current_word = word_id
current_word_index += 1
mapping[current_word_index].append(idx)
# Randomly mask words
mask = np.random.binomial(1, wwm_probability, (len(mapping),))
input_ids = feature["input_ids"]
labels = feature["labels"]
new_labels = [-100] * len(labels)
for word_id in np.where(mask)[0]:
word_id = word_id.item()
for idx in mapping[word_id]:
new_labels[idx] = labels[idx]
input_ids[idx] = tokenizer.mask_token_id
feature["labels"] = new_labels
return default_data_collator(features)
接下來,我們可以在相同的樣本上再次嘗試。
samples = [lm_datasets["train"][i] for i in range(2)]
batch = whole_word_masking_data_collator(samples)
for chunk in batch["input_ids"]:
print(f"\n'>>> {tokenizer.decode(chunk)}'")
'>>> [CLS] bromwell high is a cartoon comedy [MASK] it ran at the same time as some other programs about school life, such as " teachers ". my 35 years in the teaching profession lead me to believe that bromwell high\'s satire is much closer to reality than is " teachers ". the scramble to survive financially, the insightful students who can see right through their pathetic teachers\'pomp, the pettiness of the whole situation, all remind me of the schools i knew and their students. when i saw the episode in which a student repeatedly tried to burn down the school, i immediately recalled.....'
'>>> .... [MASK] [MASK] [MASK] [MASK]....... high. a classic line : inspector : i\'m here to sack one of your teachers. student : welcome to bromwell high. i expect that many adults of my age think that bromwell high is far fetched. what a pity that it isn\'t! [SEP] [CLS] homelessness ( or houselessness as george carlin stated ) has been an issue for years but never a plan to help those on the street that were once considered human who did everything from going to school, work, or vote for the matter. most people think of the homeless'
✏️ **試一試!** 多次執行上面的程式碼片段,親眼看看隨機掩碼的發生!還可以將 tokenizer.decode()
方法替換為 tokenizer.convert_ids_to_tokens()
,以檢視給定單詞的標記始終一起被掩碼。
現在我們有了兩個資料校對器,其餘的微調步驟都是標準操作。在 Google Colab 上訓練可能需要一段時間,如果你沒有幸運地獲得一個傳說中的 P100 GPU 😭,所以我們首先將訓練集的大小下采樣到幾千個示例。別擔心,我們仍然會得到一個相當不錯的語言模型!在 🤗 Datasets 中下采樣資料集的一種快速方法是使用我們在 第 5 章中看到的 Dataset.train_test_split()
函式。
train_size = 10_000
test_size = int(0.1 * train_size)
downsampled_dataset = lm_datasets["train"].train_test_split(
train_size=train_size, test_size=test_size, seed=42
)
downsampled_dataset
DatasetDict({
train: Dataset({
features: ['attention_mask', 'input_ids', 'labels', 'word_ids'],
num_rows: 10000
})
test: Dataset({
features: ['attention_mask', 'input_ids', 'labels', 'word_ids'],
num_rows: 1000
})
})
這自動建立了新的 train
和 test
分割,訓練集大小設定為 10,000 個示例,驗證集大小為訓練集的 10%——如果您有強大的 GPU,可以隨意增加!接下來我們需要做的是登入 Hugging Face Hub。如果您在筆記本中執行此程式碼,可以使用以下實用函式:
from huggingface_hub import notebook_login
notebook_login()
這將顯示一個您可以在其中輸入憑據的視窗。或者,您可以在您喜歡的終端中執行
huggingface-cli login
並在此處登入。
登入後,我們可以指定 Trainer
的引數。
from transformers import TrainingArguments
batch_size = 64
# Show the training loss with every epoch
logging_steps = len(downsampled_dataset["train"]) // batch_size
model_name = model_checkpoint.split("/")[-1]
training_args = TrainingArguments(
output_dir=f"{model_name}-finetuned-imdb",
overwrite_output_dir=True,
evaluation_strategy="epoch",
learning_rate=2e-5,
weight_decay=0.01,
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
push_to_hub=True,
fp16=True,
logging_steps=logging_steps,
)
在這裡,我們調整了一些預設選項,包括 logging_steps
以確保我們跟蹤每個 epoch 的訓練損失。我們還使用了 fp16=True
來啟用混合精度訓練,這又為我們提高了速度。預設情況下,Trainer
將刪除任何不屬於模型 forward()
方法的列。這意味著,如果您使用全詞掩碼校對器,您還需要設定 remove_unused_columns=False
,以確保我們在訓練期間不會丟失 word_ids
列。
請注意,您可以使用 hub_model_id
引數指定要推送到的倉庫名稱(特別是,您必須使用此引數才能推送到組織)。例如,當我們推送到 huggingface-course
組織時,我們向 TrainingArguments
添加了 hub_model_id="huggingface-course/distilbert-finetuned-imdb"
。預設情況下,使用的倉庫將位於您的名稱空間中,並以您設定的輸出目錄命名,因此在我們的例子中將是 "lewtun/distilbert-finetuned-imdb"
。
現在我們擁有了例項化 Trainer
的所有要素。這裡我們只使用標準 data_collator
,但您可以嘗試使用全詞掩碼 collator 並比較結果作為練習。
from transformers import Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=downsampled_dataset["train"],
eval_dataset=downsampled_dataset["test"],
data_collator=data_collator,
tokenizer=tokenizer,
)
我們現在準備執行 trainer.train()
—— 但在此之前,我們先簡單瞭解一下*困惑度*,它是評估語言模型效能的常用指標。
語言模型的困惑度
與其他任務(如文字分類或問答)不同,在這些任務中我們有一個帶標籤的語料庫可以進行訓練,而在語言建模中,我們沒有任何顯式標籤。那麼我們如何確定什麼是一個好的語言模型呢?就像手機中的自動更正功能一樣,一個好的語言模型會為語法正確的句子賦予高機率,為無意義的句子賦予低機率。為了讓您更好地瞭解這是什麼樣子,您可以線上找到整套“自動更正失敗”的例子,其中人們手機中的模型產生了一些非常有趣(且通常不恰當)的補全!
假設我們的測試集主要由語法正確的句子組成,那麼衡量語言模型質量的一種方法是計算它為測試集中所有句子的下一個詞分配的機率。高機率表明模型對未見示例沒有“感到驚訝”或“困惑”,並表明它已經學會了語言中的基本語法模式。困惑度有各種數學定義,但我們這裡將使用的定義是交叉熵損失的指數。因此,我們可以透過使用 Trainer.evaluate()
函式計算測試集上的交叉熵損失,然後取結果的指數來計算預訓練模型的困惑度。
import math
eval_results = trainer.evaluate()
print(f">>> Perplexity: {math.exp(eval_results['eval_loss']):.2f}")
>>> Perplexity: 21.75
較低的困惑度分數意味著更好的語言模型,我們在這裡可以看到我們的起始模型值有些大。讓我們看看是否可以透過微調來降低它!為此,我們首先執行訓練迴圈。
trainer.train()
然後像以前一樣計算測試集上的困惑度。
eval_results = trainer.evaluate()
print(f">>> Perplexity: {math.exp(eval_results['eval_loss']):.2f}")
>>> Perplexity: 11.32
很好——困惑度大大降低了,這說明模型已經學會了電影評論領域的一些知識!
訓練完成後,我們可以將包含訓練資訊的模型卡推送到 Hub(檢查點在訓練期間本身就會儲存)。
trainer.push_to_hub()
✏️ **輪到你了!** 更改資料校對器為全詞掩碼校對器後,執行上述訓練。你是否獲得了更好的結果?
在我們的用例中,我們不需要對訓練迴圈做任何特殊處理,但在某些情況下,您可能需要實現一些自定義邏輯。對於這些應用程式,您可以使用 🤗 Accelerate — 讓我們來看看!
使用 🤗 Accelerate 微調 DistilBERT
正如我們在 Trainer
中所看到的,微調掩碼語言模型與第三章中的文字分類示例非常相似。事實上,唯一的細微之處在於使用了特殊的資料校對器,而我們已經在前面討論過了!
然而,我們發現 DataCollatorForLanguageModeling
在每次評估時也會應用隨機掩碼,因此每次訓練執行時,我們的困惑度分數都會出現一些波動。消除這種隨機性來源的一種方法是對整個測試集應用一次掩碼,然後使用 🤗 Transformers 中的預設資料校對器在評估期間收集批次。為了瞭解這是如何工作的,讓我們實現一個簡單的函式,該函式在批次上應用掩碼,類似於我們第一次接觸 DataCollatorForLanguageModeling
時的情況。
def insert_random_mask(batch):
features = [dict(zip(batch, t)) for t in zip(*batch.values())]
masked_inputs = data_collator(features)
# Create a new "masked" column for each column in the dataset
return {"masked_" + k: v.numpy() for k, v in masked_inputs.items()}
接下來,我們將把這個函式應用到我們的測試集並刪除未掩碼的列,這樣我們就可以用掩碼的列替換它們。您可以使用全詞掩碼,方法是將上面的 data_collator
替換為相應的那個,在這種情況下,您應該刪除這裡的首行。
downsampled_dataset = downsampled_dataset.remove_columns(["word_ids"])
eval_dataset = downsampled_dataset["test"].map(
insert_random_mask,
batched=True,
remove_columns=downsampled_dataset["test"].column_names,
)
eval_dataset = eval_dataset.rename_columns(
{
"masked_input_ids": "input_ids",
"masked_attention_mask": "attention_mask",
"masked_labels": "labels",
}
)
然後我們可以像往常一樣設定資料載入器,但我們將使用 🤗 Transformers 中的 default_data_collator
用於評估集。
from torch.utils.data import DataLoader
from transformers import default_data_collator
batch_size = 64
train_dataloader = DataLoader(
downsampled_dataset["train"],
shuffle=True,
batch_size=batch_size,
collate_fn=data_collator,
)
eval_dataloader = DataLoader(
eval_dataset, batch_size=batch_size, collate_fn=default_data_collator
)
從這裡開始,我們遵循 🤗 Accelerate 的標準步驟。首先是載入一個全新的預訓練模型版本。
model = AutoModelForMaskedLM.from_pretrained(model_checkpoint)
然後我們需要指定最佳化器;我們將使用標準的 AdamW
。
from torch.optim import AdamW
optimizer = AdamW(model.parameters(), lr=5e-5)
有了這些物件,我們現在就可以使用 Accelerator
物件為訓練準備好所有東西。
from accelerate import Accelerator
accelerator = Accelerator()
model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare(
model, optimizer, train_dataloader, eval_dataloader
)
現在我們的模型、最佳化器和資料載入器都已配置好,我們可以如下指定學習率排程器:
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,
)
訓練前還有最後一件事要做:在 Hugging Face Hub 上建立一個模型倉庫!我們可以使用 🤗 Hub 庫首先生成倉庫的完整名稱:
from huggingface_hub import get_full_repo_name
model_name = "distilbert-base-uncased-finetuned-imdb-accelerate"
repo_name = get_full_repo_name(model_name)
repo_name
'lewtun/distilbert-base-uncased-finetuned-imdb-accelerate'
然後使用 🤗 Hub 中的 Repository
類建立並克隆倉庫。
from huggingface_hub import Repository
output_dir = model_name
repo = Repository(output_dir, clone_from=repo_name)
完成這些後,只需簡單地編寫完整的訓練和評估迴圈即可。
from tqdm.auto import tqdm
import torch
import math
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()
losses = []
for step, batch in enumerate(eval_dataloader):
with torch.no_grad():
outputs = model(**batch)
loss = outputs.loss
losses.append(accelerator.gather(loss.repeat(batch_size)))
losses = torch.cat(losses)
losses = losses[: len(eval_dataset)]
try:
perplexity = math.exp(torch.mean(losses))
except OverflowError:
perplexity = float("inf")
print(f">>> Epoch {epoch}: Perplexity: {perplexity}")
# 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
)
>>> Epoch 0: Perplexity: 11.397545307900472
>>> Epoch 1: Perplexity: 10.904909330983092
>>> Epoch 2: Perplexity: 10.729503505340409
太棒了,我們已經能夠評估每個 epoch 的困惑度,並確保多次訓練執行是可重現的!
使用我們微調後的模型
您可以透過 Hugging Face Hub 上的微調模型小部件或在本地使用 🤗 Transformers 的 pipeline
與您的微調模型進行互動。讓我們使用後者透過 fill-mask
pipeline 下載我們的模型。
from transformers import pipeline
mask_filler = pipeline(
"fill-mask", model="huggingface-course/distilbert-base-uncased-finetuned-imdb"
)
然後,我們可以將示例文字“This is a great [MASK]”輸入到 pipeline 中,看看前 5 個預測是什麼。
preds = mask_filler(text)
for pred in preds:
print(f">>> {pred['sequence']}")
'>>> this is a great movie.'
'>>> this is a great film.'
'>>> this is a great story.'
'>>> this is a great movies.'
'>>> this is a great character.'
棒極了——我們的模型顯然已經調整了其權重,可以預測出與電影更強烈相關的詞語!
至此,我們完成了語言模型訓練的第一次實驗。在第六節中,您將學習如何從頭開始訓練一個自迴歸模型,如 GPT-2;如果您想了解如何預訓練自己的 Transformer 模型,請前往該章節!
✏️ **試一試!** 為了量化領域適應的好處,在 IMDb 標籤上為預訓練和微調的 DistilBERT 檢查點微調一個分類器。如果你需要文字分類的複習,請檢視第 3 章。