前言
YOLOv8作为目标检测领域的经典模型,凭借其高效的检测能力在诸多应用场景中得到了广泛应用。然而,随着任务复杂性的增加,如在医学影像中的脑肿瘤检测,标准的YOLOv8模型可能会遇到一些性能瓶颈。在这篇博客中,我将展示如何通过将YOLOv8中的C2f模块替换为RCSOSA模块来改进模型性能,并提升其在复杂场景下的表现。
什么是RCSOSA模块?
RCSOSA模块简介
RCSOSA模块全称是“基于通道混洗和一击聚合的重参数化卷积”(Reparameterized Convolution based on channel Shuffle and One-Shot Aggregation),是RCS-YOLO模型中的一个核心组件。它的设计初衷是通过减少推理时的计算负担来加速处理,同时在训练阶段增强特征提取能力。这种模块在医学影像处理,特别是脑肿瘤检测等复杂任务中,表现出了极高的有效性。
为什么选择RCSOSA模块?
在传统YOLO模型中,C2f模块是常用的特征处理组件。然而,在处理复杂场景或小目标时,C2f模块的表现可能有所欠缺。RCSOSA模块的引入正是为了弥补这些不足,具体来说,它带来了以下几方面的改进:
-
结构重参数化:这种技术在训练阶段允许模型学习更多的细节特征,然后在推理阶段通过简化结构来降低计算量,从而既保证了速度又不损失精度。
-
通道混洗(Channel Shuffle):RCSOSA模块通过通道混洗技术,使得不同通道的特征信息能够更好地交流,从而提高了模型在处理复杂图像时的准确性。
-
一击聚合(One-Shot Aggregation):通过“一击聚合”,RCSOSA模块可以在最后一步高效地汇总所有重要信息,这不仅提升了模型处理的效率,还确保了特征信息的充分利用。
RCSOSA模块在脑肿瘤检测中的应用
在医学影像处理,尤其是脑肿瘤检测任务中,RCSOSA模块表现出了显著的优势。实验结果显示,与YOLOv8模型相比,RCS-YOLO模型的检测速度提高了60%,每秒可以处理大约114张图像,精度也提升了1%。这些改进使得RCS-YOLO模型在医学图像检测任务中脱颖而出,特别是在需要高度精细检测的场景中。
如何在YOLOv8中替换C2f模块
接下来,我们将介绍如何在YOLOv8中使用RCSOSA模块替换C2f模块。这个过程分为以下几步:
第一步:在 block.py
中注册 RCSOSA 模块
1. 打开 ultralytics/nn/modules/block.py
文件。
2. 在文件的顶部,通常在其他模块导入的区域,添加以下代码以导入 RCSOSA
模块,并注册它的名称。
"RCSOSA",
第二步:在 ultralytics/nn/modules/block.py
中添加 RCSOSA
模块代码
在第一步中,我们已经在 ultralytics/nn/modules/block.py
文件的顶部注册了 RCSOSA
模块。现在,我们要将 SR
和 RCSOSA
类的实现代码添加到 block.py
文件中。
1. 打开 ultralytics/nn/modules/block.py
文件。
2. 在 block.py
文件中,添加以下代码:
class SR(nn.Module):
# Shuffle RepVGG
def __init__(self, c1, c2):
super().__init__()
c1_ = int(c1 // 2)
c2_ = int(c2 // 2)
self.repconv = RepConv(c1_, c2_, bn=True)
def forward(self, x):
x1, x2 = x.chunk(2, dim=1)
out = torch.cat((x1, self.repconv(x2)), dim=1)
out = self.channel_shuffle(out, 2)
return out
def channel_shuffle(self, x, groups):
batchsize, num_channels, height, width = x.data.size()
channels_per_group = num_channels // groups
x = x.view(batchsize, groups, channels_per_group, height, width)
x = torch.transpose(x, 1, 2).contiguous()
x = x.view(batchsize, -1, height, width)
return x
class RCSOSA(nn.Module):
# VoVNet with Res Shuffle RepVGG
def __init__(self, c1, c2, n=1, se=False, g=1, e=0.5):
super().__init__()
n_ = n // 2
c_ = make_divisible(int(c1 * e), 8)
self.conv1 = RepConv(c1, c_, bn=True)
self.conv3 = RepConv(int(c_ * 3), c2, bn=True)
self.sr1 = nn.Sequential(*[SR(c_, c_) for _ in range(n_)])
self.sr2 = nn.Sequential(*[SR(c_, c_) for _ in range(n_)])
self.se = None
if se:
self.se = SEAttention(c2)
def forward(self, x):
x1 = self.conv1(x)
x2 = self.sr1(x1)
x3 = self.sr2(x2)
x = torch.cat((x1, x2, x3), 1)
return self.conv3(x) if self.se is None else self.se(self.conv3(x))
3. 保存 block.py
文件。
第三步:在 ultralytics-main/ultralytics/utils/torch_utils.py
中添加 make_divisible
函数
你需要在 ultralytics-main/ultralytics/utils/torch_utils.py
文件中添加 make_divisible
函数,并确保该函数在其他模块中可以正确导入和使用。按照以下步骤进行操作:
1. 打开 torch_utils.py
文件:进入 ultralytics-main/ultralytics/utils/
目录,找到并打开 torch_utils.py
文件。
2. 添加 make_divisible
函数:在文件中合适的位置添加 make_divisible
函数。你可以将其放置在 def scale_img(img, ratio=1.0, same_shape=False, gs=32):
函数之后,或在文件的其他合适位置。
def scale_img(img, ratio=1.0, same_shape=False, gs=32):
"""Scales and pads an image tensor of shape img(bs,3,y,x) based on given ratio and grid size gs, optionally
retaining the original shape.
"""
if ratio == 1.0:
return img
h, w = img.shape[2:]
s = (int(h * ratio), int(w * ratio)) # new size
img = torch.nn.functional.interpolate(img, size=s, mode="bilinear", align_corners=False) # resize
if not same_shape: # pad/crop img
h, w = (math.ceil(x * ratio / gs) * gs for x in (h, w))
return torch.nn.functional.pad(img, [0, w - s[1], 0, h - s[0]], value=0.447) # value = imagenet mean
def make_divisible(x, divisor):#添加该函数
"""Returns nearest x divisible by divisor."""
if isinstance(divisor, torch.Tensor):
divisor = int(divisor.max()) # 将 Tensor 转换为 int
return math.ceil(x / divisor) * divisor
3. 确保在其他模块中正确导入 make_divisible
函数:确保你在 ultralytics/nn/modules/block.py
文件中通过以下方式正确导入 make_divisible
函数:
from ultralytics.utils.torch_utils import make_divisible # 从 torch_utils 导入 make_divisible 函数
第四步:将 SEAttention
类添加到 common.py
并在 block.py
中导入
在第四步中,你需要将 SEAttention
类添加到 ultralytics-main/ultralytics/nn/modules/common.py
文件中,并在 block.py
文件中正确导入该类。请按照以下步骤操作:
1. 在 common.py
中添加 SEAttention
类
-
打开
common.py
文件:- 进入
ultralytics-main/ultralytics/nn/modules/
目录,找到并打开common.py
文件。如果common.py
文件不存在,可以创建一个新的文件。
- 进入
-
添加
SEAttention
类:- 在
common.py
文件中,添加以下SEAttention
类的定义:import torch import torch.nn as nn import torch.nn.init as init class SEAttention(nn.Module): def __init__(self, channel=512, reduction=16): super().__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) self.fc = nn.Sequential( nn.Linear(channel, channel // reduction, bias=False), nn.ReLU(inplace=True), nn.Linear(channel // reduction, channel, bias=False), nn.Sigmoid() ) def init_weights(self): for m in self.modules(): if isinstance(m, nn.Conv2d): init.kaiming_normal_(m.weight, mode='fan_out') if m.bias is not None: init.constant_(m.bias, 0) elif isinstance(m, nn.BatchNorm2d): init.constant_(m.weight, 1) init.constant_(m.bias, 0) elif isinstance(m, nn.Linear): init.normal_(m.weight, std=0.001) if m.bias is not None: init.constant_(m.bias, 0) def forward(self, x): b, c, _, _ = x.size() y = self.avg_pool(x).view(b, c) y = self.fc(y).view(b, c, 1, 1) return x * y.expand_as(x)
- 在
2. 在 block.py
文件中导入 SEAttention
-
打开
block.py
文件:- 进入
ultralytics-main/ultralytics/nn/modules/
目录,找到并打开block.py
文件。
- 进入
-
添加导入语句:
- 在
block.py
文件的顶部,添加以下导入语句以使用SEAttention
from ultralytics.nn.modules.common import SEAttention
- 在
第五步:在 __init__.py
中注册 RCSOSA
模块
1. 打开 ultralytics/nn/modules/__init__.py
文件。
2. 在文件中注册 RCSOSA
模块名。
在 __init__.py
文件中,找到其他模块注册的地方,然后添加 RCSOSA
模块的导入和注册。
3. 保存文件。
第六步:在 tasks.py
顶部注册 RCSOSA
模块
-
打开
ultralytics/nn/tasks.py
文件。 -
在文件顶部导入
RCSOSA
模块。
第七步:在 parse_model
函数中添加对 RCSOSA
模块的支持
-
找到
parse_model
函数:打开ultralytics/nn/tasks.py
文件,并找到parse_model
函数的定义。这通常是处理模型配置文件的函数。 -
在
parse_model
中添加RCSOSA
模块:在
parse_model
函数中,你会看到一段代码负责根据配置文件中的模块名创建相应的模块对象。在处理C2f
模块的代码附近,添加对RCSOSA
模块的支持。 -
第一个需要添加的地方:
在
parse_model
函数中,当我们处理模块时,确保RCSOSA
被包含在处理C2f
、C3
等类似模块的地方。if m in { RCSOSA, # 添加RCSOSA支持 Classify, Conv, ConvTranspose, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, Focus, BottleneckCSP, C1, C2, C2f, RepNCSPELAN4, ELAN1, ADown, AConv, SPPELAN, C2fAttn, C3, C3TR, C3Ghost, nn.ConvTranspose2d, DWConvTranspose2d, C3x, RepC3, PSA, SCDown, C2fCIB, }:
2. 第二个需要添加的地方:
在将
args
插入到模块构造函数的参数中时,确保RCSOSA
模块与其他类似模块一起处理。if m in {BottleneckCSP, C1, C2, C2f, C2fAttn, C3, C3TR, C3Ghost, C3x, RepC3, C2fCIB, RCSOSA}: # 添加RCSOSA支持 args.insert(2, n) # number of repeats n = 1
完整代码片段示例
def parse_model(d, ch, verbose=True): # model_dict, input_channels(3) """Parse a YOLO model.yaml dictionary into a PyTorch model.""" import ast # Args max_channels = float("inf") nc, act, scales = (d.get(x) for x in ("nc", "activation", "scales")) depth, width, kpt_shape = (d.get(x, 1.0) for x in ("depth_multiple", "width_multiple", "kpt_shape")) if scales: scale = d.get("scale") if not scale: scale = tuple(scales.keys())[0] LOGGER.warning(f"WARNING ⚠️ no model scale passed. Assuming scale='{scale}'.") depth, width, max_channels = scales[scale] if act: Conv.default_act = eval(act) # redefine default activation, i.e. Conv.default_act = nn.SiLU() if verbose: LOGGER.info(f"{colorstr('activation:')} {act}") # print if verbose: LOGGER.info(f"\n{'':>3}{'from':>20}{'n':>3}{'params':>10} {'module':<45}{'arguments':<30}") ch = [ch] layers, save, c2 = [], [], ch[-1] # layers, savelist, ch out for i, (f, n, m, args) in enumerate(d["backbone"] + d["head"]): # from, number, module, args m = getattr(torch.nn, m[3:]) if "nn." in m else globals()[m] # get module for 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) n = n_ = max(round(n * depth), 1) if n > 1 else n # depth gain if m in { RCSOSA, # 添加RCSOSA支持 Classify, Conv, ConvTranspose, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, Focus, BottleneckCSP, C1, C2, C2f, RepNCSPELAN4, ELAN1, ADown, AConv, SPPELAN, C2fAttn, C3, C3TR, C3Ghost, nn.ConvTranspose2d, DWConvTranspose2d, C3x, RepC3, PSA, SCDown, C2fCIB, }: c1, c2 = ch[f], args[0] if c2 != nc: # if c2 not equal to number of classes (i.e. for Classify() output) c2 = make_divisible(min(c2, max_channels) * width, 8) if m is C2fAttn: args[1] = make_divisible(min(args[1], max_channels // 2) * width, 8) # embed channels args[2] = int( max(round(min(args[2], max_channels // 2 // 32)) * width, 1) if args[2] > 1 else args[2] ) # num heads args = [c1, c2, *args[1:]] if m in {BottleneckCSP, C1, C2, C2f, C2fAttn, C3, C3TR, C3Ghost, C3x, RepC3, C2fCIB, RCSOSA}: # 添加RCSOSA支持 args.insert(2, n) # number of repeats n = 1
通过完成这两个修改点,
RCSOSA
模块已经被成功添加到parse_model
函数中,并且在YOLOv8配置文件被解析时能够正确地处理和使用该模块。
第八步:创建或修改 yolov8-RCSOSA.yaml
配置文件
-
创建或打开配置文件:
在
ultralytics/cfg/models/v8/
目录下,创建一个名为yolov8-RCSOSA.yaml
的新文件,或者你可以复制一个现有的配置文件(如yolov8n.yaml
),并在此基础上进行修改。 -
修改配置文件以使用
RCSOSA
模块:在配置文件中,我们将原来的
C2f
模块替换为RCSOSA
模块。以下是一个示例配置文件的结构:# Ultralytics YOLO 🚀, AGPL-3.0 license # YOLOv8 object detection model with P3-P5 outputs. For Usage examples see https://docs.ultralytics.com/tasks/detect # Parameters nc: 80 # number of classes scales: # model compound scaling constants, i.e. 'model=yolov8n.yaml' will call yolov8.yaml with scale 'n' # [depth, width, max_channels] n: [0.33, 0.25, 1024] # YOLOv8n summary: 225 layers, 3157200 parameters, 3157184 gradients, 8.9 GFLOPs s: [0.33, 0.50, 1024] # YOLOv8s summary: 225 layers, 11166560 parameters, 11166544 gradients, 28.8 GFLOPs m: [0.67, 0.75, 768] # YOLOv8m summary: 295 layers, 25902640 parameters, 25902624 gradients, 79.3 GFLOPs l: [1.00, 1.00, 512] # YOLOv8l summary: 365 layers, 43691520 parameters, 43691504 gradients, 165.7 GFLOPs x: [1.00, 1.25, 512] # YOLOv8x summary: 365 layers, 68229648 parameters, 68229632 gradients, 258.5 GFLOPs # YOLOv8.0n backbone backbone: # [from, repeats, module, args] - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2 - [-1, 1, Conv, [128, 3, 2]] # 1-P2/4 - [-1, 3, RCSOSA, [128]] - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8 - [-1, 6, RCSOSA, [256]] - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16 - [-1, 6, RCSOSA, [512, True]] - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32 - [-1, 3, RCSOSA, [1024, True]] - [-1, 1, SPPF, [1024, 5]] # 9 # YOLOv8.0n head head: - [-1, 1, nn.Upsample, [None, 2, 'nearest']] - [[-1, 6], 1, Concat, [1]] # cat backbone P4 - [-1, 3, RCSOSA, [512]] # 12 - [-1, 1, nn.Upsample, [None, 2, 'nearest']] - [[-1, 4], 1, Concat, [1]] # cat backbone P3 - [-1, 3, RCSOSA, [256]] # 15 (P3/8-small) - [-1, 1, Conv, [256, 3, 2]] - [[-1, 12], 1, Concat, [1]] # cat head P4 - [-1, 3, RCSOSA, [512]] # 18 (P4/16-medium) - [-1, 1, Conv, [512, 3, 2]] - [[-1, 9], 1, Concat, [1]] # cat head P5 - [-1, 3, RCSOSA, [1024]] # 21 (P5/32-large) - [[15, 18, 21], 1, Detect, [nc]] # Detect(P3, P4, P5)