SSD: Single Shot MultiBox Detector 模型fine-tune和網絡架構

前言

博主在上一篇中提到了兩種可能的改進方法。其中方法1,擴充類似數據集,詳見Udacity Self-Driving 目標檢測數據集簡介與使用 ,由於一些原因,並未對此數據集做過多探索,一次簡單訓練下,mAP爲64%左右,這還需要進一步探索。而方法2,說的是fine-tune已經訓練好的SSD model,最近沒來得及進行調參,初次實驗效果有限,先把過程和原理記錄下來,免得忘了,然後還會說下SSD的網絡架構。

SSD模型fine-tune

CNN fine-tune

model fine-tune,就是模型微調的意思。舉個例子說明一下:假如給你一個新的數據集做圖片分類,這個數據集是關於汽車的,不過樣本量很少,如果從零開始訓練CNN(Alexnet或者VGG),容易過擬合。那怎麼辦呢?於是想到了遷移學習,用別人訓練好的Imagenet模型來做,繼承現有模型學到的特徵“知識”,這樣再訓練的效果就會好一些。

SSD檢測任務也適用fine-tune策略。之前博主介紹的SSD經典訓練方法也算是一種fine-tune,爲什麼這麼說,因爲我們使用VGG_ILSVRC_16_layers_fc_reduced.caffemodel這個預訓練模型,這其實是作者在Imagenet上訓練的一個變種VGG,已經具備了一定的分類特徵提取能力,我們繼承這些“知識”,有利於模型的快速收斂。

其實,我們也可以直接去學習訓練好的SSD的模型“知識”。比如,作者在COCO數據集上訓練了一個SSD檢測模型,這個模型對90類物體有較好的檢測能力,現在我們只對其中的汽車和行人感興趣,就只想檢測這兩類(COCO數據集當然也包含這兩類),手頭還有KITTI數據集等,現在就可以在這個模型的基礎上去fine-tune,copy大部分有用的檢測“知識”,並且專心學習兩類物體特有的“知識”。

這裏以COCO爲例,介紹一下如何使用新數據集去fine-tune一個SSD COCO model,默認是SSD300x300。首先是下載07+12+COCO ,這個是作者用VOC數據集去微調COCO模型的範例,我們以此爲藍本進行修改,主要修改點是數據集以及預訓練的模型。

下載壓縮包,找到finetune_ssd_pascal.py文件,打開後可以看到和之前的ssd_pascal.py區別不是很大,重要的是這麼一句:

pretrain_model = "models/VGGNet/VOC0712/SSD_300x300_coco/VGG_coco_SSD_300x300.caffemodel"

這就是預訓練的模型,可以下載的,在作者主頁找到COCO models: trainval35k,下載解壓可得到VGG_coco_SSD_300x300_iter_400000.caffemodel ,再改個名字放到對應路徑就行。接下來給複製一份腳本,命名爲finetune_ssd_kitti.py ,然後修改成各種KITTI相關的名稱和路徑,類別數量也要改(可參考之前博文修改)。

下面運行命令開始訓練看看:

$ cd caffe
$ python examples/ssd/finetune_ssd_kitti.py

發現有大bug,錯誤描述如下:

Cannot copy param 0 weights from layer 'conv4_3_norm_mbox_conf'; shape mismatch.  Source param shape is 324 512 3 3 (1492992); target param shape is 16 512 3 3 (73728). To learn this layer's parameters from scratch rather than copying from a saved net, rename the layer.
*** Check failure stack trace: ***
    @     0x7f0bddd2f5cd  google::LogMessage::Fail()
    @     0x7f0bddd31433  google::LogMessage::SendToLog()
    @     0x7f0bddd2f15b  google::LogMessage::Flush()
    @     0x7f0bddd31e1e  google::LogMessageFatal::~LogMessageFatal()
    @     0x7f0bde5c34cb  caffe::Net<>::CopyTrainedLayersFrom()
    @     0x7f0bde5ca225  caffe::Net<>::CopyTrainedLayersFromBinaryProto()
    @     0x7f0bde5ca2be  caffe::Net<>::CopyTrainedLayersFrom()
    @           0x40a849  CopyLayers()
    @           0x40bca4  train()
    @           0x4077c8  main
    @     0x7f0bdc4c6830  __libc_start_main
    @           0x408099  _start
    @              (nil)  (unknown)
Aborted (core dumped)

這個問題困擾了幾天,後來才知道,COCO模型有81類,而本次訓練的文件只有4類,conf層沒有辦法共享權值。解決方法就是把conf層改個名字,跳過這些層的fine-tune,意味着這部分層的參數需要從零學起。至於loc層則可以不改名,因爲位置座標和類別無關,所有類別均可共享。那麼,注意到該腳本下有兩個一模一樣的CreateMultiBoxHead函數:

mbox_layers = CreateMultiBoxHead(net, data_layer='data', from_layers=mbox_source_layers,
        use_batchnorm=use_batchnorm, min_sizes=min_sizes, max_sizes=max_sizes,
        aspect_ratios=aspect_ratios, steps=steps, normalizations=normalizations,
        num_classes=num_classes, share_location=share_location, flip=flip, clip=clip,
        prior_variance=prior_variance, kernel_size=3, pad=1, lr_mult=lr_mult)

現在,僅需要在這兩個函數中添加參數conf_postfix='_kitti'就可以把conf層的名字修改了,其實就是在所有’conf’字符後面添加了’kitti’字樣,新函數就變成了:

mbox_layers = CreateMultiBoxHead(net, data_layer='data', from_layers=mbox_source_layers,
        use_batchnorm=use_batchnorm, min_sizes=min_sizes, max_sizes=max_sizes,
        aspect_ratios=aspect_ratios, steps=steps, normalizations=normalizations,
        num_classes=num_classes, share_location=share_location, flip=flip, clip=clip,
        prior_variance=prior_variance, kernel_size=3, pad=1, lr_mult=lr_mult, conf_postfix='_kitti')

然後再運行命令,就可以正常開始訓練了。博主所用腳本可以參考一下:finetune_ssd_kitti.py

一開始使用默認solver訓練了一次,120000迭代,mAP勉強達到63%,感覺還是不夠看的。後來覺着默認solver中的學習率策略可能不太對,如果使用大的學習率(比如0.001)訓練太久的話,原有caffemodel的基礎權重可能會被破壞的比較厲害,那效果就會打折扣,因此有大神建議博主可以每隔10000次迭代降低學習率爲一半,就是暫時沒空嘗試,如果有效果再更新吧。

SSD網絡架構

VGGNet-SSD網絡結構圖

如果只是訓練想VGGNet的SSD,並不用太關心SSD的網絡結構,畢竟有python腳本可用。可是如果要想進一步修改網絡,比如把基礎網絡換成Mobilenet,Resnet等,或者是修改多層融合檢測層,那就需要簡單瞭解SSD的基本結構。

SSD論文給的圖是這樣的,大致知道了結構,細節還是看不懂。

這裏寫圖片描述

當然,有人會把prototxt放進這個Netscope 中,得到可視化的結構,這是最直接的方法了,爲了方便表示,訓練網絡train.prototxt的架構用如下簡圖表示(新標籤頁中打開可看到大圖)。

這裏寫圖片描述

說明一下,爲了簡便,沒有完整反映出6個bottom輸入層;圖中的“*”號表示省略寫法,比如第一行的 *_perm展開後就表示conv4_3_norm_mbox_loc_perm,其他以此類推;然後mbox_priorbox層均有有第二個bottom輸入,即data層,不過這裏沒有畫出來。

下面以VGGNet-SSD爲例,結合prototxt源文件說一下網絡結構。

數據層

這裏的數據層類型是AnnotatedData,是作者自己寫的。裏面有很多複雜的設置,並不能完全看懂,所以也不好隨意修改,主要注意下lmdb的source,batch_size以及label_map_file即可。

# 數據層
name: "VGG_VOC0712_SSD_300x300_train"
layer {
  name: "data"
  type: "AnnotatedData"
  top: "data"
  top: "label"
  include {
    phase: TRAIN
  }
  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
    }
  }
  data_param {
    source: "examples/VOC0712/VOC0712_trainval_lmdb"
    batch_size: 32
    backend: LMDB
  }
  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: "data/VOC0712/labelmap_voc.prototxt"
  }
}

特徵提取網絡

特徵提取網絡由兩部分構成,VGGNet和新增的8個卷積層,要點有三處。

第一,這個VGGNet不同於原版,是修改過的,全名叫VGG_ILSVRC_16_layers_fc_reduced是作者另一篇論文ParseNet: Looking Wider to See Better的成果,然後被用到了SSD中。

第二,爲什麼要新增這麼8個卷積層?因爲在300x300輸入下,conv4_3的分辨率爲38x38,而到fc7層,也還有19x19,那就必須新增一些卷積層,以產生更多低分辨率的層,方便進行多層特徵融合。最終作者選擇了6個卷積層作爲bottom,分辨率如下圖所示,然後依次接上若干檢測層。

卷積層 分辨率
conv4_3_norm 38x38
f7 19x19
conv6_2 10x10
conv7_2 5x5
conv8_2 3x3
conv9_2 1x1

第三,只有conv4_3加入了norm,這是爲什麼呢?原因仍然在論文中ParseNet中,大概是說,因爲conv4_3的scale(尺度?)和其它層相比不同,纔要加入norm使其均衡一些。

這裏寫圖片描述

# 特徵提取網絡
layer {
  name: "conv1_1"
  type: "Convolution"
  bottom: "data"
  top: "conv1_1"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 64
    pad: 1
    kernel_size: 3
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
      value: 0
    }
  }
}
#
# 省略很多層
#
layer {
  name: "fc7"
  type: "Convolution"
  bottom: "fc6"
  top: "fc7"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 1024
    kernel_size: 1
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
      value: 0
    }
  }
}
layer {
  name: "relu7"
  type: "ReLU"
  bottom: "fc7"
  top: "fc7"
}

# 到此是VGG網絡層,從conv1fc7

layer {
  name: "conv6_1"
  type: "Convolution"
  bottom: "fc7"
  top: "conv6_1"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 256
    pad: 0
    kernel_size: 1
    stride: 1
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
      value: 0
    }
  }
}
#
# 省略很多層
#
layer {
  name: "conv9_2_relu"
  type: "ReLU"
  bottom: "conv9_2"
  top: "conv9_2"

# 到此是新增的8層卷積層,從conv6_1到conv9_2

多層融合檢測網絡

這一部分,作者選擇了6個層作爲bottom,在其上連接了一系列的檢測相關層,這裏以f7層爲例,看看它作爲bottom,連接了那些層。首先是loc相關層,包括fc7_mbox_loc,fc7_mbox_loc_perm和fc7_mbox_loc_flat。

# mbox_loc層,預測box的座標,其中24=6(default box數量)x4(四個座標)
layer {
  name: "fc7_mbox_loc"
  type: "Convolution"
  bottom: "fc7"
  top: "fc7_mbox_loc"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 24
    pad: 1
    kernel_size: 3
    stride: 1
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
      value: 0
    }
  }
}
# mbox_loc_perm層,將上一層產生的mbox_loc重新排序  
layer {
  name: "fc7_mbox_loc_perm"
  type: "Permute"
  bottom: "fc7_mbox_loc"
  top: "fc7_mbox_loc_perm"
  permute_param {
    order: 0
    order: 2
    order: 3
    order: 1
  }
}
# mbox_loc_flat層,將perm層展平(例如將7x7展成1x49),方便拼接
layer {
  name: "fc7_mbox_loc_flat"
  type: "Flatten"
  bottom: "fc7_mbox_loc_perm"
  top: "fc7_mbox_loc_flat"
  flatten_param {
    axis: 1
  }
}

然後是conf相關層,包括fc7_mbox_conf,fc7_mbox_conf_perm和fc7_mbox_conf_flat。

# mbox_conf層,預測box的類別置信度,126=6(default box數量)x21(總類別數)
layer {
  name: "fc7_mbox_conf"
  type: "Convolution"
  bottom: "fc7"
  top: "fc7_mbox_conf"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 126
    pad: 1
    kernel_size: 3
    stride: 1
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
      value: 0
    }
  }
}
# box_conf_perm層,將上一層產生的mbox_conf重新排序 
layer {
  name: "fc7_mbox_conf_perm"
  type: "Permute"
  bottom: "fc7_mbox_conf"
  top: "fc7_mbox_conf_perm"
  permute_param {
    order: 0
    order: 2
    order: 3
    order: 1
  }
}
# mbox_conf_flat層,將perm層展平,方便拼接
layer {
  name: "fc7_mbox_conf_flat"
  type: "Flatten"
  bottom: "fc7_mbox_conf_perm"
  top: "fc7_mbox_conf_flat"
  flatten_param {
    axis: 1
  }
}

最後是priorbox層,就只有fc7_mbox_priorbox

# mbox_priorbox層,根據標註信息,經過轉換,在cell中產生box真實值
layer {
  name: "fc7_mbox_priorbox"
  type: "PriorBox"
  bottom: "fc7"
  bottom: "data"
  top: "fc7_mbox_priorbox"
  prior_box_param {
    min_size: 60.0
    max_size: 111.0 # priorbox的上下界爲60~111
    aspect_ratio: 2
    aspect_ratio: 3 # 共同定義default box數量爲6
    flip: true
    clip: false
    variance: 0.1
    variance: 0.1
    variance: 0.2
    variance: 0.2
    step: 16 # 不同層的值不同
    offset: 0.5
  }
}

下面做一個表格統計一下多層融合檢測層的相關重要參數:

bottom層 conv4_3 fc7 conv6_2 conv7_2 conv8_2 conv9_2
分辨率 38x38 19x19 10x10 5x5 3x3 1x1
default box數量 4 6 6 6 4 4
loc層num_output 16 24 24 24 16 16
conf層num_output 84 126 126 126 84 84
priorbox層size 30~60 60~111 111~162 162~213 213~264 264~315
priorbox層step 8 16 32 64 100 300

Concat層

這裏需要添加3個concat層,分別拼接所有的loc層,conf層以及priorbox層。

# mbox_loc層,拼接6個loc_flatlayer {
  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
  }
}
# mbox_conf層,拼接6個conf_flatlayer {
  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
  }
}
# mbox_priorbox層,拼接6個mbox_priorboxlayer {
  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
  }
}

損失層

損失層類型是MultiBoxLoss,這也是作者自己寫的,在smooth_L1損失層基礎上修改。損失層的bottom是三個concat層以及data層中的label,參數一般不需要改,注意其中的num_classes即可。

# 損失層
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
  }
}

至此,SSD的訓練網絡train.prototxt基本介紹完畢,那測試網絡test.prototxt和部署網絡deploy.prototxt就很容易理解了,只需要保持特徵檢測網絡和多層融合檢測網絡不變,copy修改其他部分就好了。

瞭解SSD網絡架構,可以方便我們對網絡進行修改,比如說訓練一個最近熱門的Mobilenet-SSD就是蠻好的的應用(自己構造網絡儘量遵照VGGNet-SSD的範例)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章