【空格的呼吸】基于BPE的Tokenizer 分词原理介绍

举个例子,本人在大模型分词时有遇到下面的现象,感到疑惑。

对同一个符号,有时候,空格的存在与否,会导致分词结果不一致。

1
2
3
4
5
tokenizer = AutoTokenizer.from_pretrained("qwen2.5-7b-instruct")
text = "◎"
print(tokenizer.encode(text))
text = " ◎"
print(tokenizer.encode(text))

结果输出

1
2
[âĹİ]
['ĠâĹ', ']

根据Qwen2
/tokenization_note_zh.md
,这里也提到了类似的现象:
一段文本在不同的上下文下可能会有不同的tokenize结果
alt text

  • 疑惑1:这些奇怪的字符是什么?
  • 疑惑2:为什么空格会影响分词结果?

接下来,就让我们解析tokenizer做了些什么。

基础tokenizer分词流程

原理描述

  1. 初步分割

    • 使用正则表达式对输入文本进行分割
    • 分割依据:标点符号、字母数字、缩写词、换行符、空白符等
    • 特点:所有分割符号(包括标点、空白符)都会被保留
  2. UTF-8编码转换

    • 将分割后的片段转换为UTF-8编码
    • ASCII字符(如英文字母)保持单字节编码
    • 中文字符使用2-4字节编码

示例:

1
2
3
4
text = "hello, 猪头"
byte_sequence = text.encode("utf-8")
print(byte_sequence)
# 输出:b'hello, \xe7\x8c\xaa\xe5\xa4\xb4'

tokenizer内部的代码逻辑

  1. Unicode字符转换
    • 将UTF-8字节序列映射为Unicode字符
    • 每个字节对应特定的Unicode字符(如\xe7对应ç)
1
2
3
4
# 字节序列转Unicode字符
unicode_sequence = [chr(c) for c in byte_sequence]
print("".join(unicode_sequence))
# 输出:hello, çªå¤´

上述代码在tokenizer中,是基于字节序列转Unicode字符,然后进行BPE算法处理。
4. BPE算法处理

  • 训练阶段

    • 统计字符对出现频次
    • 按频次高低逐步合并字符
    • 将合并规则保存至merges.txt
  • 推理阶段

    • 生成输入文本的unicode字符串的bigram组合
    • 按merges.txt中的优先级顺序进行合并
    • 循环处理直至无可合并字符对

空格影响分析:以”◎”符号为例

带空格情况(” ◎”)

  1. Unicode序列:”ĠâĹİ”
  2. 分词过程:
    • 初始bigram:Ġâ, âĹ, Ĺİ
    • 按优先级合并:ĠâĠâĹ + İ
  3. 最终结果:['ĠâĹ', 'İ']

无空格情况(”◎”)

  1. Unicode序列:”âĹİ”
  2. 分词过程:
    • 直接合并:âĹ + İâĹİ
  3. 最终结果:[âĹİ]

分词好之后,会根据vocab.json字典,将这些token子词转换成id

结论

空格的存在会显著影响分词结果,即使是同一个符号:

  • 有空格:分词为两个token
  • 无空格:保持为单个token

这种差异提醒我们在文本预处理时需要特别注意空格的处理,以确保分词的一致性和准确性。

<特别提示>
如果想对词表进行扩展,在qwen系列种不适用,因为它是基于BPE算法进行训练的。所以,就算我们往词表里加入新词(例如:”piggrain”,我家狗的名字)。但是由于merges.txt中如果没有任意子词的组合能组合成piggrain,所以,新词也仍然会被切割,并不会保留这个完整的词。


【空格的呼吸】基于BPE的Tokenizer 分词原理介绍
https://abigail61.github.io/2025/01/09/空格的呼吸/
作者
Yajing Luo
发布于
2025年1月9日
许可协议