近年来,深度学习一直是提高计算机视觉系统性能的变革力量。 无论是医疗诊断、自动驾驶车辆,还是智能滤镜、摄像头监控,许多计算机视觉领域的应用都与我们当前和未来的生活密切相关。 可以说,最先进的计算机视觉应用程序与深度学习几乎是不可分割的。
我们研究了计算机视觉中常用的各种卷积神经网络,并将它们应用到简单的图像分类任务中。 我们将介绍两种可以改进模型泛化的方法,即 图像增广 和 微调,并将它们应用于图像分类。
由于深度神经网络可以有效地表示多个层次的图像,因此这种分层表示已成功用于各种计算机视觉任务,例如 对象检测、图像语义分割 和 样式迁移。 秉承计算机视觉中利用分层表示的关键思想,我们将从物体检测的主要组件和技术开始,继而展示如何使用 完全卷积网络 对图像进行语义分割,然后我们将解释如何使用样式迁移技术来生成像本书封面一样的图像。
我们提到过大型数据集是成功应用深度神经网络的先决条件。 图像增广在对训练图像进行一系列的随机变化之后,生成相似但不同的训练样本,从而扩大了训练集的规模。
例如,我们可以以不同的方式裁剪图像,使感兴趣的对象出现在不同的位置,减少模型对于对象出现位置的依赖。 我们还可以调整亮度、颜色等因素来降低模型对颜色的敏感度。
import torch
import torchvision
from torch import nn
import matplotlib.pyplot as plt # plt 用于显示图片
import matplotlib.image as mpimg # mpimg 用于读取图片
from d2l import torch as d2l
d2l.set_figsize()
img = d2l.Image.open('cat1.jpg')
# img = mpimg.imread('cat1.jpg') # 和上面的代码功能相同
d2l.plt.imshow(img)
plt.show()
# 便于观察图像增广的效果
def apply(img, aug, num_rows=2, num_clos=4, scale=1.5):
Y = [aug(img) for _ in range(num_rows * num_clos)]
d2l.show_images(Y, num_rows, num_clos, scale=scale)
plt.show()
# 翻转
apply(img, torchvision.transforms.RandomHorizontalFlip())
apply(img, torchvision.transforms.RandomVerticalFlip())
HorizontalFlip
VerticalFlip
我们随机裁剪一个面积为原始面积10%到100%的区域,该区域的宽高比从0.5到2之间随机取值。 然后,区域的宽度和高度都被缩放到200像素。a 和 b 之间的随机数指的是在区间 [a,b] 中通过均匀采样获得的连续值。
# 裁剪
shape_aug = torchvision.transforms.RandomResizedCrop(
(200, 200), scale=(0.1, 1), ratio=(0.5, 2))
apply(img, shape_aug)
另一种增广方法是改变颜色。 我们可以改变图像颜色的四个方面**:亮度、对比度、饱和度和色调**。 随机更改图像的亮度,随机值为原始图像的50%( 1−0.5 )到150%( 1+0.5 )之间。
我们还可以创建一个 RandomColorJitter 实例,并设置如何同时随机更改图像的亮度(brightness)、对比度(contrast)、饱和度(saturation)和色调(hue)。
# 改变颜色
apply(img, torchvision.transforms.ColorJitter(
brightness=0.5, contrast=0, saturation=0, hue=0))
hue=0.5
saturation=0.5
contrast=0.5
shape_aug = torchvision.transforms.RandomResizedCrop(
(200, 200), scale=(0.1, 1), ratio=(0.5, 2))
color_aug = torchvision.transforms.ColorJitter(
brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5)
augs = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(), color_aug, shape_aug])
apply(img, augs)
让我们使用图像增广来训练模型。 这里,我们使用CIFAR-10数据集,而不是我们之前使用的Fashion-MNIST数据集。 这是因为Fashion-MNIST数据集中对象的位置和大小已被规范化,而CIFAR-10数据集中对象的颜色和大小差异更明显。
all_images = torchvision.datasets.CIFAR10(train=True, root='data',
download=True)
d2l.show_images([all_images[i][0] for i in range(32)], 4, 8, scale=0.8)
plt.show()
为了在预测过程中得到确切的结果,我们通常对训练样本只进行图像增广,且在预测过程中不使用随机操作的图像增广。 在这里,我们只使用最简单的随机左右翻转。 此外,我们使用 ToTensor 实例将一批图像转换为深度学习框架所要求的格式,即形状为(批量大小,通道数,高度,宽度)的32位浮点数,取值范围为0到1。
train_augs = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor()])
test_augs = torchvision.transforms.Compose([
torchvision.transforms.ToTensor()])
定义一个辅助函数,以便于读取图像和应用图像增广。PyTorch 数据集提供的 transform 函数应用图像增广来转化图像。
def load_cifar10(is_train, augs, batch_size):
dataset = torchvision.datasets.CIFAR10(root="../data", train=is_train,
transform=augs, download=True)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size,
shuffle=is_train, num_workers=d2l.get_dataloader_workers())
return dataloader
我们在CIFAR-10数据集上训练ResNet-18模型。定义一个函数,使用多GPU对模型进行训练和评估。
def train_batch_ch13(net, X, y, loss, trainer, devices):
if isinstance(X, list):
X = [x.to(devices[0]) for x in X]
else:
X = X.to(devices[0])
y = y.to(devices[0])
net.train()
trainer.zero_grad()
pred = net(X)
l = loss(pred, y)
l.sum().backward()
trainer.step()
train_loss_sum = l.sum()
train_acc_sum = d2l.accuracy(pred, y)
return train_loss_sum, train_acc_sum
def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
devices=d2l.try_all_gpus()):
timer, num_batches = d2l.Timer(), len(train_iter)
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1],
legend=['train loss', 'train acc', 'test acc'])
net = nn.DataParallel(net, device_ids=devices).to(devices[0])
for epoch in range(num_epochs):
# 4个维度:储存训练损失,训练准确度,实例数,特点数
metric = d2l.Accumulator(4)
for i, (features, labels) in enumerate(train_iter):
timer.start()
l, acc = train_batch_ch13(
net, features, labels, loss, trainer, devices)
metric.add(l, acc, labels.shape[0], labels.numel())
timer.stop()
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(metric[0] / metric[2], metric[1] / metric[3],
None))
test_acc = d2l.evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {metric[0] / metric[2]:.3f}, train acc '
f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on '
f'{str(devices)}')
定义 train_with_data_aug 函数,使用图像增广来训练模型。该函数获取所有的GPU,并使用Adam(类似于SGD,但对学习率不是特别敏感)作为训练的优化算法,将图像增广应用于训练集,最后调用刚刚定义的用于训练和评估模型的 train_ch13 函数。
batch_size, devices, net = 256, d2l.try_all_gpus(), d2l.resnet18(10, 3)
def init_weights(m):
if type(m) in [nn.Linear, nn.Conv2d]:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
def train_with_data_aug(train_augs, test_augs, net, lr=0.001):
train_iter = load_cifar10(True, train_augs, batch_size)
test_iter = load_cifar10(False, test_augs, batch_size)
loss = nn.CrossEntropyLoss(reduction="none")
trainer = torch.optim.Adam(net.parameters(), lr=lr)
train_ch13(net, train_iter, test_iter, loss, trainer, 10, devices)
让我们使用基于随机左右翻转的图像增广来训练模型。
train_with_data_aug(train_augs, test_augs, net)
plt.show()
loss 0.174, train acc 0.941, test acc 0.824
551.9 examples/sec on [device(type='cuda', index=0)]
# 我只有一块GPU
我们介绍了如何在只有6万张图像的Fashion-MNIST训练数据集上训练模型。 我们还描述了学术界当下使用最广泛的大规模图像数据集ImageNet,它有超过1,000万的图像和1,000类的物体。 然而,我们平常接触到的数据集的规模通常在这两者之间。此外,由于训练示例数量有限,训练模型的准确性可能无法满足实际要求。
为了解决上述问题,一个显而易见的解决方案是收集更多的数据。 但是,收集和标记数据可能需要大量的时间和金钱。
另一种解决方案是应用 迁移学习(transfer learning) 将从 源数据集 学到的知识迁移到 目标数据集。 例如,尽管 ImageNet 数据集中的大多数图像与椅子无关,但在此数据集上训练的模型可能会提取更常规的图像特征,这有助于识别边缘、纹理、形状和对象合成。 这些类似的功能也可能有效地识别椅子。
迁移学习中的常见技巧 : 微调(fine-tuning)。微调包括以下四个步骤:
当目标数据集比源数据集小得多时,微调有助于提高模型的泛化能力。
我们通过具体案例演示微调:热狗识别。 我们将在一个小型数据集上微调 ResNet 模型,该数据集已在 ImageNet 数据集上进行了预训练。 这个小型数据集包含数千张包含热狗和不包含热狗的图像,我们将使用微调模型来识别图像中是否包含热狗。
我们使用的热狗数据集来源于网络。该数据集包含 1400 张包含热狗的正面类图像以及包含其他食物的尽可能多的负面级图像。两个类别的 1000 张图片用于训练,其余的则用于测试。
import matplotlib.pyplot as plt
import os
import torch
import torchvision
from torch import nn
from d2l import torch as d2l
#@save
d2l.DATA_HUB['hotdog'] = (d2l.DATA_URL + 'hotdog.zip',
'fba480ffa8aa7e0febbb511d181409f899b9baa5')
data_dir = d2l.download_extract('hotdog')
# 创建两个实例来分别读取训练和测试数据集中的所有图像文件
train_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'train'))
test_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'test'))
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)
plt.show()
python路径拼接os.path.join()函数的用法
在训练期间,我们首先从图像中裁切随机大小和随机长宽比的区域,然后将该区域缩放为 224×224 输入图像。 在测试过程中,我们将图像的高度和宽度都缩放到 256 像素,然后裁剪中央 224×224 区域作为输入。
此外,对于三个 RGB(红、绿和蓝)颜色通道,我们 标准化 每个通道。 具体而言,通道的平均值将从该通道的每个值中减去,然后将结果除以该通道的标准差。
# 使用三个RGB通道的均值和标准偏差,以标准化每个通道
normalize = torchvision.transforms.Normalize(
[0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
train_augs = torchvision.transforms.Compose([
torchvision.transforms.RandomResizedCrop(224),
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor(),
normalize])
test_augs = torchvision.transforms.Compose([
torchvision.transforms.Resize(256),
torchvision.transforms.CenterCrop(224),
torchvision.transforms.ToTensor(),
normalize])
我们使用在 ImageNet 数据集上预训练的 Resnet-18 作为源模型。 在这里,我们指定 pretrained=True 以自动下载预训练的模型参数。 如果你首次使用此模型,则需要连接互联网才能下载。
pretrained_net = torchvision.models.resnet18(pretrained=True)
预训练的源模型实例包含许多特征层和一个输出层 fc。 此划分的主要目的是促进对除输出层以外所有层的模型参数进行微调。 下面给出了源模型的变量 fc。
print(pretrained_net.fc)
#################
Linear(in_features=512, out_features=1000, bias=True)
在 ResNet 的全局平均池化后,全连接层汇集转换为 ImageNet 数据集的 1000 个类输出。 之后,我们构建一个新的神经网络作为目标模型。 它的定义方式与预训练源模型的定义方式相同,只是最终层中的输出数量被设置为目标数据集中的类数。
目标模型实例 finetune_net 的变量特征中的模型参数被初始化为源模型相应层的模型参数。 由于功能中的模型参数是在 ImageNet数据集上预训练的,并且足够好,因此通常只需要较小的学习率即可微调这些参数。
变量输出中的模型参数是随机初始化的,通常需要更高的学习率才能从头开始训练。 假设 Trainer 实例中的学习率为1,我们将迭代中变量输出中模型参数的学习率设置为 10。我们设基本学习率为 η ,迭代输出层学习率为 10η 。
finetune_net = torchvision.models.resnet18(pretrained=True)
finetune_net.fc = nn.Linear(finetune_net.fc.in_features, 2)
nn.init.xavier_uniform_(finetune_net.fc.weight);
# 如果 `param_group=True`,输出层中的模型参数将使用十倍的学习率
def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5,
param_group=True):
train_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train'), transform=train_augs),
batch_size=batch_size, shuffle=True)
test_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'test'), transform=test_augs),
batch_size=batch_size)
devices = d2l.try_all_gpus()
loss = nn.CrossEntropyLoss(reduction="none")
if param_group:
params_1x = [param for name, param in net.named_parameters()
if name not in ["fc.weight", "fc.bias"]]
trainer = torch.optim.SGD([{'params': params_1x},
{'params': net.fc.parameters(),
'lr': learning_rate * 10}],
lr=learning_rate, weight_decay=0.001)
else:
trainer = torch.optim.SGD(net.parameters(), lr=learning_rate,
weight_decay=0.001)
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
devices)
如果遇到了GPU内存不足,可以调小batch_size
我们使用较小的学习率,通过微调预训练获得的模型参数。
train_fine_tuning(finetune_net, 5e-5)
plt.show()
loss 0.172, train acc 0.930, test acc 0.943
187.2 examples/sec on [device(type='cuda', index=0)]
为了进行比较,定义了一个相同的模型,但是将其所有模型参数初始化为随机值。 由于整个模型需要从头开始训练,因此我们需要使用更大的学习率。
scratch_net = torchvision.models.resnet18()
scratch_net.fc = nn.Linear(scratch_net.fc.in_features, 2)
train_fine_tuning(scratch_net, 5e-4, param_group=False)
loss 0.362, train acc 0.841, test acc 0.826
182.7 examples/sec on [device(type='cuda', index=0)]
意料之中,微调模型往往表现更好,因为它的初始参数值更有效。
我们介绍了各种图像分类模型。 在图像分类任务中,我们假设图像中只有一个主要物体对象,我们只关注如何识别其类别。 然而,很多时候图像里有多个我们感兴趣的目标,我们不仅想知道它们的类别,还想得到它们在图像中的具体位置。 在计算机视觉里,我们将这类任务称为目标检测(object detection)或物体检测。
目标检测在多个领域中被广泛使用。 例如,在无人驾驶里,我们需要通过识别拍摄到的视频图像里的车辆、行人、道路和障碍的位置来规划行进线路。 机器人也常通过该任务来检测感兴趣的目标。安防领域则需要检测异常目标,如歹徒或者炸弹。
import torch
from d2l import torch as d2l
import matplotlib.pyplot as plt
d2l.set_figsize()
img = plt.imread('catdog.png')
plt.imshow(img)
plt.show()
在目标检测中,我们通常使用**边界框(bounding box)**来描述对象的空间位置。 边界框是矩形的,由矩形左上角的 x 和 y 坐标以及右下角的坐标决定。 另一种常用的边界框表示方法是边界框中心的 (x,y) 轴坐标以及框的宽度和高度。
#@save
def box_corner_to_center(boxes):
"""从(左上,右下)转换到(中间,宽度,高度)"""
x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
cx = (x1 + x2) / 2
cy = (y1 + y2) / 2
w = x2 - x1
h = y2 - y1
boxes = torch.stack((cx, cy, w, h), axis=-1)
return boxes
#@save
def box_center_to_corner(boxes):
"""从(中间,宽度,高度)转换到(左上,右下)"""
cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
x1 = cx - 0.5 * w
y1 = cy - 0.5 * h
x2 = cx + 0.5 * w
y2 = cy + 0.5 * h
boxes = torch.stack((x1, y1, x2, y2), axis=-1)
return boxes
我们将根据坐标信息定义图像中狗和猫的边界框。 图像中坐标的原点是图像的左上角,右侧和向下分别是 x 和 y 轴的正方向。
# bbox是边界框的英文缩写
dog_bbox, cat_bbox = [55.0, 99.0, 261.0, 354.0], [75.0, 640.0, 317.0, 450.0]
# 定义一个辅助函数 bbox_to_rect
# 它将边界框表示成 matplotlib 的边界框格式
#@save
def bbox_to_rect(bbox, color):
# 将边界框 (左上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 = plt.imshow(img)
fig.axes.add_patch(bbox_to_rect(dog_bbox, 'blue'))
fig.axes.add_patch(bbox_to_rect(cat_bbox, 'red'));
plt.show()
目标检测算法通常会在输入图像中采样大量的区域,然后判断这些区域中是否包含我们感兴趣的目标,并调整区域边缘从而更准确地预测目标的真实边界框(ground-truth bounding box)。 不同的模型使用的区域采样方法可能不同。 这里我们介绍其中的一种方法:它以每个像素为中心生成多个大小和宽高比(aspect ratio)不同的边界框。 这些边界框被称为锚框(anchor box)
import torch
from d2l import torch as d2l
import matplotlib.pyplot as plt
torch.set_printoptions(precision=2)
torch.set_printoptions(precision=2, threshold=4)
a = torch.arange(1, 12)
print(a)
###############
tensor([ 1, 2, 3, ..., 9, 10, 11])
torch.set_printoptions(precision=2, threshold=4, edgeitems=2)
a = torch.arange(1, 12)
print(a)
###############
tensor([ 1, 2, ..., 10, 11])
假设输入图像的高度为 h ,宽度为 w 。 我们以图像的每个像素为中心生成不同形状的锚框:比例 为 s∈(0,1] ,宽高比(宽高比)为 r>0 。 那么锚框的宽度和高度分别是 ws√r 和 hs/√r 。 请注意,当中心位置给定时,已知宽和高的锚框是确定的。
要生成多个不同形状的锚框,让我们设置一系列刻度 s1,…,sn 和一系列宽高比 r1,…,rm 。 当使用这些比例和长宽比的所有组合以每个像素为中心时,输入图像将总共有 whnm 个锚框。
也就是说,以同一像素为中心的锚框的数量是 n+m−1 。 对于整个输入图像,我们将共生成 wh(n+m−1) 个锚框。
上述生成锚框的方法可以在以下 multibox_prior 函数中实现。 我们指定输入图像、尺度列表和宽高比列表,然后此函数将返回所有的锚框。
def multibox_prior(data, sizes, ratios):
in_height, in_width = data.shape[-2:]
device, num_sizes, num_ratios = data.device, len(sizes), len(ratios)
boxes_per_pixel = (num_sizes + num_ratios - 1)
size_tensor = torch.tensor(sizes, device=device)
ratio_tensor = torch.tensor(ratios, device=device)
# 为了将锚点移动到像素的中心,需要设置偏移量。
# 因为一个像素的的高为1且宽为1,我们选择偏移我们的中心0.5
offset_h, offset_w = 0.5, 0.5
steps_h = 1.0 / in_height # Scaled steps in y axis
steps_w = 1.0 / in_width # Scaled steps in x axis
# 生成锚框的所有中心点
center_h = (torch.arange(in_height, device=device) + offset_h) * steps_h
center_w = (torch.arange(in_width, device=device) + offset_w) * steps_w
shift_y, shift_x = torch.meshgrid(center_h, center_w)
shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1)
# 生成“boxes_per_pixel”个高和宽,
# 之后用于创建锚框的四角坐标 (xmin, xmax, ymin, ymax)
w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]),
sizes[0] * torch.sqrt(ratio_tensor[1:])))\
* in_height / in_width # Handle rectangular inputs
h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]),
sizes[0] / torch.sqrt(ratio_tensor[1:])))
# 除以2来获得半高和半宽
anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat(
in_height * in_width, 1) / 2
# 每个中心点都将有“boxes_per_pixel”个锚框,
# 所以生成含所有锚框中心的网格,重复了“boxes_per_pixel”次
out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y],
dim=1).repeat_interleave(boxes_per_pixel, dim=0)
output = out_grid + anchor_manipulations
return output.unsqueeze(0)
img = plt.imread('catdog.png')
h, w = img.shape[:2]
print(h, w)
X = torch.rand(size=(1, 3, h, w)) # 批量、通道、高、宽
Y = multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
print(Y.shape)
########################
733 550
torch.Size([1, 2015750, 4])(批量大小,锚框的数量,4)
2015750 == 733 * 550 * (3 + 3 - 1)
将锚框变量 Y 的形状更改为(图像高度、图像宽度、以同一像素为中心的锚框的数量,4)后,我们就可以获得以指定像素的位置为中心的所有锚框了。 在接下来的内容中,我们访问以 (148, 224) 为中心的第一个锚框。 它有四个元素:锚框左上角的 (x,y) 轴坐标和右下角的 (x,y) 轴坐标。 将两个轴的坐标分别除以图像的宽度和高度后,所得的值就介于 0 和 1 之间。
boxes = Y.reshape(h, w, 5, 4)
boxes[148, 224, 0, :]
为了显示以图像中一个像素为中心的所有锚框,我们定义了以下 show_bboxes 函数来在图像上绘制多个边界框。
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().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=9, color=text_color,
bbox=dict(facecolor=color, lw=0))
现在,我们可以绘制出图像中所有以(224, 148)为中心的锚框了。(高,宽)
d2l.set_figsize()
bbox_scale = torch.tensor((w, h, w, h))
fig = plt.imshow(img)
show_bboxes(fig.axes, boxes[224, 148, :, :] * bbox_scale,
['s=0.1, r=1', 's=0.5, r=1', 's=0.25, r=1', 's=0.1, r=2',
's=0.1, r=0.5'])
plt.show()
衡量锚框和真实边界框之间的相似性。
对于两个边界框,我们通常将他们的 Jaccard 指数称为 交并比 (intersection over union,IoU),即两个边界框相交面积与相并面积之比。交并比的取值范围在0和1之间:0表示两个边界框无重合像素,1表示两个边界框完全重合。
def box_iou(boxes1, boxes2):
box_area = lambda boxes: ((boxes[:, 1] - boxes[:, 0]) *
(boxes[:, 3] - boxes[:, 1]))
# lambda x, y: x*y 函数输入是x和y,输出是它们的积x*y
areas1 = box_area(boxes1)
areas2 = box_area(boxes2)
inter_upperlefts = torch.max(boxes1[:, None, :2], boxes2[:, :2])
inter_lowerrights = torch.min(boxes1[:, None, 2:], boxes2[:, 2:])
inters = (inter_lowerrights - inter_upperlefts).clamp(min=0)
inter_areas = inters[:, :, 0] * inters[:, :, 1]
union_areas = areas1[:, None] + areas2 - inter_areas
return inter_areas / union_areas
Python 之 lambda 函数完整详解 & 巧妙运用
在训练集中,我们将每个锚框视为一个训练样本。 为了训练目标检测模型,我们需要每个锚框的类别(class)和偏移量(offset)标签,其中前者是与锚框相关的对象的类别,后者是真实边界框相对于锚框的偏移量。在预测期间,我们为每个图像生成多个锚框,预测所有锚框的类和偏移量,根据预测的偏移量调整它们的位置以获得预测的边界框,最后只输出符合特定条件的预测边界框。
总结如下:
每一列是一个真实的物体,每一行是对应生出来锚框。每一个元素是锚框对于预测框的交并比。首先从这个矩阵中找出最大的元素,意味着第二个框用来预测第三个物体。同时划除这一行和这一列,在找出矩阵中除了这一行和这一列中的第二个最大元素,重复上述操作,直到所有的元素被划除。
def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5):
"""将最接近的真实边界框分配给锚框。"""
num_anchors, num_gt_boxes = anchors.shape[0], ground_truth.shape[0]
# 位于第i行和第j列的元素 x_ij 是锚框i和真实边界框j的IoU
jaccard = box_iou(anchors, ground_truth)
# 对于每个锚框,分配的真实边界框的张量
anchors_bbox_map = torch.full((num_anchors,), -1, dtype=torch.long,
device=device)
# 根据阈值,决定是否分配真实边界框
max_ious, indices = torch.max(jaccard, dim=1)
anc_i = torch.nonzero(max_ious >= 0.5).reshape(-1)
box_j = indices[max_ious >= 0.5]
anchors_bbox_map[anc_i] = box_j
col_discard = torch.full((num_anchors,), -1)
row_discard = torch.full((num_gt_boxes,), -1)
for _ in range(num_gt_boxes):
max_idx = torch.argmax(jaccard)
box_idx = (max_idx % num_gt_boxes).long()
anc_idx = (max_idx / num_gt_boxes).long()
anchors_bbox_map[anc_idx] = box_idx
jaccard[:, box_idx] = col_discard
jaccard[anc_idx, :] = row_discard
return anchors_bbox_map
现在我们可以为每个锚框标记分类和偏移量了。 假设一个锚框 A 被分配了一个真实边界框 B 。 一方面,锚框 A 的类将被标记为与 B 相同。 另一方面,锚框 A 的偏移量将根据 B 和 A 中心坐标的相对位置、以及这两个框的相对大小进行标记。
#@save
def offset_boxes(anchors, assigned_bb, eps=1e-6):
"""对锚框偏移量的转换。"""
c_anc = d2l.box_corner_to_center(anchors)
c_assigned_bb = d2l.box_corner_to_center(assigned_bb)
offset_xy = 10 * (c_assigned_bb[:, :2] - c_anc[:, :2]) / c_anc[:, 2:]
offset_wh = 5 * torch.log(eps + c_assigned_bb[:, 2:] / c_anc[:, 2:])
offset = torch.cat([offset_xy, offset_wh], axis=1)
return offset
如果一个锚框没有被分配真实边界框,我们只需将锚框的类标记为 “背景”类。 背景类的锚框通常被称为“负类”锚框,其余的被称为“正类”锚框。 我们使用真实边界框( labels 参数)实现以下 multibox_target 函数,来标记锚框的类和偏移量( anchors 参数)。 此函数将背景类设置为零,然后将新类的整数索引递增一。
#@save
def multibox_target(anchors, labels):
"""使用真实边界框标记锚框。"""
batch_size, anchors = labels.shape[0], anchors.squeeze(0)
batch_offset, batch_mask, batch_class_labels = [], [], []
device, num_anchors = anchors.device, anchors.shape[0]
for i in range(batch_size):
label = labels[i, :, :]
anchors_bbox_map = assign_anchor_to_bbox(
label[:, 1:], anchors, device)
bbox_mask = ((anchors_bbox_map >= 0).float().unsqueeze(-1)).repeat(
1, 4)
# 将类标签和分配的边界框坐标初始化为零
class_labels = torch.zeros(num_anchors, dtype=torch.long,
device=device)
assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32,
device=device)
# 使用真实边界框来标记锚框的类别。
# 如果一个锚框没有被分配,我们标记其为背景(值为零)
indices_true = torch.nonzero(anchors_bbox_map >= 0)
bb_idx = anchors_bbox_map[indices_true]
class_labels[indices_true] = label[bb_idx, 0].long() + 1
assigned_bb[indices_true] = label[bb_idx, 1:]
# 偏移量转换
offset = offset_boxes(anchors, assigned_bb) * bbox_mask
batch_offset.append(offset.reshape(-1))
batch_mask.append(bbox_mask.reshape(-1))
batch_class_labels.append(class_labels)
bbox_offset = torch.stack(batch_offset)
bbox_mask = torch.stack(batch_mask)
class_labels = torch.stack(batch_class_labels)
return (bbox_offset, bbox_mask, class_labels)
让我们通过一个具体的例子来说明锚箱标签。 我们在加载的图像中为狗和猫定义了地面真实边界框,其中第一个元素是类(0 代表狗,1 代表猫),其余四个元素是左上角和右下角的 (x,y) 轴坐标(范围介于 0 和 1 之间)。 我们还构建了五个锚框,用左上角和右下角的坐标进行标记: A0,…,A4 (索引从 0 开始)。 然后我们在图像中绘制这些地面真相边界框和锚框。
ground_truth = torch.tensor([[0, 0.1, 0.14, 0.47, 0.45],
[1, 0.14, 0.61, 0.58, 0.88]])
anchors = torch.tensor([[0, 0.2, 0.2, 0.3], [0.07, 0.08, 0.52, 0.52],
[0.2, 0.5, 0.53, 0.98], [0.09, 0.58, 0.7, 0.8],
[0.07, 0.7, 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'])
plt.show()
在预测期间,我们先为图像生成多个锚框,再为这些锚框一一预测类别和偏移量。 一个“预测好的边界框”则根据其中某个带有预测偏移量的锚框而生成。 下面我们实现了 offset_inverse 函数,该函数将锚框和偏移量预测作为输入,并应用逆偏移变换来返回预测的边界框坐标。
#@save
def offset_inverse(anchors, offset_preds):
"""根据带有预测偏移量的锚框来预测边界框。"""
anc = d2l.box_corner_to_center(anchors)
pred_bbox_xy = (offset_preds[:, :2] * anc[:, 2:] / 10) + anc[:, :2]
pred_bbox_wh = torch.exp(offset_preds[:, 2:] / 5) * anc[:, 2:]
pred_bbox = torch.cat((pred_bbox_xy, pred_bbox_wh), axis=1)
predicted_bbox = d2l.box_center_to_corner(pred_bbox)
return predicted_bbox
以下 nms 函数按降序对置信度进行排序并返回其索引。
#@save
def nms(boxes, scores, iou_threshold):
"""对预测边界框的置信度进行排序。"""
B = torch.argsort(scores, dim=-1, descending=True)
keep = [] # 保留预测边界框的指标
while B.numel() > 0:
i = B[0]
keep.append(i)
if B.numel() == 1: break
iou = box_iou(boxes[i, :].reshape(-1, 4),
boxes[B[1:], :].reshape(-1, 4)).reshape(-1)
inds = torch.nonzero(iou <= iou_threshold).reshape(-1)
B = B[inds + 1]
return torch.tensor(keep, device=boxes.device)
定义以下 multibox_detection 函数来将非极大值抑制应用于预测边界框。
#@save
def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5,
pos_threshold=0.009999999):
"""使用非极大值抑制来预测边界框。"""
device, batch_size = cls_probs.device, cls_probs.shape[0]
anchors = anchors.squeeze(0)
num_classes, num_anchors = cls_probs.shape[1], cls_probs.shape[2]
out = []
for i in range(batch_size):
cls_prob, offset_pred = cls_probs[i], offset_preds[i].reshape(-1, 4)
conf, class_id = torch.max(cls_prob[1:], 0)
predicted_bb = offset_inverse(anchors, offset_pred)
keep = nms(predicted_bb, conf, nms_threshold)
# 找到所有的 non_keep 索引,并将类设置为背景
all_idx = torch.arange(num_anchors, dtype=torch.long, device=device)
combined = torch.cat((keep, all_idx))
uniques, counts = combined.unique(return_counts=True)
non_keep = uniques[counts == 1]
all_id_sorted = torch.cat((keep, non_keep))
class_id[non_keep] = -1
class_id = class_id[all_id_sorted]
conf, predicted_bb = conf[all_id_sorted], predicted_bb[all_id_sorted]
# `pos_threshold` 是一个用于非背景预测的阈值
below_min_idx = (conf < pos_threshold)
class_id[below_min_idx] = -1
conf[below_min_idx] = 1 - conf[below_min_idx]
pred_info = torch.cat((class_id.unsqueeze(1),
conf.unsqueeze(1),
predicted_bb), dim=1)
out.append(pred_info)
return torch.stack(out)
现在让我们将上述算法应用到一个带有四个锚框的具体示例中。假设预测的偏移量都是零,这意味着预测的边界框即是锚框。 对于背景、狗和猫其中的每个类,我们还定义了它的预测概率。
anchors = torch.tensor([[0.1, 0.14, 0.47, 0.45], [0.08, 0.2, 0.56, 0.55],
[0.12, 0.3, 0.52, 0.5], [0.14, 0.61, 0.58, 0.88]])
offset_preds = torch.tensor([0] * anchors.numel())
cls_probs = torch.tensor([[0] * 4, # 背景的预测概率
[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'])
plt.show()
现在我们可以调用 multibox_detection 函数来执行非极大值抑制,其中阈值设置为 0.5。
output = multibox_detection(cls_probs.unsqueeze(dim=0),
offset_preds.unsqueeze(dim=0),
anchors.unsqueeze(dim=0),
nms_threshold=0.5)
fig = d2l.plt.imshow(img)
for i in output[0].detach().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)
plt.show()
#############################
tensor([[[0.00, 0.90, 0.10, 0.14, 0.47, 0.45],
[1.00, 0.90, 0.14, 0.61, 0.58, 0.88],
[-1.00, 0.80, 0.08, 0.20, 0.56, 0.55],
[-1.00, 0.70, 0.12, 0.30, 0.52, 0.50]]])
返回结果的形状是(批量大小,锚框的数量,6)。 最内层维度中的六个元素提供了同一预测边界框的输出信息。 第一个元素是预测的类索引,从 0 开始(0代表狗,1代表猫),值 -1 表示背景或在非极大值抑制中被移除了。 第二个元素是预测的边界框的置信度。 其余四个元素分别是预测边界框左上角和右下角的 (x,y) 轴坐标(范围介于 0 和 1 之间)。
实践中,在执行非极大值抑制前,我们甚至可以将置信度较低的预测边界框移除,从而减少此算法中的计算量。 我们也可以对非极大值抑制的输出结果进行后处理,例如,只保留置信度更高的结果作为最终输出。
我们以输入图像的每个像素为中心,生成了多个锚框。 基本而言,这些锚框代表了图像不同区域的样本。 然而,如果为每个像素都生成的锚框,我们最终可能会得到太多需要计算的锚框。
我们可以在输入图像中均匀采样一小部分像素,并以它们为中心生成锚框以减少图像上的锚框数量。(不是每个像素都生成)
import matplotlib.pyplot as plt
import torch
from d2l import torch as d2l
img = d2l.plt.imread('catdog.png')
h, w = img.shape[:2]
print(h, w)
#############
733 550
display_anchors函数将 均匀地 对任何输入图像中 fmap_h 行和 fmap_w 列中的像素进行采样。 以这些均匀采样的像素为中心,将会生成大小为 s(假设列表 s 的长度为 1)且宽高比( ratios )不同的锚框。
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])
display_anchors(fmap_w=2, fmap_h=2, s=[0.3])
display_anchors(fmap_w=1, fmap_h=1, s=[0.8])
plt.show()
既然我们已经生成了多尺度的锚框,我们就将使用它们来检测不同尺度下各种大小的目标。下面,我们介绍一种基于 CNN 的多尺度目标检测方法,在SSD中实现。
在某种规模上,假设我们有 c 张形状为 h×w 的特征图。 使用 5.1节 中的方法,我们生成了 hw 组锚框,其中每组都有 a 个中心相同的锚框。 例如,在 5.1节 实验的第一个尺度上,给定 10 个(通道数量) 4×4 的特征图,我们生成了 16组锚框,每组包含 3 个中心相同的锚框。 接下来,每个锚框都根据真实值边界框来标记了类和偏移量。
在当前尺度下,目标检测模型需要预测输入图像上 hw 组锚框类别和偏移量,其中不同组锚框具有不同的中心。
假设此处的 c 张特征图是 CNN 基于输入图像的正向传播算法获得的中间输出。 既然每张特征图上都有 hw 个不同的空间位置,那么相同空间位置可以看作含有 c 个单元。 根据对感受野的定义,特征图在相同空间位置的 c 个单元在输入图像上的感受野相同: 它们表征了同一感受野内的输入图像信息。 因此,我们可以将特征图在同一空间位置的 c 个单元变换为使用此空间位置生成的 a 个锚框类别和偏移量。 本质上,我们用输入图像在某个感受野区域内的信息,来预测输入图像上与该区域位置相近的锚框类别和偏移量。
当不同层的特征图在输入图像上分别拥有不同大小的感受野时,它们可以用于检测不同大小的目标。
例如,我们可以设计一个神经网络,其中靠近输出层的特征图单元具有更宽的感受野,这样它们就可以从输入图像中检测到较大的目标。
包含所有图像和CSV标签文件的香蕉检测数据集可以直接从网上下载。
import matplotlib.pyplot as plt
import os
import pandas as pd
import torch
import torchvision
from d2l import torch as d2l
d2l.DATA_HUB['banana-detection'] = (
d2l.DATA_URL + 'banana-detection.zip',
'5de26c8fce5ccdea9f91267273464dc968d20d72')
通过 read_data_bananas 函数,我们读取香蕉检测数据集。 该数据集包括一个的CSV文件,内含目标类别标签和位于左上角和右下角的真实边界框坐标。
def read_data_bananas(is_train=True):
data_dir = d2l.download_extract('banana-detection')
csv_fname = os.path.join(data_dir, 'bananas_train' if is_train
else 'bananas_val', 'label.csv')
csv_data = pd.read_csv(csv_fname)
csv_data = csv_data.set_index('img_name')
images, targets = [], []
for img_name, target in csv_data.iterrows():
images.append(torchvision.io.read_image(
os.path.join(data_dir, 'bananas_train' if is_train else
'bananas_val', 'images', f'{img_name}')))
targets.append(list(target))
return images, torch.tensor(targets).unsqueeze(1) / 256
BananasDataset 类别将允许我们创建一个自定义 Dataset 实例来加载香蕉检测数据集。
class BananasDataset(torch.utils.data.Dataset):
def __init__(self, is_train):
self.features, self.labels = read_data_bananas(is_train)
print('read' + str(len(self.features)) + (f'training examples' if
is_train else f'validation examples'))
def __getitem__(self, item):
return (self.features[item].float(), self.labels[item])
def __len__(self):
return len(self.features)
最后,我们定义 load_data_bananas 函数,来为训练集和测试集返回两个数据加载器实例。对于测试集,无须按随机顺序读取它。
#@save
def load_data_bananas(batch_size):
"""加载香蕉检测数据集。"""
train_iter = torch.utils.data.DataLoader(BananasDataset(is_train=True),
batch_size, shuffle=True)
val_iter = torch.utils.data.DataLoader(BananasDataset(is_train=False),
batch_size)
return train_iter, val_iter
让我们读取一个小批量,并打印其中的图像和标签的形状。 图像的小批量的形状为(批量大小、通道数、高度、宽度),看起来很眼熟:它与我们之前图像分类任务中的相同。 标签的小批量的形状为(批量大小, m ,5),其中 m 是数据集的任何图像中边界框可能出现的最大数量。
batch_size, edge_size = 32, 256
train_iter, _ = load_data_bananas(batch_size)
batch = next(iter(train_iter))
print(batch[0].shape, batch[1].shape)
10 幅带有真实边界框的图像。 我们可以看到在所有这些图像中香蕉的旋转角度、大小和位置都有所不同。
imgs = (batch[0][0:10].permute(0, 2, 3, 1)) / 255
axes = d2l.show_images(imgs, 2, 5, scale=2)
for ax, label in zip(axes, batch[1][0:10]):
d2l.show_bboxes(ax, [label[0][1:5] * edge_size], colors=['w'])
plt.show()
报错:
AttributeError: module 'torchvision.io' has no attribute 'read_image'
解决:
pip install torchvision==0.10.0