PyTorch图像分类实战:从零搭建AlexNet模型与自定义数据集训练
1. 从零理解AlexNet:为什么它改变了计算机视觉
2012年,一个名叫AlexNet的卷积神经网络在ImageNet竞赛中以压倒性优势夺冠,将图像分类准确率从传统方法的70%提升到80%以上。这个突破直接引爆了深度学习革命,而它的设计思想至今仍在影响现代神经网络架构。
AlexNet的成功秘诀在于几个关键创新:首次成功应用ReLU激活函数解决梯度消失问题、使用Dropout防止过拟合、采用重叠池化(Overlapping Pooling)增强特征提取能力。有趣的是,当年论文中描述的双GPU并行结构(因为显存限制)现在反而成了理解模型架构时容易混淆的地方——我们实际实现时完全可以简化为单GPU版本。
我刚开始复现这个网络时,最困惑的是它的参数计算方式。比如第一层卷积,输入224x224x3的图像,用96个11x11的卷积核,步长4,padding2,输出特征图尺寸怎么就是55x55了?其实套用公式(W-F+2P)/S+1就能验证:(224-11+2*2)/4+1=55。这种尺寸计算贯穿整个网络,建议动手在纸上推导一遍,比死记硬背效果好得多。
2. 搭建AlexNet的PyTorch实现
2.1 模型结构拆解
先看完整的PyTorch实现代码,我加了详细注释:
import torch.nn as nn import torch class AlexNet(nn.Module): def __init__(self, num_classes=5, init_weights=False): # 花分类任务设为5类 super(AlexNet, self).__init__() self.features = nn.Sequential( nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2), # [3,224,224]->[48,55,55] nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), # [48,27,27] nn.Conv2d(48, 128, kernel_size=5, padding=2), # [128,27,27] nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), # [128,13,13] nn.Conv2d(128, 192, kernel_size=3, padding=1), # [192,13,13] nn.ReLU(inplace=True), nn.Conv2d(192, 192, kernel_size=3, padding=1), # [192,13,13] nn.ReLU(inplace=True), nn.Conv2d(192, 128, kernel_size=3, padding=1), # [128,13,13] nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), # [128,6,6] ) self.classifier = nn.Sequential( nn.Dropout(p=0.5), nn.Linear(128*6*6, 2048), nn.ReLU(inplace=True), nn.Dropout(p=0.5), nn.Linear(2048, 2048), nn.ReLU(inplace=True), nn.Linear(2048, num_classes), )实际使用时有个技巧:原论文使用双GPU训练,参数量是我们实现的两倍。但实测发现,在花分类这种小数据集上,半参数量的模型效果相当,训练速度却快一倍。这也是为什么上面代码中第一层输出通道是48而不是96。
2.2 关键组件详解
ReLU激活函数:相比传统的sigmoid,计算简单且缓解梯度消失。inplace=True参数能节省内存,直接覆盖输入值。
局部响应归一化(LRN):论文中的创新点,但后续研究发现效果有限,现代实现通常省略这一层。
Dropout技巧:全连接层前的nn.Dropout(0.5)随机屏蔽50%神经元,是防止过拟合的关键。注意训练和验证时要区分model.train()和model.eval()模式。
3. 准备花分类数据集
3.1 数据集下载与解压
使用TensorFlow提供的公开花卉数据集:
wget http://download.tensorflow.org/example_images/flower_photos.tgz tar -xzf flower_photos.tgz -C flower_data解压后的目录结构包含5类花卉(雏菊、蒲公英、玫瑰、向日葵、郁金香),每类约600-900张不等分辨率的图片。我建议新建一个flower_data目录专门存放数据,保持项目整洁。
3.2 自动划分训练集/验证集
写个Python脚本自动划分数据(9:1比例):
import os from shutil import copy import random def mkdir_if_not_exist(path): if not os.path.exists(path): os.makedirs(path) # 创建训练集和验证集目录 flower_classes = [cls for cls in os.listdir('flower_data/flower_photos') if not cls.endswith('.txt')] for split in ['train', 'val']: for cls in flower_classes: mkdir_if_not_exist(f'flower_data/{split}/{cls}') # 按9:1比例随机划分 for cls in flower_classes: src_dir = f'flower_data/flower_photos/{cls}' images = os.listdir(src_dir) val_images = random.sample(images, k=int(len(images)*0.1)) for img in images: src_path = os.path.join(src_dir, img) if img in val_images: dst_path = f'flower_data/val/{cls}/{img}' else: dst_path = f'flower_data/train/{cls}/{img}' copy(src_path, dst_path) print(f'[{cls}] 划分完成,训练集:{len(images)-len(val_images)}张, 验证集:{len(val_images)}张')运行后会生成标准的PyTorch ImageFolder所需结构:
flower_data/ ├── train/ │ ├── daisy/ │ ├── dandelion/ │ └── ... └── val/ ├── daisy/ ├── dandelion/ └── ...4. 训练技巧与实战细节
4.1 数据增强配置
对比训练和验证的数据预处理:
from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomResizedCrop(224), # 随机裁剪缩放 transforms.RandomHorizontalFlip(), # 水平翻转 transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) ]) val_transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), # 中心裁剪 transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) ])关键点说明:
- 训练时使用
RandomResizedCrop和RandomHorizontalFlip增强数据多样性 - 验证集只需简单中心裁剪,保持评估一致性
- 归一化参数
mean=[0.5,0.5,0.5],std=[0.5,0.5,0.5]将像素值映射到[-1,1]范围
4.2 训练循环实现
完整训练代码包含几个关键部分:
# 1. 数据加载 train_dataset = datasets.ImageFolder('flower_data/train', transform=train_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True) # 2. 模型初始化 model = AlexNet(num_classes=5).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.0002) # 3. 训练循环 for epoch in range(10): model.train() for images, labels in train_loader: optimizer.zero_grad() outputs = model(images.to(device)) loss = criterion(outputs, labels.to(device)) loss.backward() optimizer.step() # 验证环节 model.eval() with torch.no_grad(): correct = 0 for images, labels in val_loader: outputs = model(images.to(device)) preds = torch.argmax(outputs, dim=1) correct += (preds == labels.to(device)).sum().item() accuracy = correct / len(val_dataset) print(f'Epoch {epoch+1}: Val Acc={accuracy:.3f}')实际训练时我发现几个经验:
- 学习率设为0.0002比默认的0.001更稳定
- 批量大小(batch_size)32在8GB显存显卡上刚好
- 10个epoch后验证集准确率约68%,继续训练可能过拟合
5. 模型评估与预测
训练完成后保存最佳模型:
torch.save(model.state_dict(), 'best_alexnet.pth')预测单张图像的完整流程:
def predict_image(img_path): # 加载模型 model = AlexNet(num_classes=5) model.load_state_dict(torch.load('best_alexnet.pth')) model.eval() # 预处理 img = Image.open(img_path) img = val_transform(img).unsqueeze(0) # 预测 with torch.no_grad(): output = torch.softmax(model(img), dim=1) pred = torch.argmax(output).item() # 显示结果 class_names = ['daisy', 'dandelion', 'roses', 'sunflowers', 'tulips'] plt.imshow(Image.open(img_path)) plt.title(f'Predicted: {class_names[pred]} ({output[0][pred]:.2f})') plt.show()常见问题排查:
- 输入维度不匹配:确保图像经过transform后是[1,3,224,224]
- 类别数不一致:预测时num_classes必须和训练时相同
- 归一化不一致:训练和预测必须使用相同的归一化参数
我在实际项目中遇到过验证集准确率波动大的情况,后来发现是数据划分时随机种子未固定,导致每次运行划分结果不同。解决方法是在数据划分前加上random.seed(42)保持可重复性。
