Task 06 数据增强;模型微调;目标检测基础 学习笔记

Task 06 数据增强;模型微调;目标检测基础 学习笔记

数据增强

图像增广
在5.6节(深度卷积神经网络)里我们提到过,大规模数据集是成功应用深度神经网络的前提。图像增广(image augmentation)技术通过对训练图像做一系列随机改变,来产生相似但又不同的训练样本,从而扩大训练数据集的规模。图像增广的另一种解释是,随机改变训练样本可以降低模型对某些属性的依赖,从而提高模型的泛化能力。例如,我们可以对图像进行不同方式的裁剪,使感兴趣的物体出现在不同位置,从而减轻模型对物体出现位置的依赖性。我们也可以调整亮度、色彩等因素来降低模型对色彩的敏感度。可以说,在当年AlexNet的成功中,图像增广技术功不可没。本节我们将讨论这个在计算机视觉里被广泛使用的技术。

首先,导入实验所需的包或模块。

import os
os.listdir("/home/kesci/input/img2083/")
[‘img’]
%matplotlib inline
import os
import time
import torch
from torch import nn, optim
from torch.utils.data import Dataset, DataLoader
import torchvision
import sys
from PIL import Image

sys.path.append("/home/kesci/input/")
#置当前使用的GPU设备仅为0号设备
os.environ[“CUDA_VISIBLE_DEVICES”] = “0”

import d2lzh1981 as d2l

#定义device,是否使用GPU,依据计算机配置自动会选择
device = torch.device(‘cuda’ if torch.cuda.is_available() else ‘cpu’)
print(torch.version)
print(device)
1.3.0
cpu
9.1.1 常用的图像增广方法
我们来读取一张形状为 400×500 (高和宽分别为400像素和500像素)的图像作为实验的样例。

d2l.set_figsize()
img = Image.open(’/home/kesci/input/img2083/img/cat1.jpg’)
d2l.plt.imshow(img)
<matplotlib.image.AxesImage at 0x7f8dae7aa198>

下面定义绘图函数show_images。

#本函数已保存在d2lzh_pytorch包中方便以后使用
def show_images(imgs, num_rows, num_cols, scale=2):
figsize = (num_cols * scale, num_rows * scale)
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
for i in range(num_rows):
for j in range(num_cols):
axes[i][j].imshow(imgs[i * num_cols + j])
axes[i][j].axes.get_xaxis().set_visible(False)
axes[i][j].axes.get_yaxis().set_visible(False)
return axes
大部分图像增广方法都有一定的随机性。为了方便观察图像增广的效果,接下来我们定义一个辅助函数apply。这个函数对输入图像img多次运行图像增广方法aug并展示所有的结果。

def apply(img, aug, num_rows=2, num_cols=4, scale=1.5):
Y = [aug(img) for _ in range(num_rows * num_cols)]
show_images(Y, num_rows, num_cols, scale)
9.1.1.1 翻转和裁剪
左右翻转图像通常不改变物体的类别。它是最早也是最广泛使用的一种图像增广方法。下面我们通过torchvision.transforms模块创建RandomHorizontalFlip实例来实现一半概率的图像水平(左右)翻转。

apply(img, torchvision.transforms.RandomHorizontalFlip())

上下翻转不如左右翻转通用。但是至少对于样例图像,上下翻转不会造成识别障碍。下面我们创建RandomVerticalFlip实例来实现一半概率的图像垂直(上下)翻转。

apply(img, torchvision.transforms.RandomVerticalFlip())

在我们使用的样例图像里,猫在图像正中间,但一般情况下可能不是这样。在5.4节(池化层)里我们解释了池化层能降低卷积层对目标位置的敏感度。除此之外,我们还可以通过对图像随机裁剪来让物体以不同的比例出现在图像的不同位置,这同样能够降低模型对目标位置的敏感性。

在下面的代码里,我们每次随机裁剪出一块面积为原面积 10%∼100% 的区域,且该区域的宽和高之比随机取自 0.5∼2 ,然后再将该区域的宽和高分别缩放到200像素。若无特殊说明,本节中 a 和 b 之间的随机数指的是从区间 [a,b] 中随机均匀采样所得到的连续值。

shape_aug = torchvision.transforms.RandomResizedCrop(200, scale=(0.1, 1), ratio=(0.5, 2))
apply(img, shape_aug)

9.1.1.2 变化颜色
另一类增广方法是变化颜色。我们可以从4个方面改变图像的颜色:亮度(brightness)、对比度(contrast)、饱和度(saturation)和色调(hue)。在下面的例子里,我们将图像的亮度随机变化为原图亮度的 50% ( 1−0.5 ) ∼150% ( 1+0.5 )。

apply(img, torchvision.transforms.ColorJitter(brightness=0.5, contrast=0, saturation=0, hue=0))

我们也可以随机变化图像的色调。

apply(img, torchvision.transforms.ColorJitter(brightness=0, contrast=0, saturation=0, hue=0.5))

类似地,我们也可以随机变化图像的对比度。

apply(img, torchvision.transforms.ColorJitter(brightness=0, contrast=0.5, saturation=0, hue=0))

我们也可以同时设置如何随机变化图像的亮度(brightness)、对比度(contrast)、饱和度(saturation)和色调(hue)。

color_aug = torchvision.transforms.ColorJitter(brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5)
apply(img, color_aug)

9.1.1.3 叠加多个图像增广方法
实际应用中我们会将多个图像增广方法叠加使用。我们可以通过Compose实例将上面定义的多个图像增广方法叠加起来,再应用到每张图像之上。

augs = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(), color_aug, shape_aug])
apply(img, augs)

9.1.2 使用图像增广训练模型
下面我们来看一个将图像增广应用在实际训练中的例子。这里我们使用CIFAR-10数据集,而不是之前我们一直使用的Fashion-MNIST数据集。这是因为Fashion-MNIST数据集中物体的位置和尺寸都已经经过归一化处理,而CIFAR-10数据集中物体的颜色和大小区别更加显著。下面展示了CIFAR-10数据集中前32张训练图像。

CIFAR_ROOT_PATH = ‘/home/kesci/input/cifar102021’
all_imges = torchvision.datasets.CIFAR10(train=True, root=CIFAR_ROOT_PATH, download = True)
#all_imges的每一个元素都是(image, label)
show_images([all_imges[i][0] for i in range(32)], 4, 8, scale=0.8);
Files already downloaded and verified

为了在预测时得到确定的结果,我们通常只将图像增广应用在训练样本上,而不在预测时使用含随机操作的图像增广。在这里我们只使用最简单的随机左右翻转。此外,我们使用ToTensor将小批量图像转成PyTorch需要的格式,即形状为(批量大小, 通道数, 高, 宽)、值域在0到1之间且类型为32位浮点数。

flip_aug = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor()])

no_aug = torchvision.transforms.Compose([
torchvision.transforms.ToTensor()])
接下来我们定义一个辅助函数来方便读取图像并应用图像增广。有关DataLoader的详细介绍,可参考更早的3.5节图像分类数据集(Fashion-MNIST)。

num_workers = 0 if sys.platform.startswith(‘win32’) else 4
def load_cifar10(is_train, augs, batch_size, root=CIFAR_ROOT_PATH):
dataset = torchvision.datasets.CIFAR10(root=root, train=is_train, transform=augs, download=False)
return DataLoader(dataset, batch_size=batch_size, shuffle=is_train, num_workers=num_workers)
9.1.2.1 使用图像增广训练模型
我们在CIFAR-10数据集上训练5.11节(残差网络)中介绍的ResNet-18模型。

我们先定义train函数使用GPU训练并评价模型。

#本函数已保存在d2lzh_pytorch包中方便以后使用
def train(train_iter, test_iter, net, loss, optimizer, device, num_epochs):
net = net.to(device)
print("training on ", device)
batch_count = 0
for epoch in range(num_epochs):
train_l_sum, train_acc_sum, n, start = 0.0, 0.0, 0, time.time()
for X, y in train_iter:
X = X.to(device)
y = y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
optimizer.zero_grad()
l.backward()
optimizer.step()
train_l_sum += l.cpu().item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().item()
n += y.shape[0]
batch_count += 1
test_acc = d2l.evaluate_accuracy(test_iter, net)
print(‘epoch %d, loss %.4f, train acc %.3f, test acc %.3f, time %.1f sec’
% (epoch + 1, train_l_sum / batch_count, train_acc_sum / n, test_acc, time.time() - start))
然后就可以定义train_with_data_aug函数使用图像增广来训练模型了。该函数使用Adam算法作为训练使用的优化算法,然后将图像增广应用于训练数据集之上,最后调用刚才定义的train函数训练并评价模型。

%% Below, type any markdown to display in the Graffiti tip. %% Then run this cell to save it. train_iter = load_cifar10(True, train_augs, batch_size) test_iter = load_cifar10(False, test_augs, batch_size)

def train_with_data_aug(train_augs, test_augs, lr=0.001):
batch_size, net = 256, d2l.resnet18(10)
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
loss = torch.nn.CrossEntropyLoss()
train_iter = load_cifar10(True, train_augs, batch_size)
test_iter = load_cifar10(False, test_augs, batch_size)
train(train_iter, test_iter, net, loss, optimizer, device, num_epochs=10)
下面使用随机左右翻转的图像增广来训练模型。

train_with_data_aug(flip_aug, no_aug)
training on cpu
epoch 1, loss 1.3790, train acc 0.504, test acc 0.554, time 195.8 sec
epoch 2, loss 0.4992, train acc 0.646, test acc 0.592, time 192.5 sec
epoch 3, loss 0.2821, train acc 0.702, test acc 0.657, time 193.7 sec
epoch 4, loss 0.1859, train acc 0.739, test acc 0.693, time 195.4 sec
epoch 5, loss 0.1349, train acc 0.766, test acc 0.688, time 192.6 sec
epoch 6, loss 0.1022, train acc 0.786, test acc 0.701, time 200.2 sec
epoch 7, loss 0.0797, train acc 0.806, test acc 0.720, time 191.8 sec
epoch 8, loss 0.0633, train acc 0.825, test acc 0.695, time 198.6 sec
epoch 9, loss 0.0524, train acc 0.836, test acc 0.693, time 192.1 sec
epoch 10, loss 0.0437, train acc 0.850, test acc 0.769, time 196.3 sec

模型微调

在前面的一些章节中,我们介绍了如何在只有6万张图像的Fashion-MNIST训练数据集上训练模型。我们还描述了学术界当下使用最广泛的大规模图像数据集ImageNet,它有超过1,000万的图像和1,000类的物体。然而,我们平常接触到数据集的规模通常在这两者之间。

假设我们想从图像中识别出不同种类的椅子,然后将购买链接推荐给用户。一种可能的方法是先找出100种常见的椅子,为每种椅子拍摄1,000张不同角度的图像,然后在收集到的图像数据集上训练一个分类模型。这个椅子数据集虽然可能比Fashion-MNIST数据集要庞大,但样本数仍然不及ImageNet数据集中样本数的十分之一。这可能会导致适用于ImageNet数据集的复杂模型在这个椅子数据集上过拟合。同时,因为数据量有限,最终训练得到的模型的精度也可能达不到实用的要求。

为了应对上述问题,一个显而易见的解决办法是收集更多的数据。然而,收集和标注数据会花费大量的时间和资金。例如,为了收集ImageNet数据集,研究人员花费了数百万美元的研究经费。虽然目前的数据采集成本已降低了不少,但其成本仍然不可忽略。

另外一种解决办法是应用迁移学习(transfer learning),将从源数据集学到的知识迁移到目标数据集上。例如,虽然ImageNet数据集的图像大多跟椅子无关,但在该数据集上训练的模型可以抽取较通用的图像特征,从而能够帮助识别边缘、纹理、形状和物体组成等。这些类似的特征对于识别椅子也可能同样有效。

本节我们介绍迁移学习中的一种常用技术:微调(fine tuning)。如图9.1所示,微调由以下4步构成。

在源数据集(如ImageNet数据集)上预训练一个神经网络模型,即源模型。
创建一个新的神经网络模型,即目标模型。它复制了源模型上除了输出层外的所有模型设计及其参数。我们假设这些模型参数包含了源数据集上学习到的知识,且这些知识同样适用于目标数据集。我们还假设源模型的输出层跟源数据集的标签紧密相关,因此在目标模型中不予采用。
为目标模型添加一个输出大小为目标数据集类别个数的输出层,并随机初始化该层的模型参数。
在目标数据集(如椅子数据集)上训练目标模型。我们将从头训练输出层,而其余层的参数都是基于源模型的参数微调得到的。
Image Name

当目标数据集远小于源数据集时,微调有助于提升模型的泛化能力。

9.2.1 热狗识别
接下来我们来实践一个具体的例子:热狗识别。我们将基于一个小数据集对在ImageNet数据集上训练好的ResNet模型进行微调。该小数据集含有数千张包含热狗和不包含热狗的图像。我们将使用微调得到的模型来识别一张图像中是否包含热狗。

首先,导入实验所需的包或模块。torchvision的models包提供了常用的预训练模型。如果希望获取更多的预训练模型,可以使用使用pretrained-models.pytorch仓库。

%matplotlib inline
import torch
from torch import nn, optim
from torch.utils.data import Dataset, DataLoader
import torchvision
from torchvision.datasets import ImageFolder
from torchvision import transforms
from torchvision import models
import os

import sys

sys.path.append("/home/kesci/input/")
import d2lzh1981 as d2l

os.environ[“CUDA_VISIBLE_DEVICES”] = “0”
device = torch.device(‘cuda’ if torch.cuda.is_available() else ‘cpu’)
9.2.1.1 获取数据集
我们使用的热狗数据集(点击下载)是从网上抓取的,它含有1400张包含热狗的正类图像,和同样多包含其他食品的负类图像。各类的1000张图像被用于训练,其余则用于测试。

我们首先将压缩后的数据集下载到路径data_dir之下,然后在该路径将下载好的数据集解压,得到两个文件夹hotdog/train和hotdog/test。这两个文件夹下面均有hotdog和not-hotdog两个类别文件夹,每个类别文件夹里面是图像文件。

import os
os.listdir(’/home/kesci/input/resnet185352’)
[‘resnet18-5c106cde.pth’]
data_dir = ‘/home/kesci/input/hotdog4014’
os.listdir(os.path.join(data_dir, “hotdog”))
[‘test’, ‘train’]
我们创建两个ImageFolder实例来分别读取训练数据集和测试数据集中的所有图像文件。

train_imgs = ImageFolder(os.path.join(data_dir, ‘hotdog/train’))
test_imgs = ImageFolder(os.path.join(data_dir, ‘hotdog/test’))
下面画出前8张正类图像和最后8张负类图像。可以看到,它们的大小和高宽比各不相同。

hotdogs = [train_imgs[i][0] for i in range(8)]
not_hotdogs = [train_imgs[-i - 1][0] for i in range(8)]
d2l.show_images(hotdogs + not_hotdogs, 2, 8, scale=1.4);

在训练时,我们先从图像中裁剪出随机大小和随机高宽比的一块随机区域,然后将该区域缩放为高和宽均为224像素的输入。测试时,我们将图像的高和宽均缩放为256像素,然后从中裁剪出高和宽均为224像素的中心区域作为输入。此外,我们对RGB(红、绿、蓝)三个颜色通道的数值做标准化:每个数值减去该通道所有数值的平均值,再除以该通道所有数值的标准差作为输出。

注: 在使用预训练模型时,一定要和预训练时作同样的预处理。 如果你使用的是torchvision的models,那就要求: All pre-trained models expect input images normalized in the same way, i.e. mini-batches of 3-channel RGB images of shape (3 x H x W), where H and W are expected to be at least 224. The images have to be loaded in to a range of [0, 1] and then normalized using mean = [0.485, 0.456, 0.406] and std = [0.229, 0.224, 0.225].

normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
train_augs = transforms.Compose([
transforms.RandomResizedCrop(size=224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
normalize
])

test_augs = transforms.Compose([
transforms.Resize(size=256),
transforms.CenterCrop(size=224),
transforms.ToTensor(),
normalize
])
9.2.1.2 定义和初始化模型
我们使用在ImageNet数据集上预训练的ResNet-18作为源模型。这里指定pretrained=True来自动下载并加载预训练的模型参数。在第一次使用时需要联网下载模型参数。

pretrained_net = models.resnet18(pretrained=False)
pretrained_net.load_state_dict(torch.load(’/home/kesci/input/resnet185352/resnet18-5c106cde.pth’))

下面打印源模型的成员变量fc。作为一个全连接层,它将ResNet最终的全局平均池化层输出变换成ImageNet数据集上1000类的输出。

print(pretrained_net.fc)
Linear(in_features=512, out_features=1000, bias=True)
注: 如果你使用的是其他模型,那可能没有成员变量fc(比如models中的VGG预训练模型),所以正确做法是查看对应模型源码中其定义部分,这样既不会出错也能加深我们对模型的理解。pretrained-models.pytorch仓库貌似统一了接口,但是我还是建议使用时查看一下对应模型的源码。

可见此时pretrained_net最后的输出个数等于目标数据集的类别数1000。所以我们应该将最后的fc成修改我们需要的输出类别数:

pretrained_net.fc = nn.Linear(512, 2)
print(pretrained_net.fc)
Linear(in_features=512, out_features=2, bias=True)
此时,pretrained_net的fc层就被随机初始化了,但是其他层依然保存着预训练得到的参数。由于是在很大的ImageNet数据集上预训练的,所以参数已经足够好,因此一般只需使用较小的学习率来微调这些参数,而fc中的随机初始化参数一般需要更大的学习率从头训练。PyTorch可以方便的对模型的不同部分设置不同的学习参数,我们在下面代码中将fc的学习率设为已经预训练过的部分的10倍。

output_params = list(map(id, pretrained_net.fc.parameters()))
feature_params = filter(lambda p: id§ not in output_params, pretrained_net.parameters())

lr = 0.01
optimizer = optim.SGD([{‘params’: feature_params},
{‘params’: pretrained_net.fc.parameters(), ‘lr’: lr * 10}],
lr=lr, weight_decay=0.001)
9.2.1.3 微调模型
def train_fine_tuning(net, optimizer, batch_size=128, num_epochs=5):
train_iter = DataLoader(ImageFolder(os.path.join(data_dir, ‘hotdog/train’), transform=train_augs),
batch_size, shuffle=True)
test_iter = DataLoader(ImageFolder(os.path.join(data_dir, ‘hotdog/test’), transform=test_augs),
batch_size)
loss = torch.nn.CrossEntropyLoss()
d2l.train(train_iter, test_iter, net, loss, optimizer, device, num_epochs)
train_fine_tuning(pretrained_net, optimizer)
training on cpu
epoch 1, loss 3.4516, train acc 0.687, test acc 0.884, time 298.2 sec
epoch 2, loss 0.1550, train acc 0.924, test acc 0.895, time 296.2 sec
epoch 3, loss 0.1028, train acc 0.903, test acc 0.950, time 295.0 sec
epoch 4, loss 0.0495, train acc 0.931, test acc 0.897, time 294.0 sec
epoch 5, loss 0.1454, train acc 0.878, test acc 0.939, time 291.0 sec
作为对比,我们定义一个相同的模型,但将它的所有模型参数都初始化为随机值。由于整个模型都需要从头训练,我们可以使用较大的学习率。

scratch_net = models.resnet18(pretrained=False, num_classes=2)
lr = 0.1
optimizer = optim.SGD(scratch_net.parameters(), lr=lr, weight_decay=0.001)
train_fine_tuning(scratch_net, optimizer)
training on cpu
epoch 1, loss 2.6391, train acc 0.598, test acc 0.734, time 292.4 sec
epoch 2, loss 0.2703, train acc 0.790, test acc 0.632, time 289.7 sec
epoch 3, loss 0.1584, train acc 0.810, test acc 0.825, time 290.2 sec
epoch 4, loss 0.1177, train acc 0.805, test acc 0.787, time 288.6 sec
epoch 5, loss 0.0782, train acc 0.829, test acc 0.828, time 289.8 sec
输出:

training on cuda
epoch 1, loss 2.6686, train acc 0.582, test acc 0.556, time 25.3 sec
epoch 2, loss 0.2434, train acc 0.797, test acc 0.776, time 25.3 sec
epoch 3, loss 0.1251, train acc 0.845, test acc 0.802, time 24.9 sec
epoch 4, loss 0.0958, train acc 0.833, test acc 0.810, time 25.0 sec
epoch 5, loss 0.0757, train acc 0.836, test acc 0.780, time 24.9 sec

目标检测基础

9.3 目标检测和边界框
%matplotlib inline
from PIL import Image

import sys
sys.path.append(’/home/kesci/input/’)
import d2lzh1981 as d2l
#展示用于目标检测的图
d2l.set_figsize()
img = Image.open(’/home/kesci/input/img2083/img/catdog.jpg’)
d2l.plt.imshow(img); # 加分号只显示图

9.3.1 边界框
#bbox是bounding box的缩写
dog_bbox, cat_bbox = [60, 45, 378, 516], [400, 112, 655, 493]
def bbox_to_rect(bbox, color): # 本函数已保存在d2lzh_pytorch中方便以后使用
# 将边界框(左上x, 左上y, 右下x, 右下y)格式转换成matplotlib格式:
# ((左上x, 左上y), 宽, 高)
return d2l.plt.Rectangle(
xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1],
fill=False, edgecolor=color, linewidth=2)
fig = d2l.plt.imshow(img)
fig.axes.add_patch(bbox_to_rect(dog_bbox, ‘blue’))
fig.axes.add_patch(bbox_to_rect(cat_bbox, ‘red’));

9.4 锚框
目标检测算法通常会在输入图像中采样大量的区域,然后判断这些区域中是否包含我们感兴趣的目标,并调整区域边缘从而更准确地预测目标的真实边界框(ground-truth bounding box)。不同的模型使用的区域采样方法可能不同。这里我们介绍其中的一种方法:它以每个像素为中心生成多个大小和宽高比(aspect ratio)不同的边界框。这些边界框被称为锚框(anchor box)。我们将在后面基于锚框实践目标检测。

注: 建议想学习用PyTorch做检测的童鞋阅读一下仓库a-PyTorch-Tutorial-to-Object-Detection。

先导入一下相关包。

import numpy as np
import math
import torch
import os
IMAGE_DIR = ‘/home/kesci/input/img2083/img/’
print(torch.version)
1.1.0
9.4.1 生成多个锚框
假设输入图像高为 h ,宽为 w 。我们分别以图像的每个像素为中心生成不同形状的锚框。设大小为 s∈(0,1] 且宽高比为 r>0 ,那么锚框的宽和高将分别为 wsr√ 和 hs/r√ 。当中心位置给定时,已知宽和高的锚框是确定的。

下面我们分别设定好一组大小 s1,…,sn 和一组宽高比 r1,…,rm 。如果以每个像素为中心时使用所有的大小与宽高比的组合,输入图像将一共得到 whnm 个锚框。虽然这些锚框可能覆盖了所有的真实边界框,但计算复杂度容易过高。因此,我们通常只对包含 s1 或 r1 的大小与宽高比的组合感兴趣,即

(s1,r1),(s1,r2),…,(s1,rm),(s2,r1),(s3,r1),…,(sn,r1).

也就是说,以相同像素为中心的锚框的数量为 n+m−1 。对于整个输入图像,我们将一共生成 wh(n+m−1) 个锚框。

以上生成锚框的方法已实现在MultiBoxPrior函数中。指定输入、一组大小和一组宽高比,该函数将返回输入的所有锚框。

d2l.set_figsize()
img = Image.open(os.path.join(IMAGE_DIR, ‘catdog.jpg’))
w, h = img.size
print(“w = %d, h = %d” % (w, h))

#d2l.plt.imshow(img); # 加分号只显示图
w = 728, h = 561
#本函数已保存在d2lzh_pytorch包中方便以后使用
def MultiBoxPrior(feature_map, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5]):
“”"
# 按照「9.4.1. 生成多个锚框」所讲的实现, anchor表示成(xmin, ymin, xmax, ymax).
https://zh.d2l.ai/chapter_computer-vision/anchor.html
Args:
feature_map: torch tensor, Shape: [N, C, H, W].
sizes: List of sizes (0~1) of generated MultiBoxPriores.
ratios: List of aspect ratios (non-negative) of generated MultiBoxPriores.
Returns:
anchors of shape (1, num_anchors, 4). 由于batch里每个都一样, 所以第一维为1
“”"
pairs = [] # pair of (size, sqrt(ration))

# 生成n + m -1个框
for r in ratios:
    pairs.append([sizes[0], math.sqrt(r)])
for s in sizes[1:]:
    pairs.append([s, math.sqrt(ratios[0])])

pairs = np.array(pairs)

# 生成相对于坐标中心点的框(x,y,x,y)
ss1 = pairs[:, 0] * pairs[:, 1] # size * sqrt(ration)
ss2 = pairs[:, 0] / pairs[:, 1] # size / sqrt(ration)

base_anchors = np.stack([-ss1, -ss2, ss1, ss2], axis=1) / 2

#将坐标点和anchor组合起来生成hw(n+m-1)个框输出
h, w = feature_map.shape[-2:]
shifts_x = np.arange(0, w) / w
shifts_y = np.arange(0, h) / h
shift_x, shift_y = np.meshgrid(shifts_x, shifts_y)

shift_x = shift_x.reshape(-1)
shift_y = shift_y.reshape(-1)

shifts = np.stack((shift_x, shift_y, shift_x, shift_y), axis=1)
anchors = shifts.reshape((-1, 1, 4)) + base_anchors.reshape((1, -1, 4))

return torch.tensor(anchors, dtype=torch.float32).view(1, -1, 4)

X = torch.Tensor(1, 3, h, w) # 构造输入数据
Y = MultiBoxPrior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
Y.shape
torch.Size([1, 2042040, 4])
我们看到,返回锚框变量y的形状为(1,锚框个数,4)。将锚框变量y的形状变为(图像高,图像宽,以相同像素为中心的锚框个数,4)后,我们就可以通过指定像素位置来获取所有以该像素为中心的锚框了。下面的例子里我们访问以(250,250)为中心的第一个锚框。它有4个元素,分别是锚框左上角的 x 和 y 轴坐标和右下角的 x 和 y 轴坐标,其中 x 和 y 轴的坐标值分别已除以图像的宽和高,因此值域均为0和1之间。

#展示某个像素点的anchor
boxes = Y.reshape((h, w, 5, 4))
boxes[250, 250, 0, :]# * torch.tensor([w, h, w, h], dtype=torch.float32)
#第一个size和ratio分别为0.75和1, 则宽高均为0.75 = 0.7184 + 0.0316 = 0.8206 - 0.0706
tensor([-0.0316, 0.0706, 0.7184, 0.8206])
可以验证一下以上输出对不对:size和ratio分别为0.75和1, 则(归一化后的)宽高均为0.75, 所以输出是正确的(0.75 = 0.7184 + 0.0316 = 0.8206 - 0.0706)。

为了描绘图像中以某个像素为中心的所有锚框,我们先定义show_bboxes函数以便在图像上画出多个边界框。

#本函数已保存在dd2lzh_pytorch包中方便以后使用
def show_bboxes(axes, bboxes, labels=None, colors=None):
def _make_list(obj, default_values=None):
if obj is None:
obj = default_values
elif not isinstance(obj, (list, tuple)):
obj = [obj]
return obj

labels = _make_list(labels)
colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c'])
for i, bbox in enumerate(bboxes):
    color = colors[i % len(colors)]
    rect = d2l.bbox_to_rect(bbox.detach().cpu().numpy(), color)
    axes.add_patch(rect)
    if labels and len(labels) > i:
        text_color = 'k' if color == 'w' else 'w'
        axes.text(rect.xy[0], rect.xy[1], labels[i],
                  va='center', ha='center', fontsize=6, color=text_color,
                  bbox=dict(facecolor=color, lw=0))

刚刚我们看到,变量boxes中 x 和 y 轴的坐标值分别已除以图像的宽和高。在绘图时,我们需要恢复锚框的原始坐标值,并因此定义了变量bbox_scale。现在,我们可以画出图像中以(250, 250)为中心的所有锚框了。可以看到,大小为0.75且宽高比为1的锚框较好地覆盖了图像中的狗。

#展示 250 250像素点的anchor
d2l.set_figsize()
fig = d2l.plt.imshow(img)
bbox_scale = torch.tensor([[w, h, w, h]], dtype=torch.float32)
show_bboxes(fig.axes, boxes[250, 250, :, :] * bbox_scale,
[‘s=0.75, r=1’, ‘s=0.75, r=2’, ‘s=0.75, r=0.5’, ‘s=0.5, r=1’, ‘s=0.25, r=1’])

9.4.2 交并比
我们刚刚提到某个锚框较好地覆盖了图像中的狗。如果该目标的真实边界框已知,这里的“较好”该如何量化呢?一种直观的方法是衡量锚框和真实边界框之间的相似度。我们知道,Jaccard系数(Jaccard index)可以衡量两个集合的相似度。给定集合 A 和 B ,它们的Jaccard系数即二者交集大小除以二者并集大小:

J(A,B)=|A∩B||A∪B|.

实际上,我们可以把边界框内的像素区域看成是像素的集合。如此一来,我们可以用两个边界框的像素集合的Jaccard系数衡量这两个边界框的相似度。当衡量两个边界框的相似度时,我们通常将Jaccard系数称为交并比(Intersection over Union,IoU),即两个边界框相交面积与相并面积之比,如图9.2所示。交并比的取值范围在0和1之间:0表示两个边界框无重合像素,1表示两个边界框相等。

Image Name

#以下函数已保存在d2lzh_pytorch包中方便以后使用
def compute_intersection(set_1, set_2):
“”"
计算anchor之间的交集
Args:
set_1: a tensor of dimensions (n1, 4), anchor表示成(xmin, ymin, xmax, ymax)
set_2: a tensor of dimensions (n2, 4), anchor表示成(xmin, ymin, xmax, ymax)
Returns:
intersection of each of the boxes in set 1 with respect to each of the boxes in set 2, shape: (n1, n2)
“”"
# PyTorch auto-broadcasts singleton dimensions
lower_bounds = torch.max(set_1[:, :2].unsqueeze(1), set_2[:, :2].unsqueeze(0)) # (n1, n2, 2)
upper_bounds = torch.min(set_1[:, 2:].unsqueeze(1), set_2[:, 2:].unsqueeze(0)) # (n1, n2, 2)
intersection_dims = torch.clamp(upper_bounds - lower_bounds, min=0) # (n1, n2, 2)
return intersection_dims[:, :, 0] * intersection_dims[:, :, 1] # (n1, n2)

def compute_jaccard(set_1, set_2):
“”"
计算anchor之间的Jaccard系数(IoU)
Args:
set_1: a tensor of dimensions (n1, 4), anchor表示成(xmin, ymin, xmax, ymax)
set_2: a tensor of dimensions (n2, 4), anchor表示成(xmin, ymin, xmax, ymax)
Returns:
Jaccard Overlap of each of the boxes in set 1 with respect to each of the boxes in set 2, shape: (n1, n2)
“”"
# Find intersections
intersection = compute_intersection(set_1, set_2) # (n1, n2)

# Find areas of each box in both sets
areas_set_1 = (set_1[:, 2] - set_1[:, 0]) * (set_1[:, 3] - set_1[:, 1])  # (n1)
areas_set_2 = (set_2[:, 2] - set_2[:, 0]) * (set_2[:, 3] - set_2[:, 1])  # (n2)

# Find the union
# PyTorch auto-broadcasts singleton dimensions
union = areas_set_1.unsqueeze(1) + areas_set_2.unsqueeze(0) - intersection  # (n1, n2)

return intersection / union  # (n1, n2)

9.4.3 标注训练集的锚框
在训练集中,我们将每个锚框视为一个训练样本。为了训练目标检测模型,我们需要为每个锚框标注两类标签:一是锚框所含目标的类别,简称类别;二是真实边界框相对锚框的偏移量,简称偏移量(offset)。在目标检测时,我们首先生成多个锚框,然后为每个锚框预测类别以及偏移量,接着根据预测的偏移量调整锚框位置从而得到预测边界框,最后筛选需要输出的预测边界框。

我们知道,在目标检测的训练集中,每个图像已标注了真实边界框的位置以及所含目标的类别。在生成锚框之后,我们主要依据与锚框相似的真实边界框的位置和类别信息为锚框标注。那么,该如何为锚框分配与其相似的真实边界框呢?

假设图像中锚框分别为 A1,A2,…,Ana ,真实边界框分别为 B1,B2,…,Bnb ,且 na≥nb 。定义矩阵 X∈Rna×nb ,其中第 i 行第 j 列的元素 xij 为锚框 Ai 与真实边界框 Bj 的交并比。 首先,我们找出矩阵 X 中最大元素,并将该元素的行索引与列索引分别记为 i1,j1 。我们为锚框 Ai1 分配真实边界框 Bj1 。显然,锚框 Ai1 和真实边界框 Bj1 在所有的“锚框—真实边界框”的配对中相似度最高。接下来,将矩阵 X 中第 i1 行和第 j1 列上的所有元素丢弃。找出矩阵 X 中剩余的最大元素,并将该元素的行索引与列索引分别记为 i2,j2 。我们为锚框 Ai2 分配真实边界框 Bj2 ,再将矩阵 X 中第 i2 行和第 j2 列上的所有元素丢弃。此时矩阵 X 中已有两行两列的元素被丢弃。 依此类推,直到矩阵 X 中所有 nb 列元素全部被丢弃。这个时候,我们已为 nb 个锚框各分配了一个真实边界框。 接下来,我们只遍历剩余的 na−nb 个锚框:给定其中的锚框 Ai ,根据矩阵 X 的第 i 行找到与 Ai 交并比最大的真实边界框 Bj ,且只有当该交并比大于预先设定的阈值时,才为锚框 Ai 分配真实边界框 Bj 。

如图9.3(左)所示,假设矩阵 X 中最大值为 x23 ,我们将为锚框 A2 分配真实边界框 B3 。然后,丢弃矩阵中第2行和第3列的所有元素,找出剩余阴影部分的最大元素 x71 ,为锚框 A7 分配真实边界框 B1 。接着如图9.3(中)所示,丢弃矩阵中第7行和第1列的所有元素,找出剩余阴影部分的最大元素 x54 ,为锚框 A5 分配真实边界框 B4 。最后如图9.3(右)所示,丢弃矩阵中第5行和第4列的所有元素,找出剩余阴影部分的最大元素 x92 ,为锚框 A9 分配真实边界框 B2 。之后,我们只需遍历除去 A2,A5,A7,A9 的剩余锚框,并根据阈值判断是否为剩余锚框分配真实边界框。

Image Name

现在我们可以标注锚框的类别和偏移量了。如果一个锚框 A 被分配了真实边界框 B ,将锚框 A 的类别设为 B 的类别,并根据 B 和 A 的中心坐标的相对位置以及两个框的相对大小为锚框 A 标注偏移量。由于数据集中各个框的位置和大小各异,因此这些相对位置和相对大小通常需要一些特殊变换,才能使偏移量的分布更均匀从而更容易拟合。设锚框 A 及其被分配的真实边界框 B 的中心坐标分别为 (xa,ya) 和 (xb,yb) , A 和 B 的宽分别为 wa 和 wb ,高分别为 ha 和 hb ,一个常用的技巧是将 A 的偏移量标注为

⎛⎝xb−xawa−μxσx,yb−yaha−μyσy,logwbwa−μwσw,loghbha−μhσh⎞⎠,

其中常数的默认值为 μx=μy=μw=μh=0,σx=σy=0.1,σw=σh=0.2 。如果一个锚框没有被分配真实边界框,我们只需将该锚框的类别设为背景。类别为背景的锚框通常被称为负类锚框,其余则被称为正类锚框。

下面演示一个具体的例子。我们为读取的图像中的猫和狗定义真实边界框,其中第一个元素为类别(0为狗,1为猫),剩余4个元素分别为左上角的 x 和 y 轴坐标以及右下角的 x 和 y 轴坐标(值域在0到1之间)。这里通过左上角和右下角的坐标构造了5个需要标注的锚框,分别记为 A0,…,A4 (程序中索引从0开始)。先画出这些锚框与真实边界框在图像中的位置。

bbox_scale = torch.tensor((w, h, w, h), dtype=torch.float32)
ground_truth = torch.tensor([[0, 0.1, 0.08, 0.52, 0.92],
[1, 0.55, 0.2, 0.9, 0.88]])
anchors = torch.tensor([[0, 0.1, 0.2, 0.3], [0.15, 0.2, 0.4, 0.4],
[0.63, 0.05, 0.88, 0.98], [0.66, 0.45, 0.8, 0.8],
[0.57, 0.3, 0.92, 0.9]])

fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, ground_truth[:, 1:] * bbox_scale, [‘dog’, ‘cat’], ‘k’)
show_bboxes(fig.axes, anchors * bbox_scale, [‘0’, ‘1’, ‘2’, ‘3’, ‘4’]);

compute_jaccard(anchors, ground_truth[:, 1:]) # 验证一下写的compute_jaccard函数
tensor([[0.0536, 0.0000],
[0.1417, 0.0000],
[0.0000, 0.5657],
[0.0000, 0.2059],
[0.0000, 0.7459]])
下面实现MultiBoxTarget函数来为锚框标注类别和偏移量。该函数将背景类别设为0,并令从零开始的目标类别的整数索引自加1(1为狗,2为猫)。

#以下函数已保存在d2lzh_pytorch包中方便以后使用
def assign_anchor(bb, anchor, jaccard_threshold=0.5):
“”"
# 按照「9.4.1. 生成多个锚框」图9.3所讲为每个anchor分配真实的bb, anchor表示成归一化(xmin, ymin, xmax, ymax).
https://zh.d2l.ai/chapter_computer-vision/anchor.html
Args:
bb: 真实边界框(bounding box), shape:(nb, 4)
anchor: 待分配的anchor, shape:(na, 4)
jaccard_threshold: 预先设定的阈值
Returns:
assigned_idx: shape: (na, ), 每个anchor分配的真实bb对应的索引, 若未分配任何bb则为-1
“”"
na = anchor.shape[0]
nb = bb.shape[0]
jaccard = compute_jaccard(anchor, bb).detach().cpu().numpy() # shape: (na, nb)
assigned_idx = np.ones(na) * -1 # 存放标签初始全为-1

# 先为每个bb分配一个anchor(不要求满足jaccard_threshold)
jaccard_cp = jaccard.copy()
for j in range(nb):
    i = np.argmax(jaccard_cp[:, j])
    assigned_idx[i] = j
    jaccard_cp[i, :] = float("-inf") # 赋值为负无穷, 相当于去掉这一行
 
# 处理还未被分配的anchor, 要求满足jaccard_threshold
for i in range(na):
    if assigned_idx[i] == -1:
        j = np.argmax(jaccard[i, :])
        if jaccard[i, j] >= jaccard_threshold:
            assigned_idx[i] = j
            
return torch.tensor(assigned_idx, dtype=torch.long)

def xy_to_cxcy(xy):
“”"
将(x_min, y_min, x_max, y_max)形式的anchor转换成(center_x, center_y, w, h)形式的.
https://github.com/sgrvinod/a-PyTorch-Tutorial-to-Object-Detection/blob/master/utils.py
Args:
xy: bounding boxes in boundary coordinates, a tensor of size (n_boxes, 4)
Returns:
bounding boxes in center-size coordinates, a tensor of size (n_boxes, 4)
“”"
return torch.cat([(xy[:, 2:] + xy[:, :2]) / 2, # c_x, c_y
xy[:, 2:] - xy[:, :2]], 1) # w, h

def MultiBoxTarget(anchor, label):
“”"
# 按照「9.4.1. 生成多个锚框」所讲的实现, anchor表示成归一化(xmin, ymin, xmax, ymax).
https://zh.d2l.ai/chapter_computer-vision/anchor.html
Args:
anchor: torch tensor, 输入的锚框, 一般是通过MultiBoxPrior生成, shape:(1,锚框总数,4)
label: 真实标签, shape为(bn, 每张图片最多的真实锚框数, 5)
第二维中,如果给定图片没有这么多锚框, 可以先用-1填充空白, 最后一维中的元素为[类别标签, 四个坐标值]
Returns:
列表, [bbox_offset, bbox_mask, cls_labels]
bbox_offset: 每个锚框的标注偏移量,形状为(bn,锚框总数*4)
bbox_mask: 形状同bbox_offset, 每个锚框的掩码, 一一对应上面的偏移量, 负类锚框(背景)对应的掩码均为0, 正类锚框的掩码均为1
cls_labels: 每个锚框的标注类别, 其中0表示为背景, 形状为(bn,锚框总数)
“”"
assert len(anchor.shape) == 3 and len(label.shape) == 3
bn = label.shape[0]

def MultiBoxTarget_one(anc, lab, eps=1e-6):
    """
    MultiBoxTarget函数的辅助函数, 处理batch中的一个
    Args:
        anc: shape of (锚框总数, 4)
        lab: shape of (真实锚框数, 5), 5代表[类别标签, 四个坐标值]
        eps: 一个极小值, 防止log0
    Returns:
        offset: (锚框总数*4, )
        bbox_mask: (锚框总数*4, ), 0代表背景, 1代表非背景
        cls_labels: (锚框总数, 4), 0代表背景
    """
    an = anc.shape[0]
    # 变量的意义
    assigned_idx = assign_anchor(lab[:, 1:], anc) # (锚框总数, )
    print("a: ",  assigned_idx.shape)
    print(assigned_idx)
    bbox_mask = ((assigned_idx >= 0).float().unsqueeze(-1)).repeat(1, 4) # (锚框总数, 4)
    print("b: " , bbox_mask.shape)
    print(bbox_mask)

    cls_labels = torch.zeros(an, dtype=torch.long) # 0表示背景
    assigned_bb = torch.zeros((an, 4), dtype=torch.float32) # 所有anchor对应的bb坐标
    for i in range(an):
        bb_idx = assigned_idx[i]
        if bb_idx >= 0: # 即非背景
            cls_labels[i] = lab[bb_idx, 0].long().item() + 1 # 注意要加一
            assigned_bb[i, :] = lab[bb_idx, 1:]
    # 如何计算偏移量
    center_anc = xy_to_cxcy(anc) # (center_x, center_y, w, h)
    center_assigned_bb = xy_to_cxcy(assigned_bb)

    offset_xy = 10.0 * (center_assigned_bb[:, :2] - center_anc[:, :2]) / center_anc[:, 2:]
    offset_wh = 5.0 * torch.log(eps + center_assigned_bb[:, 2:] / center_anc[:, 2:])
    offset = torch.cat([offset_xy, offset_wh], dim = 1) * bbox_mask # (锚框总数, 4)

    return offset.view(-1), bbox_mask.view(-1), cls_labels
# 组合输出
batch_offset = []
batch_mask = []
batch_cls_labels = []
for b in range(bn):
    offset, bbox_mask, cls_labels = MultiBoxTarget_one(anchor[0, :, :], label[b, :, :])
    
    batch_offset.append(offset)
    batch_mask.append(bbox_mask)
    batch_cls_labels.append(cls_labels)

bbox_offset = torch.stack(batch_offset)
bbox_mask = torch.stack(batch_mask)
cls_labels = torch.stack(batch_cls_labels)

return [bbox_offset, bbox_mask, cls_labels]

我们通过unsqueeze函数为锚框和真实边界框添加样本维。

labels = MultiBoxTarget(anchors.unsqueeze(dim=0),
ground_truth.unsqueeze(dim=0))
a: torch.Size([5])
tensor([-1, 0, 1, -1, 1])
b: torch.Size([5, 4])
tensor([[0., 0., 0., 0.],
[1., 1., 1., 1.],
[1., 1., 1., 1.],
[0., 0., 0., 0.],
[1., 1., 1., 1.]])
返回的结果里有3项,均为Tensor。第三项表示为锚框标注的类别。

labels[2]
tensor([[0, 1, 2, 0, 2]])
我们根据锚框与真实边界框在图像中的位置来分析这些标注的类别。首先,在所有的“锚框—真实边界框”的配对中,锚框 A4 与猫的真实边界框的交并比最大,因此锚框 A4 的类别标注为猫。不考虑锚框 A4 或猫的真实边界框,在剩余的“锚框—真实边界框”的配对中,最大交并比的配对为锚框 A1 和狗的真实边界框,因此锚框 A1 的类别标注为狗。接下来遍历未标注的剩余3个锚框:与锚框 A0 交并比最大的真实边界框的类别为狗,但交并比小于阈值(默认为0.5),因此类别标注为背景;与锚框 A2 交并比最大的真实边界框的类别为猫,且交并比大于阈值,因此类别标注为猫;与锚框 A3 交并比最大的真实边界框的类别为猫,但交并比小于阈值,因此类别标注为背景。

返回值的第二项为掩码(mask)变量,形状为(批量大小, 锚框个数的四倍)。掩码变量中的元素与每个锚框的4个偏移量一一对应。 由于我们不关心对背景的检测,有关负类的偏移量不应影响目标函数。通过按元素乘法,掩码变量中的0可以在计算目标函数之前过滤掉负类的偏移量。

labels[1]
tensor([[0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 0., 1., 1.,
1., 1.]])
返回的第一项是为每个锚框标注的四个偏移量,其中负类锚框的偏移量标注为0。

labels[0]
tensor([[-0.0000e+00, -0.0000e+00, -0.0000e+00, -0.0000e+00, 1.4000e+00,
1.0000e+01, 2.5940e+00, 7.1754e+00, -1.2000e+00, 2.6882e-01,
1.6824e+00, -1.5655e+00, -0.0000e+00, -0.0000e+00, -0.0000e+00,
-0.0000e+00, -5.7143e-01, -1.0000e+00, 4.1723e-06, 6.2582e-01]])
9.4.4. 输出预测边界框
在模型预测阶段,我们先为图像生成多个锚框,并为这些锚框一一预测类别和偏移量。随后,我们根据锚框及其预测偏移量得到预测边界框。当锚框数量较多时,同一个目标上可能会输出较多相似的预测边界框。为了使结果更加简洁,我们可以移除相似的预测边界框。常用的方法叫作非极大值抑制(non-maximum suppression,NMS)。

我们来描述一下非极大值抑制的工作原理。对于一个预测边界框 B ,模型会计算各个类别的预测概率。设其中最大的预测概率为 p ,该概率所对应的类别即 B 的预测类别。我们也将 p 称为预测边界框 B 的置信度。在同一图像上,我们将预测类别非背景的预测边界框按置信度从高到低排序,得到列表 L 。从 L 中选取置信度最高的预测边界框 B1 作为基准,将所有与 B1 的交并比大于某阈值的非基准预测边界框从 L 中移除。这里的阈值是预先设定的超参数。此时, L 保留了置信度最高的预测边界框并移除了与其相似的其他预测边界框。 接下来,从 L 中选取置信度第二高的预测边界框 B2 作为基准,将所有与 B2 的交并比大于某阈值的非基准预测边界框从 L 中移除。重复这一过程,直到 L 中所有的预测边界框都曾作为基准。此时 L 中任意一对预测边界框的交并比都小于阈值。最终,输出列表 L 中的所有预测边界框。

下面来看一个具体的例子。先构造4个锚框。简单起见,我们假设预测偏移量全是0:预测边界框即锚框。最后,我们构造每个类别的预测概率。

anchors = torch.tensor([[0.1, 0.08, 0.52, 0.92], [0.08, 0.2, 0.56, 0.95],
[0.15, 0.3, 0.62, 0.91], [0.55, 0.2, 0.9, 0.88]])
offset_preds = torch.tensor([0.0] * (4 * len(anchors)))
cls_probs = torch.tensor([[0., 0., 0., 0.,], # 背景的预测概率
[0.9, 0.8, 0.7, 0.1], # 狗的预测概率
[0.1, 0.2, 0.3, 0.9]]) # 猫的预测概率
在图像上打印预测边界框和它们的置信度。

fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, anchors * bbox_scale,
[‘dog=0.9’, ‘dog=0.8’, ‘dog=0.7’, ‘cat=0.9’])

下面我们实现MultiBoxDetection函数来执行非极大值抑制。

%% Below, type any markdown to display in the Graffiti tip. %% Then run this cell to save it. sorted

#以下函数已保存在d2lzh_pytorch包中方便以后使用
from collections import namedtuple
Pred_BB_Info = namedtuple(“Pred_BB_Info”, [“index”, “class_id”, “confidence”, “xyxy”])

def non_max_suppression(bb_info_list, nms_threshold = 0.5):
“”"
非极大抑制处理预测的边界框
Args:
bb_info_list: Pred_BB_Info的列表, 包含预测类别、置信度等信息
nms_threshold: 阈值
Returns:
output: Pred_BB_Info的列表, 只保留过滤后的边界框信息
“”"
output = []
# 先根据置信度从高到低排序
sorted_bb_info_list = sorted(bb_info_list, key = lambda x: x.confidence, reverse=True)

# 循环遍历删除冗余输出
while len(sorted_bb_info_list) != 0:
    best = sorted_bb_info_list.pop(0)
    output.append(best)
    
    if len(sorted_bb_info_list) == 0:
        break

    bb_xyxy = []
    for bb in sorted_bb_info_list:
        bb_xyxy.append(bb.xyxy)
    
    iou = compute_jaccard(torch.tensor([best.xyxy]), 
                          torch.tensor(bb_xyxy))[0] # shape: (len(sorted_bb_info_list), )
    
    n = len(sorted_bb_info_list)
    sorted_bb_info_list = [sorted_bb_info_list[i] for i in range(n) if iou[i] <= nms_threshold]
return output

def MultiBoxDetection(cls_prob, loc_pred, anchor, nms_threshold = 0.5):
“”"
# 按照「9.4.1. 生成多个锚框」所讲的实现, anchor表示成归一化(xmin, ymin, xmax, ymax).
https://zh.d2l.ai/chapter_computer-vision/anchor.html
Args:
cls_prob: 经过softmax后得到的各个锚框的预测概率, shape:(bn, 预测总类别数+1, 锚框个数)
loc_pred: 预测的各个锚框的偏移量, shape:(bn, 锚框个数*4)
anchor: MultiBoxPrior输出的默认锚框, shape: (1, 锚框个数, 4)
nms_threshold: 非极大抑制中的阈值
Returns:
所有锚框的信息, shape: (bn, 锚框个数, 6)
每个锚框信息由[class_id, confidence, xmin, ymin, xmax, ymax]表示
class_id=-1 表示背景或在非极大值抑制中被移除了
“”"
assert len(cls_prob.shape) == 3 and len(loc_pred.shape) == 2 and len(anchor.shape) == 3
bn = cls_prob.shape[0]

def MultiBoxDetection_one(c_p, l_p, anc, nms_threshold = 0.5):
    """
    MultiBoxDetection的辅助函数, 处理batch中的一个
    Args:
        c_p: (预测总类别数+1, 锚框个数)
        l_p: (锚框个数*4, )
        anc: (锚框个数, 4)
        nms_threshold: 非极大抑制中的阈值
    Return:
        output: (锚框个数, 6)
    """
    pred_bb_num = c_p.shape[1]
    anc = (anc + l_p.view(pred_bb_num, 4)).detach().cpu().numpy() # 加上偏移量
    
    confidence, class_id = torch.max(c_p, 0)
    confidence = confidence.detach().cpu().numpy()
    class_id = class_id.detach().cpu().numpy()
    
    pred_bb_info = [Pred_BB_Info(
                        index = i,
                        class_id = class_id[i] - 1, # 正类label从0开始
                        confidence = confidence[i],
                        xyxy=[*anc[i]]) # xyxy是个列表
                    for i in range(pred_bb_num)]
    
    # 正类的index
    obj_bb_idx = [bb.index for bb in non_max_suppression(pred_bb_info, nms_threshold)]
    
    output = []
    for bb in pred_bb_info:
        output.append([
            (bb.class_id if bb.index in obj_bb_idx else -1.0),
            bb.confidence,
            *bb.xyxy
        ])
        
    return torch.tensor(output) # shape: (锚框个数, 6)

batch_output = []
for b in range(bn):
    batch_output.append(MultiBoxDetection_one(cls_prob[b], loc_pred[b], anchor[0], nms_threshold))

return torch.stack(batch_output)

然后我们运行MultiBoxDetection函数并设阈值为0.5。这里为输入都增加了样本维。我们看到,返回的结果的形状为(批量大小, 锚框个数, 6)。其中每一行的6个元素代表同一个预测边界框的输出信息。第一个元素是索引从0开始计数的预测类别(0为狗,1为猫),其中-1表示背景或在非极大值抑制中被移除。第二个元素是预测边界框的置信度。剩余的4个元素分别是预测边界框左上角的 x 和 y 轴坐标以及右下角的 x 和 y 轴坐标(值域在0到1之间)。

output = MultiBoxDetection(
cls_probs.unsqueeze(dim=0), offset_preds.unsqueeze(dim=0),
anchors.unsqueeze(dim=0), nms_threshold=0.5)
output
tensor([[[ 0.0000, 0.9000, 0.1000, 0.0800, 0.5200, 0.9200],
[-1.0000, 0.8000, 0.0800, 0.2000, 0.5600, 0.9500],
[-1.0000, 0.7000, 0.1500, 0.3000, 0.6200, 0.9100],
[ 1.0000, 0.9000, 0.5500, 0.2000, 0.9000, 0.8800]]])
fig = d2l.plt.imshow(img)
for i in output[0].detach().cpu().numpy():
if i[0] == -1:
continue
label = (‘dog=’, ‘cat=’)[int(i[0])] + str(i[1])
show_bboxes(fig.axes, [torch.tensor(i[2:]) * bbox_scale], label)

实践中,我们可以在执行非极大值抑制前将置信度较低的预测边界框移除,从而减小非极大值抑制的计算量。我们还可以筛选非极大值抑制的输出,例如,只保留其中置信度较高的结果作为最终输出。

小结
以每个像素为中心,生成多个大小和宽高比不同的锚框。
交并比是两个边界框相交面积与相并面积之比。
在训练集中,为每个锚框标注两类标签:一是锚框所含目标的类别;二是真实边界框相对锚框的偏移量。
预测时,可以使用非极大值抑制来移除相似的预测边界框,从而令结果简洁。
9.5 多尺度目标检测
在9.4节(锚框)中,我们在实验中以输入图像的每个像素为中心生成多个锚框。这些锚框是对输入图像不同区域的采样。然而,如果以图像每个像素为中心都生成锚框,很容易生成过多锚框而造成计算量过大。举个例子,假设输入图像的高和宽分别为561像素和728像素,如果以每个像素为中心生成5个不同形状的锚框,那么一张图像上则需要标注并预测200多万个锚框( 561×728×5 )。

减少锚框个数并不难。一种简单的方法是在输入图像中均匀采样一小部分像素,并以采样的像素为中心生成锚框。此外,在不同尺度下,我们可以生成不同数量和不同大小的锚框。值得注意的是,较小目标比较大目标在图像上出现位置的可能性更多。举个简单的例子:形状为 1×1 、 1×2 和 2×2 的目标在形状为 2×2 的图像上可能出现的位置分别有4、2和1种。因此,当使用较小锚框来检测较小目标时,我们可以采样较多的区域;而当使用较大锚框来检测较大目标时,我们可以采样较少的区域。

为了演示如何多尺度生成锚框,我们先读取一张图像。它的高和宽分别为561像素和728像素。

w, h = img.size
w, h
(728, 561)
d2l.set_figsize()

def display_anchors(fmap_w, fmap_h, s):
# 前两维的取值不影响输出结果(原书这里是(1, 10, fmap_w, fmap_h), 我认为错了)
fmap = torch.zeros((1, 10, fmap_h, fmap_w), dtype=torch.float32)

# 平移所有锚框使均匀分布在图片上
offset_x, offset_y = 1.0/fmap_w, 1.0/fmap_h
anchors = d2l.MultiBoxPrior(fmap, sizes=s, ratios=[1, 2, 0.5]) + \
    torch.tensor([offset_x/2, offset_y/2, offset_x/2, offset_y/2])

bbox_scale = torch.tensor([[w, h, w, h]], dtype=torch.float32)
d2l.show_bboxes(d2l.plt.imshow(img).axes,
                anchors[0] * bbox_scale)

display_anchors(fmap_w=4, fmap_h=2, s=[0.15])

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

display_anchors(fmap_w=1, fmap_h=1, s=[0.8])

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值