並獲得增強型文件體驗
開始使用
處理資料
繼續使用來自 上一章 的示例,以下是我們在 PyTorch 中在一個批次上訓練序列分類器的步驟
import torch
from transformers import AdamW, AutoTokenizer, AutoModelForSequenceClassification
# Same as before
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
sequences = [
"I've been waiting for a HuggingFace course my whole life.",
"This course is amazing!",
]
batch = tokenizer(sequences, padding=True, truncation=True, return_tensors="pt")
# This is new
batch["labels"] = torch.tensor([1, 1])
optimizer = AdamW(model.parameters())
loss = model(**batch).loss
loss.backward()
optimizer.step()當然,僅在兩個句子上訓練模型不會產生很好的結果。要獲得更好的結果,你需要準備一個更大的資料集。
在本節中,我們將使用 MRPC(微軟研究院釋義語料庫)資料集作為示例,該資料集在 William B. Dolan 和 Chris Brockett 的 論文 中被介紹。該資料集包含 5,801 對句子,用標籤指示它們是否為釋義(即,兩句話是否意思相同)。我們選擇它用於本章,因為它是一個小型資料集,因此易於進行訓練實驗。
從 Hub 載入資料集
Hub 不僅包含模型;它還包含許多不同語言的多個數據集。你可以 在這裡 瀏覽資料集,我們建議你在完成本節後嘗試載入和處理一個新資料集(請參閱 此處 的一般文件)。但現在,讓我們關注 MRPC 資料集!這是構成 GLUE 基準 的 10 個數據集之一,它是一個學術基準,用於衡量 ML 模型在 10 種不同文字分類任務上的效能。
🤗 資料集庫提供了一個非常簡單的命令來下載和快取 Hub 上的資料集。我們可以像這樣下載 MRPC 資料集
from datasets import load_dataset
raw_datasets = load_dataset("glue", "mrpc")
raw_datasetsDatasetDict({
train: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 3668
})
validation: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 408
})
test: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 1725
})
})如你所見,我們得到一個 DatasetDict 物件,其中包含訓練集、驗證集和測試集。每個集合都包含多個列(sentence1、sentence2、label 和 idx)和可變數量的行,即每個集合中的元素數量(因此,訓練集中有 3,668 對句子,驗證集中有 408 對句子,測試集中有 1,725 對句子)。
此命令將下載並快取資料集,預設情況下在 ~/.cache/huggingface/datasets 中。回憶第二章的內容,你可以透過設定 HF_HOME 環境變數來自定義你的快取資料夾。
我們可以透過索引訪問 raw_datasets 物件中的每一對句子,就像使用字典一樣
raw_train_dataset = raw_datasets["train"]
raw_train_dataset[0]{'idx': 0,
'label': 1,
'sentence1': 'Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .',
'sentence2': 'Referring to him as only " the witness " , Amrozi accused his brother of deliberately distorting his evidence .'}我們可以看到標籤已經是整數,因此我們無需在此處進行任何預處理。要了解哪個整數對應哪個標籤,我們可以檢查 raw_train_dataset 的 features。這將告訴我們每一列的型別
raw_train_dataset.features
{'sentence1': Value(dtype='string', id=None),
'sentence2': Value(dtype='string', id=None),
'label': ClassLabel(num_classes=2, names=['not_equivalent', 'equivalent'], names_file=None, id=None),
'idx': Value(dtype='int32', id=None)}在後臺,label 的型別為 ClassLabel,整數到標籤名稱的對映儲存在 names 資料夾中。0 對應於 not_equivalent,1 對應於 equivalent。
✏️ 試試看! 檢視訓練集中的第 15 個元素和驗證集中的第 87 個元素。它們的標籤是什麼?
預處理資料集
要預處理資料集,我們需要將文字轉換為模型可以理解的數字。正如你在 上一章 中看到的,這是透過分詞器完成的。我們可以向分詞器提供一個句子或一個句子列表,因此我們可以直接像這樣對所有第一句話和所有第二句話進行分詞
from transformers import AutoTokenizer
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
tokenized_sentences_1 = tokenizer(raw_datasets["train"]["sentence1"])
tokenized_sentences_2 = tokenizer(raw_datasets["train"]["sentence2"])但是,我們不能只將兩個序列傳遞給模型並獲得對這兩個句子是否為釋義的預測。我們需要將這兩個序列作為一對進行處理,並應用適當的預處理。幸運的是,分詞器也可以接收一對序列,並將其以 BERT 模型期望的方式進行準備
inputs = tokenizer("This is the first sentence.", "This is the second one.")
inputs{
'input_ids': [101, 2023, 2003, 1996, 2034, 6251, 1012, 102, 2023, 2003, 1996, 2117, 2028, 1012, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
}我們在 第二章 中討論了 input_ids 和 attention_mask 鍵,但我們推遲了對 token_type_ids 的討論。在此示例中,這就是告訴模型輸入的哪一部分是第一句話,哪一部分是第二句話的內容。
✏️ 試試看! 獲取訓練集中的第 15 個元素,分別對兩個句子進行分詞,以及作為一對進行分詞。兩個結果有什麼區別?
如果我們將 input_ids 中的 ID 解碼回單詞
tokenizer.convert_ids_to_tokens(inputs["input_ids"])我們將得到
['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]']因此,我們看到模型期望輸入採用 [CLS] sentence1 [SEP] sentence2 [SEP] 的形式,其中包含兩個句子。將此與 token_type_ids 對齊,我們得到
['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]']
[ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1]如你所見,與 [CLS] sentence1 [SEP] 對應的輸入部分的令牌型別 ID 均為 0,而與 sentence2 [SEP] 對應的其他部分的令牌型別 ID 均為 1。
請注意,如果你選擇不同的檢查點,你的分詞輸入中不一定會有 token_type_ids(例如,如果你使用 DistilBERT 模型,它們不會被返回)。它們僅在模型知道如何處理它們時才會被返回,因為它在預訓練期間見過它們。
在這裡,BERT 使用令牌型別 ID 進行預訓練,除了我們在 第一章 中討論的掩碼語言建模目標外,它還有另一個稱為“下一句話預測”的目標。這項任務的目標是模擬句子對之間的關係。
在下一句話預測中,模型會獲得句子對(包含隨機掩碼令牌),並被要求預測第二句話是否緊跟第一句話。為了使任務不那麼容易,一半情況下句子在它們被提取的原始文件中彼此相鄰,另一半情況下兩個句子來自兩個不同的文件。
通常情況下,您無需擔心您的標記化輸入中是否包含token_type_ids:只要您使用相同的檢查點進行標記化和建模,一切都會正常,因為標記器知道如何為其模型提供什麼。
現在我們已經看到了標記器如何處理一對句子,我們可以用它來標記化整個資料集:就像在上一章中一樣,我們可以透過提供第一個句子的列表,然後提供第二個句子的列表,將標記器提供給一對句子的列表。這與我們在第 2 章中看到的填充和截斷選項相容。因此,預處理訓練資料集的一種方法是
tokenized_dataset = tokenizer(
raw_datasets["train"]["sentence1"],
raw_datasets["train"]["sentence2"],
padding=True,
truncation=True,
)這工作得很好,但它有一個缺點:它返回一個字典(帶有我們的鍵input_ids、attention_mask和token_type_ids,以及值為列表的列表)。它也只會在您的記憶體足夠大,可以在標記化期間儲存整個資料集時起作用(而來自🤗 Datasets 庫的資料集是Apache Arrow 檔案,儲存在磁碟上,因此您只保留您要求載入到記憶體中的樣本)。
為了將資料保留為資料集,我們將使用Dataset.map() 方法。如果我們需要進行比標記化更多的預處理,這也會給我們額外的靈活性。map() 方法透過對資料集的每個元素應用函式來工作,因此讓我們定義一個函式來標記化我們的輸入
def tokenize_function(example):
return tokenizer(example["sentence1"], example["sentence2"], truncation=True)此函式接受一個字典(如資料集的專案),並返回一個包含鍵input_ids、attention_mask和token_type_ids的新字典。請注意,即使example 字典包含多個樣本(每個鍵作為句子列表),它也能正常工作,因為tokenizer 在句子對列表上工作,如前所述。這將允許我們在呼叫map()時使用batched=True選項,這將大大加快標記化速度。tokenizer 由來自🤗 Tokenizers 庫的用 Rust 編寫的標記器支援。這個標記器可以非常快,但前提是我們一次給它大量輸入。
請注意,我們暫時將padding引數留在了我們的標記化函式中。這是因為將所有樣本填充到最大長度效率不高:最好在構建批次時填充樣本,因為這樣我們只需要填充到該批次中的最大長度,而不是整個資料集中的最大長度。當輸入長度變化很大時,這可以節省大量時間和處理能力!
以下是如何將標記化函式一次應用於所有資料集。我們在呼叫map時使用batched=True,因此該函式一次應用於資料集的多個元素,而不是分別應用於每個元素。這允許更快地進行預處理。
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
tokenized_datasets🤗 Datasets 庫應用此處理的方式是向資料集新增新欄位,每個欄位對應於預處理函式返回的字典中的每個鍵
DatasetDict({
train: Dataset({
features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
num_rows: 3668
})
validation: Dataset({
features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
num_rows: 408
})
test: Dataset({
features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
num_rows: 1725
})
})您甚至可以透過傳遞num_proc引數,在使用map()應用預處理函式時使用多程序。我們在這裡沒有這樣做,因為🤗 Tokenizers 庫已經使用多個執行緒來更快地標記化我們的樣本,但是如果您沒有使用此庫支援的快速標記器,這可能會加快您的預處理速度。
我們的tokenize_function 返回一個包含鍵input_ids、attention_mask和token_type_ids的字典,因此這三個欄位被新增到我們資料集的所有分割中。請注意,如果我們的預處理函式為應用了map()的資料集中的現有鍵返回了新值,我們也可以更改現有欄位。
我們最後需要做的事情是在將元素一起批處理時,將所有示例填充到最長元素的長度——我們稱之為動態填充。
動態填充
負責將樣本組合到批次中的函式稱為整理函式。它是一個引數,您可以在構建DataLoader時傳遞,預設值為一個函式,該函式將僅將您的樣本轉換為 PyTorch 張量並將其連線起來(如果您的元素是列表、元組或字典,則遞迴地進行)。這在我們的情況下將不可行,因為我們擁有的輸入不會都具有相同的尺寸。我們故意推遲了填充,以便僅在每個批次上按需應用它,並避免具有大量填充的過長輸入。這將大大加快訓練速度,但請注意,如果您在 TPU 上進行訓練,它可能會導致問題——TPU 傾向於固定形狀,即使這意味著需要額外的填充。
為了在實踐中做到這一點,我們必須定義一個整理函式,該函式將對我們要一起批處理的資料集的專案應用正確的填充量。幸運的是,🤗 Transformers 庫透過DataCollatorWithPadding為我們提供了這樣的函式。它在例項化時接受一個標記器(以瞭解使用哪個填充標記,以及模型是否期望填充位於輸入的左側或右側),並將執行您需要的一切
from transformers import DataCollatorWithPadding
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)為了測試這個新玩具,讓我們從我們的訓練集中獲取一些我們想要一起批處理的樣本。在這裡,我們刪除了列idx、sentence1和sentence2,因為它們將不再需要,並且包含字串(我們無法用字串建立張量),並檢視批次中每個條目的長度
samples = tokenized_datasets["train"][:8]
samples = {k: v for k, v in samples.items() if k not in ["idx", "sentence1", "sentence2"]}
[len(x) for x in samples["input_ids"]][50, 59, 47, 67, 59, 50, 62, 32]不出所料,我們得到了長度不同的樣本,從 32 到 67。動態填充意味著此批次中的樣本應全部填充到 67 的長度,即批次中的最大長度。如果沒有動態填充,所有樣本都必須填充到整個資料集中的最大長度,或者模型可以接受的最大長度。讓我們仔細檢查一下我們的data_collator是否正確地動態填充了批次
batch = data_collator(samples)
{k: v.shape for k, v in batch.items()}{'attention_mask': torch.Size([8, 67]),
'input_ids': torch.Size([8, 67]),
'token_type_ids': torch.Size([8, 67]),
'labels': torch.Size([8])}看起來不錯!現在我們已經從原始文字過渡到模型可以處理的批次,我們準備對其進行微調了!
✏️ 試試看! 在 GLUE SST-2 資料集上覆制預處理。它有點不同,因為它由單個句子組成,而不是句子對,但我們所做的事情的其餘部分應該看起來相同。對於更具挑戰性的任務,嘗試編寫一個適用於任何 GLUE 任務的預處理函式。