📢 转载信息
原文链接:https://machinelearningmastery.com/training-a-tokenizer-for-llama-model/
原文作者:Adrian Tam
Meta(前身为 Facebook)发布的 Llama 系列模型是大型语言模型。这些仅解码器(decoder-only)的 Transformer 模型被用于生成任务。如今,几乎所有仅解码器模型都使用 Byte-Pair Encoding (BPE) 算法进行分词。在本文中,您将了解 BPE。具体来说,您将学习:
- BPE 与其他分词算法的比较
- 如何准备数据集并训练 BPE 分词器
- 如何使用分词器
训练 Llama 模型的 Tokenizer
图片来源:Joss Woodhead。保留部分权利。
让我们开始吧。
概述
本文分为四个部分;它们是:
- 理解 BPE
- 使用 Hugging Face tokenizers 库训练 BPE 分词器
- 使用 SentencePiece 库训练 BPE 分词器
- 使用 tiktoken 库训练 BPE 分词器
理解 BPE
Byte-Pair Encoding (BPE) 是一种分词算法,用于将文本分割成子词单元。BPE 不仅将文本分割成单词和标点符号,还可以进一步分割单词的前缀和后缀,这样前缀、词干和后缀就可以与语言模型中的意义相关联。如果没有子词分词,语言模型将很难学习到 “happy” 和 “unhappy” 是一对反义词。
BPE 并非唯一的子词分词算法。WordPiece,即 BERT 的默认算法,是另一个。一个实现良好的 BPE 不需要词汇表中的“未知”(unknown),并且在 BPE 中没有任何 OOV (Out of Vocabulary,词汇外) 的情况。这是因为 BPE 可以从 256 个字节值开始(因此被称为 byte-level BPE),然后将最常见的标记对合并成一个新的词汇表,直到达到所需的词汇表大小。
如今,BPE 是大多数仅解码器模型的首选分词算法。然而,您不应该从头开始实现自己的 BPE 分词器。相反,您可以使用诸如 Hugging Face 的 tokenizers、OpenAI 的 tiktoken 或 Google 的 sentencepiece 等分词器库。
使用 Hugging Face tokenizers 库训练 BPE 分词器
要训练 BPE 分词器,您需要准备一个数据集,以便分词算法可以确定最常出现的要合并的标记对。对于仅解码器模型,模型训练数据的一个子集通常是合适的。
训练分词器非常耗时,尤其是对于大型数据集。但是,与语言模型不同,分词器不需要学习文本的语言上下文,只需要了解标记在典型文本语料库中出现的频率。虽然训练一个好的语言模型可能需要数万亿个标记,但训练一个好的分词器只需要几百万个标记。
如前一篇文章中所述,有几个著名的用于语言模型训练的文本数据集。对于一个玩具项目,您可能想要一个较小的数据集以加快实验速度。HuggingFaceFW/fineweb 数据集是此目的的一个不错的选择。其完整大小是一个 15 万亿标记的数据集,但它也有 10B、100B 和 350B 的大小可供较小的项目使用。该数据集源自 Common Crawl,并经过 Hugging Face 的筛选以提高数据质量。
下面是如何从数据集中打印一些样本的代码:
|
1
2
3
4
5
6
7
8
9
|
import datasets
dataset = datasets.load_dataset("HuggingFaceFW/fineweb", name="sample-10BT", split="train", streaming=True)
count = 0
for sample in dataset:
print(sample)
count += 1
if count >= 5:
break
|
运行此代码将打印以下内容:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
{'text': '|Viewing Single Post From: Spoilers for the Week of February 11th| |Lil||F...',
'id': '<urn:uuid:39147604-bfbe-4ed5-b19c-54105f8ae8a7>', 'dump': 'CC-MAIN-2013-20',
'url': 'http://daytimeroyaltyonline.com/single/?p=8906650&t=8780053',
'date': '2013-05-18T05:48:59Z',
'file_path': 's3://commoncrawl/crawl-data/CC-MAIN-2013-20/segments/1368696381249/war...',
'language': 'en', 'language_score': 0.8232095837593079, 'token_count': 142}
{'text': '*sigh* Fundamentalist community, let me pass on some advice to you I learne...',
'id': '<urn:uuid:ba819eb7-e6e6-415a-87f4-0347b6a4f017>', 'dump': 'CC-MAIN-2013-20',
'url': 'http://endogenousretrovirus.blogspot.com/2007/11/if-you-have-set-yourself-on...',
'date': '2013-05-18T06:43:03Z',
'file_path': 's3://commoncrawl/crawl-data/CC-MAIN-2013-20/segments/1368696381249/war...',
'language': 'en', 'language_score': 0.9737711548805237, 'token_count': 703}
...
|
对于训练分词器(甚至是语言模型),您只需要每个样本中的 text 字段。
要使用 tokenizers 库训练 BPE 分词器,只需将文本样本提供给训练器即可。以下是完整的代码:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
from typing import Iterator
import datasets
from tokenizers import Tokenizer, models, trainers, pre_tokenizers, decoders, normalizers
# Load FineWeb 10B sample (using only a slice for demo to save memory)
dataset = datasets.load_dataset("HuggingFaceFW/fineweb", name="sample-10BT", split="train", streaming=True)
def get_texts(dataset: datasets.Dataset, limit: int = 100_000) -> Iterator[str]:
"""Get texts from the dataset until the limit is reached or the dataset is exhausted"""
count = 0
for sample in dataset:
yield sample["text"]
count += 1
if limit and count >= limit:
break
# Initialize a BPE model: either byte_fallback=True or set unk_token="[UNK]"
tokenizer = Tokenizer(models.BPE(byte_fallback=True))
tokenizer.normalizer = normalizers.NFKC()
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=True, use_regex=False)
tokenizer.decoder = decoders.ByteLevel()
# Trainer
trainer = trainers.BpeTrainer(
vocab_size=25_000,
min_frequency=2,
special_tokens=["[PAD]", "[CLS]", "[SEP]", "[MASK]"],
show_progress=True,
)
# Train and save the tokenizer to disk
texts = get_texts(dataset, limit=10_000)
tokenizer.train_from_iterator(texts, trainer=trainer)
tokenizer.save("bpe_tokenizer.json")
# Reload the tokenizer from disk
tokenizer = Tokenizer.from_file("bpe_tokenizer.json")
# Test: encode/decode
text = "Let's have a pizza party! 🍕"
enc = tokenizer.encode(text)
print("Token IDs:", enc.ids)
print("Decoded:", tokenizer.decode(enc.ids))
|
当您运行此代码时,您将看到:
|
1
2
3
4
5
6
7
|
Resolving data files: 100%|███████████████████████| 27468/27468 [00:03<00:00, 7792.97it/s]
[00:00:01] Pre-processing sequences ████████████████████████████ 0 / 0
[00:00:02] Tokenize words ████████████████████████████ 10000 / 10000
[00:00:00] Count pairs ████████████████████████████ 10000 / 10000
[00:00:38] Compute merges ████████████████████████████ 24799 / 24799
Token IDs: [3548, 277, 396, 1694, 14414, 227, 12060, 715, 9814, 180, 188]
Decoded: Let's have a pizza party! 🍕
|
为避免一次性加载整个数据集,请在 load_dataset() 函数中使用 streaming=True 参数。tokenizers 库只要求文本用于训练 BPE,因此 get_texts() 函数会逐个生成文本样本。由于训练分词器不需要整个数据集,因此当达到限制时循环会终止。
要创建字节级 BPE,请在 BPE 模型中设置 byte_fallback=True 参数,并配置 ByteLevel 预分词器和解码器。还建议添加一个 NFKC 规范化器来清理 Unicode 文本,以实现更好的分词效果。
对于仅解码器模型,您还需要特殊标记,例如 <PAD>、<EOT> 和 <MASK>。<EOT> 标记用于指示文本序列的结束,使模型能够声明何时完成序列生成。
训练好分词器后,将其保存到文件中以供将来使用。要使用分词器,请调用 encode() 方法将文本转换为标记 ID 序列,或调用 decode() 方法将标记 ID 转换回文本。
请注意,上述代码将词汇表大小设置为 25,000,并将训练数据集限制为 10,000 个样本,以供演示之用,从而使训练能在合理的时间内完成。在实践中,应使用更大的词汇表大小和训练数据集,以便语言模型能够捕捉语言的多样性。作为参考,Llama 2 的词汇表大小为 32,000,Llama 3 的词汇表大小为 128,256。
使用 SentencePiece 库训练 BPE 分词器
作为 Hugging Face tokenizers 库的替代方案,您可以使用 Google 的 sentencepiece 库。该库是用 C++ 编写的,速度很快,但其 API 和文档不如 tokenizers 库完善。
使用 sentencepiece 库重写的先前代码如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
from typing import Iterator
import datasets
import sentencepiece as spm
# Load FineWeb 10B sample (using only a slice for demo to save memory)
dataset = datasets.load_dataset("HuggingFaceFW/fineweb", name="sample-10BT", split="train", streaming=True)
def get_texts(dataset: datasets.Dataset, limit: int = 100_000) -> Iterator[str]:
"""Get texts from the dataset until the limit is reached or the dataset is exhausted"""
count = 0
for sample in dataset:
yield sample["text"]
count += 1
if limit and count >= limit:
break
# Define special tokens as comma-separated string
spm.SentencePieceTrainer.Train(
sentence_iterator=get_texts(dataset, limit=10_000),
byte_fallback=True,
model_prefix="sp_bpe",
vocab_size=32_000,
model_type="bpe",
unk_id=0,
bos_id=1,
eos_id=2,
pad_id=3, # set to -1 to disable
character_coverage=1.0,
input_sentence_size=10_000,
shuffle_input_sentence=False,
)
# Load the trained SentencePiece model
sp = spm.SentencePieceProcessor(model_file="sp_bpe.model")
# Test: encode/decode
text = "Let's have a pizza party! 🍕"
ids = sp.encode(text, out_type=int, enable_sampling=False) # default: no special tokens
tokens = sp.encode(text, out_type=str, enable_sampling=False)
print("Tokens:", tokens)
print("Token IDs:", ids)
decoded = sp.decode(ids)
print("Decoded:", decoded)
|
当您运行此代码时,您将看到:
|
1
2
3
|
...
|
🚀 想要体验更好更全面的AI调用?
欢迎使用青云聚合API,约为官网价格的十分之一,支持300+全球最新模型,以及全球各种生图生视频模型,无需翻墙高速稳定,文档丰富,小白也可以简单操作。
评论区