经典论文阅读(4)-InceptionV2及TensorFlow实现
文章目录
0. 前言
InceptionV2是Google团队在InceptionV1的基础上做的一系列的改进,最终将ImageNet分类任务上,将错误率降低到了4.9%。在这篇文章中主要有两个改进:
- 引入了Batch-Normalization(批量标准化);
- 将原Inception模块中的 5 ∗ 5 5*5 5∗5卷积层用两层的 3 ∗ 3 3*3 3∗3卷积来代替。
论文:Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shif
1. 使用mini-batch进行批量标准化
文章对于解决这个问题提出了两个必要的简化:
- 通过独立标准化每一个神经元的输入,避免与每层的输入与输出混在一起。【这意味着BN操作在本层的神经元激活之前】
- 在进行标准化的时候,均值和方差从mini-batch中产生。
基于mini-batch的标准化可以写成如下的形式:

- $\varepsilon $是一个常数,加入到mini-batch normalization中用于维持数值稳定,避免除数为0的情况;
- 线性变换中, γ 、 β \gamma、\beta γ、β都是需要学习的未知量。
通过上面的变换我们不难发现, x ^ i \hat x_i x^i只是出现在batch-normalization中的一个变量,最终还是会被恢复成y,但是它的出现很关键,这个操作在论文中的解释是用于修正输入的均值和方差;让隐藏层之间有稍微不同的分布,也增加了非线性效果。
于是:
输入 x i x_i xi的分布的均值和方差为: μ B \mu_B μB, σ B \sigma_B σB;
进行标准化之后 x ^ i \hat x_i x^i的分布均值和方差为:0和1;
进行线性变换之后y的分布均值和方差为: β \beta β和 γ \gamma γ 。
为什么将的分布归一化为均值0标准差为1后又要用和进行分布变换?
因为当均值和标准差为0和1后,给每一层一个自主学习的能力,通过自我学习调整每一层数据的均值和方差和跟参数w一样进行梯度下降更新,而由于input的x减去均值会抵消掉bias的效果,所以我们可以不用处理bias的更新了。如果 γ \gamma γ是方差, β \beta β是均值,那么相当于没有做BN的操作。在训练的过程中和控制了数据集的均值和方差,使得数据与上一层的联系降低,每一层能够比较独立的训练。

2. mini-BatchNormalization的好处
- 对每层都做了标准化处理,可以避免梯度爆炸和梯度消失的问题。主要见减小了梯度与参数初始化的相关性,从而可以差用较大的学习率进行训练(一般是InceptionV1的30倍)。提高了训练速度;
- BN有部分正则化的效果,因此可以减小对于Dropout的依赖;同时也允许使用一些饱和非线性激活函数如Sigmoid,因为BN可以将输入的分布拉回到这些饱和非线性的线性区域中。
3. 训练BN和使用BN进行推理(inference)

上一部分是训练,黄色部分是推理。在推理的时候,我们需要知道每一层的均值和方差,这里采用的是记录每一个mini-batch的计算出来的均值和方差,使用这些均值和方差估计整体的均值和方差,这里就是算法中第10步中,所描述的内容。
最后一步,将x恢复成y写成了两个部分,与 y = γ x ^ i + β y=\gamma \hat x_i + \beta y=γx^i+β一样,这样写是因为 γ V a r [ x ] + ε \frac{\gamma}{\sqrt{Var[x]+\varepsilon }} Var[x]+εγ和 γ E [ x ] V a r [ x ] + ε \frac{\gamma E[x]}{\sqrt{Var[x]+\varepsilon }} Var[x]+εγE[x]可以在训练过程中存储下来,这样可以减少运算量。
4. 文章中的另一改进-将Inception模块中的5X5卷积用两层3X3卷积来代替

改进Inception模块后,在网络中去掉了全连接层。网络结构如下所示:

文中提到的几个加速BN网络训练的方法:
- 增大学习率;
- 移除Dropout;
- 减小损失函数中的L2正则化项5倍;
- 加速学习率的衰减;
- 移除LRN;
- 更加彻底的打乱训练数据;
- 减少光度失真。
5. InceptionV2的tensorflow实现
Inception模块做了部分改动,增加量BN:
import tflearn
from tflearn.layers.core import input_data, dropout, fully_connected
from tflearn.layers.conv import conv_2d, max_pool_2d, avg_pool_2d, batch_normalization
from tflearn.layers.estimator import regression
import tflearn.datasets.oxflower17 as oxflower17
import tensorflow as tf
def loadData():
# 获取Oxford Flower17训练集
X, Y = oxflower17.load_data(dirname='17flowers/', one_hot=True, resize_pics=(227, 227))
return X, Y
def Inception_model(network, para):
'''
定义Inception模块
:param network: 网络参数
:param para: 相关参数
:return:
'''
for i in range(1, para['maxlayer']+1):
stride = 1
if para[i][3][0] == 'max_pool': # max_pool
stride = 2
# branch1 : 1*1
branch1 = conv_2d(network, para[i][0], 1, strides=stride, activation='relu')
batch_normalization(branch1)
# branch2: 1*1 -> 3*3
branch2 = conv_2d(network, para[i][1][0], 1, strides=1, activation='relu')
branch2 = conv_2d(branch2, para[i][1][1], 3, strides=stride, activation='relu')
batch_normalization(branch2)
# branch3: 1*1 -> 3*3 -> 3*3
branch3 = conv_2d(network, para[i][2][0], 1, strides=1, activation='relu')
branch3 = conv_2d(branch3, para[i][2][1], 3, strides=1, activation='relu')
branch3 = conv_2d(branch3, para[i][2][1], 3, strides=stride, activation='relu')
batch_normalization(branch3)
# branch4: max_pool -> 1*1
if para[i][3][0] == 'max_pool': # max_pool
branch4 = max_pool_2d(network, 3, strides=2)
else: # avg_pool
branch4 = avg_pool_2d(network, 3, strides=1)
branch4 = conv_2d(branch4, para[i][3][1], 1, strides=1, activation='relu')
batch_normalization(branch4)
# 按通道数合并, 第三个维度
network = tf.concat([branch1, branch2, branch3, branch4], 3)
return network
搭建InceptionV2网络结构:
def InceptionV2():
# 定义输入层
network = input_data(shape=[None, 227, 227, 3])
# 定义第一层卷积层
network = conv_2d(network, 64, 7, strides=2, activation='relu')
batch_normalization(network)
network = max_pool_2d(network, 3, strides=2)
# 定义第二层卷积层
network = conv_2d(network, 64, 1, strides=1)
network = conv_2d(network, 192, 3, strides=1)
batch_normalization(network)
network = max_pool_2d(network, 3, strides=2)
# 定义Inception3a, 3b, 3c
para = {1: [64, [64, 64], [64, 96], ['avg_pool', 32]], # [1*1, [1*1, 3*3], [1*1, 3*3], pool-1*1]
2: [64, [64, 96], [64, 96], ['avg_pool', 64]],
3: [0, [128, 160], [64, 96], ['max_pool', 'pass_through']],
'maxlayer': 3,
}
network = Inception_model(network, para)
# 定义Inception4a, 4b, 4c, 4d, 4e
para = {1: [224, [64, 96], [96, 128], ['avg_pool', 128]], # [1*1, [1*1, 3*3], [1*1, 3*3], pool-1*1]
2: [192, [96, 128], [96, 128], ['avg_pool', 128]],
3: [160, [128, 160], [128, 160], ['avg_pool', 128]],
4: [96, [128, 192], [192, 256], ['avg_pool', 128]],
5: [0, [128, 192], [192, 256], ['max_pool', 'pass_through']],
'maxlayer': 5,
}
network = Inception_model(network, para)
# 定义Inception5a, 5b
para = {1: [352, [192, 320], [160, 224], ['avg_pool', 128]], # [1*1, [1*1, 3*3], [1*1, 3*3], pool-1*1]
2: [352, [192, 320], [192, 224], ['avg_pool', 128]],
'maxlayer': 2,
}
network = Inception_model(network, para)
batch_normalization(network)
# 池化层
network = avg_pool_2d(network, 7, strides=1)
# 再添加一层全连接层
# NOTE: 在原论文中,到上面的池化层就结束了,因为此时的输出就是1024个。这里使用
# 的是Oxfordflower17数据集,所以还需要更改一下输出
network = fully_connected(network, 17, activation='softmax')
return network
定义主函数进行训练即可:
def main():
# 加载数据
X, Y = loadData()
# 获取参数
network = InceptionV2()
network = regression(network, optimizer='momentum', learning_rate=0.03) # 默认为0.001,增大30倍
print('-'*15, 'Start training... ', '-'*15)
# Start training
model = tflearn.DNN(network, checkpoint_path='model_InceptionV2/', max_checkpoints=5, tensorboard_verbose=0, tensorboard_dir='logs/')
model.fit(X, Y, n_epoch=1, validation_set=0.1, shuffle=True, show_metric=True, batch_size=32, snapshot_step=200, snapshot_epoch=False, run_id='InceptionV2_oxflowers17')
model.save('E:/python_project/CNN/07-CNN/model_file/InceptionV2')
if __name__ == '__main__':
main()
整体的效果感觉并没有InceptionV1快,可能是其中的一些trick我还没加入到代码中,有待后续研究~
参考资料
【深度学习系列】用PaddlePaddle和Tensorflow实现GoogLeNet InceptionV2/V3/V4