1. 这不是“抄代码”,而是PyTorch项目落地的完整工作流
你打开GitHub,搜“pytorch cifar10”,满屏都是几十行的train.py——加载数据、定义模型、写训练循环、打印loss。看起来能跑,但真要加个早停、换种数据增强、导出ONNX、部署到边缘设备,或者第二天想复现实验结果?大概率卡在第3步:找不到当时用的随机种子、transform没保存、model.eval()忘写了、测试时batch_size设成128导致OOM……这不是代码能力问题,是缺乏一套可复现、可维护、可交接的工程化模板。
我带过不少刚从课程作业转向实际项目的同学,也帮某高校实验室重构过图像分类流水线。他们最常问的不是“ResNet怎么写”,而是:“为什么我改了学习率,结果比baseline还差?”、“上次跑出来的acc=94.2%,这次怎么只有92.1%?”、“导师让我把模型给合作方,我该打包哪些文件?”——这些问题,全指向一个被严重低估的事实:PyTorch的“完整套路”,90%不在模型结构里,而在数据、训练、评估、日志、部署这五个环环相扣的环节中。
这个模板,就是我们用CIFAR10反复打磨出来的最小可行闭环。它不追求SOTA精度,但保证:
- 每次运行结果可复现(固定所有随机源);
- 训练过程有完整快照(模型权重+优化器状态+当前epoch);
- 评估逻辑与训练严格解耦(避免data leakage);
- 日志结构化存储(支持TensorBoard和CSV双视图);
- 导出格式明确(.pt + .onnx + 推理脚本三件套)。
适合三类人直接“抄作业”:
- 新手:跳过环境配置、路径管理、参数命名等琐碎坑,专注理解训练逻辑;
- 进阶者:把模块当积木,替换backbone、换数据集、加自监督预训练,都不用重写主干;
- 团队协作者:统一config.yaml规范,新人拉下代码就能跑通baseline,不用猜“你的train.py里lr是多少”。
它不是教科书式的示例,而是我们压测过27个不同显卡型号、在3种Linux发行版上验证过的生产级起点。下面拆解每个环节为什么这么设计,以及你抄的时候最容易忽略的3个致命细节。
2. 整体架构设计:为什么必须分5层,而不是写成一个train.py?
2.1 五层解耦:数据、模型、训练、评估、部署的物理隔离
很多初学者把所有代码塞进一个文件,美其名曰“简洁”。实测下来,这种结构在第2次迭代时必然崩溃。我们强制拆分为5个独立模块,核心逻辑如下:
| 层级 | 目录/文件 | 核心职责 | 不允许做的事 | 典型错误 |
|---|---|---|---|---|
| 数据层 | data/cifar10_dataset.pytransforms.py | 定义数据集类、预处理pipeline、dataloader构建 | 调用模型、读取checkpoint、硬编码路径 | 在dataset里import resnet,或把root_path写死为/home/user/data |
| 模型层 | models/resnet_cifar.pyefficientnet_cifar.py | 实现网络结构、初始化策略、forward逻辑 | 加载数据、调用optimizer、写log | 在model.py里写torch.save(model, 'best.pth') |
| 训练层 | trainer/base_trainer.pyddp_trainer.py | 控制训练流程、loss计算、梯度更新、checkpoint保存 | 定义数据集、修改模型结构、硬编码metric | 在trainer里重新写一遍ResNet,或把val_acc>0.93作为停止条件写死 |
| 评估层 | eval/inference.pymetrics.py | 独立于训练的推理逻辑、指标计算、混淆矩阵生成 | 使用train_loader、依赖训练时的model.train()状态 | 用train_dataloader做test,或忘记调用model.eval() |
| 部署层 | deploy/export_onnx.pyrun_inference.py | 模型序列化、格式转换、轻量级推理脚本 | 调用trainer、读取train_log、依赖wandb | export脚本里import trainer.Trainer |
这种分层不是为了炫技,而是解决三个真实痛点:
- 调试效率:当你发现测试准确率异常,直接进
eval/目录查inference.py,不用在千行train.py里grep; - 快速替换:想试试ViT?只改
models/vit_cifar.py和config.yaml里的model_name,其他层完全不动; - 权限管控:给算法同事开放
models/和trainer/,给工程同事开放deploy/,数据同事只管data/,天然隔离风险。
提示:所有模块间通信只通过明确定义的接口。例如
Trainer.train()方法只接收train_loader,val_loader,model,optimizer四个参数,不接受任何路径字符串或config字典。这强迫你在main.py里完成所有参数组装,让依赖关系一目了然。
2.2 配置中心化:为什么用YAML而不是argparse?
见过太多项目用python train.py --lr 0.01 --epochs 100 --batch_size 64启动,结果三个月后没人记得--lr到底指optimizer还是scheduler的learning rate。我们坚持用config.yaml作为唯一真相源,结构如下:
# config.yaml project: name: "cifar10_baseline" seed: 42 device: "cuda" # auto fallback to cpu if cuda not available data: root: "./data/cifar10" num_workers: 4 batch_size: 128 transforms: train: - type: "RandomHorizontalFlip" p: 0.5 - type: "RandomCrop" size: 32 padding: 4 - type: "ToTensor" - type: "Normalize" mean: [0.4914, 0.4822, 0.4465] std: [0.2023, 0.1994, 0.2010] val: - type: "ToTensor" - type: "Normalize" mean: [0.4914, 0.4822, 0.4465] std: [0.2023, 0.1994, 0.2010] model: name: "resnet18_cifar" pretrained: false num_classes: 10 optimizer: name: "SGD" lr: 0.1 momentum: 0.9 weight_decay: 5e-4 scheduler: name: "StepLR" step_size: 30 gamma: 0.1 trainer: max_epochs: 100 save_dir: "./checkpoints/cifar10_baseline" log_dir: "./logs/cifar10_baseline" save_freq: 10 # save checkpoint every 10 epochs early_stopping: patience: 15 min_delta: 0.001关键设计点:
- 层级嵌套映射代码结构:
data.transforms.train直接对应data/transforms.py中的get_train_transforms()函数; - 类型安全校验:加载时用
omegaconf做schema验证,如果误写lr: "0.1"(字符串),启动即报错,不等到训练一半才发现loss nan; - 环境变量注入:支持
${oc.env:HOME}语法,root: "${oc.env:HOME}/datasets/cifar10",避免硬编码路径; - 多环境继承:
config_debug.yaml可继承config.yaml并覆盖trainer.max_epochs: 5,无需复制粘贴。
注意:我们禁用所有命令行参数覆盖配置的功能。如果需要临时调整,必须修改yaml文件并提交git记录。这是为了确保“哪次实验用了什么参数”永远可追溯——毕竟,你永远不知道哪次随手改的
--lr 0.001会成为论文里的关键超参。
2.3 日志与检查点:为什么同时存.pt和.json?
很多人只保存.pt权重文件,结果半年后想复现,发现:
- 不知道当时用的optimizer是Adam还是SGD;
- 不清楚学习率是否用了warmup;
- 甚至记不清测试时batch_size是32还是64(影响BN统计量)。
我们的checkpoint保存策略是原子化快照:每次保存都生成两个文件:
checkpoints/cifar10_baseline/ ├── epoch_50.pt # 模型权重 + 优化器状态 + 当前epoch + best_val_acc └── epoch_50.json # 完整config.yaml内容 + git commit hash + 环境信息epoch_50.json内容节选:
{ "config": { "data": {"batch_size": 128, "transforms": {...}}, "optimizer": {"name": "SGD", "lr": 0.1}, "trainer": {"max_epochs": 100} }, "git_hash": "a1b2c3d4e5f6", "env": { "pytorch_version": "2.1.0", "cuda_version": "12.1", "gpu_model": "NVIDIA A100-SXM4-40GB" } }这样做的好处:
- 零依赖复现:拿到
epoch_50.json,就能100%还原训练环境; - 跨平台兼容:
.pt文件在不同PyTorch版本可能加载失败,但json永远可读; - 审计友好:审稿人要求提供实验细节?直接发json文件,不用写额外说明文档。
实操心得:我们在trainer/base_trainer.py里封装了save_checkpoint()方法,它自动调用torch.save()和json.dump(),且强制校验两个文件的md5值一致才认为保存成功。曾有一次因磁盘满导致.json写入失败,系统立刻抛出异常中断训练,避免了“只存了权重没存配置”的灾难。
3. 核心细节解析:CIFAR10场景下的12个关键决策点
3.1 数据加载:为什么用torchvision.datasets.CIFAR10但重写__getitem__?
官方CIFAR10类开箱即用,但存在两个隐患:
- 内存泄漏风险:默认使用PIL读取,大量小图频繁open/close导致Python GC压力大;
- transform耦合度高:
transform参数只能传一个函数,无法动态切换train/val逻辑。
我们的解决方案:继承官方类,重写__getitem__,并引入缓存机制:
# data/cifar10_dataset.py class CIFAR10Dataset(torch.utils.data.Dataset): def __init__(self, root, train=True, transform=None, cache_images=True): self.root = root self.train = train self.transform = transform self.cache_images = cache_images # 预加载所有图片到内存(仅当cache_images=True) self.images = [] self.labels = [] for img, label in self._load_data(): # _load_data()调用官方逻辑 if cache_images: self.images.append(img) # PIL.Image对象 else: self.images.append((img.filename, img.mode)) # 缓存路径而非对象 self.labels.append(label) def __getitem__(self, idx): if self.cache_images: img = self.images[idx] else: # 按需加载,避免内存爆炸 filename, mode = self.images[idx] img = Image.open(filename).convert(mode) if self.transform is not None: img = self.transform(img) return img, self.labels[idx]关键参数:
cache_images=True:适合单卡训练(CIFAR10仅300MB),提升30%数据加载速度;cache_images=False:适合多卡DDP,避免各进程重复缓存同一份数据;num_workers=4:经实测,超过4个worker反而因IPC开销降低吞吐,与GPU数量无关。
注意:务必在
__del__中释放缓存,否则Jupyter notebook反复运行会内存溢出:def __del__(self): if hasattr(self, 'images') and self.cache_images: del self.images gc.collect()
3.2 数据增强:为什么train用AutoAugment而val只用ToTensor?
CIFAR10图像尺寸小(32x32),传统增强如RandomRotation会导致信息严重丢失。我们采用分层策略:
| 增强类型 | train阶段 | val/test阶段 | 理由 |
|---|---|---|---|
| 几何变换 | RandomHorizontalFlip(p=0.5), RandomCrop(32, padding=4) | 无 | Crop模拟图像边界扰动,Flip增加视角多样性,但val必须保持原始构图 |
| 色彩变换 | AutoAugment(policy='cifar10') | 无 | AutoAugment由强化学习搜索得到,对CIFAR10提升1.2% top1 acc,但策略复杂,val阶段禁用以保真 |
| 归一化 | Normalize(mean, std) | Normalize(mean, std) | 必须一致,否则输入分布偏移 |
AutoAugment的坑:
- 官方实现
torchvision.transforms.AutoAugment在PyTorch 1.10+才支持,旧版本需降级或手动实现; - policy必须指定为
'cifar10',用'imagenet'策略会导致过拟合(因CIFAR10类别更细粒度); - 实测发现,开启AutoAugment后,
RandomErasing反而降低性能——小图上擦除一块区域等于删除关键特征。
3.3 模型初始化:为什么ResNet18要重写_init_weights?
PyTorch官方ResNet对ImageNet预训练做了优化,但CIFAR10输入尺寸小、类别少,直接迁移效果不佳。我们强制重置初始化:
# models/resnet_cifar.py def _init_weights(self, m): if isinstance(m, nn.Conv2d): # CIFAR10用小卷积核,He初始化更稳定 nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') if m.bias is not None: nn.init.constant_(m.bias, 0) elif isinstance(m, nn.BatchNorm2d): # BN参数初始化为1,0,避免初始batch统计量偏差 nn.init.constant_(m.weight, 1) nn.init.constant_(m.bias, 0) elif isinstance(m, nn.Linear): # 最后一层Linear,用正态分布初始化 nn.init.normal_(m.weight, 0, 0.01) nn.init.constant_(m.bias, 0) def __init__(self, block, layers, num_classes=10, zero_init_residual=False): super().__init__() # ... 原始ResNet结构 self.apply(self._init_weights) # 关键:递归应用初始化为什么不用torch.nn.init.xavier_normal_?
- Xavier假设激活函数是tanh,而ResNet用ReLU,He初始化(
kaiming_normal_)理论更匹配; zero_init_residual=True在CIFAR10上反而使收敛变慢——小数据集需要更强的初始梯度信号。
3.4 训练循环:为什么用torch.cuda.amp但禁用torch.compile?
混合精度训练(AMP)是CIFAR10的标配,但torch.compile在小模型上收益有限:
| 技术 | CIFAR10收益 | 风险 | 我们的决策 |
|---|---|---|---|
torch.cuda.amp | 训练速度提升1.8x,显存占用降40% | loss scale不当导致梯度爆炸 | 启用,配合GradScaler自动调节 |
torch.compile | 小模型(ResNet18)仅提速12%,且首次编译耗时30秒 | 编译后模型不可序列化,checkpoint无法保存 | 禁用,留待大模型(ViT)再启用 |
AMP关键配置:
scaler = torch.cuda.amp.GradScaler() for data, target in train_loader: optimizer.zero_grad() with torch.cuda.amp.autocast(): output = model(data) loss = criterion(output, target) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() # 更新loss scale实操心得:
scaler.update()必须放在scaler.step()之后,且不能漏掉。我们曾因在early stopping逻辑里提前退出,忘记调用update(),导致后续epoch的loss scale持续衰减,最终梯度全为0。
3.5 评估指标:为什么用sklearn.metrics而不是torchmetrics?
torchmetrics功能强大,但对CIFAR10这种小任务过于重量级:
- 需要额外安装,增加环境复杂度;
- 多卡DDP下需
sync_dist=True,但CIFAR10单卡足够,徒增通信开销; ConfusionMatrix等高级指标,用原生numpy一行就能写完。
我们的评估脚本eval/metrics.py只依赖sklearn和numpy:
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report def compute_metrics(y_true, y_pred): acc = accuracy_score(y_true, y_pred) cm = confusion_matrix(y_true, y_pred) report = classification_report(y_true, y_pred, output_dict=True) return { "accuracy": acc, "confusion_matrix": cm.tolist(), # 转list便于json序列化 "per_class_f1": {k: v["f1-score"] for k, v in report.items() if k.isdigit()} } # 在inference.py中调用 y_true, y_pred = [], [] with torch.no_grad(): for data, target in test_loader: output = model(data) pred = output.argmax(dim=1) y_true.extend(target.cpu().tolist()) y_pred.extend(pred.cpu().tolist()) metrics = compute_metrics(y_true, y_pred) print(f"Test Accuracy: {metrics['accuracy']:.4f}")优势:
- 输出
metrics.json可直接被CI/CD系统解析; classification_report返回字典,方便提取特定类别F1;confusion_matrix转list后,前端可视化工具(如Streamlit)可直接渲染热力图。
4. 实操过程:从零开始跑通CIFAR10模板的7个步骤
4.1 环境准备:为什么推荐conda而非pip?
PyTorch生态对CUDA版本极其敏感。CIFAR10虽小,但若环境不一致,torch.cuda.is_available()可能返回False,或nn.DataParallel报错。我们强制使用conda管理:
# 创建专用环境(避免污染全局) conda create -n pytorch-cifar python=3.9 conda activate pytorch-cifar # 安装PyTorch(根据官网选择CUDA版本,此处以12.1为例) pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 # 安装必要依赖 pip install omegaconf scikit-learn tensorboard opencv-python为什么不用pip install torch?
- pip安装的PyTorch可能绑定旧版CUDA驱动,而conda会自动匹配系统CUDA toolkit;
omegaconf需与PyTorch版本兼容,conda能自动解决依赖冲突;- 实测发现,pip安装的
torchvision在某些Ubuntu版本上缺少ffmpeg支持,导致视频数据集加载失败(虽CIFAR10不用,但模板要通用)。
提示:在
requirements.txt中只写torch==2.1.0,不写--index-url。部署时用pip install -r requirements.txt,本地开发用conda——兼顾可复现性与灵活性。
4.2 数据下载:为什么用torchvision.datasets.CIFAR10自动下载?
手动下载CIFAR10易出错:
- 官网下载链接失效(曾因SSL证书问题中断);
- 解压后目录结构不符(
cifar-10-batches-py/vscifar-10-python/); - 文件校验缺失(MD5不匹配导致数据损坏)。
我们的data/download_cifar10.py脚本:
from torchvision.datasets import CIFAR10 import os def download_cifar10(root="./data"): """自动下载并校验CIFAR10""" # torchvision内置校验,无需手动MD5 train_set = CIFAR10(root=root, train=True, download=True) val_set = CIFAR10(root=root, train=False, download=True) print(f"Downloaded to {root}") print(f"Train samples: {len(train_set)}, Val samples: {len(val_set)}") # 验证数据完整性 assert len(train_set) == 50000 and len(val_set) == 10000 print("✓ Data integrity check passed") if __name__ == "__main__": download_cifar10()执行后自动创建:
data/ └── cifar-10-batches-py/ ├── batches.meta ├── data_batch_1 ~ data_batch_5 └── test_batch注意:
download=True会跳过已存在的文件,但不会校验内容。因此我们添加assert强制验证样本数,防止部分文件下载失败。
4.3 配置修改:如何3分钟适配自己的需求?
模板默认配置针对单卡训练。若你要:
- 换模型:修改
config.yaml中model.name: "efficientnet_b0_cifar",确保models/efficientnet_b0_cifar.py已存在; - 调学习率:改
optimizer.lr: 0.05,无需改代码; - 加早停:取消注释
early_stopping块,并设patience: 20; - 多卡训练:将
device: "cuda"改为device: "cuda:0,1",并在main.py中启用DDP分支。
最关键的自定义点是data.root:
- 绝对路径:
root: "/mnt/fastssd/datasets/cifar10"(推荐,避免相对路径歧义); - 相对路径:
root: "../data/cifar10"(需确保从main.py所在目录运行); - 环境变量:
root: "${oc.env:DATASET_ROOT}/cifar10"(适合CI/CD)。
实测案例:某公司实习生把root写成"./data",在服务器上运行时报FileNotFoundError。排查2小时才发现,他从/home/user/project/目录运行,而./data指向/home/user/data,实际数据在/data/cifar10。从此我们强制要求:所有路径必须绝对,或通过环境变量注入。
4.4 启动训练:为什么main.py只有12行?
main.py是整个模板的入口,它不做任何业务逻辑,只做三件事:
- 加载配置;
- 构建各模块实例;
- 调用训练器。
# main.py from omegaconf import OmegaConf from trainer.base_trainer import Trainer from data.cifar10_dataset import get_dataloaders from models.resnet_cifar import ResNet18CIFAR from torch.optim import SGD if __name__ == "__main__": # 1. 加载配置 cfg = OmegaConf.load("config.yaml") # 2. 构建组件 train_loader, val_loader = get_dataloaders(cfg) model = ResNet18CIFAR(num_classes=cfg.model.num_classes) optimizer = SGD(model.parameters(), lr=cfg.optimizer.lr) # 3. 启动训练 trainer = Trainer(cfg) trainer.train(model, train_loader, val_loader, optimizer)为什么不用hydra?
- Hydra功能强大但学习成本高,CIFAR10模板追求极简;
OmegaConf足够处理嵌套配置,且cfg.model.name可直接用于getattr(models, cfg.model.name)动态加载;- 所有错误都在启动时暴露(如
cfg.model.name不存在),不等到训练一半才报错。
提示:在
trainer/base_trainer.py中,我们用os.makedirs(cfg.trainer.save_dir, exist_ok=True)确保目录存在。曾有人因权限问题无法创建checkpoints/,训练直接崩溃。现在会优雅提示“请检查save_dir路径权限”。
4.5 监控训练:TensorBoard日志怎么看?
模板自动启动TensorBoard,日志结构清晰:
logs/cifar10_baseline/ ├── events.out.tfevents.123456789 # TensorBoard事件文件 ├── metrics.csv # 结构化CSV,可Excel打开 └── config.yaml # 当前配置快照关键指标解读:
- train/loss:平滑下降,若出现尖峰,检查
scaler.update()是否漏掉; - val/accuracy:每epoch一次,若连续5个epoch不升,early stopping应触发;
- lr:验证scheduler是否按预期衰减(如StepLR应在epoch30、60下降);
- grad_norm:若持续>10,可能学习率过大或梯度爆炸。
CSV日志示例(metrics.csv):
epoch,train_loss,val_accuracy,lr,grad_norm 0,2.3026,10.00,0.1000,5.23 1,1.8234,32.45,0.1000,4.89 ... 49,0.1234,94.21,0.0010,1.02实操心得:我们禁用
torch.utils.tensorboard.SummaryWriter.add_graph(),因为CIFAR10模型小,graph对调试无帮助,反而增加日志体积。大模型(ViT)才启用。
4.6 模型导出:ONNX和TorchScript哪个更适合CIFAR10?
CIFAR10部署场景通常是:
- 嵌入式设备(Jetson Nano):需要ONNX,用TensorRT加速;
- Web服务(Flask API):用TorchScript,免PyTorch依赖;
- 移动端(iOS/Android):优先ONNX,跨平台兼容性好。
我们的deploy/export_onnx.py:
import torch import onnx def export_onnx(model_path, onnx_path, input_shape=(1, 3, 32, 32)): model = torch.load(model_path) model.eval() dummy_input = torch.randn(input_shape) torch.onnx.export( model, dummy_input, onnx_path, export_params=True, opset_version=14, # 兼容TensorRT 8.6+ do_constant_folding=True, input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}} ) # 验证ONNX模型 onnx_model = onnx.load(onnx_path) onnx.checker.check_model(onnx_model) print(f"ONNX exported to {onnx_path}") if __name__ == "__main__": export_onnx("./checkpoints/cifar10_baseline/epoch_100.pt", "./deploy/cifar10.onnx")关键参数:
opset_version=14:避免旧版ONNX不支持aten::adaptive_avg_pool2d;dynamic_axes:声明batch_size可变,否则TensorRT推理时固定batch=1;do_constant_folding=True:优化常量计算,减小模型体积。
注意:ONNX导出后必须用
onnx.checker验证,否则TensorRT加载时可能静默失败。我们曾因漏掉这步,在Jetson上调试3天才发现是ONNX版本不兼容。
4.7 推理测试:如何验证导出模型和原始模型一致?
部署前必须做一致性校验,否则线上acc暴跌。我们的deploy/run_inference.py:
import torch import onnxruntime as ort import numpy as np def test_consistency(pt_path, onnx_path, test_image): # PyTorch推理 pt_model = torch.load(pt_path) pt_model.eval() with torch.no_grad(): pt_output = pt_model(test_image).softmax(dim=1) # ONNX推理 ort_session = ort.InferenceSession(onnx_path) ort_output = ort_session.run( None, {'input': test_image.numpy()} )[0] ort_output = torch.from_numpy(ort_output).softmax(dim=1) # 比较差异 diff = torch.abs(pt_output - ort_output).max().item() print(f"Max difference: {diff:.6f}") assert diff < 1e-4, f"ONNX output differs from PyTorch by {diff}" print("✓ Consistency check passed") if __name__ == "__main__": # 加载一张测试图(CIFAR10第一张) from torchvision.datasets import CIFAR10 test_set = CIFAR10(root="./data", train=False, download=False) image, _ = test_set[0] # PIL.Image image = torch.unsqueeze(transforms.ToTensor()(image), 0) # (1,3,32,32) test_consistency( "./checkpoints/cifar10_baseline/epoch_100.pt", "./deploy/cifar10.onnx", image )阈值1e-4的依据:
- FP16计算误差通常在
1e-3量级; 1e-4确保数值一致性,同时容忍硬件浮点差异;- 若diff>1e-3,立即终止部署,回查ONNX导出参数。
5. 常见问题与排查技巧实录:27个真实踩坑记录
5.1 数据加载类问题(高频,占总问题42%)
| 问题现象 | 根本原因 | 排查命令 | 解决方案 |
|---|---|---|---|
OSError: image file is truncated | CIFAR10下载不完整,test_batch文件损坏 | ls -la data/cifar-10-batches-py/test_batch | 删除data/cifar-10-batches-py/,重新运行download_cifar10.py |
RuntimeError: DataLoader worker (pid XXX) is killed by signal: Bus error | num_workers>0时共享内存不足 | nvidia-smi查看GPU内存,free -h看系统内存 | 设num_workers=0(单进程),或升级到PyTorch 2.0+(改进共享内存管理) |
ValueError: Expected more than 1 value per channel when training, got input size [1, 512, 1, 1] | BatchNorm2d在batch_size=1时失效 | 在config.yaml中设data.batch_size: 32 | 永远避免batch_size=1,CIFAR10最小设16 |
独家技巧:在
data/cifar10_dataset.py的__getitem__开头加日志:print(f"[DEBUG] Loading {idx}, shape={img.size}")
可快速定位哪张图损坏。但上线前必须注释,否则日志爆炸。
5.2 训练过程类问题(占总问题31%)
| 问题现象 | 根本原因 | 排查命令 | 解决方案 |
|---|---|---|---|
loss=nan持续出现 | AMP的loss scale过大,梯度爆炸 | print(scaler.get_scale())在训练循环中 | 在trainer/base_trainer.py的scaler.step()后加if scaler.get_scale() > 2048: scaler.update(512)限幅 |
val_accuracy stuck at 10.0% | 测试时未调用model.eval(),BN层用训练统计量 | 在eval/inference.py中加assert not model.training | 封装model.eval()到Trainer.evaluate()方法,强制调用 |
CUDA out of memory | batch_size=128在小显存卡(如GTX 1060)上溢出 | nvidia-smi实时监控显存 | 改config.yaml中data.batch_size: 64,或启用torch.cuda.amp |
实操心得:我们给
Trainer加了内存监控钩子:if torch.cuda.is_available(): mem = torch.cuda.memory_allocated() / 1024**3 print(f"[GPU Memory] {mem:.2f} GB")当显存>90%,自动降低
batch_size并警告。
5.3 评估与部署类问题(占总问题27%)
| 问题现象 | 根本原因 | 排查命令 | 解决方案 |
|---|---|---|---|
ONNX inference slower than PyTorch | 未启用TensorRT,用CPU推理 | onnxruntime.get_device() | 安装`on |