我画了十几张图,终于把GPT识字这件事,从头到尾讲透了

很多文章在讲 tokenizer 的时候,往往从「词」「子词」直接开始,但如果你真的想理解 GPT 这类模型是怎么”看懂文字”的,我们可能要从头了解。
需要强调的是,本文将要拆解的底层原理,不仅是GPT的魔法,同样也是驱动千问、豆包、DeepSeek、Kimi等所有顶尖大模型的共通基石。
这篇文章,我想完整记录:
文本是如何一步一步,从”电信号”,变成模型输入的一串整数 ID,又如何在模型输出后,被还原成人类能读懂的文字。

为什么不能只用字节?——从0-256开始的BPE之路
在任何语言模型、任何 tokenizer 出现之前,有一个不可绕过的事实:
计算机并不认识”字符”,只认识电压的高低。
这些电压状态,在逻辑上被抽象为 0 和 1(bit)。
但需要强调的是:
语言模型并不会直接处理 bit 级别的数据。
因为 bit 太底层、太长,而且不同编码方式会导致完全不同的 bit 序列。
真正进入 tokenizer 之前,所有文本都会先经过UTF-8 编码:

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

其实,在这里,我们就已经算是产生了token,只不过每个token就是个8位的二进制数字。
如果直接把上面那一长串 0101 喂给模型,序列长度会爆炸(几十亿长度),模型的计算量绝对会爆炸的。
所以我们需要将这个序列进行压缩。
我们知道:
- 数学上,8 个二进制位能表示的状态总数是 $2^8 = 256$ 种。
- 范围是从
00000000(0) 到11111111(255)。
所以,我们可以把我爱中国 china这样转换:
18✖️8=144位长度—>18个整数数字

当然,18个整数还是有点长,所以我们还需要减少长度。
于是,一个非常工程化、但极其重要的想法出现了:
能不能把”经常一起出现的字节组合”,压缩成一个新的 token?
这就是 Byte-Pair Encoding(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 会在整个训练语料中统计:
哪些 相邻 token 对 出现得最频繁?
假设它发现:
(230,136,145) |
在大量中文文本中经常一起出现 (这是很多中文字符 UTF-8 编码的公共前缀)。
于是:
- 它决定新增一个 token ID:
256 - 定义规则:
256 = (230,136,145)
- 然后在所有数据中统一替换
这时:
token
257就完整地代表了汉字「我」

同样的事情会发生在:
- 「爱」
- 「中国」
- 甚至是整个「中国」
- 英文中的
china

最终你可能会得到类似这样的结果:
| 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维矩阵呢:
当然,到了这里还没有完成,因为模型,其实还不知道token的具体位置,因为我爱中国,和中国爱我,明显意思是不一样的。但是如果在矩阵中,他们的权重是一样的话,那么他们的加权求和的值必然是一样的。所以就会对模型训练造成极大的困扰。
所以,我们需要给每一个token一个位置信息,这就好比虽然都叫‘吏部侍郎’,但我给第一个人发了个‘朝阳区’的身份证,给第二个人发了个‘通州区’的身份证。
但是,最先进的大模型(比如DeepSeek)觉得光发身份证还不够聪明。它们用了一种更天才的方法:不是发固定的地址,而是给每人发一块‘表’。
它是这么做的:
• 你是第 1 个字?请把你向量里的指针向左拨动 1度。
• 你是第 2 个字?请把你向量里的指针向左拨动 2度。
• 你是第 100 个字?请转动 100度。

为什么要这么麻烦去‘旋转’呢? 因为对于模型来说,它不关心你到底住在北京还是上海(绝对位置),它只关心‘你俩离得近不近’(相对位置)。
通过旋转,奇迹发生了:无论这两个字搬到了文章的哪里,只要它们是相邻的,它们指针之间的夹角差永远是 1度。模型只要一量夹角,瞬间秒懂:‘哦,这俩货是一起的!
恭喜你,你已经悟透了目前统治大模型界的各种模型背后的核心魔法——RoPE(旋转位置编码)。
至此,LLM预训练的token和embedding,大家应该有一个简单的了解了。
#AI #AIGC #GPT #大语言模型 #Tokenizer #Embedding #RoPE #人工智能








