Lecture 1:概览与分词

5115 字
26 分钟
Lecture 1:概览与分词
文章摘要

Stanford CS336 Language Modeling from Scratch - Lecture 1: Overview, Tokenization,时长 01:19:21。

为什么要从零构建语言模型#

在 coding agent 已经能零样本写出语言模型的今天,一个自然的问题是:亲手从零搭建语言模型还有必要吗?

答案是肯定的,但需要先理解一个正在发生的断层。回溯过去十年,AI 研究者与底层技术之间的距离不断拉大。2016 年前后,研究者自己实现并训练模型是常态;2018 年 BERT 出现后,主流范式变成下载预训练模型再 fine-tune;而到了今天,大多数人的工作方式是直接 prompting 一个 API 模型(GPT、Claude、Gemini)。

向高层抽象迁移本身是好事——它提高了生产力。但与编程语言或操作系统不同,语言模型的抽象层是泄漏的(leaky)。当你 prompting 一个模型时遇到它做不到的事情,你几乎没有任何补救手段。而如果你想做根本性的研究(fundamental research),仅仅在 prompting 层面操作,实际上极大地约束了你的设计空间。

Full understanding of this technology is necessary for fundamental research. Philosophy of this course: understanding via building.

这门课的理念很明确:通过构建来理解。但这里有一个现实问题——前沿模型的工业化已经发生。GPT-4 的训练成本据称在 1 亿美元量级,当前前沿模型的训练成本可能已接近 10 亿美元。更关键的是,这些模型的技术细节完全不公开——GPT-4 的论文明确声明因”竞争格局和安全考量”不分享任何构建细节。

那在一门大学课程中训练的”小”模型(约 1B 参数规模),真的能学到可迁移到前沿模型的知识吗?这里需要清醒认识两个现实:

  1. 计算分布随规模变化。在小模型中,MLP 层和 Attention 层的 FLOPs 占比大约各占一半(MLP ~44%);但在 175B 参数的模型中,MLP 层的 FLOPs 占比飙升到 ~80%。这意味着在小规模上优化 Attention 的效果,到大规模未必同样显著。

  2. 涌现能力(emergent abilities)的存在。在许多任务上,小模型的表现几乎是随机水平,只有当模型规模跨越某个临界点后,性能才会突然跃升。

图1:涌现能力——模型规模跨越临界点后任务性能突变
图1:涌现能力——模型规模跨越临界点后任务性能突变

这看起来似乎令人沮丧,但实际上可迁移的知识可以分为三个层次:

  • Mechanics(机械层面):Transformer 是什么、Model Parallelism 如何工作——这些是确定性的知识,完全可以从小规模学习后迁移到大规模
  • Mindset(思维方式):如何从硬件中压榨最大性能、如何严肃对待 scaling——这是一种工程思维,同样跨规模适用
  • Intuitions(直觉):哪些数据和建模决策能带来好的 accuracy——这一层不一定能跨规模迁移,有些在小规模上有效的直觉在大规模上可能完全不成立

前两类知识是这门课能可靠传授的;第三类需要保持警惕——有些设计决策至今没有很好的理论解释,纯粹来自实验。例如 Noam Shazeer 引入 SwiGLU 激活函数的论文,其结论部分坦承这些架构变体”simply not (yet) justifiable and just come from experimentation”。

关于效率的核心定位:这门课的核心问题可以表述为——给定一定的计算和数据预算,能构建出的最好模型是什么? 换言之,最大化效率。效率在大规模下尤为重要,因为浪费一次训练的代价极其高昂。[Hernandez+ 2020] 的研究显示,2012 到 2019 年间,ImageNet 上的算法效率提升了 44 倍——这说明算法层面的进步与硬件进步同样重要。

语言模型发展简史#

语言模型的历史可以清晰地划分为四个阶段,每一阶段都在效率和能力上实现了质的飞跃。

Pre-neural 时代(2010 年以前)#

语言模型的思想最早可追溯到 Shannon [1950],他提出用语言模型来测量英语的信息熵。此后数十年的主流方案是 n-gram 语言模型,被广泛应用于机器翻译和语音识别系统 [Brants+ 2007]。

Neural ingredients 阶段(2010s)#

这个阶段涌现了一系列关键技术组件,它们后来成为大语言模型的基础设施:

  • LSTM (Long-Short Term Memory) [Hochreiter+ 1997]——解决了 RNN 的长距离依赖问题
  • 第一个神经语言模型 [Bengio+ 2003]——用神经网络直接建模词序列概率
  • Sequence-to-Sequence [Sutskever+ 2014]——将编码-解码框架引入机器翻译
  • Adam 优化器 [Kingma+ 2014]——至今仍是训练深度模型的默认优化器之一
  • Attention 机制 [Bahdanau+ 2014]——让模型能”关注”输入序列的特定位置
  • Transformer 架构 [Vaswani+ 2017]——完全基于注意力机制,抛弃了递归结构
  • Mixture of Experts [Shazeer+ 2017]——通过稀疏激活实现模型容量的扩展
  • 模型并行 [Huang+ 2018][Rajbhandari+ 2019][Shoeybi+ 2019]——使训练超大模型成为可能

Early foundation models 阶段(2010 年代末)#

这一阶段的核心转变是 pretraining + fine-tuning 范式的确立:

  • ELMo [Peters+ 2018]:用 LSTM 做预训练,证明预训练特征可以提升下游任务
  • BERT [Devlin+ 2018]:用 Transformer 做预训练,fine-tuning 在 NLU 任务上取得突破
  • T5 (11B 参数) [Raffel+ 2019]:将所有任务统一为 text-to-text 格式

Embracing scaling 阶段(2020 年至今)#

从 GPT-3 [Brown+ 2020] 开始,领域进入了”拥抱规模”的时代。关键的认知转变是:如果你想让模型在某个任务上表现好,与其设计特定的架构或训练策略,不如直接把模型做大、数据做多

这个阶段诞生了两类重要的开放模型生态:

Open-weight models(公开权重但不公开训练细节):

  • Meta 的 Llama 系列 [Meta 2024, 2025]
  • DeepSeek 系列 [DeepSeek-AI+ 2025]
  • Alibaba 的 Qwen [Qwen+ 2024]
  • Moonshot 的 Kimi [Kimi Team 2025]
  • 智谱的 GLM [GLM-4.5 Team 2025]

Open-source models(同时公开权重、论文、代码和数据):

  • AI2 的 OLMo [Groeneveld+ 2024][Team OLMo 2025]
  • NVIDIA 的 Nemotron [Parmar+ 2024]
  • Marin 的开放开发模型 [Marin 8B/32B retro]

开放性对于信任和创新至关重要 [Kapoor+ 2024]。正是来自开放模型的思想和实践,使得像 CS336 这样的课程成为可能。

课程路线图:构建 LM 的六大模块#

这门课的整体设计围绕一个明确的目标展开:从零训练一个约 1B 参数的语言模型。为达成这个目标,课程分解为六个模块,每个模块对应一个核心工程问题。值得注意的是,课程采用了一种独特的 executable lecture(可执行讲座)形式——所有讲义本身就是一个可以运行的 Python 程序,代码和教学内容融为一体。

模块一:模型架构(Architecture)#

起点是原始 Transformer [Vaswani+ 2017],但现代大语言模型在此基础上做了大量修改。需要理解的核心组件包括:

Attention 机制的变体:从标准 Multi-Head Attention 到 Multi-Query Attention [Shazeer 2019]、Grouped-Query Attention [Ainslie+ 2023],以及 DeepSeek-AI [2024] 的变体。目标是在减少 KV cache 内存的同时尽量保持模型质量。

非 Transformer 架构:Recurrence/State-Space Models 如 Mamba [Katharopoulos+]、Gated DeltaNet [Dao+ 2024],以及线性 Attention 的变体 [Yang+ 2024][Lahoti+ 2026]。

MLP 层:Dense MLP 与 Mixture of Experts (MoE) [Shazeer+ 2017][Fedus+ 2021] 的选择。MoE 通过稀疏激活在不增加推理计算量的前提下扩大模型容量。

模型形状参数:hidden dimension、depth、head 数量、expert 数量等超参数的选择。

模块二:训练(Training)#

训练模块回答的核心问题是:如何设定模型的参数?

  • 损失函数:除了标准的 next-token prediction,还有 multi-token prediction [Gloeckle+ 2024][DeepSeek-AI+ 2024]
  • 优化器:从经典的 AdamW [Kingma+ 2014] 到更新的 SOAP [Vyas+ 2024]、Muon [Keller+ 2024] 和 Loshchilov+ 2017 的变体
  • 初始化策略:Xavier init [Glorot+ 2010]、muP (Maximal Update Parameterization) [Yang+ 2022]——后者尤为重要,因为它实现了超参数迁移,即在小规模上调好的超参数可以直接用于大规模
  • 学习率调度:从 cosine schedule [Loshchilov+ 2016] 到 WSD (Warmup-Stable-Decay) [Hu+ 2024]
  • 正则化:dropout、weight decay 等
  • Batch size:critical batch size 的概念 [McCandlish+ 2018]
  • MoE 的负载均衡:如 aux-free 的方案 [Wang+ 2024][DeepSeek-AI+ 2024]

模块三:系统优化(Kernels & Parallelism)#

这个模块的核心问题是:如何让 GPU 真正快起来?

GPU Kernel 是运行在 GPU 上的函数。PyTorch 中每个原语操作都会启动一个标准 kernel,但通过编写自定义 kernel 可以获得巨大的性能提升。核心原则是最小化数据搬运

  • Naive 方式:读 HBM → 计算 A → 写 HBM → 读 HBM → 计算 B → 写 HBM
  • Fused 方式:读 HBM → 计算 A 和 B → 写 HBM

相关优化策略包括 operator fusion(如 matmul + activation 融合)、tiling(FlashAttention 的核心思想)、warp divergence、memory coalescing、bank conflicts 等。实现工具有 CUDA、Triton、CUTLASS、ThunderKittens。

并行训练 解决”如果有 1024 块 GPU 怎么用?“的问题。GPU 之间的数据搬运比 GPU 内部更慢,但”最小化数据搬运”的原则不变。核心概念包括:

  • 经典 collective operations:gather、reduce、all-reduce
  • Shard memory:将参数、激活值、梯度、优化器状态分片到多块 GPU
  • 计算拆分策略:data / tensor / pipeline / sequence / expert parallelism

推理加速 也是课程覆盖的内容。加速解码的方法包括:

  • 使用更便宜的模型(pruning、quantization、distillation)
  • Speculative decoding:用一个轻量 “draft” 模型并行生成多个 token,然后用完整模型打分验证(这是 exact decoding!)
  • 系统优化:fused kernels、continuous batching

模块四:Scaling Laws#

当你已经有了模型、能让它跑得快、能并行训练之后,下一个问题是:如何科学地扩大规模?

设想你有 102510^{25} FLOPs 的计算预算(相当于数千万美元),你只能训练一个模型——如果搞砸了,这些钱就白费了。你不可能在这个规模做 hyperparameter tuning,因为每次训练都太贵了。

核心思维转变:不要把注意力放在”一个模型”上,而是关注一个 scaling recipe——它是从 FLOPs 预算到一组超参数(配置文件)的映射。

具体做法是:

  1. 在多个较小的规模(如 102410^{24} FLOPs)上训练,记录 loss
  2. 拟合一个 scaling law
  3. 用 scaling law 预测目标规模(如 102510^{25} FLOPs)上的 loss

这使得两件事成为可能:

  • 用小规模实验优化大规模的 scaling recipe
  • 在真正训练之前预测目标规模的性能

经典的 compute-optimal scaling laws 来自 [Kaplan+ 2020] 和 [Hoffmann+ 2022](Chinchilla 论文)。实践中使用 ISOFLOP curves:在多个小 FLOPs 预算下找到最优模型大小 N,然后拟合 scaling law 外推到大 FLOPs 预算。

一个关键要求是 scaling laws 依赖于精心构造的 scaling recipe。[Yang+ 2022] 的 muP 提供了 hyperparameter transfer 的理论基础,使得在小规模调好的超参数可以迁移到大规模。而 predictability 至少和 optimality 一样重要——一个次优但可预测的 recipe 比一个偶尔最优但不可预测的 recipe 更有用。

模块五:数据与评估(Data & Evaluation)#

有了模型和训练方法后,下一个问题是:用什么数据训练?

数据质量在根本上决定了模型的能力边界。对数据的选择实际上是在回答”你希望模型能做什么?“——多语言?擅长对话?还是 agentic coding?

评估(Evaluation) 分为两个层次,它们服务于不同目的,不应混淆:

  1. 内部评估(for development):关键要求是跨规模的平滑性和相对性能。Perplexity 是最典型的内部指标——一个 perplexity 的绝对值没有太大意义,但两个模型的 perplexity 比较非常有价值
  2. 外部评估(for reporting):关注生态有效性(ecological validity)。典型 benchmark 包括 GPQA、HLE、SWE-Bench、Terminal-Bench

数据类型 分为三个阶段:

  • Pretraining data:大规模、多样化
  • Mid-training data:高质量,包括 long-context 数据
  • Post-training data:supervised fine-tuning 数据(对话、agentic traces with tool calling)

数据治理 的关键环节:

  • Filtering:用分类器保留高质量内容、过滤有害内容
  • Deduplication:节省计算、避免记忆化;工具包括 Bloom filters 和 MinHash
  • Data mixing:不同数据源的权重分配 [Liu+ 2024][Chen+ 2026]
  • Rewriting / synthetic data:用 LM 改写或生成数据,使其更贴近下游任务 [Maini+ 2024]

效率作为统一主题#

回到一个核心认知:当前阶段是 compute-constrained 的,所有设计决策都围绕”在固定资源下最大化性能”展开。效率不是某个模块独有的话题,而是渗透在每一个决策中:

  • Systems:显然是关于效率的
  • Tokenization:直接操作 raw bytes 在理论上更优雅,但以当前模型架构的计算效率来看是不现实的
  • Model architecture:许多架构变化的动机是减少内存或 FLOPs(如 sharing KV cache、window attention)
  • Data filtering:避免在低质量数据上浪费宝贵的计算
  • Scaling laws:用小模型的计算来替代大模型的超参数调优

明天我们将变成 data-constrained 的……但今天,效率仍是王道。

Tokenization:从字符串到 token 序列#

语言模型的输入和输出都是 token 序列(以整数索引表示),但人类书写的文本是 Unicode 字符串。Tokenizer 的任务就是完成两者之间的双向转换:

  • encode: string → token indices
  • decode: token indices → string

一个合格的 tokenizer 必须满足 round-trip 一致性decode(encode(s)) == s。如果这个不成立,说明实现有 bug。

评价 tokenizer 的核心指标是 compression ratio(压缩比):

compression ratio=bytes数token数\text{compression ratio} = \frac{\text{bytes数}}{\text{token数}}

例如字符串 "the quick brown fox" 有 20 个 bytes,GPT-5 的 tokenizer 将其编码为 8 个 token,压缩比为 20/8=2.520/8 = 2.5 bytes/token。

压缩比越高越好——因为 Attention 的计算复杂度是序列长度的二次方,更短的 token 序列意味着更低的计算成本。但提高压缩比的直接手段(增大词表)会带来稀疏性问题:词表中越来越多的 token 出现频率极低,导致 embedding 矩阵的参数利用率下降。

现代多语言 tokenizer 的词表大小通常在 100K-200K。

几种基本 tokenizer 的对比#

在引入 BPE 之前,先看看三种朴素方案为何不够好:

Character Tokenizer:将每个 Unicode 字符映射为一个 token。Unicode 共有约 150K 个字符,所以词表大小可达 150K。但大量 Unicode 字符极为罕见,导致词表中充斥着几乎不被使用的条目。更关键的是,压缩比很低——每个字符都是独立 token,没有任何压缩。

Byte Tokenizer:先将字符串用 UTF-8 编码为字节序列,每个字节(0-255)作为一个 token。词表大小仅 256,非常紧凑。但压缩比恒为 1——序列长度等于字节数,对于 Attention 来说太长了。ASCII 字符(如英文字母)每个只占 1 个字节,但中文、emoji 等字符可能占 3-4 个字节,进一步拉长序列。

Word Tokenizer:用空格或正则表达式将文本切分成词,每个词作为一个 token。优点是每个 token 都有明确的语义(因为词是人类设计的语义单元),压缩比也不错。但有两个致命问题:

  1. 词表大小等于训练数据中不同词的数量,理论上无界
  2. 测试时遇到训练数据中未出现过的词,只能用 UNK token 替代,这不仅丑陋,还会破坏 perplexity 的计算

Character tokenizer 和 Byte tokenizer 都”不够好”——前者词表冗余、后者序列太长。Word tokenizer 看似理想,却面临开放词表和 UNK 问题。需要一种能在压缩比和词表大小之间取得平衡的方案。

Byte-Pair Encoding 的训练与实现#

Byte-Pair Encoding (BPE) 最初由 Philip Gage 在 1994 年为数据压缩提出,后被 [Sennrich+ 2015] 引入 NLP(用于神经机器翻译),并在 GPT-2 [Radford+ 2019] 中首次应用于大语言模型。

BPE 的核心直觉是:在原始字节序列上,频繁出现的相邻 byte 对应该被合并为一个新 token。高频序列(如英文中的 “th”、“he”、“in”)获得单独的 token,低频序列则被保留为更小的单元(极端情况下退化为单个字节)。这保证了:

  • 无论输入什么文本,都能被 tokenize(不存在 UNK)
  • 常见模式获得高效的压缩
  • 词表大小是可控的(= 256 + num_merges)

BPE 训练算法#

训练过程概念上非常简洁。给定一个文本语料,算法如下:

  1. 将整个语料转换为 UTF-8 字节序列,每个字节就是一个初始 token(索引 0-255)
  2. 统计所有相邻 token 对的出现次数
  3. 找到出现次数最多的那一对,为其分配一个新的 token 索引(从 256 开始递增)
  4. 在整个序列中,将所有该 pair 的出现替换为新 token
  5. 重复步骤 2-4,直到达到指定的合并次数 num_merges

以字符串 "the cat in the hat" 为例,逐步执行:

初始状态:将字符串编码为字节序列

indices = [116, 104, 101, 32, 99, 97, 116, 32, 105, 110, 32, 116, 104, 101, 32, 104, 97, 116]

(其中 116=‘t’, 104=‘h’, 101=‘e’, 32=’ ’, 99=‘c’, 97=‘a’, 105=‘i’, 110=‘n’)

第一轮合并:统计相邻对的频率。(116, 104) 即 “th” 出现 2 次(最高频)。创建新 token 256 代表 “th”,替换所有出现位置:

merges[(116, 104)] = 256
vocab[256] = b"th"
indices → [256, 101, 32, 99, 97, 116, 32, 105, 110, 32, 256, 101, 32, 104, 97, 116]

序列从 18 个 token 缩短到 16 个。

第二轮合并:此时 (256, 101) 即 “the” 出现 2 次。创建 token 257 代表 “the”:

merges[(256, 101)] = 257
vocab[257] = b"the"
indices → [257, 32, 99, 97, 116, 32, 105, 110, 32, 257, 32, 104, 97, 116]

第三轮合并:继续寻找最高频 pair 并合并……

随着合并的进行,序列不断缩短,词表不断增长。最终的 compression ratio 取决于合并次数——合并越多,压缩比越高,但词表也越大。对于这个玩具例子,3 次合并后 compression ratio 为 1.5。

Encode 与 Decode#

训练完成后得到两样东西:一个 merges 字典(记录每次合并的 pair → new_index 映射)和一个 vocab 字典(记录每个 index → bytes 映射)。

Encode(对新文本 tokenize):

  1. 将输入字符串转为 UTF-8 字节序列
  2. 按照训练时的合并顺序,依次检查序列中是否存在可合并的 pair
  3. 如果存在,执行合并
  4. 重复直到没有更多可合并的 pair

Decode(从 token indices 恢复字符串):

  1. 对每个 token index,查找 vocab 字典得到对应的 bytes
  2. 拼接所有 bytes
  3. 用 UTF-8 解码得到字符串

工程优化与 Assignment 1#

上述实现虽然正确,但极其缓慢encode() 函数需要遍历所有 merges(数量等于 vocab_size - 256),对于 100K+ 词表这意味着每次编码都要做数万次全序列扫描。

Assignment 1 的核心任务就是优化 BPE 的性能:

  • encode() 目前遍历所有 merges,应该只遍历与当前序列相关的 merges,需要构建索引结构
  • 处理 special tokens(如 <|endoftext|>)——概念上不复杂但对构建现代 tokenizer 很重要
  • 使用 pre-tokenization:先用正则表达式(如 GPT-2 的 tokenizer regex)将文本切分为 chunks,再对每个 chunk 独立做 BPE,大幅提升速度
  • 追求极致性能时,Python 本身可能成为瓶颈——欢迎用 Rust 或 C 重写

Tokenization 的未来#

当前 tokenizer 的存在本质上是一个工程妥协——直接在 raw bytes 上建模更优雅、更通用(不需要为不同语言和模态设计不同的 tokenizer),但以当前的模型架构来说计算上不可行(byte 序列太长)。

Tokenizer-free 的方案 [Xue+ 2021][Yu+ 2024][Pagnoni+ 2024][Deiseroth+ 2024][Hwang+ 2025] 正在被积极探索,但尚未在前沿规模上验证。

即便 tokenizer 最终被取代,替代方案也必须满足两个核心要求:

  1. 某种形式的抽象是必要的。Transformer 需要操作的是对原始序列的某种 abstraction——这在文本之外(如视频、DNA 序列)更为明显,原始的 byte 或 unit 信噪比太低,需要先提升到一个有意义的表示层级
  2. 可变长度的 chunk(自适应计算)是必要的。不是所有的 byte 都同等重要——信息密度高的区域应该获得更多的计算资源。任何 end-to-end 的方案如果不具备这两个性质,大概率是次优的
Lecture 1:概览与分词
https://www.xwysyy.cn/posts/cs336/lec01/
作者
xwysyy
发布于
2026-05-17
许可协议
CC BY-NC-SA 4.0
© 2026 xwysyy. All Rights Reserved.
Powered by Astro & Firefly

文章目录