文本编码与tokenizer
文本编码
ASCII编码
标准 ASCII 使用 7 个二进制位,共能表示 128 个字符。
因为 7 位二进制的组合数量是:
编号范围是:
注意,0 到 127 一共有 128 个数,不是 127 个。
例如:
A的 ASCII 码是 65a的 ASCII 码是 970的 ASCII 码是 48- ASCII 码 127 是删除控制符
DEL
之所以常说 ASCII 占一个字节,是因为计算机通常以 8 位为一个字节:
1 | 0xxxxxxx |
标准 ASCII 实际只使用低 7 位,最高位通常为 0。
因此:
| 编码 | 位数 | 编码范围 | 可表示数量 |
|---|---|---|---|
| 标准 ASCII | 7 位 | 0~127 | 128 个 |
| 一个字节 | 8 位 | 0~255 | 256 种状态 |
后来有些编码使用了第 8 位,把范围扩展到 0~255,通常被称为“扩展 ASCII”。但扩展部分并没有完全统一,不同编码页对应的字符可能不同。
一句话记忆:
最大编号是 127,但字符数量是 128,因为还包括编号 0。
无法表示中文等非英文字符
中文、日文、韩文、阿拉伯文等都无法直接用 ASCII 编码
Unicode编码
Unicode 是一种字符编码标准,旨在为世界上所有的字符提供唯一的编码。Unicode 使用不同的编码方式来表示字符,其中最常用的是 UTF-8、UTF-16 和 UTF-32。
Unicode 只是字符编号标准,不直接规定字符在文件中占几个字节
UTF-8 UTF-16 UTF-32 编码
UTF-8、UTF-16、UTF-32 都是 Unicode 的编码方式。区别主要在于:一个字符用多少字节保存,以及如何划分编码单元。
1. UTF-8
UTF-8 使用 1~4 个字节表示一个 Unicode 码点。
| 字符 | 占用空间 |
|---|---|
| 英文字母、数字、ASCII 符号 | 1 字节 |
| 大部分欧洲字符 | 2 字节 |
| 常见中文 | 3 字节 |
| Emoji、部分生僻字 | 4 字节 |
例如:
1 | A → 1 字节 |
特点:
- 完全兼容 ASCII
- 英文文本最省空间
- 网络、网页、Linux、文件存储中最常用
- 中文通常比 UTF-16 占用更多空间
2. UTF-16
UTF-16 使用 2 个或 4 个字节表示一个 Unicode 码点。
它的基本编码单元是 16 位,也就是 2 字节。
| 字符 | 占用空间 |
|---|---|
| 大多数常用字符,包括常见中文 | 2 字节 |
| Emoji、部分生僻字 | 4 字节 |
例如:
1 | A → 2 字节 |
超过一定范围的字符需要使用两个 16 位编码单元,称为代理对。
特点:
- 常见中文通常只占 2 字节
- 英文比 UTF-8 更占空间
- Java、JavaScript、Windows 的一些内部字符串表示与 UTF-16 密切相关
- 可能涉及大端序和小端序问题
3. UTF-32
UTF-32 固定使用 4 个字节表示一个 Unicode 码点。
例如:
1 | A → 4 字节 |
特点:
- 编码规则最简单
- 每个 Unicode 码点固定 4 字节
- 可以快速定位第几个码点
- 非常浪费存储空间
- 实际文件和网络传输中很少使用
直接对比
| 编码 | 每个码点占用字节 | ASCII 字符 | 常见中文 | Emoji |
|---|---|---|---|---|
| UTF-8 | 1~4 字节 | 1 | 3 | 4 |
| UTF-16 | 2 或 4 字节 | 2 | 2 | 4 |
| UTF-32 | 固定 4 字节 | 4 | 4 | 4 |
假设保存文本:
1 | A中😊 |
大致占用:
| 编码 | 总字节数 |
|---|---|
| UTF-8 | 1 + 3 + 4 = 8 |
| UTF-16 | 2 + 2 + 4 = 8 |
| UTF-32 | 4 + 4 + 4 = 12 |
为什么需要三种编码
因为它们侧重点不同:
- UTF-8:节省英文空间、兼容 ASCII,适合网络和文件
- UTF-16:常用字符大多占 2 字节,适合一些程序内部处理
- UTF-32:结构简单、固定长度,但特别浪费空间
现在通常优先选择:
文件、网页、数据库、接口传输一般使用 UTF-8。
有哪些理由导致我们在UTF-8编码的字节上训练我们的tokenizer,而不是UTF-16或者UTF-32?比较不同字符串在这些编码方式下的输出也许对你有启发。
答:UTF-8是变长编码,单个字符的字节长度为1-4,对于常见的英文字符和符号UTF-8使用1字节表示,对于其他字符UTF-8使用2-4字节表示。而UTF-16固定使用2字节表示,UTF-32固定使用4字节表示,相同的字符串转换为bytes时,UTF-8是字节数最少的,也就可以分出更少的token,提高计算效率。同时UTF-8是世界上最流行的编码方式,对于大模型的输入具有天然的优势,可以避免编码解码等问题。
还要注意:UTF-32 固定 4 字节,只能说明一个码点固定 4 字节,不能保证一个人眼看到的字符一定是 4 字节。例如某些 Emoji 可能由多个 Unicode 码点组合而成。
BPE
为什么需要tokenizer
- 计算机只能处理数字,文本需要转换为数字才能输入模型
- 我们不能把UTF-8编码的字节直接输入模型,因为按照字节为单位会导致序列太长,而注意力机制的计算复杂度是平方级别的,序列越长计算量越大
- 我们不能把每一个字符作为一个token输入模型,因为这样会忽略字符之间的语义关系,而且对于中文来说,单个汉字的语义信息有限,无法充分表达意思
- 我们不能把每一个单词作为一个token输入模型,因为这样会导致词表过大,语言中的词汇几乎是无限的,会导致模型参数量过大,训练成本过高,而且对于中文来说,单词的划分本身就是一个问题
- 我们需要在词表大小和序列长度之间找一个平衡点,既能保证模型的表达能力,又能保证计算效率,这就是BPE的作用
预分词
预分词先根据空格、标点等,把文本粗略切成若干片段。
预分词的作用,就是根据一组规则对输入的文本进行分割,这种预处理是为了确保模型不会在多个“分割”之间构建tokens。
比如如果不进行预分词,而是直接进行分词,那么可能出现这种情况:“您好 人没了” -> “您” “好 人” “没了”。
也就是说分词有可能会产生这种与我们日常经验相悖的分词效果,而预分词就可以有效地避免这一点,比如在分词前,先在使用预分词在空格上进行分割:“您好 人没了” -> “您好” “人没了”,再进行分词:“您好” “人没了” -> “您好” “人” “没了”。(zhuanlan.zhihu.com/p/692508797)
BPE 算法流程
具体的算法流程可以看这里
