Keras-Yolo训练自己的手检测器

 

Keras-yolo 源码

 本文采用了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

但此处我还要补充几点:

1. 数据集收集的尺寸要符合标准

我们知道, YOLOv3进行训练的里面是经过一个resize的。会把训练的图片进行压缩, 压成416x416的格式。 如果你拿一些像素很大的,并且比例很不均匀的图片去训练, 如5016x3344的,那么会导致一个结果就是, 你最后训练出来的检测框很小,或者很大。因为你的anchors是基于原本图像的宽高的。即使你最后训练出来的模型识别的精度在机器看来很高, 但是并不能把测试的对象完全框起来, 甚至是框变成了很小的一个点,或者是把整个屏幕给框起来。这都是我们不想要的。

而我选择的训练尺寸是resize成416x416的, 那么我尽量会把数据制作成尽可能接近416x416大小的样本。我的样本基本在500x375和375x500左右浮动, 当然到了600多x400多和400多x600多也是可以的。主要思想是尽量把采集的数据制作成靠近416x416尺寸的图片。当然如果你用608x608就要用接近608x608的数据去训练。网上有很多裁剪的代码, 但是我是手动裁剪的。因为我检测的目标在每张图片所处的位置都不同, 所以不能做到统一, 如果你的数据很相似的话, 那么你可以用代码进行批量操作。

2. 标记的样本要清晰准确

我们在使用LabelImg等标注工具时, 标注的目标其实很讲究的。主要有两个原则: 一、清晰    二、 准确

清晰主要就是指要去除掉像素低的和尺寸小的目标。在初期制作数据集的时候, 我自己标注手把很小很不清晰的手也框了起来。这就导致我训练的时候很难去达到一个很好的效果。

例如像在这张图片里面

Keras-Yolo训练自己的手检测器_第1张图片

很明显正中间那个人的手在图像中是又小又模糊的。那么我们尽量就不要选择这种样本。尽量选择像左下角正在拿叉子的人的手这样的尺寸大的, 清晰的样本。而究其原因还是和上面一样。我们在训练的时候我们标注的检测框是会影响我们kmeans的anchors的。 如果我们标注了这么小的样本, 放到darknet这么深的网络中,基本上就是一个点而已, 这会大大降低我们模型的精确度。

错误训练后的结果:

Keras-Yolo训练自己的手检测器_第2张图片

第二点准确。准确就是要只框你要检测的东西,不要框多余的东西。我在训练前几次的时候, 发现我训练出来的模型不只是框了人手, 甚至是把人脸也框起来了。那么我一开始是打算增加tensor的维度,提高网络学习形状和人手位置这两个特征的能力,但是后来觉得太麻烦了,一个是要改变网络的架构, 一个是要增加anchors的数量。于是我就重新去看了一下数据集, 发现我找到的图片有一些是人的手紧贴着脸和胳膊,那么我框手的时候, 顺便就把脸和胳膊也框进去了。我一下子就明白了, 原来是数据集还不够完美。于是就把数据集中带着人脸的检测框都去掉了。

例如这张图片。我们可以选择缩小这个检测框的大小, 防止神经网络把人脸特征也提取了。

Keras-Yolo训练自己的手检测器_第3张图片

3. xml的path和filename要和图片文件对应上

我在之前的博客详细介绍了怎么对应了。我是放在服务器上训练的, 所以上传到服务器后的xml要与服务器中图片存放的路径一直。

4. 图片的名字要符合标准

图片名字的不能出现大写字母,中文和其他奇怪的字符。最好是小写字母下划线加六位数字。例如我的数据集就制作成images_00000x.jpg。

修改YOLO参数

anchors的计算

在源码路径中有个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.py

1. 修改 train() 函数

这里的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()函数

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])

修改yolo3/model.py

修改DarknetConv2D()函数

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)

由于我在训练的时候损失经常没有下降, 于是我就把正则项参数给去掉了。损失也逐渐降到自己满意的范围。但是如果你的数据出现了过拟合,尽量建议你不要更改这个正则项。

Keras-Yolo训练自己的手检测器_第4张图片

训练好的文件放在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文件下面的scoreIOU这俩个值。score可以调小点。如果想去除准确率不高的框可以调大一点, 如我的模型基本上检测准确度都是0.7以上了, 所以可以调成0.5。防止框的赘余。

接下来优化的方法可以是调整optimizer,增加anchors, 减少网络,调整损失函数等。当然很多都要修改多处,所以要得结合源码解析来分析自己应该修改的方法。

你可能感兴趣的:(yolo,人工智能,手语识别)