第 2 章 配置体系(config.py)


文档摘要

第 2 章 配置体系(config.py) 核心思想:所有超参数集中到 dataclass,做到「配置即代码」,命令行可覆盖任意一项。 2.1 为什么配置这么重要 深度学习训练有个残酷事实:同样的代码、不同的超参数,效果天差地别。学习率差一个数量级,loss 可能从 1.5 飙到 NaN。所以工业项目都把「配置管理」当一等公民。 用 Python 标准库的 解决了三个问题: 集中:所有超参一处定义,不散落各处。 类型安全:IDE 自动补全、静态检查。 可序列化:能存进 checkpoint,下次恢复。 2.2 完整源码 整文件不到 100 行,但定义了项目的全部可调旋钮。 2.3 dataclass 入门 传统写法的痛点 问题:参数散落、无类型、无文档、易写错、难复现。

第 2 章 配置体系(config.py)

核心思想:所有超参数集中到 dataclass,做到「配置即代码」,命令行可覆盖任意一项。

2.1 为什么配置这么重要

深度学习训练有个残酷事实:同样的代码、不同的超参数,效果天差地别。学习率差一个数量级,loss 可能从 1.5 飙到 NaN。所以工业项目都把「配置管理」当一等公民。

config.py 用 Python 标准库的 dataclass 解决了三个问题:

  1. 集中:所有超参一处定义,不散落各处。
  2. 类型安全:IDE 自动补全、静态检查。
  3. 可序列化:能存进 checkpoint,下次恢复。

2.2 完整源码

""" config.py ========= GPT 模型训练项目的全局配置模块。 本模块使用 Python 标准库 dataclass 定义两组配置: - GPTConfig: 描述 GPT 模型本身的架构超参数(层数、头数、嵌入维度等)。 - TrainConfig: 描述训练过程的超参数(批大小、学习率、最大迭代步数等)。 使用 dataclass 的好处: 1. 代码简洁,自动生成 __init__ / __repr__ 等方法。 2. 类型注解清晰,便于 IDE 提示与静态检查。 3. 易于序列化为 JSON / YAML,方便实验复现。 """ from dataclasses import dataclass from typing import Tuple @dataclass class GPTConfig: """GPT 模型架构配置。 这些超参数会被映射为 HuggingFace transformers 的 GPT2Config, 从而控制一个从零初始化的小型 GPT2 模型的规模。 """ # ---------- 词表与上下文 ---------- # p50k_base (GPT-2 编码) 的词表大小,固定为 50257。 vocab_size: int = 50257 # 上下文窗口长度 (block_size),即模型一次能看到的最大 token 数。 block_size: int = 128 # ---------- Transformer 主体 ---------- # Transformer 层数。 n_layer: int = 6 # 多头注意力的头数。 n_head: int = 6 # 嵌入维度(隐藏层维度),会被均匀拆分到每个注意力头。 n_embd: int = 384 # ---------- Dropout ---------- # 统一的 dropout 概率,分别作用于残差连接、嵌入层与注意力权重。 dropout: float = 0.1 def to_gpt2_kwargs(self) -> dict: """将本配置映射为 HuggingFace transformers GPT2Config 的关键字参数。""" return { "vocab_size": self.vocab_size, "n_positions": self.block_size, "n_ctx": self.block_size, "n_embd": self.n_embd, "n_layer": self.n_layer, "n_head": self.n_head, "resid_pdrop": self.dropout, "embd_pdrop": self.dropout, "attn_pdrop": self.dropout, # GPT-2 词表中 50256 即 <|endoftext|>,用作序列起止符。 "bos_token_id": self.vocab_size - 1, "eos_token_id": self.vocab_size - 1, } @dataclass class TrainConfig: """训练流程配置。""" # ---------- 数据 ---------- batch_size: int = 32 # Windows 下若大于 0,必须保证训练入口位于 if __name__ == "__main__" 中。 num_workers: int = 0 # ---------- 优化器 (AdamW) ---------- learning_rate: float = 3e-4 weight_decay: float = 0.1 betas: Tuple[float, float] = (0.9, 0.95) # ---------- 训练步数 ---------- max_iters: int = 5000 warmup_iters: int = 100 # ---------- 梯度裁剪 ---------- grad_clip: float = 1.0 # ---------- 评估、日志与保存 ---------- log_iter: int = 10 # 每多少步打印一次平均 loss save_iter: int = 500 # 每多少步保存一次 checkpoint # ---------- 学习率调度 ---------- min_lr_ratio: float = 0.1 # 余弦退火终点的学习率比例 # ---------- 设备与随机种子 ---------- device: str = "auto" # "auto" / "cuda" / "cpu" seed: int = 42 # ---------- 目录 ---------- checkpoint_dir: str = "checkpoints"

整文件不到 100 行,但定义了项目的全部可调旋钮

2.3 dataclass 入门

传统写法的痛点

# 反面教材:散落的魔法数字 def train(): lr = 3e-4 # 这个 3e-4 哪来的?改一处忘改另一处? batch_size = 32 n_layer = 6 # ...100 行后... scheduler = CosineScheduler(lr, n_layer=6) # 又出现一个 6,跟前面那个一致吗?

问题:参数散落、无类型、无文档、易写错、难复现。

dataclass 写法

from dataclasses import dataclass @dataclass class GPTConfig: vocab_size: int = 50257 n_layer: int = 6 n_embd: int = 384

加了 @dataclass 装饰器后,Python 自动给你生成:

  • __init__GPTConfig(n_layer=12) 直接可用,参数都有默认值。
  • __repr__print(GPTConfig()) 输出 GPTConfig(vocab_size=50257, n_layer=6, n_embd=384),可读。
  • __eq__:两个 config 可直接比较相等。

你只写「字段名 + 类型 + 默认值」一行,方法全自动生成。这是「声明式编程」的典型范式。

序列化 bonus

dataclass 实例的 __dict__ 直接是 JSON 兼容的 dict(只要字段都是基本类型):

cfg = GPTConfig() print(cfg.__dict__) # {'vocab_size': 50257, 'block_size': 128, 'n_layer': 6, ...} # 存进 checkpoint import json with open("config.json", "w") as f: json.dump(cfg.__dict__, f) # 恢复 cfg2 = GPTConfig() cfg2.__dict__.update(json.load(open("config.json")))

本项目 train.py 就是这么把配置存进 checkpoint 的(见第 5 章)。

2.4 GPTConfig:模型架构详解

@dataclass class GPTConfig: vocab_size: int = 50257 block_size: int = 128 n_layer: int = 6 n_head: int = 6 n_embd: int = 384 dropout: float = 0.1

字段含义图解

输入 token 序列 (长度 ≤ block_size) │ ▼ ┌─────────────────────────────────────┐ │ Token Embedding (vocab_size × n_embd) │ ├─────────────────────────────────────┤ │ ┌─────────────────────────────┐ │ │ │ Transformer Block × n_layer │ │ ◄── 6 层堆叠 │ │ ├─ Multi-Head Attention │ │ (n_head=6 个头) │ │ │ 每头维度 = n_embd/n_head│ │ 每头 384/6 = 64 维 │ │ ├─ Feed-Forward │ │ │ │ └─ Residual + LayerNorm │ │ │ └─────────────────────────────┘ │ ├─────────────────────────────────────┤ │ LM Head (n_embd × vocab_size) │ └─────────────────────────────────────┘ │ ▼ 输出每个位置下一个 token 的概率分布

逐字段解释

vocab_size = 50257

分词器的词表大小。本项目用 tiktoken 的 p50k_base 编码(GPT-2 同款),词表恰好 50257。

⚠️ 铁律:这个值由分词器决定,不能乱改。改了会导致 token id 与 Embedding 矩阵的行数对不上。换词表 = 换分词器 = 必须同步换 vocab_size。

block_size = 128

模型一次能看到的最大 token 数(上下文窗口)。GPT-2 small 是 1024,本项目缩小到 128 是为了训练快。

影响:

  • 训练时每个样本长度 128。
  • 推理时输入超过 128 会被截断(见第 6 章)。
  • 模型的位置编码表大小也是 128(n_positions)。

n_layer = 6n_head = 6n_embd = 384

Transformer 的核心三参数。

  • n_layer:堆叠几个 Transformer Block。越深越能学复杂模式,但越难训。
  • n_head:多头注意力的头数。每个头独立学一种「关注模式」(如语法、语义、共指……)。
  • n_embd:嵌入维度,所有 token 都被表示成 384 维向量。这是模型的「带宽」。

⚠️ 约束n_embd 必须能被 n_head 整除。每个头的维度 = n_embd / n_head。本项目 384 / 6 = 64 ✓。改成 n_embd=100, n_head=3 会报错(100/3 不是整数)。

dropout = 0.1

训练时随机把 10% 的神经元置零,防止过拟合。推理时自动关闭。

本项目用同一个 dropout 值作用于三处(见 to_gpt2_kwargs):

  • resid_pdrop:残差连接后的 dropout
  • embd_pdrop:嵌入层后的 dropout
  • attn_pdrop:注意力权重上的 dropout

规模换算

默认配置(6 × 384 × 6)≈ 25M 参数。常见对照:

规模 n_layer n_embd n_head 参数量
玩具级 2 64 2 ~0.2M
本项目默认 6 384 6 ~25M
GPT-2 small 12 768 12 124M
GPT-2 XL 48 1600 25 1.5B

参数量粗略公式:

参数量 ≈ 12 × n_layer × n_embd²

(精确数字会因是否 tie weights、词表大小略有出入,第 4 章会讲。)

2.5 to_gpt2_kwargs():配置的单一来源

def to_gpt2_kwargs(self) -> dict: return { "vocab_size": self.vocab_size, "n_positions": self.block_size, "n_ctx": self.block_size, "n_embd": self.n_embd, "n_layer": self.n_layer, "n_head": self.n_head, "resid_pdrop": self.dropout, "embd_pdrop": self.dropout, "attn_pdrop": self.dropout, "bos_token_id": self.vocab_size - 1, "eos_token_id": self.vocab_size - 1, }

设计精髓:解耦

项目自己定义 GPTConfig,再用这个方法单向映射到 HuggingFace 的 GPT2Config 字段名。

项目代码 ──只认──► GPTConfig(项目自己的) │ │ to_gpt2_kwargs() 单向映射 ▼ GPT2Config(HuggingFace 的)

好处:

  1. 上层代码不被 HuggingFace 命名绑死。如果哪天想换底层实现(比如换成 LLaMA),只改这一个映射函数。
  2. 字段含义清晰。HuggingFace 把上下文长度叫 n_positions/n_ctx 两个名字(其实是一回事),项目统一叫 block_size 更好懂。
  3. dropout 一次定义三处用。项目只有一个 dropout 字段,映射到 HF 的三个独立 dropout 参数,避免不一致。

bos_token_id = eos_token_id = 50256

GPT-2 词表中第 50256 号 token 是 <|endoftext|>,它身兼三职:

  • BOS(Beginning Of Sequence):序列起始符
  • EOS(End Of Sequence):序列结束符
  • 文档分隔符:训练数据中不同文档之间用它分隔

本项目复用这一约定——推理时若没给 prompt,就用它作为起始 token(见 inference.py:64)。

2.6 TrainConfig:训练流程详解

@dataclass class TrainConfig: batch_size: int = 32 num_workers: int = 0 learning_rate: float = 3e-4 weight_decay: float = 0.1 betas: Tuple[float, float] = (0.9, 0.95) max_iters: int = 5000 warmup_iters: int = 100 grad_clip: float = 1.0 log_iter: int = 10 save_iter: int = 500 min_lr_ratio: float = 0.1 device: str = "auto" seed: int = 42 checkpoint_dir: str = "checkpoints"

按职能分组理解:

数据组

字段 默认 说明
batch_size 32 每个梯度步用的样本数
num_workers 0 DataLoader 子进程数(Windows 建议 0)

batch_size 影响:

  • 显存占用(越大越吃显存)
  • 梯度噪声(越大越平滑,但太大可能掉进局部最优)

优化器组(AdamW)

字段 默认 说明
learning_rate 3e-4 峰值学习率
weight_decay 0.1 权重衰减系数
betas (0.9, 0.95) AdamW 动量参数

💡 最重要的超参learning_rate。比模型架构还关键。第 5 章详讲 AdamW。

步数组

字段 默认 说明
max_iters 5000 总训练步数
warmup_iters 100 预热步数

💡 为什么是步数而不是 epochs?语言模型训练通常按「优化步数」控制,因为数据量远大于一个 epoch 所需。本项目用一个 while step < max_iters 循环 + 数据迭代器重置来消费数据(见第 5 章)。

稳定性组

字段 默认 说明
grad_clip 1.0 梯度裁剪的 L2 范数阈值

防梯度爆炸的「保险丝」。

观测与持久化组

字段 默认 说明
log_iter 10 每 10 步打印一次 loss
save_iter 500 每 500 步存一次 checkpoint

学习率调度组

字段 默认 说明
min_lr_ratio 0.1 余弦退火终点 lr 是峰值的 10%

环境组

字段 默认 说明
device "auto" 自动检测 CUDA
seed 42 随机种子
checkpoint_dir "checkpoints" 存盘目录

2.7 学习率曲线(最值得画图理解)

本项目用「线性预热 + 余弦退火」,曲线长这样:

学习率 ↑ │ ╭───╮ ← 峰值 learning_rate (3e-4) │ ╱ ╲ │ ╱ ╲ │ ╱ ╲ │ ╱ ╲ │ ╱ ╰───╯ ← 终点 learning_rate × min_lr_ratio (3e-5) │╱ └─────────────────────→ 训练步数 0 warmup max_iters =100 =5000

三段论:

  1. 预热期(0 → 100 步):lr 从 0 线性升到峰值。
    • 为什么?模型刚初始化,权重乱七八糟,大步走会破坏初始化。慢慢加 lr 让模型「热身」。
  2. 退火期(100 → 5000 步):按余弦曲线平滑下降到峰值的 10%。
    • 为什么?中期稳定下降找最优解,末期小步微调避免震荡。
  3. 为什么不直接保持峰值或线性下降?余弦曲线在峰值附近下降慢(充分利用高 lr 学习),在末期下降也慢(精细收敛),中间下降快。比线性下降更优雅。

具体实现见 train.py:51-82get_cosine_schedule_with_warmup,第 5 章会逐行讲数学。

2.8 命令行覆盖任意配置

train.py 用 argparse 把命令行参数映射到 config 字段:

# train.py 片段(完整见第 5 章) def apply_args_to_configs(args, gpt_config, train_config): if args.n_layer is not None: gpt_config.n_layer = args.n_layer if args.batch_size is not None: train_config.batch_size = args.batch_size # ...

所以你可以:

# 调模型规模 python train.py --n-layer 12 --n-embd 768 --n-head 12 # 调训练 python train.py --learning-rate 1e-4 --max-iters 10000 # 全自定义 python train.py --batch-size 16 --warmup-iters 200 --min-lr-ratio 0.05

没传的参数保持 dataclass 默认值——这是「配置即代码 + 命令行覆盖」的标准范式。

2.9 配置的最佳实践

学完 config.py,自己写项目时遵守这些原则:

  1. 所有超参进 dataclass,不要散落在脚本里。
  2. 字段顺序按职能分组(数据/优化器/步数……),用注释分隔。
  3. 默认值是有意的选择,不是随便填的。能解释为什么默认是 3e-4 而不是 1e-3。
  4. 关键约束写进注释(如 Windows 的 num_workers)。
  5. 配置可序列化,存进 checkpoint(本项目做到了)。
  6. 单一映射:项目自己的 config → 第三方库 config 只有一处映射,不重复定义。

2.10 动手实验

  1. 破坏性测试:把 n_embd 改成 100、n_head 改成 3,观察是否能构建模型(提示:100/3 不是整数)。

  2. 画学习率曲线:把 min_lr_ratio 改成 0.0,画一下学习率曲线会是什么样?改成 1.0 呢?

  3. 思考题:如果训练中途想换 learning_rate,直接改配置重启会丢失什么?

    • 答:优化器内部积累的动量与二阶矩估计。因为重启等于新优化器。要续训必须用 --resume(见第 5 章)。
  4. 打印 config:在 Python 里跑:

    from config import GPTConfig, TrainConfig print(GPTConfig()) print(TrainConfig()) print(GPTConfig().to_gpt2_kwargs())

    观察输出,加深印象。

2.11 小结

  • config.pydataclass 定义 GPTConfig(模型)和 TrainConfig(训练)两组配置。
  • GPTConfig.to_gpt2_kwargs() 把项目配置单向映射到 HuggingFace 命名,实现解耦。
  • 学习率曲线 = 线性预热 + 余弦退火,是训练稳定性的关键。
  • 命令行参数可覆盖任意配置项,未覆盖的保持默认。

2.12 下一章

搞懂配置后,去 第 3 章 数据流水线 看文本是怎么变成模型能吃的 (x, y) 张量对的。


发布者: 作者: 转发
评论区 (0)
U