卷积神经网络三:AlexNet

AlexNet是第一个现代深度卷积网络模型,其首次使用了很多现代深度卷积网络的一些方法,如使用GPU并行训练,采用ReLU函数作为非线性函数,使用Dropout技术防止过拟合,使用数据增强来提高模型准确率等等。AlexNet赢得了2012年ImageNet大赛冠军,并且是以领先第二名10%的准确率夺得冠军,自此,深度学习的威力才正式被人们所认识到。所以,这次我们来学习一下AlexNet网络模型。

AlexNet网络结构

AlexNet的网络结构如下图所示:


可以看到,训练分别在两个GPU上并行训练,最终合并到一起的。其实,是由于当时GPU计算能力不强,现在已经完全可以用一个GPU来代替了。下图直观的展示了LeNet-5和AlexNet的网络结构。

这里有一个问题要注意一下,如果按照网络结构说明的编写,采用3224224图像作为输入,会发现在全连接层会报错,这是因为AlexNet论文中,原图输入224 × 224,实际上进行了随机裁剪,实际大小为227 × 227。所以我们如果输入3224224的图像的话,最终卷积层输出是25655。下面我们具体说明一下每一层的计算过程(卷积运算 o = ⌊(i + 2p - k) / s⌋ + 1,向下取整):

  1. 输入图像3x224x224
  2. 卷积层,输入通道数3,输出通道数96,采用11x11的卷积核,步幅为4,输出结果为96x54x54,再做非线性激活
  3. 池化层,通道数不变,卷积核3x3,步幅为2,输出结果为96x26x26
  4. 卷积层,输入通道数96,输出通道数256,卷积核5x5,填充为2,输出结果为256x26x26,再做非线性激活
  5. 池化层,通道数不变,卷积核3x3,步幅为2,输出结果为256x12x12
  6. 卷积层,输入通道数256,输出通道数384,卷积核3x3,填充为1,输出结果为384x12x12,再做非线性激活
  7. 卷积层,输入通道数384,输出通道数384,卷积核3x3,填充为1,输出结果为384x12x12,再做非线性激活
  8. 卷积层,输入通道数384,输出通道数256,卷积核3x3,填充为1,输出结果为256x12x12,再做非线性激活
  9. 池化层,通道数不变,卷积核3x3,步幅为2,输出结果为256x5x5
  10. 全连接层,输入大小256x5x5=6400,输出结果大小4096
  11. 全连接层,输入大小4096,输出结果大小4096
  12. 全连接层,输入大小4096,输出结果1000(imagenet分类比赛数量,我们可以根据具体问题修改)

代码实现

下面,我们用pytorch实现一下AlexNet,并用cifar数据集进行训练,查看结果。
首先定义AlexNet网络结构:

import torch.nn as nn
import torch
import torch.optim as optim
import torchvision.transforms as transforms

class AlexNet(nn.Module):
    def __init__(self, num_classes) -> None:
        super(AlexNet, self).__init__()
        self.num_classes = num_classes
        self.features = nn.Sequential(
            # 卷积运算 o = ⌊(i + 2p - k) / s⌋ + 1,向下取整
            nn.Conv2d(in_channels=3, out_channels=96, kernel_size=11, stride=4), # 输出54*54*96,如果要像论文中一样55*55*96,输入需要是227*227
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2), # 输出26*26*96
            nn.Conv2d(in_channels=96, out_channels=256, kernel_size=5, padding=2), # 输出26*26*256
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2), # 输出12*12*256
            nn.Conv2d(in_channels=256, out_channels=384, kernel_size=3, padding=1), # 输出12*12*384
            nn.ReLU(),
            nn.Conv2d(in_channels=384, out_channels=384, kernel_size=3, padding=1), # 输出12*12*384
            nn.ReLU(),
            nn.Conv2d(in_channels=384, out_channels=256, kernel_size=3, padding=1), # 输出12*12*256
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2), # 输出5*5*256
        )
        self.classifier = nn.Sequential(
            nn.Dropout(), # 随机丢弃一部分参数,防止过拟合
            nn.Linear(5*5*256, 4096),
            nn.ReLU(),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Dropout(),
            nn.Linear(4096, self.num_classes),
        )
    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), 5*5*256) # x.size(0)就是batch_size
        x = self.classifier(x)
        return x

然后,定义一个训练函数

def train(net, data_loader, test_loader, epochs=300):
    net.train()
    criterion = nn.CrossEntropyLoss()
    lr = 0.01
    optimizer = optim.SGD(net.parameters(), lr=lr, momentum=0.9)
    for epoch in range(epochs):
        total_loss = 0.0
        for i, (image, label) in enumerate(data_loader):
            if torch.cuda.is_available():
                image = Variable(image).cuda()
                label = Variable(label).cuda()
                net.cuda()
            else:
                image = Variable(image)
                label = Variable(label)
            outputs = net(image)
            loss = criterion(outputs, label)
            total_loss += loss.item()
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        print('Epoch {}, Loss: {:.4f}'.format(epoch+1, total_loss/len(data_loader)))
        if epoch==80:
            lr = lr/10
            optimizer = torch.optim.SGD(net.parameters(), lr=lr, momentum=0.9)
    torch.save(net, 'checkpoints/alexnet.pt')
    print('saveing checkpoints/alexnet.pt')
    # 测试模型
    net.eval()
    with torch.no_grad():
        correct = 0
        total = 0
        for i, (image, label) in enumerate(test_loader):
            if torch.cuda.is_available():
                image = Variable(image).cuda()
                label = Variable(label).cuda()
            outputs = net(image)
            predicted = torch.max(outputs, 1)[1] # 获取到预测的结果,[1]代表的是预测出来的索引值,也就是标签的值
            total += label.size(0)
            correct += (predicted == label).sum().item()
        print('Test Accuracy: {:.2f}%'.format(100 * correct / total))
    pass

注意,因为cifar-10数据集原始大小是3232的,所以需要把cifar-10图像大小变换为224224才可以输入到AlexNet网络中。
这里简单介绍一下cifar-10数据集:
cifar-10数据集共有60000个样本,每个样本都是一张32*32像素的RGB图像(彩色图像),也就是我们之前所说的3通道图像。这60000个样本被分成了50000个训练样本和10000个测试样本。
cifar-10中有10类物体,标签值分别按照0~9来区分,他们分别是飞机( airplane )、汽车( automobile )、鸟( bird )、猫( cat )、鹿( deer )、狗( dog )、青蛙( frog )、马( horse )、船( ship )和卡车( truck )。
cifar-10数据集的内容,如下图所示:


该数据集非常适合用来验证模型的分类效果,之后的网络模型我们也会经常用到该数据集。下面我们在cifar-10数据集上训练一下AlexNet:

# 加载cifar-10数据集
import torchvision.datasets as datasets
from torchvision.transforms.functional import InterpolationMode
net = AlexNet(num_classes=10)
transform = transforms.Compose([transforms.Resize((224, 224), interpolation=InterpolationMode.BICUBIC),
                            transforms.ToTensor(),
                            # 此为训练集上的均值与方差
                            transforms.Normalize(mean=[0.4914, 0.4822, 0.4465], std=[0.247, 0.2435, 0.2616]) 
                            ])
train_dataset_cifar = datasets.CIFAR10('./data', train=True, transform=transform, download=True)
test_dataset_cifar = datasets.CIFAR10('./data', train=False, transform=transform, download=True)
train_loader_cifar = torch.utils.data.DataLoader(dataset=train_dataset_cifar, batch_size=64, shuffle=True)
test_loader_cifar = torch.utils.data.DataLoader(dataset=test_dataset_cifar, batch_size=64, shuffle=False)
train(net, train_loader_cifar, test_loader_cifar, epochs=10)
# 输出:
Epoch 1, Loss: 1.8302
Epoch 2, Loss: 1.2409
Epoch 3, Loss: 0.9477
Epoch 4, Loss: 0.7935
Epoch 5, Loss: 0.6797
Epoch 6, Loss: 0.5851
Epoch 7, Loss: 0.5176
Epoch 8, Loss: 0.4554
Epoch 9, Loss: 0.4121
Epoch 10, Loss: 0.3587
saveing checkpoints/alexnet.pt
Test Accuracy: 81.46%

可以看到,经过10个epoch的训练,测试精度已经达到了81.46%。

预训练

这里考虑一个问题,如果数据集不是cifar-10这样规整的,并且大小不一的数据集,比如我们之前在《神经网络Pytorch实现》一文中提到的猫狗数据集。



如果用这个数据集直接训练我们的AlexNet网络,你会发现损失值几乎不会下降或者下降很慢,这是由于我们的网络模型是从头构建的,很多参数都需要重新学习。这个时候可以采用公开的预训练模型。所谓的预训练模型就是已经用数据集(如Imagenet,coco等数据集)训练好了的模型,比如AlexNet,vgg等等。这些预训练模型中已经保存了之前在大数据集上训练过之后的参数。在预训练模型上对我们自己的数据集进行训练的过程,叫作“微调”。所以,我们只要在预训练模型上进行微调,即可将模型重新在我们的数据集上进行训练,效果会比我们从头开始训练好得多。
这里,我从pytorch获取到预训练的模型,并把分类输出修改为2类,猫和狗,在此基础上再训练(微调),同时我们比较一下使用预训练模型和未预训练模型进行训练的效果:

# 猫狗分类
from dogcat import myDataset

trainDataset = myDataset(r'D:\zj\自己项目\Python机器学习实践\样本库\dogcat\zip\train',transform=transform)
train_loader = DataLoader(dataset=trainDataset, batch_size=32, shuffle=True)
valDataset = myDataset(r'D:\zj\自己项目\Python机器学习实践\样本库\dogcat\zip\val',transform=transform)
val_loader = DataLoader(dataset=valDataset, batch_size=32, shuffle=False)

net = models.alexnet(pretrained=False) # 未预训练
#net = models.alexnet(pretrained=True) # 预训练
dim_in = net.classifier[-1].in_features # 最后一个分类层的输入维度
net.classifier[-1] = nn.Linear(dim_in, 2) # 网络最后一层改为分类到两个类别
train(net4, train_loader, val_loader, epochs=10)

当我们选择未预训练的模型时,输出结果如下:

Epoch 1, Loss: 0.6933
Epoch 2, Loss: 0.6932
Epoch 3, Loss: 0.6933
Epoch 4, Loss: 0.6933
Epoch 5, Loss: 0.6929
Epoch 6, Loss: 0.6931
Epoch 7, Loss: 0.6932
Epoch 8, Loss: 0.6927
Epoch 9, Loss: 0.6932
Epoch 10, Loss: 0.6931
saveing checkpoints/alexnet.pt
Test Accuracy: 50.00%

可以看到,损失值几乎没有下降,并且测试结果相当于随机猜测,只有50%的正确率。而当我们选择预训练的模型时,输出结果如下:

Epoch 1, Loss: 0.7668
Epoch 2, Loss: 0.7152
Epoch 3, Loss: 0.6424
Epoch 4, Loss: 0.6262
Epoch 5, Loss: 0.6390
Epoch 6, Loss: 0.5899
Epoch 7, Loss: 0.5850
Epoch 8, Loss: 0.5673
Epoch 9, Loss: 0.5370
Epoch 10, Loss: 0.5071
saveing checkpoints/alexnet.pt
Test Accuracy: 74.50%

可以看到,差距是非常大的,损失值有了明显的下降,并且仅仅训练了10个epoch,测试精度达到了74.5%。我做了一下实验,当训练到100个epoch时,测试精度达到了87%,由此可知,预训练模型对于实际生产环境中的数据具有很好的效果。下一篇我们将聚焦另一个重要的卷积神经网络Vgg。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容