0x00 理解 tokenizer.json 中没有中文

1330 字
7 分钟
0x00 理解 tokenizer.json 中没有中文

#1 现象#

在 Qwen3 的 tokenizer.json 中,搜索不到中文,但是模型是支持中文的。

#2 现象解读#

vocab.json 一般都是不可读的,因为词表都用的是 Unicode 编码,分词单元类似 è¿Ļæĺ¯ä¸Ģ个 这样看起来乱码的形式。

测试以下脚本:

from transformers import AutoTokenizer
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained("./model/minimind_tokenizer")
# 测试文本
text = "这是一个测试,看看分词器是否能正确处理中文。"
# 编码:将文本转换为 token IDs
encoded_ids = tokenizer.encode(text)
print("编码结果 (Token IDs):", encoded_ids)
# 解码:将 token IDs 转换回文本
decoded_text = tokenizer.decode(encoded_ids)
print("解码结果 (文本):", decoded_text)
decoded_text = tokenizer.decode([434])
print("解码结果 (文本):", decoded_text)
decoded_text = tokenizer.decode([1589])
print("解码结果 (文本):", decoded_text)
# 如果需要查看具体的 tokens(子词单元)
tokens = tokenizer.tokenize(text)
print("分词结果 (Tokens):", tokens)

结果是:

编码结果 (Token IDs): [434, 1589, 3560, 270, 1380, 1380, 554, 1582, 1179, 2010, 424, 2826, 1958, 413, 677, 286]
解码结果 (文本): 这是一个测试,看看分词器是否能正确处理中文。
解码结果 (文本): 这
解码结果 (文本): 是一个
分词结果 (Tokens): ['è¿Ļ', 'æĺ¯ä¸Ģ个', 'æµĭè¯ķ', 'ï¼Į', 'çľĭ', 'çľĭ', 'åĪĨ', 'è¯į', 'åύ', 'æĺ¯åIJ¦', 'èĥ½', 'æŃ£ç¡®', 'å¤ĦçIJĨ', 'ä¸Ń', 'æĸĩ', 'ãĢĤ']

意味着:

'è¿Ļ' -> '这'
'æĺ¯ä¸Ģ个' -> '是一个'

#3 根本原因#

词汇 你好 在 Qwen 的词汇表中使用一个 token 表示。

  1. 你好 使用 UTF-8 编码,得到 b'\xe4\xbd\xa0\xe5\xa5\xbd'
  2. 根据【大模型】tokenizer 中编码过程中提到的映射集合,进行映射:\xe4 -> ä\xbd -> ½\xa0 -> ł\xe5 -> å\xa5 -> ¥\xbd -> ½
  3. 组合得到 ä½łå¥½

和原始的输入并不一样,而是变成了乱码。主要原因在于,千问系列模型实际上对原始的字节流进行了变换,而这个变换来源于 GPT-2 的 tokenizer。

def bytes_to_unicode():
"""
生成字节到 Unicode 字符的正向映射表
返回字典:{byte_value: unicode_char}
"""
# 原始保留的字节范围
bs = (
list(range(ord("!"), ord("~") + 1)) + # ASCII 可打印字符(33-126)
list(range(ord("¡"), ord("¬") + 1)) + # 西班牙语特殊字符(161-172)
list(range(ord("®"), ord("ÿ") + 1)) # 其他扩展字符(174-255)
)
cs = bs.copy() # 初始字符列表
n = 0
# 遍历所有可能的字节(0-255)
for b in range(2**8):
if b not in bs:
bs.append(b)
cs.append(2**8 + n) # 超出原始范围的字节映射到更高 Unicode 码位
n += 1
# 将码位转换为 Unicode 字符
cs = [chr(code) for code in cs]
return dict(zip(bs, cs))
def get_reverse_mapping(forward_map):
"""
根据正向映射生成反向映射
返回字典:{unicode_char: byte_value}
"""
return {v: k for k, v in forward_map.items()}
# 生成映射表
forward_map = bytes_to_unicode()
reverse_map = get_reverse_mapping(forward_map)

查看特定字节的映射:

# 查看字节值 136 的映射
byte_val = 136
print(f"字节 {byte_val} 对应的 Unicode 字符是:{forward_map[byte_val]}")
# 输出:字节 136 对应的 Unicode 字符是:Ī

字节到 Unicode 的转换:

def bytes_to_unicode_str(byte_sequence):
return ''.join([forward_map[b] for b in byte_sequence])
# 将"你好"的 UTF-8 字节转换为 Unicode 字符串
text = "你好"
byte_sequence = text.encode('utf-8')
unicode_str = bytes_to_unicode_str(byte_sequence)
print(f"原始文本:{text}")
print(f"转换后的 Unicode 字符串:{unicode_str}")
# 输出:
# 原始文本:你好
# 转换后的 Unicode 字符串:ä½łå¥½

Unicode 到原始字节的反向转换:

def unicode_str_to_bytes(unicode_str):
return bytes([reverse_map[c] for c in unicode_str])
# 将转换后的 Unicode 字符串还原为原始文本
recovered_bytes = unicode_str_to_bytes(unicode_str)
recovered_text = recovered_bytes.decode('utf-8')
print(f"还原后的文本:{recovered_text}")
# 输出:还原后的文本:你好

双向流程展示:

from transformers import AutoTokenizer
def bytes_to_unicode_str(byte_sequence):
return ''.join([forward_map[b] for b in byte_sequence])
def unicode_str_to_bytes(unicode_str):
return bytes([reverse_map[c] for c in unicode_str])
# (缩减后的) 映射生成代码
def b():
bs = list(range(33, 127)) + list(range(161, 173)) + list(range(174, 256))
cs = bs.copy()
n = 0
for b_val in range(256):
if b_val not in bs:
bs.append(b_val)
cs.append(256 + n)
n += 1
return dict(zip(bs, [chr(c) for c in cs]))
def g(f):
return {v: k for k, v in f.items()}
forward_map = b()
reverse_map = g(forward_map)
# 加载 Qwen tokenizer
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B")
# 示例文本
text = "你好世界"
# 步骤1:原始文本 -> UTF-8 字节
bytes_data = text.encode('utf-8')
# 步骤2:字节 -> Unicode 字符串
unicode_str = bytes_to_unicode_str(bytes_data)
# 步骤3:Unicode 字符串 -> tokenizer 编码
input_ids = tokenizer(unicode_str, return_tensors="pt")["input_ids"]
# 步骤4:解码过程
decoded_unicode = tokenizer.decode(input_ids[0])
recovered_bytes = unicode_str_to_bytes(decoded_unicode)
recovered_text = recovered_bytes.decode('utf-8')
print(f"原始文本:{text}")
print(f"乱码表示:{unicode_str}")
print(f"解码后的文本:{recovered_text}")
# 输出:
# 原始文本:你好世界
# 乱码表示:ä½łå¥½ä¸ĸçķĮ
# 解码后的文本:你好世界

进一步的用以下脚本,可以测试 vocab-token 数量占比:

from transformers import AutoTokenizer
import re
tokenizer = AutoTokenizer.from_pretrained("./model/minimind_tokenizer")
vocab = tokenizer.get_vocab()
total_tokens = len(vocab)
chinese_tokens = 0
english_tokens = 0
non_chinese_or_english_tokens = 0
def is_chinese(text):
return all('\u4e00' <= char <= '\u9fff' for char in text)
def is_english(text):
return all(re.match(r"[a-zA-Z]", char) for char in text)
for token_id in range(total_tokens):
token = tokenizer.decode([token_id])
if is_chinese(token):
chinese_tokens += 1
elif is_english(token):
english_tokens += 1
else:
non_chinese_or_english_tokens += 1
chinese_ratio = chinese_tokens / total_tokens * 100
english_ratio = english_tokens / total_tokens * 100
other_ratio = non_chinese_or_english_tokens / total_tokens * 100
print(f"词表总大小: {total_tokens}")
print(f"中文 token 数量: {chinese_tokens} 占比: {chinese_ratio:.2f}%")
print(f"英文 token 数量: {english_tokens} 占比: {english_ratio:.2f}%")
print(f"其他 token 数量: {non_chinese_or_english_tokens} 占比: {other_ratio:.2f}%")

结果是:

词表总大小: 6400
中文 token 数量: 2448 占比: 38.25%
英文 token 数量: 1152 占比: 18.00%
其他 token 数量: 2800 占比: 43.75%

作为比较,Qwen2.5 的结果是:

词表总大小: 151665
中文 token 数量: 24875 占比: 16.40%
英文 token 数量: 27376 占比: 18.05%
其他 token 数量: 99414 占比: 65.55%

参考文献#

[1] 寒寒. Qwen(千问) 系列大模型的 tokenizer 为什么是乱码?[EB]//寒寒的博客. (2024-10-06). https://blog.alikia2x.com/posts/qwen-tokenizer/.

[2] 关于 tokenizer.json 词汇表中没有中文的问题 · Issue #111 · jingyaogong/minimind[EB]//GitHub. https://github.com/jingyaogong/minimind/issues/111.

[3] SUNX-T. 【大模型开发】将 vocab 解码[EB]//CSDN. (2025-03-26). https://blog.csdn.net/weixin_43571647/article/details/146529975.

[4] SUNX-T. 【大模型】tokenizer 中编码过程[EB]//CSDN. https://blog.csdn.net/weixin_43571647/article/details/146498153.

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
0x00 理解 tokenizer.json 中没有中文
https://firefly.cuteleaf.cn/posts/0x00/
作者
Firefly
发布于
2026-02-06
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Firefly
Hello, I'm Firefly.
公告
欢迎来到我的博客!
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
6
分类
3
标签
4
总字数
17,732
运行时长
0
最后活动
0 天前

文章目录