2014年,GoogLeNet获得了第一名、VGG获得了第二名
(1)参数太多,如果训练数据集有限,很容易产生过拟合;--比如VGG
(2)网络越大、参数越多,计算复杂度越大,难以应用;
(3)网络越深,容易出现梯度弥散问题(梯度越往后穿越容易消失),难以优化模型。
为了减少参数,自然就想到将全连接变成稀疏连接,但是在实现上,全连接变成稀疏连接后实际计算量并不会有质的提升,,因为大部分硬件是针对密集矩阵计算优化的,稀疏矩阵虽然数据量少,但是计算所消耗的时间却很难减少。
【问题来了】什么是Inception呢?
一、Inception V1 原始结构
该结构将CNN中常用的卷积(1x1,3x3,5x5)、池化操作(3x3)堆叠在一起(卷积、池化后的尺寸相同,将通道相加),一方面增加了网络的宽度,另一方面也增加了网络对尺度的适应性。
网络卷积层中的网络能够提取输入的每一个细节信息,同时5x5的滤波器也能够覆盖大部分接受层的的输入。还可以进行一个池化操作,以减少空间大小,降低过度拟合。在这些层之上,在每一个卷积层后都要做一个ReLU操作,以增加网络的非线性特征。然而这个Inception原始版本,所有的卷积核都在上一层的所有输出上来做,而那个5x5的卷积核所需的计算量就太大了,造成了特征图的厚度很大,为了避免这种情况,在3x3前、5x5前、max pooling后分别加上了1x1的卷积核,以起到了降低特征图厚度的作用,这也就形成了Inception v1的网络结构,如下图所示:
1x1的卷积核有什么用呢?
1x1卷积的主要目的是为了减少维度,还用于修正线性激活(ReLU)。比如,上一层的输出为100x100x128,经过具有256个通道的5x5卷积层之后(stride=1,pad=2),输出数据为100x100x256,其中,卷积层的参数为128x5x5x256= 819200。而假如上一层输出先经过具有32个通道的1x1卷积层,再经过具有256个输出的5x5卷积层,那么输出数据仍为为100x100x256,但卷积参数量已经减少为128x1x1x32 + 32x5x5x256= 204800,大约减少了4倍。
基于Inception构建了GoogLeNet的网络结构如下(共22层):
对上图说明如下:
(1)GoogLeNet采用了模块化的结构(Inception结构),方便增添和修改;
(2)网络最后采用了average pooling(平均池化)来代替全连接层,该想法来自NIN(Network in Network),事实证明这样可以将准确率提高0.6%。但是,实际在最后还是加了一个全连接层,主要是为了方便对输出进行灵活调整;
(3)虽然移除了全连接,但是网络中依然使用了Dropout ;
(4)为了避免梯度消失,网络额外增加了2个辅助的softmax用于向前传导梯度(辅助分类器)。辅助分类器是将中间某一层的输出用作分类,并按一个较小的权重(0.3)加到最终分类结果中,这样相当于做了模型融合,同时给网络增加了反向传播的梯度信号,也提供了额外的正则化,对于整个网络的训练很有裨益。而在实际测试的时候,这两个额外的softmax会被去掉。
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类似,在此就不再重复。
从GoogLeNet的实验结果来看,效果很明显,差错率比MSRA、VGG等模型都要低,对比结果如下表所示:# -- encoding:utf-8 --
"""
Create on 19/6/26 20:12
"""
import tensorflow as tf
class GoogleNet(object):
def __init__(self):
with tf.variable_scope("net", initializer=tf.random_normal_initializer(0.0, 0.001)):
with tf.variable_scope("Input"):
input_x = tf.placeholder(dtype=tf.float32, shape=[None, 28, 28, 1])
# 由于GoogleNet网络要求输入大小为224 * 224 * 3的图像,所以做一个转换。 channel不做转换了。
net = tf.image.resize_images(images=input_x, size=(224, 224))
net = self.__conv2d('conv1', net, 64, 7, 2)
net = self.__max_pool('pool2', net, 3, 2)
net = self.__conv2d('conv3', net, 192, 3, 1)
net = self.__conv2d('conv4', net, 192, 3, 1)
net = self.__max_pool('pool5', net, 3, 2)
net = self.__inception('inception6', net, 64, 96, 128, 16, 32, 32)
net = self.__inception('inception7', net, 64, 96, 128, 16, 32, 32)
net = self.__inception('inception8', net, 128, 128, 192, 32, 96, 64)
net = self.__inception('inception9', net, 128, 128, 192, 32, 96, 64)
net = self.__max_pool('pool10', net, 3, 2)
net = self.__inception('inception11', net, 192, 96, 208, 16, 48, 64)
net = self.__inception('inception12', net, 192, 96, 208, 16, 48, 64)
net = self.__inception('inception13', net, 160, 112, 224, 24, 64, 64)
net = self.__inception('inception14', net, 160, 112, 224, 24, 64, 64)
net = self.__inception('inception15', net, 128, 128, 256, 24, 64, 64)
net = self.__inception('inception16', net, 128, 128, 256, 24, 64, 64)
net = self.__inception('inception17', net, 112, 144, 288, 32, 64, 64)
net = self.__inception('inception18', net, 112, 144, 288, 32, 64, 64)
net = self.__inception('inception19', net, 256, 160, 320, 32, 128, 128)
net = self.__inception('inception20', net, 256, 160, 320, 32, 128, 128)
net = self.__max_pool('pool21', net, 3, 2)
net = self.__inception('inception22', net, 256, 160, 320, 32, 128, 128)
net = self.__inception('inception23', net, 256, 160, 320, 32, 128, 128)
net = self.__inception('inception24', net, 384, 192, 384, 48, 128, 128)
net = self.__inception('inception25', net, 384, 192, 384, 48, 128, 128)
net = self.__avg_pool('pool26', net, 7, 1, padding='VALID')
net = tf.nn.dropout(net, keep_prob=0.4)
shape = net.get_shape()
net = tf.reshape(net, shape=[-1, shape[1] * shape[2] * shape[3]])
net = self.__fc('fc27', net, 10)
logits = tf.nn.softmax(net)
self.logits = logits
def __fc(self, name, net, units, with_activation=True):
with tf.variable_scope(name):
input_units = net.get_shape()[-1]
w = tf.get_variable('w', shape=[input_units, units])
b = tf.get_variable('b', shape=[units])
net = tf.add(tf.matmul(net, w), b)
if with_activation:
net = tf.nn.relu(net)
return net
def __conv2d(self, name, net, output_channels, window_size, stride_size, padding='SAME', with_activation=True):
with tf.variable_scope(name):
input_channels = net.get_shape()[-1]
filter = tf.get_variable('w', shape=[window_size, window_size, input_channels, output_channels])
bias = tf.get_variable('b', shape=[output_channels])
net = tf.nn.bias_add(tf.nn.conv2d(input=net, filter=filter,
strides=[1, stride_size, stride_size, 1],
padding=padding), bias)
if with_activation:
net = tf.nn.relu(net)
return net
def __max_pool(self, name, net, window_size, stride_size, padding='SAME'):
with tf.variable_scope(name):
net = tf.nn.max_pool(net, ksize=[1, window_size, window_size, 1],
strides=[1, stride_size, stride_size, 1],
padding=padding)
return net
def __avg_pool(self, name, net, window_size, stride_size, padding='SAME'):
with tf.variable_scope(name):
net = tf.nn.avg_pool(net, ksize=[1, window_size, window_size, 1],
strides=[1, stride_size, stride_size, 1],
padding=padding)
return net
def __inception(self, name, net,
branch1_output_channels,
branch2_reduce_output_channels, branch2_output_channels,
branch3_reduce_output_channels, branch3_output_channels,
branch4_output_channels):
with tf.variable_scope(name):
with tf.variable_scope("branch_1"):
# 1. 第一个分支
net1 = self.__conv2d('conv1', net, branch1_output_channels, 1, 1)
with tf.variable_scope("branch_2"):
# 2. 第二个分支
tmp_net = self.__conv2d('conv1', net, branch2_reduce_output_channels, 1, 1)
net2 = self.__conv2d('conv2', tmp_net, branch2_output_channels, 3, 1)
with tf.variable_scope("branch_3"):
# 3. 第三个分支
tmp_net = self.__conv2d('conv1', net, branch3_reduce_output_channels, 1, 1)
net3 = self.__conv2d('conv2', tmp_net, branch3_output_channels, 5, 1)
with tf.variable_scope("branch_4"):
# 4. 第四个分支
tmp_net = self.__max_pool('pool1', net, 3, 1)
net4 = self.__conv2d('conv1', tmp_net, branch4_output_channels, 1, 1)
with tf.variable_scope("Concat"):
net = tf.concat([net1, net2, net3, net4], axis=-1)
return net
def train():
# TODO: 损失函数、图的运行过程等代码,自己完善,我不写了。
with tf.Graph().as_default():
net = GoogleNet()
writer = tf.summary.FileWriter(logdir='./models/graph/01', graph=tf.get_default_graph())
writer.close()
if __name__ == '__main__':
train()