NLP领域中的token和tokenization到底指的是什么?

这是一个非常简单的基本概念问题,但作为小白,真的不太清晰
收藏者
0
被浏览
106

5 个回答

哈欠 LV

发表于 2025-4-9 13:51:09

这个问题对于刚入门 NLP 的同学来说,可以说是一个比较核心的问题了。在自然语言处理领域中,token 和 tokenizer是两个核心概念,是在使用NLP模型的时候一定会接触的东西,理解这两者这个对于往后的学习是非常重要的。



NLP领域中的token和tokenization到底指的是什么?-1.jpg


图片来自于网络


一、什么是Token?


在NLP中,token是文本的基本单位。并且 OpenAI 的 ChatGPT 产品在很多应用场景下,也是根据 token 来计算的。


NLP领域中的token和tokenization到底指的是什么?-2.jpg


所以了解 token 是什么,在如今的自然语言处理中,不仅是理论的需要,也是应用的需要。我们小时候学语文,都是字组成词,词连成句,句连成文。而 token 可以是上述的任何一个单位,或者是其中某一个部分。也就是:字、词、短语、句子等。而英文里面就不太一样了,可能是:字母、单词、单词的子词、句子等。
Token对应着文本中的一个元素,通过Tokenization将文本划分成一个个的Token。例如上面的图片,在句子“We love NLP”中,“We”、“love”、“NLP”分别是三个Token。而在中文的处理上,并不可以简单通过单词就区分开每个token。类似 GPT-3.5 和 GPT-4中,“自然语言处理”就会被处理成以下 token:自、然、语、言、处理


NLP领域中的token和tokenization到底指的是什么?-3.jpg


二、  什么是Tokenizer?


Tokenizer是将文本切分成多个tokens的工具或算法。它负责将原始文本分割成tokens 序列。在NLP中,有多种不同类型的tokenizer,每种tokenizer都有其特定的应用场景和适用范围。

  • 基于字符的Tokenizer:将文本按照字符分割成token,适用于处理中文等没有空格分隔的语言。但是,正如 GPT-3.5 的切分效果,现在的大模型并不一定会按照这个方式划分,但是 bert-base-chinese 还是按照这个规则进行划分的。
  • 基于词的Tokenizer:将文本按照语言的语法规则分割成单词。适用于大部分语言,但对于某些复合词语言效果可能不佳。而最简单的语法规则应该是基于空格分割,它将文本字符串按照空白字符(如空格、制表符、换行符等)进行分割。这种方法适用于英文等使用空格分隔单词的语言,但在处理中文、日语等不使用空格分隔单词的语言时效果不佳。
  • 基于句子的Tokenizer:将文本按照句子进行划分。但是这种在实际应用中并不多见。
  • 基于深度学习的Tokenizer:利用神经网络模型来学习文本字符串的最佳分割方式。这种方法通常使用大量的标注数据进行训练,从而让模型能够捕捉到文本中的复杂特征和规律。基于深度学习的Tokenization在处理中文等不使用空格分隔单词的语言时表现出色,因为它可以学习到单词和句子的语义信息。

在中文处理中,由于中文文本不是以空格分隔单词,tokenizer的作用尤为重要。中文分词器需要识别句子中的词语边界,这通常涉及到语义、上下文以及复杂的语言规则。分词是中文自然语言处理的基础步骤,对于后续的任务如文本分类、情感分析、机器翻译等都有重要影响。分词之后的数据会被用来训练一个适合于中文的 tokenizer,训练好的 tokenizer 就可以用于切分token了,也就是基于深度学习的 tokenizer,应该算是一种新式的分词方式。
三、Token与Tokenizer的关系

Tokenization是由tokenizer完成的过程,tokenizer负责根据特定的规则将文本划分成token序列。Tokenization的质量和效果直接影响到后续NLP任务的结果。
在NLP流程中,Tokenizer通常是作为预处理步骤的一部分。在文本输入模型之前,需要先经过Tokenizer的处理,将文本转换成模型可以理解的Token序列。
  1. from transformers import AutoTokenizer, AutoModelForCausalLM
  2. tokenizer = AutoTokenizer.from_pretrained("模型/路径")
  3. model = AutoModelForCausalLM.from_pretrained("模型/路径")
  4. prompt = "我爱自然语言处理"
  5. input_ids = tokenizer(prompt, return_tensors="pt").input_ids
  6. # sample up to 30 tokens
  7. outputs = model.generate(input_ids, do_sample=True, max_length=30)
  8. print(tokenizer.decode(outputs[0], skip_special_tokens=True))
复制代码
在上述代码中,input_ids就是tokenizer处理后的tokens,但是其是以索引的形式存储的,如果想要转换成可读的文本,需要查找词表的对应索引的词语,或者是调用tokenizer的API。这
里的 model.generate 就是生成式AI生成文本,do_sample参数表示的是是否是根据概率分布采样,max_length表示的是最多生成的token数量,这是大模型生成的代码层面的一般套路。
如果对大模型不太熟悉的同学,答主推荐大家可以看一下由「知乎知学堂旗下AGI课堂」推出的【程序员的AI大模型进阶之旅】公开课,一共2天的体验课,给大家讲解大模型的有关内容,教你用LangChain+Fine-tune来训练大模型。趁着现在还免费,建议想了解大模型的都去听一下。加助教老师微信,还可以领取海量的资料。




四、  Tokenization 面临的问题


Tokenization在NLP任务中具有重要作用,但在实际应用中仍面临一些问题,主要包括以下几点:
1.  多种语言:世界上的语言种类很多,每种语言都有其独特的语法、词汇和表达方式。因此,为每种语言设计一个有效的Tokenization策略是一项具有挑战性的任务。例如上述所说的中文、日语和韩语等语言没有明显的单词分隔符,这使得Tokenization过程更加复杂。
2. 一词多义:自然语言中的词汇具有多义性,一个单词可能有多个含义和用法。在Tokenization过程中,需要准确地识别每个词汇的含义和用法,以便正确地将其分割成tokens。这需要丰富的词汇资源和上下文信息支持。比如同一个词语在不同的语境下可能需要组成一个词汇,或者分开两个词汇。
3.  多种策略:目前有许多预训练模型可供选择,如BERT、GPT、RoBERTa等。这些模型在预训练过程中使用了不同的Tokenization策略,因此在实际应用中需要根据任务需求和文本特点选择合适的预训练模型,并进行相应的调整。没有一个统一的金标准,需要人为去选择。

五、  结论

进行一个简单的总结就是,Token与Tokenizer是密切相关的概念。Token作为文本的基本单位,是Tokenizer处理的对象。Tokenizer根据特定的规则和算法将原始文本分割成Token序列,使得文本可以被进一步处理和分析。因此,Token与Tokenizer之间存在着依赖与协作的关系,缺一不可。
其次,Tokenizer的设计直接影响着Tokenization的质量和效果。不同语言、不同领域的文本可能需要不同的Tokenization策略,而Tokenizer的选择与设计则取决于具体的任务和需求。一种优秀的Tokenizer应该能够充分考虑到文本的多样性和复杂性,能够准确地识别出文本中的语义单位,同时尽量减少歧义和误差。
最后,Token与Tokenizer的研究与应用对于NLP领域的发展具有重要意义。随着NLP技术的不断进步和应用场景的不断拓展,对Token与Tokenizer的需求也在不断增加。未来,我们可以期待更加智能化、灵活性强的Tokenizer算法和技术的涌现,以更好地满足多种语言、更复杂灵活的文本处理需求,推动NLP领域的发展。

zzzss654321 LV

发表于 2025-4-9 14:00:59

前面都讲了是指什么,我来浅答一下目前大模型时代分词是怎么做的☺️,主要内容为WordPiece,Byte-Pair Encoding (BPE),Byte-level BPE(BBPE)分词方法的原理以及其代码实现,全篇阅读可能需要45分钟,建议收藏~
Tokenization(分词) 在自然语言处理(NLP)的任务中是最基本的一步,把文本内容处理为最小基本单元即token(标记,令牌,词元,没有准确的翻译)用于后续的处理,如何把文本处理成token呢?有一系列的方法,基本思想是构建一个词表通过词表一一映射进行分词,但如何构建合适的词表呢?以下以分词粒度为角度进行介绍:
1.word(词)粒度

在英文语系中,word(词)级别分词实现很简单,因为有天然的分隔符。在中文里面word(词)粒度,需要一些分词工具比如jieba,以下是中文和英文的例子:
  1. 中文句子:我喜欢看电影和读书。
  2. 分词结果:我 | 喜欢 | 看 | 电影 | 和 | 读书。
  3. 英文句子:I enjoy watching movies and reading books.
  4. 分词结果:I | enjoy | watching | movies | and | reading | books.
复制代码
word(词)粒度的优点有:

  • 语义明确:以词为单位进行分词可以更好地保留每个词的语义,使得文本在后续处理中能够更准确地表达含义。
  • 上下文理解:以词为粒度进行分词有助于保留词语之间的关联性和上下文信息,从而在语义分析和理解时能够更好地捕捉句子的意图。
缺点:

  • 长尾效应和稀有词问题: 词表可能变得巨大,包含很多不常见的词汇,增加存储和训练成本,稀有词的训练数据有限,难以获得准确的表示。
  • OOV(Out-of-Vocabulary): 词粒度分词模型只能使用词表中的词来进行处理,无法处理词表之外的词汇,这就是所谓的OOV问题。
  • 形态关系和词缀关系: 无法捕捉同一词的不同形态,也无法有效学习词缀在不同词汇之间的共通性,限制了模型的语言理解能力,比如love和loves在word(词)粒度的词表中将会是两个词。
2.char(字符)粒度

以字符为单位进行分词,即将文本拆分成一个个单独的字符作为最小基本单元,这种字符粒度的分词方法适用于多种语言,无论是英文、中文还是其他不同语言,都能够一致地使用字符粒度进行处理,因为英文就26个字母以及其他的一些符号,中文常见字就6000个左右。
  1. 中文句子:我喜欢看电影和读书。
  2. 分词结果:我 | 喜 | 欢 | 看 | 电 | 影 | 和 | 读 | 书 | 。
  3. 英文句子:I enjoy watching movies and reading books.
  4. 分词结果:I |   | e | n | j | o | y |   | w | a | t | c | h | i | n | g |   | m | o | v | i | e | s |   | a | n | d |   | r | e | a | d | i | n | g |   | b | o | o | k | s | .
复制代码
char(字符)粒度的优点有:

  • 统一处理方式:字符粒度分词方法适用于不同语言,无需针对每种语言设计不同的分词规则或工具,具有通用性。
  • 解决OOV问题:由于字符粒度分词可以处理任何字符,无需维护词表,因此可以很好地处理一些新创词汇、专有名词等问题。
缺点:

  • 语义信息不明确:字符粒度分词无法直接表达词的语义,可能导致在一些语义分析任务中效果较差。
  • 处理效率低:由于文本被拆分为字符,处理的粒度较小,增加后续处理的计算成本和时间。
3.subword(子词)粒度

在很多情况下,既不希望将文本切分成单独的词(太大),也不想将其切分成单个字符(太小),而是希望得到介于词和字符之间的子词单元。这就引入了 subword(子词)粒度的分词方法。
在BERT时代,WordPiece 分词方法被广泛应用[1],比如 BERT、DistilBERT等。WordPiece 分词方法是 subword(子词)粒度的一种方法。
3.1 WordPiece

WordPiece核心思想是将单词拆分成多个前缀符号(比如BERT中的##)最小单元,再通过子词合并规则将最小单元进行合并为子词级别。例如对于单词"word",拆分如下:
  1. w ##o ##r ##d
复制代码
然后通过合并规则进行合并,从而循环迭代构建出一个词表,以下是核心步骤:

  • 计算初始词表:通过训练语料获得或者最初的英文中26个字母加上各种符号以及常见中文字符,这些作为初始词表。
  • 计算合并分数:对训练语料拆分的多个子词单元通过合拼规则计算合并分数。
  • 合并分数最高的子词对:选择分数最高的子词对,将它们合并成一个新的子词单元,并更新词表。
  • 重复合并步骤:不断重复步骤 2 和步骤 3,直到达到预定的词表大小、合并次数,或者直到不再有有意义的合并(即,进一步合并不会显著提高词表的效益)。
  • 分词:使用最终得到的词汇表对文本进行分词。
简单举例[1]:
我们有以下的训练语料中的样例,括号中第2位为在训练语料中出现的频率:
  1. ("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)
复制代码
所以这些样例的初始词表将会是:
  1. ["b", "h", "p", "##g", "##n", "##s", "##u"]
复制代码
接下来重要的一步进行计算合并分数,也称作互信息(信息论中衡量两个变量之间的关联程度[2]),简单来说就是以下公式来计算
  1. score=(freq_of_pair)/(freq_of_first_element×freq_of_second_element)
  2. 分数 = 合并pair候选的频率 / (第一个元素的频率 × 第二个元素的频率)
复制代码
对于上述样例中这个pair("##u", "##g")出现的频率是最高的20次,但是"##u"出现的频率是36次, "##g"出现的频率是20次,所以这个pair("##u", "##g")的分数是(20)/(36*20) = 1/36,同理计算这个pair("##g", "##s")的分数为(5)/(20*5) = 1/20,所以最先合并的pair是("##g", "##s")→("##gs")。此时词表和拆分后的的频率将变成以下:
  1. Vocabulary: ["b", "h", "p", "##g", "##n", "##s", "##u", "##gs"]
  2. Corpus: ("h" "##u" "##g", 10), ("p" "##u" "##g", 5), ("p" "##u" "##n", 12), ("b" "##u" "##n", 4), ("h" "##u" "##gs", 5)
复制代码
重复上述的操作,直到达到你想要的词表的大小
  1. Vocabulary: ["b", "h", "p", "##g", "##n", "##s", "##u", "##gs", "hu", "hug"]
  2. Corpus: ("hug", 10), ("p" "##u" "##g", 5), ("p" "##u" "##n", 12), ("b" "##u" "##n", 4), ("hu" "##gs", 5)
复制代码
代码实现:
用一些包含中英文的文本作为训练语料,因为英文有天然的分隔符,所以在这个例子中,中文已经进行了分词:
  1. sentences = [
  2.     "我",
  3.     "喜欢",
  4.     "吃",
  5.     "苹果",
  6.     "他",
  7.     "不",
  8.     "喜欢",
  9.     "吃",
  10.     "苹果派",
  11.     "I like to eat apples",
  12.     "She has a cute cat",
  13.     "you are very cute",
  14.     "give you a hug",
  15. ]
复制代码
统计每个词出现的频率并初始化初始词表:
  1. from collections import defaultdict
  2. # 构建频率统计
  3. def build_stats(sentences):
  4.     stats = defaultdict(int)
  5.     for sentence in sentences:
  6.         symbols = sentence.split()
  7.         for symbol in symbols:
  8.             stats[symbol] += 1
  9.     return stats
  10. stats = build_stats(sentences)
  11. print("stats:", stats)
  12. alphabet = []
  13. for word in stats.keys():
  14.     if word[0] not in alphabet:
  15.         alphabet.append(word[0])
  16.     for letter in word[1:]:
  17.         if f"##{letter}" not in alphabet:
  18.             alphabet.append(f"##{letter}")
  19. alphabet.sort()
  20. # 初始词表
  21. vocab = alphabet.copy()
  22. print("alphabet:", alphabet)
  23. # 结果
  24. stats: defaultdict(<class 'int'>, {'我': 1, '喜欢': 2, '吃': 2, '苹果': 1, '他': 1, '不': 1, '苹果派': 1, 'I': 1, 'like': 1, 'to': 1, 'eat': 1, 'apples': 1, 'She': 1, 'has': 1, 'a': 2, 'cute': 2, 'cat': 1, 'you': 2, 'are': 1, 'very': 1, 'give': 1, 'hug': 1})
  25. # 初始词表
  26. alphabet: ['##a', '##e', '##g', '##h', '##i', '##k', '##l', '##o', '##p', '##r', '##s', '##t', '##u', '##v', '##y', '##果', '##欢', '##派', 'I', 'S', 'a', 'c', 'e', 'g', 'h', 'l', 't', 'v', 'y', '不', '他', '吃', '喜', '我', '苹']
复制代码
根据初始词表拆分每个词:
  1. splits = {
  2.     word: [c if i == 0 else f"##{c}" for i, c in enumerate(word)]
  3.     for word in stats.keys()
  4. }
  5. print("splits:", splits)
  6. # 结果
  7. splits: {'我': ['我'], '喜欢': ['喜', '##欢'], '吃': ['吃'], '苹果': ['苹', '##果'], '他': ['他'], '不': ['不'], '苹果派': ['苹', '##果', '##派'], 'I': ['I'], 'like': ['l', '##i', '##k', '##e'], 'to': ['t', '##o'], 'eat': ['e', '##a', '##t'], 'apples': ['a', '##p', '##p', '##l', '##e', '##s'], 'She': ['S', '##h', '##e'], 'has': ['h', '##a', '##s'], 'a': ['a'], 'cute': ['c', '##u', '##t', '##e'], 'cat': ['c', '##a', '##t'], 'you': ['y', '##o', '##u'], 'are': ['a', '##r', '##e'], 'very': ['v', '##e', '##r', '##y'], 'give': ['g', '##i', '##v', '##e'], 'hug': ['h', '##u', '##g']}
复制代码
根据上述提到的计算互信息的分数公式进行计算:
  1. def compute_pair_scores(splits):
  2.     letter_freqs = defaultdict(int)
  3.     pair_freqs = defaultdict(int)
  4.     for word, freq in stats.items():
  5.         split = splits[word]
  6.         if len(split) == 1:
  7.             letter_freqs[split[0]] += freq
  8.             continue
  9.         for i in range(len(split) - 1):
  10.             pair = (split[i], split[i + 1])
  11.             letter_freqs[split[i]] += freq
  12.             pair_freqs[pair] += freq
  13.         letter_freqs[split[-1]] += freq
  14.     scores = {
  15.         pair: freq / (letter_freqs[pair[0]] * letter_freqs[pair[1]])
  16.         for pair, freq in pair_freqs.items()
  17.     }
  18.     return scores
  19. pair_scores = compute_pair_scores(splits)
  20. for i, key in enumerate(pair_scores.keys()):
  21.     print(f"{key}: {pair_scores[key]}")
  22.     if i >= 5:
  23.         break
复制代码
一些结果:
  1. ('喜', '##欢'): 0.5
  2. ('苹', '##果'): 0.5
  3. ('##果', '##派'): 0.5
  4. ('l', '##i'): 0.5
  5. ('##i', '##k'): 0.5
  6. ('##k', '##e'): 0.125
复制代码
我们需要的是将分数最高的进行合并然后开始循环迭代,看一看分数最高的pair(子词对):
  1. best_pair = ""
  2. max_score = None
  3. for pair, score in pair_scores.items():
  4.     if max_score is None or max_score < score:
  5.         best_pair = pair
  6.         max_score = score
  7. print(best_pair, max_score)
  8. # 结果
  9. ('S', '##h') 1.0
复制代码
结果为('S', '##h') 1.0,所以最先合成的就是('S', '##h')→'##Sh',合并的函数如下:
  1. def merge_pair(a, b, splits):
  2.     for word in stats:
  3.         split = splits[word]
  4.         if len(split) == 1:
  5.             continue
  6.         i = 0
  7.         while i < len(split) - 1:
  8.             if split[i] == a and split[i + 1] == b:
  9.                 merge = a + b[2:] if b.startswith("##") else a + b
  10.                 split = split[:i] + [merge] + split[i + 2 :]
  11.             else:
  12.                 i += 1
  13.         splits[word] = split
  14.     return splits
复制代码
最后就是一直进行循环迭代,直到vocab达到了我们想要的数量
  1. vocab_size = 50
  2. while len(vocab) < vocab_size:
  3.     scores = compute_pair_scores(splits)
  4.     best_pair, max_score = "", None
  5.     for pair, score in scores.items():
  6.         if max_score is None or max_score < score:
  7.             best_pair = pair
  8.             max_score = score
  9.     splits = merge_pair(*best_pair, splits)
  10.     new_token = (
  11.         best_pair[0] + best_pair[1][2:]
  12.         if best_pair[1].startswith("##")
  13.         else best_pair[0] + best_pair[1]
  14.     )
  15.     vocab.append(new_token)
  16. print("vocab:", vocab)
  17. # 结果
  18. vocab: ['##a', '##e', '##g', '##h', '##i', '##k', '##l', '##o', '##p', '##r', '##s', '##t', '##u', '##v', '##y', '##果', '##欢', '##派', 'I', 'S', 'a', 'c', 'e', 'g', 'h', 'l', 't', 'v', 'y', '不', '他', '吃', '喜', '我', '苹', 'Sh', '喜欢', '苹果', '苹果派', 'li', 'lik', 'gi', 'giv', '##pl', '##ppl', '##ry', 'to', 'yo', 'ea', 'eat']
复制代码
上述就是WordPiece分词方法的代码实现,一般来说最后会在词表中加上一些特殊词汇,以及英文中26个字母加上各种符号以及常见中文字符,不过如果训练语料比较大以及词表比较大那这些应该也是已经包括了,只需要添加特殊词汇:
  1. all_vocab = vocab + ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"] + other_alphabet
复制代码
在大语言模型时代,最常用的分词方法是Byte-Pair Encoding (BPE)和Byte-level BPE(BBPE),Byte-Pair Encoding (BPE)最初是一种文本压缩算法在15年被引入到NLP用于分词[3],在训练 GPT 时被 OpenAI 用于tokenization,后续好多模型GPT,RoBERTa等都采用了这种分词方法。Byte-level BPE(BBPE)是于19年在BPE的基础上提出以Byte-level(字节)为粒度的分词方法[4],目前 GPT2,BLOOM,Llama,Falcon等采用的是该分词方法。
3.2 Byte-Pair Encoding (BPE)

Byte-Pair Encoding (BPE)核心思想是逐步合并出现频率最高的子词对而不是像Wordpiece计算合并分数,从而构建出一个词汇表,以下是核心步骤:

  • 计算初始词表:通过训练语料获得或者最初的英文中26个字母加上各种符号以及常见中文字符,这些作为初始词表。
  • 构建频率统计:统计所有子词单元对(两个连续的子词)在文本中的出现频率。
  • 合并频率最高的子词对:选择出现频率最高的子词对,将它们合并成一个新的子词单元,并更新词汇表。
  • 重复合并步骤:不断重复步骤 2 和步骤 3,直到达到预定的词汇表大小、合并次数,或者直到不再有有意义的合并(即,进一步合并不会显著提高词汇表的效益)。
  • 分词:使用最终得到的词汇表对文本进行分词。
简单的代码实现[5]:
用一些包含中英文的文本作为训练语料和上面相同,因为英文有天然的分隔符,所以在这个例子中,中文已经进行了分词:
  1. sentences = [
  2.     "我",
  3.     "喜欢",
  4.     "吃",
  5.     "苹果",
  6.     "他",
  7.     "不",
  8.     "喜欢",
  9.     "吃",
  10.     "苹果派",
  11.     "I like to eat apples",
  12.     "She has a cute cat",
  13.     "you are very cute",
  14.     "give you a hug",
  15. ]
复制代码
统计每个词出现的频率并初始化初始词表:
  1. # 构建频率统计
  2. def build_stats(sentences):
  3.     stats = defaultdict(int)
  4.     for sentence in sentences:
  5.         symbols = sentence.split()
  6.         for symbol in symbols:
  7.             stats[symbol] += 1
  8.     return stats
  9. stats = build_stats(sentences)
  10. print("stats:", stats)
  11. alphabet = []
  12. for word in stats.keys():
  13.     for letter in word:
  14.         if letter not in alphabet:
  15.             alphabet.append(letter)
  16. alphabet.sort()
  17. # 初始词表
  18. vocab = alphabet.copy()
  19. print("alphabet:", alphabet)
  20. # 结果
  21. stats: defaultdict(<class 'int'>, {'我': 1, '喜欢': 2, '吃': 2, '苹果': 1, '他': 1, '不': 1, '苹果派': 1, 'I': 1, 'like': 1, 'to': 1, 'eat': 1, 'apples': 1, 'She': 1, 'has': 1, 'a': 2, 'cute': 2, 'cat': 1, 'you': 2, 'are': 1, 'very': 1, 'give': 1, 'hug': 1})
  22. # 初始词表
  23. alphabet: ['I', 'S', 'a', 'c', 'e', 'g', 'h', 'i', 'k', 'l', 'o', 'p', 'r', 's', 't', 'u', 'v', 'y', '不', '他', '吃', '喜', '我', '果', '欢', '派', '苹']
复制代码
根据初始词表拆分每个词,计算左右pair(子词对)出现的频率
  1. splits = {word: [c for c in word] for word in stats.keys()}
  2. print("splits:", splits)
  3. def compute_pair_freqs(splits):
  4.     pair_freqs = defaultdict(int)
  5.     for word, freq in stats.items():
  6.         split = splits[word]
  7.         if len(split) == 1:
  8.             continue
  9.         for i in range(len(split) - 1):
  10.             pair = (split[i], split[i + 1])
  11.             pair_freqs[pair] += freq
  12.     return pair_freqs
  13. pair_freqs = compute_pair_freqs(splits)
  14. for i, key in enumerate(pair_freqs.keys()):
  15.     print(f"{key}: {pair_freqs[key]}")
  16.     if i >= 5:
  17.         break
  18. # 结果
  19. splits: {'我': ['我'], '喜欢': ['喜', '欢'], '吃': ['吃'], '苹果': ['苹', '果'], '他': ['他'], '不': ['不'], '苹果派': ['苹', '果', '派'], 'I': ['I'], 'like': ['l', 'i', 'k', 'e'], 'to': ['t', 'o'], 'eat': ['e', 'a', 't'], 'apples': ['a', 'p', 'p', 'l', 'e', 's'], 'She': ['S', 'h', 'e'], 'has': ['h', 'a', 's'], 'a': ['a'], 'cute': ['c', 'u', 't', 'e'], 'cat': ['c', 'a', 't'], 'you': ['y', 'o', 'u'], 'are': ['a', 'r', 'e'], 'very': ['v', 'e', 'r', 'y'], 'give': ['g', 'i', 'v', 'e'], 'hug': ['h', 'u', 'g']}
  20. ('喜', '欢'): 2
  21. ('苹', '果'): 2
  22. ('果', '派'): 1
  23. ('l', 'i'): 1
  24. ('i', 'k'): 1
  25. ('k', 'e'): 1
复制代码
然后开始循环迭代找到出现频率最高的pair(子词对):
  1. best_pair = ""
  2. max_freq = None
  3. for pair, freq in pair_freqs.items():
  4.     if max_freq is None or max_freq < freq:
  5.         best_pair = pair
  6.         max_freq = freq
  7. print(best_pair, max_freq)
复制代码
结果为【('喜', '欢') 2】,所以最先合成的就是('喜', '欢')→'喜欢',然后合并的函数如下:
  1. def merge_pair(a, b, splits):
  2.     for word in stats:
  3.         split = splits[word]
  4.         if len(split) == 1:
  5.             continue
  6.         i = 0
  7.         while i < len(split) - 1:
  8.             if split[i] == a and split[i + 1] == b:
  9.                 split = split[:i] + [a + b] + split[i + 2 :]
  10.             else:
  11.                 i += 1
  12.         splits[word] = split
  13.     return splits
复制代码
最后就是一直进行循环直到vocab达到了我们想要的数量:
  1. # 假设我们想要的词典为50
  2. merges = {}
  3. vocab_size = 50
  4. while len(vocab) < vocab_size:
  5.     pair_freqs = compute_pair_freqs(splits)
  6.     best_pair = ""
  7.     max_freq = None
  8.     for pair, freq in pair_freqs.items():
  9.         if max_freq is None or max_freq < freq:
  10.             best_pair = pair
  11.             max_freq = freq
  12.     splits = merge_pair(*best_pair, splits)
  13.     merges[best_pair] = best_pair[0] + best_pair[1]
  14.     vocab.append(best_pair[0] + best_pair[1])
  15. print("merges:", merges)
  16. print("vocab:", vocab)
  17. # 结果
  18. merges: {('喜', '欢'): '喜欢', ('苹', '果'): '苹果', ('a', 't'): 'at', ('c', 'u'): 'cu', ('cu', 't'): 'cut', ('cut', 'e'): 'cute', ('y', 'o'): 'yo', ('yo', 'u'): 'you', ('v', 'e'): 've', ('苹果', '派'): '苹果派', ('l', 'i'): 'li', ('li', 'k'): 'lik', ('lik', 'e'): 'like', ('t', 'o'): 'to', ('e', 'at'): 'eat', ('a', 'p'): 'ap', ('ap', 'p'): 'app', ('app', 'l'): 'appl', ('appl', 'e'): 'apple', ('apple', 's'): 'apples', ('S', 'h'): 'Sh', ('Sh', 'e'): 'She', ('h', 'a'): 'ha'}
  19. vocab: ['I', 'S', 'a', 'c', 'e', 'g', 'h', 'i', 'k', 'l', 'o', 'p', 'r', 's', 't', 'u', 'v', 'y', '不', '他', '吃', '喜', '我', '果', '欢', '派', '苹', '喜欢', '苹果', 'at', 'cu', 'cut', 'cute', 'yo', 'you', 've', '苹果派', 'li', 'lik', 'like', 'to', 'eat', 'ap', 'app', 'appl', 'apple', 'apples', 'Sh', 'She', 'ha']
复制代码
再加上一些特殊词汇和其他词汇:
  1. all_vocab = vocab + ["[PAD]", "[UNK]", "[BOS]", "[EOS]"] + other_alphabet
复制代码
上述就是BPE的代码实现,BPE理论上还是会出现OOV的,当词汇表的大小受限时,一些较少频繁出现的子词和没有在训练过程中见过的子词,就会无法进入词汇表出现OOV,而Byte-level BPE(BBPE)理论上是不会出现这个情况的。
3.3 Byte-level BPE(BBPE)

基础知识:
Unicode: Unicode 是一种字符集,旨在涵盖地球上几乎所有的书写系统和字符。它为每个字符分配了一个唯一的代码点(code point)用于标识字符。Unicode 不关注字符在计算机内部的具体表示方式,而只是提供了一种字符到代码点的映射。Unicode 的出现解决了字符集的碎片化问题,使得不同的语言和字符能够在一个共同的标准下共存。然而,Unicode 并没有规定如何在计算机内存中存储和传输这些字符。
UTF-8: UTF-8(Unicode Transformation Format-8)是一种变长的字符编码方案,它将 Unicode 中的代码点转换为字节序列。UTF-8 的一个重要特点是它是向后兼容 ASCII 的,这意味着标准的 ASCII 字符在 UTF-8 中使用相同的字节表示,从而确保现有的 ASCII 文本可以无缝地与 UTF-8 共存。在 UTF-8 编码中,字符的表示长度可以是1到4个字节,不同范围的 Unicode 代码点使用不同长度的字节序列表示,这样可以高效地表示整个 Unicode 字符集。UTF-8 的编码规则是:

  • 单字节字符(ASCII 范围内的字符)使用一个字节表示,保持与 ASCII 编码的兼容性。
  • 带有更高代码点的字符使用多个字节表示。UTF-8 使用特定的字节序列来指示一个字符所需的字节数,以及字符的实际数据。
例如,英文字母 "A" 的 Unicode 代码点是U+0041,在 UTF-8 中表示为 0x41(与 ASCII 编码相同);而中文汉字 "你" 的 Unicode 代码点是U+4F60,在 UTF-8 中表示为0xE4 0xBD 0xA0三个字节的序列。
所以简单的来说:

  • Unicode 是字符集,为每个字符分配唯一的代码点。
  • UTF-8 是一种基于 Unicode 的字符编码方式,用于在计算机中存储和传输字符。
Byte(字节):计算机存储和数据处理时,字节是最小的单位。一个字节包含8个(Bit)二进制位,每个位可以是0或1,每位的不同排列和组合可以表示不同的数据,所以一个字节能表示的范围是256个。
言归正传:
Byte-level BPE(BBPE)和Byte-Pair Encoding (BPE)区别就是BPE是最小词汇是字符级别,而BBPE是字节级别的,通过UTF-8的编码方式这一个字节的256的范围,理论上可以表示这个世界上的所有字符。
所以实现的步骤和BPE就是实现的粒度不一样,其他的都是一样的。

  • 初始词表:构建初始词表,包含一个字节的所有表示(256)。
  • 构建频率统计:统计所有子词单元对(两个连续的子词)在文本中的出现频率。
  • 合并频率最高的子词对:选择出现频率最高的子词对,将它们合并成一个新的子词单元,并更新词汇表。
  • 重复合并步骤:不断重复步骤 2 和步骤 3,直到达到预定的词汇表大小、合并次数,或者直到不再有有意义的合并(即,进一步合并不会显著提高词汇表的效益)。
  • 分词:使用最终得到的词汇表对文本进行分词。
简单代码实现,不做赘述,读者朋友们可以自己实现一下
  1. from collections import defaultdict
  2. sentences = [
  3.     "我",
  4.     "喜欢",
  5.     "吃",
  6.     "苹果",
  7.     "他",
  8.     "不",
  9.     "喜欢",
  10.     "吃",
  11.     "苹果派",
  12.     "I like to eat apples",
  13.     "She has a cute cat",
  14.     "you are very cute",
  15.     "give you a hug",
  16. ]
  17. # 构建初始词汇表,包含一个字节的256个表示
  18. initial_vocab = [bytes([byte]) for byte in range(256)]
  19. vocab = initial_vocab.copy()
  20. print("initial_vocab:", initial_vocab)
  21. # 构建频率统计
  22. def build_stats(sentences):
  23.     stats = defaultdict(int)
  24.     for sentence in sentences:
  25.         symbols = sentence.split()
  26.         for symbol in symbols:
  27.             stats[symbol.encode("utf-8")] += 1
  28.     return stats
  29. stats = build_stats(sentences)
  30. splits = {word: [byte for byte in word] for word in stats.keys()}
  31. def compute_pair_freqs(splits):
  32.     pair_freqs = defaultdict(int)
  33.     for word, freq in stats.items():
  34.         split = splits[word]
  35.         if len(split) == 1:
  36.             continue
  37.         for i in range(len(split) - 1):
  38.             pair = (split[i], split[i + 1])
  39.             pair_freqs[pair] += freq
  40.     return pair_freqs
  41. pair_freqs = compute_pair_freqs(splits)
  42. def merge_pair(pair, splits):
  43.     merged_byte = bytes(pair)
  44.     for word in stats:
  45.         split = splits[word]
  46.         if len(split) == 1:
  47.             continue
  48.         i = 0
  49.         while i < len(split) - 1:
  50.             if split[i:i+2] == pair:  # 检查分割中是否有这对字节
  51.                 split = split[:i] + [merged_byte] + split[i + 2 :]
  52.             else:
  53.                 i += 1
  54.         splits[word] = split
  55.     return splits
  56. vocab_size = 50
  57. while len(vocab) < vocab_size:
  58.     pair_freqs = compute_pair_freqs(splits)
  59.     best_pair = ()
  60.     max_freq = None
  61.     for pair, freq in pair_freqs.items():
  62.         if max_freq is None or max_freq < freq:
  63.             best_pair = pair
  64.             max_freq = freq
  65.     splits = merge_pair(best_pair, splits)
  66.     merged_byte = bytes(best_pair)
  67. print("vocab:", vocab)
复制代码
着重解释一下为什么Byte-level BPE(BBPE)不会出现OOV问题,初始的词表里有256个表示如下:
  1. [b'\x00', b'\x01', b'\x02', b'\x03', b'\x04', b'\x05', b'\x06', b'\x07', b'\x08', b'\t', b'\n', b'\x0b', b'\x0c', b'\r', b'\x0e', b'\x0f', b'\x10', b'\x11', b'\x12', b'\x13', b'\x14', b'\x15', b'\x16', b'\x17', b'\x18', b'\x19', b'\x1a', b'\x1b', b'\x1c', b'\x1d', b'\x1e', b'\x1f', b' ', b'!', b'"', b'#', b'$', b'%', b'&', b"'", b'(', b')', b'*', b'+', b',', b'-', b'.', b'/', b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b':', b';', b'<', b'=', b'>', b'?', b'@', b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', b'Z', b'[', b'\\', b']', b'^', b'_', b'`', b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', b'x', b'y', b'z', b'{', b'|', b'}', b'~', b'\x7f', b'\x80', b'\x81', b'\x82', b'\x83', b'\x84', b'\x85', b'\x86', b'\x87', b'\x88', b'\x89', b'\x8a', b'\x8b', b'\x8c', b'\x8d', b'\x8e', b'\x8f', b'\x90', b'\x91', b'\x92', b'\x93', b'\x94', b'\x95', b'\x96', b'\x97', b'\x98', b'\x99', b'\x9a', b'\x9b', b'\x9c', b'\x9d', b'\x9e', b'\x9f', b'\xa0', b'\xa1', b'\xa2', b'\xa3', b'\xa4', b'\xa5', b'\xa6', b'\xa7', b'\xa8', b'\xa9', b'\xaa', b'\xab', b'\xac', b'\xad', b'\xae', b'\xaf', b'\xb0', b'\xb1', b'\xb2', b'\xb3', b'\xb4', b'\xb5', b'\xb6', b'\xb7', b'\xb8', b'\xb9', b'\xba', b'\xbb', b'\xbc', b'\xbd', b'\xbe', b'\xbf', b'\xc0', b'\xc1', b'\xc2', b'\xc3', b'\xc4', b'\xc5', b'\xc6', b'\xc7', b'\xc8', b'\xc9', b'\xca', b'\xcb', b'\xcc', b'\xcd', b'\xce', b'\xcf', b'\xd0', b'\xd1', b'\xd2', b'\xd3', b'\xd4', b'\xd5', b'\xd6', b'\xd7', b'\xd8', b'\xd9', b'\xda', b'\xdb', b'\xdc', b'\xdd', b'\xde', b'\xdf', b'\xe0', b'\xe1', b'\xe2', b'\xe3', b'\xe4', b'\xe5', b'\xe6', b'\xe7', b'\xe8', b'\xe9', b'\xea', b'\xeb', b'\xec', b'\xed', b'\xee', b'\xef', b'\xf0', b'\xf1', b'\xf2', b'\xf3', b'\xf4', b'\xf5', b'\xf6', b'\xf7', b'\xf8', b'\xf9', b'\xfa', b'\xfb', b'\xfc', b'\xfd', b'\xfe', b'\xff']
复制代码
通过上述的方式其实是在一直根据训练语料循环迭代合成子词或者词,最后形成词表,比如“苹果”通过UTF-8进行编码后为“\xe8\x8b\xb9\xe6\x9e\x9c”,如果词表里面有,那“苹果”就通过词表映射成了1个表示,准确来说是1个token;如果词表里没有,那就用256中的“\xe8+\x8b+\xb9+\xe6+\x9e+\x9c”来表示“苹果”这个词,那就是6个token。在先前的各种分词方法中,如果词典里没有”苹果“这个词,也没有”苹“,”果“这样的子词的话,那就变成了[UNK]。所以在现在的大模型中,以Byte-level BPE(BBPE)这种方式进行分词是不会出现OOV,但词表中如果没有word级别的词的话,一些中英文就会分词分的很细碎,比如Llama在中文上就会把一些词分成多个token其实就是UTF-8后的中文编码,对编码效率以及语义会有影响,于是出现了一些扩充Llama中文词表的工作。
上述分词算法在工程上实现一般使用sentencpiece工具包[6],谷歌在这个包中实现了上述的一系列算法,扩充Llama中文词表的工作也都是在此上面实现的。后续我也会写一篇文章进行详细的讲解。欢迎关注~
其他文章:
Glan格蓝:LLM大语言模型Generate/Inference生成/推理参数与解码策略原理及其代码实现

木色小罗 LV

发表于 2025-4-9 14:10:58

简而言之:

token可以理解为最小语义单元,翻译的话个人喜欢叫词元(当然翻译成令牌、词都行),可以是word/char/subword。
tokenization是指分词,目的是将输入文本分成一个个词元,保证各个词元拥有相对完整和独立的语义,以供后续任务(比如学习embedding或者作为高级模型的输入)使用。
原文:

羡鱼智能:【OpenLLM 008】大模型基础组件1-分词算法与分词器(tokenization & tokenizers)0.序章

笔者在上一篇文章中对最近折腾大模型的过程进行了反思,痛定思痛,决定除了工作部分以外不再浪费太多时间去跑更大规模的模型,同时决心开一些新坑来倒逼输入并与大家交流讨论,暂时的想法是在OpenLLM下面做两个系列:LLM基础组件和LLM炼丹术。
注:从4.11开始,不知不觉居然写到OpenLLM 008了,这十几天累成狗了,最快乐的时候居然是忙里偷闲写这些东西的时候,amazing!
LLM基础组件


  • tokenization&tokenizers:分词算法与分词器
  • 位置编码
  • attention机制
  • 基础架构与attention mask
  • 归一化
  • 激活函数
行为思路

分词算法与分词器作为LLM(大语言模型)的基础组件,作用相当于文本与模型的桥梁。因此作为LLM基础组件系列的开篇,本文将对主流的分词算法和分词器进行全面的梳理和介绍。
updates


  • 2023/04/30,资料阅读+整理,完成大纲;
  • 2023/05/01,主流subword算法伪代码;bert分词代码解读;
  • 2023/05/02,+byte-level BPE、优缺点、示例、总结等,主体内容基本算是写完了;剩余的代码实现示例和具体模型的分词器示例后续有空再补(看优先级和精力);
  • 2023/05/03,XX;
1.分词算法

tokenization算法大致经历了从word/char到subword的进化,这一章首先介绍不同的分词粒度,然后对主流的三大subword分词算法进行介绍,配合代码和实例,希望可以对subword算法有一个比较全面的梳理。
0.文本应该分成什么粒度?

分词的目的是将输入文本分成一个个词元,保证各个词元拥有相对完整和独立的语义,以供后续任务(比如学习embedding或者作为高级模型的输入)使用。
首先,最自然的粒度当然是词粒度。词,作为语言最自然的基本单元,在英文等语言中有着天然的空格分隔,但是对于中文等语言可能需要额外的分词算法来进行处理(比如中文的jieba分词)。不过,我们总归是有办法获得各种各样的词的,这并不是一个致命的问题。真正影响词粒度分词算法应用问题主要有:1)词粒度的词表由于长尾效应可能会非常大,包含很多的稀有词,存储和训练的成本都很高,并且稀有词往往很难学好;2)OOV问题,对于词表之外的词无能为力;3)无法处理单词的形态关系和词缀关系:同一个词的不同形态,语义相近,完全当做不同的单词不仅增加了训练成本,而且无法很好的捕捉这些单词之间的关系;同时,也无法学习词缀在不同单词之间的泛化。
那么,一个很自然的想法就是使用字符粒度的词表,这样OOV问题迎刃而解了,但是字符粒度太细了,会造成新的问题:1)无法承载丰富的语义;2)序列长度增长,带来计算成本的增长。
所以,如何结合word和char粒度各自的优势呢?subword分词应运而生,顾名思义,粒度介于char和Word之间,基本思想为常用词应该保持原状,生僻词应该拆分成子词以共享token压缩空间,所以可以较好的平衡词表大小与语义表达能力,比如OOV问题可以通过subword的组合来解决。
目前有三种主流的Subword分词算法,分别是Byte Pair Encoding (BPE), WordPiece和Unigram Language Model。
总结一下,文本的分词粒度:

  • word:

    • 优点:词的边界和含义得到保留;
    • 缺点:1)词表大,稀有词学不好;2)OOV;3)无法处理单词形态关系和词缀关系;



  • char:

    • 优点:词表极小,比如26个英文字母几乎可以组合出所有词,5000多个中文常用字基本也能组合出足够的词汇;
    • 缺点:1)无法承载丰富的语义;2)序列长度大幅增长;



  • subword:可以较好的平衡词表大小与语义表达能力;
1.BPE

BPE最早其实是一种数据压缩算法,基本思想是将经常一起出现的数据对替换为不在数据串中的其他字符,后续可以通过一个merge表来恢复原始数据。在2015年,由论文[1508.07909] Neural Machine Translation of Rare Words with Subword Units引入NLP领域。
核心思想:

从一个基础小词表开始,通过不断合并最高频的连续token对来产生新的token。
具体做法:

输入:训练语料;词表大小V
1.准备基础词表:比如英文中26个字母加上各种符号;
2.基于基础词表将语料拆分为最小单元;
3.在语料上统计单词内相邻单元对的频率,选择频率最高的单元对进行合并;
4.重复第3步直到达到预先设定的subword词表大小或下一个最高频率为1;
输出:BPE算法得到的subword词表
下面是一个BPE的训练示例:

NLP领域中的token和tokenization到底指的是什么?-1.jpg

优势与劣势:

优势:可以有效地平衡词汇表大小和编码步数(编码句子所需的token数量,与词表大小和粒度有关)。
劣势:基于贪婪和确定的符号替换,不能提供带概率的多个分词结果(这是相对于ULM而言的);decode的时候面临歧义问题。
BPE的劣势:

NLP领域中的token和tokenization到底指的是什么?-2.jpg

代码实现:

refs:

[1508.07909] Neural Machine Translation of Rare Words with Subword Units
理解NLP最重要的编码方式 — Byte Pair Encoding (BPE),这一篇就够了 - 硅谷谷主的文章 - 知乎
https://zhuanlan.zhihu.com/p/424631681
2.Byte-level BPE

2019年12月:《Neural Machine Translation with Byte-Level Subwords》,论文提出了一种新的subword算法,称之为BBPE,即Byte-level BPE。
核心思想:

将BPE的思想从字符级别扩展到子节级别
具体做法:

摘要:几乎所有现有的机器翻译模型都建立在基于字符的词汇表之上:characters, subwords or words(只是字符的粒度不同)。 然而,来自噪声文本或字符丰富的语言(如日语和中文)的稀有字符可能会不必要地占用词汇槽并限制其紧凑性在字节级别表示文本并使用 256 字节集作为词汇表是解决此问题的潜在方法。 然而,高昂的计算成本阻碍了它在实践中的广泛部署或使用。 在本文中,我们研究了字节级子词,具体为字节级 BPE (BBPE),它比字符词汇表更紧凑,没有词汇表外的标记,但比仅使用纯字节更有效。 我们声称上下文化 BBPE 嵌入是必要的,这可以通过卷积层或循环层来实现。 我们的实验表明,BBPE 具有与 BPE 相当的性能,而其大小仅为 BPE 的 1/8在多语言设置中,BBPE 最大限度地共享多种语言的词汇并实现更好的翻译质量。 此外,我们表明 BBPE 可以在具有非重叠字符集的语言之间实现可迁移的模型

NLP领域中的token和tokenization到底指的是什么?-3.jpg

我们考虑文本的UTF8编码,它将每个Unicode字符编码成1到4个字节。这允许我们将句子建模为字节序列,而不是字符序列。虽然有覆盖150多种语言的138K Unicode字符,但我们可以将任何语言的句子表示为UTF-8字节序列(只需要256个可能的字节中的248个)。
文本的字节序列表示通常比字符序列表示长得多(高达4倍),这使得按原样使用字节(只使用256的子节集)在计算上要求很高。作为另一种选择,我们考虑将字节序列分割成可变长度的n-gram(字节级“subwords”)。具体地说,我们学习关于字节级表示的BPE词汇,该表示用字节n-gram扩展了UTF-8字节集,称之为BBPE。图一展示了BBPE与BPE的对比。

NLP领域中的token和tokenization到底指的是什么?-4.jpg

不同的词表对序列长度的影响

NLP领域中的token和tokenization到底指的是什么?-5.jpg

词表粒度由细到粗,分词序列的对比

NLP领域中的token和tokenization到底指的是什么?-6.jpg

我们可以验证一下上图中的部分编码,可以看到是一致的:
https://www.browserling.com/tools/utf8-encode

NLP领域中的token和tokenization到底指的是什么?-7.jpg


NLP领域中的token和tokenization到底指的是什么?-8.jpg


NLP领域中的token和tokenization到底指的是什么?-9.jpg

优势与劣势:

优势:1)效果与BPE相当,但词表大为减小;2)可以在多语言之间通过字节级别的子词实现更好的共享;3)即使字符集不重叠,也可以通过子节层面的共享来实现良好的迁移。
劣势:1)编码序列时,长度可能会略长于BPE,计算成本更高;2)由byte解码时可能会遇到歧义,需要通过上下文信息和动态规划来进行解码。

NLP领域中的token和tokenization到底指的是什么?-10.jpg

refs:

Neural Machine Translation with Byte-Level Subwords
https://arxiv.org/abs/1909.03341
浅谈Byte-Level BPE - CaesarEX的文章 - 知乎
https://zhuanlan.zhihu.com/p/146114164
tokenizers小结 - 马东什么的文章 - 知乎
https://zhuanlan.zhihu.com/p/360290118
3.WordPiece

WordPiece出自《JAPANESE AND KOREAN VOICE SEARCH》,并用于解决日语和韩语的语音问题。
核心思想:

与BPE类似,也是从一个基础小词表出发,通过不断合并来产生最终的词表。主要的差别在于,BPE按频率来选择合并的token对,而wordpiece按token间的互信息来进行合并。注:互信息,在分词领域有时也被称为凝固度、内聚度,可以反映一个词内部的两个部分结合的紧密程度。
具体做法:

除了合并对象的选择以外,基本同BPE;
输入:训练语料;词表大小V
1.准备基础词表:比如英文中26个字母加上各种符号;
2.基于基础词表将语料拆分为最小单元;
3.基于第2步数据训练语言模型,可以是unigram语言模型,通过极大似然进行估计即可;
4.从所有可能得token对中选择,选择合并后可以最大程度地增加训练数据概率的token对进行合并,具体的score=(freq_of_pair)/(freq_of_first_element×freq_of_second_element),当然你也可以取个log,就变成了互信息,选择最高的单元对进行合并;
5.重复第4步直到达到预先设定的subword词表大小或概率增量低于某一阈值;
输出:wordpiece算法得到的subword词表

NLP领域中的token和tokenization到底指的是什么?-11.jpg

优势与劣势:

优势:可以较好的平衡词表大小和OOV问题;
劣势:可能会产生一些不太合理的子词或者说错误的切分;对拼写错误非常敏感;对前缀的支持不够好;
复合词错误的切分:

NLP领域中的token和tokenization到底指的是什么?-12.jpg

前缀的错误处理:

NLP领域中的token和tokenization到底指的是什么?-13.jpg

一种解决方案是:将复合词拆开;将前缀也拆开

NLP领域中的token和tokenization到底指的是什么?-14.jpg


NLP领域中的token和tokenization到底指的是什么?-15.jpg

代码实现:

refs:

japanese and korean voice search
https://static.googleusercontent.com/media/research.google.com/zh-CN//pubs/archive/37842.pdf

4.ULM

ULM出自《 Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates 》。
核心思想:

初始化一个大词表,然后通过unigram 语言模型计算删除不同subword造成的损失来代表subword的重要性,保留loss较大或者说重要性较高的subword。
具体做法:

输入:训练语料;词表大小V
1.准备基础词表:初始化一个很大的词表,比如所有字符+高频ngram,也可以通过BPE算法初始化;
2.针对当前词表,用EM算法估计每个子词在语料上的概率;
3.计算删除每个subword后对总loss的影响,作为该subword的loss;
4.将子词按照loss大小进行排序,保留前x%的子词;注意,单字符不能被丢弃,以免OOV;
5.重复步骤2到4,直到词表大小减少到设定值;
输出:ULM算法得到的subword词表
可见,ULM会倾向于保留那些以较高频率出现在很多句子的分词结果中的子词,因为这些子词如果被删除,其损失会很大。

NLP领域中的token和tokenization到底指的是什么?-16.jpg

优势与劣势:

优势:1)使用的训练算法可以利用所有可能的分词结果,这是通过data sampling算法实现的;2)提出一种基于语言模型的分词算法,这种语言模型可以给多种分词结果赋予概率,从而可以学到其中的噪声;3)使用时也可以给出带概率的多个分词结果。
劣势:1)效果与初始词表息息相关,初始的大词表要足够好,比如可以通过BPE来初始化;2)略显复杂。
代码实现:

refs:

Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates
https://arxiv.org/abs/1804.10959
NLP三大Subword模型详解:BPE、WordPiece、ULM - 阿北的文章 - 知乎
https://zhuanlan.zhihu.com/p/191648421
5.SentencePiece

SentencePiece,有些文章将其看作一种分词方法,有的地方将其视为一个分词工具包。个人更倾向于后者,但是将其看作一种分词算法也未尝不可(因为不仅是分词算法的集成,还做了很多优化)。
官方介绍:
SentencePiece is an unsupervised text tokenizer and detokenizer mainly for Neural Network-based text generation systems where the vocabulary size is predetermined prior to the neural model training. SentencePiece implements subword units (e.g., byte-pair-encoding (BPE) [Sennrich et al.]) and unigram language model [Kudo.]) with the extension of direct training from raw sentences. SentencePiece allows us to make a purely end-to-end system that does not depend on language-specific pre/postprocessing.
https://github.com/google/sentencepiece
主要特性


  • 多分词粒度:支持BPE、ULM子词算法,也支持char, word分词;
  • 多语言:以unicode方式编码字符,将所有的输入(英文、中文等不同语言)都转化为unicode字符,解决了多语言编码方式不同的问题;
  • 编解码的可逆性:之前几种分词算法对空格的处理略显粗暴,有时是无法还原的。Sentencepiece显式地将空白作为基本标记来处理,用一个元符号 “▁”( U+2581 )转义空白,这样就可以实现简单且可逆的编解码;
  • 无须Pre-tokenization:Sentencepiece可以直接从raw text/setences进行训练,无须Pre-tokenization;
  • Fast and lightweight;

NLP领域中的token和tokenization到底指的是什么?-17.jpg

编解码的可逆性:
Decode(Encode(Normalized(text)))= Normalized(text)
一个中文转Unicode的示例:
https://tool.chinaz.com/tools/unicode.aspx

NLP领域中的token和tokenization到底指的是什么?-18.jpg

refs:

https://github.com/google/sentencepiece
sentencepiece原理与实践
https://www.zhihu.com/tardis/zm/art/159200073?source_id=1003
6.主流subword算法的对比

wordpiece和BPE的对比

wordpiece和BPE的对比:都是走的合并的思路,将语料拆分成最小单元(英文中26个字母加上各种符号,这些作为初始词表)然后进行合并,词表从小到大;核心区别就在于wordpiece是按token间的互信息来进行合并而BPE是按照token一同出现的频率来合并的。
wordpiece和ULM的对比:
wordpiece和ULM的对比:都使用语言模型来挑选子词;区别在于前者词表由小到大,而后者词表由大到小,先初始化一个大词表,根据评估准则不断丢弃词表,直到满足限定条件。ULM算法考虑了句子的不同分词可能,因而能够输出带概率的多个分词结果。


三种subword分词算法的关系


NLP领域中的token和tokenization到底指的是什么?-19.jpg

7.tokenizers库

优先级靠后
2.分词器

1.BERT的分词器

BERT的分词器由两个部分组成:

  • BasicTokenizer:

    • 转成 unicode:Python3,输入为str时,可以省略这一步
    • _clean_text:去除各种奇怪字符
    • _tokenize_chinese_chars:中文按字拆开
    • whitespace_tokenize:空格分词
    • _run_strip_accents:去掉变音符号
    • _run_split_on_punc:标点分词
    • 再次空格分词:whitespace_tokenize(" ".join(split_tokens)),先用空格join再按空白分词,可以去掉连续空格



  • WordpieceTokenizer:

    • 贪心最大匹配:用双指针实现;


核心代码:

NLP领域中的token和tokenization到底指的是什么?-20.jpg

tokenize(self, text):

NLP领域中的token和tokenization到底指的是什么?-21.jpg

2.T5的分词器

3.GPT的分词器

4.LLaMA的分词器

5.GLM的分词器

总结

下面对主流模型使用的分词器进行总结(待完善):

NLP领域中的token和tokenization到底指的是什么?-22.jpg

参考资料

深入理解NLP Subword算法:BPE、WordPiece、ULM - Luke的文章 - 知乎
https://zhuanlan.zhihu.com/p/86965595
NLP三大Subword模型详解:BPE、WordPiece、ULM - 阿北的文章 - 知乎
https://zhuanlan.zhihu.com/p/191648421
NLP中的subword算法及实现 - 微胖界李现的文章 - 知乎
https://zhuanlan.zhihu.com/p/112444056
NLP BERT GPT等模型中 tokenizer 类别说明详解
https://cloud.tencent.com/developer/article/1865689
BERT 客制化分词器和 WWM 的实现 - 满甲的文章 - 知乎
https://zhuanlan.zhihu.com/p/268515387
bert第三篇:tokenizer
https://blog.csdn.net/iterate7/article/details/108959082
BERT 是如何分词的
https://blog.csdn.net/u010099080/article/details/102587954
同:BERT 是如何分词的 - Alan Lee的文章 - 知乎
https://zhuanlan.zhihu.com/p/132361501
Bert系列伴生的新分词器
https://dxzmpk.github.io/2020/04/29/Bert%E7%B3%BB%E5%88%97%E4%BC%B4%E7%94%9F%E7%9A%84%E6%96%B0%E5%88%86%E8%AF%8D%E5%99%A8/
Tokenizers: How machines read
https://blog.floydhub.com/tokenization-nlp/
【HugBert11】聚沙成塔:关于tokenization(词元化)的解疑释惑 - 套牌神仙的文章 - 知乎
https://zhuanlan.zhihu.com/p/371300063
japanese and korean voice search
https://static.googleusercontent.com/media/research.google.com/zh-CN//pubs/archive/37842.pdf
[1508.07909] Neural Machine Translation of Rare Words with Subword Units
3-3 Transformers Tokenizer API 的使用
https://www.zhihu.com/tardis/zm/art/390821442?source_id=1003
关于transformers库中不同模型的Tokenizer - 莫冉的文章 - 知乎
https://zhuanlan.zhihu.com/p/121787628
NLP领域中的token和tokenization到底指的是什么? - 知乎
https://www.zhihu.com/question/64984731
NLP中的Tokenization - 薛定谔没养猫的文章 - 知乎
https://zhuanlan.zhihu.com/p/444774532
大模型中的分词器tokenizer:BPE、WordPiece、Unigram LM、SentencePiece - 眼睛里进砖头了的文章 - 知乎
https://zhuanlan.zhihu.com/p/620508648
浅谈Byte-Level BPE - CaesarEX的文章 - 知乎
https://zhuanlan.zhihu.com/p/146114164
理解NLP最重要的编码方式 — Byte Pair Encoding (BPE),这一篇就够了 - 硅谷谷主的文章 - 知乎
https://zhuanlan.zhihu.com/p/424631681
Neural Machine Translation with Byte-Level Subwords
https://arxiv.org/abs/1909.03341
tokenizers小结 - 马东什么的文章 - 知乎
https://zhuanlan.zhihu.com/p/360290118
互信息
https://zh.wikipedia.org/wiki/%E4%BA%92%E4%BF%A1%E6%81%AF
Python  unicodedata.normalize 将Unicode文本标准化
https://blog.csdn.net/weixin_43866211/article/details/98384017
Weaknesses of WordPiece Tokenization
https://medium.com/@rickbattle/weaknesses-of-wordpiece-tokenization-eb20e37fec99
Subword
https://paddlepedia.readthedocs.io/en/latest/tutorials/pretrain_model/subword.html
sentencepiece原理与实践
https://www.zhihu.com/tardis/zm/art/159200073?source_id=1003
抱抱脸:
https://huggingface.co/docs/transformers/tokenizer_summary
https://huggingface.co/learn/nlp-course/zh-CN/chapter2/4?fw=tf
https://huggingface.co/learn/nlp-course/chapter6/7?fw=pt
https://huggingface.co/learn/nlp-course/chapter6/5?fw=pt

苏打 LV

发表于 2025-4-9 14:20:06

tokenization,也叫word segmentation,是一种操作,它按照特定需求,把文本切分成一个字符串序列(其元素一般称为token,或者叫词语)。一般来说,我们要求序列的元素有一定的意义,比如“text mining is time-consuming”需要处理成"text mining/ is/ time-consuming",其中"text mining"表示"文本挖掘"。
如果我们把语料中所有的token做一个去重,就得到了一个词汇表,其中的每一个词语被称为type。
英文信息处理中,tokenization需要把"I'm Li"这样的句子转换为"I am Li",即将一些词语、短语的写法规范化。中文由于文字本身没有形态变化、不需要太多的规范化操作,大家关注的主要的是切分操作,即分词。

Zed888 LV

发表于 2025-4-9 14:31:52

token(符号):包括单词和标点
tokenization(分词):我是中国人->['我', '是', '中国人']

您需要登录后才可以回帖 登录 | 立即注册