简介
单发多框检测(SSD)
SSD (Single Shot MultiBox Detector) 是一种目标检测算法,它可以在一张图像中同时检测多个目标。
它采用单次卷积网络(single shot convolutional network)来进行检测,因此又被称为单次检测器。
SSD 通过使用预先训练的卷积神经网络来提取图像的特征,然后使用多个不同尺度的滑动窗口来检测目标。
这种方法可以同时检测不同大小的目标,并且具有较高的检测精度。、
13.7.1. 模型
13.7.1.1. 类别预测层
%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)
"""
这段代码定义了一个名为cls_predictor的函数,它用于创建一个卷积层,用于对检测框进行分类。该函数接受三个参数:
num_inputs:输入通道数
num_anchors:锚框数量
num_classes:分类类别数
函数体内使用了 PyTorch 的 nn.Conv2d 模块来创建卷积层。 它的输入通道数为 num_inputs,
输出通道数为 num_anchors * (num_classes + 1),卷积核大小为3, padding 为1。这个卷积层用于对检测框进行分类预测。
这一步是SSD中检测部分。
"""
13.7.1.2. 边界框预测层
def bbox_predictor(num_inputs, num_anchors):
return nn.Conv2d(num_inputs, num_anchors * 4, kernel_size=3, padding=1)
"""
这段代码定义了一个名为bbox_predictor的函数,它用于创建一个卷积层,用于对检测框进行边界框预测。该函数接受两个参数:
num_inputs:输入通道数
num_anchors:锚框数量
函数体内使用了 PyTorch 的 nn.Conv2d 模块来创建卷积层。 它的输入通道数为 num_inputs,
输出通道数为 num_anchors * 4,卷积核大小为3, padding 为1。这个卷积层用于对检测框进行边界框预测。
这一步是SSD中边界框预测部分。
"""
13.7.1.3. 连结多尺度的预测
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
"""这段代码定义了一个名为 forward 的函数,它接受两个参数:
x:输入张量
block:一个由 cls_predictor 或 bbox_predictor 创建的卷积层
函数的主要作用是调用这个卷积层对输入进行卷积运算。
在这里有两个示例使用这个函数,一个使用 cls_predictor 创建卷积层,输入张量为 (2, 8, 20, 20),
锚框数量为 5,类别数为 10,输出张量的形状为 (2, 50, 20, 20)。
另一个使用 cls_predictor 创建卷积层,输入张量为 (2, 16, 10, 10),锚框数量为 3,类别数为 10,输出张量的形状为 (2, 30, 10, 10)。
"""
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)
"""
这段代码定义了两个函数 flatten_pred 和 concat_preds。
flatten_pred 函数将输入预测张量进行降维处理,将预测张量的多维度拉平成一维,这样就可以方便地将多个预测张量拼接在一起。
concat_preds 函数将多个预测张量进行拼接,使用 PyTorch 的 torch.cat 函数将多个预测张量在第一维(batch size)上进行拼接。
举个例子,假设有3个预测张量,第一个预测张量的形状为 (2, 50, 20, 20),第二个预测张量的形状为 (2, 30, 10, 10),
第三个预测张量的形状为 (2, 40, 5, 5)。在调用 concat_preds 函数之后,得到的拼接后的张量的形状为 (2, 80, 20, 20)。
这两个函数主要是为了将多个预测张量进行拼接。
"""
concat_preds([Y1, Y2]).shape
"""
concat_preds 函数将多个预测张量进行拼接,在这里,我们调用了 concat_preds([Y1, Y2]),将 Y1 和 Y2 两个预测张量拼接在一起。
对于 Y1 的形状为 (2,50,20,20),对于 Y2 的形状为 (2,30,10,10)。在拼接之后得到的拼接后的张量的形状为 (2,80,20,20),
即 Y1.shape[0] = Y2.shape[0] and Y1.shape[2:] = Y2.shape[2:].
这里第一维是batch size,第二维是80个通道,是Y1和Y2的通道数的总和,第三维和第四维是20*20,是Y1和Y2的空间维度的总和。
"""
13.7.1.4. 高和宽减半块
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)
"""
这段代码定义了一个名为 down_sample_blk 的函数, 这个函数用于创建一个下采样块。它接受两个参数:
in_channels:输入通道数
out_channels:输出通道数
函数体内创建了一个空的列表 blk,然后使用循环语句进行重复添加元素,其中每次添加了3个元素,
即一个卷积层,一个批量归一化层和一个ReLU层。每次循环完成后,更新in_channels的值为out_channels, 然后最后添加一个最大池化层.
这些层的组合是一个下采样块,这个下采样块可以用来缩小图像尺寸,同时增加深度。最后返回由这些层组成的序列模块。
这一步是SSD中下采样部分。
"""
forward(torch.zeros((2, 3, 20, 20)), down_sample_blk(3, 10)).shape
"""
这句话将输入张量 (2, 3, 20, 20) 传入名为 down_sample_blk 的函数中,并将其输出传入 forward 函数中进行处理。
down_sample_blk 函数中, in_channels = 3, out_channels = 10, 首先会在 blk 列表中添加 2 个卷积层, 2 个批量归一化层和2个ReLU层,
然后最后添加一个最大池化层。
这些层的组合是一个下采样块,这个下采样块可以用来缩小图像尺寸,同时增加深度。
输入张量的形状为 (2, 3, 20, 20),首先通过两个卷积层和批量归一化层,每次进行卷积运算后都会增加通道数,
最后经过一个最大池化层进行下采样操作,
得到的输出张量的形状为 (2, 10, 10, 10)。
因此输出的张量的形状为 (2,10,10,10),这个下采样块可以用来缩小图像尺寸,同时增加深度,这样在更深的层中可以更好的捕捉细节。
"""
13.7.1.5. 基本网络块
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
"""
这段代码定义了一个名为 base_net 的函数,该函数用于创建一个基础网络。在这里,函数内部创建了一个空列表 blk。
num_filters = [3, 16, 32, 64], 用于存储输入和输出通道数,然后使用循环语句进行重复添加元素。其中每次添加一个下采样块,
并使用num_filters[i]作为输入通道数,num_filters[i+1]作为输出通道数。
输入张量的形状为 (2, 3, 256, 256), 通过循环添加的下采样块,在经过多次下采样操作后得到的输出张量的形状为 (2, 64, 32, 32)。
所以 base_net 函数返回的是这些下采样块组成的序列模块, 输入一个大尺寸图像,经过多次下采样操作后尺寸变小,通道数变多,
这样在更深的层中可以更好的捕捉细节。
"""
13.7.1.6. 完整的模型
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
"""
这段代码定义了一个名为 get_blk 的函数,该函数用于根据输入的索引 i 返回不同的模块。
函数中使用了 if-elif 语句来控制返回不同的模块:
当 i 为 0 时,返回基础网络 base_net()。
当 i 为 1 时,返回下采样块 down_sample_blk(64, 128)。
当 i 为 4 时,返回自适应最大池化层 nn.AdaptiveMaxPool2d((1,1))。
其余情况下,返回下采样块 down_sample_
"""
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)
"""
这段代码定义了一个名为blk_forward的函数,这个函数用于在给定模块和参数的情况下进行前向计算。
函数接受五个参数:
X :输入数据
blk :需要使用的模块
size :锚框大小
ratio :锚框宽高比
cls_predictor :分类预测器
bbox_predictor :边界框预测器
首先,使用传入的模块对输入数据进行前向计算,得到输出 Y。然后,使用 d2l.multibox_prior 函数计算锚框,传入输出 Y 、
锚框大小和锚框宽高比来生成锚框。之后,使用 cls_predictor 和 bbox_predictor 对 Y 进行分类预测和边界框预测,
得到 cls_preds 和 bbox_preds。
最后,函数将得到的 Y, anchors, cls_preds, bbox_preds 作为元组返回.这个函数可以用来处理不同层的预测结果,并得到最终的预测结果。
"""
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
"""
这段代码中定义了两个列表 sizes 和 ratios。
sizes 列表中存储了不同层中锚框的大小,每个子列表中存储了该层中锚框的高和宽。
ratios 列表中存储了不同层中锚框的宽高比,每个子列表中存储了该层中锚框的宽高比。
然后, num_anchors 被赋值为总锚框数,其等于每层的锚框大小数量与宽高比数量之和,其中每层减1.
这个sizes和ratios是用来生成不同层的锚框的。
"""
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
"""
这段代码定义了一个名为 TinySSD 的类,继承了 PyTorch 的 nn.Module 类。
在 TinySSD 类的构造函数中,首先将输入的类别数量赋值给 self.num_classes。然后,定义了一个名为 idx_to_in_channels 的列表,
用于存储不同层的输入通道数。
然后,使用循环语句对于每一层进行如下操作:
通过 get_blk(i) 函数获取第 i 个模块并将其赋值给 self.blk_i 。
通过 cls_predictor函数获取第 i 个分类预测器并将其赋值给 self.cls_i 。
通过 bbox_predictor 函数获取第 i 个边界框预测器并将其赋值给 self.bbox_i 。
在 forward 函数中,对于每一层使用 blk_forward 函数进行前向计算,得到锚框、分类预测和边界框预测。
最后,将所有层的锚框、分类预测和边界框预测拼接在一起并返回。
TinySSD 类包含了一个完整的单发多框检测网络,可以对图像进行预测,并得到锚框、分类预测和边界框预测。
这些预测结果可以用来计算置信度和坐标,以确定最终的目标检测结果。
在使用TinySSD类进行预测之前,需要对该类进行训练。训练时需要准备大量标记数据,来训练分类预测和边界框预测.
"""
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)
"""
这段代码创建了一个 TinySSD 网络的实例,并将其赋值给变量 net。然后创建一个大小为 (32, 3, 256, 256) 的输入张量 X,
并使用 net 进行前向计算。
前向计算返回了三个张量:锚框、分类预测和边界框预测。最后,打印了这三个张量的形状,
分别是:(32, 5444, 4)、(32, 21776, 2) 和 (32, 21776, 4)。这些张量的大小取决于输入数据的大小和模型的设计.
"""
output anchors: torch.Size([1, 5444, 4])
output class preds: torch.Size([32, 5444, 2])
output bbox preds: torch.Size([32, 21776])
13.7.2. 训练模型
13.7.2.1. 读取数据集和初始化
batch_size = 32
train_iter, _ = d2l.load_data_bananas(batch_size)
"""
这段代码使用d2l库中的函数加载了一个名为 bananas 的数据集。其中,train_iter是一个迭代器,每次迭代返回一个batch大小的数据。
这些数据用于训练TinySSD网络。
"""
read 1000 training examples
read 100 validation examples
device, net = d2l.try_gpu(), TinySSD(num_classes=1)
trainer = torch.optim.SGD(net.parameters(), lr=0.2, weight_decay=5e-4)
"""这段代码将TinySSD网络的实例转移到 GPU 上(如果可用的话),并创建了一个新的优化器。其中,
device 是一个 PyTorch tensor 类型的变量,表示当前使用的设备(CPU 或 GPU)。
net 是 TinySSD 网络的实例。
trainer 是一个 PyTorch 优化器,使用随机梯度下降 (SGD) 算法并对 net 的参数进行优化,其学习率为 0.2,权值衰减系数为 5e-4.
此后,可以使用训练数据和优化器训练网络,并使用验证数据进行评估,以提高模型性能.
"""
13.7.2.2. 定义损失函数和评价函数
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
"""
这段代码定义了两个损失函数:
cls_loss是交叉熵损失函数,用于计算类别预测和标签之间的差距。
bbox_loss是 L1 损失函数,用于计算边界框预测和标签之间的差距。
然后定义了 calc_loss 函数,用于计算类别预测和边界框预测之间的差距,并返回总损失。
其中,cls_preds 是类别预测,cls_labels 是类别标签,bbox_preds 是边界框预测,
bbox_labels 是边界框标签,bbox_masks 是边界框掩码。
其中 cls_loss(cls_preds.reshape(-1, num_classes),cls_labels.reshape(-1)) 计算了每个样本的交叉熵损失。
reshape(batch_size, -1) 得到了每个样本的所有锚框的总损失和,mean(dim=1)得到了每个样本的平均损失。
bbox_loss(bbox_preds * bbox_masks,bbox_labels * bbox_masks) 计算了每个样本的边界框的 L1 损失,
其中 bbox_masks 用于掩码掉无效的锚框。
mean(dim=1)得到了每个样本的平均边界框损失。
最终,cls 和 bbox 分别是每个样本的类别损失和边界框损失,它们相加得到了总损失。
"""
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())
"""
这段代码定义了两个评估函数,用于在训练期间评估类别预测和边界框预测的准确性:
cls_eval 函数使用 PyTorch 的 argmax 函数计算每个预测的类别与标签之间的匹配数。它返回一个浮点数,表示正确预测的数量。
bbox_eval 函数使用 PyTorch 的 abs 函数计算预测边界框和标签之间的绝对差值。它返回一个浮点数,表示所有边界框的绝对误差之和。
它们将在训练时使用,来评估类别预测和边界框预测的准确性,并通过这两个函数来监控训练过程的进度。
"""
13.7.2.3. 训练模型
num_epochs, timer = 20, d2l.Timer()
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['class error', 'bbox mae'])
net = net.to(device)
for epoch in range(num_epochs):
metric = d2l.Accumulator(4)
net.train()
for features, target in train_iter:
timer.start()
trainer.zero_grad()
X, Y = features.to(device), target.to(device)
anchors, cls_preds, bbox_preds = net(X)
bbox_labels, bbox_masks, cls_labels = d2l.multibox_target(anchors, Y)
l = calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels,
bbox_masks)
l.mean().backward()
trainer.step()
metric.add(cls_eval(cls_preds, cls_labels), cls_labels.numel(),
bbox_eval(bbox_preds, bbox_labels, bbox_masks),
bbox_labels.numel())
cls_err, bbox_mae = 1 - metric[0] / metric[1], metric[2] / metric[3]
animator.add(epoch + 1, (cls_err, bbox_mae))
print(f'class err {cls_err:.2e}, bbox mae {bbox_mae:.2e}')
print(f'{len(train_iter.dataset) / timer.stop():.1f} examples/sec on '
f'{str(device)}')
"""
上述代码是训练TinySSD模型的代码,它包括以下几个部分:
循环每个epoch:
定义一个Accumulator来累加训练精度的和,训练精度的和中的示例数,绝对误差的和,绝对误差的和中的示例数
将网络设置为训练模式
循环每个batch:
将梯度清零
将输入数据和标签移动到GPU
前向传播,生成多尺度的锚框,为每个锚框预测类别和偏移量
为每个锚框标注类别和偏移量
根据类别和偏移量的预测和标注值计算损失函数
反向传播,计算梯度并更新网络参数
累加训练精度的和,训练精度的和中的示例数,绝对误差的和,绝对误差的和中的示例数
计算类别错误率和偏移量绝对平均误差
将这些指标添加到动画器中
输出最终的类别错误率和偏移量绝对平均误差
输出每秒处理的样本数
首先使用训练数据进行训练,每个epoch都需要对所有训练数据进行前向传播和反向传播。
对于每个样本,都需要计算预测的类别和偏移量,并与标签进行比较以计算损失函数。在每个epoch结束后,
会计算并记录分类误差和坐标误差,并将结果加入动画中。最后,使用计时器计算每秒处理样本数。
"""
class err 3.33e-03, bbox mae 3.34e-03
7872.2 examples/sec on cuda:0
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RL48kq0K-1674305091964)(output_17_2.svg)]](https://i-blog.csdnimg.cn/blog_migrate/afa2156f4cc72fcce66aab65e2a9de02.png)
13.7.3. 预测目标
X = torchvision.io.read_image(r'D:\pytorch_Pycharm\imgs\banana2.png').unsqueeze(0).float()
img = X.squeeze(0).permute(1, 2, 0).long()
"""
这段代码读取了一张图片“banana2.jpg”,然后将其转化为一个4维的张量(batch_size, channel, height, width)。
将第一维的batch size设置为1,然后将其转换为3维张量(channel, height, width),
最后使用permute函数将其转换为(height, width, channel)并转换为long类型。
"""
def predict(X):
net.eval()
anchors, cls_preds, bbox_preds = net(X.to(device))
cls_probs = F.softmax(cls_preds, dim=2).permute(0, 2, 1)
output = d2l.multibox_detection(cls_probs, bbox_preds, anchors)
idx = [i for i, row in enumerate(output[0]) if row[0] != -1]
return output[0, idx]
output = predict(X)
"""
这段代码中,首先设置网络为评估模式,然后将输入图片X传入网络中并得到输出anchors, cls_preds, bbox_preds。
接着使用softmax函数对cls_preds进行归一化,使其成为概率值。由于softmax函数默认在最后一维进行softmax,
而在这里是在第二维,所以使用permute函数将其转换为(batch_size, num_anchors, num_classes)
最后,使用multibox_detection函数将预测的类别概率和偏移量转换为边界框。由于预测的类别可能为背景,
所以还需要将其过滤掉,即选出输出中类别不为背景的边界框。
我们使用列表推导式从输出结果中提取出所有预测类别不为-1的行的索引。
对于每一行的输出,第一个元素是预测的类别,如果预测类别为-1,表示当前行不是有效预测结果。
因此,我们只保留预测类别不为-1的行,这些行才是有效预测结果。
"""
def display(img, output, threshold):
d2l.set_figsize((5, 5))
fig = d2l.plt.imshow(img)
for row in output:
score = float(row[1])
if score < threshold:
continue
h, w = img.shape[0:2]
bbox = [row[2:6] * torch.tensor((w, h, w, h), device=row.device)]
d2l.show_bboxes(fig.axes, bbox, '%.2f' % score, 'w')
display(img, output.cpu(), threshold=0.4)
"""
这段代码是将图片和预测结果显示在一起。其中,img是原始图片,output是模型预测出的结果,
threshold是过滤掉置信度过低的预测结果的阈值。在显示时,会循环遍历output中的每一行,对于每一行的置信度都要比较是否大于阈值。
如果置信度大于阈值,则显示该预测结果的边界框。
"""
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1WvgRVqI-1674305091965)(output_20_1.svg)]](https://i-blog.csdnimg.cn/blog_migrate/00c70d934fff4e38a4680ce1fe074687.png)