首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >BPE 编码的基本原理——为什么大模型数字母总是出错?

BPE 编码的基本原理——为什么大模型数字母总是出错?

作者头像
柏拉图的美工刀
发布2026-07-21 15:18:39
发布2026-07-21 15:18:39
980
举报
文章被收录于专栏:编舟记编舟记

讲 BPE 之前,先抛三个问题挂着,后面逐个回答:

  1. 1. BPE 把字母"吞掉"了,这是缺陷还是设计?直觉说"看不见字母"是坏的,但 BPE 偏偏这么干,为什么?
  2. 2. 字母盲区真的要紧吗?数不出 strawberry 有几个 r,除了让人觉得模型蠢,到底影响了什么?
  3. 3. 有没有一种分词方式能让模型既能看见字母又高效处理语言?如果有,为什么大家还用 BPE?

先上代码,让它自己说话

别急着定义概念。跑一段代码,让"数字母出错"这件事自己暴露:

代码语言:javascript
复制
import tiktoken

enc = tiktoken.get_encoding("cl100k_base")  # GPT-4 用的编码

# 模型眼中的 strawberry
tokens = enc.encode("strawberry")
print([enc.decode([t]) for t in tokens])
# 输出: ['str', 'aw', 'berry']

# 模型眼中的 unbelievable
tokens = enc.encode("unbelievable")
print([enc.decode([t]) for t in tokens])
# 输出: ['un', 'belie', 'vable']

# 模型眼中的 acknowledgment
tokens = enc.encode("acknowledgment")
print([enc.decode([t]) for t in tokens])
# 输出: ['ack', 'nowled', 'gment']7;gment']

输入里压根没有 s-t-r-a-w-b-e-r-r-y 这 10 个独立的字母信号。模型看到的不是 10 个字母,是 3 个词块:str、aw、berry。

有人问"GPT-4,strawberry 里有几个 r?",模型拿到的输入是:

代码语言:javascript
复制
[str] [aw] [berry]

它得在 berry 这个词块里数出 r 的个数。但 berry 在模型眼里可能是一个不可再分的整体。拆不开。

这就是最直接的病因。停下来,先把几个术语说清楚。


先定义几个术语

BPE(Byte Pair Encoding)类似 ZIP 压缩的思路——反复出现的东西用更短的符号表示。但 BPE 是一种特定的做法:从底向上,逐步把高频字节对合并成新符号,最后每个合并产物就是一个 Token。合并顺序完全由频率决定,不是随便凑。

Token(词块)大致像"词语",但不一定是词典里的词。它可能是一个完整词(hello),可能是词根(str),可能是后缀(ing),也可能就是单个字节。模型处理语言的最小单位就是 Token,不是字母——好比快递公司处理包裹的最小单位是"一捆",不是"一本"。

Tokenizer(分词器)把原始文本切成 Token 序列。做的事像翻译,但不是英→中,而是字母流→Token流。翻译过程中,字母信号被压成了词块信号。

回到原问题:模型数不出字母,原因不在模型本身,在于分词器先把字母合并成了词块,字母信号在合并过程中就丢了。


Q1:BPE 把字母"吞掉"了,这是缺陷还是设计?

BPE 的算法——用迭代讲解迭代

顺便说一下,我在用迭代结构讲 BPE,因为 BPE 本身就是个迭代算法。形式跟着内容走。

BPE 每一轮做同一件事:找最频繁的邻居,让它们搬进同一间房。

第 1 轮,每个人住单间:

代码语言:javascript
复制
训练文本: "low lower newest widest"

初始化 → 每个字符是独立 Token:
  low    → [l, o, w]
  lower  → [l, o, w, e, r]
  newest → [n, e, w, e, s, t]
  widest → [w, i, d, e, s, t]

第 2 轮,统计谁和谁最常挨着:

代码语言:javascript
复制
相邻对频率:
  (l, o) → 2 次  ← 最高频
  (o, w) → 2 次
  (e, r) → 1 次

合并 (l, o) → 新 Token "lo":

代码语言:javascript
复制
  low    → [lo, w]
  lower  → [lo, w, e, r]

第 3 轮,继续。最高频相邻对 (lo, w) 出现 2 次,合并 → 新 Token "low":

代码语言:javascript
复制
  low    → [low]            ← 2个变1个
  lower  → [low, e, r]

第 4 轮,合并 (e, r) → "er":

代码语言:javascript
复制
  lower  → [low, er]

什么时候停?Token 总数达到预设上限(GPT-4 词表大约 10 万个 Token)。

每轮都是统计、合并、缩减。停了以后,高频组合变成独立 Token,低频组合保持拆分。

为什么这个设计看不见字母

合并顺序完全由训练语料中的频率决定。"str" 在英语里出现频率极高(string, strong, strategy, strike),它在早期就被合并成独立 Token。"berry" 也频率高(strawberry, blueberry, raspberry),也会被合并。"a-w" 这种组合频率低,可能不会被合并,所以 strawberry 分成了 [str, aw, berry]。

字母信号被合并掉,原因很简单:BPE 只关心编码效率,字母不在它的优化目标里。快递公司把书按捆打包,不是不在乎单本书,是按捆运输更划算。

BPE 从哪里来

BPE 最早不是给大模型设计的。1994 年 Philippe Gage 提出了一种数据压缩算法,想法很朴素:反复出现的模式用更短的符号表示。2015 年 Sennrich 等人在论文《Neural Machine Translation of Rare Words with Subword Units》里把 BPE 引入 NLP,解决机器翻译中罕见词处理不了的问题。

因果链是这样的:翻译要处理罕见词 → 需要兼顾常见词和罕见词的编码 → 选了 BPE(高频合并、低频拆分) → 字母信号在合并过程中被压缩掉了。

字母盲区不是 BPE 的初衷,是它实现"兼顾效率和覆盖"时甩不掉的副产品。


Q2:字母盲区真的要紧吗?

两种"看不见"

人对阅读速度的追求和模型对编码效率的追求,都会导致"看不见字母"。但人能回溯——看快了没看清,慢下来再看一遍就行。模型没有这个能力。Token 一合并,在模型眼里就是原子,拆不开。

搞一个对比:

  • • BPE 看不见字母,看见词块。原因:高频合并,追求编码效率。
  • • 人也看不见字母——熟练阅读时是整体识别词形,不是逐字母扫描。原因:大脑追求阅读速度。
  • • 后果不同:模型无法精确数字母,但语言理解没问题;人能慢下来数字母,但逐字母读会卡顿。

一句话:模型和人都在追求效率时"看不见字母",但人有回溯能力,模型没有。

什么时候字母盲区真的麻烦

字符级任务是最明显的——数字母、拼写检查、字谜游戏,这些需要看见单个字母,BPE 的合并直接把信号抹掉了。代码生成也会出问题,比如变量名 idx 可能被分成 [id, x],模型不知道 x 是变量名的末尾字母。还有罕见词的情况:BPE 对罕见词拆得很碎(回到字节级),这时模型反而"看得见"字母了,但理解能力跟着下降。

什么时候无所谓

语义理解——翻译、摘要、问答,不需要精确到字母。文本生成也没问题,写文章写代码,Token 级的统计足够撑起流畅输出。日常对话就更不用说了,你不会问朋友 banana 有几个 a。

说白了,大模型擅长语义级的事,字符级的事它干不好。这不是蠢,是专业方向不一样。


Q3:有没有更好的分词方式?

字母级分词能让模型看见每个字母,但代价是序列长度暴涨。strawberry 从 3 个 Token 变成 10 个,Transformer 的注意力机制按序列长度平方算计算量,成本直接螺旋上天。词表倒是小,大约 256 个字节就够了,但推理速度慢得没法用。

词级分词反过来,编码效率最高,序列最短。但词表要覆盖所有可能的词——包括拼写错误、新造词、代码标识符——会膨胀到百万级。更致命的是遇到训练语料里没有的新词(OOV),模型完全没法处理。

BPE 卡在中间。常见词保持完整(hello 一个 Token),罕见词拆成子词(strawberry 三个 Token),极罕见的拆成字节。词表大约 10 万,序列长度可控,新词也能兜底。

代价就是这个字母盲区。字母级分词能数字母但太贵,词级最高效但太脆,BPE 是折中。折中就得付折中的代价。

有没有更好的路?有人在探索。ByT5 这种字符级 Transformer,每个字节是 Token,确实能数字母,但算力成本让人头疼。Unicode 级分词类似字母级,对多语言更友好,但同样慢。混合分词——常见词用 BPE,需要字符精度时切字母级——理论上可行,实现起来很复杂。

BPE 目前还是最广泛使用的方案。不因为它是最好的,因为它是最不差的。


一个隐喻:BPE 像截断式快递打包

快递公司要运一仓库的书。不会每本单独寄(太贵,像字母级分词),也不会整仓库打包成一箱(遇到新书没法处理,像词级分词)。策略是:高频组合的书打包成"捆",畅销书系列捆在一起,罕见的单独寄或按页寄。

你问快递公司"第 3 捆里有几页纸?",它回答不了。只记录了"捆"的信息,没记录每捆里的页数。

但这里隐喻有个重要失真:快递公司可以拆开捆数页数,大模型在推理过程中没法拆开 Token 数字母。Token 一合并,就是不可分的原子。快递有回溯能力,模型没有。这点不能搞混。


回顾

三个问题挂在前头,现在收一下。

BPE 吞字母,既是设计也是代价。设计目标是编码效率和罕见词覆盖,字母盲区是甩不掉的副产品。字母盲区在字符级任务里麻烦,语义级任务里无所谓——模型的强项在后头。更好的分词方式?字母级能数字母但算不起,词级高效但太脆弱了,BPE 是折中,目前没有完美解。

说实话我觉得这个问题挺有意思的——大家拿"strawberry 有几个 r"来嘲笑大模型,但很少有人追问为什么。答案不在模型本身,在它看世界的方式。模型看的不是字母,是词块。这就像你嘲笑一个只能识别"人脸"的人认不出"左眼和右眼之间的距离"——他压根没有识别眼睛的感官,他识别的是整张脸。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 柏拉图的美工刀 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 先上代码,让它自己说话
  • 先定义几个术语
  • Q1:BPE 把字母"吞掉"了,这是缺陷还是设计?
    • BPE 的算法——用迭代讲解迭代
    • 为什么这个设计看不见字母
    • BPE 从哪里来
  • Q2:字母盲区真的要紧吗?
    • 两种"看不见"
    • 什么时候字母盲区真的麻烦
    • 什么时候无所谓
  • Q3:有没有更好的分词方式?
  • 一个隐喻:BPE 像截断式快递打包
  • 回顾
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档