
简介这份资源是一套基于深度学习与Python实现的课堂专注度行为识别系统面向计算机、人工智能、自动化等专业的高校学生、教师及科研从业者可用于毕业设计、课程设计、项目立项演示或自学进阶。压缩包共约2000个文件整体114.36MB涵盖Java、Vue、Python、C、JavaScript、XML、Markdown等多种类型Java与Vue支撑前后端交互界面Python与C负责模型推理与算子实现Markdown与PDF提供项目说明和设计文档结构完整、层次清晰。资源内含完整源码、模型权重与项目说明代码经过测试可正常运行读者可据此理解行为识别模型的训练与部署流程掌握从数据到界面展示的完整链路并在此基础上修改扩展功能。目前已有77人学习下载适合需要快速上手深度学习实战或完成毕设课设的读者参考借鉴。1. 课堂专注度行为识别从一段课堂录像到可落地的深度学习方案一间普通教室四五十个学生老师在上面讲课下面有人抬头听讲、有人低头写字、有人趴桌子、有人扭头说话。过去要判断这节课的听课状态只能靠老师的主观感受或者督导推门听一节。现在换个思路架一台普通摄像头把课堂录像喂给一个基于深度学习的行为识别系统让它逐帧判断每个学生的行为类别再聚合成整堂课的专注度曲线。这就是「基于深度学习python开发的课堂专注度行为识别系统」要解决的事。它适合三类人一是做教育信息化产品、想加行为分析模块的开发者二是计算机视觉方向的学生想找一个能跑通、能改、能写进简历的完整项目三是学校或机构的教研人员想用数据而不是感觉来评估课堂。核心链路并不复杂视频抽帧、人体检测、行为分类、专注度打分、可视化输出。难点在于课堂场景本身很脏——遮挡、小目标、光照变化、多人重叠这些才是决定系统能不能用的关键。下面按「先立住原理、再动手复现、最后讲坑」的顺序拆开讲。2. 课堂行为识别的技术选型为什么是检测加分类而不是端到端2.1 课堂场景的三个硬约束决定了架构课堂行为识别和通用动作识别比如 UCF101 那种不是一回事它有三个绕不开的约束。第一是多人同时出现一帧里可能有几十个目标端到端视频分类模型如 SlowFast、TimeSformer通常输出的是「整段视频属于哪一类」没法告诉你「第三排左边那个学生在干什么」。第二是行为粒度细抬头听讲和低头写字在像素层面差别很小主要靠头部姿态和手部位置区分。第三是实时性要求一节课 45 分钟如果处理速度慢于视频时长就没法做在线分析。所以主流做法是两阶段先用目标检测把人框出来再对每个人框做行为分类。检测负责「谁在哪」分类负责「在干什么」。这样每个学生的行为是独立的聚合起来就能算专注度。端到端方案不是不能用而是标注成本高、可解释性差出了问题你根本不知道是检测错了还是分类错了排查起来就是黑匣子。2.2 检测器选型YOLO 系列为什么是课堂场景的默认答案检测环节我一般直接用 YOLO 系列。原因很实际课堂视频要抽帧一节课按 1fps 抽也有 2700 帧用两阶段检测器Faster R-CNN 那类速度扛不住。YOLO 单阶段、速度快、精度够用而且 Python 生态成熟ultralytics这个库几行就能跑起来。选版本时看你的硬件。有独立显卡哪怕 6G 显存就上 YOLOv8n 或 YOLOv8sn 版本在 1080p 课堂画面上对小目标召回一般s 版本更稳。只有 CPU 或者低显存就用 YOLOv8n 配合输入尺寸降到 640 甚至 480。这里有个血泪经验课堂后排的学生在画面里可能只有 30 到 50 像素高输入尺寸降太狠会直接漏检宁可牺牲一点速度也别把分辨率压到 480 以下。from ultralytics import YOLO # 加载预训练权重课堂场景建议用 s 版本平衡精度和速度 model YOLO(yolov8s.pt) # 对单帧做检测conf 调低一点保证后排小目标不漏 results model.predict( sourceclassroom_frame.jpg, conf0.25, # 置信度阈值课堂遮挡多0.25 比默认 0.5 召回更好 iou0.5, # NMS 的 IoU 阈值人多重叠时别调太低 imgsz960, # 输入尺寸960 比 640 对后排小目标友好 classes[0], # 只保留 person 类COCO 里 person 的 id 是 0 verboseFalse ) # 提取每个人框的坐标供后续行为分类裁剪使用 boxes results[0].boxes.xyxy.cpu().numpy() print(f本帧检测到 {len(boxes)} 个人)这段代码的逻辑是加载 YOLO 权重对一帧画面做推理只保留人这一类。参数里conf0.25是关键课堂里学生被桌椅、前排同学遮挡默认 0.5 会漏掉很多人imgsz960是为了让后排小目标有足够像素classes[0]过滤掉桌椅、投影仪等无关目标减少后续分类的干扰。跑完拿到的boxes就是每个学生的位置下一步按框裁剪出小图送进行为分类模型。2.3 行为分类轻量 CNN 够用别一上来就上 Transformer行为分类这块很多人第一反应是上 Transformer 或者 3D 卷积。我的建议是先用轻量 2D CNN 把基线跑通。课堂行为类别通常就 5 到 8 类——听讲、写字、趴桌、扭头、举手、站立这些类别的区分主要靠单帧的头部姿态和身体朝向时序信息有帮助但不是决定性的。用 ResNet18 或 MobileNetV3 在裁剪出的人框上做分类单帧准确率就能到 85% 以上推理速度还快。什么时候才需要时序模型当你要区分「短暂低头看笔记」和「持续趴桌睡觉」这种需要看一段时间的类别时。这时候可以在 CNN 特征上接一个 LSTM 或者用滑动窗口把连续几帧的特征拼起来。但这是第二步优化不是第一步。我见过太多人一上来就搭 3D 网络结果数据量不够、训练不收敛、调参调到怀疑人生最后连基线都没跑出来。import torch import torch.nn as nn from torchvision import models # 用 ResNet18 做行为分类骨干类别数按你的数据集改 class BehaviorClassifier(nn.Module): def __init__(self, num_classes6): super().__init__() # 加载预训练权重课堂数据通常不够从头训 self.backbone models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) # 替换最后的全连接层输出改成行为类别数 in_features self.backbone.fc.in_features self.backbone.fc nn.Linear(in_features, num_classes) def forward(self, x): return self.backbone(x) # 输入是裁剪后的人框统一 resize 到 224x224 model BehaviorClassifier(num_classes6) dummy torch.randn(8, 3, 224, 224) # batch8 的示例输入 out model(dummy) print(out.shape) # torch.Size([8, 6])这里用预训练 ResNet18 替换最后一层是因为课堂行为数据集通常只有几千到几万张标注图从头训练容易过拟合。num_classes6对应你的行为类别数改这个数字就行。输入统一到 224x224 是 ImageNet 的标准尺寸和预训练权重匹配。实际训练时学习率要调小一般 1e-4 到 1e-3因为骨干网络已经学好了通用特征只需要微调。2.4 专注度打分把行为类别映射成一个可解释的分数检测和分类输出的是「谁在做什么」但用户要的是「这节课专注度怎么样」。中间需要一个映射规则。常见做法是给每种行为赋一个权重分听讲 1.0、举手 1.0、写字 0.8、扭头 0.3、趴桌 0.0然后按人数加权平均。这个权重不是拍脑袋最好结合教育学的课堂观察量表来定但工程上先用一套合理默认值跑通后面再根据实际反馈调。打分公式可以写成某时刻专注度 Σ(每个学生的行为分) / 学生总数。再按时间轴画出来就是一条专注度曲线。这条曲线比单帧判断有用得多——老师能直观看到哪个时间段学生状态下滑是不是讲到难点卡住了还是节奏太慢走神了。3. 从零跑通系统数据准备、训练和推理的完整步骤3.1 数据集怎么来、怎么标、怎么划分课堂行为识别最大的门槛不是模型是数据。公开数据集里SCB-Dataset 是专门做学生课堂行为的包含举手、看书、写字、趴桌等类别可以直接拿来用或者做预训练。但如果你要针对自己学校的场景还是得自己标。标注流程我一般这样走先用 YOLO 把所有人框自动检测出来导出成标注工具的预标注格式人工只需要改类别标签不用画框能省一大半时间。标注工具用 LabelImg 或者 CVAT 都行导出 YOLO 格式。类别定义要提前想清楚别标到一半又加类别返工成本很高。数据划分按视频级别分不能按帧随机分。同一节课的相邻帧长得几乎一样如果随机分到训练集和验证集验证集准确率会虚高实际部署就翻车。正确做法是训练集用 70% 的课验证集 15%测试集 15%同一节课的帧只出现在一个集合里。# 数据集目录结构YOLO 检测和行为分类各一套 dataset/ ├── detection/ # 检测数据集 │ ├── images/ │ │ ├── train/ │ │ └── val/ │ └── labels/ │ ├── train/ │ └── val/ └── classification/ # 行为分类数据集按类别分文件夹 ├── train/ │ ├── listen/ │ ├── write/ │ ├── lie/ │ └── turn/ └── val/ ├── listen/ └── ...这个结构是 YOLO 官方要求的格式检测的标签是 txt每行类别 x_center y_center width height坐标都归一化到 0 到 1。分类数据集按文件夹分每个文件夹一个类别PyTorch 的ImageFolder能直接读。注意分类数据是从检测框裁剪出来的裁剪时框往外扩 10% 到 20%保留一点上下文对区分扭头和听讲有帮助。3.2 检测模型训练关键参数和训练轮次检测模型训练用ultralytics的命令行或者 Python 都行。课堂场景我一般训 100 到 150 轮早停耐心设 30。学习率用默认的 0.01 配合余弦退火batch size 看显存8G 显存用 1612G 用 32。from ultralytics import YOLO model YOLO(yolov8s.pt) model.train( datadataset/detection/data.yaml, # 数据集配置文件路径 epochs120, # 训练轮次课堂数据一般 100 到 150 够 imgsz960, # 训练输入尺寸和推理保持一致 batch16, # 批大小按显存调 lr00.01, # 初始学习率 patience30, # 30 轮没提升就早停 device0, # 用第 0 号 GPUCPU 写 cpu projectruns/classroom, nameyolov8s_960 )data.yaml里要写清楚训练和验证图片路径、类别数和类别名。imgsz960和推理时保持一致训练和推理尺寸不一致会导致精度下降。patience30是防止过拟合课堂数据量不大训太久验证集 loss 会反弹。训练完看results.png里的 mAP50 曲线如果验证集 mAP 明显低于训练集说明过拟合了要么加数据增强要么减模型复杂度。3.3 行为分类训练迁移学习和数据增强的配合分类模型训练比检测简单核心是迁移学习加合适的数据增强。课堂行为分类的难点是类别不平衡——听讲和写字的样本远多于举手和站立。处理办法是在 loss 里加类别权重或者对少样本类别做过采样。import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms # 训练集增强随机裁剪、翻转、颜色抖动 train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), # 左右翻转行为类别不受方向影响 transforms.ColorJitter(0.2, 0.2, 0.2), # 应对教室光照变化 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) # 验证集只做 resize 和归一化不做增强 val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_ds datasets.ImageFolder(dataset/classification/train, transformtrain_tf) val_ds datasets.ImageFolder(dataset/classification/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) print(f类别: {train_ds.classes}) print(f训练样本: {len(train_ds)}, 验证样本: {len(val_ds)})增强里RandomHorizontalFlip对行为分类是安全的因为扭头向左和向右都算扭头翻转不改变类别语义。ColorJitter是应对不同教室的灯光色温差异。归一化用的 ImageNet 均值方差因为骨干是 ImageNet 预训练的。训练时用交叉熵损失优化器选 AdamW学习率 1e-4训 30 到 50 轮基本收敛。3.4 推理管线把检测、分类、打分串起来推理管线是整个系统的落地形态。输入一段课堂视频按固定帧率抽帧每帧过检测器裁出人框批量送分类器得到每个人的行为再算这一帧的专注度最后按时间聚合。import cv2 import numpy as np import torch from ultralytics import YOLO from torchvision import transforms from PIL import Image detector YOLO(runs/classroom/yolov8s_960/weights/best.pt) classifier BehaviorClassifier(num_classes6) classifier.load_state_dict(torch.load(behavior_resnet18.pth, map_locationcpu)) classifier.eval() # 行为到专注度分数的映射按你的教学场景调整 SCORE_MAP {listen: 1.0, raise: 1.0, write: 0.8, turn: 0.3, lie: 0.0, stand: 0.5} CLASS_NAMES [listen, write, lie, turn, raise, stand] preprocess transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) def process_frame(frame): results detector.predict(frame, conf0.25, imgsz960, classes[0], verboseFalse) boxes results[0].boxes.xyxy.cpu().numpy() if len(boxes) 0: return 0.0, [] crops [] for box in boxes: x1, y1, x2, y2 map(int, box) # 框往外扩 15%保留上下文 w, h x2 - x1, y2 - y1 x1 max(0, x1 - int(w * 0.15)) y1 max(0, y1 - int(h * 0.15)) x2 min(frame.shape[1], x2 int(w * 0.15)) y2 min(frame.shape[0], y2 int(h * 0.15)) crop frame[y1:y2, x1:x2] if crop.size 0: continue crops.append(preprocess(Image.fromarray(crop[:, :, ::-1]))) if not crops: return 0.0, [] batch torch.stack(crops) with torch.no_grad(): logits classifier(batch) preds logits.argmax(dim1).cpu().numpy() scores [SCORE_MAP[CLASS_NAMES[p]] for p in preds] frame_score float(np.mean(scores)) return frame_score, [CLASS_NAMES[p] for p in preds] # 按 1fps 抽帧处理整段视频 cap cv2.VideoCapture(classroom.mp4) fps cap.get(cv2.CAP_PROP_FPS) interval int(fps) # 每秒取一帧 timeline [] idx 0 while True: ret, frame cap.read() if not ret: break if idx % interval 0: score, behaviors process_frame(frame) timeline.append((idx / fps, score, behaviors)) idx 1 cap.release() # timeline 就是专注度曲线数据可存 CSV 或画图 print(f共处理 {len(timeline)} 个时间点)这段管线的逻辑逐帧检测人裁框时外扩 15% 保留上下文批量送分类器按映射表算分。interval int(fps)是每秒抽一帧45 分钟视频处理 2700 帧GPU 上大概几分钟跑完。SCORE_MAP是专注度映射你可以按实际教学场景调比如把「站立」调高还是调低取决于你是想抓走神还是抓互动。输出的timeline就是专注度曲线存成 CSV 后可以用 matplotlib 画出来。4. 避坑与排查课堂行为识别最容易翻车的五个地方4.1 后排学生漏检严重现象检测结果里前排学生框得很准后排几乎检测不到专注度曲线偏高。原因后排学生在画面里像素太少YOLO 默认输入 640 时一个学生可能只有 20 到 30 像素高低于模型的有效感受野。另外教室后排光照通常更暗对比度低。解决把推理输入尺寸提到 960 甚至 1280代价是速度下降但召回提升明显。如果速度不能降就在数据标注时专门多标后排样本让模型学会关注小目标。还可以在抽帧前对画面做一次直方图均衡化提升暗部对比度。4.2 相邻帧行为跳变曲线锯齿严重现象专注度曲线上下剧烈抖动同一秒内相邻帧分数差很多。原因单帧分类本身有噪声学生一个转头动作可能被连续几帧判成不同类别。另外检测框位置抖动也会导致裁剪出的图有差异。解决在时间维度做平滑。最简单的是滑动窗口平均窗口大小 5 到 10 帧。更好的做法是对每个学生的行为做跟踪用跟踪 ID 把同一个人的连续帧串起来再对每个人的行为序列做多数投票或隐马尔可夫平滑。滑动窗口滤波在这里是性价比最高的方案几行代码就能把曲线磨平。4.3 类别不平衡导致举手、站立几乎测不出来现象模型在验证集上整体准确率 90%但举手类别的召回只有 30%。原因课堂里举手和站立的样本天然少听讲和写字占 80% 以上模型倾向于全预测成多数类。解决训练时给交叉熵损失加类别权重权重和类别频率成反比。或者用重采样对少样本类别做过采样。评估时不能只看整体准确率要看每个类别的召回和 F1否则你根本发现不了这个问题。4.4 训练集验证集随机划分导致指标虚高现象验证集准确率 95%部署到新教室后掉到 60%。原因按帧随机划分同一节课的相邻帧几乎一样训练集和验证集高度相似模型相当于在背答案。解决按视频或按课次划分数据集确保同一节课的帧只出现在一个集合里。这个坑非常隐蔽很多人跑出高指标就以为成了实际一部署就露馅。4.5 光照和摄像头角度变化导致泛化差现象在 A 教室训的模型搬到 B 教室准确率大幅下降。原因不同教室的灯光色温、摄像头安装高度和角度都不同模型学到了和场景相关的虚假特征。解决训练数据尽量覆盖多个教室、多种光照条件。数据增强里加颜色抖动和随机裁剪。如果目标教室和训练教室差异大在目标教室采几百张图做微调效果比重新训整个模型好得多。5. 进阶技巧用跟踪加时序平滑把专注度曲线做稳前面讲的都是单帧管线实际部署时最影响体验的不是分类准确率而是曲线的稳定性。我踩过最深的坑就是单帧准确率明明有 88%但画出来的专注度曲线像心电图老师根本没法看。后来加了两步优化曲线才真正能用。第一步是目标跟踪。用 ByteTrack 或者简单的 IoU 匹配给每个学生分配一个跟踪 ID把连续帧里的同一个人串起来。这样你拿到的不再是一堆孤立的分类结果而是每个学生一条行为序列。跟踪本身不需要额外训练ultralytics里直接支持model.track()几行就能接上。第二步是时序平滑。对每个跟踪 ID 的行为序列用长度为 7 的滑动窗口做多数投票窗口内出现次数最多的类别作为当前帧的最终行为。这样单帧的误判会被邻帧纠正。如果还想更稳可以在投票前先做一次中值滤波去掉孤立的跳变。from collections import deque, Counter class BehaviorSmoother: def __init__(self, window7): self.window window self.buffers {} # 每个跟踪 ID 一个缓冲区 def update(self, track_id, behavior): if track_id not in self.buffers: self.buffers[track_id] deque(maxlenself.window) self.buffers[track_id].append(behavior) # 多数投票窗口内出现最多的类别胜出 most_common Counter(self.buffers[track_id]).most_common(1)[0][0] return most_common # 使用示例在跟踪循环里调用 smoother BehaviorSmoother(window7) # 假设 track_id3 的当前帧分类结果是 turn stable_behavior smoother.update(3, turn)这个平滑器的逻辑是每个跟踪 ID 维护一个长度为 7 的队列每次新行为进来后做多数投票。窗口大小 7 是经验值太小平滑不够太大响应迟钝。实际用的时候跟踪 ID 会断比如学生被完全遮挡几帧后 ID 变了这时候缓冲区要清理否则新 ID 会继承旧 ID 的历史导致行为判断滞后。处理办法是给每个缓冲区加一个时间戳超过 2 秒没更新就删掉。验证平滑效果的方法很直接同一段视频分别用平滑前和平滑后的管线跑一遍把两条专注度曲线画在一起对比。平滑后的曲线应该明显更连续同时整体趋势不变。如果平滑后曲线变得过于平坦说明窗口开太大了把真实的行为变化也磨掉了这时候把窗口降到 5 试试。还有一个容易被忽略的点专注度分数的映射表要可配置。不同学科、不同课型对行为的容忍度不一样。比如讨论课里「扭头」可能是在交流不该扣分自习课里「扭头」就是走神。所以映射表不要写死在代码里放到配置文件或者数据库里让教研人员能自己调。这个设计看起来小但决定了系统是「能用」还是「好用」。我自己做这类项目的习惯是先把单帧管线跑通指标能看就行别追求完美然后立刻加跟踪和平滑因为曲线稳定性对用户体验的影响远大于分类准确率从 85% 提到 90%最后再回头优化分类模型和数据。顺序反了很容易在模型调参上耗掉大半时间结果做出来的东西还是没法演示。希望帮到你。本文还有配套的精品资源点击获取