news 2026/9/23 2:45:30

基于CNN的猫狗图像识别:从数据准备到模型部署的完整实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于CNN的猫狗图像识别:从数据准备到模型部署的完整实战

简介:这份Python实战项目基于CNN的猫狗图像识别检测分类源码包,面向正在完成期末大作业、毕业设计或需要项目实战的计算机专业学生。项目由导师指导并高分通过,评审得分98分,源码均在本地编译调试可运行,难度适中,适合作为图像分类项目的完整练手案例。压缩包共2000个文件,约218.49MB,以1990张jpg猫狗图片数据集为主,附带Python源码、XML标注文件、PDF文档及工程配置,可直接用于模型训练与结果验证。已有222人学习下载。内容包含完整的CNN模型实现、数据集划分与预处理逻辑,文档PDF对设计思路和关键代码做了说明,方便快速理解并复现;同时附有.gitignore和.iml等工程配置,便于导入PyCharm后直接运行。对需要快速搭建图像识别项目或完善课程报告的学习者,这是一份结构清晰、开箱即用的参考资料。

1. 基于CNN的猫狗图像识别:为什么它是你该认真做完的第一个深度学习项目

如果你正在找第一个深度学习练手项目,猫狗图像识别几乎是绕不开的起点。这个题目听上去简单,但它覆盖了一条完整的链路:数据组织、预处理、模型搭建、训练调参、效果评估、模型导出。对于只想在简历上写“我跑过MNIST”的人来说,把猫狗分类做到验证集准确率95%以上,才有资格说自己真正碰过卷积神经网络。标题里的“高分项目”也印证了这件事——只要是认真做完并写了文档的项目,放在课程设计、毕业设计或者个人作品集里都很能打。

这个项目的价值在于:它不是一个“调包就能跑”的玩具。同样一份数据集,不同的人跑出来效果可以差10个百分点,差别全在数据划分、预处理、超参数和模型结构的选择上。新手照着教程敲一遍能跑通,熟手能在同样的框架下通过迁移学习和数据增强把准确率从80%拉到接近98%。这中间每一步都是可验证、可解释的,不像很多工业项目那样黑匣子到底。这篇笔记我会把完整方案拆开来讲,从环境搭建到模型训练,再到那些容易让人翻车的细节。

2. 猫狗分类的技术选型:从特征工程到CNN,数据目录为什么必须这样建

2.1 为什么用CNN而不是传统特征方法:卷积层到底替你做了什么

在深度学习流行之前,做猫狗图像分类的常见套路是:把图片缩放到固定尺寸,提取颜色直方图、HOG特征或SIFT特征,然后丢进SVM或随机森林里训练。这套方案的问题在于特征是人手工设计的,你设计什么,模型就学什么。猫和狗的差异在颜色上不明显,在纹理上很接近,靠手工特征很难找到一个稳定的分界面。

CNN解决的是“特征自动提取”。卷积层通过滑动窗口在图像上扫描,底层卷积核学到的是边缘、角点、颜色块这些低级特征,高层卷积核把这些组合成耳朵形状、眼睛位置、毛发纹理等语义特征。这个过程不需要人工干预,反向传播会自动调整卷积核的数值。用一句行业里的黑话讲,CNN把“特征工程”变成了“特征学习”,这也是为什么它在图像任务上明显优于传统方法。

对猫狗这个任务来说,CNN还有一个额外的好处:它对位置平移不敏感。猫可能在图片左边也可能在右边,可能大头特写也可能全身照,池化操作让模型在局部区域内有了一定的平移容忍度,这比传统方法里硬性对齐要鲁棒得多。

2.2 数据集目录结构:train/val/test 怎么分,文件名和类别标签的坑

拿到数据集之后,第一件事不是写模型,而是先把目录结构理顺。PyTorch的torchvision.datasets.ImageFolder要求按类别分子目录存放图片,这是最省事的组织方式,也是绝大多数开源代码默认的格式。标准结构如下:

data/ ├── train/ │ ├── cat/ │ │ ├── cat.1.jpg │ │ ├── cat.2.jpg │ │ └── ... │ └── dog/ │ ├── dog.1.jpg │ ├── dog.2.jpg │ └── ... ├── val/ │ ├── cat/ │ └── dog/ └── test/ ├── cat/ └── dog/

这个结构里有两个隐含约定:第一,类别名就是子目录名,ImageFolder会按目录名自动生成类别索引,cat对应0、dog对应1;第二,每个子目录下的图片文件名不重要,但格式要统一,.jpg.png混着放不会报错,但可能在读取时因为通道数不一致(png可能有透明通道)带来维度错误。

划分比例上,我一般用train : val : test = 7 : 2 : 1。数据量在2万张左右时这个比例够用;如果数据只有几千张,可以把验证集压到15%,但测试集不要动,它是你最终评估效果的唯一标准。不要在同一个文件里既做训练又做验证——这种“数据泄漏”会让你的准确率虚高,答辩时一换数据就露馅。

2.3 用ImageFolder还是自己写DataLoader:一个评分项目的关键差别

大部分开源项目用ImageFolder直接读取,它简单、不容易出错。但如果你想在项目文档里写出“我对数据做了精细控制”,那就需要自己写一个Dataset类。两者的差别在于:ImageFolder只能做“按目录读取-返回图片和标签”这件事,而自定义Dataset可以让你在数据层做更多判断。

我见过很多课程设计项目用ImageFolder能跑,但论文里写“我做了数据清洗”,实际上什么清洗都没做。反过来,一个自定义Dataset可以让你在读取时检查图片是否损坏,过滤掉小于某个尺寸的图,甚至可以做类别均衡采样。如果这是你的高分项目,写一个自定义数据类并在文档里说明原因,绝对比默认的ImageFolder更显功夫。

自定义Dataset的骨架代码就几十行,核心就是实现__len____getitem__两个方法。__getitem__里除了读图,还承担了在线数据增强的职责——每读一次就做一次随机变换,相当于让模型在训练时看到同一张图的不同版本,这是缓解过拟合最有效的手段之一。

3. 环境搭建与数据读取:PyTorch下从零跑通最小训练链路

3.1 环境准备:conda虚拟环境、依赖安装与常见翻车点

猫狗分类项目的技术栈选择,PyTorch是更稳的选择。一是它的调试体验比TensorFlow直观,print(tensor.shape)想看什么都能看;二是torchvision里集成了图像数据集和预训练模型,省去很多造轮子的时间。环境搭建我建议用conda创建独立虚拟环境,避免把系统Python搞坏。python安装教程里最常见的问题就是有人在基础环境里直接pip install torch,装到一半和系统包冲突,最后整个环境都废了。

conda create -n catdog python=3.9 conda activate catdog pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install matplotlib scikit-learn

如果机器没有NVIDIA显卡,把最后一行换成CPU版本即可:

pip install torch torchvision

安装完成后立刻验证CUDA是否可用,这一步能提前暴露很多问题:

import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU mode")

torch.cuda.is_available()返回False的原因通常有两个:一是装了CPU版的torch,二是CUDA版本和显卡驱动不匹配。前者重装即可,后者需要去NVIDIA官网确认显卡驱动支持的最高CUDA版本,然后选对应的pytorch安装命令。第一次配环境的人经常在这里卡一整天,我的建议是:先用CPU版本把整个流程跑通,再去追求GPU加速,不要一上来就同时处理环境问题和代码问题。

3.2 transforms.Compose:缩放、裁剪、归一化,每个参数为什么这么设

数据读取之后必须经过预处理才能喂给模型。torchvision.transforms里最常用的是Compose,它把多个变换按顺序串成一个管道。下面是一个标准配置,训练集和验证集用的预处理不一样:

from torchvision import transforms # 训练集:随机增强,提升泛化能力 train_transforms = transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ]) # 验证集/测试集:固定尺寸,不增强 val_transforms = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ])

这里面的参数不是随便填的。Resize(256)先把短边拉到256像素,RandomResizedCrop(224)在256x256的图上随机裁一个224x224的区域,同时做了缩放和裁剪,相当于每个epoch模型看到的都是同一张图的略不同版本。RandomHorizontalFlip是概率50%的水平翻转,猫狗的左右对称性让它成为一个安全的增强手段。Normalize的四个参数是ImageNet数据集的统计值,使用预训练模型时必须沿用这套数值,否则模型输入的分布和它训练时不一致,效果会明显下降。

如果你用的是自己从零训练的CNN(不加载预训练权重),mean和std用[0.5, 0.5, 0.5]也说得过去,但有预训练权重时一定不要乱改。还有一点常被忽略:推理阶段的预处理必须和训练阶段保持一致,我见过好几个项目训练时用RandomResizedCrop,测试时也用了随机裁剪,导致同一张图每次预测结果都不同——这属于写进代码里的逻辑错误。

3.3 训练集与验证集划分脚本:这才是评判项目完整度的地方

很多人拿到数据直接开训,训完才发现验证集和训练集有重叠图片,模型见过“考题”,导致验证准确率虚高。正确做法是写一个划分脚本,一次性完成数据洗牌、按比例复制到不同目录、打印各类别数量。这一步是项目文档里最值得写的部分,因为它体现了你对“数据泄漏”这个概念的理解。

import os import random import shutil source_dir = "raw_data" # 原始数据集路径 output_dir = "data" # 划分后的根目录 split_ratio = (0.7, 0.2, 0.1) random.seed(42) for class_name in ["cat", "dog"]: class_path = os.path.join(source_dir, class_name) images = [f for f in os.listdir(class_path) if f.endswith(".jpg")] random.shuffle(images) n_train = int(len(images) * split_ratio[0]) n_val = int(len(images) * split_ratio[1]) splits = { "train": images[:n_train], "val": images[n_train:n_train + n_val], "test": images[n_train + n_val:] } for split_name, split_images in splits.items(): target_dir = os.path.join(output_dir, split_name, class_name) os.makedirs(target_dir, exist_ok=True) for img in split_images: shutil.copy( os.path.join(class_path, img), os.path.join(target_dir, img) ) print(f"{class_name} {split_name}: {len(split_images)} images")

脚本的逻辑很简单:对每个类别分别洗牌,按比例切三段,复制到新目录。两个细节必须注意。第一是random.seed(42),固定随机种子才能保证每次运行结果一致,否则别人复现你的实验时数据划分不同,效果就对不上。第二是shutil.copy不会删除原文件,重复运行脚本会向目标目录追加图片而不是覆盖,建议划分前先检查目标目录是否为空。类别不平衡的情况也要在划分后打印出来确认,比如猫有10000张、狗有8000张,7:2:1划分后train/cat是7000张、train/dog是5600张,类别比例还算接近,可以直接训;但如果一个类别只有另一个的十分之一,就必须考虑重采样或者类别加权损失函数了。

4. 模型与训练参数:搭建CNN网络并把它练到90%以上的完整配置

4.1 从零搭一个小型CNN:conv-bn-relu-pool的标准结构

理解CNN最好的方式是亲手搭一个小的。下面这个网络只有四组卷积块加一个全连接层,参数量不到500万,在CPU上也能在半小时内完成一次训练。它足够胜任猫狗二分类,也足够让你看清楚每一层的输入输出形状变化。

import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes=2): super().__init__() self.features = nn.Sequential( # Block 1: 3x224x224 -> 16x112x112 nn.Conv2d(3, 16, kernel_size=3, padding=1), nn.BatchNorm2d(16), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2), # Block 2: 16x112x112 -> 32x56x56 nn.Conv2d(16, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2), # Block 3: 32x56x56 -> 64x28x28 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2), # Block 4: 64x28x28 -> 128x14x14 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 14 * 14, 256), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): x = self.features(x) x = self.classifier(x) return x

每池化一次,特征图的宽高减半,通道数翻倍,这是CNN设计的经典模式。padding=1配合kernel_size=3保持宽高不变,可计算性更强。BatchNorm2d放在卷积和激活之间,它做的事情是让每一层的输入分布稳定下来。没有BatchNorm的网络需要手动调低学习率才能稳定训练,加了之后收敛速度会明显加快。Dropout(0.5)是全连接层专用的防过拟合手段,训练时随机丢弃一半神经元,推理时会自动补偿,不需要你在forward里做额外处理。

4.2 用ResNet18迁移学习:更高精度的捷径与改造最后一层

自己搭的CNN能到90%左右,再往上就要增大模型或加数据,性价比最高的方案是换迁移学习。用ImageNet预训练的ResNet18作为特征提取器,只替换最后的全连接层输出维度。代码极其简洁:

import torchvision.models as models model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 把最后一层全连接改成2输出 in_features = model.fc.in_features model.fc = nn.Linear(in_features, 2)

model.fc.in_features是ResNet18分类头的输入维度,值为512,换成自己的Linear层时维度必须对齐。冻结特征层是一个可选的加速手段:

for param in model.parameters(): param.requires_grad = False for param in model.fc.parameters(): param.requires_grad = True

冻结之后反向传播只更新全连接层,显存占用大幅下降,训练速度也快得多。但代价是精度上限低,因为卷积层的特征是为ImageNet的1000类设计的,猫狗虽然也在其中,但迁移过来时底层特征不一定最优。我的建议是分两阶段:先冻结特征层,只训练融合层,跑10个epoch;然后解冻全部层,用很小的学习率(比如1e-5)再微调10个epoch。这比直接全量微调更容易收敛,效果也普遍更好。

4.3 训练循环与超参数表:学习率、batch_size、epochs的合理范围

训练代码的核心是一个循环,这里给出一段可用的最小实现:

import torch import torch.optim as optim from torch.optim.lr_scheduler import ReduceLROnPlateau device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = SimpleCNN(num_classes=2).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) scheduler = ReduceLROnPlateau(optimizer, mode="min", factor=0.5, patience=2) num_epochs = 15 best_val_acc = 0.0 for epoch in range(num_epochs): model.train() running_loss = 0.0 for images, labels in train_loader: 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() avg_loss = running_loss / len(train_loader) # 验证 model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() val_acc = correct / total print(f"Epoch {epoch+1}/{num_epochs} | Loss: {avg_loss:.4f} | Val Acc: {val_acc:.4f}") if val_acc > best_val_acc: best_val_acc = val_acc torch.save(model.state_dict(), "best_model.pth")

这段代码的关键点有三个。第一是optimizer.zero_grad()必须每轮调用,否则梯度会累积,loss表现为剧烈震荡。第二是训练和验证模式切换,model.train()model.eval()影响的是BatchNorm和Dropout的行为,忘了切回train()会导致验证后继续训练的效果异常。第三是torch.no_grad()包裹验证过程,不计算梯度,省显存也提速。

关于超参数,直接给一张我常用的参考表:

参数取值范围说明
学习率0.0001 ~ 0.001Adam下1e-3起步,迁移学习微调用1e-5
batch_size32 ~ 648G显存上限64,更大显存可到128
epochs10 ~ 20小数据集10轮足够,看验证集早停
优化器Adam / SGDSGD需要更大epochs但泛化更好
损失函数CrossEntropyLoss二分类多分类通用,内部含softmax
输入尺寸224x224兼顾细节与显存占用

早停策略是必须写的:每轮验证后记录最佳验证准确率,连续3个epoch不提升就降低学习率,连续5个不提升就停止训练。这条规则能保证你不会在训练后期白白浪费算力,还会让模型的“final”版本恰好停在最优位置,而不是训练循环的最后一步。

5. 踩坑记录:CNN训练中的5个高频问题与排查方法

5.1 训练acc在50%附近震荡:数据顺序与归一化的问题

现象:loss一直在0.69左右,训练准确率始终在50%,不管怎么加大epochs都上不去。

原因:0.69这个数值很典型,它是二分类交叉熵在均匀概率下的理论值,说明模型完全没有在学习。最常见的原因是标签错位——读取数据时猫和狗的索引顺序和预期不符,或者ToTensor()之前忘了Normalize导致输入数值范围异常。另一个隐蔽的原因是DataLoader里忘了设置shuffle=True,模型每个epoch看到的样本顺序完全一样,容易陷入糟糕的局部最优。

解决:先打印一个batch的数据确认维度——images.shape应为(batch_size, 3, 224, 224)labels.shape应为(batch_size,)。再打印labels.unique()确认两类标签都在。如果都正常,把学习率调低十倍重试。最后,确认Normalize里的mean/std没有用错位置。

5.2 验证acc远低于训练acc:过拟合的典型信号与对策

现象:训练集准确率98%,验证集只有78%,并且访问量越高差距越大。

原因:这是过拟合,也就是模型把训练集的细节特征背下来了,而没有学到泛化规律。小数据集、高模型容量、缺乏数据增强,三个条件里中了两个必然过拟合。ResNet18不冻结直接全量训练时最容易出现这个问题。

解决:第一优先级是加数据增强,RandomResizedCropRandomHorizontalFlip是最低成本的组合。第二是加Dropout,如果你用的是迁移学习模型,在全连接层前插入nn.Dropout(0.3)。第三是换用SGD优化器并配上较小的batch_size,SGD加Momentum的泛化能力普遍优于Adam,这是经验之谈。如果以上都没用,回到数据量本身,考虑用已有的图片做离线增强(旋转90度、左右翻转、调亮度),或者收集更多数据。

5.3 CUDA out of memory:小显存机器怎么跑大图

现象:开始训练第一个epoch,程序报错CUDA out of memory,显存占用瞬间打满。

原因:224x224的输入加上batch_size 64,在6GB显存上练ResNet18很容易爆掉。更常见的误操作是DataLoadernum_workers设得过大,每个worker都会拷贝一份数据到显存,多个worker同时开跑直接撑爆。

解决:按优先级操作。先把batch_size降到16或8。然后改输入尺寸,把Resize从256降到192,甚至可以用160,精度损失在1个百分点以内。第三步是检查代码里有没有把验证集或测试集也放到了GPU上——验证不需要梯度,把torch.no_grad()加对位置能省出约三分之一的显存。最后,确认后即删除了显存缓存的变量,或者用deltorch.cuda.empty_cache()手动释放。如果是自己的机器,直接换更大显存的卡是最终解,但调参技巧本身也是项目文档的素材。

5.4 加载预训练权重报size mismatch:原因与正确的加载方式

现象:model.load_state_dict(state_dict)size mismatch for fc.weight

原因:预训练模型的fc层是1000输出,你改成2输出后参数形状对不上。这是正常的,最多余的错误不是bug而是预期中的不匹配。

解决:加载时忽略不匹配的键:

ckpt = torch.load("resnet18_weights.pth") filtered = {k: v for k, v in ckpt.items() if "fc" not in k} model.load_state_dict(filtered, strict=False)

strict=False会跳过缺失的键,但你需要在加载后确认fc层已正确初始化为2输出。用strict=False的代价是,如果模型和预训练权重结构存在其他差异,也会被静默跳过,所以加载后务必打印model.fc确认参数形状。

5.5 自己找的图片全部被分到同一类:预处理不一致与类别先验

现象:训练时验证集准确率很高,但拿网上下载的猫图去预测,全部输出“狗”。

原因:大概率是你的分类模型学到的不是猫和狗的差异,而是训练集里两类图片的背景差异或其他混淆特征。另一个常见原因是推理时忘了做和训练时一致的预处理,尤其是Normalize,直接加载一张PIL图片丢进模型,数值范围在0-255,模型完全懵。

解决:推理代码必须复用val_transforms,不要另写一套。同时检查数据集的单方面特性,比如训练集中猫的图片普遍是室内背景、狗的图片普遍是草地——模型学到的是背景区分而不是动物区分,这种情况需要重新清洗数据。快速验证方法是把验证集里错误分类的图片可视化出来,看模型到底在关注什么区域。

6. 模型验证与导出:用混淆矩阵和真实图片确认效果

6.1 分类报告与混淆矩阵:怎么看模型真正的问题

准确率只能告诉你整体表现,但猫和狗各自的查准率、查全率才是你发现问题的窗口。用scikit-learn一句话就能输出全部指标:

from sklearn.metrics import classification_report, confusion_matrix import numpy as np y_true = [] y_pred = [] model.eval() with torch.no_grad(): for images, labels in val_loader: images = images.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) y_true.extend(labels.cpu().numpy()) y_pred.extend(predicted.cpu().numpy()) print(classification_report(y_true, y_pred, target_names=["cat", "dog"])) print(confusion_matrix(y_true, y_pred))

classification_report输出的recall表示“所有猫里面有多少被正确识别了”,precision表示“被识别为猫的里面有多少真的是猫”。如果狗的precision高但recall低,说明模型倾向于把狗识别成猫,也就是保守——宁可将狗判为猫也不错杀猫。这种倾向可能来自数据类别不平衡,也可能是模型学到的是“有圆耳朵就是猫”。

混淆矩阵是4个数字的事情,但它是答辩时最直观的展示材料。把混淆矩阵做成热力图,配上一句“误分类集中在长毛猫和短毛狗之间,这类样本本身视觉差异小”,比贴十行训练日志更有说服力。

6.2 单张图片推理代码:把模型用起来的最后一步

训练完成后的落地动作是写一个推理脚本,它接收任意一张图片,输出预测类别和置信度:

from PIL import Image import torch import torchvision.transforms as transforms def predict_image(img_path, model, device): model.eval() img = Image.open(img_path).convert("RGB") img_tensor = val_transforms(img).unsqueeze(0).to(device) with torch.no_grad(): outputs = model(img_tensor) probabilities = torch.softmax(outputs, dim=1) confidence, predicted = torch.max(probabilities, 1) class_names = ["cat", "dog"] result = class_names[predicted.item()] conf = confidence.item() return result, conf # 使用 model = SimpleCNN(num_classes=2).to(device) model.load_state_dict(torch.load("best_model.pth")) result, conf = predict_image("test_image.jpg", model, device) print(f"预测: {result}, 置信度: {conf:.4f}")

unsqueeze(0)把形状从(3, 224, 224)变成(1, 3, 224, 224),补充batch维度。torch.softmax拿到各类别的概率分布,torch.max返回最大值和索引——索引映射到类别名。注意Image.open之后要加.convert("RGB"),否则遇到带透明通道的PNG图或灰度图,通道数不一致会导致预处理维度错误。

6.3 保存与加载训练好的模型:避免重训的流程

训练完成后,模型权重必须落盘。两种保存方式差别很大:

# 方式一:只保存权重(推荐) torch.save(model.state_dict(), "catdog_best.pth") model = SimpleCNN(num_classes=2) model.load_state_dict(torch.load("catdog_best.pth")) # 方式二:保存整个模型(不推荐) torch.save(model, "catdog_best_full.pth") model = torch.load("catdog_best_full.pth")

只保存权重的方式要求加载时先实例化一个结构完全相同的模型,再通过load_state_dict填入权重。这种方式跨环境迁移更稳定,模型结构变了也不会加载失败。保存整个模型的方式在路径迁移时经常因为类名或模块路径不一致而报错,我吃过一次亏后就没再用过。正确落盘策略是同时保存权重和训练中间信息:

torch.save({ "model_state_dict": model.state_dict(), "optimizer_state_dict": optimizer.state_dict(), "best_val_acc": best_val_acc, "epoch": epoch + 1 }, "catdog_checkpoint.pth")

checkpoint方案多一个好处:如果训练意外中断,你可以从断点恢复,不需要从头再来。我把这个习惯保留到了所有训练任务里,因为没人保证训练过程中不出现断电、死机或者一个人手抖关掉了终端。每5个epoch存一个checkpoint,是我用血泪经验换来的习惯。

做这类项目我一直坚持一条原则:模型能跑通只是底线,能解释每一行代码为什么这么写才是高分。数据集怎么划分、预处理参数怎么选、为什么用这个优化器、踩过什么坑,这些才是一个项目文档真正有价值的地方。这套猫狗分类的方案从数据到模型到验证已经完整跑通,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 2:41:36

鸿蒙+Flutter跨平台开发:用图像分割技术打造口红试色APP全实践

鸿蒙Flutter跨平台开发:用图像分割技术做一款口红试色APP的完整实践做跨平台开发这些年,我最大的感受是:真正的痛点从来不是“能不能跑”,而是“跑起来之后体验到底行不行”。尤其是当目标平台变成鸿蒙的时候,情况就更…

作者头像 李华
网站建设 2026/9/23 2:40:16

流感时间序列预测实战:ARIMA、LSTM与Transformer对比及残差混合建模

简介:这份Python源码项目围绕流感时间序列预测展开,整合ARIMA、SARIMA、LSTM与Transformer等多类模型,面向计算机相关专业正在做课程设计、期末大作业或需要项目实战练习的学习者,帮助其完成从数据平稳性检验、差分处理到模型估计…

作者头像 李华
网站建设 2026/9/23 2:40:15

线性回归预测PM2.5:从特征工程到模型部署的完整指南

简介:面向机器学习入门者与数据分析学习者,基于线性回归的PM2.5预测系统完整源码包,围绕空气污染物浓度预测任务,提供从数据读取、预处理到模型训练与结果导出的全流程Python实现。压缩包共19个文件,以12个csv数据文件…

作者头像 李华
网站建设 2026/9/23 2:34:59

食品干燥技术:热风与红外耦合的Comsol仿真实践

1. 食品干燥技术概述食品干燥是食品加工中最基础也最关键的环节之一。作为一名在食品工程领域摸爬滚打十多年的从业者,我深知干燥工艺对食品品质的决定性影响。传统热风干燥虽然设备简单、成本低廉,但普遍存在能耗高、时间长、营养损失大等问题。而红外干…

作者头像 李华