上一篇文章把数据准备的过程写完这篇该聊聊模型准备。很多人一听到“模型准备”第一反应就是下载一个预训练权重完事。但实际在 RetinaNet 这种需要改类别数的检测模型上下载权重只是最表面的动作。真正的模型准备包含代码库选型、环境搭建、权重加载、参数修改和最小闭环验证一整套下来才算把模型“准备好”。我建议所有用 RetinaNet 训练自己数据的读者在正式投入训练前至少把这篇里的步骤过一遍尤其是准备阶段就跑通一次最小迭代能帮你省下后面排查问题的天数。这个阶段解决的核心问题其实很清晰模型能不能跑起来能不能有效地按你的数据开始训练。适合谁来参考一种是刚接触目标检测、想用 RetinaNet 复现论文效果的初学者另一种是已经准备好数据集、正准备进入训练阶段的项目开发者。不管你是哪种这篇都尽量用能落地的方式讲很多坑我会直接告诉你“千万不要这么干”。1. 模型准备到底在准备什么1.1 先搞清楚 RetinaNet 的骨架才知道改哪里在做任何操作之前我建议先花十分钟把 RetinaNet 的结构在脑子里过一遍不然后面改代码完全靠猜。RetinaNet 是典型的一阶目标检测器核心由三块组成主干网络 backbone、特征金字塔 FPN 和两个并行的输出子网络分类子网络和回归子网络。主干网络通常是 ResNet50 或 ResNet101负责从输入图像中提取多尺度特征FPN 在主干输出的多层特征图上做特征融合让检测头在不同尺度下都能拿到足够信息而两个子网络则分别在每个特征层上执行“这个位置有没有目标、是什么类”和“目标框在哪”的预测。为什么要理解这个结构因为在模型准备阶段你一定会改一个参数叫num_classes。这个参数改下去真正影响的是分类子网络最后一层卷积的输出通道数。有些实现里分类头输出的是anchor数 × 类别数有些则需要在类别数里额外加一个背景类。如果你不知道模型内部怎么组织的改了参数之后报什么错都不奇怪更别提去排查了。1.2 模型准备不只是下载权重我见过很多人在这一步吃了亏。他们在网上找了个预训练权重下载下来放在目录里就觉得自己准备好了。等到训练脚本一跑要么加载报错要么训练出的模型不收敛最后才发现权重根本没加载上。完整的模型准备应该包含下面几件事代码库选择和安装决定你用哪个 RetinaNet 实现并要求它能正常运行。预训练权重获取是根据代码库配套的 COCO 权重还是只加载主干网络在 ImageNet 上训练好的权重。类别数等关键参数修改改成你自己的数据集类别不然后面训练出来的模型没法用。数据加载路径配置让代码能找到你已经准备好的标注文件。最小闭环验证用一次简短的训练迭代证明模型能加载、能前向、能反向、能保存权重。你可以把模型准备理解成“训练前的最后一道检查”。后面一旦开始训练你肯定不希望自己被一个低级的权重加载问题卡住那种问题定位起来既烦人又容易让人自我怀疑。1.3 先跑通再调优这个原则能帮你省一周时间很多初学者特别喜欢直接从“调参”开始这个想法本身没有问题但前提是基础链路已经跑通。RetinaNet 这类模型涉及很多环境相关依赖哪怕是同一个代码库在不同机器、不同版本的 PyTorch 下都会有微妙差异。如果连一次迭代都没跑通后面出现的任何一个 loss 异常、显存溢出的问题你都得先去排查是不是环境问题这会浪费大量时间。所以我一直强调模型准备阶段的目标不是“训练得好”而是“训练链路通”。哪怕第一次跑迭代的时候 loss 高达好几千甚至训练一会儿就崩了只要你能确认模型结构加载正确、数据能正常喂进去、反向传播能执行模型准备就算成功了。后面真正调参的时候你才能把精力集中在优化模型本身而不是在跟环境较劲。2. 选型代码库和权重文件怎么选才不至于后面反复踩坑2.1 代码库选型轻量库更适合作学习和微调RetinaNet 的实现很多主流的选择大致有三个方向Facebook 的 Detectron2、MMDetection 社区、以及偏轻量的 PyTorch 实现比如 yhenon/pytorch-retinanet。它们的风格和对新手友好度的差异相当大。Detectron2 和 MMDetection 都是非常完整的检测框架功能丰富支持多模型、多任务也提供了大量训练技巧。但问题也很明显框架复杂度高、依赖链长、安装时间久。如果你只是想把 RetinaNet 用在自己的数据集上花大量时间去学习框架本身的机制在模型准备阶段是得不偿失的。轻量实现则正好反过来。代码结构清楚改动点集中非常适合想知道“每一行代码在做什么”的人。我自己在训练自定义数据时最常用的就是轻量版的 PyTorch RetinaNet。它支持 CSV 格式和 COCO 格式的数据集训练脚本也相对简洁改路径、改类别数、加载权重都直观。我的建议是如果项目最终要上线、要大量并行的实验管理那我推荐直接上 MMDetection它的生态确实省心但如果只是把 RetinaNet 作为基础模型先让你的数据跑出结果、理解算法行为那轻量库会是更高效的选择。有一个忠告是不要在模型准备阶段反复更换代码库。每个实现的逻辑细节不一样换来换去很容易造成配置混乱。2.2 预训练权重怎么选ResNet50 版 COCO 权重是大多数情况的首选选定代码库之后下一步是下载预训练权重。这里我明确推荐从 COCO 数据集上训练好的 ResNet50 版权重作为起点而不是随机初始化也建议优先于 ResNet101。原因其实很简单。ResNet50 作为主干网络在速度、显存占用和效果之间处于一个很平衡的位置。除非你的数据自带大量细节纹理或者你对推理速度不太敏感否则 ResNet101 带来的精度提升通常不够覆盖它多出来的显存和时间成本。尤其当你只是训练自己的特定场景数据样本量大概率远小于 COCOResNet50 已经足够拟合。COCO 预训练权重的价值在于迁移学习。COCO 数据集里包含 80 类常见目标这个模型已经学到了大量通用的视觉特征比如边缘、纹理、形状甚至一些中层的目标部件概念。你拿它作为起点再微调到自己的数据上收敛速度会明显快于从零随机初始化。你可以对比一下同样的迭代次数加载预训练权重的模型 loss 下降会平滑很多。顺带提醒一下如果你用 torchvision 官方提供的 ResNet50 权重它的state_dict和轻量 RetinaNet 模型的 key 结构并不完全一致。很多库本身会在初始化时使用 torchvision 的权重来加载 backbone 部分但整体上更省事的做法是直接下载作者在 README 里提供的、已经在 COCO 上完整训练过的 RetinaNet 权重。这种权重不仅包含 backbone 的参数还包括 FPN 和检测头部分的参数微调初期会更稳定。2.3 权重文件格式、来源和兼容性PyTorch 里权重文件后缀常见为.pth或.pth.tar本质是用torch.save序列化得到的对象里面可能是整个模型model也可能只是state_dict。模型准备阶段需要搞清楚的另一个问题你手里的权重到底是从哪个仓库或分支下载的举一个最常见的坑同一个 RetinaNet 模型不同实现里参数名的命名规则可能不一样。比如某些实现会用backbone.body.layer1这种前缀但另一些可能直接用layer1。如果你把 A 代码库训练出的权重直接用 B 代码库加载哪怕模型结构基本一致也会出现 key 不匹配直接报size mismatch或Missing key(s)。遇到这种情况第一反应不要是“代码是不是写错了”而是去检查权重到底属于哪个代码库。作者给的权重通常只保证在自己仓库里能加载跨库复用需要你自己写兼容代码。模型准备阶段最忌讳的就是在权重格式兼容性上花太久时间建议优先使用代码库自带或官方指定的权重链接。3. 实操从零准备一个可训练模型3.1 环境准备少走弯路的第一步模型准备阶段最先要落地的是环境。RetinaNet 训练对硬件要求不算夸张但一块支持 CUDA 的 NVIDIA GPU 基本是必须的CPU 训练实在效率太低不值得尝试。显卡显存至少 6GB 以上会更从容我用 8GB 显存训练 ResNet50 也完全可行只是 batch size 别开太大。软件环境建议直接用 Anaconda 管理环境不要图省事装进系统 Python。创建独立环境既避免和别的项目互相污染后面复现问题也方便重装。conda create -n retinanet python3.8 conda activate retinanetPyTorch 版本我一般选比较稳定的版本不需要追新。安装命令可以在 PyTorch 官网根据自己的 CUDA 版本生成。以 CUDA 11.8 为例pip install torch2.1.2 torchvision0.16.2 --index-url https://download.pytorch.org/whl/cu118接着安装项目依赖常见的有 OpenCV、Numpy、pycocotools 等。不同代码库依赖不同你可以在代码库的requirements.txt里看到清单直接执行如下命令即可pip install opencv-python numpy pycocotools有个经验要说一下安装完环境后先在终端里跑一句python -c import torch; print(torch.cuda.is_available())确保 PyTorch 能看到 GPU。很多模型准备阶段的报错根源其实是环境没配好早点确认能省很多事。3.2 下载预训练权重并整理目录我习惯把训练相关的文件都组织在同一个项目目录里这样路径清晰训练脚本不用到处找文件。目录结构大致如下retinanet-project/ ├── datasets/ │ ├── Annotations/ │ ├── JPEGImages/ │ └── classes.csv ├── weights/ │ └── retinanet_resnet50_coco.pth ├── train.py └── ...weights目录专门用来放预训练权重。下载权重时优先使用代码库 README 里提供的下载链接一般会用 wget 或者直接在浏览器下载。以常见的pytorch-retinanet为例wget -O weights/retinanet_resnet50_coco.pth https://github.com/yhenon/pytorch-retinanet/releases/download/0.5.1/retinanet_resnet50_coco.pth下载完成后最好检查一下文件大小是否和说明一致有些下载工具断了会留下残缺文件加载时才报错就很被动了。如果文件特别大可以顺手记一下它的 MD5下载源通常会提供校验值。虽然这个操作不起眼但曾经帮我在准备阶段就发现了坏权重而不是等到训练跑了一半才各种奇怪报错。权重文件放哪其实没有硬性要求只要后面训练脚本里路径写对就能加载。但集中管理的好处是如果你同时测试多个预训练权重可以清晰地对比不容易搞混。3.3 修改关键配置类别数、路径和跑通参数模型准备阶段最常见的代码改动就是三处数据路径、类别列表或者类别数量、训练脚本中的基础参数。先说数据路径。很多 RetinaNet 实现的数据加载器需要接收 CSV 格式的标注文件或 COCO 格式的 JSON 文件。在模型准备阶段至少要让数据加载器能成功解析你的标注文件否则训练根本启动不了。举个例子轻量版常见的调用方式train_dataset CsvDataset(datasets/train_annotations.csv, datasets/classes.csv, transformtransform)如果文件路径写错跑训练时会直接报 FileNotFoundError这个很好排查。真正容易出问题的是classes.csv这种类别文件里面每一行是一个类别名和类别索引如果索引不从 0 开始或者类别顺序和标注文件不一致后面训练出的模型预测结果类别对不上问题非常隐蔽。然后是类别数。我见过不少人在 RetinaNet 里把num_classes设成“自己的类别数加 1 背景”结果报告里的模型输出维度怎么都对不上。不同实现对背景类的处理方式不一样建议直接去代码里看分类头最后一层卷积的定义它的输出通道数就是代码期待的num_classes。某些情况下输出通道数可能等于anchor 数量 × 类别数这时的类别数通常不包含背景类。在轻量版代码里通常这样加载带预训练权重的模型from retinanet.model import resnet50 num_classes len(classes) # classes是类别列表 model resnet50(num_classesnum_classes, pretrainedTrue)有的实现里pretrainedTrue会加载 COCO 权重有些需要手动传入权重路径。这里强烈建议读一下代码库 README 或者对应函数源码不要凭空猜。如果模型的输出层和权重维度因为类别数不同而不一致加载权重时一般会报size mismatch这其实是正常的关键是怎么处理下一节我会专门说。最后的训练基础参数这里不用过多纠结。设置一个比较小的 batch size比如 2 或 4然后确认日志能正常打印就足够验证了。3.4 一次最小训练迭代验证模型准备成功环境配置好了、权重下载好了、参数也改了但真正判断模型准备是否完成靠的是一次最小训练闭环验证。我通常会在正式训练前强制自己跑一个差不多 1 个 epoch 的短训练有时候甚至只跑几个 step。具体做法很简单直接启动训练脚本但把 epoch 数调小batch size 调小python train.py --epochs 1 --batch_size 2跑起来之后有几个现象需要逐一确认。第一日志里能看到每个 step 的 loss 值而且 loss 是一个正常数值不是 NaN第二GPU 显存有占用且没有出现 OOM 报错第三如果代码库支持定期保存 checkpoints能正常生成权重文件第四训练结束后能顺利读取 checkpoint 继续训练。这一步的目的不是看到 loss 下降到合理范围而是把从“数据加载 → 模型前向 → 损失计算 → 反向传播 → 参数更新 → 权重保存”这些环节全部走一遍。只要链路是通的后面训练阶段无论出现什么问题你都能大概率确定是自己后续修改引入的而不是基础设施的问题。我在实际工程里甚至会用“几张图片 一个 step”的方式做烟雾测试确保代码修改没有破坏基本逻辑。这个习惯从模型准备阶段养成能省下大量调试时间。4. 常见报错与排查技巧4.1 权重加载报错 size mismatch 才是主旋律模型准备阶段遇到最多的报错就是size mismatch。表象是加载权重时某个层的要求尺寸和权重文件里的尺寸不一致。比如模型期望分类头输出是20类但权重的分类头输出是80 1类那最后一层肯定不匹配。解决这个问题之前先搞清楚哪些层应该保留哪些层必须丢弃。主干网络和 FPN 部分的参数大概率可以直接复用分类头的最后一层因为类别数变了必须丢弃或用随机初始化覆盖。这里可以先用strictFalse加载权重让 PyTorch 忽略不匹配的 keymodel.load_state_dict(torch.load(weights/retinanet_resnet50_coco.pth), strictFalse)但注意strictFalse不是万能钥匙。它会把匹配的层都加载上不匹配的层保持初始化值。如果你明明应该修改分类头结果因为某个参数名对不上导致整个 backbone 都没加载上这时候strictFalse反而会掩盖问题。所以我建议第一次加载时报错之后先打印一下权重文件里有哪些 key再打印模型当前有哪些 key对比一下差异checkpoint torch.load(weights/retinanet_resnet50_coco.pth) print(checkpoint.keys()) print(model.state_dict().keys())通过这种方式你能清楚地看到是只有分类头不匹配还是所有 key 都不对。如果所有 key 都不对那多半是权重来源和代码库不配套该换权重就换权重不要勉强写一堆转换脚本性价比太低。还有一个秘密坑如果权重是在多卡环境下保存的state_dict中的所有 key 都会带module.前缀例如module.backbone.body.conv1。单卡加载时直接会报Missing key(s)。处理方式可以在加载前把前缀剥离掉state_dict torch.load(weights/retinanet_resnet50_coco.pth) from collections import OrderedDict new_state_dict OrderedDict() for k, v in state_dict.items(): if k.startswith(module.): k k[7:] new_state_dict[k] v model.load_state_dict(new_state_dict, strictFalse)这种问题在跑过 DDP 训练的权重上特别常见记下来能少折腾很久。4.2 CUDA 显存溢出和 OOM显存溢出在训练目标检测模型时很常见特别是第一次在低显存显卡上跑 ResNet50 时候。RetinaNet 本身因为有 FPN 和两个分支子网络显存占用量比普通分类模型要大不少。遇到CUDA out of memory时最直接的办法是把 batch size 调小比如从 8 调到 2。如果还溢出可以考虑把输入图像尺寸也缩一缩但要注意训练时缩尺寸和推理时最好保持一致不然精度会受影响。另一个容易忽略的因素是num_workers。有时候 DataLoader 的 worker 数量开太大会占用额外内存也会干预到显存分配。在模型准备阶段可以先设置num_workers0排除掉数据加载的干扰。等确认模型能稳定训练再慢慢调大。跑之前建议开一个终端执行nvidia-smi -l 1实时看显存变化。如果显存一直在累积增长而不是到达一个峰值后稳定那很可能是代码里有张量未被释放。不过在自定义数据显示这个问题的概率比较低更多时候还是 batch size 太大。4.3 数据集加载崩溃或标签错误数据加载阶段常见的报错类别很多比如IndexError: list index out of range、AssertionError、FileNotFoundError等。这些报错最常出现在标签文件格式不对。比如 RetinaNet 的 CSV 格式标注中标注框的坐标顺序通常是x1,y1,x2,y2但有些工具导出的是x,y,w,h如果混用训练时损失计算虽然能跑但 Bounding Box 回归方向就是错的模型学出来的框完全不对。遇到这类问题模型准备阶段强烈建议先打开一张图片和它的标注框用 OpenCV 画出来看一眼确认坐标系的正确性。这个步骤虽然看起来是在做数据检查其实属于模型准备的一部分。因为坐标反了、类别索引错了这类问题训练过程中看不出来等推理阶段才会发现结果一团糟届时的排查成本远比现在高得多。代码层面可以增加一个小函数直接解析一张图片的标注并可视化这里就不再贴具体代码了网上的工具很多用最顺手的一种就行。4.4 常见问题速查表我把模型准备阶段最常遇到的一批问题和解决方向整理成表方便你照着排查。问题现象可能原因解决方向加载权重时报 size mismatch分类头类别数不同或权重来自其他实现打印 key 对比只加载匹配层或更换配套权重训练启动后 CUDA out of memorybatch size 过大 / 输入分辨率过高调小 batch size必要时降低输入尺寸先排除 data loader 内存干扰训练日志完全没输出数据加载卡死或配置路径错误检查数据集文件是否存在尝试 num_workers0loss 在前几步变成 NaN学习率过大或标注文件存在越界框坐标调小学习率检查标注框坐标是否在图像尺寸范围内模型训练很久 loss 不下降预训练权重没真正加载成功确认加载日志没有 Missing key评估加载后的权重参数分布权重 key 带 module. 前缀权重来自多卡训练加载前剥离前缀再 load_state_dict数据加载正常但类别对不上classes.csv 顺序与标注文件不一致统一类别文件的索引顺序并从数据可视化层面复核这张表我每次搭新环境都会看一眼尤其是前两条几乎每次都能命中一个。我个人在模型准备阶段最大的体会是不要急于启动一个轰轰烈烈的完整训练先耐住性子把最小闭环验证做扎实。很多失败的项目追根溯源都是模型准备阶段太潦草后面训练出的问题根本分不清是数据问题、环境问题还是模型问题。如果你在准备阶段就把所有底层链路确认清楚后面调参才谈得上“科学调参”而不是靠运气跑模型。这个习惯我在很多项目里都受益也推荐你试试。