ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

EfficientDet在PyTorch中的目标检测:原理、训练与优化

EfficientDet在PyTorch中的目标检测:原理、训练与优化 简介这是一套基于PyTorch的EfficientDet目标检测实战代码包面向具备一定深度学习基础、想在目标检测与模型调优上快速落地的开发者与研究者兼顾入门学习与实际部署。围绕EfficientNet骨干网络和BiFPN双向特征金字塔资源提供了完整的网络结构定义、损失函数计算、训练与验证流程并配套预训练权重、数据预处理和图像增强脚本便于在不同数据集上复现和微调。包体共452个文件整体约19.85MB其中json配置文件占绝大多数py源码为模型核心代码ini与pyc多为环境配置和编译产物另有md说明、txt文档和pth权重文件目录结构清晰便于按需查阅和边看边改。目前已有289人学习下载既能作为理解多尺度特征融合与复合缩放的入门范例也能为使用SGD、Adam等优化器进行超参数实验提供可执行模板。1. EfficientDet 在 PyTorch 里的定位一个结构巧、参数少的检测器EfficientDet 是 Google 提出的一族目标检测模型卖点不是单纯跑得快而是用复合缩放公式把输入分辨率、主干网络、BiFPN 层数和通道数一起放大或缩小让同一套代码从轻量部署到高精度科研基线都能覆盖。PyTorch 生态里以 efficientdet-pytorch-master 命名的实现通常把网络定义、数据加载和训练脚本打包在一起开箱即用适合作为对比试验的基准模型。它的核心价值在于想验证检测优化思路比如改 NMS 策略、换特征融合方式、调 anchor 分布EfficientDet 的参数化结构比 YOLO 直观比 Faster R-CNN 轻量。下面按网络结构、最小训练流程、训练与推理优化、小目标实测验证四个层面展开这条路径适合有一到两年 PyTorch 目标检测经验、想深入理解检测器内部权衡的开发者。2. 先看懂 EfficientDet 的四个骨架BiFPN、复合缩放、Anchor 与检测头2.1 从 FPN 到 BiFPN特征融合为什么是双向的传统 FPN 的做法是将主干网络产生的多尺度特征做一次自上而下的路径增强也就是先把高层语义特征上采样再与低层细节特征逐元素相加。这种单向结构有一个明显短板高层特征只影响低层输出一次而低层信息在反向路径中对高层几乎不产生作用。EfficientDet 用的 BiFPNBidirectional Feature Pyramid Network在同样计算量下增加了一条自下而上的路径让低层纹理信息也能传回高层然后再做一次自上而下融合。在 PyTorch 实现里BiFPN 最核心的部分不是那条双向路径本身而是可学习的加权融合。普通相加默认每个输入特征的重要性是 1:1但实际场景中不同分辨率的特征对最终检测结果的贡献并不一样EfficientDet 给每个输入学习一个非负权重然后做归一化加权求和import torch import torch.nn as nn import torch.nn.functional as F class WeightedFeatureFusion(nn.Module): def __init__(self, num_inputs: int, epsilon: float 1e-4): super().__init__() # 每个输入特征一个可学习权重初始为 1 self.weights nn.Parameter(torch.ones(num_inputs, dtypetorch.float32)) self.epsilon epsilon def forward(self, *features): # ReLU 保证权重非负避免符号反转破坏特征语义 w F.relu(self.weights) weighted_sum sum(wi * f for wi, f in zip(w, features)) return weighted_sum / (w.sum() self.epsilon)这里用relu约束权重非负分母加上epsilon防止除零。与 softmax 归一化相比这种分式归一化在反向传播时对每个维度的梯度是解耦的训练更稳定这也是论文里推荐的快速融合近似方案。如果你要在自己的检测头里做多尺度融合可以直接把torch.add或torch.cat替换成这一个模块一般情况下能稳定提升 0.3 到 0.5 个 mAP 点代价只是每个融合节点多了一个标量参数。2.2 复合缩放D0 到 D7 怎么选以及对应的 PyTorch 参数EfficientDet 的另一个创新是复合缩放Compound Scaling。单独放大输入分辨率或主干宽度都会很快遇到收益递减因为特征图尺寸变大后感受野和通道容量不匹配算力被浪费在冗余计算上。论文给出的解法是用一个统一系数 φ 按固定比例同时缩放四个维度主干网络宽度、输入分辨率、BiFPN 通道数和层数由此得到 D0 到 D7 八个变体变体输入分辨率BiFPN 通道数BiFPN 层数主干网络COCO mAP约D0512643B034.6D1640884B140.5D27681125B243.0D38961606B347.5D410242247B449.4D512802887B550.7在 PyTorch 的具体实现中这几个数字通常集中放在一个字典或配置类里训练时通过--compound_coef选择compound_coef 0 # 对应 D0 config { D0: {input_size: 512, bifpn_channels: 64, num_bifpn_layers: 3, backbone: efficientnet-b0}, D1: {input_size: 640, bifpn_channels: 88, num_bifpn_layers: 4, backbone: efficientnet-b1}, } cfg config[list(config.keys())[compound_coef]]选型的经验规则是显存 8GB 以下先跑 D0用 512 分辨率把数据和代码链路打通16GB 显存可以尝试 D1 或 D2想拿高精度再上 D4 以上。注意输入分辨率并不是越高越好分辨率翻倍的计算量增加是平方级的而 mAP 提升通常在 1 到 2 个点之间对实时性要求高的场景往往得不偿失。2.3 Anchor 设计每个位置几个框、尺寸比例与正负样本匹配EfficientDet 的检测头沿用 RetinaNet 的思路在 P3 到 P7 五个特征层上分别预测。每个特征图位置预设 9 个 anchor由 3 种尺度和 3 种长宽比组合而成。以输入分辨率 512 为例P3 到 P7 的 stride 分别是 8、16、32、64、128五个层加起来约五千个位置再乘以 9总共约四万九千个 anchor。这个数量比 YOLO 可预测的训练锚框多但比 Faster R-CNN 的 RPN 少是精度和召回之间的折中。anchor 生成参数在代码里是这样体现的def generate_anchors(stride: int, scales(2.0, 2**(1/3), 2**(2/3)), ratios(0.5, 1.0, 2.0)): anchors [] for s in scales: for r in ratios: w stride * s * (r ** 0.5) h stride * s / (r ** 0.5) anchors.append((w, h)) return torch.tensor(anchors) # shape: (9, 2)正负样本匹配采用 IoU 阈值策略与某个 ground truth 的 IoU 大于 0.5 的 anchor 记为正样本小于 0.4 的记为负样本介于两者之间的在训练中被忽略。如果你在自定义数据集上发现小目标漏检严重优先检查 0.4 到 0.5 这个区间或者给低层特征单独分配更小的 anchor。遥感图像常见细长目标可以把ratios扩展到 (0.2, 0.5, 1.0, 2.0, 5.0)行人检测则保留 0.4 和 0.6 附近的比值改动时只动这两个元组即可不需要重写模型结构。3. 用 efficientdet-pytorch-master 的代码结构跑通最小训练流程3.1 仓库解压后的目录结构与依赖环境以 EfficientDet 这类 PyTorch 项目常见组织方式来看代码解压后一般包含train.py作为训练入口src目录放网络结构和数据集类backbones存放 EfficientNet 预训练权重logs或weights目录用来放训练产生的 checkpoints。拿到代码第一件事不是直接跑训练而是先确认 PyTorch 和 CUDA 的匹配关系。EfficientDet 依赖torchvision的预训练权重和pycocotools计算 mAP常见组合是 Python 3.8 以上、PyTorch 1.10 以上。我一般用 conda 单独建一个环境避免污染其他项目conda create -n effdet python3.8 conda activate effdet # 先装对应 CUDA 版本的 PyTorch再装项目依赖 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118 pip install pycocotools opencv-python tqdm tensorboard装完后用一段最小代码验证环境python -c import torch; t torch.randn(2,3,512,512).cuda(); print(torch.cuda.get_device_name(0))这一步能同时确认 PyTorch 的 CUDA 支持、显存可用性和基础张量操作没问题。如果看到NVIDIA driver too old之类的报错说明 PyTorch 版本与驱动不匹配通常回退一个 PyTorch 小版本就能解决不需要重装驱动。注意--index-url指定的 CUDA 版本要和本机驱动对应否则训练时会出现CUDA error: no kernel image available的诡异报错。3.2 数据准备把 VOC 或自定义数据集转换成训练目录EfficientDet 训练脚本通常默认接受 VOC 格式数据因为 VOC 的标注是单个 XML 文件处理和校验都直观。数据集目录长这样data/VOCdevkit/VOC2007/ ├── JPEGImages/ # 所有图片jpg/png 均可 ├── Annotations/ # 每个图片对应的 XML 标注 ├── ImageSets/Main/ # train.txt、val.txt每行一个文件名 └── labels.txt # 类别名每行一个顺序与训练一致如果你手里是 COCO 格式的 JSON 标注先用脚本转换一次把category_id映射成labels.txt的索引。注意 COCO 的类别 id 是从 1 开始且不连续转换时必须做重映射否则训练时类别索引和模型的输出层对应不上loss 会一路飘红但模型实际没在学。训练时加载数据的标准调用是python train.py \ --dataset VOC \ --data_path data/VOCdevkit \ --project test_run \ --batch_size 8 \ --num_epochs 50 \ --compound_coef 0这个命令的逻辑是--dataset指定数据格式--data_path指向包含VOC2007子目录的根路径--project控制日志和权重文件保存的前缀--compound_coef 0选择 D0 模型。第一次跑建议把--num_epochs降到 5只看 forward 和 backward 是否正常确认 loss 数值在下降后再设置真实训练轮数。3.3 训练脚本的常用参数与 loss 曲线的读法训练阶段你会反复用到的参数可以整理成一张表参数作用经验取值--lr初始学习率1e-3配合 warmup--batch_size每个 step 的图片数显存允许下尽量大--num_workersDataLoader 线程数4~8--decay_rate学习率衰减系数0.1~0.5--save_interval每隔多少轮保存一次权重10~20 epoch--resume从指定 checkpoint 继续训练中断恢复时必用EfficientDet 的 loss 由三部分组成分类损失Focal Loss、框回归损失Smooth L1和总损失。训练日志里每行输出三个值判断训练是否健康重点看两个信号第一个 epoch 后 loss 应该在 1 以下且缓慢下降训练过程中回归损失和分类损失应同步变化如果只降其中一个多半是 anchor 匹配或正负样本比例出了问题。常见做法是把 Focal Loss 的alpha从 0.25 调到 0.5增加正样本的梯度占比这个参数在你的项目种通常暴露在配置文件中不需要改模型代码。4. 目标检测优化训练收敛、显存控制与推理加速4.1 训练阶段的参数优化学习率预热与 EMAEfficientDet 在 ImageNet 上预训练的 EfficientNet 主干带有一套默认的 BN 均值和方差直接进入训练阶段时如果学习率过大前几个 step 容易把 BN 统计量冲散轻则 loss 抖动重则不收敛。常见做法是加一个 2 到 5 个 epoch 的 warmup让学习率从接近 0 线性升到目标值。PyTorch 里可以直接写一个线性预热函数配合torch.optim.lr_scheduler使用def warmup_lr(epoch: int, warmup_epochs: int, base_lr: float, target_lr: float): if epoch warmup_epochs: # 前几个 epoch 线性升温避免打乱 BN 统计量 return base_lr (target_lr - base_lr) * (epoch / warmup_epochs) return target_lr除了学习率EMAExponential Moving Average是检测模型训练里几乎白捡的提升手段。它维护一组模型参数的滑动平均版本推理时用 EMA 权重而不是原始权重能有效平滑训练后期因为学习率抖动带来的参数震荡。PyTorch 没有内置 EMA但实现很短class ModelEMA: def __init__(self, model: nn.Module, decay: float 0.9999): self.model model self.decay decay self.shadow {k: v.clone().detach() for k, v in model.state_dict().items()} def update(self): for name, param in self.model.state_dict().items(): if name in self.shadow: self.shadow[name].mul_(self.decay).add_(param, alpha1 - self.decay) def apply_shadow(self): self.model.load_state_dict(self.shadow, strictFalse)EMA 的 decay 通常取 0.9999意思是权重更新的响应速度很慢适合训练集较大的场景。训练集只有几千张时建议把 decay 降到 0.999避免 EMA 权重过于滞后。推理前调用apply_shadow()验证完再切回原始权重继续训练这样一轮验证流程不需要额外复制模型。4.2 显存优化AMP 混合精度、梯度累积与冻结主干检测模型训练最怕的就是 OOMEfficientDet 的显存大头不在主干网络而是 BiFPN 的特征图分辨率越高中间变量越多。三个常用手段按改动成本从低到高排序混合精度、梯度累积、冻结部分参数。混合精度是收益最明显的直接在 forward 外包一层autocast再用GradScaler防止梯度下溢scaler torch.cuda.amp.GradScaler() for images, boxes, labels in loader: images images.cuda() with torch.cuda.amp.autocast(): loss model(images, boxes, labels) scaler.scale(loss[total_loss]).backward() scaler.step(optimizer) scaler.update() optimizer.zero_grad()注意model的输入和输出在autocast块内会自动转成 FP16但 loss 计算里的归一化操作要保留在 FP32这一步由GradScaler自动处理。开启 AMP 后 D0 显存占用通常能降 40% 左右D2 以上降幅更明显。梯度累积适合数据量大但单卡显存小的场景。逻辑是等积累若干个小 batch 的梯度再更新一次参数等效于扩大 batch sizeaccumulate_steps 4 optimizer.zero_grad() for i, (images, boxes, labels) in enumerate(loader): with torch.cuda.amp.autocast(): loss model(images, boxes, labels) scaler.scale(loss[total_loss]).backward() if (i 1) % accumulate_steps 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad()使用梯度累积时记得把学习率也按比例调整。原来 batch size 8 用 lr1e-3改成累积 4 步后等效 batch 32学习率可以提到 2e-3 到 3e-3否则收敛速度会明显变慢。如果前面两个手段都做了还超显存最后一招是冻结主干网络的前几层只训练 BiFPN 和检测头。EfficientNet 靠近输入的部分学到的多是边缘、纹理这类通用特征在自定义数据集上差异不大冻结前 3 个 stage 通常对精度影响很小但能省下近 30% 的显存。如果你要优化的是训练吞吐还可以把--num_workers调高并开启pin_memoryTrue减少 CPU 到 GPU 的拷贝时间。4.3 推理加速ONNX 导出与 NMS 参数切换训练优化只是第一步部署时往往要面对更严格的延迟要求。EfficientDet 导出 ONNX 的流程比较固定先把模型切成推理模式构造一个固定或动态 shape 的输入然后调用torch.onnx.exportimport torch model.eval() model.cuda() dummy_input torch.randn(1, 3, 512, 512).cuda() torch.onnx.export( model, dummy_input, efficientdet_d0.onnx, input_names[images], output_names[boxes, scores, labels], dynamic_axes{images: {0: batch}}, opset_version15, )导出后可以用 ONNX Runtime 或 TensorRT 二次加速。一个值得注意的细节是EfficientDet 的检测头在推理时输出的 boxes 已经是解码后的绝对坐标不需要像 YOLO 那样在模型外再做一次坐标变换这意味着 ONNX 的输入输出接口更干净部署端少写一块逻辑。如果你在导出时报Unsupported operator之类的错误多半是opset_version偏低把它升到 17 通常能解决。NMS 参数对最终效果的影响也不能忽视。训练时和推理时的置信度阈值建议分开设置训练用 0.05 收集更多低置信度正样本推理时根据任务调高到 0.3 到 0.5 过滤噪声。NMS 的 IoU 阈值控制框的合并激进程度目标密集的场景可以适当升到 0.6让相邻实例不容易被合并成一个。优化手段改动位置显存收益精度影响AMP 混合精度训练循环节省约 40%几乎无损梯度累积训练循环等效扩大 batch视学习率调整冻结主干前段网络前向节省约 30%小幅下降EMA 权重训练后处理无通常 0.3~0.5 mAP5. 小目标检测场景下的优化验证与踩坑边界5.1 用 mAP 和推理耗时验证优化前后的收益优化效果不能只看 loss。EfficientDet 这一类多尺度检测器最终性能指标是 COCO mAP 和单帧推理耗时。常见做法是用 pycocotools 在验证集上算 mAPfrom pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval coco_gt COCO(annotation.json) coco_dt coco_gt.loadRes(predictions) # predictions 是模型输出列表 evaler COCOeval(coco_gt, coco_dt, bbox) evaler.evaluate() evaler.accumulate() evaler.summarize()跑完会输出 12 个指标前三个是 AP0.5:0.95、AP0.5、AP0.75后续按面积分成了 small、medium、large。如果 small 的 AP 明显低于 medium 和 large说明低层特征和 anchor 分布还没有照顾到小目标。除了 mAP单帧耗时也要固定硬件和输入尺寸来测我这里用一段简单的计时循环去掉前 30 次预热取后面 200 次的平均时间。5.2 小目标检测最常被忽略的优化点输入分辨率与低层 Anchor对小目标检测最直接有效的优化是提高输入分辨率。EfficientDet 的复合缩放默认把分辨率和通道数绑定但在自定义数据集里可以拆开用显存只够跑 D0 时把输入从 512 提到 640同时把 P2 层stride 4纳入 BiFPN 融合比单纯换 D1 模型更划算因为 D1 加的大部分通道参数对小目标没有直接帮助。代价是特征图变大推理延迟一般要增加 20% 左右实测后如果延迟不达标再回退到原始配置。低层 Anchor 的调整则要更细致。把 P3 层的 anchor scale 从[2, 2.4, 3.0]改成[1.4, 1.8, 2.2]配合更小的长宽比能显著提高小目标的召回但要注意与中大目标产生冲突。我一般的做法是先统计训练集中目标的宽高分布按第 5 百分位和第 95 百分位设定 anchor 范围再在验证集上对比 mAP只改有统计支撑的区间不做全量替换。5.3 一张验证清单优化前后必须记录的四组数据在报告里说明优化效果时我会固定记录四组数字模型参数量、单卡训练耗时、GPU 推理延迟不含 NMS、验证集 mAP 按面积拆分。前两个决定是否值得投入后两个决定部署可行性。对比实验保持输入分辨率和 batch size 一致只在单一变量上做改动否则 mAP 的差异无法归因到具体优化手段。最后一次调整的权重和对应 ONNX 文件归档在同一目录写清输入尺寸和opset_version方便后续做回归对比迭代排错时能快速定位是哪一次改动带来了精度回退。本文还有配套的精品资源点击获取
返回列表