计算机只认数字,根本不认文字

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',是因为软件最后做了一步绘制操作:

  1. 软件告诉图形系统:把Unicode/ASCII编码是 0x41 的文字,在屏幕某个位置画出来。

  2. 图形系统去查字体:它根据当前设置的字体(比如微软雅黑),找到这个字体文件里,负责绘制 0x41 这个字符的字形数据。字体里存放的其实是图像的轮廓信息。

  3. 显示器显示:图形系统根据字形数据,把像素点亮,最终你就在屏幕上看到了一个黑体的或者宋体的 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映射成一个高维向量。这个映射关系是在训练中不断学习、调整出来的。

  1. 初始化:一开始,嵌入表里的向量是随机的小数。此时,ID 3324 的向量和ID 8921 的向量毫无关系。

  2. 训练:在大模型训练的过程中(在海量文本上,根据“预测下一个词”这个任务,用反向传播算法),这个嵌入表会被不断地、缓慢地更新

  3. 结果:训练结束后,含义相近的词,它们的向量在高维空间中的位置也会彼此靠近。例如,“苹果”和“香蕉”的向量会非常相似。模型通过这种方式,把“查表”这个简单操作,变成了“查询语义”的强大工具。

大模型内部存在一个真正意义上的“映射表”,功能很像字典:

  • :词表中的某个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的组件,实时把输入文字查词表得到的。

整个过程分两步:

  1. 文字 → Token ID(通过 Tokenizer)

  2. 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 阶段一:训练前(初始化对齐)

  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包里同时包含

    1. config.json:定义了模型的架构。

    2. pytorch_model.bin:包含嵌入矩阵等所有权重参数。

    3. 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,对于这个模型而言,是毫无意义的随机数。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐