在过去的几个月里,我们对我们的transformers
和tokenizers
库进行了一些改进,目标是让从头开始训练一个新的语言模型变得比以往更容易。
在这篇文章中,我们将演示如何在Esperanto上训练一个“小”模型(84 M参数=6层,768隐藏大小,12个注意力头)-与DistilBERT相同数量的层和头。然后,我们将对该模型进行下游的词性标注任务微调。
Esperanto是一种目标是易于学习的构造语言。我们选择它作为演示的原因有几个:
- 它是一种相对较低资源的语言(尽管有大约200万人使用),所以这个演示比训练另一个英语模型不那么无聊😁
- 它的语法非常规则(例如,所有普通名词以-o结尾,所有形容词以-a结尾),因此即使在一个小数据集上,我们也应该获得有趣的语言结果。
- 最后,该语言的根本目标是使人们更加接近(促进世界和平和国际理解),可以说这与NLP社区的目标是一致的💚
注意:您不需要了解Esperanto就能理解本文,但如果您想学习它,Duolingo有一个有280,000个活跃学习者的不错课程。
我们的模型将被称为…等待…EsperBERTo😂
1. 找到一个数据集
首先,让我们找到一个包含Esperanto文本的语料库。在这里,我们将使用INRIA的OSCAR语料库中的Esperanto部分。OSCAR是通过对Web的Common Crawl转储进行语言分类和过滤而获得的一个巨大的多语言语料库。
数据集的Esperanto部分只有299M,所以我们将与Leipzig Corpora Collection的Esperanto子语料库连接在一起,该语料库由来自新闻、文学和维基百科等多种来源的文本组成。
最终的训练语料库的大小为3 GB,这仍然很小-对于您的模型来说,您能够获得的数据越多,预训练效果就会更好。
2. 训练一个分词器
我们选择训练一个字节级的Byte-pair编码分词器(与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")]
# 初始化一个分词器
tokenizer = ByteLevelBPETokenizer()
# 自定义训练
tokenizer.train(files=paths, vocab_size=52_000, min_frequency=2, special_tokens=[
"<s>",
"<pad>",
"</s>",
"<unk>",
"<mask>",
])
# 保存文件到磁盘
tokenizer.save_model(".", "esperberto")
这是输出的稍微加速的截图:
在我们的数据集上,训练大约需要5分钟。
🔥🔥 哇,太快了! ⚡️🔥
现在我们有了一个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
)适用 – 但是基于您的语料库外观,您可能想添加许多自定义调整。
- 根据您的用例,您可能甚至不需要编写自己的
- 选择并尝试不同的超参数设置。
下面是我们的 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的最大批处理大小。
🔥🔥🔥 让我们开始训练吧!! 🔥🔥🔥
在这里,您可以检查我们的Tensorboard以获取特定的超参数集:
我们的示例脚本默认以Tensorboard格式记录,存储在
runs/
目录下。要查看您的面板,只需运行tensorboard dev upload --logdir runs
,这将设置tensorboard.dev,这是一个由Google托管的版本,可以让您与任何人共享您的机器学习实验。
4. 检查语言模型是否真的训练好了
除了查看训练和评估损失是否降低,检查语言模型是否学到了有趣的内容最简单的方法是使用FillMaskPipeline
。
Pipeline是对分词器和模型的简单封装,’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
# }
“仅仅是美好的一天的开始”,确实!
通过更复杂的提示,您可以探索您的语言模型是否捕捉到了更多语义知识,甚至某种(统计的)常识推理。
5. 在下游任务上对您的语言模型进行微调
我们现在可以在下游任务词性标注上对我们的新的世界语语言模型进行微调。
如前所述,世界语是一种高度规则的语言,词尾通常决定了语法词性。通过使用以 CoNLL-2003 格式(参见下面的示例)格式化的带有注释的世界语词性标签数据集,我们可以使用来自的run_ner.py
脚本。
词性标注是一项与命名实体识别(NER)完全相同的标记分类任务,因此我们可以直接使用相同的脚本。
同样,这是该微调的托管Tensorboard。我们使用每个 GPU 的批量大小为 64 进行训练 3 个周期。
训练和评估损失收敛到较小的残差值,因为任务相当简单(语言是规则的)- 能够从头到尾进行训练仍然很有趣 😃。
这次,让我们使用TokenClassificationPipeline
:
from transformers import TokenClassificationPipeline, pipeline
MODEL_PATH = "./models/EsperBERTo-small-pos/"
nlp = pipeline(
"ner",
model=MODEL_PATH,
tokenizer=MODEL_PATH,
)
# 或者直接实例化 TokenClassificationPipeline。
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