简介:一套面向毕业设计场景的Python卷积神经网络图像分类系统完整资料,涵盖CNN原理讲解、模型实现与训练文档,适合正在学习深度学习、需要快速上手图像分类项目的高校学生。压缩包共25个文件,包含13个Python脚本、模型备份文件、编译缓存、数据集与训练好的权重,以及Markdown和JSON格式的说明与配置文档,整体仅62KB,结构紧凑。已有64人学习使用。资料内提供LeNet-5、AlexNet、GoogLeNet、ResNet等经典网络的实现,可对比TensorFlow与PyTorch两种框架的写法,并配有README、模型调用入口、辅助脚本等,方便读者理解从数据预处理到模型训练、评估的完整流程。借助源码与文档,既能支撑毕业设计的代码复现,也能帮助系统掌握CNN在图像分类中的实际应用。
1. 图像分类系统从源码到可用模型:先别急着敲训练命令
手上拿到一套「Python 实现 CNN 卷积神经网络的图像分类系统」的源码和模型文档资料,最常见的反应是打开终端直接跑训练脚本,然后被报错、版本、路径问题劝退。这套东西本质上解决一个问题:让你用 Python 完成从图片数据到分类模型的完整闭环,中间覆盖数据整理、卷积网络搭建、训练调参、评估导出,以及文档里那些只能靠踩坑才能理解的超参数。它适合刚好有点 Python 基础、第一次接触 CNN 图像分类的读者;不适合只想抄一条命令就跑出 99% 准确率的人,因为图像分类的坑全部藏在数据和参数里,不在那一行命令里。
2. CNN 图像分类的原理与模型选型:为什么卷积核能学会“看”图像
图像分类系统选什么网络结构,取决于你手里有多少数据、多少显存、推理要跑在 CPU 还是 GPU 上。在动手改源码前,先把 CNN 的基本结构过一遍,后面调参才不会被一堆层名绕晕。
2.1 卷积层在做什么:局部感受野与参数共享
图像在程序里是一个多维张量:尺寸 224×224×3 的 RGB 图,放进全连接网络会变成 150528 个输入节点,第一层隐层如果也是 1024 个节点,光这一层就有超过 1.5 亿个权重参数。这个规模在训练时既跑不动,也极容易过拟合。卷积网络换了一种思路:用一个小尺寸的卷积核(比如 3×3 或 5×5)在图像上滑动,每次只看局部一个小区域,这就是局部感受野。
卷积核的权重在整张图上共享,同一个核提取同一种特征,比如水平边缘、颜色块、纹理方向。这样参数量从亿级降到几千级,这也是 CNN 能在图像任务上站稳的基础。输出特征图的尺寸由输入尺寸、卷积核大小、padding 和 stride 共同决定,公式是:
output = floor((H + 2*padding - kernel_size) / stride) + 1
用代码定义一个卷积层,并验证输出尺寸,是最快建立手感的方式:
import torch.nn as nn import torch # 输入: 1 张 224x224 的 RGB 图像 -> [1, 3, 224, 224] x = torch.randn(1, 3, 224, 224) # 3x3 卷积, padding=1, stride=1 时输出尺寸不变 conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, padding=1, stride=1) y = conv1(x) # [1, 16, 224, 224] print(y.shape)这里 in_channels=3 对应 RGB 三个通道,out_channels=16 表示用 16 个卷积核,得到 16 张特征图。kernel_size 越小,局部感受野越小,但可以通过堆叠多层扩大感受野;padding=1 是为了让边界像素也被卷积核覆盖。out_channels 是网络宽度的重要旋钮,从 16 加到 64 会显著增加参数量和显存开销。
2.2 池化与激活:下采样与引入非线性
卷积层是线性的滑动窗口加权求和,多个卷积层叠在一起如果中间没有激活函数,整个网络仍然是线性的,表达不了复杂映射。现在一般用 ReLU 做激活:f(x)=max(0,x),计算简单,梯度在正区间恒为 1,能缓解深层网络的梯度消失。在 CNN 基础里,激活层和卷积层通常成对出现,“卷积 -> BN -> ReLU”几乎是所有现代主干的标准写法。
池化层负责下采样,最常见的是 MaxPooling。它的作用不是增加参数,而是把 2×2 窗口里的最大值留下,丢掉其他 75% 的信息,换来平移不变性和更大的后续感受野。注意池化没有可学习参数,但 kernel_size 和 stride 直接影响特征图缩小的比例,比如 nn.MaxPool2d(2) 会让宽高各减半。源码里如果看到连续 3 个 224 输入,每次池化后变成 112、56、28,这就是一条清晰的下采样路径,也是判断网络是否写到一半的思路。
2.3 常见主干模型怎么选:LeNet / AlexNet / ResNet 的适用边界
搞图像分类系统,通常有三种起点,取决于数据量和算力:
| 模型 | 深度 | 参数量量级 | 适用场景 |
|---|---|---|---|
| LeNet-5 | 5 层 | 约 6 万 | MNIST 手写数字、教学演示 |
| AlexNet | 8 层 | 约 6000 万 | 百万级数据的开创者,已少用 |
| ResNet-18 | 18 层 | 约 1100 万 | 中小数据集迁移学习首选 |
| ResNet-50 | 50 层 | 约 2500 万 | 数据量大、GPU 充裕时更好 |
源码里如果自带训练好的模型文件,先看它是什么结构的权重,再决定怎么改分类头。常见做法是直接用 torchvision 的预训练 ResNet18 做迁移学习,把最后一层全连接改成自己的类别数:
import torch.nn as nn import torchvision.models as models num_classes = 10 # 按自己的类别数改 # weights= 表示加载 ImageNet 上预训练权重,替代旧版 pretrained=True model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 替换最后一层分类头 model.fc = nn.Linear(model.fc.in_features, num_classes)用迁移学习而不是从零训练,是小数据集图像分类系统的默认方案。ImageNet 上训好的浅层特征(边缘、颜色、纹理)对大多数自然图像都通用,需要重新学的只有后面的分类层。pretrained 参数在新版 torchvision 里推荐用 weights= 方式指定,直接写 pretrained=True 会收到弃用警告,这个坑在模型文档资料里经常被忽略。如果你的源码版本旧,用的还是 pretrained=True,先看清版本再动手。
挑选主干时还要考虑推理环境:CPU 机器上 ResNet18 比 ResNet50 快一倍以上,嵌入式设备可能更合适 MobileNet。深层模型在小数据集上不一定会更好,反而更容易过拟合。先选一个跑得动的模型把全流程走通,再换大模型,这是最稳的推进节奏。
3. 把源码跑起来:数据准备、训练脚本与关键参数
拿到源码后的第一件事是先把运行环境固定住。与其去翻 python 安装教程里那些版本选择细节,不如直接装官方 Python 3.10 或 3.11,然后为这个项目单独建一个虚拟环境,避免把老项目和当前项目互相污染。环境稳定后,再按数据、增强、训练、保存四步走。
3.1 数据集目录约定与 ImageFolder 加载
图像分类源码拿到手,第一步永远不是改模型,而是确认数据目录长什么样。PyTorch 的 ImageFolder 约定每个类别一个子文件夹,训练数据按类别分目录,这个约定一旦乱掉,标签全部错位,且报错不会告诉你哪里错。目录结构一般是:
data/ ├── train/ │ ├── cat/ # 所有猫的图片 │ ├── dog/ │ └── bird/ └── val/ ├── cat/ ├── dog/ └── bird/train 和 val 按同样类别划分。用 ImageFolder 加载:
from torchvision import datasets, transforms train_dataset = datasets.ImageFolder( root='data/train', transform=train_transform, ) val_dataset = datasets.ImageFolder( root='data/val', transform=val_transform, ) print(train_dataset.class_to_idx) # {'cat': 0, 'dog': 1, 'bird': 2} print(train_dataset.classes)ImageFolder 会按文件夹名字母序分配标签索引,也就是说 cat 是 0 不是因为它在数据里排第一,而是因为它按字母序排第一。class_to_idx 这个字典一定要打印出来看一眼,很多训练脚本里标签错位都是在这里悄悄发生的。如果数据不是按目录组织的,而是一张 CSV 记录图片路径和类别,就得自定义 Dataset,常见做法是写一个类,初始化时读 CSV,getitem里读图并返回样本和标签。
训练集和验证集的划分最好在数据准备阶段就完成,不要在训练代码里随机切,否则每次跑出来的实验不可对比。没有现成 val 目录时,用 sklearn 的 train_test_split 按 8:2 划分并保持每类比例,stratify 参数是按标签分层抽样,防止少数类别在验证集里消失。
需要补数据时,如果公开数据集不包含你的目标类别,常见做法是用 python 爬虫去图片站抓一批图,但这里提醒一句:抓图前先确认版权和平台条款,用来做学习 demo 问题不大,商用要慎重,这不是技术问题,是风险问题。
3.2 数据增强:训练集和验证集为什么不能一样
图像分类系统里最容易拉开差距的不是网络结构,而是数据增强策略。训练集要做随机变换提升泛化性,验证集只做尺寸调整和归一化,保证评估结果稳定。如果验证集也做随机翻转,每次评估的准确率都会抖动,没法比较实验。
from torchvision import transforms # 训练集增强 train_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p=0.5), transforms.ColorJitter(brightness=0.2, contrast=0.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]), ])Resize((256, 256)) 后再 RandomCrop(224) 是经典做法,它比直接 Resize 到 224 多了一点随机裁剪的空间,相当于免费的数据扩充。RandomHorizontalFlip 对左右对称的任务(猫狗、车型)有效,但对文字识别这类左右不对称的任务要关掉。ColorJitter 的值不要给太大,0.2 左右足够,太大可能把颜色分布改到偏离真实场景。
归一化的 mean 和 std,用预训练模型时一定要用 ImageNet 这套标准值,因为预训练权重是在这套归一化下训练出来的。如果用自己的数据从头训练,mean/std 应该用训练集的统计值,但多数图像分类系统都走迁移学习路线,所以直接沿用 ImageNet 的数值。ToTensor 会把 0~255 的像素值缩放到 0~1,Normalize 再按通道减均值除标准差,这个顺序不能颠倒。
3.3 训练循环:损失、优化器、学习率调度
训练循环是整套源码的核心。多数源码会有一个 train.py,里面包含损失函数、优化器、学习率调度器和训练循环。一个能直接改的最小训练函数大约长这样:
import torch.optim as optim from tqdm import tqdm def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() running_loss, correct, total = 0.0, 0, 0 for images, labels in tqdm(dataloader): images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() avg_loss = running_loss / len(dataloader) acc = 100.0 * correct / total return avg_loss, accoptimizer.zero_grad() 每步清空梯度,backward 反传,step 更新权重,这三行顺序不能乱。outputs.max(1) 返回每个样本预测类别的值和索引,predicted.eq(labels) 是逐一比较的布尔张量,sum 后就是正确数。tqdm 只负责显示进度,不影响结果,不想装可以去掉。
超参数的选择直接决定训练是否收敛,下面是常见起点:
| 参数 | 常用值 | 说明 |
|---|---|---|
| batch_size | 16 / 32 / 64 | 显存决定上限,过小导致梯度震荡 |
| epochs | 30~50 | 迁移学习通常不需要太多 |
| learning_rate | 1e-3 / 1e-4 | 替换分类头用 1e-3,微调主干用 1e-4 |
| weight_decay | 5e-4 | L2 正则,抑制过拟合 |
| optimizer | Adam 或 SGD | Adam 上手快,SGD 需要配 lr decay |
优化器选择上,Adam 适合刚上手,收敛快、对学习率不敏感;SGD 配合 cos 或 step 学习率调度,最终精度往往略高。常见做法是先把整体跑通用 Adam,稳定后换 SGD + momentum=0.9。学习率调度有三种:固定 lr、按 epoch 阶梯下降、余弦退火。阶梯下降里 milestone 一般设在总 epoch 的 1/2 和 3/4 处,每次降为原来的 0.1。用 PyTorch 自带的 CosineAnnealingLR 也可以,效果稳定且少一个需要调的数字。
3.4 保存 checkpoint:别只存 state_dict
训练到一半服务器重启、GPU 被占、loss 跑飞想回退,这些情况在图像分类项目里几乎都会遇到,所以保存模型不能只存一个 state_dict,要存完整 checkpoint。至少包含模型参数、优化器状态、当前 epoch、最佳验证准确率,这样断点续训才有后悔药吃。
import torch def save_checkpoint(state, filename): torch.save(state, filename) # 每个 epoch 结束后调用 best_acc = 0.0 for epoch in range(1, total_epochs + 1): train_loss, train_acc = train_one_epoch(...) val_acc = evaluate(model, val_loader, device) checkpoint = { 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'best_acc': max(best_acc, val_acc), 'num_classes': num_classes, } if val_acc > best_acc: best_acc = val_acc save_checkpoint(checkpoint, 'checkpoints/best.pth') save_checkpoint(checkpoint, 'checkpoints/last.pth')best.pth 保留验证集表现最好的权重,last.pth 保留 epoch 结束时权重,两个文件的用途完全不同。断点续训时先 map_location 加载再恢复 optimizer:
ckpt = torch.load('checkpoints/last.pth', map_location='cpu') model.load_state_dict(ckpt['model_state_dict']) optimizer.load_state_dict(ckpt['optimizer_state_dict']) start_epoch = ckpt['epoch'] + 1恢复 optimizer 状态很多人会漏,漏掉之后 lr 会重新变回初始值,前 10 个 epoch 的学习率调度就白做了。模型文档资料里如果提到“继续训练”却只给了 load_state_dict,就要自己补上 optimizer 恢复。加载时推荐先 map_location='cpu' 再 move 到 GPU,省掉很多跨设备加载的报错。
4. 模型评估与推理:准确率之外还要看什么
训练结束不等于项目结束。图像分类系统的交付物应该是可复现的评估结论和可用的推理接口,而不是一个只会打印训练 acc 的脚本。这一章的评估函数、混淆矩阵和推理脚本,几乎每个项目都要用到。
4.1 验证集与测试集:评估函数怎么写得稳
训练完的模型不能只看训练集准确率。图像分类系统里通常有两条独立数据路径:验证集用于挑模型和调参,测试集用于最后一次性评估。如果一直在测试集上试来试去,测试集就变成了验证集,最后报告的指标会虚高。这个道理几乎所有文档资料都会写,但源码里常见的问题是只有 val 没有 test,这时至少要留出一部分 val 数据不参与调参。
评估函数比训练函数简单,核心是 model.eval() 和 torch.no_grad():
def evaluate(model, dataloader, device): model.eval() correct, total = 0, 0 with torch.no_grad(): for images, labels in dataloader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() acc = 100.0 * correct / total return accwith torch.no_grad() 关闭梯度计算,推理速度和显存占用都更友好。model.eval() 会切换 BN 层和 Dropout 的行为,训练时 BN 用 batch 统计量,评估时用全局统计量;Dropout 在评估时被关闭。只调用 evaluate 不写 model.eval() 是很多源码翻车的原因,BN 层行为不一致会导致验证准确率忽高忽低。
验证集准确率如果要和训练集对比,还要统一输入预处理和 batch_size 的影响。batch_size 大时 BN 统计更稳定,换 batch_size 后验证结果也可能轻微波动,所以比较实验时尽量保持评估管线不变。
4.2 混淆矩阵与每类召回率:发现偏科
总准确率 95% 的模型,可能某一类只有 60% 召回率,这类“偏科”在总指标里被掩盖了。图像分类系统的评估层面,混淆矩阵比准确率更有诊断价值。用 sklearn 一行就能算:
from sklearn.metrics import confusion_matrix, classification_report import numpy as np def collect_preds(model, dataloader, device): all_preds, all_labels = [], [] model.eval() with torch.no_grad(): for images, labels in dataloader: images = images.to(device) outputs = model(images) all_preds.extend(outputs.argmax(1).cpu().numpy()) all_labels.extend(labels.numpy()) return np.array(all_preds), np.array(all_labels) preds, labels = collect_preds(model, val_loader, device) print(confusion_matrix(labels, preds)) print(classification_report(labels, preds, target_names=class_names))confusion_matrix 的行是真实类别,列是预测类别,对角线越亮越好。看到某一类被大量分到邻近类,通常是这两类在视觉上相似,数据里样本数又不均衡。classification_report 里的 f1-score 是 precision 和 recall 的调和平均,类别样本很少时只盯这一个指标就够了,因为它不会被多数类的高准确率稀释。
定位到偏科类别后,常见做法有三种:给少样本类别做过采样或复制;用 class_weight 给损失函数里的少数类加权;收集更多该类别的图片。前两种在源码里改动最小,class_weight 的计算方式一般是 n_samples / (n_classes * class_count),传入 CrossEntropyLoss 的 weight 参数即可。
4.3 单张图片推理与批量推理:别重复加载模型
训练完的模型最终要落到推理脚本。推理脚本的典型流程是:加载 checkpoint、把模型送到设备、对单张图做预处理、前向、取 top-k。图片分类系统的推理脚本里,最容易出现的坑是每张图都重复加载一遍模型,或者把预处理写错导致输出结果和图片对不上。
import torch import torch.nn as nn import torchvision.models as models from PIL import Image def load_model(checkpoint_path, num_classes, device): model = models.resnet18(weights=None) model.fc = nn.Linear(model.fc.in_features, num_classes) ckpt = torch.load(checkpoint_path, map_location=device) model.load_state_dict(ckpt['model_state_dict']) model.to(device).eval() return model def predict_one(model, image_path, transform, device, topk=5): img = Image.open(image_path).convert('RGB') x = transform(img).unsqueeze(0).to(device) # [1, C, H, W] with torch.no_grad(): logits = model(x) probs = torch.softmax(logits, dim=1) topk_probs, topk_idx = probs.topk(topk, dim=1) return [(idx.item(), prob.item()) for idx, prob in zip(topk_idx[0], topk_probs[0])]torch.softmax 把 logits 转成概率,topk 取概率最高的前 k 个类别。要注意 Image.open 默认保留图片的原始色彩模式,灰度图转 RGB 用 convert('RGB'),否则通道数对不上预处理。推理时模型要放在同一个 device 上,如果用 GPU 训练的 checkpoint 到 CPU 机器上跑,load 时 map_location='cpu',再传 device。
批量推理时瓶颈往往在单张解码上,常见做法是把一批图片文件做成 DataLoader,num_workers 开 2 到 4 个线程预读,这样能跑满 GPU。文档资料里如果给出了推理帧率或单张耗时,先确认这个数字是在什么硬件上测的,CPU 和 GPU 能差一个数量级。
4.4 模型导出:pth、ONNX 与部署边界
训练完整的模型要落地到业务,通常要导出成部署格式。pth 是 PyTorch 训练专用,部署服务大多不支持直接加载;ONNX 是中间格式,能被 ONNX Runtime、TensorRT 等推理引擎转换。图像分类系统的源码里,建议保留一条导出脚本,方便做跨平台推理。
| 格式 | 适合场景 | 熟悉程度 | 备注 |
|---|---|---|---|
| .pth/.pt | 继续训练、调试 | 高 | 依赖 PyTorch 环境 |
| .onnx | 服务端/边缘推理 | 中 | 跨框架、可转 TensorRT |
| .pt 加 torch.jit.script | 纯 PyTorch 部署 | 中 | 保留动态控制流,PyTorch 版本敏感 |
导出 ONNX 的最小写法:
model.eval() dummy = torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy, "classifier.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, opset_version=12, )dynamic_axes 声明 batch 维度可变,这样导出的模型在推理时能接受任意 batch size。opset_version 决定了算子兼容性,12 是保守选择,新版推理引擎可能要求更高,但设太高老的引擎读不了。导出后建议用 onnxruntime 快速验证一遍输出是否和 PyTorch 一致,偶尔会因为 BN 层或 upsample 算子出现数值差异。
5. CNN 图像分类复现避坑清单:5 个高频翻车点
这一章是血泪经验的汇总。前面几章把主流程走通后,真正消耗时间的通常不是原理,而是下面这几类问题。每一条都按现象、原因、解决三步写,方便照着排查。
5.1 训练集图片全对不上:ImageFolder 的标签排序坑
现象:训练正常跑完,验证准确率也有 80%,但把测试图片单独拿出来预测,结果完全不对,像是标签整体错位了。
原因:ImageFolder 按文件夹名字母序分配标签,而手工标注数据的人往往按自己习惯的类别名顺序贴标签,两个顺序不一致。源码里如果用了 ImageFolder 却没有打印 class_to_idx 并核对,标签错位一路带到训练和预测,损失函数还在正常下降,模型学到的其实是错位的映射。
解决:在加载数据集后立刻打印 class_to_idx,并和项目文档里的类别清单逐项比对。最好在数据准备阶段写一个脚本从 Excel 或 CSV 生成固定映射,存成 json,训练和推理都从同一个 json 读标签,避免两套顺序。
5.2 CUDA out of memory:显存不够不等于模型太大
现象:训练脚本一跑就报 CUDA out of memory,然后被建议换 24G 显卡,但同项目的同学用 6G 卡能跑。
原因:显存峰值往往不在模型参数,而在中间特征图和优化器状态。batch_size=64、输入 224、ResNet50 时,中间特征图加起来比模型参数还多;Adam 优化器还要额外存一阶二阶动量,大约是参数量的两倍。新手最常见的翻车是开着验证集、tensorboard 和多个日志同时占显存。
解决:先把 batch_size 降到 16 或 8 试稳,再逐项排查。设置 torch.cuda.empty_cache() 不能真正腾出显存,治标不治本。低显存运行模型有几个实用手段:启用混合精度(AMP)能减少约一半显存;用 torch.no_grad() 包住验证;把临时 Tensor 及时 del 并 detach。把迁移学习的 backbone 冻结(requires_grad_(False))也能显著减少优化器动量占的显存,效果接近显存减半。
5.3 loss NaN 或训练集准确率一直不动:学习率与初始化
现象:训练前几个 epoch loss 直接变成 nan,或者 loss 很稳定但准确率不涨,像是模型根本没在学。
原因:nan 最常见的原因是学习率过大,梯度更新一步就冲爆;如果用了预训练模型并替换分类头,新初始化的分类头输出方差偏大,与预训练特征尺度不匹配,也会让 loss 一开始就很难看。准确率不动则通常是把 backbone 也设了 requires_grad=False,只训练分类头时又忘了把分类头放到 GPU 上。
解决:先打印每一步的 loss 和梯度的范数(grad_norm),如果 grad_norm 超过几百,立刻降低学习率,把 lr 从 1e-3 降到 1e-4 重试。换分类头时,对新的 fc 层做更小的初始化:nn.init.xavier_normal_(model.fc.weight) 或者直接单独给分类头设 10 倍小的学习率,PyTorch 里可以给 optimizer 传不同参数组。数值稳定性上,自定义 dataset 的标签要确认是 0 到 num_classes-1 的整数,如果从 1 开始,CrossEntropyLoss 很容易在边界上出问题。
5.4 验证集精度高、测试集崩:数据泄漏与随机种子
现象:验证集准确率 95%,换一批测试图只剩 60%,差距大到明显不合理。
原因:最常见的泄漏是数据划分前没有洗牌,或者同一个类别的图片在 train 和 val 里来自同一批文件夹;另一个泄漏来自数据增强里的 RandomCrop,如果对同一张原始图在训练和验证里都做固定裁剪,验证集其实早就被训练过程“见过”了。另一个隐蔽来源是随机种子没固定,PyTorch 和 numpy 双随机源,数据加载的 worker 顺序每次不一样,导致 val 集合到训练集合的边界在每次 run 之间漂移。
解决:数据划分用 sklearn 的 train_test_split 并指定 random_state=42;在训练入口固定三个种子:
import random, numpy as np, torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = Falsecudnn.deterministic=True 让卷积运算在浮点层面可复现,但会牺牲一部分性能;benchmark=False 关闭自动选择最优算法。这两个开关只在需要严格对比实验时开,平时训练可以保持默认。
5.5 文档资料和源码版本对不上:先看环境再跑
现象:按 README 里的命令装依赖,一跑就是 import 报错,或者某个 API 已经不存在。
原因:模型文档资料写于 PyTorch 旧版本,现在版本的 API 换了新写法。旧版本的 models.resnet18(pretrained=True) 写法弃用了;DataLoader 的 pin_memory 行为也有变化;甚至 Python 版本都可能不同,比如老源码用 Python 3.6 的 typing 写法,新版 Python 3.11 里部分语法已不兼容。
解决:先读文档里的 requirements 文件,再看 torch 版本和 python 版本是否匹配。日常做法是为这个源码单独建一个 conda 或 venv 环境,不要直接用全局环境,避免把老项目和当前项目互相污染。遇到 API 弃用报错,去 PyTorch 的 release note 查改动路径,而不是看到一个旧写法就照抄。模型文件如果是旧版本保存的 .pth,需要在 load 时指定 map_location 兼容,这类问题在复现老源码时几乎必踩。
6. 文档资料怎么读:从模型文档反推训练细节
拿到一套源码加文档资料,最终能不能复现,取决于你会不会读文档里那些“没写出来的内容”。模型文档资料里一般有 README、训练日志、checkpoint、配置文件。这里有一个实用心法:先读 checkpoint,再读 README。
ckpt = torch.load('checkpoints/best.pth', map_location='cpu') print(ckpt.keys()) for k, v in ckpt.items(): if not hasattr(v, 'shape'): print(k, v)这个代码片段会把 checkpoint 里的非张量字段打印出来:epoch、best_acc、超参记录、类别映射,很多项目的文档没写全但 checkpoint 里留了。
| 资料类型 | 能反推出的信息 |
|---|---|
| 训练日志(loss/acc 曲线) | 学习率是否衰减、是否过拟合、最佳 epoch |
| checkpoint 里的超参字段 | batch size、epoch、输入尺寸、类别数 |
| 模型文件的参数量 | 主干结构是 ResNet 还是 VGG 级 |
| 数据目录片段 | 类别数量、标签顺序 |
我的习惯是,拿到文档先做三件事:确认 PyTorch 版本、打印 checkpoint 的 keys、查预训练模型对应的类别数。如果文档自相矛盾,以实际能跑为准,先跑通再改文档。图像分类系统的源码复现,八成精力花在数据准备和环境匹配上,模型结构反而是最稳定的部分。希望帮到你。
本文还有配套的精品资源,点击获取