cnn预测过程代码_FPN的Tensorflow代码详解——特征提取@TOC
特征⾦字塔⽹络最早于2017年发表于CVPR,与Faster RCNN相⽐其在多池度特征预测的⽅式使得其在⼩⽬标预测上取得了较好的效果。FPN也作为mmdeteciton的Neck模块,成为常⽤的⽬标检测策略之⼀。分别提供论⽂地址特征⾦字塔论⽂地址以及代码链接Github链接。
本⽂以介绍论⽂中的原理以及其具体的实现⽅式为主,代码的环境配置和以及各个脚本⽂件的内容会根据需要补充。按照数据读⼊到输出的过程带⼤家⾛⼀遍流程。
特征提取原理
FPN与传统Faster RCNN框架相⽐,其最⼤的创新点就是⾃底向上⼜⾃顶向下的⾦字塔结构。图中A对应的特征图⾦字塔,直接利⽤不同⼤⼩的图像进⾏预测推理,图中B是传统的单池度预测,选择⽹络输出的最后⼀层的结果进⾏预测,图中C的层次⾦字塔则是在不同池度的特征图上进⾏预测。到FPN则是在层次⾦字塔的基础上,将相邻层的特征进⾏融合得到新的特征图,作为特征信息进⾏预测。
1. ⾃底向上:⾃底向上即对应着CNN⽹络的特征提取的过程,以VGG16为例。VGG16由13个卷积层、3个全连接层以及5个池化层组
成。那么在特征提取的过程中,每经过⼀层Max-pooling层,特征图⼤⼩就会缩放为原来的1/4。将卷积
到每个池化操作合并为⼀次特征提取过程,记作CONV。那么VGG16可以视为由5个CONV层+3个FC层组成。
图中是VGG16的⽹络⽰意图,每个CONV块由2个3x3的卷积以及⼀个步长为2的最⼤池化构成,实现了对特征图在HxW上的信息变为原来的1/4,⽽通道信息变为原来的2倍。那么把这样的⼀系列操作记作CONV,特征图宽⾼的缩放通过POOL实现,⽽通道的升维通过3x3的卷积实现。(选择不同的backbone的CONV的实现⽅式不同,但结果⼀致),那么通过5个CONV操作得到的特征图C5即是单特征图预测的输⼊。⽽层次⾦字塔则通过不同CONV层输出的特征图C_list={C1,C2,C3,C4,C5}中选择后三层作为RPN的输⼊进⾏预测。
那么我们可以将通过不同的CONV模块获得C_list的⼀系列操作的过程称为⾃底向上。也就是传统CNN中的特征提取阶段。
2. ⾃顶向下:与传统CNN不同,FPN在得到C_list之后没有直接选择其作为RPN的输⼊进⾏⽬标预测,⽽是将不同层的特征相融合来获
得更加充分的信息。这⼀操作的思想是基于浅层的卷积层往往包含更多纹理、形状等特征,⽽⾼层的卷积层则包含更多的语义信息。
将⼆者有效结合起来则能更好的提升⽹络的表达能⼒。
可以看到,随着⽹络层数的加深,提取到的特征可视化后就越抽象。如果仅仅选⽤C_list中的某⼀层进⾏预测,则⽆法利⽤到其他层的
语义信息。特征利⽤不充分。
为了充分利⽤不同层的语义信息,FPN将最⾼层C5的通道数通过1x1的卷积固定到256得到融合层特征P5。⽽P4则是通过将P5层通过双线性插值的⽅式上采样到14x14x256⼤⼩,再与C4层通过1x1卷积固定通道得到的结果相加,实现相邻层之间的特征融合。以此类推即可相应的得到P3和P2层特征。⽽P6层特征则是通过对P5层⽤max-pooling的⽅式下采样得到的,注意有些博客写的是通过C6层得到的,是不对的。那么我们将通过特征融合的⽅式将⾼层语义传播到低层卷积得到P_list的过程叫做⾃顶向下。
对应代码分析
在掌握原理的前提下就⽐较好对代码进⾏消化吸收了。在讲代码的时候我会
从/FPN_Tensorflow_master/libs/networks/build_whole_network.py开始讲起。这⼀部分对应着⽹络的搭建,也就是Tensorflow构建图的过程。根据讲解的需要会调到具体的函数仔细分析,类似于你在Debug的过程。主要带⼤家体会⼀下Tensor在图中流动的感觉。
那么我们要看的第⼀个函数就是build_whole_detection_network这个函数,在这⾥我们搭建了整个FPN框架。我会依次的讲解FPN 的每个部件是如何实现的。
def build_whole_detection_network(lf, input_img_batch, gtboxes_batch):
if lf.is_training:
# ensure shape is [M, 5]
gtboxes_batch = tf.reshape(gtboxes_batch, [-1, 5])
gtboxes_batch = tf.cast(gtboxes_batch, tf.float32)
img_shape = tf.shape(input_img_batch)
# 1. build ba network
P_list = lf.build_ba_network(input_img_batch) # [P2, P3, P4, P5, P6]
这⾥我们可以看到,函数的输⼊由两部分构成:图⽚Batch以及标签Batch。在训练时将gtboxes_batch变形为(N,5)的tensor,其中N为批次⼤⼩。img_shape这⾥存储了图⽚形状以便后⾯使⽤。
⽽FPN⽤来预测的特征图则是通过build_ba_network函数实现的。
def build_ba_network(lf, input_img_batch):
if lf.ba_network_name.startswith('resnet_v1'):
snet_ba(input_img_batch, scope_name=lf.ba_network_name, is_training=lf.is_training)
elif lf.ba_network_name.startswith('MobilenetV2'):
return bilenetv2_ba(input_img_batch, is_training=lf.is_training)
el:
rai ValueError('Sry, we only support resnet or mobilenet_v2')
这⾥根据全局变量cfgs⽂件中的输⼊选择不同的Backbone来得到P_list。这个版本的代码只⽀持ResNet和MobileNet两个⽹络,本⽂仅以ResNet为例。根据调⽤我们接着看resnet_ba这个函数(这⾥我不全粘贴,根据需求只粘贴部分代码⽚。)
def resnet_ba(img_batch, scope_name, is_training=True):
'''
this code is derived from light-head rcnn.
/zengarden/light_head_rcnn
It is convenient to freeze blocks. So we adapt this mode.
'''
if scope_name == 'resnet_v1_50':
middle_num_units = 6
elif scope_name == 'resnet_v1_101':
middle_num_units = 23
el:
rai NotImplementedError('We only support resnet_v1_50 or resnet_v1_101. Check your jr')
blocks = [resnet_v1_block('block1', ba_depth=64, num_units=3, stride=2),
resnet_v1_block('block2', ba_depth=128, num_units=4, stride=2),
resnet_v1_block('block3', ba_depth=256, num_units=middle_num_units, stride=2),
resnet_v1_block('block4', ba_depth=512, num_units=3, stride=1)]
函数输⼊为图像Batch、变量名称以及是否为训练。变量名⽤来判断选择ResNet50还是ResNet101,其主要改变的是CONV4中残差单元的个数。这⾥采⽤的是瓶颈残差单元,⽤Conv1x1、Conv3x3、Conv1x1来代替Conv3x3的卷积操作。这样可以在低纬度上进⾏3x3运
算,减少了计算代价。
其在实现的过程也是如图所⽰,因此每个Bottleneck包含3个卷积层。ResNet101的卷积层数=(3+4+23+3)x3=99,如果加上后续的两个FC层即101层。
def bottleneck(inputs,
depth,
depth_bottleneck,
stride,
rate=1,
outputs_collections=None,
scope=None):
"""Bottleneck residual unit variant with BN after convolutions.
This is the original residual unit propod in [1]. See Fig. 1(a) of [2] for
its definition. Note that we u here the bottleneck variant which has an
extra bottleneck layer.
When putting together two concutive ResNet blocks that u this unit, one
should u stride = 2 in the last unit of the first block.
Args:
inputs: A tensor of size [batch, height, width, channels].
depth: The depth of the ResNet unit output.
depth_bottleneck: The depth of the bottleneck layers.
stride: The ResNet unit's stride. Determines the amount of downsampling of
the units output compared to its input.
rate: An integer, rate for atrous convolution.
outputs_collections: Collection to add the ResNet unit output.
scope: Optional variable_scope.
Returns:
The ResNet unit's output.
"""
with variable_scope.variable_scope(scope, 'bottleneck_v1', [inputs]) as sc:
depth_in = utils.last__shape(), min_rank=4)
if depth == depth_in:
shortcut = resnet_utils.subsample(inputs, stride, 'shortcut')
el:
shortcut = v2d(
inputs,
depth, [1, 1],
stride=stride,
activation_fn=None,
scope='shortcut')
residual = v2d(
inputs, depth_bottleneck, [1, 1], stride=1, scope='conv1')
residual = v2d_same(
residual, depth_bottleneck, 3, stride, rate=rate, scope='conv2')
residual = v2d(
residual, depth, [1, 1], stride=1, activation_fn=None, scope='conv3')
output = lu(shortcut + residual)
llect_named_outputs(outputs_collections, sc.name, output)
这⾥解释了ResNet50和101参数选择的影响。接着往下看: