深度学习实验——Pytorch实现鞋类识别
- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
文章目录
1. 简介 & 数据集介绍
利用PyTorch构建CNN模型识别运动鞋品牌为耐克还是阿迪达斯。数据集分为train和test,其中训练集中各品牌有251张图片,测试集中各品牌有38张图片。
2. 环境
- 语言环境:Python 3.12.7
- 编译器:Jupyter Notebook
- 深度学习环境:torch—2.8.0 + cu126 / torchvision—0.23.1+cu126
3. 代码实现
3.1 前期准备
3.1.1 设置GPU & 导入库
import torch
import torch.nn as nn
import torchvision.transforms as transforms
import torchvision
from torchvision import transforms, datasets
import torch.nn.functional as F
import os,PIL,pathlib,random
from datetime import datetime
import matplotlib.pyplot as plt
#隐藏警告
import warnings
warnings.filterwarnings("ignore")
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['figure.dpi'] = 100
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device

3.1.2 标签查看
data_dir = './Data/46-data/'
data_dir = pathlib.Path(data_dir)
data_paths = list(data_dir.glob('*'))
classNames = [str(path).split("\\")[2] for path in data_paths]
classNames

3.2 CNN 模型
3.2.1 图片转换读取
train_transforms = transforms.Compose(
[
transforms.Resize([224, 224]),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
]
)
test_transforms = transforms.Compose(
[
transforms.Resize([224, 224]),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
]
)
train_dataset = datasets.ImageFolder("./Data/46-data/train", transform=train_transforms)
test_dataset = datasets.ImageFolder("./Data/46-data/test", transform=test_transforms)
train_dataset.class_to_idx

3.2.2 数据集划分
batch_size = 32
train_dl = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=1)
test_dl = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=True, num_workers=1)
for X, y in test_dl:
print("Shape of X [N, C, H, W]: ", X.shape)
print("Shape of y: ", y.shape, y.dtype)
break

3.2.3 CNN 模型建立
class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()
self.conv1=nn.Sequential(nn.Conv2d(3, 12, kernel_size=5, padding=0), nn.BatchNorm2d(12), nn.ReLU())
self.conv2=nn.Sequential(nn.Conv2d(12, 12, kernel_size=5, padding=0), nn.BatchNorm2d(12),nn.ReLU())
self.pool3=nn.Sequential(nn.MaxPool2d(2))
self.conv4=nn.Sequential(nn.Conv2d(12, 24, kernel_size=5, padding=0), nn.BatchNorm2d(24),nn.ReLU())
self.conv5=nn.Sequential(nn.Conv2d(24, 24, kernel_size=5, padding=0), nn.BatchNorm2d(24),nn.ReLU())
self.pool6=nn.Sequential(nn.MaxPool2d(2))
self.dropout = nn.Sequential(nn.Dropout(0.2))
self.fc=nn.Sequential(nn.Linear(24*50*50, len(classNames)))
def forward(self, x):
batch_size = x.size(0)
x = self.conv1(x)
x = self.conv2(x)
x = self.pool3(x)
x = self.conv4(x)
x = self.conv5(x)
x = self.pool6(x)
x = self.dropout(x)
x = x.view(batch_size, -1)
x = self.fc(x)
return x
print("Using {} device".format(device))
model = Model().to(device)
model

3.2.4 训练 & 测试函数
def train(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset)
num_batches = len(dataloader)
train_loss, train_acc = 0, 0
for X, y in dataloader:
X, y = X.to(device), y.to(device)
pred = model(X)
loss = loss_fn(pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_acc += (pred.argmax(1) == y).type(torch.float).sum().item()
train_loss += loss.item()
train_acc /= size
train_loss /= num_batches
return train_acc, train_loss
def test(dataloader, model, loss_fn):
size = len(dataloader.dataset)
num_batches = len(dataloader)
test_loss, test_acc = 0, 0
with torch.no_grad():
for imgs, target in dataloader:
imgs, target = imgs.to(device), target.to(device)
target_pred = model(imgs)
loss = loss_fn(target_pred, target)
test_loss += loss.item()
test_acc += (target_pred.argmax(1) == target).type(torch.float).sum().item()
test_acc /= size
test_loss /= num_batches
return test_acc, test_loss
3.2.5 参数设定 & 正式训练
learn_rate = 1e-4
loss_fn = nn.CrossEntropyLoss()
epochs = 40
train_loss = []
train_acc = []
test_loss = []
test_acc = []
lambda1 = lambda epoch:(0.92 ** (epoch // 2))
optimizer = torch.optim.SGD(model.parameters(), lr=learn_rate)
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda1)
for epoch in range(epochs):
model.train()
epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, optimizer)
scheduler.step()
model.eval()
epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)
train_acc.append(epoch_train_acc)
train_loss.append(epoch_train_loss)
test_acc.append(epoch_test_acc)
test_loss.append(epoch_test_loss)
lr = optimizer.state_dict()['param_groups'][0]['lr']
template = ('Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%, Test_loss:{:.3f}, Lr:{:.2E}')
print(template.format(epoch+1, epoch_train_acc*100, epoch_train_loss, epoch_test_acc*100, epoch_test_loss, lr))
print('Done.')

3.2.6 结果可视化
current_time = datetime.now()
epochs_range = range(epochs)
plt.figure(figsize=(12, 3))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, train_acc, label='Training Accuracy')
plt.plot(epochs_range, test_acc, label='Test Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.xlabel(current_time)
plt.subplot(1, 2, 2)
plt.plot(epochs_range, train_loss, label='Training Loss')
plt.plot(epochs_range, test_loss, label='Test Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()

3.2.7 预测指定本地照片
from PIL import Image
classes = list(train_dataset.class_to_idx)
def predict_one_image(image_path, model, transform, classes):
test_img = Image.open(image_path).convert('RGB')
plt.imshow(test_img) # 展示预测的图片
test_img = transform(test_img)
img = test_img.to(device).unsqueeze(0)
model.eval()
output = model(img)
_,pred = torch.max(output,1)
pred_class = classes[pred]
print(f'预测结果是:{pred_class}')
predict_one_image(image_path='./Data/46-data/test/adidas/1.jpg', model=model, transform=train_transforms, classes=classes)

4. 代码优化
4.1 问题分析及解决方案
-
模型结构过于简单且参数爆炸
最初代码采用浅层自定义CNN,仅4个卷积层且通道数极少,最后连接一个参数量巨大的全连接层(60,000→2),导致模型容易过拟合。
后来升级为VGG-like深层结构(通道数64→128→256→512),并引入全局平均池化彻底去除巨型全连接层,参数大幅减少,特征提取更鲁棒,有效防止过拟合,测试准确率从约70%提升至73%左右。 -
数据增强严重不足
最初训练仅使用简单Resize和轻微水平翻转,数据多样性极低,模型容易死记硬背训练样本,泛化能力差。
后续版本加入强力数据增强,包括RandomResizedCrop、RandomRotation(25-45°)、ColorJitter和VerticalFlip等操作,使每轮训练看到的图片变化多样,大幅提升泛化能力,减少过拟合,同时使单张图片预测置信度更加稳定。 -
优化器和学习率设置不当
最初使用SGD优化器且学习率极低(1e-4),导致收敛速度极慢,甚至卡在局部最优。
后来切换为Adam优化器,初始学习率提高到0.001~0.0005,并加入weight_decay正则化,配合ReduceLROnPlateau自动降学习率策略,使训练过程更快更稳定,前10个epoch即可快速提升至65%以上。 -
从零训练导致特征提取能力弱
最初自定义CNN完全从随机初始化开始,在小数据集(几百张鞋子图片)上难以学到可靠的鞋类特征(如logo、鞋型纹理)。
最终引入预训练ResNet18(迁移学习),利用ImageNet上学到的强大视觉特征,只微调后几层和分类头,这是最关键的一步,使测试准确率从73%直接跃升至88-94%,单张图片置信度也从50-60%提升到90%以上。 -
缺少模型保存与早停机制
最初训练结束后直接使用最后一次权重,可能已发生过拟合或性能退化。
后续版本增加实时监控测试集准确率,并在提升时保存最佳权重,同时加入Early Stopping机制,确保始终使用泛化效果最好的模型进行预测,避免性能波动。 -
Batch Size过小且缺少权重初始化
最初Batch Size仅为32,梯度噪声大;卷积层权重使用默认初始化,收敛不稳定。
后来增大Batch Size至64/128,并显式采用Kaiming初始化,使训练过程更加稳定,梯度更新更可靠,整体收敛速度和稳定性显著提升。
4.2 代码修改的部分
4.2.1 Transformer
train_transforms = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.7, 1.0)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomVerticalFlip(p=0.3),
transforms.RandomRotation(45),
transforms.ColorJitter(brightness=0.5, contrast=0.5, saturation=0.5, hue=0.2),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
test_transforms = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
train_dataset = datasets.ImageFolder("./Data/46-data/train", transform=train_transforms)
test_dataset = datasets.ImageFolder("./Data/46-data/test", transform=test_transforms)
train_dataset.class_to_idx
4.2.2 Model
import torchvision.models as models
class Model(nn.Module):
def __init__(self, num_classes=2):
super(Model, self).__init__()
# 加载预训练 ResNet18
self.resnet = models.resnet18(pretrained=True)
# 只冻结前几层,解冻 layer3 和 layer4 以及 fc 层进行微调
for name, param in self.resnet.named_parameters():
if "layer3" in name or "layer4" in name or "fc" in name:
param.requires_grad = True
else:
param.requires_grad = False
# 替换分类头
self.resnet.fc = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(self.resnet.fc.in_features, num_classes)
)
def forward(self, x):
return self.resnet(x)
print("Using {} device".format(device))
model = Model().to(device)
print(model)


4.2.3 训练循环
learn_rate_fc = 1e-3 # 分类头学习率高
learn_rate_backbone = 1e-4 # 主干微调学习率低
loss_fn = nn.CrossEntropyLoss()
epochs = 50
best_test_acc = 0.0
best_model_path = "best_model_weights.pth"
train_loss_list, train_acc_list = [], []
test_loss_list, test_acc_list = [], []
patience_counter = 0
early_stop_patience = 12
# 分组优化器:不同层不同学习率
optimizer = torch.optim.Adam([
{'params': model.resnet.layer3.parameters(), 'lr': learn_rate_backbone},
{'params': model.resnet.layer4.parameters(), 'lr': learn_rate_backbone},
{'params': model.resnet.fc.parameters(), 'lr': learn_rate_fc}
], weight_decay=1e-4)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=5)
for epoch in range(epochs):
model.train()
epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, optimizer)
model.eval()
epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)
scheduler.step(epoch_test_acc)
if epoch_test_acc > best_test_acc:
best_test_acc = epoch_test_acc
torch.save(model.state_dict(), best_model_path)
print(f" Epoch {epoch+1}: 新最佳模型!Test Acc: {best_test_acc*100:.2f}% ")
patience_counter = 0
else:
patience_counter += 1
train_acc_list.append(epoch_train_acc)
train_loss_list.append(epoch_train_loss)
test_acc_list.append(epoch_test_acc)
test_loss_list.append(epoch_test_loss)
print(f'Epoch: {epoch+1:2d}/{epochs} '
f'Train Acc: {epoch_train_acc*100:6.2f}% Loss: {epoch_train_loss:.4f} '
f'Test Acc: {epoch_test_acc*100:6.2f}% Loss: {epoch_test_loss:.4f} '
f'LR_fc: {optimizer.param_groups[-1]["lr"]:.2e}')
if patience_counter >= early_stop_patience:
print("早停触发")
break
print(f"训练完成!最高测试准确率:{best_test_acc*100:.2f}%")

4.3 训练结果与可视化


更多推荐
所有评论(0)