从零实现AlexNet:PyTorch 2.0实战CIFAR-10图像分类

当2012年AlexNet在ImageNet竞赛中一举夺魁时,它向世界证明了深度卷积神经网络在计算机视觉领域的巨大潜力。如今,这个开创性的架构已成为每个深度学习初学者的必修课。本文将带你用现代PyTorch框架完整复现AlexNet的核心思想,并在更易上手的CIFAR-10数据集上验证其效果。

1. 环境准备与数据加载

在开始构建模型之前,我们需要配置合适的开发环境。推荐使用Python 3.8+和PyTorch 2.0+版本,这些版本不仅支持最新的GPU加速特性,还能自动优化许多底层计算。

import torch
import torchvision
import torch.nn as nn
import torch.optim as optim
from torchvision import transforms
from torch.utils.data import DataLoader

print(f"PyTorch版本: {torch.__version__}")
print(f"CUDA可用: {torch.cuda.is_available()}")

CIFAR-10数据集包含60,000张32x32彩色图像,分为10个类别。与原始AlexNet使用的ImageNet相比,这个尺寸更小的数据集能显著缩短实验周期:

# 数据增强和归一化
transform = transforms.Compose([
    transforms.Resize(224),  # AlexNet原始输入尺寸
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

# 加载数据集
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)
testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)

# 创建数据加载器
trainloader = DataLoader(trainset, batch_size=128, shuffle=True)
testloader = DataLoader(testset, batch_size=128, shuffle=False)

注意:虽然CIFAR-10图像原始尺寸为32x32,但我们将它们上采样到224x224以匹配AlexNet的原始架构设计。这种处理虽然会增加计算量,但能更好地还原论文中的实现细节。

2. AlexNet架构详解与实现

AlexNet的成功源于几个关键创新点,我们将逐一实现这些组件。与原始论文不同的是,我们会使用现代PyTorch的更高效实现方式。

2.1 基础卷积模块

AlexNet由5个卷积层和3个全连接层组成。第一个卷积层使用较大的11x11滤波器来捕捉宏观特征:

class AlexNet(nn.Module):
    def __init__(self, num_classes=10):
        super(AlexNet, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(64, 192, kernel_size=5, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(192, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
        )
        self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
        self.classifier = nn.Sequential(
            nn.Dropout(),
            nn.Linear(256 * 6 * 6, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Linear(4096, num_classes),
        )

    def forward(self, x):
        x = self.features(x)
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x

与原始论文的主要差异:

  • 使用ReLU替代局部响应归一化(LRN),因为实践证明BatchNorm效果更好
  • 简化了多GPU并行设计,现代GPU已能高效处理整个网络
  • 添加了自适应池化层以增强不同输入尺寸的鲁棒性

2.2 关键技术创新点实现

虽然我们对架构做了一些现代化改进,但仍保留了AlexNet的三个核心创新:

1. ReLU激活函数

# 比较不同激活函数的梯度特性
x = torch.linspace(-5, 5, 100)
relu = nn.ReLU()(x)
leaky_relu = nn.LeakyReLU(0.1)(x)
tanh = torch.tanh(x)

# 绘制函数曲线可清晰看到ReLU的非饱和特性

2. 重叠池化

# 标准池化
standard_pool = nn.MaxPool2d(kernel_size=2, stride=2)

# AlexNet使用的重叠池化
overlapping_pool = nn.MaxPool2d(kernel_size=3, stride=2)  # 步长小于核尺寸

3. Dropout正则化

# 在全连接层应用Dropout
self.classifier = nn.Sequential(
    nn.Dropout(p=0.5),  # 原始论文使用的丢弃率
    nn.Linear(256*6*6, 4096),
    nn.ReLU(inplace=True),
    nn.Dropout(p=0.5),
    nn.Linear(4096, 4096),
    nn.ReLU(inplace=True),
    nn.Linear(4096, num_classes)
)

3. 训练策略与优化技巧

AlexNet论文中提出的训练方法在当时非常先进,许多技巧至今仍在广泛使用。我们实现这些方法时需要注意现代框架的最佳实践。

3.1 权重初始化

正确的初始化对深度网络训练至关重要。AlexNet使用零均值高斯分布初始化:

def initialize_weights(m):
    if isinstance(m, nn.Conv2d) or isinstance(m, nn.Linear):
        nn.init.normal_(m.weight, mean=0, std=0.01)
        if m.bias is not None:
            nn.init.constant_(m.bias, 0)
    # 特定层使用不同的偏置初始化
    if isinstance(m, nn.Conv2d):
        if m.out_channels in [192, 384, 256]:
            nn.init.constant_(m.bias, 1)

model = AlexNet().apply(initialize_weights)

3.2 学习率调度

AlexNet采用手动调整的学习率策略,我们可以用PyTorch的调度器实现类似效果:

optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=0.0005)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)  # 每30轮学习率降10倍

3.3 数据增强

虽然CIFAR-10比ImageNet小得多,但适当的数据增强仍能显著提升模型泛化能力:

train_transform = transforms.Compose([
    transforms.Resize(224),
    transforms.RandomHorizontalFlip(),
    transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)),
    transforms.ColorJitter(brightness=0.2, contrast=0.2),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

4. 模型训练与评估

现在我们将所有组件整合起来,完成端到端的训练流程。为了更好理解训练动态,我们会记录关键指标。

4.1 训练循环实现

def train(model, device, trainloader, optimizer, epoch):
    model.train()
    total_loss = 0
    correct = 0
    for batch_idx, (data, target) in enumerate(trainloader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = nn.CrossEntropyLoss()(output, target)
        loss.backward()
        optimizer.step()
        
        total_loss += loss.item()
        pred = output.argmax(dim=1, keepdim=True)
        correct += pred.eq(target.view_as(pred)).sum().item()
        
    avg_loss = total_loss / len(trainloader)
    accuracy = 100. * correct / len(trainloader.dataset)
    return avg_loss, accuracy

4.2 测试评估

def test(model, device, testloader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in testloader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += nn.CrossEntropyLoss()(output, target).item()
            pred = output.argmax(dim=1, keepdim=True)
            correct += pred.eq(target.view_as(pred)).sum().item()

    avg_loss = test_loss / len(testloader)
    accuracy = 100. * correct / len(testloader.dataset)
    return avg_loss, accuracy

4.3 训练过程可视化

典型的训练过程会呈现以下特征:

  • 前几轮准确率快速上升(ReLU的优势)
  • 约30轮后学习率下降时损失出现明显下降
  • 验证准确率最终稳定在约80%左右(CIFAR-10上)
# 训练主循环
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = AlexNet().to(device)

for epoch in range(90):
    train_loss, train_acc = train(model, device, trainloader, optimizer, epoch)
    test_loss, test_acc = test(model, device, testloader)
    scheduler.step()
    
    print(f"Epoch {epoch}: Train Loss {train_loss:.4f} Acc {train_acc:.2f}% | "
          f"Test Loss {test_loss:.4f} Acc {test_acc:.2f}%")

5. 进阶优化与问题排查

当复现经典论文时,经常会遇到结果不如预期的情况。以下是几个常见问题及解决方案:

5.1 梯度消失/爆炸

虽然ReLU缓解了这个问题,但在深层网络中仍可能出现:

# 添加梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

# 或者使用更稳定的架构
self.features = nn.Sequential(
    nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
    nn.BatchNorm2d(64),  # 添加批归一化
    nn.ReLU(inplace=True),
    # ...其余层同理
)

5.2 过拟合处理

当训练准确率远高于验证准确率时:

# 增强数据增强
train_transform.transforms.insert(2, transforms.RandomRotation(10))

# 调整Dropout率
self.classifier = nn.Sequential(
    nn.Dropout(p=0.6),  # 提高丢弃率
    # ...其余层
)

# 添加早停机制
if test_loss > best_loss * 1.1:  # 当损失上升10%时停止
    break

5.3 现代优化技巧

我们可以融入一些后来发展的技术来提升原始AlexNet:

# 使用Adam优化器
optimizer = optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01)

# 添加学习率预热
scheduler = optim.lr_scheduler.SequentialLR(
    optim.lr_scheduler.LinearLR(optimizer, start_factor=0.1, total_iters=5),
    optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
)

# 使用混合精度训练
scaler = torch.cuda.amp.GradScaler()
with torch.cuda.amp.autocast():
    output = model(data)
    loss = criterion(output, target)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()

通过本教程,我们不仅复现了AlexNet的核心思想,还展示了如何用现代深度学习工具改进经典架构。这种"站在巨人肩膀上"的方法,正是深度学习研究不断前进的重要方式。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐