import numpy as my notetaking as a machine learning engineer

Caffe SSD 程式碼 深度剖析

前言

最近因緣際會, 在使用SSD做object detection training時, 遇到兩個需求

  • 在output log中顯示Test Loss
    • 主要目的: 了解model是否overfit or underfit
    • 雖然看detection_eval(預設顯示)多少也可以判斷, 但還是希望看到Train Loss與Test Loss之間的差距
  • 直接在dataset中添加negative background images
    • 主要目的: 降低model的false positive rate
    • SSD default不支援添加negative background, 連annotate bounding box時使用background label都會跳error. 然而, 由於測試環境較封閉且複雜. 在考量資料蒐集與標記的成本下, 讓SSD支援輸入negative background images這一選項, 變得十分吸引人

因Wei Liu的Caffe SSD code不支援以上兩種功能, 在github搜issue找大神時也得不到完整的solution, 自己動手修改成了唯一解.
為了確保coding上的調整沒有問題, 就花了幾天的時間將Caffe SSD仔細掃過一輪, 並在此分享心得
最後, 也感謝Viscovery小夥伴們的支持, 其中Ryan與Isis都給了不錯的建議

背景知識

本文撰寫時, 預設讀者已具備以下知識

  • 熟悉image classification訓練細節 (layers like softmax, regression, and etc.)
  • 理解基本ConvNN計算細節 (kernel, stride, pad, and etc.)
  • 大致理解一些Caffe術語 (blob, forward_cpu, backward_cpu, and etc.)
  • 大致理解SSD運作原理 (multi-scale anchor box with softmax and L2 regression head)

如不熟悉, 相信用括號內的關鍵字可以google到不少資訊

SSD 架構概述

稍微複習一下, 從R-CNN這類two-stage object detector演進而來的one-stage detectors(e.g. YOLO, SSD, RetinaNet等)
精神上很像是Faster R-CNN中RPN(region proposal network)的延續.

Faster R-CNN中的RPN

RPN想做的事情很單純, 就是在一張image中, 找出可能有物體的位置, 並將他用框框(bounding box; bbox)匡出來

  • input: 一張image
  • output: a list of bboxes (x, y, w, h)
    若已經熟悉image classification, 一個最簡單的做法就是
    1. 把一張大圖切成無數塊小區域
    2. 將每個小區域送進一個classifier
    3. 由這個classifier判斷吐出 “是object” 與 “不是object” 的結果

雖然這樣的做法可行, 但實際執行起來太慢 (步驟1切圖+步驟2一張一張送進去)
因此rgb大神們就想出了一個平行化的概念, 將原本

  • [大圖 -> 切碎成多個 -> 一個一個送入classifier得到ouput]
    轉換為
  • [大圖 -> 送進一個classifier集合體 -> 一次同時吐出一堆output結果]
    然而,
  • 這個classifier集合體, 到底應該由”幾個”classifer集合而成呢?
  • 每個classifier到底 負責 哪一塊 切圖區域?
    為了明確定義, Faster R-CNN就導入了anchor box的概念 (見下圖)
    每個anchor box可以近乎視為一個切圖的概念(雖然不是完全相等),
    並在network後端接出一個classification loss(cls layer)

reg layer TBD

SSD Code Tracing

coding 架構

MultiBoxLossLayer
  |
  |- GetGroundTruth: Parse Dataset Ground Truth BBoxes 資料
  |
  |- GetPriorBoxes: Parse Network 預設的 Prior Boxes
  |
  |- GetLocPredictions: Parse Network output 的 Predicted Boxes
  |
  |- FindMatches: 以ground truth bboxes為基準, 比較並找出positive matched的 predicted boxes / prior boxes
  |
  |- MineHardExamples: 根據設定而定, mine hard negative examples (MAX_NEGATIVE) 或 mine hard (positive+negative) examples (HARD_EXAMPLES)
  |
  |- EncodeLocPrediction: 最終 encode localization 結果 (localization loss 用此做計算)
  |
  -- EncodeConfPrediction: 最終 encode confidence 結果 (confidence loss 用此做計算)

重要variable格式說明

  • vector<map<int, vector<int> > >* all_match_indices
    • 簡介: 儲存每張圖, 每個position是否有match到ground truth的資訊
    • 大小: 等同於batch size. all_match_indices[0]代表第一張image的matching資訊, 以此類推
    • 內容:
      • map<int, vector<int>> match_indices
        • int
          • 內容: mapping到的ground truth class (e.g. 0是background, 1是person, 2是bicycle…etc)
        • vector<int>
          • 大小: 等同於所有可能的position size (=num_prior; 8732 by default)
          • 內容
            • mapping到ground truth bbox的index (e.g. 若這個position與第九個gt重疊, 則數值為8; 以此類推)
            • -1: 沒有與任何一個 ground truth bbox 重疊
            • -2: predict出來的bbox超出圖片界線, 不考慮 (ignore_cross_boundary_bbox = True時)

parameters 說明

  • loc_loss_type: localization regression loss; 可選用P.MultiBoxLoss.SMOOTH_L1P.MultiBoxLoss.L2
  • conf_loss_type: confidence classification loss; 可選用P.MultiBoxLoss.SOFTMAXP.MultiBoxLoss.LOGISTIC
  • loc_weight: localization 與 confidence 之間的平衡比例 weight; 其實就是paper中eq X 的 /alpha
  • num_classes: total classification的class數量 (須包含background class)
  • share_location: 預設為True. 若設為False, 則per prior box localization的結果會由4個參數(x, y, w, h)擴增為4*num_classes個參數(代表每個class都用不同的localization參數)
  • match_type: 預設為P.MultiBoxLoss.PER_PREDICTION, 同paper描述, 若任一predicted bbox/prior bbox跟ground truth(gt) bbox IoU大於overlap_threshold時, 就判斷成matched bbox(positive bbox) 若設定成P.MultiBoxLoss.BIPARTITE, 則ground truth bbox只能有一個相對應的matched bbox(挑IoU最高的那個)
  • overlap_threshold: 應用場景見match_type, 預設為0.5 (IoU)
  • use_prior_for_matching: 預設為True, 則matching的方式是用 prior bboxes 跟 gt bboxes 做比對 (若相符, 則該bboxes被添加到matched_indices中); 若設成False, 則matching方式是用 predicted bboxes 跟 gt bboxes 做比對
  • background_label_id: 背景 class id
  • use_difficult_gt: dataset 有個difficult的tag可以進行標記. 此param預設為True, 即就算困難的ground truth sample也丟進去訓練
  • mining_type: MineHardExamples裡使用, 有以下三種模式
    • P.MultiBoxLoss.MAX_NEGATIVE (default)
    • P.MultiBoxLoss.HARD_EXAMPLE
    • P.MultiBoxLoss.NONE
  • neg_pos_ratio
  • neg_overlap
  • code_type
  • ignore_cross_boundary_bbox

Python Coding Style

建議的 coding 格式

pylint

使用 pylint 檢查 程式碼
  • 透過 pip install pylint 安裝
  • pylint XXX.py 執行
  • pylint 並非完美, 僅是一個輔助工具. 你應該事情況
    • 修改程式碼
    • 將部分報錯 加入 ignore list (過多報錯, 可能導致你忽略真正需要修改的資訊)


命名 Naming

module_name, package_name, ClassName, method_name, ExceptionName, function_name, GLOBAL_CONSTANT_NAME, global_var_name, instance_var_name, function_parameter_name, local_var_name

不允許採用

  • 單一字元名稱 single character names (e.g. a, b, c)
    • counters 或 iterators 除外, 通常使用 i, j, k
  • 在 package/module name 中使用 dashes(-)
    • e.g. 創建一個module 叫做 calculate-histogram.py
  • 前後雙底線 __double_leading_and_trailing_underscore__
    • 為Python內部保留 reserved by Python

慣例 Convention

  • internal: 僅使用於某module 或 以protected/private的形式存於某class的 變數或函示
  • 前綴單底線(_): 僅 慣例上代表, 該 變數或函示 為 internal 使用
    • 前綴單底線 不具備實際 internal 效應, 僅特殊情況下提供 internal 保護
    • e.g. 在 import * from 時不會出現
  • 前綴雙底線(__): 對 編譯器interpreter 有實際意義, 將使 變數或函示 變成 internal
    • 舉下面例子, ref: https://shahriar.svbtle.com/underscores-in-python

      >>> class A(object):
      ...     def _internal_use(self):
      ...         pass
      ...     def __method_name(self):
      ...         pass
      ... 
      >>> dir(A())
      ['_A__method_name', ..., '_internal_use']
      

      可以發現 前綴雙底線 __method_name 將被編譯器 自動取代成 _A__method_name
      這在處理 繼承 inherit 時是有幫助的

      >>> class B(A):
      ...     def __method_name(self):
      ...         pass
      ... 
      >>> dir(B())
      ['_A__method_name', '_B__method_name', ..., '_internal_use']
      
  • class 名稱 使用 CapWords, module 名稱 使用 lower_with_under.py
    • e.g. 避免出現 from StringIO import StringIO 的尷尬情況

命名表格 Naming Table 整理

 Type Public Internal
Packages snake_case  
Modules snake_case _snake_case
Classes CapWords _CapWords
Exceptions CapWords  
Functions snake_case() _snake_case()
Global/Class Constants CAPS_WITH_UNDER _CAPS_WITH_UNDER
Global/Class Variables lower_with_under _snake_case
Instance Variables snake_case _snake_case (protected) or __snake_case (private)
Method Names snake_case() _snake_case() (protected) or __snake_case() (private)
Function/Method Params snake_case  
Local Variables snake_case  


縮排 Indentation

一律使用 4 spaces
  • 永遠不可將 tabsspaces 混用
  • 當需要以 多行 表示程式碼時, 可以考慮以下兩種方案
    • 使用 4 spaces 做縮排開頭
    • 使用 垂直方向 對齊
YES:   # Aligned with opening delimiter
       foo = long_function_name(var_one, var_two,
                                var_three, var_four)

       # Aligned with opening delimiter in a dictionary
       foo = {
           long_dictionary_key: value1 +
                                value2,
           ...
       }

       # 4-space hanging indent; nothing on first line
       foo = long_function_name(
           var_one, var_two, var_three,
           var_four)

       # 4-space hanging indent in a dictionary
       foo = {
           long_dictionary_key:
               long_dictionary_value,
           ...
       }
NO:    # Stuff on first line forbidden
       foo = long_function_name(var_one, var_two,
           var_three, var_four)

       # 2-space hanging indent forbidden
       foo = long_function_name(
         var_one, var_two, var_three,
         var_four)

       # No hanging indent in a dictionary
       foo = {
           long_dictionary_key:
               long_dictionary_value,
               ...
       }


註解 Comments

  • TBD

結語

保持一制性 BE CONSISTENT

  • 如果你是在修改別人的 程式碼, 花點時間熟悉 既有的 coding style.
  • “一致” 的風格 比 “正確” 的風格 更重要

References

Clean Code - part 1

入門觀念

  • 為何要考慮 易讀 程式碼 之美學?
    • 不知道, 你是否跟我一樣, 並非CS本科, 但本著對 computer vision 或 deep learning 的熱忱, 半路出家開始寫起程式碼. 隨著時間 一點一滴過去, 你手下鍵盤敲出的程式碼 一行一行累積. 從原本什麼都不會 只能複製 hello world 的 範例; 開始會用 google 然後從 stackoverflow 找有綠勾勾的解答; 到 有點組織地結合各個功能, 東拼西湊 組出一個可以動的 project.
    • 在這過程中, 不知道到你是否也跟我有過同樣的疑惑
      • 我沒受過什麼專業的訓練, 寫出來的程式碼好嗎?
      • 總覺得 自己寫的程式碼 比較醜, stackoverflow 貼來得比較漂亮, 但說不出具體原因
      • 過了 3個月~半年, 回頭看自己的程式碼, 發現根本看不懂自己在寫什麼
    • 如果我們面臨的疑惑相同, 那在此分享我找到的答案, 就是 “想辦法讓你的程式碼 變得 容易閱讀(/使用/修改, 請自行替換)”
    • 在這個 open-source code 滿天飛的世代, 身為一個 撰寫程式的 programmer, 從中獲得的成就感已經由 “哇! 我的程式碼跑得動!” 漸漸轉化成 “哇! 有好多人 fork 我的程式碼!” 一個 能夠執行的程式碼並不稀奇, 但 一個能讓 許多人喜愛使用的 程式碼 卻十分珍貴.
  • 文章編排
    • 本篇blog內容, 90%以上啟發或摘錄自 The Art of Readable Code (詳情見Ref), 所以也可以算是一篇 讀書心得整理報告. 這本書不厚, 內文+目錄不到200頁, 但頁頁精彩, 其中大部分的程式碼又以 Python 撰寫角度出發, 個人認為很適合 剛接觸 ML/DL, 同時希望寫出 “乾淨程式碼” 的 programmer閱讀. 當然, 也感謝在 Viscovery 的好夥伴 城市小模yoco 推薦我此書
    • 以下, 將以 條列式 具體描述 幾項我認為不錯的觀點

重點 1: 名稱的選擇

1.1 採用更具代表意義的名稱

  • 程式碼是寫給人看的. 撰寫時, 應該細心地為讀者思考. 怎麼用簡潔地話語, 表達出程式碼的邏輯.
  • 在這之中, 又以命名尤其重要. 使用具有代表意義的名稱, 能讓人更快地進入狀況, 了解某個function在處理的問題, 或是某個variable代表的意義.
  • 使用具有代表意義的名稱, 比在乎名稱長短來得更重要.
  • 舉例來說
    • 實作一個 從url下載圖片 的程式碼, 若使用
        def get_img(url, dst):
      
    • 因為 不清楚到底是在 get 什麼, 還不如使用更明確的
        def download_image(url, dst):
      

1.2: 避免大量使用 tmp, retval 等無意義的詞彙

  • 坦白來說, 你只是懶得 動腦思考 該取什麼名字對吧
  • 允許的例外, i, j, k 等 通用 iterators
  • tmp, return retval 這些變數不帶任何意義, 能不用則不用. 例外請見 1.3

1.3: 使用範圍 (scope) 越廣的名稱, 越重要!

  • 若某個 function 或 variable 的 scope 是通用於整個 project, 那你就要小心了! 使用範圍越廣的 function 或 variable, 越要小心地命名. 因為它將會在你的程式碼中不斷出現
  • 相對地, 使用範圍 極小的 function 或 variable, 若生命週期才短短幾行, 就算命名成西瓜也不會有人看不懂, 那倒是不用花太多心思著墨
  • 舉例:
      def swap_values(a, b):
          tmp = a
          a = b
          b = tmp
          return a, b
    
    • 這時你要叫 tmp, buffer, 或是西瓜, 我猜應該沒什麼影響

重點 2: 建立個人美學

2.1 使用普羅大眾接受的排版方式

  • clean code 的概念並非近期出現, 從程式語言誕生的那年起, 許多程式設計師就已經在致力於優化程式碼並提高可讀性. 因此, 與其從無到有建立自己的coding 美學, 還不如從 一些現有模板著手. 在此附上兩個 python 常見(亦被多人使用)的 coding guideline
    • google
      • python coding guideline: https://github.com/google/styleguide/blob/gh-pages/pyguide.md
    • python 官方
      • PEP 8: https://www.python.org/dev/peps/pep-0008/
      • PEP 257: https://www.python.org/dev/peps/pep-0257/

2.2 輔助工具: convention 自動檢查程式

  • 在 ubuntu 環境裡, 也可以安裝一些 自動檢查程式 (檢查是否follow python 官方 conventions). 舉例來說, pylint 就是個不錯的檢查工具
    • 使用方式, 安裝後
      • 在 bash command 下執行 pylint YOUR_CODE.py
      • vim 中使用 :SyntasticCheck pylint (須先安裝 vim-syntastic)
  • 注意
    • 一昧追求 convention 計算出來的 高分, 是沒有意義的
    • 檢查工具, 僅提供 的美學規則. 如何從中挑選適合地規則進行 活用, 是一個程式設計師必續具備地能力
    • 所謂 活用, 在這裡指: 如何讓讀者能更好上手

2.3 程式碼的 一致性

  • 如果你是 自己 在開發專案: 維持你的美學, 並確保該美學一致化地通用於程式碼
  • 若你是在 修改別人 的專案, 請接受他的美學, 並在修改時, 維持既有美學
  • 對於易讀性而言, 保持程式碼一致化 比 使用正確的規則 更重要

重點 3: 合理地加入註解

  • 加入註解不難. 難得是, 該如何 適時地 加入註解

3.1 什麼地方不該加入註解

  • 註解也是程式碼, 寫越多, 後續讀者要讀的也越多
  • 不要為了註解而註解
  • 不要註解那些能很快從程式碼中知道的事實
  • 舉例:
    • 脫褲子放屁
        # training function
        def train(...):
            ...
      
      • function 或 variable 名稱已清楚表達意義, 沒必要添加註解
  • 不要用註解去 解釋名稱取得不好 的 function 或 variables, 請 直接修改命名

3.2 應該加入什麼樣的註解

  • 與其把 名稱描述一遍, 不如 解釋更多實作細節
  • 舉例
    • normal
        # 在給定的 subtree 找尋指定的 name, 最多找到 depth層
        def find_node_in_subtree(subtree, name, depth)
      
    • better
        # 找出有指定 'name' 的 node 或 回傳 None
        # 如果 depth <= 0, 只會檢查 subtree
        # 如果 depth == N, 只會檢查 subtree, 以及 N層內的 nodes
        def find_node_in_subtree(subtree, name, depth)
      
  • 記下 寫程式時 重要的想法
  • 舉例
    • 適當地裡用 特殊名稱 標記, 讓未來的讀者能夠清楚了解程式碼缺陷或不足的地方
        # TODO(owner):   作者還沒處理的部份
        # FIXME(owner):  已知的問題
        # HACK(owner):   承認解決方法不夠優雅
        # XXX(owner):    危險! 重要問題
      
  • 描述 選用某些常數的特定原因
  • 舉例
      NUM_THREADS = 8   # 只要 >= 2 * num_processors 就夠了
    
  • 想像程式碼在他人眼中的樣子, 用這樣的角度添加註解ˇ

Part 1 結語

  • 以上僅針對 易讀程式之美學 的前半段進行整理, 也就是 程式碼 表層處理 的部分. 然而, 書中後半部在描述 程式碼的架構 的部分 (e.g. 如何重構, 早期跳出, 一次做一項事情等等), 尚未整理完成, 還待 後續 part 2 進行撰寫.

References

  • Dustin Boswell, Trevor Foucher, 莊弘祥 譯, The Art of Readable Code 易讀程式之美學
  • Robert C. Martin, 戴于晉 譯, Clean Code 無瑕的程式碼