03-模型简介2:词表和嵌入字典
计算机只认数字,根本不认文字。
1 引子:计算机输入输出原理
在计算机的内存和硬盘里,从来就没有存储过“苹果”这两个字的图案。它存储的永远是一个数字,比如 3324。
所谓“苹果”这个文字,只是在你眼睛看到屏幕时,操作系统根据数字 3324,去字体文件里找到对应的字形,然后画出来的一张小图片。
1.1 按下按键,产生“扫描码”
你每按下一个键,键盘里的芯片就会产生一个数字,这个数字叫做扫描码。
-
它并不对应字母'A'或数字'1',而是对应键盘上的一个物理位置。
-
举个例子:无论在哪种键盘布局下,你按下QWERTY键盘左上角第二个键,它产生的扫描码都是
0x1E(一个十六进制数)。
后续的转换步骤会根据你的键盘布局(比如美式、英式、德式)来决定 0x1E 这个扫描码最终显示什么。在美式键盘布局下,0x1E 对应字母 A,但在德式布局下,它对应的可能是 Q。所以键盘本身只发送“位置码”,具体对应哪个字母,由操作系统软件决定。
1.2 操作系统接收并转换为编码
键盘通过USB或蓝牙把扫描码发给计算机。操作系统(Windows、macOS、Linux)的键盘驱动接收后,会根据你当前设置的键盘布局,把它转换为一个标准的字符编码。
-
这个编码是计算机内部真正使用的数字。
-
最基础的编码是ASCII,只能表示英文、数字和符号。
-
全球通用的编码是Unicode(特别是它的实现方式UTF-8),可以表示世界上所有语言文字。
举个例子(按下一个键,背后的数字变化):
| 阶段 | 表现形式 | 说明 |
|---|---|---|
| 1. 物理按键 | 你按下键盘上的 A 键 |
- |
| 2. 键盘发送 | 0x1E (扫描码) |
键盘只报告按键的位置 |
| 3. 驱动转换 | 0x41 (ASCII编码) |
操作系统根据美式键盘布局,把扫描码 0x1E 解读为大写字母 A |
| 4. 存入内存 | 01000001 (二进制) |
这是计算机内存中真正存储和处理的数字 |
1.3 应用软件接收编码
操作系统会把转换好的编码数字(比如 0x41)交给当前活动窗口的应用软件。
-
文本编辑器(记事本、Word):接收到
0x41就知道用户输入了'A',于是把它存入文档数据中。 -
游戏:可能直接忽略这个'A'对应的文字含义,而是用来控制角色向左移动。
-
快捷键:如果同时按下了
Ctrl,操作系统或软件会识别为Ctrl + A,执行全选操作,而不是输入字母'A'。
1.4 显示回馈,让你“看到”字
你之所以能在屏幕上看到'A',是因为软件最后做了一步绘制操作:
-
软件告诉图形系统:把Unicode/ASCII编码是
0x41的文字,在屏幕某个位置画出来。 -
图形系统去查字体:它根据当前设置的字体(比如微软雅黑),找到这个字体文件里,负责绘制
0x41这个字符的字形数据。字体里存放的其实是图像的轮廓信息。 -
显示器显示:图形系统根据字形数据,把像素点亮,最终你就在屏幕上看到了一个黑体的或者宋体的 A。
2 词表
从物理按键到 Unicode 编码,再到 UTF-8 字节,这个前半段流程,大模型和传统程序完全一样。真正的区别,发生在“拿到 ID 之后”。
| 维度 | 传统计算机程序 | 大模型 |
|---|---|---|
| 拿到 ID 后做什么 | 作为精确指令去查表、判断、执行(如:比较 if(id == 42341)) |
作为索引去取一个高维向量,然后做数学运算 |
| 对 ID 的认知 | 离散符号,ID 之间没有数学关系(“苹果”的 ID 42341 和“香蕉”的 ID 63982 没有任何计算意义) | 连续向量,ID 之间有数学关系(vec(42341) + vec(国王) - vec(男人) ≈ vec(王后)) |
| 如何处理“苹果” | 查找字体 glyph_42341 画出来;或判断 if id == 42341 执行特定分支 |
把 vec(42341) 放入神经网络,根据上下文让它的向量值动态变化 |
| 输出方式 | 输出 ID 后,反向查表得到文字,再交给操作系统显示 | 输出下一个 ID 的概率分布,采样后再反向查表,同样交给系统显示 |
| 本质工作 | 状态机:从一个确定状态跳到另一个确定状态 | 数学函数:从输入向量计算输出向量 |
当你输入"苹果很好吃":
操作系统看到的:
字符序列: ['苹', '果', '很', '好', '吃']
↓
查字体表: 找到每个字符的字形数据
↓
屏幕显示: 🍎... (实际画出来的形状)
大模型看到的:
原始文本: "苹果很好吃"
↓
BPE分词: ['苹果', '很好', '吃'] (注意边界可能不同)
↓
查词表: [42341, 42342, 42343]
↓
模型处理这些ID的向量
| 维度 | 操作系统词表 | 大模型词表 |
|---|---|---|
| 目的 | 显示文字、处理编码 | 理解语义、预测下一个词 |
| 内容 | 字符(字母、汉字、标点、emoji) | 子词(常见词根、词缀、完整词) |
| 规模 | 几万到十几万(Unicode字符集) | 几万到几十万(BPE合并结果) |
| 例子 | 'A'、'a'、'苹'、'果'、'😊' |
"苹果"、"ing"、"un"、"##ing" |
| 谁决定的 | Unicode联盟 + 字体厂商 | 模型训练时的数据统计 |
| 能否修改 | 用户可安装新字体 |
固定,训练后不可变 |
必须先有词表(确定ID),才能创建嵌入表(确定矩阵的行数)。
2.1 对比
| 维度 | 词表 | 嵌入表 |
|---|---|---|
| 生成时机 | 训练之前 | 训练之中 |
| 输入 | 原始文本(字节/字符频率) | Token ID序列 + 监督信号 |
| 算法 | BPE、WordPiece(统计方法) | 梯度下降、反向传播(优化方法) |
| 是否可训练 | 否,固定不变 | 是,模型参数的一部分 |
| 大小 | vocab_size(如 50,000) | vocab_size × embedding_dim(如 50,000×768) |
| 内容 | 子词 ↔ ID 的映射 | 每个ID对应一个向量(浮点数数组) |
| 目的 | 确定文本如何切分 | 捕获词语的语义和语法信息 |
| 保存文件 | vocab.json, merges.txt | model.pt / model.bin(与模型其他参数一起) |
| 环节 | 类比 | 说明 |
|---|---|---|
| 原始文本 | 一座城市的所有建筑物 | 待处理的数据 |
| 词表生成 (BPE) | 绘制城市地图,给每条街道命名 | 分析文字本身,确定如何切分。只统计频率,不理解含义。 |
| 嵌入表初始化 | 在地图上随机放路标 | 开始时位置是随机的,没有任何意义 |
| 模型训练 | 无数人在城市中行走、问路、反馈 | 根据"预测下一个位置"的任务,不断调整路标位置 |
| 训练好的嵌入表 | 人们走出来的热门路径和语义地图 | "苹果"和"香蕉"的路标自动靠近(都是水果),"苹果"和"华为"的路标也靠近(都是公司) |
2.2 词表创建
在生成概率时,模型确实需要知道“总共能输出哪些东西”。这个可识别的所有Token的集合,叫做词表(Vocabulary)。
-
它是一个固定的、预先建好的列表。
-
每个Token可以是:一个完整的词、一个子词(如“ing”)、一个标点,甚至是一个字母。
-
词表大小是模型设计时决定的,比如:
-
GPT-2 / GPT-3:约 50,257
-
LLaMA:约 32,000
-
某些中文大模型(如Qwen、DeepSeek):可能 10万~20万
-
ID 本质上就是一个自增的整数索引,在构建词表的过程中按顺序分配。在模型训练之前,通过BPE等算法对纯文本进行统计分析生成。它是确定的、固定的。
# 初始:没有词表
vocab = {}
# 来第一个词
new_token = "苹果"
new_id = len(vocab) # 0
vocab["苹果"] = 0
# 来第二个词
new_token = "很好"
new_id = len(vocab) # 1
vocab["很好"] = 1
# 来第三个词
new_token = "吃"
new_id = len(vocab) # 2
vocab["吃"] = 2
# 结果
print(vocab) # {'苹果': 0, '很好': 1, '吃': 2}
GPT-2的词表大小是50,257,ID分配如下:
| ID范围 | 内容 | 说明 |
|---|---|---|
| 0-255 | 单个字节 | 基础层 |
| 256-... | 字节组合 | 通过BPE合并生成 |
| ...-50256 | 最长token | 最终词表 |
| 50257 | <|endoftext|> |
特殊token(手动添加) |
注意:特殊token(如[PAD]、[CLS]、<s>等)通常是在BPE训练完成后,手动分配额外的ID。
| 问题 | 答案 |
|---|---|
| ID怎么来的? | 按token创建的顺序,从0开始逐个递增分配 |
| 谁决定ID是多少? | 构建词表的程序逻辑(按创建顺序自然决定) |
| ID能自己选吗? | 技术上可以,但没意义且会破坏数组索引效率 |
| ID有语义吗? | 没有。ID只是索引,"苹果"的ID是42341只是因为它恰好第42342个被创建 |
| 不同模型的ID一样吗? | 不一样。不同训练语料、不同BPE实现,同一个词的ID完全不同 |
2.3 词表使用
| 阶段 | 使用方式 | 频率 |
|---|---|---|
| 训练预处理 | 用词表把所有训练文本转成ID序列,保存下来 | 一次(离线) |
| 模型内部 | 嵌入层用Token ID作为索引,查表取向量 | 每层、每步都用 |
| 推理时 | 编码用户输入(文本→ID);解码模型输出(ID→文本) | 每次请求 |
词表提供了“索引”(Token ID),模型通过一个可训练的“查找表”(嵌入矩阵),把每个ID映射成一个高维向量。这个映射关系是在训练中不断学习、调整出来的。
# 嵌入矩阵:形状 [vocab_size, embedding_dim]
embedding_matrix = [
# ID 0 对应的向量
[0.12, -0.34, 0.56, ..., 0.78], # 768个浮点数
# ID 1 对应的向量
[0.23, -0.11, 0.67, ..., 0.21],
# ...
# ID 42341 对应的向量("苹果")
[0.35, 0.72, -0.14, ..., 0.93],
# ...
]
核心操作: 给定 Token ID,直接取矩阵的第 ID 行。这就是查表,复杂度 O(1)。

关键事实:
-
大模型的词表基于Unicode字符构建
-
但大模型的词表不等于Unicode字符集
-
大模型词表中的每个子词,最终都能映射到Unicode字符序列(词表中的每个token都预先存储了它对应的原始字节序列)
-
但反过来不成立:不是每个Unicode字符都会单独出现在大模型词表中(因为BPE会把常见字符组合合并)
可视化映射关系
词表条目 (ID: 42341) ┌─────────────────────────────────────────┐ │ 存储的原始字节(创建时就固定下来) │ │ ┌────┬────┬────┬────┬────┬────┐ │ │ │0xE8│0x8B│0xB9│0xE6│0x9E│0x9C│ │ │ └────┴────┴────┴────┴────┴────┘ │ │ ↓ UTF-8解码 │ │ ┌──────────────────────────┐ │ │ │ "苹果" │ │ │ └──────────────────────────┘ │ └─────────────────────────────────────────┘ 解码时: ID 42341 → 取出预存的6个字节 → UTF-8解码 → "苹果"
没有任何一步是“识别字形”。
一句话总结:
词表提供门牌号(ID),嵌入层是一栋大楼(矩阵),每个门牌号对应一个房间(向量行)。训练的过程,就是根据语义不断"装修"这个房间——调整向量里的数字,让语义相近的词在向量空间里距离更近。
3 字典(嵌入表)
在模型训练之中,通过反向传播和梯度下降从数据中学习得到。它是可训练的、不断优化的。
嵌入向量就是一个大的“表”(矩阵),“Token ID”就是这张表的行号,直接用行号去取对应行的向量。
3.1 字典创建

词表提供了“索引”(Token ID),模型通过一个可训练的“查找表”(嵌入矩阵),把每个ID映射成一个高维向量。这个映射关系是在训练中不断学习、调整出来的。
-
初始化:一开始,嵌入表里的向量是随机的小数。此时,ID
3324的向量和ID8921的向量毫无关系。 -
训练:在大模型训练的过程中(在海量文本上,根据“预测下一个词”这个任务,用反向传播算法),这个嵌入表会被不断地、缓慢地更新。
-
结果:训练结束后,含义相近的词,它们的向量在高维空间中的位置也会彼此靠近。例如,“苹果”和“香蕉”的向量会非常相似。模型通过这种方式,把“查表”这个简单操作,变成了“查询语义”的强大工具。
大模型内部存在一个真正意义上的“映射表”,功能很像字典:
-
键:词表中的某个Token ID(整数)
-
值:该Token对应的向量(几百到几千维的浮点数)
例如:
Token ID 3324 → [0.12, -0.56, 0.78, …] Token ID 8921 → [-0.34, 0.91, -0.06, …]
这个映射:
-
在模型训练一开始随机初始化
-
在训练过程中逐步被调整
-
训练完成后固定下来
例子:
假设:
-
词表大小
V = 4(只有“苹果 好吃 很 吗”四个Token) -
嵌入维度
d = 2(为了让例子简单)
那么嵌入表 = 一个 4×2 的矩阵:
Token ID → 嵌入向量(行向量) 0 → [0.12, -0.56] 1 → [-0.34, 0.91] 2 → [0.45, -0.23] 3 → [-0.78, 0.34]
矩阵写法(L表示Embedding Layer):
L = [ [0.12, -0.56], # 行0 [-0.34, 0.91], # 行1 [0.45, -0.23], # 行2 [-0.78, 0.34] # 行3 ]
这个矩阵 L 就是可训练的模型参数,在训练过程中会被不断更新。
3.2 字典使用
找嵌入向量的操作 = 查表(Row Indexing)
你得到 Token ID = 2(比如对应“很”)。
计算机做的事极其简单:
取矩阵
L的第2行embedding = L[2] # [0.45, -0.23]
Token ID 不是直接从词表里随便拿的,而是通过最后一层的 Softmax + 采样 得到的一个“合法”的行号。
Token ID = 2
│
▼
┌─────────────────────┐
│ Embedding Table │
│ (V × d 矩阵) │
│ │
│ row 0: [0.12, -0.56] │
│ row 1: [-0.34, 0.91] │
│ row 2: [0.45, -0.23] ◄── 取这一行
│ row 3: [-0.78, 0.34] │
└─────────────────────┘
│
▼
嵌入向量 = [0.45, -0.23]
用户输入的文字到底是如何变成向量的
嵌入向量表里早就有了所有文字的向量,直接拿Token ID去取即可。而Token ID,是通过一个叫Tokenizer的组件,实时把输入文字查词表得到的。
整个过程分两步:
-
文字 → Token ID(通过 Tokenizer)
-
Token ID → 嵌入向量(通过 Embedding 层)
4 大模型中的应用
4.1 分词:文字 → Token ID
用户输入的是“苹果好吃吗”,计算机内存里存的是汉字的UTF-8编码字节。大模型本身不认识这些字节。所以,在数据进入模型之前,必须先经过一个独立的、与模型配套的 Tokenizer 组件。这个 Tokenizer 的核心,就是那张词表。
用户输入: "苹果好吃吗"
│
▼
[Tokenizer 组件]
(使用 BPE / WordPiece 等算法,并查询词表)
│
▼
Token IDs: [3324, 8921, 4093]
Token ID 就是这样得到的:它是一个查表的过程,这个映射关系是预先训练好并固定下来的。
-
"苹果" 这个词如果在词表里,就直接映射到ID
3324。 -
"好吃" 映射到
8921。 -
"吗" 映射到
4093。
关键点:
-
Token ID 不是大模型“想”出来的,是 Tokenizer 硬查表得到的。
-
这张词表和映射关系,与大模型本身是分不开的。 训练GPT-3用的词表,推理时必须用同一个。
4.2 查表:Token ID → 嵌入向量
现在,我们把 Token IDs [3324, 8921, 4093] 送入大模型。模型的第一步操作,就是一个纯粹的 嵌入层(Embedding Layer)。嵌入向量表里已经有了,直接拿ID去取。这个嵌入层本质上就是一个巨大的矩阵(词表大小 × 嵌入维度),里面的每一个行向量,就是一个Token ID对应的可学习参数。
Token IDs: [3324, 8921, 4093]
│
▼
┌───────────────────────────┐
│ Embedding Layer │
│ (一个巨大的参数矩阵) │
│ │
│ row 3324 → [0.12, -0.56...] ◄── 查第3324行
│ row 8921 → [-0.34, 0.91...] ◄── 查第8921行
│ row 4093 → [-0.78, 0.34...] ◄── 查第4093行
└───────────────────────────┘
│
▼
嵌入向量序列:
[[0.12, -0.56...],
[-0.34, 0.91...],
[-0.78, 0.34...]]

4.3 Token ID为啥能正确匹配嵌入向量表
因为这两个东西(Tokenizer的词表和模型的嵌入层)是在同一个“工厂”里,用同一份“设计图纸”生产出来的。它们之间是一种人为约定、严格对齐的关系。就像你用一把钥匙开一把锁,钥匙能开锁,不是因为钥匙认识锁,而是因为它们本来就是按同一套模具制造的。
你可以把整个过程想象成一个两方约定:
-
甲方(Tokenizer):负责把文字翻译成整数ID。它的工作手册就是词表。
-
乙方(大模型):负责用ID去查找向量。它的工作手册就是嵌入矩阵。
为了让双方能无缝合作,一个最基本的前提是:甲方的词表里的词数量,必须等于乙方嵌入矩阵的行数。并且,同一个词,在双方那里对应的ID必须完全一样。
这个“ID必须一样”的约定,是在大模型训练开始之前就确定下来的,永远不会改变。
4.3.1 阶段一:训练前(初始化对齐)
-
第一步:确定词表
开发者首先在训练数据上运行 Tokenizer 算法(如BPE),统计词频,生成一张固定的词表。假设它包含5万个词条,每个词条被分配一个从0到49999的唯一ID。
词表(Tokenizer的词典): ID 0: "a" ID 1: "the" ID 2: "苹果" ... ID 49999: "xyz"
第二步:基于同一词表初始化嵌入层
开发者用同一个词表,在大模型中创建嵌入层。这个嵌入层本质上就是一个 50000 × 嵌入维度 的矩阵。
-
矩阵的第 0 行,专门留给 Tokenizer 里 ID 为 0 的词 "a"。
-
矩阵的第 2 行,专门留给 Tokenizer 里 ID 为 2 的词 "苹果"。
关键:这个“第N行对应ID为N的词”的对应关系,是在程序代码里写死的。 不是猜测,不是匹配,就是一个硬性的下标约定。
# 伪代码:初始化嵌入层 vocabulary_size = 50000 embedding_dim = 768 embedding_matrix = torch.randn(vocabulary_size, embedding_dim) # 现在,embedding_matrix[2] 这一行,就永久地、不可改变地与 "苹果" 这个词绑定在一起了。
至此,对齐的“骨架” 已经建立。此时,embedding_matrix[2] 里面的向量是随机的,不代表任何语义。但这不重要,因为对齐的是位置,不是数值。
4.3.2 训练中(学习语义)
对齐了索引之后,训练过程开始。模型读入海量文本,通过反向传播不断更新 embedding_matrix 里的向量数值。
-
每当模型读到“苹果”这个词,它就会去操作
embedding_matrix[2]这一行的数值。 -
训练的目标是让
embedding_matrix[2]的数值,能帮助模型更好地预测下一个词。
最终结果:经过训练,embedding_matrix[2] 这个位置上的向量,被“雕刻”成了能够代表“苹果”语义的高维向量。
核心结论:Tokenizer给出的ID之所以能正确找到对应的嵌入向量,是因为它们在物理存储上就是同一个索引位置。这是开发者预先“锁定”的对齐关系。所谓的‘匹配’,就是数组下标访问;所谓的‘正确’,是指ID值没有超出矩阵的行数范围。
4.3.3 万一 ID 错了呢?
你可能会想:“万一Tokenizer查表得到的是3324,但嵌入矩阵的第3324行是别的词怎么办?”
答案是:在现代的软件工程实践中,这不可能发生。
因为Tokenizer(通常是一个独立的库,如Hugging Face的transformers库)和大模型的权重文件,是一起打包发布的。
-
你从网上下载一个叫
"bert-base-uncased"的模型,下载的zip包里同时包含:-
config.json:定义了模型的架构。 -
pytorch_model.bin:包含嵌入矩阵等所有权重参数。 -
vocab.txt:Tokenizer用来查表的词表文件。
-
当你用代码加载模型时:
from transformers import AutoTokenizer, AutoModel
# 这两行代码,保证使用的是同一份词表文件
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
model = AutoModel.from_pretrained("bert-base-uncased")
from_pretrained 函数会找到同一个目录下的 vocab.txt,并确保模型加载的嵌入矩阵的行数与之完全匹配。这套机制从根本上杜绝了ID错配的可能。
| 类比 | 含义 |
|---|---|
| 模型 | 一栋大楼 |
| 嵌入层 | 大楼里的房间 |
| Token ID | 房间号 |
| Tokenizer | 门牌号对照表 |
Tokenizer 不是“工具”,而是模型参数的一部分。它和嵌入矩阵是不可分割的一对。谁提供了模型权重,谁就必须提供与之严格绑定的 Tokenizer。第三方 Tokenizer 产生的 ID,对于这个模型而言,是毫无意义的随机数。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)