
简介基于Python的深度学习课堂行为图像识别分类项目源码适合作为毕业设计、期末大作业以及图像分类入门者的练习范例。项目聚焦课堂场景中的交流、睡觉等典型学生行为借助卷积神经网络完成图像分类整体流程包括数据准备、模型构建、训练与评估。资源包共包含一千二百零六个文件以大量课堂行为图片样本为主体配合Python源码、示意图、说明文档与授权文件压缩后大小约一百兆目录结构清晰便于按模块查阅与学习。源码已经过严格调试个人大作业评审分在九十五分以上下载后可直接运行也可在现有基础上扩展为新的分类任务或改进网络结构。目前已有五百七十余人浏览学习对理解深度学习图像分类项目的完整落地过程很有帮助也可作为课程设计或答辩展示的参考底稿。1. 这个基于 Python 的课堂行为图像识别项目到底拿来干什么的做毕业设计或者期末大作业选图像识别方向最怕的不是模型跑不通而是交上去的「项目」只有一段训练脚本连一份成体系的数据组织都没有。这个基于 Python 深度学习的课堂行为图像识别分类项目源码属于典型的个人完整大作业形态——数据集、数据划分、训练脚本、模型保存和推断流程都集成在一个工程目录里评审分能到95分以上说明它在「能跑」「能解释」「能演示」三个维度上都扛得住验收。从压缩包内的图片命名能直接看出分类目标sleep (4).jpg、jiaoliu (276).jpg、xth0 (188).jpg分别对应睡眠、交流、写题/听讲等课堂行为类别这正是深度学习图像分类最标准的落地场景给一张课堂监控画面判断学生当前处于哪种行为状态。适合谁用正在做图像分类方向课程设计、需要完整参考工程来复现的在校生或者想快速拿到一套可改可扩展的分类代码来换数据集跑实验的入门者。它不是教学 demo是一个可以直接运行、直接改类别、直接换上自己采集的图片就能二次开发的项目。2. 从数据集到训练脚本这个课堂行为识别项目是怎么组织的拿到压缩包后先别急着跑代码。这类图像分类工程的第一个隐藏难点不是模型而是数据是怎么摆的、标签是怎么映射的。很多大作业项目跑不起来十有八九是路径写死或者图片文件命名不规范。这个项目从图片名可以看出来类别信息直接嵌在文件名里sleep、jiaoliu、xth就是类别标签括号里的数字是样本序号。这种命名方式在个人项目中非常常见好处是省去了繁琐的标注文件坏处是如果你的图片是中文名或者没规律的长串字符那就要先做一轮统一的文件名规范化处理。数据组织方面我一般建议按下面的目录结构落盘这也是这个源码包大概率采用的方案dataset/ ├── sleep/ │ ├── sleep (1).jpg │ ├── sleep (2).jpg │ └── ... ├── jiaoliu/ │ ├── jiaoliu (1).jpg │ └── ... └── xth/ ├── xth0 (1).jpg └── ...如果你拿到手的压缩包不是这个结构而是所有图片平铺在一个目录里那么第一步就是要写一个批量整理脚本按文件名前缀自动归类。常见做法是提取文件名中第一个空格前的字符串作为类别名然后创建对应文件夹、移动图片代码如下import os import shutil src_dir ./all_images dst_root ./dataset os.makedirs(dst_root, exist_okTrue) for fname in os.listdir(src_dir): if not fname.lower().endswith((.jpg, .jpeg, .png)): continue # 取文件名空格前的部分作为类别名sleep (4).jpg - sleep label fname.split( )[0] label_dir os.path.join(dst_root, label) os.makedirs(label_dir, exist_okTrue) shutil.move(os.path.join(src_dir, fname), os.path.join(label_dir, fname))这段脚本的逻辑很简单遍历原始图片目录根据文件名中第一个空格前的单词创建类别文件夹再把图片挪进去。注意fname.split( )[0]这个写法是假定类别名前缀没有空格如果类别名本身含空格就需要改用正则匹配。运行之后再用tree或者资源管理器确认一下每个类别文件夹下的样本数量是否均衡这直接关系到后面训练时要不要做类别加权。数据就位后下一步就是训练集和验证集的划分。这个项目里既然有jiaoliu (1).jpg到jiaoliu (296).jpg这种规模的数据每个类别可能有几十到几百张直接用train_test_split按 8:2 划分即可。但这里有个大作业常见的坑划分前一定要固定随机种子否则每次跑结果都不一样答辩时老师让你重跑一遍准确率忽高忽低会很尴尬。from sklearn.model_selection import train_test_split import glob class_names [sleep, jiaoliu, xth] train_files [] val_files [] for cls in class_names: files glob.glob(f./dataset/{cls}/*.jpg) tr, va train_test_split(files, test_size0.2, random_state42, stratifyNone) train_files.extend(tr) val_files.extend(va) print(ftrain: {len(train_files)}, val: {len(val_files)})这里重点说两个参数。test_size0.2是验证集比例样本总量小的情况下建议提高到 0.25 或 0.3因为验证集太少会导致评估指标抖动明显你很难判断模型是真的变好了还是只是运气好。random_state42是随机种子固定下来后每次划分结果一致这是保证实验可复现的基础操作。至于stratify如果你的数据天然类别均衡比如三类都是 200 张上下那不用管但如果有的类别只有 50 张有的却有 400 张就必须设stratifyy来做分层采样否则小类别很容易在验证集里只剩几张训练出来的模型对这个小类别基本是瞎猜。数据划分完一个容易被忽视的问题是图片解码一致性。课堂行为图片是从监控视频里截帧来的分辨率可能五花八门有的sleep (4).jpg是 1920×1080有的可能就 640×480。深度学习分类模型一般要求固定输入尺寸比如 224×224 或 256×256你不能指望模型自己对不同尺寸做自适应。这一步用 PyTorch 的transforms就能统一处理会在第四章完整展开。这里先提醒一句不要用 PIL 直接resize后保存硬盘而是在训练时用transforms.Resize动态处理这样原始图片分辨率信息不会丢失以后想换输入尺寸只需要改代码不用重新处理数据集。3. 模型选型自定义 CNN 还是迁移学习这个项目给了什么方案课堂行为识别属于细粒度图像分类类别之间的差异其实挺微妙——「交流」和「写题」在画面里可能只是头部姿态和手部位置的差别。这种任务不是随便堆几层卷积就能做好的但也不是非得上百层的超大模型。对于大作业体量的数据集三类样本几百到上千张模型选型有三个方向从零训练一个轻量 CNN、用 ResNet18 做迁移学习微调、或者用 VGG16 提特征后接全连接分类头。以这个项目的场景来说我倾向于它采用的是迁移学习 微调的思路因为这是数据量有限时性价比最高的做法。ImageNet 预训练模型已经学会了边缘、纹理、形状这些底层视觉特征课堂行为识别虽然和 ImageNet 的类别完全不同但底层特征是可迁移的。你只需要把模型的最后一层全连接换成自己类别的输出维度然后对后面的层做微调就行。用 PyTorch 实现就几行代码的事import torchvision.models as models import torch.nn as nn model models.resnet18(pretrainedTrue) num_features model.fc.in_features num_classes 3 # sleep, jiaoliu, xth model.fc nn.Linear(num_features, num_classes) for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True这里的关键操作有两个。第一个是model.fc.in_features通过读取原模型全连接层的输入维度来动态构建新分类头这样不管预训练模型是 ResNet18、ResNet34 还是其他架构代码都能自适应不用硬编码 512 或 2048。第二个是冻结主干层的技巧——requires_grad False把前面的卷积层参数全部锁死只训练新加的全连接层。冻结的好处是训练速度极快而且预训练特征不会因为小数据集上的过拟合而崩塌。但这里有个很容易翻车的细节迁移学习的微调范围不一定全冻结就好。你冻结了全部主干那么模型对课堂行为这类特殊场景的适配能力会有上限。常见的做法是分层微调把最后一两个 residual block 也放开训练让高层特征向你的数据分布靠拢低层保持通用性。代码调整如下for name, param in model.named_parameters(): if layer4 in name or fc in name: param.requires_grad True else: param.requires_grad Falselayer4是 ResNet 最后一个残差阶段最靠近分类头语义特征最偏向具体任务。放开它和fc层一起训练相当于在预训练模型的肩膀上做局部定制也不会造成训练发散。如果直接用默认代码全部冻结很可能出现训练损失降到很低但验证准确率卡在 70% 上不去的「欠拟合态」——不是数据不够是模型的可学习参数被锁死太多了。如果你拿到手的源码没有用预训练模型而是一个从零构建的简单 CNN那也别慌这种选择在小数据集上完全合理。自定义 CNN 的典型结构是「卷积 池化 全连接」三件套PyTorch 实现如下import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes3): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x))这个网络的参数规模大约只有几十万个在 GPU 上训练几十个 epoch 只需几分钟CPU 也能跑得动。AdaptiveAvgPool2d(1)是个很聪明的设计不管输入图片尺寸是多少池化后特征图都会变成 1×1后面全连接层的输入维度就固定了。它的代价是模型表达能力有限准确率上限可能不如迁移学习但如果是期末大作业课程重点在流程完整性而不在刷分这个方案完全够用。选型决策上我给一个判断标准你的训练集每个类别小于 200 张首选迁移学习每类超过 500 张可以尝试从零训练一个中等规模 CNN 并配合充分的数据增强效果未必比迁移学习差。这个项目从图片数量看单类别大约一两百张走迁移学习路线更稳妥。4. 训练配置与调参细节学习率、batch size、早停和类别不平衡训练阶段的坑比模型选型更隐蔽。课堂行为识别项目的训练流程一般包含数据增强、优化器配置、学习率调度和早停机制这几个环节单独看都不难但组合在一起时参数搭配不当就是另一回事了。先看数据增强。这个项目的原始图片数量不算充裕如果不做增强模型很容易记住训练集里某个同学的脸、某个教室的固定光照到了验证集上就露馅。常见的增强组合是随机水平翻转、随机旋转和颜色抖动from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])RandomRotation(10)的 10 表示旋转 ±10 度课堂监控画面里学生头部倾斜程度一般不会超过这个范围幅度再大就会制造出不符合实际的样本。Normalize用 ImageNet 的均值和标准差迁移学习场景下必须沿用预训练模型的归一化参数否则预训练特征分布直接被破坏。验证集不做增强只有 resize 和归一化目的是用最接近真实分布的样本评估模型。优化器方面迁移学习微调场景下最稳的选择是 SGD 加 momentum而不是很多人一上来就用的 Adam。原因是预训练模型的参数已经在一个较好的局部最优附近SGD 的平稳更新不容易破坏已有特征Adam 的自适应学习率有时会步子太大。实践中的参数配置通常如下import torch.optim as optim optimizer optim.SGD(model.parameters(), lr0.001, momentum0.9, weight_decay1e-4) scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience3, verboseTrue ) criterion nn.CrossEntropyLoss()lr0.001是微调阶段常用的初始学习率如果你把更多层解冻了这个值可能要降到 0.0003 才稳。ReduceLROnPlateau会在验证损失连续 3 个 epoch 不下降时把学习率减半这是做小数据集分类最实用的学习率策略——你不需要手动盯着曲线判断什么时候该降学习率机制替你做了。weight_decay1e-4是 L2 正则对防止全连接层过拟合有点帮助但别调太大太大会让模型整体欠拟合。训练循环这里有一个课堂行为分类特有的坑类别不平衡。从图片命名规律看jiaoliu类的样本数明显多于其他类那模型就会倾向把所有输入都预测成jiaoliu因为这样能拿到最高的整体准确率。解决方式有两种一是用加权交叉熵损失让样本少的类别占据更大的损失权重class_counts torch.tensor([150, 300, 120], dtypetorch.float) class_weights 1.0 / class_counts class_weights class_weights / class_weights.sum() * num_classes class_weights class_weights.to(device) criterion nn.CrossEntropyLoss(weightclass_weights)二是在评估时不只看 accuracy要看混淆矩阵和各类别的 precision、recall。这里建议训练脚本里同时记录classification_report用 sklearn 一行就能输出from sklearn.metrics import classification_report, confusion_matrix preds torch.argmax(outputs, dim1).cpu().numpy() print(classification_report(y_true, preds, target_namesclass_names))classification_report会输出每个类别的精确率、召回率和 F1 值。如果一个类别准确率高但召回率低说明模型把它漏掉了很多反之如果召回率高但精确率低说明模型把别的类别误分到了它头上。答辩时这个输出比单一个准确率数字有说服力得多。早停也是很关键的机制大作业场景下你不可能手动盯着每个 epoch早停可以帮你自动在验证 loss 开始回升时终止训练省时间也防过拟合best_val_loss float(inf) patience_counter 0 early_stop_patience 10 for epoch in range(num_epochs): train_loss train_one_epoch(...) val_loss validate(...) scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss patience_counter 0 torch.save(model.state_dict(), best_model.pth) else: patience_counter 1 if patience_counter early_stop_patience: print(early stop at epoch, epoch) break这里的逻辑核心始终围绕先定义best_val_loss最低验证损失当损失连续 10 个 epoch 没有刷新纪录就停止训练且始终保存的是验证损失最低的那份权重而不是最后一个 epoch 的模型。大作业里常见的错误是把最后一个 epoch 的 checkpoint 当作最优模型但训练后期模型通常已经过拟合验证表现反而变差。用这种写法模型文件里存的一定是最优状态复原测试时结果可重复老师那边也好交代。5. 避坑清单运行这套课堂行为识别项目最容易踩的五个坑坑一训练时 loss 正常下降但准确率不动。现象是损失从 2.0 降到 0.3训练集准确率到了 99%验证集准确率却一直卡在 60% 左右。原因大概率是数据划分出了问题或者验证集和训练集有重叠。我拿到别人项目第一个就是检查数据划分脚本里有没有在glob之后忘记 shuffle 再切分导致验证集全是某个类别的数据。解决方法是重新仔细检查划分逻辑并且用set(train_files) set(val_files)检查交集一旦发现有重叠就必须全部重新划分训练前一定要打印每个类别在训练集和验证集的样本数量分布。坑二报错找不到图片。FileNotFoundError或者路径拼接后文件不存在多半是脚本里用相对路径但你运行脚本的工作目录不对。比如脚本在src/目录下数据集在项目根目录的dataset/里代码里写死了./dataset/xxx.jpg那在src/下运行时路径就断了。解决方法是不要在脚本里手写路径用os.path.join(os.path.dirname(os.path.abspath(__file__)), .., dataset)基于脚本所在位置向上定位数据集目录这样不管从哪个目录调用脚本都能正确找到文件。坑三图像里有中文文件名。有些课堂行为图片命名可能带中文后缀或者中文目录名PyTorch 的ImageFolder对路径解码有时会出问题。报错通常是UnicodeDecodeError或者OSError: [Errno 22] invalid mode。原因不是路径不支持中文而是某些图像库在 Windows 下默认用 GBK 解码文件路径遇到 UTF-8 的中文路径就崩了。解决方式是统一把图片文件重命名为拼音或英语比如kejian、shuijiao这种顺便把类别中文名换成英文标签以避免后续所有环节的编码坑。坑四跑推理时总是输出某一个固定的类。模型训练完测试阶段不管输入什么图片输出大概率集中在某单一类别这个前文提过就是类别不平衡惹的祸。训练时没启用加权损失模型学会了走捷径。但还有一种原因是你保存模型权重的时候误用了torch.save(model, ...)保存整个模型而不是state_dict加载时容易出现权重和设备不匹配的诡异问题。建议只保存state_dict加载时先实例化模型结构再load_state_dict注意区分你是不是在切片或张量操作时把数据顺序搞乱了。坑五transforms 和训练时不一致推理效果大跌。训练时做了RandomHorizontalFlip和RandomRotation(10)推理时如果照搬同一套 transforms模型就可能在测试时看到不真实的变化导致随机性准确率不稳定。解决方法是把推理的 transforms 独立出来只保留 resize 和归一化不加任何随机增强。这也是为什么建议前面训练代码里把train_transform和val_transform分开写之后推理直接复用val_transform就行。6. 让这个项目真正能答辩混淆矩阵、可视化与推理脚本改造模型训练完只是第一步大作业答辩时老师大概率会问这么几个问题你的模型对哪个类别分得最差随便给一张新图你的项目能不能现场出结果训练曲线能不能画出来如果你的项目里只有一个train.py那最后这一步就得补上这也是让项目从「能跑」到「耐看」的关键改造。先把训练损失和准确率的曲线画出来存成图片放到项目目录里答辩时直接展示import matplotlib.pyplot as plt plt.figure(figsize(10, 4)) plt.subplot(1, 2, 1) plt.plot(train_loss_history, labeltrain loss) plt.plot(val_loss_history, labelval loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.title(Loss Curves) plt.subplot(1, 2, 2) plt.plot(train_acc_history, labeltrain acc) plt.plot(val_acc_history, labelval acc) plt.xlabel(epoch) plt.ylabel(accuracy) plt.legend() plt.title(Accuracy Curves) plt.tight_layout() plt.savefig(training_curves.png, dpi150)这两条曲线的作用不仅是展示训练过程更能暴露模型状态。如果 train loss 持续下降但 val loss 在某个 epoch 后回升说明过拟合了此时你可以结合早停机制在答辩时正面回答老师「我从第 X 个 epoch 开始加了早停所以选的是验证集表现最好的那个 checkpoint」——这种回答是加分项。如果两条曲线几乎同步下降说明模型还没吃饱数据可以加训练轮数。一张随机测试图片跑单张推理是答辩现场最容易被验证的功能。PyTorch 里写一个单图预测脚本逻辑就是加载模型、预处理图片、前向传播、取 softmax 概率最大的类别再配上把概率打印出来的输出import torch from PIL import Image def predict_single_image(model, image_path, class_names, devicecpu): model.eval() model.to(device) img Image.open(image_path).convert(RGB) img_tensor val_transform(img).unsqueeze(0).to(device) with torch.no_grad(): outputs model(img_tensor) probs torch.softmax(outputs, dim1) pred_idx torch.argmax(probs, dim1).item() for i, cls in enumerate(class_names): print(f{cls}: {probs[0][i].item():.4f}) print(fpredicted: {class_names[pred_idx]})如果想把结果可视化得更直观可以在图上的左上角画框标出 predicted 标签。这样答辩时现场扔一张测试图片进去三秒后终端打印出每个类别的概率值和最终分类结果比对着屏幕说「准确率 95%」要有说服力得多。至于中间可视化的内容如果你想让项目显得更丰满可以再做一步 Grad-CAM 类激活映射不过这个和课堂行为场景结合不是特别稳定这里不做重点推荐。从那以后我每次拿到一个图像分类的工程包都会强制走一遍固定流程先梳理标签和数据分布再检查划分逻辑和预处理是否前后一致然后跑十来个 epoch 看训练曲线判断选型和参数是否合理最后一定要补一个单图推理脚本。这套流程花不了半小时但能让项目的可解释性和可复现性在关键时刻立住。希望帮到你。本文还有配套的精品资源点击获取