本文采用了Github上用的比较广的keras-python库。代码很简洁易懂,容易实现。在此十分感谢这位大大的分享。
Github: https://github.com/qqwweee/keras-yolo3
下载:
git clone https://github.com/qqwweee/keras-yolo3
在本篇博客之前, 我曾写过一篇如何完善自己的数据集的一篇博文。里面有关于批量修改文件名和XML文件的教程
链接: https://blog.csdn.net/weixin_42835209/article/details/96574992
但此处我还要补充几点:
我们知道, YOLOv3进行训练的里面是经过一个resize的。会把训练的图片进行压缩, 压成416x416的格式。 如果你拿一些像素很大的,并且比例很不均匀的图片去训练, 如5016x3344的,那么会导致一个结果就是, 你最后训练出来的检测框很小,或者很大。因为你的anchors是基于原本图像的宽高的。即使你最后训练出来的模型识别的精度在机器看来很高, 但是并不能把测试的对象完全框起来, 甚至是框变成了很小的一个点,或者是把整个屏幕给框起来。这都是我们不想要的。
而我选择的训练尺寸是resize成416x416的, 那么我尽量会把数据制作成尽可能接近416x416大小的样本。我的样本基本在500x375和375x500左右浮动, 当然到了600多x400多和400多x600多也是可以的。主要思想是尽量把采集的数据制作成靠近416x416尺寸的图片。当然如果你用608x608就要用接近608x608的数据去训练。网上有很多裁剪的代码, 但是我是手动裁剪的。因为我检测的目标在每张图片所处的位置都不同, 所以不能做到统一, 如果你的数据很相似的话, 那么你可以用代码进行批量操作。
我们在使用LabelImg等标注工具时, 标注的目标其实很讲究的。主要有两个原则: 一、清晰 二、 准确
清晰主要就是指要去除掉像素低的和尺寸小的目标。在初期制作数据集的时候, 我自己标注手把很小很不清晰的手也框了起来。这就导致我训练的时候很难去达到一个很好的效果。
例如像在这张图片里面
很明显正中间那个人的手在图像中是又小又模糊的。那么我们尽量就不要选择这种样本。尽量选择像左下角正在拿叉子的人的手这样的尺寸大的, 清晰的样本。而究其原因还是和上面一样。我们在训练的时候我们标注的检测框是会影响我们kmeans的anchors的。 如果我们标注了这么小的样本, 放到darknet这么深的网络中,基本上就是一个点而已, 这会大大降低我们模型的精确度。
错误训练后的结果:
第二点准确。准确就是要只框你要检测的东西,不要框多余的东西。我在训练前几次的时候, 发现我训练出来的模型不只是框了人手, 甚至是把人脸也框起来了。那么我一开始是打算增加tensor的维度,提高网络学习形状和人手位置这两个特征的能力,但是后来觉得太麻烦了,一个是要改变网络的架构, 一个是要增加anchors的数量。于是我就重新去看了一下数据集, 发现我找到的图片有一些是人的手紧贴着脸和胳膊,那么我框手的时候, 顺便就把脸和胳膊也框进去了。我一下子就明白了, 原来是数据集还不够完美。于是就把数据集中带着人脸的检测框都去掉了。
例如这张图片。我们可以选择缩小这个检测框的大小, 防止神经网络把人脸特征也提取了。
我在之前的博客详细介绍了怎么对应了。我是放在服务器上训练的, 所以上传到服务器后的xml要与服务器中图片存放的路径一直。
图片名字的不能出现大写字母,中文和其他奇怪的字符。最好是小写字母下划线加六位数字。例如我的数据集就制作成images_00000x.jpg。
在源码路径中有个kmeans.py的文件是用来计算自己数据集对应的anchors的。而常规的聚类后得出的结果每次都不一样,准确度都不同。所以参考了另外一位大大的博文,通过计算anchors的均值来获取比较好的anchors数。
博客如下:https://blog.csdn.net/cgt19910923/article/details/82154401
代码如下(微调):
import glob
import xml.etree.ElementTree as ET
import numpy as np
from kmeans import YOLO_Kmeans
# from kmeans import kmeans, avg_iou
ANNOTATIONS_PATH = "annotation目录"
CLUSTERS = 9
def load_dataset(path):
dataset = []
for xml_file in glob.glob("{}/*xml".format(path)):
tree = ET.parse(xml_file)
height = int(tree.findtext("./size/height"))
width = int(tree.findtext("./size/width"))
for obj in tree.iter("object"):
xmin = int(obj.findtext("bndbox/xmin")) / width
ymin = int(obj.findtext("bndbox/ymin")) / height
xmax = int(obj.findtext("bndbox/xmax")) / width
ymax = int(obj.findtext("bndbox/ymax")) / height
xmin = np.float64(xmin)
ymin = np.float64(ymin)
xmax = np.float64(xmax)
ymax = np.float64(ymax)
if xmax == xmin or ymax == ymin:
print(xml_file)
dataset.append([xmax - xmin, ymax - ymin])
return np.array(dataset)
if __name__ == '__main__':
#print(__file__)
a = YOLO_Kmeans(CLUSTERS, ANNOTATIONS_PATH)
data = load_dataset(ANNOTATIONS_PATH)
out = a.kmeans(data, k=CLUSTERS)
# 取消注释后可以得到coco数据集的anchors在自己的数据集上的准确度。
# clusters = [[10,13],[16,30],[33,23],[30,61],[62,45],[59,119],[116,90],[156,198],[373,326]]
# out= np.array(clusters)/416.0
print(out)
# 训练时用的416x416就乘以416, 608就乘以608
print("Accuracy: {:.2f}%".format(a.avg_iou(data, out) * 100))
print("Boxes:\n {}-{}".format(out[:, 0]*416, out[:, 1]*416))
ratios = np.around(out[:, 0] / out[:, 1], decimals=2).tolist()
# Ratio是图片尺寸的宽高比
print("Ratios:\n {}".format(sorted(ratios)))
得到自己的anchors后我选择四舍五入的把anchors整理成整数记录在yolo_anchors.txt文件,然后放入model_data目录下,替换原有的yolo_anchors.txt。
这里的train主要是减少了加载预训练模型从头开始训练, 修改batch_size, 修改optimizer, 修改epochs。
我这里用的optimizer是RMSprop, 原本是用Adam, 感觉很相似, 但是我的数据用Adam的话,val_loss波动很大, 所以改成了RMSprop。如果刚开始训练, 可以通过设定为SGD,摸索出比较好的learning rate和reduce_lr后再改成Adam等优化器。
def train(model, annotation_path, input_shape, anchors, num_classes, log_dir='logs/'):
logging = TensorBoard(log_dir=log_dir)
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=1)
# 自动减少学习率, 是根据val_loss来判断的。如果val_loss在3次epoch中没有变化就把学习率
# 变成原来的1/2, 10个epoch后重新进行判断是否要reduce_lr
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=3, verbose=3, cooldown=10)
# 提早结束, 也是根据val_loss来判断。在10个epochs中, 如果val_loss没有下降就提前结束
early_stopping = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1)
val_split = 0.1
with open(annotation_path, encoding='utf-8') as f:
lines = f.readlines()
np.random.shuffle(lines)
num_val = int(len(lines)*val_split)
num_train = len(lines) - num_val
print('Train on {} samples, val on {} samples, with batch size {}.'.format(num_train, num_val, batch_size))
if True:
model.compile(optimizer=RMSprop(lr=0.001), loss={
# use custom yolo_loss Lambda layer.
'yolo_loss': lambda y_true, y_pred: y_pred})
batch_size = 15 # 修改成你可以运行的batch。batch越大显存要求越大,迭代次数要越多。
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, 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=500,
initial_epoch=0,
callbacks=[logging, checkpoint])
model.save_weights(log_dir + 'trained_weights_stage_1.h5')
# Unfreeze and continue training, to fine-tune.
# Train longer if the result is not good.
if True:
for i in range(len(model.layers)):
model.layers[i].trainable = True
model.compile(optimizer=RMSprop(lr=0.005),
loss={'yolo_loss': lambda y_true, y_pred: y_pred}) # recompile to apply the change
print('Unfreeze all of the layers.')
batch_size = 10 # note that more GPU memory is required after unfreezing the body
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, 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=1000,
initial_epoch=500,
callbacks=[logging, checkpoint, reduce_lr, early_stopping])
model.save_weights(log_dir + 'trained_weights_final.h5')
create_model()函数中有两个参数, 一个是load_pretrained, 一个是freeze_body。都是用来加载预训练好的模型的。而我是从头开始训练, 于是都缺省为False。在create_model下面有个model_loss变量, 自定义的yolo_loss损失函数。里面有个ignore_thresh参数,就是IOU阈值,对训练时框的数量有影响,调大可以防止欠拟合,调小可以防止过拟合。一般设置在(0.5~0.7)之间。由于我的数据集数量很少, 就设置为0.7。但是后来我用0.5去训练,发现结果也是不错的。
def create_model(input_shape, anchors, num_classes, load_pretrained=False, freeze_body=False,
weights_path='model_data/yolo_weights.h5'):
model_loss = Lambda(yolo_loss, output_shape=(1,), name='yolo_loss',
arguments={'anchors': anchors, 'num_classes': num_classes, 'ignore_thresh': 0.7})(
[*model_body.output, *y_true])
def DarknetConv2D(*args, **kwargs):
"""Wrapper to set Darknet parameters for Convolution2D."""
# darknet_conv_kwargs = {'kernel_regularizer': l2(5e-4)}
darknet_conv_kwargs = {}
darknet_conv_kwargs['padding'] = 'valid' if kwargs.get('strides')==(2,2) else 'same'
darknet_conv_kwargs.update(kwargs)
return Conv2D(*args, **darknet_conv_kwargs)
由于我在训练的时候损失经常没有下降, 于是我就把正则项参数给去掉了。损失也逐渐降到自己满意的范围。但是如果你的数据出现了过拟合,尽量建议你不要更改这个正则项。
训练好的文件放在train.py同级目录下的logs/000下面, 记得先创这个文件夹。最后我训练完的权重损失大概在5左右,把训练好的trained_weights_final.h5放到model_data文件夹下面, 然后修改文件名为yolo.h5。
就可以开始测试了。
from yolo import YOLO
from PIL import Image
from keras import backend as K
def detect_image():
yolo = YOLO()
images = Image.open("2.jpg")
print(type(images))
result = yolo.detect_image(images)
yolo.close_session()
result.show()
result.save("./result.jpg")
detect_image()
K.clear_session()
测试的时候如果没有检测框显示可以修改yolo.py文件下面的score和IOU这俩个值。score可以调小点。如果想去除准确率不高的框可以调大一点, 如我的模型基本上检测准确度都是0.7以上了, 所以可以调成0.5。防止框的赘余。
接下来优化的方法可以是调整optimizer,增加anchors, 减少网络,调整损失函数等。当然很多都要修改多处,所以要得结合源码解析来分析自己应该修改的方法。