简介这是一份机器学习课程期末大作业的完整项目适合正在完成图像分类任务、需要参考高分实现的高校学生。项目以深度神经网络为核心代码结构按models、utils、layers等模块组织包含ResSppNet、NaiveLeNet、NaiveSPPNet、HybridResSPPNet等多种网络实现并配有数据预处理、训练、预测与测试脚本可以直接替换数据集运行。压缩包共17个文件以15个Python源码文件为主附带.gitignore与配置文件整体仅20KB轻量易读便于逐行理解与二次修改。作业曾获98分反映出模型设计、训练流程和代码规范都较为成熟。已有274人学习下载对想快速搭建图像分类基线或借鉴高分课程报告思路的同学很有参考价值。1. 拿到这个 zip 之前先把图像分类大作业的任务边界说清楚“机器学习课程大作业 - 基于深度神经网络的图像分类任务.zip”这类压缩包几乎是每个学校机器学习课都会出现的交付形态跑通一个深度神经网络在给定图像数据集上做分类最后交源码、报告和模型权重。很多人第一反应是解压、找代码、跑通、截图交差但真正拉开差距的是你能不能把一条图像分类流水线从数据准备、模型选型、训练调参到评估交付完整地立起来。这篇就按这个顺序讲清楚深度神经网络做图像分类时的模型选择、最小可复现代码、必调参数和常见坑位。新手能照着搭出第一个可解释的结果熟手也能在评估、归因和打包环节看到值得统一的做法。2. 模型选型深度神经网络图像分类该走哪条技术路线2.1 从全连接到卷积深度神经网络如何看一张图图像分类任务里输入是一张 (H \times W \times C) 的像素矩阵。如果直接用全连接网络224×224 的 RGB 图展开就是 15 万维输入第一层参数就会膨胀到不可训练而且全连接层看不见像素之间的局部空间关系。常见做法是引入卷积操作用一个小尺寸卷积核在图像上滑动每个位置只计算局部窗口内的加权和参数共享让同一组卷积核能识别整张图里同类局部特征。多层卷积堆叠后浅层学边缘、纹理深层学语义部件这就是深度神经网络在图像分类里最核心的归纳偏置。课程大作业里这个原理通常会被要求在报告中讲清楚建议配合一张特征图可视化图说明而不是只写“用了 CNN”。2.2 为什么课程大作业首选 ResNet梯度断裂与残差连接网络越深理论上表达力越强但直接堆层会碰到梯度消失反向传播时梯度连乘浅层几乎收不到有效更新训练集准确率反而下降。ResNet 的残差连接让每一层去拟合输入与输出的差值梯度可以跨层直传这是它比 VGG 更适合大作业的原因之一。另一个现实理由是 torchvision 里带预训练权重的 ResNet 覆盖最全从 ResNet18 到 ResNet50 都有 ImageNet 权重做课程分类任务时可以直接做迁移学习收敛快、结果稳。以下是课程场景里常见的模型选型对比模型参数量单卡训练难度大作业性价比适用场景VGG16约 1.38 亿高显存占用大低训练慢且易过拟合适合做特征可视化分析ResNet18约 1100 万低高首选小数据集、快速验证ResNet50约 2500 万中高数据量中等以上的分类EfficientNet-B0约 530 万低中强调推理速度时ViT-B/16约 8600 万高需大数据低数据量极大或对比实验我一般会建议先跑 ResNet18 作为 baseline把数据 pipeline 和训练脚本调通再决定要不要换成 ResNet50。如果课程要求对比“最新模型”再用 Vision Transformer 或 Swin Transformer 做一组消融而不是一上来就训 Transformer否则很容易在数据量不足时得到低于 ResNet 的结果反而难解释。2.3 什么时候该选 Transformer 图像分类路线Transformer 图像分类模型近年热度很高但它的强项建立在大量数据上。ViT 把图像切成 patch再用自注意力建模 patch 之间的全局关系缺少 CNN 的局部先验因此在几万张以下的小数据集上往往打不过 ResNet。课程大作业如果提供的数据集每类只有几百张老老实实用 ResNet 迁移学习如果你拿到的是 CIFAR-10、ImageNet 子集这类规模适中的数据并且想做“新模型为什么有效”的分析可以选一个小型 ViT 或 Swin-T 做对比。注意迁移学习时要加载 ImageNet 预训练权重并且把最后的分类头换成自己的类别数。2.3.1 选型时还要留意机器学习三大假设图像分类模型能 work依赖的一个前提是训练集和测试集独立同分布。课程作业里如果数据划分方式不随机比如按文件夹顺序切分或者训练集和测试集来自不同光照条件模型指标会失真。选模型前先检查数据来源和划分代码这也是答辩时老师常问的点。3. 从数据到训练搭一条能跑通的最小图像分类流水线3.1 数据集准备目录结构、划分与数据增强课程作业最常见的数据组织方式是 ImageFolder 风格每个类一个子目录子目录名就是标签。先做训练集和验证集划分再封装 DataLoader。以下是我在本地跑通的最小可执行片段import torch from torch.utils.data import DataLoader, random_split from torchvision import datasets, transforms # 1. 训练集增强随机裁剪 水平翻转 归一化 train_tf transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) # 2. 验证集只做缩放和归一化不做随机增强 val_tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) dataset datasets.ImageFolder(data/train, transformtrain_tf) # 3. 按 8:2 随机划分训练集与验证集保证类别分布一致 train_set, val_set random_split( dataset, [int(len(dataset) * 0.8), len(dataset) - int(len(dataset) * 0.8)] ) train_loader DataLoader(train_set, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_set, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) # 4. 输出类别名后续画混淆矩阵和写报告都要用 print(dataset.classes)逻辑说明RandomResizedCrop会随机裁剪一块区域并缩放到 224相当于给模型提供尺度不变性RandomHorizontalFlip在自然图像里是安全的先验。验证集必须用固定中心裁剪否则每一次评估的输入都在变指标不可比。pin_memoryTrue在 GPU 训练时能减少主机到显存的拷贝时间数据量不大时影响不明显但属于白拿的优化。如果数据集是从 zip 里解出来的注意检查是否存在损坏图片加载时报错可以用PIL.Image.open逐个验证。3.2 训练主脚本5 分钟跑通一个可复现的 ResNet模型加载和训练循环建议单独一个脚本方便提交和复现。以下代码直接加载 ImageNet 预训练的 ResNet18替换分类头用交叉熵损失和 AdamW 优化器训练import torch import torch.nn as nn from torchvision import models # 1. 固定所有随机源保证每次运行结果一致 def set_seed(seed42): import random import numpy as np random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed() device torch.device(cuda if torch.cuda.is_available() else cpu) # 2. 加载预训练模型替换最后的全连接层 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_classes len(dataset.classes) model.fc nn.Linear(model.fc.in_features, num_classes) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) # 3. 训练 10 个 epoch每个 epoch 后评估验证集 for epoch in range(10): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() running_loss loss.item() * images.size(0) # 验证集评估 model.eval() correct total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) preds model(images).argmax(dim1) correct (preds labels).sum().item() total labels.size(0) train_loss running_loss / len(train_set) val_acc correct / total print(fepoch {epoch1:02d} | train loss {train_loss:.4f} | val acc {val_acc:.4f}) # 4. 保存权重和类别名供报告和推理复用 torch.save({ state_dict: model.state_dict(), classes: dataset.classes, }, checkpoint.pth)参数说明lr3e-4是 AdamW 在迁移学习里比较稳的起点比常见 SGD 的 0.01 小因为预训练模型已经处于不错的局部最优附近学习率过大会破坏已有特征。weight_decay1e-4是 L2 正则能压低权重范数缓解过拟合。num_workers4用 4 个子进程预取数据Windows 下要注意放在if __name__ __main__:里否则多进程会递归报错。10 个 epoch 只是演示判断是否收敛要看验证集准确率曲线是否进入平台期。3.3 必调参数学习率、批量大小、轮数与优化器参数推荐起点调整方向踩坑提示学习率3e-4AdamW验证集 loss 震荡时降为 1e-4过大会发散过小收敛极慢batch_size32显存不足时降到 16太小会导致 BN 统计不稳定epoch10-20看验证 acc 平台期别盲目加轮数过拟合后 acc 会掉优化器AdamW数据少时换 SGDMomentumAdam 类在 fine-tune 时 lr 要调小分类头初始化随机初始化预训练特征复用更稳别忘了只替换最后一层这里提一个常见误区不要在加载预训练权重后把整网所有层学习率设成一样。常见做法是让分类头用较大学习率前面特征提取层用 0.1 倍学习率。实现上把参数分组交给两个优化器即可ft_params model.parameters() new_params model.fc.parameters() optimizer torch.optim.AdamW([ {params: [p for p in model.parameters() if p not in set(model.fc.parameters())], lr: 3e-5}, {params: model.fc.parameters(), lr: 3e-4}, ], weight_decay1e-4)这样前层只微调分类头快速适配新类别是机器学习课程报告里值得写一笔的细节。4. 评估与排错图像分类模型的指标怎么看、坑怎么踩4.1 指标矩阵准确率之外还要看什么课程大作业报告如果只放一个准确率数字答辩时很容易被追问。建议至少输出每一类的召回率、精确率和 F1再画一张混淆矩阵。类别不均衡时准确率会被多数类主导比如 90% 的样本属于 A 类模型全猜 A 也能有 90% 准确率此时 macro-F1 才是更有意义的指标。以下代码直接用 sklearn 算指标from sklearn.metrics import classification_report, confusion_matrix import numpy as np all_preds, all_labels [], [] model.eval() with torch.no_grad(): for images, labels in val_loader: images images.to(device) preds model(images).argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_namesdataset.classes)) print(confusion_matrix(all_labels, all_preds))逻辑说明classification_report会给出每个类别的 precision、recall、F1 和样本数以及 macro avg 和 weighted avg。confusion_matrix的行是真标签、列是预测标签对角线越亮代表分类越准。写报告时重点分析哪里在混淆比如“猫被认成狗”再结合可视化结果说明原因是背景干扰还是类间相似这个分析深度比单纯调高准确率更能体现对图像分类算法的理解。4.2 训练不收敛时的 4 个诊断动作如果训练 loss 不降或验证准确率纹丝不动按以下顺序排查先让模型在一个 batch 上过拟合去掉数据增强用固定几张图训练到 loss 接近 0。做不到说明代码逻辑有问题先修 bug 再谈收敛。检查数据归一化是否匹配预训练权重。ImageNet 权重要求用 ImageNet 的 mean 和 std忘加Normalize或用了自定义归一化迁移学习的特征会失真。排查学习率。打印第一个 epoch 的 loss 变化如果 loss 从 5 涨到 100多半是学习率过大如果 loss 从不变化检查是否把模型设成了 eval 模式训练。检查标签与类别索引是否对齐。ImageFolder按子目录名排序生成索引训练和推理必须用同一个classes列表否则画混淆矩阵时全部错位。如果训练曲线正常但验证集指标低优先怀疑数据划分泄漏有人在datasets.ImageFolder前先做了全量归一化导致验证集信息混入训练集这是数据和预处理层面的典型错误。4.3 过拟合与欠拟合正则化手段的选择顺序图像分类大作业最常见的现象是训练准确率很高、验证准确率明显低这就是过拟合。按优先级做数据增强加强增加随机旋转、色彩抖动、随机擦除相当于扩大样本分布。早停验证 loss 连续若干个 epoch 不降就停止保存最佳模型。权重衰减把weight_decay从 1e-4 调到 1e-3看验证集变化。Dropout 或标签平滑标签平滑把 one-hot 标签变为软标签能抑制模型对训练集过于自信。欠拟合时反向操作先去掉 drop 类正则增大模型容量检查学习率是否过低。每改一个变量只跑一轮对比不要同时动三个参数否则无法归因。5. 交付一份能打的作业权重存档、README 与 zip 打包5.1 让结果可复现随机种子与全链路固定课程大作业提交后老师第一件事是复现。只要代码里出现“每次运行结果不同”答辩体验就很差。除了前面提到的set_seed还要固定 CUDNN 的确定性模式torch.backends.cudnn.benchmark False torch.backends.cudnn.deterministic True注意开启 deterministic 后训练速度会下降但换来的是每一次运行完全一致适合提交前最后一次完整训练。同时把依赖版本记录到requirements.txt重点锁定torch、torchvision、numpy和scikit-learn的主版本号。5.2 文件的目录布局与 zip 压缩最佳实践提交物不能只有一个训练脚本而要让人一眼看懂项目结构。以下是我常用的交付布局图像分类大作业/ ├── data/ # 原始数据若过大可在 README 里给下载方式 ├── src/ │ ├── train.py # 训练、验证、保存权重 │ ├── evaluate.py # 评估指标与混淆矩阵 │ └── utils.py # 种子设置、工具函数 ├── weights/ │ └── checkpoint.pth # 最终权重 ├── report.md / report.pdf # 实验报告 ├── requirements.txt └── README.md打包时排除缓存文件和临时文件命令如下zip -r 机器学习课程大作业_图像分类.zip 图像分类大作业/ \ -x 图像分类大作业/data/* \ -x 图像分类大作业/src/__pycache__/* \ -x 图像分类大作业/weights/*.pth# 如果想把权重也一起交用下面这条更稳妥 zip -r 机器学习课程大作业_图像分类.zip 图像分类大作业/ \ -x 图像分类大作业/src/__pycache__/*参数说明-x用于排除指定路径防止把 Python 缓存或几个 GB 的原始图片塞进压缩包。如果对方解压时提示error read zip archive或者文件无法读取先用unzip -t 压缩包.zip测试完整性再用zip -FF 损坏文件.zip --out 修复文件.zip尝试修复传输过程中断导致的损坏重新压缩比修复更省事。在 Windows 上解压 Linux 压缩的 zip 出现中文乱码时用 7-Zip 或直接指定 UTF-8 编码解压不要让文件名变成乱码再在代码里写死路径。5.3 报告里放一张推理可视化图效果比代码更直观在 README 或 report 里贴一张带预测类别和置信度的推理样例图能显著提升交付质感。用现有的checkpoint.pth写一个只能做单图推理的脚本from PIL import Image import torch from torchvision import transforms, models checkpoint torch.load(weights/checkpoint.pth, map_locationcpu) model models.resnet18() model.fc torch.nn.Linear(model.fc.in_features, len(checkpoint[classes])) model.load_state_dict(checkpoint[state_dict]) model.eval() transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) img Image.open(sample.jpg).convert(RGB) input_tensor transform(img).unsqueeze(0) with torch.no_grad(): probs torch.softmax(model(input_tensor), dim1).squeeze() topk probs.topk(3) for prob, idx in zip(topk.values, topk.indices): print(f{checkpoint[classes][idx]}: {prob.item():.2%})最后把这张推理结果截图放进报告附上类别名映射zip 里再放一个 example 子目录。整份作业从“能跑的代码”升级成了“能被完整理解的交付物”这也正是这门课程大作业最想训练的能力。本文还有配套的精品资源点击获取