1. 项目概述:基于CNN的图像分类实战
在计算机视觉领域,图像分类是最基础也最具代表性的任务之一。这次我们要实现的是一个完整的食物图像分类系统,使用PyTorch框架构建卷积神经网络(CNN)。这个项目特别适合刚入门深度学习的朋友,因为:
- 它涵盖了数据预处理、模型构建、训练优化、评估预测的全流程
- 使用了标准的ResNet架构变体,这是工业界广泛采用的成熟方案
- 包含了防止过拟合的关键技术:数据增强和残差连接
- 代码结构清晰,注释完整,可以直接复用到其他分类任务
我在实际工业级图像分类项目中积累了一些经验,会特别强调那些教科书上不会讲、但实际工作中至关重要的细节。比如数据增强策略的选择、学习率调整的时机、模型保存的最佳实践等。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心设计思路解析
2.1 为什么选择CNN架构
卷积神经网络(CNN)是图像处理的黄金标准,这得益于它的三个独特设计:
- 局部感受野:通过小尺寸卷积核(如3x3)捕捉局部特征,比全连接网络更高效
- 参数共享:同一卷积核在整个图像上滑动,大幅减少参数量
- 层次化特征提取:浅层捕捉边缘/纹理,深层识别复杂模式
我们的模型采用4个卷积块,每块包含:
- 卷积层(Conv2d)
- 批归一化(BatchNorm)
- ReLU激活
- 最大池化(MaxPool)
这种设计在计算效率和特征提取能力间取得了良好平衡。
2.2 数据增强的必要性
原始代码中已经实现了基本的数据增强:
python复制train_tfm = transforms.Compose([
transforms.Resize((128, 128)),
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
但实际项目中,我建议增加更多增强手段:
python复制from torchvision.transforms import RandomRotation, RandomPerspective
enhanced_tfm = transforms.Compose([
transforms.Resize((150, 150)), # 先放大再随机裁剪
transforms.RandomCrop(128),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomVerticalFlip(p=0.2),
RandomRotation(15),
RandomPerspective(distortion_scale=0.2, p=0.3),
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])
])
注意:增强强度需要根据具体数据集调整。食物图像适合颜色抖动,但医学影像可能就不适合。
2.3 残差连接的价值
虽然示例代码是简单CNN,但提到了"使用残差链接"。残差网络(ResNet)的核心思想是:
- 解决深层网络梯度消失问题
- 通过shortcut connection实现恒等映射
- 允许网络学习残差函数而非直接映射
改进后的残差块实现:
python复制class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)
self.bn2 = nn.BatchNorm2d(out_channels)
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += self.shortcut(x)
return F.relu(out)
3. 完整实现细节
3.1 数据集处理实战
原始代码中的FoodDataset类已经实现了基本功能,但有几个可以优化的点:
- 缓存机制:频繁读取图像文件会拖慢训练
python复制def __init__(self, path, transform=None, cache=True):
self.cache = cache
self.cached_images = {} if cache else None
# ...其余初始化代码...
def __getitem__(self, idx):
if self.cache and idx in self.cached_images:
return self.cached_images[idx]
# ...原始图像加载代码...
if self.cache:
self.cached_images[idx] = (img, label)
return img, label
- 类别不平衡处理:
python复制# 计算类别权重
label_counts = Counter(train_dataset.labels)
total = sum(label_counts.values())
weights = [total/label_counts[i] for i in range(len(label_counts))]
sampler = torch.utils.data.WeightedRandomSampler(weights, len(weights))
train_loader = DataLoader(..., sampler=sampler)
- 可视化检查:
python复制import matplotlib.pyplot as plt
def show_batch(loader, class_names, n=4):
images, labels = next(iter(loader))
plt.figure(figsize=(12, 8))
for i in range(n):
plt.subplot(1, n, i+1)
img = images[i].permute(1, 2, 0).numpy()
img = img * np.array([0.229, 0.224, 0.225]) + np.array([0.485, 0.456, 0.406]) # 反归一化
plt.imshow(np.clip(img, 0, 1))
plt.title(class_names[labels[i].item()])
plt.axis('off')
plt.show()
3.2 模型训练技巧
训练循环中有几个关键改进点:
- 学习率预热:
python复制from torch.optim.lr_scheduler import LambdaLR
def get_linear_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps):
def lr_lambda(current_step):
if current_step < num_warmup_steps:
return float(current_step) / float(max(1, num_warmup_steps))
return max(0.0, float(num_training_steps - current_step) / float(max(1, num_training_steps - num_warmup_steps)))
return LambdaLR(optimizer, lr_lambda)
scheduler = get_linear_schedule_with_warmup(optimizer,
num_warmup_steps=100,
num_training_steps=len(train_loader)*epochs)
- 混合精度训练:
python复制from torch.cuda.amp import GradScaler, autocast
scaler = GradScaler()
for imgs, labels in pbar:
optimizer.zero_grad()
with autocast():
outputs = model(imgs)
loss = criterion(outputs, labels)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
- 梯度裁剪:
python复制torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
3.3 模型评估优化
验证阶段可以收集更多指标:
- 混淆矩阵:
python复制from sklearn.metrics import confusion_matrix
import seaborn as sns
all_preds = []
all_labels = []
with torch.no_grad():
for imgs, labels in val_loader:
# ...原有代码...
all_preds.extend(preds.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
cm = confusion_matrix(all_labels, all_preds)
plt.figure(figsize=(10, 8))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.xlabel('Predicted')
plt.ylabel('True')
plt.show()
- 分类报告:
python复制from sklearn.metrics import classification_report
print(classification_report(all_labels, all_preds, target_names=class_names))
- ROC曲线(适用于二分类):
python复制from sklearn.metrics import roc_curve, auc
fpr, tpr, _ = roc_curve(labels, probs[:, 1])
roc_auc = auc(fpr, tpr)
plt.plot(fpr, tpr, label=f'AUC = {roc_auc:.2f}')
plt.plot([0, 1], [0, 1], 'k--')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.legend()
plt.show()
4. 常见问题与解决方案
4.1 过拟合问题
症状:
- 训练准确率持续上升但验证准确率停滞
- 验证损失开始上升
解决方案:
- 增强数据多样性(更多增强手段)
- 增加Dropout比例(0.5→0.7)
- 添加L2正则化:
python复制optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)
- 使用早停(Early Stopping):
python复制best_val_loss = float('inf')
patience = 5
counter = 0
for epoch in range(epochs):
# ...训练代码...
if val_loss < best_val_loss:
best_val_loss = val_loss
counter = 0
# 保存模型
else:
counter += 1
if counter >= patience:
print("Early stopping triggered")
break
4.2 训练不收敛
可能原因:
- 学习率设置不当
- 数据预处理有问题
- 模型初始化问题
排查步骤:
- 检查数据预处理:
python复制# 显示归一化后的图像像素值范围
print(torch.min(images), torch.max(images)) # 应该在[-2.5, 2.5]左右
- 检查梯度流动:
python复制# 在训练循环中添加
for name, param in model.named_parameters():
if param.grad is None:
print(f"No gradient for {name}")
else:
print(f"{name} grad mean: {param.grad.mean().item():.6f}")
- 尝试学习率搜索:
python复制lr_finder = LRFinder(model, optimizer, criterion)
lr_finder.range_test(train_loader, end_lr=10, num_iter=100)
lr_finder.plot()
4.3 类别不平衡
处理方法:
- 加权交叉熵损失:
python复制class_weights = torch.FloatTensor(weights).to(device)
criterion = nn.CrossEntropyLoss(weight=class_weights)
- 过采样/欠采样
- 使用Focal Loss:
python复制class FocalLoss(nn.Module):
def __init__(self, alpha=1, gamma=2):
super().__init__()
self.alpha = alpha
self.gamma = gamma
def forward(self, inputs, targets):
BCE_loss = F.cross_entropy(inputs, targets, reduction='none')
pt = torch.exp(-BCE_loss)
loss = self.alpha * (1-pt)**self.gamma * BCE_loss
return loss.mean()
5. 模型部署优化
训练好的模型需要优化才能高效部署:
- 模型量化:
python复制quantized_model = torch.quantization.quantize_dynamic(
model, {nn.Linear}, dtype=torch.qint8
)
torch.save(quantized_model.state_dict(), 'quantized_model.pth')
- ONNX导出:
python复制dummy_input = torch.randn(1, 3, 128, 128).to(device)
torch.onnx.export(model, dummy_input, "model.onnx",
input_names=["input"], output_names=["output"],
dynamic_axes={'input': {0: 'batch'}, 'output': {0: 'batch'}})
- TensorRT加速:
python复制# 需要先安装torch2trt
from torch2trt import torch2trt
model_trt = torch2trt(model, [dummy_input], fp16_mode=True)
torch.save(model_trt.state_dict(), 'model_trt.pth')
在实际部署中,我发现量化后的模型大小可以减少75%,推理速度提升3-5倍,而准确率损失通常不到1%。这对于移动端和边缘设备特别重要。
