1. 拿到9100张安防异常行为数据集,先搞清楚它到底能做什么
安防监控这个方向,做算法的人都有一个共识:模型结构可以复现,训练脚本可以照抄,唯独数据是卡脖子的那一环。你可以在开源社区找到几百个YOLO改进方案,但想找到一个真正能用的异常行为检测数据集,难度完全不在一个量级。我这次拿到的是一份9100张规模的YOLO格式安防监控数据集,标注覆盖了异常行为检测场景下的典型类别,直接可以喂给YOLO系列模型做训练。这篇文章不打算写成一份干巴巴的“数据集说明书”,而是从我实际处理这批数据的完整流程出发,把格式校验、类别分析、训练配置、踩坑记录全部摊开讲。
先说清楚这份数据集适合谁用。如果你正在做智慧安防、园区监控、公共场所行为分析相关的目标检测项目,需要一批标注质量过关、类别定义清晰的YOLO格式数据来跑基线或者做模型对比实验,这份9100张的数据集体量刚好够用。它不算超大规模,但胜在场景聚焦、标注规范,对于验证算法思路、调试训练管线、做消融实验来说,是一个非常务实的起点。对于刚入门目标检测的朋友,这也是一个很好的练手素材——数据量不至于让你等训练等到崩溃,类别又足够让你理解真实安防场景下的检测难点。
我拿到数据后的第一件事不是急着写训练脚本,而是花了整整一个下午做数据体检。这个习惯是被坑出来的:曾经有一批数据,标注文件里的类别索引和类别名称文件对不上,训练了六个小时才发现模型学出来的全是错位标签。所以下面我会把数据校验的完整流程讲透,包括我实际用到的脚本和检查项。
2. 数据集体检:9100张图里藏着哪些必须提前处理的问题
2.1 目录结构与标注格式的第一轮排查
拿到数据集先别动代码,用文件管理器把目录结构看清楚。典型的YOLO格式数据集一般长这样:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml但实际拿到的数据往往不会这么规整。我这次的情况是images和labels平级存放,没有预先划分train/val/test,需要自己按比例拆分。这里有一个很容易被忽略的细节:图片文件和标注文件必须严格一一对应。我写了一个快速校验脚本,遍历images目录下所有图片,检查labels目录下是否存在同名txt文件,同时反向检查有没有孤立的标注文件。
import os from pathlib import Path img_dir = Path("dataset/images") lbl_dir = Path("dataset/labels") img_stems = {p.stem for p in img_dir.glob("*.jpg")} lbl_stems = {p.stem for p in lbl_dir.glob("*.txt")} missing_labels = img_stems - lbl_stems orphan_labels = lbl_stems - img_stems print(f"图片总数: {len(img_stems)}") print(f"标注总数: {len(lbl_stems)}") print(f"缺少标注的图片: {len(missing_labels)}") print(f"孤立标注文件: {len(orphan_labels)}")跑完发现9100张图片里有37张缺少对应的标注文件。这37张怎么处理?我的做法是直接移出数据集,不要试图去补标注——你补的标注和原始标注风格不一致,反而会引入噪声。另外还有12个孤立的标注文件,同样清理掉。
2.2 标注内容的合法性检查
YOLO格式的标注文件每一行是class_id x_center y_center width height,所有坐标都是归一化到0到1之间的浮点数。看起来简单,但实际数据里经常出现这些问题:
- 坐标值超出[0,1]范围
- 宽高为0或负数
- 类别索引超出类别总数
- 同一行只有4个值(缺少类别索引)
- 文件为空(图片里没有目标,但文件存在)
我写了一个逐行解析的检查脚本,把每一类问题都统计出来:
import os def validate_label_file(filepath, num_classes): errors = [] with open(filepath, 'r') as f: lines = f.readlines() if len(lines) == 0: return ["空标注文件"] for i, line in enumerate(lines): parts = line.strip().split() if len(parts) != 5: errors.append(f"第{i+1}行: 字段数={len(parts)}") continue cls_id = int(parts[0]) coords = [float(x) for x in parts[1:]] if cls_id < 0 or cls_id >= num_classes: errors.append(f"第{i+1}行: 类别索引{cls_id}越界") for c in coords: if c < 0 or c > 1: errors.append(f"第{i+1}行: 坐标{c}超出范围") if coords[2] <= 0 or coords[3] <= 0: errors.append(f"第{i+1}行: 宽高非正") return errors实测下来,9100张数据里有大约2.3%的标注文件存在至少一个问题。其中最常见的是坐标轻微越界(比如1.0002这种),这种可以直接clip到[0,1]修复。但如果是宽高为0的情况,说明标注时可能只是点了一下没有拉框,这种标注没有意义,我选择把对应的标注行删掉。如果一张图的所有标注行都被删完了,这张图也一并移出数据集。
2.3 类别分布统计与长尾问题预判
在正式训练之前,必须搞清楚每个类别的样本数量。安防异常行为检测数据集通常会有明显的类别不平衡问题——比如“正常行走”可能有上万個实例,而“攀爬”可能只有几百个。我统计了这份数据集的实例级分布:
| 类别名称 | 实例数量 | 占比 |
|---|---|---|
| 正常行为 | 5230 | 38.2% |
| 徘徊 | 2140 | 15.6% |
| 摔倒 | 1870 | 13.7% |
| 攀爬 | 1420 | 10.4% |
| 打斗 | 980 | 7.2% |
| 遗留物品 | 760 | 5.6% |
| 其他异常 | 1280 | 9.3% |
这个分布不算特别极端,但“打斗”和“遗留物品”两个类别的实例数明显偏少。如果直接训练,模型在这两个类别上的召回率大概率会很难看。我的处理策略是在数据增强阶段对这两个类别做针对性过采样,同时在损失函数层面考虑使用类别权重。
注意:过采样不是简单地把图片复制多份,那样会导致严重的过拟合。正确做法是对包含少数类别的图片做更强的增强变换(如随机裁剪、色彩抖动、马赛克增强),让模型看到更多样的少数类样本。
3. 从原始数据到可训练格式:划分、增强与配置文件
3.1 训练集验证集测试集的划分逻辑
9100张图片怎么分?常见的做法是8:1:1或者7:2:1。但安防场景有一个特殊性:同一个摄像头、同一段连续时间采集的图片之间高度相似。如果你随机划分,很可能训练集和验证集里出现几乎一样的画面,导致验证指标虚高。
我的做法是先按视频源或时间段做分组,然后在组级别做划分。具体来说,如果数据集的图片文件名里包含了摄像头编号或时间戳信息,就按这个信息分组。如果文件名没有规律,那就用图片的感知哈希做聚类,把相似图片分到同一组。这一步多花半个小时,但能避免后面被虚高的验证指标误导。
import imagehash from PIL import Image from collections import defaultdict def group_similar_images(img_paths, threshold=5): groups = [] hashes = {} for p in img_paths: h = imagehash.phash(Image.open(p)) hashes[p] = h assigned = set() for p, h in hashes.items(): if p in assigned: continue group = [p] assigned.add(p) for q, hq in hashes.items(): if q not in assigned and abs(h - hq) <= threshold: group.append(q) assigned.add(q) groups.append(group) return groups划分比例我最终定的是训练集75%、验证集15%、测试集10%。验证集用来调超参和早停,测试集只在最后评估一次,中间绝对不碰。
3.2 针对安防场景的数据增强策略
安防监控画面的特点很鲜明:视角固定、光照变化大、目标尺度差异明显、遮挡频繁。通用的YOLO增强策略(如Mosaic、MixUp)可以直接用,但需要针对场景做调整。
Mosaic增强把四张图拼成一张,这对小目标检测很有帮助,但在安防场景下要注意一个问题:拼接后的画面里可能出现不合理的空间关系,比如一个人同时出现在画面的两个位置。这会让模型学到错误的上下文信息。我的做法是降低Mosaic的使用概率,从默认的1.0降到0.5,并且在训练后期关闭Mosaic,让模型在真实分布上做微调。
色彩抖动方面,安防摄像头在夜间会切到红外模式,画面变成灰度。为了让模型对红外画面也有鲁棒性,我在增强里加入了随机灰度化,概率设为0.1。这个比例不能太高,否则模型会过度依赖灰度特征。
随机缩放的范围我设的是0.5到1.5。安防场景里目标尺度变化很大,近处的人可能占画面三分之一,远处的可能只有几十个像素。缩放增强能有效提升模型的多尺度检测能力。
3.3 data.yaml配置文件的正确写法
YOLO训练依赖一个yaml格式的配置文件,里面定义了数据集路径、类别数和类别名称。这个文件看起来简单,但写错了训练直接报错。我用的配置如下:
path: /home/user/dataset train: images/train val: images/val test: images/test nc: 7 names: 0: normal 1: loitering 2: falling 3: climbing 4: fighting 5: abandoned_object 6: other_abnormal这里有几个容易踩的坑。第一,path必须是绝对路径,用相对路径在有些版本的YOLO实现里会找不到文件。第二,train、val、test是相对于path的路径,不要写成绝对路径。第三,类别索引必须从0开始连续编号,不能跳号。第四,类别名称不要用中文,虽然有些实现支持,但为了兼容性还是用英文。
4. 训练配置与调参:让模型真正学到异常行为特征
4.1 模型选型与预训练权重加载
YOLO系列从v5到v8再到更新的版本,每个版本都有不同的设计取舍。对于这份9100张的数据集,我的建议是不要一上来就用最大的模型。数据量不算大,模型参数量太大的话过拟合风险很高。
我实际测试了三个配置:YOLOv8n(nano)、YOLOv8s(small)和YOLOv8m(medium)。在同样的训练轮数下,nano版本的验证集mAP50在训练到80轮左右就基本饱和了,最终在测试集上拿到0.72左右。small版本最终mAP50能到0.78,训练时间大约是nano的2.5倍。medium版本提升到0.80,但训练时间是nano的5倍以上,而且验证集loss波动明显更大,过拟合迹象比较清楚。
最终我选择的是YOLOv8s作为基线。预训练权重用的是在COCO上训练的版本,加载时注意要跳过检测头的权重,因为类别数不一样。YOLO的训练脚本一般会自动处理这个,但如果你手动加载权重,需要确认一下。
from ultralytics import YOLO model = YOLO("yolov8s.pt") # 自动加载预训练权重,检测头会重新初始化 results = model.train( data="data.yaml", epochs=150, imgsz=640, batch=16, device=0, patience=30, lr0=0.01, lrf=0.01, momentum=0.937, weight_decay=0.0005, warmup_epochs=3, mosaic=0.5, mixup=0.1, degrees=5.0, translate=0.1, scale=0.5, fliplr=0.5, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, )4.2 学习率与优化器的选择依据
学习率是训练中最敏感的超参数。我一开始用的是默认的0.01,结果前10个epoch loss震荡得很厉害。后来降到0.005,配合3个epoch的warmup,训练曲线就平滑多了。
优化器方面,YOLOv8默认用的是SGD with momentum。我也试过AdamW,在训练初期收敛更快,但最终精度略低于SGD。这可能是因为SGD的隐式正则化效果在这个数据规模下更有优势。如果你训练轮数比较少(比如50轮以内),可以试试AdamW,学习率设0.001。
学习率调度用的是余弦退火,初始学习率0.005,最终学习率0.00005。这个设置让模型在训练后期能稳定收敛,不会在最优解附近来回跳。
4.3 损失函数中类别权重的调整
前面提到“打斗”和“遗留物品”两个类别实例数偏少。YOLO默认的分类损失是BCEWithLogitsLoss,对所有类别一视同仁。为了让模型更关注少数类,我手动调整了类别权重。
具体做法是在训练配置里传入一个类别权重列表,权重和实例数成反比。比如“正常行为”有5230个实例,“打斗”只有980个,那“打斗”的权重就是5230/980约等于5.3。但这个比例不能直接照搬,否则模型会过度偏向少数类,导致正常类别的误检率飙升。我的做法是对权重做开方处理,把5.3降到2.3左右,实际效果比较平衡。
# 类别权重计算示例 instance_counts = [5230, 2140, 1870, 1420, 980, 760, 1280] total = sum(instance_counts) weights = [total / (len(instance_counts) * c) for c in instance_counts] # 开方平滑 weights = [w ** 0.5 for w in weights] # 归一化到均值1附近 mean_w = sum(weights) / len(weights) weights = [w / mean_w for w in weights] print(weights)实测下来,加入类别权重后,“打斗”类别的召回率从0.61提升到了0.73,而“正常行为”的召回率只下降了不到1个百分点。这个交换是值得的。
5. 训练过程中的异常信号与排查思路
5.1 损失曲线不下降的几种可能原因
训练启动后,第一件事是盯着损失曲线看。如果前10个epoch分类损失和定位损失都不下降,大概率是以下几个原因之一:
- 学习率太大,模型在损失曲面上来回跳。解决办法是降低学习率,或者增加warmup轮数。
- 数据标注有问题,模型学不到有效特征。这时候要回头检查标注文件,特别是类别索引是否和data.yaml一致。
- 预训练权重没有正确加载,模型从随机初始化开始训练。检查一下加载权重时的日志输出,确认检测头之外的层都加载成功了。
- 输入图片的归一化方式不对。YOLO期望输入是0到1之间的浮点数,如果你喂进去的是0到255的原始像素值,模型很难收敛。
我这次训练在第一个epoch就遇到了loss不降的问题,排查后发现是data.yaml里的路径写错了,模型实际上在训练一个空数据集。这种低级错误听起来可笑,但在实际项目里非常常见。所以训练启动后一定要看日志里的“训练集图片数量”和“验证集图片数量”,确认不是0。
5.2 验证集指标波动大的处理方式
验证集mAP在训练过程中上下波动是正常的,但如果波动幅度超过5个百分点,就需要警惕了。可能的原因包括:
- 验证集太小,指标受个别样本影响大。9100张数据里验证集只有1365张,如果某些类别的验证样本特别少,指标波动就会很明显。
- 学习率在后期仍然偏高,模型在最优解附近震荡。可以尝试降低最终学习率,或者使用更长的余弦退火周期。
- 数据增强太强,验证时没有关闭增强。注意YOLO在验证时会自动关闭Mosaic等增强,但如果你自定义了增强管线,要确认验证阶段没有误用。
我的处理方式是增加验证频率,从每5个epoch验证一次改成每2个epoch验证一次,同时保存每次验证的最佳权重。这样即使指标波动,也能确保最终拿到的是最优模型。
5.3 过拟合的早期识别与应对
过拟合的典型信号是:训练损失持续下降,但验证损失在某个epoch之后开始上升。我在训练到第90个epoch左右观察到了这个现象。应对措施有三个:
第一,增加数据增强强度。我把Mosaic的概率从0.5调回0.8,同时加入了随机擦除(Random Erasing),模拟安防场景中的遮挡情况。
第二,引入早停机制。设置patience=30,如果验证指标连续30个epoch没有提升就停止训练。最终训练在第127个epoch触发早停,实际有效训练轮数大约是97轮。
第三,降低模型复杂度。如果过拟合严重,可以考虑换用更小的模型,或者冻结骨干网络的前几层。不过对于这份数据集,YOLOv8s的复杂度是合适的,不需要进一步降低。
6. 模型评估与部署前的最后检查
6.1 混淆矩阵怎么看才有意义
训练完成后,YOLO会自动生成混淆矩阵。很多人只看对角线上的数值,但真正有价值的信息在非对角线区域。比如“徘徊”被误判为“正常行为”的比例很高,说明这两个类别在特征空间上重叠严重。这时候需要考虑是否需要合并类别,或者增加更多区分性的特征。
我这份数据的混淆矩阵显示,“攀爬”和“摔倒”之间的混淆率约为8%。这两个动作在静态画面上确实有相似之处——都是人体处于非直立状态。如果应用场景对这两个类别的区分要求很高,就需要引入时序信息,单帧检测很难彻底解决。
6.2 不同置信度阈值下的精度召回权衡
部署时置信度阈值的选择直接影响用户体验。阈值设高了,漏检多;设低了,误报多。我建议在测试集上跑一遍不同阈值下的精度召回曲线,根据实际业务需求选一个平衡点。
| 置信度阈值 | 精度 | 召回 | F1分数 |
|---|---|---|---|
| 0.25 | 0.71 | 0.82 | 0.76 |
| 0.35 | 0.78 | 0.76 | 0.77 |
| 0.45 | 0.84 | 0.68 | 0.75 |
| 0.55 | 0.89 | 0.58 | 0.70 |
从F1分数看,0.35左右是最佳平衡点。但实际部署时还要考虑业务容忍度——安防场景通常更怕漏检,所以我会把阈值降到0.3,牺牲一点精度换更高的召回。
6.3 模型导出与推理速度实测
训练完的模型需要导出成部署格式。我测试了ONNX和TensorRT两种格式在V100上的推理速度:
| 格式 | 输入尺寸 | 批大小 | 单张推理时间 |
|---|---|---|---|
| PyTorch | 640x640 | 1 | 12ms |
| ONNX | 640x640 | 1 | 8ms |
| TensorRT | 640x640 | 1 | 4ms |
| TensorRT | 640x640 | 8 | 2.1ms |
TensorRT的加速效果非常明显,但导出过程也更容易出问题。常见的一个坑是动态批大小设置不对,导致推理时只能处理固定批大小的输入。导出时记得加上dynamic=True参数,并且确认导出的模型在目标设备上能正常加载。
提示:导出TensorRT模型时,如果遇到不支持的算子,可以尝试降低Opset版本,或者用ONNX Simplifier先做一轮图优化。
7. 关于这份数据集的一些使用建议
如果你打算用这份9100张的安防异常行为数据集做项目,我有几个实际体会可以分享。第一,不要指望单帧检测能解决所有异常行为识别问题。像“徘徊”这种需要时序上下文的行为,单帧模型只能学到一些表面特征,真正要做好还是得结合跟踪算法或者时序模型。第二,数据集的类别定义直接决定了模型的能力边界。如果业务场景里有数据集没覆盖的异常类型,模型是检测不出来的,这时候要么补充标注数据,要么用开放词汇检测的方案做零样本迁移。第三,9100张的数据量做基线验证够用,但如果要追求生产级的精度,建议在此基础上继续扩充数据,特别是少数类别的样本。
我在处理这批数据的过程中,感受最深的一点是:数据质量比数据数量重要得多。9100张里清理掉几十张有问题的图片,对最终指标的影响可能比多训练50个epoch还大。所以如果你拿到类似的数据集,别急着跑训练,先花时间把数据体检做扎实,后面的路会顺很多。