基于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.