简介:这份资源是面向溺水检测场景的YOLO系列目标检测数据集,适合计算机视觉初学者、算法工程师及安全监控方向研究者使用,可用于训练和验证溺水、出水、游泳等水上行为的识别模型。压缩包共1018个文件,包含339张jpg图像、339个txt标签、339个xml标签以及1个yaml配置文件,整体约14.6MB,已按训练与验证需求划分完毕。标签同时提供YOLO格式与VOC格式两套:YOLO格式以类别索引和归一化中心点、宽高比例记录目标框,VOC格式则以xml结构保存,方便直接接入yolov5、yolov8、yolov9、yolov7、yolov10及yolo11等算法。目前已有303人学习下载,读者可快速获得一份开箱即用的水上安全检测数据,省去自行标注与格式转换的环节,将精力集中在模型选型、训练调参与效果对比上,也便于复现和扩展溺水预警相关实验。
1. 339 张溺水图像数据集:小样本 YOLO 训练到底能不能落地
溺水检测这个方向,真正卡住大多数团队的从来不是模型结构,而是数据。公开可用的溺水图像本身就少,标注质量参差不齐,涉及泳池、河道、海边等不同水域场景时,光照、水面反光、人体姿态差异极大。我拿到「339 张图像带标签」这个量级时,第一反应是:这数据量放在常规目标检测任务里连热身都不够,但放在溺水出水、游泳姿态识别这种垂直场景里,如果标注干净、类别定义清晰,反而有可能跑出一个能用的基线。
这篇文章要解决的问题很具体:手里有一份 339 张带标签的溺水/游泳图像数据集,想用 YOLO 训练一个能识别溺水出水状态的检测器,该怎么配环境、怎么划分数据、参数怎么设、训练中 BN 崩溃和混淆矩阵异常怎么排查。适合已经跑通过一次 YOLO 官方 demo、但没在小样本垂直数据集上踩过坑的工程师,也适合想评估这个方向值不值得投入的技术负责人。我不会假装见过这份数据的原始压缩包,所有步骤都按「339 张带标签图像」这个前提给出可复现路径,你按自己的实际目录结构替换即可。
2. 从 339 张图到可训练数据集:划分、增强与标签校验
2.1 小样本下为什么不能随便按 8:2 切
339 张图像如果按常见的 8:2 划分,验证集只有 67 张左右。问题在于溺水场景的图像往往成组出现——同一段视频抽帧出来的连续画面,人物姿态、光照、背景几乎一致。如果随机切分,训练集和验证集里会混入高度相似的帧,验证指标虚高,实际部署时换一个泳池就翻车。这是小样本目标检测里最隐蔽的坑之一。
我一般会先做一次「来源分组」:把同一视频、同一拍摄时段、同一水域的图像归为一组,然后按组划分,而不是按单张图像随机划分。339 张如果来自 20 到 30 个来源组,按 7:2:1 分到训练/验证/测试,每组整体进同一个集合。这样验证集指标才有参考价值。如果数据来源无法追溯,退而求其次的做法是按图像感知哈希做聚类,把相似度高的图聚成组再切。
import os import shutil import random from collections import defaultdict # 假设文件名前缀代表来源组,例如 "pool_A_001.jpg" # 实际使用时按你的命名规则替换 group_key 的提取逻辑 def group_key(filename): return filename.split('_')[0] + '_' + filename.split('_')[1] def split_dataset(img_dir, label_dir, out_dir, ratios=(0.7, 0.2, 0.1)): groups = defaultdict(list) for f in os.listdir(img_dir): if f.lower().endswith(('.jpg', '.png', '.jpeg')): groups[group_key(f)].append(f) group_list = list(groups.keys()) random.seed(42) random.shuffle(group_list) n = len(group_list) n_train = int(n * ratios[0]) n_val = int(n * ratios[1]) train_groups = group_list[:n_train] val_groups = group_list[n_train:n_train + n_val] test_groups = group_list[n_train + n_val:] for split_name, gs in [('train', train_groups), ('val', val_groups), ('test', test_groups)]: for g in gs: for f in groups[g]: stem = os.path.splitext(f)[0] shutil.copy(os.path.join(img_dir, f), os.path.join(out_dir, 'images', split_name, f)) lbl = stem + '.txt' src_lbl = os.path.join(label_dir, lbl) if os.path.exists(src_lbl): shutil.copy(src_lbl, os.path.join(out_dir, 'labels', split_name, lbl)) split_dataset('./images', './labels', './dataset')这段脚本的核心逻辑是「先分组、再切分」,group_key函数需要你根据实际文件名规则调整。random.seed(42)保证可复现,ratios参数在 339 张这种量级下建议保持 7:2:1,不要为了凑验证集数量改成 6:4,那会进一步压缩本就不多的训练样本。输出目录结构直接对齐 YOLO 官方要求的images/train、labels/train格式,省去后面再转换。
2.2 标签格式校验:别让一个越界框毁掉整个训练
YOLO 的标签格式是class_id x_center y_center width height,全部归一化到 0 到 1。339 张图像手工标注或半自动标注,出现越界坐标、宽高为负、类别号超出范围的概率不低。这些脏标签在训练初期不会报错,但会导致损失函数震荡、BN 层统计量异常,甚至训练到一半突然崩溃。我习惯在训练前跑一遍校验脚本,把问题标签直接列出来。
import os def validate_labels(label_dir, num_classes): issues = [] for f in os.listdir(label_dir): if not f.endswith('.txt'): continue path = os.path.join(label_dir, f) with open(path) as fh: for i, line in enumerate(fh): parts = line.strip().split() if len(parts) != 5: issues.append((f, i, '字段数不为5')) continue cls, x, y, w, h = parts cls = int(cls) x, y, w, h = map(float, (x, y, w, h)) if cls < 0 or cls >= num_classes: issues.append((f, i, f'类别号越界: {cls}')) if not (0 <= x <= 1 and 0 <= y <= 1): issues.append((f, i, f'中心点越界: {x},{y}')) if w <= 0 or h <= 0 or w > 1 or h > 1: issues.append((f, i, f'宽高异常: {w},{h}')) return issues issues = validate_labels('./dataset/labels/train', num_classes=2) for it in issues[:20]: print(it) print(f'共发现 {len(issues)} 处问题')num_classes按你的类别数填,溺水出水、游泳两类就填 2。校验结果里如果出现大量宽高异常,说明标注工具导出时可能用了像素坐标没归一化,需要回退到标注环节重新导出。这一步花十分钟,能省掉后面几小时的排查。
2.3 小样本增强:哪些增强有用,哪些是负优化
339 张图像做增强,思路和常规数据集不同。常规做法是 mosaic、mixup、随机缩放、色彩抖动全开,但在溺水场景里,水面反光和人体姿态是核心判别特征,过度色彩抖动会让水面纹理失真,mixup 把两个溺水目标叠在一起会产生不存在的语义。我的经验是:几何增强保留,色彩增强减半,mixup 直接关掉。
具体配置上,YOLOv8 的degrees可以设到 10 左右,translate设 0.1,scale设 0.5,flipud和fliplr各 0.5。hsv_h降到 0.01,hsv_s和hsv_v各 0.4 左右。mosaic保持 1.0 但配合close_mosaic在最后 10 个 epoch 关闭,让模型在训练末期看到真实分布。这些参数不是拍脑袋,是小样本垂直场景下反复试出来的平衡点。
3. YOLO 训练配置:从环境搭建到损失函数收敛
3.1 环境搭建与预训练模型选择
环境这块,CUDA 版本和 PyTorch 版本对不上是新手最常见的翻车点。我一般用 conda 建独立环境,Python 3.10 配 PyTorch 2.x,CUDA 11.8 或 12.1 都行,关键是torch.cuda.is_available()要返回 True。ultralytics 包直接 pip 装最新稳定版即可,不用追 nightly。
预训练模型的选择直接影响小样本训练的收敛速度。339 张图从零训练基本没戏,必须用 COCO 预训练权重。YOLOv8n 或 YOLOv8s 是合理起点,n 参数量小、过拟合风险低,s 精度略高但需要更多数据支撑。我的建议是先跑 YOLOv8n 拿到基线,如果验证集 mAP 能到 0.5 以上再考虑换 s。更大的 m 或 l 在 339 张图上几乎必然过拟合,除非你做大量冻结层微调。
conda create -n drowning python=3.10 -y conda activate drowning pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics yolo checksyolo checks会输出环境自检结果,重点看 CUDA 是否可用、显存大小、ultralytics 版本。如果显存小于 8GB,训练时batch要降到 8 或 16,配合amp=True混合精度。V100 这类 16GB 以上的卡可以上 batch 32,但小样本下 batch 太大反而梯度更新次数少,339 张图一个 epoch 才十几次迭代,batch 设 16 比较均衡。
3.2 数据配置文件与训练命令
YOLO 需要一个 YAML 描述数据路径和类别。这个文件写错路径是另一个高频翻车点,path用绝对路径最稳,train和val相对path写。
# drowning.yaml path: /home/user/drowning_dataset train: images/train val: images/val test: images/test names: 0: drowning 1: swimming类别名要和你的标注类别号严格对应。如果标注时溺水是 0、游泳是 1,这里就不能反。训练命令如下:
yolo detect train \ data=drowning.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.001 \ lrf=0.01 \ warmup_epochs=5 \ patience=30 \ close_mosaic=10 \ amp=True \ project=runs/drowning \ name=exp1lr0=0.001比默认的 0.01 低一个量级,是小样本微调的常规做法,避免预训练权重被大梯度冲垮。patience=30表示 30 个 epoch 验证指标不提升就早停,339 张图通常 80 到 120 个 epoch 就收敛了。close_mosaic=10在最后 10 个 epoch 关掉 mosaic,让模型适应真实图像分布。warmup_epochs=5让学习率从极小值线性爬升,防止训练初期 BN 统计量被异常 batch 带偏。
3.3 损失函数三项到底在看什么
YOLO 的损失由三部分组成:边界框回归损失、分类损失、目标置信度损失。小样本训练时,这三项的收敛曲线能直接反映问题。边界框损失下降慢,说明标注框质量差或增强过度导致目标形变;分类损失震荡,说明类别定义模糊,比如溺水出水和游泳的边界在标注时没统一;置信度损失居高不下,往往是背景样本太多或前景目标太小。
我习惯在训练时盯着box_loss、cls_loss、dfl_loss三条曲线。正常情况下三者同步下降,cls_loss略高于box_loss。如果cls_loss突然飙升,先检查标签里有没有类别号写错。如果box_loss降到很低但 mAP 不涨,多半是过拟合,验证集和训练集分布不一致。这些判断不需要改代码,看 ultralytics 输出的 results.csv 就行。
4. 训练中的 BN 崩溃与混淆矩阵异常排查
4.1 BN 崩溃:现象、原因与三种解法
BN 崩溃的典型现象是训练到某个 epoch 突然报NaN损失,或者验证 mAP 直接掉到 0,日志里出现AssertionError: Torch not compiled with CUDA enabled之外的数值异常。根因通常是某个 batch 的统计量方差接近零,除零后梯度爆炸。小样本 + 小 batch 是 BN 崩溃的高发组合,因为 batch 内样本太少,统计量估计不稳。
解法一:把batch提到 16 以上,让 BN 有足够样本估计均值和方差。解法二:改用SyncBN或冻结 BN 层,在 YOLOv8 里可以通过设置freeze参数冻结 backbone 的前若干层。解法三:降低学习率并加梯度裁剪,lr0降到 0.0005,同时在训练配置里开clip_grad=10.0。我一般先试提 batch,不行再冻结 BN,最后才动学习率,因为改学习率会影响整体收敛节奏。
4.2 混淆矩阵总合不唯一是怎么回事
有读者反馈混淆矩阵的行列总和对不上,怀疑代码有 bug。这其实不是 bug,是 YOLO 验证阶段的多重匹配机制导致的。一个预测框可能同时匹配到多个真实框,或者一个真实框被多个预测框匹配,混淆矩阵在统计时按匹配对累加,总合自然不等于图像总数。判断模型好坏要看对角线占比和各类的召回率,不要纠结总合数字。
如果发现溺水类被大量预测成游泳类,先看两类在标注时的定义是否清晰。溺水出水往往只露出头部和部分手臂,游泳则有完整的身体姿态,如果标注时把「水中挣扎」也标成游泳,模型学到的边界就是模糊的。这种情况回退到标注环节重新定义类别,比调模型参数有效得多。
4.3 验证指标虚高:过拟合的五个信号
339 张图训练,过拟合几乎是必然要面对的。五个信号:训练损失持续下降但验证损失在某个 epoch 后回升;训练 mAP 到 0.95 以上而验证 mAP 卡在 0.6;验证集预测框大量重叠在同一目标上;模型对训练集图像置信度普遍 0.9 以上,对验证集图像置信度集中在 0.3 到 0.5;混淆矩阵里背景被大量误检为前景。
应对手段按优先级:先加数据增强的多样性,尤其是几何变换;再降模型容量,从 s 换回 n;然后加权重衰减weight_decay=0.0005;最后考虑冻结 backbone 只训练 head。如果这些都不行,说明 339 张图对这个场景确实不够,需要考虑半监督或主动学习补充数据。
5. 小样本溺水检测的进阶技巧:从基线到可用
5.1 用测试时增强把 mAP 再抬几个点
训练完之后,推理阶段还有提升空间。测试时增强(TTA)对溺水这种目标姿态多变的场景特别有效,因为不同尺度和翻转下的预测可以互补。ultralytics 里开 TTA 很简单:
yolo detect val \ model=runs/drowning/exp1/weights/best.pt \ data=drowning.yaml \ augment=True \ imgsz=640augment=True会同时做多尺度、翻转的推理再融合。代价是推理速度降到原来的三分之一左右,如果部署在边缘设备上要权衡。我在 RK3588 这类边缘板上一般不开 TTA,在服务端 GPU 推理时开,mAP 通常能涨 2 到 4 个点。
5.2 阈值调优:溺水检测宁可误报不可漏报
溺水检测的业务逻辑和常规目标检测不同,漏报的代价远大于误报。默认置信度阈值 0.25 在这个场景下偏低,会引入大量水面反光误检。我的做法是把置信度阈值提到 0.4 到 0.5,同时把 NMS 的 IoU 阈值从 0.7 降到 0.5,减少重叠框。然后在验证集上画 PR 曲线,找到召回率 0.9 对应的精度,那个点才是业务可用的工作点。
| 参数 | 默认值 | 溺水场景建议值 | 影响 |
|---|---|---|---|
| conf | 0.25 | 0.4~0.5 | 提高减少误报,过高漏报增加 |
| iou | 0.7 | 0.5 | 降低减少重叠框 |
| imgsz | 640 | 640~960 | 提高小目标召回,显存换精度 |
| max_det | 300 | 50 | 溺水场景目标少,降低提速 |
5.3 我踩过的最大一个坑
最后说一个血泪教训。我早期做溺水检测时,验证集 mAP 跑到 0.78,兴冲冲部署到泳池摄像头,结果误检率高达每小时几十次,全是水面波纹和漂浮物。回头查才发现,验证集和训练集来自同一批视频,模型学到的是「这个泳池的水面纹理」而不是「溺水的人体姿态」。后来我把验证集换成完全没见过的水域场景,mAP 掉到 0.52,但那个数字才是真实的。
这件事之后我养成了一个习惯:任何垂直场景的小样本数据集,先花时间确认验证集的独立性,再谈模型指标。339 张图不多,但如果来源组划分干净、增强策略克制、阈值按业务调,跑出一个能用的溺水检测基线是完全可行的。值不值得做,取决于你能不能持续补充新场景的数据,而不是这 339 张本身。希望帮到你。
本文还有配套的精品资源,点击获取