浅尝深度学习 - AlexNet代码实现
简体中文 (current) | English
写在前面
在本节中,笔者将从代码实现的角度实践上一节中介绍的AlexNet网络的构建和训练过程,带领读者熟悉深度学习从数据收集到最终模型性能评估的整个过程。
笔者将从0开始构建一个AlexNet,并使用CIFAR-10数据集在上面训练,然后使用来自网络的图片进行推理。
网络将在PyTorch上构建。我们先从核心的网络结构开始。
这里假设读者有一定的Python语法和面向对象基础,如果发现任何感到困惑的方法或语法,请及时查阅相关资料。
Step 1. 数据预处理
在构建网络之前,我们首先需要处理输入数据。对于CIFAR-10数据集,图像尺寸为32×32,这与原始AlexNet的224×224输入不同,因此我们需要调整网络结构。
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])
])这里的数据预处理包含三个步骤:
- 尺寸调整:将图像调整为32×32像素
- 张量转换:将PIL图像转换为PyTorch张量
- 标准化:使用CIFAR-10数据集的均值和标准差进行归一化
标准化是深度学习中的重要预处理步骤,它可以加速模型收敛并提高训练稳定性。这里使用的数值是CIFAR-10数据集在RGB三个通道上的统计值。
Step 2. AlexNet网络构建
要构建自己的网络,首先需要导入torch.nn包,这是一个包含了神经网络方法和类的核心模块。
class AlexNet(nn.Module):
def __init__(self, num_class=NUM_CLASSES):
super(AlexNet, self).__init__()对于我们的网络来说,只需要__init__方法来初始化网络结构,forward方法来定义前向传播即可。你可能在其他地方见过别的版本的AlexNet,包含数百行代码,这些大多是对数据进行处理或者添加了数据统计和存储等功能。在理解了核心代码后,你大可以自由地完成剩下的部分。
特征提取层
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输入的各层详细参数供参考:
| Layer | Input Size | Output Size | Kernel Size | Stride | Padding |
|---|---|---|---|---|---|
| Input | 32×32×3 | - | - | - | - |
| Conv-1 | 32×32×3 | 16×16×64 | 3×3 | 2 | 1 |
| Pool-1 | 16×16×64 | 8×8×64 | 2×2 | 2 | 0 |
| Conv-2 | 8×8×64 | 8×8×192 | 3×3 | 1 | 1 |
| Pool-2 | 8×8×192 | 4×4×192 | 2×2 | 2 | 0 |
| Conv-3 | 4×4×192 | 4×4×384 | 3×3 | 1 | 1 |
| Conv-4 | 4×4×384 | 4×4×256 | 3×3 | 1 | 1 |
| Conv-5 | 4×4×256 | 4×4×256 | 3×3 | 1 | 1 |
| Pool-3 | 4×4×256 | 2×2×256 | 2×2 | 2 | 0 |
每次卷积后都要使用ReLU进行激活。
这里笔者使用ReLU而非Sigmoid,目的是增强训练的精度和防止前文提到的vanishing gradient问题。
分类器层
在前面提到,全连接层接收的是一维向量,因此我们使用nn.Sequential方法构建分类器:
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。
前向传播
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方法传递的参数是:
view(batch_size, flattened_features)因此,在确保batch_size不变的情况下展平张量。
Step 3. 训练过程
训练的过程,就是一遍遍前向传播和反向传播的循环,但在实践中要复杂得多。
这里需要先引入几个重要概念:
学习率调整策略
-
学习率:学习率控制损失函数的变化速度,过小的学习率会导致模型学习速度太慢,陷入局部最优状态。而过于激进的学习率会导致参数在最优解附近震荡,难以收敛。
-
Warmup预热:预热过程是训练初期的必要过程,它使学习率从一个极小值慢慢升至目标值。一般采用线性学习率热身:
当训练步数小于热身步数时,学习率按如下方式更新:
当训练步数大于等于热身步数时:
-
学习率调度器:在实际训练中,使用固定学习率是不可靠的,会导致准确率难以进一步提升。因此,我们可以在特定的epoch后向下调整学习率。
训练器实现
笔者实现了一个完整的训练器类,包含以下核心功能:
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实现
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训练循环
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 / totalStep 4. 模型测试和推理
训练完成后,我们需要测试模型的性能并进行实际推理。
测试函数
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. 训练监控和可视化
为了更好地理解训练过程,训练器还包含了完整的监控和可视化功能:
性能曲线绘制
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数据集上的训练,我们可以观察到:
- Warmup的重要性:前5个epoch的预热过程确保了训练的稳定性
- 学习率调度:在特定epoch降低学习率有助于模型收敛到更好的局部最优解
- 过拟合控制:Dropout层有效防止了过拟合现象
典型的训练曲线会显示:
- 训练准确率稳步上升
- 测试准确率在初期快速提升,后期趋于稳定
- 学习率调整点会出现明显的性能提升
小结
通过本节的学习,读者应该掌握了:
- 网络实现:如何使用PyTorch构建AlexNet网络
- 训练流程:包括数据预处理、模型训练、性能监控
- 调优技巧:学习率调度、Warmup、Dropout等技术
- 模型评估:如何评估和可视化模型性能
这个完整的实现展示了深度学习项目的典型流程,从数据准备到模型部署的每个环节都有涉及。在下一节中,我们将探讨更先进的网络架构和优化技术。
实践建议:建议读者在自己的环境中运行这些代码,通过实际操作来加深理解。调整超参数,观察不同设置对模型性能的影响,这是学习深度学习的最佳方式。