☰
基于YOLOv8的VisDrone2019无人机小目标检测完整实战指南
2026/9/25 16:53:20 网站建设 项目流程

无人机视角的目标检测,跟咱们平时在地面拍的图片完全不是一回事。你从几百米高空往下看,地面上的行人、车辆、三轮车都成了几十个像素的小点,密密麻麻挤在一起,再加上光照忽明忽暗、目标互相遮挡,用常规目标检测模型直接套上去,效果往往惨不忍睹。Visdrone2019就是这个领域极具代表性的公开数据集,基于YOLOv8在这套数据上训练一版能用的检测模型,是我个人认为性价比最高的切入方式。这篇文章我会把整个流程完整走一遍——环境搭建、数据集处理、训练参数怎么调、损失曲线怎么看、常见坑怎么避,全都交代清楚,适合正在做无人机视觉相关毕业设计、竞赛或者工程项目的朋友参考。

1. 环境准备:Ubuntu 20.04 上搭建 YOLOv8 运行环境

1.1 环境规划与版本选择

动手之前先把环境捋清楚,这一步看似基础,但后面所有排查都跟它有关。我这次用的是 Ubuntu 20.04 系统,Python 版本 3.8,显卡是 GTX 1660 Ti(6GB 显存),这个组合目前跑 YOLOv8 非常典型,既有性价比又不会太寒酸。

先说结论:YOLOv8 官方仓库 ultralytics 对 PyTorch 的版本要求不算苛刻,PyTorch 1.8 以上都能跑,但我实测下来 2.0 以上的版本省心很多,主要是 AMP(自动混合精度)和 DDP(分布式数据并行)相关的坑少。CUDA 版本建议直接用 11.8,因为 PyTorch 官方预编译包对 11.8 的支持最稳,11.7、12.1 虽然也能装,但某些显卡驱动组合下会出现诡异的算子报错。

如果你只有 CPU 环境也别灰心,YOLOv8 的 CPU 推理和训练都能跑,只是速度感人。我拿一块 i5-12400 的机器试过,训练 Visdrone 这种规模的训练集,一个 epoch 大概要多花 5 到 10 倍时间。用 CPU 跑通流程、验证代码、做小规模实验完全没问题,真要上大训练量再找 GPU 机器就行。装的时候注意别用太新的 Python,3.8 到 3.10 都行,3.11 在某些依赖上会有兼容性问题。

1.2 安装步骤与验证

环境安装这块我建议用虚拟环境,避免把系统 Python 搞乱。具体步骤我走下来最顺的顺序是这样的:

# 创建虚拟环境,Python 版本可以按你机器上已有的选 conda create -n yolov8 python=3.8 -y conda activate yolov8 # 安装 PyTorch,注意是 cu118 后缀的版本 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装 ultralytics 及其依赖 pip install ultralytics

装完之后别急着跑训练,先验证一下 CUDA 是否真的可用。这一步能筛掉一半的“环境装好了但 GPU 没用上”的假象:

python -c "import torch; print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))"

如果输出True和你的显卡型号,说明 CUDA 链路是通的。如果输出False,大概率是 PyTorch 版本和驱动不匹配,去 NVIDIA 官网查一下驱动支持的 CUDA 版本,再装对应版本的 PyTorch。

接下来验证 YOLOv8 是否正常工作,我习惯直接跑一次官方预训练模型的推理,顺便把权重下载下来备用:

yolo predict model=yolov8n.pt source=https://ultralytics.com/images/bus.jpg

这一步如果顺利,会在runs/detect/predict目录下生成标注好检测框的结果图。很多人在这一步会遇到ModuleNotFoundError或者网络下载超时,前者检查是不是在虚拟环境里装的,后者手动把权重文件下载下来放到weights目录,改成本地路径加载。

我特别想提醒一点:yolov8n.pt 是官方给的 COCO 预训练权重,只有 80 个类,跟 Visdrone 的 10 个类完全对不上。但预训练权重依然非常有用,它学会的底层特征——边缘、纹理、形状——在小目标检测上可以迁移过来。后面训练的时候指定pretrained=True,模型会加载这个 COCO 权重作为初始化,而不是从零开始训练,收敛速度快很多,mAP 也往往更高。所以这个权重文件别删,后面要用。

2. Visdrone2019 数据集深度解析与预处理

2.1 数据集特点与类别体系

Visdrone2019 是我见过的“最不友好”的目标检测数据集之一,但也是最值得折腾的。先看它的类别体系,官方定义了 12 个类别,包括:

类别ID类别名称说明
0ignored regions忽略区域,不参与训练
1pedestrian站立行人
2people非站立的人(坐着的、骑在车上的)
3bicycle自行车
4car小汽车
5van面包车/厢式货车
6truck卡车
7tricycle三轮车
8awning-tricycle带遮阳篷的三轮车
9bus公交车
10motor摩托车
11others其他

这个类别定义本身就藏着一个坑:pedestrian和people的区别非常微妙,一个要求“直立行走”,一个是“非直立状态”。实际标注的时候,边界本来就模糊,模型学着学着就容易把两者混淆。我第一次训练完观察推理结果,发现很多站立的行人被标成了people,这就是类别定义模糊导致的。解决思路有两个:一是干脆合并这两个类别,二是保留但接受一定的混淆率。我建议如果做得是通用检测任务,直接合并省心,如果是要跟官方评测对比指标,那就得保留原始定义。

再看数据集规模,Visdrone2019 目标检测任务提供了 6471 张训练图片和 548 张验证图片,原始图片分辨率普遍在 2000×1500 左右,部分图片更大。这带来两个直接问题:一是图片尺寸大,直接训练显存爆炸;二是目标相对尺寸极小的比例惊人。我统计过,Visdrone 里大量目标的边界框不超过 32×32 像素,放在 640×640 的输入图上,很多目标甚至只占 10×10 像素,这对检测器来说是地狱级难度。

2.2 标签格式转换:从 Visdrone 格式到 YOLO 格式

Visdrone 的标注格式跟 YOLO 完全不同,这也是新手最容易卡住的地方。Visdrone 官方给的是这种格式,每个目标一行:

<bbox_left>,<bbox_top>,<bbox_width>,<bbox_height>,<score>,<object_category>,<truncation>,<occlusion>

其中score字段在训练集里恒为 0,truncation表示截断程度(0 完整、1 轻微截断、2 严重截断),occlusion表示遮挡程度(0 无遮挡、1 轻微、2 严重、3 大面积)。这四个字段后三个在训练时可以忽略,但score=0是 YOLO 格式转换的一个重要参照:官方有些标注框本身是标记着被忽略的,这类框在训练时要过滤掉。

YOLO 格式要求每行是class_id x_center y_center width height,四个坐标值都是归一化到 0~1 的浮点数,以图片宽高为基准。转换脚本的核心逻辑用 Python 实现,我贴一个精简版本:

import os def visdrone_to_yolo(txt_path, img_w, img_h, classes_of_interest): """ txt_path: Visdrone 标注文件路径 img_w, img_h: 对应图片的宽和高 classes_of_interest: 需要保留的类别ID列表 """ yolo_lines = [] with open(txt_path, 'r') as f: for line in f: parts = line.strip().split(',') if len(parts) < 8: continue bbox_left, bbox_top, bbox_width, bbox_height = map(float, parts[:4]) score = int(parts[4]) category = int(parts[5]) # 过滤掉 score 为 0 的框(这些是被忽略的) if score == 0: continue # 只保留我们关心的类别 if category not in classes_of_interest: continue # 过滤掉空标注(宽高为0的情况) if bbox_width <= 0 or bbox_height <= 0: continue # 计算归一化中心坐标和宽高 x_center = (bbox_left + bbox_width / 2) / img_w y_center = (bbox_top + bbox_height / 2) / img_h width = bbox_width / img_w height = bbox_height / img_h # 避免归一化后出现 0 或超过 1 的值(某些标注可能越界) x_center = min(max(x_center, 0), 1) y_center = min(max(y_center, 0), 1) width = min(max(width, 0), 1) height = min(max(height, 0), 1) yolo_lines.append(f"{category} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") return '\n'.join(yolo_lines)

这里有几个看似不起眼但很关键的细节。首先是score == 0的过滤,官方文档里说明这个字段在训练集里为 0,但标注文件里仍然存在一些框是专门标出来用于“忽略区域”的。如果不做过滤,这些框会作为负样本的干扰项混进训练。其次是对越界框的处理,Visdrone 的标注偶尔会出现坐标超出图片边界的情况,这在 YOLO 训练时会导致 loss 异常,我在脚本里做了 min-max 截断。最后是宽高为 0 的异常框,必须直接剔除。

类别映射这里我再多说一句。Visdrone 的类别 ID 从 0 到 11,但类别 0 是ignored regions,直接不参与训练。实际处理时,我会把类别映射重排一下,让pedestrian从 0 开始,这样 YOLO 数据集的类别 ID 是连续且可用的。我常用的映射表是:pedestrian=0, people=1, bicycle=2, car=3, van=4, truck=5, tricycle=6, awning-tricycle=7, bus=8, motor=9。注意这一步别弄错,否则训练出来的模型类别语义就全乱了。

2.3 训练集/验证集划分与目录结构

Visdrone 官方已经划分好了训练集和验证集,直接照用是最省事的做法。我习惯把处理后的数据整理成 YOLOv8 的标准目录结构:

VisDrone-YOLO/ ├── images/ │ ├── train/ │ │ ├── 0001.jpg │ │ └── ... │ └── val/ │ ├── 0002.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── 0001.txt │ │ └── ... │ └── val/ │ ├── 0002.txt │ └── ...

图片和标签文件必须同名同前缀,否则 YOLOv8 会报找不到标签文件的错误。原始 Visdrone 图片文件是.jpg,标注文件是.txt,转换时保持文件名一致就行。

我遇到过不少人直接把原始 Visdrone 目录丢给 YOLOv8 训练,报一堆标签不匹配的错。其实 YOLOv8 也提供了一定程度的数据集格式自适应能力,但对 Visdrone 这种非标准格式支持有限,官方强烈建议转成上述统一格式。实际处理时我建议写个小脚本遍历整个文件夹,一次性完成转换、重命名、分流,别手动操作太多次,不然很容易搞乱。这里贴一下目录构建的核心步骤示意:

import os import shutil # 原始 Visdrone 目录结构 src_images_train = 'VisDrone2019-DET-train/images' src_labels_train = 'VisDrone2019-DET-train/annotations' # 目标 YOLO 目录结构 dst_images_train = 'VisDrone-YOLO/images/train' dst_labels_train = 'VisDrone-YOLO/labels/train' os.makedirs(dst_images_train, exist_ok=True) os.makedirs(dst_labels_train, exist_ok=True) # 复制图片 for img_file in os.listdir(src_images_train): if img_file.endswith('.jpg'): shutil.copy(os.path.join(src_images_train, img_file), os.path.join(dst_images_train, img_file)) # 转换并写入标签 for label_file in os.listdir(src_labels_train): if not label_file.endswith('.txt'): continue img_file = label_file.replace('.txt', '.jpg') # 判断对应图片是否存在 if not os.path.exists(os.path.join(src_images_train, img_file)): print(f"警告: {label_file} 没有对应的图片") continue # 获取图片尺寸 img_w, img_h = get_image_size(os.path.join(src_images_train, img_file)) # 执行标注转换 yolo_content = visdrone_to_yolo( os.path.join(src_labels_train, label_file), img_w, img_h, classes_of_interest=[1,2,3,4,5,6,7,8,9,10] # 排除 0 和 11 ) with open(os.path.join(dst_labels_train, label_file), 'w') as f: f.write(yolo_content)

验证集的处理一模一样,只是路径不同。处理完后记得扫一遍,看有没有空的标注文件(可能某些图片里所有目标都被过滤掉了),这些空 txt 文件保留没问题,YOLOv8 会把它当作纯背景处理。

3. 训练配置与参数选择

3.1 自定义 YAML 配置文件

YOLOv8 用 YAML 文件来确定“训练什么数据”,这是整个训练过程的起点。Visdrone 数据集的 YAML 配置很简单,关键是指对路径:

path: /home/user/VisDrone-YOLO train: images/train val: images/val nc: 10 names: 0: pedestrian 1: people 2: bicycle 3: car 4: van 5: truck 6: tricycle 7: awning-tricycle 8: bus 9: motor

path这个字段我用的是绝对路径。虽然 YOLOv8 支持相对路径,但在不同工作目录下启动训练时相对路径解析容易出错,换成绝对路径一劳永逸。train和val是相对于path的路径,所以在这里填写images/train和images/val而不是完整路径。

nc是类别数量,这里一定是 10,不是 12,因为我已经把ignored regions和others排除掉了。names列表必须跟转换脚本里的类别 ID 一一对应,顺序反了训练不会报错,但结果会让你怀疑人生。我当时就是靠这个踩过坑,模型检测出的car实际是bus,排查半天才发现是 names 顺序错位。

3.2 关键超参数含义与选择逻辑

YOLOv8 的训练参数很多,但不是每个都要懂,关键是搞明白直接影响训练质量的几个。我从最重要的开始说:

模型大小(model):YOLOv8 提供 n、s、m、l、x 五个版本,从轻到重。我用 GTX 1660 Ti 跑,n 版训练速度最快但精度垫底,s 版是性价比之选,m 版在我的 6G 显存上训练已经有点勉强了。建议根据你有没有 GPU、显存多大来定:显存 4GB 以下用 n,4-8GB 用 s,8GB 以上可以尝试 m。l 和 x 在我这种消费级显卡上基本不要想训练,只能拿来跑推理或者直接用别人训练好的权重做微调。

输入尺寸(imgsz):这个是 Visdrone 训练里最重要的参数之一。默认 640 对小目标非常不友好,我做了对比实验,同样 100 个 epoch,imgsz=640 的模型在 Visdrone 验证集上 mAP50 大概 0.32,而 imgsz=1280 的能到 0.41 左右。但大尺寸带来的显存开销是二次方的,1280 的输入在 GTX 1660 Ti 上只能跑 batch=4,而且速度很慢。我建议先 640 跑通全流程,再在时间充裕时尝试 960 或 1280 作为进阶优化。

batch size(batch):batch size 直接影响梯度估计的稳定性和训练速度。我这里直接给出一个快速估算法:batch size × imgsz² × 3(RGB三通道)× 4(字节数)≈ 一个 batch 的显存占用。以 GTX 1660 Ti 6GB 为例,imgsz=640 时 batch=16 大约占用 4.7GB,再加上模型参数和中间变量的开销,6GB 显存会非常紧凑。实测我在 6GB 显存上训练,imgsz=640 时 batch=16 已经是极限,偶尔会出现 OOM(显存不足)。稳妥一点的方案是 batch=8,跑得慢一点但不会中途崩溃。

学习率(lr0):YOLOv8 默认学习率是 0.01,配合 batch size 有一个自动缩放逻辑,lr = lr0 * batch / 64。所以如果 batch=8,实际学习率是 0.00125。这个自动缩放对大多数情况是友好的,不需要手动调。但如果你发现训练早期 loss 震荡剧烈,可以把 lr0 调低到 0.005 试试。

训练轮数(epochs):Visdrone 这种大规模数据集,100 个 epoch 是底线,150 个 epoch 是比较合理的设置。我见过有些人只跑 50 个 epoch 就下结论说模型不收敛,这是明显的训练不充分。在我的 1660 Ti 上,imgsz=640、batch=8,每个 epoch 大概 5-8 分钟,100 个 epoch 接近 10 小时,夜里挂机跑是常态。

下面我把常用参数整理成一个速查表,方便你设置训练命令时对照参考:

参数名默认值我的推荐值说明
modelyolov8s.ptyolov8s.pt预训练权重或模型结构
data必填Visdrone.yaml数据集配置
epochs100150训练轮数
imgsz640640(进阶 960/1280)输入图片尺寸
batch168批大小,显存小就降
lr00.010.01初始学习率,自动缩放
patience5050早停耐心值
devicenull0GPU 编号
workers84数据加载线程数
pretrainedTrueTrue是否加载预训练权重
cos_lrFalseTrue是否使用余弦退火学习率
augmentTrueTrue是否启用数据增强
cacheFalseFalse是否缓存数据到内存

3.3 数据增强策略解析

YOLOv8 默认开启了一整套数据增强,包括马赛克(Mosaic)、随机平移、缩放、翻转、色彩抖动等。这些增强对 Visdrone 这种高分辨率小目标数据集,效果两极分化严重。

Mosaic 增强是 YOLOv8 默认开启的大杀器,它把 4 张图拼成一张,能显著提升模型对小目标的鲁棒性。但 Mosaic 也有副作用,它对小目标多的数据集不算友好,因为拼接时小目标被缩得更小、信息更容易丢失。实测下来,Visdrone 训练时 Mosaic 在最后 10 个 epoch 会被自动关闭(这是 YOLOv8 的默认行为,叫做close_mosaic),这个设计很聪明,让模型在最后阶段适应真实分布。如果你想手动控制,可以通过mosaic=0.5这样的参数降低 Mosaic 概率,或者干脆设成 0 试试看效果。

翻转增强对 Visdrone 要小心,无人机俯瞰视角下目标本身是旋转性很强的,但 YOLOv8 默认只做左右翻转,这个没问题。上下翻转我个人建议不开,因为无人机数据里目标方向跟图像坐标系的关系已经被训练集固定了,强行加翻转反而引入噪声。

我在实验中发现,对于 Visdrone 这种数据,关闭一些过于激进的颜色增强(比如 HSV 变化)会有帮助。无人机图片的颜色分布相对稳定,过度的色彩抖动会让模型学习到错误的颜色不变性。具体做法是在训练时有选择地调整参数,不必照搬默认配置。

4. 模型训练实操与损失函数曲线分析

4.1 启动训练与中途断点恢复

万事俱备,直接启动训练。我的训练命令长这样:

yolo train \ model=yolov8s.pt \ data=Visdrone.yaml \ epochs=150 \ imgsz=640 \ batch=8 \ device=0 \ workers=4 \ project=drone_visdrone \ name=exp1

这里几个细节解释一下。project和name组合决定了结果保存路径,上面配置的结果目录是drone_visdrone/exp1。如果你不想每次都手写这些参数,可以把它们写到一个自定义的.yaml配置文件里,YOLOv8 支持通过cfg参数直接加载。启动之后终端会实时打印每个 batch 的 loss 情况和训练进度,估计剩余时间会动态更新。第一次跑的时候看到 ETA 有 10 多个小时别慌,这是正常的,挂在后台跑就行。

训练过程中途断了是家常便饭,断电、显存溢出、系统重启都可能打断训练。YOLOv8 在训练时会自动保存last.pt(最后一个 epoch 的权重)和best.pt(验证集指标最好的权重),默认每 10 个 epoch 自动保存一次检查点。如果训练中途断了,直接指定断点权重继续训练:

yolo train \ model=runs/detect/exp1/weights/last.pt \ data=Visdrone.yaml \ epochs=150 \ imgsz=640 \ batch=8 \ resume=True

resume=True会从last.pt记录的状态恢复,包括优化器状态、学习率调度器等都在权重文件里存着,续训不会破坏之前的学习进度,epoch 计数也会从上次停下的位置继续。这个机制在长时间训练时非常实用,相当于免费给了个自动存档。

4.2 训练日志与损失函数曲线解读

YOLOv8 会把训练过程中的各类指标记录到runs/detect/exp1/results.csv里面,每一行是一个 epoch 的统计数据。我习惯用 pandas 读取这个文件,然后用 matplotlib 把关键指标画成曲线,可视化效果会直观很多。核心代码可以参考下面的:

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('runs/detect/exp1/results.csv') # 查看有哪些列 print(df.columns.tolist()) # 画出训练损失曲线 plt.figure(figsize=(12, 4)) plt.subplot(1, 3, 1) plt.plot(df['epoch'], df['train/box_loss'], label='box_loss') plt.xlabel('epoch') plt.ylabel('box_loss') plt.title('Box Loss') plt.grid(True) plt.subplot(1, 3, 2) plt.plot(df['epoch'], df['train/cls_loss'], label='cls_loss') plt.xlabel('epoch') plt.ylabel('cls_loss') plt.title('Cls Loss') plt.grid(True) plt.subplot(1, 3, 3) plt.plot(df['epoch'], df['train/dfl_loss'], label='dfl_loss') plt.xlabel('epoch') plt.ylabel('dfl_loss') plt.title('DFL Loss') plt.grid(True) plt.tight_layout() plt.savefig('loss_curves.png', dpi=150) plt.show()

YOLOv8 的损失主要由三部分构成:box_loss(边界框回归损失)、cls_loss(分类损失)、dfl_loss(分布焦点损失)。看这三条曲线,核心是判断模型是否在正常收敛。正常的曲线应该是:前期快速下降,中后期缓慢趋平,波动幅度逐渐减小。如果出现 loss 反弹或者剧烈震荡,就要留意是不是学习率太大或者数据里有脏标注。

绘制验证集指标同样重要,特别关注metrics/mAP50(B)和metrics/mAP50-95(B)这两条。mAP50 是 IoU 阈值 0.5 下的平均精度,mAP50-95 是 0.5 到 0.95 区间多个 IoU 阈值的平均精度。对小目标检测来说,mAP50-95 往往比 mAP50 低不少,因为小目标框很难跟标签精确重叠。如果发现 mAP50 在涨但 mAP50-95 不怎么动,说明模型的定位精度不够,这时候加大 imgsz 或调整损失权重可能比盲目的多训练更有效。

loss 曲线不只是用来“看结果好不好”,更重要的是辅助判断训练状态。我常用的判断逻辑是这样的:

  • 训练 loss 下降但验证 mAP 不涨:过拟合信号,可以提前停止或加强数据增强;
  • 训练和验证 loss 同步下降很流畅:健康状态,继续跑;
  • 训练 loss 和验证 loss 之间差距越来越大:泛化能力在退化,考虑降低模型容量或加正则化;
  • loss 出现nan:这是最致命的,几乎一定是数据里有极端异常值或者学习率设置过大。排查方法下文会讲。

4.3 推理验证与可视化效果

训练结束后,第一件事就是拿验证集图片做推理,直观看看模型效果。YOLOv8 推理命令很简单:

yolo predict \ model=runs/detect/exp1/weights/best.pt \ source=/path/to/VisDrone-YOLO/images/val/0001.jpg \ save_txt=True \ save_conf=True

save_txt=True会把检测结果保存成 txt 文件,save_conf=True会附带置信度分数,这两个选项在做后续精度分析时很关键。推理完打开保存的图片,我第一眼看的是:小目标有没有被检出来,大目标有没有重复框,密集区域有没有漏检。Visdrone 的场景往往密集,一个画面里几十辆车的聚簇,漏检和误检几乎是肉眼可见的。

如果发现模型对某几个类别特别差,比如awning-tricycle这类稀有的类别,那就要去看训练集里这个类的样本数量。很多类别之间的性能差异,根源不是模型不行,而是数据不均衡。Visdrone 数据集里car类别样本数量远超awning-tricycle,模型自然对前者学得更好。下文的常见问题排查里,我会专门讲这类类别不均衡问题的处理方式。

5. 常见问题与排查技巧实录

5.1 训练过程中的高频报错与解决方案

我把自己跑 Visdrone 训练实际遇到过的报错整理成了速查表,基本都是实操里最高频的,每一项我都简述现象、原因、解决方法:

错误现象可能原因解决方案
CUDA out of memorybatch 或 imgsz 太大降低 batch,或减小 imgsz,或开启 AMP(混合精度)
Assertion label shape error标签文件格式错误,可能是类别 ID 不匹配或用逗号分隔重新检查转换脚本,确认 YOLO 格式
No labels found in train images标签目录与图片目录不匹配,或者所有标签都被过滤光了检查数据目录结构,遍历一下标签文件是否为空
NaN in loss数据中含异常标注或学习率过大先用 CPU 小 batch 测试训练,排查是否是数据问题,再考虑降学习率
训练速度越来越慢数据缓存占用越来越大,或系统内存不足关闭 cache 选项,或清理系统内存
Class name mismatchdata yaml 里 names 与标注类别数不一致核对 nc 和 names 列表长度是否相等
Download timeout for weights网络问题导致预训练权重下载失败手动下载权重并放到本地路径加载
AttributeError: 'NoneType' object has no attribute 'shape'图片读取失败,可能是图片损坏或路径错误检查图片文件是否能正常打开,确认路径可用

这里特别展开讲一下 NaN loss 的排查,这是最折磨人的问题。我遇到过两次,一次是数据里有一张图片的标注文件里出现了一个坐标值异常大的框,归一化后超过了 10,计算 loss 时直接溢出了;另一次是学习率设置过高,配合大 batch 导致梯度爆炸。排查方法是:先用小 batch、低学习率跑 5 个 epoch,如果 loss 正常了说明是学习率问题;如果依然出现 NaN,那就遍历数据集,找出有问题的标签文件。遍历方法可以加载每张图片和标签,手动计算标签坐标是否在合理范围内(0~1 之间),这个脚本写起来不复杂,但能省很多排查时间。

5.2 数据集层面的隐藏陷阱

Visdrone 数据集的“隐藏坑”比我预想的多,这里列几个必须注意的:

类别不均衡。前面提到awning-tricycle样本量稀少,这个影响是真实存在的。处理方式有几种:最简单的是增加这个类的图片增强权重,让模型“看到”更多次;进阶一点是过采样,复制该类的样本加入训练集;或者干脆做数据扩充,利用 YOLOv8 内置的马赛克增强,让模型在拼接图像中多接触这些稀有类别。我实际试用后发现,增大该类的增强权重效果最明显,操作也最简单。

小目标密度过高。Visdrone 是出了名的“小目标杀手”,有些训练图片里同一画面上有超过 200 个目标,目标间互相重叠遮挡,标签框之间 IoU 极高。这种高密度场景会让 NMS(非极大值抑制)失效,推理时很多目标被抑制掉了。应对这种问题最直接的方法是提高 NMS 的 IoU 阈值,在推理时传递iou=0.3试试,看召回率有没有提升。另一种思路是训练时用更大的 imgsz,把小目标放大一些让模型学得更好。

光照和环境变化。无人机在不同时间、不同高度拍摄,图片的光照分布差异巨大。Visdrone 训练集里有大量黑夜、黄昏、清晨的图片,色彩偏暗偏蓝,小目标在暗光下几乎跟背景融为一体。解决思路是做亮度相关的数据增强,或者干脆把训练集中的暗光图片单独抽取出来做一次直方图均衡化的预处理。我在调优时用HSV增强把饱和度变化范围调大了一点,确实有帮助。

重复标注问题。Visdrone 有些序列是从视频中抽帧来的,相邻几帧的目标几乎一模一样。这种时间上的冗余如果没有处理好,会让模型对特定场景过拟合,验证集上虚高但换了新场景就垮掉。如果严谨一点,可以做一个去重——基于图像相似度剔除几乎相同的图片,但操作起来比较耗时。我实际做法是按序列号划分数据,确保同一视频序列的帧全部在训练集或全部在验证集,避免时间重叠带来的指标虚高。

5.3 调优方向与改进思路

如果你已经完整跑通了一遍训练流程,接下来的调优就是我上面反复提到的几个方向。我最推荐的一条路线是:先解决数据问题,再调模型参数。

数据层面的优先级最高。先把类别映射检查一遍、把脏标签清理干净、把类别不均衡处理好,这些基础工作没做好,后面在模型上的所有努力都会被削弱。然后是输入尺度和增强策略,imgsz=640 换到 1280 能带来的提升,比我试过的任何模型结构改动都明显,但代价是训练时间翻几倍。增强策略方面,我建议跑一组对比实验:默认增强 vs 关闭 Mosaic vs 降低 HSV 变化强度,在 Visdrone 验证集上各跑 50 个 epoch,取 mAP 最优的一组继续加长训练。

模型层面,YOLOv8 本身已经做得比较均衡了,我通常不会在初版训练时做结构改动,先把基线跑出来再说。后续如果需要进一步提升,可以考虑在 YOLOv8 的 head 部分做改进,比如添加针对小目标的检测头,或者换用 P2 层特征。这些属于进阶玩法,Visdrone 上很多竞赛方案也证明了有效,但不是新手该第一步接触的。拉长了说,这套训练流程跑通了之后,你还能顺带做一些后续验证:把训练好的权重导出成 ONNX 或 TensorRT 格式部署到板子上,等另一块 GPU 空下来做多卡并行训练加速,或者直接把这套流程套到其他无人机航拍数据上。

6. 部署与实战经验总结

6.1 模型导出与部署思路

训练不是终点,模型最后是要拿来用的。YOLOv8 的部署思路很成熟,官方提供了一整套模型导出功能,支持 ONNX、TensorRT、OpenVINO 等格式。在无人机边缘设备上部署时,最常见的是导出 ONNX 再转 TensorRT,在 Jetson 系列板卡上跑推理。导出命令极其简洁:

yolo export model=runs/detect/exp1/weights/best.pt format=onnx

导出后会生成一个best.onnx文件,尺寸大约几十 MB,精度损失可以忽略。再转 TensorRT 的话,推荐用trtexec工具或 ultralytics 自带的format=engine导出选项。实测在 Jetson Orin 系列设备上,YOLOv8s 的 TensorRT FP16 推理速度能做到 30 FPS 以上,满足实时无人机检测的需求。如果部署目标平台是瑞芯微 RK3588 这类国产芯片,转换流程会稍微复杂一些,但思路是一致的:先 ONNX 再转换成芯片平台自己的推理格式。

6.2 我在实战中的几个经验体会

最后把自己这段时间折腾 Visdrone 和 YOLOv8 的一些体会摆一摆。

Visdrone2019 数据集真的不适合拿来入门目标检测,它是为进阶玩家准备的材料。如果你是刚开始接触 YOLOv8,建议先用 COCO 数据集或者自己用手机拍的照片把基本流程跑通,再上 Visdrone 也不迟。但反过来,一旦你在 Visdrone 上能训练出不差的模型,说明你已经跨过了数据处理、参数调试、问题排查这几道槛,后面再碰任何数据集都会轻松得多。

我反复强调的“先跑通,再调优”思路,放到所有 AI 项目里都成立。第一次训练的目标不是拿高分,而是让整个链路顺畅:数据能读、模型能训、loss 能降、结果能出。哪怕第一次的 mAP 只有 0.2,也比调参卡壳一整天强。在这个基础之上,每个 epoch 的损失曲线、每个验证指标的变化,都会变成你调优的养料。

最后再分享一个小技巧。跑长训练的时候,给实验目录起名时把参数和日期写进去,比如visdrone_s_640_b8_20250101,一个月后回来看结果时,你绝对不会后悔当初多花了十秒写清楚名字。做实验最怕的不是失败,而是忘了当初怎么失败的。养成记录实验的习惯,跟写好代码注释一样重要,这对我来说是整个实操过程中最有价值的一件事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询