首页 / 第三部分 · 大语言模型是怎么长出来的 / 第 09 篇

第 09 篇 · 第三部分 · 大语言模型是怎么长出来的

Token 与词表:模型眼里的文字根本不是文字

在模型眼里,你的句子是一串整数。如何切分这串整数,是一门有讲究的技术。这篇讲清楚 token 到底是什么、BPE 如何构造词表、词表大小是一笔怎样的账,以及一个让许多中文用户格外在意的问题——为什么中文更费 token。

一、模型只看数字

神经网络算的是浮点数,它无法直接处理字符串。所以任何文本进入模型后都要经过三道工序:先切成一串 token,再把每个 token 换成整数 ID,再拿 ID 去查表变成向量。到这一步,模型才开始工作。

token 是模型能看见的最小单位。它既不是一个字,也不是一个词,而是介于两者之间的一种单位:可能是整个词,可能是词根,可能是一个汉字,也可能只是一段字节。屏幕上看到的字、模型看到的整数 ID、它内部处理的向量,是三个不同层面的东西,不能互相替代。

理解 token 之后,"模型为什么这么贵""模型为什么连这个都做不好"这类疑问,有一半答案自然就有了。

"最小单位"这个说法值得多推敲一下:模型既不认识"字",也不认识"词",它只认 ID。我们感知到的语义、语气、甚至错别字,都得先被切成它认得的整数,它才能处理。切得不好,后面再强大的模型也无法弥补。

二、三种切法:词级、字符级、子词级

把文本切成 token,历史上有三条路线,各有各的适用范围与代价。

  • 词级:按空格和标点切词,词表几万到几十万。问题在于未见过的词无处安置,变形词、专有名词、拼错的词都会撞上"词表外"(OOV)这堵墙;词表规模本身也容易失控。
  • 字符级:一个字符一个 token,词表小到几百,永远不会有 OOV。代价是序列变得很长,模型得从大量字母里自己重新学出词来,效率很低。
  • 子词级:折中方案。常见词整块保留,罕见词拆成更小的片段。词表规模可控,又不怕没见过的词——大不了拆到字符级别。BPE 就属于这一类,也是如今唯一的主流。

子词级的思路简单说就是:常用的整块存,不常用的拆着存。它同时避开了词级和字符级各自的短板。

回顾这段演进过程:早期模型多半走词级,受过 OOV 的困扰;也试过纯字符级,又被超长序列拖累。子词级能成为主流,是因为它把两边的短板都弥补了大半——既不缺词,也不至于把序列拉得过长。

三、BPE:从字符合并出词根的贪心算法

BPE 全称字节对编码,原本是数据压缩里的算法,Sennrich 等 2016 年把它搬进了机器翻译,从此成了分词的标配。

它的训练过程是一个贪心的迭代过程。从单个字符起步,统计相邻两个符号一起出现的频率,找出最高频的那一对,把它们合并成一个新符号。然后重复:再统计、再合并,反反复复几万次,直到词表长到设定的大小。

效果是:出现得频繁的组合会一路被合并成整词,比如 the、ing;少见的东西就留在碎片级别,实在不行退回到单字符。用生活里的话讲,高频的东西打包存放,像压缩饼干,省地方;低频的散着放,灵活。

编码新文本时,模型把学到的合并规则挨个套上去,尽量用长片段,切不动了就用短的兜底。Karpathy 的 minbpe 用两百来行代码把这一整套讲透了,想动手的话值得照着写一遍。

合并了多少次,词表就大多少。现代字节级 BPE 从 256 个基础字节起步,一路合并到几万、十几万,多出来的每一格,都是一条被数据证明够高频的组合。所谓"词表大小"这个超参数,本质上就是"允许合并多少次"。

四、为什么中文更费 token

这是许多人调用 API 之后才发现的问题:同一段意思,用中文往往比英文消耗更多 token。

根源在于词表是拿什么语料练出来的。早期 BPE 词表是在以英文为主的语料上学的,合并表里绝大多数条目都是英文片段,中文里的常见组合几乎没有机会被合并。一个汉字信息密度高,在这种词表下,常常被拆成 2 到 3 个 token 才表示得出来。碰上生僻字或者分词器走字节回退,拆得更碎。

有粗略的测量给过数量级:中文平均大概一个字要 1 到 2 个 token,而英文平均一个词大约 1.3 个 token。也就是说,同样一段内容,中文的 token 数通常是英文的 1.5 到 2 倍,有时更多。

落到实际就是两件事:同样大的上下文窗口,中文能装的内容更少;同样的钱,中文更贵。这也正是很多国产大模型专门为中文重建词表的原因——不是审美问题,是实实在在的成本。

还有个连带影响容易被忽略:中文的 token 边界常常处在字中间或者词中间,模型看到的不是完整的字。碰上成语、专有名词这类整体性强的表达,它就更容易"只看到半截",这也是某些中文任务上它的表现不如英文的原因之一。

五、词表大小是一笔权衡(3 万还是 15 万)

词表要多大?这事没有标准答案,只有取舍。

词表大(比如不少中文、多语模型用到 10 万到 15 万):序列变得短,注意力算起来省——序列长度是平方级的开销,短一个数量级能省很多;罕见词也能整块表示,不必拆得过碎。坏处是嵌入矩阵和输出层跟着变大,而且稀有 token 出现次数太少,学得不扎实,容易变成"占住位置却没学好的条目"。

词表小(早期英文模型,GPT-2 大约 5 万,有的小模型压到 3 万):参数省,每个 token 见的次数多、学得牢。代价是序列变长,慢,还容易把词切得太碎、丢失一些信息。

怎么选,看两件事:主要面对哪种语言、主打什么任务。英语为主就小一点,中文、多语为主就得放大,成本与效果的平衡点在哪里,需要实测。

换个说法,词表大小是在两根杠杆之间找支点:一根是序列长度,一根是参数规模。往大拨,序列短了、注意力省了,但嵌入涨了;往小拨,参数省了、序列长了。支点应当定在哪里,取决于你的语料构成和预算。

六、嵌入矩阵与输出层:词表多大,参数就多大

词表大小不是个抽象的配置数字,它直接换算成参数量。词表记作 V,隐藏维度记作 d,那么嵌入矩阵是 V × d,输出层(把隐藏向量映射回词表上打分)通常是 d × V。

拿一组常见数字估算一下:V 取 5 万、d 取 4096,光嵌入矩阵一项就是 50000 × 4096,大约 2.05 亿参数;输出层再算一份,两头加起来超过 4 亿。如果让嵌入和输出共享同一批权重(叫权重绑定,权重 tying),能省掉其中一份,这是很多模型的标准操作。

反过来核算这笔开销也成立:想扩词表,就得掂量参数量、显存和训练成本。所以词表不是一个"随意调大一些就更好"的参数。

权重绑定这件小事也有讲究:它省参数,对中小模型尤其划算;模型特别大的时候,两者分开反而有时训练更稳。绑不绑是训练策略,不是非黑即白的对错。

七、容易出错的细节:空格、标点、emoji、数字

用得多了会发现分词器的行为相当古怪,问题往往藏在细节里。

  • 空格:很多分词器把"词 + 前面那个空格"当成一个 token。GPT 系里 " token" 和 "token" 是两个不同的 token,句子开头少打一个空格,切法就变了。
  • 标点:某些标点组合会被并成一块,写代码时括号和分号经常打包在一起处理,所以模型对"代码风格"的敏感度跟普通文本不一样。
  • emoji 和生僻字符:常被拆成好几个字节级 token,一个表情能占用两到四个,看着小,成本不小。
  • 数字:BPE 对数字不太友好。同一个数可能被切成 "12" 和 "34",也可能切成 "1" "234",切法随上下文变化。模型算术弱,有一部分原因要归结于此——它看到的不是"一千二百三十四"这个连续的数,而是一系列碎片。

这些细节的共同点是:它们都不在"语义"层面,而在"怎么切"这一层。提示词里多一个空格、多一个换行,模型看到的整数序列就可能变样,输出也会随之变化。同一句话换个写法、结果就不同,很多时候根源就在这里。

八、token 计费和上下文长度的现实影响

API 是按 token 计费的,输入输出都算。中文用户常觉得"怎么同样的内容我更贵",根源就在于分词:中文的单位内容占的 token 多,账单自然高。

上下文窗口也用 token 衡量。标称 128K,指的是一共 128K 个 token。换成中文汉字,能装的内容会比英文少很多——这一条在写长文档、做长对话时最容易被忽略。

省 token 有几个实用的办法:不要把对话历史整段重复写入;长文档优先给结构清晰、冗余内容少的版本;需要模型读大段材料时,先自行摘要一遍再输入。这些都是容易做到的。

还有个现实细节:上下文长度经常按 token 数标称,产品文档却不一定帮你换算成汉字。做长文档任务之前,最好自己拿个 tokenizer 估一下,免得材料刚装入一半,窗口就满了。

本质上,理解 token 就是理解了模型的第一道门。它决定了你的文字怎么变成数字,也决定了钱怎么花、窗口怎么算、模型在哪些地方天然不擅长。下一篇接着讲,拿到这些概率之后,模型怎么把它变成最终输出的字——采样策略。

参考资料

  1. Rico Sennrich, Barry Haddow, Alexandra Birch, Neural Machine Translation of Rare Words with Subword Units, ACL 2016. arXiv:1508.07909(BPE 引入 NLP 的原始论文)
  2. Taku Kudo, Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates, ACL 2018. arXiv:1804.10959(子词切分的另一条路线)
  3. Andrej Karpathy, minbpe. github.com/karpathy/minbpe(两百来行手写 BPE,适合动手)
  4. OpenAI, tiktoken. github.com/openai/tiktoken(GPT 系分词器的官方实现,可用来实测 token 数)
  5. Hugging Face, Tokenizers 文档. huggingface.co/docs/tokenizers(各类分词算法与实现参考)