系列文章: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。回顾今天的要点:

  1. 大模型不直接读文字,必须通过Tokenizer将文本转换为token ID序列

  2. 子词切分是现代Tokenizer的主流方案,在词表大小和表达能力之间取得平衡

  3. BPE是最经典的子词算法,核心就是不断合并高频相邻字符对

  4. WordPiece和SentencePiece是BPE的变体,核心思路类似但策略和适用范围有差异

  5. 不同Tokenizer对中文的支持差异很大,这直接影响你使用API的成本和效果

  6. 实际使用大模型时,要关注token数量而非字符数量,它是计费和性能的真正瓶颈

Tokenizer虽然只是大模型流程中的"第一步",但它的设计选择会影响后续所有环节——从模型能学到什么语义、推理要花多少钱、到中文用户是不是"吃亏"。理解了Tokenizer,你对大模型的理解就不再停留在"调用API"的表面,而是真正看到了它"读"文字的方式。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注三连!你的支持是我持续创作的最大动力。有任何问题欢迎评论区交流~

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐