目录
[一、方案选型:两阶段 vs 一体式](#一、方案选型:两阶段 vs 一体式)
[1、方案一:两阶段(检测 + 分类)](#1、方案一:两阶段(检测 + 分类))
[3、data.yaml 配置](#3、data.yaml 配置)
[4、是否先用粗分类模型裁剪 ROI](#4、是否先用粗分类模型裁剪 ROI)
[四、YOLOv8 原始检测头简述](#四、YOLOv8 原始检测头简述)
[六、Loss 函数改造](#六、Loss 函数改造)
[1、原始 Loss](#1、原始 Loss)
[2、改造后 Loss](#2、改造后 Loss)
[5、Loss 权重建议](#5、Loss 权重建议)
前言
在目标检测任务中,YOLOv8 默认每个目标只输出一个类别。但实际业务里,往往需要两层语义:
|-----|------|------------------|
| 层级 | 含义 | 示例 |
| 粗分类 | 大类 | 水果、动物、车辆 |
| 细分类 | 具体类别 | 苹果、香蕉、猫、狗、小汽车、卡车 |
希望模型一次推理直接输出:检测框 + 粗分类 + 细分类 + 置信度。
YOLOv8的网络结构和详情见YOLOv8 及其目标检测/实例分割应用的完整知识体系-CSDN博客
一、方案选型:两阶段 vs 一体式
1、方案一:两阶段(检测 + 分类)
流程:YOLOv8 粗分类检测 → 裁剪 ROI → 细分类模型识别
|-------------|------------|
| 优点 | 缺点 |
| 实现简单,适合快速落地 | 两个模型,推理链路长 |
| 细分类可单独优化 | 非真正端到端 |
| 新手容易上手 | 部署和维护成本高 |
2、方案二:一体式二次分类
流程:一个 YOLOv8 模型同时输出:bbox + coarse_cls + fine_cls
|------------|------------------|
| 优点 | 缺点 |
| 一次前向传播,速度快 | 需改检测头、标签、Loss、推理 |
| 部署简单,模型统一 | 实现难度略高 |
| 端到端联合优化 | 需理解 YOLOv8 源码 |
3、方案对比小结
(1)两阶段:易实现,适合验证想法、快速上线
(2)一体式:难实现,适合追求速度与部署简洁的场景
二、类别体系设计
1、粗分类与细分类的定义
(1)粗分类(coarse):0: 水果 (fruit)、1: 动物 (animal)、2: 车辆 (vehicle)
(2)细分类(fine):0: 苹果 (apple)、1: 香蕉 (banana)、2: 猫 (cat)、3: 狗 (dog)、4: 小汽车 (car)、5: 卡车 (truck)。
2、粗细分类映射关系
建立 fine_id → coarse_id 映射表:
fine_to_coarse = {
0: 0, # 苹果 → 水果
1: 0, # 香蕉 → 水果
2: 1, # 猫 → 动物
3: 1, # 狗 → 动物
4: 2, # 小汽车 → 车辆
5: 2, # 卡车 → 车辆
}
3、推荐设计原则
原则:标签文件只存细分类 id,粗分类由映射自动生成。
- 标注格式保持 YOLO 标准:
fine_id x y w h - 训练时根据
fine_to_coarse得到coarse_id - 改动最小,与 YOLOv8 原生格式兼容
三、数据集制作
1、数据目录结构
沿用 YOLOv8 标准检测数据集结构:
dataset/
├── images/
│ ├── train/ # 训练集图片
│ ├── val/ # 验证集图片
│ └── test/ # 测试集图片
├── labels/
│ ├── train/ # 训练集标签(与图片同名 .txt)
│ ├── val/
│ └── test/
└── data.yaml # 数据集配置文件
2、标签文件格式
每张图对应一个同名 .txt 文件,每行一个目标:fine_id x_center y_center width height
- 坐标均为归一化值(0~1)
fine_id为细分类 id
示例(一张图中有苹果和猫):
0 0.512 0.433 0.210 0.180
2 0.720 0.500 0.160 0.220
3、data.yaml 配置
path: dataset
train: images/train
val: images/val
test: images/test
# 细分类
nc_fine: 6
fine_names:
0: apple
1: banana
2: cat
3: dog
4: car
5: truck
# 粗分类
nc_coarse: 3
coarse_names:
0: fruit
1: animal
2: vehicle
# 细分类 → 粗分类映射
fine_to_coarse:
0: 0
1: 0
2: 1
3: 1
4: 2
5: 2
说明 :nc_fine、nc_coarse、fine_to_coarse 需在自定义数据读取和模型构建时使用,YOLOv8 原生不识别这些字段。
4、是否先用粗分类模型裁剪 ROI
推荐流程:粗分类 YOLO 检测原图 → 生成候选框 → 人工校验 → 补充细分类 → 转为 fine_id x y w h → 训练一体式模型
|-----------|----------------------------------|
| 用途 | 说明 |
| 辅助标注 | 粗分类 YOLO 检测 → 裁剪 ROI → 人工补充细分类标签 |
| 数据清洗 | 剔除误检、模糊、难例 |
| 训练独立细分类模型 | 作为两阶段方案的中间数据 |
| 替代检测框数据 | ❌ 不可行,端到端训练仍需原图检测框 |
辅助细分类标注或构建分类数据集:
from ultralytics import YOLO
import cv2
model = YOLO('yolov8n.pt') # 粗分类模型
results = model('val/image.jpg')
img = cv2.imread('val/image.jpg')
for r in results:
for det in r.boxes:
x1, y1, x2, y2 = map(int, det.xyxy[0])
crop = img[y1:y2, x1:x2]
save_path = f"cropped/{det.cls}.jpg"
cv2.imwrite(save_path, crop)
注意: ROI 裁剪适合辅助标注、清洗和训练独立细分类器;端到端一体式训练仍需原图检测框标签,不能只用 ROI 分类数据替代。
四、YOLOv8 原始检测头简述
1、检测头输出形式
原始 YOLOv8 检测头可理解为:bbox 分支 + cls 分支。每个预测位置输出:box_distribution + class_logits。其中class_logits 通道数 = nc(类别数)、若 6 类检测:cls_logits 为 6 维。
2、改造目标
将单分类头改为双分类头:bbox 分支 + coarse_cls 分支 + fine_cls 分支
五、检测分类头改造
1、结构对比
原始 YOLOv8:
Detect Head
├── bbox branch
└── cls branch
改造后:
Detect Head
├── bbox branch
├── coarse cls branch
└── fine cls branch
2、输出通道变化
假设 nc_coarse = 3,nc_fine = 6:
|--------|------------------------------|
| 版本 | 输出组成 |
| 原始 | bbox + 6 维 cls |
| 改造后 | bbox + 3 维 coarse + 6 维 fine |
概念示意:
原始: box, cls_0, cls_1, cls_2, cls_3, cls_4, cls_5
改造: box, coarse_0, coarse_1, coarse_2, fine_0, fine_1, fine_2, fine_3, fine_4, fine_5
3、改造思路(伪代码)
在 YOLOv8 检测头基础上,增加细分类预测分支,使一个模型同时预测每个框的粗类和细类。
改造思路:
-
保留原有边框回归分支
cv2 -
新增粗分类分支
cv3_coarse,输出nc_coarse通道 -
新增细分类分支
cv3_fine,输出nc_fine通道class DetectCoarseFine(nn.Module):
def init(self, nc_coarse, nc_fine, ch):
super().init()
self.cv2 = nn.ModuleList([...]) # 边框回归分支
# 粗分类分支
self.cv3_coarse = nn.ModuleList(
nn.Conv2d(c3, nc_coarse, 1) for c3 in ch_out
)
# 细分类分支
self.cv3_fine = nn.ModuleList(
nn.Conv2d(c3, nc_fine, 1) for c3 in ch_out
)def forward(self, feats): boxes = [conv(f) for conv, f in zip(self.cv2, feats)] coarse_scores = [conv(f) for conv, f in zip(self.cv3_coarse, feats)] fine_scores = [conv(f) for conv, f in zip(self.cv3_fine, feats)] return { 'boxes': boxes, 'coarse': coarse_scores, 'fine': fine_scores }
损失函数:L = L_bbox + α·L_c + β·L_f
L_bbox:边框回归损失(含 DFL)L_c:粗分类损失L_f:细分类损失α、β:权重,常取 1.0,细分类更重要时可设β > α
优点 : 单模型、推理快、端到端联合优化
缺点: 需改网络与标签;细类多或样本极不平衡时训练更难
4、源码改造步骤
- 定位 YOLOv8 的
Detect模块 - 保留原有 box 分支
- 将原 cls 分支拆为
coarse_branch和fine_branch coarse_branch输出nc_coarse通道fine_branch输出nc_fine通道forward同时返回 box、coarse、fine- Loss 中分别计算 coarse_loss、fine_loss
- Predict 中分别解析 coarse、fine 分数
5、两种检测头设计
|----------|-----------------------------|-----------|
| 方案 | 结构 | 适用 |
| 方案 A(推荐) | 全局 fine_cls + 全局 coarse_cls | 新手首选,实现简单 |
| 方案 B | 每个粗类下独立 fine 分支 | 层级更强,实现复杂 |
6、配套修改清单
仅改 Head 不够,需同步修改:
- 标签读取
- Loss 计算
- 模型输出解析
- NMS 后处理
- 验证指标
- 推理结果显示
六、Loss 函数改造
1、原始 Loss
loss = box_loss + cls_loss + dfl_loss
2、改造后 Loss
loss = box_loss + dfl_loss + coarse_cls_loss + fine_cls_loss
3、单标签任务
每个目标一个粗类、一个细类:
coarse_loss = classification_loss(coarse_pred, coarse_target)
fine_loss = classification_loss(fine_pred, fine_target)
total_loss = box_loss + dfl_loss + coarse_loss + fine_loss
4、多标签任务(进阶)
一个目标可属多个粗/细标签时,使用 multi-hot + BCEWithLogitsLoss:
coarse_target = 1, 0, 1
fine_target = 0, 1, 0, 1, 0, 0
5、Loss 权重建议
初期可设 :a = 0.5,b = 1.0(细分类通常更难、更重要)
total_loss = box_loss + dfl_loss + a * coarse_loss + b * fine_loss
七、数据读取改造
1、原始读取逻辑
class_id x y w h
2、改造后读取逻辑
- 读取
fine_id和 bbox - 通过
fine_to_coarse[fine_id]得到coarse_id - 送入 Loss 的目标:
bbox_target、fine_target、coarse_target
3、数据流示意
读取图片
→ 读取 labels/*.txt
→ 得到 fine_id、bbox
→ 根据 fine_to_coarse 得到 coarse_id
→ 数据增强(Mosaic、MixUp 等)
→ 送入模型
→ 计算 box_loss、coarse_loss、fine_loss
Mosaic、MixUp 等增强后,只要 bbox 和 fine_id 正确,coarse_id 可随时由 fine_id 重新生成。
八、训练流程
1、训练步骤
准备原图检测数据集,每框标注细分类 id
编写 fine_to_coarse 映射
修改 YOLOv8 Detect Head
修改数据读取,生成 coarse_id
修改 Loss,增加 coarse_loss、fine_loss
加载 YOLOv8 预训练权重
新增分类头随机初始化
开始训练
验证粗分类准确率、细分类准确率、检测 mAP
方案 A:单阶段一体化训练
from ultralytics import YOLO
model = YOLO('yolov8n.yaml') # 自定义含双分类头的 YAML
model.train(
data='data.yaml',
epochs=100,
imgsz=640,
batch=16,
lr0=0.01
)
需修改 ultralytics/cfg/models/ 下模型 YAML,在 head 增加 coarse/fine 输出通道,并调整 Loss。
方案 B:两阶段训练
粗分类检测:
yolo detect train data=coarse_data.yaml model=yolov8s.yaml epochs=50 imgsz=640
细分类器:
yolo classify train data=fine_data.yaml model=yolov8s-cls.yaml epochs=50
2、分阶段训练建议
|--------|----------------------|
| 阶段 | 操作 |
| 第一阶段 | 冻结 backbone,只训练 head |
| 第二阶段 | 解冻全网络微调 |
| 第三阶段 | 根据验证集调整 loss 权重 |
3、评估指标
- 检测 mAP(框定位)
- 粗分类准确率
- 细分类准确率
- 粗细一致性(细分类映射出的粗类是否与预测粗类一致)
九、推理流程
1、推理步骤
输入图片
→ YOLOv8 前向推理
→ 解码 bbox
→ 得到 coarse logits、fine logits
→ 计算 coarse score、fine score
→ NMS
→ 输出 bbox、coarse label、fine label、score
2、粗细分类不一致的处理
可能出现:粗分类预测「动物」,细分类预测「苹果」。
方式一:以细分类为准
fine = apple → coarse = fine_to_coarseapple = fruit
方式二:以粗分类为约束(推荐),更符合「先粗后细」的二次分类逻辑。
coarse = animal → fine 仅在 cat、dog 中选择最高分
3、模型导出
ONNX:
yolo export model=best.pt format=onnx imgsz=640 dynamic=True simplify=True
TorchScript:
yolo export model=best.pt format=torchscript imgsz=640
TensorRT:
yolo export model=best.pt format=engine imgsz=640 dynamic=True fp16=True
4、TensorRT 加速要点
- ONNX 转 TensorRT:
trtexec或 Python API - FP16/INT8:FP16 常用;INT8 需校准集(约 100--200 张)
- 动态 Shape:设置 min/opt/maxShapes
- NMS:可在导出时
nms=False,部署端自行 NMS
十、常见问题与注意事项
1、数据与标注
- 粗类与细类映射需稳定,避免一个细类对应多个粗类(除非明确做多标签)
- 细分类样本尽量平衡,防止偏向大类
- 端到端训练必须用原图检测框,不能只用 ROI 分类数据
2、模型与训练
- 训练与推理时
fine_to_coarse必须一致 - 细分类难区分时,可提高输入分辨率
- 细分类依赖局部细节时,可考虑 ROIAlign 或局部分支(实现更复杂)
3、部署与选型
- 追求速度:一体式双头更优
- 追求准确率:两阶段方案有时更好(细分类模型专注 ROI)