简介:目标检测是计算机视觉的核心任务之一,旨在识别图像中特定目标的位置与类别。其核心原理是通过深度学习模型,如YOLO系列,学习从像素到边界框与类别概率的映射。这项技术在工业自动化、智能安防、自动驾驶等领域具有极高的技术价值,是实现场景理解的关键。在具体的应用场景中,例如药品识别与分拣,一个规范且高质量的数据集是模型成功的基础。本文以包含板蓝根颗粒和999感冒灵的双类别药品包装盒数据集为例,详细解析了VOC与YOLO两种主流标注格式的互转原理,并提供了完整的数据预处理、YOLOv8模型训练、参数调优及结果评估的工程实践流程,为初学者构建完整的目标检测项目闭环提供了清晰的指引。

1. 项目背景与数据集价值解析

最近在整理一个挺有意思的小型数据集,是关于药品包装盒的目标检测,具体包含了板蓝根颗粒和999感冒灵这两个类别。这个数据集虽然不大,总共只有111张图像,但麻雀虽小五脏俱全,它同时提供了VOC和YOLO两种主流格式的标注文件。对于刚入门计算机视觉,特别是想动手实践目标检测的朋友来说,这是一个非常理想的“练手”材料。我自己在带新人或者做算法原型验证时,也常常会寻找这类主题明确、标注规范、体量适中的数据集来快速搭建实验环境。

你可能会问,市面上开源数据集那么多,为什么还要关注这个小小的药品数据集?这里面的价值其实挺多的。首先, 场景非常具体且贴近生活 。药品包装盒的检测在自动化药房、智能售货机、药品分拣流水线乃至家庭用药管理APP中都有实际的应用前景。其次, 双类别的设定恰到好处 。它既避免了单类别过于简单、学不到东西的问题,又不像COCO那样动辄80个类别,让初学者在数据准备和模型调试上耗费过多精力。最后, 双格式标注是最大的亮点 。VOC格式(XML文件)结构清晰,包含了目标的边界框和类别信息,是很多传统算法和评估工具的标准输入。而YOLO格式(.txt文件)则直接、紧凑,是当前YOLO系列、Ultralytics生态等主流检测框架训练时所需的格式。拥有同一批数据的两种标注,意味着你可以无缝地在不同框架、不同评估流程之间切换和对比,这对于深入理解目标检测的数据处理全链路至关重要。

从技术学习的角度看,这个数据集能帮你走通一个完整的目标检测项目闭环:从数据理解、格式解析、数据集划分(训练集/验证集/测试集),到模型选择(比如用YOLOv5/v8还是SSD)、训练配置、性能评估,再到最后可能的数据增强策略尝试。整个过程涉及到的工具和概念,比如labelImg标注工具、PASCAL VOC评估指标(mAP)、YOLO的锚框(anchor)机制、以及PyTorch或TensorFlow的DataLoader构建,你都能通过这个数据集得到实践。接下来,我就以这个“板蓝根颗粒与999感冒灵”数据集为例,拆解一下处理这类目标检测数据的完整流程和核心要点。

2. 数据集深度剖析与预处理实战

拿到一个数据集,第一步绝不是急着扔进模型里训练。花时间把数据“摸透”,是后续所有工作能顺利进行的基础。对于这个111张图像的双类别药品数据集,我们需要从图像内容、标注质量、格式解析和数据集划分几个层面入手。

2.1 图像内容与标注质量检查

首先,我们需要直观地了解数据长什么样。我通常会用一个简单的Python脚本,配合OpenCV和matplotlib,随机抽样查看一些图像及其标注框。

import os
import cv2
import random
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from xml.etree import ElementTree as ET # 用于解析VOC格式

def plot_image_with_boxes(img_path, annotation_path, format='voc'):
    img = cv2.imread(img_path)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # OpenCV读取为BGR,转为RGB显示
    fig, ax = plt.subplots(1, figsize=(12, 8))
    ax.imshow(img)

    if format.lower() == 'voc':
        # 解析VOC XML
        tree = ET.parse(annotation_path)
        root = tree.getroot()
        for obj in root.findall('object'):
            cls_name = obj.find('name').text
            bbox = obj.find('bndbox')
            xmin = int(bbox.find('xmin').text)
            ymin = int(bbox.find('ymin').text)
            xmax = int(bbox.find('xmax').text)
            ymax = int(bbox.find('ymax').text)
            # 绘制矩形框
            rect = patches.Rectangle((xmin, ymin), xmax-xmin, ymax-ymin,
                                     linewidth=2, edgecolor='r', facecolor='none')
            ax.add_patch(rect)
            # 添加类别标签
            ax.text(xmin, ymin-5, cls_name, color='red', fontsize=12, weight='bold')
    elif format.lower() == 'yolo':
        # 解析YOLO .txt, 注意YOLO格式是归一化的中心点坐标和宽高
        img_h, img_w = img.shape[:2]
        with open(annotation_path, 'r') as f:
            for line in f.readlines():
                cls_id, x_center_norm, y_center_norm, w_norm, h_norm = map(float, line.strip().split())
                # 转换回像素坐标
                x_center = x_center_norm * img_w
                y_center = y_center_norm * img_h
                w = w_norm * img_w
                h = h_norm * img_h
                xmin = int(x_center - w/2)
                ymin = int(y_center - h/2)
                xmax = int(x_center + w/2)
                ymax = int(y_center + h/2)
                rect = patches.Rectangle((xmin, ymin), w, h,
                                         linewidth=2, edgecolor='g', facecolor='none')
                ax.add_patch(rect)
                ax.text(xmin, ymin-5, f'class_{int(cls_id)}', color='green', fontsize=12, weight='bold')
    ax.axis('off')
    plt.show()

# 假设数据按如下结构组织
# dataset/
#   ├── images/ (存放所有.jpg图像)
#   ├── annotations_voc/ (存放对应的.xml文件)
#   └── annotations_yolo/ (存放对应的.txt文件)

image_dir = './dataset/images'
voc_anno_dir = './dataset/annotations_voc'
image_files = [f for f in os.listdir(image_dir) if f.endswith('.jpg')]
sample_img = random.choice(image_files)
img_path = os.path.join(image_dir, sample_img)
voc_anno_path = os.path.join(voc_anno_dir, sample_img.replace('.jpg', '.xml'))

plot_image_with_boxes(img_path, voc_anno_path, format='voc')

运行这个脚本,我们能立刻看到图像中药品包装盒的位置、大小以及标注的精细程度。对于这个数据集,你需要特别关注几点:

  1. 目标尺寸 :药品包装盒在图像中是占比较大还是较小?这关系到后续是否要针对小目标检测进行优化。
  2. 拍摄角度与光照 :图像是正面平拍,还是带有一定角度?光照是否均匀?这会影响模型的泛化能力。
  3. 标注框的准确性 :边界框是否紧密贴合药品包装盒?是否存在漏标或错标?通过抽样可视化,能快速发现明显的标注问题。
  4. 类别平衡 :板蓝根颗粒和999感冒灵的样本数量是否大致相当?严重的数据不平衡会影响模型对少数类的识别能力。

注意 :在检查YOLO格式标注时,务必确认其类别ID的映射关系。通常, 0 代表第一个类别(如 banlangen ), 1 代表第二个类别(如 999ganmaoling )。这个映射关系通常记录在一个名为 classes.txt 或 data.yaml 的文件中,如果没有,你需要根据标注文件或VOC文件反推并创建它,这是训练前的关键一步。

2.2 VOC与YOLO格式互转原理与实操

这个数据集提供了两种格式,这很棒,但我们经常遇到的情况是只有一种格式。因此,掌握两种格式的互转是必备技能。理解它们的核心差异是关键:

  • VOC格式 (XML) :使用图像的绝对像素坐标来表示边界框 (xmin, ymin, xmax, ymax) 。信息丰富,可读性强。
  • YOLO格式 (TXT) :使用归一化的相对坐标 (x_center, y_center, width, height) ,每个值都在0到1之间。格式紧凑,直接用于训练。

转换公式如下(假设图像宽度为 img_w ,高度为 img_h ):

  • VOC -> YOLO :
    • x_center = (xmin + xmax) / 2.0 / img_w
    • y_center = (ymin + ymax) / 2.0 / img_h
    • width = (xmax - xmin) / img_w
    • height = (ymax - ymin) / img_h
  • YOLO -> VOC :
    • xmin = (x_center - width/2) * img_w
    • ymin = (y_center - height/2) * img_h
    • xmax = (x_center + width/2) * img_w
    • ymax = (y_center + height/2) * img_h

这里提供一个将VOC格式批量转换为YOLO格式的实用脚本:

import os
import xml.etree.ElementTree as ET

def convert_voc_to_yolo(voc_anno_dir, output_dir, class_list):
    """
    将VOC格式标注批量转换为YOLO格式。
    Args:
        voc_anno_dir: VOC格式XML文件所在目录。
        output_dir: 输出YOLO格式TXT文件的目录。
        class_list: 类别名称列表,如 ['banlangen', '999ganmaoling']。
    """
    os.makedirs(output_dir, exist_ok=True)
    # 创建类别名到ID的映射字典
    class_to_id = {name: idx for idx, name in enumerate(class_list)}

    for xml_file in os.listdir(voc_anno_dir):
        if not xml_file.endswith('.xml'):
            continue

        tree = ET.parse(os.path.join(voc_anno_dir, xml_file))
        root = tree.getroot()

        # 获取图像尺寸
        size_elem = root.find('size')
        img_w = int(size_elem.find('width').text)
        img_h = int(size_elem.find('height').text)

        yolo_lines = []
        for obj in root.findall('object'):
            cls_name = obj.find('name').text
            if cls_name not in class_to_id:
                print(f"警告: {xml_file} 中发现未知类别 '{cls_name}',已跳过。")
                continue

            cls_id = class_to_id[cls_name]
            bbox = obj.find('bndbox')
            xmin = float(bbox.find('xmin').text)
            ymin = float(bbox.find('ymin').text)
            xmax = float(bbox.find('xmax').text)
            ymax = float(bbox.find('ymax').text)

            # 转换坐标
            x_center = (xmin + xmax) / 2.0 / img_w
            y_center = (ymin + ymax) / 2.0 / img_h
            width = (xmax - xmin) / img_w
            height = (ymax - ymin) / img_h

            # 确保坐标在[0,1]范围内
            x_center = max(0, min(1, x_center))
            y_center = max(0, min(1, y_center))
            width = max(0, min(1, width))
            height = max(0, min(1, height))

            yolo_lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}")

        # 写入YOLO格式文件
        output_txt_path = os.path.join(output_dir, xml_file.replace('.xml', '.txt'))
        with open(output_txt_path, 'w') as f:
            f.write('\n'.join(yolo_lines))

# 使用示例
class_list = ['banlangen', '999ganmaoling'] # 必须与XML中的类别名严格一致
convert_voc_to_yolo('./dataset/annotations_voc', './dataset/annotations_yolo_converted', class_list)

实操心得 :转换后一定要用2.1节的可视化脚本,随机抽查几张转换后的YOLO标注,与原始的VOC标注对比,确保转换过程没有出错。坐标归一化时可能因为四舍五入产生微小误差,但只要在合理范围内(如1e-6)即可接受。

2.3 数据集划分与YAML配置文件创建

111张图像不算多,因此划分比例需要谨慎。常见的做法是按 训练集:验证集:测试集 = 7:2:1 或 8:1:1 的比例进行随机划分。测试集用于最终评估模型泛化能力,验证集用于训练过程中的超参数调整和早停。

划分完成后,我们需要创建一个YAML配置文件(例如 data.yaml ),这是YOLOv5/v8等框架要求的数据集描述文件。

import os
import random
import shutil
from sklearn.model_selection import train_test_split

def split_dataset(image_dir, train_ratio=0.7, val_ratio=0.2, test_ratio=0.1):
    """
    划分数据集,并创建对应的文件夹结构。
    """
    assert abs(train_ratio + val_ratio + test_ratio - 1.0) < 1e-9, "比例之和必须为1"

    all_images = [f for f in os.listdir(image_dir) if f.endswith(('.jpg', '.png', '.jpeg'))]
    random.shuffle(all_images) # 随机打乱

    total = len(all_images)
    train_end = int(total * train_ratio)
    val_end = train_end + int(total * val_ratio)

    train_images = all_images[:train_end]
    val_images = all_images[train_end:val_end]
    test_images = all_images[val_end:]

    # 创建目录结构
    splits = {'train': train_images, 'val': val_images, 'test': test_images}
    base_path = './dataset_split'
    for split_name, img_list in splits.items():
        split_img_dir = os.path.join(base_path, 'images', split_name)
        split_label_dir = os.path.join(base_path, 'labels', split_name) # 假设标签文件在labels目录,与images平行
        os.makedirs(split_img_dir, exist_ok=True)
        os.makedirs(split_label_dir, exist_ok=True)

        for img_file in img_list:
            # 拷贝图像
            src_img = os.path.join(image_dir, img_file)
            dst_img = os.path.join(split_img_dir, img_file)
            shutil.copy(src_img, dst_img)

            # 拷贝对应的标签文件 (假设标签文件与图像同名,扩展名为.txt)
            label_file = img_file.rsplit('.', 1)[0] + '.txt'
            src_label = os.path.join('./dataset/annotations_yolo', label_file) # 使用YOLO格式标签
            dst_label = os.path.join(split_label_dir, label_file)
            if os.path.exists(src_label):
                shutil.copy(src_label, dst_label)
            else:
                print(f"警告: 未找到标签文件 {src_label}")

    print(f"划分完成: 训练集{len(train_images)}张, 验证集{len(val_images)}张, 测试集{len(test_images)}张")
    return base_path, train_images, val_images, test_images

# 执行划分
base_path, _, _, _ = split_dataset('./dataset/images', train_ratio=0.7, val_ratio=0.2, test_ratio=0.1)

划分好后,在 dataset_split 目录下创建 data.yaml 文件:

# data.yaml
path: ./dataset_split  # 数据集根目录
train: images/train    # 训练集图像路径,相对于path
val: images/val        # 验证集图像路径,相对于path
test: images/test      # 测试集图像路径(可选)

# 类别信息
names:
  0: banlangen         # 类别ID 0 对应板蓝根颗粒
  1: 999ganmaoling     # 类别ID 1 对应999感冒灵

# 可选:类别数量
nc: 2

这个YAML文件是连接你的数据和YOLO训练脚本的桥梁,路径一定要写对。 path 可以是绝对路径,也可以是相对于训练脚本所在位置的相对路径。

3. 基于YOLOv8的模型训练与调优全流程

数据准备好了,接下来就是选择模型和训练。这里我选择YOLOv8,因为它生态成熟,文档清晰,从训练到部署的链路非常完整。我们使用Ultralytics提供的Python API来进行,这比单纯使用命令行有更大的灵活性。

3.1 环境搭建与模型选择

首先,安装Ultralytics包:

pip install ultralytics

YOLOv8提供了不同尺度的模型,从轻量级的YOLOv8n到高精度的YOLOv8x。对于我们的111张图像的小数据集,选择过大的模型极易导致过拟合。我的建议是:

  • 首选 YOLOv8n (nano) 或 YOLOv8s (small) 。它们参数量少,训练速度快,在小数据集上更容易收敛,作为原型验证和入门学习性价比最高。
  • 如果担心精度,可以从YOLOv8s开始。如果训练后发现欠拟合(训练集和验证集损失都下不去),再考虑换稍大的模型,但务必配合更强的数据增强和正则化手段。

3.2 训练脚本与核心参数详解

下面是一个基础的训练脚本,我加入了大量注释来解释每个关键参数的作用:

from ultralytics import YOLO
import os

def train_yolov8():
    # 加载一个预训练模型。强烈建议使用预训练权重,这能极大加速收敛并提升最终性能。
    # 即使预训练模型是在COCO等通用数据集上训练的,其提取通用特征的能力也远胜于随机初始化。
    model = YOLO('yolov8s.pt') # 这里使用YOLOv8s,你可以换成'yolov8n.pt'

    # 开始训练
    results = model.train(
        data='./dataset_split/data.yaml',  # 上一步创建的配置文件路径
        epochs=100,                         # 训练轮数。小数据集可以适当增加,如150-200。
        imgsz=640,                          # 输入图像尺寸。YOLOv8默认640,也可尝试512。
        batch=16,                           # 批次大小。根据你的GPU内存调整。11G显存可设16或32。
        workers=4,                          # 数据加载线程数。CPU核心数多可以调高,加快数据读取。
        device='0',                         # 使用GPU 0。如果是CPU则设为'cpu',多卡可用'0,1'。
        name='drug_detection_v8s',          # 本次训练的实验名称,用于保存结果。
        pretrained=True,                    # 使用预训练权重(已通过加载模型指定)。
        optimizer='SGD',                    # 优化器。SGD是经典选择,'AdamW'也可能有更好效果。
        lr0=0.01,                           # 初始学习率。这是最重要的超参数之一。
        lrf=0.01,                           # 最终学习率因子 = lr0 * lrf。用于余弦退火等调度。
        momentum=0.937,                     # SGD动量参数。
        weight_decay=0.0005,                # 权重衰减,防止过拟合。
        warmup_epochs=3.0,                  # 学习率热身轮数,开始时从小学习率逐渐增大,稳定训练。
        box=7.5,                            # 边界框损失权重。
        cls=0.5,                            # 分类损失权重。对于类别少的数据集,可以调低。
        dfl=1.5,                            # Distribution Focal Loss权重(v8新增)。
        save=True,                          # 保存训练过程中的最佳模型和最后模型。
        save_period=-1,                     # 每N轮保存一次检查点。-1表示仅按标准规则保存。
        cache=False,                        # 是否缓存数据集到内存或磁盘以加速训练。小数据集可以开启。
        resume=False,                        # 是否从上次保存的检查点恢复训练。
        amp=True,                           # 自动混合精度训练,大幅减少显存占用并加速训练。
        fraction=1.0,                       # 使用数据集的比例,可用于快速实验。
        profile=False,                      # 是否在训练时进行性能分析(速度/内存)。
        seed=42,                            # 随机种子,确保实验可复现。
        deterministic=True,                 # 保证可复现性,可能会牺牲一些训练速度。
        single_cls=False,                   # 是否将所有类别视为单一类别。我们有两个类别,设为False。
        # 数据增强参数 (非常重要!)
        hsv_h=0.015,                        # 色调(H)增强幅度。
        hsv_s=0.7,                          # 饱和度(S)增强幅度。
        hsv_v=0.4,                          # 明度(V)增强幅度。
        degrees=0.0,                        # 旋转角度范围。小数据集可设小值,如5.0。
        translate=0.1,                      # 平移幅度。
        scale=0.5,                          # 缩放幅度。
        shear=0.0,                          # 剪切幅度。
        perspective=0.0,                    # 透视变换幅度。
        flipud=0.0,                         # 上下翻转概率。
        fliplr=0.5,                         # 左右翻转概率。对于水平对称目标有效。
        mosaic=1.0,                         # Mosaic数据增强的概率。小数据集建议保持1.0。
        mixup=0.0,                          # MixUp数据增强的概率。小数据集可谨慎使用或设为0。
        copy_paste=0.0,                     # 复制粘贴增强的概率。需要额外处理,通常为0。
    )

if __name__ == '__main__':
    train_yolov8()

核心参数调优经验 :

  • 学习率 ( lr0 ) :这是超参数中的“王炸”。对于小数据集,学习率不宜过大,否则容易震荡甚至发散。可以从默认的0.01开始,如果训练初期损失就变成NaN(爆炸),果断调小到0.001甚至0.0001试试。也可以使用 --lr0 0.01 --lrf 0.01 配合余弦退火,让学习率平滑下降。
  • 数据增强 :这是 防止小数据集过拟合的生命线 。 mosaic=1.0 是YOLO系列的王牌增强,必须开启。 fliplr=0.5 (水平翻转)对药品包装盒这种通常水平对称的目标很有用。 hsv 增强模拟光照和颜色变化。对于小数据集,可以适度增强 degrees (旋转)和 perspective (透视),模拟拍摄角度变化,但幅度不宜过大,避免产生不真实的图像。 mixup 和 copy_paste 等强增强在小数据集上可能带来负面效果,建议先设为0。
  • 损失权重 ( box , cls , dfl ) :通常保持默认即可。如果你的任务中定位精度远比分类重要(比如需要非常精确的框),可以稍微提高 box 的权重。
  • 图像尺寸 ( imgsz ) :更大的尺寸通常能带来更好的精度,但会显著增加显存消耗和训练时间。对于药品包装盒这种目标相对较大的场景,640甚至512可能就足够了。可以先从640开始。

3.3 训练过程监控与评估解读

训练开始后,Ultralytics会在 runs/detect/drug_detection_v8s (你指定的 name )目录下生成大量有用的文件:

  • weights/ :存放最佳模型 best.pt 和最后模型 last.pt 。
  • events.out.tfevents... :TensorBoard日志文件。使用 tensorboard --logdir runs/detect 可以在浏览器中实时查看损失曲线、学习率曲线、验证集mAP等指标,这是监控训练状态最直观的方式。
  • args.yaml :保存了本次训练的所有参数,便于复现。
  • results.csv 和 results.png :训练结果的表格和图表汇总。

你需要重点关注以下指标:

  1. 损失曲线 ( train/box_loss , train/cls_loss , val/box_loss , val/cls_loss ) :理想的曲线是训练损失和验证损失都平稳下降,并最终趋于平缓。如果 训练损失持续下降,但验证损失在某个点后开始上升 ,这是典型的过拟合信号。此时需要加强数据增强、增加正则化(如 weight_decay )、或减少模型复杂度(换更小的模型)。
  2. mAP (mean Average Precision) :这是目标检测的核心评估指标。 metrics/mAP50(B) 表示IoU阈值为0.5时的mAP, metrics/mAP50-95(B) 表示IoU阈值从0.5到0.95(步长0.05)的平均mAP。后者更严格,更能综合反映模型性能。我们的目标是看到这些曲线随着训练轮数上升。
  3. 学习率曲线 :确认学习率按照你设定的调度策略(如余弦退火)正常变化。

踩坑记录 :有一次我在训练一个类似的小数据集时,验证集mAP始终在0.5左右徘徊上不去。检查TensorBoard发现,验证集分类损失 ( val/cls_loss ) 一直很高。后来发现,是数据集中有一个类别的少量样本标注有误(类别标错了)。清洗修正数据后,模型性能立刻得到了提升。所以, 当模型性能不符合预期时,第一反应应该是回头检查数据质量 ,而不是盲目调整超参。

4. 模型验证、推理与结果分析

训练完成后,我们得到了一个 best.pt 模型。接下来需要在独立的测试集上评估其泛化能力,并学习如何使用模型进行预测。

4.1 在测试集上进行模型验证

使用YOLOv8的 val 模式,可以方便地计算模型在测试集上的各项指标。

from ultralytics import YOLO

def evaluate_model():
    # 加载训练好的最佳模型
    model = YOLO('./runs/detect/drug_detection_v8s/weights/best.pt')

    # 在测试集上验证
    metrics = model.val(
        data='./dataset_split/data.yaml',
        split='test', # 指定使用测试集。如果data.yaml中未定义test,则用val。
        imgsz=640,
        batch=16,
        save_json=True, # 保存评估结果的JSON文件,可用于进一步分析
        save_hybrid=True, # 保存混合标签图,便于可视化
        conf=0.001, # 评估时使用的置信度阈值,越低召回率可能越高
        iou=0.6, # 评估时使用的NMS IoU阈值
        device='0',
        name='val_on_test' # 验证结果保存的目录名
    )
    # 打印关键指标
    print(f"mAP50-95: {metrics.box.map:.4f}")
    print(f"mAP50: {metrics.box.map50:.4f}")
    print(f"Precision: {metrics.box.p:.4f}")
    print(f"Recall: {metrics.box.r:.4f}")
    # 打印每个类别的AP
    for i, class_name in enumerate(metrics.names.values()):
        print(f"Class {class_name} AP50-95: {metrics.box.ap[i]:.4f}")

if __name__ == '__main__':
    evaluate_model()

运行后,除了在终端输出指标,还会在 runs/detect/val_on_test 目录下生成:

  • confusion_matrix.png :混淆矩阵,直观显示模型在每个类别上的分类错误情况。
  • F1_curve.png :F1分数随置信度阈值变化的曲线,帮你选择最优的置信度阈值。
  • P_curve.png 和 R_curve.png :精确率和召回率曲线。
  • PR_curve.png :精确率-召回率曲线,曲线下面积就是AP。
  • results.png :指标汇总图。
  • labels.jpg 和 labels_correlogram.jpg :标签分布和相关性图。

如何解读结果? 假设我们得到 mAP50-95: 0.85 , mAP50: 0.92 。这表示在IoU阈值为0.5时,模型平均精度达到92%,这是一个非常不错的结果,说明模型能较好地检测出目标。如果 mAP50-95 显著低于 mAP50 ,说明模型对边界框的定位精度还有提升空间,可能需要调整 box 损失权重或使用更精细的锚框。

4.2 使用训练好的模型进行单张图像推理

验证完模型,就可以用它来预测新图片了。

from ultralytics import YOLO
import cv2

def predict_single_image():
    model = YOLO('./runs/detect/drug_detection_v8s/weights/best.pt')

    # 预测单张图像
    results = model('./test_image.jpg', imgsz=640, conf=0.25, iou=0.45, save=True)

    # 结果是一个列表,因为可能有多张图,我们取第一张
    result = results[0]

    # 可视化并保存
    annotated_img = result.plot() # 生成带标注框的图像 (BGR格式)
    cv2.imwrite('./predicted_result.jpg', annotated_img)

    # 打印检测到的目标信息
    for box in result.boxes:
        cls_id = int(box.cls)
        conf = float(box.conf)
        xyxy = box.xyxy[0].tolist() # 获取边界框坐标 [x1, y1, x2, y2]
        print(f"检测到: {result.names[cls_id]}, 置信度: {conf:.2f}, 位置: {xyxy}")

if __name__ == '__main__':
    predict_single_image()

关键推理参数 :

  • conf : 置信度阈值。高于此值的检测框才会被保留。根据你的F1曲线和实际需求调整。要求高召回率(宁可错杀不可放过)就调低,要求高精确率(确保每个框都是对的)就调高。
  • iou : 非极大值抑制 (NMS) 的IoU阈值。用于合并重叠的检测框。值越小,合并越激进,留下的框越少。

4.3 结果分析与常见问题排查

根据测试集评估和单张推理的结果,你可能会遇到以下几种情况,下面提供排查思路:

  1. 精度低 (mAP < 0.5) :

    • 数据问题 :回头用第2.1节的方法,仔细检查测试集图像的标注质量。是否存在大量漏标、错标?训练集和测试集的分布(光照、背景、角度)差异是否过大?
    • 模型容量不足 :尝试使用更大的模型,如从YOLOv8n切换到YOLOv8s或YOLOv8m。
    • 训练不充分 :增加训练轮数 epochs ,并配合学习率热身 ( warmup_epochs ) 和余弦退火。
    • 数据增强不足 :小数据集更容易过拟合,适当增强 degrees , perspective , mixup (小心使用)。
  2. 过拟合 (训练集精度远高于验证/测试集) :

    • 加强正则化 :增加 weight_decay (如从0.0005到0.001)。
    • 使用更强的数据增强 :这是对抗过拟合最有效的手段。确保 mosaic=1.0 ,尝试开启 mixup (从0.1开始),增加 hsv 增强幅度。
    • 早停 (Early Stopping) :监控验证集损失,当其在连续多个epoch不再下降时停止训练。Ultralytics内部有简单的早停逻辑,也可以自己写回调实现。
    • 减少模型复杂度 :换用更小的模型(如YOLOv8n)。
  3. 欠拟合 (训练集和验证集精度都低) :

    • 模型容量可能不足 :换用稍大的模型。
    • 学习率可能太小 :适当增大 lr0 。
    • 数据增强可能太强 :过强的增强(如大角度的旋转、扭曲)可能会让模型难以学习有效特征,尤其是对于小数据集。尝试减弱增强参数。
    • 检查数据路径和标注 :确认 data.yaml 中的路径是否正确,训练时是否真的加载了数据和标签。
  4. 某个类别检测效果特别差 :

    • 类别不平衡 :检查该类别的样本数量是否远少于其他类别。如果是,可以考虑对该类别的图像进行过采样,或在计算损失时给该类别的损失增加权重(YOLOv8中可以通过 cls 参数间接影响,但更精细的类别权重需要修改源码)。
    • 特征难以区分 :板蓝根和999感冒灵包装盒是否在颜色、形状上非常相似?可以尝试在数据增强中更多使用颜色扰动 ( hsv ),或者考虑收集更多该类别有区分度的样本。

处理这个小数据集的过程,本质上是一个完整的目标检测微调(Fine-tuning)流程的缩影。从数据准备、格式处理、模型选择、训练调优到最后的评估推理,每一步都充满了细节和选择。通过这个实战,你不仅能学会操作YOLOv8这个工具,更能理解数据驱动的AI项目背后“数据-模型-调优”的闭环逻辑。在实际工作中,遇到成千上万张图像的大数据集时,这套方法论同样适用,只是各个环节的复杂度和自动化要求会更高。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐