Skip to content

Instantly share code, notes, and snippets.

@YimianDai
Last active August 21, 2019 05:32
Show Gist options
  • Save YimianDai/fd097786a7e086454c82df8030a4079b to your computer and use it in GitHub Desktop.
Save YimianDai/fd097786a7e086454c82df8030a4079b to your computer and use it in GitHub Desktop.
Data Flow in SSD

通常我们说机器学习三要素: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 在各个环节中需要的形式以及经历的操作。

1. Training

按照先创建 train_dataset,再对其做 SSDDefaultTrainTransform,然后输入 DataLoader 通过 batchify_fn 拼成一个 batch,接着输入 SSD 的 net 实例,最后输入 SSDMultiBoxLoss 损失函数计算损失的顺序过程看一下 Data 在 Training 时在各环节的流动和变化情况。

这里用 VOCDetection 作为 train_dataset,且使用默认的 transform=None。在 transform=None 的情况下,VOCDetection 只会干一件事,就是 load imglabel,此外不做任何操作。因此

  • 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

1.2.1 Data Augmentation

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 个数

1.2.2 Normalizing

Normalizing 就两步,第一步 to_tensorimg 变成 (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 的类别标号

1.2.3 SSDTargetGenerator

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 的类别标号
1.2.3.1 BBoxCenterToCorner
anchors = self._center_to_corner(anchors.reshape((-1, 4)))

self._center_to_cornerBBoxCenterToCorner 类的实例。

输入:

  • 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 方便。因此,到这一步,anchorsgt_boxes 都是 Corner boxes 形式。

1.2.3.2 box_iou
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 的小数
1.2.3.3 matching

创建 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.
1.2.3.4 sampling

因为在创建 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
1.2.3.5 MultiClassEncoder

创建 MultiClassEncoder 实例

self._cls_encoder = MultiClassEncoder()

调用 MultiClassEncoder 实例

cls_targets = self._cls_encoder(samples, matches, gt_ids)

输入:

  • samples(1, N)mxnet.ndarray,+1 or -1
  • matches(1, N)mxnet.ndarray,bbox 的 index or -1
  • gt_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
1.2.3.6 NormalizedBoxCenterEncoder

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 -1
  • matches(1, N)mxnet.ndarray,bbox 的 index or -1
  • anchors[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_boxesList of mxnet.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]
1.2.3.7 小结

至此,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]

1.2.4 小结

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_sizeimg 给拼起来,把 batch_sizecls_target 给拼起来,把 batch_sizebox_target 给拼起来。

之所以能够拼起来,是因为输入图像的大小都一样,而经过 net 后的每一层 Layer 的 Feature 大小都是一样的,因此最后的 Anchor 大小也都是一样的,所以对于每个训练样本,每个 (img, cls_target, box_target) 大小都是一样的。

因此,Training DataLoader输出是:

  • imgs(B, 3, H, W) 的 MXNet.NDArray,每个元素的数值为 0 均值,方差为 1 的分布,Bbatch_size
  • cls_targets 是长度为 (B, N) 的 MXNet.NDArray,其中 Background Anchor 的 Label 是 0
  • box_targets(B, N, 4),如果是 Positive Anchor,那么行向量就是 Normalized 后的 (center_x, center_y, width, height) 的 Center 编码,否则就是 Background Anchor 就是 全零向量

1.4 SSD

SSD 实例的输入只有 imgs

  • imgs(B, 3, H, W) 的 MXNet.NDArray,每个元素的数值为 0 均值,方差为 1 的分布,Bbatch_size

1.4.1 features

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。

1.4.2 cls_preds

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 Number
  • cp(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

1.4.3 box_preds

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 是 Layer i 的 Feature Channel Number
  • bp(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

1.4.4 anchors

每一个 Layer 都会调用 SSDAnchorGenerator 的实例生成 anchors

1.4.4.1 SSDAnchorGenerator

创建 SSDAnchorGenerator 的实例:

anchor_generator = SSDAnchorGenerator(index, im_size, sizes, ratios, step, alloc_size=(128, 128))

SSDAnchorGenerator 的代码,主要是两部分 _generate_anchorshybrid_forward

1.4.4.1.1 _generate_anchors
  • 这个 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 这样的数值。
1.4.4.1.2 hybrid_forward
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) 的大小来做取片,x23 两个 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 这样的数值。
1.4.4.2 过程
anchors = [F.reshape(ag(feat), shape=(1, -1))
           for feat, ag in zip(features, self.anchor_generators)]
  • agSSDAnchorGenerator 的实例
  • 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 这样的正整数

1.4.5 小结

在 在 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 这样的正整数

1.5 SSDMultiBoxLoss

创建 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 是 0
  • box_targets(B, N, 4),如果是 Positive Anchor,那么行向量就是 Normalized 后的 (center_x, center_y, width, height) 的 Center 编码,否则就是 Background Anchor 就是 全零向量

2. Validation

2.1 VOCDetection

2.2 SSDDefaultValTransform

2.3 VOCMApMetric

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 操作

Reference

[1] 可解释性与 deep learning 的发展

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment