个人博客:http://www.chenjianqu.com/
原文链接:http://www.chenjianqu.com/show-59.html
本文是论文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。随着网络加深, 精度饱和,接着精度下降。如下图:
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来实现,如下图:
上图的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。
从上图可以看到,当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数据集上评估。使用的各种层级的架构如下:
上图中定义了不同层数的ResNet,图中每个方块就是一个残差模块,18层和34层使用的残差模块和50、101、152使用的残差模块并不相同,比如34层的网络前面已经给出了网络图。一个里面有两个卷积层另一个有三个,如下图所示:
训练参数
数据增强:图片被缩放至短边在[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网络训练结果对比如下:
从上图中可以发现,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。结果对比如下:
从结果上看,B好于A,是因为零填充增加的维度实际上没有进行残差学习,C好于B,是因为projection shortcut引入了额外的参数。三个结果差别不大,证明shortcut有无参数对于解决degradation不是必须的。但是有参数的shortcut带来的计算量很大,因此不够经济。
代码实现
-
定义网络
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