YOLOV3代码与原理相互结合的理解(重点在特征图的输出的元素的解析)

YOLOV3的原理浅谈

我会用非简短的语言说明一下yolov3的原理和我认为的一些特点。
我们以训练的角度来说一下
1:首先我们允许输入不同大小,不同长宽比的训练集图片,因为我们会对图片进行一个统一的处理,来使得我们的图片的输入全部维持在416x416的大小(具体操作步骤我会在下面另外解释)。
2:之后进入网络的图片会生成三个特征层,其形状分别为(b,52,52,255),(b,26,26,255),(b,13,13,255)。其实这就是一开始的预测结果,肯定会有误差,所以确定它到底和谁比非常重要。
3:首先咱们的训练集图片肯定是给出了目标框的位置的,我们以第一个特征层(b,13,13,255)为例。假设一张训练集图片只有一个物体,在我们把原图分成13x13的小格子以后,有一个框的中心点xy落在了(2,3)位置的小格子里面,那么在特征层的第(2,3)个格子里面的255个元素就负责训练这个目标框,但是有一点基础的都应该知道,一个格子里面有三个框,但是只会取iou与目标框最大的那个框进行训练

其实上面就已经是基本的最重要的步骤了,我知道肯定说的是囫囵吞枣,所以我会用后面的篇章来更加深度的分析上面的每一个步骤(有的图是我自己画的,但是尽量会表达得非常清楚,因为我表达得越清楚,也同时表示我理解得越透彻,对我来说也是一种鼓励)。

输入图片的处理(非常重要!!)

我这里所提到的输入图片的处理包含两个部分
1:输入图片上面的目标框坐标处理
2:输入图片的尺寸处理

先说第一个,在coco数据集里面,目标框的xyhw的表述对象是这个框的左上角坐标和这个框的高宽,这一点很容易改成中心点xy和高宽hw,问题就出在它对这4个参数的“归一化”上面,代码上面对它的修改是这样的

 	dw = 1. / size[0]
    dh = 1. / size[1]
    x = (box[0] + box[2]) / 2.0 - 1
    y = (box[1] + box[3]) / 2.0 - 1
    w = box[2]
    h = box[3]
    x = x * dw
    w = w * dw
    y = y * dh
    h = h * dh

dw和dh在为“归一化”做准备,我什么为什么给归一化打上引号,因为我觉得这个归一化并不完美。这里的xyhw全部取了比例
这里的思想是我自己想出来有助于理解的,那就是这个操作方法相当于把这个训练集的图直接压缩在了1x1的小格子里面,这样比例所代表的数值就是在这个1x1小方格里面的长度,这样做有一个致命的问题,那就是这个目标框基本会被扭曲,理由很简单,因为原图不一定是正方形的,压缩在1x1的这个正方形小格子里面肯定会发生扭曲,xywh四个值绝对不可能还保持原来的“比例”。那么如何可以恢复到原来大小呢,逆着运算就可以了,xywh分别去乘这个原图的宽高就可以了。

它一定程度上就是我们目标框所提供的位置和大小的标签,也就是说咱们训练的目标其实一直是扭曲的xyhw。

归一化确实一定程度上面会提高运算的效率,但是像上面这种归一化会给我们理解原理和步骤造成不小的困难。

第二个就是图片尺寸的处理
我们之前说过,可以输入任意大小的图片,然后使用某种手段使得其为网路的输入416x416
这种方法说起来其实很简单,先对图片进行等比例的放缩,也就是不改变图片的宽高比,然后一直缩放到宽或者高一边为416,另外一边小于416,就是一种等比例的缩放。那么缩放的图片肯定就是比416x416小的,缺陷的部分用128,128,128的统一像素来填充,怎么填充呢,也是有技巧的,那就是把缩放的图片放在中间,左右或者两边各填补一半就可以了。


这样做的理由其实很简单,不能直接改成416x416,因为会破坏原图物体的特征。一定要牢记,咱们最后网络输出的预测值,就是以输入的416x416的结构搭建的(说得比较抽象,我会举例来说明缘由),我们将输入网络的这个416x416称作new_input

对于输出的特征层的数字解析(最重要!)

说白了就是它输出的是个啥,其实输出是什么并不难说出来,难的是如何把输出和咱们的标签联系起来。
1:我们先看看输出的是什么,以(b,13,13,255)为例,准确地说应该是(b,13,13,[3x(4+1+80)]),3表示这个格子里面的3个框,4表示这个框的xyhw,1表示这个框存在物体的概率,80表示这个框内对于80种我们识别的物体的概率。
最重要的就是这个xyhw到底是什么,其实有很多论文里面都说过了,我用我自己的语言来概括一下,先说xy,首先这个xy的大小是任意的,可以不在[0,1]之间,但是我们会人为的利用sigmoid函数来使得其经过处理的值在01之间,sigmoid(xy)表示的是根据输出层映射出来的框的中心离它所在的这个格子的左上角的一个相对坐标,hw是转化公式(以h为例)Ph=ph*exp(h)中的h。ph表示之前聚类过的某一个anchor的高,Ph表示的是这个框在new input上面的高。
那么这里就会产生两组疑问,每一组都不太好理解
1:一个格子的长度的多少?那我利用Px=sigmoid(x)+grid算出来的Px和Ph岂不是不匹配
(grid是这个格子所在的13x13里面的坐标,Px是输出所表示的框在13x13里面的坐标,但是Ph是在416x416前提下的长度,所以会说二者不匹配)
答:一个格子的长度就是1,因为一个特征点就是一个像素点,一个像素点格子就是1x1.所以根据Px=sigmoid(x)+grid算出来的x确实是在13x13大小里面,也确实和Ph不匹配,解决方法和第二组问题一起提出。
2:我们只是以13x13为例来描述Px,那52x52,26x26的呢。这里就是理解上的第二个矛盾点。因为我们刚才说过,一个格子宽高都是1,也就是52x52的肯定比13x13的大。而同一张图被分成13x13和52x52,前者的一个格子大小肯定比后者大,所以这就是矛盾的地方,就是缺少一个统一的标准,和上一个问题的难点是一样的。

在提出解决方法之前,我们先引出一个想法:当我们在计算Px的时候,难道没发觉它就是在求一个对于13x13大小的图片上的坐标吗?如果我们把416x416的new input缩小到13x13,Px的坐标不就对上了吗。所以Px就是把new input缩小到13x13以后的框的中心点x。52x52同理,求的就是把new input缩小到52x52以后的中心点的坐标x。
而为了消除不同大小的特征层,全部缩小到1x1上面就可以了,也可以说是归一化,但是我本人不太喜欢这个词,因为归一化的方式和结果并不都是相同的,所以含义也就不同,滥用归一化可能会造成歧义。也就是说Px经过缩小以后就是new input缩小到1x1以后的坐标x。同理,Ph也需要缩小到1x1的尺寸里面,这样一来标准就统一了。可能还有疑问,那Px和Ph缩小到1x1的尺寸里面除以的数岂不是不一样?没错,还真就不一样。

		grid_size = tf.shape(feats)[1:3]
        box_xy = (tf.sigmoid(predictions[..., :2]) + grid) / tf.cast(grid_size[::-1], tf.float32)
       
        box_wh = tf.exp(predictions[..., 2:4]) * anchors_tensor / tf.cast(input_shape[::-1], tf.float32)

从上面可以明显的看到,xy除以的是这个特征图的大小,而hw除以的是new input的尺寸。

将预测值和目标标签想匹配

经过上面的解释,我们得到了新的xyhw,其表示的框是在new input的1x1版本上面的。换句话说直接乘416就是就可以在new input上面画一个框了。真的是这样吗?答:真的是这样。但是这并不是我们的目的。new input虽然好,但是有一个致命的弱点,就是它并不完全包含我们一开始的训练集部分,而是还有我们填充的像素。就算缩小到了1x1,1x1里面不还有填充的像素嘛。这里用了一个非常巧妙的算法,我会画图加以说明。

  		offset = (input_shape - new_shape) / 2. / input_shape
        scale = input_shape / new_shape
        box_yx = (box_yx - offset) * scale
        box_hw *= scale

offset就是我们填充的那两部分中的一部分(所以除以了一个2),又由于要缩小到1x1里面去,后面除以了一个(416,416),这个scale我们先不管,先看第三行右边的括号,它做了一个非常巧妙的运算,我们知道填充部分对我们影响最大的改变了xy的值,我们想把填充部分给我被迫增加的xy给去掉,所以通过画图我们可以看出来

我们只以左边为例,黑色的坐标轴是我们没有对填充进行去除的时候,当我们进行了上面的运算以后,相当于坐标轴下移,我们可以发现,填充部分的影响已经去除了,此时的xy就是在有效地原图(也不叫原图,反正就是对上了,我不知道怎么形容了,我高中作文就没上过50…)一个步骤可以根据new input中的填充方式得到上面两种中的一种,不是还有一半填充,为什么不管呢?另外一半填充又不影响你的xy,你管它干嘛丫。hw不受填充的影响,所以不用管。

也就是说现在的xywh直接放大一定的倍数就可以在训练集图上画框了,但是这不是我们的目的,我们的目的是找到这个xywh和目标框给我们的xywh之间的关系

他们目前唯一的关系就是,都被固定在了1x1的小格子里面,这就是突破口

现在我们好好掰扯掰扯这个scale
不知道看了这么多是否还记得我们一开始说过,目标框给的xyhw其实是扭曲的。

虚线表示原图在各自的1x1格子的范围,左图是我们保持原图宽高比例范围,右图是直接扭曲后的大小,填满了1x1的格子,所以我们需要做的就是把左图正常的xyhw,也扭曲成右图这样就可以了。也就是说把左图阴影部分直接拉伸成1x1就可以了,怎么拉伸呢?那就是乘一个这个scale就可以了,可以自己举一个例子算一下。这样二者训练的xywh和标签的xywh就对应上了。

其他小细节杂谈

我想强调的重点上面都基本说完了,随便看看还有什么没说到的吧。
1:比如有个框的中心落在这个格子里面,那么这个格子的所有85个元素都要参与训练,
如果中心没在这个格子里面,只有第4号元素,也就是判断是否物体会参与训练,标签是0
2:我们对预测的时候进行一些总结吧

 		anchor_mask = [[6, 7, 8], [3, 4, 5], [0, 1, 2]]
        boxes = []
        box_scores = []
     
        input_shape = tf.shape(yolo_outputs[0])[1 : 3] * 32
       
        for i in range(len(yolo_outputs)):
            _boxes, _box_scores = self.boxes_and_scores(yolo_outputs[i], 	self.anchors[anchor_mask[i]], len(self.class_names), input_shape, image_shape)
            boxes.append(_boxes)
            box_scores.append(_box_scores)
        
        boxes = tf.concat(boxes, axis = 0)
        box_scores = tf.concat(box_scores, axis = 0)

这里利用循环是按照次序输出了3个特征层的所有信息
有些代码我省略了,这里我挨个说一下输出的是什么
_boxes自然是框,但是这是我们刚才说过的在训练集上面话的框,之前说过只需要乘一个倍数就可以了。当然也可以使用扭曲过预测值的,去乘一下原图的高宽就可以了(之前也说过)。
_box_scores是每一个框代表的分数,它是是否有物体的置信度去乘该框内80个物体的各自概率,也就说一个_boxes_scores有80个分数
从上面可以看到,我们一共产生了13x13x3+26x26x3+52x52x3个框,之后会一一排除
再后面就是简单的加一块再concat的操作
重点是后面的预测

mask = box_scores >= self.obj_threshold
        max_boxes_tensor = tf.constant(max_boxes, dtype = tf.int32)
        boxes_ = []
        scores_ = []
        classes_ = []

    
        for c in range(len(self.class_names)):
            # 取出所有类为c的box
            class_boxes = tf.boolean_mask(boxes, mask[:, c])
            # 取出所有类为c的分数
            class_box_scores = tf.boolean_mask(box_scores[:, c], mask[:, c])
            # 非极大抑制
            nms_index = tf.image.non_max_suppression(class_boxes, class_box_scores, max_boxes_tensor, iou_threshold = self.nms_threshold)
            
            # 获取非极大抑制的结果
            class_boxes = tf.gather(class_boxes, nms_index)
            class_box_scores = tf.gather(class_box_scores, nms_index)
            classes = tf.ones_like(class_box_scores, 'int32') * c

            boxes_.append(class_boxes)
            scores_.append(class_box_scores)
            classes_.append(classes)
        boxes_ = tf.concat(boxes_, axis = 0)
        scores_ = tf.concat(scores_, axis = 0)
        classes_ = tf.concat(classes_, axis = 0)
        return boxes_, scores_, classes_

这里的大致步骤就是先把分低的拿掉,如果你80个分每一个分都低,断定你这里面没有物体
然后把剩下的80,80,80…(大锤大锤大锤)都是第c类最高分的拿出来,把这些框做一个非极大值抑制,这里设置了一个max boxes tensor为20,表示我一张图中最多检测出来同一个种类20个,而不是一共检测出来20个。
其实想到这里的时候我当时又有一个问题,如果某一个框的检测结果被多次挪用,也就是说它属于c和d的分数是一样的,我看了一下循环的代码,它也确实会被进入两次循环里面,但是这种框绝对过不去非极大值抑制这一关,因为我们经过nms首先要找的一个相近位置里面分最高的框,然后剔除iou和这个框很大的框。那么这种似是而非的框对于某一个种类的分绝对不可能是它周围框里面最高的,因为我那么多框,肯定有某些框框得比你好,自然得分比你多,你nms的时候绝对会被淘汰。那么有没有可能它就是最高的呢,我给出的答案是:微乎其微。如果c和d的分数都很高,我只能说你这个图是不是受过福岛核辐射产生了变异物种。要么就是网络训练得不恰当,一个成熟的网络不可能会让一个模棱两可的框的分数很高。

好了,我的阐述现在终于结束了,如果有人发现了我理解上的错误,欢迎指正,想要源码的话我只能说我这个没什么用,么有训练过程,只能帮助理解。

你可能感兴趣的:(python,深度学习,tensorflow)