news 2026/10/2 4:41:12

基于YOLOv8与ResNet18的课堂专注度行为识别系统实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于YOLOv8与ResNet18的课堂专注度行为识别系统实战

简介:这份资源是面向人工智能与教育技术方向学习者、开发者的一份深度学习实践项目包,聚焦课堂场景下的学生专注度行为识别,适合具备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 的 mAP@0.5 是 0.89,YOLOv8s 是 0.93,RT-DETR 是 0.94 但推理慢一倍。

如果部署在边缘盒子或者普通服务器 CPU 上,YOLOv8n 是性价比最高的。如果追求精度且有 GPU,上 YOLOv8s。检测类别只标“人”一类就行,不要试图检测“人头”,因为人头框在遮挡时抖动严重,人体框更稳。

2.4 分类网络:从人体框到行为标签

拿到人体框后,裁剪出 ROI,送进分类网络。这里有个细节:直接用人体框裁剪会带很多背景,我一般把框向外扩 1.2 倍,保证头部和手部完整。分类网络用 ResNet18 就够了,输入 224x224,5 分类。训练时用人体框裁剪图,推理时也一样,保持分布一致。

如果数据量少于 5000 张,建议先冻结 backbone 训练分类头,再整体微调。学习率用 1e-3 和 1e-4 分阶段。数据增强用随机水平翻转、颜色抖动、随机擦除。注意不要用随机裁剪,会破坏头部和手部的空间关系。

3. 从零跑通训练:环境、数据、代码三件事

3.1 环境配置:miniconda 加 PyTorch 的最小组合

深度学习环境配置是新手第一道坎。我一般用 miniconda 建独立环境,避免和系统 Python 打架。CUDA 版本要和显卡驱动匹配,别盲目装最新。

# 创建环境,Python 用 3.10,兼容性好 conda create -n focus python=3.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: person
yolo detect train \ model=yolov8n.pt \ data=focus_det.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ device=0

参数说明:imgsz=640是输入尺寸,教室画面建议用 640 或 960,太小后排人头会丢;batch=16看显存,8G 显存跑 640 大概能到 16;patience=20是早停,20 轮没提升就停,省时间;lr0=0.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(p=0.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_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4) # 加载预训练 ResNet18,替换分类头 model = models.resnet18(weights=models.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(), lr=1e-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(f'stage1 epoch {epoch} done') # 第二阶段:解冻全部,小学习率微调 for p in model.parameters(): p.requires_grad = True optimizer = torch.optim.Adam(model.parameters(), lr=1e-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(f'epoch {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_workers=4加快数据加载,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, fps=25, window_sec=60): 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_sec=60是一分钟一个聚合点,太短抖动大,太长看不出变化。学生 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 上单帧要 80ms,25fps 视频根本跑不动。

原因:检测和分类串行,且分类对每个学生都跑一次。

解决:检测每 5 帧跑一次,中间帧用跟踪框;分类对每个学生每 10 帧跑一次,中间帧沿用上次结果。这样整体能压到 30ms 以内。如果还慢,把 ResNet18 换成 MobileNetV3,精度掉 2 个点,速度翻倍。

5. 进阶技巧:用对比实验和阈值调优把系统调到能用

系统能跑通只是及格,能不能上线看的是稳定性和可解释性。我最后收尾一般做两件事:对比实验和阈值调优。

对比实验怎么做才有效?不要只比 mAP 和准确率。我一般固定检测模型,只换分类网络,看端到端专注度分数和人工打分的相关性。指标用 Spearman 相关系数,比准确率更贴近业务。下面是我做过的一组对比:

分类网络分类准确率端到端相关系数单帧耗时
ResNet180.910.8212ms
MobileNetV30.890.806ms
EfficientNet-B00.920.8315ms
ViT-B/160.930.8145ms

可以看到 ViT 分类准确率最高,但端到端相关系数反而降了,因为它的预测抖动大,聚合后噪声被放大。所以选型要看端到端指标,别被单模型指标带偏。

阈值调优是另一个关键。专注度分数是连续值,但业务上往往要分“专注、一般、走神”三档。分档阈值不要拍脑袋,用人工标注的课堂片段做 ROC 分析,找 Youden 指数最大的点。我一般还会加一个时间平滑:连续 3 个窗口低于阈值才判走神,避免误报。

最后说个我自己的习惯:每次改完模型,一定拿同一段 10 分钟的视频跑回归测试,把专注度曲线和上一版叠在一起看。曲线形状变了,哪怕指标涨了也要查原因。这个习惯帮我拦下过好几次“指标好看但业务不可用”的改动。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 4:38:32

办公流畅但游戏掉帧?6步排查系统设置与驱动配置

1. 问题定位:为什么办公流畅但游戏掉帧1.1 先搞清楚“卡”和“掉帧”是两码事很多人一遇到游戏不流畅,第一反应就是“电脑不行了,该换了”。但如果你办公时开几十个网页、同时跑Word和Excel都丝滑顺畅,一进游戏就掉帧,…

作者头像 李华
网站建设 2026/10/2 4:38:17

基环树路径查询:函数图、倍增表与LCA思想解析

1. 看到"每个行星只有一条出边",你就该知道这是基环树Planets Queries II 这道题,我在图论题单里碰到过好几次了。题面本身并不复杂:宇宙中有 n 个行星,每个行星恰好发射一条单向航线到另一个行星,然后给你 …

作者头像 李华
网站建设 2026/10/2 4:38:05

大模型推理集群从单卡到千卡:负载均衡与架构设计实战

1. 从单卡到千卡:先搞清楚我们要解决什么问题先说个真实场景。很多人第一次接触大模型推理,是从单卡跑Qwen、Llama这类开源模型开始的。一张卡,装个vLLM或者TGI,起个服务,接口调通,感觉“推理也没多难嘛”。…

作者头像 李华
网站建设 2026/10/2 4:38:04

风格化渲染系统实战:色阶光照、手绘贴图与混合描边方案

做风格化渲染系统,最尴尬的往往不是技术实现,而是目标模糊:想做得像吉卜力,又想要卡通渲染的干净轮廓,还想保留一点点手绘质感,最后出来的东西四不像。我在定这个项目目标的时候,直接把风格收敛…

作者头像 李华
网站建设 2026/10/2 4:37:49

pip install报403?远程wheel链接失效的排查与修复指南

前两天帮朋友排查一个 ComfyUI 环境问题,本来只是缺节点,按照提示执行pip install -r requirements.txt,结果屏幕上刷出来一大片403 Forbidden,而且错误明确指向远程 wheel 文件链接。这种报错在现在的 Python 项目里越来越常见&a…

作者头像 李华
网站建设 2026/10/2 4:37:26

Qt QLabel样式深度解析:QPalette、QFont与QSS协同原理

1. QLabel样式设置:不只是改个颜色那么简单QLabel是Qt中最基础、最常用的控件之一,表面看它只是个“贴标签”的小部件,但实际在工业级GUI开发中,它承担着状态提示、实时数据展示、图文混排、动态反馈等关键任务。我做过十几个Qt桌…

作者头像 李华