1. 程式人生 > >看Yolo3程式碼的筆記(寫得也就自己能到懂)

看Yolo3程式碼的筆記(寫得也就自己能到懂)

第一部分 ——整體:

從train.py開始看,只要這個執行步驟看懂了,其它的就都懂了。

主函式(就兩步):
1.建立了一個直譯器,將config.json的檔案讀入
2.將讀入的資料放到_main_函式中處理

_main_函式:

1.讀入了config.json中的資料
得到一個 config 的字典,裡面是config.json的引數。config:
{'model': {'max_input_size': 448, 'min_input_size': 288, 'labels': ['sit', 'stand', 'stretch', 'turn'], 'anchors': [55, 69, 75, 234, 133, 240, 136, 129, 142, 363, 203, 290, 228, 184, 285, 359, 341, 260]}, 'valid': {'valid_image_folder': '/home/jtl/keras-yolo3-master/data/img/', 'valid_times': 1, 'valid_annot_folder': '/home/jtl/keras-yolo3-master/data/ann_test/', 'cache_name': 'mouse.pkl'}, 'train': {'train_times': 4, 'nb_epochs': 100, 'noobj_scale': 1, 'tensorboard_dir': 'logs', 'batch_size': 1, 'grid_scales': [1, 1, 1], 'saved_weights_name': 'm.h5', 'train_annot_folder': '/home/jtl/keras-yolo3-master/data/ann/', 'class_scale': 1, 'xywh_scale': 1, 'ignore_thresh': 0.5, 'train_image_folder': '/home/jtl/keras-yolo3-master/data/img/', 'gpus': '0,1', 'obj_scale': 5, 'warmup_epochs': 3, 'learning_rate': 0.0001, 'debug': True, 'cache_name': 'mouse_train.pkl'}}

2.呼叫create_training_instances函式,進行資料處理。
輸入是訓練集圖片資料夾、標註資料夾、pkl,測試及圖片資料夾、標註資料夾、pkl,標籤。
輸出是 訓練集所有xml檔案的內容,測試集所有xml檔案的內容,標籤,每張圖中最多box數量(即每張圖片有幾個bounding box,取最大的數)
xml檔案的內容(字典):{'height': 0, 'width': 0, 'filename': '/home/jtl/keras-yolo3-master/data/img/000005.jpg', 'object': [{'xmax': 359, 'ymin': 152, 'ymax': 279, 'name': 'turn', 'xmin': 179}]}
標籤(列表):['sit', 'stand', 'stretch', 'turn']
max_box_per_image:1(我每張圖只標註了1個檢測目標)

3.建立生成器分別建立了訓練生成器和測試生成器的類。生成器裡會對輸入圖片進行打亂,還有一些影象增強的方法,比如隨機映象,尺寸的變化,裁剪,HSV空間隨機失真等等。
輸入:    1.例項(第二步中返回得到的xml檔案內容)
    2.anchors(最原始的anchors,從config字典中讀入)
    3.labels
    4.下采樣(神經網路輸入與輸出的比率)
    5.max_box_per_image
    6.batch_size(config中讀入,每批量的資料包)
    7&8.網路最小尺寸和網路最大尺寸
    9.shuffle(是否隨機打亂)
    10.jitter(一個比率,資料增強的隨機引數)
    11.標準化norm函式(把圖片除以255.,就是所有畫素都在0-1)
輸出:就是得到訓練集生成器的類和測試集生成器的類,但是沒見到呼叫類中的函式啊。
ps:看了一下生存器的長度,等於標註資料夾中標註檔案的個數/config.json中的batch_size。即有100個輸入 batch_size=4,生成器長度為25。有100個輸入 batch_size=1,生成器長度為100。

4.建立模型網路,如果已經存在權重,則讀入以及存在的模型(同時warmup_epochs=0),否則讀入預訓練模型backend.h5。 呼叫了create_model函式
輸入:    1.標籤的長度(幾分類)
    2.anchors(最原始的anchors,從config字典中讀入)
    3.max_box_per_image
    4.最大網格(一個二維列表,config中讀入的[max_input_size,max_input_size])
    5.batch_size(config中讀入)
    6.warmup_batches(預熱的batch,readme中說預熱部分會調整anchors。 warmup_batches=warmup_epochs(來自config)×train_times(來自config)×生成器長度 )
    7.忽略閾值(ignore_thresh來自config,判斷最大IoU與ignore_thresh,若小於則忽略?)
    8.多GPU
    9.儲存的權重(來自config,權重檔名)
    10.學習率(來自檔案)
    11.網格尺寸(來自config[1,1,1])
    12&13&14&15. obj_scale,noobj_scale,xywh_scale,class_scale。都是來自config,具體看細節。
輸出:得到一個訓練模型和一個推理模型(用於儲存)。訓練模型是在推理模型後,增加了損失函式和優化器。

5.開始訓練
建立callbacks,呼叫了create_callbacks函式,輸入:權重模型,儲存logs的資料夾,推理模型。
輸出:[earlystopping(提前停止),ModelCheckpoint(用於儲存最優模型),reduce_on_plateau(降低學習率),CustomTensorBoard(記錄每個批次後的損失)]

呼叫train_model.fit_generator開始訓練。
輸入:    1.第三步中的訓練集生成器(這裡必須為一個生成器或者keras.utils.Sequence)
    2.steps_per_epoch 在這裡等於生成器的長度乘以train_times。(原則上等於生成器的長度,這裡乘以train_times,應該是每一輪次都訓練了train_times次)
    3.epochs(來自config,加上了預熱輪次)
    4.verbose(日誌顯示模式)
    5.callbacks(之前得到的回撥函式)
    6.workers(最大程序數)
    7.max_queue_size(生成器佇列最大尺寸)

6.評價
呼叫了evaluate函式
輸入:    1.模型
    2.生成器
    以下為隱含輸入:
    3.iou_threshold (用於考慮檢測是 positive 還是 negative。 是否是一個正向的包圍檢測框)
    4.obj_thresh (用於區分物件還是非物件object,檢測框中是否含有目標)
    5.nms_thresh (用於判斷兩個檢測是否重複的閾值)------------多目標檢測可以改這個閾值
    6&7.net_h和net_w (把影象輸入到模型的尺寸)
    8.save_path=None
輸出:

第二部分——生成器、模型、評價函式

接3:
生成器類BatchGenerator:
首先,BatchGenerator繼承了keras.utils.Sequence 。查閱keras文件‘每一個 Sequence 必須實現 __getitem__ (看語法書)和 __len__ 方法。 如果你想在迭代之間修改你的資料集,你可以實現 on_epoch_end。 __getitem__ 方法應該範圍一個完整的批次。’ 
__len__(self):計算每一個epoch的迭代次數
__getitem__(self, idx):用於生成每個batch資料
            1. self._get_net_size:每十個batch改一次size。
            2.確定每個batch的左右邊界和初始化。r_bound-l_bound=batch_size,x_batch是四維的輸入圖片[r_bound-l_bound,長,寬,RGB]; 
            t_batch有6個維度[r_bound-l_bound,1(?),1(?),1(?),最多的box數,4個真實bounding box]; 初始化輸入輸出yolo_1,yolo_2,yolo_3都是5維,為yolo3的三個輸出
            [r_bound-l_bound,取樣後的長(網格的長),取樣後的寬,anchors個數(3),4+1+len(labels)],dummy_yolo_1、2、3都只有兩維[r_bound-l_bound,1]
            3.進入一個batch內部的迴圈,先對每個batch中的輸入圖片做資料增強,返回增強後的圖片和增強後的真實bounding box座標(輸入原來影象和xml讀取的值)
            4.要找到最優的先驗anchor box, 標籤的bounding box和先驗anchor做IoU(無關座標,只計算長寬最相近的,所以實際上ymin和xmin都為0,只有ymax和xmax有數),要知道每個
            先驗的anchor對應相應的yolo特徵圖(9個先驗的anchor,111222333分佈)。
            5.根據第4步得到的最優先驗的anchor,將ymin,ymax,xmin,xmax轉換 sigma(t_x) + c_x 、sigma(t_y) + c_y、t_w、t_h。(yolo要計算得到 t_x,t_y,t_w,t_h)
            6.對應yolo特徵網路中,對應batch中圖片,對應網格內,對應先驗anchor中,寫入9個標籤值
            7.真實包圍框寫入t_batch(這個為啥維數眾多,而且取特徵圖下的中心座標,和輸入下的box長和寬???)
            8.返回    [正則+資料增強後的圖片,中心點和長寬矛盾的t_batch,與最優anchor有關儲存了真實9個標籤的三個yolo×3] [只初始化別的啥也沒幹的dummy_yolo×3]
on_epoch_end:在每一次epoch結束是否需要進行一次隨機,重新隨機一下index;在yolo裡沒有用。

接4:
creat_model函式中主要呼叫了create_yolov3_model,這個函式搭建yolo模型,先看一下函式輸入:1.nb_class標籤的長度    2.原始anchors來自config中    3.max_box_per_image每張圖最多的box    4.最大網格config中讀入的[max_input_size,max_input_size](預設正方形)     5.batch_size     6.warmup_batches(預熱的batch)    7.ignore_thresh        8.grid_scales    9.obj_scale    10.noobj_scale    11.xywh_scale    12.class_scale
    create_yolov3_model中:
            1.模型的輸入,與生成器中第一個返回值一致。但是每次只讀一張或一個: 輸入圖片, true_boxes,三種尺度的ture_yolo_1、ture_yolo_2、ture_yolo_3
            2.搭建網路,前74層(注意殘差也算一層,還有1×1卷積)中有52層卷積(論文裡叫darknet-53),中間有一個_conv_block函式,設定了殘差位置,標準化資料(BatchNormalization,yolo2論文中說用這個代替了
            dropout),leakyReLU的啟用函式。第74層後,通過幾層卷積,在79層單獨分出來一路,在第81層後變成3*(5+nb_class)維作為pred_yolo_1 —— 結合input_image,pred_yolo_1、ture_yolo_1、true_boxes得到loss_yolo1
                            79層後另一路做幾個卷積,在86層中結合第61層的資料,繼續卷積在91層除分出來一路,在93層後變成3*(5+nb_class)維作為pred_yolo_2-結合input_image,pred_yolo_2、ture_yolo_2、true_boxes得到loss_yolo2
                            91層後另一路做卷積,在98層處結合36層的資料,繼續卷積,在105層後變成3*(5+nb_class)維作為pred_yolo_3-結合input_image,pred_yolo_3、ture_yolo_3、true_boxes得到loss_yolo3
                            具體說一下輸入,從模型建立開始,只用到了輸入圖片即(input_image),然後在計算三個loss_yolo中,用到了true_boxes,以及分別用到了三個真實yolo。
            3.loss_yolo由YoloLayer這個類生成,這個咱單獨說。
            4.keras.models 的Model類 ,訓練網路輸入輸入影象,真實的包圍框,分別屬於三個yolo網路的標籤,輸出三個損失
                                                           推斷模型:輸入影象,輸出三個特徵圖下的預測
返回訓練模型和推測模型

接上文:
首先,YoloLayer 繼承了keras中的Layer類。輸入1.原始anchors來自config中    2.三個loss_layer讀入不同的倍數的max_grid即[max_input_size,max_input_size],448×448,896×896,1792×1792    3.batch_size     4.warmup_batches(預熱的batch)    5.ignore_thresh        6.grid_scales    7.obj_scale    8.noobj_scale    9.xywh_scale    10.class_scale;  然後最神奇的是後面還跟了([input_image, pred_yolo_1, true_yolo_1, true_boxes])這個小尾巴(以loss_yolo1為例)。
keras文件說:對於那些包含了可訓練權重的自定義層,你應該自己實現這種層。自己搭建這個層,只要三種方法即可:
build(input_shape): 這是你定義權重的地方。這個方法必須設self.built = True,可以通過呼叫super([Layer], self).build()完成。/ 
call(x): 這裡是編寫層的功能邏輯的地方。你只需要關注傳入call的第一個引數:輸入張量,除非你希望你的層支援masking。/
compute_output_shape(input_shape): 如果你的層更改了輸入張量的形狀,你應該在這裡定義形狀變化的邏輯,這讓Keras能夠自動推斷各層的形狀。
一步步來
__init__:
    1.anchor輸入是6個數字的列表(三個yolo,每個三個先驗anchor),轉變為張量,shape=(1,1,1,3,2)
    2.張量擴張和拼接,目的是得到self.cell_grid shape=(batch_size,max_grid,max_grid,3,2),注意不同loss_layer的max_grid不同,batch=4,分別得到(4,448,448,3,2)、(4,896,896,3,2)、(4,1792,1792,3,2)
    3.super繼承於Layer,初始化Layer中的**kwarg,在這裡沒有kwarg的輸入,所以沒有用。topology.py中有Layer類的定義,複雜的很。
__build__:採用了官方給的定義方法 super(YoloLayer, self).build(input_shape),這個input_shape在keras的底層函式裡,與輸入有關。反正不用我們操心。
__call__:這個是這個類的核心了,用於計算損失loss。還記得構造類的時候的小尾巴([input_image, pred_yolo_1, true_yolo_1, true_boxes]),就是call的輸入x,上來input_image, y_pred, y_true, true_boxes=[input_image, pred_yolo_1, true_yolo_1, true_boxes]
    1.調整y_pred ,變成[batch, grid_h, grid_w, 3, 4+1+nb_class]的張量,grid_h, grid_w是特徵對映,由y_ture得到,同時得到輸入的net_h,net_w,以及tf.cast得到grid和net的factor(tf.float32), [1,1,1,1,2])
    2.得到真實的t_x,t_y,t_w,t_h,confidence,class與預測的t_x,t_y,t_w,t_h,confidence,class_probabilities。true_box =[特徵圖中的座標(浮點數比如(5.1,5.2),意思是網格加上偏移的結果),輸入影象的寬和高],根據這個得到true_xy(?,1,1,1,1,2)維度,ture_wh,比較每個預測框與真實框:由預測得到pred_xy,pred_wh。通過一系列操作得到真實區域true_areas(?,1,1,1,1)維(?是batch)

    這裡我們還是倒著看,要得到loss = loss_xy + loss_wh + loss_conf + loss_class,看計算損失的函式(yolo1的論文中有提到損失如何計算)
    object_mask:是在y_true第四個維度的基礎上在加一個維度,代表[batch的第幾個輸入,特徵圖的長,特徵圖的寬,第幾個anchor,是否有目標],這個是否有目標是增加的一個維度表示,非零即一,但是怎麼傳入的?
    true_box_xy, true_box_wh, xywh_mask:這幾個值,通過tf_cond(相當於if,1則執行lambda1,0則執行lambda2)判斷是否還在Warm-up training決定不同的調整,若不是在warm_up training,則true_box_xy和true_box_wh等於y_true中提取,xywh_mask=object_mask
    wh_scale:與true_box_wh有關,the smaller the box, the bigger the scale
    pred_box_xy、pred_box_wh、pred_box_conf:從y_pred中獲得
    基本就通過真實的和網路標籤中獲取的數,壓縮求和,得到四個loss
    obj_scale:有目標的損失權重
    noobj_scale:無目標的損失權重
    xywh_scale:定位的損失權重
    class_scale:分類的損失權重
3.最後loss乘以grid_scale(三個loss_yolo的loss權重不一樣),可知grid_scale是三個特徵圖下損失的權重。
compute_output_shape:網路輸出多行一列的。

接6:
子函式evaluate:
1.輸入:模型、生成器、iou_threshold、obj_thresh、nms_thresh、net_h和net_w、save_path=None
2.讀取測試集中每一張圖片。呼叫get_yolo_boxes函式:,返回416×416影象中的bounding_box和class
            1.batch_input =[讀入的數量,圖片的高度,圖片的寬度,3個通道]
            2.預處理圖片preprocess_input,批量調整大小(416×416),返回值的shape(1,416,416,3)第一個維是第幾張的圖片
            3.model.predict_on_batch每個batch(以下以batch=1測試)進行預測,會得到一個三個array值的列表,分別(1, 13, 13, 27), (1, 26, 26, 27) ,(1, 52, 52, 27)
            ( 重要 yolo3中最後得到13×13的特徵圖,倒數的26×26特徵圖,52×52特徵圖, 3×(4(bounding box offsets)+1(objectness prediction)+4(分類))=27 )
            4.用decode_netout進行對三個特徵圖下的結果分別進行解碼,每個grid_h×grid_w×3個anchor(9個數),前四個':4'是x,y,w,h,第五個'4'是objectness score
            (也是要大於obj_thresh才有效),後面是分類。x,y和objectness用了sigmod函式,分類還是用了softmax,大於obj_thresh的為1,否則為0。根據論文中的公式計算x,y,w,h。
            (x,y只是定位了網格的位置)    返回的boxes儲存了認為有目標的anchor,包括三個特徵圖。
            5.correct_yolo_boxes在416×416的圖片中得到正確的bounding_box的位置(anchor_box變為bounding_box)
            6.do_nms非最大抑制 輸入為每張圖片中得到的大於obj_thresh的bounding_box和非最大閾值。同一類別,IoU大於nms_thresh的,取分類分數最高框。
3.根據objectness score分數排序,讀取檢測的結果(只有分類,沒有包圍框)和標註的結果,all_detections是一個[需要檢測的資料數量,標籤的長度],10張圖,4 個標籤就是[10,4],每一維度的中都代表第幾張圖,label的檢測,有幾個。比如第一張圖的第一個標籤,all_detections[0][0]=[]即表示第一張圖沒有第一個標籤的檢測。all_detections[0][0]=[[ 237. -61. 488. 583. 0.99811834] [207. 125. 518. 396. 0.94932592]],表示第一張圖有兩個第一個標籤的檢測,前四項是位置,第五個是概率。all_annotations同樣的,但是隻包含座標,不包含概率(第一張圖第一個標籤下有座標就是有目標)
4.對每類標籤:annotations是真實的框,detections是檢測到的框和概率,scores是包圍框的記錄, num_annotations統計了每個label下測試集真實目標的個數。
scores 記錄每一個檢測detections中每檢測到包圍框(若因為閾值不到或者被非最大抑制,即有包圍框概率為0,scores增加一個0;有包圍框有目標概率,scores 增加一個概率的值),所以對每一個標籤的迴圈,scores記錄了檢測到這個標籤類別的包圍框的數目(不論概率是多少)。對於檢測到的每個包圍框,檢測有而標籤沒有,或者檢測的包圍框和標籤的包圍框重疊低於iou_threshold,則false_positives增加1,true_positives增加0;否則true_positives增加1,則false_positives增加0。
5.對每類標籤:true_positives和false_positives,np.cumsum,計算recall和precision
false_positives [ 1.  0.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.]
true_positives [ 0.  1.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
false_positives_cumsum [  1.   1.   2.   3.   4.   5.   6.   7.   8.   9.  10.  11.  12.  13.  14.  15.  16.  17.  18.  19.  20.  21.  22.]
true_positives_cumsum [ 0.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.]
recall [ 0.   0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5  0.5]
precision [ 0.          0.5         0.33333333  0.25        0.2         0.16666667  0.14285714  0.125       0.11111111  0.1         0.09090909  0.08333333  0.07692308  0.07142857  0.06666667  0.0625      0.05882353  0.05555556  0.05263158  0.05        0.04761905  0.04545455  0.04347826]
通過召回率和準確率計算平均精度
輸出:各個分類的檢測精度的字典:average_precisions  {0: 0.22866917984160928, 1: 0.78000000000000003, 2: 0.17642920804685511, 3: 0.27806373346786883}