目录
目录
一、背景介绍
二、数据集制作
三、环境搭建
四、修改源码以训练自己的数据
五、训练
六、测试
七、可视化loss、acc
八、梯度爆炸
九、后记
场景需要训练一个分割模型,参考这个场景大多数人的选择,决定用mmdet的maskrcnn,再进阶到cascade maskrcnn。
实例分割Mask rcnn = 检测Faster rcnn + 语义分割FCN+ ROIAlign
在Faster rcnn的cls+box分支基础上,增加了分割mask分支。ROIAlign替代ROIPool解决2次量化的影响。
在mmdet中主要有以下几个方面的模型可供使用,具体算法列表查询readme即可
检测 Object Detection
实例分割Instance Segmentation
全景分割 Panoptic Segmentation
对比学习 Contrastive Learning
蒸馏 Distillation
感受野搜索 Receptive Field Search
mmdet建议将数据转换成 COCO 格式,不要自己搞一套标注方式改解析标签的源码。
2.1 labelme多点标注
2.2 labelme2coco.py
将labelme的标注json文件直接转为coco的格式,当前只需要训练集和验证集,分别生成 instances_val2017.json、instances_train2017.json,放置在annotations下;train2017、val2017文件夹下是jpg图片。 数据集层级关系如下
--coco
----annotations
----train2017
----val2017
----test2017
labelme2coco.py脚本如下:
# -*- coding:utf-8 -*-
import argparse
import json
import matplotlib.pyplot as plt
import skimage.io as io
import cv2
from labelme import utils
import numpy as np
import glob
import PIL.Image
from shapely.geometry import Polygon
class labelme2coco(object):
def __init__(self, labelme_json=[], save_json_path='./new.json'):
'''
:param labelme_json: 所有labelme的json文件路径组成的列表
:param save_json_path: json保存位置
'''
self.labelme_json = labelme_json
self.save_json_path = save_json_path
self.images = []
self.categories = []
self.annotations = []
# self.data_coco = {}
self.label = []
self.annID = 1
self.height = 0
self.width = 0
# 初始化时就调用了save方法
self.save_json()
def data_transfer(self):
for num, json_file in enumerate(self.labelme_json):
with open(json_file, 'r') as fp:
data = json.load(fp) # 加载json文件
self.images.append(self.image(data, num)) # 获取图像宽高索引文件名等信息
if len(data['shapes'])==0:
print('shapes==0', json_file)
for shapes in data['shapes']:
# label=shapes['label'].split('_')
# 标签处理
label = shapes['label']
# ################################# 费用清单标注的时候区分了有线表和无线表、少线表
label = 't' # 统一处理为t
# print(json_file, label)
if label not in self.label:
self.categories.append(self.categorie(label))
self.label.append(label)
# 标注处理
points = shapes['points']
self.annotations.append(self.annotation(points, label, num, [data['imageWidth'], data['imageHeight']]))
self.annID += 1
# 当前所有类别
print(self.label)
def image(self, data, num):
"""
获取图像宽高索引文件名等信息
:param data: labelme的信息
:param num: 索引编号
:return:
"""
image = {}
# base64 转cv2的array格式
img = utils.img_b64_to_arr(data['imageData']) # 解析原图片数据
# img=io.imread(data['imagePath']) # 通过图片路径打开图片
# img = cv2.imread(data['imagePath'], 0)
height, width = img.shape[:2]
img = None
image['height'] = height
image['width'] = width
image['id'] = num + 1
image['file_name'] = data['imagePath'].split('/')[-1].split('\\')[-1]
# print(data['imagePath'], image['file_name'])
self.height = height
self.width = width
return image
def categorie(self, label):
categorie = {}
categorie['supercategory'] = label
categorie['id'] = len(self.label) + 1 # 0 默认为背景
categorie['name'] = label
return categorie
def annotation(self, points, label, num, img_size):
# print('points=',points)
# print('label=',label)
# print('num=',num)
"""
annotation['segmentation']= [[92.85714285714286, 1347.6190476190477, 1628.5714285714287, 1357.142857142857, 1629.7619047619048, 1576.1904761904761, 92.85714285714286, 1559.5238095238096]]
poly= POLYGON ((92.85714285714286 1347.619047619048, 1628.571428571429 1357.142857142857, 1629.761904761905 1576.190476190476, 92.85714285714286 1559.52380952381, 92.85714285714286 1347.619047619048))
area_= 331030.328798
返回格式:
segmentation 展品的点坐标
area 面积
iscrowd 固定值0
image_id 从1开始的图像id
bbox 边框,类似x0,y0,h,w,这种格式,具体排序没看
category_id 类别索引
id 第几个实例,不区分图像和类别,就是数据集里第几个实例
"""
annotation = {}
# 获取原始图片尺寸,对边缘点进行处理,cascade mask rcnn 训练时loss出现了nan
# [h,w] = img_size
tmp = [list(np.asarray(points).flatten())]
# print(tmp)
# print(img_size)
# 把坐标限制在【1,max-2之间】
# tmp1 = [[float(max(1, min(i, img_size[int(ind%2)]-2))) for ind,i in enumerate(tmp[0])]]
# if tmp !=tmp1:
# print(tmp)
# print(tmp1)
# print(img_size)
# print('........')
# tmp = tmp1
# 检查孤立点
# if len(tmp[0]) < 8:
# print(tmp)
# print('........')
# annotation['segmentation'] = [list(np.asarray(points).flatten())]
annotation['segmentation'] = tmp
poly = Polygon(points)
area_ = round(poly.area, 6)
annotation['area'] = area_
annotation['iscrowd'] = 0
annotation['image_id'] = num + 1
# annotation['bbox'] = str(self.getbbox(points)) # 使用list保存json文件时报错(不知道为什么)
# list(map(int,a[1:-1].split(','))) a=annotation['bbox'] 使用该方式转成list
annotation['bbox'] = list(map(float, self.getbbox(points))) # 边界框
# 人工标注时,不小心点了一下?无效的小目标框
if annotation['bbox'][2]<10 or annotation['bbox'][3]<10:
print('人工标注时,不小心点了一下?无效的小目标框',annotation['bbox'])
annotation['category_id'] = self.getcatid(label) # 类别索引
annotation['id'] = self.annID
return annotation
def getcatid(self, label):
for categorie in self.categories:
if label == categorie['name']:
return categorie['id']
return -1
def getbbox(self, points):
"""
由点画mask,再由mask反推其边框,不明白为啥不直接用min max的点值
:param points:
:return:
"""
# img = np.zeros([self.height,self.width],np.uint8)
# cv2.polylines(img, [np.asarray(points)], True, 1, lineType=cv2.LINE_AA) # 画边界线
# cv2.fillPoly(img, [np.asarray(points)], 1) # 画多边形 内部像素值为1
polygons = points
mask = self.polygons_to_mask([self.height, self.width], polygons)
return self.mask2box(mask)
def mask2box(self, mask):
'''从mask反算出其边框
mask:[h,w] 0、1组成的图片
1对应对象,只需计算1对应的行列号(左上角行列号,右下角行列号,就可以算出其边框)
'''
# np.where(mask==1)
index = np.argwhere(mask == 1)
rows = index[:, 0]
clos = index[:, 1]
# 解析左上角行列号
left_top_r = np.min(rows) # y
left_top_c = np.min(clos) # x
# 解析右下角行列号
right_bottom_r = np.max(rows)
right_bottom_c = np.max(clos)
# return [(left_top_r,left_top_c),(right_bottom_r,right_bottom_c)]
# return [(left_top_c, left_top_r), (right_bottom_c, right_bottom_r)]
# return [left_top_c, left_top_r, right_bottom_c, right_bottom_r]# [x1,y1,x2,y2]
return [left_top_c, left_top_r, right_bottom_c - left_top_c,
right_bottom_r - left_top_r] # [x1,y1,w,h] 对应COCO的bbox格式
def polygons_to_mask(self, img_shape, polygons):
mask = np.zeros(img_shape, dtype=np.uint8)
mask = PIL.Image.fromarray(mask)
xy = list(map(tuple, polygons))
PIL.ImageDraw.Draw(mask).polygon(xy=xy, outline=1, fill=1)
mask = np.array(mask, dtype=bool)
return mask
def data2coco(self):
data_coco = {}
data_coco['images'] = self.images
data_coco['categories'] = self.categories
data_coco['annotations'] = self.annotations
return data_coco
def save_json(self):
# 读取及处理
self.data_transfer()
# 生成一个大json
self.data_coco = self.data2coco()
"""
print(self.data_coco)
print(self.data_coco['annotations'][0].keys())
{'images': [{'height': 2304, 'width': 1728, 'id': 1, 'file_name': '..\\pic\\1562149745231b6cd7c4ee5.jpg'}], 'categories': [{'supercategory': 't', 'id': 1, 'name': 't'}], 'annotations': [{'segmentation': [[47.61904761904762, 1000.0, 39.285714285714285, 807.1428571428572, 41.66666666666667, 380.95238095238096, 552.3809523809524, 352.3809523809524, 996.4285714285714, 345.23809523809524, 1652.3809523809525, 348.8095238095238, 1634.5238095238096, 1226.1904761904761, 79.76190476190476, 1227.3809523809525]], 'area': 1393553.713152, 'iscrowd': 0, 'image_id': 1, 'bbox': [39.0, 345.0, 1613.0, 882.0], 'category_id': 1, 'id': 1}, {'segmentation': [[80.95238095238095, 1226.1904761904761, 1636.904761904762, 1227.3809523809525, 1627.3809523809525, 1355.952380952381, 92.85714285714286, 1347.6190476190477]], 'area': 193149.092971, 'iscrowd': 0, 'image_id': 1, 'bbox': [80.0, 1226.0, 1556.0, 129.0], 'category_id': 1, 'id': 2}, {'segmentation': [[92.85714285714286, 1347.6190476190477, 1628.5714285714287, 1357.142857142857, 1629.7619047619048, 1576.1904761904761, 92.85714285714286, 1559.5238095238096]], 'area': 331030.328798, 'iscrowd': 0, 'image_id': 1, 'bbox': [92.0, 1347.0, 1537.0, 229.0], 'category_id': 1, 'id': 3}]}
dict_keys(['segmentation', 'area', 'iscrowd', 'image_id', 'bbox', 'category_id', 'id'])
"""
# 保存json文件
json.dump(self.data_coco, open(self.save_json_path, 'w'), indent=4) # indent=4 更加美观显示
labelme_json = glob.glob('./Documents/数据集/labels/*json')
# labelme_json=['./Documents/useful_code/mask_rcnn_project/1562149745231b6cd7c4ee5.json']
# 测试
labelme2coco(labelme_json[:int(len(labelme_json)*0.1)], 'instances_val2017.json')
# 验证
# labelme2coco(labelme_json[int(len(labelme_json)*0.1):int(len(labelme_json)*0.2)], 'instances_val2017.json')
# 训练
labelme2coco(labelme_json[int(len(labelme_json)*0.1):], 'instances_train2017.json')
需要pytorch1.5以上环境,mmdet的教程维护得很好。
3.1查看CUDA及pyTorch版本
import torch
print(torch.__version__) # 1.7.0a0+8deb4fe
print(torch.version.cuda) # 11.0
print(torch.backends.cudnn.version()) # 8002
3.2安装完整版支持CUDA的mmcv-full
mmcv-full 的版本需要依赖cuda、pytorch、MMDetection。mmcv-full实际承担着训练、数据处理、注册机制的作用,是mmdet的灵魂伴侣,不是打辅助而已。
需要下载正确版本的mmcv-full,版本间关系可参考:mmdetection/get_started.md at master · open-mmlab/mmdetection · GitHub
mmcv的版本有多种方式,如:
a、pip install时指定对应的依赖“pip install mmcv-full==latest+torch1.1.0+cu901 -f https://download.openmmlab.com/mmcv/dist/index.html”。
b、下载后源码编译“ pip install -e . ”
c、下载正确的whl后安装(我采用的方法)
从https://download.openmmlab.com/mmcv/dist/cu110/torch1.7.0/index.html 下载
或所有whl查询 https://download.openmmlab.com/mmcv/dist/index.html
pip3 install mmcv_full-1.7.0-cp310-cp310-manylinux1_x86_64.whl
3.3 安装其他依赖
pip install pycocotools
3.4 安装mmdet
git clone https://github.com/open-mmlab/mmdetection.git
cd mmdetection-master_2022_11_11
pip install -r requirements/build.txt
pip install -v -e .
3.5 验证
from mmcv.ops import RoIPool
from mmdet.apis import init_detector, inference_detector
下面先以mask rcnn为例介绍怎么改代码,使其跑起来。如果是其他模型,大差不差,找一下指向的4个配置文件即可。
先看整体的配置文件
configs/mask_rcnn/mask_rcnn_r50_fpn_1x_coco.py文件内容如下,可见一次训练主要关联了4个配置文件,分别指向模型、数据、超参数的配置,可以把configs目录当成一个检索仓库。
_base_ = [
'../_base_/models/mask_rcnn_r50_fpn.py', # 模型结构的具体配置,注意num_class
'../_base_/datasets/coco_instance.py', #数据集配置
'../_base_/schedules/schedule_1x.py', '../_base_/default_runtime.py’ # 训练策略配置,主要是学习率和优化器的配置
]
4.1、修改类别,注意class后要留逗号
vim mmdetection/mmdet/datasets/coco.py ,将CLASSES替换成自己的classes
vim mmdetection/mmdet/core/evaluation/class_names.py ,将coco_classes的return替换成自己的classes
vim mmdetection/configs/_base_/models/mask_rcnn_r50_fpn.py 将num_classes改成自己的类别数量
4.2 修改图像尺寸及数据集路径
vim mmdetection/configs/_base_/datasets/coco_instance.py ,注意里面的data_root,train、val、test的ann_file、img_prefix,img_scale
4.3 修改学习率lr、epoch数量max_epochs、学习率衰减方式 等训练参数
卡只有1、2张的情况下,注意改小学习率
vim mmdetection/configs/_base_/schedules/schedules_1x.py 修改训练参数
# optimizer
# 8张GPU ==> 0.02,4张GPU就0.01,两张就0.005
optimizer = dict(type='SGD', lr=0.002, momentum=0.9, weight_decay=0.0001) # lr稳定后是0.02
optimizer_config = dict(grad_clip=None)
# learning policy
lr_config = dict(
policy='step', # 优化策略
warmup='linear', # 线性增长学习率,如0.0002、0.0004
warmup_iters=500, # 0~500 个batch size中逐渐增大学习率,即500iter后为指定的学习率0.02
warmup_ratio=0.001, # 有的博客说是起始学习率
step=[8, 11]) # 在第8和11个epoch后降低学习率,可以按照max_epochs去调整衰减策略。在日志中epoch从1开始,因此第9个epoch降低为原来10%,第12个epoch降低为原来1%。注意这里不要改成len为3的列表,会导致学习率降低至0后面白训练。
runner = dict(type='EpochBasedRunner', max_epochs=12) # 总共训练12个epochs
4.4 修改一些训练参数
vim mmdetection/configs/_base_/default_runtime.py
这里根据资源情况,改改base_batch_size就可以了,别的暂时不用改
checkpoint_config = dict(interval=1) # 保存权重的间隔
# yapf:disable
log_config = dict(
interval=50, # 每多少batch打印一下loss
hooks=[
dict(type='TextLoggerHook'),
# dict(type='TensorboardLoggerHook')
])
# yapf:enable
custom_hooks = [dict(type='NumClassCheckHook')]
dist_params = dict(backend='nccl')
log_level = 'INFO'
load_from = None # 加载权重路径
resume_from = None
workflow = [('train', 1)]
# disable opencv multithreading to avoid system being overloaded
opencv_num_threads = 0
# set multi-process start method as `fork` to speed up the training
mp_start_method = 'fork'
# Default setting for scaling LR automatically
# - `enable` means enable scaling LR automatically
# or not by default.
# - `base_batch_size` = (8 GPUs) x (2 samples per GPU).
auto_scale_lr = dict(enable=False, base_batch_size=16) # 每个batch_size的图片数量
python tools/train.py configs/mask_rcnn/mask_rcnn_r50_fpn_1x_coco.py --auto-resume
执行上面的train命令就会提示如何下载预训练模型,并放到指定路径,如Downloading: "https://download.pytorch.org/models/resnet50-0676ba61.pth" to /root/.cache/torch/hub/checkpoints/resnet50-0676ba61.pth
--auto-resume 可以从断点继续训练,也可以用--resume-from从指定检查点继续
python tools/test.py configs/mask_rcnn/mask_rcnn_r50_fpn_1x_coco.py work_dirs/mask_rcnn_r50_fpn_1x_coco/latest.pth --show-dir work_dirs/result --eval segm
注意: 测试时要用配对的配置文件和模型文件,我不小心用mask rcnn的配置+cascade mask rcnn的模型,出来的推理结果非常差还都是孔洞
--show-dir 可视化结果
--show-score-thr 低于阈值的box不进行可视化
python tools/analysis_tools/analyze_logs.py plot_curve work_dirs/mask_rcnn_r50_fpn_1x_coco/20221124_125015.log.json work_dirs/mask_rcnn_r50_fpn_1x_coco/20221115_114758.log.json --keys acc --out loss.pdf
Mmdet的训练数据在work_dirs/***/***.log.json 文件中,可视化的脚本在tools/analysis_tools/analyze_logs.py。可以将多个json的信息进行拼接。keys可以通过看json文件里有什么决定
keys包括:
loss_rpn_cls 区域候选网络region proposal network分类损失(前景+背景)
loss_rpn_bbox rpn网络目标框回归损失
loss_cls 属于mask_rcnn heads分类损失(真实类别+背景)
acc
loss_bbox 属于mask_rcnn heads回归损失
loss_mask 属于mask_rcnn heads分割损失
loss
time
同样的标注数据,先训练了mask rcnn,没异常。再训练了cascade mask rcnn,一开始损失下降,后来突然nan了,也就是发生了梯度爆炸
先说结论:这次损失变nan是数据标注问题+学习率太大双重问题引起的,导致的奇怪现象是mask rcnn不报错但cascade mask rcnn报错
以下记录解决过程,正确的操作加粗,其他未解决问题仅是记录思路。
-> 中断训练后从头开始
失败,还是很早就nan了
-> 调小学习率,vim mmdetection/configs/_base_/schedules/schedules_1x.py
vim mmdetection/configs/_base_/schedules/schedules_1x.py 修改训练参数 缩小10倍
optimizer = dict(type='SGD', lr=0.002, momentum=0.9, weight_decay=0.0001)
调小学习率是对的,但数据有问题,所以失败了
->怀疑是标注问题,标注的点有边界点,触发了边缘问题,改一下labelme2coco.py,基于图像宽高,把坐标限制在【1,max-2之间】
失败
->人工标注时,不小心点了一下?无效的小目标框
失败
->查询是哪张图引起的nan质变,源码是每50张打印一次loss,很不好找问题图片,按照如下修改,一张就打印一次loss,找loss哪里开始异常了。
vim /usr/local/lib/python3.7/dist-packages/mmcv/runner/epoch_based_runner.py 在def train 中 打印每张图的文件名
for i, data_batch in enumerate(self.data_loader):
print('/usr/local/lib/python3.7/dist-packages/mmcv/runner/epoch_based_runner.py data_batch=',data_batch['img_metas']._data[0][0]['filename'])
这里的图像数据img_metas是一个DataContainer,定义在mmcv/parallel/data_container.py中。可通过._data获取值
vim configs/_base_/default_runtime.py 将interval改成1,每张图打印一下
一开始找到了一张问题标注图,修改标注。然后在300张左右又出现了nan,看来看去标注没啥问题,此时lr还在warm up增长阶段,于是将lr减小10倍试试
-> 调小学习率,vim mmdetection/configs/_base_/schedules/schedules_1x.py
vim mmdetection/configs/_base_/schedules/schedules_1x.py 修改训练参数 缩小10倍
optimizer = dict(type='SGD', lr=0.002, momentum=0.9, weight_decay=0.0001)
默认参数训练出来的Cascade Mask R-CNN模型在部分图片上表现不佳,出现不恰当重合或各种奇奇怪怪的边缘。对此,尝试了调整学习率、调整输入图像尺寸、重新设置anchor,都没有得到显著改善。其中,调整anchor后总损失从0.05降低到0.04,无望降低到0.03,效果也不咋的,经常漏或者大面积重合,搞不出来还是得及时换模型。
loss及map的变化如下
模型 | 优化点 | loss | segm_mAP_l |
maskrcnn | / | "loss_mask": 0.06503, "loss": 0.17677, |
0.834 |
cascade maskrcnn | / | "s2.loss_mask": 0.00476, "loss": 0.09647 |
0.847 |
cascade maskrcnn | 调整输入尺寸 img_scale=(1920, 1280) |
"s2.loss_mask": 0.00462, "loss": 0.05722, |
0.873 |
cascade maskrcnn | 调整 anchor 调整输入尺寸 img_scale=(1920, 1280) |
||
推荐阅读
如何搭建环境及运行代码
ubuntu 18.04 mmdetection 训练mask_rcnn_萌新用户2.0的博客-CSDN博客_mmdetection训练mask
源码主流程梳理
mmdetection源码阅读笔记:概览 - 知乎
Faster rcnn
Faster RCNN 学习笔记 - 勇者归来 - 博客园
Faster-rcnn详解_技术挖掘者的博客-CSDN博客_faster r-cnn
Mask rcnn
Mask R-CNN详解_技术挖掘者的博客-CSDN博客_mask r-cnn
mmdet 官方教程
Welcome to MMDetection’s documentation! — MMDetection 2.26.0 文档
GitHub地址
GitHub - open-mmlab/mmdetection: OpenMMLab Detection Toolbox and Benchmark.