2014年的ImageNet图像识别挑战赛中,GoogLeNet的网络结构大放异彩。
GoogLeNet吸收了NiN中网络串联网络的思想,并在此基础上做了很大的改进。
在随后的几年里,研究人员对GoogLeNet进行了数次改进,本节将介绍这个模型系列的第一个版本。
GoogLeNet中的基础卷积块称作Inception块。
与上一节介绍的NiN块相比,这个基础块在结构上更加复杂,如下图所示:
由此可见,Inception块里有4条并行的线路。
前3条线路使用窗口大小分别是1×1、3×3和5×5的卷积层来抽取不同空间尺寸下的信息。
其中,中间2条线路会对输入先做1×1卷积来减少输入通道数,以降低模型复杂度。
第四条线路使用3×3最大池化层,后接1×1卷积层来改变通道数。
4条线路都使用了合适的填充来使输入与输出的高和宽一致。
最后将每条线路的输出在通道维上连结,并输入接下来的层中。
Inception块中可以自定义的超参数是每个层的输出通道数,以此来控制模型复杂度。
代码实现如下:
import tensorflow as tf
import numpy as np
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Layer, Conv2D, MaxPool2D, Dense, GlobalAvgPool2D
from tensorflow.keras.optimizers import SGD
for gpu in tf.config.experimental.list_physical_devices('GPU'):
tf.config.experimental.set_memory_growth(gpu, True)
class Inception(Layer):
def __init__(self,c1, c2, c3, c4):
super().__init__()
# 线路1,单1 x 1卷积层
self.p1_1 = Conv2D(c1, kernel_size=1, padding='same', activation='relu')
# 线路2,1 x 1卷积层后接3 x 3卷积层
self.p2_1 = Conv2D(c2[0], kernel_size=1, padding='same', activation='relu')
self.p2_2 = Conv2D(c2[1], kernel_size=3, padding='same', activation='relu')
# 线路3,1 x 1卷积层后接5 x 5卷积层
self.p3_1 = Conv2D(c3[0], kernel_size=1, padding='same', activation='relu')
self.p3_2 = Conv2D(c3[1], kernel_size=5, padding='same', activation='relu')
# 线路4,3 x 3最大池化层后接1 x 1卷积层
self.p4_1 = MaxPool2D(pool_size=3, strides=1, padding='same')
self.p4_2 = Conv2D(c4, kernel_size=1, padding='same', activation='relu')
def call(self, x):
p1 = self.p1_1(x)
p2 = self.p2_2(self.p2_1(x))
p3 = self.p3_2(self.p3_1(x))
p4 = self.p4_2(self.p4_1(x))
return tf.concat([p1, p2, p3, p4], axis=-1) # 在通道维上连结输出
Inception(64, (96, 128), (16, 32), 32)
<__main__.Inception at 0x7ff5c142e9d0>
同VGG,GoogLeNet在主体卷积部分中使用5个模块(block),每个模块之间使用步幅为2的3×3最大池化层来减小输出高宽。
其中,第一模块使用一个64通道的7×7卷积层。
b1 = Sequential()
b1.add(Conv2D(64, kernel_size=7, strides=2, padding='same', activation='relu'))
b1.add(MaxPool2D(pool_size=3, strides=2, padding='same'))
第二模块使用2个卷积层:
先是64通道的1×1卷积层,
再是将通道增大3倍的3×3卷积层。
b2 = Sequential()
b2.add(Conv2D(64, kernel_size=1, padding='same', activation='relu'))
b2.add(Conv2D(192, kernel_size=3, padding='same', activation='relu'))
b2.add(MaxPool2D(pool_size=3, strides=2, padding='same'))
第三模块串联了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 = Sequential()
b3.add(Inception(64, (96, 128), (16, 32), 32))
b3.add(Inception(128, (128, 192), (32, 96), 64))
b3.add(MaxPool2D(pool_size=3, strides=2, padding='same'))
第四模块串联了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。
这些线路的通道数分配和第三模块类似:
首先含3×3卷积层的第二条线路输出最多通道,
其次是仅含1×1卷积层的第一条线路,
之后是含5×5卷积层的第三条线路和含3×3最大池化层的第四条线路。
其中,第二、第三条线路都会先按比例减小通道数,这些比例在各个Inception块中都略有不同。
第五模块串联了2个Inception块,其输出通道数分别为:
256+320+128+128=832 和 384+384+128+128=1024。
其中,每条线路的通道数的分配思路和第三、第四模块中的一致,只是在具体数值上有所不同。
需要注意的是,第五模块的后面紧跟输出层。
该模块同NiN一样,使用全局平均池化层来将每个通道的高和宽变成1。
b5 = Sequential()
b5.add(Inception(256, (160, 320), (32, 128), 128))
b5.add(Inception(384, (192, 384), (48, 128), 128))
b5.add(GlobalAvgPool2D())
最后将输出变成二维数组后接上一个输出个数为标签类别数的全连接层。
net = Sequential([b1, b2, b3, b4, b5, Dense(10)])
GoogLeNet模型的计算复杂,而且不如VGG那样便于修改通道数。
本节将输入的高和宽从224降到96来简化计算。
各模块间输出的形状变化演示如下:
X = tf.random.uniform(shape=(1, 96, 96, 1))
for layer in net.layers:
X = layer(X)
print(layer.name, ' shape: ', X.shape)
sequential shape: (1, 24, 24, 64)
sequential_1 shape: (1, 12, 12, 192)
sequential_3 shape: (1, 6, 6, 480)
sequential_4 shape: (1, 3, 3, 832)
sequential_8 shape: (1, 1024)
dense_1 shape: (1, 10)
使用高和宽均为96像素的图像来训练GoogLeNet模型。
训练使用的图像依然来自Fashion-MNIST数据集。
同(四)卷积神经网络 – 6 AlexNet 小节:
class DataLoader():
def __init__(self):
# fashion_mnist = tf.keras.datasets.fashion_mnist
# (self.train_images, self.train_labels), (self.test_images, self.test_labels) = fashion_mnist.load_data()
# load data from local
with open("../input/fashionmnist/train-labels-idx1-ubyte", 'rb') as f:
self.train_labels = np.frombuffer(f.read(), np.uint8, offset=8)
with open("../input/fashionmnist/train-images-idx3-ubyte", 'rb') as f:
self.train_images = np.frombuffer(f.read(), np.uint8, offset=16).reshape(len(self.train_labels), 28, 28)
with open("../input/fashionmnist/t10k-labels-idx1-ubyte", 'rb') as f:
self.test_labels = np.frombuffer(f.read(), np.uint8, offset=8)
with open("../input/fashionmnist/t10k-images-idx3-ubyte", 'rb') as f:
self.test_images = np.frombuffer(f.read(), np.uint8, offset=16).reshape(len(self.test_labels), 28, 28)
# np.expand_dims(images, axis=-1) -- convert (10000, 28, 28) into (10000, 28, 28, 1)
self.train_images = np.expand_dims(self.train_images.astype(np.float32)/255.0,axis=-1)
self.test_images = np.expand_dims(self.test_images.astype(np.float32)/255.0,axis=-1)
self.train_labels = self.train_labels.astype(np.int32)
self.test_labels = self.test_labels.astype(np.int32)
self.num_train, self.num_test = self.train_images.shape[0], self.test_images.shape[0]
def get_batch_train(self, batch_size):
"""
Examples
--------
>>> np.random.randint(0, 10, size=2)
array([5, 7])
"""
index = np.random.randint(0, np.shape(self.train_images)[0], batch_size)
resized_images = tf.image.resize_with_pad(self.train_images[index],224,224)
return resized_images.numpy(), self.train_labels[index]
def get_batch_test(self, batch_size):
index = np.random.randint(0, np.shape(self.test_images)[0], batch_size)
resized_images = tf.image.resize_with_pad(self.test_images[index],224,224)
return resized_images.numpy(), self.test_labels[index]
batch_size = 128
dataLoader = DataLoader()
x_batch, y_batch = dataLoader.get_batch_train(batch_size)
print("x_batch shape:",x_batch.shape,"y_batch shape:", y_batch.shape)
x_batch shape: (128, 224, 224, 1) y_batch shape: (128,)
def train_googlenet():
epoch = 5
num_iter = dataLoader.num_train//batch_size
for e in range(epoch):
for n in range(num_iter):
x_batch, y_batch = dataLoader.get_batch_train(batch_size)
net.fit(x_batch, y_batch)
if n%20 == 0:
net.save_weights("5.9_googlenet_weights.h5")
optimizer = SGD(learning_rate=0.05, momentum=0.0, nesterov=False)
net.compile(optimizer=optimizer,
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
x_batch, y_batch = dataLoader.get_batch_train(batch_size)
net.fit(x_batch, y_batch)
train_googlenet()
net.load_weights("5.9_googlenet_weights.h5")
x_test, y_test = dataLoader.get_batch_test(2000)
net.evaluate(x_test, y_test, verbose=2)
《动手学深度学习》(TF2.0版)
A. Krizhevsky, I. Sutskever, and G. Hinton. Imagenet classification with deep convolutional neural networks. In NIPS, 2012.