一、AlexNet简介
AlexNet是一个卷积神经网络(CNN)的架构,由Alex Krizhevsky和Ilya Sutskever在Geoffrey Hinton的指导下设计。
AlexNet在2012年的ImageNet大规模视觉识别挑战赛中获得了第一名,它被认为是计算机视觉领域最有影响力的论文之一,推动了更多的论文使用CNN和GPU来加速深度学习。
AlexNet共有8层结构,前5层为卷积层用作特征提取,后3层为全连接层用作分类器。为了适应两个GPU的并行计算,AlexNet使用了分组卷积。
Alex网络图解:
二、AlexNet网络详解
1. 数据源
AlexNet输入的数据源是ImageNet数据库,它包含了超过一百万张的彩色图像,分为1000个不同的类别。
AlexNet输入的图像大小是227 X 227
,如果原始图像不是这个大小,需要先进行缩放或裁剪;输入的图像是RGB格式,每个通道有3个颜色分量。
本文使用的是CIFAR-100数据集,它包含60000张32x32的彩色图像,涵盖100个类别。其中50000张图像用于训练,10000张图像用于测试。每个类别包含600张图像;输入图像采用224 X 224
。
2. 数据加载
transform = {
# 进行数据增强的处理
"train": transforms.Compose([transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),
"val": transforms.Compose([transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),
}
nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8]) # number of workers
print('Using {} dataloader workers every process'.format(nw))
train_dataset = torchvision.datasets.CIFAR100(root='path/to/train/data', train=True, download=True,
transform=transform["train"])
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=nw)
validate_dataset = torchvision.datasets.CIFAR100(root='path/to/test/data', train=False, download=False,
transform=transform["val"])
validate_loader = torch.utils.data.DataLoader(validate_dataset, batch_size=batch_size, shuffle=False, num_workers=nw)
这里的数据集预处理,主要有:
-
transforms.RandomResizedCrop(224)
:随机裁剪并缩放; -
transforms.RandomHorizontalFlip()
:随机翻转; -
transforms.ToTensor()
:将PIL Image或者numpy.ndarray数据类型转化为torch.FloatTensor类型,且数值归一化到[0.0, 1.0]
; -
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])])
:对数据进行标准化处理,使得数据的均值为mean,标准差为std。
3. 各层介绍
(1)第一层卷积层
nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2)
该层主要作用是提取输入图像的特征。
- 卷积核:
11 X 11
- 步长:4
- 填充:2
- 输入通道数:3
- 输出通道数:48
- 输入形状为:
[batch_size, 3, 224, 224]
- 输出形状为:
[batch_size, 48, 54, 54]
输出形状里的54计算过程:
(2)第一个池化层
nn.MaxPool2d(kernel_size=3, stride=2)
该层主要作用是对特征进行下采样,减小特征图的大小,同时保留特征的主要信息。
- 池化核:
3 X 3
- 步长:2
- 输入形状为:
[batch_size, 48, 54, 54]
- 输出形状为:
[batch_size, 48, 27, 27]
(3)第二层卷积层
nn.Conv2d(48, 128, kernel_size=5, padding=2)
该层主要作用是进一步提取特征。
- 卷积核:
5 X 5
- 填充:2
- 输出通道数:128
- 输入形状为:
[batch_size, 48, 27, 27]
- 输出形状为:
[batch_size, 128, 27, 27]
(4)第二个池化层
nn.MaxPool2d(kernel_size=3, stride=2)
该层主要作用是对特征进行下采样,减小特征图的大小,同时保留特征的主要信息。
- 池化核:
3 X 3
- 步长:2
- 输入形状为:
[batch_size, 128, 27, 27]
- 输出形状为:
[batch_size, 128, 13, 13]
(5)第三层卷积层
nn.Conv2d(128, 192, kernel_size=3, padding=1)
该层主要作用是进一步提取特征。
- 卷积核:
3 X 3
- 填充:1
- 输出通道数:192
- 输入形状为:
[batch_size, 128, 13, 13]
- 输出形状为:
[batch_size, 192, 13, 13]
(6)第四层卷积层
nn.Conv2d(192, 192, kernel_size=3, padding=1)
该层主要作用是进一步提取特征。
- 卷积核:
3 X 3
- 填充:1
- 输出通道数:192
- 输入形状:
[batch_size, 192, 13, 13]
- 输出形状:
[batch_size, 192, 13, 13]
(7)第五层卷积层
nn.Conv2d(192, 128, kernel_size=3, padding=1)
该层主要作用是进一步提取特征。
- 卷积核:
3 X 3
- 填充:1
- 输出通道数:128
- 输入形状:
[batch_size, 192, 13, 13]
- 输出形状:
[batch_size, 128, 13, 13]
(8)第三层池化层
nn.MaxPool2d(kernel_size=3, stride=2)
该层主要作用是对特征进行下采样,减小特征图的大小,同时保留特征的主要信息。
- 池化核:
3 X 3
- 步长:2
- 输入形状:
[batch_size, 128, 13, 13]
- 输出形状:
[batch_size, 128, 6, 6]
4. 全局平均池化层
self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
将输入的特征图进行降维,将每个通道的特征图转化为一个标量,从而得到一个固定长度的特征向量。
- 输入形状为
(batch_size, 256, 4, 4)
- 输出形状为
(batch_size, 256, 6, 6)
全局平均池化层的作用是将特征图的空间信息进行压缩,从而减少模型的参数数量,防止过拟合。
5. 张量展平
x = torch.flatten(x, 1)
在前身传播的过程中,后面会使用torch.flatten对平均池化的张量处理,将输入的张量x展平成一个一维的张量,方便后面的全连接层的输入。其中,第一个参数1表示从第二个维度开始展平,即将每个样本的所有通道的像素值展平成一个一维的向量。
展平后输出形状为: (batch_size, 256 * 6 * 6)
6. 分类器
在AlexNet模型中,分类器由三个全连接层组成。
在每个全连接层之间,都加入了一个Dropout层和一个ReLU激活函数。其中:
- Dropout层的作用是随机地将一些神经元的输出置为0,以避免过拟合;
- ReLU激活函数的作用是增强模型的非线性拟合能力。
分类器代码如下:
self.classifier = nn.Sequential(
nn.Dropout(p=0.5),
# 输入形状:[batch_size, 128*6*6];输出形状:[batch_size, 2048]
nn.Linear(128 * 6 * 6, 2048),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
# 输入形状:[batch_size, 2048];输出形状:[batch_size, 2048]
nn.Linear(2048, 2048),
nn.ReLU(inplace=True),
# 输入形状:[batch_size, 2048];输出形状:[batch_size, num_classes]
nn.Linear(2048, num_classes),
)
(1)全连接层一
nn.Linear(128 * 6 * 6, 2048)
该层主要作用是将卷积层提取的特征进行分类。
- 输入形状:
[batch_size, 128 X 6 X 6]
- 输出形状:
[batch_size, 2048]
。
(2)全连接层二
nn.Linear(2048, 2048)
该层主要作用是进一步提取特征,使用了一个 2048 维的向量进行特征提取。
- 输入形状:
[batch_size, 2048]
- 输出形状:
[batch_size, 2048]
(3)全连接层三
nn.Linear(2048, num_classes)
- 输入形状:
[batch_size, 2048]
- 输出形状:
[batch_size, 100]
三、完整代码实现
import torchvision
import os
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import transforms
from tqdm import tqdm
class AlexNet(nn.Module):
def __init__(self, num_classes=1000):
super(AlexNet, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(48, 128, kernel_size=5, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(128, 192, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(192, 192, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(192, 128, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
# output[128, 6, 6]
nn.MaxPool2d(kernel_size=3, stride=2),
)
self.classifier = nn.Sequential(
nn.Dropout(p=0.5),
nn.Linear(128 * 6 * 6, 2048),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
nn.Linear(2048, 2048),
nn.ReLU(inplace=True),
nn.Linear(2048, num_classes),
)
def forward(self, x):
x = self.features(x)
for i, layer in enumerate(self.features):
x = layer(x)
print('Shape after layer {}: {}'.format(i + 1, x.shape))
x = torch.flatten(x, start_dim=1)
x = self.classifier(x)
return x
# 超参数
num_epochs = 200
batch_size = 200
learning_rate = 0.002
save_model = './path/model'
transform = {
# 进行数据增强的处理
"train": transforms.Compose([transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),
"val": transforms.Compose([transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),
}
nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8]) # number of workers
print('Using {} dataloader workers every process'.format(nw))
train_dataset = torchvision.datasets.CIFAR100(root='path/to/train/data', train=True, download=True,
transform=transform["train"])
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=nw)
validate_dataset = torchvision.datasets.CIFAR100(root='path/to/test/data', train=False, download=True,
transform=transform["val"])
validate_loader = torch.utils.data.DataLoader(validate_dataset, batch_size=batch_size, shuffle=False, num_workers=nw)
train_num = len(train_dataset)
validate_num = len(validate_dataset)
print("using {} images for training, {} images for validation.".format(train_num, validate_num))
print("using {} images for training, {} images for validation.".format(train_num, validate_num))
# 定义模型、损失函数和优化器
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print("using {} device.".format(device))
model = AlexNet(num_classes=100).to(device)
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
best_acc = 0.0
# switch to train mode
model.train()
# 记录训练产生的数据
train_acc_list = []
train_loss_list = []
val_acc_list = []
for epoch in range(num_epochs):
# train
model.train()
running_loss_train = 0.0
train_accurate = 0.0
train_bar = tqdm(train_loader)
for images, labels in train_bar:
optimizer.zero_grad()
outputs = model(images.to(device))
loss = loss_function(outputs, labels.to(device))
loss.backward()
optimizer.step()
predict = torch.max(outputs, dim=1)[1]
train_accurate += torch.eq(predict, labels.to(device)).sum().item()
running_loss_train += loss.item()
train_accurate = train_accurate / train_num
running_loss_train = running_loss_train / train_num
train_acc_list.append(train_accurate)
train_loss_list.append(running_loss_train)
print('[epoch %d] train_loss: %.7f train_accuracy: %.3f' %
(epoch + 1, running_loss_train, train_accurate))
# validate
model.eval()
acc = 0.0 # accumulate accurate number / epoch
with torch.no_grad():
validate_loader = tqdm(validate_loader)
for val_data in validate_loader:
val_images, val_labels = val_data
outputs = model(val_images.to(device))
predict_y = torch.max(outputs, dim=1)[1]
acc += torch.eq(predict_y, val_labels.to(device)).sum().item()
val_accurate = acc / validate_num
val_acc_list.append(val_accurate)
print('[epoch %d] val_accuracy: %.3f' % (epoch + 1, val_accurate))
# 选择最好的模型进行保存
if val_accurate > best_acc:
best_acc = val_accurate
torch.save(model.state_dict(), save_model)
# 测试模型
model.eval()
with torch.no_grad():
correct = 0
total = 0
for images, labels in validate_loader:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += torch.eq(predicted, labels).sum().item()
print('Test Accuracy of the model on the {} test images: {} %'.format(total, 100 * correct / total))