深度學習筆記(七)SSD 論文閱讀筆記


源碼地址:https://github.com/weiliu89/caffe/tree/ssd

YOLO V3 學習筆記

一. 算法概述

本文提出的 SSD 算法是一種直接預測目標類別和 bounding box 的多目標檢測算法。與 faster rcnn 相比,該算法沒有生成 proposal 的過程,這就極大提高了檢測速度。針對不同大小的目標檢測,傳統的做法是先將圖像轉換成不同大小(圖像金字塔),然后分別檢測,最后將結果綜合起來(NMS)。而SSD算法則利用不同卷積層的 feature map 進行綜合也能達到同樣的效果。文章的核心之一是同時采用 lower 和 upper 尺度的 feature map 做檢測。

                                                                   Fig.1 SSD 框架

算法的主網絡結構是 VGG16,將最后兩個全連接層改成卷積層,並隨后增加了 4 個卷積層來構造網絡結構。對其中 6 個不同大小的 feature map 輸出分別用兩個不同的 3×3 的卷積核進行卷積,一個輸出分類用的 confidence,每個 default box 生成 21( VOC 20 classes+1 background)個類別 confidence;一個輸出回歸用的 localization,每個 default box 生成4個坐標值(x, y, w, h)。此外,這 6 個 feature map 還經過 PriorBox 層生成 prior box(相當於 faster rcnn 的 Anchor)。上述 6 個 feature map 中每一層的 default box 的數量是給定的(8732個)。最后將前面三個計算結果分別合並然后傳給loss層。

二. Default box

文章的核心之一是作者同時采用 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個。那么可能分別有10、20個 prior box 能分別與這2個 ground truth box 匹配上,匹配成功的將會作為正樣本參與分類和回歸訓練,而未能匹配的則只會參加分類(負樣本)訓練。訓練的損失包含分類損失和回歸損失兩部分。

 

Fig.2 default boxes

作者的實驗表明 default box 的 shape 數量越多,效果越好 (當然耗時也越大)。

這里用到的 default box 和 Faster RCNN中的 Anchor 很像,在Faster RCNN中 anchor 只用在最后一個卷積層,但是在本文中,default box 是應用在多個不同層的 feature map 上,這一點倒是很像 FPN 了。

那么default box的 scale(大小,歸一化於輸入尺度)和 aspect ratio(橫縱比)要怎么定呢?假設我們用 m 個 feature maps 做預測,那么對於每個 featuer map 而言其 default box 的 scale 是按以下公式計算的: 

 $\vee$

$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 在不同的 feature 層 有不同的 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;
        }
      }
    }
  }
View Code

具體到每一個 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

1.正樣本獲得

我們已經在圖上畫出了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 \ge 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);
  }
}
View Code
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;
}
View Code
  • 將每一個prior box 與 每一個 groundtruth box 進行匹配,獲得待處理匹配 map<int, map<int, float> > overlaps(小於8732),JaccardOverlap > 0 的 prior box 才保留,其他舍去。一個 ground truth box 可能和多個 prior box 能匹配上
  • 從待處理匹配中為 ground truth box 找到最匹配的一對放入候選正樣本集  vector<int>* match_indices, vector<float>* match_overlaps
  • 剩下的每個待處理匹配中一個 ground truth box 可能匹配多個 prior box,因此我們為剩下的每個 prior box 尋找滿足與 groundtruth box 的 JaccardOverlap > 0.5 的一個最大匹配放入候選正樣本集  vector<int>* match_indices, vector<float>* match_overlaps

這就使得一個ground truth box中我們可能獲得多個候選正樣本。

2.負樣本獲得

在生成一系列的 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
};

3.回歸

以prior box為基准,SSD里的回歸目標不是簡單的中心點偏差以及寬、高縮放。因為涉及到一個編碼的過程,這里簡單說一下默認的 CENTER_SIZE 編解碼過程:

解碼一般用在將預測輸出映射到實際檢測框坐標上:

輸入: 預定義prior box =[$d^x, d^y, d^w, d^h$]

            預測輸出 predict box = [$\hat{p}^x, \hat{p}^y, \hat{p}^w, \hat{p}^h$]

            編碼系數 prior_variance = [0.1, 0.1, 0.2, 0.2]

輸出   decode_bbox

\begin{equation}
\label{decode}
\begin{split}
& p^x = 0.1 * \hat{p}^x * d^w + d^x \\
& p^y = 0.1 * \hat{p}^y * d^h + d^y \\
& p^w = e^{(0.2*\hat{p}^w)} * d^w \\
& p^h = e^{(0.2*\hat{p}^h)} * d^h \\
\end{split}
\end{equation}

編碼則一般用以歸一化 gt 來計算回歸 loss:

輸入: 預定義prior box =[$d^x, d^y, d^w, d^h$]

            原始 groundtruth box = [$g^x, g^y, g^w, g^h$]

            編碼系數 prior_variance = [0.1, 0.1, 0.2, 0.2]

輸出   encode_bbox

\begin{equation}
\label{encode}
\begin{split}
& \hat{g}^x = \frac{g^x - d^x}{d^w * 0.1} \\
& \hat{g}^y = \frac{g^y - d^y}{d^h * 0.1} \\
& \hat{g}^w = \frac{log(\frac{g^w}{d^w})}{0.2} \\
& \hat{g}^h = \frac{log(\frac{g^h}{d^h})}{0.2} \\
\end{split}
\end{equation}

 

4.Data argument

本文同時對訓練數據做了 data augmentation,數據增廣。

每一張訓練圖像,隨機的進行如下幾種選擇:

  • 使用原始的圖像
  • 隨機采樣多個 patch(CropImage),與物體之間最小的 jaccard overlap 為:0.10.30.50.7 與 0.9

采樣的 patch 是原始圖像大小比例是 [0.31.0],aspect ratio 在 0.5 或 2

當 groundtruth box 的 中心(center)在采樣的 patch 中在采樣的 patch中 groundtruth box面積大於0時,我們保留 CropImage。

在這些采樣步驟之后,每一個采樣的 patch 被 resize 到固定的大小,並且以 0.5 的概率隨機的 水平翻轉(horizontally flipped,翻轉不翻轉看prototxt,默認不翻轉)

這樣一個樣本被諸多batch_sampler采樣器采樣后會生成多個候選樣本,然后從中隨機選一個樣本送人網絡訓練。

一個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.];
}

對於選擇的sample_box的限制條件

// 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

對於一個batch進行采樣的參數設置

// 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];
}

轉存datalayer數據的參數

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;
}

SSD中的數據轉換和采樣參數設置

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"
  }
View Code

 

                                       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。

計算完后,我們來看看用來檢測的個 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 一樣。

 

訓練過程中的 prior boxes 和 ground truth boxes 的匹配,基本思路是:讓每一個 prior box 回歸並且到 ground truth box,這個過程的調控我們需要損失層的幫助,他會計算真實值和預測值之間的誤差,從而指導學習的走向。

SSD 訓練的目標函數(training objective)源自於 MultiBox 的目標函數,但是本文將其拓展,使其可以處理多個目標類別。具體過程是我們會讓每一個 prior box 經過Jaccard 系數計算和真實框的相似度,閾值只有大於 0.5 的才可以列為候選名單;假設選擇出來的是N個匹配度高於百分之五十的框吧,我們令 表示第 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) 的加權求和:

  • N 是與 ground truth box 相匹配的 prior boxes 個數
  • localization loss(loc) 是 Fast R-CNN 中 Smooth L1 Loss,用在 predict box(l 與 ground truth box(g 參數(即中心坐標位置,width、height)中,回歸 bounding boxes 的中心位置,以及 width、height
  • confidence loss(conf) 是 Softmax Loss,輸入為每一類的置信度 c
  • 權重項 α,可在protxt中設置 loc_weight,默認設置為 1

Softmax Loss

\begin{equation}
\label{SoftmaxLoss}
\begin{split}
& L_{softmax} = - \sum_{j=1}^{T} y_j \log{s_j} \\
& s_j = \frac{e_{p^j}}{\sum e_{p^i}} \\
\end{split}
\end{equation}

這里來個簡單示例,假定某個 anchor 的分類輸出為 p = [1.0, 2.0, 3.0],經過 softmax 函數后變成了 s = [0.09, 0.24, 0.67], 而 GT 類別是 2,即 y = [0, 0, 1],如此的話 loss = -log(0.67)

Smooth L1 loss

參考:L1, L2以及smooth L1 loss

\begin{equation}
\label{L1}
L_1(x) = \lvert{x}\rvert ; \ \frac{d{L_1(x)}}{d{x}} = \begin{cases}
\ 1 & if\ x \ge 0 \\
\ -1 & \ otherwise
\end{cases}
\end{equation}

\begin{equation}
\label{L2}
L_1(x) = x^2; \ \frac{d{L_2(x)}}{d{x}} = 2x
\end{equation}

\begin{equation}
\label{Smooth_L1}
smooth_{L_1}(x) = \begin{cases}
\ 0.5x^2 & if \  \lvert{x}\rvert < 1 \\
\ \lvert{x}\rvert - 0.5 & \ otherwise
\end{cases}
; \ \frac{d{smooth_{L_1}}}{d{x}} = \begin{cases}
\ x & if \  \lvert{x}\rvert < 1 \\
\ \pm{1} & \ otherwise
\end{cases}
\end{equation}

對於 $L_2$ loss, 當 $x$ 增大時,$L_2$ loss 對 $x$ 的導數也增大。這就導致訓練初期,預測值與 groud truth 差異過於大時,損失函數對預測值的梯度十分大,訓練不穩定。

而對於 $L_1$ loss, $L_1$ 對 $x$ 的導數為常數。這就這就導致訓練后期,預測值與 ground truth 差異很小時,$L_1$ loss 對預測值的導數的絕對值仍然為 1,而 learning rate 如果不變,損失函數將在穩定值附近波動,難以繼續收斂以達到更高精度。

最后 $smooth_{L_1}$ loss 在 $x$ 較小時,對 $x$ 的梯度也會變小,而在 $x$ 很大時,對 $x$ 的梯度的絕對值達到上限 1,也不會太大以至於破壞網絡參數。 $smooth_{L_1}$ loss 完美地避開了 $L_1$ 和 $L_2$ 損失的缺陷.

六. 代碼 

代碼很多,想快速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的。

annotated_data_layer.cpp  sampler.cpp  data_transformer.cpp

這部分代碼涉及到樣本讀取和data augment,同時把每張圖里的groundtruth bbox讀出來傳給下一層。從 load_batch() 函數開始,主要包含四個部分,具體見上面一個圖

DistortImage();
ExpandImage();
GenerateBatchSamples();
CropImage();

prior_box_layer.cpp

這一層完成的是給定一系列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之間均勻變化。

multibox_loss_layer.cpp

FindMatches()

我們已經在圖上畫出了prior box,同時也有了ground truth,那么下一步就是將prior box匹配到ground truth上,這是在 src/caffe/utlis/bbox_util.cpp 的 FindMatches 函數里完成的。

值得注意的是這里不光是給每個groudtruth box找到了最匹配的prior box,而是給每個prior box都找到了匹配的groundtruth box(如果有的話),這樣顯然大大增大了正樣本的數量。

MineHardExamples()

給每個prior box找到匹配(包括物體和背景)之后,似乎可以定義一個損失函數,給每個prior box標記一個label,扔進去一通訓練。

但需要注意的是,任意一張圖里負樣本一定是比正樣本多得多的,這種嚴重不平衡的數據會嚴重影響模型的性能,所以對負樣本要有所選擇。

這里簡單描述下:假設我們上一步得到了N個負樣本,接下來我們將 loc_pred 損失進行排序,選擇 N 個最大的 loc_pred 保留下來。然后只將索引存在於 loc_pred 里的 負樣本留下來。

EncodeLocPrediction() && EncodeConfPrediction ()

因為我們對prior box是有選擇的,所以數據的形狀在這里已經被打亂了,沒辦法直接在后面連接一個loss(Caffe等框架需要每一層的輸入是四維張量),

所以需要我們把選出來的數據重新整理一下,這一步是在 src/caffe/utlis/bbox_util.cpp 的 EncodeLocPrediction 和 EncodeConfPrediction 兩個函數里完成的。 

七.使用注意

1. caffe 這個版本里,使用 batch_sampler 做 data argument 時要注意是否 crop 的樣本只包含目標很小一部分,同時可以關注其他的 data argument 論文。

2. 檢查對於你的樣本來說回歸和分類問題哪個更難,以此調整 multibox_loss_param 中 loc_weight 進行訓練。

3. 正負樣本比例,默認HARD_EXAMPLE方式只取64個最高 predictions loss 來從中尋找負樣本,檢查你的樣本集中正負樣本比例是否合適。

4. 嘗試使用 Focal Loss 替換 Softmax Loss with hard example。

5. 根據需求,替換不同的 backbone

6. 嘗試使用 GIoU、DIoU、CIoU Loss 替換 Smooth L1 Loss。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM