如何使用 Transformers 和 Tokenizers 從零開始訓練新的語言模型
在過去的幾個月裡,我們對
transformers
和 tokenizers
庫進行了多項改進,旨在讓 從零開始訓練一個新的語言模型 比以往任何時候都更容易。
在這篇文章中,我們將演示如何訓練一個“小”模型(84M 引數 = 6 層,768 隱藏層大小,12 個注意力頭)——與 DistilBERT 的層數和頭數相同——在 世界語 上。然後,我們將在詞性標註的下游任務上對模型進行微調。
世界語是一種旨在易於學習的人造語言。我們選擇它作為本次演示的原因有幾個:
- 它是一種相對低資源的語言(儘管有約 200 萬人使用),因此這個演示比訓練另一個英語模型更有趣 😁
- 其語法高度規則(例如,所有普通名詞都以 -o 結尾,所有形容詞都以 -a 結尾),因此即使在小資料集上,我們也應該能獲得有趣的語言學結果。
- 最後,這門語言的根本目標是拉近人與人之間的距離(促進世界和平和國際理解),這可以說與 NLP 社群的目標是一致的 💚
注意:你不需要理解世界語就能理解這篇博文,但如果你想學習它,Duolingo 有一個很好的課程,有 28 萬活躍學習者。
我們的模型將被命名為……等等…… EsperBERTo 😂
1. 找到資料集
首先,讓我們找到一份世界語語料庫。這裡我們將使用 INRIA OSCAR 語料庫 的世界語部分。OSCAR 是一個透過對 Common Crawl 網路轉儲進行語言分類和過濾而獲得的大型多語言語料庫。

資料集的世界語部分只有 299M,因此我們將與來自 Leipzig Corpora Collection 的世界語子語料庫進行拼接,該語料庫由新聞、文學和維基百科等不同來源的文字組成。
最終的訓練語料庫大小為 3 GB,這仍然很小——對於您的模型,預訓練的資料越多,結果會越好。
2. 訓練一個分詞器
我們選擇訓練一個位元組級位元組對編碼分詞器(與 GPT-2 相同),並使用與 RoBERTa 相同的特殊標記。讓我們隨意將其大小設定為 52,000。
我們建議訓練一個位元組級 BPE(而不是像 BERT 那樣的 WordPiece 分詞器),因為它將從單個位元組的字母表中開始構建詞彙表,因此所有單詞都可以分解為標記(不再有 <unk>
標記!)。
#! pip install tokenizers
from pathlib import Path
from tokenizers import ByteLevelBPETokenizer
paths = [str(x) for x in Path("./eo_data/").glob("**/*.txt")]
# Initialize a tokenizer
tokenizer = ByteLevelBPETokenizer()
# Customize training
tokenizer.train(files=paths, vocab_size=52_000, min_frequency=2, special_tokens=[
"<s>",
"<pad>",
"</s>",
"<unk>",
"<mask>",
])
# Save files to disk
tokenizer.save_model(".", "esperberto")
這是輸出的略微加速的截圖
🔥🔥 哇,這太快了!⚡️🔥
我們現在既有 vocab.json
(按頻率排序的最常用標記列表),也有 merges.txt
合併列表。
{
"<s>": 0,
"<pad>": 1,
"</s>": 2,
"<unk>": 3,
"<mask>": 4,
"!": 5,
"\"": 6,
"#": 7,
"$": 8,
"%": 9,
"&": 10,
"'": 11,
"(": 12,
")": 13,
# ...
}
# merges.txt
l a
Ġ k
o n
Ġ la
t a
Ġ e
Ġ d
Ġ p
# ...
很棒的是,我們的分詞器針對世界語進行了最佳化。與為英語訓練的通用分詞器相比,更多的原生詞被一個單一的、未分割的標記表示。世界語中使用的重音字元,即變音符號——ĉ
、ĝ
、ĥ
、ĵ
、ŝ
和 ŭ
——都以原生方式編碼。我們還以更高效的方式表示序列。在這個語料庫上,編碼序列的平均長度比使用預訓練的 GPT-2 分詞器時小約 30%。
以下是在 tokenizers
中使用它的方法,包括處理 RoBERTa 特殊標記——當然,你也可以直接從 transformers
中使用它。
from tokenizers.implementations import ByteLevelBPETokenizer
from tokenizers.processors import BertProcessing
tokenizer = ByteLevelBPETokenizer(
"./models/EsperBERTo-small/vocab.json",
"./models/EsperBERTo-small/merges.txt",
)
tokenizer._tokenizer.post_processor = BertProcessing(
("</s>", tokenizer.token_to_id("</s>")),
("<s>", tokenizer.token_to_id("<s>")),
)
tokenizer.enable_truncation(max_length=512)
print(
tokenizer.encode("Mi estas Julien.")
)
# Encoding(num_tokens=7, ...)
# tokens: ['<s>', 'Mi', 'Ġestas', 'ĠJuli', 'en', '.', '</s>']
3. 從零開始訓練語言模型
更新: 相關 Colab 筆記本直接使用我們新的 Trainer
,而不是透過指令碼。隨意選擇你喜歡的方法。
我們現在將使用 transformers
庫中的 run_language_modeling.py
指令碼來訓練我們的語言模型(該指令碼已從 run_lm_finetuning.py
更名為此,因為它現在更無縫地支援從頭開始訓練)。只需記住將 --model_name_or_path
設定為 None
,以便從頭開始訓練,而不是從現有模型或檢查點開始。
我們將訓練一個類 RoBERTa 模型,它是 BERT 模型的變體,進行了一些修改(更多詳情請查閱文件)。
由於模型是類 BERT 模型,我們將對其進行掩碼語言建模任務的訓練,即預測如何填充資料集中我們隨機掩碼的任意標記。這由示例指令碼處理。
我們只需要做兩件事
- 實現一個簡單的
Dataset
子類,從我們的文字檔案中載入資料- 根據您的用例,您甚至可能不需要編寫自己的 Dataset 子類,如果提供的示例(
TextDataset
和LineByLineTextDataset
)之一有效——但根據您的語料庫的特點,您可能需要新增許多自定義調整。
- 根據您的用例,您甚至可能不需要編寫自己的 Dataset 子類,如果提供的示例(
- 選擇並嘗試不同的超引數集。
以下是我們 EsperantoDataset 的一個簡單版本。
from torch.utils.data import Dataset
class EsperantoDataset(Dataset):
def __init__(self, evaluate: bool = False):
tokenizer = ByteLevelBPETokenizer(
"./models/EsperBERTo-small/vocab.json",
"./models/EsperBERTo-small/merges.txt",
)
tokenizer._tokenizer.post_processor = BertProcessing(
("</s>", tokenizer.token_to_id("</s>")),
("<s>", tokenizer.token_to_id("<s>")),
)
tokenizer.enable_truncation(max_length=512)
# or use the RobertaTokenizer from `transformers` directly.
self.examples = []
src_files = Path("./data/").glob("*-eval.txt") if evaluate else Path("./data/").glob("*-train.txt")
for src_file in src_files:
print("🔥", src_file)
lines = src_file.read_text(encoding="utf-8").splitlines()
self.examples += [x.ids for x in tokenizer.encode_batch(lines)]
def __len__(self):
return len(self.examples)
def __getitem__(self, i):
# We’ll pad at the batch level.
return torch.tensor(self.examples[i])
如果您的資料集非常大,您可以選擇動態載入和標記化示例,而不是將其作為預處理步驟。
這是我們傳遞給指令碼的超引數和引數的特定集合
--output_dir ./models/EsperBERTo-small-v1
--model_type roberta
--mlm
--config_name ./models/EsperBERTo-small
--tokenizer_name ./models/EsperBERTo-small
--do_train
--do_eval
--learning_rate 1e-4
--num_train_epochs 5
--save_total_limit 2
--save_steps 2000
--per_gpu_train_batch_size 16
--evaluate_during_training
--seed 42
像往常一樣,選擇您的 GPU(s) 可以容納的最大批次大小。
🔥🔥🔥 讓我們開始訓練吧!! 🔥🔥🔥
在這裡您可以檢視我們 針對一組特定超引數 的 Tensorboard
我們的示例指令碼預設以 Tensorboard 格式記錄,位於
runs/
下。然後,要檢視您的面板,只需執行tensorboard dev upload --logdir runs
– 這將設定 tensorboard.dev,這是一個由 Google 管理的託管版本,可讓您與任何人共享您的 ML 實驗。
4. 檢查語言模型是否真正訓練成功
除了檢視訓練和評估損失下降之外,檢查我們的語言模型是否學到任何有趣東西的最簡單方法是透過 FillMaskPipeline
。
管道是對分詞器和模型的簡單封裝,'fill-mask' 管道將允許您輸入一個包含掩碼標記(這裡是 <mask>
)的序列,並返回最可能的填充序列列表及其機率。
from transformers import pipeline
fill_mask = pipeline(
"fill-mask",
model="./models/EsperBERTo-small",
tokenizer="./models/EsperBERTo-small"
)
# The sun <mask>.
# =>
result = fill_mask("La suno <mask>.")
# {'score': 0.2526160776615143, 'sequence': '<s> La suno brilis.</s>', 'token': 10820}
# {'score': 0.0999930202960968, 'sequence': '<s> La suno lumis.</s>', 'token': 23833}
# {'score': 0.04382849484682083, 'sequence': '<s> La suno brilas.</s>', 'token': 15006}
# {'score': 0.026011141017079353, 'sequence': '<s> La suno falas.</s>', 'token': 7392}
# {'score': 0.016859788447618484, 'sequence': '<s> La suno pasis.</s>', 'token': 4552}
好的,簡單的語法/語法行得通。讓我們嘗試一個稍微更有趣的提示
fill_mask("Jen la komenco de bela <mask>.")
# This is the beginning of a beautiful <mask>.
# =>
# {
# 'score':0.06502299010753632
# 'sequence':'<s> Jen la komenco de bela vivo.</s>'
# 'token':1099
# }
# {
# 'score':0.0421181358397007
# 'sequence':'<s> Jen la komenco de bela vespero.</s>'
# 'token':5100
# }
# {
# 'score':0.024884626269340515
# 'sequence':'<s> Jen la komenco de bela laboro.</s>'
# 'token':1570
# }
# {
# 'score':0.02324388362467289
# 'sequence':'<s> Jen la komenco de bela tago.</s>'
# 'token':1688
# }
# {
# 'score':0.020378097891807556
# 'sequence':'<s> Jen la komenco de bela festo.</s>'
# 'token':4580
# }
“Jen la komenco de bela tago”,確實如此!
透過更復雜的提示,您可以探究您的語言模型是否捕獲了更多的語義知識,甚至某種(統計的)常識推理。
5. 在下游任務上微調你的語言模型
我們現在可以在詞性標註的下游任務上對我們新的世界語語言模型進行微調。
如前所述,世界語是一種高度規則的語言,單詞結尾通常決定了語法詞性。使用以 CoNLL-2003 格式標註的世界語詞性標註資料集(參見下面的示例),我們可以使用 transformers
庫中的 run_ner.py
指令碼。
詞性標註與 NER 任務一樣,都是標記分類任務,所以我們可以直接使用完全相同的指令碼。
同樣,這是本次微調的託管 Tensorboard。我們使用每 GPU 64 的批次大小訓練 3 個 epoch。
訓練和評估損失收斂到很小的殘差值,因為這項任務相當容易(語言是規則的)——但能夠端到端地訓練它仍然很有趣 😃。
這次,我們使用 TokenClassificationPipeline
from transformers import TokenClassificationPipeline, pipeline
MODEL_PATH = "./models/EsperBERTo-small-pos/"
nlp = pipeline(
"ner",
model=MODEL_PATH,
tokenizer=MODEL_PATH,
)
# or instantiate a TokenClassificationPipeline directly.
nlp("Mi estas viro kej estas tago varma.")
# {'entity': 'PRON', 'score': 0.9979867339134216, 'word': ' Mi'}
# {'entity': 'VERB', 'score': 0.9683094620704651, 'word': ' estas'}
# {'entity': 'VERB', 'score': 0.9797462821006775, 'word': ' estas'}
# {'entity': 'NOUN', 'score': 0.8509314060211182, 'word': ' tago'}
# {'entity': 'ADJ', 'score': 0.9996201395988464, 'word': ' varma'}
看起來成功了!🔥
對於更具挑戰性的 NER 資料集,@stefan-it 建議我們可以在 WikiANN 的銀標準資料集上進行訓練
6. 分享你的模型 🎉
最後,當你有一個不錯的模型時,請考慮與社群分享它
- 使用 CLI 上傳您的模型:
transformers-cli upload
- 編寫一個 README.md 模型卡片並將其新增到
model_cards/
目錄下的儲存庫中。您的模型卡片最好包含- 模型描述,
- 訓練引數(資料集、預處理、超引數),
- 評估結果,
- 預期用途和限制
- 任何其他有用的資訊!🤓
大功告成!
➡️ 您的模型在 https://huggingface.co/models 上擁有一個頁面,每個人都可以使用 AutoModel.from_pretrained("username/model_name")
載入它。
如果您想檢視不同語言的模型,請訪問 https://huggingface.co/models