並獲得增強文件體驗
開始使用
微調掩碼語言模型
對於許多涉及 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_datasetDatasetDict({
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 從業者都應該在新專案的開始執行!
現在我們已經快速瀏覽了資料,讓我們深入瞭解如何為掩碼語言建模準備資料。正如我們將看到的,與我們在第 3 章中看到的序列分類任務相比,需要採取一些額外的步驟。讓我們開始吧!
資料預處理
對於自迴歸和掩碼語言建模,一個常見的預處理步驟是連線所有示例,然後將整個語料庫分成大小相等的部分。這與我們通常的做法大不相同,我們通常只是對單個示例進行標記化。為什麼要將所有內容連線在一起?原因是如果單個示例太長,可能會被截斷,這會導致丟失可能對語言建模任務有用的資訊!
因此,首先,我們將像往常一樣對語料庫進行標記化,但不在標記器中設定truncation=True選項。如果可用(如果我們使用的是快速標記器,如第 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_datasetsDatasetDict({
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 上的 GPU(如 Google Colab 上的 GPU)上執行我們的實驗,我們將選擇稍微小一點的東西,以便它可以放入記憶體中。
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_datasetsDatasetDict({
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
})
})你可以看到,對文字進行分組然後分塊產生了比我們原始的train和test分割的 25,000 個示例多得多的示例。這是因為我們現在有包含連續標記的示例,這些標記跨越了原始語料庫中的多個示例。你可以透過在其中一個塊中查詢特殊的[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
微調掩碼語言模型幾乎與微調序列分類模型相同,就像我們在第 3 章中所做的那樣。唯一的區別是我們需要一個特殊的資料整理器,它可以隨機掩碼每個文字批次中的一些標記。幸運的是,🤗 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(),以檢視給定單詞中的標記始終一起被掩碼。
現在我們有兩個資料整理器了,其餘的微調步驟都是標準的。如果你沒有足夠幸運地獲得一個神話般的 P100 GPU 😭,那麼在 Google Colab 上的訓練可能需要一段時間,因此我們首先將訓練集的大小減少到幾千個示例。別擔心,我們仍然可以獲得一個相當不錯的語言模型!在 🤗 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_datasetDatasetDict({
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,但您可以嘗試使用整個單詞掩碼的data_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中看到的,微調掩碼語言模型與第 3 章中的文字分類示例非常相似。事實上,唯一的細微差別是使用了特殊的資料整理器,我們已經在本節的前面介紹過!
但是,我們看到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太棒了,我們能夠在每個時期評估困惑度,並確保多次訓練執行的可重複性!
使用我們微調的模型
您可以透過使用 Hub 上的模型小部件或在本地使用 🤗 Transformers 中的pipeline與微調後的模型進行互動。讓我們使用後者使用fill-mask管道下載我們的模型。
from transformers import pipeline
mask_filler = pipeline(
"fill-mask", model="huggingface-course/distilbert-base-uncased-finetuned-imdb"
)然後,我們可以將示例文字“This is a great [MASK]”提供給管道,並檢視前 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.'不錯——我們的模型顯然已經調整了權重,以便預測與電影更相關的單詞!
這結束了我們第一次訓練語言模型的實驗。在第 6 節中,您將學習如何從頭開始訓練像 GPT-2 這樣的自迴歸模型;如果您想了解如何預訓練您自己的 Transformer 模型,請前往那裡!
✏️ 試試看!為了量化領域自適應的好處,請在預訓練和微調的 DistilBERT 檢查點上,針對 IMDb 標籤微調分類器。如果您需要複習文字分類,請檢視第 3 章。