简介这份资源是面向人工智能与教育技术方向学习者、开发者的一份深度学习实践项目包聚焦课堂场景下的学生专注度行为识别适合具备Python基础、希望将计算机视觉与行为分析落地到真实教学场景的中级学习者参考。压缩包共2个文件以1个Python脚本和1个XML配置文件为主前者承载模型搭建与识别逻辑后者用于项目环境或模块配置整体仅约2KB属于轻量级代码骨架。目前已有435人学习下载说明该方向具备一定关注度。项目围绕视频流输入展开涉及图像预处理、人脸检测、眼睛与嘴巴状态分析等计算机视觉环节并可能结合CNN与RNN处理面部表情与动作时序从而判断学生是否专注听讲。读者可借此理解从数据标注、特征提取到模型训练与部署的完整思路并借助TensorFlow、Keras、OpenCV等工具快速复现同时需关注课堂视频采集中的隐私与伦理边界。1. 课堂专注度行为识别从一段监控视频到可用的专注度分数课堂专注度行为识别系统本质是把一段普通教室监控视频变成每个学生、每节课的专注度分数。它要解决的是老师无法同时观察几十个学生状态的问题谁在抬头听讲、谁在低头写字、谁趴在桌上、谁转头说话。基于深度学习的方案通常用目标检测定位人头再用行为分类网络判断姿态类别最后按时间窗口聚合成专注度指标。这套东西适合做教育信息化产品、毕设项目或者学校教务的课堂质量分析。我做过几版最大的体会是模型精度不是瓶颈数据标注口径和摄像头角度才是。下面把我踩过的路径完整讲一遍从环境到训练到部署能照着复现。2. 系统拆成三段检测、分类、聚合以及为什么这样选2.1 为什么不用端到端视频分类直接出分数很多人第一反应是拿 SlowFast、TimeSformer 这类视频分类网络输入一段 clip 直接回归专注度。我试过翻车得很快。原因有三个第一教室场景里学生数量不固定端到端网络输出维度固定没法对应到具体某个人第二专注度是个人属性不是整帧属性一个班有人专注有人走神整帧标签没有意义第三端到端模型对摄像头角度极其敏感换个教室就要重新标数据。所以工程上更稳的做法是拆成三段流水线检测负责“人在哪”分类负责“这个人在干什么”聚合负责“这段时间他专注吗”。每段可以独立替换、独立调优出问题也好定位。常见做法是 YOLO 系列做检测轻量 CNN 做行为分类时间窗口做加权聚合。2.2 行为类别怎么定义才不返工这是整个项目最容易返工的地方。我第一版定义了 8 类听讲、写字、举手、趴桌、转头、站立、低头玩手机、交头接耳。标注到一半发现“低头写字”和“低头玩手机”在监控分辨率下根本分不清标注员自己都打架kappa 系数只有 0.6。后来收敛成 5 类可操作性强很多类别判定依据专注权重抬头听讲面部朝向讲台头部俯仰角小于 20 度1.0低头书写头部俯仰角大于 30 度手部在桌面区域0.8趴桌头部接近桌面平面持续超过 3 秒0.1转头头部偏航角超过 45 度0.3站立人体框高宽比明显大于坐姿0.2权重不是拍脑袋是拿一节课的人工专注度打分做线性回归拟合出来的。这套定义的好处是每一类都有明确的几何判据标注员培训半小时就能上手一致性上到 0.85 以上。2.3 检测模型选型YOLOv8n 够不够用教室场景的检测难点是遮挡和小目标。后排学生人头在 1080P 画面里可能只有 40 像素高。我对比过 YOLOv8n、YOLOv8s 和 RT-DETR。在自建 3000 张标注数据上YOLOv8n 的 mAP0.5 是 0.89YOLOv8s 是 0.93RT-DETR 是 0.94 但推理慢一倍。如果部署在边缘盒子或者普通服务器 CPU 上YOLOv8n 是性价比最高的。如果追求精度且有 GPU上 YOLOv8s。检测类别只标“人”一类就行不要试图检测“人头”因为人头框在遮挡时抖动严重人体框更稳。2.4 分类网络从人体框到行为标签拿到人体框后裁剪出 ROI送进分类网络。这里有个细节直接用人体框裁剪会带很多背景我一般把框向外扩 1.2 倍保证头部和手部完整。分类网络用 ResNet18 就够了输入 224x2245 分类。训练时用人体框裁剪图推理时也一样保持分布一致。如果数据量少于 5000 张建议先冻结 backbone 训练分类头再整体微调。学习率用 1e-3 和 1e-4 分阶段。数据增强用随机水平翻转、颜色抖动、随机擦除。注意不要用随机裁剪会破坏头部和手部的空间关系。3. 从零跑通训练环境、数据、代码三件事3.1 环境配置miniconda 加 PyTorch 的最小组合深度学习环境配置是新手第一道坎。我一般用 miniconda 建独立环境避免和系统 Python 打架。CUDA 版本要和显卡驱动匹配别盲目装最新。# 创建环境Python 用 3.10兼容性好 conda create -n focus python3.10 -y conda activate focus # 安装 PyTorch以 CUDA 11.8 为例去官网查对应命令 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装检测和工具库 pip install ultralytics opencv-python pandas scikit-learn装完先验证 GPU 是否可用import torch print(torch.__version__) print(torch.cuda.is_available()) # 必须是 True print(torch.cuda.get_device_name(0))如果cuda.is_available()是 False先查驱动版本nvidia-smi再查 PyTorch 对应的 CUDA 版本。常见坑是装了 CPU 版 PyTorch重装即可。租用服务器跑深度学习的话选镜像时直接选带 PyTorch 的省掉这一步。3.2 数据标注用 YOLO 格式还是 COCO检测标注用 YOLO 格式最省事一个 txt 对应一张图每行类别 x_center y_center width height坐标归一化到 0-1。分类标注我单独建一个 csv字段是image_path,label裁剪图存到分类数据集目录。目录结构建议这样dataset/ detection/ images/train/ images/val/ labels/train/ labels/val/ classification/ train/抬头听讲/ train/低头书写/ ... val/抬头听讲/ val/低头书写/ ...标注工具用 labelImg 或 X-AnyLabeling 都行。关键是标注规范要写清楚人体框包含完整身体被遮挡超过 50% 的不标画面边缘只露一半的不标。这些规则不写标注员会给你标出一堆噪声。3.3 检测训练YOLOv8 命令行直接开跑YOLOv8 的训练接口很干净写个 yaml 描述数据集路径和类别然后一行命令。# focus_det.yaml path: /data/dataset/detection train: images/train val: images/val names: 0: personyolo detect train \ modelyolov8n.pt \ datafocus_det.yaml \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ device0参数说明imgsz640是输入尺寸教室画面建议用 640 或 960太小后排人头会丢batch16看显存8G 显存跑 640 大概能到 16patience20是早停20 轮没提升就停省时间lr00.01是初始学习率YOLOv8 默认值就够用。训练完看runs/detect/train/weights/best.pt。验证时重点看小目标的召回率。如果后排漏检多把imgsz提到 960或者用切片推理把画面切成四块分别检测再合并。3.4 分类训练ResNet18 微调脚本分类训练我习惯自己写训练循环可控性强。核心是数据加载和分阶段学习率。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms, models # 数据增强训练用强增强验证只做 resize train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.2, 0.2, 0.2), transforms.RandomErasing(p0.25), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) 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, train_tf) val_ds datasets.ImageFolder(dataset/classification/val, val_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) # 加载预训练 ResNet18替换分类头 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(512, len(train_ds.classes)) model model.cuda() # 第一阶段冻结 backbone只训分类头 for p in model.parameters(): p.requires_grad False for p in model.fc.parameters(): p.requires_grad True criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3) for epoch in range(5): model.train() for x, y in train_loader: x, y x.cuda(), y.cuda() optimizer.zero_grad() loss criterion(model(x), y) loss.backward() optimizer.step() print(fstage1 epoch {epoch} done) # 第二阶段解冻全部小学习率微调 for p in model.parameters(): p.requires_grad True optimizer torch.optim.Adam(model.parameters(), lr1e-4) best_acc 0 for epoch in range(20): model.train() for x, y in train_loader: x, y x.cuda(), y.cuda() optimizer.zero_grad() loss criterion(model(x), y) loss.backward() optimizer.step() # 验证 model.eval() correct total 0 with torch.no_grad(): for x, y in val_loader: x, y x.cuda(), y.cuda() pred model(x).argmax(1) correct (pred y).sum().item() total y.size(0) acc correct / total print(fepoch {epoch} val_acc {acc:.4f}) if acc best_acc: best_acc acc torch.save(model.state_dict(), best_cls.pth)逻辑说明先冻结 backbone 训 5 轮让随机初始化的分类头先收敛避免一上来就把预训练特征带偏再解冻全部用 1e-4 微调。RandomErasing模拟遮挡对教室场景很有用。num_workers4加快数据加载Windows 下如果报错改成 0。验证只保存最好的权重避免过拟合。3.5 聚合逻辑从逐帧标签到一节课的专注度曲线检测和分类都是逐帧的最终要聚合成时间序列。我一般按 1 秒一个窗口统计每个学生的行为分布再按权重算分。import pandas as pd # 假设 records 是逐帧结果frame, student_id, behavior WEIGHTS {抬头听讲: 1.0, 低头书写: 0.8, 趴桌: 0.1, 转头: 0.3, 站立: 0.2} def aggregate(records, fps25, window_sec60): records[sec] records[frame] // fps records[window] records[sec] // window_sec records[score] records[behavior].map(WEIGHTS) # 每个学生每个窗口的平均分 result records.groupby([student_id, window])[score].mean().reset_index() return result参数说明fps要和视频实际帧率一致不一致会导致时间轴错位window_sec60是一分钟一个聚合点太短抖动大太长看不出变化。学生 ID 靠检测框的 IoU 跟踪来维持简单场景用 ByteTrack 就够复杂场景再上 ReID。4. 避坑与排查那些让我加班到凌晨的问题4.1 检测框抖动导致行为标签跳变现象同一个学生前一帧判“抬头听讲”后一帧判“转头”专注度曲线像心电图。原因检测框每帧有微小偏移裁剪出的 ROI 跟着抖分类网络对位置敏感。解决对检测框做指数平滑box_t 0.7 * box_t 0.3 * box_{t-1}分类结果再做多数投票连续 5 帧里取众数。这两步做完曲线就稳了。4.2 后排小目标漏检严重现象前排学生检测正常后排经常整排丢失。原因输入尺寸 640 时后排人头只有 20 多像素低于网络有效感受野。解决训练时把imgsz提到 960或者用 SAHI 切片推理把画面切成 2x2 分别检测再 NMS 合并。代价是推理时间增加约 2 倍但召回率能提 15 个点以上。4.3 行为分类把“低头书写”误判成“趴桌”现象学生正常低头写字被大量判成趴桌专注度分数异常低。原因趴桌和低头书写在人体框裁剪图里头部位置接近分类网络没学到手部特征。解决裁剪时把框向下扩展保证桌面和手部进入画面训练数据里补充“低头书写”的难例特别是手在桌面上的样本。另外可以在分类网络里加一个手部关键点分支但工程上先补数据更划算。4.4 摄像头角度一变模型集体失效现象在 A 教室训好的模型搬到 B 教室精度掉 20 个点。原因俯仰角、光照、座位排列都变了模型过拟合了 A 教室的分布。解决训练数据尽量覆盖多个教室、多个时间段、多种光照。如果只能单教室推理前做透视校正把画面映射到标准俯视角。另外检测模型比分类模型更抗角度变化所以分类的 ROI 归一化要做扎实。4.5 推理速度跟不上实时现象GPU 上单帧要 80ms25fps 视频根本跑不动。原因检测和分类串行且分类对每个学生都跑一次。解决检测每 5 帧跑一次中间帧用跟踪框分类对每个学生每 10 帧跑一次中间帧沿用上次结果。这样整体能压到 30ms 以内。如果还慢把 ResNet18 换成 MobileNetV3精度掉 2 个点速度翻倍。5. 进阶技巧用对比实验和阈值调优把系统调到能用系统能跑通只是及格能不能上线看的是稳定性和可解释性。我最后收尾一般做两件事对比实验和阈值调优。对比实验怎么做才有效不要只比 mAP 和准确率。我一般固定检测模型只换分类网络看端到端专注度分数和人工打分的相关性。指标用 Spearman 相关系数比准确率更贴近业务。下面是我做过的一组对比分类网络分类准确率端到端相关系数单帧耗时ResNet180.910.8212msMobileNetV30.890.806msEfficientNet-B00.920.8315msViT-B/160.930.8145ms可以看到 ViT 分类准确率最高但端到端相关系数反而降了因为它的预测抖动大聚合后噪声被放大。所以选型要看端到端指标别被单模型指标带偏。阈值调优是另一个关键。专注度分数是连续值但业务上往往要分“专注、一般、走神”三档。分档阈值不要拍脑袋用人工标注的课堂片段做 ROC 分析找 Youden 指数最大的点。我一般还会加一个时间平滑连续 3 个窗口低于阈值才判走神避免误报。最后说个我自己的习惯每次改完模型一定拿同一段 10 分钟的视频跑回归测试把专注度曲线和上一版叠在一起看。曲线形状变了哪怕指标涨了也要查原因。这个习惯帮我拦下过好几次“指标好看但业务不可用”的改动。希望帮到你。本文还有配套的精品资源点击获取