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

2.数据集

  这里建议大家用VOC和ILSVRC比赛的数据集,因为xml文件都是现成的,省去很多功夫。当然除非你是个执着的孩子就想凭借着非人的毅力和追逐斗鸡眼这种个性特征而自己去标记label。

  勤劳的孩子想自己标记的可以自己去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
里面类别的索引。

[cpp] view
plain
 copy

  1. #!/bin/sh  
  2.   
  3. year="VOC2012"  
  4.   
  5. mkdir /your_path/VOCperson/${year}_Anno/  #创建文件夹  
  6. mkdir /your_path/VOCperson/${year}_Image/  
  7.   
  8. cd /your_path/VOCdevkit/$year/Annotations/  
  9. grep -H -R "<name>person</name>" > /your_path/VOCperson/temp.txt  #找到有关键字的行,并把这些行存到临时文档  
  10.   
  11. cd /your_path/VOCperson/  
  12. cat temp.txt | sort | uniq > $year.txt     #根据名字排序,并把相邻的内容完全一样的多余行删除。  
  13. find -name $year.txt | xargs perl -pi -e 's|.xml:\t\t<name>person</name>||g'   #把文档中后缀名和其他无用信息删掉,只保留没后缀名的文件名  
  14.   
  15. cat $year.txt | xargs -i cp /your_path/VOCdevkit/$year/Annotations/{}.xml /your_path/VOCperson/${year}_Anno/ #根据文件名复制注释文件  
  16. cat $year.txt | xargs -i cp /your_path/VOCdevkit/$year/JPEGImages/{}.jpg /your_path/VOCperson/${year}_Image/ #根据文件名复制数据集  
  17.   
  18. rm temp.txt  

3.训练文件

  3.1 文件夹设置

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

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

      ImageSets -> Main ---- 这个文件里放一个names.txt文档,文档内容是所有训练集图片的名字,没有后缀名。

      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文件

[python] view
plain
 copy

  1. import xml.etree.ElementTree as ET  
  2. import pickle  
  3. import os  
  4. from os import listdir, getcwd  
  5. from os.path import join  
  6.   
  7. sets=[('person','train')]  
  8. classes = ["n00007846"]  
  9.   
  10. def convert(size, box):  
  11.     dw = 1./(size[0])  
  12.     dh = 1./(size[1])  
  13.     x = (box[0] + box[1])/2.0 - 1  
  14.     y = (box[2] + box[3])/2.0 - 1  
  15.     w = box[1] - box[0]  
  16.     h = box[3] - box[2]  
  17.     x = x*dw  
  18.     w = w*dw  
  19.     y = y*dh  
  20.     h = h*dh  
  21.     return (x,y,w,h)  
  22.   
  23. def convert_annotation(year, image_id):  
  24.     in_file = open('/home/hans/darknet/person/VOC%s/Annotations/%s.xml'%(year, image_id))  
  25.     out_file = open('/home/hans/darknet/person/VOC%s/labels/%s.txt'%(year, image_id), 'w')  
  26.     tree=ET.parse(in_file)  
  27.     root = tree.getroot()  
  28.     size = root.find('size')  
  29.     w = int(size.find('width').text)  
  30.     h = int(size.find('height').text)  
  31.   
  32.     for obj in root.iter('object'):  
  33.         # difficult = obj.find('difficult').text  
  34.         cls = obj.find('name').text  
  35.         if cls not in classes: # or int(difficult)==1:  
  36.             continue  
  37.         cls_id = classes.index(cls)  
  38.         xmlbox = obj.find('bndbox')  
  39.         b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text), float(xmlbox.find('ymax').text))  
  40.         bb = convert((w,h), b)  
  41.         out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')  
  42.   
  43. wd = getcwd()  
  44. for year, image_set in sets:  
  45.     if not os.path.exists('/home/hans/darknet/person/VOC%s/labels/'%(year)):  
  46.         os.makedirs('/home/hans/darknet/person/VOC%s/labels/'%(year))  
  47.     image_ids = open('/home/hans/darknet/person/VOC%s/ImageSets/Main/%s.txt'%(year, image_set)).read().strip().split()  
  48.     list_file = open('%s_%s.txt'%(year, image_set), 'w')  
  49.     for image_id in image_ids:  
  50.         list_file.write('%s/VOC%s/JPEGImages/%s.jpg\n'%(wd, year, image_id))  
  51.         convert_annotation(year, image_id)  
  52.     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,你可以尝试逐渐降低这个值看效果。

[cpp] view
plain
 copy

  1. ./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。

  3.4 开始训练

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

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

      terminal 运行:

[cpp] view
plain
 copy

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

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


[cpp] view
plain
 copy

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


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

[cpp] view
plain
 copy

  1. ./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文档中。

[cpp] view
plain
 copy

  1. script -a log.txt  
[cpp] view
plain
 copy

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

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

  下面是matlab代码:

[cpp] view
plain
 copy

  1. clear;  
  2. clc;  
  3. close all;  
  4.   
  5. train_log_file = 'log.txt';  
  6.   
  7.   
  8. [~, string_output] = dos(['cat ', train_log_file, ' | grep "avg," | awk ''{print $3}''']);  
  9. train_loss = str2num(string_output);  
  10. n = 1:length(train_loss);  
  11. idx_train = (n-1);  
  12.   
  13. figure;plot(idx_train, train_loss);  
  14.   
  15. grid on;  
  16. legend('Train Loss');  
  17. xlabel('iterations');  
  18. ylabel('avg loss');  
  19. 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训练集中挑出来的所有关于人的数据。

【计算机视觉】【神经网络与深度学习】YOLO v2 detection训练自己的数据2


6.评价模型

首先使用下面的命令:

[cpp] view
plain
 copy

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

输出是累积的,结果只有recall,没有precision。因为作者为了让自己的数据更好看,把threshold设置的特别低,这样做的好处就是precision惨不忍睹,Recall特别牛逼。当然这不是我们想要的结果。大家可以打开src/detector.c ,找到validate_detector_recall函数,其中float thresh
= .25; 就是用来设置threshold的,我这里改成了0.25,原作者的值是0.0001。所以我一开始的precision只有1%多点 -0-。 继续往下看,找到这句话

[cpp] view
plain
 copy

  1. 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);  

把它改成下面这句话

[cpp] view
plain
 copy

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

再次执行recall命令,这次就有precision啦。

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

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个大于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