简介:图像分类是计算机视觉中最基础也最典型的任务,而卷积神经网络(CNN)通过自动学习边缘、纹理、形状等多层级视觉特征,取代了传统人工特征加分类器的繁琐方案,成为解决此类问题的核心技术。在实际工程中,模型搭建只是其中一环,真正决定项目成败的往往在于数据集的整理与预处理、训练流程的细节控制以及评估方法的全面性。本文从通用技术视角切入,系统讲解如何组织图片数据集、统一尺寸与归一化、使用数据增强提升泛化能力,并深入拆解CNN的卷积层、池化层和全连接层设计原理。在此基础上,给出基于PyTorch的完整训练流程、模型保存与加载技巧,以及利用混淆矩阵、精确率和召回率进行更全面评估的方法。最后引出迁移学习、目标检测等进阶方向,帮助初学者将入门项目经验迁移到更广泛的应用场景中。 第一次用卷积神经网络做猫狗图像分类的时候,我最大的困惑不是模型怎么搭,而是数据集怎么整理。网上很多“源码+数据集”项目下载下来,目录乱成一团,图片大小不一,有的还带水印,跑起来全是坑。后来我才明白,一个能直接跑通的Python深度学习项目,真正值钱的部分往往不在model.py里,而在数据预处理和训练流程那些容易被忽略的细节中。
这篇文章从项目本身出发,围绕基于卷积神经网络实现的猫狗图像分类,结合完整源代码和数据集,把我们实际动手时最关注的几件事讲清楚:数据集该怎么整理、CNN模型该怎么搭、训练过程有哪些坑、评估结果怎么看得更全面。无论你是刚入门的深度学习初学者,还是已经在用PyTorch做分类任务、想补全整个项目细节的开发者,这篇都能给你一些可以直接抄走的经验。
1. 这个猫狗分类项目到底在解决什么问题
1.1 从需求场景说起:为什么要做图像分类
图像分类是计算机视觉里最基础、也最典型的一类任务。给定一张图片,模型输出它属于哪个类别,在这个项目里就是“猫”或“狗”。听起来简单,但背后覆盖了深度学习项目的大部分通用流程:数据准备、模型设计、训练、评估、预测部署。你把这个流程跑通了,后面换到其他分类任务,比如识别车型、判断工业零件缺陷、区分农作物病害,路线都是一样的。
有些朋友会问:“猫狗分类这种入门项目,网上不是一抓一大把吗?还有必要认真做吗?”我的看法恰恰相反。正因为它是入门项目,才值得把每一步都吃透。很多人用预训练模型跑通一个demo就觉得自己会了,但实际上,数据加载为什么要这样写、损失函数为什么选交叉熵、训练时为什么得分batch、验证集和测试集有什么区别,这些问题不亲手从头搭一遍是搞不明白的。
这个项目的“源码+数据集”形态,正好补足了纯理论学习的短板。你不需要自己满世界去找图片,也不用从零写数据爬虫,拿到材料后可以直接从数据预处理开始跑,遇到问题时又能对照源码逐行排查。这种完整闭环的学习方式,比只跑通一个notebook要扎实得多。
1.2 为什么选卷积神经网络而不是传统方法
在深度学习普及之前,做猫狗识别这类任务,通常走的是“人工特征+传统分类器”路线。比如先用HOG或者SIFT提取图片特征,再训练一个SVM分类器。这个方案有几个明显的毛病:特征需要人凭经验设计,不同数据集的适配成本高;光照、背景、遮挡稍微变一变,特征就不稳定;模型泛化能力有限。
卷积神经网络(CNN)本质上把“特征提取”这一步也交给了网络自己学习。卷积层在训练过程中自动学到边缘、纹理、形状等不同层级的视觉特征,不需要人工去设计特征算子。这个差别非常关键。你可以把传统方法理解为“按图索骥”——你得先知道要找什么线索;CNN则是“自己总结规律”——你只给它数据和标签,它自己摸索出猫和狗的区别。
具体到猫狗分类,猫和狗在体型、毛色、耳朵形态、脸部比例上都有差异,有些还很相似,比如毛发颜色相近的柯基和橘猫。CNN通过多层卷积组合,能从像素级细节里提取出足够有区分度的特征,这在传统方法下是很难实现的。
1.3 项目整体架构与预期效果
这个项目的标准流程可以用一条线串起来:图片数据 → 数据预处理 → CNN模型 → 训练 → 评估 → 预测。我习惯把整个项目分成几个层级来看:
- 数据层:原始图片、标签文件、训练/验证划分逻辑。
- 模型层:网络结构定义、权重初始化、前向传播。
- 训练层:损失计算、反向传播、优化器更新、学习率调整。
- 评估层:准确率、混淆矩阵、分类报告。
- 应用层:单张图片预测、批量预测接口。
预期效果方面,如果只用自定义的轻量CNN在几万张猫狗图片上训练,验证集准确率通常能到85%~92%左右。如果换上ResNet这类预训练模型做迁移学习,达到98%以上也不难。取决于你对模型复杂度和训练时间的取舍。
2. 数据集获取与预处理:第一次踩坑就在这
2.1 数据集来源与目录组织
猫狗分类最经典的数据集是Kaggle上的“Dogs vs. Cats”数据集,包含25000张训练图片和12500张测试图片,猫和狗各占一半。这个数据集的优点是量大、贴近真实场景,图片里有各种姿势、光线和背景;缺点是图片大小不一、有些还带水印和信息栏,需要清洗。
源码包里的数据集通常已经整理成类似下面的目录结构:
dataset/ ├── train/ │ ├── cat.0.jpg │ ├── cat.1.jpg │ ├── dog.0.jpg │ └── dog.1.jpg ├── val/ │ ├── cat.0.jpg │ ├── dog.0.jpg │ └── ... └── test/ ├── 1.jpg └── 2.jpg有些版本会按类别分子文件夹:
train/ ├── cats/ │ ├── xxx.jpg │ └── ... └── dogs/ ├── xxx.jpg └── ...这个差异会直接影响后面写DataLoader的方式。如果你从网上下载的源码用的是第一种命名方式,但你的数据集是第二种结构,那训练前就必须改加载逻辑,否则标签会全部错乱。我见过不少朋友卡在这一步,其实问题不在代码,而在目录约定。
我的习惯是,拿到任何数据集先跑一段快速扫描代码,统计每个类别的图片数量、图片格式、是不是有损坏文件,确认无误再进模型。这一步能省掉后面无数个深夜排查时间。
2.2 图片清洗与尺寸统一
CNN的输入尺寸是固定的,比如224×224或128×128,所以数据预处理里最重要的一件事就是统一尺寸。直接resize当然可以,但有几个细节需要注意。
首先是图片格式问题。数据集中可能混有灰度图、RGBA图、损坏的截断图片。用OpenCV读取时,灰度图直接读出来是单通道,RGBA多一个alpha通道,模型输入是固定三通道的话就会报维度错误。我一般在数据加载时统一做一次转换:
import cv2 def load_image(path, target_size=(224, 224)): img = cv2.imread(path) # 有些图是灰度图,需要转成三通道 if len(img.shape) == 2: img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) elif img.shape[2] == 4: img = cv2.cvtColor(img, cv2.COLOR_RGBA2RGB) img = cv2.resize(img, target_size) return img其次是归一化。像素值范围是0~255,如果直接丢给模型,数值过大容易导致梯度不稳定。一般会把像素值缩放到0~1,或者按均值和标准差做标准化。PyTorch里常用transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),这是ImageNet数据集的均值和标准差,对通用图片效果比较好。
还有一点容易被忽略:图片的方向。手机拍摄的图片经常带有EXIF旋转信息,如果不处理,缩略图方向是对的,但读成数组后是横着的。数据清洗阶段最好统一按EXIF信息校正一下方向,不然模型会学到“竖着的猫”和“横着的猫”两种特征,影响泛化。
2.3 数据增强:用有限数据榨出更多泛化能力
数据增强是图像分类里性价比最高的技巧之一。它的核心思想是,在训练时对图片随机做微小变换,让模型看到更多变体,从而提高泛化能力。你不需要增加一张真实图片,就能让模型学会应对平移、翻转、亮度变化等情况。
常用的增强操作包括:
- 随机水平翻转:猫狗图片左右翻转不影响类别,这个操作简单且有效。
- 随机旋转(±15度):增加角度鲁棒性。
- 随机裁剪缩放:模拟不同距离的观察视角。
- 颜色抖动:改变亮度、对比度、饱和度,提升对光照变化的适应能力。
PyTorch里用torchvision.transforms组合即可:
from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])注意,数据增强只在训练集上用,验证集和测试集通常只做resize和归一化。否则验证集的结果会被增强操作污染,无法反映模型在真实图片上的表现。
还要提醒一点:增强强度不是越大越好。旋转角度太大,猫到了上下颠倒的位置,虽然理论上模型也能学,但分类难度会明显增加,训练收敛变慢,小数据集上反而容易欠拟合。建议从小的增强强度开始,观察验证集准确率的变化。
3. 卷积神经网络模型搭建与核心原理拆解
3.1 卷积层、池化层、全连接层到底在干嘛
CNN的基本组件不多,但每个都有明确的职责。
卷积层的核心是“局部特征提取”。一个卷积核其实就是一个小的权重矩阵,比如3×3或5×5,它在输入图片上滑动,每一步做一次点积运算,生成一个特征图。不同的卷积核会关注不同的特征:有的负责边缘,有的负责纹理,更深的层则组合出眼睛、耳朵、四肢等语义部件。卷积层通过参数共享大大减少了参数量,这是它比全连接层更适合图像的核心原因。
池化层的作用是“下采样”和“保持平移不变性”。最常用的是最大池化,比如2×2窗口取最大值。它把特征图尺寸缩小一半,保留了最显著的特征,同时也增大了后续卷积层的感受野。你可以把池化理解为“把一张高清图缩小成缩略图,主要内容还在,细节有所损失,但整体语义更集中”。
全连接层则负责把卷积层提取的高层特征映射到类别空间。通常在网络最后,把特征图展平成一维向量,经过若干全连接层,最终输出每个类别的得分。在分类任务里,全连接层的输出维度等于类别数,猫狗分类就是2。
一个常见的误解是:网络越深越好。实际上,单纯堆叠卷积层而不加BatchNorm、残差连接等设计,网络会很难训练,甚至出现退化问题。所以搭建网络时,不要盲目追求层数,先把基础结构跑通,再逐步加深。
3.2 模型结构设计:从LeNet到自定义CNN
我们可以设计一个轻量级CNN来跑猫狗分类,结构参考LeNet思想但做了现代化调整。下面这个结构在源码里比较常见:
import torch.nn as nn class CatDogCNN(nn.Module): def __init__(self, num_classes=2): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 28 * 28, 256), nn.ReLU(inplace=True), nn.Dropout(p=0.5), nn.Linear(256, num_classes) ) def forward(self, x): return self.classifier(self.features(x))输入224×224的图片,经过三层卷积和池化后,特征图尺寸变成28×28(因为224/2/2/2=28),输出通道128,所以全连接层第一层的输入维度是128×28×28。
这里有几个设计考量:
- 卷积核都用3×3,这是当前主流做法。两个3×3卷积堆叠的感受野等同于一个5×5卷积,但参数量更少,非线性表达能力更强。
- 每层卷积后都接ReLU激活,引入非线性。没有激活函数的话,多层线性变换等价于一层线性变换,就没有“深度”的意义了。
- 分类器里加了Dropout,随机丢弃一半神经元,这是最常用的防过拟合手段。
这个模型参数量很小,在GPU上训练非常快,在CPU上也能跑。如果你觉得效果不够好,可以把卷积通道数加倍,或者再增加一层卷积,但也要注意过拟合问题。
3.3 激活函数、损失函数与优化器选择
激活函数方面,隐藏层用ReLU是默认选择,计算简单且能缓解梯度消失。但ReLU有个问题:负数区域梯度直接为0,如果学习率设置不当,很多神经元会“死掉”,权重不再更新。实际训练中如果发现损失不降,可以检查一下是否出现大量Dead ReLU,这时候适当降低学习率或者改用LeakyReLU会有帮助。输出层做二分类,一般用LogSoftmax或者直接输出裸logits,配合交叉熵损失函数。
损失函数选nn.CrossEntropyLoss()是标准做法。它内部把softmax和负对数似然结合在了一起,输入不需要手动做softmax。它的设计初衷是优化概率分布之间的差异,非常适合分类任务。要注意的是,如果你的标签是0/1,但网络输出是二维向量,CrossEntropyLoss要求标签是类别索引(0或1),不要传one-hot编码,否则会报错。
优化器常用Adam,学习率初始值取1e-3或1e-4都行。Adam自带了动量机制,收敛比较快,对学习率的敏感度也比SGD低,适合新手。但SGD配合动量在充分调参后往往能取得更好的最终精度。我的建议是:项目初期用Adam快速验证流程,后期想冲精度再用SGD+momentum配合学习率衰减。
学习率衰减也很重要。常见做法是每若干轮把学习率乘一个系数(比如每10轮乘0.1),或者用ReduceLROnPlateau在验证集指标不再提升时自动降低学习率。我实测下来,ReduceLROnPlateau更省心,不需要手动扳着指头算epoch。
4. 训练流程与代码实现细节
4.1 训练集/验证集划分与DataLoader
数据准备好后,第一步是划分训练集和验证集。验证集不能和训练集重叠,否则模型评估结果会虚高。常见的划分比例是8:2或9:1。如果数据集本身已经分了train和val目录,直接用就行;如果没有,可以用train_test_split或者手动按文件名划分。
这里有个容易被忽视的问题:数据划分时要做分层抽样,保证训练集和验证集中猫狗比例都接近1:1。如果随机划分导致验证集里全是猫,那模型预测狗的能力就没得到评估,准确率指标会失真。
PyTorch中把数据加载封装成Dataset和DataLoader。一个简化版代码如下:
from torch.utils.data import Dataset, DataLoader from PIL import Image class CatDogDataset(Dataset): def __init__(self, file_list, label_list, transform=None): self.file_list = file_list self.label_list = label_list self.transform = transform def __len__(self): return len(self.file_list) def __getitem__(self, idx): image = Image.open(self.file_list[idx]).convert('RGB') if self.transform: image = self.transform(image) label = self.label_list[idx] return image, labelDataLoader设置batch_size的时候要考虑显存大小。以224×224输入、batch_size为32为例,在6GB显存的GPU上,上面的轻量CNN可以轻松跑。如果换ResNet50,可能就得降到16甚至8。
num_workers参数也很重要。如果设成0,数据加载在主进程里做,速度会慢;设成4或8可以用多进程预取数据,大幅缩短每个epoch的时间。但要注意,在Windows上num_workers大于0时,代码必须放在if __name__ == '__main__':保护块里,否则会报错。
4.2 训练循环里那些容易忽略的细节
训练循环的骨架大家都清楚:前向传播、算损失、反向传播、更新权重。但有几个细节新手特别容易踩。
第一个是optimizer.zero_grad()。PyTorch的梯度是累加的,如果每一轮不清零,梯度会在多个batch上累积,导致更新方向完全错乱。正确做法是每个batch都要清零梯度。我习惯把这三行连着写:
optimizer.zero_grad() loss.backward() optimizer.step()第二个是模型模式切换。训练前要调model.train(),让Dropout和BatchNorm进入训练模式;评估前要调model.eval(),让它们进入推理模式。如果用torch.no_grad()包裹评估过程,能减少内存占用,因为不需要计算梯度。
第三个是梯度裁剪。在训练初期或者使用大学习率时,梯度可能爆炸,导致损失变成NaN。这时可以加一个梯度裁剪:torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)。这相当于给梯度设置了一个上限,防止更新步伐过大。
训练过程中最好每一轮都记录损失和验证集准确率。我一般会打印这样的日志:
Epoch [3/20] train_loss: 0.2384 train_acc: 0.9021 val_loss: 0.2871 val_acc: 0.8817顺带说一句,如果发现训练损失一直在降,但验证损失开始反弹,说明模型过拟合了,这时候应该减少训练轮数、增大Dropout或者加强数据增强。
4.3 模型保存与加载的坑
模型保存看起来简单,但坑不少。最常见的保存方式是把模型参数保存为state_dict:
torch.save(model.state_dict(), 'best_model.pth')加载的时候要先实例化模型,再load_state_dict:
model = CatDogCNN(num_classes=2) model.load_state_dict(torch.load('best_model.pth', map_location='cpu'))这里有一个容易踩的坑:保存模型时用GPU训练,加载时如果机器没有GPU,必须加map_location='cpu',否则会抛出类似“Ran out of memory”的错误。还有一个坑是模型结构定义变了再加载旧权重,比如全连接层维度改了,load_state_dict会因为key不匹配而报错。所以保存模型时最好连超参数和网络结构信息一起存下来:
torch.save({ 'model_state_dict': model.state_dict(), 'num_classes': num_classes, 'input_size': 224 }, 'checkpoint.pth')这样以后加载时还能知道当初是怎么训练出来的,避免“代码重写后权重废掉”的尴尬。
另外,保存模型时不要只保存“最后一个epoch”的模型,最好在验证集准确率最高的时候保存一份。我通常用一个简单的逻辑:
if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'best_model.pth')这样即使后面过拟合了,手里还有一份最优备份。
5. 预测与评估:只看准确率远远不够
5.1 混淆矩阵、精确率、召回率怎么解读
分类任务的评估不能只看准确率。在猫狗分类这种类别平衡的数据集上,准确率确实有参考价值,但如果遇到类别不平衡的数据集,准确率会骗人。假设99%是猫,模型把全部预测成猫,准确率也有99%,但显然没有任何实用价值。
这时候混淆矩阵能帮你看得更细。它描述了真实类别和预测类别的对应关系:
预测猫 预测狗 实际猫 TN FP 实际狗 FN TP召回率表示“实际是狗,被正确找回来的比例”,精确率表示“预测为狗的样本里真正是狗的比例”。在猫狗分类场景中,如果你做的是宠物自动喂食器,漏掉一只狗可能没关系,但误把猫识别成狗可能就会出问题。不同的应用场景,对精确率和召回率的侧重完全不同。
PyTorch里可以用sklearn.metrics快速生成这些指标:
from sklearn.metrics import confusion_matrix, classification_report cm = confusion_matrix(y_true, y_pred) print(classification_report(y_true, y_pred, target_names=['cat', 'dog']))我每次训练完都会打印这两样东西,比单纯看准确率能更早发现模型偏向性问题。比如模型对猫的召回率95%但对狗只有80%,那说明它在区分某些体型的狗时遇到了困难,可能需要在数据增强里增加相关的变换。
5.2 单张图片预测与批次预测的差异
训练完成后,实际使用场景里有两种预测方式:单张图片预测和批量预测。两者的实现细节有很大区别。
单张图片预测最容易出的问题是维度。训练时输入是四维张量(batch, channel, height, width),但单张图片读出来是三维(channel, height, width),需要先unsqueeze(0)加一个批次维度:
image = load_image("test_cat.jpg") image = torch.tensor(image).permute(2, 0, 1).unsqueeze(0).float() image = image / 255.0 with torch.no_grad(): output = model(image) prob = torch.softmax(output, dim=1) pred = torch.argmax(prob, dim=1).item()这里要注意PIL和OpenCV读取图片的通道顺序不同。PIL读出来是RGB,OpenCV读出来是BGR,如果混用,模型看到的就是蓝红通道互换的图片,预测结果会大幅下降。我自己就踩过这个坑,明明验证集准确率93%,随手拿一张图片测试却总是分错,最后发现是用OpenCV读取但训练时却用PIL转换,颜色通道错乱了。
批量预测则要考虑内存占用。如果图片数量很多,不要一次性全部加载到GPU显存,应该分批预测并将结果收集到列表。DataLoader的batch_size可以设大一点,反正只在推理模式下跑,不保存梯度。
5.3 可视化中间特征图:CNN到底学到了什么
很多初学者把CNN当成一个黑盒,训练完就算完事。但如果你想把模型调得更好,或者向别人解释模型到底在做什么,可视化中间特征图是非常重要的手段。
方法不复杂:在前向传播时,把某些层的输出记录下来,然后转换成图片显示。以我们前面定义的CatDogCNN为例,可以这样获取第一层卷积的输出:
activation = {} def hook_fn(name): def hook(model, input, output): activation[name] = output.detach() return hook model.features[0].register_forward_hook(hook_fn('conv1')) output = model(image)拿到特征图之后,把每个通道画出来。你会发现浅层的特征图大多是边缘、颜色块,深层的特征图则对应眼睛、耳朵、身体轮廓等更抽象的语义信息。这个过程能帮你判断模型有没有学到合理特征。
有一次我训练完一个猫狗分类模型,发现第一层卷积核几乎全是灰的,权重值特别小。排查后发现是学习率设置得太高,训练一开始就崩了,模型退化成只会输出固定类别。后来调低学习率并加了学习率预热,特征图才恢复正常。这种问题如果只看准确率,很容易误以为是数据问题。
6. 源码结构与运行环境配置
6.1 环境版本组合建议
深度学习项目最让人头痛的问题之一就是环境配置。不同版本的Python、PyTorch、CUDA之间可能存在兼容性问题,盲目装最新版不一定好用。
以这个猫狗分类项目为例,我推荐一套比较稳妥的版本组合:
- Python 3.8 或 3.10(3.9也可以,但有些旧依赖容易出问题)
- PyTorch 1.13 或 2.x
- torchvision 0.14(如果配合PyTorch 1.13;2.x的话torchvision版本会对应变化)
- CUDA 11.7 或 12.1(取决于显卡驱动)
- numpy、opencv-python、pillow、matplotlib、scikit-learn
安装PyTorch时,建议直接用PyTorch官网的安装命令,比如:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu117如果机器上没有GPU,或者显卡驱动版本较老,安装CPU版本也能跑通整个流程,只是训练速度会慢。猫狗分类这种小网络在CPU上也能训练,只是每个epoch要等比较久。我以前的旧笔记本没有独立显卡,训练20个epoch大约要四五个小时,后来换了GPU只要几分钟。
6.2 源码文件逐个说明
拿到源码包后,先别急着运行,把目录结构看清楚。一个标准的猫狗分类项目通常包含这些文件:
project/ ├── data/ │ ├── train/ │ └── val/ ├── src/ │ ├── dataset.py # 数据集加载与预处理 │ ├── model.py # CNN模型定义 │ ├── train.py # 训练脚本 │ ├── predict.py # 单张图片预测脚本 │ └── utils.py # 工具函数(画图、日志等) ├── checkpoints/ # 模型保存目录 ├── requirements.txt └── README.mddataset.py里可能定义了CatDogDataset类,负责读图、做增强、返回样本和标签。model.py里是网络结构定义。train.py是主入口,包含数据加载、训练循环、验证循环、模型保存逻辑。predict.py则是让你传入一张图片,输出猫或狗的预测结果。
我建议拿到源码后先按顺序读一遍README.md和train.py的主流程,不要上来就运行。有些源码包依赖太多,需要手动下载预训练权重,或者数据集路径写死了,不改成相对路径就会报错。你至少要先搞清楚:
- 数据集路径在哪设置?
- batch_size和epoch在哪里改?
- 模型保存到哪个目录?
这几个问题搞清楚了,运行起来基本就稳了。
6.3 运行过程中常见的依赖冲突
依赖冲突是运行源码时最常见的拦路虎。很多人会遇到“ImportError: cannot import name 'xxx' from 'torchvision'”,这通常是因为torch和torchvision版本不匹配。解决办法很简单:pip list看一下两个包的版本,然后在torchvision官方文档里确认对应关系。千万不要只升级其中一个,要一起升级或者一起降级。
另一个常见问题是numpy版本过高导致某些旧代码报错。比如np.float属性在numpy 1.24之后被移除了,如果你运行的代码用了np.float,会报AttributeError。这时可以降低numpy版本,或者把代码里的np.float改成float。我一般会避免在代码里使用带np.float这种过时的写法,因为它在新旧版本间很容易出问题。
还有opencv-python和opencv-contrib-python不能同时安装,否则会有底层库冲突,甚至导致程序崩溃。只保留其中一个就好。
7. 从基础CNN到实用改进:调优思路与后续方向
7.1 从普通CNN到迁移学习
如果你已经通过自定义CNN跑通了猫狗分类,下一步最值得做的改进就是迁移学习。迁移学习的思路很简单:利用在ImageNet这种超大数据集上预训练好的模型,比如ResNet18、ResNet50或者EfficientNet,替换掉最后的分类层,然后用自己的数据集微调。
PyTorch里实现迁移学习非常方便:
import torchvision.models as models model = models.resnet18(pretrained=True) model.fc = nn.Linear(model.fc.in_features, 2)预训练模型已经学到了丰富的通用视觉特征,即使你的训练数据不多,也能达到不错的效果。我在只有几千张猫狗图片的数据集上直接训练自定义CNN,准确率大约88%;换用ResNet18冻结前面的层、只训练最后的分类层,准确率能到95%以上;如果解冻全部层微调几个epoch,可以接近98%。
这里有一个需要权衡的点:解冻多少层微调。如果数据集很小,解冻太多层容易过拟合;数据集大,冻结太多层又限制模型表达力。我的经验是先冻结全部层只训练分类头,跑通之后逐步解冻最后一两个block,观察验证集变化。这样比较稳。
另外要注意,迁移学习的输入尺寸一般为224×224,如果你的数据预处理用了不同的resize尺寸,模型效果会受影响。
7.2 过拟合与欠拟合的排查思路
训练过程中最常见的问题就是过拟合和欠拟合。我总结了一个简单的排查思路。
如果训练损失下降很慢,准确率也低,先怀疑欠拟合。这时候优先检查:
- 模型容量够不够。网络太浅、卷积通道太少,可能学不到复杂特征。
- 学习率是否合适。学习率太大会振荡,太小会龟速。
- 数据预处理对不对。归一化是否做了,标签是否对齐,增强是否过强。
如果训练准确率很高、验证准确率偏低,基本就是过拟合。解决手段按优先级排序:
- 增加数据增强:随机翻转、旋转、颜色抖动。
- 增加Dropout比例:从0.2提到0.5。
- 减小模型容量:减少卷积层通道数,或者换成更浅的网络。
- 降低训练轮数:保存最优模型而不是最后几个epoch。
- 用迁移学习替代从头训练。
每次只改一个变量,不要同时改一堆东西,否则你很难知道是哪个改动起了作用。
7.3 还可以怎么玩:目标检测、风格迁移等
猫狗分类只是CNN应用的冰山一角。当你把分类流程吃透之后,有很多自然的延伸方向。
一个方向是目标检测。分类模型只能回答“图里有没有猫/狗”,但目标检测能回答“猫/狗在哪”。YOLO、SSD、Faster R-CNN都是常用的检测框架。你之前的分类数据可以做清洗和标注,然后转换成目标检测格式,用来训练一个同时定位和分类的模型。很多宠物自动喂食器、智能猫门背后就是这类功能。
另一个方向是细粒度分类。猫狗分类只区分两个物种,但你也可以扩展成品种识别,比如区分拉布拉多、金毛、柯基,或者区分波斯猫、暹罗猫、英短。细粒度分类的难度会比猫狗分类高很多,因为不同品种的差异更细微,需要更精细的特征提取能力。这也是一个很好的挑战项目。
还有很多和猫狗分类技术类似的视觉项目可以做:医学影像分类、卫星图像识别、工厂质检中的缺陷检测。核心流程都是“数据准备-CNN训练-评估-部署”,只是数据领域不同。所以别小看这个入门项目,把它的每个环节都搞透,后面扩展起来会顺畅很多。
最后再分享一个我做这类项目的习惯:从拿到数据和源码的第一天起,就建一个训练日志表格,记录每次实验的网络结构、学习率、数据增强方式、验证准确率。刚开始可能觉得麻烦,但一旦你开始调参,这个日志能帮你少走很多弯路。猫狗分类虽然简单,但把项目流程管理好,后面无论做多复杂的模型,都能从容应对。
本文还有配套的精品资源,点击获取