assignment 1

低资源/降级技巧:性能分析 应使⽤如 cProfile 或 scalene 等性能分析⼯具来识别实现中的瓶颈,并重点优化这些部分。

字节对编码(BPE)分词器

2.1 Unicode standard

Problem (unicode1): Understanding Unicode (1 point)

2.2 Unicode Encodings

Unicode 与编码

解决方法:Unicode 编码

Python 示例:UTF-8 编码

python
test_string = "hello! こんにちは!"
 
# 字符串编码为 UTF-8
utf8_encoded = test_string.encode("utf-8")
print(utf8_encoded)
# b'hello! \\xe3\\x81\\x93\\xe3\\x82\\x93\\xe3\\x81\\xab\\xe3\\x81\\xa1\\xe3\\x81\\xaf!'
 
# 类型:bytes
print(type(utf8_encoded))  # <class 'bytes'>
 
# 获取底层字节值(范围 0~255)
list(utf8_encoded)
# [104, 101, 108, 108, 111, 33, 32, 227, 129, 147, 227, 130, 147, 227, 129, 171, 227, 129, 161, 227, 129, 175, 33]
 
# 字符数 vs 字节数
len(test_string)      # 13
len(utf8_encoded)     # 23
 
# 解码回 Unicode 字符串
print(utf8_encoded.decode("utf-8"))  # "hello! こんにちは!"

关键现象

意义

Problem (unicode2): Unicode Encodings

2.3 Subword Tokenization

2.4 BPE Tokenizer Training

BPE 分词器的训练过程主要分为 三步

  1. Vocabulary Initialization(词表初始化)

    • 初始词表为 所有可能的字节值(0–255,共 256 个 token)。
    • 每个字节对应一个唯一的整数 ID。
  2. 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 列表)。

  3. Compute BPE Merges(计算 BPE 合并)

    • 算法核心:
      1. 迭代统计所有相邻字节对的频率。
      2. 找到频率最高的字节对 (A, B)
      3. 将所有 (A, B) 替换为新 token "AB"
      4. "AB" 加入词表。
    • 结果:最终词表大小 = 初始 256 + BPE 合并次数
    • 注意事项
      • 不跨 pre-token 边界统计合并对。

      • 若存在频率并列,按 字典序最大 的 pair 合并:

        python
        max([("A", "B"), ("A", "C"), ("B", "ZZ"), ("BA", "A")])
        # 结果: ('BA', 'A')
         
  4. Special Tokens(特殊 token)

    • 一些字符串(如 &lt;|endoftext|&gt;)需保留为单一 token,不应被拆分。
    • 在训练时,必须把这些特殊 token 加入词表,并为其分配固定 ID。
  5. 参考实现

    • 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 库进行并行处理

关键实现:

分块原理:

python
# 每个进程处理一个文本块
for start, end in zip(boundaries[:-1], boundaries[1:]):
    f.seek(start)
    chunk = f.read(end - start).decode("utf-8", errors="ignore")
    # 对chunk进行预分词处理
 

优势:

预分词前移除特殊标记

目的: 防止跨文档边界的合并操作

处理流程:

  1. 按特殊标记分割文本块:[文档1] &lt;|endoftext|&gt; [文档2]
  2. 移除特殊标记
  3. 分别对每个文档进行预分词:[文档1][文档2]

实现方式:

python
# 使用 re.split 按特殊标记分割
pattern = "|".join(re.escape(token) for token in special_tokens)
documents = re.split(pattern, text_chunk)
 

注意事项:

优化合并步骤

问题: 朴素实现每次合并都要遍历所有字节对 解决方案: 建立计数索引,增量更新统计

优化策略:

性能提升:

限制:

实施要点

数据处理流程

  1. 数据准备: 查看和理解 TinyStories 数据集结构
  2. 文件分块: 使用 find_chunk_boundaries()&lt;|endoftext|&gt; 分块
  3. 并行预分词: 多进程处理各个文本块
  4. 特殊标记处理: 移除标记,避免跨文档合并
  5. BPE训练: 使用优化的合并算法

关键技术细节

测试验证

性能优化总结

  1. 并行化: 预分词阶段通过多进程提升速度
  2. 智能分块: 保证语义完整性的同时实现并行处理
  3. 缓存优化: 合并步骤使用增量更新减少计算开销
  4. 内存优化: 分块读取避免内存溢出

这种实现方式在保证训练质量的同时,显著提升了 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