一、GoogLeNet V1(第一个版本)
一般来说,提升网络性能最直接的办法就是增加网络深度和宽度,这也就意味着更巨量的参数。但是,巨量参数容易产生过拟合,也会大大增加计算量。解决上述两个缺点的根本方法是将全连接甚至一般的卷积都转化为稀疏连接。认为对于大规模稀疏的神经网络,可以通过分析激活值的统计特性和对高度相关的输出进行聚类来逐层构建出一个最优网络。所以,为了既能保持网络结构的稀疏性,又能利用密集矩阵的高计算性能,论文提出了Inception的结构。
1、Inception结构
Inception 结构的主要思路是用密集成分来近似最优的局部稀疏结构。作者首先提出下图这样的基本结构:
对于上图的解释为:
1.采用不同大小的卷积核意味着不同大小的感受野,最后拼接意味着不同尺度特征的融合;
2.之所以卷积核大小采用1、3和5,主要是为了方便对齐。设定卷积步长stride=1之后,只要分别设定pad=0、1、2,那么卷积之后便可以得到相同维度的特征,然后这些特征就可以直接拼接在一起了;
3.文章说很多地方都表明pooling挺有效,所以Inception里面也嵌入了。
4.网络越到后面,特征越抽象,而且每个特征所涉及的感受野也更大了,因此随着层数的增加,3x3和5x5卷积的比例也要增加。
但是,使用5x5卷积核仍然会带来巨大的计算量。
关于1x1卷积核来进行降维,举个例子:
假如上一层的输出为100x100x128,经过具有256个输出的5x5卷积层之后(stride=1,pad=2),输出数据为100x100x256。其中,卷积层的参数为128x5x5x256。此时如果上一层输出先经过具有32个输出的1x1卷积层,再经过具有256个输出的5x5卷积层,那么最终的输出数据仍为为100x100x256,但卷积参数量已经减少为128x1x1x32 + 32x5x5x256,大约减少了4倍。
改进后(降维)的Inception Module如下图:
2、GoogLeNet网络结构
1.显然GoogLeNet采用了模块化的结构,方便增添和修改。
2.网络在FC之前采用了average pooling层,想法来自NIN,事实证明可以将accuracy提高0.6%。
3.可以看出,GoogLeNet网络其实也是非常深的,如果梯度从最后一层传递到第一层,梯度基本已经没有了。为了避免梯度消失,网络额外增加了2个辅助的softmax用于向前传导梯度。文章中说这两个辅助的分类器的loss应该加一个衰减系数,但看caffe中的model也没有加任何衰减。此外,实际预测时,这两个额外的softmax会被去掉。其实这种训练方式可以看作将几个不同深度的子网络合并到一块进行训练,由于网络的卷积核共享,因此计算的梯度可以累加,这样最终的梯度便不会很小。关于这一点做的最彻底的应该就是ResNet。
GoogLeNet的网络结构图细节如下:
注:上表中的“#3x3 reduce”,“#5x5 reduce”表示在3x3,5x5卷积操作之前使用了1x1卷积的数量。
GoogLeNet网络结构明细表解析如下:
0、输入
原始输入图像为224x224x3,且都进行了零均值化的预处理操作(图像每个像素减去均值)。
1、第一层(卷积层)
使用7x7的卷积核(滑动步长2,padding为3),64通道,输出为112x112x64,卷积后进行ReLU操作
经过3x3的max pooling(步长为2),输出为((112 - 3+1)/2)+1=56,即56x56x64,再进行ReLU操作
2、第二层(卷积层)
使用3x3的卷积核(滑动步长为1,padding为1),192通道,输出为56x56x192,卷积后进行ReLU操作
经过3x3的max pooling(步长为2),输出为((56 - 3+1)/2)+1=28,即28x28x192,再进行ReLU操作
3a、第三层(Inception 3a层)
分为四个分支,采用不同尺度的卷积核来进行处理
(1)64个1x1的卷积核,然后RuLU,输出28x28x64
(2)96个1x1的卷积核,作为3x3卷积核之前的降维,变成28x28x96,然后进行ReLU计算,再进行128个3x3的卷积(padding为1),输出28x28x128
(3)16个1x1的卷积核,作为5x5卷积核之前的降维,变成28x28x16,进行ReLU计算后,再进行32个5x5的卷积(padding为2),输出28x28x32
(4)pool层,使用3x3的核(padding为1),输出28x28x192,然后进行32个1x1的卷积,输出28x28x32。
将四个结果进行连接,对这四部分输出结果的第三维并联,即64+128+32+32=256,最终输出28x28x256
3b、第三层(Inception 3b层)
(1)128个1x1的卷积核,然后RuLU,输出28x28x128
(2)128个1x1的卷积核,作为3x3卷积核之前的降维,变成28x28x128,进行ReLU,再进行192个3x3的卷积(padding为1),输出28x28x192
(3)32个1x1的卷积核,作为5x5卷积核之前的降维,变成28x28x32,进行ReLU计算后,再进行96个5x5的卷积(padding为2),输出28x28x96
(4)pool层,使用3x3的核(padding为1),输出28x28x256,然后进行64个1x1的卷积,输出28x28x64。
将四个结果进行连接,对这四部分输出结果的第三维并联,即128+192+96+64=480,最终输出输出为28x28x480
第四层(4a,4b,4c,4d,4e)、第五层(5a,5b)……,与3a、3b类似。
另外,为了达到最佳的性能,除了使用上述的网络结构外,还做了大量的辅助工作:包括训练多个model求平均、裁剪不同尺度的图像做多次验证等等。
import tensorflow as tf
import tensorflow.keras as keras
import matplotlib.pyplot as plt
import numpy as np
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
def normalize(x_train, x_test):
x_train = x_train / 255.
x_test = x_test / 255.
mean = np.mean(x_train, axis=(0, 1, 2, 3))
std = np.std(x_train, axis=(0, 1, 2, 3))
print('mean:', mean, 'std', std)
x_train = (x_train - mean) / (std + 1e-7)
x_test = (x_test - mean) / (std + 1e-7)
return x_train, x_test
def preprocess(x, y):
x = tf.cast(x, tf.float32)
y = tf.cast(y, tf.int32)
y = tf.squeeze(y, axis=1)
y = tf.one_hot(y, depth=10)
return x, y
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()
x_train, x_test = normalize(x_train, x_test)
train_db = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_db = train_db.shuffle(50000).batch(128).map(preprocess)
test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_db = test_db.shuffle(10000).batch(128).map(preprocess)
class ConvBNRelu(keras.Model):
def __init__(self, filters, kernelsize=3, strides=1, padding='same'):
super().__init__()
self.medel = keras.models.Sequential([
keras.layers.Conv2D(filters=filters,
kernel_size=kernelsize,
strides=strides,
padding=padding),
keras.layers.BatchNormalization(),
keras.layers.ReLU()
])
def call(self, x, training=None):
x = self.model(x, training=training)
return x
class Inception_v2:
def __init__(self, filters, strides=1):
# strides:控制是否缩减特征图,=1不缩减, =2缩减
super().__init__()
self.conv1_1 = ConvBNRelu(filters, kernelsize=1, strides=1)
self.conv1_2 = ConvBNRelu(filters, kernelsize=3, strides=1)
self.conv1_3 = ConvBNRelu(filters, kernelsize=3, strides=strides)
self.conv2_1 = ConvBNRelu(filters, kernelsize=1, strides=1)
self.conv2_2 = ConvBNRelu(filters, kernelsize=3, strides=strides)
self.pool = keras.layers.MaxPooling2D(pool_size=3,
strides=strides,
padding='same')
def call(self, x, training=None):
x1_1 = self.conv1_1(x, training=training)
x1_2 = self.conv1_2(x1_1, training=training)
x1_3 = self.conv1_3(x1_2, training=training)
x2_1 = self.conv2_1(x, training=training)
x2_2 = self.conv2_2(x2_1, training=training)
x3 = self.pool(x)
x = tf.concat([x1_3, x2_2, x3], axis=3)
return x
class GoogleNet(keras.Model):
def __init__(self, num_blocks, num_classes, filters=16):
super().__init__()
self.filters = filters
self.conv1 = ConvBNRelu(filters) # 第一个卷积层
self.blocks = keras.models.Sequential() # 购置动态数量的
for block_id in range(num_blocks):
for Inception_id in range(2): # 每个block里 2个Inception
if Inception_id == 0:
block = Inception_v2(self.filters, strides=2) # 第一个缩放
else:
block = Inception_v2(self.filters, strides=1) # 其它不缩放
self.blocks.add(block)
self.filters *= 2 # 下一层block中卷积数量比上一层多一倍
self.avg_pool = keras.layers.GlobalAveragePooling2D()
self.fc = keras.layers.Dense(num_classes, activation='softmax')
def call(self, x, training=None):
out = self.conv1(x, training=training)
out = self.blocks(out, training=training)
out = self.avg_pool(out)
out = self.fc(out)
return out
model = GoogleNet(2, 10)
model.build(input_shape=(None, 32, 32, 3))
model.compile(optimizer=keras.optimizers.Adam(0.0001),
loss=keras.losses.CategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
history = model.fit(train_db, epochs=50)
plt.plot(history.history['loss'])
plt.title("model loss")
plt.ylabel("loss")
plt.xlabel("epoch")
plt.show()
model.evaluate(test_db)
最后准确率在0.64左右,可以提高训练次数得到更好的结果