YOLO v8n 踩的坑

容小狸 Lv3

准备魔改YOLO v8,实现一个基于YOLO v8的实时交通标识检测系统,这篇文章就是记录一下步骤。 那么问题来了,我甚至都没有做过视觉模型的项目,我该怎么做呢?

那么首先从YOLO v8开始,学习它的架构和实现。

准备工作

WSL 配置

Windows主力机就用的WSL2,可以直接按照 Microsoft的文档 安装WSL2。

使用添加程序和功能,找到Windows Subsystem for Linux,勾选它。

之后,这个:

1
2
3
wsl --update 
wsl --set-default-version 2
wsl --install --distribution debian

后来发觉自带的命令行不太舒服,就安装了Windows Terminal。 这个选装,不影响使用,如果不想看直接跳到初始化Git仓库。

安装Windows Terminal

可以参考Microsoft的文档 安装Windows Terminal。 我直接使用GitHub的版本,下载页看这里: Windows Terminal GitHub页面

下载那个后缀为msix的文件,双击安装。安装完以后Win+R输入wt,就可以打开Windows Terminal。

初始化Git仓库

初始化Git仓库,将ultralytics的仓库作为子模块复制到本地,正好帮我补充Git子模块的知识,小巧思这块儿。 (这一步是因为我的服务器没有GPU,没法使用opencv,只能使用opencv-headless代替, 之后在其他机器上的部署都是直接使用的pip内的ultralytics库)

后期注

注意,我在写这段的时候还没装GPU,但是之后我买了张P40 24G,之后我就不对CPU训练单独测试了。

1
2
git submodule init
git submodule add https://github.com/ultralytics/ultralytics.git

pip安装ultralytics库之后,会有一个叫yolo的命令,这是ultralytics提供的一个命令行工具,用于训练和推理YOLO模型。

YOLO v8 复现

YOLO 识别复现

YOLO识别前
1
2
3
4
5
from ultralytics import YOLO

model = YOLO('yolov8n-oiv7.pt')
model.info()
model.predict(source='bus.jpg', save=True)
YOLO识别后

秒了。

YOLO 训练复现

1
2
3
4
5
from ultralytics import YOLO
# Start YOLO v8 training with yaml config file.
print("Start training with yaml config file.")
yolo_yaml = YOLO('./yolov8.yaml')
yolo_yaml.train(epochs=5, data='./TT100K.yaml', resume=True)

有结果了,结果出在ultralytics/runs/detect/下,根据名字不同,每个文件夹对应一个训练任务。 就不指望这个训练结果能有什么了,上面这段只是用来验证一下能否训练的。

自定义数据集

感谢@Tianli发布的TT100K-YOLO数据集,原本的TT100K是无法直接用于YOLO训练的。Tianli这个集经过清洗,可以直接用于YOLO的训练。

当然,ultralytics自己也提供了一个数据YAML,也可以直接训练。(位置在ultralytics/cfg/datasets/TT100K.yaml) 我先是用的Tianli的数据集,然后再使用的ultralytics的YAML。

自定义数据集有点反应,但是效果比较差。试试看ultralytics的数据集。

一直被结果折磨。为啥效果还是差呢?

751.jpg
结果

参数如下:

1
2
3
4
5
yolo_coco.train(name='YOLOv8-COCO', epochs=300, data='./TT100K.yaml', 
save_period=5, patience=70, batch=16, workers=2,
close_mosaic=50, hsv_h=0.02, hsv_s=0.72, hsv_v=0.5, cutmix=0.2,
device=0,
amp=False)

DeepSeek给出了一份训练参数,如下:

1
2
3
4
5
6
7
8
9
# Training Settings
epochs: 300 # TT100K类别多(221类),需要更长训练;前两次300epoch仍未收敛
batch: 16 # 稳定批次大小,适应显存
imgsz: 640 # 若资源允许,建议1280以提升小目标检测;640为底线
patience: 50 # 验证mAP 50轮不升则早停,节省时间
seed: 0
deterministic: true
amp: true # 混合精度加速训练
# 其他参数,以下省略

其中有这么一句话:

epochs: 300 # TT100K类别多(221类),需要更长训练;前两次300epoch仍未收敛

未收敛?加到700epochs试试。

秒了。附个结果以及两条曲线。

mAP50-95
recall

附上一个最终使用的YAML(最后使用的是Ultralytics的训练平台,这样可以训练快一些。):

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
44
45
46
# Training Settings
epochs: 700 # 保持700不变
batch: 16 # 稳定批次大小,适应显存。如果使用-1则会变成batch=1,练了等于没练(P40上是这样的,5090上就没问题,怪)
imgsz: 640 # 若资源允许,建议1280以提升小目标检测;640为底线
patience: 50 # 验证mAP 50轮不升则早停,节省时间
seed: 0
deterministic: true
amp: false # 混合精度加速训练(关)
cos_lr: true # 余弦退火调度,对长训练收敛至关重要
close_mosaic: 15 # 最后15轮关闭mosaic,进行细粒度微调
save_period: 5 # 仅保存最优和最后
fraction: 1 # 全训练集训练
freeze: null # 从scratch训练不冻结;若用预训练可冻结backbone前10层
single_cls: false
rect: false
multi_scale: 0
resume: false
optimizer: auto # 自动选择优化器(AdamW或SGD)
lr0: 0.012 # 若从COCO预训练权重微调,0.01合适;若从scratch可略高
lrf: 0.01 # cos_lr下最终lr = lr0 * lrf = 0.0001
momentum: 0.937
weight_decay: 0.0005
warmup_epochs: 5 # 较长热身,帮助分类头平稳启动(比默认3更稳)
warmup_momentum: 0.8
warmup_bias_lr: 0.1
box: 7.5
cls: 0.5 # 分类损失权重;若类别不平衡严重可尝试0.7-1.0
dfl: 1.5
pose: 12
kobj: 1
label_smoothing: 0.1 # 标签平滑,缓解多类别过拟合和不平衡问题

# Image Augmentations
hsv_h: 0.05 # 交通标志颜色相对标准,轻微色调扰动即可
hsv_s: 0.5 # 饱和度扰动不宜过大,避免颜色失真
hsv_v: 0.5 # 明度扰动适中
degrees: 0.15 # 略微旋转,应对歪斜情况
translate: 0.15 # 小幅平移增强鲁棒性(较DeepSeek版略微加强)
scale: 0.65 # 多尺度缩放,帮助应对不同大小标志
shear: 0.1 # 无需错切
perspective: 0.00001 # 些许透视变换(鲁棒性)
flipud: 0 # 交通标志不应上下翻转
fliplr: 0.5 # 水平翻转合理(左右对称的标志)
mosaic: 1.0 # mosaic对小目标检测帮助大,但需close_mosaic提前关
mixup: 0.2 # mixup缓解类别不平衡,提升泛化
copy_paste: 0.1 # 小目标复制粘贴增强,缓解样本不均

魔改YOLO v8

在此之前……

说起来,为什么AMP开启之后会自动下载YOLO26呢?这个问题一直没搞懂,从开始复现YOLOv8n的训练开始,一直到在官网训练好了YOLOv8n,它一直都需要下载YOLO26n的模型权重,这件事情困扰了我很久,直到现在。AI真好用。来自DeepSeek:

问题根因分析

你遇到的不是bug,而是 Ultralytics 最新版本中 AMP 功能检查的硬编码行为

核心原因

ultralytics/utils/checks.pycheck_amp() 函数中(约第 765 行),AMP 验证硬编码使用了 YOLO("yolo26n.pt") 来进行混合精度功能检查:

1
2
3
4
5
6
7
8
def check_amp(model):
# ...
try:
from ultralytics import YOLO
assert amp_allclose(YOLO("yolo26n.pt"), im) # ← 硬编码 yolo26n.pt
LOGGER.info(f"{prefix}checks passed ✅")
except ConnectionError:
LOGGER.warning(f"{prefix}checks skipped. Offline and unable to download YOLO26n for AMP checks. ...")
为什么 amp=true 和 amp=false 行为不同?

ultralytics/engine/trainer.py_setup_train() 方法中:

1
2
3
self.amp = torch.tensor(self.args.amp).to(self.device)
if self.amp and RANK in {-1, 0}: # ← 仅当 amp=True 时才触发
self.amp = torch.tensor(check_amp(self.model), device=self.device)
  • amp: true → 触发 check_amp() → 下载 yolo26n.pt 进行 AMP 功能验证
  • amp: false → 跳过 check_amp() → 不下载任何东西,直接训练
这会改变你的模型吗?

不会。 下载的 yolo26n.pt 仅用于验证 AMP 是否在你的 GPU 上正常工作,不会替换你指定的 YOLOv8 YAML 文件。你的实际训练模型仍然是 YOLOv8n。这个下载纯粹是功能检查的副作用。

这是已知问题

GitHub 上已有 PR #23199(“Amp check yolo26”)试图将硬编码的模型名改为动态获取,但因代码缺陷(NameError)被关闭。截至目前(2026年5月),该问题尚未修复。

确实嗷,看这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try:
from ultralytics import YOLO

assert amp_allclose(YOLO("yolo26n.pt"), im)
LOGGER.info(f"{prefix}checks passed ✅")
except ConnectionError:
LOGGER.warning(f"{prefix}checks skipped. Offline and unable to download YOLO26n for AMP checks. {warning_msg}")
except (AttributeError, ModuleNotFoundError):
LOGGER.warning(
f"{prefix}checks skipped. "
f"Unable to load YOLO26n for AMP checks due to possible Ultralytics package modifications. {warning_msg}"
)
except AssertionError:
LOGGER.error(
f"{prefix}checks failed. Anomalies were detected with AMP on your system that may lead to "
f"NaN losses or zero-mAP results, so AMP will be disabled during training."
)
return False

哪个小b……聪明写的这玩意儿? 气死我了。问题解决,AMP启动,下载就下载吧,你只要别把训练目标自动切换为YOLO26n就可以了。

魔改:CBAM注意力

所以,为什么是注意力机制?YOLOv8本身的结构是一种卷积神经网络的变体,它的原有结构没有设计为为微型目标专门设计优化。 众所不周知,卷积是对所有的特征进行统一提取,没有针对性的提取不同目标的特征。 所以需要引入注意力机制,来提取不同目标的特征。

注意力有很多,比如SE-Net、CBAM、ECA-Net等。这里就取CBAM。

CBAM在YOLO仓库里有一个声明,但是好像有点小问题。那借助DSv4Pro重写一下:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
"""
CBAM 模块 + 猴子补丁(惰性初始化,适配任意缩放因子)
======================================================
基于 CBAM: Convolutional Block Attention Module (Woo et al., ECCV 2018)。
模块参数在第一次 forward 时根据输入通道数自动构建,无需在 YAML 中指定 c1。

YAML 用法:
[-1, 1, CBAM, [16, 7]] # reduction=16, kernel_size=7

导入即完成注册:
import cbam_patch
"""

import torch
import torch.nn as nn


# ═══════════════════════════════════════════════════════════════════
# 子模块(支持惰性构建)
# ═══════════════════════════════════════════════════════════════════

class ChannelAttention(nn.Module):
"""通道注意力:AvgPool + MaxPool → 共享 MLP → Sigmoid(与原论文一致)"""
def __init__(self, in_channels: int, reduction: int = 16):
super().__init__()
mid_channels = max(1, in_channels // reduction)
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.max_pool = nn.AdaptiveMaxPool2d(1)
self.mlp = nn.Sequential(
nn.Conv2d(in_channels, mid_channels, 1, bias=False),
nn.ReLU(inplace=True),
nn.Conv2d(mid_channels, in_channels, 1, bias=False),
)
self.sigmoid = nn.Sigmoid()

def forward(self, x: torch.Tensor) -> torch.Tensor:
avg_out = self.mlp(self.avg_pool(x))
max_out = self.mlp(self.max_pool(x))
return x * self.sigmoid(avg_out + max_out)


class SpatialAttention(nn.Module):
"""空间注意力:沿通道 mean+max → 拼接 → 7×7 卷积 → Sigmoid"""
def __init__(self, kernel_size: int = 7):
super().__init__()
padding = kernel_size // 2
self.conv = nn.Conv2d(2, 1, kernel_size, padding=padding, bias=False)
self.sigmoid = nn.Sigmoid()

def forward(self, x: torch.Tensor) -> torch.Tensor:
avg_map = torch.mean(x, dim=1, keepdim=True)
max_map, _ = torch.max(x, dim=1, keepdim=True)
combined = torch.cat([avg_map, max_map], dim=1)
return x * self.sigmoid(self.conv(combined))


# ═══════════════════════════════════════════════════════════════════
# CBAM 主模块(惰性初始化)
# ═══════════════════════════════════════════════════════════════════

class CBAM(nn.Module):
"""
CBAM 模块(惰性初始化,自动适配输入通道数)。

Args:
reduction : 通道注意力压缩比,默认 16
kernel_size: 空间注意力卷积核大小,默认 7
"""
def __init__(self, reduction: int = 16, kernel_size: int = 7):
super().__init__()
self.reduction = reduction
self.kernel_size = kernel_size
self.channel_att = None
self.spatial_att = None
self._built = False

def forward(self, x: torch.Tensor) -> torch.Tensor:
if not self._built:
in_channels = x.shape[1]
self.channel_att = ChannelAttention(in_channels, self.reduction)
self.spatial_att = SpatialAttention(self.kernel_size)
self.channel_att.to(device=x.device, dtype=x.dtype)
self.spatial_att.to(device=x.device, dtype=x.dtype)
self._built = True
x = self.channel_att(x)
x = self.spatial_att(x)
return x


# ═══════════════════════════════════════════════════════════════════
# 猴子补丁:注册到 ultralytics 命名空间
# ═══════════════════════════════════════════════════════════════════

def _patch():
try:
from ultralytics.nn.modules import block
from ultralytics.nn import tasks

if not hasattr(block, 'CBAM'):
setattr(block, 'CBAM', CBAM) # 猴子补丁写法
if not hasattr(tasks, 'CBAM'):
setattr(tasks, 'CBAM', CBAM) # 猴子补丁写法

print("✓ [CBAM] success")
except Exception as e:
print(f"✗ [CBAM] 注册失败:{e}")

_patch()

目前的YOLO结构文件变成:

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
44
45
46
47
# Parameters
nc: 152 # 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: 129 layers, 3157200 parameters, 3157184 gradients, 8.9 GFLOPS
s: [0.33, 0.50, 1024] # YOLOv8s summary: 129 layers, 11166560 parameters, 11166544 gradients, 28.8 GFLOPS
m: [0.67, 0.75, 768] # YOLOv8m summary: 169 layers, 25902640 parameters, 25902624 gradients, 79.3 GFLOPS
l: [1.00, 1.00, 512] # YOLOv8l summary: 209 layers, 43691520 parameters, 43691504 gradients, 165.7 GFLOPS
x: [1.00, 1.25, 512] # YOLOv8x summary: 209 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, 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, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 3, C2f, [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, C2f, [512]] # 12
- [-1, 1, CBAM, [512, 16, 7]] # !13 modified: Added CBAM

- [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- [[-1, 4], 1, Concat, [1]] # cat backbone P3
- [-1, 3, C2f, [256]] # 16 (P3/8-small)
- [-1, 1, CBAM, [256, 16, 7]] # !17 modified: Added CBAM

- [-1, 1, Conv, [256, 3, 2]]
- [[-1, 12], 1, Concat, [1]] # cat head P4
- [-1, 3, C2f, [512]] # 20 (P4/16-medium)
- [-1, 1, CBAM, [512, 16, 7]] # !21 modified: Added CBAM

- [-1, 1, Conv, [512, 3, 2]]
- [[-1, 9], 1, Concat, [1]] # cat head P5
- [-1, 3, C2f, [1024]] # 24 (P5/32-large)
- [-1, 1, CBAM, [1024, 16, 7]] # !25 modified: Added CBAM

- [[15, 18, 21], 1, Detect, [nc]] # Detect(P3, P4, P5)

那么问题来了:为什么不只在最后添加CBAM模块?CBAM需要逐层对特征图进行处理,才能实现全局特征的增强。 如果只有一层CBAM,效果不强,起不到太多作用。

魔改:Wise-IoU

Wise-IoU相较于YOLO的CIoU有什么优点呢?上公式。

这是普通的IoU:

这是CIoU:

1
cious = iou - (u + alpha * ar)

其中:

这是WIoU:

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
pb = pred_bboxes[fg_mask]   # [N_fg, 4]
tb = target_bboxes[fg_mask] # [N_fg, 4]

iou = self._iou(pb, tb) # [N_fg]
L_IoU = 1.0 - iou # [N_fg]

with torch.no_grad():
if self.training and L_IoU.numel() > 0:
mean_L = L_IoU.detach().mean()
self.running_mean_L_IoU = (
self.momentum * self.running_mean_L_IoU +
(1 - self.momentum) * mean_L
)
beta = L_IoU / (self.running_mean_L_IoU + 1e-7)
r = beta / (self.delta * self.alpha ** (beta - self.delta))

pb_cx = (pb[:, 0] + pb[:, 2]) / 2.0
pb_cy = (pb[:, 1] + pb[:, 3]) / 2.0
tb_cx = (tb[:, 0] + tb[:, 2]) / 2.0
tb_cy = (tb[:, 1] + tb[:, 3]) / 2.0
gt_w = (tb[:, 2] - tb[:, 0]).detach()
gt_h = (tb[:, 3] - tb[:, 1]).detach()
norm = gt_w.pow(2) + gt_h.pow(2) + 1e-7
dist_sq = (pb_cx - tb_cx).pow(2) + (pb_cy - tb_cy).pow(2)
R_WIoU = torch.exp(dist_sq / norm)

wiou_per_sample = r * R_WIoU * L_IoU
loss_iou = (wiou_per_sample * weight.squeeze(-1)).sum() / target_scores_sum

WIoU相对于CIoU,对于高质量的预测框和过低质量的预测框,对最后框影响降低;对于普通质量的预测框就相对提高。

魔改:P2小目标层

对于YOLOv8而言,已经有了大中小三个检测层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Backbone不变
head:
# ...
- [-1, 3, C2f, [512]] # 12

# ...
- [-1, 3, C2f, [512]] # 15

- [-1, 1, Conv, [256, 3, 2]]
- [[-1, 12], 1, Concat, [1]]
- [-1, 3, C2f, [512]] # 18

- [-1, 1, Conv, [512, 3, 2]]
- [[-1, 9], 1, Concat, [1]]
- [-1, 3, C2f, [1024]] # 21

- [[15, 18, 21], 1, Detect, [nc]] # 15, 18, 21

这里,[15, 18, 21]三层的结果作为22层输入,分别是P3,P4,P5层的特征。我们在原YOLOv8的YAML结构加入P2层之后,变成这样:

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
# Backbone不变
head:
- [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- [[-1, 6], 1, Concat, [1]]
- [-1, 3, C2f, [512]] # 12

- [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- [[-1, 4], 1, Concat, [1]]
- [-1, 3, C2f, [256]] # 15

- [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- [[-1, 2], 1, Concat, [1]]
- [-1, 3, C2f, [128]] # 18 N2

- [-1, 1, Conv, [128, 3, 2]]
- [[-1, 15], 1, Concat, [1]]
- [-1, 3, C2f, [256]] # 21 N3_new

- [-1, 1, Conv, [256, 3, 2]]
- [[-1, 12], 1, Concat, [1]]
- [-1, 3, C2f, [512]] # 24 N4_new

- [-1, 1, Conv, [512, 3, 2]]
- [[-1, 9], 1, Concat, [1]]
- [-1, 3, C2f, [1024]] # 27 N5_new

- [[18, 21, 24, 27], 1, Detect, [nc]]

这里[18, 21, 24, 27]四层就是P2到P5的特征,进入检测头。P2比原来的再深入一层卷积。

这是网络结构的叠加,还算直观。

最终修改

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
44
45
46
47
48
49
# P2 + CBAM
nc: 152
scales:
n: [0.33, 0.25, 1024]

backbone:
- [-1, 1, Conv, [64, 3, 2]]
- [-1, 1, Conv, [128, 3, 2]]
- [-1, 3, C2f, [128, True]]
- [-1, 1, Conv, [256, 3, 2]]
- [-1, 6, C2f, [256, True]]
- [-1, 1, Conv, [512, 3, 2]]
- [-1, 6, C2f, [512, True]]
- [-1, 1, Conv, [1024, 3, 2]]
- [-1, 3, C2f, [1024, True]]
- [-1, 1, SPPF, [1024, 5]]

head:
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 10
- [[-1, 6], 1, Concat, [1]] # 11
- [-1, 3, C2f, [512]] # 12
- [-1, 1, CBAM, [16, 7]] # 13

- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 14
- [[-1, 4], 1, Concat, [1]] # 15
- [-1, 3, C2f, [256]] # 16
- [-1, 1, CBAM, [16, 7]] # 17

- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 18
- [[-1, 2], 1, Concat, [1]] # 19
- [-1, 3, C2f, [128]] # 20
- [-1, 1, CBAM, [16, 7]] # 21

- [-1, 1, Conv, [128, 3, 2]] # 22
- [[-1, 16], 1, Concat, [1]] # 23
- [-1, 3, C2f, [256]] # 24
- [-1, 1, CBAM, [16, 7]] # 25

- [-1, 1, Conv, [256, 3, 2]] # 26
- [[-1, 12], 1, Concat, [1]] # 27
- [-1, 3, C2f, [512]] # 28
- [-1, 1, CBAM, [16, 7]] # 29

- [-1, 1, Conv, [512, 3, 2]] # 30
- [[-1, 9], 1, Concat, [1]] # 31
- [-1, 3, C2f, [1024]] # 32
- [-1, 1, CBAM, [16, 7]] # 33

- [[21, 25, 29, 33], 1, Detect, [nc]] # 34

拜托DS写个猴子补丁:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
"""
WIoU v3 损失函数 + 猴子补丁
===========================
基于 Wise-IoU v3 (Tong et al., 2023)。用离群度驱动的非单调聚焦机制替换 CIoU。

用法(导入即生效):
import wiou_patch # 替换 BboxLoss 为 WIoUBboxLoss
from ultralytics import YOLO
model = YOLO('yolov8n.yaml')
model.train(...)

恢复原始 CIoU:
wiou_patch.restore()
"""

import torch
import torch.nn as nn
import torch.nn.functional as F

_original_BboxLoss = None


class WIoUBboxLoss(nn.Module):
# ═══════════ 类属性(供补丁打印等使用)═══════════
alpha: float = 1.9
delta: float = 2.9
momentum: float = 0.99

def __init__(self, reg_max: int = 16,
alpha: float = 1.9,
delta: float = 2.9,
momentum: float = 0.99):
super().__init__()
self.reg_max = reg_max
self.alpha = alpha
self.delta = delta
self.momentum = momentum

# DFL(与原版一致)
if reg_max > 1:
from ultralytics.utils.loss import DFLoss
self.dfl_loss = DFLoss(reg_max)
else:
self.dfl_loss = None

self.register_buffer('running_mean_L_IoU', torch.tensor(1.0))

def forward(self, pred_dist, pred_bboxes, anchor_points, target_bboxes,
target_scores, target_scores_sum, fg_mask, imgsz=None, stride=None):
weight = target_scores.sum(-1)[fg_mask].unsqueeze(-1) # [N_fg, 1]

# ── WIoU v3 核心 ──
pb = pred_bboxes[fg_mask] # [N_fg, 4]
tb = target_bboxes[fg_mask] # [N_fg, 4]

iou = self._iou(pb, tb) # [N_fg]
L_IoU = 1.0 - iou # [N_fg]

with torch.no_grad():
if self.training and L_IoU.numel() > 0:
mean_L = L_IoU.detach().mean()
self.running_mean_L_IoU = (
self.momentum * self.running_mean_L_IoU +
(1 - self.momentum) * mean_L
)
beta = L_IoU / (self.running_mean_L_IoU + 1e-7)
r = beta / (self.delta * self.alpha ** (beta - self.delta))

# 距离注意力
pb_cx = (pb[:, 0] + pb[:, 2]) / 2.0
pb_cy = (pb[:, 1] + pb[:, 3]) / 2.0
tb_cx = (tb[:, 0] + tb[:, 2]) / 2.0
tb_cy = (tb[:, 1] + tb[:, 3]) / 2.0
gt_w = (tb[:, 2] - tb[:, 0]).detach()
gt_h = (tb[:, 3] - tb[:, 1]).detach()
norm = gt_w.pow(2) + gt_h.pow(2) + 1e-7
dist_sq = (pb_cx - tb_cx).pow(2) + (pb_cy - tb_cy).pow(2)
R_WIoU = torch.exp(dist_sq / norm)

wiou_per_sample = r * R_WIoU * L_IoU
loss_iou = (wiou_per_sample * weight.squeeze(-1)).sum() / target_scores_sum

# ── DFL(与原版完全一致)──
if self.dfl_loss is not None:
from ultralytics.utils.tal import bbox2dist
target_ltrb = bbox2dist(anchor_points, target_bboxes,
self.dfl_loss.reg_max - 1)
loss_dfl = (
self.dfl_loss(
pred_dist[fg_mask].view(-1, self.dfl_loss.reg_max),
target_ltrb[fg_mask],
) * weight
).sum() / target_scores_sum
else:
from ultralytics.utils.tal import bbox2dist
target_ltrb = bbox2dist(anchor_points, target_bboxes)
t = target_ltrb * (stride if stride is not None else 1.0)
if imgsz is not None:
t[..., 0::2] /= imgsz[1]
t[..., 1::2] /= imgsz[0]
p = pred_dist * (stride if stride is not None else 1.0)
if imgsz is not None:
p[..., 0::2] /= imgsz[1]
p[..., 1::2] /= imgsz[0]
loss_dfl = (
F.l1_loss(p[fg_mask], t[fg_mask], reduction='none')
.mean(-1, keepdim=True) * weight
).sum() / target_scores_sum

return loss_iou, loss_dfl

@staticmethod
def _iou(box1, box2, eps=1e-7):
inter_w = (torch.min(box1[:, 2], box2[:, 2])
- torch.max(box1[:, 0], box2[:, 0])).clamp(min=0)
inter_h = (torch.min(box1[:, 3], box2[:, 3])
- torch.max(box1[:, 1], box2[:, 1])).clamp(min=0)
inter = inter_w * inter_h
area1 = (box1[:, 2] - box1[:, 0]) * (box1[:, 3] - box1[:, 1])
area2 = (box2[:, 2] - box2[:, 0]) * (box2[:, 3] - box2[:, 1])
union = area1 + area2 - inter + eps
return inter / union


def _patch():
global _original_BboxLoss
try:
import ultralytics.utils.loss as loss_module
if _original_BboxLoss is None:
_original_BboxLoss = loss_module.BboxLoss
loss_module.BboxLoss = WIoUBboxLoss
print(f"✓ [WIoU v3] BboxLoss → WIoUBboxLoss (α={WIoUBboxLoss.alpha}, δ={WIoUBboxLoss.delta})")
except Exception as e:
print(f"✗ [WIoU v3] 补丁失败:{e}")


def restore():
global _original_BboxLoss
if _original_BboxLoss is not None:
import ultralytics.utils.loss as loss_module
loss_module.BboxLoss = _original_BboxLoss
print("✓ 已恢复原始 BboxLoss (CIoU)")

_patch()

最终结果就是这样了。开始训练。

结果

四大指标
mAP50-95对比
精度对比
召回率对比

唉……至少高了一点吧,mAP50-95最后到0.5几了,倒也不是不行。

  • 标题: YOLO v8n 踩的坑
  • 作者: 容小狸
  • 创建于 : 2026-05-06 07:30:14
  • 更新于 : 2026-05-23 16:16:58
  • 链接: https://blog.rongxiaoli.top/2026/05/05/YOLO-v8/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论