前面讲了 LeNet,AlexNet 和 Vgg, 这周来讲讲 GoogLeNet.GoogLeNet 是由 google 的 Christian Szegedy 等人在 2014 年的论文Going Deeper with Convolutions https://arxiv.org/abs/1409.4842 提出, 其最大的亮点是提出一种叫 Inception 的结构, 以此为基础构建 GoogLeNet, 并在当年的 ImageNet 分类和检测任务中获得第一, ps:GoogLeNet 的取名是为了向 YannLeCun 的 LeNet 系列致敬.
(本系列所有代码均在 https://github.com/huxiaoman7/PaddlePaddle_code :https://github.com/huxiaoman7/PaddlePaddle_code)
关于深度网络的一些思考
在本系列最开始的几篇文章我们讲到了卷积神经网络, 设计的网络结构也非常简单, 属于浅层神经网络, 如三层的卷积神经网络等, 但是在层数比较少的时候, 有时候效果往往并没有那么好, 在实验过程中发现, 当我们尝试增加网络的层数, 或者增加每一层网络的神经元个数的时候, 对准确率有一定的提升, 简单的说就是增加网络的深度与宽度, 但这样做有两个明显的缺点:
更深更宽的网络意味着更多的参数, 提高了模型的复杂度, 从而大大增加过拟合的风险, 尤其在训练数据不是那么多或者某个 label 训练数据不足的情况下更容易发生;
增加计算资源的消耗, 实际情况下, 不管是因为数据稀疏还是扩充的网络结构利用不充分(比如很多权重接近 0), 都会导致大量计算的浪费.
解决以上两个问题的基本方法是将全连接或卷积连接改为稀疏连接. 不管从生物的角度还是机器学习的角度, 稀疏性都有良好的表现, 回想一下在讲 AlexNet 这一节提出的 Dropout 网络以及 ReLU 激活函数, 其本质就是利用稀疏性提高模型泛化性(但需要计算的参数没变少).
简单解释下稀疏性, 当整个特征空间是非线性甚至不连续时:
学好局部空间的特征集更能提升性能, 类似于 Maxout 网络中使用多个局部线性函数的组合来拟合非线性函数的思想;
假设整个特征空间由 N 个不连续局部特征空间集合组成, 任意一个样本会被映射到这 N 个空间中并激活 / 不激活相应特征维度, 如果用 C1 表示某类样本被激活的特征维度集合, 用 C2 表示另一类样本的特征维度集合, 当数据量不够大时, 要想增加特征区分度并很好的区分两类样本, 就要降低 C1 和 C2 的重合度(比如可用 Jaccard 距离衡量), 即缩小 C1 和 C2 的大小, 意味着相应的特征维度集会变稀疏.
不过尴尬的是, 现在的计算机体系结构更善于稠密数据的计算, 而在非均匀分布的稀疏数据上的计算效率极差, 比如稀疏性会导致的缓存 miss 率极高, 于是需要一种方法既能发挥稀疏网络的优势又能保证计算效率. 好在前人做了大量实验(如On Two-Dimensional Sparse Matrix Partitioning: Models, Methods, and a Recipe http://www.bmi.osu.edu/~umit/papers/Catalyurek10-SISC.pdf ), 发现对稀疏矩阵做聚类得到相对稠密的子矩阵可以大幅提高稀疏矩阵乘法性能, 借鉴这个思想, 作者提出 Inception 的结构.
图 1 Inception 结构
把不同大小卷积核抽象得到的特征空间看做子特征空间, 每个子特征空间都是稀疏的, 把这些不同尺度特征做融合, 相当于得到一个相对稠密的空间;
采用 1×1,3×3,5×5 卷积核(不是必须的, 也可以是其他大小),stride 取 1, 利用 padding 可以方便的做输出特征维度对齐;
大量事实表明 pooling 层能有效提高卷积网络的效果, 所以加了一条 max pooling 路径;
这个结构符合直观理解, 视觉信息通过不同尺度的变换被聚合起来作为下一阶段的特征, 比如: 人的高矮, 胖瘦, 青老信息被聚合后做下一步判断.
这个网络的最大问题是 5×5 卷积带来了巨大计算负担, 例如, 假设上层输入为: 28×28×192:
直接经过 96 个 5×5 卷积层 (stride=1,padding=2) 后, 输出为: 28×28×96, 卷积层参数量为: 192×5×5×96=460800;
借鉴 NIN 网络 (Network in Network, 后续会讲), 在 5×5 卷积前使用 32 个 1×1 卷积核做维度缩减, 变成 28×28×32, 之后经过 96 个 5×5 卷积层(stride=1,padding=2) 后, 输出为: 28×28×96, 但所有卷积层的参数量为: 192×1×1×32+32×5×5×96=82944, 可见整个参数量是原来的 1/5.5, 且效果上没有多少损失.
新网络结构为
图 2 新 Inception 结构
GoogLeNet 网络结构
利用上述 Inception 模块构建 GoogLeNet, 实验表明 Inception 模块出现在高层特征抽象时会更加有效(我理解由于其结构特点, 更适合提取高阶特征, 让它提取低阶特征会导致特征信息丢失), 所以在低层依然使用传统卷积层. 整个网路结构如下:
图 3 GoogLeNet 网络结构
图 4 GoogLeNet 详细网络结构示意图
网络说明:
所有卷积层均使用 ReLU 激活函数, 包括做了 1×1 卷积降维后的激活;
移除全连接层, 像 NIN 一样使用 Global Average Pooling, 使得 Top 1 准确率提高 0.6%, 但由于 GAP 与类别数目有关系, 为了方便大家做模型 fine-tuning, 最后加了一个全连接层;
与前面的 ResNet 类似, 实验观察到, 相对浅层的神经网络层对模型效果有较大的贡献, 训练阶段通过对 Inception(4a,4d)增加两个额外的分类器来增强反向传播时的梯度信号, 但最重要的还是正则化作用, 这一点在 GoogLeNet v3 中得到实验证实, 并间接证实了 GoogLeNet V2 中 BN 的正则化作用, 这两个分类器的 loss 会以 0.3 的权重加在整体 loss 上, 在模型 inference 阶段, 这两个分类器会被去掉;
用于降维的 1×1 卷积核个数为 128 个;
全连接层使用 1024 个神经元;
使用丢弃概率为 0.7 的 Dropout 层;
网络结构详细说明:
输入数据为 224×224×3 的 RGB 图像, 图中 "S" 代表做 same-padding,"V" 代表不做.
C1 卷积层: 64 个 7×7 卷积核(stride=2,padding=3), 输出为: 112×112×64;
P1 抽样层: 64 个 3×3 卷积核(stride=2), 输出为 56×56×64, 其中: 56=(112-3+1)/2+1
C2 卷积层: 192 个 3×3 卷积核(stride=1,padding=1), 输出为: 56×56×192;
P2 抽样层: 192 个 3×3 卷积核(stride=2), 输出为 28×28×192, 其中: 28=(56-3+1)/2+1, 接着数据被分出 4 个分支, 进入 Inception (3a)
Inception (3a): 由 4 部分组成
64 个 1×1 的卷积核, 输出为 28×28×64;
96 个 1×1 的卷积核做降维, 输出为 28×28×96, 之后 128 个 3×3 卷积核(stride=1,padding=1), 输出为: 28×28×128
16 个 1×1 的卷积核做降维, 输出为 28×28×16, 之后 32 个 5×5 卷积核(stride=1,padding=2), 输出为: 28×28×32
192 个 3×3 卷积核(stride=1,padding=1), 输出为 28×28×192, 进行 32 个 1×1 卷积核, 输出为: 28×28×32
最后对 4 个分支的输出做 "深度" 方向组合, 得到输出 28×28×256, 接着数据被分出 4 个分支, 进入 Inception (3b);
Inception (3b): 由 4 部分组成
128 个 1×1 的卷积核, 输出为 28×28×128;
128 个 1×1 的卷积核做降维, 输出为 28×28×128, 进行 192 个 3×3 卷积核(stride=1,padding=1), 输出为: 28×28×192
32 个 1×1 的卷积核做降维, 输出为 28×28×32, 进行 96 个 5×5 卷积核(stride=1,padding=2), 输出为: 28×28×96
256 个 3×3 卷积核(stride=1,padding=1), 输出为 28×28×256, 进行 64 个 1×1 卷积核, 输出为: 28×28×64
最后对 4 个分支的输出做 "深度" 方向组合, 得到输出 28×28×480;
后面结构以此类推.
用 PaddlePaddle 实现 GoogLeNet
1. 网络结构 https://github.com/PaddlePaddle/models/blob/develop/image_classification/googlenet.py
在 PaddlePaddle 的 models 下面, 有关于 GoogLeNet 的实现代码, 大家可以直接学习拿来跑一下:
- import paddle.v2 as paddle
- __all__ = ['googlenet']
- def inception(name, input, channels, filter1, filter3R, filter3, filter5R,
- filter5, proj):
- cov1 = paddle.layer.img_conv(
- name=name + '_1',
- input=input,
- filter_size=1,
- num_channels=channels,
- num_filters=filter1,
- stride=1,
- padding=0)
- cov3r = paddle.layer.img_conv(
- name=name + '_3r',
- input=input,
- filter_size=1,
- num_channels=channels,
- num_filters=filter3R,
- stride=1,
- padding=0)
- cov3 = paddle.layer.img_conv(
- name=name + '_3',
- input=cov3r,
- filter_size=3,
- num_filters=filter3,
- stride=1,
- padding=1)
- cov5r = paddle.layer.img_conv(
- name=name + '_5r',
- input=input,
- filter_size=1,
- num_channels=channels,
- num_filters=filter5R,
- stride=1,
- padding=0)
- cov5 = paddle.layer.img_conv(
- name=name + '_5',
- input=cov5r,
- filter_size=5,
- num_filters=filter5,
- stride=1,
- padding=2)
- pool1 = paddle.layer.img_pool(
- name=name + '_max',
- input=input,
- pool_size=3,
- num_channels=channels,
- stride=1,
- padding=1)
- covprj = paddle.layer.img_conv(
- name=name + '_proj',
- input=pool1,
- filter_size=1,
- num_filters=proj,
- stride=1,
- padding=0)
- cat = paddle.layer.concat(name=name, input=[cov1, cov3, cov5, covprj])
- return cat
- def googlenet(input, class_dim):
- # stage 1
- conv1 = paddle.layer.img_conv(
- name="conv1",
- input=input,
- filter_size=7,
- num_channels=3,
- num_filters=64,
- stride=2,
- padding=3)
- pool1 = paddle.layer.img_pool(
- name="pool1", input=conv1, pool_size=3, num_channels=64, stride=2)
- # stage 2
- conv2_1 = paddle.layer.img_conv(
- name="conv2_1",
- input=pool1,
- filter_size=1,
- num_filters=64,
- stride=1,
- padding=0)
- conv2_2 = paddle.layer.img_conv(
- name="conv2_2",
- input=conv2_1,
- filter_size=3,
- num_filters=192,
- stride=1,
- padding=1)
- pool2 = paddle.layer.img_pool(
- name="pool2", input=conv2_2, pool_size=3, num_channels=192, stride=2)
- # stage 3
- ince3a = inception("ince3a", pool2, 192, 64, 96, 128, 16, 32, 32)
- ince3b = inception("ince3b", ince3a, 256, 128, 128, 192, 32, 96, 64)
- pool3 = paddle.layer.img_pool(
- name="pool3", input=ince3b, num_channels=480, pool_size=3, stride=2)
- # stage 4
- ince4a = inception("ince4a", pool3, 480, 192, 96, 208, 16, 48, 64)
- ince4b = inception("ince4b", ince4a, 512, 160, 112, 224, 24, 64, 64)
- ince4c = inception("ince4c", ince4b, 512, 128, 128, 256, 24, 64, 64)
- ince4d = inception("ince4d", ince4c, 512, 112, 144, 288, 32, 64, 64)
- ince4e = inception("ince4e", ince4d, 528, 256, 160, 320, 32, 128, 128)
- pool4 = paddle.layer.img_pool(
- name="pool4", input=ince4e, num_channels=832, pool_size=3, stride=2)
- # stage 5
- ince5a = inception("ince5a", pool4, 832, 256, 160, 320, 32, 128, 128)
- ince5b = inception("ince5b", ince5a, 832, 384, 192, 384, 48, 128, 128)
- pool5 = paddle.layer.img_pool(
- name="pool5",
- input=ince5b,
- num_channels=1024,
- pool_size=7,
- stride=7,
- pool_type=paddle.pooling.Avg())
- dropout = paddle.layer.addto(
- input=pool5,
- layer_attr=paddle.attr.Extra(drop_rate=0.4),
- act=paddle.activation.Linear())
- out = paddle.layer.fc(
- input=dropout, size=class_dim, act=paddle.activation.Softmax())
- # fc for output 1
- pool_o1 = paddle.layer.img_pool(
- name="pool_o1",
- input=ince4a,
- num_channels=512,
- pool_size=5,
- stride=3,
- pool_type=paddle.pooling.Avg())
- conv_o1 = paddle.layer.img_conv(
- name="conv_o1",
- input=pool_o1,
- filter_size=1,
- num_filters=128,
- stride=1,
- padding=0)
- fc_o1 = paddle.layer.fc(
- name="fc_o1",
- input=conv_o1,
- size=1024,
- layer_attr=paddle.attr.Extra(drop_rate=0.7),
- act=paddle.activation.Relu())
- out1 = paddle.layer.fc(
- input=fc_o1, size=class_dim, act=paddle.activation.Softmax())
- # fc for output 2
- pool_o2 = paddle.layer.img_pool(
- name="pool_o2",
- input=ince4d,
- num_channels=528,
- pool_size=5,
- stride=3,
- pool_type=paddle.pooling.Avg())
- conv_o2 = paddle.layer.img_conv(
- name="conv_o2",
- input=pool_o2,
- filter_size=1,
- num_filters=128,
- stride=1,
- padding=0)
- fc_o2 = paddle.layer.fc(
- name="fc_o2",
- input=conv_o2,
- size=1024,
- layer_attr=paddle.attr.Extra(drop_rate=0.7),
- act=paddle.activation.Relu())
- out2 = paddle.layer.fc(
- input=fc_o2, size=class_dim, act=paddle.activation.Softmax())
- return out, out1, out2
2. 训练模型
- import gzip
- import paddle.v2.dataset.flowers as flowers
- import paddle.v2 as paddle
- import reader
- import vgg
- import resnet
- import alexnet
- import googlenet
- import argparse
- DATA_DIM = 3 * 224 * 224
- CLASS_DIM = 102
- BATCH_SIZE = 128
- def main():
- # parse the argument
- parser = argparse.ArgumentParser()
- parser.add_argument(
- 'model',
- help='The model for image classification',
- choices=['alexnet', 'vgg13', 'vgg16', 'vgg19', 'resnet', 'googlenet'])
- args = parser.parse_args()
- # PaddlePaddle init
- paddle.init(use_gpu=True, trainer_count=7)
- image = paddle.layer.data(
- name="image", type=paddle.data_type.dense_vector(DATA_DIM))
- lbl = paddle.layer.data(
- name="label", type=paddle.data_type.integer_value(CLASS_DIM))
- extra_layers = None
- learning_rate = 0.01
- if args.model == 'alexnet':
- out = alexnet.alexnet(image, class_dim=CLASS_DIM)
- elif args.model == 'vgg13':
- out = vgg.vgg13(image, class_dim=CLASS_DIM)
- elif args.model == 'vgg16':
- out = vgg.vgg16(image, class_dim=CLASS_DIM)
- elif args.model == 'vgg19':
- out = vgg.vgg19(image, class_dim=CLASS_DIM)
- elif args.model == 'resnet':
- out = resnet.resnet_imagenet(image, class_dim=CLASS_DIM)
- learning_rate = 0.1
- elif args.model == 'googlenet':
- out, out1, out2 = googlenet.googlenet(image, class_dim=CLASS_DIM)
- loss1 = paddle.layer.cross_entropy_cost(
- input=out1, label=lbl, coeff=0.3)
- paddle.evaluator.classification_error(input=out1, label=lbl)
- loss2 = paddle.layer.cross_entropy_cost(
- input=out2, label=lbl, coeff=0.3)
- paddle.evaluator.classification_error(input=out2, label=lbl)
- extra_layers = [loss1, loss2]
- cost = paddle.layer.classification_cost(input=out, label=lbl)
- # Create parameters
- parameters = paddle.parameters.create(cost)
- # Create optimizer
- optimizer = paddle.optimizer.Momentum(
- momentum=0.9,
- regularization=paddle.optimizer.L2Regularization(rate=0.0005 *
- BATCH_SIZE),
- learning_rate=learning_rate / BATCH_SIZE,
- learning_rate_decay_a=0.1,
- learning_rate_decay_b=128000 * 35,
- learning_rate_schedule="discexp", )
- train_reader = paddle.batch(
- paddle.reader.shuffle(
- flowers.train(),
- # To use other data, replace the above line with:
- # reader.train_reader('train.list'),
- buf_size=1000),
- batch_size=BATCH_SIZE)
- test_reader = paddle.batch(
- flowers.valid(),
- # To use other data, replace the above line with:
- # reader.test_reader('val.list'),
- batch_size=BATCH_SIZE)
- # Create trainer
- trainer = paddle.trainer.SGD(
- cost=cost,
- parameters=parameters,
- update_equation=optimizer,
- extra_layers=extra_layers)
- # End batch and end pass event handler
- def event_handler(event):
- if isinstance(event, paddle.event.EndIteration):
- if event.batch_id % 1 == 0:
- print "\nPass %d, Batch %d, Cost %f, %s" % (
- event.pass_id, event.batch_id, event.cost, event.metrics)
- if isinstance(event, paddle.event.EndPass):
- with gzip.open('params_pass_%d.tar.gz' % event.pass_id, 'w') as f:
trainer.save_parameter_to_tar(f)
- result = trainer.test(reader=test_reader)
- print "\nTest with Pass %d, %s" % (event.pass_id, result.metrics)
- trainer.train(
- reader=train_reader, num_passes=200, event_handler=event_handler)
- if __name__ == '__main__':
- main()
3. 运行方式
1 python train.py googlenet
其中最后的 googlenet 是可选的网络模型, 输入其他的网络模型, 如 alexnet,vgg3,vgg6 等就可以用不同的网络结构来训练了.
用 Tensorflow 实现 GoogLeNet
tensorflow 的实现在 models 里有非常详细的代码, 这里就不全部贴出来了, 大家可以在
https://github.com/tensorflow/models/tree/master/research/slim/nets
https://github.com/tensorflow/models/tree/master/research/slim/nets https://github.com/tensorflow/models/tree/master/research/slim/nets https://github.com/tensorflow/models/tree/master/research/slim/nets https://github.com/tensorflow/models/tree/master/research/slim/nets https://github.com/tensorflow/models/tree/master/research/slim/nets https://github.com/tensorflow/models/tree/master/research/slim/nets / https://github.com/tensorflow/models/tree/master/research/slim/nets 里详细看看, 关于 InceptionV1~InceptionV4 的实现都有.
ps: 这里的 slim 不是 tensorflow 的 contrib 下的 slim, 是 models 下的 slim, 别弄混了, slim 可以理解为 Tensorflow 的一个高阶 api, 在构建这些复杂的网络结构时, 可以直接调用 slim 封装好的网络结构就可以了, 而不需要从头开始写整个网络结构. 关于 slim 的详细大家可以在网上搜索, 非常方便.
总结
其实 GoogLeNet 的最关键的一点就是提出了 Inception 结构, 这有个什么好处呢, 原来你想要提高准确率, 需要堆叠更深的层, 增加神经元个数等, 堆叠到一定层可能结果的准确率就提不上去了, 因为参数更多了啊, 模型更复杂, 更容易过拟合了, 但是在实验中转向了更稀疏但是更精密的结构同样可以达到很好的效果, 说明我们可以照着这个思路走, 继续做, 所以后面会有 InceptionV2 ,V3,V4 等, 它表现的结果也非常好. 给我们传统的通过堆叠层提高准确率的想法提供了一个新的思路.
来源: http://ai.51cto.com/art/201804/568978.htm