通过上一篇文章:【深度学习系列(四)】:基于tensorflow的YOLOV3实现 (2):YOLOV3框架结构,我们基本了解了YOLOV3网络在进行前向运算的细节,这里具体说明下YOLOV3在预测过程中的流程:
- 输入一张任意大小图片,保持长宽比不变的情况下,缩放至 w 或 h 达到416,再覆盖在416*416的新图上,作为网络的输入。即网络的输入是一张416*416,3通道的RGB图。
- 运行网络。YOLO的CNN网络把图片分成 S*S 个网格(yolov3多尺度预测,输出3层,每层 S * S个网格,分别为 13*13 ,26 *26 ,52*52),然后每个单元格负责去检测那些中心点落在该格子内的目标,如图所示。每个单元格需要测 3*(4+1+B)个值。如果将输入图片划分为 S*S 网格,那么每层最终预测值为 S*S*3*(4+1+B) 大小的张量。B为类别数(coco集为80类),即B=80。3 为每层anchorbox数量,4 为边界框大小和位置(x , y , w , h )1 为置信度。
- 通过NMS,非极大值抑制,筛选出框boxes,输出框class_boxes和置信度class_box_scores,再生成类别信息classes,生成最终的检测数据框,并返回。
当我们直到网络如何进行计算时,是否还在思考如何来学习这个网络呢?这篇文章将着重介绍下如何学习YOLOV3网络,即YOLOV3的网络的优化。
1、loss function
在YOLOV3的论文里没有明确提所用的损失函数,但我们可以从YOLOV1的论文中看到相关损失函数的,如下图所示。
图片信息来源:yolov--12--YOLOv3的原理深度剖析和关键点讲解
从公式中我们可以知道,YOLO系列的损失函数主要由三部分组成:方框位置损失、置信度损失以及分类损失组成。对于位置和置信度损失采用SSE计算,而对于分类损失,在预测对象类别时不使用softmax,YOLOV3采用改成使用logistic的输出进行预测。
为什么对象分类softmax改成logistic?
早期YOLO,作者曾用softmax获取类别得分并用最大得分的标签来表示包含再边界框内的目标,在YOLOv3中,这种做法被修正。
softmax来分类依赖于这样一个前提,即分类是相互独立的,换句话说,如果一个目标属于一种类别,那么它就不能属于另一种。
但是,当我们的数据集中存在人或女人的标签时,上面所提到的前提就是去了意义。这就是作者为什么不用softmax,而用logistic regression来预测每个类别得分并使用一个阈值来对目标进行多标签预测。比阈值高的类别就是这个边界框真正的类别。
用简单一点的语言来说,其实就是对每种类别使用二分类的logistic回归,即你要么是这种类别要么就不是,然后便利所有类别,得到所有类别的得分,然后选取大于阈值的类别就好了。
logistic回归用于对anchor包围的部分进行一个目标性评分(objectness score),即这块位置是目标的可能性有多大。这一步是在predict之前进行的,可以去掉不必要anchor,可以减少计算量。
如果模板框不是最佳的即使它超过我们设定的阈值,我们还是不会对它进行predict。
不同于faster R-CNN的是,yolo_v3只会对1个prior进行操作,也就是那个最佳prior。而logistic回归就是用来从9个anchor priors中找到objectness score(目标存在可能性得分)最高的那一个。logistic回归就是用曲线对prior相对于 objectness score映射关系的线性建模。
接下来,我们将详细讨论该损失函数是怎样计算的。我们知道,在目标检测任务里,有几个关键信息是需要确定的:(x,y,w,h,confidence,classes),这也是我们前一篇文章中得到的预测结果。这里我们假设预测结果为y_pred,维数为batch* grid_h*gride_w*n_box*80,其中batch表示预测的批量大小,grid_h、gride_w表示网格的大小,n_box表示对应的锚点框个数,一般n_box=3。对应的标签为y_true,维数为batch*80。整体的loss计算如下:
代码位于yololoss.py中:
ignore_thresh=0.5
grid_scale=1
obj_scale=5
noobj_scale=1
xywh_scale=1
class_scale=1
def lossCalculator(y_true, y_pred, anchors,image_size):
'''
计算预测与标签之间的损失
:param y_true: 标签
:param y_pred: 预测结果
:param anchors: 锚点框
:param image_size: 原图像大小[416,416]
:return:
'''
#使预测与标签一一对应
y_pred=tf.reshape(y_pred,y_true.shape)
object_mask=tf.expand_dims(y_true[...,4],4) #置信度
preds=adjust_pred_tensor(y_pred) #预测结果预处理
conf_delta = conf_delta_tensor(y_true, preds, anchors, ignore_thresh)
wh_scale = wh_scale_tensor(y_true[..., 2:4], anchors, image_size)
#计算方框损失
loss_box = loss_coord_tensor(object_mask, preds[..., :4], y_true[..., :4], wh_scale, xywh_scale)
#计算置信度损失
loss_conf = loss_conf_tensor(object_mask, preds[..., 4], y_true[..., 4], obj_scale, noobj_scale, conf_delta)
#计算分类损失
loss_class = loss_class_tensor(object_mask, preds[..., 5:], y_true[..., 5:], class_scale)
loss = loss_box + loss_conf + loss_class
return loss * grid_scale
#生成带序号的网格
def _create_mesh_xy(batch_size,grid_h,grid_w,n_box):
mesh_x = tf.cast(tf.reshape(tf.tile(tf.range(grid_w), [grid_h]), (1, grid_h, grid_w, 1, 1)), tf.float32)
mesh_y = tf.transpose(mesh_x, (0, 2, 1, 3, 4))
mesh_xy = tf.tile(tf.concat([mesh_x, mesh_y], -1), [batch_size, 1, 1, n_box, 1])
return mesh_xy
#将网格信息融入坐标,置信度做sigmoid。并重新组合
def adjust_pred_tensor(y_pred):
grid_offset=_create_mesh_xy(*y_pred.shape[:4])
# 计算该尺度矩阵上的坐标sigma(t_xy) + c_xy
pred_xy=grid_offset+tf.sigmoid(y_pred[...,:2])
# 取出预测物体的尺寸t_wh
pred_wh=y_pred[...,2:4]
# 对分类概率(置信度)做sigmoid转化
pred_conf=tf.sigmoid(y_pred[...,4])
# 取出分类结果
pred_classes=y_pred[...,5:]
#重新组合
preds=tf.concat([pred_xy,pred_wh,tf.expand_dims(pred_conf,axis=-1),pred_classes],axis=-1)
return preds
#生成一个矩阵。每个格子里放有3个候选框
def _create_mesh_anchor(anchors, batch_size, grid_h, grid_w, n_box):
mesh_anchor = tf.tile(anchors, [batch_size*grid_h*grid_w])
mesh_anchor = tf.reshape(mesh_anchor, [batch_size, grid_h, grid_w, n_box, 2])#每个候选框有2个值
mesh_anchor = tf.cast(mesh_anchor, tf.float32)
return mesh_anchor
def conf_delta_tensor(y_true, y_pred, anchors, ignore_thresh):
pred_box_xy, pred_box_wh, pred_box_conf = y_pred[..., :2], y_pred[..., 2:4], y_pred[..., 4]
#带有候选框的格子矩阵
anchor_grid = _create_mesh_anchor(anchors, *y_pred.shape[:4])#y_pred.shape为(2,13,13,3,15)
true_wh = y_true[:,:,:,:,2:4]
true_wh = anchor_grid * tf.exp(true_wh)
true_wh = true_wh * tf.expand_dims(y_true[:,:,:,:,4], 4)#还原真实尺寸,高和宽
anchors_ = tf.constant(anchors, dtype='float', shape=[1,1,1,y_pred.shape[3],2])#y_pred.shape[3]为候选框个数
true_xy = y_true[..., 0:2]#获取中心点
true_wh_half = true_wh / 2.
true_mins = true_xy - true_wh_half#计算起始坐标
true_maxes = true_xy + true_wh_half#计算尾部坐标
pred_xy = pred_box_xy
pred_wh = tf.exp(pred_box_wh) * anchors_
pred_wh_half = pred_wh / 2.
pred_mins = pred_xy - pred_wh_half#计算起始坐标
pred_maxes = pred_xy + pred_wh_half#计算尾部坐标
intersect_mins = tf.maximum(pred_mins, true_mins)
intersect_maxes = tf.minimum(pred_maxes, true_maxes)
#计算重叠面积
intersect_wh = tf.maximum(intersect_maxes - intersect_mins, 0.)
intersect_areas = intersect_wh[..., 0] * intersect_wh[..., 1]
true_areas = true_wh[..., 0] * true_wh[..., 1]
pred_areas = pred_wh[..., 0] * pred_wh[..., 1]
#计算不重叠面积
union_areas = pred_areas + true_areas - intersect_areas
best_ious = tf.truediv(intersect_areas, union_areas)#计算iou
#ios小于阈值将作为负向的loss
conf_delta = pred_box_conf * tf.cast(best_ious < ignore_thresh,tf.float32)
return conf_delta
def wh_scale_tensor(true_box_wh, anchors, image_size):
image_size_ = tf.reshape(tf.cast(image_size, tf.float32), [1,1,1,1,2])
anchors_ = tf.constant(anchors, dtype='float', shape=[1,1,1,3,2])
#计算高和宽的缩放范围
wh_scale = tf.exp(true_box_wh) * anchors_ / image_size_
#物体尺寸占整个图片的面积比
wh_scale = tf.expand_dims(2 - wh_scale[..., 0] * wh_scale[..., 1], axis=4)
return wh_scale
-
loss_coord_tensor() 计算方框损失
#位置loss为box之差乘缩放比,所得的结果,再进行平方求和
def loss_coord_tensor(object_mask, pred_box, true_box, wh_scale, xywh_scale):
xy_delta=object_mask*(pred_box-true_box)*wh_scale*xywh_scale
loss_xy=tf.reduce_sum(tf.square(xy_delta),list(range(1,5)))
return loss_xy
-
loss_conf_tensor() 计算置信度损失
def loss_conf_tensor(object_mask, pred_box_conf, true_box_conf, obj_scale, noobj_scale, conf_delta):
object_mask_ = tf.squeeze(object_mask, axis=-1)
conf_delta=object_mask_*(pred_box_conf-true_box_conf) * obj_scale + (1-object_mask_) * conf_delta * noobj_scale
loss_conf=tf.reduce_sum(tf.square(conf_delta),list(range(1,4)))
return loss_conf
-
loss_class_tensor() 计算分类损失
def loss_class_tensor(object_mask, pred_box_class, true_box_class, class_scale):
true_box_class_ = tf.cast(true_box_class, tf.int64)
class_delta = object_mask *\
tf.expand_dims(tf.nn.softmax_cross_entropy_with_logits_v2(labels=true_box_class_, logits=pred_box_class), 4) * \
class_scale
loss_class = tf.reduce_sum(class_delta, list(range(1, 5)))
return loss_class
最终我们将三种尺寸矩阵的损失相见再开方得到最终的结果:
def loss_fn(list_y_trues, list_y_preds,anchors,image_size):
inputanchors = [anchors[12:], anchors[6:12], anchors[:6]]
losses = [lossCalculator(list_y_trues[i], list_y_preds[i], inputanchors[i], image_size) for i in
range(len(list_y_trues))]
return tf.sqrt(tf.reduce_sum(losses)) # 将三个矩阵的loss相加再开平方
YOLOV3在计算loss过程中的流程总结如下:
(1)遍历YOLOV3预测列表和样本标签列表;
(2)从两个列表中取出对应矩阵;
(3)将对应矩阵和对应的候选框一起传入loss函数中计算loss;
(4)将每个矩阵的loss值结果相加,再开方,得到最终的结果。
参考链接:
2、模型训练
当你读到这时,恭喜你已经对整个网络已经足够了解了。接下来,只是了解怎么训练这个网络,放轻松,这部分不需要太费脑子,只要看看怎么运行就ok了。
#coding:utf-8
import os
import tensorflow as tf
import glob
from tqdm import tqdm
import tensorflow.contrib.eager as tfe
from generator import BatchGenerator
from yolov3 import Yolonet
from yololoss import loss_fn
tf.enable_eager_execution()
PROJECT_ROOT = os.path.dirname(__file__)#获取当前目录
print(PROJECT_ROOT)
#定义coco锚点候选框
COCO_ANCHORS = [10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326]
#定义预训练模型路径
YOLOV3_WEIGHTS = os.path.join(PROJECT_ROOT, "yolov3.weights")
#定义分类
LABELS = ['0',"1", "2", "3",'4','5','6','7','8', "9"]
#定义样本路径
ann_dir = os.path.join(PROJECT_ROOT, "data", "ann", "*.xml")
img_dir = os.path.join(PROJECT_ROOT, "data", "img")
train_ann_fnames = glob.glob(ann_dir)#获取该路径下的xml文件
imgsize =416
batch_size =2
#制作数据集
generator = BatchGenerator(train_ann_fnames,img_dir,
net_size=imgsize,
anchors=COCO_ANCHORS,
batch_size=2,
labels=LABELS,
jitter = False)#随机变化尺寸。数据增强
#定义训练参数
learning_rate = 1e-4 #定义学习率
num_epoches =85 #定义迭代次数
save_dir = "./model" #定义模型路径
#循环整个数据集,进行loss值验证
def _loop_validation(model,generator):
n_steps=generator.steps_per_epoch
loss_value=0
for _ in range(n_steps):#按批次循环
xs,yolo_1,yolo_2,yolo_3=generator.next_batch()
xs=tf.convert_to_tensor(xs)
yolo_1 = tf.convert_to_tensor(yolo_1)
yolo_2 = tf.convert_to_tensor(yolo_2)
yolo_3 = tf.convert_to_tensor(yolo_3)
ys=[yolo_1,yolo_2,yolo_3]
ys_=model(xs)
loss_value+=loss_fn(ys,ys_,anchors=COCO_ANCHORS,image_size=[imgsize,imgsize])
loss_value/=generator.steps_per_epoch
return loss_value
def _loop_train(model,optimizer,generator,grad):
n_steps=generator.steps_per_epoch
for _ in tqdm(range(n_steps)):
xs, yolo_1, yolo_2, yolo_3 = generator.next_batch()
xs = tf.convert_to_tensor(xs)
yolo_1 = tf.convert_to_tensor(yolo_1)
yolo_2 = tf.convert_to_tensor(yolo_2)
yolo_3 = tf.convert_to_tensor(yolo_3)
ys = [yolo_1, yolo_2, yolo_3]
optimizer.apply_gradients(grad(model, xs, ys))
if not os.path.exists(save_dir):
os.makedirs(save_dir)
save_fname=os.path.join(save_dir,"weights")
yolo_v3=Yolonet(n_classes=len(LABELS))
yolo_v3.load_darknet_params(YOLOV3_WEIGHTS,skip_detect_layer=True)#加载预训练模型
#定义优化器
optimizer=tf.train.AdamOptimizer(learning_rate=learning_rate)
#定义损失函数loss
def _grad_fn(yolo_v3,images_tensor,list_y_trues):
logits=yolo_v3(images_tensor)
loss=loss_fn(list_y_trues,logits,anchors=COCO_ANCHORS,image_size=[imgsize,imgsize])
return loss
grad=tfe.tfe.implicit_gradients(_grad_fn)#获得计算梯度的函数
history=[]
for i in range(num_epoches):
_loop_train(yolo_v3,optimizer,generator,grad)#训练
loss_value=_loop_validation(yolo_v3,generator)
print("{}-th loss = {}".format(i, loss_value))
# 收集loss
history.append(loss_value)
if loss_value == min(history): #只有loss创新低时再保存模型
print(" update weight {}".format(loss_value))
yolo_v3.save_weights("{}.h5".format(save_fname))