15.最终模板整合与文档
第 15 章 — 最终模板整合与文档
上一章(第 14 章)我们给训练画上了句号:
predict.py能对一张新图输出 top-5 置信度,analyze.py能把测试集"解剖"成混淆矩阵和每类指标。但到这一步,工程还是"散装"状态:功能分散在 14 章里,没有一份总文档告诉新同学怎么跑起来、每个配置开关是干什么的、每个文件从哪一章来。本章做收尾:把 14 章积攒的功能整合成一份可直接交付的工程模板——最终版config/config.yaml(全部开关 + 为第 17 章预留log.wandb)、README.md("3 条命令跑通")、最终版requirements.txt(为第 16 章 optuna、第 17 章 wandb 提前装好),并给全工程一张"文件级代码索引表"。
一、本章要解决的问题
- 之前:14 章的功能各自为政,没有一个"最终全貌"——新同学拿到代码不知道文件怎么分工、从哪个入口跑起。
- 之前:配置项散落各章、没有汇总,想切换实验只能翻源码改参数。
- 之前:README、需求文档、依赖清单缺位,代码交付后别人(包括三个月后的自己)无法快速接手。
- 现在:需要一份最终版
config/config.yaml(唯一事实来源,含全部开关)、一份"3 条命令跑通"的README.md、一份覆盖全部依赖的requirements.txt,以及一张全工程的文件级代码索引表。 - 判断标准:拿到一个全新环境的人,只照着 README 的三条命令就能装依赖、训练、评估、推理;改任何实验开关都不需要动源码;
pip install -r requirements.txt装完后,第 16 章 optuna、第 17 章 wandb 无需再补装。
二、核心概念速览
下面 5 个概念是本章的"生词"——它们不是 PyTorch API,而是工程方法。先花 5 分钟读完,再看代码和文档会轻松很多。
1. 配置驱动工程(不改源码切实验)
"配置驱动"就是把所有"可能想改的数字"从代码里抽出来,集中放进一个配置文件。代码只负责"读配置 → 按配置执行",于是同一个 train.py,改一行 epochs: 30 就能跑 30 轮,改一行 batch_size: 64 就能换 batch,完全不用碰 Python 代码。好处是双重的:调实验的人(哪怕不懂代码)也能改配置;代码本身因为不再写死超参数,变得干净、可复用。判断一个工程是不是"配置驱动",就看一句话口号:改实验,只改配置,不动源码。
2. README 的作用与"3 条命令跑通"的标准
README 是项目的第一张名片,也是别人打开仓库第一个看的文件。好的 README 不罗嗦,核心验收标准是"3 条命令跑通":装依赖一条(pip install -r requirements.txt)、训练一条(python train.py)、评估/推理一条(python analyze.py / python predict.py)。照着这三条命令能完整走一遍流程,说明文档是可执行的;如果读者还得自己去猜数据放哪、模型怎么加载、要不要先建目录,文档就失败了。所以写 README 时把"命令"放最前面,原理讲解放后面。
3. 需求文档如何约束交付(docstring/PEP8)
这里的"需求文档"不是几十页的 spec,而是用工具和约定把"代码应该长什么样"固化下来:每个模块顶部写 docstring,说明"这个文件干什么、怎么用、关键函数签名";命名遵循 PEP8(build_* 工厂函数、_ 前缀私有方法、4 空格缩进、snake_case)。当这些约定成为模板的一部分,交付就不依赖"记得",而是"照着结构就能写"——新模块照着老模块的骨架填,风格自然统一,团队协作不吵架。
4. 全开关对照表的意义(config.yaml 是唯一事实来源)
全开关对照表把 config.yaml 里每一个字段列出来,标注"它控制什么、可选值有哪些、来自第几章"。它的意义在于建立唯一事实来源:所有能调的开关都在一个文件里,查表即知,不用翻 14 章代码去猜。这直接支撑了"配置驱动"的承诺——如果有一个开关在代码里写死、没进 config.yaml,对照表就立刻暴露这个漏洞。模板的价值就是"所有可调的都进表,表即是文档"。
5. 为什么文档要与代码同步维护(漂移问题)
"漂移"指文档和代码各说各话:config 里加了新开关,README 没更新;README 说 batch_size 默认 64,代码默认其实已经改成了 128。这种漂移是工程腐烂的第一个信号——读者照着文档跑必然翻车,而且越到后面越难查。避免的办法不是"写一次完美文档",而是把"改代码 = 改文档"当成一条流水线动作:改 config.yaml 顺手更新 README 的配置说明,加依赖顺手更新 requirements.txt,加文件顺手更新项目结构树。本章的 README 就按这个原则写,后续每章改动都要同步。
三、解决思路
- 最终版
config/config.yaml:把全部开关收齐(seed / device / distributed / data / model / train / checkpoint / log),并为第 17 章预留log.wandb: false——配置里先留好开关位,代码接入留到第 17 章。 README.md:按"3 条命令跑通"标准组织——项目简介 → 环境依赖 → 快速开始 → 配置说明 → 项目结构 → 常见用法。命令全部可直接复制粘贴,配置说明直接引用 config.yaml 的字段名。- 最终版
requirements.txt:把第 1~15 章全部依赖收齐,并提前加入optuna>=3.4(第 16 章超参搜索)与wandb>=0.16(第 17 章云端实验跟踪),避免后续章节临时补装、破坏"一条命令装齐"的承诺。 - 文件级代码索引表:把最终工程每份文件的完整代码所在章节列成一张表,作为"从 14 章散件到最终模板"的导航图;第 16 章
optuna_search.py、第 17 章 wandb 也预留行列。 - 不做什么:不改任何源码逻辑——
train.py、engine/trainer.py、dataset/datasets.py等 14 个文件全部沿用,本章只做"整合 + 文档",把 14 章积攒的内容交付清楚。
trade-off:为第 16/17 章提前装 optuna、wandb,会让第 15 章的安装时间变长,但换来"后续章节零额外安装、README 承诺不破"。README 只写"怎么跑"而不写"为什么",省掉了大段原理阐述——那是刻意取舍:原理在正文各章,README 只负责把工程跑起来。
四、代码变更
相对第 14 章的改动(其余 14 个文件一律不动,只动文档与配置):
config/config.yaml # 修改:补全全部开关的行内注释,新增 log.wandb 预留开关
dataset/datasets.py # 不变
models/classifier.py # 不变
engine/trainer.py # 不变
utils/logger.py # 不变
utils/checkpoint.py # 不变
utils/metrics.py # 不变
utils/early_stopping.py # 不变
utils/seed.py # 不变
utils/dist.py # 不变
utils/eval_tools.py # 不变
train.py # 不变
predict.py # 不变
analyze.py # 不变
+ README.md # 新增:交付文档(3 条命令跑通 + 配置说明 + 项目结构)
requirements.txt # 修改:新增两行依赖(第 16/17 章预留)
+ optuna>=3.4 # 新增依赖:第 16 章超参搜索
+ wandb>=0.16 # 新增依赖:第 17 章云端实验跟踪注意:本章是 17 章教程的"交接点"——从第 1 章到第 14 章我们不停改代码,从本章起代码冻结为模板,后面第 16 章只新增
optuna_search.py、第 17 章只接入 wandb,不再回改这 14 个文件。
五、完整代码
本章完整代码 = 第 14 章工程 + 以下三个文件。其余文件(train.py、predict.py、analyze.py、dataset/datasets.py、models/classifier.py、engine/trainer.py、utils/ 下全部模块)与第 14 章完全相同,直接沿用即可。
先给一张文件级代码索引表:最终工程的每一份文件,完整代码在第几章给出。第 16/17 章尚未写到的部分也预留了行列,让你看到整个 17 章的版图。
| 文件 | 完整代码所在章节 | 职责 |
|---|---|---|
config/config.yaml | 第 15 章(本章最终版,见下) | 全部超参数开关,唯一事实来源;log.wandb 为第 17 章预留 |
dataset/datasets.py | 第 13 章 | build_transforms / build_dataloaders(含 DistributedSampler) |
models/classifier.py | 第 11 章 | SimpleCNN / build_model |
engine/trainer.py | 第 13 章 | Trainer:训练/验证/存档/早停/恢复/DDP |
utils/logger.py | 第 11 章 | setup_logger |
utils/checkpoint.py | 第 11 章 | save_checkpoint / load_checkpoint |
utils/metrics.py | 第 11 章 | MetricsTracker |
utils/early_stopping.py | 第 11 章 | EarlyStopping |
utils/seed.py | 第 12 章 | set_seed / worker_init_fn |
utils/dist.py | 第 13 章 | dist_setup / dist_cleanup / is_main_process |
utils/eval_tools.py | 第 14 章 | 混淆矩阵 / 每类指标 / matplotlib 可视化 |
train.py | 第 13 章 | 训练入口(单卡 / DataParallel / DDP) |
predict.py | 第 14 章 | 单图/目录推理(top-5 + 置信度) |
analyze.py | 第 14 章 | 测试集评估与可视化 |
README.md | 第 15 章(本章,见下) | 交付文档:"3 条命令跑通" |
requirements.txt | 第 15 章(本章最终版,见下) | 全部依赖,含第 16/17 章预留 |
optuna_search.py | 第 16 章(预告) | 超参搜索:optuna 三段式研究 + config 开关联动 |
wandb 云端跟踪(基于 log.wandb 开关) | 第 17 章(预告) | 云端实验跟踪;本章仅预留开关,不写接入代码 |
config/config.yaml(最终版,含全部开关;相对第 14 章只补了行内注释和 log.wandb 一行):
# ============================================================
# CIFAR-10 图像分类 — 实验配置(唯一事实来源)
# 修改此文件即可切换实验配置,无需改动任何源码
# ============================================================
seed: 42 # 全局随机种子(第 12 章;--seed 可覆盖)
device: auto # auto / cuda / cpu
distributed: # 分布式训练(第 13 章)
enabled: false # true = DDP 多进程训练
data_parallel: false # enabled=false 且 true 时用 DataParallel 简单多卡
world_size: 2 # DDP 进程数(通常 = GPU 数)
backend: gloo # Windows/CPU 用 gloo;Linux 多卡建议 nccl
data: # 数据(第 2、3、8、9 章)
root: ./data # 数据集存放目录
batch_size: 64 # 单卡 batch;DDP 下全局 batch = batch_size × world_size
num_workers: 2 # DataLoader 加载进程数
val_ratio: 0.1 # 从训练集切出的验证集比例
augmentation: # 数据增强,只作用于训练集(第 9 章)
random_crop_padding: 4 # 0 = 关闭 RandomCrop
horizontal_flip: true # 随机水平翻转
color_jitter: [0.2, 0.2, 0.2, 0.1] # 亮度/对比度/饱和度/色相
model: # 模型(第 1、11 章)
num_classes: 10 # 分类数;换数据集时修改
train: # 训练(第 3、4、6、10 章)
epochs: 30 # 训练轮数上限(配合早停)
lr: 0.001 # 初始学习率(--lr 可覆盖)
lr_scheduler: step # step / cosine(第 4 章)
lr_step_size: 15 # StepLR 每多少轮衰减一次
lr_gamma: 0.1 # StepLR 衰减系数
patience: 7 # 早停 patience(第 6 章)
use_amp: true # 混合精度开关,仅 CUDA 生效(第 10 章)
checkpoint: # 存档(第 5 章)
dir: ./checkpoints # last.pth 每轮保存;best.pth 验证集最优
log: # 日志(第 7 章)
dir: ./runs/cifar10 # TensorBoard 事件文件 + train.log
wandb: false # 第 17 章:云端实验跟踪开关README.md(新增;整体缩进 4 空格,避免内部 ``` 围栏与外层冲突):
# CIFAR-10 图像分类模板工程
一个渐进式构建的 PyTorch 训练工程模板,配套 17 章教程逐章演进。
## 项目简介
- 任务:CIFAR-10 图像分类(10 类,32x32 RGB)
- 模型:轻量 CNN(3 卷积块 + 全局平均池化 + 线性分类头)
- 特点:配置驱动(修改 config.yaml 即可切换实验,不改源码)
## 环境依赖
```bash
pip install -r requirements.txt需要 Python 3.9+。首次运行自动下载 CIFAR-10 数据集(约 170MB)。
## 快速开始(3 条命令跑通)
```bash
# 1. 安装依赖
pip install -r requirements.txt
# 2. 训练(自动下载数据,含增强/AMP/早停/存档)
python train.py
# 3. 评估 + 推理
python analyze.py --ckpt checkpoints/best.pth
python predict.py --image my_photo.jpg
```
查看训练曲线:
```bash
tensorboard --logdir runs
```
## 配置说明
所有超参数集中在 config/config.yaml,按模块组织:seed / device / distributed / data / model / train / checkpoint / log。命令行只保留高频覆盖项:--config --lr --epochs --batch-size --resume --seed,优先级:命令行 > 配置文件。
## 项目结构
```
project/
├── config/config.yaml
├── dataset/datasets.py
├── models/classifier.py
├── engine/trainer.py
├── utils/ # logger/checkpoint/metrics/early_stopping/seed/dist/eval_tools
├── train.py # 训练入口(单卡 / DataParallel / DDP)
├── predict.py # 推理入口
├── analyze.py # 评估入口
├── optuna_search.py # 第 16 章:超参搜索
└── requirements.txt
```
## 常见用法
```bash
python train.py --lr 1e-4 --epochs 50 # 调参
python train.py --resume checkpoints/last.pth # 断点续训
python train.py --config config/config.yaml # 多配置文件
```
> README 里的 `optuna_search.py` 和依赖里的 optuna/wandb 是**提前占位**:第 16 章会真的新增这个文件,第 17 章会真的用上 wandb。占位是为了让"README 承诺的项目结构"和"requirements.txt 承诺的依赖"在 15 章就成立,避免每章都要回头改文档(这正是"文档与代码同步维护"的落地)。
**`requirements.txt`**(最终版,新增最后两行为第 16/17 章预留):
```text
torch>=2.0
torchvision>=0.15
numpy>=1.24
pyyaml>=6.0
tensorboard>=2.13
matplotlib>=3.7
tqdm>=4.66
rich>=13.0
torchsummary>=1.5
torchmetrics>=1.2
# ---- 第 16/17 章预留(先装好,避免后续章节临时补装)----
optuna>=3.4
wandb>=0.16依赖行本体逐字沿用第 14 章,只新增两行(
optuna、wandb)和一行注释。把"预留依赖"直接写进最终版,是本章"整合"的一部分——交付清单一次到位,README 的"一条命令装齐依赖"从此不会被后续章节打破。
六、本章小结
- 学到了什么
- 配置驱动工程 = 所有开关收进
config.yaml,改实验不动源码;它的验收标准是"全开关对照表上每个字段都能在配置里找到"。 - README 的核心不是篇幅而是可执行性:"3 条命令跑通"(装依赖 / 训练 / 评估+推理)是最朴素的验收标准,命令放最前、原理靠后。
- 需求文档靠 docstring + PEP8 约束交付:每个模块说清"干什么、怎么用",
build_*工厂、_私有前缀、4 空格缩进让新模块照骨架填。 - 文档与代码必须同步维护,否则漂移;改 config 顺手改 README、加依赖顺手改 requirements、加文件顺手改结构树。
- 索引表是 17 章教程的版图:第 1~15 章的文件各归其位,第 16 章
optuna_search.py、第 17 章 wandb 已占位。
- 配置驱动工程 = 所有开关收进
- 常见坑
- 文档与代码漂移:改
config.yaml忘了同步 README,读者照着过时的文档跑必然翻车。 - 相对路径依赖:始终从项目根目录运行命令,
./data、config/config.yaml都以当前目录为基准。 - 新增依赖忘了进
requirements.txt、新增文件忘了进项目结构树——这两处是 README 最容易"说谎"的地方。 - 把
log.wandb: false当成已实现的功能:它只是第 17 章的开关位,现在改true不会有任何效果(接入代码还没写)。
- 文档与代码漂移:改
- 下一章预告:模板已冻结。第 16 章我们让模板"自己调自己"——用 optuna 对
config.yaml里的 lr、batch_size、增强强度做超参搜索,optuna_search.py会作为新文件补进工程。
七、动手练习
- 交付演练:把最终工程(14 个代码文件 + 本章三个文件)拷到一个全新目录,清空环境,只按 README 的三条命令操作,验证"照着文档能跑通"。做不到的部分,回头修 README——文档的可执行性是工程质量的试金石。
- 索引表自查:从本章的索引表里随机挑 3 个配置开关(如
use_amp、world_size、random_crop_padding),分别去对应章节找到它们的消费代码,确认"表 → 配置 → 代码"三者对得上,体会"唯一事实来源"是怎么起作用的。 - 开关矩阵测试:挑 3 个开关(如
augmentation、use_amp、lr_scheduler),做 2×2×2 组合实验,用第 7 章的 TensorBoard 对比 8 组曲线——这就是"配置驱动实验管理"的日常形态。 - 走一遍"加开关"流程:给 config 增加
train.grad_clip: 0.0(0=关闭),在Trainer._train_one_epoch中实现梯度裁剪,然后同步更新 README 的配置说明,最后把新字段补进索引表的 config 行——完整演练"改代码 = 改文档"。 - 预留开关体检:写三行 Python 读取
config.yaml的log.wandb字段(yaml.safe_load后cfg["log"]["wandb"]),确认现在是False;再想想第 17 章接入 wandb 时,这个开关该在哪里被检查(提示:在train.py的入口编排里,if cfg.log.wandb:)。
