多尺度目标检测
在特征图(fmap)上生成锚框(anchors),每个单位(像素)作为锚框的中心。
特征图的宽度和高度fmap_w
和fmap_h
,将会生成大小为s
(假设列表s
的长度为1)且宽高比(ratios
)不同的锚框。
1 2 3 4 5 6 7 8 9
| def display_anchors(fmap_w, fmap_h, s): d2l.set_figsize() fmap = torch.zeros((1, 10, fmap_h, fmap_w)) anchors = d2l.multibox_prior(fmap, sizes=s, ratios=[1, 2, 0.5]) bbox_scale = torch.tensor((w, h, w, h)) d2l.show_bboxes(d2l.plt.imshow(img).axes, anchors[0] * bbox_scale) display_anchors(fmap_w=4, fmap_h=4, s=[0.15])
|
可以看到,下图锚框分布是4*4。

1
| display_anchors(fmap_w=2, fmap_h=2, s=[0.4])
|

SSD代码
模型
类别预测层
预测一个锚框的类别,设目标类别的数量为q。
输出通道数为a(q+1),其中索引为i(q+1)+j(0≤j≤q)的通道代表了索引为i的锚框有关类别索引为j的预测。
1 2 3 4 5 6 7 8 9 10 11 12
| %matplotlib inline import torch import torchvision from torch import nn from torch.nn import functional as F from d2l import torch as d2l
def cls_predictor(num_inputs, num_anchors, num_classes): return nn.Conv2d(num_inputs, num_anchors * (num_classes + 1), kernel_size=3, padding=1)
|
边界框预测层
边界框预测层的设计与类别预测层的设计类似。 唯一不同的是,这里需要为每个锚框预测4个偏移量,而不是q+1个类别
1 2 3 4
| def bbox_predictor(num_inputs, num_anchors): return nn.Conv2d(num_inputs, num_anchors * 4, kernel_size=3, padding=1)
|
连结多尺度的预测
SSD使用多尺度特征图来生成锚框并预测其类别和偏移量。不同尺度下预测输出的形状可能会有所不同。
在以下示例中,我们为同一个小批量构建两个不同比例(Y1
和Y2
)的特征图,其中Y2
的高度和宽度是Y1
的一半。 以类别预测为例,假设Y1
和Y2
的每个单元分别生成了5个和3个锚框。 进一步假设目标类别的数量为10,对于特征图Y1
和Y2
,类别预测输出中的通道数分别为5×(10+1)=55和3×(10+1)=33,其中任一输出的形状是(批量大小,通道数,高度,宽度)。
1 2 3 4 5 6 7 8
| def forward(x, block): return block(x)
Y1 = forward(torch.zeros((2, 8, 20, 20)), cls_predictor(8, 5, 10)) Y2 = forward(torch.zeros((2, 16, 10, 10)), cls_predictor(16, 3, 10)) Y1.shape, Y2.shape
(torch.Size([2, 55, 20, 20]), torch.Size([2, 33, 10, 10]))
|
正如我们所看到的,除了批量大小这一维度外,其他三个维度都具有不同的尺寸。 为了将这两个预测输出链接起来以提高计算效率,我们将把这些张量转换为更一致的格式。
通道维包含中心相同的锚框的预测结果。我们首先将通道维移到最后一维。 因为不同尺度下批量大小仍保持不变,我们可以将预测结果转成二维的(批量大小,高×宽×通道数)的格式,以方便之后在维度1上的连结。
1 2 3 4 5
| def flatten_pred(pred): return torch.flatten(pred.permute(0, 2, 3, 1), start_dim=1)
def concat_preds(preds): return torch.cat([flatten_pred(p) for p in preds], dim=1)
|
这样一来,尽管Y1
和Y2
在通道数、高度和宽度方面具有不同的大小,我们仍然可以在同一个小批量的两个不同尺度上连接这两个预测输出。
高和宽减半块
1 2 3 4 5 6 7 8 9 10
| def down_sample_blk(in_channels, out_channels): blk = [] for _ in range(2): blk.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1)) blk.append(nn.BatchNorm2d(out_channels)) blk.append(nn.ReLU()) in_channels = out_channels blk.append(nn.MaxPool2d(2)) return nn.Sequential(*blk)
|
1
| forward(torch.zeros((2, 3, 20, 20)), down_sample_blk(3, 10)).shape
|
基本网络块
1 2 3 4 5 6 7 8
| def base_net(): blk = [] num_filters = [3, 16, 32, 64] for i in range(len(num_filters) - 1): blk.append(down_sample_blk(num_filters[i], num_filters[i+1])) return nn.Sequential(*blk)
forward(torch.zeros((2, 3, 256, 256)), base_net()).shape
|
完整的模型
每个blk后面都检测一次
1 2 3 4 5 6 7 8 9 10
| def get_blk(i): if i == 0: blk = base_net() elif i == 1: blk = down_sample_blk(64, 128) elif i == 4: blk = nn.AdaptiveMaxPool2d((1,1)) else: blk = down_sample_blk(128, 128) return blk
|
为每个块定义前向传播
1 2 3 4 5 6
| def blk_forward(X, blk, size, ratio, cls_predictor, bbox_predictor): Y = blk(X) anchors = d2l.multibox_prior(Y, sizes=size, ratios=ratio) cls_preds = cls_predictor(Y) bbox_preds = bbox_predictor(Y) return (Y, anchors, cls_preds, bbox_preds)
|
超参数
1 2 3 4
| sizes = [[0.2, 0.272], [0.37, 0.447], [0.54, 0.619], [0.71, 0.79], [0.88, 0.961]] ratios = [[1, 2, 0.5]] * 5 num_anchors = len(sizes[0]) + len(ratios[0]) - 1
|
完整网络模型
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
| class TinySSD(nn.Module): def __init__(self, num_classes, **kwargs): super(TinySSD, self).__init__(**kwargs) self.num_classes = num_classes idx_to_in_channels = [64, 128, 128, 128, 128] for i in range(5): setattr(self, f'blk_{i}', get_blk(i)) setattr(self, f'cls_{i}', cls_predictor(idx_to_in_channels[i], num_anchors, num_classes)) setattr(self, f'bbox_{i}', bbox_predictor(idx_to_in_channels[i], num_anchors))
def forward(self, X): anchors, cls_preds, bbox_preds = [None] * 5, [None] * 5, [None] * 5 for i in range(5): X, anchors[i], cls_preds[i], bbox_preds[i] = blk_forward( X, getattr(self, f'blk_{i}'), sizes[i], ratios[i], getattr(self, f'cls_{i}'), getattr(self, f'bbox_{i}')) anchors = torch.cat(anchors, dim=1) cls_preds = concat_preds(cls_preds) cls_preds = cls_preds.reshape( cls_preds.shape[0], -1, self.num_classes + 1) bbox_preds = concat_preds(bbox_preds) return anchors, cls_preds, bbox_preds
|
创建一个模型实例,然后使用它对一个256×256像素的小批量图像X
执行前向传播。
5444个锚框
1 2 3 4 5 6 7 8 9 10 11
| net = TinySSD(num_classes=1) X = torch.zeros((32, 3, 256, 256)) anchors, cls_preds, bbox_preds = net(X)
print('output anchors:', anchors.shape) print('output class preds:', cls_preds.shape) print('output bbox preds:', bbox_preds.shape)
output anchors: torch.Size([1, 5444, 4]) output class preds: torch.Size([32, 5444, 2]) output bbox preds: torch.Size([32, 21776])
|
训练
读取数据集和初始化
1 2 3 4 5
| batch_size = 32 train_iter, _ = d2l.load_data_bananas(batch_size)
device, net = d2l.try_gpu(), TinySSD(num_classes=1) trainer = torch.optim.SGD(net.parameters(), lr=0.2, weight_decay=5e-4)
|
定义损失函数和评价函数
类的损失:交叉熵损失
边界框损失:L1 loss
1 2 3 4 5 6 7 8 9 10
| cls_loss = nn.CrossEntropyLoss(reduction='none') bbox_loss = nn.L1Loss(reduction='none')
def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks): batch_size, num_classes = cls_preds.shape[0], cls_preds.shape[2] cls = cls_loss(cls_preds.reshape(-1, num_classes), cls_labels.reshape(-1)).reshape(batch_size, -1).mean(dim=1) bbox = bbox_loss(bbox_preds * bbox_masks, bbox_labels * bbox_masks).mean(dim=1) return cls + bbox
|
1 2 3 4 5 6 7
| def cls_eval(cls_preds, cls_labels): return float((cls_preds.argmax(dim=-1).type( cls_labels.dtype) == cls_labels).sum())
def bbox_eval(bbox_preds, bbox_labels, bbox_masks): return float((torch.abs((bbox_labels - bbox_preds) * bbox_masks)).sum())
|