很多文章在讲 tokenizer 的时候,往往从「词」「子词」直接开始,但如果你真的想理解 GPT 这类模型是怎么”看懂文字”的,我们可能要从头了解。
需要强调的是,本文将要拆解的底层原理,不仅是GPT的魔法,同样也是驱动千问、豆包、DeepSeek、Kimi等所有顶尖大模型的共通基石。

这篇文章,我想完整记录:

文本是如何一步一步,从”电信号”,变成模型输入的一串整数 ID,又如何在模型输出后,被还原成人类能读懂的文字。

Toekn是如何诞生的

为什么不能只用字节?——从0-256开始的BPE之路

在任何语言模型、任何 tokenizer 出现之前,有一个不可绕过的事实:

计算机并不认识”字符”,只认识电压的高低。

这些电压状态,在逻辑上被抽象为 0 和 1(bit)

但需要强调的是:

语言模型并不会直接处理 bit 级别的数据。

因为 bit 太底层、太长,而且不同编码方式会导致完全不同的 bit 序列。

真正进入 tokenizer 之前,所有文本都会先经过UTF-8 编码

UTF-8

例如,我爱中国 china在 UTF-8 编码下,在计算机中存储运算的时候,其实是以一串字节序列进行的(字节就是计算机底层的那个二进制数字01):

utf-8编码

其实,在这里,我们就已经算是产生了token,只不过每个token就是个8位的二进制数字。
如果直接把上面那一长串 0101 喂给模型,序列长度会爆炸(几十亿长度),模型的计算量绝对会爆炸的。

所以我们需要将这个序列进行压缩。

我们知道:

  • 数学上,8 个二进制位能表示的状态总数是 $2^8 = 256$ 种。
  • 范围是从 00000000 (0) 到 11111111 (255)。

所以,我们可以把我爱中国 china这样转换:

18✖️8=144位长度—>18个整数数字

字节压缩比例

当然,18个整数还是有点长,所以我们还需要减少长度。

于是,一个非常工程化、但极其重要的想法出现了:

能不能把”经常一起出现的字节组合”,压缩成一个新的 token?

这就是 Byte-Pair Encoding(BPE) 的出发点。

BPE的开始

BPE 算法是如何一步步”合并”出 Token 的?

通过刚刚的学习,我们知道初始状态:词表只有 256 个 token。

一切从最简单的状态开始:

初始词表

{0, 1, 2, ..., 255}

每一个 token 对应一个字节值。
训练语料被表示为类似这样的序列:

[..., 230, 136, 145, 231, 136, 177, 228, 184, 173, 229, 155, 189, 32, 99, 104, 105, 110, 97, ...]

BPE初始状态


BPE 会在整个训练语料中统计:

哪些 相邻 token 对 出现得最频繁?

假设它发现:

(230,136,145)

在大量中文文本中经常一起出现 (这是很多中文字符 UTF-8 编码的公共前缀)。
于是:

  1. 它决定新增一个 token ID:256
  2. 定义规则:
    256 = (230,136,145)
  3. 然后在所有数据中统一替换
    这时:

token 257 就完整地代表了汉字「我」

合并

同样的事情会发生在:

  • 「爱」
  • 「中国」
  • 甚至是整个「中国」
  • 英文中的 china

最终token表
最终你可能会得到类似这样的结果:

Token ID 对应文本
257
301
812 中国
2048 china

于是,我爱中国 china最终可能会被表示成:

[257, 301, 812, 2048]

原本十几个字节,现在变成了 4 个 token


BPE 一直合并到什么时候?

这个过程会不断重复:

  • 字节 → 字
  • 字 → 常见词
  • 词 → 常见短语
    直到:

词表大小达到预设上限

比如 3 万、5 万、10 万级别。

最终我们建立了词表

在 BPE 训练完成后,我们会得到一个确定的词表

它本质上是:token ↔ 文本片段 的映射关系

  • 0 – 255:基础字节 token(永远存在,兜底)
  • 256 – N:通过 BPE 学习到的子词、词、短语

词表

最终这个词表会被应用于模型的训练阶段,和模型的输出阶段,其实大家可以理解成字典。就是通过字找到拼音,和通过拼音找到具体的字。
映射

当然,上面只是粗略的讲解了下token化的过程,还有一些细节没有说到。
比如,实际训练的时候,在进入BPE之前,有可能还会对原有的文本序列进行各种正则化清洗。这主要是依赖于我们最终的训练目标是什么。例如你如果训练的是写代码,那么4个空格这种必然需要是一个单独的token,所以再清洗阶段,就不能清洗这种格式

那接下来呢?接下来会做什么呢?大家可能多多少少都知道一点点,LLM统计的是概率,那他的这个概率从何而来呢?

嵌入embedding(就是向量化)

我们刚刚讲了,有了一个token和文本的映射表。但是我们接下来,要建立一个静态的权重矩阵。他是一个二维矩阵,大概的形状可能是这样子:[词表大小, 特征维度]

**行数 (Rows)**:对应词表大小(例如 GPT-4 约为 100,277 行)。其实就是我们刚刚说的那个词表token的数量,每一行都对应了一个token。
**列数 (Columns)**:对应模型的特征维度(例如 Llama 3 为 4,096 列)。这个完全是随意定义的。它本质上就是在说,我要用多少个维度来描述这个token。

就比如苹果,它是一个token,那它的大小、颜色、品种、种植地区、是水果还是手机,等等不同的维度描述。

内容:每一行存储着一个特定的 Token ID 对应的特征向量

  • 第 0 行:存储 Token 0 的 4096 个浮点数。
  • 第 257 行:存储 Token 257(比如 “幻”)的 4096 个浮点数。
    权重矩阵  
    这里,你可能会说,我理解这个矩阵是干什么的了,但是他的初始值是什么呢?其实这里是完全随机生成的一个初始值,就是调用了一个随机函数生成的。后面的训练过程中,所谓的训练,其实在我的理解看来,就是在更新这个权重矩阵的值。让他的值更加接近真实。在预训练完成以后,这个参数也就固定下来了。

实际大模型训练的时候,就会这样去构建一个输入的高维矩阵,或者说张量,大家不要害怕这个名词。她可以这么简单的理解:

比如我爱中国 china,假设它对应4个token,那每一个token,取出来的向量就是[4096个特征值],但是我们向模型输入的时候,就变成了这样的矩阵:
[ [4096个特征值],
[4096个特征值],
中国[4096个特征值],
china[4096个特征值]]

好抽象啊,请看图:
张量

当然,这只是一句话的样子。大家想一下,我们训练肯定不会一句话一句话来训练,我们如果一次性输入32句话,那对模型的输入矩阵是不是就会变成一个3D立方体的3维矩阵呢:
3d张量

当然,到了这里还没有完成,因为模型,其实还不知道token的具体位置,因为我爱中国,和中国爱我,明显意思是不一样的。但是如果在矩阵中,他们的权重是一样的话,那么他们的加权求和的值必然是一样的。所以就会对模型训练造成极大的困扰。
为什么要位置编码

所以,我们需要给每一个token一个位置信息,这就好比虽然都叫‘吏部侍郎’,但我给第一个人发了个‘朝阳区’的身份证,给第二个人发了个‘通州区’的身份证。
吏部侍郎
但是,最先进的大模型(比如DeepSeek)觉得光发身份证还不够聪明。它们用了一种更天才的方法:不是发固定的地址,而是给每人发一块‘表’。

它是这么做的:
• 你是第 1 个字?请把你向量里的指针向左拨动 1度
• 你是第 2 个字?请把你向量里的指针向左拨动 2度
• 你是第 100 个字?请转动 100度

夹角2
为什么要这么麻烦去‘旋转’呢? 因为对于模型来说,它不关心你到底住在北京还是上海(绝对位置),它只关心‘你俩离得近不近’(相对位置)。
通过旋转,奇迹发生了:无论这两个字搬到了文章的哪里,只要它们是相邻的,它们指针之间的夹角差永远是 1度。模型只要一量夹角,瞬间秒懂:‘哦,这俩货是一起的!
夹角
恭喜你,你已经悟透了目前统治大模型界的各种模型背后的核心魔法——RoPE(旋转位置编码)

至此,LLM预训练的token和embedding,大家应该有一个简单的了解了。

#AI #AIGC #GPT #大语言模型 #Tokenizer #Embedding #RoPE #人工智能