上一节介绍了残差结构,还不清楚的同学可以返回上一节继续阅读。
到了这里,一个残差结构需要的算法基本都介绍完了,至少在 Resnet 这种神经网络中的残差结构是这样的。
本节我们做一个实战,基于之前几节中手写的 conv / bn 算法,来搭建一个残差结构。其中,relu 的实现和 add 的实现很简单。
relu 算法的实现用 python 来写就一行:
def ComputeReluLayer(img):
res = np.maximum(0, img)
return res
其中 img 是 relu 的输入数据,取输入数据和零的最大值即可, res 就是经过 relu 激活的结果。
而加法节点的实现就更简单,在 python 中就一个加法操作符就可以完成。
残差结构图
先看一下 resnet 中的残差结构图,在resnet50中,有两种残差结构,一种是如下的结构。
图中红框标注的是其中一种残差结构,这个结构的特点是左侧分支有3个卷积,每个卷积后面有一个 relu 激活函数。
你可能会问,左侧最后的一个卷积后面没有 relu 啊,这是因为左侧最后的一个卷积会和右侧的一个卷积相加,加完之后的结果再做 relu,实际上也相当于卷积后面都会有个relu。只要在卷积后面有一个 relu 激活函数,就会对这个卷积施加非线性因素。
这里说点题外话,在神经网络中,很多神经网络隐层的计算结果不一定非得是一模一样的,更重要的是要功能一致,比如不少神经网络调优的方法是将 relu 换成其他的激活函数,结果却是整网的推理精度会更好。
这样看来,在替换了激活函数之后,神经网络的某一层计算的结果可能不一样,但这不妨碍神经网络的推理。
上面是一个 resnet50 神经网络中典型的残差结构。网络的右侧分支多了一个卷积,该卷积是一个1×1的结构,它的作用就是将输入的 channel 维度上升为与左侧的卷积输出相有相同的channel维度,从而完成加法。
下面是 resnet50 神经网络中另一种比较常见的残差结构。这个残差结构没有左侧1×1的卷积层,它将输入直接与第三个卷积的输出进行相加。
以上两种残差结构的区别在于是否多了1层卷积。因此我们在设计残差结构的代码实现时,可以通过一个变量来控制是否有存在这个卷积,这个变量在很多地方被叫做 downsample,也就是下采样。
如果没有下采样的残差结构,比如上面第二种,我们很容易实现:其实就是将卷积,bn , relu 首尾相连即可。需要注意的是,在上面的图中并没有画出 bn 层,这是因为在很多时候,bn层的参数实际上是可以和前一层卷积的参数融合在一起的。
虽然上图没有画出来,但是我们在实现这个结构的同时还是要计算 bn 层,因为我们从预训练的 resnet50 中提取的参数时,是将卷积参数独立提取的,同时将 bn 的参数独立提取。
好了,以上背景介绍完之后,今天开始用 Python 代码来实现一个典型的残差结构。
def ComputeBottleNeck(in_data, bottleneck_layer_name, down_sample = False):
out = ComputeConvLayer(in_data, bottleneck_layer_name + "_conv1")
out = ComputeBatchNormLayer(out, bottleneck_layer_name + "_bn1")
out = ComputeReluLayer(out)
out = ComputeConvLayer(out, bottleneck_layer_name + "_conv2")
out = ComputeBatchNormLayer(out, bottleneck_layer_name + "_bn2")
out = ComputeReluLayer(out)
out = ComputeConvLayer(out, bottleneck_layer_name + "_conv3")
bn_out = ComputeBatchNormLayer(out, bottleneck_layer_name + "_bn3")
if down_sample == True:
conv_out= ComputeConvLayer(in_data, bottleneck_layer_name + "_downsample_conv2d")
short_cut_out = ComputeBatchNormLayer(conv_out, bottleneck_layer_name + "_downsample_batchnorm")
bn_out = bn_out + short_cut_out # 这里就是上面两张图中的加法。
else:
bn_out = bn_out + in_data
return ComputeReluLayer(bn_out) # 这里就是上面两张图中的加法。
仔细观察上面的 Python 代码,最开始的几行便是实现的三个卷积、三个 bn,卷积的输出为 out 变量,而 out 变量又直接给到 bn 的输入,层与层之间首尾相连。
如果 donw_sample 变量为 true 的时候,说明需要下采样,这个时候我们将原始的输入直接给下采样的卷积,后面再接一个 bn 层,然后进行加法操作。
如果 donw_sample 变量为 false 的时候,直接将最后一层卷积的输出和原始输入相加。最后经过一层 relu 激活计算,来输出整个残差结构的最终结果。
上述的代码实现基本上就是参照了上面两张图的结构来实现的,其实很简单。
每一层的具体算法实现是依据于前几节实现的代码作为基础的,可以查看前面的内容了解并实现对应的部分,这里只不过是在上面的实现基础上又重新封装了一层接口,使接口变得更加统一。
python 部分可以参考:practice/python/infer.py · iwaihou/cv_learning_from_scratch - Gitee.com
C++ 的代码实现可以参考:
practice/cpp/1st_origin/resnet.cc · iwaihou/cv_learning_from_scratch - Gitee.com