第 1 讲:课程概述与 Tokenization
1. 课程目标与结构
CS336 的核心理念是 "from scratch"——从零开始构建一个完整的语言模型。 不同于大多数 NLP 课程仅使用现成的 API 或预训练模型,本课程要求你深入理解每一个环节的工程细节, 从数据处理、Tokenizer 构建、模型架构、训练系统,到最终的评估与对齐。
课程共分为五个模块,每个模块对应一次作业:
| 模块 | 内容 | 作业 |
|---|---|---|
| Basics | Tokenizer, Transformer, Training Loop | Assignment 1 |
| Systems | Flash Attention, DDP, Kernels | Assignment 2 |
| Scaling | Scaling Laws, IsoFLOP Analysis | Assignment 3 |
| Data | Common Crawl Processing, Deduplication | Assignment 4 |
| Alignment | SFT, RLHF, GRPO | Assignment 5 |
学习建议
本课程的每次作业都建立在前一次的基础之上。建议按顺序完成,并确保每一步都能独立运行和验证。
2. 语言模型是什么
语言模型(Language Model)本质上是一个概率分布:给定一个 token 序列, 模型输出下一个 token 的概率分布。形式化地说:
这个看似简单的定义蕴含了极大的能力。通过 autoregressive generation(自回归生成), 模型可以逐步生成任意长度的文本。而通过在海量文本上训练,模型能够学到语言的语法结构、 世界知识、推理能力等。
现代大语言模型(LLM)的核心思想可以总结为:
- Pre-training:在大规模无标注文本上,通过 next token prediction 学习语言的通用表示
- Fine-tuning / Alignment:通过 SFT(Supervised Fine-Tuning)和 RLHF(Reinforcement Learning from Human Feedback)使模型符合人类偏好
3. 训练流程概述
从原始数据到可用的语言模型,完整的 pipeline 包含以下环节:
- Data Collection & Processing:从互联网(如 Common Crawl)收集文本,进行清洗、去重、过滤
- Tokenization:将文本转换为 token 序列(本讲的重点)
- Model Architecture:定义 Transformer 网络结构
- Training:在 GPU 集群上进行大规模分布式训练
- Evaluation:通过 benchmark 评估模型能力
- Alignment:通过 SFT 和 RLHF 让模型更安全、更有用
Pipeline 视角
Data → Tokenizer → Model → Training → Evaluation → Alignment。每一个环节的决策都会影响最终模型的质量。例如,Tokenizer 的词表大小直接影响模型的参数量和多语言能力。
4. Tokenization 基础
Tokenization 是将原始文本转换为模型可以处理的整数序列的过程。这看起来是个"简单"的预处理步骤, 但实际上它对模型的性能有深远影响。
4.1 为什么需要 Tokenization
神经网络不能直接处理文本字符串,它需要数值输入。Tokenizer 的作用就是建立 文本 ↔ 整数序列 之间的双向映射。这涉及两个核心操作:
- Encode:文本 → token ID 序列
- Decode:token ID 序列 → 文本
4.2 三种 Tokenization 策略
常见的 Tokenization 策略有三种,各有优劣:
| 策略 | 粒度 | 词表大小 | 优点 | 缺点 |
|---|---|---|---|---|
| Character-level | 单个字符 | ~256 | 词表极小,无 OOV 问题 | 序列过长,难以捕获语义 |
| Word-level | 完整单词 | 100K+ | 语义明确 | 词表庞大,无法处理新词(OOV) |
| Subword-level | 子词片段 | 32K~128K | 平衡词表大小和序列长度 | 需要训练算法来确定词表 |
现代 LLM 几乎全部采用 Subword Tokenization。其核心思想是: 高频词保留为完整 token(如 "the"、"is"),低频词拆分为更小的子词片段(如 "un" + "believ" + "able")。 这样既能控制词表大小,又能处理任意文本。
直觉理解
Subword tokenization 类似于信息论中的 Huffman 编码:高频模式用短码(单个 token),低频模式用长码(多个 token 拼接)。这在固定词表大小下最大化了编码效率。
5. BPE (Byte Pair Encoding) 算法详解
BPE 是目前最主流的 Subword Tokenization 算法,被 GPT 系列、LLaMA 等模型广泛采用。 它的核心思想非常简洁:反复合并出现频率最高的相邻 token pair。
5.1 训练阶段:构建词表
BPE 词表构建的完整算法如下:
- 初始化:将训练语料中的每个字符(或字节)作为初始 token,词表大小为 256(对应所有可能的字节值)
- 统计 Pair 频率:遍历语料,统计所有相邻 token pair 的出现次数
- 合并最高频 Pair:找到频率最高的 pair (A, B),将其合并为新 token "AB",加入词表
- 更新语料:将语料中所有的 (A, B) 替换为 "AB"
- 重复:回到第 2 步,直到词表大小达到目标值(如 32,000 或 128,000)
来看一个具体的例子。假设我们的训练语料只有一句话:
# Training corpus
text = "aababcab"
# Step 0: Initialize as bytes
tokens = ['a', 'a', 'b', 'a', 'b', 'c', 'a', 'b']
vocab = {'a', 'b', 'c'}
# Step 1: Count pairs
# ('a', 'b') appears 3 times
# ('a', 'a') appears 1 time
# ('b', 'a') appears 1 time
# ('b', 'c') appears 1 time
# ('c', 'a') appears 1 time
# Step 2: Merge most frequent pair ('a', 'b') -> 'ab'
tokens = ['a', 'ab', 'ab', 'c', 'ab']
vocab = {'a', 'b', 'c', 'ab'}
# Step 3: Count pairs again
# ('a', 'ab') appears 1 time
# ('ab', 'ab') appears 1 time
# ('ab', 'c') appears 1 time
# ('c', 'ab') appears 1 time
# ... continue until target vocab size is reached
5.2 关键实现细节
实际的 BPE 实现比上述简化版本复杂得多,以下是几个关键的工程细节:
Byte-level BPE
现代 BPE 实现(如 GPT-2/GPT-4 使用的)基于 字节 而非字符。 这意味着初始词表包含 256 个 byte token(0x00 到 0xFF),可以表示任意二进制数据。 UTF-8 编码的文本会被拆分为字节序列,然后在此基础上执行 BPE 合并。
这种设计的优势是:
- 无需特殊的
<UNK>token,任何输入都可以被编码 - 天然支持多语言和特殊字符
- 初始词表固定且很小(256)
Regex-based Pre-tokenization
直接在整个文本上执行 BPE 会导致不合理的合并(例如跨单词的合并)。 为了避免这个问题,GPT 系列在 BPE 之前先用正则表达式将文本切分为"词块"(chunks), BPE 合并只在每个词块内部进行。
# GPT-2 style pre-tokenization regex
import regex
pat = regex.compile(
r"""'s|'t|'re|'ve|'m|'ll|'d"""
r"""| ?\p{L}+"""
r"""| ?\p{N}+"""
r"""| ?[^\s\p{L}\p{N}]+"""
r"""|\s+(?!\S)"""
r"""|\s+"""
)
text = "Hello, world! I'm learning."
chunks = regex.findall(pat, text)
# ['Hello', ',', ' world', '!', " I", "'m", ' learning', '.']
注意这个 regex 的设计:它将标点、数字、字母、空格分别归入不同的块, 防止 BPE 将空格和字母合并为一个 token。
Special Tokens
除了从数据中学到的 token,Tokenizer 还需要一些特殊 token:
<|endoftext|>:标记文档结束<|begin_of_text|>:标记序列开始<|pad|>:填充 token(可选)
这些特殊 token 在 BPE 合并中不参与,它们有独立的固定 ID。
6. BPE 的编码与解码流程
6.1 编码(Encoding)
给定一段文本和训练好的 BPE 词表,编码流程如下:
- 用 pre-tokenization regex 将文本切分为词块
- 将每个词块转换为字节序列
- 按照训练时的合并顺序,依次应用每条合并规则
- 将最终的 token 映射为整数 ID
def encode(text: str, merges: list, vocab: dict) -> list[int]:
# Step 1: Pre-tokenize
chunks = regex.findall(PRE_TOKENIZE_PATTERN, text)
token_ids = []
for chunk in chunks:
# Step 2: Convert to bytes
tokens = list(chunk.encode('utf-8'))
# Step 3: Apply merges in order
for (a, b), new_token in merges:
i = 0
new_tokens = []
while i < len(tokens):
if i < len(tokens) - 1 and tokens[i] == a and tokens[i+1] == b:
new_tokens.append(new_token)
i += 2
else:
new_tokens.append(tokens[i])
i += 1
tokens = new_tokens
# Step 4: Map to IDs
token_ids.extend([vocab[t] for t in tokens])
return token_ids
合并顺序很重要
BPE 的合并规则必须严格按照训练时的顺序应用。不同的应用顺序可能产生不同的 tokenization 结果。这是因为前面的合并会改变后续可用的 pair。
6.2 解码(Decoding)
解码相对简单:将每个 token ID 映射回对应的字节序列,然后拼接并用 UTF-8 解码。
def decode(token_ids: list[int], vocab_reverse: dict) -> str:
# Map each ID back to bytes
byte_sequences = [vocab_reverse[id] for id in token_ids]
# Concatenate all bytes
all_bytes = b''.join(byte_sequences)
# Decode UTF-8, handling errors gracefully
return all_bytes.decode('utf-8', errors='replace')
7. Tokenizer 的实际影响
7.1 词表大小的权衡
词表大小(vocabulary size)是 Tokenizer 最重要的超参数之一。 它直接影响模型的多个方面:
| 方面 | 词表较小(如 32K) | 词表较大(如 128K) |
|---|---|---|
| Embedding 参数量 | 较少 | 较多(vocab_size × d_model) |
| 序列长度 | 较长(同样的文本需要更多 token) | 较短 |
| 计算成本 | Attention 成本更高(序列更长) | Softmax 输出层成本更高 |
| 低频 token 学习 | 每个 token 训练充分 | 部分 token 训练不足 |
实践中,GPT-2 使用 50,257 的词表,GPT-4 使用约 100,000 的词表, LLaMA 2 使用 32,000 的词表,LLaMA 3 将词表扩大到 128,000。 更大的词表通常有利于多语言支持和代码理解。
7.2 多语言 Token 效率
不同语言在同一个 Tokenizer 下的效率差异很大。以 GPT-2 的 Tokenizer 为例, 同一句话的英文版本可能只需要 10 个 token,而中文版本可能需要 30 个以上。 这是因为训练数据以英文为主,中文字符在 BPE 合并中获得的合并较少。
# Token efficiency comparison (approximate)
# English: "Hello, how are you?"
# → 5 tokens: ["Hello", ",", " how", " are", " you?"]
# Chinese: "你好,你怎么样?"
# → 可能 10+ tokens, 因为每个汉字可能被拆分为多个 byte tokens
# This means: for the same context window (e.g., 4096 tokens),
# Chinese text carries less information than English text.
Token 效率的实际影响
Token 效率不仅影响模型处理文本的能力,还直接影响 API 调用成本。如果一段中文文本需要 3 倍于英文的 token 数量,那么使用按 token 计费的 API 时,中文用户的成本就是英文用户的 3 倍。这也是为什么 LLaMA 3 大幅扩充了词表——为了提升非英语语言的 token 效率。
7.3 Tokenization 的陷阱
Tokenization 的一些已知问题:
- 数字处理:大数字可能被拆分为不自然的片段(如 "123456" → "123" + "456"),影响模型的数学能力
- 空格敏感:"Hello" 和 " Hello"(前面有空格)通常是不同的 token
- 代码缩进:不同数量的空格可能产生不同的 tokenization,影响代码生成的一致性
- Prompt Injection:某些特殊 token 序列可能触发意外行为
8. 实践:实现你自己的 BPE Tokenizer
在 Assignment 1 中,你需要从零实现一个 BPE Tokenizer。以下是一些实现建议:
8.1 数据结构选择
# Core data structures for BPE training
class BPETokenizer:
def __init__(self):
# Merge rules: ordered list of (pair, new_token)
self.merges: list[tuple[tuple[int, int], int]] = []
# Vocabulary: token_id -> bytes
self.vocab: dict[int, bytes] = {
i: bytes([i]) for i in range(256)
}
# Reverse vocab for encoding
self.token_to_id: dict[bytes, int] = {
bytes([i]): i for i in range(256)
}
def train(self, text: str, vocab_size: int):
"""Train BPE on the given text."""
# Pre-tokenize
chunks = pre_tokenize(text)
# Convert to byte sequences
sequences = [list(chunk.encode('utf-8')) for chunk in chunks]
while len(self.vocab) < vocab_size:
# Count all adjacent pairs
pair_counts = count_pairs(sequences)
if not pair_counts:
break
# Find the most frequent pair
best_pair = max(pair_counts, key=pair_counts.get)
# Create new token
new_id = len(self.vocab)
new_bytes = self.vocab[best_pair[0]] + self.vocab[best_pair[1]]
# Update data structures
self.vocab[new_id] = new_bytes
self.token_to_id[new_bytes] = new_id
self.merges.append((best_pair, new_id))
# Apply merge to all sequences
sequences = [
apply_merge(seq, best_pair, new_id)
for seq in sequences
]
8.2 性能优化提示
- Pair 计数:使用
collections.Counter或dict来快速统计 pair 频率 - 增量更新:每次合并后,不需要重新扫描整个语料。只需更新受影响的 pair 计数
- 并行化:pre-tokenization 的结果相互独立,可以并行处理
调试技巧
实现时建议先用一个非常小的语料(如 "aababcab")验证合并逻辑的正确性,然后逐步扩大到真实数据。可以参考 OpenAI 的 tiktoken 库来验证你的实现结果是否一致。
9. 本讲总结
- 语言模型的本质是 next token prediction:P(xt+1 | x1, ..., xt)
- 完整的 LLM pipeline:Data → Tokenizer → Model → Training → Evaluation → Alignment
- Subword Tokenization 是现代 LLM 的标准选择,在词表大小和序列长度之间取得平衡
- BPE 算法通过反复合并最高频的 pair 来构建词表
- Byte-level BPE + regex pre-tokenization 是当前工业界的标准实践
- 词表大小、多语言效率、数字处理是 Tokenizer 设计中需要权衡的关键因素