
簡介這是一套基于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方便后續做回歸對比迭代排錯時能快速定位是哪一次改動帶來了精度回退。本文還有配套的精品資源點擊獲取