目录
介绍深度神经网络的经典-ResNet。
禁止转载,侵权必究!
前言
上一章中我们结合iChallenge-PM问题介绍了LeNet,AlexNet,VGG。明显可以看到随着神经网络能力越强,网络越复杂,层数越多。那是不是层数越深的CNN网络就一定效果越好呢?
答案是否定的,直到ResNet出现。
我们来看看ResNet的设计者—何恺明博士是如何解决这个问题的:
假定:
H(x) = F(x) + x
其中H(x)是更深的网络,F(x)是较浅的网络。
这个假设是什么意思呢?何恺明博士发现在众多公开的数据集上,简单加深网络层数,训练loss和测试loss不但不会减小,反而还会增加。那么假定F(x)这个浅层网络之后的每一层都是恒等网络y=x,因此一个更深的网络H(x)就可以表达为上述假定。简而言之,就是假定更深的层学不到新的特征图了,那么之后的层都是恒等层。
简单加深网络层数,训练loss和测试loss不但不会减小,反而还会增加。还反映出普通的CNN网络有什么样的问题呢?
简而言之,更深的卷积层conv连恒等映射y=x都不如。因为在恒等映射下,更深网络H(x)应该和浅层网络F(x)表现相同,也就是说loss应该稳定下来,而不是增加。
那么给卷积层输出y增加输入x会怎么样呢?
何恺明博士依据这个思路,修改了普通CNN网络,对比如下:
图(b)所示网络就是H(x) = F(x) + x的图形化表示。我们只要把N个图(b)串联起来,就可以得到比普通CNN图a所示更好的神经网络—ResNet了。
何恺明博士把图(b)简称为残差块。
残差块编码
1.残差块定义
# 定义残差块
# 每个残差块会对输入图片做三次卷积,然后跟输入图片进行短接
# 如果残差块中第三次卷积输出特征图的形状与输入不一致,则对输入图片做1x1卷积,将其输出形状调整成一致
class BottleneckBlock(fluid.dygraph.Layer):
def __init__(self,
num_channels,
num_filters,
stride,
shortcut=True):
super(BottleneckBlock, self).__init__()
# 创建第一个卷积层 1x1
self.conv0 = ConvBNLayer(
num_channels=num_channels,
num_filters=num_filters,
filter_size=1,
act='relu')
# 创建第二个卷积层 3x3
self.conv1 = ConvBNLayer(
num_channels=num_filters,
num_filters=num_filters,
filter_size=3,
stride=stride,
act='relu')
# 创建第三个卷积 1x1,但输出通道数乘以4
self.conv2 = ConvBNLayer(
num_channels=num_filters,
num_filters=num_filters * 4,
filter_size=1,
act=None)
# 如果conv2的输出跟此残差块的输入数据形状一致,则shortcut=True
# 否则shortcut = False,添加1个1x1的卷积作用在输入数据上,使其形状变成跟conv2一致
if not shortcut:
self.short = ConvBNLayer(
num_channels=num_channels,
num_filters=num_filters * 4,
filter_size=1,
stride=stride)
self.shortcut = shortcut
self._num_channels_out = num_filters * 4
def forward(self, inputs):
y = self.conv0(inputs)
conv1 = self.conv1(y)
conv2 = self.conv2(conv1)
# 如果shortcut=True,直接将inputs跟conv2的输出相加
# 否则需要对inputs进行一次卷积,将形状调整成跟conv2输出一致
if self.shortcut:
short = inputs
else:
short = self.short(inputs)
y = fluid.layers.elementwise_add(x=short, y=conv2)
layer_helper = LayerHelper(self.full_name(), act='relu')
return layer_helper.append_activation(y)
注意三个卷积层的卷积核的大小分别为:1×1 3×3 1×1
- 如果输入输出数组形状相等: x–>conv0–>conv1–>conv2–>+x–>y
- 如果输入输出数组形状不等:x–>conv0–>conv1–>conv2–>+short()–>y
2.残差块的每个Layer的代码实现:
# ResNet中使用了BatchNorm层,在卷积层的后面加上BatchNorm以提升数值稳定性
# 定义卷积批归一化块
class ConvBNLayer(fluid.dygraph.Layer):
def __init__(self,
num_channels,
num_filters,
filter_size,
stride=1,
groups=1,
act=None):
"""
num_channels, 卷积层的输入通道数
num_filters, 卷积层的输出通道数
stride, 卷积层的步幅
groups, 分组卷积的组数,默认groups=1不使用分组卷积
act, 激活函数类型,默认act=None不使用激活函数
"""
super(ConvBNLayer, self).__init__()
# 创建卷积层
self._conv = Conv2D(
num_channels=num_channels,
num_filters=num_filters,
filter_size=filter_size,
stride=stride,
padding=(filter_size - 1) // 2,
groups=groups,
act=None,
bias_attr=False)
# 创建BatchNorm层
self._batch_norm = BatchNorm(num_filters, act=act)
def forward(self, inputs):
y = self._conv(inputs)
y = self._batch_norm(y)
return y
我们通常会把输入数据做归一化,方便模型使用。经过实践,科学家发现在模型的中间层,数据也需要做归一化,这个归一化的方法就是:BatchNorm
深入了解数据形状
W(kernel_size):M-输出特征(通道)数 C-输入通道数 H-高度 W-宽度
1-conv层输出特征图计算公式: (224 + 2x padding – Kh)/2 +1 = (224 + 2 x (7-1)//2 – 7)/2 +1 = (224 + 2 x 3 -7)/2 +1 = 112.5 ≈ 112,其中(7-1)//2是在ConvBNLayer中定义的。
paddle-paddle 卷积向下取整,池化向上取整。
1-conv-- kernel_size MCHW:[64, 3, 7, 7], padding:[3, 3], stride:[2, 2]
conv0-- kernel_size MCHW:[64, 64, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[64, 64, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[256, 64, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_0
conv0-- kernel_size MCHW:[64, 256, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[64, 64, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[256, 64, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_1
conv0-- kernel_size MCHW:[64, 256, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[64, 64, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[256, 64, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_2
conv0-- kernel_size MCHW:[128, 256, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[128, 128, 3, 3], padding:[1, 1], stride:[2, 2]
conv2-- kernel_size MCHW:[512, 128, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_3
conv0-- kernel_size MCHW:[128, 512, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[128, 128, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[512, 128, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_4
conv0-- kernel_size MCHW:[128, 512, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[128, 128, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[512, 128, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_5
conv0-- kernel_size MCHW:[128, 512, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[128, 128, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[512, 128, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_6
conv0-- kernel_size MCHW:[256, 512, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[256, 256, 3, 3], padding:[1, 1], stride:[2, 2]
conv2-- kernel_size MCHW:[1024, 256, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_7
conv0-- kernel_size MCHW:[256, 1024, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[256, 256, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[1024, 256, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_8
conv0-- kernel_size MCHW:[256, 1024, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[256, 256, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[1024, 256, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_9
conv0-- kernel_size MCHW:[256, 1024, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[256, 256, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[1024, 256, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_10
conv0-- kernel_size MCHW:[256, 1024, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[256, 256, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[1024, 256, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_11
conv0-- kernel_size MCHW:[256, 1024, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[256, 256, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[1024, 256, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_12
conv0-- kernel_size MCHW:[512, 1024, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[512, 512, 3, 3], padding:[1, 1], stride:[2, 2]
conv2-- kernel_size MCHW:[2048, 512, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_13
conv0-- kernel_size MCHW:[512, 2048, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[512, 512, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[2048, 512, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_14
conv0-- kernel_size MCHW:[512, 2048, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[512, 512, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[2048, 512, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_15
可以看到ResNet-50有15个ResidualBlock,它的部分层如下:
层 | w形状 | w参数个数 | b形状 | b参数个数 | 输出形状 |
1-conv | [64, 3, 7, 7] | 9408 | [3] | 3 | [10,64,112,112] |
pooling | – | – | – | – | [10,64,56,56] |
b0_conv0 | [64, 64, 1, 1] | 4096 | [64] | 64 | [10,64,56,56] |
b0_conv1 | [64, 64, 3, 3] | 36864 | [64] | 64 | [10,64,56,56] |
b0_conv2 | [256, 64, 1, 1] | 16384 | [256] | 256 | [10,256,56,56] |
b1_conv0 | [64, 256, 1, 1] | 16384 | [64] | 64 | [10,64,56,56] |
b1_conv1 | [64, 64, 3, 3] | 36864 | [64] | 64 | [10,64,56,56] |
b1_conv2 | [256, 64, 1, 1] | 16384 | [256] | 256 | [10,256,56,56] |
b2_conv0 | [64, 256, 1, 1] | 16384 | [64] | 64 | [10,64,56,56] |
b2_conv1 | [64, 64, 3, 3] | 36864 | [64] | 64 | [10,64,56,56] |
b2_conv2 | [256, 64, 1, 1] | 16384 | [256] | 256 | [10,256,56,56] |
高亮的数字为每个bottleneck_block的输入通道数。
评估ResNet-50的算力消耗
大部分残差块如下:
层 | W形状 | 输出形状 | 乘法执行次数 |
conv0 | [64, 256, 1, 1] | [10,64,56,56] | 约5.1亿 |
conv1 | [64, 64, 3, 3] | [10,64,56,56] | 约11.6亿 |
conv2 | [256, 64, 1, 1] | [10,256,56,56] | 约5.1亿 |
总共有48个残差块,约1046亿。残差块之前有一个卷积层,之后有一个全连接层,总共50层。因此本例的一个batch乘法次数约1046亿。同理,加法次数和乘法相当,因此一个batch总算力消耗约为2092亿次。
对比VGG-16的3069亿次,所以何恺明博士说ResNet-50时间消耗优于VGG-16。主要原因是ResNet-50的三层卷积核分别是1×1 3×3 1×1,而VGG-16的卷积核全部是3×3。
示例代码
ResNet更多思考
Q1:ResNet的数学表示为y = F(x) + x。那么y = F(x) + 2x行不行?
实践证明,2x和x比提升不大,每个round都白白浪费了一次乘法算力。
Q2:ResNet的形象化理解?
深度神经网络的训练过程:好比盲人摸象,它首先发现了象腿很粗,但是它进一步去了解象腿的其他特征的时候,随着网络越深它逐渐忘记了象腿很粗的知识,这个时候把已学到的知识传递到更深的层,相当于用个笔记下来,让它进一步去学习更多的细节,并继续记录下来,依此类推。
Q3:bottleneck结构的理解?
bottleneck中第一层conv为1×1,第二层conv为3×3,第三层conv为1×1但输出通道乘与4形似玻璃瓶口。和VGG网络设计有很大不同。为什么设定这样的卷积超参数呢?其实它的含义是:对稀疏的特征进行压缩。