(四)卷积神经网络 -- 11 残差网络(ResNet)

11. 残差网络(ResNet)

问题:
对神经网络模型添加新的层,充分训练后的模型是否只可能更有效地降低训练误差?

理论上,如果能将新添加的层训练成恒等映射 f ( x ) = x f(x)=x f(x)=x,新模型和原模型将同样有效。
由于新模型可能得出更优的解来拟合训练数据集,因此添加层似乎更容易降低训练误差。

然而在实践中,添加过多的层后训练误差往往不降反升。
即使利用批量归一化带来的数值稳定性使训练深层模型更加容易,该问题仍然存在。

针对这一问题,何恺明等人提出了残差网络(ResNet)。它在2015年的ImageNet图像识别挑战赛夺魁,并深刻影响了后来的深度神经网络的设计。


11.1 残差块

如下图所示,设输入为 x x x,希望学出的理想映射为 f ( x ) f(x) f(x) —— 作为图中上方激活函数的输入。

左图虚线框中的部分需要直接拟合出该映射 f ( x ) f(x) f(x),而右图虚线框中的部分则需要拟合出有关恒等映射的残差映射 f ( x ) − x f(x)−x f(x)x


残差映射在实际中往往更容易优化。

以恒等映射 f ( x ) = x f(x)=x f(x)=x为例,作为希望学出的理想映射 f ( x ) f(x) f(x),只需将上图中右图虚线框内上方的加权运算(如仿射)的权重和偏差参数学成0,则 f ( x ) f(x) f(x)即为恒等映射。

实际中,当理想映射 f ( x ) f(x) f(x)极接近于恒等映射时,残差映射也易于捕捉恒等映射的细微波动。


上图中的右图也是ResNet的基础块,即残差块(residual block)。
在残差块中,输入可通过跨层的数据线路更快地向前传播。


ResNet沿用了VGG全3×3卷积层的设计。

残差块里首先有2个有相同输出通道数的3×3卷积层。每个卷积层后接一个批量归一化层和ReLU激活函数。

然后将输入跳过这两个卷积运算后直接加在最后的ReLU激活函数前。

这样的设计要求两个卷积层的输出与输入形状一样,从而可以相加。
如果想改变通道数,就需要引入一个额外的1×1卷积层来将输入变换成需要的形状后再做相加运算。


残差块的代码实现如下:

import tensorflow as tf

import numpy as np

from tensorflow.keras import Sequential, Model
from tensorflow.keras.layers import Conv2D, BatchNormalization, Activation, MaxPool2D, Layer, Dense, GlobalAvgPool2D
from tensorflow.keras.activations import relu, softmax
class Residual(Model):
    def __init__(self, num_channels, use_1x1conv=False, strides=1, **kwargs):
        super(Residual, self).__init__(**kwargs)
        self.conv1 = Conv2D(num_channels, kernel_size=3, padding='same', strides=strides)
        self.conv2 = Conv2D(num_channels, kernel_size=3, padding='same')
        if use_1x1conv:
            self.conv3 = Conv2D(num_channels, kernel_size=1, strides=strides)
        else:
            self.conv3 = None
        self.bn1 = BatchNormalization()
        self.bn2 = BatchNormalization()

    def call(self, X):
        Y = relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        return relu(Y + X)

可以设定输出通道数、是否使用额外的1×1卷积层来修改通道数以及卷积层的步幅。

查看输入和输出形状一致的情况:

blk = Residual(3)
X = tf.random.uniform((4, 6, 6, 3))
blk(X).shape
TensorShape([4, 6, 6, 3])


11.2 ResNet模型

ResNet的前两层同GoogLeNet:
在输出通道数为64、步幅为2的7×7卷积层后接步幅为2的3×3的最大池化层。

不同之处在于,ResNet每个卷积层后增加的批量归一化层。

net = Sequential([
    Conv2D(64, padding='same', kernel_size=7, strides=2),
    BatchNormalization(), 
    Activation('relu'),
    MaxPool2D(padding='same', pool_size=3, strides=2)
])

一个模块的通道数同输入通道数一致。由于之前已经使用了步幅为2的最大池化层,所以无须减小高和宽。之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半。

该模块代码实现如下:

class ResnetBlock(Layer):
    def __init__(self,num_channels, num_residuals, first_block=False,**kwargs):
        super(ResnetBlock, self).__init__(**kwargs)
        self.listLayers=[]
        for i in range(num_residuals):
            if i == 0 and not first_block:
                self.listLayers.append(Residual(num_channels, use_1x1conv=True, strides=2))
            else:
                self.listLayers.append(Residual(num_channels))      

    def call(self, X):
        for layer in self.listLayers.layers:
            X = layer(X)
        return X

其中,对第一个模块做了特殊处理。


接着为ResNet加入所有残差块:

class ResNet(Model):
    def __init__(self,num_blocks,**kwargs):
        super(ResNet, self).__init__(**kwargs)
        self.conv=Conv2D(64, kernel_size=7, strides=2, padding='same')
        self.bn=BatchNormalization()
        self.relu=Activation('relu')
        self.mp=MaxPool2D(pool_size=3, strides=2, padding='same')
        self.resnet_block1=ResnetBlock(64,num_blocks[0], first_block=True)
        self.resnet_block2=ResnetBlock(128,num_blocks[1])
        self.resnet_block3=ResnetBlock(256,num_blocks[2])
        self.resnet_block4=ResnetBlock(512,num_blocks[3])
        self.gap=GlobalAvgPool2D()
        self.fc=Dense(units=10,activation=softmax)

    def call(self, x):
        x=self.conv(x)
        x=self.bn(x)
        x=self.relu(x)
        x=self.mp(x)
        x=self.resnet_block1(x)
        x=self.resnet_block2(x)
        x=self.resnet_block3(x)
        x=self.resnet_block4(x)
        x=self.gap(x)
        x=self.fc(x)
        return x

mynet=ResNet([2,2,2,2])

其中,每个模块使用两个残差块。
同GoogLeNet,加入全局平均池化层后接上全连接层输出。


这里每个模块里存在4个卷积层(不计算 1×1卷积层),加上最开始的卷积层和最后的全连接层,共计18层。由此,该模型通常也被称为ResNet-18。

通过配置不同的通道数和模块里的残差块数可以得到不同的ResNet模型,例如更深的含152层的ResNet-152。

虽然ResNet的主体架构跟GoogLeNet类似,但ResNet结构更简单,修改也更方便。
这些因素都导致了ResNet迅速被广泛使用。


观察输入形状在ResNet不同模块之间的变化:

X = tf.random.uniform(shape=(1, 224, 224, 1))
for layer in mynet.layers:
    X = layer(X)
    print(layer.name, 'output shape:\t', X.shape)
conv2d_25 output shape:	 (1, 112, 112, 64)
batch_normalization_22 output shape:	 (1, 112, 112, 64)
activation_4 output shape:	 (1, 112, 112, 64)
max_pooling2d_3 output shape:	 (1, 56, 56, 64)
resnet_block_4 output shape:	 (1, 56, 56, 64)
resnet_block_5 output shape:	 (1, 28, 28, 128)
resnet_block_6 output shape:	 (1, 14, 14, 256)
resnet_block_7 output shape:	 (1, 7, 7, 512)
global_average_pooling2d_1 output shape:	 (1, 512)
dense_1 output shape:	 (1, 10)




参考

《动手学深度学习》(TF2.0版)

A. Krizhevsky, I. Sutskever, and G. Hinton. Imagenet classification with deep convolutional neural networks. In NIPS, 2012.

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值