原文链接:https://blog.csdn.net/yapifeitu/article/details/105749693

    看了下 yolov4 的作者给的操作说明,链接如下:https://github.com/AlexeyAB/darknet#how-to-compile-on-linux-using-make,有兴趣的可以去看看,总结起来,跟 yolov3 的操作方式基本一样,所以现在记录一下这次的整个操作流程。

    在几个月前,一直在准备一个项目,那个项目已经让人用 lableme 这个标注软件标注好了图片,但是直到现在,这个项目依旧还没有动工的苗头,估计是悬了,之前也用标注好的数据进行过 yolov3 的训练,但是说实话,那时候对于 yolov3 是一头雾水,只知道按照他人的给的操作流程去做,但是为什么这样做,我是不清楚的,这也导致我自己都不知道当初的训练模型的流程是否正确,现在好了,前段时间把 yolov3,ssd 学习了,现在来操作,就知道这样操作的原理了,整个操作流程就知道的很详细了,也知道怎么训练自己的数据了,不再茫然。

    环境说明:yolo 系列一直都有 linux 版本和 windows 版本,我看了哈,很明显 linux 版本更加简单,windows 版本还需要搭配 vs 来操作,光 vs 这个软件就十几个 g, 太大了,所以我就采取在 ubuntu 上训练数据了。

    首先,要感谢下面的这篇文章的作者:https://www.cnblogs.com/Assist/p/11091501.html,这篇文章为我梳理了整个思路,这也为我想处理自己的数据,训练自己的模型有了想法。

    一般来说,训练 coco, vol 这些数据集,网上很多教程来进行数据的预处理,但是训练自己的数据,就得自己根据网上的教程来改代码了,因为 coco 这些数据集的标注文件和图片存储结果可能跟自己的数据都不一样,对于我用 labelme 来标注的图像来说,每一张图片会对应一个 json 文件,这个 json 文件里面拥有这标注的类别坐标这些信息,所以,我这里值将所有图片放到一个文件夹,所有 json 文件放到一个文件夹,但是为了区分训练数据和 val 数据,我将 json 进行了划分,一部分放到了 train 文件夹,一部分放到了 val 文件夹,最后整个文件的结构如下:
    yolov4训练自己的数据模型 - 图1
    接下来咱就可以开始正式的数据预处理了,这一块是最麻烦的,我个人觉得哈。我们运行第一个 Py 文件后,要创建出下面这几个文件夹:

    yolov4训练自己的数据模型 - 图2

    第一个文件夹用来装 xml 文件,这个 xml 文件里面有标签类别,图片的宽高,标签的左上角和右下角坐标这些信息,每一张照片对应一个 xml 文件,imagesets 这个文件夹里面还有一个文件夹:/Main, 在这个 Main 文件夹下面会产生 train.txt 和 val.txt,这些文件里面的信息只是图片的名字,不带. jpg 这些后缀的名字,而在 JPEGImages 里面就是所有图片了。

    我们假设这第一个 py 文件叫 convertvoc.py,我下面提供我的代码,大家不能照搬,有些地方要改。

    1. '''author:nike hu'''
    2. import shutil
    3. import os
    4. import json
    5. import cv2
    6. headstr = """\
    7. <annotation>
    8. <folder>VOC2007</folder>
    9. <filename>%06d.jpg</filename>
    10. <source>
    11. <database>My Database</database>
    12. <annotation>PASCAL VOC2007</annotation>
    13. <image>flickr</image>
    14. <flickrid>NULL</flickrid>
    15. </source>
    16. <owner>
    17. <flickrid>NULL</flickrid>
    18. <name>company</name>
    19. </owner>
    20. <size>
    21. <width>%d</width>
    22. <height>%d</height>
    23. <depth>%d</depth>
    24. </size>
    25. <segmented>0</segmented>
    26. """
    27. objstr = """\
    28. <object>
    29. <name>%s</name>
    30. <pose>Unspecified</pose>
    31. <truncated>0</truncated>
    32. <difficult>0</difficult>
    33. <bndbox>
    34. <xmin>%d</xmin>
    35. <ymin>%d</ymin>
    36. <xmax>%d</xmax>
    37. <ymax>%d</ymax>
    38. </bndbox>
    39. </object>
    40. """
    41. tailstr = '''\
    42. </annotation>
    43. '''
    44. def writexml(idx, head, bbxes, tail):
    45. filename = ("Annotations/%06d.xml" % (idx))
    46. f = open(filename, "w")
    47. f.write(head)
    48. for bbx in bbxes:
    49. f.write(objstr % (bbx[0], bbx[1], bbx[2], bbx[3], bbx[4]))
    50. f.write(tail)
    51. f.close()
    52. def clear_dir():
    53. if shutil.os.path.exists(('Annotations')):
    54. shutil.rmtree(('Annotations'))
    55. if shutil.os.path.exists(('ImageSets')):
    56. shutil.rmtree(('ImageSets'))
    57. if shutil.os.path.exists(('JPEGImages')):
    58. shutil.rmtree(('JPEGImages'))
    59. shutil.os.mkdir(('Annotations'))
    60. shutil.os.makedirs(('ImageSets/Main'))
    61. shutil.os.mkdir(('JPEGImages'))
    62. def excute_datasets(json_path, tr, idx):
    63. json_path = os.path.join(json_path, tr)
    64. json_file = os.listdir(json_path)
    65. savename = open(('ImageSets/Main/' + tr + '.txt'), 'a')
    66. for file in json_file:
    67. file_path = os.path.join(json_path, file)
    68. with open(file_path, 'r', encoding='utf-8') as f:
    69. file_json = json.load(f)
    70. imagename = file_json["imagePath"].split('\\')[-1]
    71. image_path = os.path.join('./images', imagename)
    72. image = cv2.imread(image_path)
    73. if image is None:
    74. continue
    75. label_shape_type = file_json['shapes'][0]['shape_type']
    76. if label_shape_type != 'rectangle':
    77. continue
    78. head = headstr % (idx, image.shape[1], image.shape[0], image.shape[2])
    79. shapes = file_json['shapes']
    80. boxes = []
    81. for i in range(len(shapes)):
    82. classname = file_json['shapes'][i]['label']
    83. '''接下来转化类别为英文,因为labelme在标注的时候,为了标注人员的遍历,类别是中文,但是我们训练模型的时候必须是英文,这里就需要转化了,这里得xxxx不代表真的是xxxx,是你自己训练的类别,为了不让我老板看出我做的是他的项目,这里隐藏了'''
    84. if 'xxxxxxx' in classname:
    85. classname = 'xxxxxxx'
    86. if 'xxxxxxx' in classname:
    87. classname = 'xxxxxxx'
    88. if 'xxxxxxx' in classname:
    89. classname = 'xxxxxxx'
    90. if 'xxxxxxx' in classname:
    91. classname = 'xxxxxxx'
    92. if 'xxxxxxx' in classname:
    93. classname = 'xxxxxxx'
    94. if 'xxxxxxx' in classname:
    95. classname = 'xxxxxxx'
    96. box = [classname, file_json['shapes'][i]['points'][0][0],
    97. file_json['shapes'][i]['points'][0][1], file_json['shapes'][i]['points'][1][0],
    98. file_json['shapes'][i]['points'][1][1]]
    99. boxes.append(box)
    100. writexml(idx, head, boxes, tailstr)
    101. cv2.imwrite('JPEGImages/%06d.jpg' % (idx), image)
    102. savename.write('%06d\n' % (idx))
    103. idx += 1
    104. savename.close()
    105. return idx
    106. if __name__ == '__main__':
    107. clear_dir()
    108. idx = 1
    109. idx = excute_datasets('./jsonhot', 'train', idx)
    110. idx = excute_datasets('./jsonhot', 'val', idx)
    111. print('Complete...')

    大概需要改的地方我都标注出来了,大家根据自己的实际情况去改上面的代码。上面的代码借鉴了我最上面给的作者的一些代码,毕竟原理都这样,只需要改一改细节上的东西。之前写过一篇文章,在构造 xml 结构的时候用的是 xml 这个库区一个一个节点的构造,现在看来,还是直接向上面的代码那样做简单很多。

    我们运行这个代码之后,就会达到我们上面说的目的,生成 annotations 这些文件夹和数据,这个 py 文件运行完后大家一定要去看看产生了什么数据,不然对后面的操作会懵逼的,接下来,我们要写一个 py 文件,我们假设这个文件叫 getdata.py, 我们要达到的目的是:在主目录下生成两个文件夹 train.txt 和 val.txt,这文件名字随意,这两个文件里面存储的是图片的绝对路径,比如:/media/yunyi/file/code/darknet/data/voc/VOCface/JPEGImages/000003.jpg,这个 Py 文件运行后还会生成一个 label 文件夹,这个文件夹里面也会生成以图片名称命名的 txt 文件,这些文件保存的是图片中的标签类别,中心点的 x, 中心点的 y, 标签的宽,边框的高,接下来,我们看看代码:

    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. sets=['train', 'val']
    7. classes = ['......']
    8. def convert(size, box):
    9. dw = 1./size[0]
    10. dh = 1./size[1]
    11. x = (box[0] + box[1])/2.0
    12. y = (box[2] + box[3])/2.0
    13. w = box[1] - box[0]
    14. h = box[3] - box[2]
    15. x = x*dw
    16. w = w*dw
    17. y = y*dh
    18. h = h*dh
    19. return (x,y,w,h)
    20. def convert_annotation(image_id):
    21. in_file = open(wd + '/Annotations/%s.xml'%(image_id))
    22. out_file = open( wd + '/labels/%s.txt'%(image_id), 'w')
    23. tree=ET.parse(in_file)
    24. root = tree.getroot()
    25. size = root.find('size')
    26. w = int(size.find('width').text)
    27. h = int(size.find('height').text)
    28. for obj in root.iter('object'):
    29. difficult = obj.find('difficult').text
    30. cls = obj.find('name').text
    31. if cls not in classes or int(difficult) == 1:
    32. continue
    33. cls_id = classes.index(cls)
    34. xmlbox = obj.find('bndbox')
    35. b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text), float(xmlbox.find('ymax').text))
    36. bb = convert((w,h), b)
    37. out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')
    38. if __name__=='__main__':
    39. wd = getcwd()
    40. wd = wd.replace('\\', '/')
    41. for image_set in sets:
    42. if not os.path.exists(wd + '/labels/'):
    43. os.makedirs(wd + '/labels/')
    44. image_ids = open(wd +'/ImageSets/Main/%s.txt' % image_set).read().strip().split()
    45. list_file = open('%s.txt' % image_set, 'w')
    46. for image_id in image_ids:
    47. list_file.write(wd + '/JPEGImages/%s.jpg\n' % image_id)
    48. convert_annotation(image_id)
    49. list_file.close()

    代码里面很多 xml.find() 啥的函数,就是找 xml 里面的节点用的,这些代码,我个人感觉当我们第一个 py 文件执行之后,这个 py 文件改一改类别就可以运行了,当然,有可能会出现意外情况,这时候根据不同的报错解决吧,我这反正是没问题的。

    好了,最麻烦的预处理弄完了,接下来咱看看怎么配置 yolov4 了,直接看图片,形象:
    yolov4训练自己的数据模型 - 图3

    yolov4训练自己的数据模型 - 图4

    yolov4训练自己的数据模型 - 图5

    yolov4训练自己的数据模型 - 图6

    在这个 yolov4.cfg 里面,我们一般要更改的就是 batchsize,subdivisions, classes, 这几个参数,对于 filters=255 这一行,根据上面的说明进行更改,max_batches = 500200,steps=400000,450000 这两行大家根据实际情况看该不该,电脑配置给力就没必要改,配置不行就改一下,一般是改为 max_batches = 2000,steps=1600,1800,大家更改的时候直接 ctrl+f 进行定位就行。

    对 cfg 文件进行更改之后,我们就要创建两个文件了,比如叫 face.data,face.names,名字你随意,在 face.data 里面,你的内容范本如下:
    yolov4训练自己的数据模型 - 图7

    classes 就是你的数据的类别总数,train 就是你上面 getdata.py 生成的 train.txt 文件的路径,这里是相对路径,val 同理,names 就是刚刚创建的 face.names 的路径,backup 就是训练过程保存模型的路径。至于 face.names, 里面的内容就是你的类别名字了,类似下面这张:
    yolov4训练自己的数据模型 - 图8

    做了上面那些工作,终于可以开始训练了,只需要一行命令:./darknet detector train data/xxx.data cfg/yolov4.cfg yolov4.conv.137 -map,其中 yolov4.conv.137 是预训练模型,需要另外进行下载,我有空去下载好然后分享一下,如果没有这个文件,那么只需要执行./darknet detector train data/xxx.data cfg/yolov4.cfg -map(没有 - map 也可以) 即可,yolov4 和 yolov3 训练的时候区别就出来了,yolov4 训练的时候会用一张动态图来显示训练的效果,如下所示:
    yolov4训练自己的数据模型 - 图9

    至于训练的效果,哎,不提了,我的 gtx2060 还在学校,现在还没有返校,只能用多年前的笔记本了,这笔记本显卡是 720m,但是不知道是不是 cuda 这些版本没有选择好,yolo 不能使用我这台的 gpu,以前折腾了很久,最后放弃了,所以,我只能用 cpu 来跑,但是我这 cpu 还是四代 i5 的,跑不动,所以。。。。。。。。没有训练效果。

    好了,训练过程终于理清并且写完了,觉得有用的,留个赞再走呗。

    补充:刚刚把作者给的预训练模型下载了,免费分享链接如下:https://blog.csdn.net/yapifeitu/article/details/105756274,拿走的时候记得点赞哦。

    ps:5.7 号用百度的显卡跑了一下 yolov3 的模型,果然出问题了,最后,补上上面没有说到的地方:我们在训练的时候,对于 cfg 文件,我们要将 test 注释掉,类似于下面这种:
    yolov4训练自己的数据模型 - 图10
    哎,就这里没注释然后去跑模型,让我纠结了好久,希望大家后面注意。

    5.8 号我在 aistudio 上用上面的 tesla v100 跑了一个人脸检测的 yolov4 模型 (由于这篇文章涉及到的数据是我老板发钱买来的,不适合放到公开平台去,所以就用公开数据集跑了人脸检测的模型),显卡给力,跑模型的速度就给力,模型跑的 avgloss 在 3 左右后一直在这附近徘徊,知道应该是降不下去多少了,然后把模型下载了,放到自己的的电脑上来测试了几张照片,效果挺不错的,给大家看看效果吧:
    yolov4训练自己的数据模型 - 图11

    yolov4训练自己的数据模型 - 图12
    很明显这里误检测了一个地方,其余的 16 张人脸都检测到了,还别说,那个误检测的还真有些像。

    yolov4训练自己的数据模型 - 图13
    至于这张图片,用 yolov4 训练的模型,检测完美,但是用 yolov3 训练好的模型然后用 yolov3 去测试,会出现误检的情况,不知道是不是模型训练不够的原因。

    2020 4.25
    https://blog.csdn.net/yapifeitu/article/details/105749693