阅读 3550

探索 YOLO v3 源码 - 第1篇 训练

YOLO,即You Only Look Once(你只看一次)的缩写,是一个基于卷积神经网络(CNN)的物体检测算法。而YOLO v3是YOLO的第3个版本(即YOLOYOLO 9000YOLO v3),检测效果,更准更强。

更多细节,可以参考YOLO的官网

官网

YOLO是一句美国的俗语,You Only Live Once,人生苦短,及时行乐。

本文介绍YOLO v3算法的实现细节,Keras框架。这是第1篇,训练。当然还有第2篇,至第n篇,毕竟,这是一个完整版 :)

本文的GitHub源码github.com/SpikeKing/k…

已更新:

欢迎关注,微信公众号 深度算法 (ID: DeepAlgorithm) ,了解更多深度技术!!


1. 参数

模型的训练参数,5个参数:

(1) 已标注框的图片数据集,格式如下:

图片的位置 框的4个坐标和1个类别ID(xmin,ymin,xmax,ymax,label_id) ...
dataset/image.jpg 788,351,832,426,0 805,208,855,270,0
复制代码

(2) 标注框类别的汇总,即数据集所标注物体的全部类别列表,如下:

aeroplane
bicycle
bird
...
复制代码

(3) 预训练模型,用于迁移学习(Transfer Learning)中的微调(Fine Tune),可选YOLO v3已训练完成的COCO模型权重,即:

pretrained_path = 'model_data/yolo_weights.h5'
复制代码

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

  • 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
复制代码

(5) 图片输入尺寸,默认为416x416。

  • 图片尺寸满足32的倍数,在DarkNet网络中,含有5次步长为2的降采样卷积(32=2^5)。降采样卷积的实现如下:
x = DarknetConv2D_BN_Leaky(num_filters, (3, 3), strides=(2, 2))(x)
复制代码
  • 在最底层时,特征图尺寸需要满足为奇数,如13,以保证中心点落在唯一框中。如果为偶数时,则中心点落在中心的4个框中,导致歧义。

2. 创建模型

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

  • input_shape:图片尺寸;
  • anchors:9个anchor box;
  • num_classes:类别数;
  • freeze_body:冻结模式,1是冻结DarkNet53的层,2是冻结全部只保留最后3层;
  • weights_path:预训练模型的权重。

实现:

model = create_model(input_shape, anchors, num_classes,
                     freeze_body=2,
                     weights_path=pretrained_path)
复制代码

其中,网络的最后3层:

3个1x1的卷积层(代替全连接层),用于将3个尺度的特征图,转换为3个尺度的预测值。

实现:

out_filters = num_anchors * (num_classes + 5)
// ...
DarknetConv2D(out_filters, (1, 1))
复制代码

即:

conv2d_59 (Conv2D)      (None, 13, 13, 18)   18450       leaky_re_lu_58[0][0]    
conv2d_67 (Conv2D)      (None, 26, 26, 18)   9234        leaky_re_lu_65[0][0]    
conv2d_75 (Conv2D)      (None, 52, 52, 18)   4626        leaky_re_lu_72[0][0]    
复制代码

3. 样本数量

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

实现:

val_split = 0.1  # 训练和验证的比例
with open(annotation_path) as f:
    lines = f.readlines()
np.random.seed(47)
np.random.shuffle(lines)
np.random.seed(None)
num_val = int(len(lines) * val_split)  # 验证集数量
num_train = len(lines) - num_val  # 训练集数量
复制代码

4. 第1阶段训练

第1阶段,冻结部分网络,只训练底层权重。

  • 优化器使用常见的Adam;
  • 损失函数,直接使用,模型的输出y_pred,忽略真值y_true

实现:

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

其中,损失函数yolo_loss,以及y_truey_pred

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

关于Python的Lambda表达式:

f = lambda y_true, y_pred: y_pred
print(f(1, 2))  # 输出2
复制代码

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

batch_size = 32  # batch
model.fit_generator(data_generator_wrapper(lines[:num_train], batch_size, input_shape, anchors, num_classes),
                    steps_per_epoch=max(1, num_train // batch_size),
                    validation_data=data_generator_wrapper(
                        lines[num_train:], batch_size, input_shape, anchors, num_classes),
                    validation_steps=max(1, num_val // batch_size),
                    epochs=50,
                    initial_epoch=0,
                    callbacks=[logging, checkpoint])
# 存储最终的去权重,再训练过程中,也通过回调存储
model.save_weights(log_dir + 'trained_weights_stage_1.h5')  
复制代码

在训练过程中,也会存储epoch完成的模型权重,其中,只存储权重(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权重
复制代码

5. 第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阶段类似,从第50个epoch开始,一直训练到第100个epoch,触发条件,则提前终止。额外增加了两个回调reduce_lrearly_stopping,用于控制训练提取终止:

  • reduce_lr:当评价指标不在提升时,减少学习率,每次减少10%(factor),当验证损失3次未减少(patience)时,则终止训练。
  • early_stopping:验证集准确率,连续增加小于0(min_delta)时,持续10个epoch(patience),则终止训练。

实现:

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)  # 验证集准确率,下降前终止

batch_size = 32
model.fit_generator(data_generator_wrapper(lines[:num_train], batch_size, input_shape, anchors, num_classes),
                    steps_per_epoch=max(1, num_train // batch_size),
                    validation_data=data_generator_wrapper(lines[num_train:], batch_size, input_shape, anchors,
                                                           num_classes),
                    validation_steps=max(1, num_val // batch_size),
                    epochs=100,
                    initial_epoch=50,
                    callbacks=[logging, checkpoint, reduce_lr, early_stopping])
model.save_weights(log_dir + 'trained_weights_final.h5')
复制代码

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


补1. K-Means

K-Means算法是聚类算法,将一组数据划分为多个组(group),每个组都含有一个中心。

YOLOv3,获取数据集中全部的anchor box,通过K-Means算法,将这些框聚类为9类,获取9个聚类中心,面积从小到大排列,作为9个anchor box。

模拟K-Means算法:

  1. 创建测试点,X是数据,y是标签,如X:(300,2), y:(300,);
  2. 将数据聚类为9类;
  3. 输入数据X,训练;
  4. 预测X的类别,为y_kmeans
  5. 使用scatter绘制散点图,颜色范围是viridis
  6. 获取聚类中心cluster_centers_,以黑色(black)点表示;

源码:

import matplotlib.pyplot as plt
import seaborn as sns
sns.set()  # for plot styling
from sklearn.cluster import KMeans
from sklearn.datasets.samples_generator import make_blobs


def test_of_k_means():
    # 创建测试点,X是数据,y是标签,X:(300,2), y:(300,)
    X, y_true = make_blobs(n_samples=300, centers=9, cluster_std=0.60, random_state=0)
    kmeans = KMeans(n_clusters=9)  # 将数据聚类
    kmeans.fit(X)  # 数据X
    y_kmeans = kmeans.predict(X)  # 预测

    # 颜色范围viridis: https://matplotlib.org/examples/color/colormaps_reference.html
    plt.scatter(X[:, 0], X[:, 1], c=y_kmeans, s=20, cmap='viridis')  # c是颜色,s是大小

    centers = kmeans.cluster_centers_  # 聚类的中心
    plt.scatter(centers[:, 0], centers[:, 1], c='black', s=40, alpha=0.5)  # 中心点为黑色

    plt.show()  # 展示


if __name__ == '__main__':
    test_of_k_means()
复制代码

输出:

K-Means


补2. EarlyStopping

EarlyStopping是Callback(回调类)的子类,Callback用于指定在每个阶段开始和结束的时候执行的操作。在Callback中,有一些已经实现的简单子类,如accval_acclossval_loss等,还有一些复杂子类,如ModelCheckpoint(用于存储模型权重)和TensorBoard(用于画图)等。

Callback的回调接口,如下:

def on_epoch_begin(self, epoch, logs=None):
def on_epoch_end(self, epoch, logs=None):
def on_batch_begin(self, batch, logs=None):
def on_batch_end(self, batch, logs=None):
def on_train_begin(self, logs=None):
def on_train_end(self, logs=None):
复制代码

EarlyStopping是用于提前停止训练的Callback子类。具体地,当训练或验证集中的loss不再减小,即减小的程度小于某个阈值时,则会停止训练。这样做,可以提高调参效率,避免浪费资源。

在model的fit数据中,以列表形式设置callbacks回调,支持设置多个Callback,如:

callbacks=[logging, checkpoint, reduce_lr, early_stopping]
复制代码

EarlyStopping的参数:

  • monitor:监控数据的类型,支持acc、val_acc、loss、val_loss等;
  • min_delta:停止的阈值,与mode参数配合,增加或下降最少的阈值;
  • mode:min是最少,max是最多,auto是自动,与min_delta配合;
  • patience:达到阈值之后,能够容忍的epoch数,避免停止在抖动中;
  • verbose:日志的繁杂程度,值越大,输出的信息越多。

min_delta和patience需要相互配合,避免模型停止在抖动过程中,在设置的时候,需要相互协调。min_delta降低,patience减少;min_delta增加,则patience增加。

实例:

early_stopping = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1)
复制代码

OK, that's all! Enjoy it!

欢迎关注,微信公众号 深度算法 (ID: DeepAlgorithm) ,了解更多深度技术!

关注下面的标签,发现更多相似文章
评论