经典卷积神经网络算法(5):ResNet
1 引言¶
神经网络算法有时候又被称为深度学习,究其原因就是因为神经网络模型可以通过添加网络层数来扩展网络的深度以获得更优越的性能。以CNN网络为例,卷积层数越多,模型越能够提取到更高层次的特征,信息更加丰富。所以,我们不禁要猜想,是不是网络的深度越深,模型的性能越好。如果真是这样,那神经网络就是近乎无所不能的算法,没有什么是添加一层网络不能解决的,如果有,那就添加两层。
在前面几篇博客中,我们实现了LeNet、AlexNet、VGG、GoogLeNet等几个景点的卷积神经网络算法,这些算法层数对比如下所示:
初步一看,可以发现网络的层数确实是在增加的,但如果这就认为网络层数越多,模型性能越高,那就太天真了(如果真这么简单,当时的这些学者直接将网络层数提升到成千上万层,性能不比上表中一二十次高得多?还有这几个算法什么事)。事实确实也是如此,经过反复验证,人们发现,当网络层数到达一定数量时,继续添加网络层数,模型性能不但得不到提升,反而下降。
这是为什么呢?
针对这一问题进行探索、优化,最终就有了本文的主角——残差网络(ResNet)的诞生。
2 退化现象¶
ResNet是我国华人学者何凯明团队在其论文《Deep Residual Learning for Image Recognition》中提出。在改论文中,作者先对网络深度与模型性能的关系进行探索,如下所示:
对比上面两图红黄两线,可以发现,网络层数多的(56层)训练效果远比网络层数少(20层)模型差。其实,在其他诸多实验也发现,随着网络层级的不断增加,模型精度不断得到提升,而当网络层级增加到一定的数目以后,训练精度和测试精度迅速下降,这说明当网络变得很深以后,深度网络就变得更加难以训练了。
为什么会出现这种现象呢?
VGGNet和GoogLeNet等网络都表明有足够的深度是模型表现良好的前提,但是在网络深度增加到一定程度时,更深的网络意味着更高的训练误差。 神经网络在反向传播过程中通过链式法则不断地传播梯度,而当网络层数加深时,梯度在传播过程中会逐渐消失(假如采用Sigmoid函数,对于幅度为1的信号,每向后传递一层,梯度就衰减为原来的0.25,层数越多,衰减越厉害),导致无法对前面网络层的权重进行有效的调整,导致训练和测试效果变差,这一现象称为退化。
那么,该如何解决退化问题,使得在加深网络层数的同时,又能解决梯度消失问题,从而提升模型精度呢?
3 残差网络(ResNet)¶
通过前文分析我们知道了,网络模型的深度到达一定量时,模型的精度就不再随着深度的提升而增加,更有可能会跟着降低。这里,我们有一个设想,假设模型总共有100层,但是模型在第10层的时候精度就达到了巅峰,那么,后续的90层都是多余的,我们不需要它对输出结果进行任何改变,这90层只需要保证第10层网络的输出能够原模原样映射到最后一层即可——这就是本文的主角——残差网络(ResNet)算法灵感的来源。在论文中,作者将后续90层这种将输入原样输出的映射层为恒等映射。
我们回到ResNet算法来看看恒等映射具体是如何发挥作用的。如下图所示,假设$x$为浅层网络的输出,在原始的网络中,$x$会继续前向传播,经过至少一层网络后输出为$F(x)$,在ResNet网络中,从浅层玩那个罗的输出$x$到$F(x)$之间添加了一条捷径,这条捷径不会对$x$做任何改变,保留原样输出,所以,在图中捷径与原始网络的交汇处将获得的输出变为$H(x)=F(x)+x$此时,$F(x)=H(x)-x$,浅层网络后续的更深层次网络只需要对残差$H(x)-x$进行拟合,这也是为什么作者将这一模型称为残差网络的原因。如果浅层网络的输出,也就是$x$的进度已经到达最优,那么,$H(x)=x$将是最优的输出,$F(x)$只需要对$0$进行拟合,这样的拟合会比没有残差时的拟合简单得多。另外,也因为有恒等映射的捷径存在,因此在反向传播过程中梯度的传导也多了更简便的路径,仅仅经过一个relu就可以把梯度传达给上一个模块。
这种残差跳跃式的结构,打破了传统的神经网络n-1层的输出只能给n层作为输入的惯例,使某一层的输出可以直接跨过几层作为后面某一层的输入,其意义在于为叠加多层网络而使得整个学习模型的错误率不降反升的难题提供了新的方向。
当然,在卷积网络的残差块设计中,并不都是设计成恒等映射,这也是允许的,如下图所示。
- 实线连接部分表示通道数不变,如图中的第二个矩形和第三个矩形,都是3x3x64的特征图,由于通道相同,所以采用计算方式为$H(x)=F(x)+x$;
- 虚线连接部分表示通道发生变化,如上图的第一个绿色矩形和第三个绿色矩形,分别是3x3x64和3x3x128的特征图,通道不同,采用的计算方式为$H(x)=F(x)+Wx$,其中$W$是卷积操作,用来调整$x$的通道数。
在论文中,作者进一步证明了深度残差网络的确解决了退化问题,如下图所示,左图为平原网络(plain network)网络层次越深(34层)比网络层次浅的(18层)的误差率更高;右图为残差网络ResNet的网络层次越深(34层)比网络层次浅的(18层)的误差率更低。
至此,神经网络的层数可以超越之前的约束,达到几十层、上百层甚至千层,为更高层次特征提取和分类提供了可行性。
4 代码实现¶
import os
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, Sequential, datasets, optimizers
os.environ["TF_CPP_MIN_LOG_LEVEL"]='2'
tf.random.set_seed(2345)
# 使用cifar100数据集
(x_train, y_train), (x_test, y_test) = datasets.cifar100.load_data()
def preprocess(x, y):
x = tf.cast(x, dtype=tf.float32) / 255. # 将每个像素值映射到[0, 1]内
y = tf.cast(y, dtype=tf.float32)
return x, y
y_train = tf.squeeze(y_train, axis=1)
y_test = tf.squeeze(y_test, axis=1)
train_db = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_db = train_db.shuffle(1000).map(preprocess).batch(256)
test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_db = test_db.shuffle(1000).map(preprocess).batch(256)
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,input,training=None):
out=self.conv1(input)
out=self.bn1(out)
out=self.relu(out)
out=self.conv2(out)
out=self.bn2(out)
identity=self.downsample(input)
output=layers.add([out,identity])
output=tf.nn.relu(output)
return output
class ResNet(keras.Model):
def __init__(self,layer_dims,num_classes=100):
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')
])
# resblock
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)
# there are [b,512,h,w]
# 自适应
self.avgpool=layers.GlobalAveragePooling2D()
self.fc=layers.Dense(num_classes)
def call(self,input,training=None):
x=self.stem(input)
x=self.layer1(x)
x=self.layer2(x)
x=self.layer3(x)
x=self.layer4(x)
# [b,c]
x=self.avgpool(x)
x=self.fc(x)
return x
def build_resblock(self,filter_num,blocks, stride=1):
res_blocks= Sequential()
# may down sample
res_blocks.add(BasicBlock(filter_num, stride))
# just down sample one time
for pre in range(1, blocks):
res_blocks.add(BasicBlock(filter_num, stride=1))
return res_blocks
def resnet18():
return ResNet([2, 2, 2, 2])
def main():
model=resnet18()
model.build(input_shape=(None,32,32,3))
model.summary()
optimizer=optimizers.Adam(lr=1e-3)
for epoch in range(50):
for step,(x,y) in enumerate(train_db):
with tf.GradientTape() as tape:
logits=model(x)
y = tf.cast(y, tf.int32)
y_onehot=tf.one_hot(y,depth=100)
loss=tf.losses.categorical_crossentropy(y_onehot,logits,from_logits=True)
loss=tf.reduce_mean(loss)
grads=tape.gradient(loss,model.trainable_variables)
optimizer.apply_gradients(zip(grads,model.trainable_variables))
if step%100==0:
print(epoch,step,'loss',float(loss))
total_num=0
total_correct=0
for x,y in test_db:
logits=model(x)
prob=tf.nn.softmax(logits,axis=1)
pred=tf.argmax(prob,axis=1)
pred=tf.cast(pred,dtype=tf.int32)
correct=tf.cast(tf.equal(pred,y),dtype=tf.int32)
correct=tf.reduce_sum(correct)
total_num+=x.shape[0]
total_correct+=int(correct)
acc=total_correct/total_num
print(epoch,'acc:',acc)
main()
Model: "res_net_2" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= sequential_16 (Sequential) multiple 2048 _________________________________________________________________ sequential_17 (Sequential) multiple 148736 _________________________________________________________________ sequential_18 (Sequential) multiple 526976 _________________________________________________________________ sequential_20 (Sequential) multiple 2102528 _________________________________________________________________ sequential_22 (Sequential) multiple 8399360 _________________________________________________________________ global_average_pooling2d_2 ( multiple 0 _________________________________________________________________ dense_2 (Dense) multiple 51300 ================================================================= Total params: 11,230,948 Trainable params: 11,223,140 Non-trainable params: 7,808 _________________________________________________________________ 0 0 loss 4.609687805175781
作者:奥辰
微信号:chb1137796095
Github:https://github.com/ChenHuabin321
欢迎加V交流,共同学习,共同进步!
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。