Resnet V2论文阅读和代码解析

目录

论文阅读

代码解析

小结

论文阅读

1.介绍

在Resnet V1的论文中介绍的‘Residual Units'可以用公式表示如下:

x_l表示这个unit的输入,x_{l+1}表示这个unit的输出,FF是残差函数,resnet V1的论文中建议h(x_l) = x_lf是RELU。

h(x_l) = x_l是一个很关键的选择,能让网络表现不错的效果。

在这篇文章中,作者会研究信息传播的路径对结果的影响。如果h(x_l)f(y_l)都是等式映射,那么信号会直接从一个单元传播给另一个单元。作者的经验发现如果h(x_l)f(y_l)越接近等式,那么网络训练起来会更加容易。

作者分析和比较了各种形式的h(x_l),比如缩放,比如1x1的卷积计算等,都没有h(x_l) = x_l收敛速度快,loss值降得更低。这个实验表明更干净的信息传递通道可以使网络更容易接近最优值。

而为了构建f(y_l) = y_l的结构,作者将激活函数调整了位置,使用先激活,而不是之前的后激活,这样就得到了一种新的残差网络结构,而且这个结构取得了不错的成绩。

(a)是原来的残差结构,(b)是新的残差结构。可以看到改进后是先进行BN和RELU计算,然后再用weight进行计算。

2.分析深度残差网络

对于原始的残差单元h(x_l) = x_l,如果f(y_l) = y_l,那么x_{l+1} \equiv y_l,那么公式可以变成

将所有的x_l的值都带入,可以得到

这个公式显示了一些属性:

  • 任意深度的x_L都可以由x_l加上一个残差公式组成,所以就意味着x_Lx_l之间就有残差关系
  • x_L = x_0 + \sum_{0}^{L-1}F(X_i, W_i)公式也成立,这表明x_L可以表示为前面所有层的残差公式算出来的值相加,再加上x_0

根据公式4,我们进行反向传播求导

x_l的求导可以分成两个公式,一个是\frac{\partial \varepsilon }{\partial x_L},这部分跟参数w没有关系,另外一个式子是\frac{\partial \varepsilon }{\partial x_L}(\frac{\partial }{\partial x_l}\sum_{i=1}^{L-1}F),这个式子跟参数w有关系。所以信息能够反向传播到任意浅层的单元。

另外公式5也表明,因为(\frac{\partial }{\partial x_l}\sum_{i=1}^{L-1}F)不可能永远为-1,所以对x_l的求偏导不可能永远为0,所以mini batch也不会出现梯度消失的现象。

讨论

公式4和公式5表明,无论是正向传播还是反向传播,信号可以直接从任意一层传播到另一层。要公式4成立需要满足两个条件,一个是h(x_l) = x_l,另一个是f(y_l) = y_l。在上面的残差结构示意图中,用灰色通道表示这部分。如果h和f都是等式,那么灰色部分就会很干净。

3.等式连接的重要性

现在来考虑一下,假设h(x_l) = \lambda_ix_if仍然是等式,那么公式3可以变成

这样可以推导出公式

                                                                   x_L = (\prod_{i=l}^{L-1}\lambda_i)x_l + \sum_{i=l}^{L-1}(\prod_{j=i+1}^{L-1}\lambda_j)F(x_i, w_i)

次公式可以简化为

x_l求偏导可以得到公式

跟公式5非常像,只是第一个相加的式子前面需要乘以\prod_{i=l}^{L-1}\lambda_i,对于非常深的网络,如果对所有\lambda_i > 1,那么这个元素会变得很大;如果对所有\lambda_i < 1,那么这个元素会非常小并且消失掉。这样会阻碍反向传播中信息的传递,我们实验也发现网络会更难训练。

如果h(x_i)是更复杂的公式,相当于前面相乘的因子是\sum_{i=l}^{L-1}h\textup{'}_i,同样的会影响信息的传播。

作者试验了以下几种结构

图(a)就是resnet v1中提到的结构,一共有110层,有54个残差单元,每个残差单元有两层3 x 3的卷积层。现在让f都为RELU,比较一下hF不同带来的影响。

图(b)是做一个常数的缩放,\lambda=0.5。作者做了两种尝试,一种是F不做缩放,一种是F(1-\lambda )的缩放。结果测试集错误率有12.35%,比原始结构图a显示的还要高。

图(c)叫做exclusive gating,g(x) = \sigma (W_gx + b_g),其中\sigma (x) = \frac{1}{1+e^{-x}}。让Fg(x)的缩放,让h1-g(x)的缩放。这种结构对b_g的初始化要求比较高,测试下来最好的情况测试集错误率是8.7%,结果并不是太理想。因为如果1-g(x)接近1,h的shortcut路径更加接近于等式,有利于信息的传播,但是这种情况下g(x)接近0,抑制了F通道信息的传播。

图(d)针对图(c)引发的思考,考虑只对h的shortcut做1-g(x)的控制,对F不做控制。这种情况下b_g的选择也很重要,如果b_g为0则1-g(x)接近0.5,训练结果是测试集错误率高达12.86%。如果b_g选择-6,那么1-g(x)接近于1,测试集错误率是6.91%,这是因为1-g(x)接近于1就近似shortcut是等式。

图(e)是使用1x1的卷积计算来替代h的等式,实验发现如果只有16个残差单元效果还不错,但是如果残差单元增加到54个,错误率会升高到12.22%。

图(f)是在shortcut的path上使用比例为0.5的dropout,训练没有成功,0.5的dropout类似于0.5的缩放的效果,影响了信息的传播。

以上试验都可以证明在shortcut path上增加任何处理都会影响最后的结果,只有用等式效果会最好。

4.使用激活函数

在上面讨论的公式5和公式8中是假设了激活函数f为等式,但是实际上我们的激活函数一般为RELU,下面就来研究一下激活函数的影响。我们想让f为等式,就需要重新调整RELU和BN等的顺序。

图(a)是原始的结构,f为RELU

图(b)是相加后再进行BN和RELU,此时f表示BN和RELU。可以预见这种结果效果会更差,因为BN和RELU阻碍了信息的传递,试验结果也确实如此,loss在训练开始阶段下降很慢,最后的结果也不如原始的结构。

图(c)是在相加之前做RELU。一个很简单的让f变成等式的方法就是将RELU移到相加操作的前面,但是这会导致F之后输出的值永远是非负数,直觉上残差函数输出的值应该在(-\infty, +\infty)(-\infty,+\infty)之间。这会导致正向传播的信息单调增加,影响了表达能力,训练结果是错误率7.84%也确实比原始的错误率要高。

是选择前激活还是后激活呢?

对于上面的公式1和公式2,由于上一节的讨论,我们现在就认为h(x) = x,所以

y_{l+1} = h(x_{l+1}) + F(x_{l+1}, w_{l+1})

y_{l+1} = x_{l+1} + F(x_{l+1}, w_{l+1})

y_{l+1} = f(y_l) + F(f(y_l), w_{l+1})

现在假设f是一种非对称的形式,就是前面一个f(y_l) = y_l,后面一个F中的f(y_l)改写为\hat{}{f}(y_l),那么上面的公式可以变成

y_{l+1} = y_l + F(\hat{f}(y_l), w_{l+1})现在将y换成x来表达,就可以得到下面的公式(从上面的推导过程看论文中应该有错误,是w_{l+1}

现在这个公式9和公式4很像,这个公式表明我们可以应用一种非对称激活函数,在shortcut部分是等式,在残差函数部分是先进行激活函数,在与权重w进行计算。所以我们现在就要尝试使用前激活的方式,有下面两种方式。

图(d)表示调整RELU的位置进行前激活。这种结构的表现和原始的结构类似,可能是因为激活函数之前没有享受到BN带来的好处。

图(e)表示调整RELU和BN的位置进行前激活。这种结构会有比较明显的效果提升。

使用图(e)结构带来的好处是双重的:

更容易优化:这个影响在训练1001层的resnet的时候特别明显,使用原始的结构,training error在训练之初下降的非常慢,因为如果相加上进行RELU激活对传递过来的负数信息是有影响的。而如果f是等式,信息可以直接从一个单元传递到另一个单元,1001层的resnet loss下降的非常快。另外对于层数少一些的resnet比如164层,f为relu似乎对性能的影响很小。

降低过拟合:将BN放入RELU的前面可以带来正则化的效果,降低过拟合,会得到更高的精度。

5.训练结果

代码分析

代码地址:resnet_v2

resnet_v2代码的实现和resnet_v1基本都是类似的,只是在bottleneck函数的实现上稍有差别,因为bottleneck函数就代表了一个residual block。整体代码架构不清楚的可以阅读之前的一篇博文地址。在本文中我想只需要比较一下两个网络bottleneck的实现就可以很清晰的看出resnet v2的改进了。

Resnet V1

@slim.add_arg_scope
def bottleneck(inputs,
               depth,
               depth_bottleneck,
               stride,
               rate=1,
               outputs_collections=None,
               scope=None,
               use_bounded_activations=False):
...
    residual = slim.conv2d(inputs, depth_bottleneck, [1, 1], stride=1,
                           scope='conv1')
    residual = resnet_utils.conv2d_same(residual, depth_bottleneck, 3, stride,
                                        rate=rate, scope='conv2')
    residual = slim.conv2d(residual, depth, [1, 1], stride=1,
                           activation_fn=None, scope='conv3')

    if use_bounded_activations:
      # Use clip_by_value to simulate bandpass activation.
      residual = tf.clip_by_value(residual, -6.0, 6.0)
      output = tf.nn.relu6(shortcut + residual)
    else:
      output = tf.nn.relu(shortcut + residual)
...

从可以可以很清楚的看到是先做了卷积运算(BN和RELU包含在slim.conv2d中实现,是先卷积,然后BN,最后RELU),在相加后再做了RELU计算。

Resnet V2

@slim.add_arg_scope
def bottleneck(inputs, depth, depth_bottleneck, stride, rate=1,
               outputs_collections=None, scope=None):
...
    preact = slim.batch_norm(inputs, activation_fn=tf.nn.relu, scope='preact')
...
    residual = slim.conv2d(preact, depth_bottleneck, [1, 1], stride=1,
                           scope='conv1')
    residual = resnet_utils.conv2d_same(residual, depth_bottleneck, 3, stride,
                                        rate=rate, scope='conv2')
    residual = slim.conv2d(residual, depth, [1, 1], stride=1,
                           normalizer_fn=None, activation_fn=None,
                           scope='conv3')

    output = shortcut + residual

代码中可以看出是先调用了slim.batch_norm进行了BN和RELU,然后是正常的卷积,在相加后没有做任何计算了。

小结

Resnet V2的论文写的非常清晰易懂,公式也不复杂,基本是猜想加上实验的方式得出的结论,是一篇很容易阅读的论文。提出的二代残差结构对训练非常深的网络,比如1001层,会有很好的效果。

以上为本文所有内容,感谢阅读,欢迎留言。

猜你喜欢

转载自blog.csdn.net/stesha_chen/article/details/82589829