失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > 动手学深度学习——目标检测 SSD R-CNN Fast R-CNN Faster R-CNN Mask R-CNN

动手学深度学习——目标检测 SSD R-CNN Fast R-CNN Faster R-CNN Mask R-CNN

时间:2019-10-28 06:54:44

相关推荐

动手学深度学习——目标检测 SSD R-CNN Fast R-CNN Faster R-CNN Mask R-CNN

来源:13.4. 锚框 — 动手学深度学习 2.0.0-beta1 documentation

目标检测:锚框算法原理与实现、SSD、R-CNN_神洛华的博客

目录

目标检测简介

目标检测模型

​编辑​编辑目标检测数据集

数据集示例

目标检测评价指标

目标检测研究方向

基本概念

边界框bounding box

转换函数

画出边界框

锚框anchor box

以每个像素为中心点生成锚框

画出锚框

交并比(IoU)

训练:使用训练数据标记锚框

为锚框确定对应的真实边界框

确定锚框相对于真实边界框的偏移量

为锚框标记类别和偏移量

举例测试

预测:为图片生成预测边界框

使用锚框和偏移量生成预测边界框

使用非极大值抑制处理预测边界框

举例测试

算法过程总结

目标检测算法:SDD(单发多框检测)

特点

多尺度目标检测

SSD模型搭建

类别预测层

边界框预测层

连结多尺度的预测

前向传播

完整的模型

训练模型

读取数据集和初始化

定义损失函数和评价函数

训练

预测目标

区域卷积神经网络(R-CNN)

R-CNN

Fast R-CNN

RoI 池化层

Faster R-CNN

Mask R-CNN

小结

需要注意的函数

对Series对象使用list

torch.set_printoptions

torch.meshgrid

reshape(-1)

repeat和repeat_interleave

扩维后相减操作

torch.stack()

切片时使用None

unique

列表和常数相乘

目标检测简介

很多时候图像里有多个我们感兴趣的目标,我们不仅想知道它们的类别,还想得到它们在图像中的具体位置。 在计算机视觉里,我们将这类任务称为目标检测(object detection)或目标识别(object recognition)。

目标检测模型

深度学习目标检测方法分为分为Anchor-Based(锚框法)Anchor-Free(无锚框)两大类,根据有无区域提案阶段划分为双阶段模型单阶段检测模型。

双阶段模型:区域检测模型将目标检测任务分为区域提案生成、特征提取和分类预测三个阶段。 在区域提案生成阶段,检测模型利用搜索算法如选择性搜索(SelectiveSearch,SS)、EdgeBoxes、区域提案网络(Region Proposal Network,RPN) 等在图像中搜寻可能包含物体的区域。在特征提取阶段,模型利用深度卷积网络提取区域提案中的目标特征。在分类预测阶段,模型从预定义的类别标签对区域提案进行分类和边框信息预测。单阶段模型:单阶段检测模型联合区域提案和分类预测,输入整张图像到卷积神经网络中提取特征,最后直接输出目标类别和边框位置信息。这类代表性的方法有:YOLO、SSD和CenterNet等。

目标检测评价指标

当前用于评估检测模型的性能指标主要有帧率每秒(Frames Per Second,FPS)、准确率(accuracy)、精确率(precision)、召回率(recall)、平均精度(Average Precision,AP)、平均 精度均值(mean Average Precision,mAP)等。

FPS即每秒识别图像的数量,用于评估目标检测模型的检测速度;P-R曲线:以Recall、Precision为横纵坐标的曲线

如下图所示,当检测框和标注框的IoU>设定阈值(比如0.3)时,可以认为这个检测框正确检测出物体。IoU>=阈值的检测框的数量就是TP。

AP(Average Precision):对不同召回率点上的精确率进行平均,在PR曲线图上表现为某一类别的 PR 曲线下的面积;mAP(mean Average Precision):所有类别AP的均值

目标检测研究方向

目标检测方法可分为检测部件、数据增强、优化方法和学习策略四个方面 。其中检测部件包含基准模型和基准网络;数据增强包含几何变换、光学变换等;优化方法包含特征图、上下文模型、边框优化、区域提案方法、类别不平衡和训练策略六个方面,学习策略涵盖监督学习、弱监督学习和无监督学习。

目标检测数据集

目前主流的通用目标检测数据集有PASCAL VOC、ImageNet、MS COCO(80类物体,330K图片,所有图片共标注1.5M物体)、Open Images和Objects365。

目标检测数据集的常见表示:每一行表示一个物体,对于每一个物体而言,用“图片文件名,物体类别,边缘框”表示,由于边缘框用4个数值表示,因此对于每一行的那一个物体而言,需要用6个数值表示

数据集示例

收集并标记了一个小型数据集。 首先,拍摄了一组香蕉的照片,并生成了1000张不同角度和大小的香蕉图像。 然后,我们在一些背景图片的随机位置上放一张香蕉的图像。 最后,在图片上为这些香蕉标记了边界框。

label.csv格式如下:

读出来csv_data数据集格式如下:

定义三个辅助函数:读取数据,创建dataset和创建dataloader。

%matplotlib inlineimport osimport pandas as pdimport torchimport torchvisionfrom PIL import Imagefrom d2l import torch as d2limport torchvision.transforms as transformstorch.set_printoptions(2) # 精简输出精度d2l.DATA_HUB['banana-detection'] = (d2l.DATA_URL + 'banana-detection.zip','5de26c8fce5ccdea9f91267273464dc968d20d72')def read_data_bananas(is_train=True):"""读取香蕉检测数据集中的图像和标签"""# Image.open()读出来的图片是PIL格式,要转换为tensor格式totensor = transforms.ToTensor() data_dir = d2l.download_extract('banana-detection')csv_fname = os.path.join(data_dir, 'bananas_train' if is_trainelse '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(totensor(Image.open(os.path.join(data_dir, 'bananas_train' if is_train else'bananas_val', 'images', f'{img_name}'))))# 这里的target包含(类别,左上角x,左上角y,右下角x,右下角y),# 其中所有图像都具有相同的香蕉类(索引为0)targets.append(list(target)) # target格式是Series,用list函数变成列表格式#返回图片和标签tensor。/256是除以高宽(图片是256*256),得到一个0-1的数return images, torch.tensor(targets).unsqueeze(1) / 256class 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' ifis_train else f' validation examples'))def __getitem__(self, idx):return (self.features[idx].float(), self.labels[idx])def __len__(self):return len(self.features)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

读取数据的时候遇到问题No such operator image::read_file,解决方法参考:No such operator image::read_file问题解决_iwill323的博客-CSDN博客

读取一个小批量,并打印其中的图像和标签的形状。

batch_size, edge_size = 32, 256train_iter, _ = load_data_bananas(batch_size)batch = next(iter(train_iter))batch[0].shape, batch[1].shape

结果:(torch.Size([32, 3, 256, 256]), torch.Size([32, 1, 5]))

图像的小批量batch[0]的形状为(批量大小、通道数、高度、宽度)。 标签的小批量batch[1]的形状为(批量大小,m,5),其中m是数据集的任何图像中边界框可能出现的最大数量。

通常来说,图像可能拥有不同数量的主体,则有不同数据的边界框,这样会造成每个批量标签不一样。所以限制每张图片主体最多有m个。对于不到m个主体的图像将被非法边界框填充。这样,每个边界框的标签将被长度为5的数组表示, 即[𝑙𝑎𝑏𝑒𝑙,𝑥𝑚𝑖𝑛,𝑦𝑚𝑖𝑛,𝑥𝑚𝑎𝑥,𝑦𝑚𝑎𝑥] (坐标值域在0到1之间)。这样每个批次物体数量一样。 对于香蕉数据集而言,由于每张图像上只有一个边界框,因此 𝑚=1 。

展示10幅带有真实边界框的图像

imgs = (batch[0][0:10].permute(0, 2, 3, 1)) # 不能除以255axes = 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'])

基本概念

边界框bounding box

在目标检测中,我们通常使用边界框(bounding box)来描述对象的空间位置。 边界框是矩形的,两种常用的边界框表示:(中心,宽度,高度)和(左上,右下)。坐标的原点是图像的左上角,向右的方向为𝑥轴的正方向,向下的方向为𝑦轴的正方向。

转换函数

定义在这两种表示法之间进行转换的函数:box_corner_to_center从两角表示法转换为中心宽度表示法,而box_center_to_corner反之。 输入参数boxes可以是长度为4的张量,也可以是形状为(𝑛,4)的二维张量,其中𝑛是边界框的数量。

def box_corner_to_center(boxes):"""从(左上,右下)转换到(中间,宽度,高度)"""x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]cx = (x1 + x2) / 2cy = (y1 + y2) / 2w = x2 - x1h = y2 - y1boxes = torch.stack((cx, cy, w, h), axis=-1) # 其他维度不变,-1维叠在一起return boxesdef box_center_to_corner(boxes):"""从(中间,宽度,高度)转换到(左上,右下)"""cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]x1 = cx - 0.5 * wy1 = cy - 0.5 * hx2 = cx + 0.5 * wy2 = cy + 0.5 * hboxes = torch.stack((x1, y1, x2, y2), axis=-1)return boxes

画出边界框

定义一个辅助函数bbox_to_rect,将边界框表示成matplotlib的边界框格式。

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 = d2l.plt.imshow(img)fig.axes.add_patch(bbox_to_rect(dog_bbox, 'blue'))fig.axes.add_patch(bbox_to_rect(cat_bbox, 'red'));

锚框anchor box

目标检测算法通常会在输入图像中采样大量的区域,然后判断这些区域中是否包含我们感兴趣的目标。 不同的模型使用的区域采样方法可能不同, 这里介绍其中的一种方法:以每个像素为中心,生成多个缩放比和宽高比(aspect ratio)不同的边界框。 这些边界框被称为锚框(anchor box)。

读取图片和已经标记好的边缘框生成大量锚框,每个锚框是一个训练样本预测每个锚框是否包含目标物体如果是,则预测从锚框到实际边界框的偏移量

以每个像素为中心点生成锚框

假设输入图像的高度为ℎ,宽度为𝑤,缩放比(scale)为𝑠∈(0,1](即锚框占图片大小的比例),宽高比(aspect ratio)为𝑟>0。 那么锚框的宽度和高度分别是ws√r和hs/√r(二者相除保证高宽比是r)。设置缩放比取值𝑠1,…,𝑠𝑛和宽高比取值𝑟1,…,𝑟𝑚。当使用这些比例和长宽比的所有组合以每个像素为中心时,输入图像将总共有 whnm个锚框,计算复杂性很容易过高。于是只考虑包含𝑠1或𝑟1的组合:(𝑠1,𝑟1), (𝑠1,𝑟2), …, (𝑠1,𝑟𝑚), (𝑠2,𝑟1), (𝑠3,𝑟1), …, (𝑠𝑛,𝑟1)。也就是说,以同一像素为中心的锚框的数量是𝑛+𝑚−1。 对于整个输入图像,我们将共生成𝑤ℎ(𝑛+𝑚−1)个锚框。

s1​、r1​是最合适的缩放比和高宽比。比如选取锚框的sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5]。 s1​=0.75、r1​=1是最合适的取值,是一定要选的。

multibox_prior:指定输入图像、尺寸列表和宽高比列表,然后返回所有的锚框。函数对data的使用其实就是取出图像的高和宽(in_height和in_width),生成锚框中心点的时候,将高和宽分别分为in_height和in_width份,所以是在每个像素点上生成锚框。

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.5offset_h, offset_w = 0.5, 0.5steps_h = 1.0 / in_height # 在y轴上缩放步长steps_w = 1.0 / in_width # 在x轴上缩放步长# 生成锚框的所有中心点 center_h = (torch.arange(in_height, device=device) + offset_h) * steps_hcenter_w = (torch.arange(in_width, device=device) + offset_w) * steps_wshift_y, shift_x = torch.meshgrid(center_h, center_w, indexing='ij')shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1) # shift_y和shift_x对应元素组合就是中心点。reshape(-1)变成一维,shape:[center_h*center_w]# 生成“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 # 只包含 r1 或 s1 的组合 shape:[boxes_per_pixel] h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]),sizes[0] / torch.sqrt(ratio_tensor[1:])))# 除以2来获得半高和半宽# torch.stack:扩展0维,并做拼接,转置后的形状:[boxes_per_pixel,4],每一行是一个anchor# 上面是对于一个像素点的。像素点总共有in_height*in_width个,于是行复制in_height*in_width倍,列数不变# repeat复制的时候是将原矩阵整体复制,在不同维度上按规定的数量堆积# 每boxes_per_pixel行就对应boxes_per_pixel种偏移方式anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat(in_height * in_width, 1) / 2# 每个中心点都将有“boxes_per_pixel”个锚框,所以重复“boxes_per_pixel”次来生成含所有锚框中心的网格 # repeat_interleave复制的时候是按照规定的维度(行),先将第一行复制boxes_per_pixel次,再将第二行复制boxes_per_pixel次# 于是每boxes_per_pixel行就对应同一个像素点的中心位置# out_grid 行:in_height*in_width*boxes_per_pixel 列:4out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y],dim=1).repeat_interleave(boxes_per_pixel, dim=0) # 中心位置加四个偏移量output = out_grid + anchor_manipulationsreturn output.unsqueeze(0)

应用举例

img = d2l.plt.imread('../img/catdog.jpg')h, w = img.shape[:2]print(h, w) # 561 728X = torch.rand(size=(1, 3, h, w))Y = multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])Y.shape

torch.Size([1, 2042040, 4])

将锚框变量Y的形状更改为(图像高度,图像宽度,以同一像素为中心的锚框的数量,4)后,可以获得以指定像素的位置为中心的所有锚框。 在接下来的内容中,访问以(250,250)为中心的第一个锚框。 它有四个元素:锚框左上角的(𝑥,𝑦)轴坐标和右下角的(𝑥,𝑦)轴坐标,他们都分别除以了图像的宽度和高度,所得的值介于0和1之间。

boxes = Y.reshape(h, w, 5, 4)boxes[250, 250, 0, :]

tensor([0.06, 0.07, 0.63, 0.82])

画出锚框

显示以图像中以某个像素为中心的所有锚框

def show_bboxes(axes, bboxes, labels=None, colors=None):"""显示所有边界框"""def _make_list(obj, default_values=None):if obj is None:obj = default_valueselif not isinstance(obj, (list, tuple)):obj = [obj]return objlabels = _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))

变量boxes中𝑥轴和𝑦轴的坐标值已分别除以图像的宽度和高度。 绘制锚框时,我们需要恢复它们原始的坐标值,因此,在下面定义了变量bbox_scale。 现在可以绘制出图像中所有以(250,250)为中心的锚框了

d2l.set_figsize()bbox_scale = torch.tensor((w, h, w, h))fig = d2l.plt.imshow(img)show_bboxes(fig.axes, boxes[250, 250, :, :] * bbox_scale,['s=0.75, r=1', 's=0.5, r=1', 's=0.25, r=1', 's=0.75, r=2','s=0.75, r=0.5'])

交并比(IoU)

对于两个边界框,通常将它们的杰卡德系数称为交并比(intersection over union,IoU)。给定集合A和B,交集的大小除以并集的大小:

给定两个锚框或边界框的列表,以下box_iou函数将在这两个列表中计算它们成对的交并比。

def box_iou(boxes1, boxes2):"""计算两个锚框或边界框列表中成对的交并比"""#计算box面积box_area = lambda boxes: ((boxes[:, 2] - boxes[:, 0]) *(boxes[:, 3] - boxes[:, 1])) # boxes1,boxes2:(boxes1的数量,4) (boxes2的数量,4) # areas1,areas2:(boxes1的数量,), (boxes2的数量,) areas1 = box_area(boxes1)areas2 = box_area(boxes2)# inter_upperlefts,inter_lowerrights,inters的形状:(boxes1的数量,boxes2的数量,2)# 切片的时候使用None起到升维的作用。升维的效果是boxes1的每一行都和boxes2的所有行比较(前2个元素)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和union_areas的形状:(boxes1的数量,boxes2的数量)inter_areas = inters[:, :, 0] * inters[:, :, 1]# areas1[:, None] + areas2 结果形状:(boxes1的数量,boxes2的数量)union_areas = areas1[:, None] + areas2 - inter_areas return inter_areas / union_areas

训练:使用训练数据标记锚框

在训练时,训练集带有真实边界框(ground-truth bounding box)的位置及其物体类别的标签。为每个图像生成多个锚框后,通过神经网络预测每个锚框的目标类别和相对于真实边界框的偏移量,然后与真实边界框比较得到每个锚框真实的类别和偏移量,从而计算预测损失,更新各种参数。下面说明如何使用训练数据标记锚框。

为锚框确定对应的真实边界框

将锚框和所有真实边界框比较,把最接近的真实边界框分配给锚框,用该真实边界框的位置和类别标签标记锚框。

给定图像,假设生成好的锚框是𝐴1,𝐴2,…,𝐴𝑛𝑎,真实边界框是𝐵1,𝐵2,…,𝐵𝑛𝑏,其中𝑛𝑎≥𝑛𝑏。 让我们定义一个矩阵𝐗∈ℝ𝑛𝑎×𝑛𝑏,其中元素𝑥𝑖𝑗是锚框𝐴𝑖和真实边界框𝐵𝑗的IoU。 该算法包含以下步骤:

在矩阵𝐗中找到最大的元素,行索引和列索引分别表示为𝑖1和𝑗1。将真实边界框𝐵𝑗1分配给锚框𝐴𝑖1,丢弃矩阵中𝑖1行和𝑗1列中的所有元素。在矩阵𝐗中找到剩余元素中最大的元素,行索引和列索引分别表示为𝑖2和𝑗2。将真实边界框𝐵𝑗2分配给锚框𝐴𝑖2,并丢弃矩阵中𝑖2行和𝑗2列中的所有元素。继续,直到丢弃掉矩阵𝐗中𝑛𝑏列中的所有元素。此时,我们已经为这𝑛𝑏个锚框各自分配了一个真实边界框。遍历剩下的𝑛𝑎−𝑛𝑏个锚框。例如,给定任何锚框𝐴𝑖,在矩阵𝐗的第𝑖行中找到与𝐴𝑖的IoU最大的真实边界框𝐵𝑗,只有当此IoU大于预定义的阈值时,才将𝐵𝑗分配给𝐴𝑖。

例子:矩阵𝐗中的最大值为𝑥23、𝑥71、𝑥54、𝑥92,之后,只需要遍历剩余的锚框𝐴1, 𝐴3, 𝐴4, 𝐴6, 𝐴8,然后根据阈值确定是否为它们分配真实边界框

其实可以考虑这样的算法:对于每一个anchor,找出与其IoU最大的真实边界框,根据阈值,决定是否配对(也就是只采取上面算法的第四步)。这种算法存在问题:可能一个真实边界框和几个anchor的IoU都很大,于是用它给这几个anchor标注,但是其他真实边界框能配对的anchor变少了。其实有一个anchor来预测一个真实边界框就足够了,不如将其他anchor释放掉,这样其他真实边界框能配对的anchor选择空间更大了。

下面的代码和上面介绍的步骤不太一样。for循环之前执行的是上面的第4步,for循环执行的是第1-3步。顺序颠倒,是因为第4步实现的时候,对每一行都进行max操作,但是对于1-3步已经配对的一些anchor来说,分配的真实边界框并不是最大 IoU,所以第4步会对这些anchor产生错误,因此将第4步提前了。

def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5):"""将最接近的真实边界框分配给锚框ground_truth:边界框 ,anchors:锚框iou_threshold=0.5表示某个锚框和任何其它锚框小于0.5,就把它删掉"""num_anchors, num_gt_boxes = anchors.shape[0], ground_truth.shape[0]# 计算所有锚框和边界框的IoU,生成IoU矩阵,元素xij是锚框i和真实边界框j的IoUjaccard = box_iou(anchors, ground_truth)# 对于每个锚框,记录分配的真实边界框的索引anchors_bbox_map = torch.full((num_anchors,), -1, dtype=torch.long,device=device) # 给每一个anchor分配交并比最大的真实边界框,并且经过阈值筛选max_ious, indices = torch.max(jaccard, dim=1)anc_i = max_ious >= iou_threshold anchors_bbox_map[anc_i] = indices[anc_i]# 遍历矩阵中最大IoU,将每个真实边界框分配给与其最大IoU的anchorcol_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() # max_idx位于的列,代表边界框的索引anc_idx = (max_idx / num_gt_boxes).long() # max_idx位于的行,代表anchor的索引anchors_bbox_map[anc_idx] = box_idx # 配对jaccard[:, box_idx] = col_discard # 配对完了,将二者从矩阵中丢弃jaccard[anc_idx, :] = row_discardreturn anchors_bbox_map

确定锚框相对于真实边界框的偏移量

假设一个锚框𝐴被分配了一个真实边界框𝐵。 一方面,锚框𝐴的类别将被标记为与𝐵相同。 另一方面,锚框𝐴的偏移量将根据𝐵和𝐴中心坐标的相对位置以及这两个框的相对大小进行标记。给定框𝐴和𝐵,中心坐标分别为(𝑥𝑎,𝑦𝑎)和(𝑥𝑏,𝑦𝑏),宽度分别为𝑤𝑎和𝑤𝑏,高度分别为ℎ𝑎和ℎ𝑏。 我们可以将𝐴的偏移量标记为:

其中常量的默认值为𝜇𝑥=𝜇𝑦=𝜇𝑤=𝜇ℎ=0,𝜎𝑥=𝜎𝑦=0.1,𝜎𝑤=𝜎ℎ=0.2。这样做使数值分的比较开,均值方差都比较好做预测。

def offset_boxes(anchors, assigned_bb, eps=1e-6):"""对锚框偏移量的转换"""c_anc = box_corner_to_center(anchors)c_assigned_bb = 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

为锚框标记类别和偏移量

如果一个锚框没有被分配真实边界框,那么只需将锚框的类别标记为“背景”(background)。 背景类别的锚框通常被称为“负类”锚框,其余的被称为“正类”锚框。使用以下multibox_target函数,来标记锚框的类别和偏移量anchors参数)。 此函数将背景类别的索引设置为零,然后将新类别的整数索引递增一。

assign_anchor_to_bbox为每个anchor获得了对应的真实边界框标号,multibox_target要做的就是把真实边界框的类别和位置取出来,然后为anchor确定类别、真实偏移量,还有就是mask矩阵用于mask背景anchor

def multibox_target(anchors, labels):"""使用真实边界框标记锚框"""batch_size, anchors = labels.shape[0], anchors.squeeze(0) # 所有批次使用同一套anchors?batch_offset, batch_mask, batch_class_labels = [], [], []device, num_anchors = anchors.device, anchors.shape[0]for i in range(batch_size):# labels:第0维大小是批量的数量(可能是图片的数量), 第1维大小是真实边界框的数量# 第2维是每个真实边界框的类别(第0个元素)和位置label = labels[i, :, :]# anchors_bbox_map是一维向量,记录了给每个anchor分配的真实边界框anchors_bbox_map = assign_anchor_to_bbox(label[:, 1:], anchors, device)# 初始化给anchor分配的类标签和的边界框为零class_labels = torch.zeros(num_anchors, dtype=torch.long, device=device)assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32, device=device)# 使用真实边界框来标记锚框的类别# anchors_bbox_map小于0对应的是没有配对真实边界框的anchor。小于IoU阈值则分配的值为-1indices_true = torch.nonzero(anchors_bbox_map >= 0) bb_idx = anchors_bbox_map[indices_true] # 索引与anchor配对的边界框class_labels[indices_true] = label[bb_idx, 0].long() + 1 # 0是留给背景的,所以其他分类标签要加一batch_class_labels.append(class_labels)# 有的anchor对应的是背景,要对其使用mask。重复4遍是因为每个anchor有四个offsetbbox_mask = ((anchors_bbox_map >= 0).float().unsqueeze(-1)).repeat(1, 4) batch_mask.append(bbox_mask.reshape(-1))# 偏移量转换并使用bbox_mask筛选assigned_bb[indices_true] = label[bb_idx, 1:] # anchors是最开始给定的,assigned_bb是给每个anchor分配的真实边界框的坐标。# 有的anchor没有分配到真实边界框,算出来的offset没意义,所以要mask掉offset = offset_boxes(anchors, assigned_bb) * bbox_maskbatch_offset.append(offset.reshape(-1)) 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)

上面函数返回:锚框对应真实边界框的偏移量offset,bbox_mask=0表示锚框是背景框,不需要预测类别;bbox_mask=1表示锚框对应一个真实边缘框。class_labels 表示锚框对应的类别

举例测试

定义真实边界框,其中第一个元素是类别(0代表狗,1代表猫),其余四个元素是左上角和右下角的(𝑥,𝑦)轴坐标(范围介于0和1之间)。

构建了五个锚框,用左上角和右下角的坐标进行标记:𝐴0,…,𝐴4(索引从0开始)

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']);

使用上面定义的multibox_target函数,使用狗和猫的真实边界框,标注这些锚框的分类和偏移量。 在这个例子中,背景、狗和猫的类索引分别为0、1和2。注意输入参数增加了一个维度(批量大小)

#unsqueeze表示增加一个维度labels = multibox_target(anchors.unsqueeze(dim=0),ground_truth.unsqueeze(dim=0))print(labels[0])print(labels[1])print(labels[2])

返回的结果中有三个元素,都是张量格式

第三个元素包含标记的输入锚框的类别。 首先,在所有的锚框和真实边界框配对中,锚框𝐴4与猫的真实边界框的IoU是最大的。 因此,𝐴4的类别被标记为猫。在剩下的配对中,锚框𝐴1和狗的真实边界框有最大的IoU。 因此,𝐴1的类别被标记为狗。遍历剩下的三个未标记的锚框:𝐴0、𝐴2和𝐴3。 对于𝐴0,与其拥有最大IoU的真实边界框的类别是狗,但IoU低于预定义的阈值(0.5),因此该类别被标记为背景; 对于𝐴2,与其拥有最大IoU的真实边界框的类别是猫,IoU超过阈值,所以类别被标记为猫; 对于𝐴3,与其拥有最大IoU的真实边界框的类别是猫,但值低于阈值,因此该类别被标记为背景。

tensor([[0, 1, 2, 0, 2]])

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

tensor([[0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 0., 1., 1.,1., 1.]])

第一个元素包含了为每个锚框标记的四个偏移值。 负类锚框的偏移量被标记为零。

#背景类偏移为0tensor([[-0.00e+00, -0.00e+00, -0.00e+00, -0.00e+00, 1.40e+00, 1.00e+01,2.59e+00, 7.18e+00, -1.20e+00, 2.69e-01, 1.68e+00, -1.57e+00,-0.00e+00, -0.00e+00, -0.00e+00, -0.00e+00, -5.71e-01, -1.00e+00,4.17e-06, 6.26e-01]])

预测:为图片生成预测边界框

预测的时候,为每个锚框预测类别和偏移量,根据预测的偏移量生成预测边界框,最后只输出符合特定条件的预测边界框

使用锚框和偏移量生成预测边界框

预测好的边界框则根据其中某个带有预测偏移量的锚框而生成。下面的offset_inverse函数, 将锚框和偏移量作为输入,应用逆偏移变换来返回预测的边界框坐标:

def offset_inverse(anchors, offset_preds):"""根据锚框四角坐标和预测偏移量,计算预测边界框"""anc = 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 = box_center_to_corner(pred_bbox)return predicted_bbox

使用非极大值抑制处理预测边界框

当有许多锚框时,可能会输出许多相似的具有明显重叠的预测边界框。 为了简化输出,我们可以使用非极大值抑制(non-maximum suppression,NMS)合并属于同一目标的预测边界框。

对于一个预测边界框𝐵,目标检测模型会计算每个类别的预测概率。 假设最大的预测概率为𝑝,称为预测边界框𝐵的置信度(confidence),将所有预测的非背景边界框都按置信度降序排序(重要的不是每个预测边界框预测的类别,而是置信度大小),生成列表𝐿。然后通过以下步骤操作排序列表𝐿:

从𝐿中选取置信度最高的预测边界框𝐵1作为基准,然后将所有与𝐵1的IoU超过预定阈值𝜖(说明与其太过相似)的预测边界框从𝐿中移除。简而言之,那些具有非极大值置信度的边界框被抑制了。从𝐿中选取置信度第二高的预测边界框𝐵2作为又一个基准,然后将所有与𝐵2的IoU大于𝜖的预测边界框从𝐿中移除。重复上述过程,直到𝐿中的所有预测边界框都曾被用作基准。此时,𝐿中任意一对预测边界框的IoU都小于阈值𝜖;因此,没有一对边界框过于相似。输出列表𝐿中的所有预测边界框。

实践中,在执行非极大值抑制前,可以将置信度较低的预测边界框移除,从而减少算法中的计算量。 也可以对非极大值抑制的输出结果进行后处理。例如,只保留置信度更高的结果作为最终输出。

输入:boxes:预测边界框的坐标,(anchor数,4); scores:每个anchor的置信度(anchor数)

输出:保留的anchor的id

def nms(boxes, scores, iou_threshold):"""对预测边界框的置信度进行排序,并且做非极大值抑制"""# scores是每个anchor的置信度,B保存的是置信度从大到小所对应的anchor序号B = torch.argsort(scores, dim=-1, descending=True) keep = [] # 保留预测边界框的序号while B.numel() > 0: keep.append(B[0])if B.numel() == 1: break# 求最大置信度的预测边界框和其他预测边界框的IoUiou = box_iou(boxes[B[0], :].reshape(-1, 4),boxes[B[1:], :].reshape(-1, 4)).reshape(-1)inds = torch.nonzero(iou <= iou_threshold).reshape(-1) # 大于阈值的不要 # 这地方不能用 inds=iou <= iou_threshold,因为下面inds+1 B = B[inds + 1] # 求iou的时候,boxes[B[1:], :]比B短1个,所以inds要+1return torch.tensor(keep, device=boxes.device)

定义以下multibox_detection函数来将nms应用于预测边界框。

作用:输入每个anchor目标检测的类别和相对于边界框的偏移量,产生预测边界框的类别和位置

输入:

cls_probs:(batch size,类别数,anchor数)

offset_preds:(batch size,anchor数*4)

anchors:(batch size,anchor数,4)

输出:

(batch size,anchor数,1+1+4)

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) # 对于不同批次,anchors用一套固定的值num_classes, num_anchors = cls_probs.shape[1], cls_probs.shape[2] # 行是类,列是anchorout = []for i in range(batch_size):cls_prob, offset_pred = cls_probs[i], offset_preds[i].reshape(-1, 4)predicted_bb = offset_inverse(anchors, offset_pred) # 计算出预测实际边界框conf, class_id = torch.max(cls_prob[1:], 0) # 第0行是背景,不用管。max返回每一个anchor置信度和类id # 非极大值抑制根据置信度和边界框的IoU来抑制keep = nms(predicted_bb, conf, nms_threshold) # 经过非极大值抑制筛选出的anchor序号# 找到所有的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) # counts是每一个uniques出现的次数non_keep = uniques[counts == 1] # non_keep中元素只出现一次,说明不在keep中class_id[non_keep] = -1 # non_keep被抑制了,将类设置为背景 # all_id_sorted能让经过nms保留的预测边界框排在前面,使结果更好看all_id_sorted = torch.cat((keep, non_keep))class_id = class_id[all_id_sorted]conf, predicted_bb = conf[all_id_sorted], predicted_bb[all_id_sorted]# pos_threshold是一个用于非背景预测的阈值,小于该值即认为预测anchor为背景below_min_idx = (conf < pos_threshold)class_id[below_min_idx] = -1# 转变置信度为anchor是背景的置信度,这是因为一开始默认类别不是背景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.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] * 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'])

调用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)output

tensor([[[ 0.00, 0.90, 0.10, 0.08, 0.52, 0.92],[ 1.00, 0.90, 0.55, 0.20, 0.90, 0.88],[-1.00, 0.80, 0.08, 0.20, 0.56, 0.95],[-1.00, 0.70, 0.15, 0.30, 0.62, 0.91]]])

可以看到返回结果的形状是(批量大小,锚框的数量,6)。 最内层维度中的六个元素提供了同一预测边界框的输出信息。 第一个元素是预测的类索引,从0开始(0代表狗,1代表猫),值-1表示背景或在非极大值抑制中被移除了。 第二个元素是预测的边界框的置信度。 其余四个元素分别是预测边界框左上角和右下角的(𝑥,𝑦)轴坐标(范围介于0和1之间)

删除-1类别(背景)的预测边界框后,可以输出由非极大值抑制保存的最终预测边界框

fig = d2l.plt.imshow(img)for i in output[0].detach().numpy():if i[0] == -1:continuelabel = ('dog=', 'cat=')[int(i[0])] + str(i[1])show_bboxes(fig.axes, [torch.tensor(i[2:]) * bbox_scale], label)

算法过程总结

使用multibox_prior函数为每个图片生成大量锚框,每个锚框是一个训练样本训练阶段: 上游网络生成大量anchor,将样本送入网络,为每个anchor预测类别和偏移量使用multibox_target函数为每个锚框标注真实的类别和偏移量根据类别和偏移量的预测和标注值计算损失函数,反向传播 预测阶段: 上游网络生成大量anchor,将样本送入网络,为每个anchor预测类别和偏移量multibox_detection函数根据nms算法去除相似的锚框和无效的锚框(背景框),输出

目标检测算法:SDD(单发多框检测)

特点

在神经网络不同层featrue map上生成锚框,通过预测这些锚框的类别和偏移量,来检测不同大小的目标。在训练单发多框检测模型时,损失函数是根据锚框的类别和偏移量的预测及标注值计算得出的

多尺度目标检测

假设CNN网络给出的中间输出是 𝑐 张feature map,每张feature map上都有 ℎ𝑤 个不同的空间位置,于是每个空间位置含有 𝑐 个单元,这些单元在输入图像上的感受野相同,表征了同一感受野内的输入图像信息。 本质上讲,这是用输入图像感受野内的信息,来预测对应锚框的类别和偏移量

深层神经网络对图像进行分层特征提取,不同层的featrue map中,每个单元在输入图像上拥有不同大小的感受野:

靠近输入层的featrue map单元具有更窄的感受野,可以从输入图像中检测到较小的目标,于是每个锚框更小一点(scale小一点),从而采样更多的区域来检测较小的物体靠近输出层的featrue map单元具有更大的感受野,可以从输入图像中检测到较大的目标,于是每个锚框更大一点(scale大一点),从而采样更少的区域来检测较大的物体

通过这种方式,达到多尺度目标检测的效果。

下面通过画图来理解。首先读取图片,得到图片的长和宽

img = d2l.plt.imread('../img/catdog.jpg')h, w = img.shape[:2]h, w

定义函数display_anchors,在特征图上生成锚框(每个锚框以像素作中心。形状为 h×w的特征图,共生成hw组锚框,每一组锚框有n+m-1个),并在输入图像img上展示出来。输入是featrue map的宽、高和缩放比例。

def display_anchors(fmap_w, fmap_h, s):d2l.set_figsize()# 前两个维度上的值不影响输出fmap = torch.zeros((1, 10, fmap_h, fmap_w)) # 10是通道数,在这里没什么影响anchors = 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)

假设某一个featrue map宽是4,长是4,共有16个单元。在featrue map每个单元上生成锚框,一共有16个中心。由于整个输入图片被映射成为16个单元,所以每个中心上的锚框检测的是整张图片的1/16(也是该单元在输入图片上的感受野)。如下图,将输入图片也画了出来,可以看到4×4的featrue map对输入图片进行了4×4采样

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

将featrue map的⾼度和宽度减⼩⼀半。用4个单元映射了输入图像,与上图相比,每个单元具有更大的感受野,锚框检测的是输入图片的1/4,从输入图像中检测到更大的目标,锚框的缩放程度应该更小(s更大,从0.15增长到了0.4)。

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

通过上面例子可以看到,在不同featrue map上生成的锚框,检测的输入图像范围不一样,能够起到多尺度检测的效果。另一个好处是,相比于直接在图像上的每个点生成锚框,在featrue map上生成锚框相当于在输入图像上均匀采样,以小部分像素(而不是输入图像的每个像素)为中心生成锚框,减少了锚框数量

SSD模型搭建

模型主要由基础网络组成,其后是几个多尺度特征块。

基本网络用于从输入图像中提取特征,可以使用深度卷积神经网络。SSD原论文中选用了VGG(丢弃分类层),现在也常用ResNet替代。让基础网络的输出的高和宽较大,用来检测尺寸较小的目标。每个多尺度特征块将上一层提供的feature map的高和宽缩小(如减半),用来检测尺寸较大的目标。

%matplotlib inlineimport torchimport torchvisionfrom torch import nnfrom torch.nn import functional as Ffrom d2l import torch as d2l

高和宽减半块

为了在多个尺度下检测目标,定义feature map高和宽减半块down_sample_blk。 该块应用 VGG模块设计,由两个填充为1的3×3的卷积层、以及步幅为2的2×2最大汇聚层组成。前者不改变feature map的形状,后者将输入feature map的高度和宽度减少了一半。 输出中的每个单元在输入上都有一个6×6的感受野[1×2+(3−1)+(3−1)=6]。输出的通道是out_channels,高和宽是输入的一半

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_channelsblk.append(nn.MaxPool2d(2))return nn.Sequential(*blk)

基本网络块

基本网络块用于从输入图像中抽取特征。为了计算简洁,构造了一个小的基础网络,该网络串联3个高和宽减半块,并逐步将通道数翻倍。 给定输入图像的形状为256×256,此基本网络块输出的特征图形状为32×32(256/8=32)。

def base_net():blk = []num_filters = [3, 16, 32, 64]for i in range(len(num_filters) - 1):# 3个高宽减半块,通道数(3,16)、(16,32)、(32,64)blk.append(down_sample_blk(num_filters[i], num_filters[i+1]))return nn.Sequential(*blk)

类别预测层

设目标类别的数量为𝑞,于是锚框有𝑞+1个类别,其中0类是背景。设feature map的高和宽分别为ℎ和𝑤,以每个单元为中心生成𝑎个锚框,那么需要对ℎ𝑤𝑎个锚框进行分类。如果使用全连接层作为输出,很容易导致模型参数过多。 图像分类模型使用卷积层来预测类别,单发多框检测采用同样的方法来降低模型复杂度。

具体来说,卷积层保持输入高和宽不变,输出通道数为𝑎(𝑞+1)。对于这种卷积层,输出特征图上(𝑥、𝑦)坐标的 𝑎(𝑞+1) 个通道对应于以输入特征图(𝑥、𝑦)坐标为中心生成的所有锚框的类别预测,其中索引为 𝑖(𝑞+1)+𝑗(0≤𝑗≤𝑞)的通道代表了锚框 𝑖 关于类别 𝑗 的预测。

在下面,我们定义了这样一个类别预测层,通过参数num_anchorsnum_classes分别指定了𝑎和𝑞。 使用填充为1的3×3的卷积层。

这个地方不明白,为什么不使用1×1的卷积层?

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个偏移量,而不是𝑞+1个类别。

def bbox_predictor(num_inputs, num_anchors):return nn.Conv2d(num_inputs, num_anchors * 4, kernel_size=3, padding=1)

连结多尺度的预测

不同层的feature map的形状和每个单元生成的锚框数可能会有所不同, 因此它们预测输出的形状可能会有所不同。

在以下示例中,为同一个batch构建两个不同比例(Y1Y2)的feature map,其中Y2的高度和宽度是Y1的一半。 以类别预测为例,假设Y1Y2的每个单元分别生成了5个和3个锚框,且目标类别的数量为10,则类别预测输出中的通道数分别为5×(10+1)=55和3×(10+1)=33。

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]))

除了批量大小这一维度外,其他三个维度(输出通道数、高、宽)都具有不同的尺寸。 为了将这两个预测输出链接起来以提高计算效率,我们将把这些张量转换为更一致的格式,方便后面进行loss计算等,而不用对每个不同的尺度做loss

通道维包含中心相同的锚框的预测结果,首先将通道维移到最后一维(方便未来做softmax预测类别)。 因为不同尺度下批量大小仍保持不变,所以可以将预测结果转成二维的(批量大小,高×宽×通道数)的格式,然后在维度1上进行连结。

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)

这样一来,尽管Y1Y2在通道数、高度和宽度方面具有不同的大小,我们仍然可以在同一个小批量的两个不同尺度上连接这两个预测输出。

concat_preds([Y1, Y2]).shape

torch.Size([2, 25300])

前向传播

完整的SSD模型由五个模块组成。在这五个模块中,第一个是基本网络块,第二个到第四个是高和宽减半块,最后一个模块使用全局最大池将高度和宽度都降到1。第二到第五个区块都是前面SSD模型图中的多尺度特征块。

def get_blk(i):if i == 0:blk = base_net()#([batch, 64, 32, 32])elif i == 1:blk = down_sample_blk(64, 128)elif i == 4:blk = nn.AdaptiveMaxPool2d((1,1))#最后一个特征图压到1*1else:blk = down_sample_blk(128, 128)#数据集较小,所以后两个没有再倍增通道数return blk

与图像分类任务不同,每个块输出包括:CNN特征图Y;在当前尺度下根据Y生成的锚框;预测的这些锚框的类别和偏移量(基于Y

注意:预测锚框的目标类别和偏移量不需要锚框的信息,仅仅需要feature map就行了。

def blk_forward(X, blk, size, ratio, cls_predictor, bbox_predictor):Y = blk(X)anchors = multibox_prior(Y, sizes=size, ratios=ratio) # 根据Y生成的锚框cls_preds = cls_predictor(Y) # 预测的锚框的类别(基于Y)bbox_preds = bbox_predictor(Y) # 预测的锚框偏移量(基于Y)return (Y, anchors, cls_preds, bbox_preds)

较接近顶部的多尺度特征块是用于检测较大目标的,因此需要生成更大的锚框。 在上面的前向传播中,通过multibox_prior函数的sizes参数传递两个比例值。 在下面,0.2和1.05之间的区间被均匀分成五个部分,不同模块的size较小值:0.2、0.37、0.54、0.71和0.88。 size较大值由√(0.2×0.37)=0.272、√(0.37×0.54)=0.447等给出。每个尺度块有2个size,ratios3个,则每个像素生成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]] * 5num_anchors = len(sizes[0]) + len(ratios[0]) - 1

完整的模型

class TinySSD(nn.Module):def __init__(self, num_classes, **kwargs):super(TinySSD, self).__init__(**kwargs)self.num_classes = num_classesin_channels = [64, 128, 128, 128, 128] #5个块的输出通道数for i in range(5): # 每一个模块用于提取特征的卷积层不一样,用于预测种类和偏移量的卷积层一样setattr(self, f'blk_{i}', get_blk(i)) # 即赋值语句self.blk_i=get_blk(i) setattr(self, f'cls_{i}', cls_predictor(in_channels[i],num_anchors, num_classes))setattr(self, f'bbox_{i}', bbox_predictor(in_channels[i],num_anchors))def forward(self, X):anchors, cls_preds, bbox_preds = [None] * 5, [None] * 5, [None] * 5for i in range(5):# getattr(self,'blk_%d'%i)即访问self.blk_iX, 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}'))# 合并不同模块的anchor、预测种类和偏移量anchors = torch.cat(anchors, dim=1) # anchors形状:(1, 锚框数, 4)cls_preds = concat_preds(cls_preds) # 形状:(批量大小,锚框数乘以类别数+1)cls_preds = cls_preds.reshape(cls_preds.shape[0], -1, self.num_classes + 1) # #2d变3d,方便做softmax预测类别,其中-1那一维代表锚框数bbox_preds = concat_preds(bbox_preds) # 形状:(批量大小,锚框数乘以4)return anchors, cls_preds, bbox_preds

模型举例:创建一个模型实例,然后使用它对一个256×256像素的小批量图像X执行前向传播。

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])

第一个模块经过三次减半,输出特征图的形状为32×32,第二到第四个模块为高和宽减半块,第五个模块为全局汇聚层。 由于以特征图的每个单元为中心有4个锚框生成,因此在所有五个尺度下,每个图像总共生成(32×32+16×16+8×8+4×4+1)×4=5444个锚框。

训练模型

读取数据集和初始化

注意d2l自带的load_data_bananas可能有问题,所以使用前面修改过的load_data_bananas函数。香蕉检测数据集中,目标的类别数为1。定义好模型后,初始化其参数并定义优化算法

batch_size = 32train_iter, _ = 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)

定义损失函数和评价函数

目标检测有两种类型的损失。

锚框类别的损失:可以简单地使用交叉熵损失函数来计算,真实值是为锚框分配的真实边界框正类锚框偏移量的损失:预测偏移量是一个回归问题,使用𝐿1范数损失。之所以不用L2损失,是因为很多锚框离边界框很远,平方之后数值会特别大,而我们只关心几个比较好的锚框,那些离得远的锚框无所谓,所以也不需要MSE那样将误差大的进行平方加权。 使用掩码变量bbox_masks令负类锚框和填充锚框不参与损失的计算。

最后,我们将锚框类别和偏移量的损失相加,以获得模型的最终损失函数。

cls_preds:[batch size, 总anchor数, 类别数]

cls_labels:[batch size, 总anchor数]

bbox_preds:[batch size, 总anchor数*4]

bbox_labels: [batch size, 总anchor数*4]

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_preds.reshape是指每个锚框是一个样本,所以把batch_size*锚框个数cls = cls_loss(cls_preds.reshape(-1, num_classes),cls_labels.reshape(-1)).reshape(batch_size, -1).mean(dim=1)#背景框bbox_masks=0不计算损失,其它类bbox_masks=1bbox = bbox_loss(bbox_preds * bbox_masks,bbox_labels * bbox_masks).mean(dim=1)return cls + bbox #这里可以加权

用准确率评价分类结果。 由于偏移量使用了𝐿1范数损失,我们使用平均绝对误差来评价边界框的预测结果。这些预测结果是从生成的锚框及其预测偏移量中获得的。

def cls_eval(cls_preds, cls_labels):# 由于类别预测结果放在最后一维,argmax需要指定最后一维。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())

训练

在训练模型时,我们需要在模型的前向传播过程中生成多尺度锚框(anchors),并预测其类别(cls_preds)和偏移量(bbox_preds)。 然后,我们根据标签信息Y这些锚框标记类别(cls_labels)和偏移量(bbox_labels)。 最后,我们根据类别和偏移量的预测和标注值计算损失函数。为了代码简洁,这里没有评价测试数据集。

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 = 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)}')

预测目标

读取并调整测试图像的大小,然后将其转成卷积层需要的四维格式。

X = torchvision.io.read_image('../img/banana.jpg').unsqueeze(0).float()img = X.squeeze(0).permute(1, 2, 0).long()

使用下面的multibox_detection函数,根据锚框及其预测偏移量得到预测边界框,通过非极大值抑制来移除相似的预测边界框。输出结果每一行对应一个anchor,第一个元素是预测的类别,第二个元素是置信度,后面跟着预测边界框(通过非极大值抑制来移除相似的预测边界框)。

def predict(X):net.eval()# net输出的维度:[batch size, 总anchor数,4],[batch size, 总anchor数,类别数],[batch size, 总anchor数*4] anchors, cls_preds, bbox_preds = net(X.to(device))cls_probs = F.softmax(cls_preds, dim=2).permute(0, 2, 1)# multibox_detection输入的维度:(batch size,类别数,anchor数),(batch size,anchor数*4),(batch size,anchor数,4)output = multibox_detection(cls_probs, bbox_preds, anchors) # batch size,anchor数, 1+1+4# 取第一个batch,看每一个anchor的类别是不是-1(背景),记录下不是背景的框的ididx = [i for i, row in enumerate(output[0]) if row[0] != -1]return output[0, idx]output = predict(X)

最后,我们筛选所有置信度不低于0.9的边界框,做为最终输出。

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:continueh, 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.9)

区域卷积神经网络(R-CNN)

介绍区域卷积神经网络(region-based CNN或regions with CNN features,R-CNN)及其一系列改进方法:快速的R-CNN(Fast R-CNN)、更快的R-CNN(Faster R-CNN) 和掩码R-CNN(Mask R-CNN)。

R-CNN

R-CNN首先从输入图像中选取若干(例如2000个)提议区域(其实锚框也是一种选取方法),并标注它们的类别和边界框(如偏移量)。然后,用卷积神经网络对每个提议区域进行前向传播以抽取其特征。 接下来,用每个提议区域的特征来预测类别和边界框。

R-CNN包括以下四个步骤:

对输入图像使用选择性搜索(selective search)来选取多个高质量的提议区域。这些提议区域通常是在多个尺度下选取的,具有不同的形状和大小。每个提议区域都将被标注类别和真实边界框。选择一个预训练的卷积神经网络,将其在输出层之前截断。将每个提议区域变形为该网络需要的输入尺寸,并通过前向传播输出特征。将每个提议区域的特征连同其标注的类别作为一个样本。训练多个支持向量机对目标分类,其中每个支持向量机用来判断样本是否属于某一个类别。将每个提议区域的特征连同其标注的边界框作为一个样本,训练线性回归模型来预测真实边界框。

R-CNN三大不足:

候选区域太多,每个候选区域都需要通过CNN计算特征,计算量大,速度很慢Selective Search基于传统特征提取的区域质量不够好特征提取、SVM分类器是分模块独立训练,没有联合起来系统性优化,训练耗时长

Fast R-CNN

R-CNN的主要性能瓶颈在于,对每个提议区域,卷积神经网络的前向传播是独立的,没有共享计算。 由于这些区域通常有重叠,独立的特征抽取会导致重复的计算。Fast R-CNN对R-CNN的主要改进之一,是仅在整张图像上执行卷积神经网络的前向传播,提取特征

Fast R-CNN模型的主要计算如下:

使用整个图像作为CNN的输入,提取特征。设输入为一张图像,将卷积神经网络的输出的形状记为1×𝑐×ℎ1×𝑤1。选择性搜索生成𝑛个提议区域,将CNN的输出和提议区域作为输入,抽取出形状相同的特征(比如指定高度ℎ2和宽度𝑤2),然后连结,形状为𝑛×𝑐×ℎ2×𝑤2。 按照锚框在原始图片的位置比例,将其在Featrue Map找出来,再抽取锚框的特征通过全连接层将输出形状变换为𝑛×𝑑,其中超参数𝑑取决于模型设计。预测𝑛个提议区域中每个区域的类别和边界框:将全连接层的输出分别转换为形状为𝑛×𝑞的输出和形状为𝑛×4的输出,作为每个提议区域关于类别(𝑞是类别的数量)和边界框的预测。其中预测类别时使用softmax回归。

RoI 池化层

这里有一个问题:提议区域的大小是不一样的,如何让输出大小一致?通过兴趣区域池化层(region of interest pooling)实现。

给定一个形状为ℎ×𝑤的兴趣区域窗口,输出ℎ2×𝑤2的子窗口网格,其中每个子窗口的大小约为(ℎ/ℎ2)×(𝑤/𝑤2)。 在实践中,任何子窗口的高度和宽度都应向上取整,其中的最大元素作为该子窗口的输出,输出形状便成为ℎ2×𝑤2。

见下图,在4×4的输入中,选取了左上角3×3的兴趣区域。 对于该兴趣区域,通过2×2的RoI pooling得到一个2×2的输出。 请注意,四个划分后的子窗口中分别含有元素0、1、4、5(5最大);2、6(6最大);8、9(9最大);以及10

下面,我们演示了RoI pooling的计算方法。 假设卷积神经网络抽取的特征X的高度和宽度都是4,且只有单通道。

import torchimport torchvisionX = torch.arange(16.).reshape(1, 1, 4, 4)X

tensor([[[[ 0., 1., 2., 3.],[ 4., 5., 6., 7.],[ 8., 9., 10., 11.],[12., 13., 14., 15.]]]])

假设输入图像的高度和宽度都是40像素,且选择性搜索在此图像上生成了两个提议区域。 每个区域由5个元素表示:区域目标类别、左上角和右下角的(𝑥,𝑦)坐标。

rois = torch.Tensor([[0, 0, 0, 20, 20], [0, 0, 10, 30, 30]])

由于X的高和宽是输入图像高和宽的1/10,因此,两个提议区域的坐标先按spatial_scale乘以0.1。 然后,在X上分别标出这两个兴趣区域X[:, :, 0:3, 0:3]X[:, :, 1:4, 0:4]。 最后,在2×2的RoI pooling层中,每个兴趣区域被划分为子窗口网格,并进一步抽取相同形状2×2的特征。

torchvision.ops.roi_pool(X, rois, output_size=(2, 2), spatial_scale=0.1)

tensor([[[[ 5., 6.],[ 9., 10.]]],[[[ 9., 11.],[13., 15.]]]])

下图对比了几个模型的训练和预测的耗时,右下图蓝色部分表示整体的预测耗时,红色部分表示提取候选区域以外的时间。可见Fast R-CNN模型做预测时,大部分时间耗在提取候选区域上。

Faster R-CNN

为了较精确地检测目标结果,Fast R-CNN模型通常需要在选择性搜索中生成大量的提议区域。Faster R-CNN将生成提议区域的方法从选择性搜索算法替换为RPN(区域提议网络region proposal network),从而减少提议区域的生成数量,并保证目标检测的精度,模型的其余部分保持不变。

RPN类似一个很粗糙的目标检测,计算步骤如下:

左侧CNN的输出进入一个卷积层(填充为1,核为3×3),得到通道数为 𝑐 的feature map。以feature map的每个像素为中心,生成多个不同大小和宽高比的锚框(启发式搜索或者别的方式),并标注它们。使用锚框中心单元的特征(特征数量为𝑐),分别预测该锚框的二元类别(含目标还是背景)和边界框。使用非极大值抑制,从预测类别为目标的预测边界框中移除相似的结果,输出的预测边界框即是RoI pooling所需的提议区域。

值得一提的是,RPN作为Faster R-CNN模型的一部分,是和整个模型一起训练得到的。 换句话说,Faster R-CNN的目标函数不仅包括目标检测中的类别和边界框预测,还包括RPN中锚框的二元类别和边界框预测。 作为端到端训练的结果,RPN能够学习到如何生成高质量的提议区域,从而在减少提议区域的数量的情况下,仍保持目标检测的精度。

Mask R-CNN

如果在训练集中还标注了每个目标在图像上的像素级位置(比如无人机、自动驾驶领域),那么Mask R-CNN能够有效地利用这些详尽的标注信息进一步提升目标检测的精度

Mask R-CNN将Faster R-CNN的RoI pooling层替换为了兴趣区域对齐层RoI align。RoI pooling在无法均分时有填充,但对于像素级标号来说,这种填充会造成像素级偏移,在边界处标号预测不准。RoI align简单来说不会填充,而使用双线性插值(bilinear interpolation)来保留特征图上的空间信息,输出的feature map与所有与兴趣区域的形状相同,从而更适于像素级预测。这些feature map不仅被用于预测每个兴趣区域的类别和边界框,还通过额外的全卷积网络预测目标的像素级位置。

小结

R-CNN对图像选取若干提议区域,使用卷积神经网络对每个提议区域执行前向传播以抽取其特征,然后再用这些特征来预测提议区域的类别和边界框。Fast R-CNN对R-CNN的一个主要改进:只对整个图像做卷积神经网络的前向传播。它还引入了兴趣区域汇聚层,从而为具有不同形状的兴趣区域抽取相同形状的特征。Faster R-CNN将Fast R-CNN中使用的选择性搜索替换为参与训练的区域提议网络,这样后者可以在减少提议区域数量的情况下仍保证目标检测的精度。Mask R-CNN在Faster R-CNN的基础上引入了一个全卷积网络,从而借助目标的像素级位置进一步提升目标检测的精度。

需要注意的函数

对Series对象使用list

>>sd = {'xiaoming':14,'tom':15,'john':13}

>>s4 = pd.Series(sd)

>>print(s4)

xiaoming 14tom 15john 13dtype: int64

>>print([s4])

[xiaoming 14tom 15john 13dtype: int64]

>>print(list(s4))

[14, 15, 13]

torch.set_printoptions

精简输出精度

>>torch.set_printoptions(2)

>>a = torch.tensor([1.2345])

>>print(a)

tensor([1.23])

>>torch.set_printoptions(threshold=5)

>>torch.arange(10)

tensor([0, 1, 2, ..., 7, 8, 9])

torch.meshgrid

>>a = torch.tensor([1,2,3])

>>b = torch.tensor([4,5,6])

>>xx, yy = torch.meshgrid(a,b)

tensor([[1, 1, 1],[2, 2, 2],[3, 3, 3]])tensor([[4, 5, 6],[4, 5, 6],[4, 5, 6]])

reshape(-1)

变成一维数据

>>print(xx.reshape(-1))

tensor([1, 1, 1, 2, 2, 2, 3, 3, 3])

>>a = torch.arange(10)

>>b = torch.nonzero(a>5)

tensor([[6],[7],[8],[9]])

>>a[b]

tensor([[6],[7],[8],[9]])

b的形状是[4, 1],切片出来的张量形状也是

[4, 1]

>>c = b.reshape(-1)

tensor([6, 7, 8, 9])

经过reshape(-1),c的形状是[4],切片出来的张量形状也是[4]

>>d = a>5

>>a[d]

tensor([6, 7, 8, 9])

结果一样

repeat和repeat_interleave

>>a = torch.tensor([[1,2],[4,5]])

repeat(第一维度复制的次数, 第二维度复制的次数)。repeat相当于将该张量复制,然后在某一维度concat起来

>>a.repeat([1,3])

tensor([[1, 2, 1, 2, 1, 2],[4, 5, 4, 5, 4, 5]])

>>a.repeat([2,2])

tensor([[1, 2, 1, 2],[4, 5, 4, 5],[1, 2, 1, 2],[4, 5, 4, 5]])

repeat_interleave(复制的次数, dim=复制的维度)。repeat_interleave是将张量中的元素沿某一维度复制n次,即复制后的张量沿该维度相邻的n个元素是相同的

>>a.repeat_interleave(3, dim=0)

tensor([[1, 2],[1, 2],[1, 2],[4, 5],[4, 5],[4, 5]])

>>a.repeat_interleave(3, dim=1)

tensor([[1, 1, 1, 2, 2, 2],[4, 4, 4, 5, 5, 5]])

也可以写成a.repeat_interleave(3, 1)

对比:

>>a = torch.tensor([[1,2],[4,5]])

>>a.repeat(2,1) # 0维乘以2,1维不变

tensor([[1, 2],[4, 5],[1, 2],[4, 5]])

>>a.repeat_interleave(2,0) # 0维乘以2,1维不变

tensor([[1, 2],[1, 2],[4, 5],[4, 5]])

扩维后相减操作

>>a = torch.tensor([[1,2,3],[4,5,6]])

>>b = torch.tensor([[1,1,1],[2,2,2],[3,3,3]])

>>c = a[:, None] - b

>>a[:, None].shape, b.shape, c.shape

(torch.Size([2, 1, 3]), torch.Size([3, 3]), torch.Size([2, 3, 3]))

可以看到a[:, None] 和 b 的形状并不一样,相减的过程中进行了扩维。其中a[:, None]在1维进行了复制,a[0,0:3,:]是将[1,2,3]复制了三遍;b增加了0维,在0维数上进行了复制,扩维后的b[0]就是原来的b。得到的结果是[2, 3, 3]的矩阵,c[0]中每一行是的[1,2,3]减去b中的每一行

>>c

tensor([[[ 0, 1, 2],[-1, 0, 1],[-2, -1, 0]],[[ 3, 4, 5],[ 2, 3, 4],[ 1, 2, 3]]])

以下命令和a[:, None] - b得到的结果一样

a[:, None].repeat_interleave(3, dim=1) - b.repeat(2,1,1)

a[:, None].repeat_interleave(3, dim=1) - b[None, :].repeat_interleave(2,dim=0)

a[:, None].repeat(1,3,1) - b.repeat(2,1,1)

>>c = a - b[:, None]

>>a.shape, b[:, None].shape, c.shape

(torch.Size([2, 3]), torch.Size([3, 1, 3]), torch.Size([3, 2, 3]))

>>c

tensor([[[ 0, 1, 2],[ 3, 4, 5]],[[-1, 0, 1],[ 2, 3, 4]],[[-2, -1, 0],[ 1, 2, 3]]])

a - b[:, None] 结果和 a[:, None] - b不一样了,简单的说就是让a的每一行和b的每一行相减,具体结果看谁先扩维

max操作也类似

>>torch.max(a[:, None],b)

tensor([[[1, 2, 3],[2, 2, 3],[3, 3, 3]],[[4, 5, 6],[4, 5, 6],[4, 5, 6]]])

torch.stack()

stack(tensors, dim=0)

官方解释:沿着一个新维度对输入张量序列进行连接。 序列中所有的张量都应该为相同形状。

浅显说法:把多个2维的张量凑成一个3维的张量;多个3维的凑成一个4维的张量…以此类推,也就是在新增的维度进行堆叠。

>>a = torch.ones([3,3])

>>torch.stack((a,a), dim=0).shape

torch.Size([2, 3, 3])

>>torch.stack((a,a), dim=1).shape

torch.Size([3, 2, 3])

>>torch.stack((a,a), dim=2).shape

torch.Size([3, 3, 2])

切片时使用None

增加维度

a = torch.tensor([[1,2,3],[4,5,6]])

>>print(a[:, None].shape) # 在第1维增加维度

torch.Size([2, 1, 3])

>>a[:, None, None].shape # 在第1、2维增加维度

torch.Size([2, 1, 1, 3])

>>a[:, 1].shape

torch.Size([2])

>>a[:, None, 1].shape # 1代表没有维度,所以放在哪里都一样

torch.Size([2, 1])

>>a[:, 1, None].shape

torch.Size([2, 1])

>>a[:, None, None, 1].shape

torch.Size([2, 1, 1])

>>a[:, None, 1, None].shape

torch.Size([2, 1, 1])

>>a = torch.tensor([[1,2,3],[4,5,6]])

>>a[:, None].shape

torch.Size([2, 1, 3])

>>a[:, None, None].shape

torch.Size([2, 1, 1, 2])

>>a[:, :2].shape

torch.Size([2, 2])

>>a[:, None, :2].shape

torch.Size([2, 1, 2])

>>a[:, :2, None].shape

torch.Size([2, 2, 1])

>>a[:, None, None, :2].shape

torch.Size([2, 1, 1, 2])

>>a[:, None, :2, None].shape

torch.Size([2, 1, 2, 1])

用于计算:

>>a = torch.tensor([[1,2,3,5],[12,5,6,7],[13,8,9,8]])

>>b = torch.tensor([[11,12,13,8],[15,15,16,9]])

如果不升维,由于两个矩阵行数不一样,torch.max(a[:, :2], b[:, :2])会报错

>>torch.max(a[:, None, :1], b[:, :1])

tensor([[[11],[14]],[[12],[14]],[[15],[15]]])

>>torch.max(a[:, None, :1], b[:, :1]).shape

torch.Size([3, 2, 1])

可见,升维的效果是a的每一行都和b的所有行比较

>>a = torch.rand([2,2])[:, 0]

tensor([0.24, 0.80])

>>b = torch.rand([3,3])[:, 0]

tensor([0.30, 0.39, 0.91])

a + b会报错,因为长度不一样

>>a[:, None] + b

tensor([[0.54, 0.63, 1.15],[1.10, 1.19, 1.71]])

结果的第一行是a的第一个数加b的所有数,以此类推

unique

>>a = torch.tensor([5,1,2,3,2,4,5,6,3])

>>uniques, counts = a.unique(return_counts=True)

>>uniques, counts

tensor([1, 2, 3, 4, 5, 6]) tensor([1, 2, 2, 1, 2, 1])

>>uniques[counts == 1]

tensor([1, 4, 6])

列表和常数相乘

a = [1,2,3,4,5,6,7,8,9]

b = a[2:7]

>>b

[3, 4, 5, 6, 7]

>>b*2

[3, 4, 5, 6, 7, 3, 4, 5, 6, 7]

>>b*2+b

[3, 4, 5, 6, 7, 3, 4, 5, 6, 7, 3, 4, 5, 6, 7]

只有对python的list类型才如此,对于pytorch的tensor类型和numpy的数据类型是正常的算术运算

如果觉得《动手学深度学习——目标检测 SSD R-CNN Fast R-CNN Faster R-CNN Mask R-CNN》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。