首页 / 资讯中心 / 文章详情

基于深度学习的课堂学生行为识别系统:从YOLO检测到Django部署实践

基于深度学习的课堂学生行为识别系统:从YOLO检测到Django部署实践 ★ FEATURED ARTICLE
简介一款基于深度学习的上课学生行为识别系统项目面向毕业设计、课程设计、大作业及工程实训场景。系统采用YOLOv5模型进行目标检测结合OpenCV实时分析学生行为后端基于PythonDjango数据存储使用MySQL整体分为管理员与学生两个操作端。压缩包共718个文件、约304MB含Python源码、SQL数据库脚本、HTML/CSS/JS前端页面、224张PNG与207张JPG图片、96个GIF动图以及docx、pdf等配套文档目录结构清晰。目前已有86人学习适合需要快速搭建完整案例的深度学习初学者也可作为课程设计与工程实践的基础模板。项目内含可运行源码与SQL文件覆盖管理员账号管理、学生审批、课程信息维护、行为检测记录、通知发送等后台功能学生端可调用摄像头实时识别坐姿、站立、写字等行为并生成分析报告有助于完整掌握目标检测与Web后台的整合开发流程。1. 为什么做上课学生行为识别先看清这个系统到底值不值得投入教室里的摄像头越来越便宜但拍下来的视频如果不经过分析就只是一堆占用硬盘的素材。上课学生行为识别这个方向做的事情就是把教室内监控画面实时解析成可量化的课堂数据学生抬头听讲、低头玩手机、趴桌睡觉、举手回答问题、前后桌交头接耳。基于深度学习的方案意味着不需要人为设定传感器或穿戴设备只用视觉模型就能从画面里直接推算行为类别再用 Django 把识别结果包装成可查询、可统计的 Web 服务——这正是这个标题背后完整的落地面貌。适合谁去做两类人一类是高校和教育信息化方向的技术团队要做课堂督导或教学质量评估平台另一类是刚接触深度学习目标检测与 Web 部署的开发者想找一个既能练手又能放进简历的完整项目。这条技术路线已跑通多年既有成熟的检测算法做支撑又有清晰的 Django 工程化路径可复用。它解决的核心矛盾是教室场景难以靠人工持续盯屏但课堂行为数据又是教学管理里最真实的过程性证据。2. 方案拆解与数据准备把摄像头画面变成模型能学习的样本2.1 行为定义与模型选型先定类别边界再选骨架网络做行为识别最容易犯的第一个错误是上来就找模型、跑开源代码结果发现识别出来的类别跟学校想要的口径对不上。我一般会先花半天时间把行为类别定义清楚。常见的做法是采用六类或八类方案听讲Looking、举手HandUp、读书Reading、趴桌Sleeping、玩手机UsingPhone、交头接耳TurningAround有的学校还会加一类“其他Other”兜底。类别一旦定下来就是一个封闭集合的分类问题不需要做时序建模单帧画面就能判断——这也是这个标题下最主流的落地选型。基于深度学习的单帧行为识别技术栈上基本锁定在目标检测 图像分类的组合拳。检测层可以用 YOLO 系列把画面里的学生框出来分类层对每个框内的图像做行为判断。相比直接做端到端的时空卷积网络如 C3D、SlowFast这个组合有三个实际优点第一教室场景学生密度高检测框能天然隔离不同学生的行为避免多个学生混在一个特征图里互相干扰第二单帧推理更稳定摄像头角度不佳或画面抖动时不会像视频模型那样前后帧联动放大误差第三训练数据容易凑检测模型用公开人体数据集预训练分类模型只需采集标注几百张课堂照片就能达到可用的效果。选骨架网络时我建议分类分支用轻量级卷积神经网络ResNet18 就够用。重型模型如 ResNet50 或 EfficientNet-B4 在教室场景不会有质变提升但推理速度下降明显。检测层如果机器显存有限优先考虑 YOLOv5s 或 YOLOv8n如果机器性能好且对精度要求高再换 YOLOv8m。这里有一个容易被忽略的选型原则行为识别系统的瓶颈通常不在模型上限而在训练数据的分布——教室里的摄像头视角、光线、学生坐姿千差万别模型只有在贴近真实场景的数据上训练才能泛化。2.2 课堂行为数据集怎么攒网络公开集打底自采数据补差很多开发者在动手前卡在数据集上——以为需要几十万张标注图才能启动。实际上课堂行为识别有一个更务实的路子用公开数据集打底用少量自采数据做迁移。公开集方面有三个高频选择第一个是 classroom 行为识别相关的学术数据集这类数据通常已经按照听讲、举手、睡觉等行为做过帧级标注第二个是 COCO 数据集中的 person 类别用于预训练人体检测器第三个是 SCUT-HEAD 等头部检测数据集可以用来辅助生成学生定位的预训练权重。但公开数据集普遍存在两个问题角度太正、场景太干净。真实教室是斜上方 45 度视角有课桌椅遮挡前后排学生互相遮挡光照在阴天和晴天差距很大。这就必须引入自采数据环节。我做了这么多年落地方案最实用的方法是找一间空教室组织同事或同学坐在不同位置每人按照行为清单录制 1 到 2 分钟视频再从视频里按每 3 秒抽一帧的方式切图。一个 10 人的班级配合不同行为轮换大概能得到 6000 到 8000 张有效帧比直接从网上下载散图可靠得多。标注环节推荐用一个开源的图像标注工具这类工具普遍支持矩形框和标签下拉选择。标注策略上有一个血泪经验检测框紧贴学生身体躯干不要只框头部因为趴桌行为和举手行为都依赖手臂和躯干姿态框住头部会让分类分支丢失关键信息。如果时间紧可以在标注完 800 张后就先训练一版模型用模型的预测结果做半自动标注——人工修正错框和错标签再补训练。这个迭代法能把标注成本压缩到纯人工标注的三分之一左右。2.3 训练数据目录结构与标签编码先立好工程规范再动模型数据准备工作不做规范后面 Django 对接时一定会反工。我习惯在项目根目录下按这样的目录结构组织数据集这个结构也兼容主流的 YOLO 和分类训练脚本dataset/ detection/ images/train/ # 检测模型训练图 images/val/ # 检测模型验证图 labels/train/ # YOLO 格式的 txt 标签 labels/val/ classes.txt behavior/ train/ listening/ # 听讲行为 handup/ # 举手 reading/ # 读书 sleeping/ # 趴桌 phone/ # 玩手机 turning/ # 交头接耳 val/检测标签的 YOLO 格式每行对应一个学生框五个字段分别是类别编号、归一化中心点 x 坐标、归一化中心点 y 坐标、归一化框宽、归一化框高。行为分类的数据不需要另建文件夹可以直接按检测框裁剪出学生区域的图像再移动到behavior/目录下对应的行为类别目录。这里有一个关键的顺序问题先训练检测模型再用检测模型批量裁剪学生区域最后训练行为分类模型。两个模型分开训练的优势在于检测模型可以只用 person 单类别训练不必关心行为标签训练数据需求和难度大幅下降分类模型面对的输入已经是整齐的学生框干扰因素被提前过滤。如果检测不准后续分类再强也救不回来因为分类器看到的是残缺的输入。3. 行为识别模型训练用两段式训练把精度稳定在可用线上3.1 检测模型训练单类别 person 检测的收敛速度快到超出预期检测模型不建议从零训练直接用预训练权重起步。训练脚本通常基于 YOLO 官方仓库的train.py命令大致如下python train.py \ --data classroom.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 50 \ --device 0classroom.yaml里只需定义两个关键路径和一个类别数train指向检测训练图目录val指向验证图目录nc设为 1names列表里只有一个person。实验跑下来单类别 person 检测器在 640 分辨率下、训练 50 个 epoch 就能达到 0.85 以上的 mAP0.5 因为预训练权重已经在 COCO 上见过海量人体样本微调成本极低。在推理侧检测模型的作用是输出学生框。对每个画面帧模型返回的是形如[x1, y1, x2, y2, confidence, class_id]的框信息。这里我建议把置信度阈值设在 0.25 到 0.35 之间。阈值调高会漏检后排小目标学生调低则会把椅子、书包误检成人。教室场景还有一个特殊的干扰源如果讲台附近有假人模型或人形立牌检测模型会被欺骗需要你把这类样本加入训练集并标注为背景即不标注用负样本抑制误检。3.2 行为分类模型训练输入尺寸和增强策略决定了 5 个百分点的精度差学生区域裁剪图在进入分类模型之前要做统一的尺寸缩放。ResNet18 的标准输入是 224×224但直接用resize会让原图比例变形尤其是瘦高的站姿学生框被压扁后姿态特征受损。我建议先按长边等比缩放再对短边做边缘填充把填充值设为 0黑色。这样做的好处是尽可能保留学生的原始姿态比例让趴桌和举手的判别特征不被扭曲。分类模型训练用 PyTorch 写一个标准数据加载流程即可transform_train transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) transform_val transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])训练时的关键超参数值得记录一组常用起点初始学习率 0.0001权重衰减设为 0.0005batch size 32采用余弦退火调度器训练约 30 个 epoch。随机水平翻转不要开得过猛因为上课时学生面向讲台识别“举手”和“玩手机”依赖左右手方向过度翻转会混淆类内差异。ColorJitter 的光照扰动对教室场景很关键不同时段的自然光和灯光会让整体色调漂移这个增强项在实验中能稳定提升 2 到 3 个百分点的精度。行为分类模型的损失函数直接用交叉熵即可。数据集类别不平衡是必然的——听讲和玩手机的样本远多于举手和趴桌。应对方法不是盲目加样本权重而是先算一下每个类别的 F1 分数如果只是少数类偏低用类别权重[1, 2, 1.5, 2, 1, 1.5]这类经验值去压低多数类的梯度如果多个类别都偏低说明标注质量或输入裁剪存在问题不要通过调权重来掩盖。3.3 精度卡在 70% 上不去时的三个检查顺序训练完成后最常见的沮丧时刻是验证集精度停在 70% 左右怎么看都上不去。这时候我不会盲目换模型而是按下面的顺序排查数据集第一步可视化每个类别的混淆矩阵。如果“玩手机”和“听讲”互相混淆大概率是低头看桌面的姿态太相似。解决方向是扩充“玩手机”类别中手机边缘清晰可见的样本或者把“低头看桌面”定义为第三种行为把边界移出两个旧类。第二步检查裁剪框是否包含过多的同桌干扰区域。检测框有时把两个相邻学生的肩膀都框进来分类模型会学到错误的上下文特征。解决方向不是改模型而是回到检测模型训练把 tight box 的标注规范加强——框的四个边界要紧贴人体边缘不留背景余量。第三步看训练集和验证集是否来自同一场景。如果只在 A 教室采集训练数据验证集用 B 教室的图模型会因背景差异而下降好几个点。更好的做法是采集时就横跨两到三间不同装修风格的教室让模型学到与学生行为强相关的特征而不是背景色。4. Django 工程化封装把训练好的模型变成可访问的行为识别服务4.1 Django 项目结构与模型加载不要每次请求都重新载入权重模型训练好之后剩下的事情是工程化。Django 在这个系统里承担的职责是接收视频帧或图片上传调用检测和分类模型推理把行为识别结果写入数据库并提供 Web 页面和 API 给用户查询。我把 Django 项目结构组织成下面这样清晰且易维护classroom_behavior/ manage.py detection/ # 检测和分类的推理逻辑 yolo_detector.py behavior_classifier.py web/ settings.py urls.py api/ urls.py views.py apps/student/ models.py views.py static/ media/模型加载放在 Django 的apps.py的ready()方法中或者用一个单例模块做延迟加载。一个常见的错误是把torch.load()写在视图函数内部这会导致每个请求都加载一遍权重文件显存直接爆炸且响应延迟到不可用。正确做法是进程启动时一次性加载到 GPU 或内存之后所有请求复用同一个模型实例。推理时的代码可以这样写import torch from torchvision import models _model None def get_behavior_model(): global _model if _model is None: _model models.resnet18(num_classes6) checkpoint torch.load(weights/behavior_best.pth, map_locationcpu) _model.load_state_dict(checkpoint[state_dict]) _model.eval() return _model这里的逻辑要说明全局变量_model保证模型只加载一次map_locationcpu为了兼容没有 GPU 的部署机加载后必须切换到eval()模式否则 BatchNorm 层会因推理时统计量变化而输出不稳定。4.2 推理流水线实现把检测框扩展到分类输入核心推理函数要串联检测器和分类器两段逻辑。收到一帧图像后先送入 YOLO 检测器拿学生框再对每个框内的图像区域做预处理后送进 ResNet18最后把行为类别和置信度一起返回。完整代码如下import cv2 import numpy as np import torchvision.transforms as transforms def inference_frame(frame): dets yolo_detector.predict(frame, conf_thres0.3) results [] for det in dets: x1, y1, x2, y2, conf, _ det margin int(0.05 * (x2 - x1)) x1 max(0, int(x1) - margin) y1 max(0, int(y1) - margin) x2 min(frame.shape[1], int(x2) margin) y2 min(frame.shape[0], int(y2) margin) crop frame[y1:y2, x1:x2] crop cv2.cvtColor(crop, cv2.COLOR_BGR2RGB) crop cv2.resize(crop, (224, 224)) crop transforms.ToTensor()(crop) crop transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] )(crop).unsqueeze(0) with torch.no_grad(): prob torch.softmax(behavior_model(crop), dim1)[0] label_id torch.argmax(prob).item() confidence prob[label_id].item() results.append({ box: [x1, y1, x2, y2], behavior: class_names[label_id], confidence: round(confidence, 4), det_conf: round(conf, 4) }) return results推理代码有几个细节值得注意。给检测框外扩5%的 margin 是必要的因为检测器输出框通常贴身直接裁剪会切掉手部动作的边缘。置信度分数${confidence}可以当作质量信号低于 0.5 的行为结果建议在 UI 端标记为“待确认”而不是直接展示为确定结果。with torch.no_grad()块不能省否则推理过程会累积计算图显存占用会随请求数增加而线性上涨。4.3 数据库模型与 ORM 设计行为记录要支撑按班级、按时间聚合后端的数据模型要往前想一步行为识别不只是保存一条条孤立记录还要支撑“某节课举手次数分布”“哪个班趴桌比例最高”这类聚合查询。我把表结构设计成三张核心表from django.db import models class Classroom(models.Model): name models.CharField(max_length100) location models.CharField(max_length200, blankTrue) class CourseSession(models.Model): classroom models.ForeignKey(Classroom, on_deletemodels.CASCADE) start_time models.DateTimeField() end_time models.DateTimeField(nullTrue, blankTrue) course_name models.CharField(max_length100, blankTrue) class BehaviorRecord(models.Model): session models.ForeignKey(CourseSession, on_deletemodels.CASCADE) student_box models.CharField(max_length30) # x1,y1,x2,y2 behavior models.CharField(max_length20) confidence models.FloatField() frame_time models.DateTimeField()BehaviorRecord之所以不直接存外键到Classroom是因为记录只需要归属于某节课而课程再归属教室。这样设计的好处是当你按班级、按时间范围查询时只需要先定位CourseSession的 ID 列表再用session__in查记录Django ORM 的values().annotate()可以方便地按行为类别做聚合计数。学生身份识别在当前方案中不做——因为摄像头视角不足以稳定判定个人身份系统的输出以群体行为统计为主而非个体画像。这一点要在需求沟通时跟校方讲清楚避免后期期望落空。4.4 API 层与页面展示Django 视图返回 JSON 和渲染模板API 层我推荐用 Django REST Framework 做序列化但如果你不想引入额外框架纯 Django 的JsonResponse也能完成任务。下面是一个接收图片并返回识别结果的视图写法import json from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt csrf_exempt def upload_frame(request): if request.method ! POST: return JsonResponse({error: method not allowed}, status405) file_obj request.FILES.get(frame) if file_obj is None: return JsonResponse({error: no frame uploaded}, status400) data np.frombuffer(file_obj.read(), np.uint8) frame cv2.imdecode(data, cv2.IMREAD_COLOR) results inference_frame(frame) return JsonResponse({count: len(results), results: results})csrf_exempt装饰器在这里是为了方便外部摄像头推流服务直接 POST 上报不用处理 CSRF Token。生产环境如果接口暴露在公网需要用签名校验替代这个装饰器否则任何人都能往里灌图片消耗你的 GPU 资源。管理后台页面基于 Django 模板渲染核心展示内容是选课时段内的行为比例柱状图和实时识别结果的表格。用 Chart.js 这类轻量前端库足够不必上重型前端框架。5. 部署避坑与性能调优把识别服务从“能跑”推到“稳跑”5.1 生产环境部署时最常见的 5 个坑坑一换了机器Torch 版本不一致导致加载报错现象把训练好的权重文件拷贝到另一台服务器torch.load()报RuntimeError: Invalid magic number或参数名不匹配。原因训练机和部署机的 PyTorch 版本差异过大权重序列化格式不兼容。解决在训练机上用torch.save(model.state_dict(), behavior_best.pth)只保存参数不保存完整模型对象部署机尽量保持与训练机相同的 PyTorch 版本。如果不方便对齐版本可以在加载后用weights_onlyTrue参数限制反序列化内容。坑二GPU 显存碎片导致多路视频流推理 OOM现象服务运行几小时后某路视频帧上报时进程崩溃错误显示CUDA out of memory。原因每帧的中间张量在显存上反复分配释放且 Django 默认开启的线程池会让并发推理叠加。解决在视图外面加一个信号量锁把并发推理数限制为 1确保同一时间只有一个推理任务占用 GPU。如果必须并行处理多路流给每一路流分配独立的 GPU 显存空间不要让推理代码共享同一个 torch 默认device。坑三cv2.imdecode读不了来自 DjangoUploadedFile的字节流现象图片上传后识别结果全为空打印日志发现frame is None。原因request.FILES的read()返回已经是bytes但np.frombuffer默认dtype不对导致imdecode无法解析。解决在np.frombuffer(data, np.uint8)中明确指定uint8类型这是最容易忽略的细节。坑四无 GPU 环境下用 CPU 推理慢到无法接受现象没有 NVIDIA 显卡的服务器上单帧推理耗时接近 1 秒实时性完全丧失。原因ResNet18 和 YOLO 在 CPU 上计算量依然大且默认不启用 MKLDNN 加速。解决在推理前调用torch.backends.mkldnn.enabled True并确认推理输入是torch.channels_last内存格式。有条件的情况下把输入分辨率从 640×640 降到 480×480检测精度损失不大但推理耗时能下降约 30%。坑五数据库记录增长速度超出预期查询开始变慢现象接入十间教室后BehaviorRecord 表每天增加几十万条后台报表页面响应时间超过 5 秒。原因没有按时间做分区或归档策略。解决在frame_time字段上建复合索引联合behavior字段并且设置定期归档任务——把一个月前的原始记录转移到历史表实时查询只保留近期热数据。5.2 性能瓶颈的量化定位顺序与配置清单如果系统整体响应慢先不要急着调模型用分段计时确定瓶颈位置。常见做法是在视图函数里对以下五个阶段分别打点文件读取与解码耗时、检测模型推理耗时、分类模型推理耗时、数据库写入耗时、JSON 序列化耗时。我的参考经验是一次典型的 POST 请求中检测推理占比 60%分类推理占比 25%图像解码占比 10%数据库写入占比不到 5%。如果数据库写入占比异常升高优先检查是否在for循环内逐条调用save()批量写入应改为bulk_create()。Django 自带的开发服务器runserver绝不能用于生产。生产环境下用 Gunicorn 配合 Nginx 是主流组合但要注意 Gunicorn 的 worker 类型需要用gthread而不是默认的同步 worker否则图片上传接口会被阻塞。更稳妥的做法是让 Gunicorn worker 数和 GPU 推理锁数量保持一致避免多 worker 同时进入推理代码造成显存竞争。5.3 模型导出与中间表示避开训练框架和部署框架的版本纠缠如果部署机器不方便安装完整 PyTorch 环境可以先把模型导出成 ONNX 格式再用 ONNX Runtime 做推理。以行为分类模型为例导出命令只需要十几行代码import torch from torchvision import models model models.resnet18(num_classes6) checkpoint torch.load(weights/behavior_best.pth, map_locationcpu) model.load_state_dict(checkpoint[state_dict]) model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, behavior_model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, opset_version11 )导出后验证环节容易被忽略正确做法是拿同一张图分别走 PyTorch 模型和 ONNX Runtime对比输出的概率分布差异。两者误差在 1e-4 量级属于正常如果误差明显偏大多半是因为 BatchNorm 层没有冻结回到代码检查是否在导出前调用了model.eval()。ONNX 格式的真正价值不只是脱离 PyTorch 环境而是可以进一步转成 TensorRT 或 OpenVINO 格式在特定硬件上获得两三倍的推理加速。不过这些高级优化只在单路视频流都撑不住的时候才值得投入常规部署下 ONNX Runtime 的 CPU 推理性能已经够用。6. 进阶用法模型量化与视频流接入换来自动化课堂分析系统跑通之后最值得做的进阶工作是两件事把单张图片识别的能力延伸到实时视频流再把模型从 FP32 压到 INT8让成本更低的机器也能扛住多路推流。视频流接入层面最省事的方案是用 OpenCV 的VideoCapture拉取 RTSP 流在循环中按帧调用现有推理函数。但考虑到 RTSP 流的断线重连问题——摄像头偶尔会掉线我开始时没处理结果服务静默空转了一整节课。后来在封装层加了心跳检测并用线程池管理多路摄像头才把稳定性提上去。一个可靠的视频流消费循环结构大致是连接 → 读帧 → 推理 → 推送结果 → 按帧率控制 sleep断线时自动退避重连重连三次失败则标记通道离线并上报告警。量化的收益非常直观ResNet18 从 FP32 转成 INT8 后参数量不变但计算量大幅下降CPU 推理延迟通常能降低 50%模型文件也缩小到原来的四分之一。PyTorch 的量化接口几行代码就能完成动态量化非常适合 CPU 部署场景import torch from torchvision import models model models.resnet18(num_classes6) checkpoint torch.load(weights/behavior_best.pth, map_locationcpu) model.load_state_dict(checkpoint[state_dict]) model.eval() quantized_model torch.quantization.quantize_dynamic( model, {torch.nn.Linear, torch.nn.Conv2d}, dtypetorch.qint8 ) torch.save(quantized_model.state_dict(), behavior_quantized.pth)量化后的模型会有极其细微的精度变化通常在 1 个百分点以内但推理速度的提升却很明显。这里要提醒一个常见翻车点量化模型在 CPU 上速度快但如果你部署机上有 GPU量化模型反而可能比 FP32 更慢——因为 GPU 对低精度运算的支持依赖 TensorRT 这类专门工具原生 PyTorch 在 GPU 上跑 INT8 没有优势。所以我的习惯是先确定部署硬件再决定是否量化而不是先量化再去找机器。整套系统的验收方法我会在真实教室录一段 30 分钟的视频然后人眼统计片段中抬头、趴桌、玩手机各自出现的时间段再和系统输出做对比。准确率能对上八成就说明模型真正在目标场景上工作了。这个动作看起来笨却是最有效的上线前测试——比任何评测指标都能暴露问题。我吃过亏曾因贪图快、跳过真实场景验证直接上线结果第一节课就因为窗帘拉上后光线骤暗、误报率飙升而翻车。后来在数据增强里加了更重的亮度扰动又在部署侧对输出做了置信度平滑才把这个场景彻底稳定下来。这个方向值得投入但投入前一定要想清楚你要交付的是一套能跑通的数据链路而不是一个孤零零的模型精度数字。希望这些踩坑换来的经验能帮你少走一段弯路把课堂行为识别系统稳稳落到地。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站