浅尝深度学习 - AlexNet代码实现

published: and updated:

Deep Learning Deep Learning , CNN , AlexNet

This article is hidden from post list
Language / 语言

简体中文 (current) | English

写在前面#

在本节中,笔者将从代码实现的角度实践上一节中介绍的AlexNet网络的构建和训练过程,带领读者熟悉深度学习从数据收集到最终模型性能评估的整个过程。

笔者将从0开始构建一个AlexNet,并使用CIFAR-10数据集在上面训练,然后使用来自网络的图片进行推理。

网络将在PyTorch上构建。我们先从核心的网络结构开始。

TIP

这里假设读者有一定的Python语法和面向对象基础,如果发现任何感到困惑的方法或语法,请及时查阅相关资料。

Step 1. 数据预处理#

在构建网络之前,我们首先需要处理输入数据。对于CIFAR-10数据集,图像尺寸为32×32,这与原始AlexNet的224×224输入不同,因此我们需要调整网络结构。

python
import torch
import torch.nn as nn
from torchvision import transforms
from PIL import Image

NUM_CLASSES = 10

# 数据预处理管道
tran = transforms.Compose([
    transforms.Resize((32,32), interpolation=Image.BICUBIC),
    transforms.ToTensor(), # 转换为张量,高维数组
    transforms.Normalize([0.4914, 0.4822, 0.4465], [0.2023, 0.1994, 0.201])
])

这里的数据预处理包含三个步骤:

  1. 尺寸调整:将图像调整为32×32像素
  2. 张量转换:将PIL图像转换为PyTorch张量
  3. 标准化:使用CIFAR-10数据集的均值和标准差进行归一化
TIP

标准化是深度学习中的重要预处理步骤,它可以加速模型收敛并提高训练稳定性。这里使用的数值是CIFAR-10数据集在RGB三个通道上的统计值。

Step 2. AlexNet网络构建#

要构建自己的网络,首先需要导入torch.nn包,这是一个包含了神经网络方法和类的核心模块。

python
class AlexNet(nn.Module):
    def __init__(self, num_class=NUM_CLASSES):
        super(AlexNet, self).__init__()

对于我们的网络来说,只需要__init__方法来初始化网络结构,forward方法来定义前向传播即可。你可能在其他地方见过别的版本的AlexNet,包含数百行代码,这些大多是对数据进行处理或者添加了数据统计和存储等功能。在理解了核心代码后,你大可以自由地完成剩下的部分。

特征提取层#

python
self.features = nn.Sequential(
    nn.Conv2d(3, 64, kernel_size=3, stride=2, padding=1),
    nn.ReLU(inplace=True), 
    nn.MaxPool2d(kernel_size=2),
    nn.Conv2d(64, 192, kernel_size=3, padding=1),
    nn.ReLU(inplace=True),
    nn.MaxPool2d(kernel_size=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=2),
)

读者可能对这里的各种参数感到复杂,这里列出针对32×32输入的各层详细参数供参考:

LayerInput SizeOutput SizeKernel SizeStridePadding
Input32×32×3----
Conv-132×32×316×16×643×321
Pool-116×16×648×8×642×220
Conv-28×8×648×8×1923×311
Pool-28×8×1924×4×1922×220
Conv-34×4×1924×4×3843×311
Conv-44×4×3844×4×2563×311
Conv-54×4×2564×4×2563×311
Pool-34×4×2562×2×2562×220

每次卷积后都要使用ReLU进行激活。

TIP

这里笔者使用ReLU而非Sigmoid,目的是增强训练的精度和防止前文提到的vanishing gradient问题。

分类器层#

在前面提到,全连接层接收的是一维向量,因此我们使用nn.Sequential方法构建分类器:

python
self.classifier = nn.Sequential(
    nn.Dropout(),
    nn.Linear(256*2*2, 4096),
    nn.ReLU(inplace=True),
    nn.Dropout(),
    nn.Linear(4096, 4096),
    nn.ReLU(inplace=True),
    nn.Linear(4096, num_class),
)

这里的256*2*2计算来自最后一个池化层的输出尺寸:2×2×256。

前向传播#

python
def forward(self, x):
    x = self.features(x)
    x = x.view(x.size(0), 256*2*2)
    x = self.classifier(x)
    return x

这里的view方法起到一个桥梁的作用,输入的图像经过多层卷积后,以张量的形式输出,但全连接层必须接收一维向量,因此需要展平。view方法传递的参数是:

python
view(batch_size, flattened_features)

因此,在确保batch_size不变的情况下展平张量。

Step 3. 训练过程#

训练的过程,就是一遍遍前向传播和反向传播的循环,但在实践中要复杂得多。

这里需要先引入几个重要概念:

学习率调整策略#

  1. 学习率:学习率控制损失函数的变化速度,过小的学习率会导致模型学习速度太慢,陷入局部最优状态。而过于激进的学习率会导致参数在最优解附近震荡,难以收敛。

  2. Warmup预热:预热过程是训练初期的必要过程,它使学习率从一个极小值慢慢升至目标值。一般采用线性学习率热身:

    当训练步数小于热身步数时,学习率按如下方式更新:

    lr=start_lr+(end_lrstart_lr)×epochwarmup_stepslr = start\_lr + (end\_lr - start\_lr) \times \frac{epoch}{warmup\_steps}

    当训练步数大于等于热身步数时:

    lr=learning_ratelr = learning\_rate
  3. 学习率调度器:在实际训练中,使用固定学习率是不可靠的,会导致准确率难以进一步提升。因此,我们可以在特定的epoch后向下调整学习率。

训练器实现#

笔者实现了一个完整的训练器类,包含以下核心功能:

python
class Trainer(object):
    def __init__(self, model_name, model, lr, train_on_gpu=False):
        self.model = model
        self.lr = lr
        self.model_name = model_name
        self.train_on_gpu = train_on_gpu
        self.best_acc = 0
        self.best_epoch = 0
        
        if self.train_on_gpu:
            self.model = self.model.cuda()
        
        # 优化器设置
        self.optimizer = optim.SGD(
            self.model.parameters(),
            self.lr,
            momentum=0.9,
            weight_decay=5e-4
        )
        
        # 学习率调度器
        self.scheduler = MultiStepLR(
            self.optimizer, 
            milestones=[10, 20, 50, 100, 180], 
            gamma=0.1
        )

Warmup实现#

python
def warmup_learning_rate(self, init_lr, no_of_steps, epoch, len_epoch):
    """前5个epoch进行学习率预热"""
    factor = no_of_steps // 30
    lr = init_lr * (0.1**factor)
    # Warmup计算
    lr = lr * float(1 + epoch + no_of_steps * len_epoch) / (5. * len_epoch)
    return lr

训练循环#

python
def train(self, epoch, no_of_steps, trainloader):
    self.model.train()
    train_loss, correct, total = 0, 0, 0
    
    # 前5个epoch使用warmup,之后使用调度器
    if epoch < 5:
        lr = self.warmup_learning_rate(self.lr, no_of_steps, epoch, len(trainloader))
        for param_group in self.optimizer.param_groups:
            param_group['lr'] = lr
    elif epoch == 5:
        for param_group in self.optimizer.param_groups:
            param_group['lr'] = self.lr
    
    criterion = nn.CrossEntropyLoss()
    
    for idx, (inputs, targets) in enumerate(trainloader):
        if self.train_on_gpu:
            inputs, targets = inputs.cuda(), targets.cuda()
            
        self.model.zero_grad()
        outputs = self.model(inputs)
        loss = criterion(outputs, targets)
        loss.backward()
        self.optimizer.step()
        
        train_loss += loss.item()
        _, predicted = outputs.max(1)
        total += targets.size(0)
        correct += (targets == predicted).sum().item()
    
    if epoch >= 5:
        self.scheduler.step()
    
    return 100. * correct / total

Step 4. 模型测试和推理#

训练完成后,我们需要测试模型的性能并进行实际推理。

测试函数#

python
def test():
    net = AlexNet().cuda()
    model_path = os.path.join("weights", "alexnet.pt")
    
    # 加载训练好的模型
    checkpoint = torch.load(model_path)
    net.load_state_dict(checkpoint['net'])
    
    # 加载测试图像
    test_image = os.path.join('test.jpg')
    img = Image.open(test_image)
    img_tensor = tran(img)  # 应用预处理
    
    # 添加batch维度并移至GPU
    input_tensor = img_tensor.unsqueeze_(0).cuda()
    
    # 模型推理
    y = net(input_tensor)
    
    # 计算概率分布
    percentage = torch.softmax(y[0], dim=0) * 100
    cl_fp32, index_fp32 = torch.max(percentage, 0)
    
    # CIFAR-10类别
    classes = ['plane', 'car', 'bird', 'cat', 'deer', 
               'dog', 'frog', 'horse', 'ship', 'truck']
    
    # 在图像上添加预测结果
    font = ImageFont.truetype('LiberationSans-Regular.ttf', 30)
    draw = ImageDraw.Draw(img)
    text = str(classes[index_fp32]) + ' (' + '{:.2f}'.format(cl_fp32.item()) + '%)'
    draw.text((0, 0), text, font=font, fill="#ff00ff")
    
    img.save(test_image, 'jpeg')
    print(f'预测结果: {classes[index_fp32]} ({cl_fp32.item():.2f}%)')

Step 5. 训练监控和可视化#

为了更好地理解训练过程,训练器还包含了完整的监控和可视化功能:

性能曲线绘制#

python
def plot_accuracy_curves(self):
    import matplotlib.pyplot as plt
    
    epochs = range(1, len(self.train_acc_history) + 1)
    
    plt.figure(figsize=(12, 8))
    plt.plot(epochs, self.train_acc_history, 'b-', label='Training Accuracy', linewidth=2)
    plt.plot(epochs, self.test_acc_history, 'r-', label='Test Accuracy', linewidth=2)
    
    # 标记最佳测试准确率
    best_epoch = self.best_epoch + 1
    best_acc = self.best_acc
    plt.plot(best_epoch, best_acc, 'ro', markersize=10,
             label=f'Best Test Acc: {best_acc:.2f}% (Epoch {best_epoch})')
    
    # 标记学习率调整点
    milestones = [10, 20, 50, 100, 180]
    for ms in milestones:
        if ms <= len(epochs):
            plt.axvline(x=ms, color='gray', linestyle='--', alpha=0.7)
    
    plt.xlabel('Epoch')
    plt.ylabel('Accuracy (%)')
    plt.title(f'AlexNet Training on CIFAR-10\\nBest Test Acc: {best_acc:.2f}%')
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.savefig('accuracy_curves.png', dpi=300)

实验结果和分析#

通过在CIFAR-10数据集上的训练,我们可以观察到:

  1. Warmup的重要性:前5个epoch的预热过程确保了训练的稳定性
  2. 学习率调度:在特定epoch降低学习率有助于模型收敛到更好的局部最优解
  3. 过拟合控制:Dropout层有效防止了过拟合现象

典型的训练曲线会显示:

  • 训练准确率稳步上升
  • 测试准确率在初期快速提升,后期趋于稳定
  • 学习率调整点会出现明显的性能提升

小结#

通过本节的学习,读者应该掌握了:

  1. 网络实现:如何使用PyTorch构建AlexNet网络
  2. 训练流程:包括数据预处理、模型训练、性能监控
  3. 调优技巧:学习率调度、Warmup、Dropout等技术
  4. 模型评估:如何评估和可视化模型性能

这个完整的实现展示了深度学习项目的典型流程,从数据准备到模型部署的每个环节都有涉及。在下一节中,我们将探讨更先进的网络架构和优化技术。

TIP

实践建议:建议读者在自己的环境中运行这些代码,通过实际操作来加深理解。调整超参数,观察不同设置对模型性能的影响,这是学习深度学习的最佳方式。