0%

目标检测篇:目标检测快速训练与推理框架

最早在天池玩耍的时候接触到了目标检测。当时真的啥都不知道,头铁,一点点的开坑造轮子。后来再看前几名开源的程序,发现有很多库可以使用,切图,目标检测等,比如数据增强、目标检测都有现成的工具箱。所以想着,就先用调库的形式写一个简单的 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:
# no need to download the backbone if pretrained is set
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 td

def 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)
# https://github.com/pytorch/vision/blob/master/torchvision/models/detection/faster_rcnn.py

# faster rcnn,网络会再次对 图像数据进行重定义尺寸
# https://github.com/pytorch/vision/blob/c2ab0c59f42babf9ad01aa616cd8a901daac86dd/torchvision/models/detection/transform.py#L64
detector = td.fasterrcnn_resnet50_fpn(
rpn_anchor_generator=rpn_anchor_generator, pretrained=True)
num_classes = num_class
# ROI head 是 backbone 后,预测盒子和类别的位置
# box_predictor 是 FastRCNNPredictor 类,cls_score 是类别分类器
# in_features 是模型的输入特征
# https://github.com/pytorch/vision/blob/5339e63148304ce32fd1cbd1e8bb74ea79458691/torchvision/models/detection/faster_rcnn.py#L263-L276
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 COCO
class train_data_set(Dataset):
def __init__(self, image_dir, anno_path):
super().__init__()
self.image_dir = image_dir
# COCO api class that loads COCO annotation file and prepare data structures
self.coco = COCO(anno_path)
# 获取 image 的 id,字典转为 list
self.ids = list(self.coco.imgs.keys())
self.transform = transforms.Compose([
lambda x: Image.open(x).convert('RGB'),
transforms.Resize((224, 224)),
transforms.ToTensor(),
# transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
])

# 数据集很大时,要在 getitem 中获取
def __getitem__(self, idx):
# 某个图片的 id
img_id = self.ids[idx]
# 图片路径
path = self.image_dir + self.coco.loadImgs(img_id)[0]['file_name']
# 获取图片的 annotations 的 id
ann_ids = self.coco.getAnnIds(imgIds=img_id)
# 根据 annotations 的 id 获取 annotions
target = self.coco.loadAnns(ann_ids)
return self.transform(path), target

但是,还没结束,这里还要改一些 key,因为提供的 json 文件中,目标区域的名字是 bbox,但 FasterRCNN 要求的 keyboxes。所以,这部分修改也在数据加载里面完成。

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)
# For inference
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)
# it's key:value for t in targets.items
# This is the format the fasterrcnn expects for targets
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


  1. 1.https://github.com/pytorch/vision/blob/730c5e1eab130e2900c8e839ea08fa11f024516f/torchvision/models/detection/faster_rcnn.py#L23
  2. 2.https://github.com/pytorch/vision/issues/2740
感谢上学期间打赏我的朋友们。赛博乞讨:我,秦始皇,打钱。

欢迎订阅我的文章