1. 项目概述:为什么你需要一个“PyTorch Wrapper”而不是直接写nn.Module?
你有没有在深夜调参时,对着一屏又一屏重复的model = MyNet(); criterion = nn.CrossEntropyLoss(); optimizer = torch.optim.Adam(model.parameters(), lr=3e-4)发过呆?有没有在给新同事讲代码时,不得不从import torch开始,一路解释DataLoader的num_workers为什么设成min(8, os.cpu_count()),而对方眼神已经飘向窗外?有没有在复现三篇论文时,发现每份代码里都藏着几乎一模一样的训练循环——只是print(f"Epoch {epoch} | Loss: {loss.item():.4f}")的格式略有不同?这些不是小问题,是工程熵增的具象化表现。PyTorch Wrapper 就是为解决这个而生的:它不是另一个深度学习框架,而是一层有温度、可配置、带记忆的胶水层,把 PyTorch 原生能力封装成“开箱即用”的构建块。它核心解决三个真实痛点:第一,消除样板代码——把模型定义、数据加载、训练循环、评估逻辑、日志记录、检查点保存这些高度模式化的流程,抽象成几行声明式调用;第二,统一实验接口——让“换一个 backbone”变成改一个字符串(如"resnet50"→"vit_b_16"),而不是重写整个__init__和forward;第三,降低协作门槛——实习生能看懂Trainer(config).fit(),资深工程师能通过继承BaseWrapper注入自定义梯度裁剪策略或混合精度逻辑。它不替代 PyTorch,而是让 PyTorch 的强大能力像乐高积木一样,拧紧螺丝就能拼出完整模型。关键词PyTorch Wrapper、neural networks、model training、deep learning workflow在这里不是标签,而是你每天和 GPU 打交道时,真正想甩掉的那几行重复代码。
2. 整体设计思路与架构选型:为什么是“Wrapper”而不是“Framework”?
2.1 核心哲学:最小侵入性原则
很多初学者看到“Wrapper”第一反应是:“这不就是套壳吗?有啥技术含量?”恰恰相反,真正的 Wrapper 难度在于克制。我见过太多项目,为了“功能完备”,硬生生把torch.nn包裹成一个黑盒,结果用户想改一行loss.backward()的逻辑,就得去翻三层装饰器源码。所以本 Wrapper 的设计铁律只有一条:所有底层 PyTorch 对象必须可访问、可替换、可调试。这意味着model属性永远是原生torch.nn.Module实例,optimizer永远是torch.optim.Optimizer子类,dataloader永远是torch.utils.data.DataLoader。你随时可以执行wrapper.model.encoder[0].weight.data[:] = 0,或者next(iter(wrapper.train_dataloader))查看原始 batch 结构。这种设计不是偷懒,而是对 PyTorch 生态的尊重——它承认 PyTorch 已经足够好,我们只需补足它在工程落地时缺失的“胶水”。
2.2 架构分层:四层洋葱模型
整个 Wrapper 采用清晰的四层洋葱结构,由外向内逐级解耦:
最外层:配置驱动层(Config Layer)
所有行为由YAML或dict配置驱动。例如,config.yaml中model: {name: "efficientnet_v2_s", pretrained: true}这一行,会触发内部自动导入torchvision.models.efficientnet_v2_s并加载 ImageNet 权重。这里的关键是配置即文档——新人不用读源码,看配置文件就能知道当前实验用了什么模型、什么优化器、学习率多少。我们刻意避免@dataclass或pydantic等强类型校验,因为实验过程中参数常需快速试错(比如临时把lr从1e-3改成5e-4),强类型反而成为阻碍。第二层:组件工厂层(Factory Layer)
这是 Wrapper 的“大脑”。它接收配置,按需实例化对象。比如ModelFactory.from_config(config["model"])不是简单eval()字符串,而是维护一个注册表:{"resnet50": ResNet50Builder, "vit_b_16": ViTBuilder}。每个 Builder 类负责处理该模型特有的初始化逻辑——ResNet 要处理pretrained参数,ViT 要处理image_size和patch_size的兼容性。这种设计让新增一个模型(比如刚发布的convnext_v3)只需写一个 Builder 类并注册,无需修改任何训练逻辑。第三层:流程编排层(Orchestration Layer)
这里封装了训练循环的骨架。但它绝不写死for epoch in range(num_epochs)。而是提供train_step(),val_step(),on_epoch_end()等钩子(hook)方法,用户可通过继承BaseTrainer覆盖任意钩子。例如,做半监督学习时,你只需重写train_step()加入一致性正则项;做知识蒸馏时,重写compute_loss()同时计算 student loss 和 KL 散度。所有钩子默认调用 PyTorch 原生 API,比如train_step()默认就是loss = self.criterion(y_pred, y_true); loss.backward(); self.optimizer.step(),你覆盖时也只需写这几行,而非面对一个抽象的self.run_step()。最内层:工具增强层(Utility Layer)
提供真正提升效率的“小确幸”功能:ProgressBar自动适配 Jupyter Notebook 和终端;CheckpointManager智能保存最佳模型(按val_acc)和最新模型(按epoch),并自动清理旧文件;MetricTracker用collections.defaultdict(list)记录每个 epoch 的loss,acc,lr,最后一键生成pandas.DataFrame供绘图。这些功能都不改变核心流程,但省去了 90% 的胶水代码。
2.3 为什么拒绝“全功能框架”?
有人会问:“既然都做了,为什么不做成 Hugging Face Transformers 那样的大框架?”答案很实在:场景错配。Transformers 解决的是 NLP 领域的通用范式(tokenization → model → head),而我们的 Wrapper 面向的是更碎片化的场景——可能今天训一个医学图像分割模型(需要 Dice Loss + 3D CNN),明天训一个时序预测模型(需要nn.LSTM+ 自定义滑动窗口 dataloader)。强行统一会导致两种后果:一是为支持所有场景,API 变得极其臃肿(想想Trainer类里上百个参数);二是为兼容旧模型,不得不保留大量过时设计(如对DataParallel的特殊处理)。Wrapper 的选择是“做减法”:只封装 80% 场景共有的 20% 逻辑,剩下 20% 的长尾需求,留给用户用原生 PyTorch 直接 hack。这就像厨房里的刀——顶级主厨不会要一把能削苹果、切肉、雕花、开瓶的“万能刀”,他只要一把锋利、平衡、手感好的主厨刀,其他活交给专业工具。
3. 核心细节解析与实操要点:从零构建一个可用 Wrapper
3.1 配置系统:YAML 为何比 JSON 更适合实验管理?
配置是 Wrapper 的入口,选型直接影响开发体验。我们坚持用 YAML 而非 JSON,理由非常具体:
- 注释支持:
# 这是学习率,对收敛速度影响极大这样的注释,在实验迭代中价值巨大。当你回看三个月前的配置,注释能瞬间唤醒记忆,而 JSON 里只能靠文件名config_lr_1e-3_v2.json猜测。 - 锚点与别名:YAML 支持
&anchor和*anchor,这对超参搜索极有用。例如:optim: name: "adamw" lr: &base_lr 3e-4 weight_decay: 0.05 scheduler: name: "cosine" T_max: 100 eta_min: *base_lr # 复用 base_lr,避免手误 - 多环境配置:用
---分隔不同环境:
加载时# config.yaml model: name: "resnet18" num_classes: 10 --- # dev environment <<: *default data: batch_size: 32 num_workers: 2 --- # prod environment <<: *default data: batch_size: 128 num_workers: 8OmegaConf.load("config.yaml")会自动合并,开发时用--config-name dev切换。
提示:不要用
json.loads(open("config.json")),而要用OmegaConf库。它能处理变量插值(${oc.env:HOME})、类型安全(cfg.model.num_classes == 10返回int而非str),且错误提示友好——当batch_size写成"32"(字符串)时,它会明确报错Expected int, got str,而不是在DataLoader初始化时报TypeError: 'str' object cannot be interpreted as an integer。
3.2 模型工厂:如何让model: "vit_b_16"自动加载预训练权重?
模型工厂的核心挑战是:如何让字符串映射到可执行逻辑,同时保持扩展性。我们采用“注册-发现”模式,而非硬编码if model_name == "vit_b_16": ...:
# factory/model_factory.py from typing import Dict, Type, Callable from torch.nn import Module class ModelRegistry: _registry: Dict[str, Callable] = {} @classmethod def register(cls, name: str): def decorator(builder_cls: Type): cls._registry[name] = builder_cls return builder_cls return decorator @classmethod def get_builder(cls, name: str) -> Callable: if name not in cls._registry: raise ValueError(f"Unknown model: {name}. Available: {list(cls._registry.keys())}") return cls._registry[name] # 使用示例:注册 ViT @ModelRegistry.register("vit_b_16") class ViTBuilder: def __init__(self, config: dict): self.config = config def build(self) -> Module: from torchvision.models import vit_b_16 model = vit_b_16( weights="DEFAULT" if self.config.get("pretrained", False) else None, image_size=self.config.get("image_size", 224) ) # 替换分类头以匹配目标类别数 num_classes = self.config.get("num_classes", 1000) model.heads.head = torch.nn.Linear(model.heads.head.in_features, num_classes) return model这样做的好处是:新增模型只需加一个文件models/convnext_v3_builder.py,写一个@ModelRegistry.register("convnext_v3")类,然后import models.convnext_v3_builder即可。注册过程完全解耦,主程序无需 import 所有可能的模型 builder。
注意:
weights="DEFAULT"是 torchvision 0.13+ 的新写法,替代了旧版的pretrained=True。如果你用老版本,需做兼容处理:weights=torchvision.models.get_weight("ViT_B_16_Weights.IMAGENET1K_V1") if pretrained else None。这是 Wrapper 必须处理的“版本碎片化”现实——PyTorch 生态更新快,Wrapper 要做的是屏蔽差异,而非要求用户升级。
3.3 数据加载器工厂:为什么num_workers不该写死?
数据加载是训练瓶颈最常见的环节。Wrapper 的DataLoaderFactory不仅创建DataLoader,更根据硬件智能配置:
def create_dataloader(dataset, config: dict) -> DataLoader: # 自动设置 num_workers:最多用满 CPU 核心,但不超过 8(避免 fork 开销) cpu_count = os.cpu_count() num_workers = min(8, max(1, cpu_count // 2)) # 笔记本双核 → 1,服务器 64 核 → 8 # 自动启用 pin_memory:仅当设备是 CUDA 时 pin_memory = config.get("device", "cpu") != "cpu" # 自动设置 persistent_workers:True 可减少 worker 重启开销,但需 num_workers > 0 persistent_workers = num_workers > 0 return DataLoader( dataset=dataset, batch_size=config["batch_size"], shuffle=config.get("shuffle", True), num_workers=num_workers, pin_memory=pin_memory, persistent_workers=persistent_workers, # 其他参数... )关键点在于:这些参数不是配置项,而是推导项。用户配置里只需写batch_size: 64,Wrapper 自动算出num_workers。为什么?因为num_workers的最优值高度依赖硬件(笔记本 vs A100 服务器),写死会导致本地跑得慢,集群上又浪费资源。我们实测过:在 32 核 CPU 上,num_workers=32反而比num_workers=8慢 15%,因为过多进程竞争内存带宽。Wrapper 的职责是把这种经验固化为代码,而非让用户查文档、做实验。
3.4 训练循环:如何让Trainer.fit()既简洁又可控?
Trainer.fit()表面只有一行,背后是精心设计的钩子链:
class Trainer: def fit(self): for epoch in range(self.config["num_epochs"]): self.on_epoch_start(epoch) self.train_epoch() self.val_epoch() self.on_epoch_end(epoch) # 这里触发 checkpoint, log, lr_scheduler.step() def train_epoch(self): for batch in self.train_dataloader: self.optimizer.zero_grad() loss = self.compute_loss(batch) loss.backward() self.on_before_optimizer_step() # 钩子:梯度裁剪、log grad norm self.optimizer.step() self.on_after_optimizer_step() # 钩子:EMA 更新、log lr def compute_loss(self, batch) -> torch.Tensor: x, y = batch y_pred = self.model(x) return self.criterion(y_pred, y)用户扩展方式极其直接:
想加梯度裁剪?继承
Trainer,重写on_before_optimizer_step():class MyTrainer(Trainer): def on_before_optimizer_step(self): torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0)想用混合精度?重写
train_epoch(),插入autocast和scaler:def train_epoch(self): scaler = torch.cuda.amp.GradScaler() for batch in self.train_dataloader: self.optimizer.zero_grad() with torch.cuda.amp.autocast(): loss = self.compute_loss(batch) scaler.scale(loss).backward() scaler.step(self.optimizer) scaler.update()
没有魔法,只有清晰的扩展点。这比某些框架的Trainer(..., fp16=True)更透明——你知道每一行代码在哪执行,出了问题能精准断点。
4. 实操过程与核心环节实现:从配置到训练完成的完整 walkthrough
4.1 第一步:初始化配置(5 分钟)
假设你要训一个 CIFAR-10 分类模型。新建config_cifar10.yaml:
# config_cifar10.yaml model: name: "resnet18" pretrained: false # CIFAR-10 图像小,ImageNet 预训练不一定有益 num_classes: 10 data: dataset: "cifar10" batch_size: 128 val_split: 0.2 # 20% 数据作验证集 transform: train: - name: "random_crop" size: 32 padding: 4 - name: "random_horizontal_flip" p: 0.5 - name: "to_tensor" - name: "normalize" mean: [0.491, 0.482, 0.447] std: [0.247, 0.243, 0.262] val: - name: "to_tensor" - name: "normalize" mean: [0.491, 0.482, 0.447] std: [0.247, 0.243, 0.262] optim: name: "sgd" lr: 0.1 momentum: 0.9 weight_decay: 5e-4 scheduler: name: "step" step_size: 30 gamma: 0.1 trainer: num_epochs: 100 device: "cuda" if torch.cuda.is_available() else "cpu" log_every_n_steps: 10 save_dir: "./checkpoints/cifar10_resnet18"注意几个细节:transform是一个列表,每个字典代表一个torchvision.transforms操作,Wrapper 会自动按顺序组合成Compose;val_split: 0.2表示从训练集中划分 20% 作验证,无需手动准备val文件夹;save_dir路径自动创建,不存在时os.makedirs(save_dir, exist_ok=True)。
4.2 第二步:构建 Wrapper 实例(2 分钟)
from wrapper.trainer import Trainer from wrapper.config import load_config # 1. 加载配置 config = load_config("config_cifar10.yaml") # 2. 创建 Trainer(自动构建 model, dataloader, optimizer...) trainer = Trainer(config) # 3. 查看构建结果(调试必备) print(f"Model: {trainer.model}") print(f"Train dataloader length: {len(trainer.train_dataloader)}") print(f"Optimizer: {trainer.optimizer}") print(f"Device: {trainer.device}")输出类似:
Model: ResNet( (conv1): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) ... ) Train dataloader length: 391 Optimizer: SGD ( Parameter Group 0 dampening: 0 lr: 0.1 momentum: 0.9 nesterov: False weight_decay: 0.0005 ) Device: cuda实操心得:
len(trainer.train_dataloader)是第一个验证点。CIFAR-10 训练集 50000 张图,batch_size=128,应为50000 // 128 = 390.625 → 390,但这里显示391,说明DataLoader默认drop_last=False,最后一轮 batch 只有50000 % 128 = 80张图。这是合理行为,但如果你做分布式训练,可能需要drop_last=True避免 rank 间 batch 数不一致。Wrapper 允许你在配置中加drop_last: true,它会透传给DataLoader。
4.3 第三步:启动训练(1 行命令)
# 开始训练! trainer.fit()训练过程中,你会看到实时进度条:
Epoch 1/100: 100%|██████████| 391/391 [01:22<00:00, 4.75it/s, loss=1.824, acc=0.321] Epoch 2/100: 100%|██████████| 391/391 [01:21<00:00, 4.81it/s, loss=1.542, acc=0.456] ...关键指标loss和acc是自动计算的:loss来自criterion输出,acc是MetricTracker对y_pred.argmax(dim=1)和y_true的匹配率。这些指标在on_epoch_end()时被记录,并在save_dir下生成metrics.csv:
epoch,train_loss,train_acc,val_loss,val_acc,lr 1,1.824,0.321,1.752,0.356,0.1 2,1.542,0.456,1.489,0.472,0.1 ...提示:
metrics.csv是后续分析的黄金数据。你可以用pandas.read_csv("checkpoints/cifar10_resnet18/metrics.csv")加载,一行代码画出训练曲线:df.plot(x="epoch", y=["train_acc", "val_acc"], title="Accuracy Curve")
4.4 第四步:推理与评估(3 分钟)
训练完,用Trainer.predict()做单样本推理:
from PIL import Image import torchvision.transforms as T # 加载一张测试图 img = Image.open("test_cat.jpg").convert("RGB") # 复用训练时的 transform(确保预处理一致) transform = trainer.val_transform # 自动从配置中构建 x = transform(img).unsqueeze(0).to(trainer.device) # 添加 batch 维度 # 推理 with torch.no_grad(): y_pred = trainer.model(x) prob = torch.softmax(y_pred, dim=1) pred_class = prob.argmax().item() confidence = prob.max().item() print(f"Predicted class: {pred_class}, Confidence: {confidence:.3f}")更强大的是Trainer.evaluate(),它自动在验证集上计算所有指标:
results = trainer.evaluate() print(f"Final val accuracy: {results['acc']:.4f}") print(f"Final val loss: {results['loss']:.4f}") # 输出: Final val accuracy: 0.9234, Final val loss: 0.2456evaluate()内部会:
- 重置
MetricTracker - 遍历
val_dataloader - 对每个 batch 调用
compute_loss()和compute_metrics()(后者可扩展,如加 F1-score) - 返回聚合结果字典
4.5 第五步:模型导出与部署(2 分钟)
训练好的模型要部署,Wrapper 提供标准导出接口:
# 导出为 TorchScript(推荐,跨平台) scripted_model = torch.jit.script(trainer.model) scripted_model.save("cifar10_resnet18.pt") # 或导出为 ONNX(兼容 TensorRT, OpenVINO) dummy_input = torch.randn(1, 3, 32, 32).to(trainer.device) torch.onnx.export( trainer.model, dummy_input, "cifar10_resnet18.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}} )导出前,Wrapper 会自动执行model.eval()和torch.no_grad(),避免训练模式残留。这是新手最容易忽略的坑:用model.train()状态导出的模型,在推理时 BatchNorm 和 Dropout 行为异常,导致精度暴跌。Wrapper 把这种“常识”变成了强制约束。
5. 常见问题与排查技巧实录:那些踩过的坑和速查表
5.1 “CUDA out of memory” —— 显存爆炸的 5 种根因与对策
显存不足是 Wrapper 用户最高频问题。我们整理了 5 种典型场景及对应方案,按发生概率排序:
| 现象 | 根因 | 快速诊断命令 | 解决方案 |
|---|---|---|---|
| 训练初期就 OOM | batch_size过大,或模型太大(如 ViT 在 224x224 输入) | nvidia-smi查看初始显存占用 | 1. 配置中batch_size减半2. model.image_size降为192(ViT)或224→160(CNN)3. 启用梯度检查点: config.model.gradient_checkpointing: true |
| 训练中显存缓慢增长 | DataLoader的num_workers过高,导致 worker 进程内存泄漏 | htop查看 CPU 进程内存是否持续上涨 | 1.config.data.num_workers: 0(禁用多进程)2. 或 config.data.persistent_workers: false |
| 验证阶段 OOM | val_dataloader的batch_size未缩小,验证时无梯度但模型仍驻留显存 | torch.cuda.memory_allocated()打印训练/验证前后显存 | 1. 配置中data.val_batch_size: 256(验证 batch 可比训练大 2 倍)2. Trainer.evaluate()内部自动torch.cuda.empty_cache() |
| 混合精度训练 OOM | autocast未正确关闭,或GradScaler未update()导致 scale 值过大 | print(scaler.get_scale()),正常应在65536附近浮动 | 1. 确保scaler.step(optimizer)后必有scaler.update()2. scaler = GradScaler(init_scale=65536)显式初始化 |
| 模型保存时 OOM | torch.save()保存整个Trainer对象,包含dataloader等大对象 | print(sys.getsizeof(trainer)) | 1.只保存模型权重:torch.save(trainer.model.state_dict(), "model.pth")2. 或用 torch.save({"state_dict": trainer.model.state_dict()}, "model.pth") |
实操心得:我们曾在一个 ViT 实验中遇到“训练 10 个 epoch 后 OOM”,
nvidia-smi显示显存从 8GB 涨到 12GB。最终定位是persistent_workers=True时,worker 进程未正确释放缓存。解决方案不是关掉persistent_workers(它能提速 15%),而是加一行torch.cuda.empty_cache()在on_epoch_end()结尾。Wrapper 已内置此修复,但如果你自定义 Trainer,务必记得。
5.2 “Loss is NaN” —— 梯度爆炸的 3 个隐藏开关
NaN loss 是训练失败的“无声杀手”。它往往在第 100 个 batch 才出现,难以复现。我们总结了 3 个最隐蔽的开关:
学习率过高 + AdamW 的 weight_decay:AdamW 的
weight_decay是在param上直接加,而非像 SGD 那样在梯度上加。当lr=0.01且weight_decay=0.05时,param -= lr * (grad + weight_decay * param),如果param很大(如初始化偏差),这一项会主导更新,导致参数发散。对策:optim.weight_decay: 1e-4(比 SGD 低一个数量级),或改用optim.name: "adam"(传统 Adam)。BatchNorm 的
running_mean/running_var未初始化:当batch_size=1时,BN 的running_var可能为 0,导致1/sqrt(running_var)除零。Wrapper 在ModelFactory中强制对 BN 层调用model.apply(lambda m: setattr(m, 'running_var', torch.ones_like(m.running_var)) if isinstance(m, torch.nn.BatchNorm2d) else None),确保初始方差为 1。自定义损失函数未处理数值稳定性:比如用
nn.BCELoss时,y_pred是 raw logits,需先torch.sigmoid();若直接喂logits,log(0)会产出-inf,再nan。对策:Wrapper 的CriterionFactory对bce类损失自动包装nn.BCEWithLogitsLoss,它内部已做sigmoid + log的数值稳定实现。
5.3 “Accuracy stuck at 10%” —— 数据管道的 4 个致命陷阱
CIFAR-10 有 10 类,随机猜测准确率 10%。如果训练半天卡在这里,90% 是数据问题:
标签未对齐:
Dataset.__getitem__()返回(img, label),但label是字符串(如"cat")而非整数索引。Wrapper 的DataLoaderFactory会检测isinstance(label, str),并自动构建LabelEncoder,但前提是label在所有样本中出现过。对策:在config.data.dataset: "cifar10"时,Wrapper 内置了标准CIFAR10类,确保标签是0-9整数。图像通道错乱:PIL 读取的 RGB 图,被
ToTensor()转为C x H x W,但某些模型(如 OpenCV 加载的)可能是 BGR。Wrapper 的transform链中,ToTensor()前加了ConvertImageDtype(torch.float32),确保类型一致;若你用自定义 transform,务必确认img = img.convert("RGB")。归一化参数错误:CIFAR-10 的
mean/std是[0.491, 0.482, 0.447]/[0.247, 0.243, 0.262],若错用 ImageNet 的[0.485, 0.456, 0.406],模型输入分布偏移,特征提取失效。Wrapper 的TransformFactory对常用数据集(CIFAR, MNIST, ImageNet)内置了正确参数,配置中data.transform.train[3].mean会自动覆盖。数据增强破坏语义:
RandomRotation(180)对文字识别可行,但对猫狗分类,倒置的猫不是猫。Wrapper 的transform注册表中,"random_rotation"默认degrees=15,而非180。你可以在配置中显式覆盖:- name: "random_rotation", degrees: 15。
5.4 配置与代码协同工作速查表
| 你想实现 | 配置写法 | 代码中如何访问 | 注意事项 |
|---|---|---|---|
| 只训最后两层 | model.freeze_layers: ["layer3", "layer4"] | ModelFactory.freeze_layers(model, config["model"]["freeze_layers"]) | freeze_layers是 list,支持正则(如"layer.*") |
| 自定义学习率衰减 | scheduler.name: "custom" | CustomLRScheduler(optimizer, config) | 需实现get_lr(epoch)方法 |
| 多卡 DDP 训练 | trainer.distributed: true | DistributedTrainer(config)自动 wrap model | 需torchrun --nproc_per_node=2 train.py启动 |
| 早停(Early Stopping) | trainer.early_stopping: {patience: 10, monitor: "val_acc", mode: "max"} | EarlyStoppingCallback钩子 | monitor必须是MetricTracker记录的 key |
| TensorBoard 日志 | trainer.logger: "tensorboard" | TensorBoardLogger(save_dir) | save_dir下自动生成runs/目录 |
最后一个小技巧:当配置越来越复杂,建议用
OmegaConf.to_yaml(config)打印最终生效的配置(含所有默认值和插值),而不是只看原始 YAML 文件。因为config.data.batch_size可能被config.trainer.batch_size覆盖,OmegaConf的 merge 逻辑有时反直觉。打印出来,一眼看清真相。
6. 进阶应用与领域适配:不止于图像分类
6.1 时序预测:如何用 Wrapper 训 LSTM?
时序预测和图像分类的数据流完全不同:没有DataLoader的shuffle,输入是滑动窗口序列。Wrapper 通过DatasetFactory的扩展点支持:
# config_timeseries.yaml data: dataset: "timeseries" window_size: 128 # 用前 128 步预测下一步 horizon: 1 # 预测未来 1 步 # 无需 transform,时序数据通常已归一化DatasetFactory检测到dataset: "timeseries",会返回TimeSeriesDataset,其__getitem__返回(x_window, y_future),其中x_window形状为(128, features)。Wrapper 的Trainer不关心x是图像还是序列,只要model(x)输出y_pred,criterion(y_pred, y_true)就能算 loss。我们实测过:用config.model.name: "lstm",Wrapper 自动构建nn.LSTM(input_size=10, hidden_size=64, num_layers=2),训练 50 个 epoch,MAE 下降 32%。**Wrapper 的威力