【摘要】 YOLO作为一个one-stage目标检测算法,在速度和准确度上都有杰出的表现。而YOLO v3是YOLO的第3个版本(即YOLO、YOLO 9000、YOLO v3),检测效果,更准更强。

YOLO场景运用: YOLO作为一个one-stage目标检测算法,在速度和准确度上都有杰出的表现。

在ModelArts 实战营第四期中,我们学习使用了YOLO V3算法进行的物体检测训练和推理,这里对notebook代码的学习做个整理

准备数据

有很多开源的数据集可以用来进行目标检测任务的训练,如COCO数据集,PASCAL VOC数据集,BDD100K等,可以根据不同的需求和偏好进行选择。在获得数据集之后,需要对数据进行格式统一,然后便可以进行训练了。

本案例中使用的是COCO数据集,

解压后图片文件路径:data_path = "./coco/coco_data",

一、参数

1、标注信息

coco数据标注信息文件存储位置:

annotation_path = './coco/coco_train.txt'

格式如下:

图片的文件名 框的四个坐标(xmin,ymin,xmax,ymax,label_id)

923 168,131,289,270,6

400 1,64,636,477,8

... ...

 

2、coco类型

coco类型定义文件存储位置,

classes_path = './model_data/coco_classes.txt'

共有80个分类:

person
bicycle
car
motorbike
aeroplane
bus
train
truck
boat
traffic light
... ...

 

3、预测特征图(Prediction Feature Map)的anchor框(anchor box)集合

coco数据anchor值文件存储位置:

anchors_path = './model_data/yolo_anchors.txt'

  • 3个尺度(scale)的特征图,每个特征图3个anchor框,共9个框,从小到大排列;
  • 1 ~ 3是大尺度(52x52)特征图所使用的,4 ~ 6是中尺度(26x26),7 ~ 9是小尺度(13x13)
  • 大尺度特征图检测小物体,小尺度检测大物体;
  • 9个anchor来源于边界框(Bounding Box)的k-means聚类。

COCO的anchors,如下:

[[ 10.,  13.],       [ 16.,  30.],       [ 33.,  23.],       [ 30.,  61.],       [ 62.,  45.],       [ 59., 119.],       [116.,  90.],       [156., 198.],       [373., 326.]]

4、预训练模型

用于迁移学习(Transfer Learning)中的微调(Fine Tune),支持使用已训练完成的COCO模型参数,即:

预训练权重文件存储位置:

weights_path = "./model_data/yolo.h5"
# 模型文件存储位置
save_path = "./result/models/"

 

5、图片输入尺寸

默认为416x416。图片尺寸满足32的倍数,在DarkNet网络中,含有5次步长为2的降采样卷积(32=2^5),其中,卷积操作如下:

x = DarknetConv2D_BN_Leaky(num_filters, (3, 3), strides=(2, 2))(x)

在最底层时,特征图尺寸需要满足为奇数,如13,以保证中心点落在唯一框中。当为偶数时,则导致中心点落在中心的4个框中。

二、创建模型

创建YOLOv3的网络模型,输入:

  • input_shape:图片尺寸
  • anchors:9个anchor box;
  • num_classes:类别数;
  • weights_path:预训练模型的权重

实现如下:

 

# 初始化session
K.clear_session()
input_shape = (416,416)
image_input = Input(shape=(None, None, 3))
h, w = input_shape
# 对模型预测结果形状进行定义
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)]
# 构建YOLO模型结构
model_body = yolo_body(image_input, num_anchors//3, num_classes)
# 将YOLO权重文件加载进来,如果希望不加载预训练权重,从头开始训练的话,可以将这条语句删掉
model_body.load_weights(weights_path, by_name=True, skip_mismatch=True)
# 定义YOLO损失函数
model_loss = Lambda(yolo_loss,
                   output_shape=(1,), name='yolo_loss',
                   arguments={'anchors': anchors, 'num_classes': num_classes, 'ignore_thresh': 0.5})(
   [*model_body.output, *y_true])
# 构建Model,为训练做准备
model = Model([model_body.input, *y_true], model_loss)

 

三、划分数据集

val_split = 0.1
with open(annotation_path) as f:
lines = f.readlines()
np.random.seed(10101)
np.random.shuffle(lines)
np.random.seed(None)
num_val = int(len(lines)*val_split)
num_train = len(lines) - num_val

样本洗牌(shuffle),将数据集拆分为10份,训练9份,验证1份。

四、第1阶段训练过程

第1阶段,冻结部分网络,训练底层参数,优化器使用Adam;

损失函数,直接使用模型的输出y_pred,忽略真值y_true;

代码如下:

model.compile(optimizer=Adam(lr=1e-4), loss=
# 使用定制的 yolo_loss Lambda层
{'yolo_loss': lambda y_true, y_pred: y_pred})  # 损失函数

对于损失函数yolo_loss,以及y_true和y_pred:

把y_true当成一个输入,构成多输入模型,把loss写成层(Lambda层),作为最后的输出。这样,构建模型的时候,就只需要将模型的输出(output)定义为loss即可。而编译(compile)的时候,直接将loss设置为y_pred,因为模型的输出就是loss,即y_pred就是loss,因而无视y_true。训练的时候,随便添加一个符合形状的y_true数组即可。

模型训练,使用数据生成包装器(data_generator_wrapper),按批次生成训练和验证数据。最终,模型model存储权重。实现如下:

# 开始训练
batch_size = 16
print('Train on {} samples, val on {} samples, with batch size {}.'.format(num_train, num_val, batch_size))
model.fit_generator(data_generator_wrapper(lines[:num_train], batch_size, input_shape, data_path,anchors, num_classes),
steps_per_epoch=max(1, num_train//batch_size),
validation_data=data_generator_wrapper(lines[num_train:], batch_size, input_shape, data_path,anchors, num_classes),
validation_steps=max(1, num_val//batch_size),
epochs=5,
initial_epoch=0,
callbacks=[reduce_lr, early_stopping])
保存训练数据
import os
os.makedirs(save_path)
# 存储第1阶段的参数,再训练过程中,也通过回调存储
model.save_weights(log_dir + 'trained_weights_stage_1.h5')
在训练过程中,也会存储模型的参数,只存储权重(save_weights_only),只存储最优结果(save_best_only),每隔3个epoch存储一次(period),即:
checkpoint = ModelCheckpoint(log_dir + 'ep{epoch:03d}-loss{loss:.3f}-val_loss{val_loss:.3f}.h5',
monitor='val_loss', save_weights_only=True,
save_best_only=True, period=3)  # 只存储weights权重

五、第2阶段训练

第2阶段,使用第1阶段已训练的网络权重,继续训练:

将全部的参数都设置为可训练,而第1阶段则是冻结(freeze)部分参数;

优化器,仍是Adam,只是学习率(lr)有所下降,从1e-3减少至1e-4,细腻地学习最优参数;

损失函数,仍是只使用y_pred,忽略y_true。

for i in range(len(model.layers)):
model.layers[i].trainable = True
model.compile(optimizer=Adam(lr=1e-4), loss={'yolo_loss': lambda y_true, y_pred: y_pred})

第2阶段的模型fit数据,与第1阶段类似,从第5个epoch开始,一直训练到第10个epoch,触发条件则提前终止。

额外增加了两个回调reduce_lr和early_stopping

# 定义callbacks方法
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, verbose=1)
early_stopping = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1)

reduce_lr:当评价指标不在提升时,减少学习率,每次减少10%(factor),当学习率3次未减少(patience)时,终止训练。

early_stopping:验证集准确率,连续增加小于0(min_delta)时,持续10个epoch(patience),则终止训练。

batch_size = 16
print('Train on {} samples, val on {} samples, with batch size {}.'.format(num_train, num_val, batch_size))
model.fit_generator(data_generator_wrapper(lines[:num_train], batch_size, input_shape, data_path,anchors, num_classes),
steps_per_epoch=max(1, num_train//batch_size),
validation_data=data_generator_wrapper(lines[num_train:], batch_size, input_shape, data_path,anchors, num_classes),
validation_steps=max(1, num_val//batch_size),
epochs=10,
initial_epoch=5,
callbacks=[reduce_lr, early_stopping])

至此,在第2阶段训练完成之后,输出的网络参数,就是最终的模型参数。

作者:hellfire