基于Tensorflow2的YOLOV4 网络结构及代码解析(4)——Loss和input

基于Tensorflow2的YOLOV4 网络结构及代码解析(4)——Loss和input

本部分介绍yolov4源码中Train中内容,包括Input,Loss和训练方式等。

在训练的过程中用到了一些tricks,包括mosaic数据增强,Cosine_scheduler 余弦退火学习率,CIOU以及label_smoothing 表情平滑等。在这篇博客中不做详解,将在下篇博客中进行详细剖析。本篇博客重点放在训练流程以及Input和Loss算法。

在训练之前,首先将将模型结构和参数载入(之前几篇博客已经详细介绍)

   model_body = yolo_body(image_input, num_anchors//3, num_classes)
   model_body.load_weights(weights_path, by_name=True, skip_mismatch=True)

Loss:

yolov4损失函数公式见:https://blog.csdn.net/bblingbbling/article/details/106910026

从train的主函数进入Loss函数,具体如下:

  y_true = [Input(shape=(h//{0:32, 1:16, 2:8}[l], w//{0:32, 1:16, 2:8}[l], num_anchors//3, num_classes+5)) for l in range(3)]
    loss_input = [*model_body.output, *y_true]
    model_loss = Lambda(yolo_loss, output_shape=(1,), name='yolo_loss',arguments={'anchors': anchors, 'num_classes': num_classes, 'ignore_thresh': 0.5,'label_smoothing': label_smoothing})(loss_input)

我们开始逐一进行分析:

y_true是我们已知的标签,这段代码乍看有点费解,我们来逐一分析:

语法1:“for l in range(3)”表示遍历“l”的值为“0,1,2”。:h//{0:32, 1:16, 2:8}[l],与标准的字典用法有些区别。该用法等同于tmp={0:32, 1:16, 2:8},h//tmp[l],如下所示

y=[416//{0:32, 1:16, 2:8}[l] for l in range(3)]
print(y)
#结果:
[13, 26, 52]

语法2:keras.layers.Input返回一个象征性的Keras tensor,可配置各种参数用于输入Model使用。

loss_input将y_true和output组合起来。“*”号在列表中表示方法如下所示:

a=[1,2,3]
b=[4,5,6]
c=[a,b]
d=[*a,*b]
print("c=",c)
print("d=",d)
#结果:
c= [[1, 2, 3], [4, 5, 6]]
d= [1, 2, 3, 4, 5, 6]

语法3:Lambda函数。之前博文中已经有过介绍,等同于创造一个“Layer",是一种简便构造layer的方法。在这里,将arguments中的对应参数传入model_loss中。

model = Model([model_body.input, *y_true], model_loss)

利用已经搭建的model_loss层组成Model。因为在训练时需要用到compile()函数和fit()函数,故必须在这里组成一个模型。既:inputs=[model_body.input, *y_true],outputs=model_loss函数(层)

最重要的的yolo_loss来了

def yolo_loss(args, anchors, num_classes, ignore_thresh=.5, label_smoothing=0.1, print_loss=False, normalize=True):

笔者在这里遇到个传参的疑惑,yolo_loss函数如何接收到“loss_input”。作者的理解是Lambda函数通过内部的_call_函数将loss_input传递给yolo_loss函数,其余arguments通过字典传入函数,如下示例:

def fun(arg):
    print(arg)

tf.keras.layers.Lambda(fun)(10)
#结果:tf.Tensor(10, shape=(), dtype=int32)
input_shape = K.cast(K.shape(yolo_outputs[0])[1:3] * 32, K.dtype(y_true[0]))

input_shape为416,416。yolo_outputs[0]对应的维度是[none,13,13,255],也就是最后一个特征图输出的特征。并转换为tf.Tensor类型。

  m = K.shape(yolo_outputs[0])[0]
  mf = K.cast(m, K.dtype(yolo_outputs[0]))

取出batch,并将int32转换为float32。

for l in range(num_layers):

开始遍历所有outputs,共计3层(本次博客仅用一层举例说明):

object_mask = y_true[l][..., 4:5]
true_class_probs = y_true[l][..., 5:]

y_true原始维度为(m,13,13,3,85),object_mask表示是否存在目标,true_class_probs表示各个种类获取的得分。

 grid, raw_pred, pred_xy, pred_wh = yolo_head(yolo_outputs[l],anchors[anchor_mask[l]], num_classes, input_shape, calc_loss=True)

利用yolo_head解码yolo_outputs。其中calc_loss参数的不同,获取的返回值不同。在Predict时直接返回预测结果,其他部分一样,详见上篇博客。

pred_box = K.concatenate([pred_xy, pred_wh])

将解码后的box_xy,pre_wh进行拼接,获得box位置信息。

        ignore_mask = tf.TensorArray(K.dtype(y_true[0]), size=1, dynamic_size=True)
        object_mask_bool = K.cast(object_mask, 'bool')

找到负样本群组,其中注意TensorArray语法:

1.TensorArray可以看做是具有动态size功能的Tensor数组,可动态的增加数据,在之后迭代时用于存放负样本。

2.将object_mask转为bool类型,用于之后判断负样本。

完成基本数据整理和获取后,进入loop_body循环

def loop_body(b, ignore_mask):

 

 true_box = tf.boolean_mask(y_true[l][b,...,0:4], object_mask_bool[b,...,0])

函数tf.boolean_mask的用法获取y_true获取真实位置框(n,4)。boolean_mask的用法是返回object_mask_bool中为True时,y_true的值,即真实box的位置信息。具体用法见下示例:

tensor = [0, 1, 2, 3]  # 1-D example
mask = np.array([True, False, True, False])
tf.boolean_mask(tensor, mask)
结果为:tf.Tensor([2 6], shape=(2,), dtype=int32)

得到真实值和预测值后,计算他们直接的IOU.也就是获取他们的重合度。其中IOU维度为(13,13,3,n)

 ignore_mask = ignore_mask.write(b, K.cast(best_iou<ignore_thresh, K.dtype(true_box)))

最后将小于ignore_thresh的目标作为负样本,增加近ignore_mask中。这里需要注意的是:忽略掉iou大于阈值的框,因为这些框以及比较准确,不适合做负样本。

完成后需要对批次中每张图进行相同操作,获取负样本。最后得到负样本维度为(m,13,13,3,1)

        box_loss_scale = 2 - y_true[l][...,2:3]*y_true[l][...,3:4]
        raw_true_box = y_true[l][...,0:4]
        ciou = box_ciou(pred_box, raw_true_box)
        ciou_loss = object_mask * box_loss_scale * (1 - ciou)

上述代码用于计算位置信息的损失值,box_loss_scale表示真实框越大,比重越小。

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)

location_loss = K.sum(tf.where(tf.math.is_nan(ciou_loss), tf.zeros_like(ciou_loss), ciou_loss))
confidence_loss = K.sum(tf.where(tf.math.is_nan(confidence_loss), tf.zeros_like(confidence_loss), confidence_loss))
class_loss = K.sum(tf.where(tf.math.is_nan(class_loss), tf.zeros_like(class_loss), class_loss))
        #-----------------------------------------------------------#
        #   计算正样本数量
        #-----------------------------------------------------------#
num_pos += tf.maximum(K.sum(K.cast(object_mask, tf.float32)), 1)
loss += location_loss + confidence_loss + class_loss

上面这段代码就是根据YOLOV4损失函数的公式编写,个人觉得没什么特别的。值得注意的是:

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

Input:

相对于LOSS部分,Input部分讲的东西要少很多,因为tensorflow已经帮我们封装了很多了。

model.fit(data_generator(lines[:num_train], batch_size, input_shape, anchors, num_classes, mosaic=mosaic, random=True),
steps_per_epoch=max(1, num_train//batch_size),
validation_data=data_generator(lines[num_train:], batch_size, input_shape, anchors, num_classes, mosaic=False, random=False),
validation_steps=max(1, num_val//batch_size),
epochs=Freeze_epoch,
initial_epoch=Init_epoch,
callbacks=[logging, checkpoint, reduce_lr, early_stopping])

 model.fit()是我们用TF进行训练时使用的函数,具体参数

fit(
    x=None, y=None, batch_size=None, epochs=1, verbose=1, callbacks=None,
    validation_split=0.0, validation_data=None, shuffle=True, class_weight=None,
    sample_weight=None, initial_epoch=0, steps_per_epoch=None,
    validation_steps=None, validation_batch_size=None, validation_freq=1,
    max_queue_size=10, workers=1, use_multiprocessing=False
)

个人觉得,参数名基本可以表达参数的意思了,故不作过多解释。唯一想说的就是参数“x"。x表示用于训练的数据,可以接受的形式为:

1.numpy array。

2.Tensorflow tensor。

3.字典

4.tf.data数据结构

5.generator 或者keras.utills.Sequence。 (本例使用)

接下来详细讲解

def data_generator(annotation_lines, batch_size, input_shape, anchors, num_classes, mosaic=False, random=True)

这是一个函数生成器,代码中唯一值得注意的yied用法:

yied用于生成器中,类似于“return"的用法。重点注意的时:yield就是 return 返回一个值,并且记住这个返回的位置,下次迭代就从这个位置后开始。示例如下:

def test(n):
    for i in range(n):
        yield mul(i)
        print("i=",i)

    print("do else something.")

def mul(i):
    return i**2  

#使用for循环
for i in test(3):
    print(i,",")
#结果:
0 ,
i= 0
1 ,
i= 1
4 ,
i= 2
do else something.

 

 

上一篇:基于tensorflow2.x版本python代码实现深度学习回归预测(以lstm为例)


下一篇:Tensorflow2+keras实现BiLSTM+CRF中文命名实体识别