- 使用keras实作 ResNet,DenseNet
ResNet
核心原理
- 通常我们直观的感觉让网络更深一定能学到更多的特征,但事实并非如此,增加深度带来的首个问题就是梯度爆炸/消失,这是由于随着层数的增多,在网络中反向传播的梯度会随着连乘变得不稳定,变得特别大或者特别小。同时随着深度的增加,会出现网络衰退(degradation),这说明越深的网络反向梯度越难传导。
- ResNet解决的就是这个问题,ResNet作者认为网络的浅层应该是深度网络的子集,比浅层网络更深的网络至少不会有更差的结果,但是因为网络衰退的问题,这假设并不成立。于是作者提出residual模块来实现一种前后的恒等映射(identity mapping),让深度网络后面的层至少等于前面层的效果。
- 下图是论文中提出的residual概念模块,X为浅层输出,F(X)为中间两层对X卷积后的输出,将F(X)与X累加作为最终输出,这意味着如果当X学到的特征足够好,如果更新会使loss增加的话,那F(X)部分将在训练过程中趋向于学习成0,从而实现一种恒等映射。
- 除此外,residual模块更有利于梯度对参数的更新,residual模块会明显减网络中参数值的大小,使参数对反向传播的梯度更敏感。eg:假设X=20,经过中间两层F(X)后输出Y=30(将中间两层简化看成线性运算)。在没有residual模块时,可算得F的参数W=1.5,在有residual模块时,因为F(X)+X = 30,所以F的参数W=0.5。若此时假设真值32,对于输出30,Loss为2,对Loss计算的梯度假设是0.2时,可直观看出0.2相对于W=0.5是2/5,而对W=1.5是2/15,相同梯度对两种参数的影响力一目了然。
- 所以对于深度网络来所,residual模块虽然没有解决网络深后回传的损失小的问题,但是却让参数减小,从而相对增加了回传损失的效果。
网络结构
- 在论文中作者提出了两种实作residual模块,如下图,左边为基本模块,右图为bottleneck residual模块,通过采用1*1卷积核增加非线性和减小输出的深度以减小参数量,在更深的网络中起到更佳的效果。
- 下图为论文中设计的几种网络结构,50层以上的网络结构都采用bottleneck residual模块。
keras实作
以ResNet50为例,这里采用卷积块是Conv+Relu+BN,其实还可以采用BN+Relu+Conv
当然里面的小细节就不赘述无伤大雅。
def Conv2D_BN(inputs,filter,kernel,padding,stride):
outputs = keras.layers.Conv2D(filters=filter,kernel_size=kernel,padding=padding,strides=stride,activation='relu')(inputs)
outputs = keras.layers.BatchNormalization()(outputs)
return outputs
def residual_block(inputs,filter,stride,whether_identity_change=False):
x = Conv2D_BN(inputs, filter[0], kernel=(1,1), padding='same', stride=stride)
x = Conv2D_BN(x, filter[1], kernel=(3,3), padding='same', stride=1)
x = Conv2D_BN(x, filter[2] ,kernel=(1,1), padding='same', stride=1)
# 累加必须保持尺寸一致,控制恒等层是否需要变channel数和压缩尺寸
if whether_identity_change:
identity = Conv2D_BN(inputs, filter[2], kernel=(1,1), padding='same', stride=stride)
x = keras.layers.add([x,identity])
return x
else:
x = keras.layers.add([x,inputs])
return x
def ResNet()
inputs = keras.Input(shape=(224,224,3))
x = Conv2D_BN(inputs,64,(7,7),'same',2)
x = keras.layers.MaxPool2D(pool_size=(3,3), strides=2, padding='same')(x)
x = residual_block(x,[64,64,256],1,True)
x = residual_block(x,[64,64,256],1)
x = residual_block(x,[64,64,256],1)
x = residual_block(x,[128,128,512],2,True)
x = residual_block(x,[128,128,512],1)
x = residual_block(x,[128,128,512],1)
x = residual_block(x,[128,128,512],1)
x = residual_block(x,[256,256,1024],2,True)
x = residual_block(x,[256,256,1024],1)
x = residual_block(x,[256,256,1024],1)
x = residual_block(x,[256,256,1024],1)
x = residual_block(x,[256,256,1024],1)
x = residual_block(x,[256,256,1024],1)
x = residual_block(x,[512,512,2048],2,True)
x = residual_block(x,[512,512,2048],1)
x = residual_block(x,[512,512,2048],1)
x = keras.layers.AveragePooling2D(pool_size=(7,7))(x)
x = keras.layers.Flatten()(x)
x = keras.layers.Dense(17,activation='softmax')(x)
model = keras.Model(inputs=inputs,outputs=x)
model.summary()
return model
DenseNet
核心原理
- 有了ResNet这样里程碑式的网络架构,在其基础上进一步催生了这个更强大的网络DenseNet。
- DenseNet作者发现,当对ResNet随机Dropout一些层可以显著提高ResNet泛化性能,说明了 ResNet 具有比较高的冗余性,网络中的每一层都只提取了很少的特征(即所谓的残差)。另外也发现神经网络其实并不一定要是一个递进层级结构,就是说网络中的某一层不仅仅依赖于上一层的特征,而可以依赖于更前面层学习的特征。
- 于是作者将网络中的每一层都直接与前面所有层相连,实现特征的重复利用;同时把网络的每一层设计得特别「窄」,即只学习非常少的特征图(最极端情况就是每一层只学习一个特征图),达到降低冗余性的目的。因此DenseNet诞生。
- 如下图,卷积块组合为BN-ReLU-Conv,输入为6通道X0,经过第一个卷积块H1,输出4通道X1,在输入H2前对X0和X1进行拼接,注意ResNet中是将特征图进行累加而DenseNet是拼接,即H2的输入是10通道的X。每层都将与前面所有层连接。
- 当然CNN网络不能避免的需要通过池化层做特征整合提取进行下采样,这样就会出现池化层后的特征图大小与之前不一致无法进行拼接的问题。于是DendeNet设计成由多个密集块(Dense Block)组合成,如下图:
- 每一个密集块内的卷积块采用 BN-Relu-Conv(1x1) - BN-Relu-Conv(3x3) 结构又称为Bottleneck Layers(瓶颈层),后面的卷积块都是连接了它前面所有卷积块的。在密集块间采用 Conv(1x1)-Pooling 结构过度,又称为Transition Layers(压缩层)。
- 瓶颈层中BN-Relu-Conv(1x1)作用是将输入通道固定转化成K*4通道的输出,再经过BN-Relu-Conv(3x3)固定输出K通道的特征图,之所有需要用BN-Relu-Conv(1x1)是为了整合并减少特征,因为就算每次拼接K个通道,当网络很深时也会变得非常巨大。(K称为增长率)
- 压缩层中的Conv(1x1)起到的作用与瓶颈层中是一致的,将输出通道数固定为输入通道数的1/2。下图是论文中设计的几种网络架构。
Keras实作
- 以DenseNet-121(k=32)为例。
def Conv2D_BN(inputs,growth_rate):
nfilter = growth_rate * 4
outputs = keras.layers.Activation('relu')(inputs)
outputs = keras.layers.BatchNormalization()(outputs)
outputs = keras.layers.Conv2D(filters = nfilter,kernel_size = (1,1),padding='same',strides=1)(outputs)
outputs = keras.layers.Activation('relu')(outputs )
outputs = keras.layers.BatchNormalization()(outputs)
outputs = keras.layers.Conv2D(filters = growth_rate,kernel_size=(3,3),padding='same',strides=1)(outputs)
return outputs
def dense_block(inputs,growth_rate,n_filter,layers):
concat = inputs
for i in range(layers):
outputs = Conv2D_BN(concat,growth_rate)
concat = keras.layers.concatenate([outputs,concat])
n_filter += growth_rate
return concat,n_filter
def transition_block(inputs,n_filter):
print(n_filter)
nfilter = int(n_filter*0.5)
outputs = keras.layers.Activation('relu')(inputs)
outputs = keras.layers.BatchNormalization()(outputs)
outputs = keras.layers.Conv2D(filters = nfilter,kernel_size = (1,1),padding='same',strides=1)(outputs )
outputs = keras.layers.AveragePooling2D(pool_size=2)(outputs)
return outputs,nfilter
def get_model():
nfilter = 32
growth_rate = 32
inputs = keras.Input(shape=(224,224, 3))
x = keras.layers.Conv2D(filters = nfilter,kernel_size=(7,7),padding='same',strides=2,activation='relu')(inputs)
x = keras.layers.BatchNormalization()(x)
x = keras.layers.MaxPooling2D(pool_size=2)(x)
x,nfilter = dense_block(x,growth_rate,nfilter,6)
x,nfilter = transition_block(x,nfilter)
x,nfilter = dense_block(x,growth_rate,nfilter,12)
x,nfilter = transition_block(x,nfilter)
x,nfilter = dense_block(x,growth_rate,nfilter,24)
x,nfilter = transition_block(x,nfilter)
x,nfilter = dense_block(x,growth_rate,nfilter,16)
x = keras.layers.GlobalAveragePooling2D()(x)
x = keras.layers.Dense(1000,activation='softmax')(x)
model = keras.Model(inputs=inputs,outputs=x)
return model