ResNet原理和实现

    本文是论文Deep Residual Learning for Image Recognition.Kaiming He,Xiangyu Zhang,Shaoqing Ren,Jian Sun,etc的阅读笔记,最后给出Keras实现。


论文笔记

1.解决了什么

深层网络的很难训练的问题。

2.使用的方法

提出了残差(Residual)学习的方法。

3.实验结果

使用了一个152层的残差网络赢得ILSVRC2015图像分类的第一名,错误率降低到3.57%。

在COCO 2015目标检测比赛中获得第一,精度比之前提升了28%。

4.待解决的问题

网络达到152层,但是更深的网络错误率反而上升。


残差网络

    神经网络的深度是至关重要的,因此产生了一个问题:Is learning better networks as easy as stacking more layers? 然而搭建一个更深的网络去验证这个问题面临两个挑战。

    首先第一个是梯度消失/梯度爆炸,现在这可以通过normalized initialization和intermediate normalization layers解决。

    第二个挑战是degradation。随着网络加深, 精度饱和,接着精度下降。如下图:

1.png

    degradation不是由overfit引起的,overfit的话只是泛化能力差而已,但是训练精度是上升的。

    假设你有一个浅层的网络,你想直接在上面堆叠一个新层得到一个更深的网络,极端的情况是增加的层什么都没学习,该层的输出等于上一层的输出,这样的层被称为identity mapping(恒等映射层)。这种情况下,更深网络的性能至少和更浅网络的性能一样,也就是说,深层网络的训练误差应该是小于等于浅层网络的。但是现在出现degradation问题,证明目前的训练方法存在一些问题使得网络无法收敛。

    因此作者提出了deep residual learning框架来解决degradation问题。不是直接用网络拟合underlying mapping,而是拟合一个residual mapping。设我们想要的underlying mapping为H(x),设网络拟合的是另一个映射F(x),F(x)=H(x)-x。作者猜想神经网络更容易拟合一个residual mapping而不是一个原始的无参照的mapping。在极端情况下,如果恒等映射是最佳的,那么将残差逼近至零要比直接用非线性神经网络拟合恒等映射容易。

    H(x)=F(x)+x可以用feedforward neural networks和shortcut connection来实现,如下图:

2.png

    上图的identity shortcut connections不增加额外的参数和计算复杂度。

    深度残差网络在各种数据集上的实验表明:

    1.非常深的残差网络很容易优化,

    2深度残差网络很容易通过增加深度获得精度提升。

    3.残差学习原理具有通用性。

    多层神经网络可以渐进地近似一个复杂函数H(x),那么也可以近似残差函数F(x)=H(x)-x,原来的函数H(x)变成了F(x)+x。通过构造恒等映射,更深的网络的训练误差应该更小。但是degradation的存在表明,多层非线性层不容易近似成恒等映射。利用残差学习,如果恒等映射是最优的设计,那么可以将多个非线性层的权值趋于0逼近恒等映射。当然实际上恒等映射不一定是最优设计。

    当x和F的维度相等时,残差单元定义为:y=F(x,{Wi})+x,x是输入,{Wi}是网络参数。对于Figure2中的模块,F表示为F=W2*relu(W1*x),x和F逐元素相加得到H,称为identity shortcut connection

    当x和F的维度不相等时,可以通过线性投影到相同的维度,公式:y=F(x,{Wi})+Ws*x,Ws是投影变换参数,称为projection shortcut connection

    F里的层既可以是全连接层,也可是卷积层。当F只有一层时,y=W1*x+x,就是一个线性层,作者说这样for which we have not observed advantages。


ResNet网络

    论文定义两中模型用于对比,下图左边是VGG-19;中间是定义的Plain Network,有34参数层;右边是深度残差网络,也是34参数层,其中虚线表示带权重的shortcut。

3.png

    从上图可以看到,当feature map下采样分辨率减半时,通道数翻倍,这样保持网络层的复杂度不变;使用stride=2的卷积层进行下采样;网络的最后是global average pooling和fully-connected layer。右边的残差网络是在中间的plain networks基础上加入shortcut connection构建的。图中的实线是identity shortcut connection恒等映射,输入输出维度一致。图中的虚线输入输出的feature map的solution和channel都不同,有两种方案:1). 仍然使用恒等映射,先做一个downsample减小分辨率,增加的通道使用零填充,这个方法不增加参数。2).使用projection shortcut,通常采用卷积实现,这个方法增加额外的参数。

    在ImageNet2012数据集上评估。使用的各种层级的架构如下:

4.png

    上图中定义了不同层数的ResNet,图中每个方块就是一个残差模块,18层和34层使用的残差模块和50、101、152使用的残差模块并不相同,比如34层的网络前面已经给出了网络图。一个里面有两个卷积层另一个有三个,如下图所示:

5.png



训练参数  

数据增强:图片被缩放至短边在[256,480]范围,然后随机裁剪为224x224x3作为模型输入。而且随机水平翻转,输入图片减去数据集像素均值。使用standard color augmentation。

训练:在每次卷积后,激活前,使用BN层。使用SGD训练网络,batch_size=256。学习率初始为0.1,每次学习停滞学习率除以10。模型训练60x10^4 iterations。使用权重衰减因子为0.0001,动量为0.9,没有使用dropout。   

测试:采用标准10-crop测试,采用全卷积,平均多尺度的分数。


结果分析  

    34层和18层的plain和residual网络训练结果对比如下:

6.png

    从上图中可以发现,plain networks:34层的相比18层的有更高的训练误差。深层网络的优化困难不是由梯度消失引起的,因为使用了BN层,确保前向传播有非零方差。作者还校验了反向传播梯度,BN层是存在healthy norms的。因此作者猜想深层plain network也许存在指数级的低收敛速率,因此收敛非常困难。

    评估的residual networks是在plain networks的3x3卷积层加入shortcut构造的,观察Figure 4.可以发现34的模型表现优于18层模型,也就是说degradation问题得到了解决。对比与plain networks,34层的残差网络表现好得多,这也验证了残差学习可以帮助训练极深的网络。从Figure 4.中也可以发现plain networks和residual networks达到同样的精度,但是residual networks收敛更快,说明ResNet有助于收敛。

    再对比看看是identity shortcut和projection shortcut。定义A网络使用identity shortcut和zero-padding shortcut,所有的shortcut都是无参数的;定义B网络使用projection shortcut用于增加维度,其它使用identiry shortcut;定义C网络均使用projection shortcut。结果对比如下:

7.png

    从结果上看,B好于A,是因为零填充增加的维度实际上没有进行残差学习,C好于B,是因为projection shortcut引入了额外的参数。三个结果差别不大,证明shortcut有无参数对于解决degradation不是必须的。但是有参数的shortcut带来的计算量很大,因此不够经济。



代码实现

  1. 定义网络

from keras.models import *
from keras.layers import *
from keras import models
from keras import initializers
from keras.utils import plot_model
import keras.backend as K

def Conv2d_BN(x, nb_filter, kernel_size, strides=(1, 1), padding='same', name=None):
    if name is not None:
        bn_name = name + '_bn'
        conv_name = name + '_conv'
    else:
        bn_name = None
        conv_name = None
 
    x = Conv2D(nb_filter, kernel_size, padding=padding, strides=strides, activation='relu', name=conv_name)(x)
    x = BatchNormalization(axis=3, name=bn_name)(x)
    return x
 
 
def identity_Block(inpt, nb_filter, kernel_size, strides=(1, 1), with_conv_shortcut=False):
    x = Conv2d_BN(inpt, nb_filter=nb_filter, kernel_size=kernel_size, strides=strides, padding='same')
    x = Conv2d_BN(x, nb_filter=nb_filter, kernel_size=kernel_size, padding='same')
    if with_conv_shortcut:
        shortcut = Conv2d_BN(inpt, nb_filter=nb_filter, strides=strides, kernel_size=kernel_size)
        x = add([x, shortcut])
        return x
    else:
        x = add([x, inpt])
        return x
inputs = Input(shape=(224, 224, 3))
x = ZeroPadding2D((3, 3))(inputs)
#conv1
x = Conv2d_BN(x, nb_filter=64, kernel_size=(7, 7), strides=(2, 2), padding='valid',name='conv1')
x = MaxPooling2D(pool_size=(3, 3), strides=(2, 2), padding='same',name='maxpooling1')(x)
#conv2_x
x = identity_Block(x, nb_filter=64, kernel_size=(3, 3))
x = identity_Block(x, nb_filter=64, kernel_size=(3, 3))
x = identity_Block(x, nb_filter=64, kernel_size=(3, 3))
#conv3_x
x = identity_Block(x, nb_filter=128, kernel_size=(3, 3), strides=(2, 2), with_conv_shortcut=True)
x = identity_Block(x, nb_filter=128, kernel_size=(3, 3))
x = identity_Block(x, nb_filter=128, kernel_size=(3, 3))
x = identity_Block(x, nb_filter=128, kernel_size=(3, 3))
#conv4_x
x = identity_Block(x, nb_filter=256, kernel_size=(3, 3), strides=(2, 2), with_conv_shortcut=True)
x = identity_Block(x, nb_filter=256, kernel_size=(3, 3))
x = identity_Block(x, nb_filter=256, kernel_size=(3, 3))
x = identity_Block(x, nb_filter=256, kernel_size=(3, 3))
x = identity_Block(x, nb_filter=256, kernel_size=(3, 3))
x = identity_Block(x, nb_filter=256, kernel_size=(3, 3))
#conv5_x
x = identity_Block(x, nb_filter=512, kernel_size=(3, 3), strides=(2, 2), with_conv_shortcut=True)
x = identity_Block(x, nb_filter=512, kernel_size=(3, 3))
x = identity_Block(x, nb_filter=512, kernel_size=(3, 3))
x = AveragePooling2D(pool_size=(7, 7))(x)
x = Flatten()(x)
x = Dense(100, activation='softmax')(x)
model = Model(inputs=inputs, outputs=x)
model.summary()
plot_model(model,to_file='ResNet34.png',show_shapes=True)


2.定义数据生成器,编译并训练网络

    数据集使用 AlexNet原理和实现 里面的。

from keras.preprocessing import image
from keras import initializers
from keras import optimizers
import os
from keras.models import load_model
from keras import metrics
from keras import losses
MODEL_SAVE_PATH=r'D:/Jupyter/cv/ResNet_log/resnet34.h5'
TRAIN_DATA_PATH=r'C:/dataset/images_normal'
VAL_DATA_PATH=r'F:\BaiduNetdiskDownload\mini-imagenet\image_normal_test'
BATCH_SIZE=8
EPOCHS=1
#定义训练集生成器
train_gen=image.ImageDataGenerator(
    featurewise_center=True,#输入数据数据减去数据集均值
    rotation_range=30,#旋转的度数范围
    width_shift_range=0.2,#水平平移
    height_shift_range=0.2,#垂直平移
    shear_range=0.2,#斜切强度
    horizontal_flip=True,#水平翻转
    brightness_range=[-0.1,0.1],#亮度变化范围
    zoom_range=[0.5,1.5],#缩放的比例范围
    preprocessing_function=None,#自定义的处理函数
)
x=train_gen.flow_from_directory(
        TRAIN_DATA_PATH,
        target_size=(224,224),
        batch_size=BATCH_SIZE,
        class_mode='categorical'
)
#定义验证集生成器
val_datagen = image.ImageDataGenerator()
x_val=val_datagen.flow_from_directory(
        VAL_DATA_PATH,
        target_size=(224,224),
        batch_size=BATCH_SIZE,
        class_mode='categorical'
)
if(os.path.exists(MODEL_SAVE_PATH)==False):
    #编译模型
    model.compile(optimizer=optimizers.SGD(lr=1e-2,momentum=0.9,decay=1e-6),
                  metrics=['acc',metrics.top_k_categorical_accuracy],
                  loss=losses.categorical_crossentropy
    )
else:
    model=load_model(MODEL_SAVE_PATH)
    
#训练模型
history=model.fit_generator(
        x,
        steps_per_epoch=int(50000/BATCH_SIZE),#每回合的步数
        epochs=EPOCHS,
        #validation_data=validation_generator,
        #validation_steps=int(10000/BATCH_SIZE),
        shuffle=True,
)
#保存模型
model.save(filepath=MODEL_SAVE_PATH

首页 所有文章 机器人 计算机视觉 自然语言处理 机器学习 编程随笔 关于