1. 结构和创新点
创新点
AlexNet是Alex小哥在自己的电脑上用两块 GTX 580 3GB GPUs 显卡捣鼓出来的模型,当时刷新了image classification的记录(自己捣鼓的东西直接成为state-of-the-art,不愧是大神啊),除了模型结构外,AlexNet的主要创新点在于:
- 采用了ReLU作为激活函数,解决了Sigmoid函数在网络较深时出现的梯度消失问题,并且使用ReLU的速度也更快。
- 采用多GPU的方式训练(这更偏向于工程上的改进,在科研领域影响不大)
- 训练时使用Dropout方法随机失活一些神经元,在AlexNet中,主要是在最后的全连接层使用了这项技术,因为最后的全连接层非常大(4096 * 4096),这就导致很容易发生过拟合问题,Dropout本质也是一种正则项,可以有效地抑制过拟合问题。
- 使用重叠的最大池化,此前CNN中普遍使用平均池化,AlexNet全部使用最大池化,避免平均池化的模糊化效果。并且AlexNet中提出让步长比池化核的尺寸小,这样池化层的输出之间会有重叠和覆盖,提升了特征的丰富性。
- 提出了LRN层(局部响应归一化),增强了模型的泛化能力,但在后来被证明这是无效的操作,被Batch Normalization替代。
- 采用原始的RGB图像进行训练,开启了端到端的训练方式(虽然但是Alex没有意识到这一工作的重大意义)
结构
论文中给出模型的结构如下图所示,需要注意的是,Alex当时使用了两块GPU进行模型并行化,所以下图展示的是一个完整的模型(底部),以及半个模型(顶部),两个相同的模型分别在两个GPU中进行训练,并在第3个卷积层以及后面的全连接层中进行了数据交流。
由于Alex使用并行化实现,上下两个模型是一样的,我们将其合并起来也可能当做一个完整的AlexNet网络,它由5个卷积层和3个全连接层实现,具体参数如下:
- Input 层,输入为 3 * 224 * 224 的图片(分别对应通道–长–宽,下同)
- Conv1层,采用 96 个 3 * 11 * 11 的卷积核,步长为 4 ,padding为2 ,得到输出为 96 * 55 * 55 的特征矩阵(这里是96是因为把两个并行模型合并了)
- MaxPool1层,池化核为 3 * 3,步长为2, 得到特征矩阵为 96 * 27 * 27
- Conv2层,使用了 256 个 96 * 5 * 5 的卷积核,步长为1, padding为2,输出的特征矩阵为 256 * 27 * 27
- MaxPool2层,池化核为 3 * 3,步长为2, 得到特征矩阵为 256 * 13 * 13
- Conv3层,使用 384 个 256 * 3 * 3 的卷积核,步长为1, padding为1,输出特征矩阵为 384 * 13 * 13
- Conv4层,使用 384 个 384 * 3 * 3 的卷积核,步长为1, padding为1,输出特征矩阵为 384 * 13 *13
- Conv5层, 使用 256个 384 * 3 * 3 的卷积核,步长为1,padding为1,输出特征矩阵为 256 * 13 * 13
- MaxPool3层,池化核为 3 * 3,步长为2, 得到特征矩阵为 256 * 6 * 6
- FC6层,全连接层,扁平化成 1 *9216的输入,输出为 1 *4096
- Dropout6层,以0.5的概率随机失活,输出维度不变
- FC7层,输出还是 1 * 4096
- Dropout7层,以0.5的概率随机失活,维度不变
- FC8层,输出维度为1 * 1000
2. 代码实现
模型搭建
模型中主要包括网络结构的搭建和权值初始化,按照论文中的说法,对于所有层的输出都进行标准化,使得数据成高斯分布,方差为0.01,均值为0,对于第2、4、5个卷积层和所有的全连接层的偏置置为0。另外,在每两个全连接层之间加入一个概率为0.5的Dropout,一共有两个Dropout层。
from torchvision import datasets, transforms
import torch
class AlexNet(nn.Module):
def __init__(self, class_num=100, init_weights=False):
super(AlexNet, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(96, 256, kernel_size=5, stride=1, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(256, 384, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 384, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Flatten(start_dim=1), # 按照第一维度展开,因为tensor进来的第0维为batch
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
nn.Linear(4096, class_num)
)
if init_weights:
self.initialize_wights()
def forward(self, x):
x = self.features(x)
return x
def initialize_wights(self):
idx = 0
for m in self.modules():
if isinstance(m, nn.Conv2d):
idx = idx + 1
nn.init.normal_(m.weight, 0, 0.01)
if m.bias is not None and (idx in [2, 4, 5]):
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
数据预处理
对于输入的数据,论文中的做法是将图片先按照短边将图片等比缩放到宽为256的图片,然后按照中心裁剪成256*256,接着再随机在256 * 256 的图片中提取出一个224 * 224的图片并随机进行翻转,这样做的好处是增加了随机性,减少过拟合的问题。这里为了方便,直接将图片随机裁剪成224 * 224,然后再随机翻转。论文中对训练集的一个batch大小设置为128,这里和论文中保持一致,但我采用的是CIFAR100数据集进行训练。
"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)), # cannot 224, must (224, 224)
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])}
train_data = datasets.CIFAR100(root='../Dataset/CIFAR100', train=True, transform=data_transform['train'],
download=True)
validate_data = datasets.CIFAR100(root='../Dataset/CIFAR100', train=False, transform=data_transform['val'],
download=True)
batch_size = 128
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_loader = torch.utils.data.DataLoader(train_data,
batch_size=batch_size, shuffle=True,
num_workers=nw)
validate_loader = torch.utils.data.DataLoader(validate_data,
batch_size=4, shuffle=False,
num_workers=nw)
训练模型
在论文中,主要有以下几个细节需要注意:
- 采用SGD优化器,momentum=0.9,weight_decay =0.0005
- 学习率设置为0.01,当损失值不变化时,将学习率缩小10倍
net.to(device)
loss_function = nn.CrossEntropyLoss()
# pata = list(net.parameters())
optimizer = optim.Adam(net.parameters(), lr=0.0002)
epochs = 10
save_path = './AlexNet.pth'
best_acc = 0.0
train_steps = len(train_loader)
for epoch in range(epochs):
# train
net.train()
running_loss = 0.0
train_bar = tqdm(train_loader, file=sys.stdout)
for step, data in enumerate(train_bar):
images, labels = data
optimizer.zero_grad()
outputs = net(images.to(device))
loss = loss_function(outputs, labels.to(device))
loss.backward()
optimizer.step()
# print statistics
running_loss += loss.item()
train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1,
epochs,
loss)
# validate
net.eval() # 进入评估模式,取消dropout
acc = 0.0 # accumulate accurate number / epoch
with torch.no_grad():
val_bar = tqdm(validate_loader, file=sys.stdout)
for val_data in val_bar:
val_images, val_labels = val_data
outputs = net(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 / len(validate_data)
print('[epoch %d] train_loss: %.3f val_accuracy: %.3f' %
(epoch + 1, running_loss / train_steps, val_accurate))
if val_accurate > best_acc:
best_acc = val_accurate
torch.save(net.state_dict(), save_path)
print('Finished Training')