人工智能学习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 partialimport 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), xywhb2: 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=yb1_wh = b1[..., 2:4] # 取2-4两个元素,对应宽高 2=w,3=hb1_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=yb2_wh = b2[..., 2:4] # 矩形b2宽高 2=2,3=hb2_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*vciou = 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_halfb1_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_halfb2_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=boolobject_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_maskclass_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_lossif print_loss:loss = tf.Print(loss, [loss, location_loss, confidence_loss, class_loss, tf.shape(ignore_mask)],summarize=100, message='loss: ')return lossdef 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_itersif iters <= warmup_total_iters:#调整学习率lrlr = (lr - warmup_lr_start) * pow(iters / float(warmup_total_iters), 2) + warmup_lr_startelif iters >= total_iters - no_aug_iter:#如果迭代次数iters大于总次数total_iters-no_aug_iter,取最小学习率lr = min_lrelse:#其他情况,调整学习率lrlr = 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_sizeout_lr = lr * decay_rate ** nreturn out_lrif 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_numfunc = 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]中…用法
-
省略号
Ellipsis就是省略号(…),省略号(…)就是Ellipsis。而Ellipsis是ellipsis类的唯一实例(singleton object)
print(type(…)) # output: <class ‘ellipsis’>
print(Ellipsis == …) # True
print(…) # Ellipsis -
类型提示
关于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 = 1log(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 = 1log(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)) # 输出:6from 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 partialdef 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