ResNet
ResNet(Residual Neural Network)由微软研究院的Kaiming He等4名华人提出,通过使用Residual Unit成功训练152层深的神经网络,在ILSVRC 2015比赛中获得了冠军,取得3.57%的top-5错误率,同时参数量却比VGGNet低,效果非常突出。
ResNet的结构可以极快地加速超深神经网络的训练,模型的准确率也有非常大的提升。
ResNet最初的灵感出自这个问题:在不断加神经网络的深度时,会出现一个Degradation的问题,即准确率会先上升然后达到饱和,再持续增加深度则会导致准确率下降。
这并不是过拟合的问题,因为不光在测试集上误差增大,训练集本身误差也会增大。假设有一个比较浅的网络达到了饱和的准确率,那么后面再加上几个的全等映射层,起码误差不会增加,即更深的网络不应该带来训练集上误差上升。
而这里提到的使用全等映射直接将前一层输出传到后面的思想,就是ResNet的灵感来源。假定某段神经网络的输入是x,期望输出是H(x),如果我们直接把输入x传到输出作为初始结果,那么此时我们需要学习的目标就是F(x)=H(x)-x。
这就是一个ResNet的残差学习单元(Residual Unit),ResNet相当于将学习目标改变了,不再是学习一个完整的输出H(x),只是输出和输入的差别H(x)-x,即残差。
上图所示为VGGNet-19,以及一个34层深的普通卷积网络,和34层深的ResNet网络的对比图。可以看到普通直连的卷积神经网络和ResNet的最大区别在于,ResNet有很多旁路的支线将输入直接连到后面的层,使得后面的层可以直接学习残差,这种结构也被称为shortcut或skip connections。
传统的卷积层或全连接层在信息传递时,或多或少会存在信息丢失、损耗等问题。ResNet在某种程度上解决了这个问题,通过直接将输入信息绕道传到输出,保护信息的完整性,整个网络则只需要学习输入、输出差别的那一部分,简化学习目标和难度。
上图所示为ResNet在不同层数时的网络配置,其中基础结构很类似,都是前面提到的两层和三层的残差学习单元的堆叠。
在使用了ResNet的结构后,可以发现层数不断加深导致的训练集上误差增大的现象被消除了,ResNet网络的训练误差会随着层数增大而逐渐减小,并且在测试集上的表现也会变好。
代码实现
import tensorflow as tf
from tensorflow.keras import layers, Sequential
from tensorflow import keras
# basic Block模块
class BasicBlock(layers.Layer):
def __init__(self, filter_num, stride=1):
super(BasicBlock, self).__init__()
self.c1 = layers.Conv2D(filter_num, (3, 3), strides=stride, padding='same')
self.b1 = layers.BatchNormalization()
self.relu = layers.Activation('relu')
self.c2 = layers.Conv2D(filter_num, (3, 3), strides=1, padding='same')
self.b2 = 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):
out = self.c1(inputs)
out = self.b1(out)
out = self.relu(out)
out = self.c2(out)
out = self.b2(out)
identity = self.downsample(inputs)
out = layers.add([out, identity])
output = tf.nn.relu(out)
return output
class ResNet(keras.Model):
def __init__(self, layer_dims, num_class=100):
super(ResNet, self).__init__()
# 预处理层;实现起来比较灵活可以加 MAXPool2D,可以没有。
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))])
# 创建4个Res Block;注意第1项不一定以2倍形式扩张,都是比较随意的,这里都是经验值。
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_class)
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)
# 做一个global average pooling,得到之后只会得到一个channel,不需要做reshape操作了。
x = self.avgpool(x)
x = self.fc(x)
return x
def build_resblock(self, filter_num, blocks, stride=1):
res_blocks = Sequential()
# 对于当前Res Block中的Basic Block,我们要求每个Res Block只有一次下采样的能力
res_blocks.add(BasicBlock(filter_num, stride))
for _ in range(1, blocks):
res_blocks.add(BasicBlock(filter_num, stride=1)) # 这里stride设置为1,只会在第一个Basic Block做一个下采样。
return res_blocks
def resnet18():
return ResNet([2, 2, 2, 2])
model = resnet18()
model.build(input_shape=(None, 32, 32, 3))
model.summary()