参考链接:https://blog.csdn.net/mr_health/article/details/80676799
一、数据准备
我的数据格式是voc,而detectron要求的数据格式是json,因此首先要进行格式的转换。
1.数据放置
官方网站上的要求的数据存放见左图,也就是说无论我们自己要训练什么数据,文件夹的名称必须是VOC+year。但是为什么要是VOC+year这样的形式呢,我们再翻看一下datasets/dadataset_catalog.py这个函数(右图),可以看到里面有各种数据集,包括voc2007、voc2012。实际上这些就是数据接口,也就是说我们把自己的数据按照接口的形式准备好,detectron就会调用。
一般情况下,我们的数据集可以分为三部分,一部分是训练数据(train),第二部分验证数据(val)、第三部分是测试数据(test)。实际上验证数据和测试数据都可以被称为测试数据,只是功能有所不同。验证数据(val)由于其带标签,在测试中往往会输出mAP,用以评估模型的性能,而测试数据(test)不含有标签,实际上就是当我们训练好了模型,那就在我的一个数据库上试试呗,看检测出来好玩不,测试数据(test)就这样一个数据库。右图中voc_2007_train就是用于训练,voc_2007_val用于验证,voc_2007_test用于测试。
但是实际上在我们训练模型的时候,只将数据集分为训练数据和测试数据,只是这时候的测试数据功能上是对模型的评估,所以在代码的书写上都以val指代,称呼上就是测试数据(下文就这么称呼了)。
注意:只有train,val可以生成AP,AP50,mAP结果,只要包含test这些结果为-1
dataset_catalog.py当中的文件,按照这个文件名命名
文件结构:在/home/test/detectron/detectron/datasets/data当中新建一个文件夹:VOC2007
1)annotations里面有两个json文件,分别是voc_2007_train.json,voc2007_val.json
将annotations文件夹当中的xml拷贝出来,复制到train,val两个文件夹当中,利用上述代码,生成train,val这两个json文件
修改相应的路径
#coding=utf-8
#fork from https://github.com/CivilNet/Gemfield/blob/master/src/python/pascal_voc_xml2json/pascal_voc_xml2json.py
import xml.etree.ElementTree as ET
import os
import json
coco = dict()
coco['images'] = []
coco['type'] = 'instances'
coco['annotations'] = []
coco['categories'] = []
category_set = dict()
image_set = set()
category_item_id = 0
image_id = 20180000000
annotation_id = 0
def addCatItem(name):
global category_item_id
category_item = dict()
category_item['supercategory'] = 'none'
category_item_id += 1
category_item['id'] = category_item_id
category_item['name'] = name
coco['categories'].append(category_item)
category_set[name] = category_item_id
return category_item_id
def addImgItem(file_name, size):
global image_id
if file_name is None:
raise Exception('Could not find filename tag in xml file.')
if size['width'] is None:
raise Exception('Could not find width tag in xml file.')
if size['height'] is None:
raise Exception('Could not find height tag in xml file.')
image_id += 1
image_item = dict()
image_item['id'] = image_id
image_item['file_name'] = file_name
image_item['width'] = size['width']
image_item['height'] = size['height']
coco['images'].append(image_item)
image_set.add(file_name)
return image_id
def addAnnoItem(object_name, image_id, category_id, bbox):
global annotation_id
annotation_item = dict()
annotation_item['segmentation'] = []
seg = []
# bbox[] is x,y,w,h
# left_top
seg.append(bbox[0])
seg.append(bbox[1])
# left_bottom
seg.append(bbox[0])
seg.append(bbox[1] + bbox[3])
# right_bottom
seg.append(bbox[0] + bbox[2])
seg.append(bbox[1] + bbox[3])
# right_top
seg.append(bbox[0] + bbox[2])
seg.append(bbox[1])
annotation_item['segmentation'].append(seg)
annotation_item['area'] = bbox[2] * bbox[3]
annotation_item['iscrowd'] = 0
annotation_item['ignore'] = 0
annotation_item['image_id'] = image_id
annotation_item['bbox'] = bbox
annotation_item['category_id'] = category_id
annotation_id += 1
annotation_item['id'] = annotation_id
coco['annotations'].append(annotation_item)
def parseXmlFiles(xml_path):
for f in os.listdir(xml_path):
if not f.endswith('.xml'):
continue
print(f)
name =f.split('.')[0]
valPath = '/home/test/detectron/detectron/datasets/data/VOC2007/VOCdevkit2007/VOC2007/ImageSets/Main/'
valPath = os.path.join(valPath,"val.txt")
with open(valPath,'a+') as ff:
ff.write(name)
ff.write('\n')
bndbox = dict()
size = dict()
current_image_id = None
current_category_id = None
file_name = None
size['width'] = None
size['height'] = None
size['depth'] = None
xml_file = os.path.join(xml_path, f)
print(xml_file)#输出xml文件的全路径
tree = ET.parse(xml_file)
root = tree.getroot() #抓根结点元素
if root.tag != 'annotation': #根节点标签
raise Exception('pascal voc xml root element should be annotation, rather than {}'.format(root.tag))
# elem is <folder>, <filename>, <size>, <object>
for elem in root:
current_parent = elem.tag
current_sub = None
object_name = None
#elem.tag, elem.attrib,elem.text
if elem.tag == 'folder':
continue
if elem.tag == 'filename':
file_name = elem.text
if file_name in category_set:
raise Exception('file_name duplicated')
# add img item only after parse <size> tag
elif current_image_id is None and file_name is not None and size['width'] is not None:
if file_name not in image_set:
current_image_id = addImgItem(file_name, size)#图片信息
print('add image with {} and {}'.format(file_name, size))
else:
raise Exception('duplicated image: {}'.format(file_name))
# subelem is <width>, <height>, <depth>, <name>, <bndbox>
for subelem in elem:
bndbox['xmin'] = None
bndbox['xmax'] = None
bndbox['ymin'] = None
bndbox['ymax'] = None
current_sub = subelem.tag
if current_parent == 'object' and subelem.tag == 'name':
object_name = subelem.text
if object_name not in category_set:
current_category_id = addCatItem(object_name)
else:
current_category_id = category_set[object_name]
elif current_parent == 'size':
if size[subelem.tag] is not None:
raise Exception('xml structure broken at size tag.')
size[subelem.tag] = int(subelem.text)
# option is <xmin>, <ymin>, <xmax>, <ymax>, when subelem is <bndbox>
for option in subelem:
if current_sub == 'bndbox':
if bndbox[option.tag] is not None:
raise Exception('xml structure corrupted at bndbox tag.')
bndbox[option.tag] = int(option.text)
# only after parse the <object> tag
if bndbox['xmin'] is not None:
if object_name is None:
raise Exception('xml structure broken at bndbox tag')
if current_image_id is None:
raise Exception('xml structure broken at bndbox tag')
if current_category_id is None:
raise Exception('xml structure broken at bndbox tag')
bbox = []
# x
bbox.append(bndbox['xmin'])
# y
bbox.append(bndbox['ymin'])
# w
bbox.append(bndbox['xmax'] - bndbox['xmin'])
# h
bbox.append(bndbox['ymax'] - bndbox['ymin'])
print(
'add annotation with {},{},{},{}'.format(object_name, current_image_id, current_category_id, bbox))
addAnnoItem(object_name, current_image_id, current_category_id, bbox)
if __name__ == '__main__':
# / home / test / detectron / detectron / datasets / data / VOC2007 / VOCdevkit2007 / VOC2007 / Annotations
xml_path = '/home/test/detectron/detectron/datasets/data/VOC2007/VOCdevkit2007/VOC2007/Annotations/'
json_file = '/home/test/detectron/detectron/datasets/data/VOC2007/annotations/voc_2007_val.json'
parseXmlFiles(xml_path)
json.dump(coco, open(json_file, 'w'))
2)JPEGImages里就是所有的训练的数据
3)VOCdevkit2007就是将pascal voc 2007直接解压得到的结果,将名字由VOCdevkit改为VOCdevkit2007
创建一个VOC2007的文件夹,将数据从VOCdevkit/VOC2007/JPEGImages中取出来放到VOC2007/JPEGImages中,将训练数据和测试数据的xml文件进行划分,分别放置在train_Annotations和val_Annotations中。同时可以创建一个Annotations文件夹,用来存放转换后的json文件,也就是上图中的annotations。由于我选择的year=2007,因此转换后的训练数据和测试数据文件名称分别为:voc_2007_train.json,voc_2007_val.json。
最后创建VOCdevkit2007文件夹,其结构如下(里面还有一个VOC2007文件),注意其中的ImageSets/Main目录中存放的是自己数据的train.txt和val.txt,文件内容是图片名称的前缀,也就是6位数字
二、模型训练
在/home/yantianwang/clone/detectron中创建一个文件夹experiments,用于存放模型,配置文件(yaml文件),以及最后训练核测试后的结果。
这里我采用Retianet50+FPN来训练自己的数据
1.下载模型
官方下载地址:model-zoo
下载R-50.pkl到experiments文件夹中
2.设置配置文件
将configs/12_2017_baselines中的retinanet_R-50-FPN_1x.yaml拷贝到experiments文件中,主要修改
NUM_CLASSES: 你自己的类别数+1(因为有背景)
MAX_ITER:迭代次数
TRAIN:
WEIGHTS: /home/yantianwang/clone/detectron/experiments/R-50.pkl (因为我习惯在spyder中运行,所以路径是绝对路径)
DATASETS: ('voc_2007_train',) 也就是修改为我们刚刚转换格式后json文件
TEST:
DATASETS: ('voc_2007_val',) 修改成我们的val,训练完毕后会自动进行测试,输出mAP
OUTPUT_DIR: /home/yantianwang/clone/detectron/experiments/result 训练结果和测试结果的存放路径
注意,训练中很多参数是可以修改的,见detectron/core/config.py,如果你要修改的话,不用在py文件中,直接在这个.yaml文件中的相应位置写出参数名字,后面跟上修改的数值即可。例如我想修改:
该参数是归在TRAIN下的(因为是C.TRAIN) ,因此我在.yaml文件中的TRAIN下作如下添加即可:
最终修改结果:
MODEL:
TYPE: retinanet
CONV_BODY: FPN.add_fpn_ResNet50_conv5_body
#设置总共的类别,前景+背景,20+1
NUM_CLASSES: 21
NUM_GPUS: 1
SOLVER:
WEIGHT_DECAY: 0.0001
LR_POLICY: steps_with_decay
#基础学习率必须调小一些,否则loss会出现为0的情况
BASE_LR: 0.00001
GAMMA: 0.1
#最大迭代次数
MAX_ITER: 5000
STEPS: [0, 60000, 80000]
FPN:
FPN_ON: True
MULTILEVEL_RPN: True
RPN_MAX_LEVEL: 7
RPN_MIN_LEVEL: 3
COARSEST_STRIDE: 128
EXTRA_CONV_LEVELS: True
RETINANET:
RETINANET_ON: True
NUM_CONVS: 4
ASPECT_RATIOS: (1.0, 2.0, 0.5)
SCALES_PER_OCTAVE: 3
ANCHOR_SCALE: 4
LOSS_GAMMA: 2.0
LOSS_ALPHA: 0.25
TRAIN:
#基础网络,resnet50
WEIGHTS: /home/test/detectron/experiments/R-50.pkl
#也就是修改为我们刚刚转换格式后json文件
DATASETS: ('voc_2007_train',)
SCALES: (800,)
MAX_SIZE: 1333
RPN_STRADDLE_THRESH: -1 # default 0
TEST:
#也就是修改为我们刚刚转换格式后json文件
DATASETS: ('voc_2007_val',)
SCALE: 800
MAX_SIZE: 1333
NMS: 0.5
RPN_PRE_NMS_TOP_N: 10000 # Per FPN level
RPN_POST_NMS_TOP_N: 2000
#将结果保存位置
OUTPUT_DIR: /home/test/detectron/experiments/result
3.开始训练
用spyder打开Detectron/tools/train_net.py文件,修改parse_args()函数:
注释掉 :
if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)
修改:
parser.add_argument(
'--cfg',
dest='cfg_file',
help='Config file for training (and optionally testing)',
default= '/home/yantianwang/clone/detectron/experiments/retinanet_R-50-FPN_1x.yaml', #配置文件的绝对路径
type=str
修改完毕点击运行就可以了
注意:如果你不想利用spyder进行调试,而只是想通过下面的命令终端运行,那么就不用修改train_net.py,同时.yaml配置文件中的路径也统统修改为相对路径。
python2 tools/train_net.py \
--cfg experiments/retinanet_R-50-FPN_1x.yaml \
OUTPUT_DIR experiments/result
4.对于训练结果的评估
在训练之后,会对于训练结果进行AP值的测评,此时有个坑:在每次训练完之后会在detectron/detectron/datasets/data/VOC2007/VOCdevkit2007/annotations_cache当中生成一个cache文件,如果不删除该文件夹当中的文件,那么会在下次验证的时候报错。
detectron截至2018年9月18日为止,仅仅支持类coco数据集的评估,不支持voc格式的评估,如果数据集前面含有VOC_这个前缀,就会调用_voc_eval_to_box_results函数,然而该函数默认初始值,从而导致AP值为-1
修改方法:在dataset_category.py文件中将val数据集改为含有coco字符,使其进入coco数据集的测评函数中,并且验证的json文件中不能含有test字样,否则AP值都会为-1
最终可以得到一个AP值不为-1的结果