简介:这份资源面向学习深度学习与图像分类的初学者及课程设计学生,提供一套基于Python与PyTorch的完整实验方案,帮助理解如何利用计算机对图像进行定量分析,将图像或像元划归到不同类别,替代人工视觉判读。压缩包共7个文件,约239KB,包含4个py源码文件、1个md说明、1个docx设计报告和1个license,源码覆盖线性分类器、多层感知机、卷积神经网络及统一运行入口,报告则记录实验设计与分析过程,便于对照代码理解模型原理。目前已有2091人学习下载,说明该方案在同类课程设计中具有一定参考价值。读者可借此掌握从数据加载、模型搭建到训练评估的完整流程,并参考报告中的实验思路完成自己的图像分类任务,适合作为入门实践与课程作业的参考模板。
1. 从一份图像分类源码包说起:它到底能跑出什么结果
如果你手头正好有一份「基于 Python 实现图像分类.zip」,大概率是课程设计、实训作业或者自学练手时拿到的。它不是一个能直接上生产的工业级框架,而是一套把「数据读取 → 模型搭建 → 训练 → 评估 → 推理」串起来的完整可运行代码。对新手来说,它的价值在于省去了从零搭目录、写训练循环的时间;对熟手来说,它是一块可以快速替换骨干网络、改损失函数、调数据增强的实验田。我见过太多人拿到压缩包后卡在环境配置和路径报错上,最后连一次完整的训练都没跑通,所以这篇笔记不聊虚的,直接把这份资源拆开,告诉你每一块代码在干什么、参数怎么改、哪里最容易翻车。适合正在做课程设计、想跑通第一个 CNN 分类项目、或者需要一份能改的 PyTorch 模板的人。
2. 拆开压缩包:目录结构与 PyTorch 训练主链路
2.1 先认清这份源码的骨架
拿到压缩包解压后,常见做法是看到类似data/、models/、train.py、predict.py、utils.py、requirements.txt这样的结构。不同作者命名习惯不一样,但核心逻辑跑不出这几块:数据加载、模型定义、训练脚本、推理脚本、依赖清单。我一般会先打开requirements.txt和train.py,因为这两个文件决定了你能不能跑起来、以及跑起来后改哪里。
先看依赖。图像分类项目绕不开torch、torchvision、numpy、Pillow,有些还会带matplotlib画 loss 曲线、tqdm显示进度条。这里有个血泪经验:不要盲目pip install -r requirements.txt,因为作者写版本号时可能锁死了 CUDA 版本,而你本机是 CPU 或者另一版 CUDA,装完直接报torch not compiled with CUDA enabled。稳妥做法是先确认自己的环境,再手动装匹配的 torch。
# 先看本机 Python 版本,建议 3.8 - 3.11 python --version # 查看是否有 NVIDIA 显卡及驱动支持的 CUDA 版本 nvidia-smi # CPU 版本安装示例(没有显卡或不想折腾 CUDA 时用) pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 其余轻量依赖 pip install numpy Pillow matplotlib tqdm上面这段命令的逻辑是:先摸清底牌,再决定装哪个 torch 轮子。--index-url指向官方 CPU 轮子仓库,避免默认源里混入 GPU 版本导致体积巨大且跑不起来。参数上,torchvision必须和torch版本对应,比如 torch 2.0 配 torchvision 0.15,错配会在 import 时直接抛异常。
2.2 数据加载与 Dataset 写法
图像分类的数据组织方式通常有两种:一种是按文件夹分好类,每个类别一个子目录,用ImageFolder直接读;另一种是 CSV 里写图片路径和标签,自定义Dataset。这份资源里常见的是前者,因为课程设计数据集一般不大,按类分文件夹最直观。
import os from torch.utils.data import DataLoader from torchvision import datasets, transforms # 定义训练集预处理:随机裁剪、翻转、转张量、归一化 train_transform = transforms.Compose([ transforms.Resize((224, 224)), # 统一尺寸,CNN 输入要求固定 transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转,轻量增强 transforms.ToTensor(), # 转成 C,H,W 的张量,值域 0-1 transforms.Normalize( # 按 ImageNet 均值方差归一化 mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ]) # 假设数据目录结构为 data/train/class_a/*.jpg, data/train/class_b/*.jpg train_dataset = datasets.ImageFolder(root='data/train', transform=train_transform) train_loader = DataLoader( train_dataset, batch_size=32, # 显存不够就降到 16 或 8 shuffle=True, # 训练集必须打乱,否则模型学到顺序偏差 num_workers=4 # Windows 下如果报错就改成 0 ) print('类别列表:', train_dataset.classes) print('样本总数:', len(train_dataset))这段代码的关键点有三个。第一,Resize((224, 224))不是随便写的,如果你后面要用 ResNet、VGG 这类在 ImageNet 上预训练过的骨干,输入尺寸就得对齐,否则全连接层维度对不上。第二,Normalize的均值和方差也是 ImageNet 统计出来的,用预训练权重时保持一致,从头训练时可以用自己数据集的统计值,但新手直接用这套不会出大错。第三,num_workers在 Windows 上经常因为多进程 spawn 机制报BrokenPipeError,改成 0 虽然慢一点,但能跑通比什么都重要。
验证集和测试集用同样的ImageFolder,但 transform 里去掉随机翻转和裁剪,只保留 Resize、ToTensor、Normalize。这一点很多人忽略,结果验证指标忽高忽低,还以为是模型玄学,其实是验证集也在做随机增强。
2.3 模型定义:从简单 CNN 到迁移学习
这份资源里大概率包含一个自定义的 CNN 类,也可能直接调用torchvision.models.resnet18。两种写法我都拆一下,因为课程设计答辩时老师常问「你为什么选这个网络」。
自定义 CNN 的典型写法:
import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super(SimpleCNN, self).__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), # 输入 3 通道 RGB nn.ReLU(inplace=True), nn.MaxPool2d(2), # 尺寸减半 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.AdaptiveAvgPool2d((1, 1)) # 全局平均池化,替代展平 ) self.classifier = nn.Linear(128, num_classes) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) # 展平成 (batch, 128) x = self.classifier(x) return xAdaptiveAvgPool2d((1, 1))是个好东西,它让网络对输入尺寸不再敏感,后面接全连接层时不用手算特征图大小。num_classes必须和你数据集的类别数一致,比如森林图像分类有 6 类就写 6,写错了训练不报错但结果全乱。
如果资源里用的是迁移学习,常见做法是加载预训练 ResNet18,把最后的fc层换掉:
import torchvision.models as models import torch.nn as nn model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) num_features = model.fc.in_features model.fc = nn.Linear(num_features, 10) # 10 换成你的类别数这里weights参数在新版 torchvision 里替代了旧的pretrained=True。用预训练权重的理由是:小数据集上从头训练容易过拟合,而 ImageNet 上学到的边缘、纹理特征对大多数自然图像都有用。代价是模型体积大、推理慢一点,课程设计里完全可接受。
2.4 训练循环与参数设置
训练脚本是整份资源的核心,通常包含损失函数、优化器、学习率、epoch 循环、验证、保存权重。我按可复现的顺序写一遍:
import torch import torch.nn as nn import torch.optim as optim from tqdm import tqdm device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = SimpleCNN(num_classes=10).to(device) criterion = nn.CrossEntropyLoss() # 多分类标准损失 optimizer = optim.Adam(model.parameters(), lr=1e-3) # Adam 对新手友好 scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) best_acc = 0.0 for epoch in range(30): model.train() running_loss = 0.0 for imgs, labels in tqdm(train_loader, desc=f'Epoch {epoch+1}'): imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() # 梯度清零,否则会累加 outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() # 反向传播 optimizer.step() # 更新参数 running_loss += loss.item() scheduler.step() # 每个 epoch 后调整学习率 # 验证阶段 model.eval() correct, total = 0, 0 with torch.no_grad(): # 关闭梯度,省显存 for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() acc = correct / total print(f'Epoch {epoch+1}, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {acc:.4f}') if acc > best_acc: best_acc = acc torch.save(model.state_dict(), 'best_model.pth') print('已保存最佳模型')参数说明:lr=1e-3是 Adam 的常用起点,如果 loss 震荡厉害就降到 1e-4;batch_size=32在 8G 显存下跑 224×224 的 ResNet18 基本够用,不够就减半;step_size=10, gamma=0.1表示每 10 个 epoch 学习率乘 0.1,适合训练轮数在 30 到 50 之间的场景。torch.no_grad()在验证时必须加,否则显存会随着 batch 累积暴涨,这是新手最常见的翻车点之一。
3. 推理与评估:把模型用起来才算闭环
3.1 单张图片推理脚本
训练完保存了best_model.pth,接下来要能对一张新图片给出类别。推理脚本和训练脚本的区别在于:不需要标签、不需要反向传播、需要把预测结果映射回类别名。
import torch from PIL import Image from torchvision import transforms # 类别名要和训练时 ImageFolder 的 classes 顺序一致 class_names = ['class_a', 'class_b', 'class_c'] # 按实际替换 infer_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]) ]) model = SimpleCNN(num_classes=len(class_names)) model.load_state_dict(torch.load('best_model.pth', map_location='cpu')) model.eval() img = Image.open('test.jpg').convert('RGB') # 强制转 RGB,防止灰度图报错 input_tensor = infer_transform(img).unsqueeze(0) # 增加 batch 维度 with torch.no_grad(): output = model(input_tensor) prob = torch.softmax(output, dim=1) conf, pred = torch.max(prob, 1) print(f'预测类别: {class_names[pred.item()]}, 置信度: {conf.item():.4f}')convert('RGB')这行看着不起眼,但如果你测试的图片是 PNG 带透明通道或者灰度图,不转直接进ToTensor()会得到 4 通道或 1 通道,和模型第一层Conv2d(3, ...)不匹配,报错信息还特别绕。unsqueeze(0)是给单张图补上 batch 维度,因为模型 forward 默认接受 4 维输入。
3.2 评估指标:别只看准确率
课程设计里老师常要求画混淆矩阵、算精确率和召回率。如果数据集类别不均衡,比如森林图像分类里某类样本特别少,准确率会骗人。常见做法是用sklearn.metrics的classification_report:
from sklearn.metrics import classification_report, confusion_matrix import numpy as np model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in test_loader: imgs = imgs.to(device) outputs = model(imgs) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_names=class_names)) print(confusion_matrix(all_labels, all_preds))classification_report会给出每个类别的 precision、recall、f1-score,比一个笼统的 accuracy 有说服力得多。混淆矩阵能看出模型把哪两类搞混了,比如把「松树」认成「杉树」,这比单纯说「准确率 85%」更有分析价值。
4. 避坑与排查:那些让训练跑不起来的常见问题
4.1 路径与中文目录导致的读取失败
现象:ImageFolder报FileNotFoundError或者读到的样本数为 0。原因通常是数据目录层级不对,比如data/train/class_a/class_a/*.jpg多套了一层,或者路径里带中文、空格。解决:先用os.listdir打印目录内容确认层级,路径尽量用英文;Windows 下如果路径有反斜杠,在 Python 字符串里用r'data\train'或正斜杠。
4.2 CUDA out of memory
现象:训练几个 batch 后报RuntimeError: CUDA out of memory。原因可能是 batch_size 太大、图片分辨率太高、或者验证阶段没加torch.no_grad()导致显存持续累积。解决:先把 batch_size 降到 8 或 16,再把输入尺寸从 224 降到 128 试试;确认验证和推理代码都在with torch.no_grad():块里;实在不行用torch.cuda.empty_cache()手动清缓存,但根治还是减负载。
4.3 损失不下降或变成 nan
现象:loss 一直停在 2.3 左右(10 分类的随机水平),或者几个 epoch 后变成 nan。原因常见的有:学习率太大、数据没归一化、标签越界、损失函数选错。解决:先把学习率降到 1e-4 观察;确认Normalize的均值和方差与输入匹配;检查num_classes是否等于实际类别数,标签从 0 开始连续;多分类用CrossEntropyLoss,不要用BCELoss。
4.4 验证集准确率远高于训练集
现象:训练 loss 很高,验证准确率却高得离谱。原因通常是数据泄漏——训练集和验证集用了同一批图片,或者验证集太小且恰好简单。解决:确认train和val目录没有重叠文件;验证集至少占总数 20%;如果数据集本身很小,用 K 折交叉验证代替单次划分。
4.5 Windows 下 num_workers 报错
现象:BrokenPipeError或RuntimeError: DataLoader worker exited unexpectedly。原因是 Windows 的多进程启动方式和 Linux 不同,num_workers > 0时容易出问题。解决:把num_workers设为 0,训练慢一点但稳定;或者在if __name__ == '__main__':保护下写训练代码,这是 Windows 多进程的硬性要求。
5. 进阶技巧:用预训练模型和冻结策略把准确率再拉一截
如果你已经把基础版本跑通,准确率卡在某个数上不去,最划算的升级不是换更复杂的网络,而是用迁移学习加分层学习率。我一般会这么做:加载 ResNet18 或 ResNet50 的预训练权重,先把骨干网络冻结,只训练最后的全连接层几个 epoch,让分类头先适应你的数据分布;然后解冻后面几层,用更小的学习率微调。这样既不会因为随机初始化的分类头把预训练特征带偏,也能在有限数据上拿到比从头训练高出一截的结果。
import torchvision.models as models import torch.nn as nn import torch.optim as optim model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 第一步:冻结所有骨干参数 for param in model.parameters(): param.requires_grad = False # 替换分类头,这部分参数默认 requires_grad=True model.fc = nn.Linear(model.fc.in_features, 10) # 只优化分类头 optimizer = optim.Adam(model.fc.parameters(), lr=1e-3) # 训练 5 个 epoch 后解冻 layer4 和 fc,用更小学习率 for param in model.layer4.parameters(): param.requires_grad = True optimizer = optim.Adam([ {'params': model.layer4.parameters(), 'lr': 1e-4}, {'params': model.fc.parameters(), 'lr': 1e-3} ])这段代码的关键在于requires_grad的切换和参数组的学习率设置。冻结阶段只更新fc,解冻后layer4用 1e-4、fc用 1e-3,是因为浅层特征更通用、不需要大改,深层特征更贴近具体任务、需要小幅调整。如果你的数据集和 ImageNet 差异很大(比如医学影像、工业缺陷),可以解冻更多层,但学习率要相应调小。
另一个实用技巧是保存最佳模型时同时保存类别映射。很多人训练完过几天再推理,忘了class_names的顺序,结果预测标签全错位。我习惯在训练脚本里把train_dataset.classes写进一个classes.json,推理时直接读,省得靠记忆。
import json with open('classes.json', 'w', encoding='utf-8') as f: json.dump(train_dataset.classes, f, ensure_ascii=False) # 推理时 with open('classes.json', 'r', encoding='utf-8') as f: class_names = json.load(f)从那以后我每次跑完训练都强制走一遍「保存权重 + 保存类别映射 + 用测试图跑一次推理」的流程,确认闭环通了再关终端。这份资源的价值不在于代码多高级,而在于它给了你一个能改、能跑、能交作业的起点,剩下的调参和排错才是真正长本事的地方。希望帮到你。
本文还有配套的精品资源,点击获取