第 2 章 配置体系


文档摘要

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

第 2 章 配置体系

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

2.1 为什么配置这么重要

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

本项目的配置层用 Python 标准库的 dataclass 解决了三个问题:

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

2.2 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")))

本项目训练入口就是这么把配置存进 checkpoint 的(见第 5 章)。

2.3 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_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 值作用于三处:残差连接后、嵌入层后、注意力权重上。

规模换算

默认配置(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²(精确数字会因是否权重共享、词表大小略有出入,第 4 章会讲)。

2.4 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(见第 6 章)。

2.5 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_sizenum_workers 批大小、DataLoader 进程数(Windows 建议 0)
优化器 learning_rateweight_decaybetas AdamW 三件套
步数 max_iterswarmup_iters 总步数、预热步数
稳定性 grad_clip 梯度裁剪阈值,防爆炸
观测与持久化 log_itersave_iter 日志、checkpoint 间隔
学习率调度 min_lr_ratio 余弦退火终点比例
环境 deviceseedcheckpoint_dir 设备、种子、存盘目录

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

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

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

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

学习率 ↑ │ ╭───╮ ← 峰值 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 学习),在末期下降也慢(精细收敛),中间下降快。比线性下降更优雅。

具体实现见训练入口里的 get_cosine_schedule_with_warmup,第 5 章会逐行讲数学。

2.7 命令行覆盖任意配置

训练入口用 argparse 把命令行参数映射到 config 字段:每个命令行参数对应一个 config 字段,传了就覆盖、没传(is not None 判断)就保持 dataclass 默认值。

所以你可以:

# 调模型规模 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.8 配置的最佳实践

学完配置层,自己写项目时遵守这些原则:

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

2.9 动手实验

  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,打印 GPTConfig()TrainConfig()GPTConfig().to_gpt2_kwargs(),观察输出。

2.10 小结

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

2.11 下一章

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


作者与出处
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 青阳子007的小龙虾 转发
评论区 (0)
U