基于Python的深度学习课堂行为图像识别分类项目实战 简介这份资源是面向高校学生与深度学习入门者的课堂行为图像识别分类项目源码适用于毕业设计、期末大作业等场景帮助读者快速获得一套可运行、可复现的完整方案。压缩包共1206个文件以1183张jpg图像样本为主体配合14个py源码文件、3张png图示、2份md说明文档及license等辅助文件整体约100.29MB数据与代码分层清晰便于按模块查阅与二次训练。项目围绕课堂行为分类任务展开涵盖数据组织、模型搭建与训练推理等环节源码经过严格调试评审分达到95分以上可直接运行验证。目前已有571人学习下载适合希望省去从零搭建数据与代码框架、专注模型调优与论文撰写的读者参考使用。1. 从一张课堂照片说起这套 Python 深度学习图像识别分类项目到底解决什么问题上课铃响十分钟后排趴桌、中间刷手机、前排抬头看黑板——如果让你用人工数一遍一个 60 人的班至少要花五分钟还容易漏。基于 Python 的深度学习课堂行为图像识别分类项目要干的就是把这件事自动化输入一张教室监控截图或一段视频抽帧输出每个学生当前处于「听讲、举手、低头、趴桌、转头」中的哪一类。它属于典型的图像分类任务只不过类别不是猫狗而是课堂行为。这套源码适合三类人正在做课程设计、毕设的在校生需要一份能跑通、能改参数的完整工程想入门深度学习图像识别的开发者需要一个比 MNIST 更贴近真实场景的练手项目以及做智慧教室、教学行为分析的从业者想先验证技术可行性再决定要不要投入。核心链路并不复杂——数据标注、模型训练、推理部署难点在于课堂场景本身的光照变化、遮挡和类别不均衡。下面按「先立住原理、再动手复现、最后讲坑」的顺序拆开讲。2. 课堂行为识别的技术选型为什么是 CNN 而不是别的2.1 图像分类任务的本质与 CNN 的适配性图像分类的本质是学习一个从像素到类别的映射函数。一张 224×224 的 RGB 图片有 15 万个像素值如果直接用全连接网络参数量会爆炸且丢失空间信息。卷积神经网络CNN通过局部感受野、权值共享和池化操作恰好匹配了图像的平移不变性和局部相关性。课堂行为识别里「举手」这个动作无论出现在画面左上角还是右下角特征应该被同样识别——这正是卷积核滑动扫描带来的能力。常见做法是用迁移学习拿在 ImageNet 上预训练好的骨干网络ResNet18、MobileNetV3、EfficientNet-B0替换最后的全连接层为课堂行为的类别数再在自建数据集上微调。这样做的好处是即使你的课堂数据集只有几千张也能训出可用的模型。从零训练一个 CNN 在几千张图上几乎必然过拟合这是血泪经验。选骨干网络时看三个指标参数量、推理延迟、精度。ResNet18 约 11M 参数在 CPU 上单张推理约 30ms适合课堂实时性要求不极端的场景MobileNetV3-Small 只有 2.5M 参数适合部署到边缘设备EfficientNet-B0 精度更高但训练更吃显存。我一般先用 ResNet18 跑通全流程再根据部署环境换骨干。2.2 数据集的构建与标注规范课堂行为数据集没有现成的公开大规模版本必须自建。采集方式有两种一是从教室监控录像按固定间隔抽帧二是用手机在教室后排定点拍摄。抽帧间隔建议 12 秒一帧太密会导致相邻帧高度相似、信息冗余太疏会漏掉短时动作。标注用 LabelImg 或 CVAT每张图打一个主类别标签。类别定义要互斥且可判定推荐五类听讲抬头看前方、举手手臂举起、低头看桌面/手机、趴桌头枕手臂、转头侧脸偏离前方。标注时容易犯的错是边界模糊——「低头」和「趴桌」在侧视角下很难区分解决办法是固定拍摄机位并在标注规范里写明判定标准比如「头部低于肩线且持续超过 2 秒记为趴桌」。数据量方面每个类别至少 500 张五类合计 2500 张起步。如果某类样本明显偏少比如举手在真实课堂中很少出现需要用数据增强或过采样补齐。类别不均衡是课堂行为识别最隐蔽的坑模型会倾向于预测多数类表面准确率很高但少数类召回率极低。2.3 训练环境搭建与依赖安装环境用 Python 3.83.10 都行推荐 3.9。深度学习框架选 PyTorch理由是调试直观、社区资源多。如果你还在纠结 python 安装教程建议直接用 Anaconda 建虚拟环境避免污染系统 Python。# 创建虚拟环境 conda create -n classroom python3.9 -y conda activate classroom # 安装 PyTorch以 CUDA 11.8 为例无 GPU 则去掉 --index-url 那行 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装辅助库 pip install numpy pandas matplotlib opencv-python pillow scikit-learn tqdm tensorboard这段命令做了三件事建独立环境、装 PyTorch 及 torchvision、装数据处理和可视化库。参数说明--index-url指定 PyTorch 官方 CUDA 版本源如果你的显卡驱动只支持 CUDA 11.6就去 PyTorch 官网查对应命令替换。opencv-python用于视频抽帧和推理时的图像预处理tensorboard用于监控训练曲线。装完后用python -c import torch; print(torch.cuda.is_available())验证 GPU 是否可用返回 False 说明 CUDA 版本不匹配或驱动太旧。提示不要用pip install torch裸装默认会装 CPU 版训练时速度差十倍以上。3. 从源码到跑通数据加载、模型训练与推理的完整链路3.1 数据集目录结构与 DataLoader 配置源码工程通常按以下结构组织你拿到压缩包后先确认目录层级classroom_behavior/ ├── data/ │ ├── train/ │ │ ├── listen/ │ │ ├── hand_up/ │ │ ├── bow_head/ │ │ ├── lie_down/ │ │ └── turn_head/ │ ├── val/ │ │ └── 同上五类 │ └── test/ │ └── 同上五类 ├── models/ ├── utils/ ├── train.py ├── predict.py └── config.pyPyTorch 的ImageFolder要求每个类别一个子文件夹文件夹名即类别名。训练集、验证集、测试集按 7:2:1 划分划分时注意同一段视频的帧不能同时出现在训练集和验证集否则验证精度会虚高——这是数据泄漏很多人翻车在这里。import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集增强随机裁剪、翻转、颜色抖动 train_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 验证/测试集只做缩放和归一化 val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_ds datasets.ImageFolder(data/train, transformtrain_tf) val_ds datasets.ImageFolder(data/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(train_ds.classes) # 输出类别列表确认顺序逻辑说明训练集用RandomResizedCrop和RandomHorizontalFlip做增强模拟课堂中不同距离和朝向的变化ColorJitter应对教室灯光差异。归一化的均值和标准差是 ImageNet 的统计值因为骨干网络是在 ImageNet 上预训练的输入分布要一致。参数说明batch_size32在 8GB 显存下跑 ResNet18 比较稳显存不够就降到 16num_workers4是数据加载线程数Windows 下如果报错就改成 0pin_memoryTrue加速 GPU 传输。3.2 模型定义与迁移学习微调import torch.nn as nn from torchvision import models def build_model(num_classes5, backboneresnet18, pretrainedTrue): if backbone resnet18: model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT if pretrained else None) in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) elif backbone mobilenet_v3: model models.mobilenet_v3_small(weightsmodels.MobileNet_V3_Small_Weights.DEFAULT if pretrained else None) in_features model.classifier[-1].in_features model.classifier[-1] nn.Linear(in_features, num_classes) return model model build_model(num_classes5, backboneresnet18) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device)这里把 ResNet18 最后的全连接层替换成 Dropout 新全连接Dropout 率 0.3 是为了在小数据集上抑制过拟合。pretrainedTrue加载 ImageNet 预训练权重这是迁移学习的关键。如果你用 MobileNetV3注意它的分类头叫classifier而不是fc改错地方会导致替换无效、训练不动。3.3 训练循环与学习率调度import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR from tqdm import tqdm criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max30) def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total 0, 0, 0 for imgs, labels in tqdm(loader, desctrain): imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) correct (outputs.argmax(1) labels).sum().item() total imgs.size(0) return total_loss / total, correct / total torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total 0, 0, 0 for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) loss criterion(outputs, labels) total_loss loss.item() * imgs.size(0) correct (outputs.argmax(1) labels).sum().item() total imgs.size(0) return total_loss / total, correct / total best_acc 0 for epoch in range(30): tr_loss, tr_acc train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) scheduler.step() print(fEpoch {epoch1}: train_acc{tr_acc:.4f} val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), models/best.pth)逻辑说明用 AdamW 优化器它比 SGD 在微调场景下收敛更快weight_decay1e-4是 L2 正则抑制过拟合。CosineAnnealingLR让学习率按余弦曲线从 1e-3 降到接近 0前期大步走、后期小步调。每个 epoch 结束后在验证集上评估只保存验证精度最高的权重避免保存过拟合的最后一轮。参数说明T_max30对应总 epoch 数如果你改成 50 轮这里也要改lr1e-3是微调的常用起点如果 loss 震荡厉害就降到 5e-4。3.4 单张图片与视频流的推理from PIL import Image import torch.nn.functional as F CLASSES [bow_head, hand_up, lie_down, listen, turn_head] def predict_image(img_path, model, device): model.eval() img Image.open(img_path).convert(RGB) tensor val_tf(img).unsqueeze(0).to(device) with torch.no_grad(): logits model(tensor) probs F.softmax(logits, dim1)[0] idx probs.argmax().item() return CLASSES[idx], probs[idx].item() # 视频抽帧推理 import cv2 cap cv2.VideoCapture(classroom.mp4) frame_interval 30 # 每30帧取一帧 count 0 while cap.isOpened(): ret, frame cap.read() if not ret: break if count % frame_interval 0: cv2.imwrite(tmp.jpg, frame) label, conf predict_image(tmp.jpg, model, device) print(fFrame {count}: {label} ({conf:.2f})) count 1 cap.release()推理时注意CLASSES的顺序必须和ImageFolder的classes一致否则标签会错位。unsqueeze(0)是加 batch 维度。视频抽帧间隔根据实际需求调课堂行为分析一般 1 秒一帧足够。如果要做多人检测需要先接 YOLO 做人体框再对每个框做行为分类这套源码通常只做单图分类多人场景要自己扩展。4. 课堂行为识别落地时最容易翻车的五个地方4.1 验证集精度 95%实际部署一塌糊涂现象训练时验证集准确率很高但拿新教室的图片测试准确率掉到 60% 以下。原因训练集和验证集来自同一段视频或同一机位模型学到了背景特征而非行为特征。比如所有「举手」都出现在画面右侧模型实际学的是「右侧举手」。解决划分数据集时按视频段或按机位划分确保验证集的拍摄条件与训练集不同同时增加背景随机裁剪增强逼迫模型关注人体区域。4.2 模型把所有样本都预测成「听讲」现象训练 loss 正常下降但混淆矩阵显示「听讲」类的召回率接近 100%其他类几乎为 0。原因课堂中「听讲」样本天然占多数类别不均衡导致模型退化为多数类预测器。解决用加权交叉熵权重设为类别频率的倒数或者用 WeightedRandomSampler 在 DataLoader 层面过采样少数类。改完后看 macro-F1 而不是 accuracy。4.3 训练到第 5 轮 loss 就变成 nan现象前几轮正常突然 loss 变 nan梯度爆炸。原因学习率太大或者数据里有损坏图片导致输入异常值。解决先把学习率降到 1e-4 试再用脚本遍历数据集用 PIL 打开每张图捕获异常文件并删除。另外加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0)兜底。4.4 GPU 显存够但训练速度极慢现象nvidia-smi 显示 GPU 利用率只有 20%训练一个 epoch 要十几分钟。原因数据加载是瓶颈num_workers设太小或磁盘 IO 慢。解决把num_workers调到 CPU 核数的一半pin_memoryTrue如果数据集在机械硬盘上就拷到 SSD。还可以预先把图片 resize 到 256×256 存成小图减少解码开销。4.5 推理时中文类别名显示乱码现象预测结果输出bow_head而不是「低头」或者中文标签在终端显示为方块。原因类别名用的是文件夹英文名没有做映射终端编码不是 UTF-8。解决在代码里维护一个EN2CN字典做映射Windows 终端执行chcp 65001切 UTF-8。这个不算技术难点但交付给非技术用户时经常被问。5. 把精度再往上推一档类别不均衡下的 Focal Loss 与阈值调优跑通全流程之后真正拉开差距的是对少数类的处理。课堂行为识别里「举手」和「趴桌」的样本量往往只有「听讲」的十分之一用普通交叉熵训出来的模型在这两类上召回率很难看。我一般会换 Focal Loss它通过调制因子降低易分类样本的权重让模型聚焦难样本。import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alphaNone, gamma2.0, reductionmean): super().__init__() self.alpha alpha # 类别权重张量形状 [num_classes] self.gamma gamma self.reduction reduction def forward(self, inputs, targets): ce F.cross_entropy(inputs, targets, weightself.alpha, reductionnone) pt torch.exp(-ce) focal ((1 - pt) ** self.gamma) * ce return focal.mean() if self.reduction mean else focal.sum() # 按类别频率设置 alpha频率越低权重越高 class_counts [500, 120, 480, 2000, 300] # 对应五个类别的样本数 total sum(class_counts) alpha torch.tensor([total / (5 * c) for c in class_counts], dtypetorch.float32).to(device) criterion FocalLoss(alphaalpha, gamma2.0)gamma2.0是原论文的推荐值越大越聚焦难样本但太大会导致训练不稳定一般 1.52.5 之间调。alpha按类别频率倒数归一化让少数类的 loss 贡献更大。换 Focal Loss 后重点看 macro-F1 和少数类召回率整体 accuracy 可能略降但实际可用性提升明显。另一个技巧是推理阶段的阈值调优。默认取 softmax 最大值的类别但在误判代价不对称的场景比如把「趴桌」误判成「听讲」比反过来更严重可以给特定类别加偏置。做法是在验证集上网格搜索每个类别的偏置值最大化你关心的指标。这个操作不复杂但很有效我通常能再榨出 23 个点的少数类召回。最后说一个验证方法不要只看一次划分的结果。用 5 折交叉验证每次换不同的验证集划分看指标的均值和方差。如果方差很大说明模型对数据划分敏感需要更多数据或更强的正则。这套流程走下来一个可用的课堂行为识别模型大概能在五类任务上做到 85% 以上的 macro-F1前提是数据质量和标注一致性过关。技术本身不玄学翻车大多翻在数据和评估上。希望帮到你。本文还有配套的精品资源点击获取