说说优秀的目标识别yolov3算法

前言

yolo系列一直是我喜欢的算法,虽然yolov4出来了很久了,但是yolov3还是具有很大的研究空间。今天我们来解析下yolov3.(虽然我以前写过,但是我最近对这个算法有了新的理解)
在这里插入图片描述

论文地址

https://pjreddie.com/media/files/papers/YOLOv3.pdf

github地址

https://github.com/yanjingke/yolo3

预备知识

ps:花10分钟理解

Yolov3边框预测

论文中边框预测公式如下:
在这里插入图片描述
其中,Cx,Cy是feature map中真实框的左上角坐标,在yolov3中每个grid cell在feature map中的宽和高均为1。在下面情况中bbox边界框的中心属于第二行第二列的真实框,它的左上角坐标为(1,1),故Cx=1,Cy=1.公式中的Pw、Ph是预设的anchor box映射到feature map中的宽和高。 最终得到的边框坐标值是bx,by,bw,bh即边界框bbox相对于feature map的位置和大小,是我们需要的预测输出坐标。但我们网络实际上的学习目标是tx,ty,tw,th这4个offsets,其中tx,ty是预测的坐标偏移值,tw,th是尺度缩放,有了这4个offsets,自然可以根据之前的公式去求得真正需要的bx,by,bw,bh4个坐标。
在这里插入图片描述
这里解释一下anchor box,YOLO3为每种FPN预测特征图(13x13,26x26,52x52)设定3种anchor box,总共聚类(这里大家如果想聚类可以自己使用我代码的kmeans_for_anchors.py)出9种尺寸的anchor box。在COCO数据集这9个anchor box是:(10x13),(16x30),(33x23),(30x61),(62x45),(59x119),(116x90),(156x198),(373x326)。分配上,在最小的13x13特征图上由于其感受野最大故应用最大的anchor box (116x90),(156x198),(373x326)。适合检测较大的目标。中等的26x26特征图上由于其具有中等感受野故应用中等的anchor box (30x61),(62x45),(59x119),适合检测中等大小的目标。较大的52x52特征图上由于其具有较小的感受野故应用最小的anchor box(10x13),(16x30),(33x23),适合检测较小的目标。同Faster-Rcnn一样,特征图的每个像素(即每个grid)都会有对应的三个anchor box,如13*13特征图的每个grid都有三个anchor box (116x90),(156x198),(373x326)(这几个坐标需除以32缩放尺寸)
代码:

def yolo_head(feats, anchors, num_classes, input_shape, calc_loss=False): num_anchors = len(anchors) # [1, 1, 1, num_anchors, 2]-.(1,1,1,3,2) anchors_tensor = K.reshape(K.constant(anchors), [1, 1, 1, num_anchors, 2]) # 获得x,y的网格 # (13, 13, 1, 2) grid_shape = K.shape(feats)[1:3] # # height, width (?,13,13,255)  -> (13,13) #13,1,1,1-> shape=(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]) #1,13,1,1->shape=(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]) #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,分别代表x_offset、y_offset、h和w、置信度、分类结果。 feats = K.reshape(feats, [-1, grid_shape[0], grid_shape[1], num_anchors, num_classes + 5]) # 将预测值调成真实值 # box_xy对应框的中心点 # box_wh对应框的宽和高 #所以a[::-1]相当于 a[-1:-len(a)-1:-1],也就是从最后一个元素到第一个元素复制一遍,即倒序。 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的时候返回如下参数 if calc_loss == True: return grid, feats, box_xy, box_wh return box_xy, box_wh, box_confidence, box_class_probs


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

Yolov3边框解码

解码的操作是为了在训练时方便与预测值作为对比,用来计算loss。在faster RCNN中解码其公式如下图,Px,Py在是预设的anchor box在feature map上的中心角坐标。 Pw、Ph是预设的anchor box的在feature map上的宽和高。至于Gx、Gy、Gw、Gh自然就是ground truth在这个feature map的4个坐标了。用x,y坐标减去anchor box的x,y坐标得到偏移量好理解,**为何要除以feature map上anchor box的宽和高呢?**我认为可能是为了把绝对尺度变为相对尺度,毕竟作为偏移量,不能太大了对吧。而且不同尺度的anchor box如果都用Gx-Px来衡量显然不对,有的anchor box大有的却很小,都用Gx-Px会导致不同尺度的anchor box权重相同,而大的anchor box肯定更能容忍大点的偏移量,小的anchor box对小偏移都很敏感,故除以宽和高可以权衡不同尺度下的预测坐标偏移量。
在这里插入图片描述
但是在yolov3中与faster-rcnn系列文章用到的公式在前两行是不同的,yolov3里Px和Py就换为了feature map上的grid cell左上角坐标Cx,Cy了,即在yolov3里是Gx,Gy减去grid cell左上角坐标Cx,Cy。x,y坐标并没有针对anchon box求偏移量,所以并不需要除以Pw,Ph。
也就是说是tx = Gx - Cx

 ty = Gy - Cy

  
 
  • 1

这样就可以直接求bbox中心距离真实框左上角的坐标的偏移量。
对于某个ground truth框,究竟是哪个anchor负责匹配它呢?和YOLOv1一样,对于训练图片中的ground truth,若其中心点落在某个cell内,那么该cell内的3个anchor box负责预测它,具体是哪个anchor box预测它,需要在训练中确定,即由那个与ground truth的IOU最大的anchor box预测它,而剩余的2个anchor box不与该ground truth匹配。YOLOv3需要假定每个cell至多含有一个grounth truth,而在实际上基本不会出现多于1个的情况。与ground truth匹配的anchor box计算坐标误差、置信度误差(此时target为1)以及分类误差,而其它的anchor box只计算置信度误差(此时target为0)。有了平移(tx,ty)和尺度缩放(tw,th)才能让anchor box经过微调与grand truth重合。利用边框回归最简单的想法就是通过平移加尺度缩放进行微调嘛。
那么训练时用的groundtruth的4个坐标去做差值和比值得到tx,ty,tw,th,测试时就用预测的bbox就好了,公式修改就简单了,把Gx和Gy改为预测的x,y,Gw、Gh改为预测的w,h即可。
网络可以不断学习tx,ty,tw,th偏移量和尺度缩放,预测时使用这4个offsets求得bx,by,bw,bh即可,那么问题是:tx,ty为何要sigmoid一下啊?
前面讲到了在yolov3中没有让Gx - Cx后除以Pw得到tx,而是直接Gx - Cx得到tx,这样会有问题是导致tx比较大且很可能>1.(因为没有除以Pw归一化尺度。一旦tx,ty算出来大于1就会落入必须其他真实框中,而不能出现在它旁边网格中,引起矛盾,因而必须归一化。
在这里插入图片描述

代码讲解

预测部分

主干特征提取darkNet53

它采用了一个新的网络来提取特征,它融合了YOLOv2、Darknet-19以及其他新型残差网络,由连续的3×3和1×1卷积层组合而成,当然,其中也添加了一些shortcut connection,整体体量也更大。因为一共有53个卷积层,所以叫做称它为Darknet-53。
Darknet-53
1.采用Inverted residual在3x3网络结构前利用1x1卷积降维,在3x3网络结构后,利用1x1卷积升维,相比直接使用3x3网络卷积效果更好,参数更少,先进行压缩,再进行扩张。
2.采用残差网络,后面的特征层的内容会有一部分由其前面的某一层线性贡献。
3.采用Leaky ReLUs, ReLU是将所有的负值都设为零,相反,Leaky ReLU是给所有负值赋予一个非零斜率。Leaky ReLU激活函数是在声学模型(2013)中首次提出的。以数学的方式我们可以表示为:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

from functools import wraps
from keras.layers import Conv2D, Add, ZeroPadding2D, UpSampling2D, Concatenate, MaxPooling2D
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.normalization import BatchNormalization
from keras.regularizers import l2
from utils.utils import compose


#--------------------------------------------------#
#   单次卷积
#--------------------------------------------------#
@wraps(Conv2D)
def DarknetConv2D(*args, **kwargs): darknet_conv_kwargs = {'kernel_regularizer': l2(5e-4)} darknet_conv_kwargs['padding'] = 'valid' if kwargs.get('strides')==(2,2) else 'same' darknet_conv_kwargs.update(kwargs) return Conv2D(*args, **darknet_conv_kwargs)

#---------------------------------------------------#
#   卷积块
#   DarknetConv2D + BatchNormalization + LeakyReLU
#---------------------------------------------------#
def DarknetConv2D_BN_Leaky(*args, **kwargs): no_bias_kwargs = {'use_bias': False} no_bias_kwargs.update(kwargs) return compose( DarknetConv2D(*args, **no_bias_kwargs), BatchNormalization(), LeakyReLU(alpha=0.1))

#---------------------------------------------------#
#   卷积块
#   DarknetConv2D + BatchNormalization + LeakyReLU
#---------------------------------------------------#
def resblock_body(x, num_filters, num_blocks): x = ZeroPadding2D(((1,0),(1,0)))(x) x = DarknetConv2D_BN_Leaky(num_filters, (3,3), strides=(2,2))(x) for i in range(num_blocks): y = DarknetConv2D_BN_Leaky(num_filters//2, (1,1))(x) y = DarknetConv2D_BN_Leaky(num_filters, (3,3))(y) x = Add()([x,y]) return x

#---------------------------------------------------#
#   darknet53 的主体部分
#---------------------------------------------------#
def darknet_body(x): x = DarknetConv2D_BN_Leaky(32, (3,3))(x) x = resblock_body(x, 64, 1) x = resblock_body(x, 128, 2) x = resblock_body(x, 256, 8) feat1 = x x = resblock_body(x, 512, 8) feat2 = x x = resblock_body(x, 1024, 4) feat3 = x return feat1,feat2,feat3



  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

特征金字塔构建,获得预测结果

通过前面的特征提取我们获得五个特征层,分别是feat1、feat2、feat3,7。他们的大小分别是(52x52x256)(26x26x512)(13x13x1024)
三个特征层进行5次卷积处理,处理完后一部分用于输出该特征层对应的预测结果,一部分用于进行反卷积UmSampling2d后与其它特征层进行结合。输出层的shape分别为(13,13,255),(26,26,255),(52,52,255),最后一个维度为255是因为该图是基于coco数据集的,85包含了4+1+80,分别代表x_offset、y_offset、h和w、置信度(是否有对象的概率)、分类结果。coco数据集种类为80种,yolo3只有针对每一个特征层存在3个先验框,所以最后维度为3x(4+1+80)。

#--------------------------------------------------#
#   单次卷积
#--------------------------------------------------#
@wraps(Conv2D)
def DarknetConv2D(*args, **kwargs): darknet_conv_kwargs = {'kernel_regularizer': l2(5e-4)} darknet_conv_kwargs['padding'] = 'valid' if kwargs.get('strides')==(2,2) else 'same' darknet_conv_kwargs.update(kwargs) return Conv2D(*args, **darknet_conv_kwargs)

#---------------------------------------------------#
#   卷积块
#   DarknetConv2D + BatchNormalization + LeakyReLU
#---------------------------------------------------#
def DarknetConv2D_BN_Leaky(*args, **kwargs): no_bias_kwargs = {'use_bias': False} no_bias_kwargs.update(kwargs) return compose( DarknetConv2D(*args, **no_bias_kwargs), BatchNormalization(), LeakyReLU(alpha=0.1))

#---------------------------------------------------#
#   特征层->最后的输出
#---------------------------------------------------#
def make_last_layers(x, num_filters, out_filters): # 五次卷积 x = DarknetConv2D_BN_Leaky(num_filters, (1,1))(x) x = DarknetConv2D_BN_Leaky(num_filters*2, (3,3))(x) x = DarknetConv2D_BN_Leaky(num_filters, (1,1))(x) x = DarknetConv2D_BN_Leaky(num_filters*2, (3,3))(x) x = DarknetConv2D_BN_Leaky(num_filters, (1,1))(x) # 将最后的通道数调整为outfilter y = DarknetConv2D_BN_Leaky(num_filters*2, (3,3))(x) y = DarknetConv2D(out_filters, (1,1))(y) return x, y

#---------------------------------------------------#
#   特征层->最后的输出
#---------------------------------------------------#
def yolo_body(inputs, num_anchors, num_classes): # 生成darknet53的主干模型 feat1,feat2,feat3 = darknet_body(inputs) darknet = Model(inputs, feat3) # 第一个特征层 # y1=(batch_size,13,13,3,85) x, y1 = make_last_layers(darknet.output, 512, num_anchors*(num_classes+5)) x = compose( DarknetConv2D_BN_Leaky(256, (1,1)), UpSampling2D(2))(x) x = Concatenate()([x,feat2]) # 第二个特征层 # y2=(batch_size,26,26,3,85) x, y2 = make_last_layers(x, 256, num_anchors*(num_classes+5)) x = compose( DarknetConv2D_BN_Leaky(128, (1,1)), UpSampling2D(2))(x) x = Concatenate()([x,feat1]) # 第三个特征层 # y3=(batch_size,52,52,3,85) x, y3 = make_last_layers(x, 128, num_anchors*(num_classes+5)) return Model(inputs, [y1,y2,y3])

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

预测结果的解码(可以看预备知识)

预测框的解码部分,我们通过主干网络和特征金字塔获得了
num_priors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。
num_priors x (num_classes+1)的卷积 用于预测 该特征层上 每一个网格点上 每一个预测框对应的种类和是否包含物体。
利用预设好的 预先设定好的3个anchor框进行调整。
先验框虽然可以代表一定的框的位置信息与框的大小信息,但是其是有限的,无法表示任意情况,因此还需要调整,num_priors x (80+5)的卷积的结果对先验框进行调整。85包含了4+1+80,分别代表x_offset、y_offset、h和w、置信度、分类结果。yolov3将每个网格点加上它对应的x_offset和y_offset,加完后的结果就是预测框的中心,然后再利用 先验框和h、w结合 计算出预测框的长和宽。这样就能得到整个预测框的位置了。

#---------------------------------------------------#
#   将预测值的每个特征层调成真实值
#---------------------------------------------------#
def yolo_head(feats, anchors, num_classes, input_shape, calc_loss=False): num_anchors = len(anchors) # [1, 1, 1, num_anchors, 2] anchors_tensor = K.reshape(K.constant(anchors), [1, 1, 1, num_anchors, 2]) # 获得x,y的网格 # (13, 13, 1, 2) grid_shape = K.shape(feats)[1:3] # height, width 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 = K.tile(K.reshape(K.arange(0, stop=grid_shape[1]), [1, -1, 1, 1]), [grid_shape[0], 1, 1, 1]) grid = K.concatenate([grid_x, grid_y]) grid = K.cast(grid, K.dtype(feats)) # (batch_size,13,13,3,85) feats = K.reshape(feats, [-1, grid_shape[0], grid_shape[1], num_anchors, num_classes + 5]) # 将预测值调成真实值 # 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的时候返回如下参数 if calc_loss == True: return grid, feats, box_xy, box_wh return box_xy, box_wh, box_confidence, box_class_probs

#---------------------------------------------------#
#   对box进行调整,使其符合真实图片的样子
#---------------------------------------------------#
def yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape): box_yx = box_xy[..., ::-1] box_hw = box_wh[..., ::-1] input_shape = K.cast(input_shape, K.dtype(box_yx)) image_shape = K.cast(image_shape, K.dtype(box_yx)) new_shape = K.round(image_shape * K.min(input_shape/image_shape)) offset = (input_shape-new_shape)/2./input_shape scale = input_shape/new_shape box_yx = (box_yx - offset) * scale box_hw *= scale box_mins = box_yx - (box_hw / 2.) box_maxes = box_yx + (box_hw / 2.) boxes =  K.concatenate([ box_mins[..., 0:1],  # y_min box_mins[..., 1:2],  # x_min box_maxes[..., 0:1],  # y_max box_maxes[..., 1:2]  # x_max ]) boxes *= K.concatenate([image_shape, image_shape]) return boxes

#---------------------------------------------------#
#   获取每个box和它的得分
#---------------------------------------------------#
def yolo_boxes_and_scores(feats, anchors, num_classes, input_shape, image_shape): # 将预测值调成真实值 # box_xy对应框的中心点 # box_wh对应框的宽和高 # -1,13,13,3,2; -1,13,13,3,2; -1,13,13,3,1; -1,13,13,3,80 box_xy, box_wh, box_confidence, box_class_probs = yolo_head(feats, anchors, num_classes, input_shape) # 将box_xy、和box_wh调节成y_min,y_max,xmin,xmax boxes = yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape)

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74

当然得到最终的获得真实框的调整结果后,取出取出每一类得分大于confidence_threshold的框和得分。利用框的位置和得分进行非极大抑制,得到最终结果。

#---------------------------------------------------#
#   图片预测
#---------------------------------------------------#
def yolo_eval(yolo_outputs, anchors, num_classes, image_shape, max_boxes=20, score_threshold=.6, iou_threshold=.5): # 获得特征层的数量 num_layers = len(yolo_outputs) # 特征层1对应的anchor是678 # 特征层2对应的anchor是345 # 特征层3对应的anchor是012 anchor_mask = [[6,7,8], [3,4,5], [0,1,2]] input_shape = K.shape(yolo_outputs[0])[1:3] * 32 boxes = [] box_scores = [] # 对每个特征层进行处理 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) boxes.append(_boxes) box_scores.append(_box_scores) # 将每个特征层的结果进行堆叠 boxes = K.concatenate(boxes, axis=0) box_scores = K.concatenate(box_scores, axis=0) mask = box_scores >= score_threshold max_boxes_tensor = K.constant(max_boxes, dtype='int32') boxes_ = [] scores_ = [] classes_ = [] for c in range(num_classes): # 取出所有box_scores >= score_threshold的框,和成绩 class_boxes = tf.boolean_mask(boxes, mask[:, c]) class_box_scores = tf.boolean_mask(box_scores[:, c], mask[:, c]) # 非极大抑制,去掉box重合程度高的那一些 nms_index = tf.image.non_max_suppression( class_boxes, class_box_scores, max_boxes_tensor, iou_threshold=iou_threshold) # 获取非极大抑制后的结果 # 下列三个分别是 # 框的位置,得分与种类 class_boxes = K.gather(class_boxes, nms_index) class_box_scores = K.gather(class_box_scores, nms_index) classes = K.ones_like(class_box_scores, 'int32') * c boxes_.append(class_boxes) scores_.append(class_box_scores) classes_.append(classes) boxes_ = K.concatenate(boxes_, axis=0) scores_ = K.concatenate(scores_, axis=0) classes_ = K.concatenate(classes_, axis=0) return boxes_, scores_, classes_

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57

训练部分

真实框的编码

实际上用于训练的的数据是经过input_shape归一化后的,所以要对标注的数据进行归一化。标注的是左上角和右下角的坐标,所在归一化前需要把标注变成标注框的中心和长宽的形式。同时,对于一个标注边框而言,作者说:“our system only assigns one bounding box prior for each ground truth object.”也就是说,对于一个标注,只能有一个最佳的anchor与之匹配(其实还没捋顺)这个最佳通过IoU来判断。
首先判断类别正常。然后按照feature map的grid的大小将anchor划分。第一层对应较大的anchor,因为该feature map 对应的感受野大。

 assert (true_boxes[..., 4]<num_classes).all(), 'class id must be less than num_classes' # 一共有三个特征层数 num_layers = len(anchors)//3 # 先验框 # 678为116,90,  156,198,  373,326 # 345为30,61,  62,45,  59,119 # 012为10,13,  16,30,  33,23,   anchor_mask = [[6,7,8], [3,4,5], [0,1,2]] if num_layers==3 else [[3,4,5], [1,2,3]]


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

先将标注边框的两点坐标的形式,转化为训练的中心加边长的形式。并进行归一化。这里应该是boxes_wh 的高宽与input_shape的高宽顺序不一样。

 true_boxes = np.array(true_boxes, dtype='float32') input_shape = np.array(input_shape, dtype='int32') # 416,416 # 读出xy轴,读出长宽 # 中心点(m,n,2) boxes_xy = (true_boxes[..., 0:2] + true_boxes[..., 2:4]) // 2 boxes_wh = true_boxes[..., 2:4] - true_boxes[..., 0:2] # 计算比例 true_boxes[..., 0:2] = boxes_xy/input_shape[:] true_boxes[..., 2:4] = boxes_wh/input_shape[:]


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

y_ture的初始化。可知y_ture有三个子项,分别对应着13x13,26x26,52x52的三个输出。也就是三个检测器的输入。每个子项的形式,去第一个,(13,13,3,5+num_classes),3代表每一个13x13的feature map的一个元素(?不止有一个数,是一个矩阵)对应着3个anchor。5代表这位置大小和置信度(1)。num_classes是一个one_hot编码。

  # m张图 m = true_boxes.shape[0] # 得到网格的shape为13,13;26,26;52,52 # for l in range(num_layers): # # # print({0: 32, 1: 16, 2: 8}[l]) #字典 grid_shapes = [input_shape//{0:32, 1:16, 2:8}[l] for l in range(num_layers)] # y_true的格式为(m,13,13,3,85)(m,26,26,3,85)(m,52,52,3,85) y_true = [np.zeros((m,grid_shapes[l][0],grid_shapes[l][1],len(anchor_mask[l]),5+num_classes), dtype='float32') for l in range(num_layers)]

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

第一增加维度是为更好的计算。而anchor/2和取反则是为了计算IoU。可以看作把anchor的中心移到坐标原点,boxes也进行相似的处理。valid_mask主要是有一张图片不一定有20个物体,要把有效边框找出来。(true_boxes: array, shape=(m, T, 5))

  # [1,9,2] anchors = np.expand_dims(anchors, 0) #而anchor/2和取反则是为了计算IoU。可以看作把anchor的中心移到坐标原点 anchor_maxes = anchors / 2. anchor_mins = -anchor_maxes # 长宽要大于0才有效 valid_mask = boxes_wh[..., 0]>0

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这里插入图片描述
遍历整个batch,依次对每一张的图片进行处理( for b in range(m))。

 for b in range(m): # 对每一张图进行处理 wh = boxes_wh[b, valid_mask[b]] if len(wh)==0: continue # [n,1,2] wh = np.expand_dims(wh, -2) box_maxes = wh / 2. box_mins = -box_maxes # 计算真实框和哪个先验框最契合、对于有效的边框,采用与anchor相似的处理方法。 #4,1,2 #1,9,2 intersect_mins = np.maximum(box_mins, anchor_mins) intersect_maxes = np.minimum(box_maxes, anchor_maxes) intersect_wh = np.maximum(intersect_maxes - intersect_mins, 0.) intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1] box_area = wh[..., 0] * wh[..., 1] anchor_area = anchors[..., 0] * anchors[..., 1] iou = intersect_area / (box_area + anchor_area - intersect_area) # 维度是(n) 感谢 消尽不死鸟 的提醒 best_anchor = np.argmax(iou, axis=-1) for t, n in enumerate(best_anchor): for l in range(num_layers): if n in anchor_mask[l]: # floor用于向下取整 i = np.floor(true_boxes[b, t, 0] * grid_shapes[l][1]).astype('int32')  # 中心点x在grid中对应的位置 j = np.floor(true_boxes[b, t, 1] * grid_shapes[l][0]).astype('int32') k = anchor_mask[l].index(n)  # 返回真实框对应最契合的先验框 c = true_boxes[b, t, 4].astype('int32')  # 该框所对应的类别(class) # 将真实框的信息放进y_true与之对应的特征层的相对应中心点中 y_true[l][b, j, i, k, 0:4] = true_boxes[b, t, 0:4] y_true[l][b, j, i, k, 4] = 1  # 置信度,表示有需要检测的物体 y_true[l][b, j, i, k, 5 + c] = 1  # 类别,类似于one_hot编码

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

对于有效的边框,采用与anchor相似的处理方法。

   # 对每一张图进行处理 wh = boxes_wh[b, valid_mask[b]] if len(wh)==0: continue # [n,1,2] wh = np.expand_dims(wh, -2) box_maxes = wh / 2. box_mins = -box_maxes

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

计算重合度最好的iou

 # 计算真实框和哪个先验框最契合、对于有效的边框,采用与anchor相似的处理方法。 #4,1,2 #1,9,2 intersect_mins = np.maximum(box_mins, anchor_mins) intersect_maxes = np.minimum(box_maxes, anchor_maxes) intersect_wh = np.maximum(intersect_maxes - intersect_mins, 0.) intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1] box_area = wh[..., 0] * wh[..., 1] anchor_area = anchors[..., 0] * anchors[..., 1] iou = intersect_area / (box_area + anchor_area - intersect_area) # 维度是(n) 感谢 消尽不死鸟 的提醒 best_anchor = np.argmax(iou, axis=-1)


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在这里插入图片描述
寻找最合适的anchor,也就是一个ground truth box只与一个最佳的anchor box对应。

 for t, n in enumerate(best_anchor): for l in range(num_layers): if n in anchor_mask[l]: # floor用于向下取整 i = np.floor(true_boxes[b, t, 0] * grid_shapes[l][1]).astype('int32')  # 中心点x在grid中对应的位置 j = np.floor(true_boxes[b, t, 1] * grid_shapes[l][0]).astype('int32') k = anchor_mask[l].index(n)  # 返回真实框对应最契合的先验框 c = true_boxes[b, t, 4].astype('int32')  # 该框所对应的类别(class) # 将真实框的信息放进y_true与之对应的特征层的相对应中心点中 y_true[l][b, j, i, k, 0:4] = true_boxes[b, t, 0:4] y_true[l][b, j, i, k, 4] = 1  # 置信度,表示有需要检测的物体 y_true[l][b, j, i, k, 5 + c] = 1  # 类别,类似于one_hot编码

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

loss值计算

正负样本是按照以下规则决定的:
如果一个预测框与所有的Ground Truth的最大IoU<ignore_thresh时,那这个预测框就是负样本。
如果Ground Truth的中心点落在一个区域中,该区域就负责检测该物体。将与该物体有最大IoU的预测框作为正样本(注意这里没有用到ignore thresh,即使该最大IoU<ignore thresh也不会影响该预测框为正样本)
loss函数分四部分:
(1)计算xy(物体中心坐标)的损失
object_mask就是置信度
box_loss_scale可以理解为2-wxh
raw_true_xy就是真实的xy坐标点了
raw_pred[…, :2]是xy预测坐标点
所以第一个式子想对还挺直观,简化为: 在这里插入图片描述
bool是置信度
其中bce是xy值的二值交叉熵损失,这个值越小整个损失值越小
boolx(2-areaPred)的值越小,则需要在确保置信度(bool)的情况下,areaPred需要越大。因此,这部分损失主要优化xy的预测值(bce)和置信度(bool)以及wh回归值(areaPred)
(2)计算wh(anchor长宽回归值)的损失
跟(1)式 的差距就在最后一项,所以我们直接简化之:whloss=boolx(2-areaPred) x(whtrue-whpred)^2,在确保置信度(bool)的情况下,areaPred需要越大,wh需要尽可能靠近真实值wh,这部分主要优化置信度(bool)wh回归值(areaPred、whTrue)
(3)计算置信度损失(前背景)损失
简化为:
在这里插入图片描述
其中bool为置信度,bce为预测值和实际置信度的二值交叉熵,ignore表示iou低于一定阈值的但确实存在的物体,相当于frcnn中的中性点位,既不是前景也不是背景,是忽略的,暂时不计的。
在确保置信度(bool)的情况下,预测值需要尽可能靠近真实值,同时没有物体的部分需要尽可能靠近背景真实值,同时乘以相应的需要忽略点位
这部分主要优化置信度,同时缩减了检测的目标量级。
(4)计算类别损失
这个不用多说了,直接就是置信度乘上个多分类的交叉熵,这部分优化置信度损失和类别损失。
(5)最后,总损失为所有损失之和相加

#---------------------------------------------------#
#   loss值计算
#---------------------------------------------------#
def yolo_loss(args, anchors, num_classes, ignore_thresh=.5, print_loss=False): # 一共有三层 num_layers = len(anchors)//3 # 将预测结果和实际ground truth分开,args是[*model_body.output, *y_true] # y_true是一个列表,包含三个特征层,shape分别为(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。 # yolo_outputs是一个列表,包含三个特征层,shape分别为(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。 y_true = args[num_layers:] yolo_outputs = args[:num_layers] # 先验框 # 678为116,90,  156,198,  373,326 # 345为30,61,  62,45,  59,119 # 012为10,13,  16,30,  33,23,   anchor_mask = [[6,7,8], [3,4,5], [0,1,2]] if num_layers==3 else [[3,4,5], [1,2,3]] # 得到input_shpae为416,416  input_shape = K.cast(K.shape(yolo_outputs[0])[1:3] * 32, K.dtype(y_true[0])) # 得到网格的shape为13,13;26,26;52,52 grid_shapes = [K.cast(K.shape(yolo_outputs[l])[1:3], K.dtype(y_true[0])) for l in range(num_layers)] loss = 0 # 取出每一张图片 # m的值就是batch_size m = K.shape(yolo_outputs[0])[0] mf = K.cast(m, K.dtype(yolo_outputs[0])) # y_true是一个列表,包含三个特征层,shape分别为(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。 # yolo_outputs是一个列表,包含三个特征层,shape分别为(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。 for l in range(num_layers): # 以第一个特征层(m,13,13,3,85)为例子 # 取出该特征层中存在目标的点的位置。(m,13,13,3,1) object_mask = y_true[l][..., 4:5] # 取出其对应的种类(m,13,13,3,80) true_class_probs = y_true[l][..., 5:] # 将yolo_outputs的特征层输出进行处理 # grid为网格结构(13,13,1,2),raw_pred为尚未处理的预测结果(m,13,13,3,85) # 还有解码后的xy,wh,(m,13,13,3,2) grid, raw_pred, pred_xy, pred_wh = yolo_head(yolo_outputs[l], anchors[anchor_mask[l]], num_classes, input_shape, calc_loss=True) # 这个是解码后的预测的box的位置 # (m,13,13,3,4) pred_box = K.concatenate([pred_xy, pred_wh]) # 找到负样本群组,第一步是创建一个数组,[] ignore_mask = tf.TensorArray(K.dtype(y_true[0]), size=1, dynamic_size=True) object_mask_bool = K.cast(object_mask, 'bool') # 对每一张图片计算ignore_mask def loop_body(b, ignore_mask): # 取出第b副图内,真实存在的所有的box的参数 # n,4 true_box = tf.boolean_mask(y_true[l][b,...,0:4], object_mask_bool[b,...,0]) # 计算预测结果与真实情况的iou # pred_box为13,13,3,4 # 计算的结果是每个pred_box和其它所有真实框的iou # 13,13,3,n iou = box_iou(pred_box[b], true_box) # 13,13,3,1 best_iou = K.max(iou, axis=-1) # 判断预测框的iou小于ignore_thresh则认为该预测框没有与之对应的真实框 # 则被认为是这幅图的负样本 ignore_mask = ignore_mask.write(b, K.cast(best_iou<ignore_thresh, K.dtype(true_box))) return b+1, ignore_mask # 遍历所有的图片 _, ignore_mask = K.control_flow_ops.while_loop(lambda b,*args: b<m, loop_body, [0, ignore_mask]) # 将每幅图的内容压缩,进行处理 ignore_mask = ignore_mask.stack() #(m,13,13,3,1,1) ignore_mask = K.expand_dims(ignore_mask, -1) # 将真实框进行编码,使其格式与预测的相同,后面用于计算loss raw_true_xy = y_true[l][..., :2]*grid_shapes[l][:] - grid raw_true_wh = K.log(y_true[l][..., 2:4] / anchors[anchor_mask[l]] * input_shape[::-1]) # object_mask如果真实存在目标则保存其wh值 # switch接口,就是一个if/else条件判断语句 raw_true_wh = K.switch(object_mask, raw_true_wh, K.zeros_like(raw_true_wh)) box_loss_scale = 2 - y_true[l][...,2:3]*y_true[l][...,3:4] xy_loss = object_mask * box_loss_scale * K.binary_crossentropy(raw_true_xy, raw_pred[...,0:2], from_logits=True) wh_loss = object_mask * box_loss_scale * 0.5 * K.square(raw_true_wh-raw_pred[...,2:4]) # 如果该位置本来有框,那么计算1与置信度的交叉熵 # 如果该位置本来没有框,而且满足best_iou<ignore_thresh,则被认定为负样本 # best_iou<ignore_thresh用于限制负样本数量 confidence_loss = object_mask * K.binary_crossentropy(object_mask, raw_pred[...,4:5], from_logits=True)+ \ (1-object_mask) * K.binary_crossentropy(object_mask, raw_pred[...,4:5], from_logits=True) * ignore_mask class_loss = object_mask * K.binary_crossentropy(true_class_probs, raw_pred[...,5:], from_logits=True) xy_loss = K.sum(xy_loss) / mf wh_loss = K.sum(wh_loss) / mf confidence_loss = K.sum(confidence_loss) / mf class_loss = K.sum(class_loss) / mf loss += xy_loss + wh_loss + confidence_loss + class_loss if print_loss: loss = tf.Print(loss, [loss, xy_loss, wh_loss, confidence_loss, class_loss, K.sum(ignore_mask)], message='loss: ') return loss

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110

文章来源: blog.csdn.net,作者:快了的程序猿小可哥,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/qq_35914625/article/details/108443561

(完)