包含并行连结的网络(GoogLeNet)
在2014年的ImageNet图像识别挑战赛中,一个名叫GoogLeNet的网络结构大放异彩。它虽然在名字上向LeNet致敬,但在网络结构上已经很难看到LeNet的影子。GoogLeNet吸收了NiN中网络串联网络的思想,并在此基础上做了很大改进。在随后的几年里,研究人员对GoogLeNet进行了数次改进。
Inception块
GoogLeNet中的基础卷积块叫作Inception块,得名于同名电影《盗梦空间》(Inception)。
Inception块里有4条并行的线路。
- 前3条线路使用窗口大小分别是1×11\times 11×1、3×33\times 33×3和5×55\times 55×5的卷积层来抽取不同空间尺寸下的信息,其中中间2个线路会对输入先做1×11\times 11×1卷积来减少输入通道数,以降低模型复杂度。
- 第四条线路则使用3×33\times 33×3最大池化层,后接1×11\times 11×1卷积层来改变通道数。
- 4条线路都使用了合适的填充来使输入与输出的高和宽一致。
- 最后将每条线路的输出在通道维上连结,并输入接下来的层中去。
Inception块中可以自定义的超参数是每个层的输出通道数,以此来控制模型复杂度
import time
import torch
from torch import nn, optim
import torch.nn.functional as Fdevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')class Inception(nn.Module):# c1 - c4为每条线路里的层的输出通道数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模型
GoogLeNet跟VGG一样,在主体卷积部分中使用5个模块(block),每个模块之间使用步幅为2的3×33\times 33×3最大池化层来减小输出高宽。
这里我们假设输入为96*96
的图片,来推算每一个模块的输入输出尺寸
(1) 第一模块使用一个64通道的7×77\times 77×7卷积层。
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))
input=1∗96∗96卷积1:size=⌊(96+2+3∗2−7)/2⌋=48∣∣output1=64∗48∗48池化2:size=⌊(48+2+1∗2−3)/2⌋=24∣∣output2=64∗24∗24output=64∗24∗24input = 1*96*96 \\ 卷积1:size= \lfloor \left(96+2+3*2-7\right)/2 \rfloor=48||output1 = 64*48*48\\ 池化2:size= \lfloor \left(48+2+1*2-3\right)/2 \rfloor=24||output2=64*24*24\\ output = 64*24*24 input=1∗96∗96卷积1:size=⌊(96+2+3∗2−7)/2⌋=48∣∣output1=64∗48∗48池化2:size=⌊(48+2+1∗2−3)/2⌋=24∣∣output2=64∗24∗24output=64∗24∗24
(2) 第二模块使用2个卷积层:首先是64通道的1×11\times 11×1卷积层,然后是输出通道为192的3×33\times 33×3卷积层。它对应着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))
input=64∗24∗24卷积1:size=⌊(24+1+0∗2−1)/1⌋=24∣∣output1=64∗24∗24卷积2:size=⌊(24+1+1∗2−3)/1⌋=24∣∣output1=192∗24∗24池化3:size=⌊(24+2+1∗2−3)/2⌋=12∣∣output2=192∗12∗12output=192∗12∗12input = 64*24*24 \\ 卷积1:size= \lfloor \left(24+1+0*2-1\right)/1 \rfloor=24||output1 = 64*24*24\\ 卷积2:size= \lfloor \left(24+1+1*2-3\right)/1 \rfloor=24||output1 = 192*24*24\\ 池化3:size= \lfloor \left(24+2+1*2-3\right)/2 \rfloor=12||output2=192*12*12\\ output = 192*12*12 input=64∗24∗24卷积1:size=⌊(24+1+0∗2−1)/1⌋=24∣∣output1=64∗24∗24卷积2:size=⌊(24+1+1∗2−3)/1⌋=24∣∣output1=192∗24∗24池化3:size=⌊(24+2+1∗2−3)/2⌋=12∣∣output2=192∗12∗12output=192∗12∗12
(3) 第三模块串联2个完整的Inception块。
- 第一个Inception块的输出通道数为64+128+32+32=25664+128+32+32=25664+128+32+32=256,其中4条线路的输出通道数比例为64:128:32:32=2:4:1:164:128:32:32=2:4:1:164:128:32:32=2:4:1:1。其中第二、第三条线路先分别将输入通道数减小至96/192=1/296/192=1/296/192=1/2和16/192=1/1216/192=1/1216/192=1/12后,再接上第二层卷积层。
- 第二个Inception块输出通道数增至128+192+96+64=480128+192+96+64=480128+192+96+64=480,每条线路的输出通道数之比为128:192:96:64=4:6:3:2128:192:96:64 = 4:6:3:2128:192:96:64=4:6:3:2。其中第二、第三条线路先分别将输入通道数减小至128/256=1/2128/256=1/2128/256=1/2和32/256=1/832/256=1/832/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))
(4) 第四模块更加复杂。
- 它串联了5个Inception块,其输出通道数分别是192+208+48+64=512192+208+48+64=512192+208+48+64=512、160+224+64+64=512160+224+64+64=512160+224+64+64=512、128+256+64+64=512128+256+64+64=512128+256+64+64=512、112+288+64+64=528112+288+64+64=528112+288+64+64=528和256+320+128+128=832256+320+128+128=832256+320+128+128=832。
- 这些线路的通道数分配和第三模块中的类似,首先含3×33\times 33×3卷积层的第二条线路输出最多通道,其次是仅含1×11\times 11×1卷积层的第一条线路,之后是含5×55\times 55×5卷积层的第三条线路和含3×33\times 33×3最大池化层的第四条线路。其中第二、第三条线路都会先按比例减小通道数。这些比例在各个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))
(5) 第五模块有输出通道数为256+320+128+128=832256+320+128+128=832256+320+128+128=832和384+384+128+128=1024384+384+128+128=1024384+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))
GoogLeNet模型的计算复杂,而且不如VGG那样便于修改通道数。本节里我们将输入的高和宽从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)
输出:
output shape: torch.Size([1, 64, 24, 24])
output shape: torch.Size([1, 192, 12, 12])
output shape: torch.Size([1, 480, 6, 6])
output shape: torch.Size([1, 832, 3, 3])
output shape: torch.Size([1, 1024, 1, 1])
output shape: torch.Size([1, 1024])
output shape: torch.Size([1, 10])
获取数据训练模型
def load_data_fashion_mnist(batch_size, resize=None, root='~/Datasets/FashionMNIST'):"""Download the fashion mnist dataset and then load into memory."""trans = []if resize:trans.append(torchvision.transforms.Resize(size=resize))trans.append(torchvision.transforms.ToTensor())transform = torchvision.transforms.Compose(trans)mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True, download=True, transform=transform)mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False, download=True, transform=transform)if sys.platform.startswith('win'):num_workers = 0 # 0表示不用额外的进程来加速读取数据else:num_workers = 4train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)return train_iter, test_iterdef train(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs):net = net.to(device)print("training on ", device)loss = torch.nn.CrossEntropyLoss()for epoch in range(num_epochs):train_l_sum, train_acc_sum, n, batch_count, start = 0.0, 0.0, 0, 0, time.time()for X, y in train_iter:X = X.to(device)y = 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)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))
batch_size = 128
# 如出现“out of memory”的报错信息,可减小batch_size或resize
train_iter, test_iter = load_data_fashion_mnist(batch_size, resize=96)lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
train(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)