文本编码

ASCII编码

标准 ASCII 使用 7 个二进制位,共能表示 128 个字符。

因为 7 位二进制的组合数量是:

$$ 2^7 = 128 $$

编号范围是:

$$ 0 \sim 127 $$

注意,0 到 127 一共有 128 个数,不是 127 个。

例如:

  • A 的 ASCII 码是 65
  • a 的 ASCII 码是 97
  • 0 的 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
2
3
A   → 1 字节
中 → 3 字节
😊 → 4 字节

特点:

  • 完全兼容 ASCII
  • 英文文本最省空间
  • 网络、网页、Linux、文件存储中最常用
  • 中文通常比 UTF-16 占用更多空间

2. UTF-16

UTF-16 使用 2 个或 4 个字节表示一个 Unicode 码点。

它的基本编码单元是 16 位,也就是 2 字节。

字符 占用空间
大多数常用字符,包括常见中文 2 字节
Emoji、部分生僻字 4 字节

例如:

1
2
3
A   → 2 字节
中 → 2 字节
😊 → 4 字节

超过一定范围的字符需要使用两个 16 位编码单元,称为代理对

特点:

  • 常见中文通常只占 2 字节
  • 英文比 UTF-8 更占空间
  • Java、JavaScript、Windows 的一些内部字符串表示与 UTF-16 密切相关
  • 可能涉及大端序和小端序问题

3. UTF-32

UTF-32 固定使用 4 个字节表示一个 Unicode 码点。

例如:

1
2
3
A   → 4 字节
中 → 4 字节
😊 → 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

  1. 计算机只能处理数字,文本需要转换为数字才能输入模型
  2. 我们不能把UTF-8编码的字节直接输入模型,因为按照字节为单位会导致序列太长,而注意力机制的计算复杂度是平方级别的,序列越长计算量越大
  3. 我们不能把每一个字符作为一个token输入模型,因为这样会忽略字符之间的语义关系,而且对于中文来说,单个汉字的语义信息有限,无法充分表达意思
  4. 我们不能把每一个单词作为一个token输入模型,因为这样会导致词表过大,语言中的词汇几乎是无限的,会导致模型参数量过大,训练成本过高,而且对于中文来说,单词的划分本身就是一个问题
  5. 我们需要在词表大小和序列长度之间找一个平衡点,既能保证模型的表达能力,又能保证计算效率,这就是BPE的作用

预分词

预分词先根据空格、标点等,把文本粗略切成若干片段。

预分词的作用,就是根据一组规则对输入的文本进行分割,这种预处理是为了确保模型不会在多个“分割”之间构建tokens。

比如如果不进行预分词,而是直接进行分词,那么可能出现这种情况:“您好 人没了” -> “您” “好 人” “没了”。

也就是说分词有可能会产生这种与我们日常经验相悖的分词效果,而预分词就可以有效地避免这一点,比如在分词前,先在使用预分词在空格上进行分割:“您好 人没了” -> “您好” “人没了”,再进行分词:“您好” “人没了” -> “您好” “人” “没了”。(zhuanlan.zhihu.com/p/692508797)

BPE 算法流程

具体的算法流程可以看这里