ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python的深度学习课堂行为识别图像分类项目源码解析

基于Python的深度学习课堂行为识别图像分类项目源码解析 简介这是一份面向高校学生与深度学习入门者的课堂行为图像识别分类项目源码适用于毕业设计、期末大作业等场景帮助读者快速搭建可运行的图像分类实验环境。压缩包共1206个文件约100.29MB其中1183个jpg为课堂行为图像数据集14个py脚本负责模型构建与训练流程另有png、md说明文档及license等辅助文件目录结构清晰便于按模块查阅与二次开发。项目经过严格调试评审分达到95分以上可直接运行已有571人学习下载。读者可从中获得完整的图像分类方案、数据组织方式与训练脚本理解从数据加载、模型搭建到结果输出的整体流程并参考排错思路与代码组织习惯适合作为课程实践与项目复现的参考模板。1. 课堂行为识别项目拆包一份能跑通的深度学习图像分类源码长什么样带过几届毕业设计的同学都知道课堂行为识别这个题目每年都有人选但真正能跑通、能讲清楚、能过答辩的源码包并不多。我手上这份基于 Python 的深度学习课堂行为图像识别分类项目源码解压后第一眼看到的是jiaoliu、sleep、xth这几类文件夹配合.gitignore和一堆按序号命名的 jpg结构非常典型——按行为类别分目录存放原始图像这是图像分类任务最朴素也最稳妥的组织方式。它解决的核心问题很具体给定一张课堂场景截图判断学生当前处于听讲、睡觉、交头接耳还是其他状态。适合谁用正在做深度学习课程大作业、毕业设计或者需要一个完整图像分类 pipeline 参考的本科生和初级工程师。评审分 95 分以上这个信息说明它的完成度不低但能不能直接套用到你自己的场景得看下面几章的拆解。2. 数据组织与预处理从 jiaoliu、sleep 文件夹到可训练张量2.1 目录结构背后的分类逻辑拿到源码包先别急着跑train.py。我一般会先花十分钟把目录树看一遍因为图像分类项目的成败七成在数据组织上。这份源码的图片命名方式很直白jiaoliu (276).jpg、sleep (4).jpg、xth0 (188).jpg括号里的数字是原始采集时的序号前缀就是类别标签。jiaoliu对应交头接耳sleep对应睡觉xth大概率是“小动作”或“写作业”的拼音缩写具体含义得看源码里的类别映射字典。这种“文件夹即标签”的做法配合 PyTorch 的ImageFolder或 TensorFlow 的image_dataset_from_directory可以直接读取省去手写标注文件的麻烦。但有个前提每个类别文件夹下的图片数量不能差太多。我见过太多翻车案例sleep文件夹里塞了 800 张jiaoliu只有 60 张训出来的模型把所有图都预测成 sleep准确率看着有 80%实际是个废物。import os from collections import Counter data_dir ./dataset for split in [train, val]: split_dir os.path.join(data_dir, split) if not os.path.exists(split_dir): continue counts {} for cls in os.listdir(split_dir): cls_dir os.path.join(split_dir, cls) if os.path.isdir(cls_dir): # 只统计常见图像格式过滤掉 .DS_Store 之类的杂项 imgs [f for f in os.listdir(cls_dir) if f.lower().endswith((.jpg, .jpeg, .png))] counts[cls] len(imgs) print(split, counts)这段脚本干的事很简单遍历 train 和 val 两个 split统计每个类别下的有效图片数量。参数上data_dir指向数据集根目录split列表按你实际的结构改。跑完之后如果发现某个类别的数量不到最大类别的三分之一就得考虑过采样、数据增强或者干脆合并类别。常见做法是给少数类做随机旋转、水平翻转、颜色抖动把有效样本量拉上来。2.2 图像尺寸统一与归一化参数课堂场景的原始截图尺寸往往不统一有 1920×1080 的监控截帧也有手机拍的 1080×2340。深度学习模型要求输入张量尺寸一致所以预处理里必须包含 resize。这份源码我推测用的是 224×224因为 ResNet、MobileNet 这些常用骨干网络的标准输入就是 224。如果你打算换骨干比如换成 EfficientNet-B0输入尺寸可以到 256 或 300但显存占用会上去。归一化参数别拍脑袋写。用 ImageNet 预训练权重做迁移学习时mean 和 std 必须跟预训练时一致mean[0.485, 0.456, 0.406]std[0.229, 0.224, 0.225]。我见过有人改成mean[0.5, 0.5, 0.5]结果收敛慢了一半还以为是学习率没调好这就是典型的血泪经验。from torchvision import transforms train_tf transforms.Compose([ transforms.Resize((224, 224)), # 统一尺寸短边缩放到 224 再中心裁剪更稳 transforms.RandomHorizontalFlip(p0.5), # 课堂场景左右翻转不改变行为语义 transforms.RandomRotation(10), # 小角度旋转模拟拍摄角度偏差 transforms.ColorJitter(brightness0.2, contrast0.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]), ])训练集和验证集的 transform 必须分开写。训练集加随机增强验证集只做确定性的 resize 和归一化。如果把增强也用到验证集上每次评估结果都在抖你根本不知道模型是真的变好了还是运气好。RandomHorizontalFlip的 p 值设 0.5 是常规操作但要注意如果你的类别里有“举手”和“放下手”这种左右手语义敏感的翻转可能会引入噪声课堂行为识别里一般没这个问题。3. 模型选型与训练脚本迁移学习为什么比从头训更靠谱3.1 骨干网络选择ResNet18 还是 MobileNetV3课堂行为识别这种任务类别数通常不超过 10 类每类样本量在几百到几千之间。这种规模下从头训练一个 CNN 基本等于自杀——收敛慢、容易过拟合、准确率还上不去。正确姿势是用 ImageNet 预训练权重做迁移学习。骨干网络的选择上ResNet18 是稳妥牌参数量 11M 左右单张 1080Ti 就能跑MobileNetV3-Small 更轻参数量不到 3M适合部署到边缘设备但准确率会低一两个点。我一般会先跑 ResNet18 建立 baseline如果准确率达标就不折腾了。如果显存吃紧或者要往树莓派上搬再换 MobileNetV3。源码里如果用的是自定义 CNN那大概率是教学性质实际效果不如迁移学习。判断方法很简单看model.py或train.py里有没有pretrainedTrue或者weightsIMAGENET1K_V1这样的字眼。import torch import torch.nn as nn from torchvision import models def build_model(num_classes, backboneresnet18, freeze_backboneTrue): if backbone resnet18: model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) # 冻结除 fc 层外的所有参数小数据集上防止过拟合 if freeze_backbone: for name, param in model.named_parameters(): if fc not in name: param.requires_grad False in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) elif backbone mobilenet_v3_small: model models.mobilenet_v3_small(weightsmodels.MobileNet_V3_Small_Weights.IMAGENET1K_V1) if freeze_backbone: for name, param in model.named_parameters(): if classifier not in name: param.requires_grad False in_features model.classifier[3].in_features model.classifier[3] nn.Linear(in_features, num_classes) return modelfreeze_backboneTrue这个参数很关键。小数据集上先冻结骨干只训分类头等 loss 稳定后再解冻做微调这是标准的两阶段训练策略。如果一上来就全量微调预训练权重容易被小数据带偏反而比冻结还差。num_classes按你实际的类别数填比如 jiaoliu、sleep、xth 三类就是 3。3.2 训练循环里的学习率与早停训练脚本里最容易出问题的是学习率。分类头刚初始化时学习率可以设 1e-3解冻骨干微调时要降到 1e-4 甚至 1e-5。如果全程用同一个学习率要么分类头学不动要么骨干被训崩。优化器用 AdamW 比 SGD 省心weight_decay 设 1e-4 能压住过拟合。import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(num_classes3).to(device) # 只优化 requires_gradTrue 的参数 optimizer optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max30) criterion nn.CrossEntropyLoss() best_acc 0.0 patience, counter 5, 0 for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() loss criterion(model(imgs), labels) loss.backward() optimizer.step() scheduler.step() # 验证阶段 model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) preds model(imgs).argmax(dim1) correct (preds labels).sum().item() total labels.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.pth) counter 0 else: counter 1 if counter patience: print(early stop) break早停的 patience 设 5 是经验值。验证集准确率连续 5 个 epoch 不涨就停避免在训练集上磨到过拟合。CosineAnnealingLR让学习率按余弦曲线衰减比阶梯衰减平滑小数据集上更稳。保存模型时只存state_dict()别存整个模型对象不然换设备加载会报错。4. 避坑与排查课堂行为识别项目里最容易翻车的五个点4.1 类别不平衡导致模型“摆烂”现象训练 loss 一直降但验证集上所有样本都被预测成样本量最大的那个类准确率卡在 60% 左右上不去。原因sleep类可能有 500 张jiaoliu只有 80 张交叉熵损失被多数类主导模型发现全猜 sleep 就能拿到不错的 loss直接摆烂。解决在CrossEntropyLoss里传weight参数按类别频率的倒数加权或者用WeightedRandomSampler在 DataLoader 层面做平衡采样。from torch.utils.data import WeightedRandomSampler import numpy as np # 假设 train_dataset 是 ImageFoldertargets 是类别索引列表 class_counts np.bincount(train_dataset.targets) class_weights 1.0 / class_counts sample_weights [class_weights[t] for t in train_dataset.targets] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_dataset, batch_size32, samplersampler)4.2 验证集泄漏同一张图既在训练又在验证现象验证准确率高得离谱95% 以上但拿新图片一测就废。原因划分数据集时用了随机划分同一段视频截出来的相邻帧被分到了 train 和 val模型等于在“背答案”。解决按视频源或采集批次划分同一批次的所有图片只能进同一个 split。如果源码里没有划分脚本自己写一个按文件名前缀分组的划分逻辑。4.3 图像通道数不匹配导致加载报错现象RuntimeError: Given groups1, weight of size [64, 3, 7, 7], expected input[32, 1, 224, 224] to have 3 channels。原因部分课堂截图是灰度图PIL 读进来是单通道而 ResNet 第一层卷积要求 3 通道。解决在 transform 里加transforms.Grayscale(num_output_channels3)或者用transforms.Lambda(lambda x: x.convert(RGB))强制转三通道。4.4 显存溢出batch_size 设太大现象CUDA out of memory训练跑不起来。原因224×224 的图batch_size 设 64ResNet18 在 4G 显存卡上直接爆。解决先把 batch_size 降到 16 或 8配合梯度累积模拟大 batch。如果还不行把输入尺寸降到 128×128准确率会掉一点但能跑起来。accum_steps 4 optimizer.zero_grad() for i, (imgs, labels) in enumerate(train_loader): loss criterion(model(imgs.to(device)), labels.to(device)) / accum_steps loss.backward() if (i 1) % accum_steps 0: optimizer.step() optimizer.zero_grad()4.5 模型保存与加载的 state_dict 键名不匹配现象RuntimeError: Error(s) in loading state_dict for ResNet: Missing key(s) in state_dict: fc.weight...。原因保存时用了torch.save(model, model.pth)存整个对象加载时换了设备或改了模型定义键名对不上。解决统一用torch.save(model.state_dict(), model.pth)保存加载时先实例化模型再model.load_state_dict(torch.load(model.pth, map_locationdevice))。map_location参数在 CPU 加载 GPU 训练的权重时必加。5. 推理与部署把训练好的模型接进实际课堂场景5.1 单张图片推理脚本训练完拿到best.pth下一步是验证它在真实图片上的表现。推理脚本要跟训练时的预处理保持一致尤其是归一化参数差一点结果就偏。from PIL import Image import torch from torchvision import transforms def predict(image_path, model, class_names, device): 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]), ]) img Image.open(image_path).convert(RGB) tensor tf(img).unsqueeze(0).to(device) # 增加 batch 维度 model.eval() with torch.no_grad(): logits model(tensor) probs torch.softmax(logits, dim1) conf, pred probs.max(dim1) return class_names[pred.item()], conf.item() class_names [jiaoliu, sleep, xth] model build_model(num_classes3, freeze_backboneFalse).to(device) model.load_state_dict(torch.load(best.pth, map_locationdevice)) label, confidence predict(test.jpg, model, class_names, device) print(f预测: {label}, 置信度: {confidence:.4f})unsqueeze(0)这步不能省模型 forward 要求输入是 4 维张量[batch, channel, height, width]单张图只有 3 维。torch.softmax把 logits 转成概率max(dim1)同时拿到最大概率和对应索引。置信度低于 0.6 的样本建议人工复核课堂场景里误判比漏判更麻烦。5.2 批量推理与结果导出实际用的时候往往是一整个文件夹的截图要过一遍逐张调predict太慢。写个批量版本把结果导成 CSV方便后续统计。import csv import os def batch_predict(folder, model, class_names, device, out_csvresult.csv): results [] for fname in os.listdir(folder): if not fname.lower().endswith((.jpg, .png, .jpeg)): continue path os.path.join(folder, fname) label, conf predict(path, model, class_names, device) results.append({file: fname, label: label, confidence: round(conf, 4)}) with open(out_csv, w, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnames[file, label, confidence]) writer.writeheader() writer.writerows(results) return results导出 CSV 的好处是可以直接用 Excel 或 pandas 做透视表统计一节课里睡觉行为出现了多少次、集中在哪个时间段。如果要做课堂行为分析报告这个 CSV 就是原始数据。5.3 模型导出为 ONNX 的注意事项如果要把模型部署到非 Python 环境比如 C 或边缘设备导出 ONNX 是常见做法。但有两个坑一是动态 batch 维度要显式指定二是 opset 版本别选太高不然目标推理引擎可能不支持。dummy_input torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy_input, classroom_behavior.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version11, )dynamic_axes让导出的模型支持变长 batchopset_version11兼容性最好。导出后用onnxruntime跑一遍对比 PyTorch 和 ONNX 的输出差异如果 max diff 超过 1e-3说明导出过程有问题得检查有没有不支持的算子。6. 从 95 分项目到自己的作品三个让答辩加分的改造方向这份源码能拿到 95 分以上说明基础功能是完整的。但如果你想在答辩时让老师眼前一亮或者把它写进简历光跑通还不够。我一般会建议做三个改造成本不高但效果明显。第一个方向是加混淆矩阵和分类报告。很多同学只报一个准确率但老师一问“哪个类最容易混”就答不上来。用sklearn.metrics的confusion_matrix和classification_report把每个类的 precision、recall、f1-score 都打出来。课堂行为识别里jiaoliu和xth往往容易混因为都是上半身有动作混淆矩阵能直观暴露这个问题。from sklearn.metrics import classification_report, confusion_matrix import seaborn as sns import matplotlib.pyplot as plt all_preds, all_labels [], [] model.eval() with torch.no_grad(): for imgs, labels in val_loader: preds model(imgs.to(device)).argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_namesclass_names)) cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, xticklabelsclass_names, yticklabelsclass_names) plt.savefig(confusion_matrix.png, dpi150)第二个方向是加 Grad-CAM 可视化。老师问“模型到底看哪里判断这是睡觉”你总不能说“我也不知道”。Grad-CAM 能把模型关注区域热力图叠在原图上睡觉类通常关注头部和桌面区域交头接耳类关注头部之间的区域。这个图往答辩 PPT 里一放说服力直接拉满。第三个方向是做一个极简的推理界面。不用搞 Web 服务用 Gradio 或 Streamlit 写个十行脚本上传图片就能看到预测结果和置信度。答辩现场演示的时候比对着命令行输出念数字直观得多。import gradio as gr def gradio_predict(img): img Image.fromarray(img).convert(RGB) tensor val_tf(img).unsqueeze(0).to(device) with torch.no_grad(): probs torch.softmax(model(tensor), dim1)[0] return {class_names[i]: float(probs[i]) for i in range(len(class_names))} gr.Interface(fngradio_predict, inputsimage, outputslabel).launch()这三个改造做完你的项目就不只是“跑通了”而是“有分析、有解释、有演示”。从那以后我每次带毕业设计都要求学生至少把混淆矩阵和 Grad-CAM 加上答辩时被问到模型可解释性直接翻到那一页就行。希望帮到你。本文还有配套的精品资源点击获取
返回列表