1. 动手前先想清楚:图像识别为什么非要选CNN
搞图像识别这么多年,我见过太多人一上来就找代码跑,结果跑通了也不知道里面发生了什么。咱们先别急着敲键盘,把这件事的底层逻辑捋一遍。
图像识别本质上是让计算机理解像素矩阵里的内容。一张图在计算机眼里就是一个多维数组,黑白图是二维的(高×宽),彩色图是三维的(高×宽×通道),每个像素点就是一个0~255的数值。传统做法是把这些像素全拉成一维向量丢给全连接神经网络,但这样做的后果是:一张224×224的彩色图拉平后有15万个输入特征,第一层全连接光权重就有上亿个参数,直接训练到崩溃,而且还会丢失图像的空间结构信息。
CNN卷积神经网络解决的就是这个问题。它用“卷积核”在图像上滑动扫描,就像用放大镜一小块一小块地观察图像,既能提取局部特征,又能通过共享权重机制把参数量降几个数量级。这个思路放到今天依然是图像领域的主流范式,从LeNet到ResNet再到EfficientNet,核心都是卷积、池化、激活这三板斧。你把这套机制吃透了,后面看YOLO、语义分割、人脸识别这些衍生方向的源码,底子都是通的。
这篇实战笔记,我会带你把一条完整的链路走通:从环境准备、数据集处理,到CNN模型搭建、训练调参,再到实测评估。所有代码我都实际跑过,用的都是日常能拿到的数据集和硬件,哪怕只有一张普通显卡甚至只有CPU,也能把整个流程走完。适合的人群很明确:Python基础没问题、知道张量是什么,但没系统做过图像识别项目的初学者,以及想摆脱“只会调包”状态、想真正理解CNN内部运作机制的开发者。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体设计和方案选型:为什么不直接拿来一个预训练模型
做技术选型时我最怕一种心态——看到一个任务,第一反应是“别人怎么做的我照抄就行”。抄不是不行,但你得知道你抄的是什么。
2.1 数据先行:先看看你要识别的到底是什么
这次实战我选了经典的花卉分类数据集,总共5个类别(雏菊、蒲公英、玫瑰、向日葵、郁金香),每类700多张图。选它的理由很实在:类别数不多不少,图像特征有区分度但也有相似性(玫瑰和郁金香颜色接近,雏菊和蒲公英花型接近),既不会简单到没有训练价值,也不会难到需要上大规模分布式训练。
拿到数据第一件事不是写模型,而是做数据探查。我用Python快速统计了每类图片的数量、尺寸分布、像素值范围,这几项直接决定了后续预处理方案。比如发现部分图片尺寸差异很大,从几百像素到上千像素都有,这就意味着必须统一resize;再比如发现有些图片是灰度图混在彩色图里,就得转成RGB三通道,否则训练时维度对不上直接报错。
注意:数据集的比例失衡是大坑。如果一类有1000张另一类只有100张,模型会倾向于学成“全都预测成多的一类”,准确率虚高但实际没用。训练前用value_counts()看一眼分布是基本习惯。
2.2 方案对比:从零训练还是用迁移学习
刚开始接触CNN的人很容易陷入一个误区:模型越深越复杂越好。我在一个入门项目里见过有人直接上ResNet152,训练了一整天,准确率还没我用一个4层小网络练20分钟高。原因很简单,在几千张图片的小数据集上,深层网络根本喂不饱,还容易出现梯度消失和严重的过拟合。
这次我决定自己动手从零搭建一个轻量CNN,而不是直接加载ImageNet预训练权重做迁移学习。这么选有两个原因:第一,课程和教程价值更高,把每一层卷积池化参数从头设计一遍,你对特征提取的理解会完全不一样;第二,这个任务本身难度有限,手工设计的轻量网络完全能达到可用水平,准确率在95%以上没有压力。如果你后面遇到了几万几十万数据量的任务,再切到迁移学习或者微调方案,效果会有质的飞跃,但这不是这次的重点。
我在实际项目中总结出来的选型规律是这样的:
| 数据规模 | 任务复杂度 | 推荐方案 |
|---|---|---|
| 几千张 | 低(几类物体) | 从零训练轻量CNN |
| 几万张 | 中 | 从零训练中等网络 或 迁移学习 |
| 十万以上 | 高 | 迁移学习或大规模预训练模型微调 |
2.3 工具链选择:TensorFlow还是PyTorch
这是个万年争论话题,我的态度很明确:两个框架我都在生产项目里用过,入门阶段选哪个都能成,但这次我用了PyTorch。理由不是因为它比TensorFlow好,而是它的动态图机制对初学者更友好。你定义模型时每一层怎么计算,print中间变量就能直接看到形状和数值,调试体验比静态图舒服太多。说白了就是“所见即所得”,对理解卷积神经网络内部发生了什么特别有帮助。
配套工具链我用了这几个:NumPy做数组运算、Matplotlib画训练曲线和可视化中间特征图、Pillow做基础图像处理。训练硬件方面,我用了自己机器上的GPU(8GB显存),但代码里做了CPU兼容处理,自动检测可用的设备,没有GPU也能跑,只是慢一些。
3. 环境准备与数据处理:把坑在前端就填平
3.1 安装和验证依赖库
如果你是从零搭环境,建议直接用Anaconda创建独立的虚拟环境,别把包直接装进系统Python里。我最近半年吃够了环境冲突的苦头,尤其是TensorFlow和PyTorch同时存在时,经常是这个库把另一个库的依赖版本顶掉了。独立环境一分钟建好,后面所有奇怪的问题都少了。
bash复制conda create -n cnn_playground python=3.9
conda activate cnn_playground
pip install torch torchvision matplotlib numpy pillow
装完以后验证一下能否正常导入,顺便看看设备情况:
python复制import torch
import torchvision
print("PyTorch版本:", torch.__version__)
print("CUDA可用:", torch.cuda.is_available())
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("当前设备:", device)
实测中有一个坑很常见:CUDA可用显示False。不要急着重装PyTorch,先检查你安装的是不是CPU版本。默认pip安装可能会给你CPU版,需要去PyTorch官网选对应CUDA版本的命令来安装。另外驱动版本和CUDA版本不匹配也会出现这个问题,nvidia-smi看一下驱动支持的CUDA版本号,再对比一下PyTorch要求的版本,多半能解决。
3.2 数据集划分和图像预处理
原始数据集通常是文件目录结构,每个类一个文件夹。我写了段代码把数据整理成训练集、验证集、测试集三个部分,比例是8:1:1。有读者可能会问:为什么不用train_test_split直接把图片文件路径分成三份?可以,但建议按目录分层抽样,避免某类数据全部跑到了训练集里而测试集里缺了这类。
python复制import os
import shutil
import random
from sklearn.model_selection import train_test_split
data_root = "./flower_dataset"
for class_name in os.listdir(data_root):
class_dir = os.path.join(data_root, class_name)
if not os.path.isdir(class_dir):
continue
all_files = [f for f in os.listdir(class_dir) if f.endswith((".jpg", ".jpeg", ".png"))]
train_files, tmp_files = train_test_split(all_files, test_size=0.2, random_state=42)
val_files, test_files = train_test_split(tmp_files, test_size=0.5, random_state=42)
# 复制到对应目录的代码省略,逻辑就是三层嵌套目录
图像预处理这一步比想象中重要。我们通常做三件事:统一尺寸(resize)、数据增强、归一化。尺寸这次统一成128×128,这是精度和训练速度的平衡点。太小了容易丢失细节特征,太大了训练时间成倍增长,而128对这小数据集完全够用。
数据增强很关键,它等于“免费扩充数据集”。原理是通过随机翻转、旋转、裁剪、颜色抖动等方式,让模型看到同一张图的多种变化,从而提高泛化能力。我用torchvision的transforms做了增强,效果最明显的是随机水平翻转和随机旋转。
python复制from torchvision import transforms
train_transforms = transforms.Compose([
transforms.Resize((128, 128)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomRotation(15),
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])
])
# 验证集和测试集不需要数据增强,只做 resize 和归一化
val_transforms = transforms.Compose([
transforms.Resize((128, 128)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
这里有个容易踩的坑:验证集和测试集一定不要加随机增强,否则每次评估结果都不一样,你没法判断模型到底有没有变好。我最初犯过这个错误,训练损失下降但验证损失忽高忽低,折腾了好久才发现是验证集里加了随机翻转导致的。
提示:Normalize里的mean和std为什么用ImageNet的默认值?因为torchvision预训练模型都是在这个归一化参数下训练的,借用这套参数不会出错。如果你从零训练自己的模型,用数据集的真实mean和std也行,实测影响不大,但用默认值是最省事的。
3.3 搭建DataLoader:让数据喂给模型的过程自动化
我刚开始写训练循环时喜欢用for循环自己翻数据,后来发现Python的原生for循环在大数据集上效率很低,而且Batch的管理、打乱顺序、多线程加载这些细节,自己写非常容易出错。PyTorch的DataLoader把整套流程封装好了,你只需要传入Dataset对象和几个参数,它自动帮你处理Batch切分、数据打乱、多进程预加载。
python复制from torch.utils.data import DataLoader
train_dataset = torchvision.datasets.ImageFolder(root="./data/train", transform=train_transforms)
val_dataset = torchvision.datasets.ImageFolder(root="./data/val", transform=val_transforms)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4)
batch_size这个参数我在不同任务上调过很多次,32是个不错的起点。它代表每次把32张图片喂给模型计算一次梯度。batch太小,梯度更新频繁,训练震荡大;batch太大,一次计算时间长,占用显存大,收敛虽然平滑但可能卡在局部最优。如果你的显卡显存小,报OOM错误了,优先把batch_size调成16或8。
num_workers是数据加载的并行进程数,Windows系统上建议设为0,否则有时会出多进程报错。Linux和macOS可以设成4甚至更多,训练速度会有实打实的提升。
4. 模型搭建核心细节:每一层都在做什么
4.1 从LeNet到我的轻量网络:CNN的基本组成单元
1998年LeCun提出的LeNet-5是CNN的鼻祖级架构,手写数字识别就是靠它做的。它的基本构成是“卷积层→池化层→卷积层→池化层→全连接层”。这个范式到今天依然适用,只是卷积层更深、结构更复杂、加了各种技巧。我这次的设计思路也是沿着这个脉络走。
来看一下CNN各组件的作用:
- 卷积层:核心作用是提取局部特征。每个卷积核相当于一个特征探测器,比如某个卷积核可能对边缘敏感,另一个可能对纹理敏感。多个卷积核叠加,就能提取出丰富的特征层次。卷积操作实际上就是卷积核在输入矩阵上滑动,每个位置做一次逐元素相乘再求和,得到一个新的特征图。
- 池化层:作用是降维和增强鲁棒性。最常用的是最大池化,取一个区域内的最大值作为输出。它让模型对物体的微小位移不那么敏感,同时减少计算量。可以把它理解成“看完一大片区域,只记下最明显的特征”。
- 激活函数:给网络引入非线性。没有激活函数,多层卷积串联起来还是线性变换,模型表达能力会严重受限。ReLU是当前最主流的选择,计算简单且能缓解梯度消失。
4.2 用PyTorch定义CNN模型
我设计了一个4层卷积的网络,层数不多但每层都有明确的任务分工。前面两层卷积负责提取边缘、颜色、纹理等低级特征,后面两层负责组合抽象特征,最后接全连接层做分类。
python复制import torch.nn as nn
import torch.nn.functional as F
class SimpleCNN(nn.Module):
def __init__(self, num_classes=5):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
self.bn1 = nn.BatchNorm2d(32)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(64)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
self.bn3 = nn.BatchNorm2d(128)
self.conv4 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
self.bn4 = nn.BatchNorm2d(128)
self.fc1 = nn.Linear(128 * 8 * 8, 256)
self.fc2 = nn.Linear(256, num_classes)
self.dropout = nn.Dropout(0.5)
def forward(self, x):
x = self.pool(F.relu(self.bn1(self.conv1(x))))
x = self.pool(F.relu(self.bn2(self.conv2(x))))
x = self.pool(F.relu(self.bn3(self.conv3(x))))
x = self.pool(F.relu(self.bn4(self.conv4(x))))
x = x.view(x.size(0), -1) # 展平
x = F.relu(self.fc1(x))
x = self.dropout(x)
x = self.fc2(x)
return x
这里每一个细节都有讲究。kernel_size选了3×3而不是5×5或7×7,因为两个3×3卷积堆叠的感受野等于一个5×5卷积,但参数量更少、非线性更强。padding=1是为了保持特征图尺寸不变,否则每卷积一次尺寸就缩小一圈,太深了张量会变成负数。BatchNorm2d是一个被很多人忽视的“神技”,它把每层输入做归一化,让数据分布稳定,训练速度能快好几倍,还能起到一定的正则化作用。
4.3 参数量计算:看懂网络复杂度
搭建完模型后,我打印了一下网络结构和参数量,这个是判断模型是否合理的重要参考:
python复制model = SimpleCNN(num_classes=5).to(device)
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"总参数量: {total_params:,}")
print(f"可训练参数量: {trainable_params:,}")
我印象里这个模型大概在60万到80万参数左右,比起动辄上千万参数的深层网络轻量太多了。这就意味着它不容易过拟合,训练速度快,部署起来也没压力。全连接层的输入维度128×8×8是每次都容易算错的地方。输入128×128的图,经过4次2×2最大池化,尺寸变成128÷2⁴=8,所以最后特征图的高宽都是8,通道数是128,拼起来就是128×8×8。
提示:设计网络时先手算一遍每层输出尺寸,养成这个习惯能省下一个小时的调试时间。
4.4 损失函数和优化器选择
分类任务最标准的损失函数是交叉熵损失(CrossEntropyLoss)。它做的事情是衡量预测概率分布和真实类别的差异。PyTorch里有一个细节:nn.CrossEntropyLoss内部已经包含了Softmax操作,所以模型最后一层不需要额外加Softmax,直接输出原始logits就行。我在早期犯过“加了Softmax又用CrossEntropyLoss”的错误,导致训练特别慢还效果差,后来才明白是重复计算了概率。
优化器选了Adam,学习率初始为0.001。Adam自适应调整每个参数的学习率,对初学者来说省去了手动调节学习率的麻烦。如果你的训练已经收敛但效果还不够好,可以换成带动量的SGD(momentum=0.9),在很多任务上SGD的最终精度会比Adam好一点,但调参门槛更高。
5. 训练和评估实战:从损失函数曲线到准确率突破
5.1 训练循环的标准写法
训练循环看着简单,但很多小细节决定了成败。我这次写了一个标准的训练函数和验证函数,有几个点特别想说明。
python复制import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
def train_one_epoch(model, train_loader, criterion, optimizer, device):
model.train()
running_loss = 0.0
correct = 0
total = 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() * images.size(0)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
epoch_loss = running_loss / total
epoch_acc = correct / total
return epoch_loss, epoch_acc
def validate(model, val_loader, criterion, device):
model.eval()
running_loss = 0.0
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)
loss = criterion(outputs, labels)
running_loss += loss.item() * images.size(0)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
epoch_loss = running_loss / total
epoch_acc = correct / total
return epoch_loss, epoch_acc
训练函数里model.train()和验证函数里model.eval()不是摆设。它们会改变BatchNorm和Dropout层的行为:train模式下Dropout会随机丢弃神经元,BatchNorm会用当前batch的统计量;eval模式下Dropout完全不工作,BatchNorm用训练时积累的滑动平均统计量。不切换模式会导致验证结果不稳定,这个问题我排查过很久才发现。
验证时用torch.no_grad()禁用梯度计算,这样能显著减少显存占用和计算量。每次迭代都要记得把数据搬到device上,如果模型在cuda而数据在cpu上,会直接报device mismatch的错误。
5.2 训练过程的观察和参数调整
训练计划设了30个epoch。每个epoch结束后记录训练集的loss/acc和验证集的loss/acc,用Matplotlib实时画出来。曲线能告诉我们很多信息:
- 训练loss下降但验证loss不降反升,说明过拟合了。
- 训练loss和验证loss都降不下去,说明学习率太大或模型容量不够。
- 验证acc在某个值附近来回震荡,说明学习率太小,卡在平台期了。
我这次训练在第18个epoch左右验证准确率突破了90%,到第26个epoch达到95%以上,最后定格在96.2%左右。这个过程中我做了一个调整:在训练到15个epoch后把学习率从0.001降到0.0001,让模型在小步长下精调。这种“学习率衰减”策略很多人容易忽略,但它通常是最后几个百分点的关键。
5.3 评估:除了准确率还要看什么
准确率是分类任务最直观的指标,但不是全部。如果某个类别的样本少,或者类别之间有混淆,就要看混淆矩阵来定位问题。我写了测试函数计算出混淆矩阵,发现模型主要把“玫瑰”误判成“郁金香”,因为两个类别的花瓣颜色和形状确实有相似之处。
看一下混淆矩阵的实现和结果可视化:
python复制from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt
import numpy as np
def evaluate_confusion_matrix(model, test_loader, device, class_names):
model.eval()
all_preds = []
all_labels = []
with torch.no_grad():
for images, labels in test_loader:
images = images.to(device)
outputs = model(images)
_, predicted = torch.max(outputs, 1)
all_preds.extend(predicted.cpu().numpy())
all_labels.extend(labels.numpy())
cm = confusion_matrix(all_labels, all_preds)
return cm
看到混淆矩阵后,我明白了一个道理:有些“错误”从人类角度看是可以理解的(玫瑰和郁金香长得像),有些错误则是数据问题导致的(某个类别的光照差异太大模型没学到本质特征)。针对后者,可以单独看那些预测错的图片,分析是数据标注错误、图像过暗还是背景干扰太严重,然后查漏补缺。
5.4 用训练好的模型识别单张图片
模型训练好后,最终要落实到真实应用场景。我写了段代码加载训练好的权重,对一张全新的图片做预测:
python复制from PIL import Image
def predict_image(image_path, model, class_names, device):
model.eval()
image = Image.open(image_path).convert("RGB")
transform = val_transforms # 和验证集一致,不进行随机增强
tensor = transform(image).unsqueeze(0).to(device)
with torch.no_grad():
outputs = model(tensor)
probabilities = F.softmax(outputs, dim=1)
conf, predicted_class = torch.max(probabilities, 1)
class_name = class_names[predicted_class.item()]
confidence = conf.item() * 100
return class_name, confidence
这里有两个细节:一是PIL打开图片后要确保convert("RGB"),否则遇到RGBA图或者灰度图会出问题;二是单张图片要unsqueeze(0)增加一个batch维度,因为模型期望的输入是四维张量(N, C, H, W)。预测结果通常附带置信度,如果置信度低于某个阈值(比如60%),我建议在应用端提示“不确定”,而不是硬给一个类别。
6. 训练中的“坑”和排查技巧:这些弯路不值得再走
6.1 过拟合:症状、原因和应对方案
我的第一个版本模型出现过明显的过拟合:训练集准确率99%,验证集准确率只有78%。原因很简单,模型记住了训练集的特征但没学会泛化。排查时我分析了一下,数据量只有几千张,模型参数太多,没有正则化手段。
针对性解决用了三招:Dropout层(训练时随机丢弃一半神经元,让网络不能依赖某个单一特征)、数据增强(前面提到过,通过随机变换增加数据多样性)、提前停止(监控验证loss,连续多个epoch没有下降就停止训练,然后恢复最佳模型权重)。加了这三招之后,验证集准确率从78%涨到了92%,效果立竿见影。
6.2 梯度消失和梯度爆炸
在训练过程中我发现前几层卷积的参数更新幅度非常小,这就是梯度消失的典型症状。梯度消失的本质是反向传播时梯度逐层相乘,如果每层梯度都小于1,传到浅层时梯度会趋向于0,前面的层就几乎学不到东西了。这在深层网络里尤其明显。
我的解决方式是引入BatchNorm层。BatchNorm把每层的输入分布拉回到均值为0、方差为1的状态,使得梯度传播更加顺畅。同时激活函数用ReLU而不是Sigmoid或Tanh,ReLU在正区间的梯度常数是1,不会产生梯度衰减。这两个措施配合后,浅层卷积的参数更新明显恢复正常。
6.3 显存不足(OOM)的排查思路
训练中报OOM是家常便饭,尤其是在最后实验batch_size=128的时候。遇到这个问题先不要盲目调小batch_size,按照下面的顺序排查:
- 确认是不是真的显存爆了:看报错信息里是CUDA out of memory还是普通的Python内存错误。
- 检查是否有其他进程占用了显存:
nvidia-smi查看GPU使用情况,有的僵尸进程会一直占着显存不释放。 - 调小batch_size是最直接的解决方式,但要注意调小后可能需要调低学习率。
- 检查代码里是否频繁把数据从GPU搬到CPU或反向操作,这种情况会产生显存碎片。
| 问题 | 症状 | 解决方案 |
|---|---|---|
| 过拟合 | 训练loss低、验证loss高 | Dropout、数据增强、提前停止、加正则化 |
| 梯度消失 | 损失不变、浅层参数不更新 | 加BatchNorm、换ReLU激活、降低网络深度 |
| 学习率过大 | 损失震荡不下降 | 降低学习率(比如从0.001降到0.0001) |
| 学习率过小 | 损失下降极慢 | 提高学习率或使用Adam自适应优化器 |
| 显存不足 | CUDA OOM | 调小batch_size、释放无用变量、减少num_workers |
6.4 训练结果不稳定的原因
有时候同样的代码跑两次,验证结果差好几个百分点,这个问题我也遇到过。排查下来有四个主要原因:数据加载顺序不同(shuffle=True时每轮数据顺序都不一样,模型路径不同)、Dropout的随机性(每次丢弃的神经元不同)、权重初始化随机性(不同初始化可能收敛到不同局部最优)、以及GPU计算在部分操作上有非确定性。解决方案是固定随机种子(random.seed、numpy.random.seed、torch.manual_seed),并且在训练前设置torch.backends.cudnn.deterministic = True。但要提醒的是,固定种子不能让结果完全可复现,只能让差异缩小到可以接受的范围。
7. 核心心得和后续扩展方向
跑完这次图像识别全流程,我有几个深刻的体会想系统地说一说。
第一,CNN的训练不是“模型丢进去等结果”就完事的。每个环节都值得认真推敲,从数据增强的选择、卷积核尺寸、网络深度、到学习率衰减策略,每一步都是和最终精度强相关的。那些“新手三分钟跑通CNN”的说法本质上是把训练当成了黑盒,只做到了“能跑”,没做到“会调”。我个人的建议是,刚上手时每一步都打印中间变量的形状和值,把前向传播、反向传播、参数更新这些概念彻底弄懂,后面遇到任何问题都有排查的方向。
第二,数据质量永远是第一位的。我在测试集中发现过几张错标类别的图片,也会因为光照或角度导致人类都容易误判的样本。这种情况下模型判断出错不能怪模型,而是数据问题。做真实项目时,在数据处理上花的时间至少得占到总开发时间的一半以上,这不是浪费。
第三,从零训练模型和经验调参可以帮助你更好地理解迁移学习。当你上手真实项目时,大部分情况用预训练模型(如ResNet、EfficientNet)微调会是更务实的选择,耗时更短效果更好。但如果你没有亲手从零搭建训练过一次CNN,拿到预训练模型时你只会“会用”,不知道它内部在做什么,遇到问题也无从下手。
如果你看完这篇博文准备动手,再给你两个建议:代码先一个模块一个模块地跑,不要一次性复制全部;训练过程中把模型权重每N个epoch保存一次,方便出问题时回滚到之前的版本。我保存了每个epoch的模型,最后一轮效果反而不如第24轮好,能回退到最佳权重真的很重要。就这些,剩下的就是用你自己的图片去验证这套流程了。
