当我们和大模型聊天时,需要先将人类的自然语言转为 token,再输入到大模型中,大模型计算完成之后,再将结果进行逆转换,形成自然语言返回给用户。

那么在转换的过程中,自然语言到底被转成什么了呢?
如果玩过 Elasticsearch 的小伙伴,有可能会把这个按照中文分词去理解,但是这两个其实还是不一样。
在大模型中,Token(词元) 是文本处理的最小单位,相当于计算机理解人类语言的“基础砖块”。它可以是单词、子词、字符或标点符号,具体取决于模型的分词策略。
大模型中的 token 有如下几种常见的不同形式
将用户输入的自然语言转为 Token 的过程就是 Tokenizer。
Byte Pair Encoding(BPE)是一种广泛应用于自然语言处理(NLP)的子词分词算法,最初用于数据压缩,后由 Sennrich 等人(2016 年)引入 NLP 领域,用于解决传统分词方法的局限性。
传统分词方法存在几个问题:一个是词汇表庞大,易出现未登录词(OOV)问题;还有一个是序列过长,丢失语义信息。
OOV问题(Out-Of-Vocabulary Problem) 是自然语言处理(NLP)中因词表覆盖不足导致的核心挑战。当模型在测试或推理阶段遇到未出现在训练词表中的词时,即触发 OOV 问题。
出现 OOV 问题的原因是因为词表(Vocabulary)是模型训练前预设的、包含所有可识别词的集合(比如 BERT 词表含 30,000 个子词),但是对于一些人名、品牌名、科技术语、低频次甚至一些拼写错误的词,常常在预设词表中查询不到,进而就会触发 OOV 问题。
通过子词分词可以在一定程度上消除 OOV 问题。
比如输入一个 OOV 词: "unbreakable",这个词会被拆解为 ["un", "break", "able"],而这三个子词全部在词表内,OOV 被消除。
OOV 问题是 NLP 模型泛化能力的核心瓶颈。子词分词(BPE/WordPiece)通过将 OOV 词分解为可管理的语义单元,成为当前最有效的解决方案。然而,其对未见过词根的新词(如全新创造的品牌名)仍存在局限。
假设有这样一个句子 "Natural language processing is fascinating."
字符级分词结果(按字母和空格拆分):
['N','a','t','u','r','a','l',' ','l','a','n','g','u','a','g','e',' ','p','r','o','c','e','s','s','i','n','g',' ','i','s',' ','f','a','s','c','i','n','a','t','i','n','g','.'] 序列长度:43 个单元
子词分词(BPE)结果(示例词表假设):
["Natural", " language", " process", "ing", " is", " fascin", "ating", "."] 序列长度:8 个单元
很明显,字符级分词结果存在这样一些问题:
字符级分词缺乏对语义单元(词根/词缀)的识别能力,因此会导致三方面的问题,我们分别来看。
比如单词 "unhappiness"。
这样表示之后,存在的问题就是语义单元被拆散,比如:
但是如果使用子词表示(BPE),那就是 ["un", "happi", "ness"] 这样表示的优势就很明显了:
比如现在有一组相关词 ["play", "player", "playing"] 如果用字符级表示,那么结果就是:
这样的问题很明显,模型无法从字符中直接识别共享语义单元 "play"(需从头学习三个独立序列的关联)。
但是如果使用子词表示(BPE),结果就是:
这样通过共享子词 "play" 就能直接传递核心语义,模型只需学习后缀变化("er" 表人, "ing" 表进行时)。
比如有个地址 "Room 205B" 如果是字符级表示:['R','o','o','m',' ','2','0','5','B'] 这样就导致房间号 "205B" 被拆为无意义的数字串,模型难以重建其整体含义(如 "205B" 可能代表特定楼层和区域)。
但是如果用子词表示:["Room", " 205", "B"] 或 ["Room", " 205B"](取决于词表) 这样 "205B" 作为整体保留,携带完整语义信息。
总结一下就是字符级分词是语义表达的“碎片化”过程,而 BPE 等子词方法通过保留具有实际语义的子词单元(如词根、常用后缀、常见数字组合),在序列长度和语义完整性之间取得了平衡,成为现代 NLP 模型的更优选择。
Byte-level BPE(字节级字节对编码)是传统 BPE(Byte Pair Encoding)的一种改进变体,核心区别在于操作的基本单位从字符(Character)降级到字节(Byte)。这一改动带来了多语言兼容性、更强的泛化能力等优势,但也牺牲了部分可读性。
现在我们使用的大模型基本上都支持多语言,甚至包括很多 emoji 和特殊符号,但是传统的 BPE 依赖字符编码,无法处理多语言混合文本。
传统 BPE 以字符为基本单位,但不同语言的字符编码方式不同(如中文是 Unicode 多字节字符,英文是 ASCII 单字节字符)。
举个简单的例子,中文“你好”在 UTF-8 中占 6 字节(\xe4\xbd\xa0\xe5\xa5\xbd),但 BPE 可能直接拆成两个汉字 ["你", "好"],无法处理字节级别的合并,如果训练语料只有英文,遇到中文、emoji(🚀)或特殊符号(∑)时,BPE 可能无法正确拆分,导致 OOV(未登录词)问题。
同时传统 BPE 在处理一些特殊字符如数学公式、拼写错误的词时,可能会被当作未知字符,进而导致信息丢失。
Byte-Level BPE 在处理时先将所有文本转为 UTF-8 字节序列(每个字符 1~4 字节),并且将初始词表固定为 256 个字节(0x00-0xFF),不受语言影响。
这样改进之后,就可以支持任意语言(中文、日文、阿拉伯语、emoji、代码等),并且统一处理所有文本,无需为不同语言调整词表。
Byte-Level BPE 能够天然解决 OOV 问题,即使遇到训练数据中未出现的词(如新造词“栓Q”),Byte-Level BPE 也能拆解为字节组合:
"栓Q" → UTF-8 字节 \xe6\xa0\x93\x51 → 可拆分为 \xe6\xa0\x93(“栓”) + \x51(“Q”)。
如果使用传统 BPE,那么当“栓”不在训练词表中,BPE 可能直接标记为,而 Byte-Level BPE 仍能保留部分信息。
同时,Byte-Level BPE 初始词表仅 256 个字节,远小于 BPE 的数千个字符(如中文 BPE 词表可能包含 5000+ 汉字),这样训练时内存占用更低,适合大规模语料。并且对于代码、数学公式甚至连二进制数据理论上也能处理了。
虽然 Byte-Level BPE 解决了 BPE 的许多问题,但仍有一些缺点:
那么 Byte Pair Encoding(BPE)的词表是怎么得到的呢?
词表训练是一个数据驱动的迭代合并过程,通过统计语料中的高频字符/子词组合逐步构建。
具体训练步骤是这样:
首先我们需要有一个初始词表,这个初始词表是语料中所有唯一字符的集合,如果是英文,那么就是 {a, b, ..., z, A, ..., Z, 0, ..., 9, !, , ...}(约100+个token);如果是中文,那么就是所有出现的汉字 + 符号(可能数千个)。
接下来我们就开始训练。
这样就得到最终词表:初始字符 + 所有合并生成的子词。
下面松哥再通过一个例子,和小伙伴们演示一下上面的过程。
首先假设我们有如下语料:["low", "low", "low", "newer", "newer", "newest", "newest", "newest", "newest"] ("low"出现3次,"newer"出现2次,"newest"出现4次)。
训练过程如下:
这个和前面的训练过程类似,区别主要是以下方面:
\xe4\xbd\xa0(3字节)。\xbd\xa0)。\xe4\xbd → \xe4\xbd\xa0)。