去年做智慧工地项目的时候,客户那边的安全主管给我看了段监控录像:几个工人蹲在塔吊阴影下抽烟,旁边就是材料堆放区。画面里并没有发生什么大事,但主管说,如果当时系统能识别出"人员聚集+抽烟"这个组合动作,就能提前干预,避免上个月差点发生的火灾。问题在于,市面上现成的异常行为数据集要么太"实验室",要么只覆盖商店偷盗场景,拿到工地、校园、园区这类真实安防场景里,误报率高到没法用。
所以后来我干脆自己做了一套面向安防监控场景的异常行为检测数据集,总共9100张图像,全部用YOLO格式标注完成。这篇博文就把我构建这套数据集的全过程、踩过的坑、以及基于它训练YOLO模型的心得完整记录下来,希望能给正在做安防监控、行为识别、或者准备自己造数据的同学一些参考。
1. 为什么我会自己动手做一套异常行为数据集
1.1 公开数据集的三个尴尬
先说公开数据集。UCF-Crime、ShanghaiTech这些学术数据集在论文里很漂亮,但拿到真实安防项目里直接训练,基本会撞上三堵墙:
第一堵墙是场景错位。学术数据集里的"异常"多是从电影、新闻、或者固定机位的实验室场景中截取的,视角往往偏高或偏正,而真实安防摄像头大多是屋檐下斜俯视、画面里有树影摇晃、有车辆穿行、有光线忽明忽暗。用这些数据训练出来的模型,放到新场景里第一个月你会发现它把"风吹树叶大幅摆动"都当成异常来报警。
第二堵墙是标注格式各搞一套。有的数据集只给视频级标签(整个视频是不是异常),有的给帧级标签,有的给边界框但用的是类似COCO的JSON格式。做工程项目时,模型大多需要落到YOLO系(v5/v8),还得手动写一堆转换脚本,转完还得抽检标注质量,一来一回,两周时间就没了。
第三堵墙是负样本太少。学术数据集里"正常行为"的占比往往很低,因为它们的研究目的是做"异常检测",不需要太多正常帧。但真实项目里我们需要模型既能识别异常,又能在大量正常行为中不误报。正负样本比例失衡直接导致的后果就是:模型上线后,全天误报几百次,安保人员直接把系统关了。
1.2 9100张这个规模是怎么定下来的
定9100张这个数量,绝不是拍脑袋。我当时的逻辑是这样推的:
目标场景有工地、校园、园区周界、养老院公共区四类,每类场景至少要保证有足够的异常类别激活。计划覆盖15类异常行为加1类正常行为(后续会讲类别细节)。如果要让YOLO系模型学到"同一类行为在不同角度、光照、遮挡情况下的泛化特征",每个类别至少要300张以上的有效标注图。16个类别合计就需要大约4800~5000张。但考虑到部分图像里会同时出现多个目标、多个异常行为(比如一群人打架且有人在破坏公物),实际每张图能提供2~3个有效标注,所以9100张图的规模可以产出大约1.5万~1.6万个目标框,能满足一个中等规模工程项目的前期数据需求。
另外,9100张这个量级对标注成本和训练效率也是友好的——人工精标大概需要8到10人日,单张V100上训练YOLOv8m大约3到4小时一轮,迭代试错成本完全可控。再往上加,比如到3万张,性能提升可能有限,但标注和算力成本会翻倍,项目前期完全没必要。
2. 数据集的组成与标注规范:9100张图到底覆盖了什么
2.1 15+1的类别体系
做安防异常行为检测,类别设计不能太"算法竞赛"——不能搞一堆学术上很酷但实际项目用不上的行为,得先问一句:客户在现场到底关心什么?我最终定下来的是15类异常行为加1类正常行为,表格如下:
| 编号 | 类别名称 | 典型场景举例 | 标注难度 | 大概样本数 |
|---|---|---|---|---|
| 0 | person_normal | 正常行走、站立、坐着、打电话 | 低 | 2800 |
| 1 | fight | 两人及以上互相推搡、击打 | 中 | 500 |
| 2 | robbery | 抢夺物品、强行拉扯 | 高 | 280 |
| 3 | fallen_person | 跌倒、倒地不起 | 中 | 420 |
| 4 | crowd_aggregation | 人群异常聚集、围观 | 中 | 360 |
| 5 | running | 非运动场合的快速奔跑 | 低 | 480 |
| 6 | smoke_visible | 可见烟雾(含抽烟产生的烟雾) | 高 | 540 |
| 7 | fire_visible | 明火 | 高 | 260 |
| 8 | vandalism | 破坏公物、踢打门窗/器材 | 高 | 310 |
| 9 | fence_climbing | 翻越围栏、围墙 | 中 | 290 |
| 10 | vehicle_illegal_park | 违规停车、堵塞通道 | 中 | 320 |
| 11 | intrusion | 闯入警戒区域(周界) | 中 | 450 |
| 12 | weapon_carried | 携带刀具等疑似危险物品 | 高 | 240 |
| 13 | throwing_object | 高空抛物、投掷物品 | 高 | 180 |
| 14 | climbing_height | 攀爬高处、塔吊、窗台 | 中 | 220 |
| 15 | slouching_sleep | 在工作/值守岗位睡觉或长时间姿态异常 | 低 | 260 |
这里有个细节想提醒大家:把"正常行为"单独作为一类放进数据集,看起来占了类别体系的一个坑位,但在实际项目里极其重要。因为安防场景中正常行为是绝大多数,模型如果只学异常正样本,推理时会把所有没见过的情况都判为异常,误报爆炸。加入person_normal且数量给到2800张,本质上是在给模型建立"正常基线"。
2.2 YOLO格式的标注细节
整个数据集全部采用YOLO格式的TXT标注文件,每张图对应一个同名的.txt文件,文件里的每一行代表一个目标框,格式是:
class_id x_center y_center width height注意,这里全部是归一化坐标,取值范围0到1。举个例子,一张分辨率为1920x1080的图,假设一个人跌倒后的边界框左上角是(350, 420)、右下角是(580, 900),那么对应的归一化结果是:
- x_center = (350 + 580) / 2 / 1920 ≈ 0.242
- y_center = (420 + 900) / 2 / 1080 ≈ 0.611
- width = (580 - 350) / 1920 ≈ 0.120
- height = (900 - 420) / 1080 ≈ 0.444
对应标注行就是:
3 0.242 0.611 0.120 0.444类别的编号必须严格跟数据集根目录下的配置文件对齐。我是用YOLOv8的命名规范做的,比如:
# dataset.yaml path: ./anomaly_dataset train: images/train val: images/val test: images/test names: 0: person_normal 1: fight 2: robbery 3: fallen_person 4: crowd_aggregation 5: running 6: smoke_visible 7: fire_visible 8: vandalism 9: fence_climbing 10: vehicle_illegal_park 11: intrusion 12: weapon_carried 13: throwing_object 14: climbing_height 15: slouching_sleep很多新手喜欢在标注软件里直接导出,然后路径不对、类名对不上、索引偏移,训练时报错"class 12 out of bounds"还不知道怎么回事。所以我在做数据集的第一天就写死了这套映射,后面任何人都不能随便改类别顺序。
2.3 图像来源与场景分布
9100张图像的构成,按来源主要分三块:
- 公开可用的监控类开源视频片段(占比约45%):从一些开放数据集中抽取关键帧,覆盖白天、黑夜、黄昏、雨雾等不同光照条件
- 自采集场景模拟(占比约40%):在允许的场地内,请同事配合演示打架、跌倒、攀爬、抛掷物品等动作,用手机和摄像头按监控视角拍摄后抽帧
- 网络公开的安防新闻/案例图像(占比约15%):从正规新闻媒体、官方通报中采集的异常事件现场图,仅作补充场景多样性
说实话,自采集这部分是最耗体力的。为了模拟真实监控视角,我不会站着平拍,而是把设备架高、略带俯角,有时甚至隔着树叶、栏杆去拍,故意制造遮挡。因为真实安防摄像头就是这样——它不可能给你一个完美的正视角。
场景分布上我也做了刻意控制:避免某一类异常行为只在单一背景出现。比如fight(打架)这个类别,我要求必须覆盖工地泥地、校园操场、园区广场、室内走廊4种子场景,否则模型很容易学到"背景=打架",换个地砖颜色就失灵。
3. 数据质量把控:标注流程、审核机制与踩坑记录
3.1 标注团队的搭建和培训
很多个人开发者习惯自己一张张画框,但9100张图、1.5万个框,一个人标到崩溃不说,标准也很难前后一致。我的做法是找了两名有标注经验的兼职,加上我自己,组成三人小团队,先花一上午统一标准。
统一标准最关键的不是"怎么画框",而是"边界情况怎么处理"。我整理了一份标注问答清单,挑几条最典型的:
- 目标被大面积遮挡,只露出30%以下的身体部位,是否标注?答:不标。
- 两个人扭打在一起,边界框重叠超过70%,怎么处理?答:各标各的,允许重叠,但框要贴合各自身体主体,不能合并成一个"大框"。
- 远处小目标,比如画面里50x20像素的人,要不要标?答:标,但要保证框中心大致准确。因为安防场景大量目标就是小目标,不标会削弱模型的小目标检测能力。
- 同一画面里既有正常走路的人,又有一个人在打架。正常走路的人要不要标?答:要标,标为person_normal。模型需要学会区分同一场景下的正常和异常目标。
这些问题看似琐碎,其实直接决定数据集的可用性。如果标注标准不统一,模型学到的边界就是模糊的,后期调参怎么调都别扭。
3.2 交叉审核与抽检机制
标注完成之后不能直接开训。我设计了两层检查:
第一层是逐张快速筛查。每张标注完的图,用标注可视化工具把框画出来,按目录快速浏览一遍。主要看有没有漏标、框有没有严重偏离目标、类别有没有明显标错。这一层通常能筛掉大概5%的低质量样本。
第二层是抽检统计。对每个类别随机抽取20%的图片,统计标注框面积分布。这里有个很容易踩的坑:如果某个类别的框面积普遍偏大(比如占图70%以上),说明标注员可能把"整片打斗区域"画成了一个框,而不是"打斗的两个人各自一个框"。这种框会让YOLO学到一个巨大的、语义不准确的框,推理时根本没法用。发现问题后,我会把该类别全部返工重新画。
3.3 我踩过的几个标注坑
这里必须吐槽一下我自己的失误,给后来的同学提个醒。
第一个坑是同源帧泄漏。我从开源视频里抽关键帧时,图方便连续抽了同一段视频的50帧。结果训练集和验证集都被分到了这50帧里的不同帧,模型在验证集上性能虚高得离谱。训练mAP 0.85,验证mAP居然也有0.84,我还开心了一阵子。后来发现同源帧泄漏后,把验证集里所有同源视频的帧全部剔除,重新评估,mAP直接掉到0.68。这件事给我的教训是:划分训练/验证集时,必须按"视频源"分,而不是按"单帧"分。不然你测出来的指标根本代表不了真实场景。
第二个坑是类别相似度造成的标注歧义。violent行为里,"fight"和"robbery"看起来很像——都是两个人纠缠在一起。区别在于robbery要有"抢夺物品"的语义,而fight重点是"击打对方身体"。同一段素材,我标成fight,标注员可能标成robbery。这种错标会直接抑制模型对该类别的召回率。后来我在标注说明里强行加了一条规则:除非能明确看到"手部与物品的争夺动作",否则一律按fight标注。歧义必须在标注规则里消解,不能靠标注员自由发挥。
第三个坑是细小但致命的格式错误。有一次训练的时候YOLO报错,说某一行坐标值超出0-1范围,查了半天发现是某张图是从视频抽帧的,图像被旋转过,但标注数据还是按旋转前的坐标写的,导致归一化后出现了负数。后来我在所有标注脚本里强制加入数据合法性校验——解析每一行,如果x_center、y_center、width、height有任何一项小于0或大于1,直接打印文件名并中断,不允许带着脏数据进入训练环节。
4. 基于YOLO的训练实战:配置、调参与效果验证
4.1 数据划分与目录结构
9100张图我按7:2:1划分成训练集、验证集、测试集(注意是按视频源划分,不是按帧随机划分,前面说过了),目录结构如下:
anomaly_dataset/ ├── images/ │ ├── train/ # 约6370张 │ ├── val/ # 约1820张 │ └── test/ # 约910张 ├── labels/ │ ├── train/ # 约6370个TXT │ ├── val/ # 约1820个TXT │ └── test/ # 约910个TXT └── dataset.yamltrain、val、test的图片和TXT同名同前缀,保证一一对应。YOLO的标签文件不需要单独的JSON或XML,一个TXT就搞定,省事。
4.2 训练配置与超参数
我推荐从YOLOv8m起步,不建议一上来就上YOLOv8x——安防场景对推理速度有硬性要求(后面会细说)。在单张V100上,YOLOv8m训练9100张图大约3个多小时,多卡并行还能更快,完全在可接受范围内。
训练命令大概是这样的:
yolo detect train \ data=./anomaly_dataset/dataset.yaml \ model=yolov8m.pt \ epochs=100 \ imgsz=1280 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ augment=True \ patience=20 \ seed=42 \ project=./runs \ name=anomaly_v8m_1280关键参数说明:
- 输入分辨率imgsz=1280。安防场景里大量目标是小目标,用默认的640会让小目标特征严重损失。实测提升到1280后,小目标(小于32x32像素)的召回率大约能提升8~10个百分点。代价是训练和推理速度变慢,但对安防场景来说这是值得的。
- batch=16:V100 16G显存能扛住1280分辨率下的batch=16,再大就会OOM。如果显存不够,优先降batch,图像分辨率别降。
- lr0=0.01:用预训练权重微调时,这个初始学习率比较稳。如果是从零开始训练(不用预训练权重),需要降到0.005以下。
- patience=20:早停耐心值,如果20个epoch验证集mAP没有提升就自动停止。实际我观察到大约在第70~85个epoch收敛。
数据增强方面,YOLOv8默认开启马赛克增强,对安防场景特别有用——因为它能模拟出多目标互相遮挡、拥挤的复杂场景,这和真实监控画面的特性很贴合。我额外关闭了hsv_h的增强幅度,因为安防监控画面经常是固定的白平衡和色彩风格,色彩扰动太猛反而会让模型学到不真实的颜色关联。
4.3 训练过程观察与结果
训练过程中的loss曲线我重点看两个点:
第一个点是验证集box_loss和cls_loss是否持续走低且没有明显反弹。如果遇到epoch 30之后验证集loss开始回升,基本就是过拟合了,应该立刻停止训练并增加数据增强或者缩小模型。
第二个点是我关注各类别单独的AP(平均精度)输出,而不是只看整体的mAP。因为整体mAP会被person_normal这类样本数量大、易学类别拉高,掩盖掉fight、weapon_carried这些关键小类的性能问题。YOLO训练结束后会输出每个类别的AP,我建议你一把它们记下来。
我跑出来的基线数据大概是这样的(具体数字因数据版本可能会有差异,但量级可以给个参考):
| 类别 | AP@0.5 | 说明 |
|---|---|---|
| person_normal | 0.92 | 正常行为类,容易学 |
| fight | 0.74 | 互相遮挡严重,AP偏低 |
| robbery | 0.68 | 样本量少,且与fight存在语义混淆 |
| fallen_person | 0.81 | 姿态明确,效果尚可 |
| crowd_aggregation | 0.77 | 人群密集目标多,存在漏检 |
| running | 0.86 | 运动特征明显 |
| smoke_visible | 0.71 | 透明、半透明目标,框不紧 |
| fence_climbing | 0.79 | 姿态模式和翻越动作相关 |
| weapon_carried | 0.62 | 小目标+遮挡,最差类别 |
训练结束后还有一个容易被忽略的操作:用同样的超参数再训练一次,但把随机种子改掉。因为YOLO训练有随机性,单次结果可能因为运气好/差产生几个点的偏差。跑两次取稳定的那个结果,才算摸到底。
5. 这些指标才是硬道理:正确评估异常行为检测模型
5.1 为什么mAP不是唯一标准
如果你把安防异常行为检测当成一个纯学术任务,可能看mAP就够了。但做工程项目,客户更关心的是两个数字:误报率和漏报率。这两个指标和mAP有关,但不完全等价。
举个例子,mAP很高不代表误报少。因为mAP是"框级别"的检测指标,它计算的是一张图里所有类别预测框和真实框的匹配度。而安防场景里,客户真正在意的是"这个模型一天到晚乱报警几次"。哪怕模型把一辆正常的白色货车识别成intrusion,框还画得特别准,对mAP的影响可能只有0.01,但对用户体验的伤害是100%。
所以我在评估时额外做了两件事:
第一件事是给每个类别设定置信度阈值。比如person_normal的阈值设为0.35,而fight的阈值设为0.55。这样做的原因是不同类别的误报成本不同——把正常行人标成打架,比漏掉打架更让客户崩溃。YOLO的detect命令里可以传conf参数,但逐类别阈值需要自己写推理脚本处理。
第二件事是计算"每千帧误报率"。我取了一段连续的真实监控视频(约2000帧),统计模型在这段视频上产生虚假报警的次数。理想目标是小于每千帧2次。如果在测试集上mAP很漂亮,但真实监控视频上每千帧误报10次,这个模型就是不能上线。
5.2 按场景拆分评估
我强烈建议在评估阶段就把测试集按子场景再拆开:工地、校园、园区、室内。分别跑一遍mAP和误报率。
原因很简单:安防项目通常不是全场景通吃的。同一个模型可能在校园场景下表现很好,但到工地因为光照太强、尘土干扰,误报翻倍。如果你只报一个总指标,甲方和算法团队都没法定位问题。
我当时拆分后发现,园区周界场景的误报率比其他场景高出3倍,查了半天发现是围栏外面有一条马路,车辆经过时被识别成vehicle_illegal_park。后来针对这个问题增加了一批"马路上正常行驶车辆"的负样本,重新微调后误报率降到正常水平。这就是按场景评估的价值——帮你精准定位数据缺口。
5.3 半监督和自训练的补充思路
9100张标注数据训出来的模型能保底,但如果想再往上冲,我试过一条性价比很高的路径:用训练好的模型去跑大量未标注的监控视频,拿到高置信度的预测结果(比如置信度>0.85)作为伪标签,加入训练集再训一轮。这叫自训练,也叫模型蒸馏到自身。
我这里提醒一句:伪标签的置信度阈值一定要拉高,宁缺毋滥。低置信度伪标签的噪声会直接污染模型,反而不如不加。此外,伪标签只适合补充"正样本多样性",不适合创造新类别——如果你想让模型学一个原来类别体系里没有的行为,还得靠人工标注。
6. 真实场景落地的几个关键挑战
6.1 单模型vs多模型联动
直接用单个YOLO模型跑全部15类异常,推理端看起来省事,但实际坑很大。不同异常行为的时空特征差异巨大——打架需要连续几帧的动作变化,而fire_visible单帧就能判断。YOLO是单帧检测器,天然缺少时序信息,所以我会建议在工程上做多模型联动:
- 第一级用YOLO做单帧目标检测,输出行为类别和位置
- 第二级对fight、falling_person、running这些需要时序判断的类别,接一个轻量级的时序分类器(比如TSM或简单的LSTM),输入连续10~15帧的检测结果,过滤单帧误检
我在项目里落地这个方案后,打架检测的误报率下降了大约40%。第一级YOLO负责"找到可疑目标",第二级时序模型负责"确认是否真的异常"。这条思路比单靠调大YOLO阈值有效得多。
6.2 实时性与卡顿问题
安防监控通常要求多路视频流同时分析。如果在NVR旁边放一台服务器,16路1080p视频,每路要求至少10FPS的分析速度,那么总吞吐是160FPS。YOLOv8m在1280分辨率下用TensorRT加速的FP16推理大约能跑到60~80FPS,距离160FPS还差一倍。
我常用的三个优化手段:
- 把检测分辨率拆成两路——全局用640分辨率做目标发现,对检测到人的区域用1280分辨率做二次精细分类。这样大部分没有目标的画面帧只需跑一次低成本检测,整体吞吐能提高50%以上。
- 跳帧处理:对行为变化缓慢的类别(如smoke_visible),每2帧分析一次;对fight这类快速变化的行为,保持每帧分析。这种按类别动态调帧率的策略在工程上很实用。
- 用NVIDIA DeepStream或自研的流水线框架,把解码、缩放、推理、后处理放到不同线程/硬件单元上并行。CPU解码和GPU推理如果串行做,性能损耗非常大。
6.3 告警之后的事:可解释性与证据留存
安防项目里,模型输出一个"打架"告警,客户不可能直接冲过去处理——他们需要看到证据:什么时间、在哪个摄像头、画面截图、甚至一段5秒的短视频。所以在做落地时,光有检测框不够,还需要保存每次报警对应的原始帧和时序片段。
我建议在推理链路里加一个"报警缓冲池":当某类异常置信度超过阈值时,自动从视频流缓冲区取前2秒和后3秒的帧序列,连同检测框标注一起落盘存档。后续客户申诉"误报"时,这套证据链能帮你快速定位是模型问题还是场景问题。
7. 训练与部署中的常见问题排查
7.1 模型训练不收敛怎么办
如果你训练时发现loss一直不降,先检查三件事:
- 标签文件是不是真的和图片对齐了——建议写个脚本,随机抽10张图,把TXT标签画回图上,肉眼看一遍。
- 类别映射是不是正确——YOLO训练时如果names数量和你TXT里的class_id最大值不匹配,会大概率出现梯度异常。
- 数据增强是不是太猛——如果开了马赛克增强、随机透视、旋转90度,部分类别(如smoke_visible、fire_visible)的语义会被完全破坏,模型学到的东西就不对了。
7.2 小目标漏检率高
安防场景的小目标问题无解之一就是"远距离人物只有几十个像素"。除了前面提到的imgsz=1280,还可以尝试在标注阶段强制保留那些远处小目标框,而不是顺手删掉。我遇到过很多标注员习惯性把小目标标得特别粗糙,甚至漏标。YOLO对漏标的小目标不仅不会学,还会把它当成背景负样本,反而抑制对小目标的检测。所以数据集的标注规范里必须明确规定:只要人能看出画面里有对象,不管多小都要标。
7.3 置信度阈值到底怎么调
这个问题没有标准答案,但有一条经验法则:先看测试集上各类别AP从高到低排序,AP最高的类别,阈值可以设低一点(0.3~0.4),因为这类目标漏检损失比误报损失更可惜;AP最低的类别,阈值必须调高(0.55~0.7),因为低AP类别往往意味着模型对它的预测不可靠,宁可少报也不能乱报。
比如weapon_carried这类AP只有0.62,如果阈值设0.25,每天可能会误报几十次"发现刀具"。我最后把它的阈值拉到0.6,误报降到几乎为零,代价是漏掉一些真实但很小很模糊的刀具目标。在安防场景里,误报的后果(安保人员疲劳)比一定程度的漏检更严重,这个取舍方向是符合客户利益的。
8. 这套数据集的后续扩展思路
9100张、15+1类别,只是一个起点。我接下来的想法有两条主线:
一条是继续加数据。目前fight、robbery、weapon_carried这几个类别的样本量和AP明显偏低,下一步重点是补充夜间红外、雨雾天气、不同摄像头型号拍的素材。安防行业的数据积累是持续性的,模型上线后每天都能从真实场景中采集大量难例(hard examples),这些难例经过人工确认后回填到数据集里,迭代效果会非常明显。
另一条是引入更丰富的行为建模维度。YOLO能提供的单帧检测信息是一个地基,但如果想识别更复杂的"异常",比如长时间逗留、徘徊、尾随,需要把YOLO检测结果送到时序模型或轨迹分析模块。我在数据集设计时特意保留了person_normal的连续轨迹信息,就是为了给后续这些时序模型留好扩展口子。
如果你也想自建一套类似的数据集,我的建议是:别等数据完美了才开始训练,先用最小可行版本(比如先标3000张、先跑一个模型)把流程整个捋顺,然后边训边补。数据标注是一个越标越准的过程,前期标准没定好就开干,后面返工成本远高于一开始多花两天定标准。
最后分享一个我自己印象最深的操作细节:第一次在暗光环境下测试模型时,发现smoke_visible的置信度普遍掉到0.4以下,后来排查发现是因为训练集中的烟雾多半是白天明亮背景下拍的。补了一批黄昏和夜间的烟雾帧之后,暗光下的检测率立刻回到正常水平。数据分布覆盖是决定模型真实效果的第一因素,这比调任何参数都重要。