深度学习第19讲:CNN经典论文研读之残差网络ResNet及其keras实现

简介:

在 VGG 网络论文研读中,我们了解到卷积神经网络也可以进行到很深层,VGG16 和 VGG19 就是证明。但卷积网络变得更深呢?当然是可以的。深度神经网络能够从提取图像各个层级的特征,使得图像识别的准确率越来越高。但在2014年和15年那会儿,将卷积网络变深且取得不错的训练效果并不是一件容易的事。

深度卷积网络一开始面临的最主要的问题是梯度消失和梯度爆炸。那什么是梯度消失和梯度爆炸呢?所谓梯度消失,就是在深层神经网络的训练过程中,计算得到的梯度越来越小,使得权值得不到更新的情形,这样算法也就失效了。而梯度爆炸则是相反的情况,是指在神经网络训练过程中梯度变得越来越大,权值得到疯狂更新的情形,这样算法得不到收敛,模型也就失效了。当然,其间通过设置 relu 和归一化激活函数层等手段使得我们很好的解决这些问题。但当我们将网络层数加到更深时却发现训练的准确率在逐渐降低。这种并不是由过拟合造成的神经网络训练数据识别准确率降低的现象我们称之为退化(degradation)。

1353079b5027a8ff52d6d14332fd5bb03b8f49fe

由上图我们可以看到 56 层的普通卷积网络不管是在训练集还是测试集上的训练误差都要高于 20 层的卷积网络。是个典型的退化现象。

这退化问题不解决,咱们的深度学习就无法 go deeper. 于是何凯明等一干大佬就发明了今天我们要研读的论文主题——残差网络 ResNet.

残差块与残差网络

要理解残差网络,就必须理解残差块(residual block)这个结构,因为残差块是残差网络的基本组成部分。回忆一下我们之前学到的各种卷积网络结构(LeNet-5/AlexNet/VGG),通常结构就是卷积池化再卷积池化,中间的卷积池化操作可以很多层。类似这样的网络结构何凯明在论文中将其称为普通网络(Plain Network),何凯明认为普通网络解决不了退化问题,我们需要在网络结构上作出创新。

何凯明给出的创新在于给网络之间添加一个捷径(shortcuts)或者也叫跳跃连接(skip connection),这使得捷径之间之间的网络能够学习一个恒等函数,使得在加深网络的情形下训练效果至少不会变差。残差块的基本结构如下:

c90e0c934ee31924f65cb40b9f124b437eab7088

以上残差块是一个两层的网络结构,输入 X 经过两层的加权和激活得到 F(X) 的输出,这是典型的普通卷积网络结构。但残差块的区别在于添加了一个从输入 X 到两层网络输出单元的 shortcut,这使得输入节点的信息单元直接获得了与输出节点的信息单元通信的能力 ,这时候在进行 relu 激活之前的输出就不再是 F(X) 了,而是 F(X)+X。当很多个具备类似结构的这样的残差块组建到一起时,残差网络就顺利形成了。残差网络能够顺利训练很深层的卷积网络,其中能够很好的解决网络的退化问题。

或许你可能会问凭什么加了一条从输入到输出的捷径网络就能防止退化训练更深层的卷积网络?或是是说残差网络为什么能有效?我们将上述残差块的两层输入输出符号改为 和 ,相应的就有:

0cd281fb213b9891173e615e8f951a7991670f43

加入的跳跃连接后就有: 148fd99c8c17d608f277603ed3a367171d0621a6

在网络中加入 L2 正则化进行权值衰减或者其他情形下,l+2 层的权值 W 是很容易衰减为零的,假设偏置同样为零的情形下就有 = 。深度学习的试验表明学习这个恒等式并不困难,这就意味着,在拥有跳跃连接的普通网络即使多加几层,其效果也并不逊色于加深之前的网络效果。当然,我们的目标不是保持网络不退化,而是需要提升网络表现,当隐藏层能够学到一些有用的信息时,残差网络的效果就会提升。所以,残差网络之所以有效是在于它能够很好的学习上述那个恒等式,而普通网络学习恒等式都很困难,残差网络在两者相较中自然胜出。

由很多个残差块组成的残差网络如下图右图所示:

9030205da0f462b752ab601c7b82c97a62a9c040

残差块的 keras 实现

要实现一个残差块,关键在于实现一个跳跃连接。实际处理中跳跃连接会随着残差块输入输出大小的不同而分为两种。一种是输入输出一致情况下的 Identity Block,另一种则是输入输出不一致情形下的 Convolutional Block,顾名思义,就是跳跃连接中包含卷积操作,用来使得输入输出一致。且看二者的 keras 实现方法。

Identity Block 的图示如下:

ee8cf8de071e6a57199d8ddef9c09c86e133bf5e

编写实现代码如下:


AI 代码解读
def identity_block(X, f, filters, stage, block):
"""
Implementation of the identity block as defined in Figure 3
Arguments:
X -- input tensor of shape (m, n_H_prev, n_W_prev, n_C_prev)
f -- integer, specifying the shape of the middle CONV's window for the main path
filters -- python list of integers, defining the number of filters in the CONV layers of the main path
stage -- integer, used to name the layers, depending on their position in the network
Returns:
block -- string/character, used to name the layers, depending on their position in the network X -- output of the identity block, tensor of shape (n_H, n_W, n_C)
""" # defining name basis
conv_name_base = 'res' + str(stage) + block + '_branch'
bn_name_base = 'bn' + str(stage) + block + '_branch' # Retrieve Filters
F1, F2, F3 = filters
# Save the input value. You'll need this later to add back to the main path.
X_shortcut = X
# First component of main path
X = Conv2D(filters = F1, kernel_size = (1, 1), strides = (1,1), padding = 'valid', name = conv_name_base + '2a', kernel_initializer = glorot_uniform(seed=0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2a')(X)
X = Activation('relu')(X)
# Second component of main path
X = Conv2D(filters = F2, kernel_size = (f, f), strides= (1, 1), padding = 'same', name = conv_name_base + '2b', kernel_initializer = glorot_uniform(seed=0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2b')(X)
X = Activation('relu')(X)
# Third component of main path
X = Conv2D(filters = F3, kernel_size = (1, 1), strides = (1, 1), padding = 'valid', name = conv_name_base + '2c', kernel_initializer = glorot_uniform(seed=0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2c')(X)
# Final step: Add shortcut value to main path, and pass it through a RELU activation
X = Add()([X, X_shortcut])
X = Activation('relu')(X)
return X

可见残差块的实现特殊之处就在于添加一条跳跃连接。

Convolutional Block 的图示如下:

3128681d852260ee284a9712a196b3c6dfd64750

编写实现代码如下:


AI 代码解读
def convolutional_block(X, f, filters, stage, block, s = 2):
"""
Implementation of the convolutional block as defined in Figure 4
Arguments:
f -- integer, specifying the shape of the middle CONV's window for the main path
X -- input tensor of shape (m, n_H_prev, n_W_prev, n_C_prev)
filters -- python list of integers, defining the number of filters in the CONV layers of the main path
stage -- integer, used to name the layers, depending on their position in the network
X -- output of the convolutional block, tensor of shape (n_H, n_W, n_C)
block -- string/character, used to name the layers, depending on their position in the network s -- Integer, specifying the stride to be used Returns:
""" # defining name basis
conv_name_base = 'res' + str(stage) + block + '_branch'
bn_name_base = 'bn' + str(stage) + block + '_branch' # Retrieve Filters
F1, F2, F3 = filters
# Save the input value
X_shortcut = X
##### MAIN PATH ##### # First component of main path
X = Conv2D(filters = F1, kernel_size = (1, 1), strides = (s,s), padding = 'valid', name = conv_name_base + '2a', kernel_initializer = glorot_uniform(seed=0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2a')(X)
X = Activation('relu')(X)
# Second component of main path
X = Conv2D(filters = F2, kernel_size = (f, f), strides = (1,1), padding = 'same', name = conv_name_base + '2b', kernel_initializer = glorot_uniform(seed=0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2b')(X)
X = Activation('relu')(X)
# Third component of main path
X = Conv2D(filters = F3, kernel_size = (1, 1), strides = (1,1), padding = 'valid', name = conv_name_base + '2c', kernel_initializer = glorot_uniform(seed=0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2c')(X)
##### SHORTCUT PATH ####
X_shortcut = Conv2D(filters = F3, kernel_size = (1, 1), strides = (s, s), padding = 'valid', name = conv_name_base + '1', kernel_initializer = glorot_uniform(seed=0))(X_shortcut)
X_shortcut = BatchNormalization(axis = 3, name = bn_name_base + '1')(X_shortcut)
# Final step: Add shortcut value to main path, and pass it through a RELU activation
X = Add()([X, X_shortcut])
X = Activation('relu')(X)
return X

残差网络 resnet50 的 keras 实现

搭建好组件残差块之后就是确定网络结构,将一个个残差块组成残差网络。下面搭建一个 resnet50 的残差网络,其基本结构如下:

CONV2D -> BATCHNORM -> RELU -> MAXPOOL -> CONVBLOCK -> IDBLOCK2 -> CONVBLOCK -> IDBLOCK3 -> CONVBLOCK -> IDBLOCK5 -> CONVBLOCK -> IDBLOCK2 -> AVGPOOL -> TOPLAYER

45a852c5562e59d93e9f79f4315863ae20bf6e2e

编写实现代码如下:


AI 代码解读
def ResNet50(input_shape = (64, 64, 3), classes = 6):

# Define the input as a tensor with shape input_shape
X_input = Input(input_shape)
# Zero-Padding
X = ZeroPadding2D((3, 3))(X_input)
# Stage 1
X = Conv2D(64, (7, 7), strides = (2, 2), name = 'conv1', kernel_initializer = glorot_uniform(seed=0))(X)
X = BatchNormalization(axis = 3, name = 'bn_conv1')(X)
X = Activation('relu')(X)
X = MaxPooling2D((3, 3), strides=(2, 2))(X)
# Stage 2
X = convolutional_block(X, f = 3, filters = [64, 64, 256], stage = 2, block='a', s = 1)
X = identity_block(X, 3, [64, 64, 256], stage=2, block='b')
X = identity_block(X, 3, [64, 64, 256], stage=2, block='c')
# Stage 3
X = convolutional_block(X, f = 3, filters = [128, 128, 512], stage = 3, block='a', s = 2)
X = identity_block(X, 3, [128, 128, 512], stage=3, block='b')
X = identity_block(X, 3, [128, 128, 512], stage=3, block='c')
X = identity_block(X, 3, [128, 128, 512], stage=3, block='d')
# Stage 4
X = convolutional_block(X, f = 3, filters = [256, 256, 1024], stage = 4, block='a', s = 2)
X = identity_block(X, 3, [256, 256, 1024], stage=4, block='b')
X = identity_block(X, 3, [256, 256, 1024], stage=4, block='c')
X = identity_block(X, 3, [256, 256, 1024], stage=4, block='d')
X = identity_block(X, 3, [256, 256, 1024], stage=4, block='e')
X = identity_block(X, 3, [256, 256, 1024], stage=4, block='f')
# Stage 5
X = convolutional_block(X, f = 3, filters = [512, 512, 2048], stage = 5, block='a', s = 2)
X = identity_block(X, 3, [512, 512, 2048], stage=5, block='b')
X = identity_block(X, 3, [512, 512, 2048], stage=5, block='c')
# AVGPOOL (≈1 line). Use "X = AveragePooling2D(...)(X)"
X = AveragePooling2D((2, 2), strides=(2, 2))(X)
# output layer
X = Flatten()(X)
X = Dense(classes, activation='softmax', name='fc' + str(classes), kernel_initializer = glorot_uniform(seed=0))(X)
# Create model
model = Model(inputs = X_input, outputs = X, name='ResNet50')
return model

这样一个 resnet50 的残差网络就搭建好了,其关键还是在于搭建残差块,残差块搭建好之后只需根据网络结构构建残差网络即可。当然,其间也可以看到 keras 作为一个优秀的深度学习框架的便利之处。


原文发布时间为:2018-10-13

本文作者:louwill

本文来自云栖社区合作伙伴“Python爱好者社区”,了解相关信息可以关注“Python爱好者社区”。

相关文章
NSDI'24 | 阿里云飞天洛神云网络论文解读——《Poseidon》揭秘新型超高性能云网络控制器
NSDI'24 | 阿里云飞天洛神云网络论文解读——《Poseidon》揭秘新型超高性能云网络控制器
117 63
基于ssm的网络直播带货管理系统,附源码+数据库+论文
该项目为网络直播带货网站,包含管理员和用户两个角色。管理员可进行主页、个人中心、用户管理、商品分类与信息管理、系统及订单管理;用户可浏览主页、管理个人中心、收藏和订单。系统基于Java开发,采用B/S架构,前端使用Vue、JSP等技术,后端为SSM框架,数据库为MySQL。项目运行环境为Windows,支持JDK8、Tomcat8.5。提供演示视频和详细文档截图。
53 10
SIGCOMM 2024 | 洛神云网络学术活动与论文分享
SIGCOMM 2024 | 洛神云网络学术活动与论文分享
NSDI'24 | 阿里云飞天洛神云网络论文解读——《LuoShen》揭秘新型融合网关 洛神云网关
NSDI'24 | 阿里云飞天洛神云网络论文解读——《LuoShen》揭秘新型融合网关 洛神云网关
网络安全与信息安全:知识分享####
【10月更文挑战第21天】 随着数字化时代的快速发展,网络安全和信息安全已成为个人和企业不可忽视的关键问题。本文将探讨网络安全漏洞、加密技术以及安全意识的重要性,并提供一些实用的建议,帮助读者提高自身的网络安全防护能力。 ####
106 17
网络安全与信息安全:关于网络安全漏洞、加密技术、安全意识等方面的知识分享
随着互联网的普及,网络安全问题日益突出。本文将从网络安全漏洞、加密技术和安全意识三个方面进行探讨,旨在提高读者对网络安全的认识和防范能力。通过分析常见的网络安全漏洞,介绍加密技术的基本原理和应用,以及强调安全意识的重要性,帮助读者更好地保护自己的网络信息安全。
72 10
网络安全与信息安全:关于网络安全漏洞、加密技术、安全意识等方面的知识分享
随着互联网的普及,网络安全问题日益突出。本文将介绍网络安全的重要性,分析常见的网络安全漏洞及其危害,探讨加密技术在保障网络安全中的作用,并强调提高安全意识的必要性。通过本文的学习,读者将了解网络安全的基本概念和应对策略,提升个人和组织的网络安全防护能力。
网络安全与信息安全:关于网络安全漏洞、加密技术、安全意识等方面的知识分享
在数字化时代,网络安全和信息安全已成为我们生活中不可或缺的一部分。本文将介绍网络安全漏洞、加密技术和安全意识等方面的内容,并提供一些实用的代码示例。通过阅读本文,您将了解到如何保护自己的网络安全,以及如何提高自己的信息安全意识。
92 10
网络安全与信息安全:漏洞、加密与意识的交织
在数字时代的浪潮中,网络安全与信息安全成为维护数据完整性、保密性和可用性的关键。本文深入探讨了网络安全中的漏洞概念、加密技术的应用以及提升安全意识的重要性。通过实际案例分析,揭示了网络攻击的常见模式和防御策略,强调了教育和技术并重的安全理念。旨在为读者提供一套全面的网络安全知识框架,从而在日益复杂的网络环境中保护个人和组织的资产安全。
云计算与网络安全:云服务、网络安全、信息安全等技术领域的融合与挑战
本文将探讨云计算与网络安全之间的关系,以及它们在云服务、网络安全和信息安全等技术领域中的融合与挑战。我们将分析云计算的优势和风险,以及如何通过网络安全措施来保护数据和应用程序。我们还将讨论如何确保云服务的可用性和可靠性,以及如何处理网络攻击和数据泄露等问题。最后,我们将提供一些关于如何在云计算环境中实现网络安全的建议和最佳实践。