流程

一般来说我们的tokenizer有如下流程

The tokenization pipeline.

规范化

规范化是对字符做大小写处理之类的我们可以通过如下API查看底层的normalization方法

1
2
3
4
5
6
7
8
9
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
print(type(tokenizer.backend_tokenizer))
# <class 'tokenizers.Tokenizer'>

print(tokenizer.backend_tokenizer.normalizer.normalize_str("Héllò hôw are ü?"))
# 'hello how are u?'

预分词

通过如下api查看分词器是如何做pre_tokenize的

1
2
3
tokenizer.backend_tokenizer.pre_tokenizer.pre_tokenize_str("Hello, how are  you?")
''''
[('Hello', (0, 5)), (',', (5, 6)), ('how', (7, 10)), ('are', (11, 14)), ('you', (16, 19)), ('?', (19, 20))]'''

可以看到后面的偏移量坐标,这也是上一节offset-mapping的由来

不同的预分词

gpt

1
2
3
4
tokenizer = AutoTokenizer.from_pretrained("gpt2")
tokenizer.backend_tokenizer.pre_tokenizer.pre_tokenize_str("Hello, how are you?")
'''
[('Hello', (0, 5)), (',', (5, 6)), ('Ġhow', (6, 10)), ('Ġare', (10, 14)), ('Ġ', (14, 15)), ('Ġyou', (15, 19)),('?', (19, 20))]''''

t5

1
2
3
tokenizer = AutoTokenizer.from_pretrained("t5-small")
tokenizer.backend_tokenizer.pre_tokenizer.pre_tokenize_str("Hello, how are you?")
# [('▁Hello,', (0, 6)), ('▁how', (7, 10)), ('▁are', (11, 14)), ('▁you?', (16, 20))]

三种分词算法总览

如上,不同的模型适用不同的分词算法

sentencepiece

它经常与unigram算法一起,且并不需要预分词,是特攻中文日文,这种无法分词的语言的

算法预览

Model BPE WordPiece Unigram
Training Starts from a small vocabulary and learns rules to merge tokens Starts from a small vocabulary and learns rules to merge tokens Starts from a large vocabulary and learns rules to remove tokens
Training step Merges the tokens corresponding to the most common pair Merges the tokens corresponding to the pair with the best score based on the frequency of the pair, privileging pairs where each individual token is less frequent Removes all the tokens in the vocabulary that will minimize the loss computed on the whole corpus
Learns Merge rules and a vocabulary Just a vocabulary A vocabulary with a score for each token
Encoding Splits a word into characters and applies the merges learned during training Finds the longest subword starting from the beginning that is in the vocabulary, then does the same for the rest of the word Finds the most likely split into tokens, using the scores learned during training

BPE

简述

BPE是Byte-Pair Encoding 的简写,他有三步

  1. 将corpus所有独一无二字符拆出来,如英文中的26个字母,标点和其他特殊符号
  2. 在有基础字符的基础上,以频率作为选取标准,将两个字符匹配在一起,选择频率最高的词进行入库
  3. 重复第二步直到满足你设定的词库大小

The GPT-2 and RoBERTa tokenizers (which are pretty similar) have a clever way to deal with this: they don’t look at words as being written with Unicode characters, but with bytes. This way the base vocabulary has a small size (256), but every character you can think of will still be included and not end up being converted to the unknown token. This trick is called byte-level BPE.

GPT和roberta使用的是比特级别的字符,就是0100这种,这就是他们的基础语料库,然后在基础上融合出来词进行构建词库

实例

下面进行实例解析设定语料库如下

语料库: "hug", "pug", "pun", "bun", "hugs"

词频: ("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)

1
# ("h" "u" "g", 10), ("p" "u" "g", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "u" "g" "s", 5)

第一轮

最多的是 ug的组合,20次

1
2
3
'''
Vocabulary: ["b", "g", "h", "n", "p", "s", "u", "ug"]
Corpus: ("h" "ug", 10), ("p" "ug", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "ug" "s", 5)'''

第二轮

最多是un

1
2
3
'''
Vocabulary: ["b", "g", "h", "n", "p", "s", "u", "ug", "un"]
Corpus: ("h" "ug", 10), ("p" "ug", 5), ("p" "un", 12), ("b" "un", 4), ("h" "ug" "s", 5)''

第三轮

最多的是hug

1
2
3
'''
Vocabulary: ["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"]
Corpus: ("hug", 10), ("p" "ug", 5), ("p" "un", 12), ("b" "un", 4), ("hug" "s", 5)'''

…如何循环到设定的词库大小

简要代码

语料库

1
2
3
4
5
6
corpus = [
"This is the Hugging Face Course.",
"This chapter is about tokenization.",
"This section shows several tokenizer algorithms.",
"Hopefully, you will be able to understand how they are trained and generate tokens.",
]

统计词频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from transformers import AutoTokenizer
from collections import defaultdict

tokenizer = AutoTokenizer.from_pretrained("gpt2")

word_freqs = defaultdict(int)

for text in corpus:
words_with_offsets = tokenizer.backend_tokenizer.pre_tokenizer.pre_tokenize_str(text)
new_words = [word for word, offset in words_with_offsets]
for word in new_words:
word_freqs[word] += 1

print(word_freqs)
'''
defaultdict(int, {'This': 3, 'Ġis': 2, 'Ġthe': 1, 'ĠHugging': 1, 'ĠFace': 1, 'ĠCourse': 1, '.': 4, 'Ġchapter': 1,
'Ġabout': 1, 'Ġtokenization': 1, 'Ġsection': 1, 'Ġshows': 1, 'Ġseveral': 1, 'Ġtokenizer': 1, 'Ġalgorithms': 1,
'Hopefully': 1, ',': 1, 'Ġyou': 1, 'Ġwill': 1, 'Ġbe': 1, 'Ġable': 1, 'Ġto': 1, 'Ġunderstand': 1, 'Ġhow': 1,
'Ġthey': 1, 'Ġare': 1, 'Ġtrained': 1, 'Ġand': 1, 'Ġgenerate': 1, 'Ġtokens': 1})'''
  • 首先载入gpt的分词器,做预分词
  • 再载入collection中的defaultdict设定为int类型

基础词汇表

1
2
3
4
5
6
7
8
9
10
11
12
alphabet = []

for word in word_freqs.keys():
for letter in word:
if letter not in alphabet:
alphabet.append(letter)
alphabet.sort()

print(alphabet)
'''
[ ',', '.', 'C', 'F', 'H', 'T', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's',
't', 'u', 'v', 'w', 'y', 'z', 'Ġ']'''

加个表头vocab = ["<|endoftext|>"] + alphabet.copy()

将单词映射为{‘word’: [‘w’, ‘o’, ‘r’, ‘d’]}的形式进行训练

1
2
3
4
splits = {word: [c for c in word] for word in word_freqs.keys()}

# 我觉可以改一下
`splits = {word: list(word) for word in word_freqs.keys()}`

字母对频率函数

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
def compute_pair_freqs(splits):
pair_freqs = defaultdict(int)
for word, freq in word_freqs.items():
split = splits[word] # 取得word对应的值如['w', 'o', 'r', 'd']
if len(split) == 1:
continue
for i in range(len(split) - 1):
pair = (split[i], split[i + 1])
pair_freqs[pair] += freq # 记录字母对的频率
return pair_freqs

# 示例
pair_freqs = compute_pair_freqs(splits)

for i, key in enumerate(pair_freqs.keys()):
print(f"{key}: {pair_freqs[key]}")
if i >= 5:
break
'''
('T', 'h'): 3
('h', 'i'): 3
('i', 's'): 5
('Ġ', 'i'): 2
('Ġ', 't'): 7
('t', 'h'): 3'''

# 取最大值
best_pair = ""
max_freq = None

for pair, freq in pair_freqs.items():
if max_freq is None or max_freq < freq:
best_pair = pair
max_freq = freq

print(best_pair, max_freq)
# ('Ġ', 't') 7

# 合并入库
merges = {("Ġ", "t"): "Ġt"}
vocab.append("Ġt")

将字符对构建进新的基础词表split (不是vocab)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def merge_pair(a, b, splits):
for word in word_freqs:
split = splits[word] # 取得word对应的值如['w', 'o', 'r', 'd']
if len(split) == 1:
continue

i = 0
while i < len(split) - 1:
if split[i] == a and split[i + 1] == b:
# 找到词对的位置,将ab字符串连接起来,做个列表存起来
split = split[:i] + [a + b] + split[i + 2 :]
else:
i += 1
splits[word] = split # 更新 ['w', 'o', 'r', 'd'] -> ['wo', 'r', 'd']
return splits

splits = merge_pair("Ġ", "t", splits)
print(splits["Ġtrained"])
# ['Ġt', 'r', 'a', 'i', 'n', 'e', 'd']

构建循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vocab_size = 50

while len(vocab) < vocab_size:
pair_freqs = compute_pair_freqs(splits) # 计算配对的频率
best_pair = ""
max_freq = None
for pair, freq in pair_freqs.items(): # 最大值
if max_freq is None or max_freq < freq:
best_pair = pair
max_freq = freq
splits = merge_pair(*best_pair, splits) # merge并更新
merges[best_pair] = best_pair[0] + best_pair[1] # 记录merge规则
vocab.append(best_pair[0] + best_pair[1])

print(merges)
'''
{('Ġ', 't'): 'Ġt', ('i', 's'): 'is', ('e', 'r'): 'er', ('Ġ', 'a'): 'Ġa', ('Ġt', 'o'): 'Ġto', ('e', 'n'): 'en',
('T', 'h'): 'Th', ('Th', 'is'): 'This', ('o', 'u'): 'ou', ('s', 'e'): 'se', ('Ġto', 'k'): 'Ġtok',
('Ġtok', 'en'): 'Ġtoken', ('n', 'd'): 'nd', ('Ġ', 'is'): 'Ġis', ('Ġt', 'h'): 'Ġth', ('Ġth', 'e'): 'Ġthe',
('i', 'n'): 'in', ('Ġa', 'b'): 'Ġab', ('Ġtoken', 'i'): 'Ġtokeni'}'''

查看词表vocab

1
2
3
4
5
print(vocab)
'''
['<|endoftext|>', ',', '.', 'C', 'F', 'H', 'T', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'k', 'l', 'm', 'n', 'o',
'p', 'r', 's', 't', 'u', 'v', 'w', 'y', 'z', 'Ġ', 'Ġt', 'is', 'er', 'Ġa', 'Ġto', 'en', 'Th', 'This', 'ou', 'se',
'Ġtok', 'Ġtoken', 'nd', 'Ġis', 'Ġth', 'Ġthe', 'in', 'Ġab', 'Ġtokeni']'''

运用分词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def tokenize(text):
pre_tokenize_result = tokenizer._tokenizer.pre_tokenizer.pre_tokenize_str(text)
pre_tokenized_text = [word for word, offset in pre_tokenize_result]
splits = [[l for l in word] for word in pre_tokenized_text]
# splits = [list(word) for word in pre_tokenized_text]
for pair, merge in merges.items():
for idx, split in enumerate(splits):
i = 0
while i < len(split) - 1:
if split[i] == pair[0] and split[i + 1] == pair[1]:
split = split[:i] + [merge] + split[i + 2 :]
else:
i += 1
splits[idx] = split

return sum(splits, [])

不怎么用,不理解也可

WordPiece

它运用在 BERT, such as DistilBERT, MobileBERT, Funnel Transformers, and MPNE等模型

它的基本词库基于频率构建,但是merge是通过最大似然等算法构建

这是HF团队对google算法的预测,因为google没公开他的分词算法

Unigram

它运用在AlBERT, T5, mBART, Big Bird, and XLNet.等模型

它直接构建一个超大的词汇表,然后计算删除这个单词造成的损失,每次删除造成最低损失的单词

构建基础的词汇表有使用预分词所有单词,按词频节选一部分然后计算损失,或者以BPE构建大词库然后做减法

原链