0x00 理解 tokenizer.json 中没有中文
#1 现象
在 Qwen3 的 tokenizer.json 中,搜索不到中文,但是模型是支持中文的。
#2 现象解读
vocab.json 一般都是不可读的,因为词表都用的是 Unicode 编码,分词单元类似 è¿Ļæĺ¯ä¸Ģ个 这样看起来乱码的形式。
测试以下脚本:
from transformers import AutoTokenizer
# 加载分词器tokenizer = AutoTokenizer.from_pretrained("./model/minimind_tokenizer")
# 测试文本text = "这是一个测试,看看分词器是否能正确处理中文。"
# 编码:将文本转换为 token IDsencoded_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 表示。
- 对
你好使用 UTF-8 编码,得到b'\xe4\xbd\xa0\xe5\xa5\xbd'。 - 根据【大模型】tokenizer 中编码过程中提到的映射集合,进行映射:
\xe4 -> ä;\xbd -> ½;\xa0 -> ł;\xe5 -> å;\xa5 -> ¥;\xbd -> ½。 - 组合得到
ä½łå¥½。
和原始的输入并不一样,而是变成了乱码。主要原因在于,千问系列模型实际上对原始的字节流进行了变换,而这个变换来源于 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 = 136print(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 tokenizertokenizer = 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 AutoTokenizerimport re
tokenizer = AutoTokenizer.from_pretrained("./model/minimind_tokenizer")vocab = tokenizer.get_vocab()total_tokens = len(vocab)
chinese_tokens = 0english_tokens = 0non_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 * 100english_ratio = english_tokens / total_tokens * 100other_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.
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!