基于Tensorflow2的YOLOV4 网络结构及代码解析(3)——yolo_head部分

笔者以tensorflow2代码作为基础,解析yolovV4的网络结构。

继上篇《基于Tensorflow2的YOLOV4 网络结构及代码解析(2)——NECK部分》博文后继续解析yoloV4的yolohead

本篇博客主要介绍两个个方面:

1.yolo解码

2.门限筛选和非极大值抑制

 

在进入yolo_head之前,先看一下源码中的参数配置以及一些语法细节,代码如下:

       if self.eager:
            self.input_image_shape = Input([2,],batch_size=1)
            inputs = [*self.yolo_model.output, self.input_image_shape]
            outputs = Lambda(yolo_eval, output_shape=(1,), name='yolo_eval',arguments={'anchors': self.anchors, 'num_classes': len(self.class_names), 'image_shape': self.model_image_size, 'score_threshold': self.score, 'eager': True, 'max_boxes': self.max_boxes})(inputs)
            self.yolo_model = Model([self.yolo_model.input, self.input_image_shape], outputs)

这段代码中,有几个值得注意的地方:

1.inputs = [*self.yolo_model.output, self.input_image_shape]中“*”的用法:

*号的意思表示将列表解开,当作独立的参数传入函数。**的意思是将字典解开,当作独自的参数传入函数。上面代码意思就是将3个model.output的Tensor和Input生成的Tensor组成inputs列表

2."Lambda"的用法:

此处的Lambad不是python自带的lambda语法。他更应该理解为自定义层的一种简便写法,生成层对象,适用于简单的操作。因此,源码可以理解为将诸如anchors,num_class,image_shape等参数传给yolo_eval函数,得到1维结果

3.eager模式:

tf1.5之后引入eager模式,到了tf2之后默认采用eager模式。之前调试tf的时候,需要先构建好完整的图后再run.这样debug的时候极其麻烦,更不利于自定义层的创建。而通过eager模式,做一步便可以看到结果,调试难度大大降低。

 

完成传参后进入“yolo_eval"函数,该函数实现了解码,非极大值抑制,门限删选等一系列工作。

    for l in range(num_layers):
        _boxes, _box_scores =yolo_boxes_and_scores(yolo_outputs[l],anchors[anchor_mask[l]], num_classes, input_shape, image_shape)

该函数中调用了”yolo_head“和”yolo_correct_boxes“。它们的作用是将特征图解码和对应到原图上的位置和尺寸。

该函数代码中,利用变量anchor_mask对anchor进行配置(可能为了美观,在anchor初始时未调整顺序)。anchor与特征图相对于关系为:

#   13x13的特征层对应的anchor是[142, 110], [192, 243], [459, 401]
#   26x26的特征层对应的anchor是[36, 75], [76, 55], [72, 146]
#   52x52的特征层对应的anchor是[12, 16], [19, 36], [40, 28]

yolo解码:

box_xy, box_wh, box_confidence, box_class_probs = yolo_head(feats, anchors, num_classes, input_shape)

函数"yolo_head"做了大量的维度转换,故直接在代码注解中逐步分析。

def yolo_head(feats, anchors, num_classes, input_shape, calc_loss=False):
    #num_anchors=3
    num_anchors = len(anchors)
    #转换为tensor类型。实际测试发现没必要转换为tensor,因为传入的feats数据类型就是tensor
    feats = tf.convert_to_tensor(feats)
    anchors_tensor = K.reshape(K.constant(anchors), [1, 1, 1, num_anchors, 2])

    #---------------------------------------------------#
    #获取grid的宽和高
    #---------------------------------------------------#
    grid_shape = K.shape(feats)[1:3]  # height, width
    #对grid_y进行维度编号,得到(13,13,1,1)
    grid_y = K.tile(K.reshape(K.arange(0, stop=grid_shape[0]), [-1, 1, 1, 1]),
                    [1, grid_shape[1], 1, 1])
    #对grid_x进行维度编号,得到(13,13,1,1)
    grid_x = K.tile(K.reshape(K.arange(0, stop=grid_shape[1]), [1, -1, 1, 1]),
                    [grid_shape[0], 1, 1, 1])
    #对grid 进行维度编号,得到(13,13,1,2)
    grid = K.concatenate([grid_x, grid_y])
    grid = K.cast(grid, K.dtype(feats))

    #---------------------------------------------------#
    #   将预测结果调整成(batch_size,13,13,3,85)
    #   85可拆分成4 + 1 + 80
    #   4代表的是中心宽高的调整参数
    #   1代表的是框的置信度
    #   80代表的是种类的置信度
    #---------------------------------------------------#
    feats = K.reshape(feats, [-1, grid_shape[0], grid_shape[1], num_anchors, num_classes + 5])

   

上述代码中有几点细节需要注意:

1.Tensor和EagerTensor:笔者在写文档过程中本来想获取Tensor中的具体指,以便更直断的进行分析。而在此过程中发现,若数据类型为Tensor时,仅仅可以获取维度而无法获取具体的值。查看tf官网发现,若数据类型为EagerTensor时,可获取Value,而Tensor只有shape和name两个属性。个人理解,应该将Tensor理解为一个操作或者一个占位符更加合适,而不是一个数据对象。(尝试网上说的如tf.session(),numpy()等方法,均无效)

2.K.tile的用法:用于tensor的扩展。input为输入维度,multiples为扩张倍数。注:扩张维度必须与input维度相同,扩张倍数一一对应

tf.tile(
    input,
    multiples,
    name=None
)

3.K.concatenate的用法:tf.keras.backend.concatenate与tf.concat等价。相对维度做连接。axis默认为-1。也就是在这里使用的。直观的理解是grid_x表示13*13个x,grid_y表示13*13个y。连接起来后表示13*13个【x,y】,这样就可以表示每个grid的位置。

 #---------------------------------------------------#
    #   将预测值调成真实值
    #   box_xy对应框的中心点
    #   box_wh对应框的宽和高
    #---------------------------------------------------#
    box_xy = (K.sigmoid(feats[..., :2]) + grid) / K.cast(grid_shape[...,::-1], K.dtype(feats))
    box_wh = K.exp(feats[..., 2:4]) * anchors_tensor / K.cast(input_shape[...,::-1], K.dtype(feats))
    box_confidence = K.sigmoid(feats[..., 4:5])
    box_class_probs = K.sigmoid(feats[..., 5:])

    #---------------------------------------------------------------------#
    #   在计算loss的时候返回grid, feats, box_xy, box_wh
    #   在预测的时候返回box_xy, box_wh, box_confidence, box_class_probs
    #---------------------------------------------------------------------#
    if calc_loss == True:
        return grid, feats, box_xy, box_wh
    return box_xy, box_wh, box_confidence, box_class_probs

上述代码为grid位置坐标的转换。网上有太多相关接受,故不作详解。这里唯一值得注意的是,获取位置偏移后,做了归一化处理。

对于初学者这个图也有一定的迷惑性质,可以把上图的每个网格想象成feature map上的一个点,则第一个像素对应的偏移为(0,0),第一行第二个偏移为(1,0)以此类推。图中标注的点偏移量为(1,1)。

 yolo_head中转换为真实值时gride偏移相对于特征图尺寸做了归一化。

           代码对于预测出的值进行了Sigmoid操作目的是为了让坐标值在0-1之间。

           假设蓝色点为13*13的feature map 中的cell预测的中心点坐标为x,y,取sigmoid后其坐标为 (0.3, 0.5),则真实框在这个尺度上的中心点坐标值为(0.3+1, 0.5+1),映射到原图尺度为(1.3,1,5)*scale。参考(https://www.cnblogs.com/wangxinzhe/p/10648465.html

整个yolo_head的输出值根据train与test有所不同。在train下,输出参数(grid, feats, box_xy, box_wh),Test下输出参数为(box_xy, box_wh, box_confidence, box_class_probs)

    #-----------------------------------------------------------------#
    #   在图像传入网络预测前会进行letterbox_image给图像周围添加灰条
    #   因此生成的box_xy, box_wh是相对于有灰条的图像的
    #   我们需要对齐进行修改,去除灰条的部分。
    #   将box_xy、和box_wh调节成y_min,y_max,xmin,xmax
    #-----------------------------------------------------------------#
    boxes = yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape)

上述函数将box_xy和box_wh对应到原始图像中。做了类似图像缩放,对应点偏移的工作,个人认为没有讨论必要,如有需要可私信我。

2.门限筛选和非极大值抑制

mask = box_scores >= score_threshold

利用预设的门限值,摒弃部分置信值较低的类别。

        nms_index = tf.image.non_max_suppression(class_boxes, class_box_scores, max_boxes_tensor, iou_threshold=iou_threshold)

TF中自带非极大值抑制函数,可直接使用。

非极大值抑制的处理步骤如下”

1.遍历图片中所有识别出目标的类,对每个类进行单独分析

2.遍历某个类中所有的置信框

3.选出得分最大的置信框

4.去除与该置信框IOU超过阈值的框

5.继续遍历剩余的框,并重复步骤2-4.

api定义为:

tf.image.non_max_suppression(
    boxes, scores, max_output_size, iou_threshold=0.5,
    score_threshold=float('-inf'), name=None
)

官方解释:摒弃在之前选定框中高重合(IOU)框,Bounding box 提供形式为[y1,x1,y2,x2],其中(y1,x1)和(y2,x2)是对角线坐标并且进行归一化。

参数:

boxes: 二维Tensor,shape为[num_boxes,4]

scores:一维Tensor,shape[num_boxes]表示单个根据每个box获取的单个分数

max_output_size:标量,表示通过非极大值抑制可选择最大的Box个数

iou_threshold:iou阈值

score_threshold:根据分数移除box

 

经过NMS后,再进行维度堆叠,可获得最终结果。

到这里,已经将yoloV4整个模型全部解释完整,包括算法与相关源码的解析。个人认为,其中最为晦涩难懂的是yolohead部分,大量的维度转换以及坐标映射关系。

本篇博客均为自己对yoloV4以及tensorflow2的部分理解,如果有错误,欢迎纠正和提出意见。

后续将持续更新

  •  YOLOV4的YoloHead

  •  YOLOV4的Loss和Input

  •  YOLOV4的创新点以及一些tricks

 

E-mail:wangxiaoyang0307@foxmail.com

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
需要学习Windows系统YOLOv4的同学请前往《Windows版YOLOv4目标检测实战:原理与源码解析》,课程链接 https://edu.csdn.net/course/detail/29865【为什么要学习这门课】 Linux创始人Linus Torvalds有一句名言:Talk is cheap. Show me the code. 冗谈不够,放码过来!  代码阅读是从基础到提高的必由之路。尤其对深度学习,许多框架隐藏了神经网络底层的实现,只能在上层调包使用,对其内部原理很难认识清晰,不利于进一步优化和创新。YOLOv4是最近推出的基于深度学习的端到端实时目标检测方法。YOLOv4的实现darknet是使用C语言开发的轻型开源深度学习框架,依赖少,可移植性好,可以作为很好的代码阅读案例,让我们深入探究其实现原理。【课程内容与收获】 本课程将解析YOLOv4的实现原理和源码,具体内容包括:- YOLOv4目标检测原理- 神经网络及darknet的C语言实现,尤其是反向传播的梯度求解和误差计算- 代码阅读工具及方法- 深度学习计算的利器:BLAS和GEMM- GPU的CUDA编程方法及在darknet的应用- YOLOv4的程序流程- YOLOv4各层及关键技术的源码解析本课程将提供注释后的darknet的源码程序文件。【相关课程】 除本课程《YOLOv4目标检测:原理与源码解析》外,本人推出了有关YOLOv4目标检测的系列课程,包括:《YOLOv4目标检测实战:训练自己的数据集》《YOLOv4-tiny目标检测实战:训练自己的数据集》《YOLOv4目标检测实战:人脸口罩佩戴检测》《YOLOv4目标检测实战:中国交通标志识别》建议先学习一门YOLOv4实战课程,对YOLOv4的使用方法了解以后再学习本课程。【YOLOv4网络模型架构图】 下图由白勇老师绘制  
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值