Skip to content

【新手上路】手把手教你复现 Drone-YOLO:优化 YOLOv8 小目标检测

大家好,我是 RainbowRain! 🚀

你是否对目标检测很感兴趣,尤其是想提升模型在无人机航拍图像中对那些小小的、难以捕捉的目标的检测能力?今天,我将带你一步步复现 CSDN 博主分享的 Drone-YOLO 项目。这个项目基于强大的 YOLOv8,并针对性地做了几项改进,非常适合想动手实践模型优化的新手朋友们。

Drone-YOLO 的三大法宝:

  1. 主干网络下采样层换上 RepVGG: RepVGG 是一种特殊的卷积结构,训练时用多分支(比如 3x3 卷积、1x1 卷积、恒等映射),推理时能融合成一个单独的 3x3 卷积。这样做的好处是,训练时模型能学到更丰富的特征,但推理速度基本不受影响,甚至可能更快!
  2. 引入 P2 小目标检测头: YOLOv8 默认有 P3, P4, P5 三个检测头,分别检测不同大小的目标。Drone-YOLO 增加了一个 P2 检测头,它对应的特征图分辨率更高 (比如 160x160),专门用来“盯防”那些微小目标。
  3. Neck 部分采用“三明治结构”: 这是论文作者提出的一个新颖结构。在特征融合网络 (Neck) 中,它不仅仅是简单地把不同层级的特征拼接 (Concat) 起来,还额外引入了来自更上层、更大感受野的特征(通过深度可分离卷积下采样后),试图让模型更好地理解目标的空间位置信息。

本教程目标读者:

  • 对 YOLOv8 有基本了解。
  • 想学习如何修改 YOLOv8 网络结构。
  • 想提升模型对小目标的检测效果。
  • 不害怕动手改代码和配置文件!😉

准备工作:

  1. Python 环境: 确保你的 Python 版本 >= 3.8。
  2. PyTorch: 安装 PyTorch (版本 >= 1.8),并确保 CUDA 配置正确(如果你有 NVIDIA GPU)。
    bash
    # 示例:根据你的 CUDA 版本选择合适的命令
    # 访问 https://pytorch.org/ 获取最新安装指令
    pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
  3. Ultralytics YOLOv8: 我们需要修改 YOLOv8 的源码,所以建议克隆官方仓库并在本地进行修改。
    bash
    git clone https://github.com/ultralytics/ultralytics.git
    cd ultralytics
    pip install -e .  # 使用 -e 参数进行可编辑模式安装,这样你的修改会立即生效
  4. 数据集 (可选): 如果你想自己训练,可以准备一个无人机视角的数据集,比如 VisDrone。确保数据集已转换为 YOLO 格式。如果只是想学习如何修改模型,可以暂时跳过这一步。

🚀 第一步:让 YOLOv8 拥有 P2 小目标检测头

YOLOv8 本身就支持添加 P2 检测头。我们不需要从零开始,可以直接利用官方提供的带有 P2 层的配置文件。

  1. 找到 P2 配置文件: 在你克隆的 ultralytics 仓库中,找到 ultralytics/cfg/models/v8/yolov8-p2.yaml 文件。这个文件就是为 YOLOv8n/s/m/l/x 模型添加了 P2 检测头的版本。

  2. 复制并重命名: 为了后续修改,我们将基于 yolov8s-p2.yaml (假设我们以 YOLOv8s 为基础) 进行。

    • ultralytics/cfg/models/v8/ 目录下,复制 yolov8-p2.yaml
    • 将其重命名为 yolov8s-drone.yaml (或者你喜欢的任何名字,比如博客中用的 yolov8s-p2-repvgg-sf.yaml)。

    这个 yolov8s-drone.yaml 将是我们后续所有修改的起点。打开它,你会看到 backbonehead 部分的定义,其中 head 部分已经包含了处理 P2 层特征的逻辑,并且 Detect 模块会接收来自 P2, P3, P4, P5 四个层级的特征。


🛠️ 第二步:给主干网络换上 RepVGG“引擎”

Drone-YOLO 将主干网络中的标准下采样卷积层替换为了 RepVGG 模块。

1. 实现 RepVGGBlock 模块:

打开 ultralytics/nn/modules/block.py 文件。在文件的末尾,添加博主提供的 RepVGGBlock 代码。 注意:RepVGGBlock 内部用到了 conv_bnSEBlock (可选),以及 YOLOv8 自带的 ConvDWConv。确保这些依赖在 block.py 或其导入的模块中可用。博主提供的代码中 conv_bnSEBlock 是内联定义的,这很方便。

python
# 在 ultralytics/nn/modules/block.py 文件末尾添加以下代码:
# (代码来自 CSDN 博客,为了简洁,这里省略了 DFL, Proto 等原有代码)
# ... (block.py 中已有的其他类) ...

def conv_bn(in_channels, out_channels, kernel_size, stride, padding, groups=1):
    result = nn.Sequential()
    result.add_module('conv', nn.Conv2d(in_channels=in_channels, out_channels=out_channels,
                                        kernel_size=kernel_size, stride=stride, padding=padding, groups=groups,
                                        bias=False))
    result.add_module('bn', nn.BatchNorm2d(num_features=out_channels))
    return result

class SEBlock(nn.Module):
    def __init__(self, input_channels, internal_neurons):
        super(SEBlock, self).__init__()
        self.down = nn.Conv2d(in_channels=input_channels, out_channels=internal_neurons, kernel_size=1, stride=1,
                              bias=True)
        self.up = nn.Conv2d(in_channels=internal_neurons, out_channels=input_channels, kernel_size=1, stride=1,
                            bias=True)
        self.input_channels = input_channels

    def forward(self, inputs):
        x = F.avg_pool2d(inputs, kernel_size=inputs.size(3)) # 需要 import torch.nn.functional as F
        x = self.down(x)
        x = F.relu(x)
        x = self.up(x)
        x = torch.sigmoid(x)
        x = x.view(-1, self.input_channels, 1, 1)
        return inputs * x

class RepVGGBlock(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size=3,
                 stride=1, padding=1, dilation=1, groups=1, padding_mode='zeros', deploy=False, use_se=False):
        super(RepVGGBlock, self).__init__()
        self.deploy = deploy
        self.groups = groups
        self.in_channels = in_channels
        padding_11 = padding - kernel_size // 2
        self.nonlinearity = nn.SiLU() # YOLOv8 常用 SiLU
        # self.nonlinearity = nn.ReLU()
        if use_se:
            self.se = SEBlock(out_channels, internal_neurons=out_channels // 16)
        else:
            self.se = nn.Identity()
        if deploy:
            self.rbr_reparam = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size,
                                         stride=stride,
                                         padding=padding, dilation=dilation, groups=groups, bias=True,
                                         padding_mode=padding_mode)

        else:
            self.rbr_identity = nn.BatchNorm2d(
                num_features=in_channels) if out_channels == in_channels and stride == 1 else None
            self.rbr_dense = conv_bn(in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size,
                                     stride=stride, padding=padding, groups=groups)
            self.rbr_1x1 = conv_bn(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=stride,
                                   padding=padding_11, groups=groups)
            # print('RepVGG Block, identity = ', self.rbr_identity)
  
    # switch_to_deploy, get_equivalent_kernel_bias, _pad_1x1_to_3x3_tensor, _fuse_bn_tensor 方法
    # 请从博客中完整复制这些辅助方法到 RepVGGBlock 类内部
    # ... (此处省略这些辅助方法的具体实现,请确保从博客中完整复制) ...
    def switch_to_deploy(self):
         if hasattr(self, 'rbr_1x1'):
             kernel, bias = self.get_equivalent_kernel_bias()
             self.rbr_reparam = nn.Conv2d(in_channels=self.rbr_dense.conv.in_channels, out_channels=self.rbr_dense.conv.out_channels,
                                     kernel_size=self.rbr_dense.conv.kernel_size, stride=self.rbr_dense.conv.stride,
                                     padding=self.rbr_dense.conv.padding, dilation=self.rbr_dense.conv.dilation, groups=self.rbr_dense.conv.groups, bias=True)
             self.rbr_reparam.weight.data = kernel
             self.rbr_reparam.bias.data = bias
             for para in self.parameters():
                 para.detach_()
             self.__delattr__('rbr_dense') # 直接删除训练时的分支
             self.__delattr__('rbr_1x1')
             if hasattr(self, 'rbr_identity'):
                 self.__delattr__('rbr_identity')
             if hasattr(self, 'id_tensor'):
                 self.__delattr__('id_tensor')
             self.deploy = True

    def get_equivalent_kernel_bias(self):
        kernel3x3, bias3x3 = self._fuse_bn_tensor(self.rbr_dense)
        kernel1x1, bias1x1 = self._fuse_bn_tensor(self.rbr_1x1)
        kernelid, biasid = self._fuse_bn_tensor(self.rbr_identity)
        return kernel3x3 + self._pad_1x1_to_3x3_tensor(kernel1x1) + kernelid, bias3x3 + bias1x1 + biasid

    def _pad_1x1_to_3x3_tensor(self, kernel1x1):
        if kernel1x1 is None:
            return 0
        else:
            # Pytorch的pad是(左右上下),这里kernel是(out,in,h,w),所以pad最后两个维度
            return torch.nn.functional.pad(kernel1x1, [1, 1, 1, 1])


    def _fuse_bn_tensor(self, branch):
        if branch is None:
            return 0, 0
        if isinstance(branch, nn.Sequential): # Conv + BN
            kernel = branch.conv.weight
            running_mean = branch.bn.running_mean
            running_var = branch.bn.running_var
            gamma = branch.bn.weight
            beta = branch.bn.bias
            eps = branch.bn.eps
        else: # BN (identity branch)
            assert isinstance(branch, nn.BatchNorm2d)
            if not hasattr(self, 'id_tensor'):
                # 创建一个恒等映射的卷积核 (3x3),中心为1,其余为0
                input_dim = self.in_channels // self.groups
                kernel_value = torch.zeros((self.in_channels, input_dim, 3, 3), dtype=branch.weight.dtype, device=branch.weight.device)
                for i in range(self.in_channels):
                    kernel_value[i, i % input_dim, 1, 1] = 1
                self.id_tensor = kernel_value
            kernel = self.id_tensor
            running_mean = branch.running_mean
            running_var = branch.running_var
            gamma = branch.weight
            beta = branch.bias
            eps = branch.eps
        std = (running_var + eps).sqrt()
        t = (gamma / std).reshape(-1, 1, 1, 1)
        return kernel * t, beta - running_mean * gamma / std
      
    def forward(self, inputs):
        if self.deploy: # 推理阶段
            return self.nonlinearity(self.rbr_reparam(inputs)) # 使用融合后的单路3x3卷积
        # 训练阶段
        if self.rbr_identity is None:
            id_out = 0
        else:
            id_out = self.rbr_identity(inputs)
        return self.nonlinearity(self.se(self.rbr_dense(inputs) + self.rbr_1x1(inputs) + id_out))

# 确保在 block.py 顶部有:
# import torch
# import torch.nn as nn
# import torch.nn.functional as F
# from .conv import Conv, DWConv, GhostConv, LightConv, RepConv (如果RepVGGBlock内部没有用到这些,则不必需)
# from .transformer import TransformerBlock (同上)

重要: 确保 RepVGGBlock 类中的 forward 方法、switch_to_deploy 方法以及相关的辅助方法 (_fuse_bn_tensor, _pad_1x1_to_3x3_tensor, get_equivalent_kernel_bias) 都已从博客中完整、正确地复制。特别是 _fuse_bn_tensorid_tensor 的创建,需要确保设备和数据类型正确。

2. 注册 RepVGGBlock 模块:

  • 修改 ultralytics/nn/modules/__init__.py 打开此文件,在顶部导入 RepVGGBlock,并将其添加到 __all__ 列表中。
  ```python
  # ultralytics/nn/modules/__init__.py
  # ... (其他导入)
  from .block import (..., RepVGGBlock) # 在末尾或其他合适位置添加 RepVGGBlock
  
  __all__ = (..., 'RepVGGBlock') # 将 'RepVGGBlock' 添加到 __all__ 列表
  ```
  • 修改 ultralytics/nn/tasks.py 中的 parse_model 函数: 这个函数负责解析 YAML 配置文件并构建模型。我们需要让它认识 RepVGGBlock。 打开 ultralytics/nn/tasks.py,找到 parse_model 函数。在其中一个 elif m in (...) 条件语句中,加入 RepVGGBlock
  ```python
  # ultralytics/nn/tasks.py
  # 在 parse_model 函数内
  # ...
  elif m in (Classify, Conv, ConvTranspose, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, Focus,
             BottleneckCSP, C1, C2, C2f, C3, C3TR, C3Ghost, nn.ConvTranspose2d, DWConvTranspose2d, C3x, RepC3,
             RepVGGBlock): # <--- 在这里添加 RepVGGBlock
      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(c2 * gw, 8)
      args = [c1, c2, *args[1:]]
      # RepVGGBlock 的参数是 (in_channels, out_channels, kernel_size=3, stride=1, ...)
      # YAML 中 args 通常是 [out_channels, kernel_size, stride, ...]
      # 所以 [c1, c2, *args[1:]] 对应 (c1, args[0]_as_c2, args[1]_as_kernel_size, args[2]_as_stride, ...)
      # 这与 RepVGGBlock 的 __init__ 签名 (in_channels, out_channels, kernel_size, stride, ...) 匹配
      if m in (BottleneckCSP, C2, C2f, C3, C3TR, C3Ghost, C3x, RepC3): # RepVGGBlock 不在这里
          args.insert(2, n)  # number of repeats
          n = 1
  # ...
  ```
  *新手提示:`parse_model` 函数有点复杂。这里的修改是告诉 YOLOv8,当在 YAML 文件中看到 `RepVGGBlock` 时,应该如何处理它的参数并创建这个模块的实例。`c1` 是输入通道数 (自动从上一层获取),`args[0]` (即 `c2`) 是 YAML 中指定的输出通道数。`*args[1:]` 是 YAML 中指定的其他参数 (如 `kernel_size`, `stride`)。*

3. 在 YAML 配置文件中使用 RepVGGBlock:

现在,打开我们之前创建的 yolov8s-drone.yaml 文件。找到 backbone 部分,将原来的 Conv 下采样层替换为 RepVGGBlock。 根据 Drone-YOLO 论文,RepVGGBlock 用于主干网络的下采样层。

yaml
# yolov8s-drone.yaml

# Parameters
nc: 80  # number of classes
scales: # model compound scaling constants
  s: [0.33, 0.50, 1024] # YOLOv8s scales

# YOLOv8.0 backbone with RepVGG for downsampling
backbone:
  # [from, repeats, module, args]
  - [-1, 1, Conv, [64, 3, 2]]             # 0-P1/2 (第一个下采样通常还是普通 Conv)
  - [-1, 1, RepVGGBlock, [128, 3, 2]]     # 1-P2/4 (原Conv替换为RepVGGBlock)
  - [-1, 3, C2f, [128, True]]
  - [-1, 1, RepVGGBlock, [256, 3, 2]]     # 3-P3/8 (原Conv替换为RepVGGBlock)
  - [-1, 6, C2f, [256, True]]
  - [-1, 1, RepVGGBlock, [512, 3, 2]]     # 5-P4/16 (原Conv替换为RepVGGBlock)
  - [-1, 6, C2f, [512, True]]
  - [-1, 1, RepVGGBlock, [1024, 3, 2]]    # 7-P5/32 (原Conv替换为RepVGGBlock)
  - [-1, 3, C2f, [1024, True]]
  - [-1, 1, SPPF, [1024, 5]]              # 9

解释:RepVGGBlock, [128, 3, 2] 表示创建一个 RepVGGBlock,输出通道为 128,卷积核大小为 3,步长为 2。parse_model 会自动传入输入通道数。


🥪 第三步:打造“三明治结构”的 Neck

“三明治结构”是 Drone-YOLO 在 Neck 部分的一个特色改进。它在标准的特征金字塔网络 (FPN) 基础上,为每个融合层额外引入了一个来自更上层(但经过下采样)的特征。

修改 yolov8s-drone.yamlhead 部分:

根据博客提供的 yolov8s-p2-repvgg-sf.yaml(sf 代表 sandwich fusion)的 head 结构,我们需要:

  1. 在进行上采样和与 Backbone 特征 Concat 之前,从一个更靠后的层(对于 P4 的融合,是 Backbone 的 P3;对于 P3 的融合,是 Backbone 的 P2;对于 P2 的融合,是 Backbone 的 P1)取特征,用 DWConv (深度可分离卷积) 进行下采样。
  2. 然后将这个下采样后的特征、上采样后的特征、以及来自 Backbone 对应层级的特征三者一起 Concat。
yaml
# yolov8s-drone.yaml (继续修改)


<NolebasePageProperties />





# ... (backbone 部分如上) ...

# YOLOv8.0-p2 head with Sandwich Fusion
head:
  # [from, repeats, module, args]
  # P5 -> P4 path
  - [-1, 1, nn.Upsample, [None, 2, 'nearest']]  # 10 (from SPPF, layer 9)
  # Sandwich part for P4 fusion:
  # 从 backbone 的 P3 (layer 4) 获取特征,通过 DWConv 下采样 (模拟从更上层获取信息再下采样)
  # 注意:原文博客的图示和YAML中,DWConv的输入是backbone的对应层,然后与上采样层、以及更深层的主干特征concat
  # 这里我们严格按照博客提供的最终YAML结构来。
  # 博客的YAML中,DWConv的输入是backbone中较低一级的特征图,然后进行下采样。
  # 例如,在构建P4级别的Neck特征时,它会用到backbone的P4(layer 6)和上采样后的P5(layer 10)。
  # “三明治”的第三部分是来自backbone的P3(layer 4)经过DWConv下采样得到的。
  # DWConv [output_channels, kernel_size, stride]
  # 对于YOLOv8s,P3(layer 4)输出256通道,P4(layer 6)输出512通道。
  # DWConv的输出通道数需要匹配,或者Concat时要小心。
  # 博客的YAML中,DWConv的输出通道数是根据目标融合层的一半来设定的。
  # 例如,目标融合到P4 (512通道),则DWConv输出通道可能是256。
  # 但博客的YAML中,DWConv的输出通道数是根据其输入层来定的,例如输入P3(256通道),DWConv输出也是256。
  # 仔细看博客的YAML:
  # 目标是融合生成P4级别的特征 (最终输出给C2f是512通道)
  # 它用了:
  #   1. 上采样后的P5特征 (来自layer 10, 1024通道上采样后尺寸变大,通道数不变)
  #   2. Backbone的P4特征 (来自layer 6, 512通道)
  #   3. Backbone的P3特征 (来自layer 4, 256通道) 经过DWConv(stride=2)下采样,通道数256
  # Concat: [10 (P5_up), 6 (P4_bb), 11 (P3_dw_down)] -> C2f [512]
  # 通道数:1024(P5_up) + 512(P4_bb) + 256(P3_dw_down) = 1792. 这输入给C2f(512)是合理的。

  # 按照博客最终的YAML结构 (yolov8s-p2-repvgg-sf.yaml)
  # Neck for P4
  - [-1, 1, nn.Upsample, [None, 2, 'nearest']]    # 10: from layer 9 (SPPF out, P5 level)
  - [4, 1, DWConv, [256, 3, 2]]                   # 11: from layer 4 (backbone P3 out, 256ch), DWConv s=2. Output: 256ch, size / 2
                                                  # 注意:博客中 YOLOv8s 的 DWConv 通道为 L (上一层) 的一半,这里直接用了输入通道数。
                                                  # 博客的YAML中,s版本的DWConv通道数是输入通道数,例如[256,3,2]或[128,3,2]
                                                  # 我们遵循博客的YAML示例:
                                                  # 对于YOLOv8s (scale: [0.33, 0.50, 1024])
                                                  # P3 (layer 4) out: 256 * 0.50 = 128. DWConv [128, 3, 2]
                                                  # P2 (layer 2) out: 128 * 0.50 = 64.  DWConv [64, 3, 2]
                                                  # P1 (layer 0) out: 64 * 0.50 = 32.   DWConv [32, 3, 2]
                                                  # 我们将使用博客中提供的具体通道数。

  # Neck for P4 (最终输出给C2f是512通道)
  # Upsample P5
  - [-1, 1, nn.Upsample, [None, 2, 'nearest']]  # 10 (from layer 9, P5 level)
  # DWConv from Backbone P3 (layer 4)
  - [4, 1, DWConv, [128, 3, 2]]                 # 11 (from layer 4, P3_out=256 for base, 128 for 's'). Output 128 ch.
  # Concat P5_upsampled, Backbone_P4 (layer 6), DWConv_P3_downsampled
  - [[10, 6, 11], 1, Concat, [1]]               # 12. Channels: P5_up(512) + P4_bb(256) + P3_dw(128) = 896. (For 's' scale)
                                                  # P5_up (1024*0.5=512), P4_bb (512*0.5=256), P3_dw (256*0.5=128)
  - [-1, 3, C2f, [512, True]]                   # 13. Output P4 neck: 512 ch.

  # Neck for P3 (最终输出给C2f是256通道)
  - [-1, 1, nn.Upsample, [None, 2, 'nearest']]  # 14 (from layer 13, P4_neck)
  # DWConv from Backbone P2 (layer 2)
  - [2, 1, DWConv, [64, 3, 2]]                  # 15 (from layer 2, P2_out=128 for base, 64 for 's'). Output 64 ch.
  # Concat P4_neck_upsampled, Backbone_P3 (layer 4), DWConv_P2_downsampled
  - [[14, 4, 15], 1, Concat, [1]]               # 16. Channels: P4n_up(512) + P3_bb(128) + P2_dw(64) = 704
  - [-1, 3, C2f, [256, True]]                   # 17. Output P3 neck: 256 ch. (P3/8-small in blog YAML)

  # Neck for P2 (最终输出给C2f是128通道)
  - [-1, 1, nn.Upsample, [None, 2, 'nearest']]  # 18 (from layer 17, P3_neck)
  # DWConv from Backbone P1 (layer 0)
  - [0, 1, DWConv, [32, 3, 2]]                  # 19 (from layer 0, P1_out=64 for base, 32 for 's'). Output 32 ch.
  # Concat P3_neck_upsampled, Backbone_P2 (layer 2), DWConv_P1_downsampled
  - [[18, 2, 19], 1, Concat, [1]]               # 20. Channels: P3n_up(256) + P2_bb(64) + P1_dw(32) = 352
  - [-1, 3, C2f, [128, True]]                   # 21. Output P2 neck: 128 ch. (P2/4-xsmall in blog YAML)

  # PANet downsample path
  - [-1, 1, Conv, [128, 3, 2]]                  # 22 (from P2_neck, layer 21)
  - [[-1, 17], 1, Concat, [1]]                  # 23 cat with P3_neck (layer 17)
  - [-1, 3, C2f, [256, True]]                   # 24 (Output P3 PAN)

  - [-1, 1, Conv, [256, 3, 2]]                  # 25 (from P3_PAN, layer 24)
  - [[-1, 13], 1, Concat, [1]]                  # 26 cat with P4_neck (layer 13)
  - [-1, 3, C2f, [512, True]]                   # 27 (Output P4 PAN)

  - [-1, 1, Conv, [512, 3, 2]]                  # 28 (from P4_PAN, layer 27)
  - [[-1, 9], 1, Concat, [1]]                   # 29 cat with SPPF output (P5_backbone, layer 9)
                                                  # 注意:博客的YAML这里是 concat layer 9 (SPPF)
                                                  # 而不是concat backbone P5 (layer 8)
  - [-1, 3, C2f, [1024, True]]                  # 30 (Output P5 PAN)

  # Detect head
  - [[21, 24, 27, 30], 1, Detect, [nc]]  # Detect(P2, P3, P4, P5)
                                          # P2_neck (layer 21), P3_PAN (layer 24), P4_PAN (layer 27), P5_PAN (layer 30)

重要说明和新手提示:

  • 通道数和层索引: YAML 文件对层索引(from 列的数字)和通道数非常敏感。这里的通道数是基于 YOLOv8s (width_multiple: 0.50) 计算的。如果你用的是 YOLOv8n/m/l/x,这些通道数需要相应调整。
    • scales: s: [0.33, 0.50, 1024] 中的 0.50 是宽度乘数。Backbone 中定义的通道数会乘以这个值。例如,Conv, [64, 3, 2] 实际输出通道是 make_divisible(64 * 0.50, 8)
    • DWConv, [128, 3, 2] 中的 128 是输出通道数。你需要确保这个通道数与 Concat 层的期望以及后续 C2f 层的输入兼容。博客中提到 "YOLOv8s 三明治 DW 通道为 L 的一半",L 指的是输入 DWConv 的特征图通道数。但其最终 YAML 中 DWConv 的输出通道数是根据 scales 调整后的固定值(如 128, 64, 32)。
  • Concat 层的输入: [[10, 6, 11], 1, Concat, [1]] 表示将第 10 层、第 6 层和第 11 层的输出在通道维度 (dim=1)上拼接。
  • C2f 的 shortcut 参数: 在 Neck 部分,C2fshortcut 参数通常设为 True (如博客的 YAML 所示),如果输入和输出通道数相同,或者 C2f 内部能处理。如果通道数不同且没有内部调整,可能需要设为 False 或调整结构。博客的 YAML 中均为 True
  • 仔细核对博客的 YAML: 上述 YAML 是根据博客的描述和最终 yolov8s-p2-repvgg-sf.yaml 的结构意图编写的。强烈建议你直接参考并使用博主提供的完整 YAML 文件,因为它经过了测试。我的转写可能存在细微偏差。

🎉 第四步:训练你的 Drone-YOLO 模型

  1. 准备数据集配置文件: 例如 VisDrone.yaml,内容类似:

    yaml
    # VisDrone.yaml
    path: /path/to/your/VisDrone_dataset # 数据集根目录
    train: images/train  # 训练集图片路径 (相对于 path)
    val: images/val    # 验证集图片路径 (相对于 path)
    # test: images/test # 可选
    
    names:
      0: pedestrian
      1: person
      2: car
      3: van
      4: bus
      5: truck
      6: motor
      7: bicycle
      8: awning-tricycle
      9: tricycle
  2. 创建训练脚本 train_drone_yolo.py 在你的 ultralytics 文件夹(或其他工作目录)下创建一个 Python 文件,例如 train_drone_yolo.py

    python
    import os
    from ultralytics import YOLO
    
    if __name__ == '__main__':
        # 指定使用哪个 GPU (如果有多张卡)
        os.environ['CUDA_VISIBLE_DEVICES'] = '0'
    
        # 加载你的 Drone-YOLO 模型配置文件
        # model = YOLO(model="yolov8s.yaml")  # 原始 yolov8s
        # model = YOLO(model="yolov8s-p2.yaml")  # yolov8s + P2 头
        # model = YOLO(model="yolov8s-p2-repvgg.yaml") # yolov8s + P2 + RepVGG (如果单独测试)
        model = YOLO(model="cfg/models/v8/yolov8s-drone.yaml")  # 我们修改后的完整 Drone-YOLO (确保路径正确)
                                                                # 或者使用博客提供的: "yolov8s-p2-repvgg-sf.yaml"
    
        # 论文中提到未加载预训练权重,如果你想从头训练:
        # model = YOLO(model="cfg/models/v8/yolov8s-drone.yaml").load('') # 传入空字符串表示不加载权重
    
        # 如果想加载预训练的 YOLOv8s 权重作为起点 (推荐,可以加速收敛):
        # model.load('yolov8s.pt') # 这会加载权重到匹配的层
    
        # 开始训练
        results = model.train(
            data="path/to/your/VisDrone.yaml",  # 数据集配置文件路径
            imgsz=640,
            epochs=300, # 根据你的需求调整
            workers=8,
            batch=8,     # 根据你的显存调整
            cache=True,  # 缓存图片到内存或磁盘,加速训练
            project='runs/train_drone_yolo', # 训练结果保存路径
            name='exp'   # 实验名称
        )
    
        # (可选) 导出模型为 ONNX 格式
        # path = model.export(format="onnx", dynamic=True)
        print("训练完成!")
  3. 运行训练脚本:

    bash
    python train_drone_yolo.py

🧐 第五步:理解与分析

  • 监控训练: 训练过程中,你会看到 mAP、loss 等指标的变化。关注小目标类别的 AP 是否有提升。
  • 参考博客结论:
    • P2 检测头是涨点大户: 对于小目标密集的数据集,增加 P2 头通常效果显著。
    • RepVGG 锦上添花: 能在不牺牲(甚至优化)推理速度的前提下提升模型性能。
    • 三明治结构效果有限: 博客提到这个结构对性能提升不大,反而可能增加推理耗时。你可以尝试去掉三明治结构,只保留 P2 和 RepVGG,看看效果如何。

给新手的小贴士:

  1. 从简单开始: 如果觉得一次性改动太多容易出错,可以分步进行。先只加 P2 头,训练看看效果;再加入 RepVGG,再试三明治结构。
  2. 仔细检查路径: YAML 文件路径、数据集路径、权重路径等一定要写对。
  3. 版本问题: 确保你的 ultralytics 库、PyTorch 版本与教程或博客中使用的尽量一致,减少不必要的麻烦。
  4. 利用 model.info() 在加载模型后,可以使用 model.info(verbose=True) 打印模型结构和参数量,帮助你检查 YAML 是否被正确解析。
  5. 多看官方文档和社区: Ultralytics 的 GitHub 仓库有很多有用的 Issue 和讨论。

总结

恭喜你!如果能一步步跟下来,相信你对如何修改和定制 YOLOv8 有了更深的理解。Drone-YOLO 的复现是一个很好的实践项目,它融合了当前一些流行的改进思路。记住,目标检测是一个不断发展的领域,多动手、多思考,你也能成为大神!

我是 RainbowRain,希望这篇新手教程能帮到你!如果你在实践中遇到任何问题,欢迎随时提问。祝你玩得开心,模型越调越牛!🎉

贡献者

The avatar of contributor named as RainbowRain9 RainbowRain9
The avatar of contributor named as Cai Hongyu Cai Hongyu

文件历史

撰写