经过个人整理。来源:13.4. 锚框 — 动手学深度学习 2.0.0-beta1 documentation
目录
基本概念
边界框
转换函数
画出边界框
锚框
生成锚框
画出锚框
交并比(IoU)
使用训练数据标记锚框
将真实边界框分配给锚框
求锚框相对于边界框的偏移量
为锚框标记类别和偏移量
预测
使用锚框和偏移量生成预测边界框
使用非极大值抑制处理预测边界框
举例测试
目标检测算法:SDD(单发多框检测)
特点
多尺度检测
数据集
SSD模型
类别预测层
边界框预测层
连结多尺度的预测
前向传播
完整的模型
训练模型
读取数据集和初始化
定义损失函数和评价函数
训练
预测目标
区域卷积神经网络(R-CNN)
R-CNN
Fast R-CNN
Faster R-CNN
Mask R-CNN
小结
需要注意的函数
torch.set_printoptions
torch.meshgrid
reshape(-1)
repeat和repeat_interleave
torch.stack()
切片时使用None
unique
很多时候图像里有多个我们感兴趣的目标,我们不仅想知道它们的类别,还想得到它们在图像中的具体位置。 在计算机视觉里,我们将这类任务称为目标检测(object detection)或目标识别(object recognition)。
%matplotlib inline
import torch
from d2l import torch as d2l
torch.set_printoptions(2) # 精简输出精度
在目标检测中,我们通常使用边界框(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) / 2
cy = (y1 + y2) / 2
w = x2 - x1
h = y2 - y1
boxes = torch.stack((cx, cy, w, h), axis=-1)
return boxes
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
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'));
目标检测算法通常会在输入图像中采样大量的区域,然后判断这些区域中是否包含我们感兴趣的目标。 不同的模型使用的区域采样方法可能不同, 这里介绍其中的一种方法:以每个像素为中心,生成多个缩放比和宽高比(aspect ratio)不同的边界框。 这些边界框被称为锚框(anchor box)。
假设输入图像的高度为ℎ,宽度为,缩放比为∈(0,1],宽高比为>0。 那么锚框的宽度和高度分别是ws√r和hs/√r。设置缩放比(scale)取值1,…,和宽高比(aspect ratio)取值1,…,,只考虑包含1或1的组合:(1,1),(1,2),…,(1,),(2,1),(3,1),…,(,1). 也就是说,以同一像素为中心的锚框的数量是+−1。 对于整个输入图像,我们将共生成ℎ(+−1)个锚框。
指定输入图像、尺寸列表和宽高比列表,然后此将返回所有的锚框。函数对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.5
offset_h, offset_w = 0.5, 0.5
steps_h = 1.0 / in_height # 在y轴上缩放步长
steps_w = 1.0 / in_width # 在x轴上缩放步长
# 生成锚框的所有中心点
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, 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来获得半高和半宽
# repeat前,转置的形状:[boxes_per_pixel,4]
anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat(
in_height * in_width, 1) / 2
# 每个中心点都将有“boxes_per_pixel”个锚框,
# 所以生成含所有锚框中心的网格,重复了“boxes_per_pixel”次
# shift_x, shift_y, shift_x, shift_y是要加载-w, -h, w, h上的
# out_grid 行:in_height*in_width*boxes_per_pixel 列:4
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 = d2l.plt.imread('../img/catdog.jpg')
h, w = img.shape[:2]
print(h, w) # 561 728
X = 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_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))
变量boxes
中轴和轴的坐标值已分别除以图像的宽度和高度。 绘制锚框时,我们需要恢复它们原始的坐标值
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'])
通过像素集的杰卡德(Jaccard)系数来测量两个边界框的相似性, 对于两个边界框,通常将它们的杰卡德系数称为交并比(intersection over union,IoU)。给定集合A和B,他们的杰卡德系数是他们交集的大小除以他们并集的大小:
#@save
def box_iou(boxes1, boxes2):
"""计算两个锚框或边界框列表中成对的交并比"""
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的所有行比较
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)的位置及其包围物体类别的标签。训练时,将每个锚框视为一个训练样本,通过锚框预测的准确与否来更新各种参数。我们为每个图像生成多个锚框,显然目标物体边界框不可能和锚框的大小和位置一模一样,因此模型要预测真实边界框相对于锚框的偏移量(offset),通过该偏移量可以计算得到预测的真实边界框,此外还要给出每个锚框的类别(class)。
没有比较将锚框和所有真实边界框比较。把最接近的真实边界框分配给锚框,用该真实边界框的位置和类别标签标记锚框
给定图像,假设锚框是1,2,…,,真实边界框是1,2,…,,其中≥。 让我们定义一个矩阵∈ℝ×,其中元素是锚框和真实边界框的IoU。 该算法包含以下步骤:
例子:矩阵中的最大值为23、71、54、92,之后,我们只需要遍历剩余的锚框1, 3, 4, 6, 8,然后根据阈值确定是否为它们分配真实边界框
其实可以考虑这样的算法:对于每一个anchor,找出与其IoU最大的真实边界框,根据阈值,决定是否配对(也就是只采取上面算法的第四步)。这种算法存在问题:可能一个真实边界框和几个anchor的IoU都很大,于是分配给这几个anchor,但是其他真实边界框能配对的anchor变少了。其实有一个anchor来预测一个真实边界框就足够了,不如将其他anchor释放掉,这样其他真实边界框能配对的anchor选择空间更大了。
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]
# 生成IoU矩阵,元素xij是锚框i和真实边界框j的IoU
jaccard = 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的anchor
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() # 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_discard
return 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
参数)。 此函数将背景类别的索引设置为零,然后将新类别的整数索引递增一。
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 = 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
indices_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对应的是背景。重复4遍是因为每个anchor有四个offset
bbox_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:]
offset = offset_boxes(anchors, assigned_bb) * bbox_mask
batch_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)
真实边界框,其中第一个元素是类别(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。注意输入参数增加了一个维度
labels = multibox_target(anchors.unsqueeze(dim=0),
ground_truth.unsqueeze(dim=0))
返回的结果中有三个元素,都是张量格式
labels[2]:tensor([[0, 1, 2, 0, 2]])
labels[1]:tensor([[0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 0., 1., 1., 1., 1.]])
labels[0]:tensor([[-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]])
在预测时,我们为每个图像生成多个锚框,为这些锚框一一预测类别和偏移量,根据预测的偏移量调整它们的位置以获得预测的边界框,最后只输出符合特定条件的预测边界框
预测好的边界框则根据其中某个带有预测偏移量的锚框而生成
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
当有许多锚框时,可能会输出许多相似的具有明显重叠的预测边界框,都围绕着同一目标。 为了简化输出,我们可以使用非极大值抑制(non-maximum suppression,NMS)合并属于同一目标的类似的预测边界框。
对于一个预测边界框,目标检测模型会计算每个类别的预测概率。 假设最大的预测概率为,则该概率所对应的类别即为预测的类别。 具体来说,我们将称为预测边界框的置信度(confidence)。 在同一张图像中,所有预测的非背景边界框都按置信度降序排序(重要的不是每个预测边界框预测的类别,而是置信度大小),以生成列表。然后我们通过以下步骤操作排序列表:
实践中,在执行非极大值抑制前,可以将置信度较低的预测边界框移除,从而减少算法中的计算量。 也可以对非极大值抑制的输出结果进行后处理。例如,只保留置信度更高的结果作为最终输出。
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
# 求最大置信度的预测边界框和其他预测边界框的IoU
iou = 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中不包含B[0]自己,所以inds要+1,并且把B[0]排除了
return torch.tensor(keep, device=boxes.device)
将非极大值抑制应用于预测边界框
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] # 行是类,列是anchor
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) # 每一个anchor预测置信度最高的类
predicted_bb = offset_inverse(anchors, offset_pred) # 计算出预测实际边界框
# 边界框最大预测概率所对应的类别即为其预测的类别。
# 非极大值抑制是不关注边界框预测的,真正想要的是conf排序后的索引信息
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中元素只出现一次,即被nms函数从all_idx中删除了
class_id[non_keep] = -1 # 将类设置为背景
# 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是背景的置信度,这是因为背景的预测概率一开始一律设为0
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:
continue
label = ('dog=', 'cat=')[int(i[0])] + str(i[1])
show_bboxes(fig.axes, [torch.tensor(i[2:]) * bbox_scale], label)
在featrue map上生成锚框(卷积层输出的每个通道叫featrue map),使用锚框在输入图像上感受野区域内的信息,来预测真实边界框类别和偏移量。
SDD可以利用深层神经网络在多个层次上对图像进行分层表示,从而实现多尺度目标检测。经过层层卷积得到的featrue map都是对整个输入图像的映射。不同层的featrue map中,每个单元在输入图像上拥有不同大小的感受野:靠近输出层的featrue map更小,每个单元具有更大的感受野,可以从输入图像中检测到较大的目标,每个锚框使用较小的缩放比例scale,即采样更多的区域来检测较小的物体;靠近输入层的featrue map单元具有更窄的感受野,可以从输入图像中检测到较小的目标,每个锚框使用较大的缩放比例scale,即采样更多的区域来检测较小的物体。下面通过画图来理解。
首先读取图片,得到图片的长和宽
img = d2l.plt.imread('../img/catdog.jpg')
h, w = img.shape[:2]
h, w
下面的函数输入是featrue map的宽、高和缩放比例。
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)
假设某一个featrue map宽是4,长是4,共有16个单元。在这个featrue map上,依然在每个单元上生成锚框,一共有16个中心。由于整个输入图片被映射成为16个单元,所以每个中心上的锚框检测的是整张图片的1/16(也是该单元在输入图片上的感受野)。如下图,对输入图片进行了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上生成多尺度的锚框,起到了多尺度检测的效果。另一个好处是,相比于直接在图像上的每个点生成锚框,在featrue map上生成锚框相当于在输入图像上均匀采样,以小部分像素(而不是输入图像的每个像素)为中心生成锚框,减少了锚框数量
用于目标检测的数据加载与图像分类的数据加载类似。但是,在目标检测中,标签还包含了真实边界框的信息。
拍摄了一组香蕉的照片,并生成了1000张不同角度和大小的香蕉图像。 然后,我们在一些背景图片的随机位置上放一张香蕉的图像。 最后,我们在图片上为这些香蕉标记了边界框。
%matplotlib inline
%matplotlib inline
import os
import pandas as pd
import torch
import torchvision
from PIL import Image
from d2l import torch as d2l
import torchvision.transforms as transforms
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_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(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))
return images, torch.tensor(targets).unsqueeze(1) / 256
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, 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
上面三个辅助函数的作用分别是:读取数据,创建dataset和创建dataloader。读取数据的时候遇到问题No such operator image::read_file,解决方法参考:No such operator image::read_file问题解决_iwill323的博客-CSDN博客
展示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'])
模型主要由基础网络组成,其后是几个多尺度特征块。
通过多尺度特征块,SSD检测生成不同大小的锚框,并通过预测边界框的类别和偏移量来检测大小不同的目标,因此这是一个多尺度目标检测模型。
%matplotlib inline
import torch
import torchvision
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
基本网络块
基本网络块用于从输入图像中抽取特征。为了计算简洁,构造了一个小的基础网络,该网络串联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):
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
高和宽减半块
基本网络块使用了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]。
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)
输出的通道是out_channels,高和宽是输入的一半
设目标类别的数量为。这样一来,锚框有+1个类别,其中0类是背景。设feature map的高和宽分别为ℎ和,以每个单元为中心生成个锚框,那么需要对ℎ个锚框进行分类。如果使用全连接层作为输出,很容易导致模型参数过多。 图像分类模型使用卷积层来预测类别,单发多框检测采用同样的方法来降低模型复杂度。
假设CNN网络给出的中间输出是张feature map,每张feature map上都有ℎ个不同的空间位置,在相同空间位置可以看作含有个单元,这些单元在输入图像上的感受野相同,表征了同一感受野内的输入图像信息。 因此,可以使用feature map在同一空间位置的个单元生成使用此空间位置生成的个锚框类别和偏移量。本质上讲,这是用输入图像在某个感受野区域内的信息,来预测输入图像上与该区域位置相近的锚框的类别和偏移量。
具体来说,类别预测层使用一个保持输入高和宽的卷积层,对于这种卷积层,输出特征图上(、)坐标的通道里包含了以输入特征图(、)坐标为中心生成的所有锚框的类别预测。 因此输出通道数为(+1),其中索引为(+1)+(0≤≤)的通道代表了锚框关于类别的预测。
在下面,我们定义了这样一个类别预测层,通过参数num_anchors
和num_classes
分别指定了和。 该图层使用填充为1的3×3的卷积层。
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的形状,或以同一单元为中心生成的锚框的数量可能会有所不同。 因此,它们预测输出的形状可能会有所不同。
在以下示例中,我们为同一个小批量构建两个不同比例(Y1
和Y2
)的feature map,其中Y2
的高度和宽度是Y1
的一半。 以类别预测为例,假设Y1
和Y2
的每个单元分别生成了5个和3个锚框,且目标类别的数量为10,对于特征图Y1
和Y2
,类别预测输出中的通道数分别为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]))
正如我们所看到的,除了批量大小这一维度外,其他三个维度(输出通道数、高、宽)都具有不同的尺寸。 为了将这两个预测输出链接起来以提高计算效率,我们将把这些张量转换为更一致的格式。
通道维包含中心相同的锚框的预测结果,首先将通道维移到最后一维(因为后面模型中对预测结果进行了reshape处理:cls_preds.reshape(cls_preds.shape[0], -1, self.num_classes + 1))。 因为不同尺度下批量大小仍保持不变,我们可以将预测结果转成二维的(批量大小,高×宽×通道数)的格式,以方便之后在维度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)
这样一来,尽管Y1
和Y2
在通道数、高度和宽度方面具有不同的大小,我们仍然可以在同一个小批量的两个不同尺度上连接这两个预测输出。
concat_preds([Y1, Y2]).shape
torch.Size([2, 25300])
完整的SSD模型由五个模块组成。每个块生成的特征图既用于生成锚框,又用于预测这些锚框的类别和偏移量。在这五个模块中,第一个是基本网络块,第二个到第四个是高和宽减半块,最后一个模块使用全局最大池将高度和宽度都降到1。从技术上讲,第二到第五个区块都是前面SSD模型图中的多尺度特征块。
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
现在为每个块blk定义前向传播。与图像分类任务不同,每个块除了要生成feature map,还要生成锚框,及预测类别和偏移量。
def blk_forward(X, blk, size, ratio, cls_predictor, bbox_predictor):
Y = blk(X)
anchors = d2l.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等给出。
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
class TinySSD(nn.Module):
def __init__(self, num_classes, **kwargs):
super(TinySSD, self).__init__(**kwargs)
self.num_classes = num_classes
in_channels = [64, 128, 128, 128, 128]
for i in range(5):
# 即赋值语句self.blk_i=get_blk(i)
# 每一个模块用于提取特征的卷积层不一样,用于预测种类和偏移量的卷积层一样
setattr(self, f'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] * 5
for i in range(5):
# getattr(self,'blk_%d'%i)即访问self.blk_i
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}'))
# 合并不同模块的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
执行前向传播。
第一个模块经过三次减半,输出特征图的形状为32×32,第二到第四个模块为高和宽减半块,第五个模块为全局汇聚层。 由于以特征图的每个单元为中心有4个锚框生成,因此在所有五个尺度下,每个图像总共生成(32×32+16×16+8×8+4×4+1)×4=5444个锚框。
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])
注意d2l自带的load_data_bananas可能有问题,所以使用前面修改过的load_data_bananas函数(前面提到过)。香蕉检测数据集中,目标的类别数为1。定义好模型后,初始化其参数并定义优化算法
batch_size = 32
train_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)
目标检测有两种类型的损失。
bbox_masks
令负类锚框和填充锚框不参与损失的计算。最后,我们将锚框类别和偏移量的损失相加,以获得模型的最终损失函数。
cls_preds.reshape(-1, num_classes)和cls_labels.reshape(-1)生成的行都是锚框数
cls_loss = nn.CrossEntropyLoss(reduction='none')
bbox_loss = nn.L1Loss(reduction='none')
def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks):
batch_size, num_classes = cls_preds.shape[0], cls_preds.shape[2]
cls = cls_loss(cls_preds.reshape(-1, num_classes),
cls_labels.reshape(-1)).reshape(batch_size, -1).mean(dim=1)
bbox = bbox_loss(bbox_preds * bbox_masks,
bbox_labels * bbox_masks).mean(dim=1)
return cls + bbox
用准确率评价分类结果。 由于偏移量使用了1范数损失,我们使用平均绝对误差来评价边界框的预测结果。这些预测结果是从生成的锚框及其预测偏移量中获得的。
cls_preds的最后一维是类别预测结果,每个锚框对每个类都有一个预测值,cls_preds.argmax(dim=-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 = 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)}')
在预测阶段,我们希望能把图像里面所有我们感兴趣的目标检测出来。在下面,我们读取并调整测试图像的大小,然后将其转成卷积层需要的四维格式。
X = torchvision.io.read_image('../img/banana.jpg').unsqueeze(0).float()
img = X.squeeze(0).permute(1, 2, 0).long()
使用下面的multibox_detection
函数,输入是类别可能醒醒, 预测的偏移量, anchor位置,输出结果每一行对应一个anchor,第一个元素是预测的类别,第二个元素是置信度,后面跟着预测边界框(通过非极大值抑制来移除相似的预测边界框)。
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)
最后,我们筛选所有置信度不低于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:
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.9)
介绍区域卷积神经网络(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首先从输入图像中选取若干(例如2000个)提议区域(如锚框也是一种选取方法),并标注它们的类别和边界框(如偏移量)。然后,用卷积神经网络对每个提议区域进行前向传播以抽取其特征。 接下来,我们用每个提议区域的特征来预测类别和边界框。
R-CNN包括以下四个步骤:
尽管R-CNN模型通过预训练的卷积神经网络有效地抽取了图像特征,但它的速度很慢。 想象一下,我们可能从一张图像中选出上千个提议区域,这需要上千次的卷积神经网络的前向传播来执行目标检测。 这种庞大的计算量使得R-CNN在现实世界中难以被广泛应用。
R-CNN的主要性能瓶颈在于,对每个提议区域,卷积神经网络的前向传播是独立的,没有共享计算。 由于这些区域通常有重叠,独立的特征抽取会导致重复的计算。 Fast R-CNN对R-CNN的主要改进之一,是仅在整张图象上执行卷积神经网络的前向传播。
Fast R-CNN模型的主要计算如下:
RoI pooling可从形状各异的兴趣区域中均抽取出形状相同的特征,并且对输出形状是可以直接指定的。例如,指定每个区域输出的高和宽分别为ℎ2和2。 对于任何形状为ℎ×的兴趣区域窗口,该窗口将被划分为ℎ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 torch
import torchvision
X = 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 将生成提议区域的方法从选择性搜索算法替换为RPN(区域提议网络region proposal network),从而减少提议区域的生成数量,并保证目标检测的精度,模型的其余部分保持不变。
RPN的计算步骤如下:
值得一提的是,RPN作为Faster R-CNN模型的一部分,是和整个模型一起训练得到的。 换句话说,Faster R-CNN的目标函数不仅包括目标检测中的类别和边界框预测,还包括RPN中锚框的二元类别和边界框预测。 作为端到端训练的结果,RPN能够学习到如何生成高质量的提议区域,从而在减少提议区域的数量的情况下,仍保持目标检测的精度。
如果在训练集中还标注了每个目标在图像上的像素级位置,那么Mask R-CNN能够有效地利用这些详尽的标注信息进一步提升目标检测的精度
Mask R-CNN将Faster R-CNN的RoI pooling层替换为了 兴趣区域对齐层RoI align。RoI pooling在无法均分时有填充,但对于像素级标号来说,这种填充会造成像素级偏移,在边界处标号预测不准。RoI align简单来说不会填充,而使用双线性插值(bilinear interpolation)来保留特征图上的空间信息,输出的feature map与所有与兴趣区域的形状相同,从而更适于像素级预测。这些feature map不仅被用于预测每个兴趣区域的类别和边界框,还通过额外的全卷积网络预测目标的像素级位置。
精简输出精度
>>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])
>>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]])
变成一维数据
>>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])
结果一样
>>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)
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])
增加维度
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的所有数,以此类推
>>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的数据类型是正常的算术运算