yolov8n

yolov8网络详解

yolov8的yaml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# Parameters
nc: 80 # 类别数 (COCO数据集默认80类)
scales: # 模型缩放系数 (n, s, m, l, x)
n: [0.33, 0.25, 1024]

# ================= Backbone (骨干网络) =================
# 负责从图像中提取特征,图片越来越小,特征越来越抽象
backbone:
# [from, repeats, module, args]
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2 (320x320)
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4 (160x160)
- [-1, 3, C2f, [128, True]]
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8 (80x80) -> 负责检测小物体
- [-1, 6, C2f, [256, True]]
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16 (40x40) -> 负责检测中物体
- [-1, 6, C2f, [512, True]]
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32 (20x20) -> 负责检测大物体 (RK3566的噩梦)
- [-1, 3, C2f, [1024, True]]
- [-1, 1, SPPF, [1024, 5]] # 9

# ================= Head (头部/颈部) =================
# 负责特征融合 (FPN + PAN),让网络同时拥有语义信息和定位信息
head:
# --- Top-down (自顶向下): 传达语义信息 ---
- [-1, 1, nn.Upsample, [None, 2, 'nearest']]
- [[-1, 6], 1, Concat, [1]] # 融合 P4
- [-1, 3, C2f, [512]] # 12

- [-1, 1, nn.Upsample, [None, 2, 'nearest']]
- [[-1, 4], 1, Concat, [1]] # 融合 P3
- [-1, 3, C2f, [256]] # 15 (这里是 P3 的最终特征)

# --- Bottom-up (自底向上): 传达定位信息 ---
- [-1, 1, Conv, [256, 3, 2]] # 下采样
- [[-1, 12], 1, Concat, [1]] # 融合之前的 P4 结果
- [-1, 3, C2f, [512]] # 18 (这里是 P4 的最终特征)

- [-1, 1, Conv, [512, 3, 2]] # 下采样
- [[-1, 9], 1, Concat, [1]] # 融合之前的 P5 结果
- [-1, 3, C2f, [1024]] # 21 (这里是 P5 的最终特征)

# --- Detect (检测头) ---
- [[15, 18, 21], 1, Detect, [nc]] # 输出 3 个尺度的检测结果

训练文件:datasets robot_data.yaml train.py yolov8n-2head.yaml

网络结构:yolov8n-2head.yaml

训练文件:train.py

配置文件:robot_data.yaml

train.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from ultralytics import YOLO

def main():
# ================= 修改重点在这里 =================
# 1. 指定我们要使用的“魔改版”网络结构
# 这会告诉程序:我要建立一个只有 2 个头的网络
model = YOLO('yolov8n-2head.yaml')

# 2. 加载官方预训练权重 (迁移学习)
# 这会告诉程序:把 yolov8n.pt 里能用的参数(比如 Backbone 的前几层)搬过来
# 这样训练比从零开始快得多
try:
model.load('yolov8n.pt')
except Exception as e:
print(f"权重加载警告 (正常现象,因为我们改了层数): {e}")
# =================================================

# 3. 开始训练
model.train(
data='robot_data.yaml', # 你的数据集配置
epochs=100,
imgsz=640, # 【建议】扫地机不需要640,480x480 或 448x448 在 RK3566 上更快
batch=16,
workers=4,
device=0,
name='robot_2head_v1', # 改个名字,标记这是2头版本

# --- NPU 优化参数 ---
amp=False, # 保持 False,这对 RKNN 转换非常重要!
cos_lr=True,
close_mosaic=10
)

if __name__ == '__main__':
main()

robot_data.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. 数据集根目录 (建议写绝对路径,或者相对于运行目录的路径)
path: 'datasets' # 修改为你刚才脚本输出的 datasets 文件夹路径

# 2. 训练集和验证集路径 (相对于上面的 path)
train: train/images
val: val/images

# 3. 类别数量 (线团、拖鞋、充电桩 -> 3类)
nc: 3

# 4. 类别名称 (这里的顺序 0,1,2 非常重要!)
# 建议按照这个顺序,方便后续映射到 SDK 的 ID (5, 4, 6)
names:
0: dock
1: shoes
2: person

yolov8n-2head.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# Ultralytics YOLOv8n-2head (RK3566 极速版)
# 针对类别:dock, shoes, person

# Parameters
nc: 3 # <--- 这里改成 3
scales:
n: [0.33, 0.25, 1024]

# Backbone (去掉了 P5)
backbone:
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- [-1, 3, C2f, [128, True]]
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 6, C2f, [256, True]]
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 6, C2f, [512, True]]
- [-1, 1, SPPF, [512, 5]] # 7

# Head (只出 2 个头)
head:
- [-1, 1, nn.Upsample, [None, 2, 'nearest']]
- [[-1, 4], 1, Concat, [1]] # cat P3
- [-1, 3, C2f, [256]] # 10

- [-1, 1, Conv, [256, 3, 2]]
- [[-1, 7], 1, Concat, [1]] # cat P4
- [-1, 3, C2f, [512]] # 13

- [[10, 13], 1, Detect, [nc]] # Detect(P3, P4)

指标:

YOLOv8的整体架构:

  • Backbone:特征提取网络,负责从图片中提取特征,把图片从清晰的像素点变成抽象的特征图。

  • Neck:特征融合网络 ,负责把深层的语义信息(是什么)和浅层的位置信息(在哪里)结合起来。

  • Head:检测头 。最后预测出框的位置和类别。

每一行参数 [depth, width, max_channels] 分别控制模型的三个维度:

  • depth (深度系数)控制网络的层数
    • 作用:决定了网络里的 C2f 模块重复堆叠多少次。
    • 举例:如果 depth=0.33,原本设计要堆 3 次的层,现在只堆 $3 \times 0.33 \approx 1$ 次。
    • 影响:深度越小,模型越浅,推理速度越快,但学习复杂特征的能力越弱。
  • width (宽度系数)控制通道数 (Channel)
    • 作用:决定了每一层卷积核的数量(也就是特征图的厚度)。
    • 举例:如果 width=0.25,原本标准是 128 个通道,现在只用 $128 \times 0.25 = 32$ 个通道。
    • 影响:宽度越小,显存占用越少,参数量越少,适合嵌入式设备(如 RK3566)。
  • max_channels (最大通道数)上限限制
    • 作用:不管怎么缩放,通道数都不能超过这个值。这通常是为了防止大模型在最后几层通道数爆炸,导致显存溢出。

语法结构:[from, repeats, module, args]

每一行都代表网络的一层,由 4 个参数组成:

  • from: 输入来自哪里?
    • -1 代表上一层(最常见)。
    • 如果写 -2 代表上上一层。
  • repeats: 这个模块重复堆叠几次?
    • 注意:这个数字会被顶部的 depth 系数(如 0.33)相乘。如果这里写 3,实际可能只堆 1 次。
  • module: 用什么算子?
    • Conv: 卷积层(负责下采样/提取特征)。
    • C2f: 核心特征提取模块(残差结构)。
    • SPPF: 空间金字塔池化(扩大视野)。
  • args: 传给模块的参数。
    • 对于 Conv,通常是 [输出通道数, 卷积核大小, 步长]
  1. nn.Upsample, [None, 2, "nearest"] (上采样)

这是要把图片“放大”。

  • nn.Upsample: 算子名称。作用是把小尺寸的特征图变大。
  • None: 代表不指定输出的具体像素大小(比如不写死成 40x40)。
  • 2: 缩放倍数。意思是把长和宽都乘以 2
  • "nearest": 最近邻插值。这是最简单的放大算法,速度最快,适合 RK3566。

大白话:把上一层(P5)那个 20x20 的极小缩略图,放大成 40x40,这样它就能跟 P4 层对齐了。

  1. Concat, [1] (特征拼接)

这是要把两张图“叠在一起”。

  • [-1, 6]: 代表这一层有两个输入。
    • -1: 上一层(刚放大的 P5)。
    • 6: Backbone 的第 6 层(原始的 P4)。
  • Concat: 拼接算子。
  • [1]: 在 维度 1(通道维) 进行拼接。

大白话:左手拿着刚放大的“语义图”,右手拿着 Backbone 里的“细节图”,把它们侧着粘在一起。

  • 注意:这时候图片的“厚度”(通道数)会相加。比如 256 通道加 256 通道,变成了 512 通道。
  1. C2f, [512] (特征融合)

这是要把拼接后的两股力量“拧成一股绳”。

  • 512: 输出通道数
  • 由于刚才 Concat 之后变厚了,我们需要用一个 C2f 模块来重新提取特征,把两者的信息深度融合,并把厚度固定在 512。
  1. Conv, [256, 3, 2] (下采样/缩小)

在后面“自底向上”的部分,你会看到这个:

YAML

1
- [15, 1, Conv, [256, 3, 2]]
  • 256: 输出通道数。
  • 3: 卷积核是 3x3。
  • 2: 步长 (Stride)。意思是每隔 2 个像素跳着走。
  • 结果:长宽会除以 2

大白话:这是把 80x80 的精细图缩小回 40x40,准备去跟深层的信息进行“二次融合”。

  1. YOLOEDetect, [nc, 512, True] (预测头)

这是最后那个“判官”。

  • nc: 你定义的类别数(3 类:dock, shoes, person)。
  • 512: 中间层的通道宽度。
  • True: 是否使用多尺度预测优化。

C2f模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class C2f(nn.Module):
"""Faster Implementation of CSP Bottleneck with 2 convolutions."""

def __init__(self, c1: int, c2: int, n: int = 1, shortcut: bool = False, g: int = 1, e: float = 0.5):
"""Initialize a CSP bottleneck with 2 convolutions.

Args:
c1 (int): Input channels.
c2 (int): Output channels.
n (int): Number of Bottleneck blocks.
shortcut (bool): Whether to use shortcut connections.
g (int): Groups for convolutions.
e (float): Expansion ratio.
"""
super().__init__()
self.c = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, 2 * self.c, 1, 1)#用一个 1x1 卷积,把输入的 c1 直接变成两倍的隐藏通道数。为什么要变两倍?为了后面能平分成两份。
self.cv2 = Conv((2 + n) * self.c, c2, 1) # optional act=FReLU(c2)
self.m = nn.ModuleList(Bottleneck(self.c, self.c, shortcut, g, k=((3, 3), (3, 3)), e=1.0) for _ in range(n))#shortcut:开启残差连接,e=1:因为 C2f 在最开始的 cv1 处已经把通道砍了一半(降维了),所以在中间的这些 Bottleneck 里,就不再进行第二次降维了

def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Forward pass through C2f layer."""
y = list(self.cv1(x).chunk(2, 1))#把刚才那个加厚的特征图,沿着通道维度(dim=1)一刀切成两半
y.extend(m(y[-1]) for m in self.m)
return self.cv2(torch.cat(y, 1))

def forward_split(self, x: torch.Tensor) -> torch.Tensor:#为了部署
"""Forward pass using split() instead of chunk()."""
y = self.cv1(x).split((self.c, self.c), 1)
y = [y[0], y[1]]
y.extend(m(y[-1]) for m in self.m)
return self.cv2(torch.cat(y, 1))

SPPF模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def forward(self, x: torch.Tensor) -> torch.Tensor:
# 1. 先过第一个卷积,降维
# 此时 y 列表里有一个元素:[特征图_原始] (感受野 1x1)
y = [self.cv1(x)]

# 2. 循环 n 次进行串行池化
# getattr 是为了兼容,默认 n=3
for _ in range(getattr(self, "n", 3)):
# y[-1] 取出上一次计算的结果进行池化
# 第一次循环:m(特征图_原始) -> 得到 [特征图_5x5]
# 第二次循环:m(特征图_5x5) -> 得到 [特征图_9x9] (因为 5x5 叠加 5x5)
# 第三次循环:m(特征图_9x9) -> 得到 [特征图_13x13]
y.append(self.m(y[-1]))

# 此时 y 列表里有 4 个张量:
# [特征图_原始, 特征图_5x5, 特征图_9x9, 特征图_13x13]

# 3. 拼接 (Concatenation)
# 将这 4 个尺度的特征图在通道维度 (dim=1) 拼接。
# 拼接实现了“多尺度特征融合”:同时拥有局部细节和全局语义信息。
# 拼接后的通道数为 4 * c_ = 2 * c1 (如果 n=3)
y = torch.cat(y, 1)

# 4. 融合输出
# 通过 cv2 将通道数调整为 c2
y = self.cv2(y)

# 5. 残差连接 (可选)
return y + x if getattr(self, "add", False) else y