☰
YOLOv5教室行为检测实战:数据标注、训练调参与部署避坑指南
2026/10/14 4:30:42 网站建设 项目流程

简介:面向智慧校园场景的YOLOv5教室行为检测系统代码包,主要服务教育信息化开发者、AI初学者及教学管理人员,可解决课堂中人数统计、学生行为检测与老师行为检测等多维管理需求。包体非常轻量,共5个文件,以HTML、JavaScript前端文件为主,配合TODO.md、.inscode及.gitignore;其中HTML/JS构成可视化入口,TODO.md记录待办与扩展方向,.gitignore便于工程化同步,整个zip压缩包仅12KB,适合快速拉取预览。目前已有40人浏览学习。描述中覆盖了从数据预处理、模型训练到推理部署、可视化交互的完整模块划分,并提及学生端坐、举手提问、低头书写及教师板书、巡视等课堂行为识别维度;虽然受限于包体规模,并非完整训练工程,但保留了清晰的目录结构与后续优化线索,适合需要快速了解YOLOv5课堂行为检测方案、搭建演示原型或做二次开发的读者。

1. 教室行为检测为什么比通用目标检测更“吃代码”:这套 YOLOv5 代码包要怎么拆

通用目标检测在公开数据集上跑出再漂亮的精度,搬到教室监控画面里往往直接翻车:学生目标小、互相遮挡、动作相似,同一个权重在别的场景好用的,进教室就频繁漏检和误报。教室行为检测系统这份代码包,本质上不是“YOLOv5 换了个名字”,而是把数据标注规则、训练参数、推理平滑策略都围绕教室座椅场景重新调了一遍的源码工程。它解决的问题是学生睡觉、玩手机、举手、站立、读写等行为的自动检测与汇总,适合正在开发校园行为管理软件的人,适合给安防集成商做现场验证的工程师,也适合想拿真实工程结构做课设修改的新手。它比普通目标检测示例多出来的部分,正是最需要在本地复刻的工程边界。

2. 代码包结构和数据准备:把下载的源码包改造成能直接训练的工程

2.1 先看目录边界:数据区高频改,算法区只读

解压代码包后我一般留 10 分钟做目录巡查,这个时间在后续排错中会被十倍省回来。教室行为检测工程延续了 YOLOv5 系列的布局,但增加了一些自定义目录,典型的文件树长这样:

classroom-detect/ ├── data/ │ ├── classroom.yaml │ └── images/ # 按 train/val 划分的原始图片 ├── labels/ # 与 images 同名的 YOLO 格式标签 ├── models/ │ ├── yolov5s.yaml │ └── yolov5m.yaml ├── utils/ ├── runs/ # 训练输出、best.pt、测试结果 ├── train.py ├── detect.py ├── export.py └── requirements.txt

data/classroom.yaml 是训练入口必须读的核心文件,类别名、类别数量、训练集路径都写在这里。labels 目录则是逐张图片对应的标注文件,每个 .txt 里每一行代表一个目标框,格式是class_id cx cy w h。models 目录我建议只读,除非你要自己加注意力模块,否则行为检测这种任务不需要动网络结构,先把损失函数、I/O、NMS 这几个参数吃透更重要。utils 里是锚点计算、数据增强、损失计算等底层逻辑,我的习惯是只读不写,避免后续拉新数据时静默改坏行为。

真正需要高频修改的是 data、labels 和 runs。data 决定模型学什么,labels 决定学得准不准,runs 是实验结果,删掉旧的 runs 再做对比实验,视角会更清楚。很多人拿到代码包直接把整包丢进去跑,遇到问题也不知道是代码问题、数据问题还是参数问题,就是没做第一步的目录边界划分。

2.2 最小数据集合:images、labels 与 classroom.yaml 的三者对应

YOLO 格式有一个容易翻车的地方:训练时给的路径是 images 目录,但代码会自动去同级的 labels 目录找同名标签。如果标注文件被放在 images 里面,训练日志会提示找不到标签,然后悄悄跳过大量图片。我见过有一个小组 5000 张验证图,实际参与评估的只有 300 张,原因就是 labels 目录和 images 目录的对应写错了。

正确结构应该是:

dataset/ ├── images/ │ ├── train/ │ │ └── 0001.jpg │ └── val/ │ └── 0001.jpg └── labels/ ├── train/ │ └── 0001.txt └── val/ └── 0001.txt

classroom.yaml 里的路径写成相对根目录的方式,兼容性最好:

# classroom.yaml path: ../dataset train: images/train val: images/val nc: 6 names: ['sleeping', 'phone', 'hand_raising', 'standing', 'reading', 'writing']

names 的顺序非常关键。如果标注工具导出的 classes.txt 顺序和这里不一致,模型会把“玩手机”识别成“睡觉”。我一般会先看标注工具导出顺序,再照抄到 yaml 里,绝不凭感觉排。nc 必须和 names 长度一致,否则训练脚本会在 shape 检查环节直接报错。

标签文件里每一行的归一化坐标也值得检查。比如某一行是2 0.45 0.35 0.12 0.18,表示类别 2(hand_raising)中心点在图像宽高 45% 和 35% 的位置,框宽高分别占图像宽高的 12% 和 18%。训练前应该写一个极小的扫描脚本,把坐标小于 0 或大于 1 的标签列出来。这类脏数据会造成空标注框,Loss 变成 NaN,模型直接学崩。

2.3 环境安装:先锁定 PyTorch 与 CUDA,再做 30 秒冒烟测试

代码包里的 requirements.txt 通常会固定 torch、torchvision、opencv-python 等版本。不要直接无脑pip install -r requirements.txt,因为本机 CUDA 版本不匹配时,安装过程看起来正常,但训练时模型会退回 CPU 模式,速度慢到你怀疑代码包有问题。我先用这条命令确认本机状态:

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

如果 cuda.is_available 是 False,就去装匹配本机 CUDA 的 PyTorch 版本,再回来看 requirements。安装完成后,用代码包自带的一张测试图跑一次最简推理,这个动作我称为冒烟测试:

python detect.py --weights yolov5s.pt --source data/images/smoke.jpg --conf 0.25

冒烟测试通过,说明源码包、依赖、权重文件三条链路是通的。之后训练出问题,就不会再花一整晚去查环境问题。这一步成本只有一分钟,但能把环境问题从模型问题里分离出来,排错快很多。

2.4 数据划分脚本:随机种子定死验证集分布

训练和验证的划分如果每次随机种子不同,你看到的 mAP 会忽高忽低,很难判断模型改得好不好。我在接入行为检测代码包时,会把数据划分脚本固定下来,并把划分结果落盘:

import random from pathlib import Path random.seed(2025) image_root = Path("raw_images") train_out = Path("dataset/images/train") val_out = Path("dataset/images/val") images = list(image_root.glob("*.jpg")) random.shuffle(images) val_count = int(len(images) * 0.2) for img in images: label = image_root / (img.stem + ".txt") if img in images[:val_count]: dest_img = val_out / img.name dest_lbl = Path("dataset/labels/val") / label.name else: dest_img = train_out / img.name dest_lbl = Path("dataset/labels/train") / label.name dest_img.parent.mkdir(parents=True, exist_ok=True) dest_lbl.parent.mkdir(parents=True, exist_ok=True) img.rename(dest_img) label.rename(dest_lbl)

这里的 20% 是行为检测场景里比较稳的比例。教室行为类别分布差异大,如果验证集只有几十张,很可能一个睡觉样本都没有,验证分数完全失真。运行完要检查 images 和 labels 的文件数是否一致,这一步我会做成一个简单的 pytest 入口,每次数据更新都跑一遍。还有一件容易被忽略的事:分类别统计一下验证集里的目标数量,尽量保证每个类别在 train 和 val 里都有出现,避免“训练时见过手机、验证时一张手机都没有”这种假 mAP。

2.5 标签清洗:过滤空标签、重复标注和损坏图片

标注数据并非越多越好。行为检测这类任务,标注员会因为“睡觉”和“趴桌”分不清,产生大量边界模糊框。我的经验是,模型第一版跑出来的错误,很大比例可以从标签里找到根源。

空标签文件会让模型隐式学出一种背景分支,数量多了之后,误检率明显上升。重复标注则会造成同一个目标有两个重叠框,训练时计算 loss 会来回拉扯。损坏 JPEG 会在 mosaic 增强时读入半个文件,导致这一轮训练 Loss 骤降或直接中断。

我常用的清洗脚本如下:

from PIL import Image from pathlib import Path for img_path in Path("dataset/images").rglob("*.jpg"): try: Image.open(img_path).load() except Exception: print("损坏图片:", img_path) img_path.unlink()

这份清洗动作在训练前执行一次,之后每次扩展数据集时再执行一次,能明显减少“训练到第三天突然 OOM”的玄学问题。数据准备做到这个程度,再去开训练命令,后面调参时的心智负担会小很多。

3. 训练参数设置:预训练模型、输入尺寸与增强策略的调优方向

3.1 预训练模型怎么选:COCO 权重迁移到教室行为的边界

行为检测代码包默认给出 YOLOv5s 和 YOLOv5m 两个尺寸。我的建议是从 s 起步,因为教室行为标注工程通常只有几万张图,属于中小规模数据量,s 模型在显存占用和实时推理上的压力最小。等你把类别数、输入尺寸、置信度阈值这些变量都理清楚,再切到 m 去刷精度,才是合理的路线。

不要跳过预训练权重。COCO 预训练权重前几层已经学到颜色、边缘、纹理等底层特征,后层则学到了通用形状结构,这些能力可以迁移到“睡觉”“举手”这类行为,因为行为识别的外观特征依然来自人、手机、书本这些物理目标。真正需要重新学的是最后分类器层的类别分布,所以在训练日志里看到Transferred 346/365 layers属于正常提示,那几十层没有迁移的正好是分类和回归头,由随机初始化重新训练。

如果在本地小数据集上从 0 开始训练,收敛会慢很多,而且容易陷在局部最优。我试过按社区里瞎传的“从随机权重练起,防过拟合”做法,结果 mAP 卡在 0.3 上不去,后来换回预训练权重,同样参数直接到 0.7。行为检测在这个问题上没有例外,回头路可走。

3.2 训练命令与核心参数:从 batch 到 mosaic 的取舍

YOLOv5 的训练入口是 train.py,我用得最多的一条命令长这样:

python train.py \ --data classroom.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 80 \ --device 0 \ --multi-scale \ --mosaic 1.0

--img 640 表示训练时把输入图缩放到 640×640。更高的 1280 对检测小目标有好处,但显存占用和训练时间会非线性上升。教室监控画面中,如果摄像头覆盖的是半个教室,学生头部大约 40×60 像素,640 就能用;如果覆盖一整间大教室,学生头部只有 20 像素,我会先用 768 做一轮对比,再决定是否上 960。

--batch 16 在 8GB 显存上比较安全,显存不够就先降到 8,同时加 --accumulate 来模拟大 batch。--mosaic 1.0 表示每张训练图由四张图拼接,这个增强在密集目标场景帮助明显,但它也会短暂打乱 loss 曲线,让 loss 出现周期抖动,这是正常现象。看曲线时不要被单次尖峰吓到。

还应该用 --cache 参数把训练图缓存进内存,如果服务器内存够 32GB 的话。教室图像集模型加载速度会快一大截,训练时不会因为频繁读磁盘把 GPU 利用率拉低。若内存不足且有机械硬盘拖后腿,训练等待时间会非常感人。

3.3 训练验证:mAP、PR 曲线和 loss 曲线怎么配合看

训练停止时,不要只盯着 best.pt 的 mAP。mAP 是一个汇总值,两个模型可能 mAP 一样,但实际行为检测表现完全不同。我会同时看三个东西:

loss 曲线的 trend,val 正常 rebalancing,分类别 PR 曲线。

如果 mAP@0.5 高但 mAP@0.5:0.95 很低,说明模型检测到的框定位精度差。行为检测场景里,监考人员更关心“有没有这个人站起来”,而不是框精确到什么程度,所以 mAP@0.5 的重要性高于 COCO 风格的长程均值。

用自带的 val.py 或代码包里的验证脚本,可以输出每个类别的 AP 值。我把这个表当作“行为检测体检报告”。比如 sleeping 的 AP 远低于 phone,那问题大概率在标签数量或标签一致性,而不是模型。此时增加训练轮数是没有意义的,应该回去补数据。

训练产生的 runs/train/exp 目录下会有 results.csv,把它按列画线,观察 val loss 在 epoch 60 之后是否还稳定下降。如果 val loss 开始回升而训练 loss 还在降,那就是过拟合,应该提前停掉而不是继续等到 80 epoch。我用 early stopping 的逻辑,只在 val loss 连续 10 个 epoch 没有改善时切到 last.pt 之前保存的快照。

3.4 超参数文件:不动的代码包也能调行为分类

YOLOv5 默认的 hyp 文件是针对 COCO 调的,换到教室行为这个小数据场景,有两处我经常改。第一处是 weight_decay,默认值约 0.0005,数据量不足 5000 张时这个值偏大,模型权重被压制,小目标框不稳定。第二处是 cls_pw,睡觉样本可能只有手机样本的三分之一,需要适当提高分类损失权重。

我把修改放到一个独立的 hyp 文件,不改代码包源码:

# hyp.classroom.yaml lr0: 0.01 lrf: 0.01 momentum: 0.85 weight_decay: 0.00025 warmup_epochs: 3 cls_pw: 1.2

然后在训练命令后追加--hyp hyp.classroom.yaml。这样代码包保持原样,对比实验时只要切换不同的 hyp 文件,就可以清楚隔离变量。momentum 从 0.937 降到 0.85 会让训练更稳,但收敛稍慢,配合 warmup_epochs 3 比较合适。在实际部署中,我一般只动 weight_decay 和 cls_pw,其他维持源码默认,减少变量干扰。

4. 推理验证与工程化部署:从单张图片到视频流的检测链路

4.1 detect.py 快速验证:置信度阈值与 IoU 阈值的组合判断

训练完成后,先用代码包里的 detect.py 跑一遍测试目录,不要急着上视频流。单张测试才能看清模型到底学到了什么,视频流里的动态干扰容易让人误判。常用命令如下:

python detect.py \ --weights runs/train/exp/best.pt \ --source test_images/ \ --conf 0.25 \ --iou 0.45 \ --save-txt \ --save-conf

--conf 0.25 表示置信度超过 25% 才输出。教室多目标场景我习惯先用 0.25,再降到 0.2 对比是否漏检增加。--iou 0.45 是 NMS 的 IoU 阈值,数值越低抑制越狠。学生坐得很密时,相邻两个目标高度重叠,0.45 很容易把后排的人吞掉,这时可以调到 0.5 或 0.55 再观察。

输出结果里同时保存的labels/和 图片画框图,和真实标注做叠加对比。如果大量把“站立”错检成“睡觉”,大概率是标注边界问题或视角问题,而不是继续训练能解决的。单帧验证通过后,才进入部署环节。

4.2 导出 ONNX:CPU 服务器与老旧推理机的现实选择

很多教室监控的现场机房没有独立显卡,只有 CPU 服务器。PyTorch 直接在 CPU 上推理,速度会让人崩溃。一般做法是把 best.pt 导出成 ONNX,再交给 OpenCV DNN 或 ONNX Runtime 推理:

python export.py --weights runs/train/exp/best.pt --include onnx --opset 12 --simplify

导出时有一个我踩过多次的坑:训练时用了 --multi-scale,模型没有固定输入尺寸,导出 ONNX 时会取一个批量维度,但后续部署代码如果用了不同的 imgsz,前处理会 resize 到不一致的尺寸,导致输出坐标错乱。我通常导出时就固定--img 640,部署端也跟随 640,统一预处理的填充方式。

ONNX 模型冷暖不均时,教室窗户外阳光导致局部过曝,OpenCV 推理的预处理要按训练时的 normalize 参数来。很多同学直接把像素值除以 255 就开始推理,忘记了原代码在训练时用的标准化手段,结果是 mAP 很高,现场完全不可用。我把blobFromImage(frame, 1/255.0, (640, 640), swapRB=True)这一步当成固定模板,每次导出和部署都核对一遍。

4.3 视频流接入:抽帧、断线重连和结果回传

教室行为检测业务上更多是接多路 RTSP 视频流,对推理程序的稳定性要求比单张高得多。我常用的最小推理结构是:

import cv2 cap = cv2.VideoCapture("rtsp://your-camera-stream") model = cv2.dnn.readNetFromONNX("best.onnx") frame_count = 0 lost_count = 0 while True: ok, frame = cap.read() if not ok: lost_count += 1 if lost_count > 10: cap.release() cap = cv2.VideoCapture("rtsp://your-camera-stream") lost_count = 0 continue lost_count = 0 frame_count += 1 if frame_count % 5 != 0: continue blob = cv2.dnn.blobFromImage(frame, 1/255.0, (640, 640), swapRB=True) model.setInput(blob) detections = model.forward() # 后续把 detections 映射回原图坐标,存入行为事件

这个脚本关键是抽帧而非逐帧分析。25 帧/秒的视频降到 5 帧/秒,CPU 占用能降一半以上,而行为识别这种相对慢速的动作不会漏掉。参数 frame_count 的 5 是“抽帧间隔”,如果你接的是 15 帧摄像头,可以改成 3,保证每秒处理 5 帧左右。lost_count 是断线重连计数,RTSP 流长时间运行后会偶发断开,没有重连逻辑的进程会一直退出或卡死。

结果回传部分,我会把检测到的行为类别、置信度、目标中心坐标、时间戳拼成一行 JSON,写进本地队列再异步上报。不要直接在全局 executive 循环里访问数据库或写日志文件,避免推理阻塞导致画面丢帧。教室行为检测系统的工程价值,很大程度体现在这里。

4.4 坐标映射:模型输出坐标到原画面的正确换算

ONNX 输出是归一化坐标,部署时要把它们换算到原视频画面的像素坐标系。常见错误是将归一化坐标直接乘上原图宽高,而忘了预处理时的 letterbox padding。YOLOv5 的前处理通常会把原图缩放到 640×640,同时留下灰边,推理坐标是相对于加边之后画面的。因此要保存 pad 和 scale 信息:

def post_process(boxes, scale, pad): x_center, y_center, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] x1 = (x_center - w / 2 - pad[0]) / scale y1 = (y_center - h / 2 - pad[1]) / scale x2 = (x_center + w / 2 - pad[0]) / scale y2 = (y_center + h / 2 - pad[1]) / scale return np.stack([x1, y1, x2, y2], axis=-1)

scale 是原图到 640 的缩放比例,pad 是被填充的灰边宽度。这里只要写错一个,画框位置就会整体偏移。教室的课桌区域对框定位非常敏感,框偏移半个身位就会导致行为归因到隔壁学生身上,因此这个函数务必单独做单元测试。

5. 避坑指南:教室行为检测训练部署的五个真实翻车点

5.1 现象:训练 mAP 很高,现场检测率却惨不忍睹

有一次我把行为检测模型训练到 mAP@0.5 接近 0.9,以为可以直接部署到教学楼,结果现场画面里三分之一的学生都没被识别出来。原因分析到最后,是训练集图片全部来自一个年级同一间教室,座位排列、颜色、摄像头视角高度相似,模型把这间教室的“环境细节”当成关键特征。解决方法是把不同教室、不同摄像头高度的录像混合进训练集,并且验证集按教室来源分组抽样。在那之后我强制要求至少包含三个不同物理环境的图片,再开始训练。数据来源的多样性比多调 0.01 学习率重要得多。

5.2 现象:睡觉行为被大量识别成趴桌或低头

现象就是模型把闭眼睡觉的学生框成 normal 或者 writing。原因在于“睡觉”和“趴桌”在视觉上没有清晰边界,尤其是侧面视角,头部低垂和看书姿态几乎一样。解决方法是重新定类别标注规范:睡觉必须包含闭眼或头部低于桌沿且持续时间长两个条件;趴桌单帧如果无法判断,就归到不感兴趣的背景类,避免模型学到一个混淆分类。同时清洗已经标好的边界样本,把明显贴边的标签全部重标。行为检测的标注规范如果没有人盯,模型再训练也救不回来。

5.3 现象:手机目标太小,置信度一直很低

小目标问题在教室里非常突出。一支手机在整教室画面里可能只有 12×30 像素,模型在小特征图上几乎无法分辨。原因不只是 imgsz 低,而是训练集中手机目标的正样本数量远少于其他类别。解决办法有三步:先提高 imgsz 到 768 做一个对比;再把手机特写片段单独抽帧并入训练集,提升小目标比例;最后适当调高 hyp 里 cls_pw,让损失函数更关注少数类别。我还发现,用小目标切片抽帧训练时,不要只截手机局部,还要保留周围十几像素背景,否则推理时模型的上下文信息不够,误报反而增加。

5.4 现象:GPU 显存充足但训练中途报 CUDA out of memory

这个现象很迷惑,单张卡 24GB,看起来没吃满,却中途 OOM。原因通常是 mosaic 增强把四张大图同时加载到显存计算,再加上自动锚点计算阶段的临时 buffer,峰值占用远超平均值。解决方法是先降低 batch 一半,确认训练能过第一个 epoch,再逐步加 batch 并配合--accumulate。如果降到 batch 4 仍然 OOM,就要检查代码包所在磁盘是不是满了,因为 YOLOv5 会把缓存图片写到临时目录,磁盘满了也会表现为显存异常。另外,损坏图片在 mosaic 阶段读取时会额外申请临时内存,这也是隐藏的显存杀手。

5.5 现象:导出 ONNX 后行为类别全部错乱

部署端加载 ONNX 模型后,检测框数量正常,但类别名对不上,比如一直输出睡觉,而原模型训练时明明是手机。原因大多出在导出前的 names 顺序不一致。训练时 yaml 里 names 顺序已经固定,但 export 后如果部署代码从另一个coco.names或旧版classes.txt读取类别,就会发生错位。解决办法是在导出脚本和部署端同时固定从一个 yaml 读取 names,禁止手工复制列表。另一个原因是导出时模型权重路径写错,加载了上次实验的旧权重。我在部署目录里会专门建立一个deploy_config.yaml,把权重路径、names 顺序、imgsz、conf 阈值全部放一起,每次发布前用 sha256 校验 best.pt 和 ONNX 是否对应,从根上杜绝类别错乱和版本串台。

6. 进阶技巧:把单帧检测升级成行为时序判罚,一个滑动窗口方案

单帧检测只能告诉你“这一刻有个人低头”,但业务方想知道的是“这个学生是不是已经睡了超过两分钟”。如果每一帧低头都上报,系统会被噪声淹没。教室行为检测系统的价值,恰好体现在如何把单帧结果稳定成行为事件。

我在推理脚本里维护一个滑动窗口缓存,每个类别对应一个固定长度的 deque,每处理一帧就更新一次,窗口内比例超过阈值才触发行为判定:

from collections import deque window_size = 15 # 保持与抽帧间隔联动,5帧/秒下等于3秒 threshold = 0.7 # 窗口内出现比例阈值 behavior_state = {cls_id: deque(maxlen=window_size) for cls_id in range(6)} def judge_behavior(cls_results): for cls_id in range(6): behavior_state[cls_id].append(1 if cls_results.get(cls_id) else 0) active = {} for cls_id, queue in behavior_state.items(): if len(queue) < window_size: continue ratio = sum(queue) / window_size active[cls_id] = ratio > threshold return active

这段逻辑看起来简单,真正部署后会发现它抑制抖动的效果非常明显。我用 15 帧窗口、0.7 阈值,把“睡觉误报”从每节课二十多次降到三次以内。参数 window_size 和 threshold 必须和抽帧间隔联动:上面代码里假设 5 帧/秒,15 帧约等于 3 秒;如果你改成每秒处理 2 帧,窗口至少要 40 才够覆盖 20 秒的持续行为。阈值 0.7 的意思是一秒内大约 7 帧里有 5 帧被检测成睡觉,才算事件成立,这个比例可以根据现场误报情况上下浮动。

这个升级方案并不增加训练成本,只改部署端,但在交付验收时给用户的感受是完全不同的:单帧系统像演示 Demo,滑动窗口系统才像行为管理软件。后来我每次接入新的教室数据,都会强制走一遍“单帧置信度过滤 → 滑动窗口时序判定 → 业务上报”的三层链路,把窗口长度、抽帧间隔、上报接口三处参数确认时间戳一致,再也不会出现现场屏幕上一个人同时“既睡觉又举手”的尴尬。掉过一次这种坑之后就长了记性,希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询