本文提出的SSD算法是一种直接预测目标类别和bounding box的多目标检测算法。与faster rcnn相比,该算法没有生成 proposal 的过程,这就极大提升了检测速度。针对不一样大小的目标检测,传统的作法是先将图像转换成不一样大小(图像金字塔),而后分别检测,最后将结果综合起来(NMS)。而SSD算法则利用不一样卷积层的 feature map 进行综合也能达到一样的效果。文章的核心之一是同时采用 lower 和 upper 尺度的 feature map 作检测。ios
Fig.1 SSD 框架git
算法的主网络结构是 VGG16,将最后两个全链接层改为卷积层,并随后增长了 4 个卷积层来构造网络结构。对其中 5 个不一样大小的卷积层的输出(feature map)分别用两个不一样的 3×3 的卷积核进行卷积,一个输出分类用的 confidence,每一个 default box 生成21个类别 confidence;一个输出回归用的 localization,每一个 default box 生成4个坐标值(x, y, w, h)。此外,这 5 个 feature map 还通过 PriorBox 层生成 prior box(生成的是坐标)。上述 5 个 feature map 中每一层的 default box 的数量是给定的(8732个)。最后将前面三个计算结果分别合并而后传给loss层。github
文章的核心之一是做者同时采用 lower 和 upper 的 feature map 作检测。如图Fig 2 所示,这里假定有 8×8 和 4×4 两种不一样的 feature map。第一个概念是 feature map cell,feature map cell 是指feature map 中每个小格子,如图中分别有 64 和 16 个cell。另外有一个概念:default box,是指在 feature map 的每一个小格(cell)上都有一系列固定大小的 box,以下图有 4 个(下图中的虚线框,仔细看格子的中间有比格子还小的一个box)。假设每一个 feature map cell 有 k 个 default box,对于每一个 default box 都须要预测 c 个类别 score 和 4 个 offset,那么若是一个 feature map 的大小是 m×n,也就是有 m*n 个feature map cell,那么这个feature map就一共有(c+4)*k * m*n 个输出。这些输出个数的含义是:采用3×3的卷积核对该层的feature map卷积时卷积核的个数,包含两部分:数量 c*k*m*n 是 confidence 输出,表示每一个 default box 的 是某一类别的 confidence;数量 4*k*m*n 是 localization 输出,表示每一个 default box 回归后的坐标)。训练中还有一个东西:prior box,是指实际中选择的 default box(你能够认为 default box 是抽象类,而 prior box 是具体实例)。训练中对于每一个图像样本,须要先将 prior box 与 ground truth box 作匹配,匹配成功说明这个 prior box 所包含的是个目标,但离完整目标的 ground truth box 还有段距离,训练的目的是保证 prior box 的分类confidence 的同时将 prior box 尽量回归到 ground truth box。 举个列子:假设一个训练样本中有2个 ground truth box,全部的 feature map 中获取的 prior box一共有8732个。那个可能分别有十、20个prior box能分别与这2个 ground truth box 匹配上。训练的损失包含分类损失和回归损失两部分。算法
Fig.2 default boxes网络
做者的实验代表 default box 的 shape 数量越多,效果越好 (固然耗时也越大)。app
这里用到的 default box 和Faster RCNN中的 anchor 很像,在Faster RCNN中 anchor 只用在最后一个卷积层,可是在本文中,default box 是应用在多个不一样层的 feature map 上。框架
那么default box的 scale(大小)和 aspect ratio(横纵比)要怎么定呢?假设咱们用 m 个 feature maps 作预测,那么对于每一个 featuer map 而言其 default box 的 scale 是按如下公式计算的: dom
$\vee$ide
$S_k=S_{min} + \frac{S_{max} - S_{min}}{m-1}(k-1), k\in{[1, m]}$函数
这里smin是0.2,表示最底层的scale是0.2;smax是0.9,表示最高层的scale是0.9。
至于aspect ratio,用$a_r$表示为下式:注意这里一共有5种aspect ratio
$a_r = \{1, 2, 3, 1/2, 1/3\}$
所以每一个default box的宽的计算公式为:
$w_k^a=s_k\sqrt{a_r}$
高的计算公式为:(很容易理解宽和高的乘积是scale的平方)
$h_k^a=s_k/\sqrt{a_r}$
另外当aspect ratio为1时,做者还增长一种scale的default box:
$s_k^{'}=\sqrt{s_{k}s_{k+1}}$
所以,对于每一个 feature map cell 而言,一共能够有 6 种 default box。
能够看出这种 default box 在不一样的f eature层 有不一样的 scale,在同一个 feature 层又有不一样的 aspect ratio,所以基本上能够覆盖输入图像中的各类形状和大小的 object!
(训练本身的样本的时候能够在 FindMatch() 以后检查是否覆盖了全部得 ground truth box,其实是全覆盖了,由于会至少找一个最大匹配)
具体到代码 ssd_pascal.py 中是这样设计的:这里与论文中的公式有细微变化,本身体会。。。
mbox_source_layers = ['conv4_3', 'fc7', 'conv6_2', 'conv7_2', 'conv8_2', 'conv9_2'] # in percent % min_ratio = 20 max_ratio = 90 step = int(math.floor((max_ratio - min_ratio) / (len(mbox_source_layers) - 2))) min_sizes = [] max_sizes = [] for ratio in xrange(min_ratio, max_ratio + 1, step): min_sizes.append(min_dim * ratio / 100.) max_sizes.append(min_dim * (ratio + step) / 100.) min_sizes = [min_dim * 10 / 100.] + min_sizes max_sizes = [min_dim * 20 / 100.] + max_sizes steps = [8, 16, 32, 64, 100, 300] aspect_ratios = [[2], [2, 3], [2, 3], [2, 3], [2], [2]]
caffe 源码 prior_box_layer.cpp 中是这样提取 prior box 的:
for (int h = 0; h < layer_height; ++h) { for (int w = 0; w < layer_width; ++w) { float center_x = (w + offset_) * step_w; float center_y = (h + offset_) * step_h; float box_width, box_height; for (int s = 0; s < min_sizes_.size(); ++s) { int min_size_ = min_sizes_[s]; // first prior: aspect_ratio = 1, size = min_size box_width = box_height = min_size_; // xmin top_data[idx++] = (center_x - box_width / 2.) / img_width; // ymin top_data[idx++] = (center_y - box_height / 2.) / img_height; // xmax top_data[idx++] = (center_x + box_width / 2.) / img_width; // ymax top_data[idx++] = (center_y + box_height / 2.) / img_height; if (max_sizes_.size() > 0) { CHECK_EQ(min_sizes_.size(), max_sizes_.size()); int max_size_ = max_sizes_[s]; // second prior: aspect_ratio = 1, size = sqrt(min_size * max_size) box_width = box_height = sqrt(min_size_ * max_size_); // xmin top_data[idx++] = (center_x - box_width / 2.) / img_width; // ymin top_data[idx++] = (center_y - box_height / 2.) / img_height; // xmax top_data[idx++] = (center_x + box_width / 2.) / img_width; // ymax top_data[idx++] = (center_y + box_height / 2.) / img_height; } // rest of priors for (int r = 0; r < aspect_ratios_.size(); ++r) { float ar = aspect_ratios_[r]; if (fabs(ar - 1.) < 1e-6) { continue; } box_width = min_size_ * sqrt(ar); box_height = min_size_ / sqrt(ar); // xmin top_data[idx++] = (center_x - box_width / 2.) / img_width; // ymin top_data[idx++] = (center_y - box_height / 2.) / img_height; // xmax top_data[idx++] = (center_x + box_width / 2.) / img_width; // ymax top_data[idx++] = (center_y + box_height / 2.) / img_height; } } } }
具体到每个 feature map上得到 prior box 时,会从这 6 种中进行选择。以下表和图所示最后会获得(38*38*4 + 19*19*6 + 10*10*6 + 5*5*6 + 3*3*4 + 1*1*4)= 8732 个 prior box。
feature map | feature map size | min_size($s_k$) | max_size($s_{k+1}$) | aspect_ratio | step | offset | variance |
conv4_3 | 38×38 | 30 | 60 | 1,2 | 8 | 0.50 | 0.1, 0.1, 0.2, 0.2 |
fc6 | 19×19 | 60 | 111 | 1,2,3 | 16 | ||
conv6_2 | 10×10 | 111 | 162 | 1,2,3 | 32 | ||
conv7_2 | 5×5 | 162 | 213 | 1,2,3 | 64 | ||
conv8_2 | 3×3 | 213 | 264 | 1,2 | 100 | ||
conv9_2 | 1×1 | 264 | 315 | 1,2 | 300 |
将 prior box 和 grount truth box 按照IOU(JaccardOverlap)进行匹配,匹配成功则这个 prior box 就是positive example(正样本),若是匹配不上,就是 negative example(负样本),显然这样产生的负样本的数量要远远多于正样本。这里默认作了难例挖掘:简单描述起来就是,将全部的匹配不上的 negative prior box 按照前向 loss 进行排序,选择最高的 num_sel 个 prior box 序号集合做为最终的负样本集。这里就能够利用 num_sel 来控制最后正、负样本的比例在 1:3 左右。
Fig.3 positive and negtive sample VS ground_truth box
咱们已经在图上画出了prior box,同时也有了ground truth,那么下一步就是将prior box匹配到ground truth上,这是在 src/caffe/utlis/bbox_util.cpp
的 FindMatches 以及子函数 MatchBBox 函数里完成的。具体的:先从 groudtruth box 出发,为每一个 groudtruth box 找到最匹配的一个 prior box 放入候选正样本集;而后再尝试从剩下的每一个 prior box 出发,寻找与 groundtruth box 知足 $IOU>0.5$ 的 最大匹配,若是找到了这样的一个匹配结果就放入候选正样本集。这个作的目的是保证每一个 groundtruth box 都至少有一个匹配正样本
void FindMatches(const vector<LabelBBox>& all_loc_preds, // const map<int, vector<NormalizedBBox> >& all_gt_bboxes, // 全部的 ground truth const vector<NormalizedBBox>& prior_bboxes, // 全部的default boxes,8732个 const vector<vector<float> >& prior_variances, const MultiBoxLossParameter& multibox_loss_param, vector<map<int, vector<float> > >* all_match_overlaps, // 全部匹配上的default box jaccard overlap vector<map<int, vector<int> > >* all_match_indices) { // 全部匹配上的default box序号 const int num_classes = multibox_loss_param.num_classes(); // 类别总数 = 21 const bool share_location = multibox_loss_param.share_location(); // 共享? true const int loc_classes = share_location ? 1 : num_classes; // 1 const MatchType match_type = multibox_loss_param.match_type(); // MultiBoxLossParameter_MatchType_PER_PREDICTION const float overlap_threshold = multibox_loss_param.overlap_threshold(); // jaccard overlap = 0.5 const bool use_prior_for_matching =multibox_loss_param.use_prior_for_matching(); // true const int background_label_id = multibox_loss_param.background_label_id(); const CodeType code_type = multibox_loss_param.code_type(); const bool encode_variance_in_target = multibox_loss_param.encode_variance_in_target(); const bool ignore_cross_boundary_bbox = multibox_loss_param.ignore_cross_boundary_bbox(); // Find the matches. int num = all_loc_preds.size(); for (int i = 0; i < num; ++i) { map<int, vector<int> > match_indices; // 匹配上的default box 序号 map<int, vector<float> > match_overlaps; // 匹配上的default box jaccard overlap // Check if there is ground truth for current image. if (all_gt_bboxes.find(i) == all_gt_bboxes.end()) { // There is no gt for current image. All predictions are negative. all_match_indices->push_back(match_indices); all_match_overlaps->push_back(match_overlaps); continue; } // Find match between predictions and ground truth. const vector<NormalizedBBox>& gt_bboxes = all_gt_bboxes.find(i)->second; // N个ground truth if (!use_prior_for_matching) { for (int c = 0; c < loc_classes; ++c) { int label = share_location ? -1 : c; if (!share_location && label == background_label_id) { // Ignore background loc predictions. continue; } // Decode the prediction into bbox first. vector<NormalizedBBox> loc_bboxes; bool clip_bbox = false; DecodeBBoxes(prior_bboxes, prior_variances, code_type, encode_variance_in_target, clip_bbox, all_loc_preds[i].find(label)->second, &loc_bboxes); MatchBBox(gt_bboxes, loc_bboxes, label, match_type, overlap_threshold, ignore_cross_boundary_bbox, &match_indices[label], &match_overlaps[label]); } } else { // Use prior bboxes to match against all ground truth. vector<int> temp_match_indices; vector<float> temp_match_overlaps; const int label = -1; MatchBBox(gt_bboxes, prior_bboxes, label, match_type, overlap_threshold, ignore_cross_boundary_bbox, &temp_match_indices, &temp_match_overlaps); if (share_location) { match_indices[label] = temp_match_indices; match_overlaps[label] = temp_match_overlaps; } else { // Get ground truth label for each ground truth bbox. vector<int> gt_labels; for (int g = 0; g < gt_bboxes.size(); ++g) { gt_labels.push_back(gt_bboxes[g].label()); } // Distribute the matching results to different loc_class. for (int c = 0; c < loc_classes; ++c) { if (c == background_label_id) { // Ignore background loc predictions. continue; } match_indices[c].resize(temp_match_indices.size(), -1); match_overlaps[c] = temp_match_overlaps; for (int m = 0; m < temp_match_indices.size(); ++m) { if (temp_match_indices[m] > -1) { const int gt_idx = temp_match_indices[m]; CHECK_LT(gt_idx, gt_labels.size()); if (c == gt_labels[gt_idx]) { match_indices[c][m] = gt_idx; } } } } } } all_match_indices->push_back(match_indices); all_match_overlaps->push_back(match_overlaps); } }
void MatchBBox(const vector<NormalizedBBox>& gt_bboxes, const vector<NormalizedBBox>& pred_bboxes, const int label, const MatchType match_type, const float overlap_threshold, const bool ignore_cross_boundary_bbox, vector<int>* match_indices, vector<float>* match_overlaps) { int num_pred = pred_bboxes.size(); match_indices->clear(); match_indices->resize(num_pred, -1); match_overlaps->clear(); match_overlaps->resize(num_pred, 0.); int num_gt = 0; vector<int> gt_indices; if (label == -1) { // label -1 means comparing against all ground truth. num_gt = gt_bboxes.size(); for (int i = 0; i < num_gt; ++i) { gt_indices.push_back(i); } } else { // Count number of ground truth boxes which has the desired label. for (int i = 0; i < gt_bboxes.size(); ++i) { if (gt_bboxes[i].label() == label) { num_gt++; gt_indices.push_back(i); } } } if (num_gt == 0) { return; } // Store the positive overlap between predictions and ground truth. map<int, map<int, float> > overlaps; for (int i = 0; i < num_pred; ++i) { if (ignore_cross_boundary_bbox && IsCrossBoundaryBBox(pred_bboxes[i])) { (*match_indices)[i] = -2; continue; } for (int j = 0; j < num_gt; ++j) { float overlap = JaccardOverlap(pred_bboxes[i], gt_bboxes[gt_indices[j]]); if (overlap > 1e-6) { (*match_overlaps)[i] = std::max((*match_overlaps)[i], overlap); overlaps[i][j] = overlap; } } } // Bipartite matching. vector<int> gt_pool; for (int i = 0; i < num_gt; ++i) { gt_pool.push_back(i); } while (gt_pool.size() > 0) { // Find the most overlapped gt and cooresponding predictions. int max_idx = -1; int max_gt_idx = -1; float max_overlap = -1; for (map<int, map<int, float> >::iterator it = overlaps.begin(); it != overlaps.end(); ++it) { int i = it->first; if ((*match_indices)[i] != -1) { // The prediction already has matched ground truth or is ignored. continue; } for (int p = 0; p < gt_pool.size(); ++p) { int j = gt_pool[p]; if (it->second.find(j) == it->second.end()) { // No overlap between the i-th prediction and j-th ground truth. continue; } // Find the maximum overlapped pair. if (it->second[j] > max_overlap) { // If the prediction has not been matched to any ground truth, // and the overlap is larger than maximum overlap, update. max_idx = i; max_gt_idx = j; max_overlap = it->second[j]; } } } if (max_idx == -1) { // Cannot find good match. break; } else { CHECK_EQ((*match_indices)[max_idx], -1); (*match_indices)[max_idx] = gt_indices[max_gt_idx]; (*match_overlaps)[max_idx] = max_overlap; // Erase the ground truth. gt_pool.erase(std::find(gt_pool.begin(), gt_pool.end(), max_gt_idx)); } } switch (match_type) { case MultiBoxLossParameter_MatchType_BIPARTITE: // Already done. break; case MultiBoxLossParameter_MatchType_PER_PREDICTION: // Get most overlaped for the rest prediction bboxes. for (map<int, map<int, float> >::iterator it = overlaps.begin(); it != overlaps.end(); ++it) { int i = it->first; if ((*match_indices)[i] != -1) { // The prediction already has matched ground truth or is ignored. continue; } int max_gt_idx = -1; float max_overlap = -1; for (int j = 0; j < num_gt; ++j) { if (it->second.find(j) == it->second.end()) { // No overlap between the i-th prediction and j-th ground truth. continue; } // Find the maximum overlapped pair. float overlap = it->second[j]; if (overlap >= overlap_threshold && overlap > max_overlap) { // If the prediction has not been matched to any ground truth, // and the overlap is larger than maximum overlap, update. max_gt_idx = j; max_overlap = overlap; } } if (max_gt_idx != -1) { // Found a matched ground truth. CHECK_EQ((*match_indices)[i], -1); (*match_indices)[i] = gt_indices[max_gt_idx]; (*match_overlaps)[i] = max_overlap; } } break; default: LOG(FATAL) << "Unknown matching type."; break; } return; }
这就使得一个ground truth box中咱们可能得到多个候选正样本。
在生成一系列的 prior boxes 以后,会产生不少个符合 ground truth box 的 positive boxes(候选正样本集),但同时,不符合 ground truth boxes 更多,这个 negative boxes(候选负样本集)远多于 positive boxes。这会形成 negative boxes、positive boxes 之间的不均衡。训练时难以收敛。
所以本文采起,先将每个物体位置上对应 predictions(prior boxes)loss 进行排序。 对于候选正样本集:选择 loss 最高的几个 prior box 集合与候选正样本集进行匹配(box索引同时存在于这两个集合里则匹配成功),匹配不成功则删除这个正样本(由于这个正样本不在难例里已经很接近ground truth box了,不须要再训练了);对于候选负样本集:选择 loss 最高的几个 prior box 与候选负样本集匹配,匹配成功则做为负样本。这就是一个难例挖掘的过程,举个例子,假设在这8732个prior box里,通过 FindMatches 后获得候选正样本 $P$ 个,候选负样本那就有 $8732-P$ 个。将 prior box 的 prediction loss 按照从大到小顺序排列后选择最高的 $M$ 个 prior box。若是这 $P$ 个候选正样本里有 $a$ 个box不在这 $M$ 个prior box里,将这 $a$ 个box从候选正样本集中踢出去。若是这 $8732-P$ 个候选负样本集中包含的$ 8732-P$ 有 $b$ 个在这 $M$ 个 prior box,则将这 $b$ 个候选负样本做为最终的负样本。总归一句话就是:选择 loss 值高的难例做为最后的负样本参与 loss 反传计算。SSD算法中经过这种方式来保证 positives、negatives 的比例。实际代码中有三种负样本挖掘方式:
若是选择HARD_EXAMPLE方式(源于论文Training Region-based Object Detectors with Online Hard Example Mining),则默认$M = 64$,因为没法控制正样本数量,这种方式就有点相似于分类、回归按比重不一样交替训练了。
若是选择MAX_NEGATIVE方式,则 $M = P*neg\_pos\_ratio$,这里当 $neg\_pos\_ratio = 3$ 的时候,就是论文中的正负样本比例 1:3 了。
enum MultiBoxLossParameter_MiningType { MultiBoxLossParameter_MiningType_NONE = 0, MultiBoxLossParameter_MiningType_MAX_NEGATIVE = 1, MultiBoxLossParameter_MiningType_HARD_EXAMPLE = 2 };
以prior box为基准,SSD里的回归目标不是简单的中心点误差以及宽、高缩放。由于涉及到一个编码的过程,这里简单说一下默认的解码过程,编码是个反过程:
输入: 预约义prior box = [prior_center_x, prior_center_y, prior_width, prior_height]
预测输出 predict box = [bbox.xmin(), bbox.ymin(), bbox.xmax), bbox.ymax()]
编码系数 prior_variance = [0.1, 0.1, 0.2, 0.2]
输出 decode_bbox
decode_bbox_center_x = prior_variance[0] * bbox.xmin() * prior_width + prior_center_x; decode_bbox_center_y = prior_variance[1] * bbox.ymin() * prior_height + prior_center_y; decode_bbox_width = exp(prior_variance[2] * bbox.xmax()) * prior_width; decode_bbox_height = exp(prior_variance[3] * bbox.ymax()) * prior_height; decode_bbox->set_xmin(decode_bbox_center_x - decode_bbox_width / 2.); decode_bbox->set_ymin(decode_bbox_center_y - decode_bbox_height / 2.); decode_bbox->set_xmax(decode_bbox_center_x + decode_bbox_width / 2.); decode_bbox->set_ymax(decode_bbox_center_y + decode_bbox_height / 2.);
本文同时对训练数据作了 data augmentation,数据增广。
每一张训练图像,随机的进行以下几种选择:
采样的 patch 是原始图像大小比例是 [0.3,1.0],aspect ratio 在 0.5 或 2。
当 groundtruth box 的 中心(center)在采样的 patch 中且在采样的 patch中 groundtruth box面积大于0时,咱们保留CropImage。
在这些采样步骤以后,每个采样的 patch 被 resize 到固定的大小,而且以 0.5 的几率随机的 水平翻转(horizontally flipped,翻转不翻转看prototxt,默认不翻转)
这样一个样本被诸多batch_sampler采样器采样后会生成多个候选样本,而后从中随机选一个样本送人网络训练。
// Sample a bbox in the normalized space [0, 1] with provided constraints. message Sampler { // 最大最小scale数 optional float min_scale = 1 [default = 1.]; optional float max_scale = 2 [default = 1.]; // 最大最小采样长宽比,真实的长宽比在这两个数中间取值 optional float min_aspect_ratio = 3 [default = 1.]; optional float max_aspect_ratio = 4 [default = 1.]; }
// Constraints for selecting sampled bbox. message SampleConstraint { // Minimum Jaccard overlap between sampled bbox and all bboxes in // AnnotationGroup. optional float min_jaccard_overlap = 1; // Maximum Jaccard overlap between sampled bbox and all bboxes in // AnnotationGroup. optional float max_jaccard_overlap = 2; // Minimum coverage of sampled bbox by all bboxes in AnnotationGroup. optional float min_sample_coverage = 3; // Maximum coverage of sampled bbox by all bboxes in AnnotationGroup. optional float max_sample_coverage = 4; // Minimum coverage of all bboxes in AnnotationGroup by sampled bbox. optional float min_object_coverage = 5; // Maximum coverage of all bboxes in AnnotationGroup by sampled bbox. optional float max_object_coverage = 6; } 咱们们每每只用max_jaccard_overlap
// Sample a batch of bboxes with provided constraints. message BatchSampler { // 是否使用原来的图片 optional bool use_original_image = 1 [default = true]; // sampler的参数 optional Sampler sampler = 2; // 对于采样box的限制条件,决定一个采样数据positive or negative optional SampleConstraint sample_constraint = 3; // 当采样总数知足条件时,直接结束 optional uint32 max_sample = 4; // 为了不死循环,采样最大try的次数. optional uint32 max_trials = 5 [default = 100]; }
message TransformationParameter { // 对于数据预处理,咱们能够仅仅进行scaling和减掉预先提供的平均值。 // 须要注意的是在scaling以前要先减掉平均值 optional float scale = 1 [default = 1]; // 是否随机镜像操做 optional bool mirror = 2 [default = false]; // 是否随机crop操做 optional uint32 crop_size = 3 [default = 0]; optional uint32 crop_h = 11 [default = 0]; optional uint32 crop_w = 12 [default = 0]; // 提供mean_file的路径,可是不能和mean_value同时提供 // if specified can be repeated once (would substract it from all the // channels) or can be repeated the same number of times as channels // (would subtract them from the corresponding channel) optional string mean_file = 4; repeated float mean_value = 5; // Force the decoded image to have 3 color channels. optional bool force_color = 6 [default = false]; // Force the decoded image to have 1 color channels. optional bool force_gray = 7 [default = false]; // Resize policy optional ResizeParameter resize_param = 8; // Noise policy optional NoiseParameter noise_param = 9; // Distortion policy optional DistortionParameter distort_param = 13; // Expand policy optional ExpansionParameter expand_param = 14; // Constraint for emitting the annotation after transformation. optional EmitConstraint emit_constraint = 10; }
transform_param { mirror: true mean_value: 104 mean_value: 117 mean_value: 123 resize_param { prob: 1 resize_mode: WARP height: 300 width: 300 interp_mode: LINEAR interp_mode: AREA interp_mode: NEAREST interp_mode: CUBIC interp_mode: LANCZOS4 } emit_constraint { emit_type: CENTER } distort_param { brightness_prob: 0.5 brightness_delta: 32 contrast_prob: 0.5 contrast_lower: 0.5 contrast_upper: 1.5 hue_prob: 0.5 hue_delta: 18 saturation_prob: 0.5 saturation_lower: 0.5 saturation_upper: 1.5 random_order_prob: 0.0 } expand_param { prob: 0.5 max_expand_ratio: 4.0 } } annotated_data_param { batch_sampler { max_sample: 1 max_trials: 1 } batch_sampler { sampler { min_scale: 0.3 max_scale: 1.0 min_aspect_ratio: 0.5 max_aspect_ratio: 2.0 } sample_constraint { min_jaccard_overlap: 0.1 } max_sample: 1 max_trials: 50 } batch_sampler { sampler { min_scale: 0.3 max_scale: 1.0 min_aspect_ratio: 0.5 max_aspect_ratio: 2.0 } sample_constraint { min_jaccard_overlap: 0.3 } max_sample: 1 max_trials: 50 } batch_sampler { sampler { min_scale: 0.3 max_scale: 1.0 min_aspect_ratio: 0.5 max_aspect_ratio: 2.0 } sample_constraint { min_jaccard_overlap: 0.5 } max_sample: 1 max_trials: 50 } batch_sampler { sampler { min_scale: 0.3 max_scale: 1.0 min_aspect_ratio: 0.5 max_aspect_ratio: 2.0 } sample_constraint { min_jaccard_overlap: 0.7 } max_sample: 1 max_trials: 50 } batch_sampler { sampler { min_scale: 0.3 max_scale: 1.0 min_aspect_ratio: 0.5 max_aspect_ratio: 2.0 } sample_constraint { min_jaccard_overlap: 0.9 } max_sample: 1 max_trials: 50 } batch_sampler { sampler { min_scale: 0.3 max_scale: 1.0 min_aspect_ratio: 0.5 max_aspect_ratio: 2.0 } sample_constraint { max_jaccard_overlap: 1.0 } max_sample: 1 max_trials: 50 } label_map_file: "E:/tyang/caffe-master_/data/VOC0712/labelmap_voc.prototxt" }
Fig.4 SSD data argument
SSD的结构在VGG16网络的基础上进行修改,训练时一样为conv1_1,conv1_2,conv2_1,conv2_2,conv3_1,conv3_2,conv3_3,conv4_1,conv4_2,conv4_3,conv5_1,conv5_2,conv5_3(512),fc6通过3*3*1024的卷积(原来VGG16中的fc6是全链接层,这里变成卷积层,下面的fc7层同理),fc7通过1*1*1024的卷积,conv6_1,conv6_2(对应上图的conv8_2),conv7_1,conv7_2,conv,8_1,conv8_2,conv9_1,conv9_2,loss。而后针对conv4_3(4),fc7(6),conv6_2(6),conv7_2(6),conv8_2(4),conv9_2(4)的每个再分别采用两个3*3大小的卷积核进行卷积,这两个卷积核是并列的(括号里的数字表明prior box的数量,能够参考Caffe代码,因此上图中SSD结构的倒数第二列的数字8732表示的是全部prior box的数量,是这么来的38*38*4+19*19*6+10*10*6+5*5*6+3*3*4+1*1*4=8732),这两个3*3的卷积核一个是用来作localization的(回归用,若是prior box是6个,那么就有6*4=24个这样的卷积核,卷积后map的大小和卷积前同样,由于pad=1,下同),另外一个是用来作confidence的(分类用,若是prior box是6个,VOC的object类别有20个,那么就有6*(20+1)=126个这样的卷积核)。以下图是conv6_2的localizaiton的3*3卷积核操做,卷积核个数是24(6*4=24,因为pad=1,因此卷积结果的map大小不变,下同):这里的permute层就是交换的做用,好比你卷积后的维度是32×24×19×19,那么通过交换层后就变成32×19×19×24,顺序变了而已。而flatten层的做用就是将32×19×19×24变成32*8664,32是batchsize的大小。
Fig.5 SSD 流程
SSD 网络中输入图片尺寸是3×300×300,通过pool5层后输出为512×19×19,接下来通过fc6(改为卷积层)
layer { name: "fc6" type: "Convolution" bottom: "pool5" top: "fc6" param { lr_mult: 1.0 decay_mult: 1.0 } param { lr_mult: 2.0 decay_mult: 0.0 } convolution_param { num_output: 1024 pad: 6 kernel_size: 3 weight_filler { type: "xavier" } bias_filler { type: "constant" value: 0.0 } dilation: 6 } }
这里来简单提下卷积和池化层先后尺寸的变化:
公式以下,其中,input表明height或者width; dilation默认为1,因此默认kernel_extent=kernel:
$output = \frac{(input + 2*pad - kernel\_extern)}{stride} +1$
$kernel\_extern = dilation * (kernel - 1) + 1$
注意:具体计算时,对于没法整除的状况,Convolution向下取整,Pooling向上取整。。
计算下来会获得fc6层的输出维度:1024×19×19,fc7层:1024×19×19,conv6_1:256×19×19,conv6_2:512×10×10,conv7_1:128×10×10,conv7_2:256×5×5,conv8_1:128×5×5,conv8_2:256×3×3,conv9_1:128×3×3,conv9_2:256×1×1。
计算完后,咱们来看看用来检测的6个 feature map 的维度:
feature map | conv4_3 | fc7 | conv6_2 | conv7_2 | conv8_2 | conv9_2 |
size | 512×38×38 | 1024×19×19 | 512×10×10 | 256×5×5 | 256×3×3 | 256×1×1 |
每一个用来检测的特征层,可使用一系列 convolutional filters,去产生一系列固定大小的 predictions。对于一个大小为$m×n$,具备$p$通道的feature map,使用的 convolutional filters 就是$3×3×p$的kernels。产生的 predictions,要么就是归属类别的一个score,要么就是相对于 prior box coordinate 的 shape offsets。
对于score,通过卷积预测器后的输出维度为(c*k)×m×n,这里c是类别总数,k是该层设定的default box种类(不一样层k的取值不一样,分别为4,6,6,6,4,4)
layer { name: "conv6_2_mbox_conf" type: "Convolution" bottom: "conv6_2" top: "conv6_2_mbox_conf" param { lr_mult: 1.0 decay_mult: 1.0 } param { lr_mult: 2.0 decay_mult: 0.0 } convolution_param { num_output: 126 pad: 1 kernel_size: 3 stride: 1 weight_filler { type: "xavier" } bias_filler { type: "constant" value: 0.0 } } }
维度变化为:
layer | conv4_3_norm_mbox_ conf |
fc7_mbox_ conf |
conv6_2_mbox_ conf |
conv7_2_mbox_ conf |
conv8_2_mbox_ conf |
conv9_2_mbox_ conf |
size | 84 38 38 | 126 19 19 | 126 10 10 | 126 5 5 | 84 3 3 | 84 1 1 |
最后通过 permute 层交换维度
layer | conv4_3_norm_mbox_ conf_perm |
fc7_mbox_ conf_perm |
conv6_2_mbox_ conf_perm |
conv7_2_mbox_ conf_perm |
conv8_2_mbox_ conf_perm |
conv9_2_mbox_ conf_perm |
size | 38 38 84 | 19 19 126 | 10 10 126 | 5 5 126 | 3 3 84 | 1 1 84 |
最后通过 flatten 层整合
layer | conv4_3_norm_mbox_ conf_flat |
fc7_mbox_ conf_flat |
conv6_2_mbox_ conf_flat |
conv7_2_mbox_ conf_flat |
conv8_2_mbox_ conf_flat |
conv9_2_mbox_ conf_flat |
size | 121296 | 45486 | 12600 | 3150 | 756 | 84 |
对于offset,通过卷积预测器后的输出值为(4*k)×m×n,这里4是(x,y,w,h),k是该层设定的prior box数量(不一样层k的取值不一样,分别为4,6,6,6,4,4):
layer { name: "conv6_2_mbox_loc" type: "Convolution" bottom: "conv6_2" top: "conv6_2_mbox_loc" param { lr_mult: 1.0 decay_mult: 1.0 } param { lr_mult: 2.0 decay_mult: 0.0 } convolution_param { num_output: 24 pad: 1 kernel_size: 3 stride: 1 weight_filler { type: "xavier" } bias_filler { type: "constant" value: 0.0 } } }
维度变化为:
layer | conv4_3_norm_mbox_ loc |
fc7_mbox_ loc |
conv6_2_mbox_ loc |
conv7_2_mbox_ loc |
conv8_2_mbox_ loc |
conv9_2_mbox_ loc |
size | 16 38 38 | 24 19 19 | 24 10 10 | 24 5 5 | 16 3 3 | 16 1 1 |
随后通过 permute 层交换维度
layer | conv4_3_norm_mbox_ loc_perm |
fc7_mbox_ loc_perm |
conv6_2_mbox_ loc_perm |
conv7_2_mbox_ loc_perm |
conv8_2_mbox_ loc_perm |
conv9_2_mbox_ loc_perm |
size | 38 38 16 | 19 19 24 | 10 10 24 | 5 5 24 | 3 3 16 | 1 1 16 |
最后通过 flatten 层整合
layer | conv4_3_norm_mbox_ loc_flat |
fc7_mbox_ loc_flat |
conv6_2_mbox_ loc_flat |
conv7_2_mbox_ loc_flat |
conv8_2_mbox_ loc_flat |
conv9_2_mbox_ loc_flat |
size | 23104 | 8664 | 2400 | 600 | 144 | 16 |
同时,各个feature map层通过priorBox层生成prior box
生成prior box的操做:根据最小尺寸 scale以及横纵比aspect ratio按照步长step生成,step表示该层的一个像素点至关于输入图像里的尺寸,简单讲就是感觉野,源码里面是经过将原始的 input image的大小除以该层 feature map的大小来获得的。variance是一个尺度变换因子,本文的四个坐标采用的是中心坐标加上长宽,计算loss的时候可能须要对中心坐标的loss和长宽的loss作一个权衡,因此有了这个variance。若是采用的是box的四大顶点坐标这种方式,默认variance都是0.1,即相互之间没有权重差别。It is used to encode the ground truth box w.r.t. the prior box. You can check this function. Note that it is used in the original MultiBox paper by Erhan etal. It is also used in Faster R-CNN as well. I think the major goal of including the variance is to scale the gradient. Of course you can also think of it as approximate a gaussian distribution with variance of 0.1 around the box coordinates.
layer { name: "conv6_2_mbox_priorbox" type: "PriorBox" bottom: "conv6_2" bottom: "data" top: "conv6_2_mbox_priorbox" prior_box_param { min_size: 111.0 max_size: 162.0 aspect_ratio: 2.0 aspect_ratio: 3.0 flip: true clip: false variance: 0.10000000149 variance: 0.10000000149 variance: 0.20000000298 variance: 0.20000000298 step: 32.0 offset: 0.5 } }
维度变化为:
layer | conv4_3_norm_mbox_ priorbox |
fc7_mbox_ priorbox |
conv6_2_mbox_ priorbox |
conv7_2_mbox_ priorbox |
conv8_2_mbox_ priorbox |
conv9_2_mbox_ priorbox |
size | 2 23104 | 2 8664 | 2 2400 | 2 600 | 2 144 | 2 16 |
通过上述3个操做后,对每一层feature的处理就结束了。
对前面所列的5个卷积层输出都执行上述的操做后,就将获得的结果合并:采用Concat,相似googleNet的Inception操做,是通道合并而不是数值相加。
layer { name: "mbox_loc" type: "Concat" bottom: "conv4_3_norm_mbox_loc_flat" bottom: "fc7_mbox_loc_flat" bottom: "conv6_2_mbox_loc_flat" bottom: "conv7_2_mbox_loc_flat" bottom: "conv8_2_mbox_loc_flat" bottom: "conv9_2_mbox_loc_flat" top: "mbox_loc" concat_param { axis: 1 } } layer { name: "mbox_conf" type: "Concat" bottom: "conv4_3_norm_mbox_conf_flat" bottom: "fc7_mbox_conf_flat" bottom: "conv6_2_mbox_conf_flat" bottom: "conv7_2_mbox_conf_flat" bottom: "conv8_2_mbox_conf_flat" bottom: "conv9_2_mbox_conf_flat" top: "mbox_conf" concat_param { axis: 1 } } layer { name: "mbox_priorbox" type: "Concat" bottom: "conv4_3_norm_mbox_priorbox" bottom: "fc7_mbox_priorbox" bottom: "conv6_2_mbox_priorbox" bottom: "conv7_2_mbox_priorbox" bottom: "conv8_2_mbox_priorbox" bottom: "conv9_2_mbox_priorbox" top: "mbox_priorbox" concat_param { axis: 2 } }
这是几个通道合并后的维度:
layer | mbox_loc | mbox_conf | mbox_priorbox |
size | 34928(8732*4) | 183372(8732*21) | 2 34928(8732*4) |
最后就是做者自定义的损失函数层,这里的overlap_threshold表示prior box和ground truth的重合度超过这个阈值则为正样本。另外我以为具体哪些prior box是正样本,哪些是负样本是在loss层计算出来的,不过这个细节与算法关系不大:
layer { name: "mbox_loss" type: "MultiBoxLoss" bottom: "mbox_loc" bottom: "mbox_conf" bottom: "mbox_priorbox" bottom: "label" top: "mbox_loss" include { phase: TRAIN } propagate_down: true propagate_down: true propagate_down: false propagate_down: false loss_param { normalization: VALID } multibox_loss_param { loc_loss_type: SMOOTH_L1 conf_loss_type: SOFTMAX loc_weight: 1.0 num_classes: 21 share_location: true match_type: PER_PREDICTION overlap_threshold: 0.5 use_prior_for_matching: true background_label_id: 0 use_difficult_gt: true neg_pos_ratio: 3.0 neg_overlap: 0.5 code_type: CENTER_SIZE ignore_cross_boundary_bbox: false mining_type: MAX_NEGATIVE } }
损失函数方面:和Faster RCNN的基本同样,由分类和回归两部分组成,能够参考Faster RCNN,这里不细讲。总之,回归部分的loss是但愿预测的box和prior box的差距尽量跟ground truth和prior box的差距接近,这样预测的box就能尽可能和ground truth同样。
上面获得的8732个目标框通过Jaccard Overlap筛选剩下几个了;其中不知足的框标记为负数,其他留下的标为正数框。紧随其后:
训练过程当中的 prior boxes 和 ground truth boxes 的匹配,基本思路是:让每个 prior box 回归而且到 ground truth box,这个过程的调控咱们须要损失层的帮助,他会计算真实值和预测值之间的偏差,从而指导学习的走向。
SSD 训练的目标函数(training objective)源自于 MultiBox 的目标函数,可是本文将其拓展,使其能够处理多个目标类别。具体过程是咱们会让每个 prior box 通过Jaccard系数计算和真实框的类似度,阈值只有大于 0.5 的才能够列为候选名单;假设选择出来的是N个匹配度高于百分之五十的框吧,咱们令 i 表示第 i 个默认框,j 表示第 j 个真实框,p表示第p个类。那么$x_{ij}^p$ 表示 第 i 个 prior box 与 类别 p 的 第 j 个 ground truth box 相匹配的Jaccard系数,若不匹配的话,则$x_{ij}^p=0$。总的目标损失函数(objective loss function)就由 localization loss(loc) 与 confidence loss(conf) 的加权求和:
代码不少,想快速SSD算法只须要详细了解下它的样本增广、正负样本获取方式、损失函数这三个方面就好,主要包括 include/caffe/ 目录下面的 annotated_data_layer.hpp 、 detection_evaluate_layer.hpp 、 detection_output_layer.hpp 、 multibox_loss_layer.hpp 、 prior_box_layer.hpp ,以及对应的 src/caffe/layers/ 目录下面的cpp和cu文件,另外还有 src/caffe/utlis/ 目录下面的 bbox_util.cpp 。从名字就能够看出来, annotated_data_layer 是提供数据的、 detection_evaluate_layer 是验证模型效果用的、 detection_output_layer 是输出检测结果用的、 multibox_loss_layer 是loss、 prior_box_layer 是计算prior bbox的。
这部分代码涉及到样本读取和data augment,同时把每张图里的groundtruth bbox读出来传给下一层。从 load_batch() 函数开始,主要包含四个部分,具体见上面一个图
① DistortImage();
② ExpandImage();
③ GenerateBatchSamples();
④ CropImage();
这一层完成的是给定一系列feature map后如何在上面生成prior box。从函数 Forward_cpu() 函数开始。SSD的作法颇有意思,对于输入大小是 W×H 的feature map,生成的prior box中心就是 W×H 个,均匀分布在整张图上,像下图中演示的同样。在每一个中心上,能够生成多个不一样长宽比的prior box,如[1/3, 1/2, 1, 2, 3]。因此在一个feature map上能够生成的prior box总数是 W×H×length_of_aspect_ratio ,对于比较大的feature map,如VGG的conv4_3,生成的prior box能够达到数千个。固然对于边界上的box,还要作一些处理保证其不超出图片范围,这都是细节了。
这里须要注意的是,虽然prior box的位置是在 W×H 的格子上,但prior box的大小并非跟格子同样大,而是人工指定的,原论文中随着feature map从底层到高层,prior box的大小在0.2到0.9之间均匀变化。
咱们已经在图上画出了prior box,同时也有了ground truth,那么下一步就是将prior box匹配到ground truth上,这是在
src/caffe/utlis/bbox_util.cpp
的FindMatches
函数里完成的。值得注意的是这里不光是给每一个groudtruth box找到了最匹配的prior box,而是给每一个prior box都找到了匹配的groundtruth box(若是有的话),这样显然大大增大了正样本的数量。
给每一个prior box找到匹配(包括物体和背景)以后,彷佛能够定义一个损失函数,给每一个prior box标记一个label,扔进去一通训练。
但须要注意的是,任意一张图里负样本必定是比正样本多得多的,这种严重不平衡的数据会严重影响模型的性能,因此对负样本要有所选择。
这里简单描述下:假设咱们上一步获得了N个负样本,接下来咱们将 loc_pred 损失进行排序,选择 N 个最大的 loc_pred 保留下来。而后只将索引存在于 loc_pred 里的 负样本留下来。
由于咱们对prior box是有选择的,因此数据的形状在这里已经被打乱了,没办法直接在后面链接一个loss(Caffe等框架须要每一层的输入是四维张量),
因此须要咱们把选出来的数据从新整理一下,这一步是在
src/caffe/utlis/bbox_util.cpp
的EncodeLocPrediction
和EncodeConfPrediction
两个函数里完成的。