assignment 1
低资源/降级技巧:性能分析
应使⽤如 cProfile 或 scalene 等性能分析⼯具来识别实现中的瓶颈,并重点优化这些部分。
字节对编码(BPE)分词器
2.1 Unicode standard
Problem (unicode1): Understanding Unicode (1 point)
2.2 Unicode Encodings
Unicode 与编码
- Unicode 标准定义了 字符到码点(整数)的映射。
- 直接用 Unicode 码点训练 tokenizer 不现实:
解决方法:Unicode 编码
- 将 Unicode 字符转换为 字节序列。
- 标准三种编码:
- UTF-8(互联网主流,占比 >98%)
- UTF-16
- UTF-32
Python 示例:UTF-8 编码
python
test_string = "hello! こんにちは!"
utf8_encoded = test_string.encode("utf-8")
print(utf8_encoded)
print(type(utf8_encoded))
list(utf8_encoded)
len(test_string)
len(utf8_encoded)
print(utf8_encoded.decode("utf-8"))
关键现象
bytes 类型可通过 list() 查看每个字节的整数值(0–255)。
- 一个 Unicode 字符 ≠ 一个字节(UTF-8 可变长编码)。
- 示例:
len("hello! こんにちは!") = 13 (13 个字符)
len(utf8_encoded) = 23 (23 个字节)
意义
- 将 Unicode 码点序列(0–154,997) 转换为 字节序列(0–255)。
- 字节词表大小仅 256,更易处理。
- 字节级分词优势:
- 不会有 OOV(out-of-vocabulary)问题。
- 任意文本都能转化为字节序列。
Problem (unicode2): Unicode Encodings
2.3 Subword Tokenization
- 背景
- 词级分词器:词汇外(OOV)问题严重,但序列较短(例如 10 个词 = 10 个 token)。
- 字节级分词器:没有 OOV 问题,但序列过长(10 个词可能变成 50+ 个 token),训练更慢,并引入更长的依赖关系。
- 子词分词器(Subword Tokenizer)
- 介于词级和字节级之间,平衡 OOV 问题和序列长度。
- 字节级分词器词表只有 256 个 token(所有字节)。
- 子词分词器通过增加词表规模,压缩输入字节序列。
- 例如:字节序列
b'the' 出现频繁 → 在词表中加入 'the' → 原本 3 个 token 变成 1 个 token。
- BPE(Byte-Pair Encoding)
- 一种压缩算法(Gage, 1994;Sennrich et al., 2016 提出用于 NLP)。
- 迭代合并训练语料中最频繁的字节对,生成新的 token。
- 如果一个词出现足够多次,它就会被整体作为单个子词单元。
- 结果
- BPE 分词器(BPE Tokenizer) = 字节或合并后的字节序列。
- 既能避免 OOV 问题,又能保持合理的输入长度。
- 词表的构建过程称为 训练分词器。
2.4 BPE Tokenizer Training
BPE 分词器的训练过程主要分为 三步:
-
Vocabulary Initialization(词表初始化)
- 初始词表为 所有可能的字节值(0–255,共 256 个 token)。
- 每个字节对应一个唯一的整数 ID。
-
Pre-tokenization(预分词)
- 目的:避免逐字节扫描语料库的高计算开销,并减少语义相近但带标点的词被割裂(如 dog! vs dog.)。
- 方法:
- 将语料进行粗粒度分词(pre-token)。
- 每个 pre-token 转换为 UTF-8 字节序列。
- 统计字节对的频率时,按 pre-token 出现次数加权(如
'text' 出现 10 次,则 't' 与 'e' 的相邻统计 +10)。
- 实现方式:
-
Sennrich et al. (2016):用空格分词 s.split(" ")。
-
GPT-2 / tiktoken:使用 正则表达式预分词器:
python
PAT = r"""'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""
-
推荐用 re.finditer 来遍历预分词结果(避免存储大规模 token 列表)。
-
Compute BPE Merges(计算 BPE 合并)
- 算法核心:
- 迭代统计所有相邻字节对的频率。
- 找到频率最高的字节对
(A, B)。
- 将所有
(A, B) 替换为新 token "AB"。
- 将
"AB" 加入词表。
- 结果:最终词表大小 = 初始 256 + BPE 合并次数。
- 注意事项:
-
不跨 pre-token 边界统计合并对。
-
若存在频率并列,按 字典序最大 的 pair 合并:
python
max([("A", "B"), ("A", "C"), ("B", "ZZ"), ("BA", "A")])
-
Special Tokens(特殊 token)
- 一些字符串(如
<|endoftext|>)需保留为单一 token,不应被拆分。
- 在训练时,必须把这些特殊 token 加入词表,并为其分配固定 ID。
-
参考实现
- Sennrich et al. (2016) 的 Algorithm 1:给出了一版低效的 BPE 实现,可作为练习帮助理解上述过程。
👉 总结一句话:
BPE 分词器训练 = 初始化 256 字节词表 → 预分词(统计频率)→ 迭代字节对合并 → 加入特殊 token,最终得到既紧凑又可扩展的词表。
Example (bpe_example): BPE training example
2.5 Experimenting with BPE Tokenizer Training
在 TinyStories 数据集上训练一个字节级 BPE(Byte Pair Encoding)分词器,并通过多进程并行化来优化性能。
并行化预分词处理
问题: 预分词是主要性能瓶颈
解决方案: 使用 multiprocessing 库进行并行处理
关键实现:
- 将语料库按特殊标记
<|endoftext|> 分块
- 确保分块边界始终在特殊标记开头,避免跨文档合并
- 使用提供的
find_chunk_boundaries() 函数获取分块边界
分块原理:
python
for start, end in zip(boundaries[:-1], boundaries[1:]):
f.seek(start)
chunk = f.read(end - start).decode("utf-8", errors="ignore")
优势:
- 语义完整性:不会在文档中间切断
- 独立处理:各块可以并行处理
- 内存效率:每次只处理部分数据
预分词前移除特殊标记
目的: 防止跨文档边界的合并操作
处理流程:
- 按特殊标记分割文本块:
[文档1] <|endoftext|> [文档2]
- 移除特殊标记
- 分别对每个文档进行预分词:
[文档1] 和 [文档2]
实现方式:
python
pattern = "|".join(re.escape(token) for token in special_tokens)
documents = re.split(pattern, text_chunk)
注意事项:
- 使用
re.escape() 处理特殊字符(如 |)
- 避免跨文档的字节对合并
优化合并步骤
问题: 朴素实现每次合并都要遍历所有字节对
解决方案: 建立计数索引,增量更新统计
优化策略:
- 建立索引: 创建所有字节对的计数缓存
- 增量更新: 只更新与已合并字节对重叠的配对统计
- 避免全遍历: 不再显式遍历每个字节对进行频率统计
性能提升:
限制:
- 合并环节在 Python 中无法并行化
- 但缓存机制仍能带来可观加速
实施要点
数据处理流程
- 数据准备: 查看和理解 TinyStories 数据集结构
- 文件分块: 使用
find_chunk_boundaries() 按 <|endoftext|> 分块
- 并行预分词: 多进程处理各个文本块
- 特殊标记处理: 移除标记,避免跨文档合并
- BPE训练: 使用优化的合并算法
关键技术细节
- 分块边界: 确保在
<|endoftext|> 处分割
- 内存管理: 使用 4KB 小块逐步读取
- 编码处理: 使用
decode("utf-8", errors="ignore") 处理编码问题
- 去重排序:
sorted(set(chunk_boundaries)) 确保边界唯一性
测试验证
- 使用
test_train_bpe_special_tokens 测试用例验证特殊标记处理功能
- 确保分块和合并逻辑的正确性
性能优化总结
- 并行化: 预分词阶段通过多进程提升速度
- 智能分块: 保证语义完整性的同时实现并行处理
- 缓存优化: 合并步骤使用增量更新减少计算开销
- 内存优化: 分块读取避免内存溢出
这种实现方式在保证训练质量的同时,显著提升了 BPE 分词器的训练效率。
Problem (train_bpe): BPE Tokenizer Training
Problem (train_bpe_tinystories): BPE Training on TinyStories
Build and Train GPT-4 Tokenizer from scratch
Implementing A Byte Pair Encoding (BPE) Tokenizer From Scratch
How to Train BPE, WordPiece, and Unigram Tokenizers from Scratch using Hugging Face
NLP From Scratch— Part 1: BPE Tokenization
https://github.com/karpathy/minbpe
https://github.com/JohannesVod/QuickBPE
https://github.com/openai/tiktoken
https://github.com/glample/fastBPE
https://github.com/bheinzerling/bpemb