从零开始用PyTorch编写AlexNet

简介

本文是关于在PyTorch中从头开始构建最流行的卷积神经网络系列的延续。您可以在这里查看上一篇文章在此处,我们构建了LeNet5。在本文中,我们将构建AlexNet,这是计算机视觉中最具里程碑意义的突破性算法之一。

我们将首先调查并理解AlexNet的结构。然后,我们将通过加载我们的数据集CIFAR-10并对其进行一些预处理,直接进入代码。接下来,我们将使用PyTorch从头开始构建AlexNet,并在预处理后的数据上对其进行训练。最后,将在未见过的(测试)数据上对训练好的模型进行测试,以评估其性能。

先决条件

了解神经网络将对理解本文有所帮助。这包括熟悉神经网络的不同层(输入层、隐藏层、输出层)、激活函数、优化算法(梯度下降的各种变体)、损失函数等。此外,熟悉Python语法和PyTorch库对于理解本文中呈现的代码片段至关重要。

了解卷积神经网络(CNNs)是必不可少的,这包括对卷积层、池化层以及它们从输入数据中提取特征的作用的了解。理解诸如步长、填充以及核/滤波器大小对的影响等概念也是有益的。

AlexNet

AlexNet是一种深度卷积神经网络,最初是由Alex Krizhevsky和他的同事们在2012年开发的。该网络被设计用于为ImageNet LSVRC-2010比赛分类图像,并取得了最先进的结果。您可以在原始研究论文

详细了解该模型。

数据集

我们首先加载并预处理数据。对于我们来说,我们将使用CIFAR-10数据集。该数据集包括10类共60000张32×32彩色图像,每类有6000张图像。有50000张训练图像和10000张测试图像。

以下是数据集中的类别以及每个类别的10张随机样本图像:

来源:source

类别是完全相互独立的。没有汽车和卡车的重叠。“汽车”包括轿车、SUV等。“卡车”只包括大型卡车。两者都不包括皮卡。

导入库

首先,我们导入所需的库,并定义一个变量device,以便笔记本知道如果可用的话,使用GPU来训练模型。

import numpy as np
import torch
import torch.nn as nn
from torchvision import datasets
from torchvision import transforms
from torch.utils.data.sampler import SubsetRandomSampler

# 设备配置
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

加载数据集

我们将使用torchvision(一个计算机视觉任务的辅助库)来加载我们的数据集。这个方法有一些辅助函数,使得预处理变得相当容易和直接。让我们定义函数get_train_valid_loaderget_test_loader,然后调用它们来加载和处理我们的CIFAR-10数据:

def get_train_valid_loader(data_dir,
                           batch_size,
                           augment,
                           random_seed,
                           valid_size=0.1,
                           shuffle=True):
    normalize = transforms.Normalize(
        mean=[0.4914, 0.4822, 0.4465],
        std=[0.2023, 0.1994, 0.2010],
    )

    # 定义转换
    valid_transform = transforms.Compose([
            transforms.Resize((227,227)),
            transforms.ToTensor(),
            normalize,
    ])
    if augment:
        train_transform = transforms.Compose([
            transforms.RandomCrop(32, padding=4),
            transforms.RandomHorizontalFlip(),
            transforms.ToTensor(),
            normalize,
        ])
    else:
        train_transform = transforms.Compose([
            transforms.Resize((227,227)),
            transforms.ToTensor(),
            normalize,
        ])

    # 加载数据集
    train_dataset = datasets.CIFAR10(
        root=data_dir, train=True,
        download=True, transform=train_transform,
    )

    valid_dataset = datasets.CIFAR10(
        root=data_dir, train=True,
        download=True, transform=valid_transform,
    )

    num_train = len(train_dataset)
    indices = list(range(num_train))
    split = int(np.floor(valid_size * num_train))

    if shuffle:
        np.random.seed(random_seed)
        np.random.shuffle(indices)

    train_idx, valid_idx = indices[split:], indices[:split]
    train_sampler = SubsetRandomSampler(train_idx)
    valid_sampler = SubsetRandomSampler(valid_idx)

    train_loader = torch.utils.data.DataLoader(
        train_dataset, batch_size=batch_size, sampler=train_sampler)

    valid_loader = torch.utils.data.DataLoader(
        valid_dataset, batch_size=batch_size, sampler=valid_sampler)

    return (train_loader, valid_loader)


def get_test_loader(data_dir,
                    batch_size,
                    shuffle=True):
    normalize = transforms.Normalize(
        mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225],
    )

    # 定义转换
    transform = transforms.Compose([
        transforms.Resize((227,227)),
        transforms.ToTensor(),
        normalize,
    ])

    dataset = datasets.CIFAR10(
        root=data_dir, train=False,
        download=True, transform=transform,
    )

    data_loader = torch.utils.data.DataLoader(
        dataset, batch_size=batch_size, shuffle=shuffle
    )

    return data_loader

# CIFAR10 数据集 
train_loader, valid_loader = get_train_valid_loader(data_dir = './data',                                      batch_size = 64,
                       augment = False,                                          random_seed = 1)

test_loader = get_test_loader(data_dir = './data',
                              batch_size = 64)

让我们来解析代码:

  • 我们定义了两个函数get_train_valid_loaderget_test_loader分别用来加载训练/验证集和测试集
  • 我们首先定义了一个变量normalize,它包含了数据集中每个通道(红、绿、蓝)的平均值和标准差。这些可以通过手动计算得到,但由于CIFAR-10非常流行,这些值也可以在网上找到
  • 对于我们的训练数据集,我们增加了数据增强的选项,以便进行更健壮的训练并增加图像数量。注意:增强只应用于训练子集,而不应用于验证和测试子集,因为它们只用于评估目的
  • 我们将训练数据集分为训练和验证集(比例为90:10),并从整个训练集中随机子集它
  • 我们指定了批量大小,并在加载数据时打乱数据,以便每个批次在类型上有不同的标签。这将增加我们结果模型的效果
  • 最终,我们使用了数据加载器。在像CIFAR-10这样的小数据集上,这可能不会影响性能,但在大型数据集上,它确实可能阻碍性能,并且通常被认为是一个好习惯。数据加载器允许我们批量迭代数据,而且数据是在迭代过程中加载的,而不是一开始就全部加载到RAM中。

从零开始构建AlexNet

让我们先从代码开始:

class AlexNet(nn.Module):
    def __init__(self, num_classes=10):
        super(AlexNet, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=0),
            nn.BatchNorm2d(96),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 3, stride = 2))
        self.layer2 = nn.Sequential(
            nn.Conv2d(96, 256, kernel_size=5, stride=1, padding=2),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 3, stride = 2))
        self.layer3 = nn.Sequential(
            nn.Conv2d(256, 384, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(384),
            nn.ReLU())
        self.layer4 = nn.Sequential(
            nn.Conv2d(384, 384, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(384),
            nn.ReLU())
        self.layer5 = nn.Sequential(
            nn.Conv2d(384, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 3, stride = 2))
        self.fc = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(9216, 4096),
            nn.ReLU())
        self.fc1 = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU())
        self.fc2= nn.Sequential(
            nn.Linear(4096, num_classes))

    def forward(self, x):
        out = self.layer1(x)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.layer4(out)
        out = self.layer5(out)
        out = out.reshape(out.size(0), -1)
        out = self.fc(out)
        out = self.fc1(out)
        out = self.fc2(out)
        return out

定义AlexNet模型

让我们深入了解一下上面的代码是如何工作的:

  • 在PyTorch中定义任何神经网络(无论是CNN还是其他)的第一步是定义一个继承自nn.Module的类,因为它包含了许多我们需要利用的方法
  • 之后有两个主要步骤。首先是在__init__中初始化我们将在CNN中使用的层,另一个是定义这些层处理图像的顺序。这在内置的forward函数中定义。
  • 对于架构本身,我们首先使用nn.Conv2D函数定义卷积层,指定合适的核大小和输入/输出通道。我们还将使用nn.MaxPool2D函数应用最大池化。PyTorch的一个好处是我们可以将卷积层、激活函数和最大池化组合成一个单一的层(它们将分别应用,但有助于组织)使用nn.Sequential函数
  • 然后我们使用线性(nn.Linear)和丢弃(nn.Dropout)以及ReLu激活函数(nn.ReLU)定义全连接层,并使用nn.Sequential函数组合这些层
  • 最后,我们的最后一层输出10个神经元,这是我们对10个对象类别的最终预测

设置超参数

在训练之前,我们需要设置一些超参数,例如要使用的损失函数和优化器,以及批处理大小、学习率和训练轮次。

num_classes = 10
num_epochs = 20
batch_size = 64
learning_rate = 0.005

model = AlexNet(num_classes).to(device)

# 损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, weight_decay = 0.005, momentum = 0.9)  

# 训练模型
total_step = len(train_loader)

我们首先定义了一些简单的超参数(轮次、批处理大小和学习率),并使用类别数(在此案例中为10)作为参数初始化我们的模型,同时将模型转移到适当的设备(CPU或GPU)。然后我们定义了损失函数为交叉熵损失,优化器为Adam。对于这些,有很多选择,但这些通常能给模型和给定数据带来良好的结果。最后,我们定义了total_step来更好地跟踪训练步骤

训练

到此为止,我们已经准备好训练我们的模型了:

total_step = len(train_loader)

for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):  
        # 将张量移动到配置的设备
        images = images.to(device)
        labels = labels.to(device)

        # 前向传播
        outputs = model(images)
        loss = criterion(outputs, labels)

        # 反向传播和优化
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}' 
                   .format(epoch+1, num_epochs, i+1, total_step, loss.item()))

    # Validation
    with torch.no_grad():
        correct = 0
        total = 0
        for images, labels in valid_loader:
            images = images.to(device)
            labels = labels.to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
            del images, labels, outputs

        print('Accuracy of the network on the {} validation images: {} %'.format(5000, 100 * correct / total)) 

让我们看看代码是如何实现的:

  • 我们首先遍历轮次,然后遍历训练数据中的批次
  • 我们根据所使用的设备(即GPU或CPU)转换图像和标签
  • 在前向传播过程中,我们使用模型进行预测,并根据预测和实际标签计算损失
  • 接下来,我们进行反向传播,实际上是通过更新权重来改善我们的模型
  • 然后,我们使用optimizer.zero_grad()函数在每次更新之前将梯度设置为零
  • 接着,我们使用loss.backward()函数计算新的梯度。
  • 最后,我们使用`optimizer.step()`函数更新权重
  • 此外,在每一轮的结束时,我们使用验证集来计算模型的准确性。在这种情况下,我们不需要梯度,因此我们使用`with torch.no_grad()`进行更快的评估

我们可以看到以下输出:

训练损失和验证准确性

正如我们所见,每一次轮的损失都在下降,这表明我们的模型确实在学习。请注意,这个损失是在训练集上计算的,如果损失太小,可能会表明过拟合。这就是我们还要使用验证集的原因。在验证集上的准确性似乎在增加,这表明过拟合的可能性不大。现在让我们测试一下模型,看看它的表现如何。

测试

现在,我们看看模型在面对未见数据时的表现:

with torch.no_grad():
    correct = 0
    total = 0
    for images, labels in test_loader:
        images = images.to(device)
        labels = labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
        del images, labels, outputs

    print('Accuracy of the network on the {} test images: {} %'.format(10000, 100 * correct / total))   

请注意,这段代码与我们的验证目的完全相同。

仅通过训练6个轮次,使用该模型,我们似乎在验证集上获得了大约78.8%的准确性。

测试准确性

结论

现在让我们总结一下本文的内容:

  • 我们首先理解了AlexNet模型的架构以及不同类型的层
  • 然后,我们使用torchvision加载和预处理了CIFAR-10数据集
  • 接着,我们从头开始用PyTorch构建了我们的AlexNet模型
  • 最后,我们在CIFAR-10数据集上训练和测试了我们的模型,并且模型在测试数据上表现得很好,即使训练时间很短(6个周期)

未来工作

本文提供了一个坚实的基础和实用的经验,但您通过进一步探索和发现您还能完成更多的事情,您将获得更多的知识。

  • 您可以尝试使用不同的数据集。这样一个数据集是CIFAR-100,它是CIFAR-10数据集的一个扩展,有100个类别
  • 您可以尝试不同的超参数组合,看看它们对模型的最佳组合
  • 最后,您可以尝试从数据集中添加或删除层,以观察它们对模型能力的影響

Source:
https://www.digitalocean.com/community/tutorials/alexnet-pytorch