前言
博主在上一篇中提到了兩種可能的改進方法。其中方法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網絡層,從conv1到fc7
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_flat層
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
}
}
# mbox_conf層,拼接6個conf_flat層
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
}
}
# mbox_priorbox層,拼接6個mbox_priorbox層
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
}
}
損失層
損失層類型是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的範例)。