Deep Residual Net 分析和实现

Deep residual net 微软亚洲研究院视觉计算组提出的工作, 开创了152层的deep network, 在ILSVRC2015的几个主要类别中取得第一名. 而且, 网络的 tansfer 性能非常好, 在MS COCO(Microsoft COmmon Objects in Context)中同样获得冠军.

Deep Residual Learning for Image Recognition

Deep residual network 可以看做是 Highway networks 的一个特例. 一个直观的想法是, 如果一个 shallow network 经过训练之后得到了一个比较好的解, 那么, 如果把它做成 deep 的版本, 其结果至少应该不能比 shallow 的版本差. 但是, 事实并不是如此, 这种现象, 在论文中作者称为是 degradation problem. 那么如何解决这个问题呢? 这就是作者提出的 residual network.
假设我要去逼近一个函数 \(H(x)\), 作者去 fit a residual mapping, 而不是去 fit the desired underlying mapping. 也就是说, 去 fit \(F(x)=H(x)-x\), 那么原始的 desired underlying mapping 就是 \(F(x)+x\). 考虑一种极端的情况, 假设 identity mapping 是最优化的, 那么, 原来的方法的优化目标是 \(H(x)=x\), 注意, 这里的 \(H(x)\) 是非线性映射, residual network 的优化目标是 \(H(x)=F(x)+x\), 所以只需要优化 \(F(x)=0\) 就可以了, 显然, 这个优化目标要简单地多.

实现方法


residual net 的一个 building block 如上图所示. 可以看到, 与传统的网络中的唯一的区别就是输入 x 直接做一个 identity map 加到输出上面去.

维数匹配

这里可能会有人问为什么还要做 identity map, 为什么不直接加到输出上面? 这是因为要考虑到输入和输出的维数不想同的情况. 例如在下图中的情况.


输入是 64 维, 而下一个的 building block 维数是 128, 此时必须要做维数匹配.

stride=2 的 convolution 操作

上图中可以看到, 作者没有使用 pooling 操作, 而是使用 stride=2 的 conv 代替了 pooling. 之前曾经看过一篇文章是讲 conv 降维至少不会比 pooling 效果差. 找到了再补充.

代码实现

MXNet 的实现方法要比 CAFFE 方便很多, 所以, 我用 MXNet 来实现.

基本的 convolution 操作的封装

首先我们要定义一个 bulid block, 而 build block 中主要部分就是 conv-BN-activation, 所以, 我们先定义一个 ConvFactory 来封装上述 3 个操作.

1
2
3
4
5
6
7
8
9
def ConvFactory(data, num_filter, kernel, stride=(1, 1), pad=(0, 0), act_type = 'relu',last=False):
# 只有在一个build block 输出的时候才有activation 操作
conv = mx.symbol.Convolution(data = data, num_filter = num_filter, kernel = kernel, stride = stride, pad = pad)
if last:
return bn
else:
bn = mx.symbol.BatchNorm(data=conv)
act = mx.symbol.Activation(data=bn, act_type=act_type)
return act

Residual Net 的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def ResidualFactory(data, num_filter, diff_dim=False):
if diff_dim:
conv1 = ConvFactory(data=data, num_filter=num_filter[0], kernel=(3,3), stride=(2,2), pad=(1,1), last=False)
conv2 = ConvFactory(data=conv1, num_filter=num_filter[1], kernel=(3,3), stride=(1,1), pad=(1,1), last=True)
# 输入的build block的维数和当前build block的维数不同, 所以, 使用conv操作来进行维数匹配
_data = mx.symbol.Convolution(data=data, num_filter=num_filter[1], kernel=(3,3), stride=(2,2), pad=(1,1))
data = _data+conv2
bn = mx.symbol.BatchNorm(data=data)
act = mx.symbol.Activation(data=bn, act_type='relu')
return act
else:
_data=data
conv1 = ConvFactory(data=data, num_filter=num_filter[0], kernel=(3,3), stride=(1,1), pad=(1,1), last=False)
conv2 = ConvFactory(data=conv1, num_filter=num_filter[1], kernel=(3,3), stride=(1,1), pad=(1,1), last=True)
data = _data+conv2
bn = mx.symbol.BatchNorm(data=data)
act = mx.symbol.Activation(data=bn, act_type='relu')
return act

组合成 Deep Residual Network

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def ResidualSymbol(data, n=9):
# 通过改变n的值可以得到论文中几种层数的网络
"stage 1"
for i in xrange(n):
data = ResidualFactory(data, (16, 16))
"stage 2"
for i in xrange(n):
if i == 0:
data = ResidualFactory(data, (32, 32), True)
else:
data = ResidualFactory(data, (32, 32))
"stage 3"
for i in xrange(n):
if i == 0:
data = ResidualFactory(data, (64, 64), True)
else:
data = ResidualFactory(data, (64, 64))
return data

实现备注说明

  1. 使用的是 \(1 \times 1\) 的卷积操作来实现的维数匹配, 而不是论文中的 identity mapping
  2. 修改 ResidualSymboln 的值为3, 5, 7, 9分别对应的网络层数为20, 32, 44, 56
  3. 最终结果训练准确率为 98% 左右, 测试准确率为 90%
  4. 代码是我很久之前为了练习使用 MXNet 写的, 后来 Kaiming 公开了模型, 发现有很多地方与原始论文都有区别
  5. 完整的代码可以在我的github上面下载到.

参考文献

Deep Residual Learning for Image Recognition
CAFFE model of Deep Residual Networks