LLM 課程文件
資料處理
並獲得增強的文件體驗
開始使用
資料處理
接著上一章的例子,下面是我們如何在一個批次上訓練序列分類器:
import torch
from torch.optim import AdamW
from transformers import 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 (Microsoft Research Paraphrase Corpus) 資料集為例,該資料集由 William B. Dolan 和 Chris Brockett 在一篇論文中介紹。該資料集包含 5,801 對句子,並帶有一個標籤,指示它們是否為釋義(即,兩句話是否意思相同)。我們選擇它用於本章,因為它是一個小型資料集,易於進行訓練實驗。
從 Hub 載入資料集
Hub 不僅包含模型;它還擁有多種語言的多個數據集。你可以在這裡瀏覽資料集,我們建議你在閱讀完本節後嘗試載入和處理新資料集(請參閱此處的一般文件)。但現在,我們先關注 MRPC 資料集!它是構成 GLUE 基準的 10 個數據集之一,GLUE 基準是用於衡量機器學習模型在 10 個不同文字分類任務上效能的學術基準。
🤗 Datasets 庫提供了一個非常簡單的命令來從 Hub 下載和快取資料集。我們可以像這樣下載 MRPC 資料集:
💡 更多資源:有關更多資料集載入技術和示例,請查閱 🤗 Datasets 文件。
from datasets import load_dataset
raw_datasets = load_dataset("glue", "mrpc")
raw_datasets
DatasetDict({
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"])
💡 深入探討:有關更高階的分詞技術以及理解不同分詞器如何工作,請探索 🤗 Tokenizers 文件 和操作手冊中的分詞指南。
然而,我們不能僅僅將兩個序列傳遞給模型,然後預測這兩個句子是否是釋義。我們需要將這兩個序列作為一對來處理,並應用適當的預處理。幸運的是,分詞器也可以接收一對序列,並按照我們的 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]
}
我們在第 2 章討論了 `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]` 的部分都具有 `0` 的標記型別 ID,而其他部分(對應於 `sentence2 [SEP]`)都具有 `1` 的標記型別 ID。
請注意,如果你選擇不同的檢查點,你的分詞輸入中不一定會包含 `token_type_ids`(例如,如果你使用 DistilBERT 模型,它們就不會返回)。它們僅在模型知道如何處理它們時才返回,因為模型在預訓練期間見過它們。
在這裡,BERT 是用 token type ID 預訓練的,除了我們在第一章中討論的掩碼語言建模目標之外,它還有一個額外的目標,稱為下一句預測。這項任務的目標是建模句子對之間的關係。
透過下一句預測,模型會收到句子對(帶有隨機遮蔽的標記),並被要求預測第二個句子是否緊跟第一個句子。為了使任務不那麼簡單,有一半的時間,句子在它們被提取的原始文件中是彼此相連的,而另一半的時間,這兩個句子來自兩個不同的文件。
一般來說,你不需要擔心你的分詞輸入中是否有 `token_type_ids`:只要你對分詞器和模型使用相同的檢查點,一切都會正常,因為分詞器知道要為模型提供什麼。
現在我們已經瞭解了分詞器如何處理一對句子,我們可以用它來對整個資料集進行分詞:就像上一章中那樣,我們可以透過提供第一個句子列表,然後是第二個句子列表,將句子對列表饋送給分詞器。這也與我們在第二章中看到的填充和截斷選項相容。因此,預處理訓練資料集的一種方法是:
tokenized_dataset = tokenizer(
raw_datasets["train"]["sentence1"],
raw_datasets["train"]["sentence2"],
padding=True,
truncation=True,
)
這很好用,但它的缺點是返回一個字典(包含我們的鍵 `input_ids`、`attention_mask` 和 `token_type_ids`,以及值是列表的列表)。而且,只有當你擁有足夠的 RAM 來在分詞期間儲存整個資料集時,它才能工作(而 🤗 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` 引數。這是因為將所有樣本填充到最大長度效率不高:最好在構建批次時填充樣本,因為這樣我們只需要填充到該批次中的最大長度,而不是整個資料集中的最大長度。當輸入長度變化很大時,這可以節省大量時間和處理能力!
📚 效能提示:在 🤗 Datasets 效能指南中瞭解更多高效資料處理技術。
下面是我們如何一次性將分詞函式應用於所有資料集。我們在 `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
})
})
你甚至可以在使用 `map()` 應用預處理函式時使用多程序,方法是傳遞 `num_proc` 引數。我們這裡沒有這樣做,因為 🤗 Tokenizers 庫已經使用多個執行緒來更快地對我們的樣本進行分詞,但如果你沒有使用由該庫支援的快速分詞器,這可能會加快你的預處理速度。
我們的 `tokenize_function` 返回一個包含 `input_ids`、`attention_mask` 和 `token_type_ids` 鍵的字典,因此這三個欄位將新增到我們資料集的所有拆分中。請注意,如果我們的預處理函式為我們應用 `map()` 的資料集中已有的鍵返回一個新值,我們也可以更改現有欄位。
我們需要做的最後一件事是在批次處理元素時,將所有示例填充到最長元素的長度——我們稱之為動態填充的技術。
動態填充
負責將樣本組合成批次的功能稱為整理函式。它是你在構建 `DataLoader` 時可以傳遞的一個引數,預設情況下,它將你的樣本轉換為 PyTorch 張量並(如果你的元素是列表、元組或字典,則遞迴地)連線它們。在我們的例子中,這不可能實現,因為我們擁有的輸入不會都具有相同的大小。我們故意推遲了填充,以便僅在每個批次上根據需要進行填充,並避免出現帶有大量填充的過長輸入。這會大大加快訓練速度,但請注意,如果你在 TPU 上訓練,這可能會導致問題——TPU 更喜歡固定的形狀,即使這需要額外的填充。
🚀 最佳化指南:有關最佳化訓練效能的更多詳細資訊,包括填充策略和 TPU 考慮事項,請參閱 🤗 Transformers 效能文件。
為了實際操作,我們必須定義一個整理函式,它將為我們想要批次處理的資料集項應用正確的填充量。幸運的是,🤗 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 任務的預處理函式。
📖 更多練習:檢視 🤗 Transformers 示例中的這些動手示例。
太棒了!現在我們已經使用 🤗 Datasets 庫的最新最佳實踐對資料進行了預處理,我們已準備好使用現代 Trainer API 訓練我們的模型。下一節將向你展示如何利用 Hugging Face 生態系統中可用的最新功能和最佳化來有效地微調模型。
本節測驗
測試你對資料處理概念的理解
1. 使用 Dataset.map() 並設定 batched=True 的主要優點是什麼?
2. 為什麼我們使用動態填充,而不是將所有序列填充到資料集中的最大長度?
3. 在 BERT 分詞中,token_type_ids 欄位代表什麼?
4. 當使用 load_dataset('glue', 'mrpc') 載入資料集時,第二個引數指定了什麼?
5. 在訓練前移除“sentence1”和“sentence2”等列的目的是什麼?
💡 要點:
- 使用 `Dataset.map()` 並設定 `batched=True` 以顯著加快預處理速度
- 使用 `DataCollatorWithPadding` 進行動態填充比固定長度填充更高效
- 始終對資料進行預處理,以使其符合模型預期(數值張量,正確的列名)
- 🤗 Datasets 庫提供了強大的工具,可實現高效的大規模資料處理