简介
本文是关于在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_loader
和get_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_loader
和get_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