现在的大模型不只处理文本,还能处理图片、音频、视频等多种模态。有趣的是,这些不同类型的数据最终都会转换成Token的形式。
比如图片,会经过视觉编码器处理,最终也变成一系列Token喂给模型。这就是为什么现在的多模态模型能够统一处理各种类型数据的原因——在Token这个层面,它们本质上都是数字序列。
这就为什么token是大模型的第一步,本文将会从原理介绍下分词。以及现在最普遍用的BPE的原理与优缺点
什么是分词(Tokenization)?
首先说说Token这个概念。很多人把Token简单理解为"词",但这样理解其实不够准确。Token更像是大模型处理文本的"最小语义单元"。
为什么需要Token呢?原因很简单——计算机只认识数字,不认识文字。当我们输入"你好世界"这样的文本时,模型需要把它转换成数字才能处理。这个转换过程就需要用到Tokenizer(分词器)。
Tokenizer的作用就是把人类的文本转换成模型能理解的数字ID序列。整个过程大概是这样的:
文本输入 → Tokenizer处理 → Token ID序列 → 模型处理
我们选择如何分解文本会显著影响模型理解语言的能力和计算效率。
因此下文将会展开各个分词方法的优缺点。
分词方法 | 说明 | 优点 | 缺点 | 示例 |
---|---|---|---|---|
基于词的分词 (Word-based) | 将文本分割成完整的词,按空格和标点符号分隔 | • 保留完整语义 • 直观易懂 | • 词汇表庞大(17万+) • 无法处理未见词 • 词形变化困难 | “Working on NLP” → [“Working”, “on”, “NLP”] |
基于字符的分词 (Character-based) | 将文本分解为单个字符 | • 词汇表极小 • 无未见词问题 | • 丢失语义信息 • 序列过长 • 计算成本高 | “Working” → [“W”, “o”, “r”, “k”, “i”, “n”, “g”] |
子词分词(BPE) (Byte Pair Encoding) | 基于频率合并字符对,形成子词单元 | • 平衡词表大小 • 处理未见词 • 保留语义 • 主流选择 | • 中文等语言效率低 | “tokenization” → [“token”, “ization”] “unusualification” → [“un”, “usual”, “ification”] |
1. 基于词的分词(Word-based)
最直观的方法是将文本分割成完整的词。对于英文,这可以简单到按空格和标点符号进行分割。
优点:
- 保留完整词作为有意义的语义单位
- 比基于字符的方法更能表达语义
缺点:
- 导致词汇表极其庞大(牛津词典有超过170,000个词)
- 难以处理词汇表外(OOV)的词
- 无法处理词的细微变化(例如"cat"与"cats")
2. 基于字符的分词(Character-based)
在另一端,基于字符的分词将文本分解为单个字符。
优点:
- 词汇表极小(几千个字符就能覆盖大多数语言)
- 只要字符在词汇表中就没有OOV问题
缺点:
- 丢失词级语义信息
- 创建非常长的标记序列
- 增加表示文本的计算成本
3. 子词分词
子词方法在词和字符方法之间取得平衡。
它是基于统计相邻词当作词表的处理算法。
分词的结果类似于jieba分词,但它是基于传入的数据本身训练得到的。
字节对编码(BPE)
BPE最初是一种压缩算法,现已成为NLP的基础。它被GPT等模型使用。
💡需要注意的是,token本身是一个较为模糊的概念,是由设计者人为定义的。同一个符号序列是否成为一个token,取决于其在语料库中的出现频率:
这种分词策略完全由语言模型的设计者决定,每个语言模型采用的token规则也各不相同。例如,GPT-3.5和GPT-4使用的token系统就有所区别。
BPE工作原理:
- 初始化:从单个字符的词汇表开始
- 训练:
- 统计语料库中相邻字符对的频率
- 将最频繁的对合并为新的标记
- 将这个新标记添加到词汇表
- 重复直到达到所需的词汇表大小
完整的BPE训练过程演示
1. 准备数据
假设我们要训练一个BPE分词器,语料库如下:
语料:{
"usual": 5,
"unusual": 3,
"usualness": 2,
"super": 4,
"superb": 3,
"supermarket": 2
}
初始状态
- 字符级词汇表:[‘u’, ‘s’, ‘l’, ‘a’, ‘n’, ‘e’, ‘p’, ‘r’, ‘b’, ‘m’, ‘k’, ‘t’, ‘’]
- 初始分词状态:
- “usual” → [‘u’, ‘s’, ‘u’, ‘a’, ‘l’, ‘’]
- “unusual” → [‘u’, ‘n’, ‘u’, ‘s’, ‘u’, ‘a’, ‘l’, ‘’]
- “usualness” → [‘u’, ‘s’, ‘u’, ‘a’, ‘l’, ‘n’, ‘e’, ‘s’, ‘s’, ‘’]
- …
第1次合并:寻找最频繁字符对
统计相邻字符对的出现频率:
- (‘u’, ‘s’): 10次(出现在usual、unusual、usualness中)
- (‘s’, ‘u’): 5次
- (‘u’, ‘a’): 5次
- (‘p’, ‘e’): 9次(出现在super、superb、supermarket中)
选择:(‘u’, ‘s’) → 合并为 ‘us’
更新后:
- 词汇表:[‘u’, ‘s’, ‘l’, ‘a’, ‘n’, ‘e’, ‘p’, ‘r’, ‘b’, ‘m’, ‘k’, ‘t’, ‘’, ‘us’]
- “usual” → [‘us’, ‘u’, ‘a’, ‘l’, ‘’]
- “unusual” → [‘u’, ‘n’, ‘us’, ‘u’, ‘a’, ‘l’, ‘’]
第2次合并
寻找最频繁对:
- (‘us’, ‘u’): 10次
- (‘s’, ‘u’): 5次(现在减少了)
- (‘p’, ‘e’): 9次
选择:(‘us’, ‘u’) → 合并为 ‘usu’
第3-5次合并
继续合并过程:
- (‘usu’, ‘a’): 10次 → 合并为 ‘usua’
- (‘usua’, ‘l’): 10次 → 合并为 ‘usual’
- (‘p’, ‘e’): 9次 → 合并为 ‘pe’
最终BPE词典(部分)
标准词汇:
- 'usual': 完整的token
- 'us': 高频前缀
- 'un': 否定前缀
- 'super': 完整的token
- 'er': 高频后缀
- 'ness': 高频后缀
有了这个词表,当用户输入词时我们就可以通过词表分割及标记了。
未见词处理演示
示例1:处理"unusualification"
由于这个词未在训练数据中出现,BPE会分解它:
- 先找出能识别的子词
- 分解结果:[‘un’, ‘usual’, ‘ification’]
- 或者:[‘un’, ‘usual’, ‘ify’, ‘cation’]
示例2:处理"supernewest"
- 分解结果:[‘super’, ‘new’, ‘est’]
- 即使"supernewest"从未出现过,我们仍然可以理解其含义
- “super” - 前缀,表示"超级"
- “new” - 新的
- “est” - 最高级后缀
如图模拟openai的分词结果。
可见通过BPE算法,一个未知的词能被分解为多个token,而后续我们对该词的embedding我们也可以通过拆分这些词能猜到这个未见词的含义
如下图将这些文字/单词用数字表示就实现了分词Tokenization。
批处理中的Attention Mask
在实际应用中,模型通常会批量处理多个句子以提高效率。但问题来了——不同句子的长度往往不一样,怎么办?
这时候就需要用到Padding和Attention Mask。简单来说:
- 对于较短的句子,我们会用特殊的padding token(通常是0)填充到相同长度
- Attention Mask用来标记哪些位置是真实的内容(标记为1),哪些是填充的(标记为0)
这样做的好处是,模型在计算时会自动忽略那些填充的部分,同时也为后续的优化(比如变长序列优化)提供了基础。
深入理解Tokenizer配置
tokenizer.json文件解析
对于做模型部署的同学来说,理解tokenizer.json文件的结构很重要。这个文件定义了分词器的所有配置信息。
一个典型的token条目包含这几个关键字段:
{
"151643": {
"content": "<|endoftext|>",
"special": true,
"lstrip": false,
"rstrip": false
}
}
- ID(151643):Token在词汇表中的唯一标识符
- content:Token的实际内容
- special:标记是否为特殊Token
- lstrip/rstrip:控制左右空格的处理方式
特殊Token的作用
特殊Token在大模型中扮演着重要角色,常见的包括:
- BOS Token:序列开始标记,告诉模型"文本从这里开始"
- EOS Token:序列结束标记,告诉模型"文本到这里结束"
- UNK Token:未知词标记,用于处理词汇表中没有的词
- PAD Token:填充标记,用于批处理时的长度对齐
值得注意的是,有些模型(比如LLaMA)默认配置中可能没有设置PAD Token,这会在批处理时报错。解决方法是在配置文件或代码中手动添加PAD Token的定义。