这是25年春CS336的课堂笔记和作业,课程网站为Stanford CS336 | Language Modeling from Scratch,课程视频可在哔哩哔哩上观看:斯坦福CS336:大模型从0到1

此课程内容涵盖分词、模型架构、系统优化、数据处理和模型对齐等方面,通过从零开始构建语言模型,深入理解NLP和AI的核心技术。

我的作业备份仓库:zlh123123/CS336_spring2025: CS336的作业与课程笔记

Tokenization

什么是分词(Tokenization)

分词是将**字符串(文本)转换为令牌(tokens,通常是整数索引)**的过程,以便语言模型处理。反过来,也需要将令牌解码回字符串。分词器(Tokenizer)需要实现以下两个方法:

  • encode:将字符串编码为整数序列(tokens)。
  • decode:将整数序列解码回字符串。
1
2
3
string = "Hello, 🌍! 你好!"

indices = [15496, 11, 995, 0]

分词评估指标

  • 词汇表大小(Vocabulary Size)

    指分词器能够生成的所有可能 token 的数量,通常表示为整数索引的范围。

  • 压缩率(Compression Ratio)

    压缩率衡量每个 token 平均代表的字节数,计算公式为:

    Compression Ratio=num_bytesnum_tokens\text{Compression Ratio} = \frac{\text{num\_bytes}}{\text{num\_tokens}}

    其中num_bytes表示输入字符串的 UTF-8 编码字节数;num_tokens表示分词后生成的 token 数量。

  • 序列长度(Sequence Length)

    分词后生成的 token 序列的长度,即 len(indices)。它直接影响语言模型的计算成本和性能。

分词策略

基于字符的分词(Character-based Tokenization)

顾名思义就是将每个 Unicode 字符转换为对应的代码点(整数),例如:

1
"Hello, 🌍!" → [72, 101, 108, 108, 111, 44, 32, 127757, 33]

这种分词方法简单直接,适用于任何 Unicode 文本;同时解码和编码过程明确且可逆。

但是:

  • 词汇表过大:Unicode 字符约有 150,000 个,导致词汇表巨大,模型学习效率低。
  • 稀有字符问题:如 🌍 等字符使用频率低,浪费词汇表空间。
  • 压缩率低:每个字符一个 token,序列长度长,影响模型效率(尤其是 Transformer 的上下文长度受限)。

基于字节的分词(Byte-based Tokenization)

将字符串编码为 UTF-8 字节序列,每个字节是一个整数(0-255)。例如:

1
2
3
4
assert bytes("a", encoding="utf-8") == b"a"

# UTF-8 编码可能包含多个字节,尤其是对于非 ASCII 字符(如 🌍)
assert bytes("🌍", encoding="utf-8") == b"\xf0\x9f\x8c\x8d"

这种分词方式显然词汇表很小,毕竟只有256种可能。但是压缩率差,因为一个字符可能对应多个byte,压缩率几乎为1,导致 token 序列过长(注意力机制的二次方复杂度,token过长效率下降)。

基于单词的分词(Word-based Tokenization)

使用正则表达式(如 \w+|.)将文本分割为单词或标点。例如:

1
"I'll say supercalifragilisticexpialidocious!" → ["I'll", "say", "supercalifragilisticexpialidocious", "!"]

这种分词更符合人类语言的语义单位(单词),压缩率也能比较高,它面临的问题就是:

  • 词汇表过大:单词数量可能非常多(尤其是罕见单词)。
  • 稀有单词问题:新词或未见过的词需要用特殊 token(如 <UNK>)表示,影响模型性能。
  • 词汇表大小不固定:依赖训练数据,难以控制。

字节对编码(Byte Pair Encoding, BPE)

字节对编码(Byte Pair Encoding, BPE)是一种数据压缩算法,最初用于压缩文本,后来被适配到NLP中,BPE 的核心思想是:

  • 字节级别开始,将最常见的**相邻字节对(byte pairs)**合并为新的 token,逐步构建一个词汇表。
  • 通过合并规则,将常见的字符序列表示为单个 token,减少 token 序列长度,提高压缩率,同时保持适中的词汇表大小。

BPE 的核心是通过统计语料中的字节对频率,迭代合并最常见的字节对,生成新的 token。它分为两个阶段:

  1. 训练阶段:根据语料统计字节对频率,生成词汇表(vocab)和合并规则(merges)。
  2. 编码/解码阶段:使用训练好的词汇表和合并规则,将输入字符串编码为 token 序列,或将 token 序列解码回字符串。

训练过程是这样的:假设输入为the cat in the hat

  • Step 1:把文字拆成最小的字节

    将输入的文字转成 UTF-8 字节(每个字节是个 0-255 的数字),例如the cat in the hat被转换为:

    1
    [116, 104, 101, 32, 99, 97, 116, 32, 105, 110, 32, 116, 104, 101, 32, 104, 97, 116]
  • Step 2:找最常见的组合

    BPE 会数一数,哪些两个字节老是挨着出现。比如,“t”和“h”(116, 104)出现了两次(“the”和“the”里各一次)。因此它决定把“t”和“h”粘在一起,变成一个新块,编号是 256(因为 0-255 已经被字节用完了),然后把所有“t h”替换成 256。现在句子变成:

    1
    2
    # 256 表示“th”
    [256, 101, 32, 99, 97, 116, 32, 105, 110, 32, 256, 101, 32, 104, 97, 116]
  • Step 3:再找下一个常见组合

    现在发现“th”和“e”(256, 101)出现了两次,把“th”和“e”粘成一个新块,编号 257(表示“the”)。替换后,句子变成:

    1
    2
    # 257 表示“the”
    [257, 32, 99, 97, 116, 32, 105, 110, 32, 257, 32, 104, 97, 116]
  • Step 4:还在GO

    再找常见组合,比如“a”和“t”(97, 116)出现两次(“cat”和“hat”里),粘成新块,编号 258(表示“at”)。替换后,句子变成:

    1
    2
    # 258 表示“at”
    [257, 32, 99, 258, 32, 105, 110, 32, 257, 32, 104, 258]
  • Step 5:STOP条件

    可以设定一个次数(比如合并 3 次),或者直到词汇表大小合适(比如 500 个块)就停下来。

    最终得到:

    • 词汇表:记录每个编号对应的块,比如 {116: b't', 104: b'h', 256: b'th', 257: b'the', 258: b'at'}
    • 合并规则:记录哪些块粘在一起,比如 {(116, 104): 256, (256, 101): 257, (97, 116): 258}

对于编码,假设输入为the quick brown fox,按步骤操作:

  • 先转成字节

    1
    [116, 104, 101, 32, 113, 117, 105, 99, 107, 32, 98, 114, 111, 119, 110, 32, 102, 111, 120]
  • 按合并规则替换:

    1. “t h” → 256(th):[256, 101, 32, 113, 117, 105, 99, 107, …]
    2. “th e” → 257(the):[257, 32, 113, 117, 105, 99, 107, …]
    3. 没有“a t”组合,停止。
  • 最终

    1
    [257, 32, 113, 117, 105, 99, 107, 32, 98, 114, 111, 119, 110, 32, 102, 111, 120]

BPE能够实现:

  • 动态词汇表:通过训练自动生成词汇表,适应不同语料(如英语、中文、代码等);可以通过 num_merges 控制词汇表大小。
  • 高压缩率:常见序列被合并为单一 token,减少序列长度。
  • 未见字符或词可以通过字节级别表示,不会产生<UNK> token。

Assignment 1 (Part 2)