基于 Tutorial Part 1 期间的 QA 记录、3 次提交的错误分析生成 📅 2026-04-17
3 次提交中有 2 次的核心错误都与 Python 编码类型混淆有关:
- 第 1 次:
ord(b)对 int 类型调用导致 TypeError(不知道遍历 bytes 得到的是 int) - 第 1 次:vocab/merges 构建时把 token ID(int)当字节值传给 bytes_to_unicode
- QA 中:主动问了
ord(b'\xe4')和encode/decode的关系,说明这个知识点不牢固
-
Python bytes 遍历:
bytes对象遍历时每个元素是int(0-255),不是bytesfor b in b'\xe4\xbd': # b 是 int,不是 bytes print(type(b)) # <class 'int'>
-
类型转换关系:
int ←→ bytes: bytes([228]) ↔ b'\xe4'[0] int ←→ str: chr(228) ↔ ord('ä') str ←→ bytes: 'ä'.encode('utf-8') ↔ b'\xc3\xa4'.decode('utf-8') -
latin1 vs UTF-8:latin1 是单字节编码(码点 0-255 = 字节 0-255),UTF-8 对非 ASCII 字符用多字节
- 写一个函数,实现
bytes_to_unicode和unicode_to_bytes的互转,手动验证每个步骤 - 用 Python REPL 测试:
b'\xe4'的type、遍历结果、ord和chr的输入输出类型
- 第 1 次:apply_merges 用逐条遍历所有 merges,时间复杂度过高
- 第 2 次:变量
token_chars和word混淆导致合并结果未生效 - 第 3 次:属性名
self.merges_ranksvsself.merge_ranks拼写错误
-
rank-based BPE 合并:不应该遍历所有 merge 规则,而是从当前 token 序列中找 rank 最小的 pair 合并。这是 GPT-2 的标准实现方式。
-
变量作用域意识:循环中更新变量时,确保后续迭代使用的是更新后的值,不是原始值。这是一个常见的编程陷阱。
-
命名一致性:属性名在
__init__中定义后,后续引用必须完全一致。建议定义后立即在apply_merges中写一个测试验证。
- 手动模拟 BPE 合并过程:给定
['h', 'e', 'l', 'l', 'o']和 merges[('h','e'), ('he','l')],逐步跟踪 rank-based 合并过程 - 对比自己的
apply_merges_old(逐条遍历)和apply_merges(rank-based)的输出是否一致
- special token 查找方式在第 1 次提交时未考虑 bytes_to_unicode 映射
- encode_iterable 的流式处理在第 1 次未考虑内存限制
- 代码中保留了
apply_merges_old的旧实现(建议清理)
- 接口约定仔细阅读:原作业的
vocab: dict[int, bytes]和merges: list[tuple[bytes, bytes]]类型注解已经明确说明输入格式,实现前应仔细对照 - 边界条件测试:空字符串、单字符、多字节 Unicode 字符、special token 边界情况
- 测试驱动开发:先写简单测试脚本,每改一处就跑一次,而不是改完所有问题再统一测试
| 主题 | 资源 |
|---|---|
| Python bytes/str | Python 官方文档 Unicode HOWTO |
| GPT-2 BPE 实现 | OpenAI GPT-2 encoder.py 源码 |
| tiktoken 参考实现 | tiktoken _educational.py |
基于 Tutorial Part 2 期间的调试经历和错误分析生成 📅 2026-04-19
Part 2 经历了多轮调试,主要问题集中在三个层面:
- 正则表达式缺失数字匹配:GPT-2 预分词正则缺少
| ?\p{N}+,导致数字字符被静默丢弃,训练到 merge 127 才暴露差异 - Tie-breaking 排序错误:
bytes_to_unicode映射改变了字符排序(如空格 0x20 → Ġ U+0120),直接用 Unicode 字符串做字典序 tie-breaking 与参考实现不一致 - 合并逻辑 bug:
word[i:i+2] = best[0] + best[1]时 Python 会迭代字符串拆成两个字符,需要[best[0] + best[1]]包裹为列表
-
GPT-2 完整预分词正则:
r"""'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+""" # ^^^^^^^^ 数字匹配,不能遗漏
每个分支的作用:缩写 → 字母 → 数字 → 标点 → 行末空白 → 其他空白
-
bytes_to_unicode对排序的影响:- 原始字节:
0x20(空格)<0x74('t') - 映射后:
Ġ(U+0120, 288)>'t'(U+0074, 116) - 结论:tie-breaking 必须转回原始字节再比较
- 原始字节:
-
Python 列表切片赋值:
word[i:i+2] = "ab" # 拆成 ['a','b'],2换2,未合并 word[i:i+2] = ["ab"] # 包裹为列表,2换1,正确合并
-
dict 方案 vs list 方案:
- List 方案:每个词实例单独存储 → 27758 个元素 → 慢(5.6s)
- Dict 方案:
{word_tuple: freq}去重 → 4763 个元素 → 快(0.9s) - 按频率加权计数,结果等价
- 手动追踪 BPE 训练的前 5 轮:给定文本 "the cat the dog",写出每轮的 pair 频率和合并结果
- 对比
max(stats, key=stats.get)和max(stats, key=lambda x: (stats[x], raw_bytes_key(x)))在有 tie 时的不同结果 - 理解为什么
bytes_to_unicode存在(GPT-2 为了让 tokenizer.json 可读)
- 初始实现有多处 bug(语法错误、逻辑错误),一次性修改后无法定位具体问题
- 同一 bug 反复出现(合并逻辑修复后又在不同位置复发)
-
增量验证:先确保正则匹配正确(打印 findall 结果),再确保 pair 计数正确,最后验证合并逻辑。不要一次性写完再测。
-
对比参考实现:遇到问题时直接读 tiktoken 的
_educational.py源码对比,比自己猜测 tie-breaking 规则高效得多。 -
性能意识:dict 去重是 BPE 训练最基本的空间/时间优化。在实现正确后再考虑性能优化(先正确,再高效)。
| 主题 | 资源 |
|---|---|
| BPE 算法详解 | Sennrich et al. 2016 原始论文 |
| tiktoken 教育实现 | tiktoken/_educational.py |
| GPT-2 分词器 | OpenAI GPT-2 encoder.py |
基于 Tutorial Part 4-8 期间的调试经历和错误分析生成 📅 2026-07-08
Parts 4-5 的核心错误几乎都是命名不一致:
self.weightvsself.weights(Embedding 参数名)self.d_modalvsself.d_model(拼写错误)self.qkvvsself.w_q/w_k/w_v(接口与测试不匹配)float('-inft')vsfloat('-inf')(Python 常量拼写)
-
state_dict key 由属性名决定:
self.weight = nn.Parameter(...)的 key 是"weight",self.weights = nn.Parameter(...)的 key 是"weights"。测试用state_dict检查时必须完全匹配。 -
测试驱动实现:写代码前先读测试,看测试检查什么属性名、什么输入输出格式,再决定实现。
-
IDE 的价值:拼写错误(
d_modalvsd_model)用 IDE 的类型检查或 linter 可以自动发现。
- 实现新模块时,先写
__init__和属性定义,然后立即写一个最小测试验证state_dict的 key - 养成
hasattr(obj, 'attr')检查的习惯
- softmax 多加了
eps破坏概率和 - cross_entropy 混用 PyTorch/numpy
-
softmax 不需要 eps:exp 结果恒 > 0,分母不会为 0。减去 max_val 是为了防溢出,不是防除零。
-
cross_entropy 的 log-sum-exp 技巧:
log_sum_exp = log(Σ exp(z_i - max_z)) + max_z直接
log(Σ exp(z_i))在 z_i 很大时会溢出。 -
dtype 意识:numpy 的
randint在 Windows 上默认 int32,PyTorch embedding 需要 int64。跨平台代码要显式.astype(np.int64)。
- AdamW 解耦权重衰减:先 Adam 更新,再乘
(1 - lr * wd),而非 L2 正则化 - 余弦退火:warmup 从 0 → alpha_max,cosine 衰减 → alpha_min
- 梯度裁剪:全局 L2 范数,不是逐元素裁剪
- generate 的 temperature:
logits / T,T→0 趋近 argmax,T>1 更随机
| 主题 | 资源 |
|---|---|
| AdamW 论文 | Loshchilov & Hutter 2019 |
| RoPE 论文 | Su et al. 2021 |
| SwiGLU 论文 | Shazeer 2020 |
| Pre-Norm vs Post-Norm | Xiong et al. 2020 |
| LLM 训练实践 | Karpathy - Let's build GPT |