最早在天池玩耍的时候接触到了目标检测。当时真的啥都不知道,头铁,一点点的开坑造轮子。后来再看前几名开源的程序,发现有很多库可以使用,切图,目标检测等,比如数据增强、目标检测都有现成的工具箱。所以想着,就先用调库的形式写一个简单的 baseline
,下一个任务直接用现成的代码,省点事,所以写了一个这样的简单的平台。
注意:
本文调库,且只针对 COCO
数据集。
平台很简单很简单,我不想写的太复杂。原因是:复杂的代码容易让使用者头晕,其次,如果真的要改形如损失函数等细节,调库肯定满足不了,这时候就要自己写。但平台应该足够抽象和简单,只是用来观察初步效果,不应该包含这些复杂的东西。对修改封闭,对扩展开放。
因为这次接的任务涉及到了对抗样本,所以后期肯定要改损失函数,到时候在写针对细节的程序。
程序 1 2 3 torch 1.7.1+cu92 torchvision 0.8.2+cu92 pycocotools 2.0.2
https://github.com/muyuuuu/Faster-RCNN-COCO
preprocess
文件夹,这俩代码都是单独执行的
mean-std.py
,计算样本三通道的均值与方差,用于数据标准化
reconstruct-anno.py
,重构 json
,支持多个 batchsize
model
文件夹
data_helper.py
,加载数据
engine.py
,训练与推理的具体过程
eval.py
,推理程序,python eval.py
执行
model.py
,模型
train.py
,训练程序,python train.py
执行
utils.py
,保存、加载模型和写入日志
框架 首先,掏出 torchvision
,找到其实现的 fasterrcnn_resnet50_fpn
模型。查看其源程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def fasterrcnn_resnet50_fpn (pretrained=False , progress=True , num_classes=91 , pretrained_backbone=True , trainable_backbone_layers=None , **kwargs ): trainable_backbone_layers = _validate_trainable_layers( pretrained or pretrained_backbone, trainable_backbone_layers, 5 , 3 ) if pretrained: pretrained_backbone = False backbone = resnet_fpn_backbone('resnet50' , pretrained_backbone, trainable_layers=trainable_backbone_layers) model = FasterRCNN(backbone, num_classes, **kwargs) if pretrained: state_dict = load_state_dict_from_url( model_urls['fasterrcnn_resnet50_fpn_coco' ], progress=progress) model.load_state_dict(state_dict) overwrite_eps(model, 0.0 ) return model
pretrained
取值为 False
的情况下,pretrained_backbone
取值会为 True
,会返回在 ImageNet
上预训练的 backbone;pretrained
取值为 True
的情况下,pretrained_backbone
取值会为 False
,将会返回一个在 COCO train2017
上预训练的模型;而无论如何,backbone
是使用了 FPN
机制的。
num_classes
设置成自己需要检测的类的数量,注意多一个背景类 ;
观察 **kwargs
参数,传给了 FasterRCNN
,所以追踪这个类,发现它能设置很多参数 1 ,比如 anchor
的大小和比例。这个时候对原始数据 EDA
一下,设置合适的参数就可以了;
此时创建模型的程序为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import torchvision.models.detection as tddef get_model (num_class ): anchor_sizes = ((64 , ), (128 , ), (256 , ), (512 , ), (1024 , )) aspect_ratios = ((0.5 , 1.0 , 2.0 ), ) * len (anchor_sizes) rpn_anchor_generator = td.anchor_utils.AnchorGenerator( anchor_sizes, aspect_ratios) detector = td.fasterrcnn_resnet50_fpn( rpn_anchor_generator=rpn_anchor_generator, pretrained=True ) num_classes = num_class in_features = detector.roi_heads.box_predictor.cls_score.in_features detector.roi_heads.box_predictor = td.faster_rcnn.FastRCNNPredictor( in_features, num_classes) return detector
数据制作 之前头铁自己写库解析 anno_json
去制作数据集,后来发现有它人最好的第三方工具,直接 pip install pycocotools
上车。然后去写自己的 dataloader
类就好了。注意,在构造函数中,只初始化一些工具,不要加载数据,否则会内存溢出;将数据加载推迟到 __getitem__
方法中,反正者里面只拿一小部分的数据。此时的核心代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from pycocotools.coco import COCOclass train_data_set (Dataset ): def __init__ (self, image_dir, anno_path ): super ().__init__() self.image_dir = image_dir self.coco = COCO(anno_path) self.ids = list (self.coco.imgs.keys()) self.transform = transforms.Compose([ lambda x: Image.open (x).convert('RGB' ), transforms.Resize((224 , 224 )), transforms.ToTensor(), ]) def __getitem__ (self, idx ): img_id = self.ids[idx] path = self.image_dir + self.coco.loadImgs(img_id)[0 ]['file_name' ] ann_ids = self.coco.getAnnIds(imgIds=img_id) target = self.coco.loadAnns(ann_ids) return self.transform(path), target
但是,还没结束,这里还要改一些 key
,因为提供的 json
文件中,目标区域的名字是 bbox
,但 FasterRCNN
要求的 key
是 boxes
。所以,这部分修改也在数据加载里面完成。
1 2 3 4 5 6 7 8 9 10 11 12 for d in target: v = {} v['boxes' ] = d.pop('bbox' ) d['boxes' ] = v['boxes' ] x = [] for y in v['boxes' ]: x.append(y) x[2 ], x[3 ] = x[0 ] + x[2 ], x[1 ] + x[3 ] d['boxes' ] = torch.tensor(x, dtype=torch.float32) v = {} v['labels' ] = d.pop('category_id' ) d['labels' ] = torch.tensor(v['labels' ] - 1 , dtype=torch.int64)
pycocotools
做出来的 boxes
是一个列表,列表的每个元素是张量;但我们需要的是张量,张量的尺寸是 [1, 4]
,所以有了上面的修改;此次任务不涉及 mask
,所以没有考虑 segementation
的修改;
如果遇到 AssertionError: target boxes must of float type 此类错误,直接打开源代码:
1 2 3 floating_point_types = (torch.float , torch.double, torch.half) assert t["boxes" ].dtype in floating_point_types, 'target boxes must of float type' assert t["labels" ].dtype == torch.int64, 'target labels must of int64 type'
一目了然,这并不是说 boxes 是浮点数,而是说,张量中的元素应该是 torch.float32, torch.float64, torch.float16
类型的,创建张量的时候要加上这个参数;
如果遇到 ValueError: All bounding boxes should have positive height and width. 错误,并不是说盒子里面出现的负数,而是说,FasterRCNN 希望看到 [xmin, ymin, xmax, ymax]
这样的数据,而你传入的是 [xmin, ymin, width, height]
这样的数据。但在后面 torchvision
中修复了这个问题 2 。
如果遇到 sampled_pos_inds_subset = torch.where(labels > 0)[0] CUDA error: device-side assert triggered 这样的错误,也不要慌。有两种解决方案,选一个就好。
这里出错的原因很简单,数据提供的类是从 1 开始编号的,假设是 label = [1,2,3]
;在预测时,正确的标签是 3,但程序中 label[3]
会出错,所以,将标签改为 [0,1,2]
就可以了。
传入训练模型时,增加背景类。这里看自己,如果需要背景类,就第二种方案;如果不需要背景类,就第一种方案。
而如果要追求多个 batchsize
,只能重构 json
,此部分程序在 github
中给出。
训练与推理 首先来看官方提供训练的例子(注意这个例子在某些版本上不一定能跑通):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 model = torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained=True ) images, boxes = torch.rand(4 , 3 , 600 , 1200 ), torch.rand(4 , 11 , 4 ) labels = torch.randint(1 , 91 , (4 , 11 )) images = list (image for image in images) targets = [] for i in range (len (images)): d = {} d['boxes' ] = boxes[i] d['labels' ] = labels[i] targets.append(d) output = model(images, targets) model.eval () x = [torch.rand(3 , 300 , 400 ), torch.rand(3 , 500 , 400 )] predictions = model(x)
所以,训练的时候,把 image 和 target 转成列表并输入就可以了。注意,列表中的每个元素的类型必须和模型参数的类型一致,别忘了 to(device)
。我把最终训练和推理的程序放到 engine
程序中了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 def train_fn (train_dataloader, detector, optimizer, device, epoch, scheduler ): detector.train() loss_value = 0 cnt = 0 for images, target in tqdm(train_dataloader): cnt += 1 images = list (image.to(device) for image in images) targets = [] for l, b in zip (target['labels' ], target['boxes' ]): d = {} d['labels' ] = l.view(-1 ).to(device) d['boxes' ] = b.view(-1 , 4 ).to(device) targets.append(d) loss_dict = detector(images, targets) losses = sum (loss for loss in loss_dict.values()) loss_value = losses.item() optimizer.zero_grad() losses.backward() optimizer.step() scheduler.step() if cnt % 1000 == 999 : cnt = 0 utils.save_checkpoint_state("model_tmp.pth" , epoch, detector, optimizer, scheduler) return loss_value def predict (val_dataloader, detector, device ): results = [] for images, image_names in tqdm(val_dataloader): images = list (image.to(device) for image in images) model_time = time.time() outputs = detector(images) model_time = time.time() - model_time for i, image in enumerate (images): boxes = (outputs[i]["boxes" ].data.cpu().numpy().tolist()) scores = outputs[i]["scores" ].data.cpu().numpy() labels = outputs[i]["labels" ].data.cpu().numpy() image_id = image_names[i] for b, s, l in zip (boxes, scores, labels): if s > 0.5 : result = { "image_id" : image_id, "boxes" : b, "scores" : s.astype(float ), "labels" : l.astype(float ), } results.append(result) return results
预测无输出 在推理时,网络对所有图片的输出都是:
1 2 3 [{'boxes' : tensor([], size=(0 , 4 )), 'labels' : tensor([], dtype=torch.int64), 'scores' : tensor([])}]
这个 bug
当时我也佷头疼,翻遍了全网,总结下可能导致此类错误的原因吧:
类别数目没有加一,也就是没有考虑背景类。这个解决方案在上面已经给出。传入训练模型时,增加一个背景类
pretrained=False
。当时是我考虑少了,以为 pretrained=False
的情况下,毕竟模型的 backbone 是经过预训练的,以为能应付简单的目标检测,后来发现是我草率了;pretrained=True
,不仅 backbone,整个模型都是预训练的,这样才能检测到目标。
references