GoogLeNet 是谷歌 (Google) 研究出来的深度网络结构, 为什么不叫 "GoogleNet", 而叫 "GoogLeNet", 据说是为了向 "LeNet" 致敬, 因此取名为 "GoogLeNet", 所以我们这里题目就叫 GoogLeNet. 后面我们为了方便就叫 inception.NET.
Google Inception.NET 首次出现在 ILSVRC 2014 的比赛中 (和 VGGNet 同年), 就以较大优势取得了第一名. 那一届比赛中的 Inception.NET 通常被称为 inception V1, 它最大的特点就是控制了计算量和参数量的同时, 获得了非常好的分类性能 --top-5 错误率 6.67%, 只有 AlexNet 的一半不到. Inception V1 有 22 层深, 比 AlexNet 的 8 层或者 VGGNet 的 19 层还要更深. 但其大小却比 AlexNet 和 VGG 小很多, 计算量只有 15 亿次浮点运算, 同时只有 500 万的参数量, 仅为 AlexNet 参数量(6000 万) 的 1/12, 却可以达到远胜于 AlexNet 的准确率, 可以说是非常优秀且非常实用的模型. 因此在内存或计算资源有限时, GoogLeNet 是比较好的选择; 从模型结果来看, GoogLeNet 的性能更加优越.
1,GoogLeNet 是如何进一步提升性能?
GoogLeNet 带来的性能提升很大程度上要归功于 "降维", 也就是卷积分解的一种. 考虑到网络邻近的激活单元高度相关, 因此聚合之前进行降维可以得到类似于局部特征的东西. 接下来主要讨论其他的卷积分解方法. 既然 Inception 网络是全卷积, 卷积计算变少也就意味着计算量变小, 这些多出来的计算资源可以来增加 filter-bank 的尺寸大小.
一般来说, 提升网络性能最直接的办法就是增加网络深度和宽度, 深度指网络层次数量, 宽度指神经元数量. 但这种方式存在以下问题:
(1)参数太多, 如果训练数据集有限, 很容易产生过拟合;
(2)网络越大, 参数越多, 计算复杂度越大, 难以应用;
(3)网络越深, 容易出现梯度弥散问题(梯度越往后穿越容易消失), 难以优化模型.
解决这些问题的方法当然就是在增加网络深度和宽度的同时减少参数, 为了减少参数, 自然就想到将全连接变成稀疏连接. 但是在实现上, 全连接变成稀疏连接后实际计算量并不会有质的提升, 因为大部分硬件是针对密集矩阵计算优化的, 稀疏矩阵虽然数据量少, 但是计算所消耗的时间却很难减少.
如何减少参数?
第一步通过 2 个 3*3 的卷积核来代替一个 5*5 的卷积核, 感受野相同的情况下, 两个 3*3 的卷积核的参数为 2*3*3=18, 而 5*5 卷积核的参数为 25 个;
在卷积之前通过 1*1 的卷积核来降低 feature map 维度, 之后再卷积;
将 n*n 的卷积核替换为 1*n 和 n*1 两个卷积核.
2,GoogLeNet 的特点
2.1, 参数更少
GoogLeNet 参数为 500 万个, AlexNet 参数个数为 GoogLeNet 的 12 倍, VGGNet 参数又是 AlexNet 的 3 倍.
2.2, 性能更好
占用更少的内存和计算资源, 且模型结果的性能却更加优越.
Inception 历经了 V1,V2,V3,V4 等多个版本的发展, 不断趋于完善, 下面一一进行介绍.
3, 稀疏结构和 Hebbian 原理的学习
人脑神经元的连接是稀疏的, 因此研究者认为大型神经网络的合理连接方式应该也是稀疏的. 稀疏结构是非常适合神经网络的一种结构, 尤其是对非常大型, 非常深的神经网络, 可以减轻过拟合并降低计算量, 例如卷积神经网络就是稀疏的连接. Inception.NET 的主要目标就是找到最优的稀疏结构单元 (即 Inception Module), 论文中提到其稀疏结构基于 Hebbian 原理, 这里简单解释一下 Hebbian 原理: 神经反射活动的持续与重复会导致神经元连接稳定性的持久提升, 当两个神经元细胞 A 和 B 距离很近, 并且 A 参与了对 B 重复, 持续的兴奋, 那么某些代谢会导致 A 将作为能使 B 兴奋的细胞. 总结一下即 "一起发射的神经元会连接一起"(Cells that fire together, were together), 学习过程中的刺激会使神经元间的突触强度增加. 受 Hebbian 原理启发, 另一篇文章 Provable Bounds for learning Some Deep Representations 提出, 如果数据集的概率分布可以被一个很大很稀疏的神经网络所表达, 那么构筑这个网络的最佳方法时逐层构筑网络: 将上一层高度相关(correlated) 的节点聚类, 并将聚类出来的每一个小簇 (cluster) 连接到一起, 如下图所示, 这个相关性高的节点应该被连接在一起的结论, 即使从神经网络的角度对 Hebbian 原理有效性的证明.
因此一个 "好" 的稀疏结构, 应该是符合 Hebbian 原理的, 我们应该把相关性高的一簇神经元节点连接在一起. 在普通的数据集中, 这可能需要对神经元节点聚类, 但是在图片数据中, 天然的就是临近区域的数据相关性高, 因此相邻的像素点被卷积操作连接在一起. 而我们可能有多个卷积核, 在同一空间位置但在不同通道的卷积核的输出结果相关性极高. 因此, 一个 1*1 的卷积就可以很自然的把这些相关性很高的, 在同一个空间位置但是不同通道的特征连接在一起, 这就是为什么 1*1 卷积这么频繁的被应用到 Inception.NET 中的原因. 1*1 卷积所连接的节点的相关性是最高的, 而稍微大一点尺寸的卷积, 比如 3*3 5*5 的卷积所连接的节点的相关性是最高的, 而稍微大一点的卷积, 比如 3*3,5*5 的卷积所连接的节点相关性也很高, 因此也可以适当地使用一些大尺寸的卷积, 增加多样性(diversity). 最后 Inception Module 通过 4 个分支中不同尺寸的 1*1 3*3 5*5 等小型卷积将相关性很高的节点连接在一起, 就完成了其设计初衷, 构建出了很高效的符合 Hebbian 原理的稀疏结构.
在 Inception Module 中, 通常 1*1 卷积的比例 (输出通道数占比) 最高, 3*3 卷积和 5*5 卷积稍低. 而在整个网络中, 会有多个堆叠的 Inception Module , 我们希望靠后的 Inception Module 可以捕捉更高阶的抽象特征, 因此靠后的 Inception Module 的卷积的空间几何度应该逐渐降低, 这样可以捕获更大面积的特征. 因此, 越靠后的 Inception Module 中, 3*3 5*5 这两个大面积的 卷积核的占比 (输出通道数) 应该更多.
inception.NET 有 22 层深, 除了最后一层的输出, 其中间节点的分类效果也很好. 因此在 Inception.NET 中, 还使用到了辅助分类节点 (auxiliary classifiers), 即将中间某一层的输出用作分类, 并按一个较小的权重(0.3) 加到最终分类结果中. 这样相当于做了模型融合, 同时给网络增加了反向传播的梯度信息, 也提供了额外的正则化, 对于整个 Inception Net 的训练很有裨益.
当年的 Inception V1 还是跑在 TensorFlow 的前辈 DistBelief 上的, 并且只允许在 CPU 上, 当时使用了异步的 SGD 训练, 学习速率每迭代 8 个 epoch 降低 4%, 同时, Inception V1 也使用了 Multi-Scale,Multi-Crop 登上护具增强方法, 并在不同的采样数据上训练了 7 个模型进行融合, 得到了最后的 ILSVRC
2014 的比赛成绩 --top -5 错误率 6.67%.
4,inception V1
通过设计一个稀疏网络结构, 但是能够产生稠密的数据, 既能增加神经网络表现, 又能保证计算资源的使用效率. 谷歌提出了最原始的 Inception 的基本结构:
该结构将 CNN 中常用的卷积 (1*1,3*3, 5*5), 池化操作(3*3) 堆叠在一起(卷积, 池化后的尺寸相同, 将通道相加), 一方面增加了网络的宽度, 另一方面也增加了网络对尺寸的适应性.
网络卷积层中的网络能够提取输入的每一个细节信息, 同时 5*5 的滤波器也能够覆盖大部分接受层的输入. 还可以进行一个池化操作, 以减少空间大小, 降低过度拟合. 在这些层之上, 在每一个卷积层后都要做一个 ReLU 操作, 以增加网络的非线性特征.
然而这个 Inception 原始版本, 所有的卷积核都在上一层的所有输出上来做, 而那个 5*5 的卷积核所需要的计算量就太大了, 造成了特征图的厚度很大, 为了避免这种情况, 在 3*3, 5*5 前, max_pooling 后分别加上了 1*1 的卷积核, 以起到了降低特征图厚度的作用, 这也就形成了 Inception V1 的网络结构, 如下图所示:
对上面的 inception 模块的四个并行线路解释如下:
1. 一个 1 x 1 的卷积, 一个小的感受野进行卷积提取特征
2. 一个 1 x 1 的卷积加上一个 3 x 3 的卷积, 1 x 1 的卷积降低输入的特征
通道, 减少参数计算量, 然后接一个 3 x 3 的卷积做一个较大感受野的卷积
3. 一个 1 x 1 的卷积加上一个 5 x 5 的卷积, 作用和第二个一样
4. 一个 3 x 3 的最大池化加上 1 x 1 的卷积, 最大池化改变输入的特征排列,
1 x 1 的卷积进行特征提取
下面学习 Inception Module 的基本结构, 其中有 4 个分支: 第一个分支对输入进行 1*1 的卷积, 这其实也是 NIN 中提出的一个重要结构, 1*1 的卷积是一个非常优秀的结构, 它可以跨通道组织信息, 提高网络的表达能力, 同时可以对输出通道升维和降维. 可以看到 Inception Module 的四个分支都用到了 1*1 卷积, 来进行低成本 (计算量比 3*3 小很多) 的跨通道的特征变换. 第二个分支先使用了 1*1 卷积, 然后连接 3*3 卷积, 相当于进行了两次特征变换. 第三个分支类似, 先是 1*1 卷积. 然后连接 5*5 卷积, 最后一个分支则是 3*3 最大池化后直接使用 1*1 卷积. 我们可以发现, 有的分支只使用 1*1 卷积, 有的分支使用了其他尺寸的卷积时也会再使用 1*1 卷积, 这是因为 1*1 卷积的性价比很高, 用很小的计算量就能增加一层特征变换和非线性化, Inception Module 的四个分支在最后通过一个聚合操作合并(在输出通道数这个维度上聚合). Inception Module 的四个分支在最后通过一个聚合操作合并(在输出通道数这个维度上聚合). Inception Module 中包含了 3 种不同尺寸的卷积和 1 个最大池化, 增加了网络对不同尺度的适应性, 这一部分和 Multi-Scale 的思想类似. 早期计算机视觉的研究中, 受灵长类神经视觉系统的启发, Serre 使用不同尺寸的 Gabor 滤波器处理不同的图片, Inception V1 借鉴了这种思想. inception V1 的论文中指出, Inception Module 可以让网络的深度和宽度高效果地扩充, 提升准确率且不至于过拟合.
4.1 辅助分类器
inception v1 引入了辅助分类器的概念, 以改善非常深的网络的收敛. 最初的动机是将有用的梯度推向较低层, 使其立即有用, 并通过抵抗非常深的网络中的消失梯度问题来提高训练过程中的收敛. 有趣的是, 我们发现辅助分类器在训练早期并没有导致改善收敛: 在两个模型达到高精度之前, 有无侧边网络的训练进度看起来几乎相同. 接近训练结束, 辅助分支网络开始超越没有任何分支的网络的准确性, 达到了更高的稳定水平.
另外, inception V1 在网络的不同阶段使用了两个侧分支. 移除更下面的辅助分支对网络的最终质量没有任何不利影响. 再加上前一段的观察结果, 这意味着这些分支有助于演变低级特征很可能是不适当的. 相反, 我们认为辅助分类器起着正则化项的作用. 这是由于如果侧分枝是批标准化的 (BN) 或具有丢弃层(Dropout), 则网络的主分类器性能更好. 这也为推测 BN 作为正则化项给出了一个弱支持证据.
4.2 1*1 的卷积核有什么用呢?
GoogLeNet 性能优异很大程度在于使用了降维. 降维可以看做卷积网络的因式分解. 例如 1*1 卷积层后跟着 3*3 卷积层.
1*1 卷积的主要目的是为了减少维度, 还用于修正线性激活(ReLU). 比如, 上一层的输出为 100*100*128, 经过具有 256 个通道的 5*5 卷积层之后(stride=1, pad=2), 输出数据为 100*100*256, 其中, 卷积层的参数为 128*5*5*256=819200. 而加入上一层输出先经过具有 32 个通道的 1*1 卷积层, 再经过具有 256 个输出的 5*5 卷积层, 那么输出数据仍为 100*100*256, 但卷积参数量已经减少为 128*1*1*32 + 32*5*5*256 = 204800, 大约减少了 4 倍.
4.3 Inception V1 降低参数量的目的
1, 参数越多模型越庞大, 需要供模型学习的数据量就越大, 而且目前高质量的数据非常昂贵;
2, 参数越多, 耗费的计算资源也会更大;
inception V1 参数少但是效果好的原因除了模型层数更深, 表达能力更强外, 还有两点: 一是去除最后的全连接层, 用全局平均池化层 (即将图片尺寸变为 1*1) 来取代它. 全连接层几乎占据了 AlexNet 或者 VGGNet 中 90% 的参数量, 而且会引起过拟合, 去除全连接层后模型训练更快并且减轻了过拟合. 用全局平均池化层取代全连接层的做法借鉴了 Network in Network(以下简称 NIN)论文. 二是 Inception V1 中精心设计的 Inception Module 提高了参数的利用效率, 其结构如图所示. 这一部分也借鉴了 NIN 的思想, 形象的解释就是 inception Module 本身如同大网络中的一个小网络, 其结构可以反复堆叠在一起形成大网络. 不过 Inception V1 比 NIN 更进一步的时增加了分支网络, NIN 则主要是级联的卷积层和 MLPConv 层. 一般来说卷积层要提升表达能力, 主要依靠增加输出通道数, 但副作用是计算量增大和过拟合. 每一个输出通道对应一个滤波器, 同一个滤波器共享参数, 只能提取一类特征, 因此一个输出通道只能做一种特征处理. 而 NIN 中的 MLPConv 则拥有更强大的能力, 允许在输出通道之间组合信息, 因此效果明显. 可以说, MLPConv 基本等效于普通卷积层后再连接 1*1 的卷积和 ReLU 激活函数.
基于 Inception 构建了 GoogLeNet 的网络结构如下(共 22 层):
对上图说明如下:
(1) GoogLeNet 采用了模块化的结构(Inception 结构), 方便增添和修改;
(2)网络最后采用了 average pooling (平均池化)来代替全连接层, 该想法来自于 NIN(Network in Network), 事实证明这样可以将准确率提高 0.6%. 但是, 实际在最后还是加了一个全连接层, 主要是为了方便对输出进行灵活调整;
(3)虽然移除了全连接层, 但是网络中依然使用了 Dropout;
(4)为了避免梯度小时, 网络额外增加两个辅助的 softmax 用于前向传导梯度 (辅助分类器). 辅助分类器是将中间某一层的输出用作分类, 并按一个较小的权重(0.3) 加到最终分类结果中, 这样相当于做了模型融合, 同时给网络增加了反向传播的梯度信号, 也提供了额外的正则化, 对于整个网络的训练很有裨益. 而在实际测试的时候, 这两个额外的 softmax 会被去掉.
所以总结来说就是: inception V1 参数少但是效果好的原因除了模型层数更深, 表达能力更强外, 还有两点: 一是去除最后的全连接层, 用全局平均池化层 (即将图片尺寸变为 1*1) 来取代它. 全连接层几乎占据了 AlexNet 或者 VGGNet 中 90% 的参数量, 而且会引起过拟合, 去除全连接层后模型训练更快并且减轻了过拟合. 用全局平均池化层取代全连接层的做法借鉴了 Network in Network(以下简称 NIN)论文. 二是 Inception V1 中精心设计的 Inception Module 提高了参数的利用效率, 其结构如上图所示. 这一部分也借鉴了 NIN 的思想, 形象的解释就是 inception Module 本身如同大网络中的一个小网络, 其结构可以反复堆叠在一起形成大网络. 不过 Inception V1 比 NIN 更进一步的时增加了分支网络, NIN 则主要是级联的卷积层和 MLPConv 层. 一般来说卷积层要提升表达能力, 主要依靠增加输出通道数, 但副作用是计算量增大和过拟合. 每一个输出通道对应一个滤波器, 同一个滤波器共享参数, 只能提取一类特征, 因此一个输出通道只能做一种特征处理. 而 NIN 中的 MLPConv 则拥有更强大的能力, 允许在输出通道之间组合信息, 因此效果明显. 可以说, MLPConv 基本等效于普通卷积层后再连接 1*1 的卷积和 ReLU 激活函数.
GoogLeNet 的网络结构图细节如下:
注意: 上表的 "#3*3 reduce","# 5*5 reduce" 表示在 3*3 , 5*5 卷积操作之前使用了 1*1 卷积的数量.
4.3 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 等模型都要低, 对比结果如下图:
5,Inception V2
GoogLeNet 凭借其优秀的表现, 得到了很多研究人员的学习和使用, 因此 GoogLeNet 团队又对其进行了进一步地发掘改进, 产生了升级版本的 GoogLeNet.GoogLeNet 设计的初衷就是要又准又快, 而如果只是单纯的堆叠网络虽然可以提高准确率, 但是会导致计算效率有明显的下降, 所以如何在不增加过多计算量的同时提高网络的表达能力就成为了一个问题.
Inception V2 版本的解决方案就是修改 Inception 的内部计算逻辑, 提出了比较特殊的 "卷积" 计算结构.
inception V2 学习了 VGGNet, 用两个 3*3 的卷积代替了 5*5 的大卷积 (用以降低参数量并减轻过拟合), 还提出了 Batch Normalization(以下简称 BN) 方法. BN 是一个非常有效地正则化方法, 可以让大型卷积网络的训练速度加快很多倍, 同时收敛后的分类准确率也可以得到大幅的提高. BN 在用于神经网络某层时, 会对每一个 mini-batch 数据的内部进行标准化 (normalization) 处理, 使输出规范化到 N(0,1)的正态分布, 减少了 Internal Convarate Shift(内部神经元分布的改变).BN 的论文指出, 传统的深度神经网络在训练时, 每一层的输入的分布都在变化, 导致训练变得困难, 我们只能使用一个很小的学习速率解决这个问题. 而对每一层使用 BN 之后, 我们就可以有幸的解决这个问题, 学习速率可以增大很多倍, 达到之前的准确率所需要的迭代次数只有 1/14, 训练时间大大缩短. 而达到之前的准确率后, 可以继续训练, 并最终远超于 Inception V1 模型的性能 --Top-5 错误率 4.8%, 已经优于人眼水平. 因为 BN 某种意义上还起到了正则化的作用. 所以可以减少或者取消 Dropout, 简化网络结构.
当然, 只是单纯的使用 BN 获得的增益还不明显, 还需要一些相应的调整: 增大学习速率并加快学习衰减速度以适用 BN 规范化后的数据; 去掉 Dropout 并减轻 L2 正则(因 BN 已起到正则化的作用); 去掉 LRN; 更彻底地对训练样本进行 shuffle; 减少数据增强过程中队数据的光学畸变(因为 BN 训练更快, 每个样本被训练的次数更少, 因此更真实的样本对训练更有帮助). 在使用了这些措施后, Inception V2 在训练达到 Inception V1 的准确率时快了 14 倍, 并且模型在收敛时的准确率上限更高.
5.1, 卷积分解(Factorizing Convolutions)
卷积核大, 计算量也是平方的增大. 大尺寸的卷积核可以带来更大的感受野, 但也意味着会产生更多的参数, 比如 5x5 卷积核的参数有 25 个, 3x3 卷积核的参数有 9 个, 前者是后者的 25/9=2.78 倍. 因此, GoogLeNet 团队提出可以用 2 个连续的 3x3 卷积层组成的小网络来代替单个的 5x5 卷积层, 即在保持感受野范围的同时又减少了参数量, 虽然 5*5 的卷积可以捕捉到更多的临近关联信息, 但是两个 3*3 组合起来, 能观察到的 "视野" 就和 5*5 的一样了, 如下图:
那么这种替代方案会造成表达能力的下降吗? 通过大量的实验表明, 并不会造成表达缺失.
可以看出, 大卷积核完全可以由一系列的 3*3 卷积核来替代, 那能不能再分解得更小一些呢? GoogLeNet 团队考虑了 n*1 的卷积核, 其实, 对于分解的卷积层, 实验表明非线性激活比线性激活更好, 所以, 以上的卷积分解还不是最优策略, 3*3 卷积还可以进一步分解成 1*3 和 3*1 , 两个卷积分别捕捉不同方向的而信息, 参数只有之前的 6/9. 其实, 这个可以推广到 n*n 卷积的情况, n*n 卷积因式分解为 1*n 和 n*1. 这个方法在网络前面部分似乎表现欠佳, 但在中间层起到很好的效果.
上述结果表明, 大于 3*3 的卷积滤波器可能不是通常有用的, 因为他们总是可以简化为 3*3 卷积层序列. 我们仍然可以问这个问题, 是否应该把他们分解成更小的, 例如 2*2 的卷积. 然而, 通过使用非对称卷积, 可以做出甚至比 2*2 更好的效果, 即 n*1. 例如使用 3*1 卷积后接一个 1*3 卷积, 相当于以与 3*3 卷积相同的感受野滑动两层网络(如下图). 如果输入和输出老滤波器的数量相等, 那么对于相同数量的输出滤波器, 两层解决方案便宜 33%. 相比之下, 将 3*3 卷积分解为两个 2*2 卷积表示仅节省了 11% 的计算量.
如下图所示, 用 3 个 3*1 取代 3*3 卷积:
在理论上, 我们可以进一步论证, 可以通过 1*n 卷积和后面接一个 n*1 卷积替换任何 n*n 卷积, 并且随着 n 增长, 计算成本节省显著增加(如下图所示). 实际上, GoogLeNet 团队发现在网络的前期试验这种分解效果并不好, 但是对于中等网格尺寸(在 m*m 特征图上, 其中 m 范围在 12 到 20 之间), 其给出了非常好的结果. 在这个水平上, 通过使用 1*7 卷积, 然后是 7*1 卷积可以获得非常好的结果.
5.2, 降低特征图尺寸
假设有一个 d*d*k 的特征图, 为了转换成 d/2 * d/2 *2k 大小, 可以先用 1*1 卷积变成 d*d*2k, 再进行池化, 这样的计算量很大, 而先池化再增加通道则会出现 representational bottlenecks 的问题.
传统上, 卷积网络使用一些池化操作来缩减特种图的网络大小. 为了避免表示瓶颈, 在应用最大池化或平均池化之前, 需要扩展网络滤波器的激活维度. 例如, 开始需要一个带有 K 个滤波器的 d*d 网络, 如果我们想要达到一个带有 2k 个滤波器的 d/2 * d/2 网格, 我们首先需要用 2k 个滤波器计算步长为 1 的卷积, 然后应用一个额外的池化步骤. 这意味着总体计算成本由在较大的网格上使用 2d2k2 次运算的昂贵卷积支配. 一种可能性是转换为带有卷积的池化, 因此导致 2(d/2)2k2 次运算, 将计算成本降低为原来的四分之一. 然而, 由于表示的整体维度下降到 (d/2)2k , 会导致表示能力较弱的网格 (如下图所示). 这会产生一个表示瓶颈. 我们建议另一种变体, 其甚至进一步降低了计算成本, 同时消除了表示瓶颈(下下图所示), 而不是这样做. 我们可以使用两个平行的步长为 2 的块: P 和 C.P 是一个池化层(平均池化或最大池化) 的激活, 两者都是步长为 2.
一般情况下, 如果想让图像缩小, 可以有如下两种方式:
先池化再作 Inception 卷积, 或者先作 Inception 卷积再作池化. 但是方法一 (左图) 先作 pooling(池化)会导致特征表示遇到瓶颈 (特征缺失), 方法二(右图) 是正常的缩小, 但计算量很大(右边的计算量昂贵 3 倍). 为了同时保持特征表示且降低计算量, 将网络结构改为下图, 使用两个并行化的模块来降低计算量(卷积, 池化并行执行, 再进行合并).
使用 Inception V2 作改进版的 GoogLeNet, 网络结构如下:
注意: 上表中的 Figure 5 指的时没有进化的 Inception ,Figure 6 指的时小卷积版的 Inception (用 3*3 卷积核代替 5*5 卷积核),Figure 7 是指不对称版的 Inception(用 1*n, n*1 卷积核代替 n*n 卷积核). 把 7*7 卷积替换为 3 个 3*3 卷积. 包含 3 个 inception 部分. 第一部分为 35*35*288, 使用了 2 个 3*3 卷积代替了传统的 5*5 ; 第二部分减少了 feature map, 增多了 filters, 为 17*17*768, 使用了 n*1 ---> 1*n 结构, 第三部分多了 filter, 使用了卷积池化并行结构. 网络有 42 层, 但是计算量只有 GoogleNet 的 2.5 倍.
经试验, 模型结果与旧的 GoogLeNet 相比有较大的提升, 如下表所示:
6,Inception V3
Inception V3 网络则主要有两方面的改造: 一是引入了分解 ( Factorization into small convilutions ) 的思想, 将一个较大的二维卷积拆成两个较小的一维卷积, 比如将 7*7 卷积拆成 1*7 卷积和 7*1 卷积, 或者将 3*3 卷积拆成 1*3 卷积和 3*3 卷积, 如下图所示, 一方面节省了大量参数, 加速运算并减轻了过拟合(比将 7*7 卷积拆成 1*7 卷积和 7*1 卷积, 比拆成 3 个 3*3 卷积更节省参数), 同时增加了一层非线性扩展模型表达能力. 论文中指出, 这种非对称的卷积结构拆分, 其结果比对称的拆为几个相同的晓娟及核下过更明显, 可以处理更多, 更丰富的空间特征, 增加特征多样性.
另一方面, Inception V3 优化了 Inception Module 的结构, 现在 Inception Module 有 35*35,17*17 和 8*8 三种不同的结构, 如下图所示, 这些 Inception Module 只在网络的后部出现, 前部还是普通的卷积层. 并且 Inception V3 除了在 Inception Module 中使用分支, 还在分支中使用了分支(8*8 的结构中), 可以说是 Network In Network In Network, 网络输入从 224x224 变为了 299x299.
分析: 因此问题依然存在: 如果计算量保持不变, 更高的输入分辨率会有多少帮助?
普遍的看法是, 使用更高分辨率感受野的模型倾向于导致显著改进的识别性能.
为了这个目的我们进行了以下三个实验:
1)步长为 2, 大小为 299*299 的感受野和最大池化.
2)步长为 1, 大小为 151*151 的感受野和最大池化.
3)步长为 1, 大小为 79*79 的感受野和第一层之后没有池化.
所有三个网络具有几乎相同的计算成本. 虽然第三个网络稍微便宜一些, 但是池化层的成本是无足轻重的(在总成本的 1% 以内). 在每种情况下, 网络都进行了训练, 直到收敛, 并在 ImageNet ILSVRC 2012 分类基准数据集的验证集上衡量其质量. 结果如表所示. 虽然分辨率较低的网络需要更长时间去训练, 但最终结果却与较高分辨率网络的质量相当接近.
当感受野尺寸变化时, 识别性能的比较, 但计算代价是不变的. 但是, 如果只是单纯地按照输入分辨率减少网络尺寸, 那么网络的性能就会差得多.
总结:
Inception V3 网络主要有两方面的改造: 一是引入了 Factorization into small convolutions 的思想, 将一个较大的二维卷积拆成两个较小的一维卷积, 比如将 77 卷积拆成 17 卷积和 71 卷积, 或者将 33 卷积拆成 13 卷积核 31 卷积. 一方面节约了大量参数, 加快运算并减轻过拟合, 同时增加了一层非线性扩展模型表达能力. 论文中指出, 这种非对称的卷积结构拆分, 其结果比对称地拆分为几个相同的小卷积核效果更明显, 可以处理更多, 更丰富的空间特征, 增加特征多样性.
另一方面, Inception V3 优化了 Inception Module 的结构, 现在 Inception Module 有 35*35,17*17 和 8*8 三种不同结构. 这些 Inception Module 只在网络的后部出现, 前面还是普通的卷积层. 并且 Inception V3 除了在 Inception Module 中使用分支, 还在分支中使用了分支(8*8 的结构中, 可以说是 Network In Network In Network.
7,Inception V4
Inception V4 研究了 Inception 模块与残差连接的结合. ResNet 结构大大地加深了网络深度, 还极大地提升了训练速度, 同时性能也有提升.
Inception V4 主要利用残差连接 (Residual Connection) 来改进 V3 结构, 得到 Inception-ResNet-v1,Inception-ResNet-v2,Inception-v4 网络.
ResNet 的残差结构如下:
将该结构与 Inception 相结合(即 Inception V3 结合了微软的 ResNet), 变为下图:
通过 20 个类似的模块组合, Inception-ResNet 构建如下:
8,TensorFlow 实现 Inception V3
本文主要实现的是 Inception V3, 其整个网络结果如图所示. 由于 Google Inception.NET V3 相对比较复杂, 所以这里使用 tf.contrib.slim 辅助设计这个网络. contrib.slim 中一些功能和组件可以大大减少设计 Inception.NET 的代码量, 我们只需要使用少量代码就可以构建好 有 42 层深的 Inception V3.
首先定义一个简单的函数 trunc_normal, 产生截断的正态分布. 下面代码主要来自 TensorFlow 的开源实现.
- #_*_coding:utf-8_*_
- import tensorflow as tf
- slim = tf.contrib.slim
- trunc_normal = lambda stddev: tf.truncated_normal_initializer(0.0, stddev)
下面定义函数 inception_v3_arg_scope, 用来生成网络中经常用到的函数的默认参数, 比如卷积的激活函数, 权重初始化方式, 标准化器等. 设置 L2 正则的 weight_decay 默认值为 0.00004, 标准差 stddev 默认值为 0.1, 参数 batch_norm_var_collection 默认值为 moving_vars. 接下来, 定义 batch normalization 的参数字典, 定义其衰减稀疏 decay 为 0.9997,epsilon 为 0.001,updates_collections 为 tf.GrpahKeys.UPDATE_OPS, 然后字典 varibales_collections 中 beta 和 gamma 均设置为 None,moving_mean 和 moving_variance 均设置为前面的 batch_norm_var_collection.
接下来使用 slim.arg_scope, 这是一个非常有用的工具, 它可以给函数的参数自动赋予某些默认值. 例如, 这句 with slim.arg_scope([slim.conv2d, slim.fully_connected],weights_regularizer = slim.l2_regularizer(weight_decay)), 会对 [slim.conv2d, slim.fully_connected] 这两个函数的参数自动赋值, 将参数 weights_regularizer 的值默认设为 slim.l2_regularizer(weight_decay). 使用了 slim.arg_scope 后就不需要每次都重复设置参数了, 只需要在有修改时设置. 接下来, 嵌套一个 slim.arg_scope, 对卷积层生成函数 slim.conv2d 的几个参数 赋予默认值, 其权重初始化器 weights_initializer 设置为 trunc_normal(stddev), 激活函数设置为 ReLU, 标准化器设置为 slim.batch_norm, 标准化器的参数设置为前面定义的 batch_norm_params. 最后返回定义好的 scope.
因为事先定义好了 slim.conv2d 中的各种默认参数, 包括激活函数和标准化器, 因此后面定义一个卷积层将会变得非常方便. 我们可以用一行代码定义一个卷积层, 整体代码会变得非常简洁美观, 同时设计网络的工作量也会大大减轻.
- #_*_coding:utf-8_*_
- import tensorflow as tf
- slim = tf.contrib.slim
- trunc_normal = lambda stddev: tf.truncated_normal_initializer(0.0, stddev)
- def inception_v3_arg_scope(weight_decay=0.00004, stddev=0.1,
- batch_norm_var_collection='moving_vars'):
- batch_norm_params = {
- 'decay': 0.9997,
- 'epsilon': 0.001,
- 'updates_collections': tf.GraphKeys.UPDATE_OPS,
- 'variables_collections': {
- 'beta': None,
- 'gamma': None,
- 'moving_mean': [batch_norm_var_collection],
- 'moving_variance': [batch_norm_var_collection],
- }
- }
- with slim.arg_scope([slim.conv2d, slim.fully_connected],
- weights_regularizer=slim.l2_regularizer(weight_decay)):
- with slim.arg_scope(
- [slim.conv2d],
- weights_initializer=tf.truncated_normal_initializer(stddev=stddev),
- activation_fn=tf.nn.relu,
- normalizer_fn=slim.batch_norm,
- normalizer_params=batch_norm_params
- ) as sc:
- return sc
接下来我们就定义函数 inception_v3_base, 它可以生成 Inception V3 网络的卷积部分, 参数 Inputs 为输入的图片数据的 tensor,scope 为包含了函数默认参数的环境. 我们定义一个字典表 end_points, 用来保存某些关键节点供以后使用. 接着再使用 slim.arg_scope, 对 slim,conv2d, slim.max_pool2d 和 slim_avg_pool2d 这三个函数的参数设置默认值, 将 stride 设为 1 ,padding 设为 VALID. 下面正式开始定义 Inception V3 的网络结构, 首先是前面的非 Inception Module 的卷积层. 这里直接使用 slim.conv2d 创建卷积层, slim.conv2d 的第一个参数为输入的 tensor, 第二个参数为输出的通道数, 第三个参数为卷积核尺寸, 第四个参数为步长 stride, 第五个参数为 padding 模式. 我们的第一个卷积层的输出通道数为 32, 卷积核尺寸为 3*3, 步长为 2,padding 模式则是默认的 VALID. 后面的几个卷积层采用相同的形式, 按照论文中的定义, 逐层定义好网络结构. 因为使用了 slim 及 slim.arg_scope, 我们一行代码就可以定义好的一个卷积层, 相比之前 AlexNet 的实现中使用好几行代码定义一个卷积层, 或者 VGGNet 中专门写一个函数来定义卷积层, 都更加方便.
我们可以观察到, 在前面几个普通的非 Inception Module 的卷积层中, 主要使用了 3*3 的小卷积核, 这是充分借鉴了 VGGNet 的结构, 同时, Inception V3 论文中也提出了 Factorization into small convolutions 思想, 利用两个 1 维卷积模拟大尺寸的 2 维卷积, 减少参数数量同时增加非线性. 前面几层卷积中还有一层 1*1 卷积, 这也是前面提到的 Inception Module 中经常使用的结果之一, 可低成本的跨通道的对特征进行组合. 另外可以看到, 除了第一个卷积层步长为 2, 其余的卷积层步长均为 1, 而池化层则是尺寸为 3*3, 步长为 2 的重叠最大池化, 这是 AlexNet 中使用过的结构. 网络的输入数据尺寸为 299*299*3, 在经历 3 个步长为 2 的层之后, 尺寸最后缩小为 35*35*192, 空间尺寸大大降低, 但是输出通道增加了很多. 这部分代码中一共有 5 个卷积层, 2 个池化层, 实现了对输入图片数据的尺寸压缩, 并对图片特征进行了抽象.
- def inception_v3_base(inputs, scope=None):
- end_points = {}
- with tf.variable_scope(scope, 'InceptionV3', [inputs]):
- with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d],
- stride=1, padding='VALID'):
- # 一共 5 个卷积层, 两个池化层, 实现了对输入图片数据的尺寸压缩, 并对图片特征进行了抽象
- net = slim.conv2d(inputs, 32, [3, 3], stride=2, scope='Conv2d_1a_3x3')
- net = slim.conv2d.NET, 32, [3, 3], scope='Conv2d_2a_3x3')
- net = slim.conv2d.NET, 64, [3, 3], padding='SAME', scope='Conv2d_2b_3x3')
- net = slim.max_pool2d.NET, [3, 3], stride=2, scope='MaxPool_3a_3x3')
- net = slim.conv2d.NET, 80, [1, 1], scope='Conv2d_3b_1x1')
- net = slim.conv2d.NET, 192, [3, 3], scope='conv2d_4a_3x3')
- net = slim.max_pool2d.NET, [3, 3], stride=2, scope='MaxPool_5a_3x3')
接下来就是将三个连续的 Inception 模块组, 这三个 Inception 模块组中各自分别由多个 Inception Module, 这部分的网络结构即是 Inception V3 的精华所在. 每个 Inception 模块组内部的几个 Inception Module 结构非常类似, 但是存在一些细节不同.
第 1 个 Inception 模块组包含了 3 个结构类似的 Inception Module, 他们的结构和上面分解的第一幅图非常相似. 其中第一个 Inception Module 的名称为 Mixed_5d. 我们先使用 slim.arg_scope 设置所有 Inception 模块组的默认参数, 将所有卷积层, 最大池化, 平均池化层的步长设为 1,padding 模式设为 SAME. 然后设置这个 Inception Module 的 variable_scope 名称为 Mixed_5d. 这个 Inception Module 中有 4 个分支, 从 Branch_0 到 Branch_3, 第一个分支为有 64 输出通道的 1*1 卷积; 第二个分支为有 48 输出通道的 1*1 卷积, 连接有 64 输出通道的 5*5 卷积; 第三个分支为有 64 输出通道的 1*1 卷积, 再连续 2 个有 96 输出通道的 3*3 卷积; 第四个分支为 3*3 的平均池化, 连接有 32 输出通道的 1*1 卷积. 最后, 使用 tf.concat 将 4 个分支的输出合并在一起(在第 3 个维度合并, 即输出通道上合并), 生成这个 Inception Module 的最终输出. 因为这里所有的层步长均为 1, 并且 padding 模式为 SAME, 所以图片的尺寸并不会缩小, 依然维持在 35*35. 不过通道数增加了, 4 个分支的输出通道数之和 64+64+96+32=256, 即最终输出的 tensor 尺寸为 35*35*256. 这里需要注意, 第一个 Inception 模块组中所有 Inception Module 输出的图片尺寸均为 35*35, 但是后两个 Inception Module 的通道数会发生变化.
- with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d],
- stride=1, padding='SAME'):
- with tf.variable_scope('Mixed_5b'):
- with tf.variable_scope('Branch_0'):
- branch_0 = slim.conv2d.NET, 64, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope("Branch_1"):
- branch_1 = slim.conv2d.NET, 48, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 64, [5, 5], scope='Conv2d_0b_5x5')
- with tf.variable_scope('Branch_2'):
- branch_2 = slim.conv2d.NET, 64, [1, 1], scope='Conv2d_0a_1x1')
- branch2 = slim.conv2d(branch_2, 96, [3, 3], scope='COnv2d_0b_1x1')
- branch2 = slim.conv2d(branch_2, 96, [3, 3], scope='COnv2d_0c_1x1')
- with tf.variable_scope("Branch_3"):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 32, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
接下来是第一个 Inception 模块组的第 2 个 Inception Module--Mixed_5c, 这里依然使用前面设置的默认参数: 步长为 1,padding 模式为 SAME. 这个 Inception Module 同样有 4 个分支, 唯一不同的是第 4 个分支最后接的是 64 输出通道的 1*1 卷积, 而此前是 32 输出通道. 因此, 我们输出 tensor 的最终尺寸为 35*35*288, 输出通道数相比之前增加了 32.
- with tf.variable_scope('Mixed_5c'):
- with tf.variable_scope("Branch_0"):
- branch_0 = slim.conv2d.NET, 64, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope("Branch_1"):
- branch_1 = slim.conv2d.NET, 48, [1, 1], scope='Conv2d_0b_1x1')
- branch_1 = slim.conv2d(branch_1, 64, [5, 5], scope='Conv_1_0c_5x5')
- with tf.variable_scope("Branch_2"):
- branch_2 = slim.conv2d.NET, 64, [1, 1], scope='Conv2d_0a_1x1')
- branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0b_3x3')
- branhc_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0c_3x3')
- with tf.variable_scope("Branch_3"):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 64, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
而第一个 Inception 模块组的第 3 个 Inception Module--Mixed_5d 和上一个 Inception Module 完全相同, 4 个分支的结构, 参数一模一样, 输出 tensor 的尺寸也为 35*35*288.
- with tf.variable_scope('Mixed_5d'):
- with tf.variable_scope("Branch_0"):
- branch_0 = slim.conv2d.NET, 64, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope("Branch_1"):
- branch_1 = slim.conv2d.NET, 48, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 64, [5, 5], scope='Conv2d_0b_5x5')
- with tf.variable_scope("Branch_2"):
- branch_2 = slim.conv2d.NET, 64, [1, 1], scope='Conv2d_0a_1x1')
- branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0b_3x3')
- branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0c_3x3')
- with tf.variable_scope("Branch_3"):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 64, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
第二个 Inception 模块组是一个非常大的模块组, 包含了 5 个 Inception Module, 其中第二个到第五个 Inception Module 的结构非常类似, 他们的结构如因式分解图第二幅所示. 其中第一个 Inception Module 名称为 Mixed_6a , 它包含 3 个分支. 第一个分支是一个 384 输出通道的 3*3 卷积, 这个分支的通道数一下就超过了之前的通道数之和. 不过步长为 2, 因此图片尺寸将会被压缩, 且 padding 模式为 VALID, 所以图片尺寸缩小为 17*17; 第二个分支有三层, 分布是一个 64 输出通道的 1*1 卷积和两个 96 输出通道的 3*3 卷积. 这里需注意, 最后一层的步长为 2,padding 模式为 VALID, 因此图片尺寸也被压缩, 本分支最终输出的 tensor 尺寸为 17*17*96. 最后依然是使用 tf.concat 将三个分支在输出通道上合并, 最后的输出尺寸为 17*17*(384+96+256)=17*17*768. 在第二个 Inception 模块组中, 5 个 Inception Module 输出 tensor 的尺寸将全部定格为 17*17*768, 即图片尺寸和输出通道数都没有发生变化.
- with tf.variable_scope('Mixed_6a'):
- with tf.variable_scope("Branch_0"):
- branch_0 = slim.conv2d.NET, 384, [3, 3], strides=2,
- padding='VALID', scope='Conv2d_0a_1x1')
- with tf.variable_scope("Branch_1"):
- branch_1 = slim.conv2d.NET, 64, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 96, [3, 3], scope='Conv2d_0b_3x3')
- branch_1 = slim.conv2d(branch_1, 96, [3, 3], scope='Conv2d_1a_3x3')
- with tf.variable_scope("Branch_2"):
- branch_2 = slim.conv2d.NET, 64, [3, 3], strides=2,
- padding='VALID', scope='MaxPool_1a_3x3')
- net = tf.concat([branch_0, branch_1, branch_2], 3)
接下来是第 2 个 Inception 模块组的第二个 Inception Module--Mixed_6b, 它有 4 个分支. 第一个分支是一个简单的 192 输出通道的 1*1 卷积; 第二个分支由三个卷积层组成, 第一层是 128 输出通道的 1*1 卷积, 第二层是 128 通道数的 1*7 卷积, 第三次是 192 输出通道数的 7*1 卷积. 这里既是前面提到的 Factorization into small convolutions 思想, 串联的 1*7 卷积和 7*1 卷积相当于合成了一个 7*7 卷积, 不过参数量大大减少了 (只有后者的 2/7) 并减轻了过拟合, 同时多了一个激活函数增强了非线性特征变换; 第 3 个分支一下子拥有了 5 个卷积层, 分别是 128 输出通道的 1*1 卷积, 128 输出通道的 7*1 卷积, 128 输出通道的 1*7 卷积, 128 输出通道的 7*1 卷积和 192 输出通达的 1*7 卷积. 这个分支可以算是利用 Factorization into small convolutions 的典范, 反复的将 7*7 卷积进行拆分; 最后, 第四个分支是一个 3*3 的平均池化层, 再连接 192 输出通道的 1*1 卷积. 最后将 4 个分支合并, 这一层输出 tensor 的尺寸即为 17*17*(192+192+192+192)=17*17*768.
- with tf.variable_scope('Mixed_6b'):
- with tf.variable_scope("Branch_0"):
- branch_0 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope("Branch_1"):
- branch_1 = slim.conv2d.NET, 128, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 128, [1, 7], scope='Conv2d_0b_1x7')
- branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1')
- with tf.variable_scope("Branch_2"):
- branch_2 = slim.conv2d.NET, 128, [1, 1], scope='Conv2d_0a_1x1')
- branch_2 = slim.conv2d(branch_2, 128, [7, 1], scope='Conv2d_0b_7x1')
- branch_2 = slim.conv2d(branch_2, 128, [1, 7], scope='Conv2d_0c_1x7')
- branch_2 = slim.conv2d(branch_2, 128, [7, 1], scope='Conv2d_0d_7x1')
- branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7')
- with tf.variable_scope("Branch_3"):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
然后是我们第二个 Inception 模块组的第三个 Inception Module--Mixed_6c.Mixed_6c 和前面一个 Inception Module 非常相似, 只有一个地方不同, 即第二个分支和第三个分支中前几个卷积层的输出通道数不同, 从 128 变成了 160, 但是这两个分支的最终输出通道数不变, 都是 192. 其他地方则完全一致. 需要注意的是, 我们的网络每经过一个 Inception Module , 即使输出 tensor 尺寸不变, 但是特征都相当于被重新精炼了一遍, 其中丰富的卷积和非线性化对提升网络性能帮助很大.
- with tf.variable_scope('Mixed_6c'):
- with tf.variable_scope("Branch_0"):
- branch_0 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope("Branch_1"):
- branch_1 = slim.conv2d.NET, 160, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 160, [1, 7], scope='Conv2d_0b_1x7')
- branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1')
- with tf.variable_scope("Branch_2"):
- branch_2 = slim.conv2d.NET, 160, [1, 1], scope='Conv2d_0a_1x1')
- branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0b_7x1')
- branch_2 = slim.conv2d(branch_2, 160, [1, 7], scope='Conv2d_0c_1x7')
- branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0d_7x1')
- branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7')
- with tf.variable_scope("Branch_3"):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
Mixed_6d 和前面的 Mixed_6c 完全一致, 目的是通过 Inception Module 精心设计的结构增加卷积和非线性, 提炼特征.
- with tf.variable_scope('Mixed_6d'):
- with tf.variable_scope("Branch_0"):
- branch_0 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope("Branch_1"):
- branch_1 = slim.conv2d.NET, 160, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 160, [1, 7], scope='Conv2d_0b_1x7')
- branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1')
- with tf.variable_scope("Branch_2"):
- branch_2 = slim.conv2d.NET, 160, [1, 1], scope='Conv2d_0a_1x1')
- branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0b_7x1')
- branch_2 = slim.conv2d(branch_2, 160, [1, 7], scope='Conv2d_0c_1x7')
- branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0d_7x1')
- branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7')
- with tf.variable_scope("Branch_3"):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
Mixed_6e 也和前面两个 Inception Module 完全一致. 这是第二个 Inception 模块组的最后一个 Inception Module. 我们将 Mixed_6e 存储于 end_points 中, 作为 Auxiliary Classifier 辅助模型的分类.
- with tf.variable_scope('Mixed_6e'):
- with tf.variable_scope("Branch_0"):
- branch_0 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope("Branch_1"):
- branch_1 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 192, [1, 7], scope='Conv2d_0b_1x7')
- branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1')
- with tf.variable_scope("Branch_2"):
- branch_2 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- branch_2 = slim.conv2d(branch_2, 192, [7, 1], scope='Conv2d_0b_7x1')
- branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0c_1x7')
- branch_2 = slim.conv2d(branch_2, 192, [7, 1], scope='Conv2d_0d_7x1')
- branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7')
- with tf.variable_scope("Branch_3"):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
- end_points['Mixed_6e'] = net
第 3 个 Inception 模块组包含了三个 Inception Module, 其中后两个 Inception Module 的结构非常类似, 他们的结构如因式分解的第三幅图所示. 其中第一个 Inception Module 的名称为 Mixed_7a, 包含了 3 个分支. 第一个分支是 192 输出通道的 1*1 卷积, 再接 320 输出通道数的 3*3 卷积, 不过步长为 2,padding 模式为 VALID, 因此图片尺寸缩小为 8*8; 第二个分支有 4 个卷积层, 分别是 192 输出通道的 1*1 卷积, 192 输出通道的 1*7 卷积, 192 输出通道的 7*1 卷积, 以及 192 输出通道的 3*3 卷积. 注意最后一个卷积层同样步长为 2,padding 为 VALID, 因此最后输出的 tensor 尺寸为 8*8*192; 第三个分支则是一个 3*3 的最大池化层, 步长为 2,padding 为 VALID, 而池化层不会对输出通道产生改变, 因此这个分支的输出尺寸为 8*8*768. 最后, 我们将 3 个分支在输出通道上合并, 输出 tensor 尺寸为 8*8*(320+192+768)=8*8*1280. 从这个 Inception Module 开始, 输出的图片尺寸又被缩小了, 同时通道数也增加了, tensor 的总 size 在持续下降中.
- with tf.variable_scope('Mixed_7a'):
- with tf.variable_scope("Branch_0"):
- branch_0 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- branch_0 = slim.conv2d.NET, 320, [3, 3], stride=2,
- padding='VALID', scope='Conv2d_1a_3x3')
- with tf.variable_scope("Branch_1"):
- branch_1 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 192, [1, 7], scope='Conv2d_0b_1x7')
- branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1')
- with tf.variable_scope("Branch_2"):
- branch_2 = slim.max_pool2d.NET, [3, 3], stride=2,
- padding='VALID', scope='MaxPool_1a_3x3')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
接下来是第三个 Inception 模块组的第二个 Inception Module, 它有四个分支. 第一个分支是一个简单的 320 输出通道和 1*1 卷积; 第二个分支先是 1 个 384 输出通道的 1*1 卷积, 随后在分支内开了两个分支, 这两个分支分别是 384 输出通道的 3*1 卷积, 然后使用 tf.concat 合并两个分支, 得到的输出 tensor 尺寸为 8*8*(384+384)=8*8*768; 第三个分支更复杂, 先是 448 输出通道的 1*1 卷积, 然后是 384 输出通道的 3*3 卷积, 然后同样在分支内拆成两个分支, 分别是 384 输出通道的 1*3 卷积和 384 输出通道的 3*1 卷积, 最后合并得到 8*8*768 的输出 tensor; 第四个分支是在一个 3*3 卷积, 然后同样在分支内拆成两个分支, 分别是 384 输出通道的 1*3 卷积和 384 输出通道的 3*1 卷积, 最后合并德达 8*8*768 的输出 tensor; 第四个分支是在一个 3*3 的平均池化层后接一个 192 输出通道的 1*1 卷积. 最后, 将这个非常复杂的 Inception Module 的四个分支合并在一起, 得到的输出 tensor 尺寸为 8*8*(320+768+78+192)=8*8*2048. 到这个 Inception Module, 输出通道数从 1280 增加到 2048.
- with tf.variable_scope('Mixed_7b'):
- with tf.variable_scope("Branch_0"):
- branch_0 = slim.conv2d.NET, 320, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope("Branch_1"):
- branch_1 = slim.conv2d.NET, 384, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = tf.concat([
- slim.conv2d(branch_1, 384, [1, 3], scope='Conv2d_0b_1x3'),
- slim.conv2d(branch_1, 384, [3, 1], scope='Conv2d_0c_3x1')
- ], 3)
- with tf.variable_scope("Branch_2"):
- branch_2 = slim.conv2d.NET, 448, [1, 1], scope='COnv2d_0a_1x1')
- branch_2 = slim.conv2d(branch_2, 384, [3, 3],
- scope='Conv2d_0b_3x3')
- branch_2 = tf.concat([
- slim.conv2d(branch_2, 384, [1, 3], scope='Conv2d_0c_1x3'),
- slim.conv2d(branch_2, 384, [3, 1], scope='Conv2d_0d_3x1')
- ], 3)
- with tf.variable_scope("Branch_3"):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='COnv2d_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 192, [1, 1],
- scope='Conv2d_0b_1x3')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
Mixed_7c 是第三个 Inception 模块组的最后一个 Inception Module, 不过他们和前面的 Mixed_7b 是完全一致的, 输出 tensor 也是 8*8*2048. 最后, 我们返回这个 Inception Module 的结果, 作为 inception_v3_base 函数的最终输出.
- with tf.variable_scope('Mixed_7c'):
- with tf.variable_scope("Branch_0"):
- branch_0 = slim.conv2d.NET, 320, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope("Branch_1"):
- branch_1 = slim.conv2d.NET, 384, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = tf.concat([
- slim.conv2d(branch_1, 384, [1, 3], scope='Conv2d_0b_1x3'),
- slim.conv2d(branch_1, 384, [3, 1], scope='Conv2d_0c_3x1')
- ], 3)
- with tf.variable_scope("Branch_2"):
- branch_2 = slim.conv2d.NET, 448, [1, 1], scope='COnv2d_0a_1x1')
- branch_2 = slim.conv2d(branch_2, 384, [3, 3],
- scope='Conv2d_0b_3x3')
- branch_2 = tf.concat([
- slim.conv2d(branch_2, 384, [1, 3], scope='Conv2d_0c_1x3'),
- slim.conv2d(branch_2, 384, [3, 1], scope='Conv2d_0d_3x1')
- ], 3)
- with tf.variable_scope("Branch_3"):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 192, [1, 1],
- scope='Conv2d_0b_1x3')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
- return.NET, end_points
至此, Inception V3 网络的核心部分, 即卷积层部分就完成了. 回忆一下 Inception V3 的网络结构: 首先是五个卷积层和两个池化层交替的普通结构, 然后是三个 Inception 模块组, 每个模块组内包含多个结构类似的 Inception Module. 设计 Inception.NET 的一个重要原则是, 图片尺寸是不断缩小的, 从 299*299 通过五个步长为 2 的卷积层或池化层后, 缩小为 8*8, 同时, 输出通道数持续增加, 从一开始的 3(RGB 的三色)到 2048. 从这里可以看出, 每一层卷积, 池化或 Inception 模块组的目的都是将空间结构简化, 同时将空间信息转化为高阶抽象的特征信息, 即将空间的维度转化为通道的维度. 这一过程同时也使每层输出 tensor 的总 size 持续下降, 降低了计算量. 我们可能也发现了 Inception Module 的规律, 一般情况下有四个分支, 第一个分支一般是 1*1 卷积, 第二个分支一般是 1*1 卷积再接分解后 (factorized) 的 1 x n 和 n x 1 卷积, 第三个分支和第二个分支类似, 但是一般更深一点, 第四个分支一般具有最大池化或平均池化. 因此, Inception Module 是通过组合比较简单的特征抽象 (分支 1), 比较复杂的特征抽象(分支 2 和分支 3) 和一个简化结构的池化层(分支 4), 一共四种不同程度的特征抽象和变换来选择地保留不同层次的高阶特征, 这样可以最大程度地丰富网络的表达能力.
接下来, 我们来实现 Inception V3 网络的最后一部分 -- 全局平均池化, Softmax 和 Auxiliary Logits. 先看函数 Inception V3 的输入参数, num_classes 即最后需要分类的数据量, 这里默认的 1000 是 ILSVRC 比赛数据集的种类数; is_training 标志是否是训练过程, 对 Batch Normalization 和 Dropout 有影响, 只有在训练时 Batch Normalization 和 Dropout 才会被启用; dropout_keep_prob 即训练时 Dropout 所需保留节点的比例, 默认为 0.8;predicetion_fn 是最后用来进行分类的函数, 这里默认是使用 slim.softmax;spatial_squeeze 参数标志是否对输出进行 squeeze 操作(即去除维数为 1 的维度, 比如 5*3*1 转为 5*3);reuse 标志是否会对网络和 Variable 进行重复使用; 最后, scope 为包含了函数默认参数的环境. 首先, 使用 tf.variable_scope 定义网络的 name 和 reuse 等参数的默认值, 然后使用 slim.arg_scope 定义 Batch Normalization 和 Dropout 的 is_training 标志的默认值. 最后, 使用前面定义好的 inception_v3_base 构筑整个网络的卷积部分, 拿到最后一层的输出 net 和重要节点的字典表 end_points.
- def inception_v3(inputs,
- num_classes=1000,
- is_training=True,
- dropout_keep_prob=0.8,
- prediction_fn=slim.softmax,
- spatial_squeeze=True,
- reuse=None,
- scope='InceptionV3'):
- with tf.variable_scope(scope, 'InceptionV3', [inputs, num_classes],
- reuse=reuse) as scope:
- with slim.arg_scope([slim.batch_norm, slim.dropout],
- is_training=is_training):
- net, end_points = inception_v3_base(inputs, scope=scope)
接下来处理 Auxiliary Logits 这部分的逻辑, Auxiliary Logits 作为辅助分类的节点, 对分类结果预测有很大帮助, 先使用 slim.arg_scope 将卷积, 最大池化, 平均池化的默认步长设为 1, 默认 padding 模式设为 SAME . 然后通过 end_points 取到 Mixed_6e, 并在 Mixed_6e 之后再接一个 5*5 的平均池化, 步长为 3,padding 设为 VALID, 这样输出的尺寸就从 17*17*768 变为 5*5*768. 接着连接一个 128 输出通道的 1*1 卷积和一个 768 输出通道的 5*5 卷积, 这里权重初始化方式重设为标准差为 0.01 的正态分布, padding 模式设为 VALID, 输出尺寸为 1*1*768. 然后再连接一个输出通道数为 num_classes 的 1*1 卷积, 不设激活函数和规范化函数, 权重初始化重设为标准差为 0.001 的正态分布, 这样输出变为了 1*1*1000. 接下来, 使用 tf.squeeze 函数消除输出 tensor 中前两个为 1 的维度, 最后将辅助分类节点的输出 aux_logits 储存到字典表 end_points 中.
- with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d],
- stride=1, padding='SAME'):
- aux_logits = end_points['Mixed_6e']
- with tf.variable_scope('AuxLogits'):
- aux_logits = slim.avg_pool2d(
- aux_logits, [5, 5], stride=3, padding='VALID',
- scope='AvfPool_1a_5x5'
- )
- aux_logits = slim.conv2d(aux_logits, 128, [1, 1],
- scope='COnv2d_1b_1x1')
- aux_logits = slim.conv2d(
- aux_logits, 768, [5, 5],
- weights_initializer=trunc_normal(0.01),
- padding='VALID', scope='COnv2d_2a_5x5'
- )
- aux_logits = slim.conv2d(
- aux_logits, num_classes, [1, 1], activation_fn=None,
- normalizer_fn=None, weights_initializer=trunc_normal(0.001),
- scope='Conv2d_2b_1x1'
- )
- if spatial_squeeze:
- aux_logits = tf.squeeze(aux_logits, [1, 2],
- name='SpatialSqueeze')
- end_points['AuxLogits'] = aux_logits
下面处理正常的分类预测的逻辑. 我们直接对 Mixed_7e 即最后一个卷积层的输出进行一个 8*8 全局平均池化, padding 模式为 VALID, 这样输出 tensor 的尺寸就变为了 1*1*2048. 然后连接一个 Dropout 层, 节点保留率为 dropout_keep_prob. 接着连接一个输出通道数为 1000 的 1*1 卷积, 激活函数和规范化函数设为空. 下面使用 tf.squeeze 去除输出 tensor 中维度为 1 的维度. 再连接一个 Softmax 对结果进行分类预测. 最后返回输出结果 Logits 和包含辅助节点的 end_points.
- with tf.variable_scope('Logits'):
- net = slim.avg_pool2d.NET, [8, 8], padding='VALID',
- scope='AvgPool_1a_8x8')
- net = slim.dropout.NET, keep_prob=dropout_keep_prob,
- scope='Dropout_1b')
- end_points['PreLogits'] = net
- logits = slim.conv2d.NET, num_classes, [1, 1], activation_fn=None,
- normalizer_fn=None, scope='Conv2d_1c_1x1')
- if spatial_squeeze:
- logits = tf.squeeze(logits, [1, 2], name='SpatialSqueeze')
- end_points['Logits'] = logits
- end_points['Predictions'] = prediction_fn(logits, scope='Predictions')
- return logits, end_points
至此, 整个 Inception V3 网络的构建就完成了. Inception V3 是一个非常复杂, 精妙的模型, 其中用到了非常多值钱积累下来的设计大型卷积网络的经验和技巧. 不过, 虽然 Inception V3 论文中给出了设计卷积网络的几个原则, 但是其中很多超参数的选择, 包含层数, 卷积核的尺寸, 池化的位置, 步长的大小, Factorization 使用的时机, 以及分支的设计, 都很难一一解释. 目前, 我们只能认为深度学习, 尤其是大型卷积网络的设计, 是一门实验学科, 其中需要大量的探索和实践. 我们很难正面某种网络结构一定更好, 更多的是通过实验积累下来的经验总结出一些结论. 深度学习的研究中, 理论正面部分依然是短板, 但是通过实验得到的结论通常也具有不错的推广性, 在其他数据集上泛化性良好.
下面对 Inception v3 进行运算性能测试, 这里使用的 time_tensorflow_run 函数和 ALexNet 一样, 这里直接写代码, 不重复说明了. 因为 Inception V3 网络结构较大, 所以依然令 batch_size 为 32, 以便 GPU 显存不够, 图片尺寸设为 299*299, 并用 tf.random_uniform 生成随机图片数据作为 Input. 接着, 我们使用 slim.arg_sope 加载前面定义好的 inception_v3_arg_scope() , 在这个 scope 中包含了 Batch Normalization 的默认参数, 以及激活函数和参数初始化方式的默认值. 然后在这个 arg_scope 下, 调用 inception_v3 函数, 并传入 Inputs, 获取 logits 和 end_points. 下面创建 Session 并初始化全部模型参数, 最后我们设置测试的 batch 数量为 100, 并使用 time_tensorflow_run 测试 Inception V3 网络的 forward 性能.
- def time_tensorflow_run(session, target, info_string):
- num_steps_burn_in = 10
- total_duration = 0.0 # 记录总时间
- total_duration_squared = 0.0 # 记录平方和 total_duration_squared 用于计算方差
- for i in range(num_batches + num_steps_burn_in):
- start_time = time.time()
- _ = session.run(target)
- duration = time.time() - start_time
- if i>= num_steps_burn_in:
- if not i % 10:
- print('%s: stpe %d, duration=%.3f'%(datetime.now(),
- i-num_steps_burn_in, duration))
- total_duration += duration
- total_duration_squared += duration * duration
- mn = total_duration / num_batches
- vr = total_duration_squared / num_batches - mn*mn
- sd = math.sqrt(vr)
- print('%s: %s across %d steps, %.3f +/- %.3f sec / batch'%(datetime.now(),
- info_string, num_batches, mn, sd))
代码 2:
- if __name__ == '__main__':
- batch_size = 32
- height, width = 299, 299
- inputs = tf.random_uniform((batch_size, height, width, 3))
- with slim.arg_scope(inception_v3_arg_scope()):
- logits, end_points = inception_v3(inputs, is_training=False)
- init = tf.global_variables_initializer()
- sess = tf.Session()
- sess.run(init)
- num_batches = 100
- time_tensorflow_run(sess, logits, 'Forward')
从结果来看, Inception V3 网络的 forward 性能不错, 在 GPU 的环境下, 每个 batch(包含 32 张图片)预测耗时仅为 0.071s. 虽然输入图片的面积比 VGGNet 的 224*224 大了 78%, 但是 forward 速度却比 VGGNet 的 0.072s 更快. 这主要归功于其较小的参数量, Inception V3 网络仅有 2500 万个参数, 虽然比 Inception V1 的 700 万多了很多, 不过任然不到 AlexNet 的 6000 万参数量的一半, 相比于 VGGNet 的 1.4 亿参数量就更少了, 这对一个 42 层深的大型网络来说极为不易的. 同时, 整个网络的浮点计算量仅为 50 亿次, 虽也比 Inception V1 的 15 亿次大了不少, 但是相比 VGGNet 仍然不算大. 较小的计算量让 Inception V3 网络变得非常实用, 我们可以将其轻松的移到普通的服务器上提供快速响应的服务, 甚至是移植到收集上进行实时的图像识别.
- 2019-09-16 09:47:11.291271: step 0, duration=0.072
- 2019-09-16 09:47:12.007825: step 10, duration=0.072
- 2019-09-16 09:47:12.723585: step 20, duration=0.072
- 2019-09-16 09:47:13.437683: step 30, duration=0.071
- 2019-09-16 09:47:14.151189: step 40, duration=0.071
- 2019-09-16 09:47:14.864866: step 50, duration=0.071
- 2019-09-16 09:47:15.579139: step 60, duration=0.071
- 2019-09-16 09:47:16.291750: step 70, duration=0.071
- 2019-09-16 09:47:17.005981: step 80, duration=0.071
- 2019-09-16 09:47:17.721084: step 90, duration=0.071
- 2019-09-16 09:47:18.362285: Forward across 100 steps, 0.007 +/- 0.021 sec / batch
Inception V3 作为一个极深的卷积神经网络, 拥有非常精妙的设计和构造, 整个网络的结构和分支非常复杂, 我们平时可能不必设计这么复杂的网络的, 但是 Inception V3 中让然有许多 CNN 的思想和 Trick 值得借鉴.
(1) Factorization into small convolutions 很有效, 可以降低参数量, 减轻过拟合, 增加网络非线性的表达能力.
(2)卷积网络从输入到输出, 应该让图片尺寸逐渐减少, 输出通道数逐渐增加, 即让空间结构简化, 将空间信息转化为高阶抽象的特征信息.
(3)Inception Module 用多个分支提取不同抽象程度的高阶特征的思路很有效, 可以丰富网络的表达能力.
完整代码如下:
- import tensorflow as tf
- slim = tf.contrib.slim
- trunc_normal = lambda stddev: tf.truncated_normal_initializer(0.0, stddev)
- def inception_v3_base(inputs, scope=None):
- end_points = {}
- with tf.variable_scope(scope, 'InceptionV3', [inputs]):
- with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d],
- stride=1, padding='VALID'):
- # 299 x 299 x 3
- net = slim.conv2d(inputs, 32, [3, 3], stride=2, scope='Conv2d_1a_3x3')
- # 149 x 149 x 32
- net = slim.conv2d.NET, 32, [3, 3], scope='Conv2d_2a_3x3')
- # 147 x 147 x 32
- net = slim.conv2d.NET, 64, [3, 3], padding='SAME', scope='Conv2d_2b_3x3')
- # 147 x 147 x 64
- net = slim.max_pool2d.NET, [3, 3], stride=2, scope='MaxPool_3a_3x3')
- # 73 x 73 x 64
- net = slim.conv2d.NET, 80, [1, 1], scope='Conv2d_3b_1x1')
- # 73 x 73 x 80.
- net = slim.conv2d.NET, 192, [3, 3], scope='Conv2d_4a_3x3')
- # 71 x 71 x 192.
- net = slim.max_pool2d.NET, [3, 3], stride=2, scope='MaxPool_5a_3x3')
- # 35 x 35 x 192.
- # Inception blocks
- with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d],
- stride=1, padding='SAME'):
- # mixed: 35 x 35 x 256.
- with tf.variable_scope('Mixed_5b'):
- with tf.variable_scope('Branch_0'):
- branch_0 = slim.conv2d.NET, 64, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope('Branch_1'):
- branch_1 = slim.conv2d.NET, 48, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 64, [5, 5], scope='Conv2d_0b_5x5')
- with tf.variable_scope('Branch_2'):
- branch_2 = slim.conv2d.NET, 64, [1, 1], scope='Conv2d_0a_1x1')
- branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0b_3x3')
- branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0c_3x3')
- with tf.variable_scope('Branch_3'):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 32, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
- # mixed_1: 35 x 35 x 288.
- with tf.variable_scope('Mixed_5c'):
- with tf.variable_scope('Branch_0'):
- branch_0 = slim.conv2d.NET, 64, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope('Branch_1'):
- branch_1 = slim.conv2d.NET, 48, [1, 1], scope='Conv2d_0b_1x1')
- branch_1 = slim.conv2d(branch_1, 64, [5, 5], scope='Conv_1_0c_5x5')
- with tf.variable_scope('Branch_2'):
- branch_2 = slim.conv2d.NET, 64, [1, 1], scope='Conv2d_0a_1x1')
- branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0b_3x3')
- branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0c_3x3')
- with tf.variable_scope('Branch_3'):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 64, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
- # mixed_2: 35 x 35 x 288.
- with tf.variable_scope('Mixed_5d'):
- with tf.variable_scope('Branch_0'):
- branch_0 = slim.conv2d.NET, 64, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope('Branch_1'):
- branch_1 = slim.conv2d.NET, 48, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 64, [5, 5], scope='Conv2d_0b_5x5')
- with tf.variable_scope('Branch_2'):
- branch_2 = slim.conv2d.NET, 64, [1, 1], scope='Conv2d_0a_1x1')
- branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0b_3x3')
- branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0c_3x3')
- with tf.variable_scope('Branch_3'):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 64, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
- # mixed_3: 17 x 17 x 768.
- with tf.variable_scope('Mixed_6a'):
- with tf.variable_scope('Branch_0'):
- branch_0 = slim.conv2d.NET, 384, [3, 3], stride=2,
- padding='VALID', scope='Conv2d_1a_1x1')
- with tf.variable_scope('Branch_1'):
- branch_1 = slim.conv2d.NET, 64, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 96, [3, 3], scope='Conv2d_0b_3x3')
- branch_1 = slim.conv2d(branch_1, 96, [3, 3], stride=2,
- padding='VALID', scope='Conv2d_1a_1x1')
- with tf.variable_scope('Branch_2'):
- branch_2 = slim.max_pool2d.NET, [3, 3], stride=2, padding='VALID',
- scope='MaxPool_1a_3x3')
- net = tf.concat([branch_0, branch_1, branch_2], 3)
- # mixed4: 17 x 17 x 768.
- with tf.variable_scope('Mixed_6b'):
- with tf.variable_scope('Branch_0'):
- branch_0 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope('Branch_1'):
- branch_1 = slim.conv2d.NET, 128, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 128, [1, 7], scope='Conv2d_0b_1x7')
- branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1')
- with tf.variable_scope('Branch_2'):
- branch_2 = slim.conv2d.NET, 128, [1, 1], scope='Conv2d_0a_1x1')
- branch_2 = slim.conv2d(branch_2, 128, [7, 1], scope='Conv2d_0b_7x1')
- branch_2 = slim.conv2d(branch_2, 128, [1, 7], scope='Conv2d_0c_1x7')
- branch_2 = slim.conv2d(branch_2, 128, [7, 1], scope='Conv2d_0d_7x1')
- branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7')
- with tf.variable_scope('Branch_3'):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
- # mixed_5: 17 x 17 x 768.
- with tf.variable_scope('Mixed_6c'):
- with tf.variable_scope('Branch_0'):
- branch_0 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope('Branch_1'):
- branch_1 = slim.conv2d.NET, 160, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 160, [1, 7], scope='Conv2d_0b_1x7')
- branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1')
- with tf.variable_scope('Branch_2'):
- branch_2 = slim.conv2d.NET, 160, [1, 1], scope='Conv2d_0a_1x1')
- branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0b_7x1')
- branch_2 = slim.conv2d(branch_2, 160, [1, 7], scope='Conv2d_0c_1x7')
- branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0d_7x1')
- branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7')
- with tf.variable_scope('Branch_3'):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
- # mixed_6: 17 x 17 x 768.
- with tf.variable_scope('Mixed_6d'):
- with tf.variable_scope('Branch_0'):
- branch_0 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope('Branch_1'):
- branch_1 = slim.conv2d.NET, 160, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 160, [1, 7], scope='Conv2d_0b_1x7')
- branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1')
- with tf.variable_scope('Branch_2'):
- branch_2 = slim.conv2d.NET, 160, [1, 1], scope='Conv2d_0a_1x1')
- branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0b_7x1')
- branch_2 = slim.conv2d(branch_2, 160, [1, 7], scope='Conv2d_0c_1x7')
- branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0d_7x1')
- branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7')
- with tf.variable_scope('Branch_3'):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
- # mixed_7: 17 x 17 x 768.
- with tf.variable_scope('Mixed_6e'):
- with tf.variable_scope('Branch_0'):
- branch_0 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope('Branch_1'):
- branch_1 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 192, [1, 7], scope='Conv2d_0b_1x7')
- branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1')
- with tf.variable_scope('Branch_2'):
- branch_2 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- branch_2 = slim.conv2d(branch_2, 192, [7, 1], scope='Conv2d_0b_7x1')
- branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0c_1x7')
- branch_2 = slim.conv2d(branch_2, 192, [7, 1], scope='Conv2d_0d_7x1')
- branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7')
- with tf.variable_scope('Branch_3'):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
- end_points['Mixed_6e'] = net
- # mixed_8: 8 x 8 x 1280.
- with tf.variable_scope('Mixed_7a'):
- with tf.variable_scope('Branch_0'):
- branch_0 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- branch_0 = slim.conv2d(branch_0, 320, [3, 3], stride=2,
- padding='VALID', scope='Conv2d_1a_3x3')
- with tf.variable_scope('Branch_1'):
- branch_1 = slim.conv2d.NET, 192, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = slim.conv2d(branch_1, 192, [1, 7], scope='Conv2d_0b_1x7')
- branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1')
- branch_1 = slim.conv2d(branch_1, 192, [3, 3], stride=2,
- padding='VALID', scope='Conv2d_1a_3x3')
- with tf.variable_scope('Branch_2'):
- branch_2 = slim.max_pool2d.NET, [3, 3], stride=2, padding='VALID',
- scope='MaxPool_1a_3x3')
- net = tf.concat([branch_0, branch_1, branch_2], 3)
- # mixed_9: 8 x 8 x 2048.
- with tf.variable_scope('Mixed_7b'):
- with tf.variable_scope('Branch_0'):
- branch_0 = slim.conv2d.NET, 320, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope('Branch_1'):
- branch_1 = slim.conv2d.NET, 384, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = tf.concat([
- slim.conv2d(branch_1, 384, [1, 3], scope='Conv2d_0b_1x3'),
- slim.conv2d(branch_1, 384, [3, 1], scope='Conv2d_0b_3x1')], 3)
- with tf.variable_scope('Branch_2'):
- branch_2 = slim.conv2d.NET, 448, [1, 1], scope='Conv2d_0a_1x1')
- branch_2 = slim.conv2d(
- branch_2, 384, [3, 3], scope='Conv2d_0b_3x3')
- branch_2 = tf.concat([
- slim.conv2d(branch_2, 384, [1, 3], scope='Conv2d_0c_1x3'),
- slim.conv2d(branch_2, 384, [3, 1], scope='Conv2d_0d_3x1')], 3)
- with tf.variable_scope('Branch_3'):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(
- branch_3, 192, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
- # mixed_10: 8 x 8 x 2048.
- with tf.variable_scope('Mixed_7c'):
- with tf.variable_scope('Branch_0'):
- branch_0 = slim.conv2d.NET, 320, [1, 1], scope='Conv2d_0a_1x1')
- with tf.variable_scope('Branch_1'):
- branch_1 = slim.conv2d.NET, 384, [1, 1], scope='Conv2d_0a_1x1')
- branch_1 = tf.concat([
- slim.conv2d(branch_1, 384, [1, 3], scope='Conv2d_0b_1x3'),
- slim.conv2d(branch_1, 384, [3, 1], scope='Conv2d_0c_3x1')], 3)
- with tf.variable_scope('Branch_2'):
- branch_2 = slim.conv2d.NET, 448, [1, 1], scope='Conv2d_0a_1x1')
- branch_2 = slim.conv2d(
- branch_2, 384, [3, 3], scope='Conv2d_0b_3x3')
- branch_2 = tf.concat([
- slim.conv2d(branch_2, 384, [1, 3], scope='Conv2d_0c_1x3'),
- slim.conv2d(branch_2, 384, [3, 1], scope='Conv2d_0d_3x1')], 3)
- with tf.variable_scope('Branch_3'):
- branch_3 = slim.avg_pool2d.NET, [3, 3], scope='AvgPool_0a_3x3')
- branch_3 = slim.conv2d(
- branch_3, 192, [1, 1], scope='Conv2d_0b_1x1')
- net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
- return.NET, end_points
- def inception_v3(inputs,
- num_classes=1000,
- is_training=True,
- dropout_keep_prob=0.8,
- prediction_fn=slim.softmax,
- spatial_squeeze=True,
- reuse=None,
- scope='InceptionV3'):
- with tf.variable_scope(scope, 'InceptionV3', [inputs, num_classes],
- reuse=reuse) as scope:
- with slim.arg_scope([slim.batch_norm, slim.dropout],
- is_training=is_training):
- net, end_points = inception_v3_base(inputs, scope=scope)
- # Auxiliary Head logits
- with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d],
- stride=1, padding='SAME'):
- aux_logits = end_points['Mixed_6e']
- with tf.variable_scope('AuxLogits'):
- aux_logits = slim.avg_pool2d(
- aux_logits, [5, 5], stride=3, padding='VALID',
- scope='AvgPool_1a_5x5')
- aux_logits = slim.conv2d(aux_logits, 128, [1, 1],
- scope='Conv2d_1b_1x1')
- # Shape of feature map before the final layer.
- aux_logits = slim.conv2d(
- aux_logits, 768, [5, 5],
- weights_initializer=trunc_normal(0.01),
- padding='VALID', scope='Conv2d_2a_5x5')
- aux_logits = slim.conv2d(
- aux_logits, num_classes, [1, 1], activation_fn=None,
- normalizer_fn=None, weights_initializer=trunc_normal(0.001),
- scope='Conv2d_2b_1x1')
- if spatial_squeeze:
- aux_logits = tf.squeeze(aux_logits, [1, 2], name='SpatialSqueeze')
- end_points['AuxLogits'] = aux_logits
- # Final pooling and prediction
- with tf.variable_scope('Logits'):
- net = slim.avg_pool2d.NET, [8, 8], padding='VALID',
- scope='AvgPool_1a_8x8')
- # 1 x 1 x 2048
- net = slim.dropout.NET, keep_prob=dropout_keep_prob, scope='Dropout_1b')
- end_points['PreLogits'] = net
- # 2048
- logits = slim.conv2d.NET, num_classes, [1, 1], activation_fn=None,
- normalizer_fn=None, scope='Conv2d_1c_1x1')
- if spatial_squeeze:
- logits = tf.squeeze(logits, [1, 2], name='SpatialSqueeze')
- # 1000
- end_points['Logits'] = logits
- end_points['Predictions'] = prediction_fn(logits, scope='Predictions')
- return logits, end_points
- def inception_v3_arg_scope(weight_decay=0.00004,
- stddev=0.1,
- batch_norm_var_collection='moving_vars'):
- batch_norm_params = {
- 'decay': 0.9997,
- 'epsilon': 0.001,
- 'updates_collections': tf.GraphKeys.UPDATE_OPS,
- 'variables_collections': {
- 'beta': None,
- 'gamma': None,
- 'moving_mean': [batch_norm_var_collection],
- 'moving_variance': [batch_norm_var_collection],
- }
- }
- with slim.arg_scope([slim.conv2d, slim.fully_connected],
- weights_regularizer=slim.l2_regularizer(weight_decay)):
- with slim.arg_scope(
- [slim.conv2d],
- weights_initializer=trunc_normal(stddev),
- activation_fn=tf.nn.relu,
- normalizer_fn=slim.batch_norm,
- normalizer_params=batch_norm_params) as sc:
- return sc
- from datetime import datetime
- import math
- import time
- def time_tensorflow_run(session, target, info_string):
- num_steps_burn_in = 10
- total_duration = 0.0
- total_duration_squared = 0.0
- for i in range(num_batches + num_steps_burn_in):
- start_time = time.time()
- _ = session.run(target)
- duration = time.time() - start_time
- if i>= num_steps_burn_in:
- if not i % 10:
- print('%s: step %d, duration = %.3f' %
- (datetime.now(), i - num_steps_burn_in, duration))
- total_duration += duration
- total_duration_squared += duration * duration
- mn = total_duration / num_batches
- vr = total_duration_squared / num_batches - mn * mn
- sd = math.sqrt(vr)
- print('%s: %s across %d steps, %.3f +/- %.3f sec / batch' %
- (datetime.now(), info_string, num_batches, mn, sd))
- if __name__ == '__main__':
- batch_size = 32
- height, width = 299, 299
- inputs = tf.random_uniform((batch_size, height, width, 3))
- with slim.arg_scope(inception_v3_arg_scope()):
- logits, end_points = inception_v3(inputs, is_training=False)
- init = tf.global_variables_initializer()
- sess = tf.Session()
- sess.run(init)
- num_batches = 100
- time_tensorflow_run(sess, logits, "Forward")
本文是学习 GoogLeNet 网络的笔记, 参考了《tensorflow 实战》这本书中关于 GoogLeNet 的章节, 写的非常好, 所以在此做了笔记, 侵删.
而且本文在学习中, 摘抄了下面博客的 GoogLeNet 笔记, 也写的通俗易通: https://www.zybuluo.com/rianusr/note/1419006
https://my.oschina.net/u/876354/blog/1637819
在学习后, 确实对 GoogLeNet 理解了不少, 在此很感谢! 侵删, 谢谢
强烈建议:
2014 至 2016 年, GoogLeNet 团队发表了多篇关于 GoogLeNet 的经典论文《Going deeper with convolutions》,《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》,《Rethinking the Inception Architecture for Computer Vision》,《Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning》, 在这些论文中对 Inception v1,Inception v2,Inception v3,Inception v4 等思想和技术原理进行了详细的介绍, 建议阅读这些论文以全面了解 GoogLeNet.
inception-4 论文地址: https://arxiv.org/pdf/1602.07261.pdf
inception-3 论文地址: https://arxiv.org/pdf/1409.4842v1.pdf
Rethinking the Inception Architecture for Computer Vision, 3.5% test error : http://arxiv.org/abs/1512.00567
来源: https://www.cnblogs.com/wj-1314/p/11337807.html