通常我们说机器学习三要素:Model、Loss、Optimization,大多数计算机视觉的论文也主要关注在 Model 和 Loss 上。在 Deep Learning 统治的当今,主流的范式往往是设计一个新的 loss,或者提出一个新的网络结构,把传统的 heuristic 方法 hard encoded 到网络结构中去实现端对端学习 [1]。
随着 SSD 这样特别强调 Data Augmentation 的方法的流行,以及 SNIP 这样强调 Data Scale 的 Argument 得到认可,Data 本身也已经是计算机视觉需要关心的一大要素。但现实是,当我们去接触代码的时候,Data Pre-processing 会是个又臭又长的过程。 又因为 Data 是Model、Loss、Metric 最重要的输入,如果 Data 与函数预想的不一样,就得不到想要的结果,特别是自己从头到尾写一个实现的时候。这篇日志以 GluonCV 中 SSD 实现中的 Data 为例,剖析一下 Data 在各个环节中需要的形式以及经历的操作。
按照先创建 train_dataset
,再对其做 SSDDefaultTrainTransform
,然后输入 DataLoader
通过 batchify_fn
拼成一个 batch,接着输入 SSD
的 net 实例,最后输入 SSDMultiBoxLoss
损失函数计算损失的顺序过程看一下 Data 在 Training 时在各环节的流动和变化情况。
1.1 VOCDetection
这里用 VOCDetection
作为 train_dataset
,且使用默认的 transform=None
。在 transform=None
的情况下,VOCDetection
只会干一件事,就是 load img
和 label
,此外不做任何操作。因此
VOCDetection
返回的是(img, label)
这样的一对 pair。- 对于彩色图像,
img
就是一个(H, W, 3)
的 MXNet.NDArray,每个元素的数值在 0 - 255 之间。 label
是一个(M, 6)
的 Numpy.NDArray,每一行都是[xmin, ymin, xmax, ymax, cls_id, difficult]
这样的xmin, ymin, xmax, ymax
这些都是范围在 0 - 图像宽或高 之间的整数cls_id
是不带 Background 的类别标号
创建 SSDDefaultTrainTransform
实例
train_transform = presets.ssd.SSDDefaultTrainTransform(width, height, anchors)
调用 SSDDefaultTrainTransform
实例
train_dataset.transform(train_transform)
输入:
img
是(H, W, 3)
的mxnet.ndarray
,每个元素的数值在 0 - 255 之间。label
是一个(M, 6)
的numpy.ndarray
,每一行都是[xmin, ymin, xmax, ymax, cls_id, difficult]
这样的xmin, ymin, xmax, ymax
这些都是范围在 0 - 图像宽或高 之间的整数cls_id
是不带 Background 的类别标号- M 是 这幅图像中的 Object 个数
在对 VOCDetection
返回的数据做 transform 这块,一共有三部分:Data Augmentation、Normalizing、SSDTargetGenerator
。
SSD 默认会采用 random cropping、resize with random interpolation 和 random horizontal flip 这些操作,图像改变的时候,bbox 也会做相应改变。但这不紧要到此为止的 (img, bbox) 还是
输出:
img
是(H, W, 3)
的mxnet.ndarray
,每个元素的数值在 0 - 255 之间。label
是一个(M, 6)
的numpy.ndarray
,每一行都是[xmin, ymin, xmax, ymax, cls_id, difficult]
这样的xmin, ymin, xmax, ymax
这些都是范围在 0 - 图像宽或高 之间的整数cls_id
是不带 Background 的类别标号- M 是 这幅图像中的 Object 个数
Normalizing 就两步,第一步 to_tensor
将 img
变成 (3, H, W)
的 MXNet.NDArray,且每个元素的数值在 0 - 1 之间;第二步 normalize
进一步将 img
的数值变成 0 均值,方差为 1;不对对 bbox
做任何操作,至此
输出:
img
就是一个(3, H, W)
的 MXNet.NDArray,每个元素的数值为 0 均值,方差为 1 的分布bbox
是一个(M, 6)
的 Numpy.NDArray,每一行都是[xmin, ymin, xmax, ymax, cls_id, difficult]
这样的xmin, ymin, xmax, ymax
这些都是范围在 0 - 图像宽或高 之间的整数cls_id
是不带 Background 的类别标号
SSDTargetGenerator
这一步是将 Object Label 转成 Anchor Label,即为每一个 Anchor 生成对应的 Label。
创建 SSDTargetGenerator
实例
self._target_generator = SSDTargetGenerator(
iou_thresh=iou_thresh, stds=box_norm, negative_mining_ratio=-1, **kwargs)
调用 SSDTargetGenerator
实例
cls_targets, box_targets, _ = self._target_generator(
self._anchors, None, gt_bboxes, gt_ids)
因为在创建 SSDTargetGenerator
实例 self._target_generator
的时候因为设置了 negative_mining_ratio=-1
,因此并不会用 OHEMSampler
,而是 NaiveSampler
。
SSDTargetGenerator
定义中的输入
def forward(self, anchors, cls_preds, gt_boxes, gt_ids):
gt_bboxes = mx.nd.array(bbox[np.newaxis, :, :4])
gt_ids = mx.nd.array(bbox[np.newaxis, :, 4:5])
上面只是将 bboxes 和 ids 分离,然后增加了 1 维而已,最后将其都变成 mxnet.ndarray
。
输入:
anchors
是由 Training mode 的SSD
类实例net
产生的,是 Center boxes 形式的,形状是(1, N, 4)
的mxnet.ndarray
cls_preds
在这里是None
gt_boxes
是(1, M, 4)
的mxnet.ndarray
,是[xmin, ymin, xmax, ymax]
的 Corner 编码,这些都是范围在 0 - 图像宽或高 之间的整数gt_ids
是(1, M, 1)
的mxnet.ndarray
,是不带 Background 的类别标号
anchors = self._center_to_corner(anchors.reshape((-1, 4)))
self._center_to_corner
是 BBoxCenterToCorner
类的实例。
输入:
anchors
是(center_x, center_y, width, height)
的 Center boxes 形式,形状是(1, N, 4)
的mxnet.ndarray
输出:
anchors
是[xmin, ymin, xmax, ymax]
的 Corner boxes 的形式,形状是(N, 4)
的mxnet.ndarray
这一步是将 anchors
由 Center boxes 的形式 (center_x, center_y, width, height)
变成 Corner boxes 的形式 (xmin, ymin, xmax, ymax)
。因为此时的 gt_boxes
还是 Corner boxes 的形式,且也的确是 Corner boxes 形式计算 IoU 方便。因此,到这一步,anchors
和 gt_boxes
都是 Corner boxes 形式。
ious = nd.transpose(nd.contrib.box_iou(anchors, gt_boxes), (1, 0, 2))
输入:
- anchors 是
[xmin, ymin, xmax, ymax]
的形式,形状是(N, 4)
的mxnet.ndarray
- gt_boxes 是
[xmin, ymin, xmax, ymax]
的形式、形状是(1, M, 4)
的mxnet.ndarray
过程:
nd.contrib.box_iou(anchors, gt_boxes)
返回的应该是一个(N, 1, M)
的mxnet.ndarray
;nd.transpose( , (1, 0, 2))
操作使其变成一个(1, N, M)
的mxnet.ndarray
输出:
- ious 是
(1, N, M)
的mxnet.ndarray
,里面的数值都是 IoU 也就是 0-1 的小数
创建 CompositeMatcher
实例
self._matcher = CompositeMatcher([BipartiteMatcher(), MaximumMatcher(iou_thresh)])
调用 CompositeMatcher
实例
matches = self._matcher(ious)
输出:
matches
是一个(1, N)
的mxnet.ndarray
,N 是所有 Layer 所有 Anchor 的总数,里面的数值是:- 如果 match 了,就是对应 gt_bboxes 里面 Groundtruth BBox 的 index;
- 如果不 match,那么里面的元素就是 -1.
因为在创建 SSDTargetGenerator
实例 self._target_generator
的时候因为设置了 negative_mining_ratio=-1
,因此并不会用 OHEMSampler
,而是 NaiveSampler
。
创建 NaiveSampler
实例
self._sampler = NaiveSampler()
调用 NaiveSampler
实例
samples = self._sampler(matches)
输出:
- samples 是一个
(1, N)
的mxnet.ndarray
,N 是所有 Layer 所有 Anchor 的总数,里面的数值是:- 如果 match 了,就是 +1
- 如果不 match,就是 -1
创建 MultiClassEncoder 实例
self._cls_encoder = MultiClassEncoder()
调用 MultiClassEncoder 实例
cls_targets = self._cls_encoder(samples, matches, gt_ids)
输入:
samples
是(1, N)
的mxnet.ndarray
,+1 or -1matches
是(1, N)
的mxnet.ndarray
,bbox 的 index or -1gt_ids
是(1, M, 1)
的mxnet.ndarray
,是不带 Background 的类别标号
过程:
def hybrid_forward(self, F, samples, matches, refs):
refs = F.repeat(refs.reshape((0, 1, -1)), axis=1, repeats=matches.shape[1])
target_ids = F.pick(refs, matches, axis=2) + 1
targets = F.where(samples > 0.5, target_ids, nd.ones_like(target_ids) * self._ignore_label)
targets = F.where(samples < -0.5, nd.zeros_like(targets), targets)
return targets
- 输入的 refs 是
(1, M, 1)
的mxnet.ndarray
,输出的 refs 是(1, N, M)
的mxnet.ndarray
target_ids
是(1, N)
的mxnet.ndarray
,里面的数值,如果某个样本是 match 的,那么就是 match 的那个 bbox 对应的 gt_id,如果不是 match 的,那么就是 最后一个 bbox 的 (gt_id + 1),因为 不 match 返回的是 -1,-1 作为 index 返回的是最后一个 bbox 的 (gt_id + 1),+1 是因为这里把 Background 也算作了一类,标号为 0- 返回的
targets
是一个(1, N)
的矩阵,如果是 match 的,就保留上面的target_ids
里面的数值,如果是不 match 的,类标就为 0 ,背景类不 match,所以虽然在target_ids
中被设置了不正确的类标,但在这一步中被纠正了过来
输出:
cls_targets
是(1, N)
的mxnet.ndarray
N
是所有 Layer 所有 Anchor 的总数- 里面的数值是
0
-(num_classes + 1)
的整数,其中 Background Anchor 的 Label 是0
NormalizedBoxCenterEncoder
顾名思义,这里的 BBox 是「normalize the ground-truth regression targets to have zero mean and unit variance」。
创建 NormalizedBoxCenterEncoder
实例
self._box_encoder = NormalizedBoxCenterEncoder(stds=stds)
调用 NormalizedBoxCenterEncoder
实例
box_targets, box_masks = self._box_encoder(samples, matches, anchors, gt_boxes)
输入:
samples
是(1, N)
的mxnet.ndarray
,+1 or -1matches
是(1, N)
的mxnet.ndarray
,bbox 的 index or -1anchors
是[xmin, ymin, xmax, ymax]
的 Corner boxes 的形式,形状是(N, 4)
的mxnet.ndarray
gt_boxes
是(1, M, 4)
的mxnet.ndarray
,是[xmin, ymin, xmax, ymax]
的 Corner 编码
过程:
def forward(self, samples, matches, anchors, refs):
"""Forward"""
F = nd
# TODO(zhreshold): batch_pick, take multiple elements?
ref_boxes = nd.repeat(refs.reshape((0, 1, -1, 4)), axis=1, repeats=matches.shape[1])
ref_boxes = nd.split(ref_boxes, axis=-1, num_outputs=4, squeeze_axis=True)
ref_boxes = nd.concat(*[F.pick(ref_boxes[i], matches, axis=2).reshape((0, -1, 1)) \
for i in range(4)], dim=2)
g = self.corner_to_center(ref_boxes)
a = self.corner_to_center(anchors)
t0 = ((g[0] - a[0]) / a[2] - self._means[0]) / self._stds[0]
t1 = ((g[1] - a[1]) / a[3] - self._means[1]) / self._stds[1]
t2 = (F.log(g[2] / a[2]) - self._means[2]) / self._stds[2]
t3 = (F.log(g[3] / a[3]) - self._means[3]) / self._stds[3]
codecs = F.concat(t0, t1, t2, t3, dim=2)
temp = F.tile(samples.reshape((0, -1, 1)), reps=(1, 1, 4)) > 0.5
targets = F.where(temp, codecs, F.zeros_like(codecs))
masks = F.where(temp, F.ones_like(temp), F.zeros_like(temp))
return targets, masks
refs.reshape((0, 1, -1, 4))
得到的refs
是(1, 1, M, 4)
,是[xmin, ymin, xmax, ymax]
的 Corner 编码nd.repeat
得到的ref_boxes
是(1, N, M, 4)
nd.split
之后得到的ref_boxes
是List
ofmxnet.ndarray
,里面有 4 个mxnet.ndarray
,每一个都是(1, N, M)
的mxnet.ndarray
F.pick(ref_boxes[i], matches, axis=2)
得到的是 (1, N) 的,里面的元素如果 match,那么就是 GT bbox 的 index,如果不 match,matches 里面的数值是 -1,则得到的是最后一个 GT BBox 的 index,不过这个不要紧,会在后面被修正掉- reshape((0, -1, 1) 后得到的 ref_boxes[i] 是 (1, N, 1)
- nd.concat 后得到的 ref_boxes 是 (1, N, 4),是
[xmin, ymin, xmax, ymax]
的 Corner 编码 - g = self.corner_to_center(ref_boxes) ,因为 split = True,所以得到的 g 是一个 List of mxnet.ndarray,里面的每个元素都是 (1, N, 1),但还是 (center_x, center_y, width, height) 的 Center 编码
- a = self.corner_to_center(anchors),因为 split = True,所以得到的 a 是一个 List of mxnet.ndarray,里面的每个元素都是 (N, 1)
- t0,t1,t2,t3 因为 broadcasting,最后都是 (1, N, 1) 的
- codecs = F.concat(t0, t1, t2, t3, dim=2) 得到的 codecs 是一个 (1, N, 4) 的
- samples.reshape((0, -1, 1)) 是 (1, N, 1),F.tile(samples.reshape((0, -1, 1)), reps=(1, 1, 4)) 之后得到的是 (1, N, 4),> 0.5 得到的 temp 还是一个 (1, N, 4),里面 Positive Anchor 对应的 4 维行向量 是 [1, 1, 1, 1],其余都为 0
- targets = F.where(temp, codecs, F.zeros_like(codecs)) 得到的 targets 就是 (1, N, 4),如果是 Positive Anchor,那么最里面的行向量就是 Normalized 后的 (center_x, center_y, width, height) 的 Center 编码,否则就是 Background Anchor 就是 全零向量
- masks = F.where(temp, F.ones_like(temp), F.zeros_like(temp)),masks 就是一个 mask,是 (1, N, 4),如果是 Positive Anchor,那么最里面的行向量就是 [1,1,1,1],否则就是 Background Anchor 就是 [0,0,0,0]
输出:
- box_targets:(1, N, 4),如果是 Positive Anchor,那么最里面的行向量就是 Normalized 后的 (center_x, center_y, width, height) 的 Center 编码,否则就是 Background Anchor 就是 全零向量
- masks:是 (1, N, 4),如果是 Positive Anchor,那么最里面的行向量就是 [1,1,1,1],否则就是 Background Anchor 就是 [0,0,0,0]
至此,SSDTargetGenerator 的输出,也是 SSDDefaultTrainTransform 的输出就是
cls_targets
:(1, N)
的mxnet.ndarray
N
是所有 Layer 所有 Anchor 的总数- 里面的数值是
0
-(num_classes + 1)
的整数,其中 Background Anchor 的 Label 是0
box_targets
:(1, N, 4)
,如果是 Positive Anchor,那么最里面的行向量就是 Normalized 后的(center_x, center_y, width, height)
的 Center 编码,否则就是 Background Anchor 就是 全零向量box_masks
:是(1, N, 4)
,如果是 Positive Anchor,那么最里面的行向量就是[1,1,1,1]
,否则就是 Background Anchor 就是[0,0,0,0]
return img, cls_targets[0], box_targets[0]
可见,SSDDefaultTrainTransform
返回的是:
img
就是一个(3, H, W)
的 MXNet.NDArray,每个元素的数值为 0 均值,方差为 1 的分布cls_target
是长度为N
的 MXNet.NDArray,注意是N
,不是(1, N)
box_target
:(N, 4)
,如果是 Positive Anchor,那么行向量就是 Normalized 后的(center_x, center_y, width, height)
的 Center 编码,否则就是 Background Anchor 就是 全零向量
对于 Training,其 batchify_fn
是如下定义的:
train_batchify_fn = Tuple(Stack(), Stack(), Stack())
Tuple
函数很有意思,它里面的每个元素都是函数,依次对 Dataset 返回的每个元素做对应的函数。Training 的 batchify_fn
里的每个都是 Stack()
,而 SSDDefaultTrainTransform
后的 Dataset
每一次都返回的都是 (img, cls_target, box_target)
这样的 Pair。而 Stack()
就是把 batch_size
个 img
给拼起来,把 batch_size
个 cls_target
给拼起来,把 batch_size
个 box_target
给拼起来。
之所以能够拼起来,是因为输入图像的大小都一样,而经过 net 后的每一层 Layer 的 Feature 大小都是一样的,因此最后的 Anchor 大小也都是一样的,所以对于每个训练样本,每个 (img, cls_target, box_target)
大小都是一样的。
因此,Training DataLoader
的输出是:
imgs
是(B, 3, H, W)
的 MXNet.NDArray,每个元素的数值为 0 均值,方差为 1 的分布,B
是batch_size
cls_targets
是长度为(B, N)
的 MXNet.NDArray,其中 Background Anchor 的 Label 是 0box_targets
:(B, N, 4)
,如果是 Positive Anchor,那么行向量就是 Normalized 后的(center_x, center_y, width, height)
的 Center 编码,否则就是 Background Anchor 就是 全零向量
SSD
实例的输入只有 imgs
imgs
是(B, 3, H, W)
的 MXNet.NDArray,每个元素的数值为 0 均值,方差为 1 的分布,B
是batch_size
features = self.features(x)
self.features
是一个 VGGAtrousExtractor
类这样的实例函数,输入 x 也就是上面的 imgs
是一个 (B, 3, H, W)
,每一层的 Layer 的输出是 (B, 3, H_i, W_i)
这样的 MXNet.NDArray。
因此,features
是一个 List
of MXNet.NDArray
,里面每一个元素是 (B, 3, H_i, W_i)
这样的 MXNet.NDArray。
cls_preds = [F.flatten(F.transpose(cp(feat), (0, 2, 3, 1)))
for feat, cp in zip(features, self.class_predictors)]
过程:
feat
是(B, C_i, H_i, W_i)
,C_i 是 Layer i 的 Feature Channel Numbercp(feat)
是(batch_size, num_anchors * (num_classes + 1), out_height, out_width)
,简化下就是(B, K_i * (C + 1), H_i, W_i)
。- 经过 transpose 之后得到的是
(B, H_i, W_i, K_i * (C + 1))
- 经过 flatten 之后得到的是
(B, H_i * W_i * K_i * (C + 1))
- 至此,cls_preds 是一个 List of MXNet.NDArray,里面的每个元素是
(B, H_i * W_i * K_i * (C + 1))
cls_preds = F.concat(*cls_preds, dim=1).reshape((0, -1, self.num_classes))
F.concat(*cls_preds, dim=1)
之后得到的是(B, H_0 * W_0 * K_0 * (C + 1) + ... + H_5 * W_5 * K_5 * (C + 1))
,简化写就是(B, N*(C+1))
reshape((0, -1, self.num_classes))
之后得到的cls_preds
是(B, N, C+1)
输出:
cls_preds
是(B, N, C+1)
的MXNet.NDArray
box_preds = [F.flatten(F.transpose(bp(feat), (0, 2, 3, 1)))
for feat, bp in zip(features, self.box_predictors)]
过程:
feat
是(B, C_i, H_i, W_i)
,C_i
是 Layeri
的 Feature Channel Numberbp(feat)
是(batch_size, num_anchors * 4, out_height, out_width)
,简化下就是(B, K_i * 4, H_i, W_i)
transpose
之后得到的是(B, H_i, W_i, K_i * 4)
flatten
之后得到的是(B, H_i * W_i * K_i * 4)
- 至此,box_preds 是是一个 List of MXNet.NDArray,里面每一个元素是
(B, H_i * W_i * K_i * 4)
box_preds = F.concat(*box_preds, dim=1).reshape((0, -1, 4))
过程:
F.concat
得到的是(B, H_0 * W_0 * K_0 * 4 + ... + H_5 * W_5 * K_5 * 4)
,简化写就是(B, N*4)
reshape((0, -1, 4)
之后得到的box_preds
是(B, N, 4)
输出:
box_preds
是(B, N, 4)
的MXNet.NDArray
每一个 Layer 都会调用 SSDAnchorGenerator
的实例生成 anchors
。
创建 SSDAnchorGenerator
的实例:
anchor_generator = SSDAnchorGenerator(index, im_size, sizes, ratios, step, alloc_size=(128, 128))
看 SSDAnchorGenerator
的代码,主要是两部分 _generate_anchors
和 hybrid_forward
- 这个 Layer 尺寸是
(alloc_size[0], alloc_size[1])
,但因为我们默认的alloc_size=(128, 128)
,所以在return
之前,_generate_anchors
得到的anchors
是一个 128 * 128 * num_depth 个元素的 List of List,每个元素 List 是 [cx, cy, w, h],注意 这里的 [cx, cy, w, h] 都是 0 - 图像宽或高这样的 整数,并没有是 0 - 1 这样归一化的 scale。 np.array(anchors).reshape(1, 1, alloc_size[0], alloc_size[1], -1)
则是将其变成(1, 1, alloc_size[0], alloc_size[1], num_depth*4)
的numpy.ndarray
输出:
anchors
是(1, 1, alloc_size[0], alloc_size[1], num_depth*4)
的numpy.ndarray
,里面的内容均是[cx, cy, w, h]
这样的 Center 编码,且数值不是被 scale 到 0 - 1 的,而是 0 - 480 这样的数值。
a = F.slice_like(anchors, x * 0, axes=(2, 3))
- 注意,这里的
anchors
是由_generate_anchors(self, sizes, ratios, step, alloc_size, offsets)
产生的,本身和 输入的 feat 没关系 - anchors 是
(1, 1, alloc_size[0], alloc_size[1], num_depth*4)
- x 是
(batch_size, in_channels, height, width)
- 这句代码的意思,就是 把
anchors
按照 x 的axes=(2, 3)
的大小来做取片,x
的2
、3
两个axes
正好是(height, width)
,其余 保持不变,此时得到的a
是一个(1, 1, height, width, num_depth*4)
的mxnet.ndarray
- 注意这里有个前提假设,就是假设
feat
的(height, width)
都是小于alloc_size
的。alloc_size
的默认值是 128,也就是 512 下采样 4 倍的大小。对于大部分图片,比如480 * 480
的图片,做过 3 次 down-sampling 后就只有 60 了,因此,大部分图片都还可以,但是训练大图像或者在很底层就要求输出的时候要注意这一点。
a = a.reshape((1, -1, 4))
则是进一步把 a
变成 (1, height*width*num_depth, 4)
的 mxnet.ndarray
输出:
- 返回的是
(1, height*width*num_depth, 4)
的mxnet.ndarray
,里面的内容均是[cx, cy, w, h]
这样的 Center 编码,且数值不是被 scale 到 0 - 1 的,而是 0 - 480 这样的数值。
anchors = [F.reshape(ag(feat), shape=(1, -1))
for feat, ag in zip(features, self.anchor_generators)]
ag
是SSDAnchorGenerator
的实例feat
是某一 Layer 的 feature map 输出,是(batch_size, in_channels, height, width)
- ag(feat) 是
(1, height*width*num_depth, 4)
的mxnet.ndarray
,里面的内容均是[cx, cy, w, h]
这样的 Center 编码,且数值不是被 scale 到 0 - 1 的,而是 0 - 480 这样的数值。 - 经过 F.reshape 得到的是
(1, height*width*num_depth*4)
的mxnet.ndarray
- 至此,anchors 是一个 List of
mxnet.ndarray
,里面的每一个元素都是(1, H_i * W_i * K_i * 4)
的mxnet.ndarray
anchors = F.concat(*anchors, dim=1).reshape((1, -1, 4))
- F.concat 之后得到的是
(1, H_0 * W_0 * K_0 * 4 + ... + H_5 * W_5 * K_5 * 4)
的mxnet.ndarray
,简化写就是(1, N * 4)
- 经过
reshape
之后得到的是(1, N, 4)
的mxnet.ndarray
输出:
- 在 Training mode 中 SSD net 返回的 anchors 是
(1, N, 4)
的mxnet.ndarray
,N 是所有 Layer 所有 Anchor 的总数。而这个 4 所带表的是[cx, cy, w, h]
这样的 Center Boxes 形式,且不是归一化的 0-1 的那种,而是 100 这样的正整数
在 在 Training mode 中 SSD
net 返回的 [cls_preds, box_preds, anchors]
分别是:
cls_preds
是(B, N, C+1)
的MXNet.NDArray
box_preds
是(B, N, 4)
的MXNet.NDArray
anchors
是(1, N, 4)
的mxnet.ndarray
,N 是所有 Layer 所有 Anchor 的总数。而这个 4 所带表的是[cx, cy, w, h]
这样的 Center Boxes 形式,且不是归一化的 0-1 的那种,而是 100 这样的正整数
创建 SSDMultiBoxLoss
实例:
mbox_loss = SSDMultiBoxLoss(lambd=0.0001, rho = 1)
调用 SSDMultiBoxLoss
实例:
sum_losses, cls_losses, box_losses = mbox_loss(cls_preds, box_preds, gt_labels, gt_bboxes)
输入:
cls_preds
是(B, N, C+1)
的MXNet.NDArray
box_preds
是(B, N, 4)
的MXNet.NDArray
gt_labels
是长度为(B, N)
的 MXNet.NDArray,其中 Background Anchor 的 Label 是 0box_targets
:(B, N, 4)
,如果是 Positive Anchor,那么行向量就是 Normalized 后的(center_x, center_y, width, height)
的 Center 编码,否则就是 Background Anchor 就是 全零向量
SSDMultiBoxLoss 应该是 归一化的位置,还是 pixel 位置?从 Fast R-CNN 看,计算 L1 Loss 是 Center 编码的(x, y, w, h),在 Fast R-CNN 中有这句话 We normalize the ground-truth regression targets v i to have zero mean and unit variance. vi 是 Groundtruth BBox,在 VOCDetection 完了后会调用 SSDDefaultTrainTransform,这里面会调用 SSDTargetGenerator,而 SSDTargetGenerator 里面会调用 NormalizedBoxCenterEncoder,而这里面会做,但这里的 means=(0., 0., 0., 0.) 和 stds=(0.1, 0.1, 0.2, 0.2) 都预设的
在 NormalizedBoxCenterEncoder 中,
a = self.corner_to_center(anchors)
anchors 会在 NormalizedBoxCenterEncoder 中经历 corner_to_center 操作