小白也能看懂的Tokenizer:大模型是怎么“读“文字的
系列文章:AI大模型知识体系 | 第三篇
引言:大模型其实是个"文盲"
你可能觉得ChatGPT、文心一言这些大模型精通各国语言,但事实可能出乎你的意料——它们根本不认识汉字,也不认识英文字母。
这听起来像是在开玩笑,但确实如此。大模型的神经网络只能处理数字,不能直接理解"text"或"你好"这样的文字。在文字和模型之间,存在一个关键的"翻译官",它的名字叫做Tokenizer(分词器)。
Tokenizer的工作就是把人类阅读的文字,切分成一个个叫做token的小单元,再把每个token映射成一个整数ID。比如"你好"可能被变成 [1265, 892] 这样一串数字,模型才能真正处理。
那token到底是什么?它是怎么切分的?为什么有时候一个汉字算一个token,有时候又不算?这篇文章就带你从零搞懂Tokenizer。
为什么需要Tokenizer?
原因很简单:计算机只懂数字。
神经网络的所有运算——矩阵乘法、注意力机制、Softmax——全部基于数值。你不可能把字符串 "hello" 直接扔进一个矩阵里做运算。所以我们需要一种方式,把自然语言中的文字系统性地转换成数字序列。
最粗暴的方式是给每个词编一个号:"苹果"=1, "香蕉"=2, "橘子"=3...。但这立刻会遇到问题:
-
英语有几十万个单词,中文词汇更是海量,编号表得有多大?
-
遇到没见过的词(比如"yyds"、"绝绝子")怎么办?直接报错?
-
"playing"和"played"明明有关系,却被编成完全不同的号,模型怎么学到它们之间的联系?
所以我们需要一种更聪明的切分方式——既不能让词表太大,也不能切得太碎。这就是Tokenizer要解决的核心问题。
最简单的方案:按字切分 vs 按词切分
在介绍现代Tokenizer之前,我们先看两种最直觉的方案。
方案一:按字切分(Character-level)
把每个字符当作一个token:
"我喜欢编程" → ["我", "喜", "欢", "编", "程"]
"hello" → ["h", "e", "l", "l", "o"]
-
优点:词表极小(中文几千常用字,英文26个字母),不会遇到未知字符
-
缺点:序列太长,"我喜欢编程"就5个token了,一篇长文得切成几万个token,模型根本处理不过来,而且单个字符携带的语义信息太少
方案二:按词切分(Word-level)
把每个完整的词当作一个token:
"我喜欢编程" → ["我", "喜欢", "编程"]
"I love coding" → ["I", "love", "coding"]
-
优点:每个token语义完整,序列较短
-
缺点:词表巨大,而且无法处理未登录词。比如"内卷"、"元宇宙"这些新词,词表里没有就只能标记为
[UNK](未知),信息直接丢失
有没有折中方案?既能控制词表大小,又能保留足够语义?这就是子词切分(Subword) 的思路——现代Tokenizer的核心思想。
BPE算法:从字符"长"出词汇
BPE(Byte Pair Encoding,字节对编码) 是目前最主流的子词算法之一,GPT系列模型都在用它。它的思想非常巧妙:从单个字符开始,不断把最常一起出现的相邻字符合并成更大的单元,直到词表达到预设大小。
来看一个具体的小例子。假设我们的训练语料就是这几句话:
"ab" 出现了5次
"ac" 出现了3次
"bc" 出现了2次
第0步:初始化——每个字符就是一个token:
词表:{a, b, c}
第1轮:统计相邻字符对出现频次,找最高频的合并
(a, b) → 5次 ← 最高频,合并!
(a, c) → 3次
(b, c) → 2次
合并 (a,b) 为 ab,词表变为:{a, b, c, ab}
第2轮:继续统计
现在语料中 ab 已经是一个整体了,重新统计相邻对:
(ab, c) → 3次 ← 最高频,合并!
(b, c) → 2次
合并 (ab,c) 为 abc,词表变为:{a, b, c, ab, abc}
第3轮:如果还有高频对就继续合并,直到词表达到我们设定的大小为止。
就这么简单!经过大量语料训练后,常见的词(如"the""hello")会作为整体token保留,而罕见的词会被拆成多个子词。比如"unhappiness"可能被拆成 ["un", "happiness"],而"xyzabc"这种生造词可能被拆成 ["x", "yz", "abc"]。
核心要点:BPE的本质就是在"字符级"和"词级"之间找到一个平衡点——常见组合合并成大token,罕见组合保持小token。
WordPiece和SentencePiece:BPE的"兄弟姐妹"
除了BPE,你可能还听说过WordPiece和SentencePiece,它们有什么区别?
WordPiece(BERT用的就是它)和BPE的整体思路几乎一样,都是自底向上不断合并。唯一的区别在于选择合并对象的策略不同:
-
BPE选的是出现频次最高的相邻对
-
WordPiece选的是合并后让语言模型概率提升最大的相邻对
另外,WordPiece有个显著特征:用##前缀标记非词首的子词。比如"playing" 会被切成 ["play", "##ing"],一眼就能看出##ing是接在前一个token后面的。
SentencePiece(LLaMA、T5等模型使用)则走得更远——它不依赖空格做预分词,直接把原始文本当作字符流来处理。空格被当作一个普通字符(用▁这个特殊符号表示)。这让它天然适合处理中文、日文等没有空格分隔的语言。
简单总结:
|
特性 |
BPE |
WordPiece |
SentencePiece |
|---|---|---|---|
|
代表模型 |
GPT系列 |
BERT |
LLaMA、T5 |
|
合并策略 |
频次最高 |
似然增益最大 |
支持BPE或Unigram |
|
子词标记 |
无特殊前缀 |
|
|
|
中文友好度 |
一般 |
一般 |
较好 |
你不需要记住这些细节,只需要知道:它们都是"子词切分"的不同实现方式,核心目标相同——在词表大小和表达能力之间取得平衡。
实际体验:动手玩转Tokenizer
说了这么多理论,不如直接跑几行代码看看效果。我们用HuggingFace的transformers库来体验不同Tokenizer的实际表现。
代码1:同一个英文句子,不同Tokenizer
from transformers import AutoTokenizer
text = "I love natural language processing."
# GPT-2 使用的是 BPE
gpt2_tok = AutoTokenizer.from_pretrained("gpt2")
# BERT 使用的是 WordPiece
bert_tok = AutoTokenizer.from_pretrained("bert-base-uncased")
gpt2_result = gpt2_tok.tokenize(text)
bert_result = bert_tok.tokenize(text)
print(f"GPT-2 tokens ({len(gpt2_result)}个): {gpt2_result}")
print(f"BERT tokens ({len(bert_result)}个): {bert_result}")
输出大致如下:
GPT-2 tokens (6个): ['I', 'Ġlove', 'Ġnatural', 'Ġlanguage', 'Ġprocessing', '.']
BERT tokens (6个): ['i', 'love', 'natural', 'language', 'processing', '.']
注意GPT-2的Ġ代表词前的空格(BPE的标记方式),BERT则直接转小写。这个简单句子两者切分结果差不多,但遇到复杂词汇差异就大了。
代码2:中文句子的Token化
from transformers import AutoTokenizer
zh_text = "大语言模型的核心是Transformer架构"
# 用GPT-2的Tokenizer处理中文
gpt2_tok = AutoTokenizer.from_pretrained("gpt2")
zh_gpt2 = gpt2_tok.tokenize(zh_text)
# 用Qwen(通义千问)的Tokenizer处理中文
qwen_tok = AutoTokenizer.from_pretrained("Qwen/Qwen2-7B", trust_remote_code=True)
zh_qwen = qwen_tok.tokenize(zh_text)
print(f"GPT-2切分中文 ({len(zh_gpt2)}个token): {zh_gpt2}")
print(f"Qwen切分中文 ({len(zh_qwen)}个token): {zh_qwen}")
你会发现一个有趣的现象:同一个中文句子,GPT-2切出来的token数量远多于Qwen。这是因为GPT-2的词表主要是基于英文训练的,大量中文字和词不在词表中,只能被拆成单个字符甚至UTF-8字节;而Qwen专门为中文优化了词表,"大语言模型"、"Transformer"、"架构"这些都可能作为整体token存在。
这就是为什么中文用户在使用国外大模型API时,同样意思的prompt往往消耗更多token——你的文字被"切得更碎"了。
Tokenizer对使用大模型的实际影响
理解了Tokenizer的原理,我们再来看几个实际场景中的问题。
为什么API按token收费?
因为大模型的计算量直接取决于输入的token数量,而不是字符数量。Transformer的自注意力机制复杂度与序列长度的平方成正比,所以token数才是衡量计算成本的真正单位。按token收费比按字符或按字数收费更合理,因为它直接反映了模型实际消耗的计算资源。
为什么同一个prompt在不同模型下token数不同?
因为每个模型使用的Tokenizer不同——词表大小不同、合并规则不同、对中文的支持程度也不同。举个极端例子:一句"人工智能改变世界",在Qwen的词表里可能只有4-5个token("人工"、"智能"、"改变"、"世界"),但在GPT-2里可能被拆成十几个甚至更多token,因为每个中文字可能都要单独编码。
怎么估算token数量?
几个经验法则:
-
英文:1个token大约等于4个字符,或约0.75个单词。1000个英文单词大约是1300个token
-
中文:对于中文优化的模型,1个汉字大约是1-2个token;对于英文为主的模型,1个汉字可能是2-5个token甚至更多
-
最靠谱的方法:直接调用对应模型的Tokenizer来数。HuggingFace的
tokenizer.encode(text)返回的列表长度就是精确的token数
# 一行代码快速计算token数
token_count = len(tokenizer.encode("你的prompt文本"))
print(f"本次输入消耗 {token_count} 个token")
在调用API之前先算一下token数,既能预估费用,也能避免超出模型的上下文长度限制。
总结
Tokenizer是大模型的"文字入口",它决定了模型看到的不是一个一个的字或词,而是一串数字ID。回顾今天的要点:
-
大模型不直接读文字,必须通过Tokenizer将文本转换为token ID序列
-
子词切分是现代Tokenizer的主流方案,在词表大小和表达能力之间取得平衡
-
BPE是最经典的子词算法,核心就是不断合并高频相邻字符对
-
WordPiece和SentencePiece是BPE的变体,核心思路类似但策略和适用范围有差异
-
不同Tokenizer对中文的支持差异很大,这直接影响你使用API的成本和效果
-
实际使用大模型时,要关注token数量而非字符数量,它是计费和性能的真正瓶颈
Tokenizer虽然只是大模型流程中的"第一步",但它的设计选择会影响后续所有环节——从模型能学到什么语义、推理要花多少钱、到中文用户是不是"吃亏"。理解了Tokenizer,你对大模型的理解就不再停留在"调用API"的表面,而是真正看到了它"读"文字的方式。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注三连!你的支持是我持续创作的最大动力。有任何问题欢迎评论区交流~
更多推荐


所有评论(0)