前言
Hello,大家好,我是
GISer Liu
😁,一名熱愛AI技術的GIS開發者。本系列是作者參加DataWhale 2025年6月份Yolo原理組隊學習的技術筆記文檔,這里整理為博客,希望能幫助Yolo的開發者少走彎路!
🚀 歡迎來到YOLO進階系列教程的核心,也是最后一篇文章——模型“魔改”!在目標檢測領域,YOLO系列憑借其卓越的速效平衡成為了標桿。然而,無論是為了發表學術論文,還是應對復雜多變的業務場景,僅僅滿足于使用官方模型、調整參數是遠遠不夠的。我們需要的,是突破“調參工程師”的局限,真正深入模型內部,進行結構級的創新。
這正是Datawhale YOLO Master
項目的初衷。它提供了一套即插即用的先進模塊和一套系統性的魔改方法論,旨在幫助開發者:
- 系統性理解YOLO架構:拆解模型為Backbone、Neck、Head等核心組件。
- 掌握模塊化創新:像搭樂高一樣,將前沿的模塊(如SwinTransformer, CBAM等)無縫集成到YOLOv8/v10/v11中。
- 提升工程與科研能力:從源碼層面理解并改造SOTA模型,為自己的項目或研究注入創新力。
本教程將手把手帶你走完從環境準備到模型改造、訓練的全過程。無論你是希望在CV領域深造的大學生,還是尋求技術突破的開發者,相信本教程都能為你提供堅實的起點。OK,讓我們開始“造”自己的YOLO吧!
更新記錄:(本文隨時更新)
20250709:本文當前只是理論的堆砌,閱讀感覺并不好;不夠直觀;后續我作者通過一個具體的需求,例如遙感影像目標識別去魔改我們的yolo模型結構;測試性能的變化;
一、YOLO“魔改”:從“調參”到“改結構”
1. 為什么要“魔改”YOLO?
標準的YOLO模型雖然強大,但在特定任務上未必是最優解。例如,在遙感影像中檢測微小目標,或是在工業流水線上識別密集物體,都對模型的特征提取能力、多尺度融合等方面提出了更高的要求。此時,僅僅調整學習率、優化器等超參數,帶來的性能提升是有限的。
真正的突破來自于對網絡結構的創新——也就是我們常說的“魔改”。這好比我們不是簡單地調整一輛車的懸掛軟硬(調參),而是給它換上一臺更強勁的發動機或者更先進的空氣動力學套件(魔改)。
模型魔改是網絡結構上的修改和替換,而非簡單調參;這需要開發者對模型組成和原理有深刻的理解🤔
2. “魔改”的哲學:像搭樂高一樣構建網絡
YOLO Master
項目的核心思想是將復雜的神經網絡解構成一系列可插拔的、標準化的“積木塊”。YOLO模型經典的三段式結構(主干、頸部、頭部)為這種模塊化改造提供了完美的框架。
- 主干網絡 (Backbone):負責從輸入圖像中提取基礎特征,是模型的“地基”。我們可以將其替換為更先進的結構,如
SwinTransformer
、ConvNeXtV2
等,以獲取更強的特征表達能力。 - 頸部網絡 (Neck):負責融合主干網絡在不同階段提取出的特征圖,增強模型對不同尺寸目標的感知能力。可以引入
GFPN
等結構進行優化。 - 檢測頭 (Head):根據融合后的特征進行最終的邊界框回歸和類別預測。我們可以嘗試
DyHead
等動態頭部來提升檢測性能。 - 注意力機制 (Attention):像“插件”一樣,可以插入到網絡中的任何位置,讓模型“關注”到最重要的特征區域。
CBAM
和SE
是常用的選擇。
二、準備工作:搭建你的“魔改”基礎環境
在開始之前,我們需要準備好兩個核心的代碼庫:ultralytics
官方庫和yolo-master
魔改項目庫。
1. 克隆項目倉庫
我們提供三種下載方式,推薦使用git clone
,如果遇到網絡問題,可以嘗試國內的GitCode鏡像。
① 下載 ultralytics
源碼
# 方法一:從GitHub直接克隆 (推薦)
git clone https://github.com/ultralytics/ultralytics.git# 方法二:從國內鏡像GitCode克隆
git clone https://gitcode.com/gh_mirrors/ul/ultralytics.git
其中,我們要對ultralytics文檔目錄結構有個相對完整的了解:
ultralytics/????
?assets:靜態資源:測試圖像、預訓練模型等示例文件
cfg:配置文件中心??
- datasets/:數據集定義(路徑、類別、預處理)
- models/:模型架構配置(YOLOv8n/v8s/v8m等)
- trackers/:跟蹤算法參數
- default.yaml:全局默認配置(訓練/推理/導出參數)
?data/?:數據預處理與增強邏輯
engine/?:核心功能引擎
- exporter.py:模型導出(ONNX/TensorRT等)
- model.py:模型生命周期管理
- predictor.py:推理接口
- trainer.py:訓練流程控制
- validator.py:驗證指標計算
?hub/??:PyTorch Hub 集成接口
models/??:模型架構實現
- yolo/:YOLO系列主代碼
- detect/:檢測任務
- segment/:分割任務
- pose/:姿態估計
- classify/:分類任務
- model.py:模型構建核心
- rtdetr/:實時DETR架構
- nas/:神經架構搜索
- sam/:SAM優化策略
- fastsam/:快速分割模型
?nn/??:神經網絡組件:自定義層/激活函數等
?solutions/??:高級應用模塊:線計數/熱力圖生成等場景化解決方案
trackers/??:多目標跟蹤(MOT)算法實現
utils/??:工具庫
- callbacks/:訓練回調函數
- metrics.py:性能評估指標
- plotting.py:可視化工具
- torch_utils.py:PyTorch擴展功能
- downloads.py:資源下載管理
- ops.py:張量操作擴展
?init.py?:包初始化入口(版本/API暴露)
② 下載 yolo-master
魔改教程源碼
git clone https://github.com/datawhalechina/yolo-master.git
2. 環境配置
進入yolo-master
目錄,其中包含了requirements.txt
文件,我們可以使用pip
進行安裝。為了加速下載,建議使用國內的清華鏡像。
# 進入yolo-master項目目錄
cd yolo-master# 安裝依賴,-e . 表示以可編輯模式安裝ultralytics
# 假設你的ultralytics目錄與yolo-master在同一級
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install -e ../ultralytics
pip install -e .
的-e
代表 “editable”(可編輯)。這種方式安裝后,你對ultralytics
源碼的任何修改(比如我們后續的“魔改”操作)會立刻生效,而無需重新安裝。這對于模型開發和調試至關重要。
三、主干(BackBone)的替換
要實現真正的“即插即用”,我們需要對ultralytics
的源碼進行一些通用性的改造,讓它能夠識別并正確處理我們添加的自定義模塊,特別是復雜的主干網絡。修改的核心位于ultralytics/nn/tasks.py
文件中,這個文件負責解析YAML配置文件并構建整個神經網絡模型。
1. 魔改的挑戰與思路
挑戰:ultralytics
原生的parse_model
函數設計時,主要考慮的是構建由一系列“標準”層(如Conv
、C2f
、Concat
)組成的網絡。這些層有一個共同點:輸入一個張量,輸出一個張量。而我們想替換的先進主干網絡(如RepViT, SwinTransformer等)通常是作為一個整體模塊,它輸入一個圖像張量,一次性輸出多個不同尺度的特征圖(例如,同時輸出P3, P4, P5三個層級的特征)。原生的解析和前向傳播邏輯無法直接處理這種“一對多”的復雜模塊。
解決思路:我們的魔改思路可以分為兩步,就像進行一次精密的“外科手術”:
- 改造
parse_model
函數(模型構建階段):讓它在解析YAML時,能夠“識別”出我們自定義的、作為整體的主干網絡。識別后,它需要特殊處理:不再將它看作一個普通層,而是作為一個特殊的“多輸出模塊”,并正確記錄下它所有輸出頭的通道信息。 - 改造
_predict_once
函數(模型推理階段):讓它在前向傳播時,如果遇到這個被標記過的特殊主干網絡,就執行特殊的傳播邏輯。這個邏輯會一次性接收主干網絡輸出的多個特征圖,并將它們正確地存放到一個列表中(y
列表),以供后續的Neck網絡層使用。
2. 實戰:一步步改造YOLOv8主干
主干網絡是決定模型性能的基石。YOLO Master提供了大量先進的Backbone供我們選擇。
可選主干網絡 | 核心思想 |
---|---|
RepViT | 融合CNN的效率和ViT的性能 |
StarNet | 輕量級、高效的星狀結構 |
EfficientViT | 高效的Vision Transformer變體 |
FasterNet | 極速推理,專注于硬件友好 |
ConvNeXtV2 | 現代化的純卷積網絡,性能媲美Transformer |
SwinTransformer | 經典的層級化窗口注意力Transformer |
VanillaNet | 極簡主義設計,返璞歸真但效果強大 |
… | (還有更多) |
讓我們以RepViT
為例,演示完整的替換步驟。
接下來,我們將以RepViT為例,完整地展示如何實現這一“手術”。這個方法具有普適性,適用于本文中介紹的所有主干網絡。
① 準備模塊代碼和配置文件
-
第一步:安放模塊代碼
在ultralytics/ultralytics/nn/
目錄下,創建一個新文件夾new_modules
。然后,將yolo-master
項目中的Backbone_RepViT.py
文件復制到這個新文件夾中,并重命名為repvit.py
。
- 目的:將我們自定義的模塊代碼與
ultralytics
的官方代碼分離開,便于管理和維護。
- 目的:將我們自定義的模塊代碼與
-
第二步:安放YAML文件
將yolo-master
中的RepViT-P345.yaml
文件復制到ultralytics/cfg/models/v8/
(或者你指定的其他版本目錄)下。這個YAML文件描述了使用RepViT作為主干的網絡結構。
- 目的:讓YOLO的訓練引擎能夠找到并加載我們的新模型定義。
② 引用新模塊
打開ultralytics/nn/tasks.py
文件,在文件的開頭部分,找到導入模塊的區域,添加以下代碼:
# ultralytics/nn/tasks.py# ... 其他 import 語句 ...
from ultralytics.utils.torch_utils import (fuse_conv_and_bn, fuse_deconv_and_bn, initialize_weights, intersect_dicts,make_divisible, model_info, scale_img, time_sync)# ==================== 在這里添加我們的新模塊導入 ====================
from .new_modules.repvit import *
from .new_modules.starnet import * # 為后續其他模塊預留
from .new_modules.efficientvit import * # 為后續其他模塊預留
# ... 你可以繼續添加其他自定義模塊的導入 ...
# ===================================================================#LOGGER = logging.getLogger(__name__)
# ... 文件后續內容 ...
- 目的:將
repvit.py
中定義的所有類(如repvit_m2_3
)導入到tasks.py
的全局命名空間中,這樣parse_model
函數在解析YAML時才能通過字符串名字找到并實例化它們。
③ 核心改造一:parse_model
函數
在tasks.py
中,使用Ctrl+F
找到parse_model
函數。這是整個魔改工程的核心。我們將分三部分進行修改。
i) 修改讀取模型參數部分(增強解析器的靈活性)
這部分修改的目的是讓解析器更加健壯和靈活,能夠處理更復雜的參數和模塊名,為后續所有類型的魔改(包括主干、頸部等)打下基礎。
原代碼:
for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']): # from, number, module, argsm = getattr(torch.nn, m[3:]) if 'nn.' in m else globals()[m] # get modulefor j, a in enumerate(args):if isinstance(a, str):with contextlib.suppress(ValueError):args[j] = locals()[a] if a in locals() else ast.literal_eval(a)```* **修改后代碼**:```pythonis_backbone = Falsefor i, (f, n, m, args) in enumerate(d['backbone'] + d['head']): # from, number, module, args# ==================== 增強的模塊獲取邏輯 ====================# 原理:使用 try-except 塊來優雅地處理模塊查找。# 原生代碼直接使用 globals()[m] 查找,如果m不是一個已知的模塊名,程序會報錯退出。# 修改后,我們嘗試獲取模塊,如果失敗(比如m是一個我們后續要特殊處理的字符串),# 就暫時跳過,給予后續代碼處理它的機會。try:if m == 'node_mode': # 為更復雜的頸部(如GFPN)預留的邏輯m = d[m]if len(args) > 0:if args[0] == 'head_channel':args[0] = int(d[args[0]])t = m # 臨時保存模塊名字符串,用于打印日志m = getattr(torch.nn, m[3:]) if 'nn.' in m else globals()[m] # get moduleexcept:pass # 如果在globals()中找不到模塊名,暫時忽略# ==========================================================# ==================== 增強的參數解析邏輯 ====================# 原理:同樣使用 try-except 塊增強魯棒性。# 有些參數可能是字符串(如路徑),ast.literal_eval 會解析失敗。# 修改后,如果解析失敗,就保持其原始的字符串類型。for j, a in enumerate(args):if isinstance(a, str):with contextlib.suppress(ValueError):try:args[j] = locals()[a] if a in locals() else ast.literal_eval(a)except:args[j] = a # 解析失敗時,保留為字符串# ==========================================================
ii) 添加自定義主干的參數接收邏輯
這是識別我們自定義主干的“秘密握手”。
- 實現思路:我們在
parse_model
函數的循環內部,計算每個模塊的輸出通道數c2
之后,添加一個判斷。如果m
是我們自定義的主干網絡類(如repvit_m2_3
),我們就調用它特有的一個方法(我們約定所有自定義主干都實現一個名為channel
的屬性或方法)來獲取它所有輸出層的通道列表。
在c1, c2 = ch[f], args[0]
這行代碼之后,添加如下邏輯:
# ... 在 parse_model 函數的循環內 ...if m in (Classify, Detect, RTDETR, Segment):# ... 省略 ...elif m is nn.BatchNorm2d:# ... 省略 ...else:c2 = ch[f] if c2 == -1 else c2# ==================== 自定義主干接收參數部分 ====================# 原理:這是識別自定義主干的核心。# 我們約定,所有即插即用的主干網絡模塊,都會有一個名為 `channel` 的屬性,# 這個屬性返回一個列表,包含了它所有輸出特征圖的通道數。# 通過檢查模塊 m 是否有 'channel' 屬性,我們就能識別出它。if hasattr(globals()[t], 'channel'): # 使用臨時變量t(模塊名字符串)來檢查# 實例化主干網絡。注意,這里的m是模塊的類本身。m = m()# 獲取輸出通道列表,例如 [128, 256, 512]c2 = m.channel# ==============================================================m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args) # module# ... 后續代碼 ...
iii) 修改模型實例化部分
這部分是整個改造的“執行”階段。在這里,我們將真正地區分處理標準層和我們的自定義主干。
原代碼:
# 原代碼部分一m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args) # modulet = str(m)[8:-2].replace('__main__.', '') # module typem_.np = sum(x.numel() for x in m_.parameters()) # number paramsm_.i, m_.f, m_.type = i, f, t # attach index, 'from' index, typeif verbose:LOGGER.info(f'{i:>3}{str(f):>20}{n_:>3}{m_.np:10.0f} {t:<45}{str(args):<30}') # printsave.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist# 原代碼部分二if i == 0:ch = []ch.append(c2)
修改后代碼 (整合了ii和iii的邏輯):
# ... 在 c1, c2 = ch[f], args[0] 之后 ...# 這是更完整的邏輯,取代了ii)中的簡單判斷if m in (Conv, ConvTranspose, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, Focus,BottleneckCSP, C1, C2, C2f, C3, C3TR, C3Ghost, nn.Conv2d, DWConvTranspose, C3x, RepC3):c2 = m.get_nc(ch, f, args) # get c2 output channels# ... (省略其他elif)# ==================== 修改后的模型實例化 ====================# --- 步驟 1: 實例化與識別 ---# 巧妙的識別方法:我們先按常規方式實例化模塊。# 如果 m 是我們的自定義主干,它沒有輸入參數,直接 m() 即可。# 然后,我們檢查它的 `channel` 屬性,得到 `c2`。# 如果 `c2` 是一個列表,就說明這是一個自定義主干!if isinstance(c2, list):# 是自定義主干,is_backbone標志位設為Trueis_backbone = True# m_ 直接就是我們實例化的主干對象 m (在之前的步驟中已經 m=m() )m_ = m# 給模塊實例動態添加一個 'backbone' 屬性,值為 True。# 目的:這是一個“標記”,為了在后續的前向傳播 `_predict_once` 函數中識別它。m_.backbone = Trueelse:# 是標準層,按原邏輯構建m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args) # modulet = str(m)[8:-2].replace('__main__.', '') # module type# --- 步驟 2: 計算參數并附加信息 ---# 計算參數量m_.np = sum(x.numel() for x in m_.parameters()) # number params# 附加索引信息。注意這里的 `i+4`# 原理:我們的自定義主干會輸出多個特征圖,為了給這些特征圖在內部留出索引位置(0,1,2,3),# 我們將主干模塊本身的索引號人為地增加,例如 `0 -> 4`。# 這樣,后續Neck部分的層索引就不會與Backbone的輸出索引沖突。# 4是一個經驗值,通常主干輸出P2-P5四層特征,但只要比主干輸出層數多即可。m_.i, m_.f, m_.type = i + 4 if is_backbone else i, f, t # attach index, 'from' index, typeif verbose:LOGGER.info(f'{i:>3}{str(f):>20}{n_:>3}{m_.np:10.0f} {t:<45}{str(args):<30}') # print# 將需要保存的層的索引添加到 savelist。同樣,對主干索引進行偏移。save.extend(x % (i + 4 if is_backbone else i) for x in ([f] if isinstance(f, int) else f) if x != -1)# --- 步驟 3: 追蹤通道數 ---if i == 0:ch = []# 關鍵修改!if isinstance(c2, list):# 如果 c2 是列表 (我們的主干),則用 extend 將所有輸出通道加入 ch 列表ch.extend(c2)# 補位操作:確保 ch 列表的長度至少為5。# 目的:為了與 `_predict_once` 中的邏輯對齊,方便通過索引訪問不同尺度的特征。# 即使某個主干不輸出P1, P2層,也用0占位,避免索引錯誤。for _ in range(5 - len(ch)):ch.insert(0, 0)else:# 如果是標準層,按原邏輯 append 單個輸出通道ch.append(c2)# ==============================================================
④ 核心改造二:_predict_once
函數
這個函數負責執行模型的前向傳播。我們需要修改它,以便能正確處理我們標記了backbone=True
的特殊模塊。
原代碼:
def _predict_once(self, x, profile=False, visualize=False, embed=None):y, dt, embeddings = [], [], [] # outputsfor m in self.model:if m.f != -1: # if not from previous layerx = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] # from earlier layersif profile:self._profile_one_layer(m, x, dt)x = m(x) # runy.append(x if m.i in self.save else None) # save output# ... 省略 visualize 和 embed 的代碼 ...return x
修改后代碼:
def _predict_once(self, x, profile=False, visualize=False, embed=None):y, dt, embeddings = [], [], [] # outputsfor m in self.model:if m.f != -1: # if not from previous layerx = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] # from earlier layersif profile:self._profile_one_layer(m, x, dt)# ==================== 自定義主干前向傳播邏輯 ====================# 原理:檢查在 parse_model 中添加的 'backbone' 標記。if hasattr(m, 'backbone'):# 如果是主干模塊,直接調用它,它會返回一個特征圖列表x = m(x)# 補位操作,與 parse_model 中的邏輯對應。# 目的:確保輸出列表 x 的長度固定,即使主干輸出的特征圖數量不同,# 后續的層可以通過固定的索引(如 y[4])來獲取特征。for _ in range(5 - len(x)):x.insert(0, None) # 用 None 填充不存在的低層特征# 遍歷主干輸出的每一層特征圖for i_idx, i in enumerate(x):# 根據 savelist 判斷這一層是否需要保存給后續層使用if i_idx in self.save:y.append(i)else:y.append(None) # 如果不需要,用 None 占位# 將主干的最后一層輸出作為下一個模塊的輸入x = x[-1]else:# 如果是標準層,執行原有的邏輯x = m(x) # runy.append(x if m.i in self.save else None) # save output# ==============================================================if visualize:feature_visualization(x, m.type, m.i, save_dir=visualize)if embed and m.i in embed:# ... (省略)return x
如此修改后,模型的網絡結構就會發生變化:
四、頸部(Neck)的替換
我們已經成功地改造了tasks.py
,建立了一個強大的、可兼容自定義主干的框架。現在,我們將利用這個框架,對模型的“頸部”進行替換。
1-GFPN (GiraffeDet FPN)
GFPN(長頸鹿特征金字塔網絡)通過其獨特的、類似長頸鹿脖子的交錯連接方式,高效地融合深層語義信息和淺層空間信息。我們將用它來替換YOLOv8原生的PANet結構。
① 文件準備與引用(此步驟與之前一致)
- 代碼:
Neck-GFPN.py
->new_modules/GFPN.py
- 配置:
GFPN-P345.yaml
->cfg/models/v8/
- 引用:在
tasks.py
中添加from .new_modules.GFPN import *
② GFPN的YAML配置與parse_model
的聯動
思考:GFPN是如何被我們的新parse_model
函數解析的?
讓我們深入GFPN-P345.yaml
和tasks.py
的代碼;。
-
第一步:分析
GFPN-P345.yaml
的配置打開
GFPN-P345.yaml
,你會發現它巧妙地使用變量來定義網絡結構,這是一種非常優雅的工程實踐。# ultralytics/cfg/models/v8/GFPN-P345.yaml (內容示例)# ------------------ YAML 頂層參數定義 ------------------ # 定義了頸部和頭部的默認通道數 widen_factor: 1.0 head_channel: 256# 核心:定義了頸部中重復使用的核心模塊的名稱 # 這樣做的好處是,如果想把所有 CSPStage 換成其他模塊, # 只需修改下面這一行,無需改動 head 中的每一處。 node_mode: CSPStage# ... (nc, depth_multiple, width_multiple 等定義)# ------------------ Backbone (主干) ------------------ backbone:- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2# ... (主干網絡定義,這里可能是標準的YOLOv8主干,也可能是我們之前替換的RepViT等)# 假設主干的第4、6、9層分別輸出P3, P4, P5 特征# ------------------ Head (頸部 + 頭部) ------------------ head:# in_channels: [256, 512, 1024] # 來自Backbone的 P3, P4, P5# out_channels: [256, 512, 1024]# --- GFPN 頸部結構 ---- [-1, 1, Conv, [256, 1, 1]] # 10- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 11- [[-1, 6], 1, Concat, [1]] # 12 (Concat P4)- [-1, 3, node_mode, [head_channel, 3]] # 13 <--- 關鍵!使用了node_mode- [-1, 1, Conv, [256, 1, 1]] # 14- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 15- [[-1, 4], 1, Concat, [1]] # 16 (Concat P3)- [-1, 3, node_mode, [head_channel, 3]] # 17 <--- 關鍵!使用了node_mode- [-1, 1, Conv, [256, 3, 2]] # 18- [[-1, 14], 1, Concat, [1]] # 19- [-1, 3, node_mode, [head_channel, 3]] # 20 <--- 關鍵!使用了node_mode- [-1, 1, Conv, [256, 3, 2]] # 21- [[-1, 10], 1, Concat, [1]] # 22- [-1, 3, node_mode, [head_channel, 3]] # 23 <--- 關鍵!使用了node_mode# --- Detect Head ---- [[17, 20, 23], 1, Detect, [nc]] # Detect(P3, P4, P5)
-
第二步:追蹤
parse_model
的執行流程當
parse_model
函數解析到第13層[-1, 3, node_mode, [head_channel, 3]]
時,我們之前修改的代碼開始發揮作用:# 在 ultralytics/nn/tasks.py 的 parse_model 中# 此時,循環變量的值為: # i = 13, f = -1, n = 3, m = 'node_mode', args = ['head_channel', 3]try:# 1. 檢查到 m == 'node_mode',條件成立if m == 'node_mode':# 2. 將 m 的值從字符串 'node_mode' 替換為 YAML 頂層定義的實際值# m = d['node_mode'] --> m 變成了 'CSPStage'm = d[m]# 3. 檢查 args 列表if len(args) > 0: # len(['head_channel', 3]) > 0, 成立# 4. 檢查第一個參數是否為 'head_channel'if args[0] == 'head_channel': # 成立# 5. 將 args[0] 從字符串 'head_channel' 替換為 YAML 頂層定義的實際值# args[0] = int(d['head_channel']) --> args[0] 變成了整數 256args[0] = int(d[args[0]])# 經過處理后,模塊定義從 'node_mode', ['head_channel', 3]# 變成了實際的 'CSPStage', [256, 3]t = m # t 被賦值為 'CSPStage'# 6. 最后,通過 globals()['CSPStage'] 找到我們導入的 CSPStage 類m = getattr(torch.nn, m[3:]) if 'nn.' in m else globals()[m] except:pass # ... 后續代碼將使用 m = CSPStage 類, args = [256, 3] 來實例化模塊
結論:我們為
parse_model
添加的try-except
和if m == 'node_mode'
邏輯,本質上是創建了一個“宏替換”機制。它使得YAML的編寫者可以像定義宏一樣預設模塊名和參數,極大地增強了配置文件的靈活性和復用性。
③ 魔改前后模型參數對比
我們可以通過打印模型結構來直觀地看到變化。
魔改前模型結構 (標準 YOLOv8 Neck)
# yolov8.yaml summary: 225 layers, 3157200 parameters, 3157184 gradients, 8.9 GFLOPs... (backbone)10 -1 1 ultralytics.nn.modules.conv.Conv [512, 1, 1]11 -1 1 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest']12 [-1, 6] 1 ultralytics.nn.modules.container.Concat [1]13 -1 3 ultralytics.nn.modules.block.C2f [512, True]14 -1 1 ultralytics.nn.modules.conv.Conv [256, 1, 1]15 -1 1 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest']16 [-1, 4] 1 ultralytics.nn.modules.container.Concat [1]17 -1 3 ultralytics.nn.modules.block.C2f [256, True]18 -1 1 ultralytics.nn.modules.conv.Conv [256, 3, 2]19 [-1, 14] 1 ultralytics.nn.modules.container.Concat [1]20 -1 3 ultralytics.nn.modules.block.C2f [512, True]21 -1 1 ultralytics.nn.modules.conv.Conv [512, 3, 2]22 [-1, 10] 1 ultralytics.nn.modules.container.Concat [1]23 -1 3 ultralytics.nn.modules.block.C2f [1024, True]24 [17, 20, 23] 1 ultralytics.nn.modules.head.Detect [80]
魔改后模型結構 (GFPN-P345 Neck)
# GFPN-P345.yaml summary: 237 layers, 3348644 parameters, 3348628 gradients, 9.2 GFLOPs... (backbone)10 -1 1 ultralytics.nn.modules.conv.Conv [256, 1, 1]11 -1 1 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest']12 [-1, 6] 1 ultralytics.nn.modules.container.Concat [1]13 -1 3 new_modules.GFPN.CSPStage [256, 3] # <-- 核心模塊被替換14 -1 1 ultralytics.nn.modules.conv.Conv [256, 1, 1]15 -1 1 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest']16 [-1, 4] 1 ultralytics.nn.modules.container.Concat [1]17 -1 3 new_modules.GFPN.CSPStage [256, 3] # <-- 核心模塊被替換18 -1 1 ultralytics.nn.modules.conv.Conv [256, 3, 2]19 [-1, 14] 1 ultralytics.nn.modules.container.Concat [1]20 -1 3 new_modules.GFPN.CSPStage [512, 3] # <-- 核心模塊被替換21 -1 1 ultralytics.nn.modules.conv.Conv [512, 3, 2]22 [-1, 10] 1 ultralytics.nn.modules.container.Concat [1]23 -1 3 new_modules.GFPN.CSPStage [1024, 3] # <-- 核心模塊被替換24 [17, 20, 23] 1 ultralytics.nn.modules.head.Detect [80]
通過對比可以清晰地看到,原生的C2f
模塊被我們自定義的CSPStage
(來自GFPN.py
)所取代,這證明我們的魔改成功了。
五、頭部(Head)的革新
1-DyHead (Dynamic Head)
DyHead通過統一的注意力機制,動態地選擇最重要的特征,極大地提升了檢測頭的表征能力。
① 文件準備與引用(同上)
- 代碼:
Head-DyHead.py
->new_modules/DyHead.py
- 配置:
DyHead-P345.yaml
->cfg/models/v8/
- 引用:
tasks.py
中添加from .new_modules.DyHead import *
② 核心解析:DyHead的“無縫”集成
思考:DyHead為什么不需要對tasks.py
做任何新的修改?
答案在于,DyHead的設計模式與YOLO原生Detect
頭高度兼容,并且我們之前對parse_model
的通用化改造已經能夠處理它。
第一步:分析DyHead-P345.yaml
的結構
DyHead模塊將替換掉原Detect
層以及之前的一些卷積層。
# DyHead-P345.yaml (head部分示例)head:# ... (頸部融合層)# 假設頸部輸出三層特征分別在索引 17, 20, 23# 原生YOLOv8中,這里會有一系列解耦的卷積層,最后連接Detect層# 使用DyHead后,直接將三層特征輸入給DyHead模塊- [[17, 20, 23], 1, DyHead, [nc]] # <--- 直接替換
第二步:追蹤_predict_once
的執行流程
當模型前向傳播到DyHead
層時:
# 在 ultralytics/nn/tasks.py 的 _predict_once 中# 此時,m 是實例化的 DyHead 對象,m.f 是 [17, 20, 23]# 1. 進入獲取輸入的邏輯if m.f != -1: # 成立# 2. m.f 是一個列表, 執行 else 分支# 這行代碼會從 y 列表中,根據索引 17, 20, 23,# 取出對應的三層特征圖張量,并打包成一個新的列表 `x`# x = [y[17], y[20], y[23]]x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f]# 3. 檢查 'backbone' 屬性, DyHead沒有這個標記, 跳過if hasattr(m, 'backbone'):...else:# 4. 執行常規的前向傳播# 將包含三個特征圖的列表 x, 整體傳遞給 DyHead 模塊# x = m(x) 等價于 outputs = dyhead_instance([feature_p3, feature_p4, feature_p5])x = m(x)y.append(x if m.i in self.save else None)
結論:DyHead
模塊本身被設計為接收一個特征圖列表作為輸入。而YOLO的_predict_once
函數中,處理多輸入的from
(如[-1, 6]
或[17, 20, 23]
)的邏輯,天然地就會將多個來源的特征圖打包成一個列表。兩者一拍即合,實現了無縫對接。我們不需要為DyHead
編寫任何特殊的解析或執行代碼。
六、注意力機制(Attention)的融合
歡迎來到“魔改”系列中最靈活、最有趣的部分——集成注意力機制。
核心思想與類比:想象一下,當您在一張雜亂的桌面上尋找鑰匙時,您的大腦并不會平均地掃描每一個平方厘米。相反,您的目光會自動聚焦于桌面上的高亮區域,比如金屬反光處、顏色鮮艷的物體旁。注意力機制(Attention Mechanism)賦予了神經網絡類似的能力。它讓模型在處理海量信息時,能夠智能地“聚焦”于最關鍵的特征,并“忽略”次要或無關的背景,從而用有限的計算資源做出更精準的判斷。
本節,我們將以CBAM (Convolutional Block Attention Module) 為例,進行一次完整、詳盡的“即插即用”式集成。我們將一起分析其代碼原理,選擇合適的插入位置,完成一次無死角的YAML文件修改,并最終驗證我們的工作。
1. 深入理解CBAM模塊
在動手之前,我們先快速理解CBAM的工作原理,這將有助于我們決定將它放在網絡中的哪個位置。
CBAM由兩個串聯的子模塊組成:
- 通道注意力模塊 (Channel Attention):它回答的問題是“什么特征更重要?”。比如,在一個人像識別任務中,包含“眼睛”、“鼻子”等信息的特征通道,其重要性就應該高于包含“背景墻壁”信息的通道。該模塊會學習一個權重,對各個通道進行加權,增強重要特征,抑制次要特征。
- 空間注意力模塊 (Spatial Attention):它回答的問題是“特征圖的哪個位置更重要?”。在識別出一張人臉后,其五官所在的位置顯然比背景區域更關鍵。該模塊會生成一個空間“熱力圖”,告訴網絡應該重點關注特征圖上的哪些像素區域。
最重要的特性:CBAM模塊的forward
函數接收一個張量x
,經過內部一系列計算后,輸出一個與x
尺寸完全相同 的張量。這正是它能被“即插即用”的關鍵。
2. 集成CBAM
① 準備工作
- 代碼: 將
yolo_master/.../Attention-CBAM.py
復制到ultralytics/ultralytics/nn/new_modules/
并重命名為CBAM.py
。 - 引用: 在
ultralytics/nn/tasks.py
的頂部添加from .new_modules.CBAM import *
。
② CBAM應該放在哪里?
這是一個開放性問題,但有一些常用的策略:
- 放在特征提取之后:通常將注意力模塊放置在核心特征提取塊(如
C2f
)之后。這樣做的好處是,C2f
已經產生了豐富的特征組合,此時使用CBAM可以立刻對這些新特征進行“精煉”和“篩選”,讓最有用的信息傳遞給下一層。 - 放在下采樣之前:在網絡通過步進卷積(
Conv
)進行下采樣、縮小特征圖尺寸之前,使用CBAM可以確保在信息被壓縮前,關鍵特征已經被充分“關注”,減少重要信息的丟失。
本教程決策:我們將遵循以上策略,選擇在Backbone的第4個模塊(一個C2f
層)之后,第5個模塊(一個Conv
下采樣層)之前插入CBAM。這個位置非常理想,它能精煉P3級別的特征,再將其傳遞給更深的網絡。
③ YAML修改
這是最關鍵的一步。我們將以yolov8n.yaml
為藍本,創建yolov8n-CBAM.yaml
。
-
第一步:復制并重命名
yolov8n.yaml
為yolov8n-CBAM.yaml
。 -
第二步:進行修改。 下面是完整的修改前后對比,所有改動都用注釋明確標出。
yolov8n.yaml
(原始文件)
# ultralytics/cfg/models/v8/yolov8n.yamlnc: 80 # number of classes
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.25 # layer channel multiple# anchors
anchors:- [10,13, 16,30, 33,23] # P3/8- [30,61, 62,45, 59,119] # P4/16- [116,90, 156,198, 373,326] # P5/32# YOLOv8.0n backbone
backbone:# [from, number, module, args]- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4- [-1, 3, C2f, [128, True]] # 2- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8- [-1, 6, C2f, [256, True]] # 4- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16- [-1, 6, C2f, [512, True]] # 6- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32- [-1, 3, C2f, [1024, True]] # 8- [-1, 1, SPPF, [1024, 5]] # 9# YOLOv8.0n head
head:- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 10- [[-1, 6], 1, Concat, [1]] # 11- [-1, 3, C2f, [512, False]] # 12- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 13- [[-1, 4], 1, Concat, [1]] # 14- [-1, 3, C2f, [256, False]] # 15- [-1, 1, Conv, [256, 3, 2]] # 16- [[-1, 12], 1, Concat, [1]] # 17- [-1, 3, C2f, [512, False]] # 18- [-1, 1, Conv, [512, 3, 2]] # 19- [[-1, 9], 1, Concat, [1]] # 20- [-1, 3, C2f, [1024, False]]# 21- [[15, 18, 21], 1, Detect, [nc]] # Detect(P3, P4, P5)
yolov8n-CBAM.yaml
(修改后的文件)
# ultralytics/cfg/models/v8/yolov8n-CBAM.yaml# ... (nc, depth_multiple, width_multiple, anchors 定義與上面完全相同) ...# YOLOv8.0n backbone with CBAM
backbone:# [from, number, module, args]- [-1, 1, Conv, [64, 3, 2]] # 0- [-1, 1, Conv, [128, 3, 2]] # 1- [-1, 3, C2f, [128, True]] # 2- [-1, 1, Conv, [256, 3, 2]] # 3- [-1, 6, C2f, [256, True]] # 4- [-1, 1, CBAM, [256]] # 5 <--- 新增CBAM層. 它接收第4層的256通道輸出- [-1, 1, Conv, [512, 3, 2]] # 6 (原索引為5)- [-1, 6, C2f, [512, True]] # 7 (原索引為6)- [-1, 1, Conv, [1024, 3, 2]] # 8 (原索引為7)- [-1, 3, C2f, [1024, True]] # 9 (原索引為8)- [-1, 1, SPPF, [1024, 5]] # 10 (原索引為9)# YOLOv8.0n head with updated indices
head:- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 11# Concat 融合 Neck P5 和 Backbone P4. Backbone P4 現在是第7層- [[-1, 7], 1, Concat, [1]] # 12 (原為[-1, 6]) <--- 索引更新!- [-1, 3, C2f, [512, False]] # 13- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 14# Concat 融合 Neck P4 和 Backbone P3. Backbone P3 的輸出現在經過了第4層的C2f和第5層的CBAM,所以我們從第5層引出- [[-1, 5], 1, Concat, [1]] # 15 (原為[-1, 4]) <--- 索引更新!- [-1, 3, C2f, [256, False]] # 16- [-1, 1, Conv, [256, 3, 2]] # 17# Concat 融合 Neck P3 和 Neck P4(第13層)- [[-1, 13], 1, Concat, [1]] # 18 (原為[-1, 12]) <--- 索引更新!- [-1, 3, C2f, [512, False]] # 19- [-1, 1, Conv, [512, 3, 2]] # 20# Concat 融合 Neck P4 和 Backbone P5(第10層)- [[-1, 10], 1, Concat, [1]] # 21 (原為[-1, 9]) <--- 索引更新!- [-1, 3, C2f, [1024, False]] # 22# Detect 層的輸入來自第16, 19, 22層- [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5) (原為[15, 18, 21]) <--- 索引更新!
(4) 追蹤parse_model
如何處理CBAM
讓我們看看parse_model
解析新增的第5層[-1, 1, CBAM, [256]]
時,發生了什么。
- 確定輸入通道
c1
:from
是-1
,所以c1
來自上一層(第4層)的輸出。我們知道第4層C2f
的輸出通道是256,所以c1 = 256
。 - 確定模塊
m
:m
是我們導入的CBAM
類。 - 確定參數
args
:args
是列表[256]
。 - 實例化模塊
m_
:對于CBAM
這樣的通用模塊,parse_model
會執行m(c1, *args)
來實例化。這會調用CBAM(256, 256)
。 - 確定輸出通道
c2
:對于通用模塊,parse_model
會默認將args[0]
作為輸出通道數,即c2 = 256
。 - 更新通道列表
ch
:執行ch.append(c2)
,將256添加到通道列表中。
結論:由于CBAM輸入和輸出通道數相同(c1=c2=256
),它完美地融入了網絡的數據流,對后續層的通道數計算沒有任何影響。唯一的、也是最容易出錯的復雜性,在于手動更新所有后續層(尤其是Concat
和Detect
層)的from
索引。
(5) 可視化對比:魔改前后的模型結構
下面是模擬model.info()
命令輸出的文本,可以清晰地看到變化。
魔改前模型結構 (yolov8n.yaml)
# ... (層 0-3)4 -1 6 ultralytics.nn.modules.block.C2f 119296 (256, 128, 6, True, False, 1, 0.5)5 -1 1 ultralytics.nn.modules.conv.Conv 147968 (512, 256, 3, 2)6 -1 6 ultralytics.nn.modules.block.C2f 476160 (512, 256, 6, True, False, 1, 0.5)# ...11 [-1, 6] 1 ultralytics.nn.modules.container.Concat 0 (1)# ...14 [-1, 4] 1 ultralytics.nn.modules.container.Concat 0 (1)# ...22 [15, 18, 21] 1 ultralytics.nn.modules.head.Detect 649920 (80, (256, 512, 1024))
魔改后模型結構 (yolov8n-CBAM.yaml)
# ... (層 0-3)4 -1 6 ultralytics.nn.modules.block.C2f 119296 (256, 128, 6, True, False, 1, 0.5)+ 5 -1 1 new_modules.CBAM.CBAM 704 (256, None) # <--- 新增層,參數量極小6 -1 1 ultralytics.nn.modules.conv.Conv 147968 (512, 256, 3, 2)7 -1 6 ultralytics.nn.modules.block.C2f 476160 (512, 256, 6, True, False, 1, 0.5)# ...- 11 [-1, 6] 1 ultralytics.nn.modules.container.Concat 0 (1)+ 12 [-1, 7] 1 ultralytics.nn.modules.container.Concat 0 (1) # <--- 索引更新# ...- 14 [-1, 4] 1 ultralytics.nn.modules.container.Concat 0 (1)+ 15 [-1, 5] 1 ultralytics.nn.modules.container.Concat 0 (1) # <--- 索引更新# ...- 22 [15, 18, 21] 1 ultralytics.nn.modules.head.Detect 649920 (80, (256, 512, 1024))+ 23 [16, 19, 22] 1 ultralytics.nn.modules.head.Detect 649920 (80, (256, 512, 1024)) # <--- 索引更新
通過這個對比,我們可以百分之百地確認,我們的CBAM模塊已成功插入,并且整個模型的后續連接也已正確更新。對于SE模塊的集成,過程與此完全相同,不再贅述。
七、核心組件的優化
在掌握了對Backbone、Neck、Head三大件的“大刀闊斧”式改造后,我們再來學習如何對網絡中的基礎“零件”——如上下采樣和卷積模塊——進行“精雕細琢”的單元。這要求我們更深入地理解YOLO的parse_model
函數是如何處理標準模塊的。
1. 上下采樣模塊:EUCB (Efficient Up-sampling with Channel Balancing)
背景與目的:在特征金字塔網絡(FPN)中,上采樣負責將高層(小尺寸、強語義)的特征圖放大,以便與低層(大尺寸、強細節)的特征圖融合。YOLOv8默認使用的nn.Upsample
(配合mode='nearest'
)雖然速度極快,但它是一種固定的、非學習性的插值方法,僅僅是簡單地復制像素,可能會在放大過程中產生偽影或丟失細節。
EUCB (Efficient Up-sampling with Channel Balancing) 旨在解決這個問題。它是一種可學習的上采樣模塊。這意味著網絡可以通過反向傳播,學習到如何以最優的方式從低分辨率特征中“生成”高分辨率特征,同時還能調整通道數量,從而可能帶來更平滑、更有效的特征融合效果。
① 集成步驟
- 代碼: 將
yolo_master/.../Upsample-EUCB.py
復制到ultralytics/ultralytics/nn/new_modules/
并重命名為EUCB.py
。 - 引用: 在
tasks.py
的頂部添加from .new_modules.EUCB import *
。
② 核心解析:EUCB的參數與parse_model
的自動適配
思考:nn.Upsample
和 EUCB
在參數上有何不同?parse_model
如何處理這種不同?
-
第一步:分析模塊的
__init__
簽名-
torch.nn.Upsample
的定義很簡單,它只關心縮放因子和模式,不改變通道數。其YAML中的args
為[None, 2, 'nearest']
,None
代表輸出尺寸(由scale_factor=2
決定),通道數保持不變。 -
我們打開
new_modules/EUCB.py
文件(或根據其用法推斷),可以發現EUCB
模塊的定義更像一個卷積層。一個合理的__init__
簽名應該是:def __init__(self, c1, c2, scale_factor=2):
。c1
: 輸入通道數。c2
: 輸出通道數。這是與nn.Upsample
最大的不同。scale_factor
: 縮放因子。
-
-
第二步:修改YAML文件并追蹤解析過程
現在,我們在
yolov8.yaml
的head
部分進行替換。# yolov8-EUCB.yaml (head 部分示例)head:# ...# 假設解析到第14層,其輸入來自第13層(C2f),輸出通道數為512# ch = [..., 512]# 第15層: 上采樣層# --- 原代碼 ---# - [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 15# --- 修改后 ---# 我們希望上采樣后,通道數從512變為256- [-1, 1, EUCB, [256, 2]] # 15. args為[輸出通道數, 縮放因子]# 第16層: Concat層# 它的輸入來自上一層(第15層)和主干的第4層 (假設通道數為256)- [[-1, 4], 1, Concat, [1]] # 16# 第17層: C2f層# 它的輸入來自第16層的Concat。Concat后的通道數 = 256(來自EUCB) + 256(來自Backbone) = 512- [-1, 3, C2f, [256, False]] # 17. 該C2f層輸出通道數為256# ...
當
parse_model
解析到我們修改的第15層[-1, 1, EUCB, [256, 2]]
時,其標準模塊處理邏輯會執行以下操作:# 在 ultralytics/nn/tasks.py 的 parse_model 中# 此時: m = EUCB 類, args = [256, 2] # 假設上一層的輸出通道數 ch[-1] 是 512# 1. 獲取輸入通道數 c1 # c1 = ch[f] --> c1 = ch[-1] --> c1 = 512 c1 = ch[f]# 2. 獲取輸出通道數 c2 # c2 = args[0] if isinstance(args[0], int) else ... # 這里的 args[0] 是 256, 是整數。所以 c2 被賦值為 256 c2 = args[0] # 3. 實例化模塊 # m_ = m(*args) 等價于 m_ = EUCB(512, 256, 2) # 注意!這里的`*args`展開會把所有參數傳進去,所以我們的模塊定義要與之匹配 # 一個更嚴謹的寫法是在YAML中只寫輸出通道數,讓模塊內部處理 # 假設YAML為 [-1, 1, EUCB, [256]] # 那么實例化將是 m_ = EUCB(c1, *args) --> EUCB(512, 256) m_ = nn.Sequential(...) if n > 1 else m(c1, *args) # 注意這里隱式的 c1 參數# 4. 更新通道列表 ch # ch.append(c2) --> ch.append(256) # 現在 ch 列表的最后一個元素是 256,供下一層使用 ch.append(c2)
結論:我們不需要為
EUCB
編寫任何特殊解析代碼。只要一個模塊遵循“接收輸入通道c1
,并通過args
接收其他參數(包括輸出通道c2
)”這一標準模式,parse_model
的通用邏輯就能自動完成輸入/輸出通道的推斷和模塊的正確實例化。
③ 魔改前后模型結構對比
魔改前 (使用 nn.Upsample)
# ...# 假設第14層輸出512通道14 ... [..., 512, True]15 -1 1 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest']# 第15層輸出通道仍為512# ...
魔改后 (使用 EUCB)
# ...# 第14層輸出512通道14 ... [..., 512, True]15 -1 1 new_modules.EUCB.EUCB [512, 256, 2]# 第15層輸出通道變為256,參數量增加,因為它有可學習的權重# ...
2. 卷積模塊:C2f_CMUNeXtBlock
這是對YOLOv8中最重要的特征提取單元C2f
的直接替換。CMUNeXtBlock
可能借鑒了ConvNeXt
的設計,例如使用更大的卷積核、深度可分離卷積等,旨在用相似的參數量換取更強的特征表達能力。
① 集成步驟(同上)
- 代碼:
C2f_CMUNeXtBlock.py
->new_modules/C2f_CMUNeXtBlock.py
。 - 引用: 在
tasks.py
中添加from .new_modules.C2f_CMUNeXtBlock import *
。
② 實現“無痛”替換
思考:為什么C2f_CMUNeXtBlock
可以如此輕易地替換C2f
?
答案在于接口兼容性。C2f_CMUNeXtBlock
在設計時,刻意模仿了C2f
的__init__
參數簽名,使得它可以直接使用C2f
在YAML文件中的參數定義。
-
第一步:對比
__init__
簽名C2f
的簽名(簡化后):__init__(self, c1, c2, n=1, shortcut=False, ...)
C2f_CMUNeXtBlock
的簽名(推斷):__init__(self, c1, c2, n=1, shortcut=False, ...)
只要兩者都接收相同的核心參數(輸入通道
c1
,輸出通道c2
,重復次數n
,快捷連接shortcut
),它們在YAML層面就是可互換的。 -
第二步:修改YAML
這個修改是最簡單的,只需更換模塊名即可。# yolov8-CMUNeXt.yaml (backbone 部分示例)backbone:- [-1, 1, Conv, [64, 3, 2]] # 0- [-1, 1, Conv, [128, 3, 2]] # 1# --- 原代碼 ---# - [-1, 3, C2f, [128, True]] # 2# --- 修改后 ---- [-1, 3, C2f_CMUNeXtBlock, [128, True]] # 2. 參數完全相同!
-
第三步:追蹤
parse_model
的執行流程
這個流程與解析C2f
時完全一樣。parse_model
獲取模塊名C2f_CMUNeXtBlock
。- 它從
ch
列表獲取輸入通道c1
。 - 它從
args
([128, True]
)中獲取輸出通道c2=128
以及其他參數。 - 它根據
n=3
,循環3次來實例化模塊。 - 它將輸出通道
c2=128
添加到ch
列表中。
整個過程行云流水,因為C2f_CMUNeXtBlock
完美地扮演了C2f
的角色。
結論:對于想要替換現有標準模塊的自定義模塊,最佳實踐就是讓新模塊的參數接口與舊模塊保持高度一致。這樣就能實現“無痛”替換,將修改成本降至最低,僅需更改YAML中的一個字符串。
OK,今天我們就學習到這里🏆🎉👌!
文章參考
- YOLO系列官方論文:
- YOLOv8 by Ultralytics
- YOLOv1: You Only Look Once: Unified, Real-Time Object Detection
- 核心項目與代碼:
- YOLO Master GitHub by DataWhale
- Ultralytics YOLOv8 Documentation
拓展閱讀
- Ultralytics GitHub 倉庫 (YOLOv3/v5/v8 的主流實現)
- 作者的算法專欄 (包含更多YOLO技術文章)
💖 感謝您的耐心閱讀!
如果您覺得本文對您理解和實踐YOLO模型改造有所幫助,請考慮點贊、收藏或分享給更多有需要的朋友。您的支持是我持續創作優質內容的動力!歡迎在評論區交流討論,共同進步。