CS336 · 第 1 课

课程总览与 Tokenization

语言模型从字符串开始——如何把文本切分成模型可以理解的单元,是整个流程的第一步。这一课建立对全课的宏观理解,然后深入这个看似简单却影响深远的问题。

授课:Percy Liang 预计用时:80 分钟 难度:入门 → 中等

Spring 2026 官方来源:Lecture 1 trace · 课程日程

本页是中文学习导读;讲次主题、教师与材料版本以 Stanford 官方页面和讲义为准。

本讲问题:本讲先回答为什么语言模型必须把文本变成有限符号序列,再比较 character、byte、word 与 BPE 分别保留什么信息、付出什么代价。

30 秒知识地图

原始文本 → token 边界选择 → vocabulary 与 sequence length 权衡 → BPE merge 机制 → tokenizer 如何改变模型输入、计算量与跨语言表现。


这门课为什么存在?

抽象层在泄漏

八年前,研究者会自己实现和训练模型。六年前,研究者下载 BERT 做 fine-tune。今天,研究者直接调用 GPT-4/Claude/Gemini 的 API。

向上抽象提升了生产力,但语言模型的抽象层和编程语言或操作系统不同——它是泄漏的。不理解底层,就无法做真正的研究。

这门课的核心信念:理解源于构建 (understanding via building)。

不是读论文、不是调 API,而是从第一行代码开始,构建一个语言模型的每一个组件。

工业化的现实

GPT-4 据称有 1.8T 参数,训练成本约 1 亿美元。xAI 用 20 万张 H100 训练 Grok。Stargate 项目计划 4 年投资 5000 亿美元。

更关键的是:没有公开细节。GPT-4 的技术报告明确说不会披露架构、训练数据或方法。

"More is different"

前沿模型遥不可及。但课堂上训练的小模型 (<1B 参数) 能代表大模型吗?

例 1:Attention 和 MLP 的 FLOPs 占比随规模变化。小模型训得好的超参数,大模型上不一定最优。

例 2:某些能力是"涌现"的——在小模型上不存在,到一定规模才突然出现。

什么知识可以迁移?

Mechanics 机制

事物如何运作——Transformer 是什么、模型并行如何利用 GPU

Mindset 思维

榨干硬件、认真对待规模——scaling laws 的思维方式

Intuitions 直觉

哪些数据和建模决策效果好——这部分不能完全教会

这门课能教的是前两者。机制(how things work)和 mindset(efficiency-first)在任何规模都适用。直觉则需要你自己在大规模实验中积累。

The Bitter Lesson(苦涩教训):错误的解读是"规模万能,算法不重要"。正确的解读是"能随规模扩展的算法才是关键"。效率在大规模时更加重要——你承受不起浪费。

公式:accuracy = efficiency × resources


语言模型发展简史

前神经网络时代 (2010 年之前)

神经网络组件时代 (2010s)

年份突破意义
2003Bengio 神经语言模型首次用神经网络建模语言
2014Sutskever seq2seq序列到序列建模
2014Adam 优化器训练深度网络的基础设施
2015Bahdanau Attention注意力机制的诞生
2017Transformer架构范式转变

早期基础模型 (2018-2020)

拥抱规模、走向封闭 (2020-2022)

开放模型时代 (2022-至今)

GPT-J、OPT、BLOOM、Llama 系列、Mistral、Qwen、DeepSeek、OLMo——开放权重和开放数据的模型生态快速发展。

开放度的三个层次:

  1. 闭源模型(如 GPT-4o):只能 API 调用
  2. 开放权重(如 DeepSeek):权重可用,论文有架构细节,无数据细节
  3. 开源模型(如 OLMo):权重+数据可用,论文有大部分细节

全课框架:效率驱动一切

这门课围绕一个核心问题:给定固定资源,如何训练最好的模型?

资源 = 数据 + 硬件(算力、内存、通信带宽)。设计决策围绕"榨干硬件"展开:

阶段效率驱动的决策
数据处理避免在低质量数据上浪费算力
Tokenization直接用字节优雅但对当前架构计算低效
模型架构为减少内存/FLOPs 而改动(共享 KV cache、滑动窗口 attention)
训练可以只做 single epoch
Scaling Laws用小模型做超参搜索,预测大模型最优配置
Alignment更精准的调优 → 需要更小的基座模型

五大模块

Basics

Tokenizer → Transformer → Training,跑通完整流程

Systems

Kernels → Parallelism → Inference,榨干硬件

Scaling Laws

小规模实验 → 预测大规模最优配置

Data

来源 → 过滤 → 去重,数据决定模型上限

以及最后一个模块:Alignment(SFT → RLHF → DPO → GRPO),让模型真正有用。


Tokenization:核心内容

本节灵感来自 Andrej Karpathy 的 Tokenization 讲解,强烈推荐观看。

本节真正要解决的问题

神经网络只能接收有限范围内的整数索引,但人类文本是任意长、包含任意 Unicode 字符的字符串。我们要构造一个确定、可逆、词表大小可控的映射,同时又不让序列长得难以计算。

先修缺口:如果你还会把“字符”“字节”“token”“token ID”混为一谈,先不要记 BPE 算法。下面会从 UTF-8 的最小例子把四者分开。

什么是 Tokenization?

原始文本通常表示为 Unicode 字符串。语言模型对 token 序列(通常用整数索引表示)放置概率分布。

所以我们需要两个过程:

词表大小 (vocabulary size) 是可能的 token 数量。

先把对象放进正确抽屉

Unicode 文本
  ↓ UTF-8 编码
bytes(唯一的初始单位)
  ↓ 按已有 merge rule 逐步合并
current segmentation(这一次输入当前怎样切)
  ↓ 查 vocabulary
token IDs(送进模型的整数)

vocabulary: token → token_id
merge table: adjacent pair → merge rank

vocabulary ≠ current segmentation
token ID ≠ merge rank

后面每一步都只改变其中某些对象。只要能持续回答“现在改的是哪张表、哪条序列”,BPE 就不会再显得神秘。

关键指标:压缩比 (compression ratio)

compression_ratio = num_bytes / num_tokens

即每个 token 对应多少字节。压缩比越高,同样信息用更少的 token 表示,模型处理效率越高。


方案一:Character Tokenization(字符级)

Unicode 字符串是字符序列。每个字符可通过 ord() 转为码点(整数):

ord("a")  = 97
ord("🌍") = 127757
ord("你") = 20320

问题:

若统一使用前面定义的 bytes/token,字符级 tokenizer 的数值取决于 UTF-8:ASCII 字符约为 1 byte/token,而常见中文字符通常约为 3 bytes/token。不能把“1 字符/token”与 bytes/token 当成同一个单位;这里真正的问题是 token 数仍大致随字符数增长。


方案二:Byte Tokenization(字节级)

Prerequisite refresher:Unicode character 是抽象字符;UTF-8 是把字符写成 bytes 的一种编码规则;byte 是 8 个 bit,因此恰有 (2^8=256) 个可能值,即 0 到 255。两者不是同一个对象。

UTF-8 为每个合法 Unicode 字符串规定了可逆的字节表示,所以“先 UTF-8、再按 byte 处理”覆盖任意语言,不需要为中文、emoji 或未来出现的罕见字符准备 UNK:

"a"  → b"a"              # 1 字节
"🌍"  → b"\xf0\x9f\x8c\x8d"  # 4 字节
"你"  → b"\xe4\xbd\xa0"      # 3 字节

优势:初始词表极小且固定——只有 256 个可能的 byte。注意,“你”是一个 Unicode character,却是三个 byte;byte-level BPE 的初始序列是三个单位,不是一个“中文字 token”。

劣势:压缩比 = 1 字节/token,压缩效果极差。序列会非常长。

为什么序列长度重要? Transformer 的 attention 是 O(n²) 复杂度。序列越长,计算和内存开销越大。上下文长度受限于序列长度,所以极低的压缩比意味着模型能"看到"的有效内容更少。

Exact identityUTF-8 编码后,每个单位一定是 0–255 中的一个值;把 bytes 原样拼回并按 UTF-8 解码,可以恢复原文本。

Asymptotic complexity标准 dense self-attention 对 token 数 (n) 的时间与 attention-score 内存是 (O(n^2))。这不等于整个 Transformer 的所有计算都严格等于 (n^2),但说明过长的 byte 序列代价很高。


方案三:Word Tokenization(词级)

经典 NLP 的做法:把文本按词分割。

import regex
pattern = r"\w+|."
segments = regex.findall(pattern, "I'll say supercalifragilisticexpialidocious!")
# → ["I", "'", "ll", " ", "say", " ", "supercalifragilisticexpialidocious", "!"]

更精细的版本(GPT-2 使用的预分词正则):

GPT2_TOKENIZER_REGEX = r"""'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""

问题:

词级 tokenization 往往能得到更高的 bytes/token,但具体数值取决于语言、UTF-8 byte 长度和语料中的词长分布;不能把“约 4 字符/token”直接塞进 bytes/token 口径。它更根本的问题仍是开放词表、UNK 与巨大 vocabulary。


方案四:Byte Pair Encoding (BPE)

BPE 最初由 Philip Gage(1994)用于数据压缩,Sennrich et al.(2016)把它引入 subword NLP。它试图同时保留 byte 的完备性与较长 token 的压缩效率:从唯一的 byte 分割出发,只学习数据中反复出现的相邻组合。

五个对象:不要急着合并

对象它回答的问题示例
bytes不可再假设更小的初始单位是什么?97, 98, 99
vocabulary哪些 token 存在、各自 ID 是什么?ab → 256
current segmentation当前 corpus/input 此刻怎样切?[ab][c]
merge table哪对相邻 token 可以合并、优先级是什么?(a,b) → rank 0
token IDs最终 token 用什么整数交给模型?[256, 99]

Vocabulary 与 current segmentation 为什么不相等

初始 corpus segmentation:
[a][b][c]
[a][b][c]
[a][b][d]

初始 vocabulary 至少有:
a, b, c, d

执行 a + b → ab 后,corpus segmentation:
[ab][c]
[ab][c]
[ab][d]

新的 vocabulary:
a, b, c, d, ab

合并改写的是训练 corpus 的当前切分,并把 ab 加进词表;它没有删除 a 或 b。未来输入里如果没有合适的 merge,仍可能产生单独的 a、b。

算法一:Training 学什么

训练输入不是“一个大字符串直接开合并”。常见实现先做 pre-tokenization,确定不允许跨越的边界,再把每个 piece 变成 UTF-8 bytes。然后每一轮都在当前 segmentation上重新计数。

raw text
  ↓ pre-tokenization(例如不跨特殊 token 或某些空白边界)
UTF-8 bytes
  ↓ 每个 byte 作为初始 token
current corpus segmentation
  ↓ 重复:计数 → 选 pair → 新 token → 记录 rank → 改 segmentation
vocabulary + ordered merge table

两轮训练,手算 frequency 与 rank

为便于看清对象,用字母代替相应单字节 token:

初始 segmentation:
a b c
a b c
a b d

Round 1 pair frequency:
(a,b): 3
(b,c): 2
(b,d): 1

选择 a+b → ab
vocabulary 新增 ab
merge table 记录 rank 0: (a,b) → ab

更新后的 segmentation:
ab c
ab c
ab d

Round 2 pair frequency:
(ab,c): 2
(ab,d): 1

选择 ab+c → abc
vocabulary 新增 abc
merge table 记录 rank 1: (ab,c) → abc

rank 不是凭空定义的。它就是规则在训练中被学到的次序:rank 0 最早,rank 1 次之。若最高频率并列,训练 specification 必须规定 deterministic tie-break(例如比较 pair 的 byte 序);否则同一 corpus 在不同运行中可能学到不同 tokenizer。

Training 会做的事:统计 frequency;创建新 token;扩展 vocabulary;修改训练 corpus segmentation;按学习次序产生 merge rank。

训练停止条件:达到目标词表大小、达到 merge 次数,或已没有可合并 pair。若有 256 个基础 byte 且没有额外 special tokens,做 \(M\) 次有效 merge 后词表大小是 \(256+M\)。这是 Exact identity,不是经验近似。

算法二:Encoding 只执行已有规则

现在假设训练已经结束,得到:

rank 0: b + c  → bc
rank 1: a + b  → ab
rank 2: a + bc → abc

编码输入 abc。先转 UTF-8 bytes,初始 segmentation 唯一为 [a][b][c]。此时两对都能合并:

(a,b) 有规则,rank 1
(b,c) 有规则,rank 0

选当前可执行规则中 rank 最小者:
[a][b][c] → [a][bc]

重新查看新的相邻 pair:
(a,bc) 有规则,rank 2
[a][bc] → [abc]

重点不是“先把 rank 0 的规则在整段文字上扫完,再扫 rank 1”,而是:每一步查看当前 segmentation 中所有可执行 merge,选择 rank 最小的一项,然后更新邻接关系。高效实现会用 priority queue,但语义必须与这个过程相同。

Encoding 不会做的事:不重新统计输入 frequency;不创建 token;不改变 vocabulary;不产生新 rank。它只消费训练阶段冻结的 vocabulary 与 merge table。

Training · 学规则

  • 输入:训练 corpus 的当前 segmentation
  • 决策依据:adjacent pair frequency
  • 会新增:token、vocabulary entry、merge rule / rank
  • 会改写:训练 corpus segmentation

Encoding · 用规则

  • 输入:新文本的唯一 byte segmentation
  • 决策依据:当前可执行规则的最小 rank
  • 不会新增:token、vocabulary entry 或 rank
  • 只改写:这一次输入的 current segmentation

为什么 encoding 必须从 bytes 开始

假设 vocabulary 同时有 a, b, c, ab, bc, abc。若允许“从任意已有 token 开始”,同一输入一开始就有四种合法状态:

[a][b][c]    [ab][c]    [a][bc]    [abc]

算法还没执行就已经不确定。byte 序列则由 UTF-8 唯一决定:

唯一初始 bytes + 冻结的 merge ranks + deterministic tie handling
→ 唯一最终 segmentation

所以“从 bytes 开始”不只是因为 byte 小,而是为了给编码算法一个规范、唯一的初始状态。

为什么 BPE 不是 longest-match

Vocabulary 只声明 token 存在,不声明该字符串应该怎样切。下面的反例让两种算法产生不同结果:

vocabulary:
a, b, c, d, ab, abc, bc, bcd

merge rules:
rank 0: b+c  → bc
rank 1: bc+d → bcd
rank 2: a+b  → ab
rank 3: ab+c → abc

输入 abcd,BPE:
[a][b][c][d]
→ [a][bc][d]       # rank 0
→ [a][bcd]         # rank 1

longest-match 可能得到:
[abc][d]

两者都只使用 vocabulary 中存在的 token,却得到不同 segmentation。因此 merge ranks 定义 BPE 的切分;“词表中有最长 token”不是选择规则。

Token ID 与 merge rank 是两张表

vocabulary:
abc → token_id 261       # 模型 embedding lookup 使用

merge table:
(ab,c) → merge rank 1    # tokenizer 决定合并顺序使用

token ID 可以按实现任意分配;merge rank 必须保留训练顺序。某些实现恰好让后加 token 的 ID 与 merge 顺序相关,这只是编号约定,不是概念同一。

Decoding 为什么更简单

给定 token IDs,先在 vocabulary 中查到每个 token 对应的 byte string,按顺序拼接,再做 UTF-8 decode。Decoding 不需要 merge table,因为切分已经由 ID 边界给出。单个 token 的 bytes 甚至可能不是独立合法的 UTF-8;只要拼接后的完整 byte stream 合法即可。

BPE 与 Huffman:都有 frequency,但优化对象不同

BPEHuffman
贪心信号最高频的相邻 pair最低频的两个 node
每步产物更长的 token 与一条 merge ruleprefix-code tree 的父节点
最终目标学习 subword vocabulary 与确定切分给符号分配无前缀歧义的变长 bit code

共同点只是“由 frequency 驱动的 greedy construction”;不能据此认为 BPE 也在构造 prefix code,或 Huffman 也在合并文本中的相邻片段。

常见错误理解与反例

1. “merge 后旧 token 消失。”错。旧 token 留在 vocabulary;改变的是当前 segmentation。

2. “encoding 会统计当前输入的 pair frequency。”错。即使输入里 (a,b) 出现一万次,也不能越过 rank 更小的可执行规则。

3. “merge rank 就是 token ID。”错。一个映射 pair 的优先级,一个索引 token。

4. “vocabulary 越大,sequence 越长。”通常相反:更多可用 merge 往往提高压缩率、缩短序列;但 embedding/output matrix 也随 vocabulary 变大,且具体语料上的长度不保证单调改善。

5. “只要 vocab 有长 token 就直接用。”longest-match 反例已经证明错误;必须由 merge ranks 走到它。

6. “BPE 就是 Huffman。”两者的贪心方向、对象与输出结构都不同。

GPT-2 的改进

原始的 BPE 直接在字节上运行。GPT-2 做了两个关键改动:


Tokenization 的权衡

方法词表大小压缩比OOV 问题无损
Character~150K~1.0无是
Byte256~1.0无是
Word无限~4.0有 (UNK)否
BPE可控~3-4.0无 (fallback to bytes)是

BPE 是必要的恶

原版 Transformer 论文用的是 BPE。今天的主流模型(GPT 系列、Llama、Mistral)都用 BPE 或其变体。

但 BPE 也有问题:

未来方向:Tokenizer-free

直接用字节作为输入(ByT5、MEGABYTE、BLT、TFree),让模型自己学组合。理论上更优雅,但目前尚未扩展到前沿规模。这是一个开放问题。


知识检查

问题 1:UTF-8 编码中,一个中文字符"你"占多少字节?

Concept Check · 按 rank 编码 a b c

规则为 rank 0: b+c→bc,rank 1: a+b→ab,rank 2: a+bc→abc。最终 segmentation 是什么?

答案:[abc]。初始虽然 (a,b) 和 (b,c) 都可合并,但先选 rank 0 得到 [a][bc],再用 rank 2。

如果你先得到 [ab][c],你把“从左到右”当成了优先级;如果你重新数 pair 次数,你把 training 与 encoding 混在了一起。

Concept Check · merge 以后哪些对象改变?

训练时执行 a+b→ab。在 vocabulary、current segmentation、merge table 三者中,哪些改变?a、b 是否被删除?

答案:三者都改变:vocabulary 新增 ab;当前 corpus 中匹配位置变成 [ab];merge table 新增一条带 rank 的规则。a、b 不删除。

若你认为旧 token 被删除,说明你把“当前怎么切”误当成了“系统允许哪些 token 存在”。

Concept Check · 新输入出现一个从未见过的 emoji 会怎样?

答案:只要输入是合法 UTF-8,它总能退回对应的 1–4 个基础 byte tokens;已有 merge 可能进一步合并其中某些相邻 bytes,但编码不会创建新 token。

若你回答 UNK,说明你仍在用 word-level vocabulary 的 mental model 理解 byte-level BPE。

问题 2:为什么 byte-level tokenization 的压缩比差是个问题?

问题 3:如果 BPE 训练做了 100 次合并,最终词表大小是多少?


你现在应该能够解释

通往第 2 课:tokenizer 已经开始决定资源账本

Tokenizer 把同一段文本变成 \(D\) 个 training tokens;序列更长会改变 attention 工作量与 activation memory。下一课会把 token 数、参数量、dtype、FLOPs 和显存逐项换成可以手算的资源账本。


如果只能记住 5 件事

  1. Bytes 是唯一、完备的起点;token 是可包含多个 bytes 的词表项;token ID 只是索引。
  2. Vocabulary 说明“什么 token 存在”,current segmentation 说明“这段输入此刻怎样切”。
  3. Training 按当前 corpus 的 frequency 学规则;encoding 不计频率,只选当前可执行规则中 rank 最小者。
  4. Merge rank 决定切分过程,所以 BPE 不是 longest-match;token ID 与 rank 属于不同映射。
  5. 更大 vocabulary 往往缩短序列,却增大 embedding/output 层;tokenizer 是模型质量与资源成本的共同设计变量。

延伸阅读

Reference · 延伸阅读与一手资料