1. 程式人生 > >SSD原始碼解讀之ssd_pascal.py

SSD原始碼解讀之ssd_pascal.py

#########全部解讀完畢,歡迎關注############


from __future__ import print_function
import caffe
from caffe.model_libs import *
from google.protobuf import text_format


import math
import os
import shutil
import stat
import subprocess
import sys


# 給基準網路後面增加額外的卷積層(為了避免此處的卷積層的名稱和基準網路卷積層的名稱重複,這裡可以用基準網路最後一個層的名稱進行開始命名),這一部分的具體實現方法可以對照檔案~/caffe/python/caffe/model_libs.py檢視,SSD的實現基本上就是ssd_pascal.py和model_libs.py兩個檔案在控制,剩下的則是caffe底層程式碼中編寫各個功能模組。
def AddExtraLayers(net, use_batchnorm=True, lr_mult=1):
    use_relu = True


    # Add additional convolutional layers.
    # 19 x 19
    ######################################生成附加網路的第一個卷積層,卷積核的數量為256,卷積核的大小為1*1,pad的尺寸為0,stride為1.
    from_layer = net.keys()[-1] #獲得基準網路的最後一層,作為conv6-1層的輸入


    # TODO(weiliu89): Construct the name using the last layer to avoid duplication.
    # 10 x 10
    out_layer = "conv6_1"
    ConvBNLayer(net, from_layer, out_layer, use_batchnorm, use_relu, 256, 1, 0, 1,
        lr_mult=lr_mult)
    ########################################conv6_1生成完畢
    ######################################生成附加網路的第一個卷積層,卷積核的數量為512,卷積核的大小為3*3,pad的尺寸為1,stride為2.
    from_layer = out_layer
    out_layer = "conv6_2"
    ConvBNLayer(net, from_layer, out_layer, use_batchnorm, use_relu, 512, 3, 1, 2,
        lr_mult=lr_mult)
    #########################################conv6_2生成完畢
    # 5 x 5
    from_layer = out_layer
    out_layer = "conv7_1"
    ConvBNLayer(net, from_layer, out_layer, use_batchnorm, use_relu, 128, 1, 0, 1,
      lr_mult=lr_mult)
    #########################################conv7_1生成完畢
    from_layer = out_layer
    out_layer = "conv7_2"
    ConvBNLayer(net, from_layer, out_layer, use_batchnorm, use_relu, 256, 3, 1, 2,
      lr_mult=lr_mult)
    #########################################conv7_2生成完畢
    # 3 x 3
    from_layer = out_layer
    out_layer = "conv8_1"
    ConvBNLayer(net, from_layer, out_layer, use_batchnorm, use_relu, 128, 1, 0, 1,
      lr_mult=lr_mult)
    #########################################conv8_1生成完畢
    from_layer = out_layer
    out_layer = "conv8_2"
    ConvBNLayer(net, from_layer, out_layer, use_batchnorm, use_relu, 256, 3, 0, 1,
      lr_mult=lr_mult)
    #########################################conv8_2生成完畢
    # 1 x 1
    from_layer = out_layer
    out_layer = "conv9_1"
    ConvBNLayer(net, from_layer, out_layer, use_batchnorm, use_relu, 128, 1, 0, 1,
      lr_mult=lr_mult)
    #########################################conv9_1生成完畢
    from_layer = out_layer
    out_layer = "conv9_2"
    ConvBNLayer(net, from_layer, out_layer, use_batchnorm, use_relu, 256, 3, 0, 1,
      lr_mult=lr_mult)
    #########################################conv9_2生成完畢
    return net




### 相應地修改一下引數 ###
# 包含caffe程式碼的路徑
# 我們假設你是在caffe跟目錄下執行程式碼
caffe_root = os.getcwd() #獲取caffe的根目錄


# 如果你想在生成所有訓練檔案之後就開始訓練,這裡run_soon給予引數Ture.
run_soon = True    
#如果你想接著上次的訓練,繼續進行訓練,這裡的引數為Ture,(這個就是說可能你訓練一般停止了,重新啟動的時候,這裡的Ture保證繼續接著你上次的訓練進行訓練)
#否則為False,表示我們將從下面定義的預訓練模型處進行載入。(這個表示就是不管你上次訓練一半的模型了,我們直接從預訓練好的基準模型哪裡開始訓練)
resume_training = True
# 如果是Ture的話,表示我們要移除舊的模型訓練檔案,否則是不移除的。

remove_old_models = False

# 訓練資料的資料庫檔案. Created by data/VOC0712/create_data.sh
train_data = "examples/VOC0712/VOC0712_trainval_lmdb"
# 測試資料的資料庫檔案. Created by data/VOC0712/create_data.sh
test_data = "examples/VOC0712/VOC0712_test_lmdb"
# 指定批量取樣器。
resize_width = 300
resize_height = 300
resize = "{}x{}".format(resize_width, resize_height)
batch_sampler = [
        {
                'sampler': {
                        },
                'max_trials': 1,
                'max_sample': 1,
        },
        {
                '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_trials': 50,
                'max_sample': 1,
        },
        {
                '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_trials': 50,
                'max_sample': 1,
        },
        {
                '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_trials': 50,
                'max_sample': 1,
        },
        {
                '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_trials': 50,
                'max_sample': 1,
        },
        {
                '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_trials': 50,
                'max_sample': 1,
        },
        {
                '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_trials': 50,
                'max_sample': 1,
        },

        ]

#以上這一部分就是文中所說的資料增強部分,抱歉的是這一部分我也沒太看懂。具體可檢視~/caffe/src/caffe/util/sampler.cpp檔案中的詳細定義。

#以下是轉換引數設定,具體意思可在caffe底層程式碼中檢視引數的定義。路徑為~/caffe/src/caffe/proto/caffe.proto

train_transform_param = {
        'mirror': True,
        'mean_value': [104, 117, 123],############均值
        'resize_param': {  #################儲存資料轉換器用於調整大小策略的引數的訊息。
                'prob': 1,  ###############使用這個調整策略的可能性
                'resize_mode': P.Resize.WARP, ########重定義大小的模式,caffe.proto中定義的是列舉型別
                'height': resize_height,
                'width': resize_width,
                'interp_mode': [ ###########插值模式用於調整大小,定義為列舉型別
                        P.Resize.LINEAR,
                        P.Resize.AREA,
                        P.Resize.NEAREST,
                        P.Resize.CUBIC,
                        P.Resize.LANCZOS4,
                        ],
                },
        'distort_param': {##########################儲存資料轉換器用於失真策略的引數的訊息
                'brightness_prob': 0.5,  ###########調整亮度的概率,預設為1。
                'brightness_delta': 32,  ###########要新增到[-delta,delta]內的畫素值的數量。可能的值在[0,255]之內。 推薦32。
                'contrast_prob': 0.5, #######調整對比度的概率。
                'contrast_lower': 0.5, #######隨機對比因子的下界。 推薦0.5。
                'contrast_upper': 1.5, #######隨機對比因子的上界。 推薦1.5。
                'hue_prob': 0.5, ##########調整色調的概率。
                'hue_delta': 18, ##########新增到[-delta,delta]內的色調通道的數量。 可能的值在[0,180]之內。 推薦36。
                'saturation_prob': 0.5, ########調整飽和的概率。
                'saturation_lower': 0.5, ########隨機飽和因子的下界。 推薦0.5。
                'saturation_upper': 1.5, ########隨機飽和因子的上界。 推薦1.5。
                'random_order_prob': 0.0, ########隨機排列影象通道的概率。
                },
        'expand_param': {   ##################儲存資料轉換器用於擴充套件策略的引數的訊息
                'prob': 0.5,   ###############使用這個擴充套件策略的可能性
                'max_expand_ratio': 4.0,   ######擴大影象的比例。
                },
        'emit_constraint': {    ########給定註釋的條件。
            'emit_type': caffe_pb2.EmitConstraint.CENTER,    ##############型別定義為列舉,此處選定為CENTER
            }
        }
test_transform_param = {    ###############測試轉換引數,類似於訓練轉換引數。
        'mean_value': [104, 117, 123],
        'resize_param': {
                'prob': 1,
                'resize_mode': P.Resize.WARP,
                'height': resize_height,
                'width': resize_width,
                'interp_mode': [P.Resize.LINEAR],
                },

        }

# 如果為true,則對所有新新增的圖層使用批量標準。
# 目前只有非批量規範版本已經過測試。
use_batchnorm = False   ###############是否使用批量標準
lr_mult = 1    #############基礎學習率設定為1,用於下面的計算以改變初始學習率。
# 使用不同的初始學習率。
if use_batchnorm:
    base_lr = 0.0004
else:
    # 當batch_size = 1, num_gpus = 1時的學習率。
    base_lr = 0.00004   ############由於上面use_batchnorm = false,所以我們一般調整初始學習率時只需更改這一部分,目前為0.001。

#儲存模型.prototxt檔案的目錄。
save_dir = "models/VGGNet/VOC0712/{}".format(job_name)
# 儲存模型快照的目錄。
snapshot_dir = "models/VGGNet/VOC0712/{}".format(job_name)
# 儲存作業指令碼和日誌檔案的目錄。
job_dir = "jobs/VGGNet/VOC0712/{}".format(job_name)
# 儲存檢測結果的目錄。
output_result_dir = "{}/data/VOCdevkit/results/VOC2007/{}/Main".format(os.environ['HOME'], job_name)

# 模型定義檔案。
train_net_file = "{}/train.prototxt".format(save_dir)
test_net_file = "{}/test.prototxt".format(save_dir)
deploy_net_file = "{}/deploy.prototxt".format(save_dir)
solver_file = "{}/solver.prototxt".format(save_dir)
# 快照字首。
snapshot_prefix = "{}/{}".format(snapshot_dir, model_name)
# 作業指令碼路徑。
job_file = "{}/{}.sh".format(job_dir, model_name)

# 儲存測試影象的名稱和大小。 Created by data/VOC0712/create_list.sh
name_size_file = "data/VOC0712/test_name_size.txt"
# 預訓練模型。 我們使用完卷積截斷的VGGNet。
pretrain_model = "models/VGGNet/VGG_ILSVRC_16_layers_fc_reduced.caffemodel"
# 儲存LabelMapItem。
label_map_file = "data/VOC0712/labelmap_voc.prototxt"

# 多框損失層MultiBoxLoss的引數。在~/caffe/src/caffe/proto/caffe.proto可查詢具體定義
num_classes = 21  ##########要預測的類的數量。 需要!
share_location = True   #########位置共享,如果為true,邊框在不同的類中共享。
background_label_id=0   ########是否使用先驗匹配,一般為true。
train_on_diff_gt = True    ########是否考慮困難的ground truth,預設為true。
normalization_mode = P.Loss.VALID    ######如何規範跨越批次,空間維度或其他維度聚集的損失層的損失。 目前只在SoftmaxWithLoss和SigmoidCrossEntropyLoss圖層中實現。按照批次中的示例數量乘以空間維度。 在計算歸一化因子時,不會忽略接收忽略標籤的輸出。定義為列舉,四種類型分別是:FULL,除以不帶ignore_label的輸出位置總數。 如果未設定ignore_label,則表現為FULL;VALID;BATCH_SIZE,除以批量大小;NONE,不要規範化損失。
code_type = P.PriorBox.CENTER_SIZE     #########bbox的編碼方式。此引數定義在PriorBoxParameter引數定義解釋中,為列舉型別,三種類型為:CORNER,CENTER_SIZE和CORNER_SIZE。
ignore_cross_boundary_bbox = False    ########如果為true,則在匹配期間忽略跨邊界bbox。 跨界bbox是一個在影象區域之外的bbox。即將超出影象的預測邊框剔除,這裡我們不踢除,否則特徵圖邊界點產生的先驗框就沒有任何意義。
mining_type = P.MultiBoxLoss.MAX_NEGATIVE   訓練期間的挖掘型別。定義為列舉,分別為三種類型: 若為NONE則表示什麼都不使用,這樣會導致正負樣本的嚴重不均衡;若為MAX_NEGATIVE則根據分數選擇底片;若為HARD_EXAMPLE則選擇基於“線上硬示例挖掘的基於訓練區域的物件探測器”的硬例項,此型別即為SSD原文中所使用的Hard_negative_mining(負硬挖掘)策略。
neg_pos_ratio = 3.  #####負/正比率,即文中所說的1:3。
loc_weight = (neg_pos_ratio + 1.) / 4.    #########位置損失的權重,
multibox_loss_param = {        ############儲存MultiBoxLossLayer使用的引數的訊息
    'loc_loss_type': P.MultiBoxLoss.SMOOTH_L1,   ###########位置損失型別,定義為列舉,有L2和SMOOTH_L1兩種型別。
    'conf_loss_type': P.MultiBoxLoss.SOFTMAX,   #########置信損失型別,定義為列舉,有SOFTMAX和LOGISTIC兩種。
    'loc_weight': loc_weight,
    'num_classes': num_classes,
    'share_location': share_location,
    'match_type': P.MultiBoxLoss.PER_PREDICTION,   #########訓練中的匹配方法。定義為列舉,有BIPARTITE和PER_PREDICTION兩種。如果match_type為PER_PREDICTION(即每張圖預測),則使用overlap_threshold來確定額外的匹配bbox。
    'overlap_threshold': 0.5,   #########閥值大小。即我們所說的IoU的大小。
    'use_prior_for_matching': True,   ########是否使用先驗匹配,一般為true。
    'background_label_id': background_label_id,   ##########背景標籤的類別編號,一般為0。
    'use_difficult_gt': train_on_diff_gt,  ########是否考慮困難的ground truth,預設為true。
    'mining_type': mining_type,    #######訓練期間的挖掘型別。定義為列舉,分別為三種類型: 若為NONE則表示什麼都不使用,這樣會導致正負樣本的嚴重不均衡;若為MAX_NEGATIVE則根據分數選擇底片;若為HARD_EXAMPLE則選擇基於“線上硬示例挖掘的基於訓練區域的物件探測器”的硬例項,此型別即為SSD原文中所使用的Hard_negative_mining(負硬挖掘)策略。
    'neg_pos_ratio': neg_pos_ratio,   #####負/正比率,即文中所說的1:3。
    'neg_overlap': 0.5,   ####對於不匹配的預測,上限為負的重疊。即如果重疊小於0.5則定義為負樣本,Faster R-CNN設定為0.3。
    'code_type': code_type,   #########bbox的編碼方式。此引數定義在PriorBoxParameter引數定義解釋中,為列舉型別,三種類型為:CORNER,CENTER_SIZE和CORNER_SIZE。
    'ignore_cross_boundary_bbox': ignore_cross_boundary_bbox,  ########如果為true,則在匹配期間忽略跨邊界bbox。 跨界bbox是一個在影象區域之外的bbox。即將超出影象的預測邊框剔除,這裡我們不踢除,否則特徵圖邊界點產生的先驗框就沒有任何意義。
    }
loss_param = {   ###儲存由損失層共享的引數的訊息
    'normalization': normalization_mode,    ######如何規範跨越批次,空間維度或其他維度聚集的損失層的損失。 目前只在SoftmaxWithLoss和SigmoidCrossEntropyLoss圖層中實現。按照批次中的示例數量乘以空間維度。 在計算歸一化因子時,不會忽略接收忽略標籤的輸出。定義為列舉,四種類型分別是:FULL,除以不帶ignore_label的輸出位置總數。 如果未設定ignore_label,則表現為FULL;VALID;BATCH_SIZE,除以批量大小;NONE,不要規範化損失。
    }

#引數生成先驗。
#輸入影象的最小尺寸
min_dim = 300   #######維度
# conv4_3 ==> 38 x 38
# fc7 ==> 19 x 19
# conv6_2 ==> 10 x 10
# conv7_2 ==> 5 x 5
# conv8_2 ==> 3 x 3
# conv9_2 ==> 1 x 1
mbox_source_layers = ['conv4_3', 'fc7', 'conv6_2', 'conv7_2', 'conv8_2', 'conv9_2'] #####prior_box來源層,可以更改。很多改進都是基於此處的調整。
# in percent %
min_ratio = 20 ####這裡即是論文中所說的Smin=0.2,Smax=0.9的初始值,經過下面的運算即可得到min_sizes,max_sizes。具體如何計算以及兩者代表什麼,請關注我的部落格SSD詳解。這裡產生很多改進。
max_ratio = 90
####math.floor()函式表示:求一個最接近它的整數,它的值小於或等於這個浮點數。
step = int(math.floor((max_ratio - min_ratio) / (len(mbox_source_layers) - 2)))####取一個間距步長,即在下面for迴圈給ratio取值時起一個間距作用。可以用一個具體的數值代替,這裡等於17。
min_sizes = []  ###經過以下運算得到min_sizes和max_sizes。
max_sizes = []
for ratio in xrange(min_ratio, max_ratio + 1, step):  ####從min_ratio至max_ratio+1每隔step=17取一個值賦值給ratio。注意xrange函式的作用。
########min_sizes.append()函式即把括號內部每次得到的值依次給了min_sizes。
  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]  ###這一步要仔細理解,即計算卷積層產生的prior_box距離原圖的步長,先驗框中心點的座標會乘以step,相當於從feature map位置映射回原圖位置,比如conv4_3輸出特徵圖大小為38*38,而輸入的圖片為300*300,所以38*8約等於300,所以對映步長為8。這是針對300*300的訓練圖片。
aspect_ratios = [[2], [2, 3], [2, 3], [2, 3], [2], [2]]  #######這裡指的是橫縱比,六種尺度對應六個產生prior_box的卷積層。具體可檢視生成的train.prototxt檔案一一對應每層的aspect_ratio引數,此引數在caffe.proto中有定義,關於aspect_ratios如何把其內容傳遞給了aspect_ratio,在model_libs.py檔案中有詳細定義。
##在此我們要說明一個事實,就是文中的長寬比是如何產生的,這裡請讀者一定要參看博主博文《SSD詳解(一)》中的第2部分內容,關於prior_box的產生。
# L2 normalize conv4_3.
normalizations = [20, -1, -1, -1, -1, -1]  ##對卷積層conv4_3做歸一化。model_libs.py裡產生了normallize層,具體的層定義,參看底層程式碼~/caffe/src/layers/Normalize_layer.cpp,為什麼這裡設定conv4_3為20我也沒看懂,原諒C++太渣,這裡每個數對應每個先驗層,只要哪個層對應的數不為-1則產生normal。
# 用於對之前的bbox進行編碼/解碼的方差。
if code_type == P.PriorBox.CENTER_SIZE:  ########兩種選擇,根據引數code_type的選擇決定,由於上面已經將code_type選定。有人理解為變數variance用來對bbox的迴歸目標進行放大,從而加快對應濾波器引數的收斂。除以variance是對預測box和真實box的誤差進行放大,從而增加loss,增大梯度,加快收斂。另外,top_data += top[0]->offset(0, 1);已經使指標指向新的地址,所以variance不會覆蓋前面的結果。prior_variance在model_libs.py中傳遞給了variance變數,然後利用prior_box_layer.cpp將其運算定義至priorbox_layer層中,具體可檢視train.prototxt中的每一個先驗卷積層層中產生先驗框的層中,即**_mbox_priorbox。
  prior_variance = [0.1, 0.1, 0.2, 0.2]
else:
  prior_variance = [0.1]
flip = True   ###如果為true,則會翻轉每個寬高比。例如,如果有縱橫比“r”,我們也會產生縱橫比“1.0 / r”。故產生{1,2,3,1/2,1/3}。
clip = False  ###做clip操作是為了讓prior的候選座標位置保持在[0,1]範圍內。在caffe.proto檔案中有關於引數clip的解釋,為”如果為true,則將先驗框裁剪為[0,1]“。
#以上兩個引數所產生的結果均在prior_box_layer.cpp中實現。

# 求解引數。
# 定義要使用的GPU。
gpus = "0,1,2,3"  #多塊GPU的編號,如果只有一塊,這裡只需保留0,否則會出錯。
gpulist = gpus.split(",") #獲取GPU的列表。
num_gpus = len(gpulist) #獲取GPU編號。

# 將小批量分成不同的GPU.
batch_size = 32  #設定訓練樣本輸入的數量,不要超出記憶體就好。
accum_batch_size = 32 #這裡與batch_size相搭配產生下面的iter_size。在看了下一行你就知道它的作用了。
iter_size = accum_batch_size / batch_size #如果iter_size=1,則前向傳播一次後進行一次反向傳遞,如果=2,則兩次前傳後進行一次反傳,這樣做是減少每次傳播所佔用的記憶體空間,有的硬體不行的話就無法訓練,但是增加iter會使訓練時間增加,但是總的迭代次數不變。
solver_mode = P.Solver.CPU
device_id = 0
batch_size_per_device = batch_size #批次傳遞,沒什麼好講的。
if num_gpus > 0:
  batch_size_per_device = int(math.ceil(float(batch_size) / num_gpus))  #這裡指如果你有多塊GPU則可以將這些訓練任務均分給多塊GPU訓練,從而加快訓練速度。
  iter_size = int(math.ceil(float(accum_batch_size) / (batch_size_per_device * num_gpus))) #多塊GPU的iter_size大小計算,上面的是一塊的時候。
  solver_mode = P.Solver.GPU
  device_id = int(gpulist[0])

if normalization_mode == P.Loss.NONE: ##如果損失層的引數NormalizationMode選擇NONE,即沒有歸一化模式,則基礎學習率為本檔案之上的base_lr=0.0004除以batch_size_per_device=32得到新的base_lr=1.25*10^(-5)。
  base_lr /= batch_size_per_device
elif normalization_mode == P.Loss.VALID: ##同理,根據不同的歸一化模式選擇不同的base_lr。在本檔案上面我們看到了normalization_mode = P.Loss.VALID,而loc_weight = (neg_pos_ratio + 1.) / 4==1,所以新的base_lr=25*0.0004=0.001,這就是為什麼我們最後生成的solver.prototxt檔案中的base_lr=0.001的原因,所以如果訓練發散想通過減小base_lr來實驗,則要更改最上面的base_lr=0.0004才可以。
  base_lr *= 25. / loc_weight
elif normalization_mode == P.Loss.FULL:  #同上理。
  # 每幅影象大概有2000個先驗bbox。
  # TODO(weiliu89): 估計確切的先驗數量。
  base_lr *= 2000. #base_lr=2000*0.0004=0.8。

# 評估整個測試集。
num_test_image = 4952 #整個測試集影象的數量。
test_batch_size = 8 #測試時的batch_size。
# 理想情況下,test_batch_size應該被num_test_image整除,否則mAP會略微偏離真實值。
test_iter = int(math.ceil(float(num_test_image) / test_batch_size)) #這裡計算每測試迭代多少次可以覆蓋整個測試集,和分類網路中的是一致的。這裡4952/8=619,如果你的測試圖片除以你的test_batch_size不等於整數,那麼這裡會取一個近似整數。

solver_param = { ##solver.prototxt檔案中的各引數的取值,這裡相信做過caffe訓練的人應該大致有了解。
    # 訓練引數
    'base_lr': base_lr, #把上面的solver拿下來。
    'weight_decay': 0.0005,
    'lr_policy': "multistep",
    'stepvalue': [80000, 100000, 120000], #多步衰減
    'gamma': 0.1,
    'momentum': 0.9,
    'iter_size': iter_size,
    'max_iter': 120000,
    'snapshot': 80000,
    'display': 10,
    'average_loss': 10,
    'type': "SGD",
    'solver_mode': solver_mode,
    'device_id': device_id,
    'debug_info': False,
    'snapshot_after_train': True,
    # 測試引數
    'test_iter': [test_iter],
    'test_interval': 10000, #測試10000次輸出一次測試結果
    'eval_type': "detection",
    'ap_version': "11point",
    'test_initialization': False,
    }

# 生成檢測輸出的引數。
det_out_param = {
    'num_classes': num_classes,  #類別數目
    'share_location': share_location,  #位置共享。
    'background_label_id': background_label_id, #背景類別編號,這裡為0。
    'nms_param': {'nms_threshold': 0.45, 'top_k': 400}, #非最大抑制引數,閥值為0.45,top_k表示最大數量的結果要保留,文中介紹,非最大抑制的作用就是消除多餘的框,就是使評分低的框剔除。引數解釋在caffe.proto中有介紹。
    'save_output_param': {  #用於儲存檢測結果的引數,這一部分引數在caffe.proto中的SaveOutputParameter有定義。
        'output_directory': output_result_dir,  #輸出目錄。 如果不是空的,我們將儲存結果。前面我們有定義結果儲存的路徑。
        'output_name_prefix': "comp4_det_test_", #輸出名稱字首。
        'output_format': "VOC", #輸出格式。VOC  -  PASCAL VOC輸出格式。COCO  -  MS COCO輸出格式。
        'label_map_file': label_map_file, #如果要輸出結果,還必須提供以下兩個檔案。否則,我們將忽略儲存結果。標籤對映檔案。這在前面中有給label_map_file附檔案,也就是我們在訓練的時候所做的labelmap.prototxt檔案的位置,詳情參看博主博文《基於caffe使用SSD訓練自己的資料》。
        'name_size_file': name_size_file, #即我們在訓練時定義的test_name_size.txt檔案的路徑。該檔案表示測試圖片的大小。
        'num_test_image': num_test_image, #測試圖片的數量。
        },
    'keep_top_k': 200, ##nms步之後每個影象要保留的bbox總數。-1表示在nms步之後保留所有的bbox。
    'confidence_threshold': 0.01, #只考慮可信度大於閾值的檢測。 如果沒有提供,請考慮所有的框。
    'code_type': code_type,  #bbox的編碼方式。
    }

# 評估檢測結果的引數。
det_eval_param = {  #位於caffe.proto檔案中的DetectionEvaluateParameter定義。
    'num_classes': num_classes, #類別數
    'background_label_id': background_label_id, #背景編號,為0。
    'overlap_threshold': 0.5, #重疊閥值,0.5。
    'evaluate_difficult_gt': False, #如果為true,也要考慮難以評估的grountruth。
    'name_size_file': name_size_file, #test_name_size.txt路徑。
    }

###希望你不需要改變以下###
# 檢查檔案。這一部分是檢查你的所有訓練驗證過程必須有的檔案與資料提供。
check_if_exist(train_data)
check_if_exist(test_data)
check_if_exist(label_map_file)
check_if_exist(pretrain_model)
make_if_not_exist(save_dir)
make_if_not_exist(job_dir)
make_if_not_exist(snapshot_dir)

# 建立訓練網路。這一部分主要是在model_libs.py中完成的。
net = caffe.NetSpec()
##呼叫model_libs.py中的CreateAnnotatedDataLayer()函式,建立標註資料傳遞層,將括號中的引數傳遞進去。model_libs.py檔案中提供了四種基礎網路,即VGG、ZF、ResNet101和ResNet152。
net.data, net.label = CreateAnnotatedDataLayer(train_data, batch_size=batch_size_per_device,
        train=True, output_label=True, label_map_file=label_map_file,
        transform_param=train_transform_param, batch_sampler=batch_sampler)
#呼叫model_libs.py中的VGGNetBody()函式建立截斷的VGG基礎網路。引數傳遞進去。model_libs.py檔案中提供了四種基礎網路,即VGG、ZF、ResNet101和ResNet152。可以分別檢視不同基礎網路的呼叫方式。
VGGNetBody(net, from_layer='data', fully_conv=True, reduced=True, dilated=True,
    dropout=False)  ##這些引數分別表示:from_layer表示本基礎網路的資料來源來自data層的輸出,fully_conv=Ture表示使用全卷積,reduced=Ture在該檔案中可以發現是負責選用全卷積層的某幾個引數的取值和最後選擇不同引數的全連結層,dilated=True表示是否需要fc6和fc7間的pool5層以及選擇其引數還有配合reduced共同選擇全卷積層的引數選擇,dropout表示是否需要dropout層flase表示不需要。

#以下為新增特徵提取的層,即呼叫我們本檔案最上面定義的需要額外新增的幾個層,即conv6_1,conv6_2等等。
AddExtraLayers(net, use_batchnorm, lr_mult=lr_mult)

#呼叫CreateMultiBoxHead()函式建立先驗框的提取及匹配等層數,下面這些引數其實我們在上面全部都有解釋,具體仍然可以參照caffe.proto和model_libs.py以及該層對應的cpp實現檔案去閱讀理解。這些層包括conv_mbox_conf、conv_mbox_loc、對應前兩者的perm和flat層(這兩層的作用在我博文《SSD詳解》中有解釋)、還有conv_mbox_priorbox先驗框產生層等。
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)

# 建立MultiBoxLossLayer。即建立損失層。這裡包括置信損失和位置損失的疊加。具體計算的實現在multibox_loss_layer.cpp中實現,其中的哥哥引數想multi_loss_param和loss_param等引數在前面均有定義。
name = "mbox_loss"
mbox_layers.append(net.label)
net[name] = L.MultiBoxLoss(*mbox_layers, multibox_loss_param=multibox_loss_param,
        loss_param=loss_param, include=dict(phase=caffe_pb2.Phase.Value('TRAIN')),
        propagate_down=[True, True, False, False]) #這裡重點講一下引數propagate_down,指定是否反向傳播到每個底部。如果未指定,Caffe會自動推斷每個輸入是否需要反向傳播來計算引數梯度。如果對某些輸入設定為true,則強制向這些輸入反向傳播; 如果對某些輸入設定為false,則會跳過對這些輸入的反向傳播。大小必須是0或等於底部的數量。具體解讀cpp檔案中的引數propagate_down[0]~[3]。

with open(train_net_file, 'w') as f: #開啟檔案將上面編輯的這些層寫入到prototxt檔案中。
    print('name: "{}_train"'.format(model_name), file=f)
    print(net.to_proto(), file=f)
shutil.copy(train_net_file, job_dir) #將寫入的訓練檔案train.prototxt複製一份給目錄job_dir。

 # 建立測試網路。前一部分基本上與訓練網路一致。
net = caffe.NetSpec()
net.data, net.label = CreateAnnotatedDataLayer(test_data, batch_size=test_batch_size,
        train=False, output_label=True, label_map_file=label_map_file,
        transform_param=test_transform_param)

VGGNetBody(net, from_layer='data', fully_conv=True, reduced=True, dilated=True,
    dropout=False)

AddExtraLayers(net, use_batchnorm, lr_mult=lr_mult)

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_name = "mbox_conf"  #置信的交叉驗證。
if multibox_loss_param["conf_loss_type"] == P.MultiBoxLoss.SOFTMAX:
  reshape_name = "{}_reshape".format(conf_name)
  net[reshape_name] = L.Reshape(net[conf_name], shape=dict(dim=[0, -1, num_classes]))
  softmax_name = "{}_softmax".format(conf_name)
  net[softmax_name] = L.Softmax(net[reshape_name], axis=2)
  flatten_name = "{}_flatten".format(conf_name)
  net[flatten_name] = L.Flatten(net[softmax_name], axis=1)
  mbox_layers[1] = net[flatten_name]
elif multibox_loss_param["conf_loss_type"] == P.MultiBoxLoss.LOGISTIC:
  sigmoid_name = "{}_sigmoid".format(conf_name)
  net[sigmoid_name] = L.Sigmoid(net[conf_name])
  mbox_layers[1] = net[sigmoid_name]

#下面這一部分是test網路獨有的,為檢測輸出和評估網路。
net.detection_out = L.DetectionOutput(*mbox_layers,
    detection_output_param=det_out_param,
    include=dict(phase=caffe_pb2.Phase.Value('TEST')))
net.detection_eval = L.DetectionEvaluate(net.detection_out, net.label,
    detection_evaluate_param=det_eval_param,
    include=dict(phase=caffe_pb2.Phase.Value('TEST')))

with open(test_net_file, 'w') as f: #寫入test.txt。
    print('name: "{}_test"'.format(model_name), file=f)
    print(net.to_proto(), file=f)
shutil.copy(test_net_file, job_dir)

# 建立deploy網路。
# 從測試網中刪除第一層和最後一層。
deploy_net = net
with open(deploy_net_file, 'w') as f:
    net_param = deploy_net.to_proto()
    # 從測試網中刪除第一個(AnnotatedData)和最後一個(DetectionEvaluate)層。
    del net_param.layer[0] #刪除首層
    del net_param.layer[-1] #刪除尾層。
    net_param.name = '{}_deploy'.format(model_name) #建立網路名。
    net_param.input.extend(['data']) #輸入擴充套件為data。
    net_param.input_shape.extend([
        caffe_pb2.BlobShape(dim=[1, 3, resize_height, resize_width])]) #deploy.prototxt檔案中特有的輸入資料維度資訊,這裡應該為[1,3,300,300]。
    print(net_param, file=f) #輸出到檔案
shutil.copy(deploy_net_file, job_dir) #複製一份到job_dir中。

# 建立Slover.prototxt。
solver = caffe_pb2.SolverParameter(  #將上面定義的solver引數統統拿下來。
        train_net=train_net_file,
        test_net=[test_net_file],
        snapshot_prefix=snapshot_prefix,
        **solver_param)

with open(solver_file, 'w') as f: #將拿下來的引數統統寫入solver.prototxt中。
    print(solver, file=f)
shutil.copy(solver_file, job_dir) #複製一份到job_dir中。

max_iter = 0  #最大迭代次數首先初始化為0。
# 找到最近的快照。即如果中途中斷訓練,再次訓練首先尋找上次中斷時儲存的模型繼續訓練。
for file in os.listdir(snapshot_dir): #依次在快照模型所儲存的檔案中查詢相對應的模型。
  if file.endswith(".solverstate"): #如果存在此模型,則繼續往下訓練。
    basename = os.path.splitext(file)[0]
    iter = int(basename.split("{}_iter_".format(model_name))[1])
    if iter > max_iter: #如果已迭代的次數大於max_iter,則賦值給max_iter。
      max_iter = iter

#以下部分為訓練命令。
train_src_param = '--weights="{}" \\\n'.format(pretrain_model) #載入與訓練微調模型命令。
if resume_training:
  if max_iter > 0:
    train_src_param = '--snapshot="{}_iter_{}.solverstate" \\\n'.format(snapshot_prefix, max_iter) #權重的初始引數即從我們定義的imagenet訓練VGG16模型中獲取。

if remove_old_models:
  # 刪除任何小於max_iter的快照。上一段和本段程式主要的目的是隨著訓練的推進,max_iter隨之逐漸增大,知道訓練至120000次後把前面生成的快照模型都刪除了,就是儲存下一次的模型後刪除上一次的模型。
  for file in os.listdir(snapshot_dir):  #遍歷查詢模型檔案。
    if file.endswith(".solverstate"): #找到字尾為solverstate的模型檔案。
      basename = os.path.splitext(file)[0]
      iter = int(basename.split("{}_iter_".format(model_name))[1]) #獲取已迭代的次數。
      if max_iter > iter: #如果迭代滿足條件,則下一條語句去刪除。
        os.remove("{}/{}".format(snapshot_dir, file))
    if file.endswith(".caffemodel"):  #找到字尾為caffemodel的模型檔案。
      basename = os.path.splitext(file)[0]
      iter = int(basename.split("{}_iter_".format(model_name))[1]) #獲取迭代次數iter。
      if max_iter > iter: #判斷如果滿足條件則刪除已存在的模型。
        os.remove("{}/{}".format(snapshot_dir, file))

# 建立工作檔案。
with open(job_file, 'w') as f: #將訓練檔案寫入執行檔案中生成.sh可執行檔案後執行命令訓練。
  f.write('cd {}\n'.format(caffe_root))
  f.write('./build/tools/caffe train \\\n')
  f.write('--solver="{}" \\\n'.format(solver_file))
  f.write(train_src_param)
  if solver_param['solver_mode'] == P.Solver.GPU:
    f.write('--gpu {} 2>&1 | tee {}/{}.log\n'.format(gpus, job_dir, model_name))
  else:
    f.write('2>&1 | tee {}/{}.log\n'.format(job_dir, model_name))

# 複製本指令碼只job_dir中。
py_file = os.path.abspath(__file__)
shutil.copy(py_file, job_dir)

# 執行。
os.chmod(job_file, stat.S_IRWXU)
if run_soon:
  subprocess.call(job_file, shell=True)