具体数据集介绍及下载地址:https://blog.csdn.net/weixin_44402973/article/details/96028312
具体结构介绍详细参照博客:https://my.oschina.net/u/876354/blog/1637819
2014年,GoogLeNet和VGG是当年ImageNet挑战赛(ILSVRC14)的双雄,GoogLeNet获得了第一名、VGG获得了第二名,这两类模型结构的共同特点是层次更深了。VGG继承了LeNet以及AlexNet的一些框架结构,而GoogLeNet则做了更加大胆的网络结构尝试,虽然深度只有22层,但大小却比AlexNet和VGG小很多,GoogleNet参数为500万个,AlexNet参数个数是GoogleNet的12倍,VGGNet参数又是AlexNet的3倍,因此在内存或计算资源有限时,GoogleNet是比较好的选择;从模型结果来看,GoogLeNet的性能却更加优越。
GoogLeNet是如何进一步提升性能的呢?
一般来说,提升网络性能最直接的办法就是增加网络深度和宽度,深度指网络层次数量、宽度指神经元数量。但这种方式存在以下问题:
参数太多,如果训练数据集有限,很容易产生过拟合;网络越大、参数越多,计算复杂度越大,难以应用;网络越深,容易出现梯度弥散问题(梯度越往后穿越容易消失),难以优化模型。解决上述问题的方法当然就是在增加网络深度和宽度的同时减少参数,为了减少参数,自然就想到将全连接变成稀疏连接。但是在实现上,全连接变成稀疏连接后实际计算量并不会有质的提升,因为大部分硬件是针对密集矩阵计算优化的,稀疏矩阵虽然数据量少,但是计算所消耗的时间却很难减少。
那么,有没有一种方法既能保持网络结构的稀疏性,又能利用密集矩阵的高计算性能。大量的文献表明可以将稀疏矩阵聚类为较为密集的子矩阵来提高计算性能,就如人类的大脑是可以看做是神经元的重复堆积,因此,GoogLeNet团队提出了Inception网络结构,就是构造一种“基础神经元”结构,来搭建一个稀疏性、高计算性能的网络结构。
什么是Inception呢?
Inception历经了V1、V2、V3、V4等多个版本的发展,不断趋于完善,下面一一进行介绍
一、Inception V1
通过设计一个稀疏网络结构,但是能够产生稠密的数据,既能增加神经网络表现,又能保证计算资源的使用效率。谷歌提出了最原始Inception的基本结构:
该结构将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层):
对上图说明如下:
GoogLeNet采用了模块化的结构(Inception结构),方便增添和修改;网络最后采用了average pooling(平均池化)来代替全连接层,该想法来自NIN(Network in Network),事实证明这样可以将准确率提高0.6%。但是,实际在最后还是加了一个全连接层,主要是为了方便对输出进行灵活调整;虽然移除了全连接,但是网络中依然使用了Dropout ; 为了避免梯度消失,网络额外增加了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,最终输出28x28x2563b、第三层(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等模型都要低,对比结果如下表所示:
1.文件组织形式:
2.代码
# coding: utf-8 import tensorflow as tf import os import pickle import numpy as np CIFAR_DIR = "./cifar-10-batches-py" print(os.listdir(CIFAR_DIR)) def load_data(filename): """read data from data file.""" with open(filename, 'rb') as f: data = pickle.load(f, encoding='latin1') return data['data'], data['labels'] class CifarData: def __init__(self, filenames, need_shuffle): all_data = [] all_labels = [] for filename in filenames: data, labels = load_data(filename) all_data.append(data) all_labels.append(labels) self._data = np.vstack(all_data) self._data = self._data / 127.5 - 1 self._labels = np.hstack(all_labels) print(self._data.shape) print(self._labels.shape) self._num_examples = self._data.shape[0] self._need_shuffle = need_shuffle self._indicator = 0 if self._need_shuffle: self._shuffle_data() def _shuffle_data(self): # [0,1,2,3,4,5] -> [5,3,2,4,0,1] p = np.random.permutation(self._num_examples) self._data = self._data[p] self._labels = self._labels[p] def size(self): """获取数据总量""" return self._num_examples def next_batch(self, batch_size): """return batch_size examples as a batch.""" if batch_size > self._num_examples: raise Exception("the batch size gt the size of all examples") end_indicator = self._indicator + batch_size if self._indicator<self._num_examples and end_indicator > self._num_examples: end_indicator = self._num_examples elif self._indicator >= self._num_examples: self._indicator = 0 end_indicator = batch_size batch_data = self._data[self._indicator: end_indicator] batch_labels = self._labels[self._indicator: end_indicator] self._indicator = end_indicator return batch_data, batch_labels def inception_block(x, output_channel_for_each_path, name): """inception block implementation""" """ Args: - x: - output_channel_for_each_path: eg: [10, 20, 5] - name: """ with tf.variable_scope(name): conv1_1 = tf.layers.conv2d(x, output_channel_for_each_path[0], (1, 1), strides = (1,1), padding = 'same', activation = tf.nn.relu, name = 'conv1_1') conv3_3 = tf.layers.conv2d(x, output_channel_for_each_path[1], (3, 3), strides = (1,1), padding = 'same', activation = tf.nn.relu, name = 'conv3_3') conv5_5 = tf.layers.conv2d(x, output_channel_for_each_path[2], (5, 5), strides = (1,1), padding = 'same', activation = tf.nn.relu, name = 'conv5_5') max_pooling = tf.layers.max_pooling2d(x, (2, 2), (2, 2), name = 'max_pooling') max_pooling_shape = max_pooling.get_shape().as_list()[1:] input_shape = x.get_shape().as_list()[1:] width_padding = (input_shape[0] - max_pooling_shape[0]) // 2 height_padding = (input_shape[1] - max_pooling_shape[1]) // 2 padded_pooling = tf.pad(max_pooling, [[0, 0], [width_padding, width_padding], [height_padding, height_padding], [0, 0]]) # 对通过多个卷积和池化之后的feature map 按照通道连接起来 concat_layer = tf.concat( [conv1_1, conv3_3, conv5_5, padded_pooling], axis = 3) return concat_layer def inception_cnn(): '''定义具有inception结构的cnn网络''' x = tf.placeholder(tf.float32, [None, 3072]) y = tf.placeholder(tf.int64, [None]) # [None], eg: [0,5,6,3] x_image = tf.reshape(x, [-1, 3, 32, 32]) # 32*32 x_image = tf.transpose(x_image, perm=[0, 2, 3, 1]) # conv1: 神经元图, feature_map, 输出图像 conv1 = tf.layers.conv2d(x_image, 32, # output channel number (3,3), # kernel size padding = 'same', activation = tf.nn.relu, name = 'conv1') pooling1 = tf.layers.max_pooling2d(conv1, (2, 2), # kernel size (2, 2), # stride name = 'pool1') inception_2a = inception_block(pooling1, [16, 16, 16], name = 'inception_2a') inception_2b = inception_block(inception_2a, [16, 16, 16], name = 'inception_2b') pooling2 = tf.layers.max_pooling2d(inception_2b, (2, 2), # kernel size (2, 2), # stride name = 'pool2') inception_3a = inception_block(pooling2, [16, 16, 16], name = 'inception_3a') inception_3b = inception_block(inception_3a, [16, 16, 16], name = 'inception_3b') pooling3 = tf.layers.max_pooling2d(inception_3b, (2, 2), # kernel size (2, 2), # stride name = 'pool3') flatten = tf.contrib.layers.flatten(pooling3) y_ = tf.layers.dense(flatten, 10) with tf.name_scope("loss"): # y_ -> sofmax # y -> one_hot # loss = ylogy_ loss = tf.losses.sparse_softmax_cross_entropy(labels=y, logits=y_) # indices predict = tf.argmax(y_, 1) # [1,0,1,1,1,0,0,0] correct_prediction = tf.equal(predict, y) accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float64)) with tf.name_scope('train_op'): train_op = tf.train.AdamOptimizer(1e-3).minimize(loss) return x,y,accuracy,loss,train_op batch_size = 32 #训练轮数 num_epoch = 10 train_filenames = [os.path.join(CIFAR_DIR, 'data_batch_%d' % i) for i in range(1, 6)] test_filenames = [os.path.join(CIFAR_DIR, 'test_batch')] x,y,accuracy,loss,train_op = inception_cnn() best_test_acc = 0 with tf.Session() as sess: init = tf.global_variables_initializer().run() iters = 0 for epoch in range(num_epoch): train_data = CifarData(train_filenames, True) train_size = train_data.size() # 获取每个epoch中batch个数 batch_num = np.ceil(train_size / batch_size) for i in range(int(batch_num)): batch_data, batch_labels = train_data.next_batch(batch_size) loss_val, acc_val, _ = sess.run([loss, accuracy, train_op],feed_dict={x: batch_data,y: batch_labels}) if iters % 1000 == 0 and iters != 0: print('Epoch:%d, [Train] Step: %d, [Train] loss: %4.5f, [Train] acc: %4.5f' % (epoch,iters, loss_val, acc_val)) iters += 1 # 每个epoch之后对数据评估 test_data = CifarData(test_filenames, False) test_size = test_data.size() test_all_acc = [] test_all_loss = [] test_batch_num = np.ceil(test_size / batch_size) for j in range(int(test_batch_num)): test_batch_data, test_batch_labels = test_data.next_batch(batch_size) test_acc,test_loss= sess.run([accuracy,loss],feed_dict = {x:test_batch_data, y: test_batch_labels}) test_all_acc.append(test_acc) test_all_loss.append(test_loss) if best_test_acc < np.mean(test_all_acc): best_test_acc = np.mean(test_all_acc) print('[Test] loss: %4.5f, [Test] acc: %4.5f Flag:*' % (np.mean(test_all_loss), np.mean(test_all_acc))) else: print('[Test] loss: %4.5f, [Test] acc: %4.5f Flag:-' % (np.mean(test_all_loss), np.mean(test_all_acc)))
