1
在过去的几周里,我们对 transformers 和 tokenizers 库进行了一些改进,目的是让从头开始训练新的语言模型变得更加容易。
在本文中,我们将演示如何用世界语训练一个「小」模型(84 M,6 个层,768 个隐藏层,12 个注意力头)——这与 DistilBERT 的层数和注意力头数相同。然后,我们将在词性标记的下游任务上微调模型。
世界语是一种以易学性为目标的结构化语言。我们选择它有几个原因:
它是一种资源相对较少的语言(尽管大约有 200 万人使用它),所以这个演示不像训练一个英语模型那样枯燥?。
它的语法规则性很强(例如所有常用名词都以 -o 结尾,所有形容词都以 -a 结尾),所以即使是在一个小的数据集上,我们也可以得到有趣的结果。
最后,语言的总体目标是缩短人与人之间的距离,促进世界和平和国际理解,可以说是与 NLP 社区的目标一致?。
PS:你不需要了解世界语就可以理解这篇文章。
我们的模型将被称为…「wait for it… EsperBERTo」?。
1.查找数据集
首先,让我们找到一个世界语文本的语料库。这里我们将使用来自 INRIA 的 OSCAR 语料库(https://traces1.inria.fr/oscar/ )中的世界语部分。
OSCAR 是一个庞大的多语种语料库,它是通过对 Web 上爬取的文本进行语言分类和过滤而获得的。
数据集的世界语部分只有 299M,因此我们将与 Leipzig 语料库集合(https://wortschatz.uni-leipzig.de/en/download )中的世界语子语料库相连接,该语料库由来自新闻、文学和维基百科等不同来源的文本组成。
最终的训练语料库的大小为3 GB,仍然很小。当然,对于你的模型,你可以获得更多的数据来进行预训练,从而获得更好的结果。
2.训练标记器
我们选择使用与 RoBERTa 相同的特殊令牌来训练字节级字节对编码标记器(与 GPT-2 相同)。让我们任意选择它的大小,这里设置为 52000。
我们建议训练字节级的 BPE(而不是像 BERT 这样的词条标记器),因为它将从单个字节的字母表开始构建词汇表,所以所有单词都可以分解为标记(不再是 <unk> 标记)。
#! pip install tokenizers==0.4.2
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 trainingtokenizer.train(files=paths, vocab_size=52_000, min_frequency=2, special_tokens=[
"<s>",
"<pad>",
"</s>",
"<unk>",
"<mask>",
])
# Save files to disk
tokenizer.save(".", "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%。
下面是如何在标记器中使用它的方法,包括处理 RoBERTa 特殊标记——当然,你也可以直接从 transformer 中使用它。
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.从头开始训练语言模型
我们现在将使用来自 transformer 的 run_language_modeling.py 脚本(https://github.com/huggingface/transformers/blob/master/examples/run_language_modeling.py )(由 run_lm_finetuning.py 重新命名而来,因为它现在更无缝地支持从头开始的训练)来训练我们的语言模型。只需记住从零开始训练,而不是从现有的模型或检查点开始训练。
我们将训练一个类似于 RoBERTa 的模型,这是一个类似于 BERT 的模型,并进行了一些更改(查看文档https://huggingface.co/transformers/model_doc/roberta.html 了解更多细节)。
由于该模型类似于 BERT,我们将对其进行屏蔽语言建模任务的训练,即预测如何填充我们在数据集中随机屏蔽的任意令牌。这由示例脚本处理。
我们只需要做两件事:
实现从文本文件加载数据集的简单子类。
根据你的用例,如果所提供的示例(TextDataset 和 LineByLineTextDataset)中的一个有效,你甚至可能不需要编写自己的 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(https://tensorboard.dev/experiment/8AjtzdgPR1qG6bDIe1eKfw/#scalars )以获取一组特定的超参数:
默认情况下,我们的示例脚本会登录到 Tensorboard 格式,在 runs/ 下。然后,要查看你的面板,只需运行 tensorboard dev upload --logdir runs,这将设置 tensorboard.dev,它是一个 Google 托管的版本,允许你与任何人共享 ML 实验。
4.检查 LM 是否受过训练
除了观察正在下降的训练和评估损失之外,检查我们的语言模型是否学习到了有趣的东西的最简单方法是使用 FillMaskPipeline。
管道是标记器和模型周围的简单包装器,「填充掩码」允许你输入一个包含屏蔽令牌的序列(这里是 <mask>),并返回一个最可能填充序列的列表及其概率。
from transformers import pipeline
fill_mask = pipeline(
"fill-mask",
model="./models/EspertBERTo-small",
tokenizer="./models/EspertBERTo-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}
OK,使用简单的语法就可以了。让我们尝试一个更有趣的提示:
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.在下游任务上微调 LM
我们现在可以在词性标注的下游任务上微调我们的新的世界语语言模型。
如前所述,世界语是一种规则性很强的语言,词尾通常制约着词性的语法部分。使用 CoNLL-2003 格式的带注释的世界语 POS 标记数据集(见下面的示例),我们可以使用 transformer 中的 run_ner.py(https://github.com/huggingface/transformers/blob/master/examples/run_ner.py )脚本。
POS 标记和 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,
)
# 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'}
看起来很有效!?
6.分享你的模型?
最后,当你有一个好的模型时,请考虑与社区分享:
使用 CLI 上载模型:transformers CLI upload
编写 README.md 模型卡并将其添加到 model_cards/ 下的存储库中。理想情况下,你的模型卡应包括:
模型描述
训练参数(数据集、预处理、超参数)
评估结果
预期用途和限制
任何其他有用的!?
➡️你的模型在 http://huggingface.co/models 上有一个页面,每个人都可以使用 AutoModel.from_pretrained(“用户名/模型名”)加载它。
via:https://huggingface.co/blog/how-to-train
雷锋网雷锋网雷锋网
雷峰网版权文章,未经授权禁止转载。详情见转载须知。