Hello GoogLeNetV3
前言
本篇文章会使用Keras去复现GoogleNetV3。要复现GoogleNet 需要明白一下几个问题。辅助分类器的作用,GoogleNet中Inception块的特点和并行网络应该如何计算。GoogLeNetV3会有一个辅助分类器,三种不同结构的Inception块。
内容 | 地址 |
---|---|
kaggle地址 | 连接 |
百度网盘 | 连接 提取码:1l01 |
一、GoogLeNet
上一篇的VGGNet验证了随着卷积神经网络的加深,网络的性能能有更大的提升。所以GoogleNet就专注于建立更深的网络结构。同时呢,GoogLeNet 的关键呢 也在于使用了Inception块。根据GoogleNet的版本不同Inception块也会有很大的差距。
1.Inception块
我们先来看图
但是这样做会导致参数过多。所以引入了1*1的卷积,对Inception块进行降维。首先看图
增加卷积应该会增加参数,为什么这里反而降低参数了呢?在这里假设我们现在有一个大小维28*28*192的特征图,这几个卷积的通道都为64,那么卷积核的参数就为(1*1*192*64)+(3*3*192*64)+(5*5*192*64)=430080
假设引入的96和16的1*1的卷积核。那么参数就变成了
(1*1*192*64)+(1*1*192*96+3*3*96*64)+(1*1*192*16+5*5*16*64)=114688
它的思想是减少大卷积核输入的通道数,来减少大卷积核的参数量,从而达到降低参数数量的一个效果,参数量降低了2/3!!!InceptionV2呢,参考了VGGNet 将5*5的卷积核用两个3*3的卷积核代替。InceptionV3中 引入了三种不用的Inception块。但大体思路都是一样的。在InceptionV4中,不仅仅有传统的Inception块,还引入了残差结构。我们来看一个对比表格:
网络 | 特点 | Top5错误率 |
---|---|---|
GoogLeNetV1 | 1. 所有卷积层均采用ReLU激活函数. 2. 输入为均值后224x224的彩色图像 3. 总损失由3个损失加权构成 | 6.67% |
GoogLeNetV2 | 1. 参考VGG 使用使用两个3*3卷积核代替5x5的卷积核 2.减少了一个辅助分类器 3.采用BN,减少和取消LRN和dropout的使用 | 4.8% |
GoogLeNetV3 | 1.采用分解卷积,例如将N*N的分解成一维卷积N*1 和1*N的卷积 2.还提出了三种不用的Inception 结构 | 3.5% |
GooLleNetV4 | Inception块的构造不同 除了传统的Inception V4(Inception-A,Inception-B,Inception-C)外,引入残差 Inception V4(Inception-ResNet-A,Inception-ResNet-B,Inception-ResNet-C) | 3.08% |
2. InceptionV3
在InceptionV3中,引入了三种不用的Inception结构,其中三种大小分别对应35*35,17*17,8*8(H*W)。下面我们用图片来看一下他的详细结构。
这里针对的是大小维35*35大小,使用两个3*3代替一个5*5大小的卷积核。那么是如何代替的呢。先看论文中给的图:
假设我现在有一个5*5大小的特征图 如果 我用一个5*5的卷积核做卷积运算,那么会得到1*1的特征图。同样我用一个3*3的卷积核对5*5的特征图做步长为1的卷积运算得到一个3*3的特征图,再使用同样大小的卷积核对这个3*3的特征图,最后也会得到1*1的特征图。
下面我们再来看一张图:
这个Inception块的特点就是将。n*n的卷积核分解成n*1 和1*n的卷积核。同样,我们来看看它是如何分解的。
假设我们有一个3*3的特征图,如果通过一个3*3卷积核运算,同样得到一个1*1的特征图。如果我们先用一个3*1的卷积核进行运算得到一个1*3的特征图,再用1*3的卷积核进行运算,同样会得到1*1的特征图。
于是我们看第三种图:
这张图没有什么特殊的,只是再并行结构中加入了并行结构。
3.辅助分类器
在GoogLeNet中,会引入辅助分类器GoogLeNetV1 中会引入两个辅助分类器,而在后面的GoogLeNet系列网络中呢,会删掉一个辅助分类器。辅助分类器在训练时使用,在真正的测试时则需要把它删掉。目前了解到得信息来看,引入辅助分类器的主要作用是为了防止网络过深造成的梯度弥散问题。
同时确保隐藏单元的中间层也参与计算。
二、实现步骤
在实现过程中需要去注意的是每个块之间的Concat问题,例如 输出的维度是[None,35,35,128]和[None,35,35,192] (BHWC),那么Concat之后就变层楼[None,35,35,128+192]。每一个Inception块开始的第一个卷积的padding 当时都为’valid’ 后续的都为’same’。GoogLeNet前面几层就是简单的叠加。这里就不在赘述,我主要介绍Inception块。
1.Inception块的实现
GoogLeNetV3中虽然只有三种不同的Inception块,但是每个Inception块的参数是不太相同的,网络结构比较复杂,这里只要拿出Inception块和辅助分类器说明一下,具体的网络结构图和代码在Kaggle中都会一一展示。
首先我们来看一张图:
这里是输入到网络中的第一个Incetion块,那么需要将输入分别分开计算,通过几层计算之后,再将得到的结果进行叠加输出。
代码:
# x-x3 从左往右表示
# 计算x
x0 = Conv2D(filters=64, kernel_size=1, strides=1, padding='valid', activation='relu')(x)
x0 = BatchNormalization()(x0)
# 计算x1
x1 = Conv2D(filters=48, kernel_size=1, strides=1, padding='valid', activation='relu')(x)
x1 = BatchNormalization()(x1)
x1 = Conv2D(filters=64, kernel_size=5, strides=1, padding='same', activation='relu')(x1)
x1 = BatchNormalization()(x1)
# 计算x2
x2 = Conv2D(filters=64, kernel_size=1, strides=1, padding='valid', activation='relu')(x)
x2 = BatchNormalization()(x2)
x2 = Conv2D(filters=96, kernel_size=3, strides=1, padding='same', activation='relu')(x2)
x2 = BatchNormalization()(x2)
x2 = Conv2D(filters=96, kernel_size=3, strides=1, padding='same', activation='relu')(x2)
x2 = BatchNormalization()(x2)
# 计算x3
x3 = AveragePooling2D(pool_size=3, strides=1,padding='same')(x)
x3 = Conv2D(filters=64, kernel_size=1, strides=1, padding='same', activation='relu')(x3)
# 合并
x = Concatenate(axis=3)([x0, x1, x2, x3])
网络中的输入为x 维度为[None,35,35,288],x0~x3分别表示从左往右的计算结果,为了使网络更快的收敛。每一层卷积后我都使用了BatchNormalization 和ReLU激活函数。
下面我们来看第二中Inception块:
这种Inception块就是将正方形的卷积核,分解成长方形卷积核。实现方式都是类似的:
x0 = Conv2D(filters=192, kernel_size=1, strides=1, padding='valid', activation='relu')(x)
x0 = BatchNormalization()(x0)
x1=Conv2D(filters=128, kernel_size=1, strides=1, padding='valid', activation='relu')(x)
x1 = BatchNormalization()(x1)
x1=Conv2D(filters=128, kernel_size=(1,7), strides=1, padding='same', activation='relu')(x1) #注意 这里是方形卷积核
x1 = BatchNormalization()(x1)
x1=Conv2D(filters=192, kernel_size=(7,1), strides=1, padding='same', activation='relu')(x1)
x1 = BatchNormalization()(x1)
x2=Conv2D(filters=128, kernel_size=1, strides=1, padding='valid', activation='relu')(x)
x2 = BatchNormalization()(x2)
x2=Conv2D(filters=128, kernel_size=(7,1), strides=1, padding='same', activation='relu')(x2)
x2 = BatchNormalization()(x2)
x2=Conv2D(filters=128, kernel_size=(1,7), strides=1, padding='same', activation='relu')(x2)
x2 = BatchNormalization()(x2)
x2=Conv2D(filters=128, kernel_size=(7,1), strides=1, padding='same', activation='relu')(x2)
x2 = BatchNormalization()(x2)
x2=Conv2D(filters=192, kernel_size=(1,7), strides=1, padding='same', activation='relu')(x2)
x2 = BatchNormalization()(x2)
x3=AveragePooling2D(3,1,padding='same')(x)
x3=Conv2D(filters=192, kernel_size=1, strides=1, padding='valid', activation='relu')(x3)
x = Concatenate(axis=3)([x0, x1, x2,x3])
x0~x3表示从左到右的计算结果。然后再叠加输出。
第三种Inception块如下图所示:
这个Inception块在并行结构中加入了并行结构。看起来比较复杂 其实计算思路都是类似的。
x0 = Conv2D(filters=320, kernel_size=1, strides=1, padding='valid', activation='relu')(x)
x0 = BatchNormalization()(x0)
#计算x1
x1= Conv2D(filters=384, kernel_size=1, strides=1, padding='valid', activation='relu')(x)
x1_1=Conv2D(filters=384, kernel_size=(1,3), strides=1, padding='same', activation='relu')(x1)
x1_1= BatchNormalization()(x1_1)
x1_2=Conv2D(filters=384, kernel_size=(3,1), strides=1, padding='same', activation='relu')(x1)
x1_2= BatchNormalization()(x1_2)
x1=Concatenate(axis=3)([x1_1,x1_2])
x2=Conv2D(filters=448, kernel_size=1, strides=1, padding='valid', activation='relu')(x)
x2 = BatchNormalization()(x2)
x2=Conv2D(filters=384, kernel_size=3, strides=1, padding='same', activation='relu')(x2)
x2 = BatchNormalization()(x2)
x2_1=Conv2D(filters=384, kernel_size=(1,3), strides=1, padding='same', activation='relu')(x2)
x2_1 = BatchNormalization()(x2_1)
x2_2=Conv2D(filters=384, kernel_size=(3,1), strides=1, padding='same', activation='relu')(x2)
x2_2= BatchNormalization()(x2_2)
x2=Concatenate(axis=3)([x2_1,x2_2])
x3=AveragePooling2D(3,1,padding='same')(x)
x3=Conv2D(filters=192, kernel_size=1, strides=1, padding='same', activation='relu')(x3)
x=Concatenate(axis=3)([x0,x1,x2,x3])
x表示输入和输出,x0~x3表示从左到右的计算,xi_j表示并行结构中再一次并行计算的结果。
2.辅助分类器实现
辅助分类器并不是高大上的东西,就是在网络中引出来一个输出去辅助网络结构,GoogLeNetV3中只有一个辅助分类器,是在网络的如下位置引出:
这里我也是采用了一个全局平均池化,然后再连接全连接层进行分类:
#这里
out1=x #辅助分类器
out1=AveragePooling2D(5,3)(out1)
out1= Conv2D(filters=128, kernel_size=5, strides=5, padding='valid', activation='relu')(out1)
out1=GlobalAveragePooling2D()(out1)
out1=Dense(classes,activation='softmax',name='out1')(out1)
到这里呢,需要讲解的部分就完了。这次复现再训练配置部分,使用上一篇文章中提到的数据读取方法。使用的数据集为oxford-flower-17categories-labelled,但是它只有15类…。训练30个epochs 最后能达到70%的一个准确率。
这是它最后一个训练结果:
总结
这篇文章,没有百分之百的还原GoogLeNetV3的网络结构,复现也只是采用了最传统的方法,一层一层的叠加,由于每层网络的参数不同没有找到一个特殊的办法去减少代码量,这是我现在所欠缺的地方。通过这篇文章的学习,我们可以清楚的了解到并行网络结构的特点。网络不仅仅有深度,还有了宽度。里面有很多的网络优化技巧可以学习。下一篇要去复现ResNet啦!!由于图像预处理知识比较欠缺,我可能做一个python opencv的分栏。期待和大家一起学习。最后图片参考来源于论文,其他的图都是自己在 visual-paradigm画的。创作不易,点赞鼓励。