hans

hans

【Darknet】【yolo v2】训练自己数据集的一些心得----VOC格式


-------【2017.11.2 更新】------------SSD 传送门 ----------

http://blog.csdn.net/renhanchi/article/details/78411095

------- 【2017.10.30 更新】------------ 一些要说的 -----------

虽然写完这篇博客后我几乎再没使用 darknet 和 yolo,但这半年多我一直在不断更新这篇博客,也在不断为大家解决问题。

如果你想通过这篇博客学习使用 darknet 和 yolo,包括一些 detection 的知识,我希望你能仔细、认真的读本文的每一句话。


最近发现作者代码改动很大,导致很多地方跟我写的博客内容都不一样了。我现在把我当初下载的 darknet 打包给大家。

** 新老版本的区别在于代码,算法架构还是一样的,所以放心使用老版本。
**

解压缩后进入目录,配置好 Makefile 后直接 make all 就可以了。

https://pan.baidu.com/s/1jIR2oTo

1. 前言

关于用 yolo 训练自己 VOC 格式数据的博文真的不少,但是当我按照他们的方法一步一步走下去的时候发现出了其他作者没有提及的问题。这里就我自己的经验讲讲如何训练自己的数据集。

2. 数据集

这里建议大家用 VOC 和 ILSVRC 比赛的数据集,因为 xml 文件都是现成的,省去很多功夫。

想自己标记的可以自己去 github 搜索 labelImg , 下载好 make 后直接运行就可以。具体使用方法先不做赘述。

也可以直接去下载现成的数据集

ILSVRC2015 比赛的地址是: http://image-net.org/challenges/LSVRC/2015/download-
images-3j16.php

VOC 比赛地址是: http://host.robots.ox.ac.uk/pascal/VOC/index.html

我的数据集是把 VOC2007,VOC2012,ILSVRC2013 和 ILSVRC2014 所有关于人的数据集单独拿了出来,我只想做单独检测人的训练。注意 ILSVRC 后缀名是 JPEG 的,可以自己改成 jpg,也可以不改,因为 darknet 代码里也兼容 JPEG 格式。但为了以后省事儿,我是都给改成 jpg 后缀名了。

关于怎么把 VOC 所有关于人的数据集单独拿出来,大家可以用下面这个 shell 脚本,稍微改一改就也能用来提取 ILSVRC 的数据。ILSVRC 数据集里面人的类别不是 person,是 n00007846,这个在 xml 文件里不影响后面的训练,所以大家也不需要特意把 n00007846 都改成 person。原因是 labels.txt 文件里面是用数字 0,1,2,3 等等表示类别的,而不是单词。这些数字是对应 data/names.list
里面类别的索引。

#!/bin/sh

year="VOC2012"

mkdir /your_path/VOCperson/${year}_Anno/  #创建文件夹
mkdir /your_path/VOCperson/${year}_Image/

cd /your_path/VOCdevkit/$year/Annotations/
grep -H -R "<name>person</name>" > /your_path/VOCperson/temp.txt  #找到有关键字的行,并把这些行存到临时文档

cd /your_path/VOCperson/
cat temp.txt | sort | uniq > $year.txt     #根据名字排序,并把相邻的内容完全一样的多余行删除。
find -name $year.txt | xargs perl -pi -e 's|.xml:\t\t<name>person</name>||g'   #把文档中后缀名和其他无用信息删掉,只保留没后缀名的文件名

cat $year.txt | xargs -i cp /your_path/VOCdevkit/$year/Annotations/{}.xml /your_path/VOCperson/${year}_Anno/ #根据文件名复制注释文件
cat $year.txt | xargs -i cp /your_path/VOCdevkit/$year/JPEGImages/{}.jpg /your_path/VOCperson/${year}_Image/ #根据文件名复制数据集

rm temp.txt

3. 训练文件

3.1 文件夹设置

Annotations ---- 这个文件夹是放所有 xml 描述文件的。

JPEGImages ---- 这个文件夹是放所有 jpg 图片文件的。

ImageSets -> Main ----
这个文件里放一个 names.txt 文档(我的这个文档的名字是:train.txt,注意这个名字要跟下面 python 代码中列表 sets 的第二个元素内容一致),文档内容是所有训练集图片的名字,没有后缀名。

PS: 官网自带的训练 VOC 方法里面图片是分别放在 2007 和 2012 两个不同路径的,我觉得麻烦,就一股脑把所有文件都放在一个文件里面了。

3.2 txt 文档

一共需要准备三种 txt 文档:

先是上面提到的在 ImageSets 文件下所有训练数据名的 names.txt 文档。

然后是所有图片一一对应的 labels.txt。这些文档是通过 scripts/voc_label.py
这个文件生成的,里面路径是要改一改的。我把所有图片文件和 xml 文件都分别放到一个文件夹下了,而且训练类别只有 person。所以开头的 sets 和 classes,也是需要改的。这里值得注意的是如果你是用的 ILSVRC 数据集,并且你也和我一样懒懒的没有把 xml 文件里面的 n00007846 改成人的话,那么你需要把 classes 改成 n00007846 这样才能找到关于这一类别的 bbox 信息。还有一点需要注意,就是 ILSVRC 数据集的 xml 文件里面没有 difficult 这个信息,所以.py 文件里关于这一点的东西注释掉就好了。

最后是保存有所有训练图片绝对路径的 paths.txt 文档。注意这个文档里面图片文件名是带 jpg 后缀名的。生成上面 labels.txt 文档的时候最后会自动生成 paths.txt。

下面是我的.py 文件

import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join

sets=[('person','train')]
classes = ["n00007846"]

def convert(size, box):
    dw = 1./(size[0])
    dh = 1./(size[1])
    x = (box[0] + box[1])/2.0 - 1
    y = (box[2] + box[3])/2.0 - 1
    w = box[1] - box[0]
    h = box[3] - box[2]
    x = x*dw
    w = w*dw
    y = y*dh
    h = h*dh
    return (x,y,w,h)

def convert_annotation(year, image_id):
    in_file = open('/home/hans/darknet/person/VOC%s/Annotations/%s.xml'%(year, image_id))
    out_file = open('/home/hans/darknet/person/VOC%s/labels/%s.txt'%(year, image_id), 'w')
    tree=ET.parse(in_file)
    root = tree.getroot()
    size = root.find('size')
    w = int(size.find('width').text)
    h = int(size.find('height').text)

    for obj in root.iter('object'):
        # difficult = obj.find('difficult').text
        cls = obj.find('name').text
        if cls not in classes: # or int(difficult)==1:
            continue
        cls_id = classes.index(cls)
        xmlbox = obj.find('bndbox')
        b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
float(xmlbox.find('ymax').text))
        bb = convert((w,h), b)
        out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')

wd = getcwd()
for year, image_set in sets:
    if not os.path.exists('/home/hans/darknet/person/VOC%s/labels/'%(year)):
        os.makedirs('/home/hans/darknet/person/VOC%s/labels/'%(year))
    image_ids = open('/home/hans/darknet/person/VOC%s/ImageSets/Main/%s.txt'%(year, image_set)).read().strip().split()
    list_file = open('%s_%s.txt'%(year, image_set), 'w')
    for image_id in image_ids:
        list_file.write('%s/VOC%s/JPEGImages/%s.jpg\n'%(wd, year, image_id))
        convert_annotation(year, image_id)
    list_file.close()

3.3 训练配置文件

先是在 data 文件夹下创建一个 person.names 文件,内容只有 person。

然后是修改 cfg 文件夹下 voc.data 文件,classes 改成 1。 train 对应的路径是上文的 paths.txt。
names 对应路径是 person.names 文件的。backup 对应路径是备份训练权重文件的。

最后是选取.cfg 网络,在 darknet 官网还有很多博文里面都是用的 yolo-
voc.cfg,我用这个网络训练一直失败。体现在训练好久后 test,没有 bbox 和 predict 结果。出现这种问题有两种情况,一是训练发散了。二是训练不充分,predict 结果置信概率太低。对于训练发散,其实很容易发现的,就是在训练的时候观察迭代次数后边的两个 loss 值,如果这两个值不断变大到几百就说明训练发散啦。可以通过降低 learning
rate 和 提高 batch 数量解决这个问题。关于参数修改和训练输出分别表示什么意思我会在后文提及。
对于训练不充分的时候如何显示出 predict 结果,可以在 test 的时候设置 threshold,darknet 默认是.25,你可以尝试逐渐降低这个值看效果。

./darknet detector test cfg/voc.data cfg/yolo_voc.cfg -thresh 0.25

当时对 darknet 理解还不够深,出现上面问题当时我解决不了的时候,我换成了现在用的 yolo-voc.2.0.cfg 这个网络。关于里面的参数我简单说说:

batch:
每一次迭代送到网络的图片数量,也叫批数量。增大这个可以让网络在较少的迭代次数内完成一个 epoch。在固定最大迭代次数的前提下,增加 batch 会延长训练时间,但会更好的寻找到梯度下降的方向。如果你显存够大,可以适当增大这个值来提高内存利用率。这个值是需要大家不断尝试选取的,过小的话会让训练不够收敛,过大会陷入局部最优。

subdivision:
这个参数很有意思的,它会让你的每一个 batch 不是一下子都丢到网络里。而是分成 subdivision 对应数字的份数,一份一份的跑完后,在一起打包算作完成一次 iteration。这样会降低对显存的占用情况。如果设置这个参数为 1 的话就是一次性把所有 batch 的图片都丢到网络里,如果为 2 的话就是一次丢一半。

angle: 图片旋转角度,这个用来增强训练效果的。从本质上来说,就是通过旋转图片来变相的增加训练样本集。

saturation,exposure,hue: 饱和度,曝光度,色调,这些都是为了增强训练效果用的。

learning_rate: 学习率,训练发散的话可以降低学习率。学习遇到瓶颈,loss 不变的话也减低学习率。

max_batches: 最大迭代次数。

policy: 学习策略,一般都是 step 这种步进式。

step,scales: 这两个是组合一起的,举个例子:learn_rate: 0.001, step:100,25000,35000
scales: 10, .1, .1 这组数据的意思就是在 0-100 次 iteration 期间 learning
rate 为原始 0.001,在 100-25000 次 iteration 期间 learning
rate 为原始的 10 倍 0.01,在 25000-35000 次 iteration 期间 learning rate 为当前值的 0.1 倍,就是 0.001,
在 35000 到最大 iteration 期间使用 learning
rate 为当前值的 0.1 倍,就是 0.0001。随着 iteration 增加,降低学习率可以是模型更有效的学习,也就是更好的降低 train loss。

最后一层卷积层中 filters 数值是 5×(类别数 + 5)。具体原因就不多说了,知道就好哈。

region 里需要把 classes 改成你的类别数。

最后一行的 random
,是一个开关。如果设置为 1 的话,就是在训练的时候每一 batch 图片会随便改成 320-640(32 整倍数)大小的图片。目的和上面的色度,曝光度等一样。如果设置为 0 的话,所有图片就只修改成默认的大小
416*416。(2018.04.08 更新,评论给里有朋友说这里如果设置为 1 的话,训练的时候 obj 和 noobj 会出现全为 0 的情况,设置为 0 后一切正常。)

3.4 开始训练

可以自己去下载 pre_trained 文件来提高自己的训练效率。

以上所有准备工作做好后就可以训练自己的模型了。

terminal 运行:

./darknet detector train cfg/voc.data cfg/yolo_voc.cfg darknet19_448.conv.23

-------- 【2017.06.29 更新】 --------- 新版本源代码被作者做了很大改动,老版本的可以看看下面内容 --------------------------------------

这里有一点我还是要说一下, 其他博文有让我们修改.c 源文件。其实这个对于我等懒人是真的不用修改的。原因是这样的,在官网里有一段执行 test 的代码是:

./darknet detect cfg/yolo.cfg yolo.weights data/dog.jpg

这是一段简写的执行语句。它的完整形式是这样的:

./darknet detector test cfg/coco.data cfg/yolo.cfg yolo.weights data/dog.jpg

其实修改.c 文件的作用就是让我们可以使用简写的 test 执行语句,程序会自动调用.c 里面设置好的路径内容。我个人觉得这个很没有必要。还有就是最新的版本中已经没有 yolo.cu 这个文件了。


4. 训练输出

这里我讲一讲关于输出的东西都是些什么东西,有些我也不太懂,只挑些有用的,我懂得讲讲。

Region Avg IOU: 这个是预测出的 bbox 和实际标注的 bbox 的交集 除以 他们的并集。显然,这个数值越大,说明预测的结果越好。

Avg Recall: 这个表示平均召回率, 意思是 检测出物体的个数 除以 标注的所有物体个数。

count: 标注的所有物体的个数。 如果 count = 6, recall = 0.66667,
就是表示一共有 6 个物体(可能包含不同类别,这个不管类别),然后我预测出来了 4 个,所以 Recall 就是 4 除以 6 = 0.66667 。

有一行跟上面不一样的,最开始的是 iteration 次数,然后是 train loss,然后是 avg train loss, 然后是学习率,
然后是一 batch 的处理时间, 然后是已经一共处理了多少张图片。 重点关注 train loss 和 avg train
loss,这两个值应该是随着 iteration 增加而逐渐降低的。如果 loss 增大到几百那就是训练发散了,如果 loss 在一段时间不变,就需要降低 learning
rate 或者改变 batch 来加强学习效果。当然也可能是训练已经充分。这个需要自己判断。

5. 可视化

这里我给大家分享一个关于 loss 可视化的 matlab 代码。可以很直观的看到你的 loss 的变化曲线。

首先在训练的时候,可以通过 script 命令把 terminal 的输出都录像到一个 txt 文档中。

script -a log.txt




./darknet detector train cfg/voc.data cfg/yolo_voc.cfg darknet19_448.conv.23

训练完成后记得使用 ctrl+D 或者输入 exit 结束屏幕录像。

下面是 matlab 代码:

clear;
clc;
close all;

train_log_file = 'log.txt';


[~, string_output] = dos(['cat ', train_log_file, ' | grep "avg," | awk ''{print $3}''']);
train_loss = str2num(string_output);
n = 1:length(train_loss);
idx_train = (n-1);

figure;plot(idx_train, train_loss);

grid on;
legend('Train Loss');
xlabel('iterations');
ylabel('avg loss');
title(' Train Loss Curve');

我画的是 avg train
loss 的曲线图。我这里 batch 为 8,曲线震荡幅度很大。学习率是 0.0001,25000 次迭代后降到 0.00001,。大家注意看后面 loss 趋于稳定在 7-8 左右降低就不明显,并且趋于不变了。我的理解是可能遇到两种情况,一是陷入局部最优,二是学习遇到瓶颈。但没有解决这个问题。降低 batch,提高学习效果,loss 不变。提高 batch,让网络更顾全全局,loss 降低一点点,继续不变。降低学习率提高学习效果,降低一点点,继续不变。希望有大神看到此文请予以指教。25000 次迭代后有一点下降是因为我降低了 learning
rate,但降低不明显,而且很快就又趋于平稳了。训练集 4.3W+,是从 ILSVRC2015 训练集和 VOC2012 训练集中挑出来的所有关于人的数据。

----------- 【2017.09.26 更新】 -----------------

一直犯懒没有写这个补充说明,上面我说可能陷入局部最优。做了很多工作并没有提升。我当时一直用 caffe 的 loss 标准来对待 darknet,
其实这个有很大问题。因为两种框架输出 loss 并不一定是相同单位的。其实我上面训练好的模型的实际使用效果是非常非常好的,远远的一颗小小人头都能检测的到.

并且现在看上面训练参数应该可以有所改进,我在这里记一下如果以后再次用到 darknet 的话方便查阅。因为做的 finetune, 学习率很低这个没毛病,
但是相应 momentum 可以考虑适当增加,从 0.9 调整到 0.99. 还可以考虑将学习策略改成 poly.


----------- 【2017.10.30 更新】 ------------------------

最近开始着手做一些 detection
的东西,回过头来看 darknet,再纠正一点。对于 detection 而言,应该不要盲目用 loss 来评判一个模型的好坏。loss 应该用作在训练当中判断训练是否正常进行。比如训练一开始 loss 一直升高,最后 NAN,说明学习率大了。那我们要用哪个数值来评判模型的好坏呢?应该是 mAP,这个应该是当前主流标准,其实就是平均的 precision。在 darknet 训练过程中并不输出 precision 的结果,所以我们也可以通过 recall 来判断。recall 越接近 1.0,就说明模型检测到实际的物体数量越准确。如果用老版本的 darknet,并通过修改源代码,可以在测试阶段输出 precision,具体内容我已经在本文中写出来了。


----------- 【2017.11.30 更新】添加 IOU --------------

IOU 这个输出我之前是忽略了,这个输出其实也可以在训练阶段判断模型是否在正确训练,以及最后效果如何。


1668629536594.jpg

----------------- 【 2017.09.19 更新】 python 可视化代码 --------------------------------------------

我一开始是写给 caffe 的,不过道理都一样,就顺便把这个也更新了.

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
"""
Created on Tue Aug 29 10:05:13 2017

@author: hans

http://blog.csdn.net/renhanchi
"""
import matplotlib.pyplot as plt
import numpy as np
import commands

train_log_file = "vegetable_squeezenet.log"

display = 10 #solver
test_interval = 100 #solver

train_output = commands.getoutput("cat " + train_log_file + " | grep 'avg,' | awk '{print $3}'")  #train loss

train_loss = train_output.split("\n")

_,ax1 = plt.subplots()

l1, = ax1.plot(display*np.arange(len(train_loss)), train_loss)

ax1.set_xlabel('Iteration')
ax1.set_ylabel('Train Loss')

plt.legend([l1], ['Train Loss'], loc='upper right')
plt.show()

如果觉得波动太大,不好看变化趋势。可以参考
http://blog.csdn.net/renhanchi/article/details/78411095
里可视化代码,改一改就好了。


6. 评价模型

-------- 【2017.07.03 更新】 -------------------------------------------------------------------------

下面命令不适用于新版本 darknet( 如果你下载了最上面百度云盘中老版本的 darknet,下面命令是适用的

首先使用下面的命令:

./darknet detector recall cfg/xxx.data cfg/xxx.cfg backup/xxx.weights

下面命令在新版本上是适用

在 xxxx.data 文件中 train 下面加上一句话: valid=path/to/valid/images.txt

./darknet detector valid cfg/.....data cfg/.....cfg backup/....weights

然后输出一连串的数字数字数字,什么鬼??


------ 【2017.06.29 更新】 --- 源代码被作者做了很大改动,新版本已经不适用下面内容了 - -----------

如果你下载了最上面百度云盘中老版本的 darknet,下面命令还是适用的

输出是累积的,结果只有 recall,没有 precision。因为第一代 yolo 有一定缺陷,precision 跟其他方法相比很低,所以作者干脆把 threshold 设置的特别低,这样就只看 recall,放弃 precision 了。第二代 yolo 已经修复了这个缺陷,所以可以自己修改代码把 precision 调出来。大家打开 src/detector.c
,找到 validate_detector_recall 函数,其中 float thresh = .25;
就是用来设置 threshold 的,我这里改成了 0.25,原作者的值是 0.0001。所以我一开始的 precision 只有 1% 多点 -0-。
继续往下看,找到这句话

fprintf(stderr, "%5d\t%5d\t%5d\tRPs/Img: %.2f\tIOU: %.2f%%\tRecall:%.2f%%\t", i, correct, total, (float)proposals/(i+1), 
avg_iou*100/total, 100.*correct/total);

把它改成下面这句话

fprintf(stderr, "Number: %5d\tCorrect: %5d\tTotal: %5d\tRPs/Img: %.2f\tIOU: %.2f%%\tRecall:%.2f%%\tProposals: %5d\t
Precision: %.2f%%\n", i, correct, total, (float)proposals/(i+1), avg_iou*100/total, 100.*correct/total, proposals, 
100.*correct/(float)proposals);

~~
~~

重新 make 后,再次执行 recall 命令,就有 precision 啦。


------ 【2017.12.22 更新】 --- 重新表述一下 correct -----------------------------------------

Correct 表示正确的识别出了多少 bbox。这个值算出来的步骤是这样的,丢进网络一张图片,网络会对每一类物体都预测出很多 bbox,每个 bbox 都有其置信概率,概率大于 threshold 的某一类物体的 bbox 与其实际的 bbox(也就是 labels 中 txt 的内容)计算 IOU,找出当前类物体 IOU 最大的 bbox,如果这个最大值大于预设的 IOU 的 threshold,说明当前类物体分类正确,那么 correct 加一。
我再多说两句,对于 bbox 的 threshold,我们可以在命令行通过 -
thres 调整的,同时也是上面修改源代码输出 precision 所提及的 threshold。 而 IOU 的 threshold 只能通过源代码调整。


关于输出的参数,我的理解如下

Number 表示处理到第几张图片。

Correct
表示正确的识别除了多少 bbox。这个值算出来的步骤是这样的,丢进网络一张图片,网络会预测出很多 bbox,每个 bbox 都有其置信概率,概率大于 threshold 的 bbox 与实际的 bbox(也就是 labels 中 txt 的内容)计算 IOU,找出 IOU 最大的 bbox,如果这个最大值大于预设的 IOU 的 threshold,那么 correct 加一。

Total 表示实际有多少个 bbox。

Rps/img 表示平均每个图片会预测出来多少个 bbox。

IOU 我上面解释过哈。

Recall 我上面也解释过。通过代码我们也能看出来就是 Correct 除以 Total 的值。

Proposal 表示所有预测出来的 bbox 中,大于 threshold 的 bbox 的数量。

Precision 表示精确度,就是 Correct 除以 Proposal 的值。

关于预测的框和 recall,precision 一些逻辑上面的东西,我再多说点,给大家捋一捋顺序。

例如识别 人 这一类

  1. 图片中实际有 n 个人,对应 n 个 bbox 信息。这个 n 的值就是 Total 的值

  2. 图片丢进网络,预测出 N 个 bbox,这 N 个 bbox 中置信概率大于 threshold 的 bbox 的数量就是 Proposal 的值,记为 Npro

  3. n 个实际 bbox 分别和 Npro 个 bbox 计算 IOU,得到 n 个 IOU 最大的 bbox,这 n 个 IOU 最大的 bbox 再与 IOU threshold 比较,得到 nCor 个大于 IOU threshold 的 bbox。nCor 就是 Correct 的值。nCor 小于等于 n。

  4. 有了 Total,Proposal 和 Correct,就能算出 Recall 和 Precision 了。

总结一下就是图片丢网络里,预测出了 Npro 个物体,但有的预测对了,有的预测的不对。nCor 就是预测对了的物体数量。Recall 就是表示预测对的物体数量(nCor)和实际有多少个物体数量(Total)的比值。Precision 就是预测对的物体数量(nCor)和一共预测出所有物体数量(Proposal)的比值。联系上面提到的作者一开始把 threshold 设置为 0.0001,这将导致 Proposal 的值(Npro)非常大,所以 Correct 的值(nCor)也会随之相应大一点。导致计算出的 Recall 值很大,但 Precision 值特别低。

注明:以上内容部分参考自 http://blog.csdn.net/hysteric314/article/details/54097845

关于 recall,precision 和 IOU,可以看看这边文章
http://blog.csdn.net/hysteric314/article/details/54093734

Loading Comment
Loading...
Loading...