CS336 Lecture Notes 1
本文为本人学习相关开源课程过程中,整理的个人学习笔记及作业解答,核心目的仅用于记录个人学习轨迹、巩固所学知识、梳理学习思路,全程为个人自主学习使用,不具备任何商业用途,也不构成任何形式的课程辅导或标准答案参考。
需特别说明的是,由于本人学习进度及知识储备有限,笔记内容及作业解答中可能存在大量纰漏、思路偏差甚至错误,仅代表本人当时的学习理解,不具备权威性和准确性。
在此郑重提醒:请勿将本文中的任何作业解答复制粘贴,作为自身所修课程的提交答案。任何因抄袭本文内容导致的课程成绩问题、学术诚信问题,均由抄袭者自行承担全部责任,本人不承担任何相关连带责任。
同时,本文所分享的内容均基于开源课程的公开内容整理,尊重原课程创作者的知识产权,若涉及相关内容的版权问题,请及时联系本人,本人将第一时间进行调整或删除。
感谢各位读者的理解与支持,也欢迎大家针对笔记及解答中的问题提出宝贵建议,共同交流学习、共同进步。
- 课程网站: https://cs336.stanford.edu/
- Lec01 资料: lecture_01.py
Tokenization
为什么需要 Tokenization?
原始文本通常以 Unicode 字符串表示,但语言模型需要处理的是 token 序列(通常用整数索引表示)。因此,我们需要:
- 编码 (Encode):将字符串转换为 token 索引序列
- 解码 (Decode):将 token 索引序列转换回字符串
# 示例string = "Hello, 🌍! 你好!"indices = tokenizer.encode(string) # [15496, 11, 995, 0]reconstructed_string = tokenizer.decode(indices)Tokenizer 的核心功能:实现 encode() 和 decode() 方法,确保能够往返转换。
关键观察(通过 GPT-5 Tokenizer)
通过 tiktoken 交互式工具可以观察到:
- 空格处理:单词及其前导空格通常属于同一个 token(如
" world") - 位置敏感性:同一单词在开头和中间的表示不同(如
"hello hello") - 数字处理:数字被切分成每几位一个 token
压缩比 (Compression Ratio)
compression_ratio = get_compression_ratio(string, indices)- 含义:每个 token 对应的平均字节数
- 意义:压缩比越高,序列越短(注意力机制复杂度与序列长度平方成正比)
- 权衡:增加词汇表大小可以提高压缩比,但会导致稀疏性问题
字符级 Tokenizer (Character Tokenizer)
原理
Unicode 字符串是 Unicode 字符的序列,每个字符可通过 ord() 转换为码点(整数)。
assert ord("a") == 97assert ord("🌍") == 127757assert chr(97) == "a"优缺点分析
| 优点 | 缺点 |
|---|---|
| 实现简单 | 词汇表巨大(约 150K Unicode 字符) |
| 可处理任意文本 | 字符分布不均(如 🌍 很罕见) |
| 压缩比低 |
问题:词汇表大 + 压缩比低 = 最差组合
字节级 Tokenizer (Byte Tokenizer)
原理
Unicode 字符串可以表示为字节序列(UTF-8 编码),每个字节是 0-255 的整数。
# 单字节字符assert bytes("a", encoding="utf-8") == b"a"
# 多字节字符assert bytes("🌍", encoding="utf-8") == b"\xf0\x9f\x8c\x8d"优缺点分析
| 优点 | 缺点 |
|---|---|
| 词汇表小(256) | 压缩比 = 1(最差) |
| 可处理任意文本 | 序列过长 |
| 受 Transformer 上下文长度限制 |
核心问题:序列太长,导致注意力计算开销过大
词级 Tokenizer (Word Tokenizer)
原理
使用正则表达式将文本切分为单词。
import regexstring = "I'll say supercalifragilisticexpialidocious!"chunks = regex.findall(r"\w+|.", string) # 分词优缺点分析
| 优点 | 缺点 |
|---|---|
| 每个 token 有语义 | 词汇表可能很大 |
| 压缩比较高 | 稀有词难以学习 |
| 无法固定词汇表大小 | |
| 未登录词需要 UNK token |
BPE (Byte Pair Encoding) Tokenizer
历史背景
核心思想
在原始文本上训练 tokenizer,构建针对数据的定制词汇表:
- 常见字节序列 → 单个 token
- 罕见字节序列 → 多个 token
训练算法
def train_bpe(string: str, num_merges: int) -> BPETokenizerParams: # 1. 从字节列表开始 indices = list(map(int, string.encode("utf-8")))
# 2. 初始化合并规则和词汇表 merges: dict[tuple[int, int], int] = {} # (idx1, idx2) -> merged_idx vocab: dict[int, bytes] = {x: bytes([x]) for x in range(256)}
for i in range(num_merges): # 3. 统计相邻 token 对的出现频率 counts = count_adjacent_pairs(indices)
# 4. 找到最常见的相邻对 pair = max(counts, key=counts.get)
# 5. 合并该对 new_index = 256 + i merges[pair] = new_index vocab[new_index] = vocab[pair[0]] + vocab[pair[1]]
# 6. 更新序列 indices = merge(indices, pair, new_index)
return BPETokenizerParams(vocab=vocab, merges=merges)统计相邻对的辅助函数
def count_adjacent_pairs(indices: list[int]) -> dict[tuple[int, int], int]: """返回每个相邻 token 对的出现次数""" counts = defaultdict(int) for index1, index2 in zip(indices, indices[1:]): counts[(index1, index2)] += 1 return countsTokenizer 方法对比
| 方法 | 词汇表大小 | 压缩比 | 优点 | 缺点 |
|---|---|---|---|---|
| Character | ~150K | 低 | 简单、通用 | 词汇表大、序列长 |
| Byte | 256 | 1 (最差) | 词汇表小、通用 | 序列太长 |
| Word | 很大 | 高 | 有语义 | 稀有词问题、UNK |
| BPE | 可控 | 较高 | 平衡、自适应 | 需要训练 |
Assignment 1 扩展方向
基础实现之外,作业要求:
- 优化编码效率:只遍历相关的合并规则,而非全部
- 特殊 token 处理:检测并保留如
<|endoftext|>等特殊 token - 预分词 (Pre-tokenization):使用 GPT-2 的正则表达式预分词
- 性能优化:尽可能提升实现速度
关键要点总结
-
Tokenization 是连接文本和模型的桥梁
- 编码:文本 → token 索引
- 解码:token 索引 → 文本
-
设计权衡
- 词汇表大小 vs. 压缩比
- 通用性 vs. 效率
- 语义完整性 vs. 稀疏性
-
BPE 是现代 LLM 的主流选择
- 从字节开始,自底向上构建
- 自动适应数据分布
- 平衡词汇表大小和压缩效率
-
压缩比的重要性
- 序列长度影响注意力计算复杂度(O(n²))
- 更高的压缩比 → 更短的序列 → 更快的训练/推理
参考资料
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!