在2014年的ImageNet图像识别挑战赛中,一个名叫GoogLeNet的网络结构大放异彩。它虽然在名字上向LeNet致敬,但在网络结构上已经很难看到LeNet的影子。GoogLeNet吸收了NiN中网络串联网络的思想,并在此基础上做了很大改进。在随后的几年里,研究人员对GoogLeNet进行了数次改进,本节将介绍这个模型系列的第一个版本。
Inception块中可以自定义的超参数是每个层的输出通道,我们以此来控制模型的复杂度
import time
import torch
from torch import nn, optim
import torch.nn.functional as F
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
device = torch.device("cuda" if torch.cuda.is_available() else 'cpu')
class Inception(nn.Module):
def __init__(self, in_c, c1, c2, c3, c4):
super(Inception, self).__init__()
# 路线1: 单 1 x 1 卷积层
self.p1_1 = nn.Conv2d(in_c, c1, kernel_size =1)
# 路线2: 1 x 1 卷积层后接3 x 3卷积层
self.p2_1 = nn.Conv2d(in_c, c2[0], kernel_size = 1)
self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size = 3, padding = 1)
# 路线3: 1 x 1卷积层后接5 x 5卷积层
self.p3_1 = nn.Conv2d(in_c, c3[0], kernel_size = 1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size = 5, padding = 2)
# 路线4: 3 x 3 最大池化层后接1 x 1卷积层
self.p4_1 = nn.MaxPool2d(kernel_size = 3, stride = 1, padding = 1)
self.p4_2 = nn.Conv2d(in_c, 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跟VGG一样,在主体卷积部分中使用5个模块(block),每个模块之间使用步幅为2的3x3最大池化层来减少输出宽高。第一模块使用一个64通道的7x7卷积层。
b1 = nn.Sequential(
nn.Conv2d(1, 64, kernel_size =7, stride =2, padding =3),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 3, stride = 2, padding = 1)
)
第二模块使用2个卷积层: 首先通过64通道的 1x1 卷积层,然后是将通道增大3倍的 3x3 卷积层。它对应Inception块中的第二条线路。
b2 = nn.Sequential(
nn.Conv2d(64, 64, kernel_size = 1),
nn.Conv2d(64, 192, kernel_size = 3, padding =1),
nn.MaxPool2d(kernel_size = 3, stride = 2, padding = 1)
)
第三模块串联2个完整的Inception块。第一个Inception块的输出通道数为 64 + 128 + 32 + 32 = 256,其中4条路线的输出通道数比例为 64: 128 : 32: 32 = 2: 4: 1: 1。其中第二、第三条路线先分别将输入通道数减少至96/192 = 1/2 和 16/192 = 1/ 12后,再接上第二层卷积层。第二个Inception块输出通道数增至 128 +192 +96 + 64 = 480,每条线路的输出通道数之比为 128: 192: 96: 64 = 4: 6 : 3 : 2。其中第二、第三条路线先分别将输入通道数减少至 128/ 256 = 1/2 和 32/ 256 = 1/8。
b3 = nn.Sequential(
Inception(192, 64, (96, 128), (16, 32), 32),
Inception(256, 128, (128, 192),(32, 96), 64),
nn.MaxPool2d(kernel_size = 3, stride =2, padding =1)
)
第四模块更加复杂。它串联了5个Inception块,其输出通道数分别是192 + 208 + 48 + 64 = 512、160 + 224 + 64 + 64 = 512、128 + 256 + 64 + 64 = 512、 112 + 288 + 64 + 64 = 528 和 256 + 320 + 128 + 128 = 832。这些线路的通道数分配和第三模块中类似,首先含3x3卷积层的第二条线路输出最多通道,其次是仅含1x1卷积层的第一条线路,之后是含5x5卷积层的第3条线路和含3x3最大池化层的第4条线路。其中第二、第三条线路都会按比例减小通道数。这些比例在各个Inception块中都略有不同。
b4 = 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)
)
第五模块有输出通道数为 256 + 320 + 128 + 128 = 832 和 834 + 384 + 128 + 128 = 1024的两个Inception块。其中每条线路的通道数的分配思路和第三、第四模块中的一致,只是在具体数值上有所不同。需要注意的是,第五模块的后面紧跟输出层,该模块同NiN一样使用全局平均池化层来将每个通道的宽和高变成1。最后我们将输出变成二维数组后接上一个输出个数为标签类别数的全连接层。
b5 = nn.Sequential(
Inception(832, 256, (160, 320), (32, 128), 128),
Inception(832, 384, (192, 384), (48, 128), 128),
d2l.GlobalAvgPool2d()
)
net = nn.Sequential(b1, b2, b3, b4, b5, d2l.FlattenLayer(), nn.Linear(1024, 10))
print(net)
将输入的高和宽从224降到96来简化计算,下面演示各个模块之间的输出的形状变化
net = nn.Sequential(b1, b2, b3, b4, b5, d2l.FlattenLayer(), nn.Linear(1024, 10))
X = torch.rand(1, 1, 96, 96)
for blk in net.children():
X = blk(X)
print("output shape: ", X.shape)
batch_size = 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr = lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)