注:阅读本博客之前,你需要先掌握:全连接神经网络,卷积神经网络的基本原理。
随着LeNet-5,AlexNet,VGG等神经网络结构的出现,卷积神经网络逐渐从单纯的只拥有卷积操作和下采样的神经网络发展为具有max pooling、dropout以及非线性函数的神经网络。在网络结构变得不断复杂的同时,人们发现,神经网络的效率并没有预期得到提升,反而容易出现梯度消失等情况,因而导致loss难以减少等现象。
对于出现梯度消失的原因,大家可以参考这篇:
https://blog.csdn.net/jasonleesjtu/article/details/89185185
ResNet通过在两个卷积层之间添加短路(shortcut)的方式,有效地解决了在神经网络层数不断增加的情况下难以训练的问题。
上图所示就是一个Basic Block。其中,weight layer可看成是卷积层,F(x)是x通过两个卷积层之后所学习到的。该结构的特点,就是在两个卷积层外面添加了一条shortcut,使得x经过两个卷积层之后可以以x+F(x)的形式输出。
那么,为什么这种结构可以有效地解决因网络层数增加而导致的模型难以训练的问题呢?我们以解决梯度消失为例:
y l y_l yl=h( x l x_l xl)+F( x l x_l xl, w l w_l wl)
x l + 1 x_{l+1} xl+1=f( y l y_l yl)
其中: x l x_l xl和 x l + 1 x_{l+1} xl+1分别是这个Basic Block的输入与输出项;h( x l x_l xl)= x l x_l xl,即identity,ResNet的核心;F( x l x_l xl, w l w_l wl)是x通过两个卷积层之后所学习到的,也称为残差(residual);f表示relu函数。
如果两个卷积层之间还有其他卷积层或者一般层,则可以如下表示:
x L x_L xL= x l x_l xl+ ∑ i = 1 L − 1 F ( x i , w i ) \sum_{i=1}^{L-1} F(x_i,w_i) ∑i=1L−1F(xi,wi)
d l o s s d x l \left. \frac{dloss}{dx_l} \right. dxldloss= d l o s s d x L \left. \frac{dloss}{dx_L} \right. dxLdloss* d x L d x l \left. \frac{dx_L}{dx_l} \right. dxldxL= d l o s s d x L \left. \frac{dloss}{dx_L} \right. dxLdloss(1+ d ∑ i = 1 L − 1 F ( x i , w i ) d x l \left. \frac{d{\sum_{i=1}^{L-1} F(x_i,w_i)}}{dx_l} \right. dxld∑i=1L−1F(xi,wi))
其中,1表示shortcut可以无条件地继承梯度,当 d ∑ i = 1 L − 1 F ( x i , w i ) d x l \left. \frac{d{\sum_{i=1}^{L-1} F(x_i,w_i)}}{dx_l} \right. dxld∑i=1L−1F(xi,wi)接近0的时候,模型仍可保持网络层数较少时的梯度,因此Basic Block可以有效解决因网络层数增加而导致的模型难以训练的问题。
ResNet的一大特点,就是:当feature map的大小减少一半时,feature map的数量增加一倍。
在h( x l x_l xl)+F( x l x_l xl, w l w_l wl)的时候,很多人可能会想到维度不同的问题:以 x l x_l xl的维度为[b,32,32,3]为例,如果经过了stride=2的卷积层,维度(大小)会减少为[b,16,16,c],这时在与identity相加之前,一般会先用1*1的卷积核(kernel)对 x l x_l xl进行同步长卷积处理,这样就完成了当feature map的大小减少一半时,feature map的数量增加一倍的目的;当然也可以0来填充增加出来的维度。
ResBlock由多个Basic Block连接而成。
ResNet18包含开始的一个卷积层,4个ResBlock(每个ResBlock包含2个Basic Block,每个Basic Block包含两个卷积层),和最后的一个全连接层,即:1+4*4+1=18。
导入相关的包
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, Sequential
定义Basic Block
#定义Basic Block
class BasicBlock(layers.Layer):
def __init__(self, filter_num, stride=1):
super(BasicBlock, self).__init__()
#第一小块
self.conv1 = layers.Conv2D(filter_num, (3, 3), strides=stride, padding='same')
self.bn1 = layers.BatchNormalization()
self.relu = layers.Activation('relu')
#第二小块
self.conv2 = layers.Conv2D(filter_num, (3, 3), strides=1, padding='same')
self.bn2 = layers.BatchNormalization()
if stride != 1:
self.downsample =Sequential()
self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
else:
self.downsample = lambda x:x
def call(self, inputs, training=None):
identity = self.downsample(inputs)
out = self.conv1(inputs)
out = self.bn1(out,training=training)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out,training=training)
out = layers.add([out, identity])
out = tf.nn.relu(out)
return out
代码解释:
1、第一小块和第二小块分别是下图的两个weight layer。
2、判断stride是否为1,若stride=1,说明卷积前后维度相同,可以直接相加;stride!=1,则进行同stride卷积操作,再进行相加。
定义ResNet
#定义ResNet
class ResNet(keras.Model):
def build_resblock(self, filter_num, blocks, stride=1):
res_blocks = Sequential()
res_blocks.add(BasicBlock(filter_num, stride))
for _ in range(1, blocks):
res_blocks.add(BasicBlock(filter_num, stride=1))
return res_blocks
def __init__(self, layer_dims, num_classes=10):#mnist有10类
super(ResNet, self).__init__()
self.stem = Sequential([layers.Conv2D(64, (3, 3), strides=(1, 1)),
layers.BatchNormalization(),
layers.Activation('relu'),
layers.MaxPool2D(pool_size=(2, 2), strides=(1, 1), padding='same')
])
self.layer1 = self.build_resblock(64, layer_dims[0])
self.layer2 = self.build_resblock(128, layer_dims[1], stride=2)
self.layer3 = self.build_resblock(256, layer_dims[2], stride=2)
self.layer4 = self.build_resblock(512, layer_dims[3], stride=2)
self.avgpool = layers.GlobalAveragePooling2D()
self.fc = layers.Dense(num_classes)
def call(self, inputs, training=None):
x = self.stem(inputs,training=training)
x = self.layer1(x,training=training)
x = self.layer2(x,training=training)
x = self.layer3(x,training=training)
x = self.layer4(x,training=training)
x = self.avgpool(x)
x = self.fc(x)
return x
代码解释:
1、build_resblock根据给定的blocks值,构建ResBlock。
2、GlobalAveragePooling2D()可以在不确定输出维度的情况下,把[b,512,h,w]变成[b,512],值为每一个channel上所有h,w像素的均值。
定义ResNet18。
def ResNet18():
return ResNet([2, 2, 2, 2])
由于定义ResNet的代码较长,我们可以把它保存,然后当作包使用,例如:保存为resnet.py,在需要时
from resnet import ResNet18
model=ResNet18(),即可。
1.https://www.jianshu.com/p/ec0967460d08