专栏:神经网络复现目录
本章介绍的是现代神经网络的结构和复现,包括深度卷积神经网络(AlexNet),VGG,NiN,GoogleNet,残差网络(ResNet),稠密连接网络(DenseNet)。
文章部分文字和代码来自《动手学深度学习》
GoogleNet,也叫Inception v1,是Google在2014年提出的深度卷积神经网络,它的主要特点是使用了Inception模块来替代原来的单一卷积层,以更好地提取特征。GoogleNet是在ImageNet数据集上训练出来的,它在当年的ImageNet比赛中获得了最好的结果。
GoogleNet的整体架构相对于之前的模型更为复杂,具有22层。其中,它的最大特点是使用了Inception模块,使得模型的参数量比之前的模型更小,但是精度更高。
Inception模块主要是将不同卷积核大小的卷积层和最大池化层进行组合,以获得不同大小的感受野,从而更好地提取特征。同时,Inception模块使用了1x1的卷积层进行通道数的调整,进一步减少了参数量。
除了Inception模块之外,GoogleNet还采用了全局平均池化层来代替全连接层,这也是后续很多模型都采用的做法,可以减少过拟合,同时减少参数量。
Inception模块是GoogleNet中的一个核心组成部分,用于提取图像特征。该模块采用并行的多个卷积层和池化层来提取不同尺度的特征,然后将它们在通道维度上进行拼接,得到更丰富的特征表达。
一个基本的Inception模块包含了四个分支(Branch),每个分支都有不同的卷积核或者池化核,如下图所示:
在这个模块中,我们可以看到分支1、2、3都是卷积层,分支4则是最大池化层,其目的是提取图像中不同大小的特征。通过这些分支的组合,Inception模块可以有效地提取图像的多尺度特征,且计算代价相对较小。
具体来说,假设输入的特征图的大小是h×w×ch\times w\times ch×w×c,Inception模块将其输入到四个分支中,这四个分支分别是:
分支1:1×1卷积层,用于对通道数进行降维,可以看做是对输入的特征进行了线性变换,从而提取空间信息;
分支2:1×1卷积层后接3×3卷积层,先通过1×1卷积层将通道数降维,然后再通过3×3卷积层提取特征。在这个分支中,1×1卷积层可以用来降低计算量,3×3卷积层用于提取空间信息,组合起来可以在计算效率和特征表达能力之间取得平衡;
分支3:1×1卷积层后接5×5卷积层,和分支2类似,该分支也是先通过1×1卷积层将通道数降维,然后再通过5×5卷积层提取特征。与3×3卷积核相比,5×5卷积核可以捕捉更大的空间范围,从而更好地提取空间信息;
分支4:3×3最大池化层后接1×1卷积层,通过池化层可以提取图像中不同大小的特征,然后使用1×1卷积层进行通道数降维。
最后,将四个分支的输出在通道维度上进行拼接,得到的结果就是Inception模块的输出。
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2lclass Inception(nn.Module):# c1--c4是每条路径的输出通道数def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):super(Inception, self).__init__(**kwargs)# 线路1,单1x1卷积层self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)# 线路2,1x1卷积层后接3x3卷积层self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)# 线路3,1x1卷积层后接5x5卷积层self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)# 线路4,3x3最大汇聚层后接1x1卷积层self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)def forward(self, x):p1 = F.relu(self.p1_1(x))p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))p4 = F.relu(self.p4_2(self.p4_1(x)))# 在通道维度上连结输出return torch.cat((p1, p2, p3, p4), dim=1)
GoogLeNet一共使用9个Inception块和全局平均汇聚层的堆叠来生成其估计值。Inception块之间的最大汇聚层可降低维度。 第一个模块类似于AlexNet和LeNet,Inception块的组合从VGG继承,全局平均汇聚层避免了在最后使用全连接层。
class GoogLeNet(nn.Module):def __init__(self, in_channels=3, num_classes=1000):super(GoogLeNet, self).__init__()# 第一阶段self.stage1 = nn.Sequential(nn.Conv2d(in_channels, 64, kernel_size=7, stride=2, padding=3),nn.ReLU(inplace=True),nn.MaxPool2d(kernel_size=3, stride=2, padding=1))# 第二阶段self.stage2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),nn.ReLU(inplace=True),nn.Conv2d(64, 192, kernel_size=3, padding=1),nn.ReLU(inplace=True),nn.MaxPool2d(kernel_size=3, stride=2, padding=1))# 第三阶段self.stage3 = nn.Sequential(Inception(in_channels=192, c1=64, c2=(96, 128), c3=(16, 32), c4=32),#64+128+32+32=256Inception(256, 128, (128, 192), (32, 96), 64),#128+192+96+64=480nn.MaxPool2d(kernel_size=3, stride=2, padding=1))# 第四阶段self.stage4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64),Inception(512, 160, (112, 224), (24, 64), 64),Inception(512, 128, (128, 256), (24, 64), 64),Inception(512, 112, (144, 288), (32, 64), 64),Inception(528, 256, (160, 320), (32, 128), 128),nn.MaxPool2d(kernel_size=3, stride=2, padding=1))# 第五阶段self.stage5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),Inception(832, 384, (192, 384), (48, 128), 128),nn.AdaptiveAvgPool2d((1, 1)),nn.Flatten())# 全连接层self.fc = nn.Linear(1024, num_classes)def forward(self, x):x = self.stage1(x)x = self.stage2(x)x = self.stage3(x)x = self.stage4(x)x = self.stage5(x)x = self.fc(x)return x
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
torch.manual_seed(1234)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)
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))
])transform_test = transforms.Compose([transforms.Resize(256),transforms.CenterCrop(224),transforms.ToTensor(),transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,download=True, transform=transform_train)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=32,shuffle=True, num_workers=2)testset = torchvision.datasets.CIFAR10(root='./data', train=False,download=True, transform=transform_test)
testloader = torch.utils.data.DataLoader(testset, batch_size=32,shuffle=False, num_workers=2)
net = GoogLeNet(num_classes=10).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
import time
def evaluate_accuracy(data_iter, net, device):net.eval() # 评估模式acc_sum, n = 0.0, 0with torch.no_grad():for X, y in data_iter:X, y = X.to(device), y.to(device)acc_sum += (net(X).argmax(dim=1) == y).float().sum().cpu().item()n += y.shape[0]net.train() # 改回训练模式return acc_sum / ndef train(net, train_iter, test_iter, loss, optimizer, device, epochs):net = net.to(device)print("training on ", device)batch_count = 0for epoch in range(epochs):train_l_sum, train_acc_sum, n, start = 0.0, 0.0, 0, time.time()for X, y in train_iter:X, y = X.to(device), y.to(device)y_hat = net(X)l = loss(y_hat, y)optimizer.zero_grad()l.backward()optimizer.step()train_l_sum += l.cpu().item()train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().item()n += y.shape[0]batch_count += 1test_acc = evaluate_accuracy(test_iter, net, device)print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, time %.1f sec' % (epoch + 1, train_l_sum / batch_count, train_acc_sum / n, test_acc, time.time() - start))
train(net,trainloader,testloader,criterion,optimizer,device,10)