人工智能学习70-Yolo损失函数

人工智能学习70-Yolo损失函数 —快手视频
人工智能学习71-Yolo损失函数 —快手视频
人工智能学习72-Yolo损失函数 —快手视频
人工智能学习73-Yolo损失函数 —快手视频

Yolo算法损失函数

损失函数是指定Yolo网络训练的标尺,故此损失函数定义非常关键,Yolo网络主要完成目标检测,目标检测涉及量化指标包括包含物体的预测框Box(Box的中心坐标x,y,Box的宽高数据);每个规格特征图提供三个先验框,先验框中是否包含物体的置信度;预测框Box中包含物体的类别。因此损失函数需要度量这三方面量化指标预测框Box位置及大小,是否包含物体置信度confidence,预测框Box中物体类别指标。
在这里插入图片描述

损失函数定义

yolo_training.py

import math
from functools import partial

import tensorflow as tf
from keras import backend as K
from utils_bbox import get_anchors_and_decode

# ---------------------------------------------------#
#   box_ciou包含两个矩形交并比例,还有一个修正值,修正值具体含义有待深究
#   box_iou与box_ciou优缺点可参考
#   https://zhuanlan.zhihu.com/p/648882134
# ---------------------------------------------------#
def box_ciou(b1, b2):
    """
    输入为:
    ----------
    b1: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh
    b2: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh

    返回为:
    -------
    ciou: tensor, shape=(batch, feat_w, feat_h, anchor_num, 1)
    """
    # -----------------------------------------------------------#
    #   求出预测框左上角右下角
    #   b1_mins     (batch, feat_w, feat_h, anchor_num, 2)
    #   b1_maxes    (batch, feat_w, feat_h, anchor_num, 2)
    # -----------------------------------------------------------#
    b1_xy = b1[..., :2]  # 矩形b1中心的坐标 0=x,1=y
    b1_wh = b1[..., 2:4]  # 取2-4两个元素,对应宽高 2=w,3=h
    b1_wh_half = b1_wh/2.  # 宽与高都取半数
    b1_mins = b1_xy - b1_wh_half  # 矩形框左上角坐标
    b1_maxes = b1_xy + b1_wh_half  # 矩形框右下角坐标
    # -----------------------------------------------------------#
    #   求出真实框左上角右下角
    #   b2_mins     (batch, feat_w, feat_h, anchor_num, 2)
    #   b2_maxes    (batch, feat_w, feat_h, anchor_num, 2)
    # -----------------------------------------------------------#
    b2_xy = b2[..., :2]  # 矩形b2中心的坐标 0=x,1=y
    b2_wh = b2[..., 2:4]  # 矩形b2宽高 2=2,3=h
    b2_wh_half = b2_wh/2.
    b2_mins = b2_xy - b2_wh_half  # 左上角坐标
    b2_maxes = b2_xy + b2_wh_half  # 右下角坐标

    # -----------------------------------------------------------#
    #   求真实框和预测框所有的iou
    #   iou         (batch, feat_w, feat_h, anchor_num)
    # -----------------------------------------------------------#
    intersect_mins = K.maximum(b1_mins, b2_mins)  # 取两个矩形框最左上角坐标最大值
    intersect_maxes = K.minimum(b1_maxes, b2_maxes)  # 取两个矩形框最右下角坐标最小值
    intersect_wh = K.maximum(intersect_maxes - intersect_mins, 0.)  # 取两个矩形框叠加后宽与高较大者
    intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]  # 计算两个矩形框组合后面积
    b1_area = b1_wh[..., 0] * b1_wh[..., 1]  # 矩形框1的面积
    b2_area = b2_wh[..., 0] * b2_wh[..., 1]  # 矩形框2的面积
    union_area = b1_area + b2_area - intersect_area  # 取两个矩形框面积的并集,去掉一份重回部分面积
    iou = intersect_area / K.maximum(union_area, K.epsilon())  # 计算两个矩形框组合后面积与重叠面积比例iou

    # -----------------------------------------------------------#
    #   计算中心的差距
    #   center_distance (batch, feat_w, feat_h, anchor_num)
    # -----------------------------------------------------------#
    center_distance = K.sum(K.square(b1_xy - b2_xy), axis=-1)  # 计算两矩形框中心间距离的平方
    enclose_mins = K.minimum(b1_mins, b2_mins)  # 获取两矩形框最左上角坐标
    enclose_maxes = K.maximum(b1_maxes, b2_maxes)  # 获取两矩形框最右下角坐标
    enclose_wh = K.maximum(enclose_maxes - enclose_mins, 0.0)  # 获取两矩形框组合最大宽与高
    # -----------------------------------------------------------#
    #   计算预测框与真实框组合最大矩形对角线距离平方和
    #   enclose_diagonal (batch, feat_w, feat_h, anchor_num)
    #   K.epsilon() 返回数值表达式中使用的模糊因子的值
    # -----------------------------------------------------------#
    enclose_diagonal = K.sum(K.square(enclose_wh), axis=-1)  # 获取两矩形框组合宽与高平方和(w^2+h^2)
    # 其中 center_distance / K.maximum(enclose_diagonal, K.epsilon()) 代表
    # "两矩形中心之间距离平方" 与 "两矩形组合后对角线平方和" 之比
    # 可理解为两矩形中心偏离程度比例
    # iou代表两矩形交集与并集面积之比
    # ciou代表交并比例减去两矩形中心偏离程度比例,
    ciou = iou - 1.0 * center_distance / K.maximum(enclose_diagonal, K.epsilon())

    # (atan2计算 矩形1的w/h反正切值, 与矩形2的w/h反正切值之差)的平方
    # 变量v是此平方值与math.pi平方之比的4倍
    # 变量v代表两个矩形宽与高比例平均值的4倍
    v = 4 * K.square(tf.math.atan2(b1_wh[..., 0], K.maximum(b1_wh[..., 1], K.epsilon())) - tf.math.atan2(b2_wh[..., 0],
                                 K.maximum(b2_wh[..., 1],K.epsilon()))) / (math.pi * math.pi)
    #alpha为一比例值,计算为 v/(1.0-iou+v)
    alpha = v / K.maximum((1.0 - iou + v), K.epsilon())
    #ciou重新赋值,其值减去alpha*v
    ciou = ciou - alpha * v
    #变量ciou在最后轴添加一维度,具体含义包含两矩形交并比例,还有一定修正值,更深层含义待分析
    ciou = K.expand_dims(ciou, -1)
    return ciou

# ---------------------------------------------------#
#   用于计算两个矩形交并比例
#   当预测框与真实框不存在重叠时,方法box_iou存在缺陷
# ---------------------------------------------------#
def box_iou(b1, b2):
    # ---------------------------------------------------#
    #   num_anchor,1,4
    #   计算左上角的坐标和右下角的坐标
    # ---------------------------------------------------#
    b1          = K.expand_dims(b1, -2)
    b1_xy       = b1[..., :2]  # 取前两个元素
    b1_wh       = b1[..., 2:4]  # 取2-4两个元素
    b1_wh_half  = b1_wh/2.
    b1_mins     = b1_xy - b1_wh_half
    b1_maxes    = b1_xy + b1_wh_half

    # ---------------------------------------------------#
    #   1,n,4
    #   计算左上角和右下角的坐标
    # ---------------------------------------------------#
    b2          = K.expand_dims(b2, 0)
    b2_xy       = b2[..., :2]
    b2_wh       = b2[..., 2:4]
    b2_wh_half  = b2_wh/2.
    b2_mins     = b2_xy - b2_wh_half
    b2_maxes    = b2_xy + b2_wh_half

    # ---------------------------------------------------#
    #   计算重合面积
    # ---------------------------------------------------#
    intersect_mins  = K.maximum(b1_mins, b2_mins)
    intersect_maxes = K.minimum(b1_maxes, b2_maxes)
    intersect_wh    = K.maximum(intersect_maxes - intersect_mins, 0.)
    intersect_area  = intersect_wh[..., 0] * intersect_wh[..., 1]
    b1_area         = b1_wh[..., 0] * b1_wh[..., 1]  # 计算矩形框1的面积
    b2_area         = b2_wh[..., 0] * b2_wh[..., 1]  # 计算矩形框2的面积
    iou             = intersect_area / (b1_area + b2_area - intersect_area) #两矩形交叉面积与总面积比例
    return iou

#---------------------------------------------------#
#   loss值计算
#---------------------------------------------------#
def yolo_loss(
    args, # 由nets/yolo.py中[*model_body.output, *y_true]打包成的列表
    input_shape, 
    anchors, 
    anchors_mask, 
    num_classes, 
    ignore_thresh   = 0.5,
    balance         = [0.4, 1.0, 4],
    box_ratio       = 0.05, 
    obj_ratio       = 1, 
    cls_ratio       = 0.5 / 4, 
    ciou_flag       = True, 
    print_loss      = False
):
    print('2...args=', (args))
    #2...args= [<tf.Tensor 'conv2d_59/BiasAdd:0' shape=(?, ?, ?, 255) dtype=float32>,
    # <tf.Tensor 'conv2d_67/BiasAdd:0' shape=(?, ?, ?, 255) dtype=float32>,
    # <tf.Tensor 'conv2d_75/BiasAdd:0' shape=(?, ?, ?, 255) dtype=float32>,
    # <tf.Tensor 'input_2:0' shape=(?, 13, 13, 3, 85) dtype=float32>,
    # <tf.Tensor 'input_3:0' shape=(?, 26, 26, 3, 85) dtype=float32>,
    # <tf.Tensor 'input_4:0' shape=(?, 52, 52, 3, 85) dtype=float32>]
    print('yolo_training.py yolo_loss() anchors_mask=',anchors_mask)
    #yolo_training.py yolo_loss() anchors_mask= [[6, 7, 8], [3, 4, 5], [0, 1, 2]]
    num_layers      = len(anchors_mask)
    # ---------------------------------------------------------------------------------------------------#
    #   将预测结果和实际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中x,y是离散取值的;yolo_outputs中x,y是连续取值的
    # ---------------------------------------------------------------------------------------------------#
    y_true          = args[num_layers:]  # 列表中后三个数据,标签数据
    yolo_outputs    = args[:num_layers]  # 列表中前三个数据,模型预测数据
    # -----------------------------------------------------------#
    #   得到input_shape为416,416
    #   k.cast 将张量强制转换为不同的类型并返回
    # -----------------------------------------------------------#
    input_shape = K.cast(input_shape, 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)]
    #yolo_training.py yolo_loss() grid_shapes=
    # [<tf.Tensor 'yolo_loss/Cast_1:0' shape=(2,) dtype=float32>,
    # <tf.Tensor 'yolo_loss/Cast_2:0' shape=(2,) dtype=float32>,
    # <tf.Tensor 'yolo_loss/Cast_3:0' shape=(2,) dtype=float32>]
    # -----------------------------------------------------------#
    #   取出图片数量
    #   m的值就是batch_size
    #   K.shape 返回张量或变量的符号形状
    # -----------------------------------------------------------#
    m = K.shape(yolo_outputs[0])[0]
    loss    = 0  #损失标量
    # ---------------------------------------------------------------------------------------------------#
    #   遍历3个特征层
    #   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]  # y_true张量中第5个元素,真实框内是否存在物体置信度
        #  object_mask=== Tensor("yolo_loss/strided_slice_4:0", shape=(?, 13, 13, 3, 1), dtype=float32)
        #  object_mask=== Tensor("yolo_loss/strided_slice_41:0", shape=(?, 26, 26, 3, 1), dtype=float32)
        #  object_mask=== Tensor("yolo_loss/strided_slice_78:0", shape=(?, 52, 52, 3, 1), dtype=float32)
        # -----------------------------------------------------------#
        #   取出其对应的种类(m,13,13,3,80)
        #   y_true张量中第5个元素以后所有元素,真实框内存在物体的种类,最后一维度第5元素以后是物体种类数据,一共80个物体分类
        # -----------------------------------------------------------#
        true_class_probs    = y_true[l][..., 5:]
        #  true_class_probs= Tensor("yolo_loss/strided_slice_5:0", shape=(?, 13, 13, 3, 80), dtype=float32)
        #  true_class_probs= Tensor("yolo_loss/strided_slice_42:0", shape=(?, 26, 26, 3, 80), dtype=float32)
        #  true_class_probs= Tensor("yolo_loss/strided_slice_79:0", shape=(?, 52, 52, 3, 80), dtype=float32)
        # -----------------------------------------------------------#
        #   根据yolo_outputs的特征层和先验框anchors获取预测框归一化数据
        #   get_anchors_and_decode训练时:返回归一化grid, feats, box_xy, box_wh
        #   其中:
        #   grid        (13,13,3,2) 网格坐标
        #   raw_pred    (m,13,13,3,85) 尚未处理的预测结果
        #   pred_xy     (m,13,13,3,2) 解码后的中心坐标
        #   pred_wh     (m,13,13,3,2) 解码后的宽高坐标
        # -----------------------------------------------------------#
        grid, raw_pred, pred_xy, pred_wh = get_anchors_and_decode(yolo_outputs[l],
              anchors[anchors_mask[l]], num_classes, input_shape, calc_loss=True)
        # -----------------------------------------------------------#
        #   预测框pred_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.shape=(?, 13, 13, 3, 1), dtype=bool
        # object_mask_bool.shape=(?, 26, 26, 3, 1), dtype=bool
        # object_mask_bool.shape=(?, 52, 52, 3, 1), dtype=bool
        object_mask_bool = K.cast(object_mask, 'bool')
        # -----------------------------------------------------------#
        #   对每一张图片计算ignore_mask
        # -----------------------------------------------------------#
        def loop_body(b, ignore_mask):
            # -----------------------------------------------------------#
            #   b为图片数量维度,取出n个真实框:n,4;前四维度为x,y,w,h;object_mask_bool存储是否存在物体
            #   1-D 例程
            #   tensor = [0, 1, 2, 3]
            #   mask = np.array([True, False, True, False])
            #   boolean_mask(tensor, mask)  # [0, 2]
            #   true_box代表存在物体的真实框
            #   object_mask_bool[b, ..., 0].shape=(13, 13, 3), dtype=bool
            #   object_mask_bool[b, ..., 0].shape=(26, 26, 3), dtype=bool
            #   object_mask_bool[b, ..., 0].shape=(52, 52, 3), dtype=bool
            # -----------------------------------------------------------#
            true_box = tf.boolean_mask(y_true[l][b, ..., 0:4], object_mask_bool[b, ..., 0])
            # -----------------------------------------------------------#
            #   计算预测框与真实框的iou的交并比例,数据都是归一化的数据
            #   pred_box    13,13,3,4 预测框的坐标
            #   true_box    n,4 真实框的坐标
            #   iou         shape=(?, ?, 3, ?) 预测框和真实框的iou,其中3代表3个特征层预测,每个特征层有3个先验框
            # -----------------------------------------------------------#
            iou = box_iou(pred_box[b], true_box)
            # -----------------------------------------------------------#
            #   best_iou    shape=(?, ?, 3) 每个特征点与真实框的最大重合程度,其中3代表每一特征层3个预测框
            # -----------------------------------------------------------#
            best_iou = K.max(iou, axis=-1)
            # -----------------------------------------------------------#
            #   判断预测框和真实框的最大iou小于ignore_thresh
            #   则认为该预测框没有与之对应的真实框
            #   该操作的目的是:
            #   忽略预测结果与真实框非常对应特征点,因为这些框已经比较准了
            #   不适合当作负样本,所以忽略掉。
            #   k.cast(x,dtype) 转化张量为dtype类型
            #   tf.TensorArray.write(pos,val)通过序号索引b向数组tf.TensorArray写入值val
            #   ignore_mask 存储0.0或1.0两类数据
            # -----------------------------------------------------------#
            ignore_mask = ignore_mask.write(b, K.cast(best_iou<ignore_thresh, K.dtype(true_box)))
            return b+1, ignore_mask

        # -----------------------------------------------------------#
        #   在这个地方进行一个循环、循环是对每一张图片进行的.返回值_, ignore_mask就是loop_body的返回值
        #   tf.while_loop(cond,  body, loop_vars) 条件cond为真循环执行body,loop_vars为body传入参数
        #   可以这样理解:
        #   loop_vars = []
        #   while cond(loop_vars):
        #    loop_vars = body(loop_vars)
        #   即loop_vars参数先传入cond 判断条件是否成立,成立之后,把 loop_vars参数传入body 执行操作, 然后返回 操作后的 loop_vars 参数,
        #   即loop_vars参数已被更新,再把更新后的参数传入cond, 依次循环,直到不满足条件。
        #
        #   b初始值为0,m是图片数量,将[0, ignore_mask]传递给(lambda b, *args: b < m),条件为真时,再将[0, ignore_mask]传递给loop_body
        #   计算b和ignore_mask并更新它们,再次循环判断(lambda b, *args: b < m)是否为真,决定是否继续执行下去
        # -----------------------------------------------------------#
        _, ignore_mask = tf.while_loop(lambda b, *args: b < m, loop_body, [0, ignore_mask])
        # -----------------------------------------------------------#
        #   ignore_mask用于提取出作为负样本的特征点
        #   (m,13,13,3)
        # -----------------------------------------------------------#
        ignore_mask = ignore_mask.stack()  #shape=(?, ?, ?, 3), dtype=float32
        #  k.expand_dims(x, axis=-1)  在最后轴扩充一维度
        ignore_mask = K.expand_dims(ignore_mask, -1) #shape=(?, ?, ?, 3, 1), dtype=float32)
        # -----------------------------------------------------------#
        #   y_true[l][..., 2:3]和y_true[l][..., 3:4]
        #   表示真实框的宽高,二者均在0-1之间
        #   真实框越大,比重越小,小框的比重更大。
        #   box_loss_scale定义为2-真实框w*h
        # -----------------------------------------------------------#
        box_loss_scale = 2 - y_true[l][..., 2:3] * y_true[l][..., 3:4]
        if ciou_flag: #如果使用ciou计算交并比例
            # -----------------------------------------------------------#
            #   计算Ciou loss
            #   k.sum(x, axis=None, keepdims=False)  沿某轴合并张量求和
            # -----------------------------------------------------------#
            raw_true_box    = y_true[l][..., 0:4] #真实框x,y,w,h,都是归一化数据
            ciou            = box_ciou(pred_box, raw_true_box)
            ciou_loss       = object_mask * (1 - ciou)
            location_loss   = K.sum(ciou_loss)
        else:
            # -----------------------------------------------------------#
            #   将真实框进行编码,使其格式与预测的相同,后面用于计算loss
            #   k.log(x) 求x对数
            # -----------------------------------------------------------#
            raw_true_xy     = y_true[l][..., :2] * grid_shapes[l][::-1] - grid #转化为相对网格数
            # 转化为raw_pred数据规格,pred_wh是tf.exp(x)* anchors_tensor/input_shape[::-1]  x=feats[..., 2:4] 作为真数,真正x需要求对数
            # y_true[l][..., 2:4]*input_shape[::-1] / anchors_tensor 转化为归一化数据作为真数,需要求对数
            raw_true_wh     = K.log(y_true[l][..., 2:4] / anchors[anchors_mask[l]] * input_shape[::-1])

            # -----------------------------------------------------------#
            #   object_mask如果真实存在目标则保存其wh值
            #   switch接口,就是一个if/else条件判断语句
            #   k.switch(condition, then_expression, else_expression) 根据标量值在两个操作之间切换
            #   k.zeros_like 初始化全部为0
            # -----------------------------------------------------------#
            raw_true_wh     = K.switch(object_mask, raw_true_wh, K.zeros_like(raw_true_wh))
            # -----------------------------------------------------------#
            #   利用binary_crossentropy计算中心点偏移情况,效果更好
            #   k.binary_crossentropy(target, output, from_logits=False) 输出张量和目标张量之间的二进制交叉熵
            # -----------------------------------------------------------#
            xy_loss         = object_mask * box_loss_scale * K.binary_crossentropy(raw_true_xy, raw_pred[..., 0:2], from_logits=True)
            # -----------------------------------------------------------#
            #   wh_loss用于计算宽高损失
            #   k.square(x) 求x平方
            # -----------------------------------------------------------#
            wh_loss         = object_mask * box_loss_scale * 0.5 * K.square(raw_true_wh - raw_pred[..., 2:4])
            location_loss   = (K.sum(xy_loss) + K.sum(wh_loss)) * 0.1

        # ------------------------------------------------------------------------------#
        #   如果该位置本来有框,那么计算1与置信度的交叉熵
        #   如果该位置本来没有框,那么计算0与置信度的交叉熵
        #   在这其中会忽略一部分样本,这些被忽略的样本满足条件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)

        # -----------------------------------------------------------#
        #   计算正样本数量,负样本数量
        #   tf.maximum(x, y, name=None) 求x,y中最大者
        # -----------------------------------------------------------#
        num_pos         = tf.maximum(K.sum(K.cast(object_mask, tf.float32)), 1)
        num_neg         = tf.maximum(K.sum(K.cast((1 - object_mask) * ignore_mask, tf.float32)), 1)
        # -----------------------------------------------------------#
        #   将所有损失求和
        # -----------------------------------------------------------#
        location_loss   = location_loss * box_ratio / num_pos #位置损失*box比率/正样本数
        confidence_loss = K.sum(confidence_loss) * balance[l] * obj_ratio / (num_pos + num_neg) #物体置信度*平衡系数*物体比率/总样本数
        class_loss      = K.sum(class_loss) * cls_ratio / num_pos / num_classes #分类损失*分类比率/正样本数/分类总数

        loss            += location_loss + confidence_loss + class_loss
        if print_loss:
            loss = tf.Print(loss, [loss, location_loss, confidence_loss, class_loss, tf.shape(ignore_mask)],
                            summarize=100, message='loss: ')
    return loss


def get_lr_scheduler(lr_decay_type, lr, min_lr, total_iters, warmup_iters_ratio = 0.05, warmup_lr_ratio = 0.1,
                     no_aug_iter_ratio = 0.05, step_num = 10):
    def yolox_warm_cos_lr(lr, min_lr, total_iters, warmup_total_iters, warmup_lr_start, no_aug_iter, iters):
        #如果迭代次数iters小于warmup_total_iters
        if iters <= warmup_total_iters:
            #调整学习率lr
            lr = (lr - warmup_lr_start) * pow(iters / float(warmup_total_iters), 2) + warmup_lr_start
        elif iters >= total_iters - no_aug_iter:
            #如果迭代次数iters大于总次数total_iters-no_aug_iter,取最小学习率
            lr = min_lr
        else:
            #其他情况,调整学习率lr
            lr = min_lr + 0.5 * (lr - min_lr) * (
                1.0
                + math.cos(
                    math.pi
                    * (iters - warmup_total_iters)
                    / (total_iters - warmup_total_iters - no_aug_iter)
                )
            )
        return lr

    #根据迭代次数调整学习率
    def step_lr(lr, decay_rate, step_size, iters):
        if step_size < 1:
            raise ValueError("step_size must above 1.")
        n       = iters // step_size
        out_lr  = lr * decay_rate ** n
        return out_lr

    if lr_decay_type == "cos":
        warmup_total_iters  = min(max(warmup_iters_ratio * total_iters, 1), 3)
        warmup_lr_start     = max(warmup_lr_ratio * lr, 1e-6)
        no_aug_iter         = min(max(no_aug_iter_ratio * total_iters, 1), 15)
        func = partial(yolox_warm_cos_lr, lr, min_lr, total_iters, warmup_total_iters, warmup_lr_start, no_aug_iter)
    else:
        decay_rate  = (min_lr / lr) ** (1 / (step_num - 1))
        step_size   = total_iters / step_num
        func = partial(step_lr, lr, decay_rate, step_size)

    return func

代码解释部分

方法box_iou第122行
intersect_mins = K.maximum(b1_mins, b2_mins)
intersect_maxes = K.minimum(b1_maxes, b2_maxes)
intersect_wh = K.maximum(intersect_maxes - intersect_mins, 0.)
intersect_area = intersect_wh[…, 0] * intersect_wh[…, 1]
当两个预测框存在重叠时,使用box_iou计算交并比是正确的,如果两个预测框不存在重叠时,使用上述代码计算交并比是错误的,如下图:
在这里插入图片描述

方法box_ciou第62行
在这里插入图片描述

Box_ciou在box_iou基础上减去两个调整因子,一个是两Box中心距D2与外接矩形对角线D1的平方比。
在这里插入图片描述

二是将两个Box矩形宽高比例因素作为调整因子,两个矩形宽高比例差距越大,需要调整交并比例的数值就越大。
在这里插入图片描述

参考文档:https://zhuanlan.zhihu.com/p/648882134
在这里插入图片描述

方法yolo_loss第135行
Yolo算法损失函数是使用Lambda封装了方法yolo_loss作为网络的最后一层。由于需要自定义损失,故此必须将网络预测值和真实值y_true作为参数传递到方法yolo_loss。
在这里插入图片描述

参数args通过Lambda封装函数传入,在文件yolo_model.py的get_train_model()方法。
在这里插入图片描述

参数([*model_body.output, *y_true])将model_body.output与y_true封装为List列表作为参数传递到yolo_loss的args。model_body.output是网络预测输出,y_true是真实值替位符形式参数,其真实值是通过
train.py中的model.fit_generator()方法传入的。
调用过程:
参考keras.engine.training.py 第1951行 fit_generator --> 第2228行 self.train_on_batch -->第1883行self.train_function(ins) 将x,y连接
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

方法yolo_loss第181行
grid_shapes = [K.cast(K.shape(yolo_outputs[l])[1:3], K.dtype(y_true[0])) for l in range(num_layers)]
循环遍历每个层,将yolo_outputs中第1,2个元素(w,h)转化为y_true类型,生成三个列表List,分别是[13,13],[26,26],[52,52]。
例程:
yolo_outputs0 = np.random.normal(0, 10, 13 * 13 * 3 * 85)
yolo_outputs1 = np.random.normal(0, 10, 26 * 26 * 3 * 85)
yolo_outputs2 = np.random.normal(0, 10, 52 * 52 * 3 * 85)
yolo_outputs0 = yolo_outputs0.reshape(13, 13, 3, 85)
yolo_outputs1 = yolo_outputs1.reshape(26, 26, 3, 85)
yolo_outputs2 = yolo_outputs2.reshape(52, 52, 3, 85)
yolo_outputs = [yolo_outputs0, yolo_outputs1, yolo_outputs2]
yolo_outputs = np.array(yolo_outputs)
y_true = np.arange(0, 1, 1)
y_true = K.cast(y_true, tf.float32)
num_layers = 3
grid_shapes = [K.cast(K.shape(yolo_outputs[l])[1:3], K.dtype(y_true)) for l in range(num_layers)]
print(‘grid_shapes=’, grid_shapes)
with tf.Session() as sess:
print(‘yolo_outputs尺寸=’, K.shape(yolo_outputs[0]).eval() )
grid_shapes= [<tf.Tensor ‘Cast_2:0’ shape=(2,) dtype=float32>, <tf.Tensor ‘Cast_3:0’ shape=(2,) dtype=float32>, <tf.Tensor ‘Cast_4:0’ shape=(2,) dtype=float32>]
yolo_outputs尺寸= [13 13 3 85]

方法yolo_loss第224行

grid, raw_pred, pred_xy, pred_wh = get_anchors_and_decode(yolo_outputs[l],
anchors[anchors_mask[l]], num_classes, input_shape, calc_loss=True)
根据Yolo网络预测值返回归一化grid,feats,box_xy,box_wh。

方法yolo_loss第235行
ignore_mask = tf.TensorArray(K.dtype(y_true[0]), size=1, dynamic_size=True)

定义TensorArray数组
例程
a = tf.TensorArray(tf.float32, size=2, dynamic_size=True)
a = a.write(0, [0, 1]) # 这里的write需要赋值给对方.
a = a.write(1, [1, 0])
a = a.write(2, [1, 1])

read_value = a.read(0) # 读取某个索引下的值=[0. 1.]
stack_value = a.stack() #[[0. 1.] [1. 0.] [1. 1.]]
concat_value = a.concat() #[0. 1. 1. 0. 1. 1.]
gather_value = a.gather([1, 2]) # gather是look up的意思. [[1. 0.] [1. 1.]]

方法yolo_loss第255行
true_box = tf.boolean_mask(y_true[l][b, …, 0:4], object_mask_bool[b, …, 0])
object_mask_bool[b, …, 0]中…用法

  1. 省略号
    Ellipsis就是省略号(…),省略号(…)就是Ellipsis。而Ellipsis是ellipsis类的唯一实例(singleton object)
    print(type(…)) # output: <class ‘ellipsis’>
    print(Ellipsis == …) # True
    print(…) # Ellipsis

  2. 类型提示
    关于Python中的类型提示(type hints)详见【Python】作为动态语言,Python中的“类型声明”有什么用?。省略号(…)在类型提示中经常被使用
    from typing import Callable, Tuple

print(Callable[…, int]) # 输入参数随意,返回值为int
print(Tuple[int, …]) # int组成的元组

3.函数内部,相当于pass
def m1(): pass

def m2(): …

4.索引切片
nd = np.arange(13133*3)
nd = np.reshape(nd,(1, 13,13, 3, -1))
t = tf.convert_to_tensor(nd)
print(‘t===’, t)
print(‘t[0,…,0]=', t[0, …, 0]) 取最后一维第1个元素,返回shape=(13, 13, 3)
print('t[0,…,0]
=’, t[0,…,0:1]) 取最后一维第1个元素,返回shape=(13, 13, 3, 1)
print(‘t[0,…,0]===’, t[0, …, 0:3]) 取最后一维第1,2,3个元素,返回shape=(13, 13, 3, 1)

方法yolo_loss第288行
_, ignore_mask = tf.while_loop(lambda b, *args: b < m, loop_body, [0, ignore_mask])
b初始值为0,m是图片数量,将[0, ignore_mask]传递给(lambda b, *args: b < m),条件为真时,再将[0, ignore_mask]传递给loop_body,计算b和ignore_mask并更新它们,再次循环判断(lambda b, *args: b < m)是否为真,决定是否继续执行下去。

def loop_body(b, ignore_mask):
# -----------------------------------------------------------#
# b为图片数量维度,取出n个真实框:n,4;前四维度为x,y,w,h;object_mask_bool存储是否存在物体
# 1-D 例程
# tensor = [0, 1, 2, 3]
# mask = np.array([True, False, True, False])
# boolean_mask(tensor, mask) # [0, 2]
# true_box代表存在物体的真实框
# -----------------------------------------------------------#
true_box = tf.boolean_mask(y_true[l][b, …, 0:4], object_mask_bool[b, …, 0])
# -----------------------------------------------------------#
# 计算预测框与真实框的iou的交并比例,数据都是归一化的数据
# pred_box 13,13,3,4 预测框的坐标
# true_box n,4 真实框的坐标
# iou shape=(?, ?, 3, ?) 预测框和真实框的iou,其中3代表3个特征层预测,每个特征层有3个先验框
# -----------------------------------------------------------#
iou = box_iou(pred_box[b], true_box)
# -----------------------------------------------------------#
# best_iou shape=(?, ?, 3) 每个特征点与真实框的最大重合程度,其中3代表每一特征层3个预测框
# -----------------------------------------------------------#
best_iou = K.max(iou, axis=-1)
# -----------------------------------------------------------#
# 判断预测框和真实框的最大iou小于ignore_thresh
# 则认为该预测框没有与之对应的真实框
# 该操作的目的是:
# 忽略预测结果与真实框非常对应特征点,因为这些框已经比较准了
# 不适合当作负样本,所以忽略掉。
# k.cast(x,dtype) 转化张量为dtype类型
# tf.TensorArray.write(pos,val)通过序号索引b向数组tf.TensorArray写入值val
# ignore_mask 存储0.0或1.0两类数据
# -----------------------------------------------------------#
ignore_mask = ignore_mask.write(b, K.cast(best_iou<ignore_thresh, K.dtype(true_box)))
return b+1, ignore_mask
_, ignore_mask = tf.while_loop(lambda b, *args: b < m, loop_body, [0, ignore_mask])
M是图片数量,b从0循环递增到m,遍历每幅图片,每个特征层3个先验框,当交并比小于ignore_thresh时,忽略此先验框,故此ignore_mask最大维度(?,?,?,3),其中数值为0.0或1.0。

方法yolo_loss第295行
ignore_mask = K.expand_dims(ignore_mask, -1) #shape=(?, ?, ?, 3, 1), dtype=float32)
例程
list = [[1,2],[3,4]]
nd = np.array(list)
print(‘nd=’, nd.shape)
nd1 = np.expand_dims(nd, axis=0)
print(‘nd1=’, nd1.shape)
nd2 = np.expand_dims(nd, axis=-1)
print(‘nd2=’, nd2.shape)
nd3 = np.expand_dims(nd, axis=-2)
print(‘nd3=’, nd3.shape)
nd= (2, 2)
nd1= (1, 2, 2)
nd2= (2, 2, 1)
nd3= (2, 1, 2)

方法yolo_loss第310行
当参数ciou_flag为True时执行此分支。
raw_true_box = y_true[l][…, 0:4] #真实框x,y,w,h,都是归一化数据
ciou = box_ciou(pred_box, raw_true_box)
ciou_loss = object_mask * (1 - ciou)
location_loss = K.sum(ciou_loss)
张量object_mask尺寸为(?,13,13,3,1)、 (?,26,26,3,1)、(?,52,52,3,1)其中3代表先验框,最后一维数据为1时代表此先验框存在物体,为0时代表不存在物体。object_mask * (1 - ciou)含义为存在物体的预测框交并比损失,以此作为Box预测的位置损失。

方法yolo_loss第317行
raw_true_xy = y_true[l][…, :2] * grid_shapes[l][::-1] - grid #转化为相对网格数
当参数ciou_flag为False时执行此分支。
标签数据y_true是通过dataloader.py计算获取的,都是归一化的相对数据,因此需要再次减去grid的网格数量。
在这里插入图片描述
方法yolo_loss第320行

raw_true_wh = K.log(y_true[l][…, 2:4] / anchors[anchors_mask[l]] * input_shape[::-1])
标签数据y_true是通过dataloader.py计算获取的,都是归一化的相对数据。
在这里插入图片描述
数值y_true[l][…, 2:4] / anchors[anchors_mask[l]] * input_shape[::-1]作为真数求其对数作为标签数据。
在这里插入图片描述
预测数据raw_pred来自于utils_bbox.py的方法get_anchors_and_decode()
在这里插入图片描述
y_true[l][…, 2:4]与raw_pred做差值运算,数据算法必须统一口径。

方法yolo_loss第349行
在这里插入图片描述
置信度损失包括两部分,一是预测框中存在物体的置信度,二是预测框中不存在物体的置信度。
交叉熵损失函数
在这里插入图片描述
预测框中存在物体的置信度(预测为无物体):
object_mask * K.binary_crossentropy(object_mask, raw_pred[…, 4:5], from_logits=True)
比如: object_mask = (…,1…),
真实框存在物体,如果预测没有物体,预测值raw_pred[…, 4:5] = (…,0…),预测错误
L = 1log(0) = ∞,说明预测错误,损失为∞;
真实框存在物体,如果预测有物体,如果预测值raw_pred[…, 4:5] = (…,1…),预测正确
L = 1
log(1) = 0,说明预测正确,损失为0

预测框中不存在物体的置信度
(1 - object_mask) * K.binary_crossentropy(object_mask, raw_pred[…, 4:5], from_logits=True) * ignore_mask
比如: (1- object_mask) = (…,1…),
真实框不存在物体,如果预测没有物体,如果预测值raw_pred[…, 4:5] = (…,0…),ignore_mask = (…,0…)预测正确
L = 1log(1-0) * 0 = 0,说明预测正确,损失为0;
真实框不存在物体,如果预测有物体,如果预测值raw_pred[…, 4:5] = (…,1…),ignore_mask = (…,1…)预测错误
L = 1
log(1-1) * 1 = ∞,说明预测错误,损失为∞

方法yolo_loss第352行
class_loss = object_mask * K.binary_crossentropy(true_class_probs, raw_pred[…, 5:], from_logits=True)
交叉熵损失函数
在这里插入图片描述
当预测物体分类正确时:
比如: true_class_probs = (…,1…), raw_pred[…, 5:] = (…,1…),
L = log(1) = 0,说明预测正确,损失为0;
当预测物体分类错误时:
比如: true_class_probs = (…,1…), raw_pred[…, 5:] = (…,0…),
L = log(0) = ∞,说明预测错误,损失为∞

方法yolo_loss第358,359行
num_pos = tf.maximum(K.sum(K.cast(object_mask, tf.float32)), 1)
num_neg = tf.maximum(K.sum(K.cast((1 - object_mask) * ignore_mask, tf.float32)), 1)
用于统计正样本数量,就是真实框中包含物体的总数量,负样本数量是统计真实框中不包含物体的数量。

方法yolo_loss第363行
location_loss = location_loss * box_ratio / num_pos #位置损失box比率/正样本数
confidence_loss = K.sum(confidence_loss) * balance[l] * obj_ratio / (num_pos + num_neg) #物体置信度
平衡系数物体比率/总样本数
class_loss = K.sum(class_loss) * cls_ratio / num_pos / num_classes #分类损失
分类比率/正样本数/分类总数
计算位置损失时除以正样本数量,计算置信度损失时除以总样本数,计算物体分类损失时除以正样本数量。

方法yolo_loss第408行
if lr_decay_type == “cos”:
warmup_total_iters = min(max(warmup_iters_ratio * total_iters, 1), 3)
warmup_lr_start = max(warmup_lr_ratio * lr, 1e-6)
no_aug_iter = min(max(no_aug_iter_ratio * total_iters, 1), 15)
func = partial(yolox_warm_cos_lr, lr, min_lr, total_iters, warmup_total_iters, warmup_lr_start, no_aug_iter)
else:
decay_rate = (min_lr / lr) ** (1 / (step_num - 1))
step_size = total_iters / step_num
func = partial(step_lr, lr, decay_rate, step_size)
学习率动态调整方法,partial作用是将函数部分封装。
例程:

from functools import partial
def add(a, b):
    return a + b
add_two = partial(add, 2)
print(add_two(3))  # 输出:5
print(add_two(4))  # 输出:6


from functools import partial
def hello(name, greet="Hello"):
    return f"{greet}, {name}!"
hi_greet = partial(hello, greet="Hi")
print(hi_greet("Tom"))  # 输出:Hi, Tom!
print(hi_greet("Jack"))  # 输出:Hi, Jack!


def repeat_function(func, times):
    for _ in range(times):
        func()
from functools import partial

def log_msg(message):
    print(message)

# 使用partial定义新函数
log_function = partial(log_msg, "Python partial fuction...")
repeat_function(log_function, 2)

train.py中调用学习率方法get_lr_scheduler()
在这里插入图片描述
传入参数分别为:lr_decay_type,Init_lr_fit,Min_lr_fit,UnFreeze_Epoch,
当lr_decay_type=’cos‘时分别对应
Init_lr_fit <------> lr
Min_lr <------> min_lr
UnFreeze_Epoch <------> total_iters
当lr_decay_type=’step‘时分别对应
Init_lr_fit <------> lr

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值