使用FCOS训练自己的数据

使用FCOS训练自己的数据

FCOS网络是发布于2019年的一种全卷积one-stage目标检测算法。该算法摆脱了anchor的需求,以逐像素的方式进行像素级的分类与回归,并且在准确率上面超越了以往的网络。

一、下载源码并编译

  1. 源码地址: https://github.com/tianzhi0549/FCOS

  2. 编译安装。参照项目地址给出的教程安装并测试

pip install torch  # install pytorch if you do not have it
pip install git+https://github.com/tianzhi0549/FCOS.git
# run this command line for a demo 
fcos https://github.com/tianzhi0549/FCOS/raw/master/demo/images/COCO_val2014_000000000885.jpg

​ !这里需要额外指出的是,如果使用了conda环境或者pip安装时指定了其他源比如清华源,其安装的代码未必是最新版本的,这会引发KeyError: 'Non-existent config key: MODEL.FCOS.NORM_REG_TARGETS'错误。请严格按照author的指令进行安装。如果有必要,请下载源码进行编译安装以获得最大的灵活性,使用python install setup.py build_ext --inplace && python install setup.py build_ext install。安装前有必要卸载之前版本pip uninstall fcos。(ps. 笔者下载了最新的代码,为了图快安装了清华源的版本导致训练出现该错误,后发现这个错误emmmm很低级)

​ !如果使用了该方法仍旧有上述报错,尝试修改相关键值为False或者直接注释。

​ - {FCOS_PATH}/configs/fcos/{CONFIG_YOU_CHOOSE}.yaml中修改NORM_REG_TARGETS: TrueFalse

​ - {FCOS_PATH}/fcos_core/config/defaults.py中修改_C.MODEL.FCOS.NORM_REG_TARGETS = TrueFalse

二、构建数据集

FCOS项目在MS COCO数据集上进行训练测试,不过程序也给出了pascal VOC数据集的文件格式。如果需要训练自己的数据集,可以将数据文件于相应的数据集进行覆盖,但是不要修改文件名和路径目录。也可以在{FCOS_PATH}/fcos_core/config/paths_catalog.py中进行添加。其文件内容如下:

#该文件是一个python大字典,存储了不同数据集的根路径、图像和标注路径以及名称。
import os

class DatasetCatalog(object):
    DATA_DIR = "/home/kyle/Programs/FCOS/FCOS/datasets"
    DATASETS = {
############################################################################
        #COCO数据集的格式
        "coco_2017_train": {
            "img_dir": "coco/train2017",
            "ann_file": "coco/annotations/instances_train2017.json"
        },
        "coco_2017_val": {
            "img_dir": "coco/val2017",
            "ann_file": "coco/annotations/instances_val2017.json"
        },
        "coco_2017_test_dev": {
            "img_dir": "coco/test2017",
            "ann_file": "coco/annotations/image_info_test-dev2017.json"
        }
############################################################################
        #Pascal VOC的格式
        "voc_2007_train": {
            "data_dir": "voc/VOC2007",
            "split": "train"
        },
        "voc_2007_val": {
            "data_dir": "voc/VOC2007",
            "split": "val"
        },
        "voc_2007_test": {
            "data_dir": "voc/VOC2007",
            "split": "test"
        }
############################################################################
        #此外还有coco样式的Pascal VOC的数据集文件
        #使用者可以按照上示的格式自行为自己的数据集创建

MS COCO数据集的制作请参考:COCO

PASCAL VOC2007数据集制作请参考:PASCAL VOC2007

​ ! PASCAL转MS COCO。有些开发者可能使用过voc数据集进行训练,再使用fcos训练进行对比。这时候如果不想重新制作不同格式的数据集,需要将数据集转化。格式不正确很容易引起KeyError错误,通常是由于空的键值引起,之所以会出现空的键值是因为数据集的标注文件格式错误,比如:转行、空行等。笔者试了很多代码,仅此代码生效:Pascal2COCO(需要将制表转行等配置删除才通过)

...
    def save_json(self):
        self.data_transfer()
        self.data_coco = self.data2coco()
        # 保存json文件
        json.dump(self.data_coco, open(self.save_json_path, 'w'))  # indent=4 更加美观显示
 
...

三、训练数据集

​ ! 训练数据集的调整方法在作者项目底下的 #54 号issue列出。

@EDG-Zola You do not need to change this code.
In order to train FCOS on your own dataset, you need to,
Add you dataset to 
FCOS/fcos_core/config/paths_catalog.py 
Line 10 in efb76e4 

 "coco_2017_train": { 
. Please use _coco_style as the suffix of your dataset names.
In https://github.com/tianzhi0549/FCOS/blob/master/configs/fcos/fcos_R_50_FPN_1x.yaml, change DATASETS to your own ones.
Modify MODEL.FCOS.NUM_CLASSES in 
FCOS/maskrcnn_benchmark/config/defaults.py 
Line 284 in ff8376b 

 _C.MODEL.FCOS.NUM_CLASSES = 81  # the number of classes including background 
if your dataset has a different number of classes.

​ ! 需要额外指出的是,class_num和具体内容的修改不仅仅在作者提到的文件当中,此外还有很多。就笔者发现的:

  1. demo相关,使用demo功能时修改

    • demo相关的predictor.py文件中CATEGORIES大list的数量以及类名需要修改
    • demo相关的fcos_demo.py文件中thresholds_for_classes的阈值数量需要根据实际情况修改
  2. fcos二进制文件相关,使用fcos命令时修改

  • fcos文件夹下相关的字典、配置都需要修改

首先给出训练命令

python -m torch.distributed.launch \
    --nproc_per_node=8 \
    --master_port=$((RANDOM + 10000)) \
    tools/train_net.py \
    --config-file {YOUR_YAML}.yaml \
    DATALOADER.NUM_WORKERS 1 \
    SOLVER.IMS_PER_BATCH 1\
    TEST.IMS_PER_BATCH 8\
    OUTPUT_DIR {YOUR_SAVING_DIR}

--nproc_per_node=8: 作者使用8 Nvidia V100 GPUs。如果配置不同,在训练的时候指定此参数为训练GPU节点数

OUTPUT_DIR: 模型输出地址
DATALOADER.NUM_WORKERS 1:数据提取进程数

SOLVER.IMS_PER_BATCH 1 && TEST.IMS_PER_BATCH 8\指定训练和测试时使用的batch_size

--config-file: 指定训练配置的yaml文件

Yaml文件

制作属于自己训练的yaml文件,只需要修改两个地方:

  1. 将DATASETS修改为与{FCOS_PATH}/fcos_core/config/paths_catalog.py中一致的dataset键名:

    DATASETS:
      TRAIN: ("voc_2007_train_cocostyle", "voc_2007_val_cocostyle")
      TEST: ("voc_2007_test_cocostyle",)
    
  2. 修改训练参数

SOLVER:
  BASE_LR: 0.00001					#学习速率
  WEIGHT_DECAY: 0.00001				#权重衰减系数
  STEPS: (60000, 80000)				
  MAX_ITER: 90000					#最大迭代次数
  IMS_PER_BATCH: 8					#batch_size最终以训练命令中的数量为准
  WARMUP_METHOD: "constant"

测试网络

该项目的训练过程不像YOLOv3那样清晰可视化,需要测试每个节点保存的模型。测试结果标准严格按照coco数据集的评测标准分为AP、AP50、AP75、APs、APm、APl分别对应mAP,IOU在50、75时的AP以及小中大目标的AP。测试使用到的命令:

python ../tools/test_net.py \
    --config-file {YOUR_YAML_PATH} \
    MODEL.WEIGHT {YOUR_MODEL_PATH} \
    TEST.IMS_PER_BATCH 4 

训练结果:

 Average Precision  (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.916
 Average Precision  (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = 1.000
 Average Precision  (AP) @[ IoU=0.75      | area=   all | maxDets=100 ] = 0.990
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.916
 Average Precision  (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = -1.000
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = -1.000
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=  1 ] = 0.942
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets= 10 ] = 0.942
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.942
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.942
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = -1.000
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = -1.000
Maximum f-measures for classes:
[0.9965156794425087]
Score thresholds for classes (used in demos for visualization purposes):
[0.5761297345161438]
2020-03-10 11:12:26,546 fcos_core.inference INFO: OrderedDict([('bbox', OrderedDict([('AP', 0.9162189350170181), ('AP50', 0.9999312431243125), ('AP75', 0.9896128697376781), ('APs', 0.9162189350170181), ('APm', -1.0), ('APl', -1.0)]))])

由于笔者的数据集仅有小目标,因此APm和APl值为空。最终迭代75000次达到91%的AP,然而AP50过拟合不知道具体是什么原因。

问题汇总(2020.6.9日更)

过拟合的问题

之前遗留的问题很早之前就已经解决了,发现测试集和验证集完全一致没有区别划分,原因是错误的使用了早先学习时候使用的divide_dataset.py脚本。
原则上数据集的划分不允许有交叉部分。验证集会在训练过程中验证出性能指标,在评估和选择模型的时候给予一定的参考。而测试集用来测试模型的泛化能力,来验证所挑选的模型的性能指标是否如验证集的结果所述。一般来讲,模型都需要训练的稍微过拟合一点,欠拟合是需要继续训练的。微量的过拟合是可以接受的,但是最好表现相差无几。就是train_loss

定制voc数据的图像格式

distribute的voc数据读取默认使用了jpg的格式。如果担心数据集从别的格式转化为jpg产生一些精度下降,那么就必须要修改fcos_core的voc读取方式。该文件路径是{fcos}/fcos_core/data/datasets/voc.py, 在该文件大概31行指定了图像数据所在的路径和格式:

	self._imgpath = os.path.join(self.root, {data_path}, {data_format})

修改其中后两个参数可以进行voc数据位置和格式的定制。

一些其他的辅助脚本
Spilt_Dataset

上文中给出的voc2coco脚本默认是将整个文件夹下的标注文件全部划分为一个set。因此这里还需要一个额外的分离数据集的脚本:

import os
import shutil
sets = os.listdir("ImageSets/Main/")
an = os.listdir("Annotations/")
print(an)


for set in sets:
	print(set)
	file = open("ImageSets/Main/"+set,"r")
	for line in file:
		name = line.strip()+".xml"
		print(name)
		shutil.copyfile("Annotations/"+name, "split/"+set[:-4]+"/"+name)

它会根据数据集所划分的set将不同的xml文件放入spilt路径下的相应的文件夹,然后再根据这些文件夹来转化coco.json标注。

Max_size

该脚本用来显示图像数据当中的最大尺寸:

import os
import cv2
import numpy as np
X,Y=0,0
img_lists = os.listdir("JPEGImages")
for img in img_lists:
	print(img)
	read = cv2.imread("JPEGImages/"+img)
	read = np.array(read)
	x,y,z = read.shape
	if x>X:
		X = x
	if y>X:
		Y = y
print("max x,y: ",X,Y)

可以以此为根据修改yaml配置文件当中的图尺寸限制。这边我将图像最大和最小尺寸都修改为和YoloV3同样的416。发现目前使用的gtx1060 6g显卡能够运行的batch_size能够达到16。

监视负载

另开一个termin, 使用如下代码可以动态监视gpu的负载:
watch --color -n 1 gpustat -cpu

Every 1.0s: gpustat -cpu              kyle-X8Ti: Tue Jun  9 11:58:17 2020

kyle-X8Ti            Tue Jun  9 11:58:17 2020  435.21
[0] GeForce GTX 1060 | 70'C, 100 % |  4789 /  6078 MB | kyle:python/16401
(4591M) kyle:Xorg/2244(105M) kyle:gnome-shell/2400(87M) kyle:firefox/2165
4(1M)

另开一个termin, 使用如下代码可以动态监视cpu和内存的负载:
htop

使用FCOS训练自己的数据_第1张图片

自定义数据集以及名称(2020.8.14日更)

制作相应的voc/coco数据集

在data目录下,放置数据集

  • VOC数据集要包含必要的ImageSet、images/JPEGImage以及Annotations文件夹以及相关文件。
  • COCO数据集要包含对应的.json文件。图像的存储位置必须和.json文件当中要保持一致。
    这里补充一个通过VOC imageSet 的数据集分割的train、test、val的test文件来生成coco数据集的代码:

import os
import json
import xml.etree.ElementTree as ET
import numpy as np
import cv2
 
 
def _isArrayLike(obj):
    return hasattr(obj, '__iter__') and hasattr(obj, '__len__')
 
 
class voc2coco:
    def __init__(self, devkit_path=None, year=None):
        self.classes = ('__background__',  
                        'nova')
 
        self.num_classes = len(self.classes)
        #assert 'data716' in devkit_path, 'VOC地址不存在: {}'.format(devkit_path)
        self.data_path = os.path.join(devkit_path, 'VOC' + year)
        self.annotaions_path = os.path.join(self.data_path, 'Annotations')
        self.image_set_path = os.path.join(self.data_path, 'ImageSets')
        self.year = 2019
        self.categories_to_ids_map = self._get_categories_to_ids_map()
        self.categories_msg = self._categories_msg_generator()
 
    def _load_annotation(self, ids=[]):
        ids = ids if _isArrayLike(ids) else [ids]
        image_msg = []
        annotation_msg = []
        annotation_id = 1
        for index in ids:
            filename = '{:0>4}'.format(index)
            json_file = os.path.join(self.data_path, 'Segmentation_json', filename + '.json')
            if os.path.exists(json_file):
                img_file = os.path.join(self.data_path, 'JPEGImages', filename + '.jpg')
                im = cv2.imread(img_file)
                width = im.shape[1]
                height = im.shape[0]
                seg_data = json.load(open(json_file, 'r'))
                assert type(seg_data) == type(dict()), 'annotation file format {} not supported'.format(type(seg_data))
                for shape in seg_data['shapes']:
                    seg_msg = []
                    for point in shape['points']:
                        seg_msg += point
                    one_ann_msg = {"segmentation": [seg_msg],
                                   "area": self._area_computer(shape['points']),
                                   "iscrowd": 0,
                                   "image_id": int(index),
                                   "bbox": self._points_to_mbr(shape['points']),
                                   "category_id": self.categories_to_ids_map[shape['label']],
                                   "id": annotation_id,
                                   "ignore": 0
                                   }
                    annotation_msg.append(one_ann_msg)
                    annotation_id += 1
            else:
                xml_file = os.path.join(self.annotaions_path, filename + '.xml')
                tree = ET.parse(xml_file)
                size = tree.find('size')
                objs = tree.findall('object')
                width = size.find('width').text
                height = size.find('height').text
                for obj in objs:
                    bndbox = obj.find('bndbox')
                    [xmin, xmax, ymin, ymax] \
                        = [int(bndbox.find('xmin').text) - 1, int(bndbox.find('xmax').text),
                           int(bndbox.find('ymin').text) - 1, int(bndbox.find('ymax').text)]
                    if xmin < 0:
                        xmin = 0
                    if ymin < 0:
                        ymin = 0
                    bbox = [xmin, xmax, ymin, ymax]
                    one_ann_msg = {"segmentation": self._bbox_to_mask(bbox),
                                   "area": self._bbox_area_computer(bbox),
                                   "iscrowd": 0,
                                   "image_id": int(index),
                                   "bbox": [xmin, ymin, xmax - xmin, ymax - ymin],
                                   "category_id": self.categories_to_ids_map[obj.find('name').text],
                                   "id": annotation_id,
                                   "ignore": 0
                                   }
                    annotation_msg.append(one_ann_msg)
                    annotation_id += 1
            one_image_msg = {"file_name": filename + ".jpg",
                             "height": int(height),
                             "width": int(width),
                             "id": int(index)
                             }
            image_msg.append(one_image_msg)
        return image_msg, annotation_msg
    def _bbox_to_mask(self, bbox):
        assert len(bbox) == 4, 'Wrong bndbox!'
        mask = [bbox[0], bbox[2], bbox[0], bbox[3], bbox[1], bbox[3], bbox[1], bbox[2]]
        return [mask]
    def _bbox_area_computer(self, bbox):
        width = bbox[1] - bbox[0]
        height = bbox[3] - bbox[2]
        return width * height
    def _save_json_file(self, filename=None, data=None):
        json_path = os.path.join(self.data_path, 'cocoformatJson')
        assert filename is not None, 'lack filename'
        if os.path.exists(json_path) == False:
            os.mkdir(json_path)
        if not filename.endswith('.json'):
            filename += '.json'
        assert type(data) == type(dict()), 'data format {} not supported'.format(type(data))
        with open(os.path.join(json_path, filename), 'w') as f:
            f.write(json.dumps(data))
    def _get_categories_to_ids_map(self):
        return dict(zip(self.classes, range(self.num_classes)))
    def _get_all_indexs(self):
        ids = []
        for root, dirs, files in os.walk(self.annotaions_path, topdown=False):
            for f in files:
                if str(f).endswith('.xml'):
                    id = int(str(f).strip('.xml'))
                    ids.append(id)
        assert ids is not None, 'There is none xml file in {}'.format(self.annotaions_path)
        return ids
    def _get_indexs_by_image_set(self, image_set=None):
        if image_set is None:
            return self._get_all_indexs()
        else:
            image_set_path = os.path.join(self.image_set_path, 'Main', image_set + '.txt')
            assert os.path.exists(image_set_path), 'Path does not exist: {}'.format(image_set_path)
            with open(image_set_path) as f:
                ids = [x.strip() for x in f.readlines()]
            return ids
    def _points_to_mbr(self, points):
        assert _isArrayLike(points), 'Points should be array like!'
        x = [point[0] for point in points]
        y = [point[1] for point in points]
        assert len(x) == len(y), 'Wrong point quantity'
        xmin, xmax, ymin, ymax = min(x), max(x), min(y), max(y)
        height = ymax - ymin
        width = xmax - xmin
        return [xmin, ymin, width, height]
    def _categories_msg_generator(self):
        categories_msg = []
        for category in self.classes:
            if category == '__background__':
                continue
            one_categories_msg = {"supercategory": "none",
                                  "id": self.categories_to_ids_map[category],
                                  "name": category
                                  }
            categories_msg.append(one_categories_msg)
        return categories_msg
    def _area_computer(self, points):
        assert _isArrayLike(points), 'Points should be array like!'
        tmp_contour = []
        for point in points:
            tmp_contour.append([point])
        contour = np.array(tmp_contour, dtype=np.int32)
        area = cv2.contourArea(contour)
        return area
    def voc_to_coco_converter(self):
        img_sets = ['train', 'test', 'val']
        for img_set in img_sets:
            ids = self._get_indexs_by_image_set(img_set)
            img_msg, ann_msg = self._load_annotation(ids)
            result_json = {"images": img_msg,
                           "type": "instances",
                           "annotations": ann_msg,
                           "categories": self.categories_msg}
            self._save_json_file('voc_' + str(self.year) + '_' + img_set, result_json)
def demo():
    # 转换pascal地址是'./VOC2007/VOCdevkit/VOC2007/ImageSets/Main/trainval.txt'
    converter = voc2coco('/home/kyle/Programs/FCOS/FCOS/datasets/voc_cocostyle/', '2007/')
    converter.voc_to_coco_converter()
if __name__ == "__main__":
    demo()

定义

这一部分之前简单的讲过,现在详细的描述一下这个过程。放置好数据集以后,需要在maskrcnn benchwork的fcos核心代码当中添加数据库,以便在训练yaml配置文件当中使用。相当于给数据库起了别名。首先打开{FCOS_PATH}/fcos_core/paths_catalog.py:

	#所有COCO和VOC数据集的名称开头都必须是coco_或者是voc_。
	#COCO数据集格式
	"coco_{dataset-name}_train": {
            "img_dir": "{Path_to_your_images}",
            "ann_file": "{Path_to_your_json_file}.json"
        },

        "coco_{dataset-name}_val": {
            "img_dir": "{Path_to_your_images}",
            "ann_file": "{Path_to_your_json_file}.json"
        },

        "coco_{dataset-name}_test": {
            "img_dir": "{Path_to_your_images}",
            "ann_file": "{Path_to_your_json_file}.json"
        },
      #####################################################
      #VOC数据集格式
        "voc_{dataset-name}_train": {
            "data_dir": "{your_voc_path}/VOC2012",
            "split": "train"
        },
		"voc_{dataset-name}_val": {
            "data_dir": "{your_voc_path}/VOC2012",
            "split": "val"
        },
        "voc_{dataset-name}_test": {
            "data_dir": "{your_voc_path}/VOC2012",
            "split": "test"
        },
    可以定义自己的非主流数据集名字。但是一定要注意会解析数据集前缀来判断数据集的类型,所以第一前缀不能丢失,其次不能混淆。

因为VOC数据集的评估方式略微粗糙,仅有mAP等,如果想获得COCO数据集的详细评估结果,一方面是将数据集通过文章中给出的脚本转化为COCO数据集。还有一种方式就是使用带_cocostyle后缀的数据集别名。
结束之后,卸载之前安装的fcos并重新编译安装。这样fcos_core的代码才会生效。

	pip3 uninstall fcos -y
	python3 setup.py build_ext --inplace
	python3 setup.py build_ext install
   只有这样,才能够在yaml文件当中反问到刚才添加的数据集:
...
DATASETS:
  TRAIN: ("coco_{dataset-name}_train", "coco_{dataset-name}_val")
  TEST: ("coco_{dataset-name}_test",)
...

你可能感兴趣的:(炼丹,python,pytorch,神经网络,深度学习)