一. 背景
本文檔以ssd300作為背景網絡進行解讀,以Tensorflow,Keras為架構
原始代碼:https://github.com/pierluigiferrari/ssd_keras
分析後的代碼:https://github.com/Freshield/LEARN_detection/tree/master/a4_github_better_ssd/a2_analyze_model
目前網上基本都網絡部分講的比較多,但是真正訓練和預測部分都相對粗略,是以自己網上找了一個相對比較好的ssd檢測作為藍本來分析,然後把相應的過程用大白話給表達出來,友善大家可以更好的了解網絡。
二 到 三請看上一篇
https://zhuanlan.zhihu.com/p/122357989
四. 資料部分
資料這個部分之是以重要是因為不同于分割和分類,資料基本直接從原始資料獲得即可,最多隻是做一些資料增強。SSD的資料部分,尤其是label部分需要通過許多步驟,最終生成和預測一樣的大小,也就是(batch, 8732, 33)這樣的矩陣,而原始的資料隻是表示了分類号和相關坐标,其中轉換可見一斑。
資料主要分為原資料讀取(對原始資料轉換為二進制格式儲存),
生成器的讀取資料batchX,batchY(這部分就是構造生成器周遊從二進制格式中恢複資料),
資料增強(對圖像進行資料增強,同時最主要的是把圖像進行resize),
encoder對batchY進行處理,這部分是資料部分最重要的處理部分,把原始的batchY進行一系列處理最後編碼成預測的格式,然後就可以進行之後的loss計算。
1.原資料讀取 a.解析xml這裡以原始的SSD舉例,是使用的VOC資料集,其中包括了VOC2007以及VOC2012。這個資料集圖像為大小不一的RGB圖像,而label以xml的形式存在。一張圖像對應一個xml檔案。總共20個類别,之後如果加上背景類則一共21個類别。同時,資料集還有有一個索引檔案,表明了資料的id以及是如果配置設定trainval的資料集的。是以這裡的第一步就是要解析原資料。
首先周遊圖像路徑,label路徑和索引目錄
讀取圖像的索引
生成圖像的檔案路徑,并放入filenames清單
如果label存在則進行解析
讀取label的xml,周遊所有的objects
得到一個object的類名,檢測框,生成字典,放入這個label的boxes清單
把本label的boxes放入labels清單
如果是難例,放入eval_neutral清單(并沒有用到)
儲存資料集長度,索引并傳回
b.生成hdf5(可以想成dict)這裡之是以生成hdf5的原因是hdf5是一種二進制格式,并且支援多平台并且對分布式友好,是一種比較友善的格式,這裡要把資料都存進單獨的hdf5檔案中友善之後的訓練時使用。
首先生成原始的檔案,生成原始的key,value結構
周遊資料集的長度
讀取圖像資料,儲存圖像的shape到hdf5_image_shape,拉平圖像存到hdf5_images
如果有label,儲存shape到hdf5_label_shapes,并把label拉平存到hdf5_labels
儲存圖像索引到hdf5_image_ids
儲存難例到hdf5_eval_neutral(沒有用到)
最終資料會trainval存為一個檔案,test存為一個檔案
其中圖像資料和label資料雖然已經拉平,如果恢複回原始shape的話,如下所示
image (???, ???, 3),label [[class_id, xmin, ymin, xmax, ymax], ...],這裡需要注意,我們的網絡預測需要的是中心點長寬這種形式,而原始資料為左上右下點坐标的形式。
這裡的圖像之是以為問号是因為VOC資料集的圖像大小是不一的,這個會之後在生成器生成資料時實時resize,而這裡label的長度也是不定的,因為一張圖會有n個objects,是以最後label的shape應該是(n_objects, 5)
2.生成器的讀取資料batchX, batchY a.shuffle首先,需要把相應的圖像索引亂序排列下
b.batchX首先從hdf5中得到batch索引的圖像,然後得到batchX的圖像并恢複回原圖大小,需要注意的是這裡依然為任意大小,并非300,300,3
c.batchY通過同樣的序列從hdf5獲得對應的label矩陣,然後恢複會原來的大小,舉例的值如下:
[[3,170,110,465,294], [1,180,190,250,330]],這裡代表這張圖有兩個目标框,第一個框類别号為3,第二個框類别号為1,對應的坐标順序為xmin,ymin,xmax,ymax。這裡需要注意,我們的網絡預測需要的是中心點長寬這種形式,而原始資料為左上右下點坐标的形式。是以之後需要做相應的轉換。是以batchY為(b,n_objects,5)
3.資料增強在得到了原始的資料batchX和batchY之後,下一步是做資料增強以及資料轉換。
這部分的操作很值得借鑒,把要做的所有操作也就是transform,都是用同樣的輸入輸出,然後建立一個操作清單,把相應的增強操作按照順序放到操作清單中,這樣之後被生成器順序調用。
可以看到,這裡通過這個操作清單把相關的操作和生成器進行了解耦,生成器隻知道順序調用操作清單中的transform而不知道也無需知道都是哪些transform,隻知道如果調用相關的接口即可。
對于圖像來說有許多的資料增強操作如随機亮度,随機飽和度,随機色度等等,但是這些其實都是可選項,而且對于其他圖像如醫學圖像等,可能并不合适,這裡最重要最需要知道的就是Resize這個transform。
這個transform就像名字那樣是用來轉換尺寸的,再通過這個操作之後,圖像就統一從原始的各自不同的大小變換為(300,300,3),同時batchY由于坐标是和原圖對應的,是以當原圖的大小進行變換時,
batchY的坐标位置也會進行相應的大小。總體來說,在經過資料增強之後,我們得到batchX(b,300,300,3),batchY也是對應的相應的位置,shape依然是(b, n_objects, 5)
4.encoder對batchY進行處理在進行資料增強後,便是SSD的資料部分最重要的地方了,也就是對batchY進行處理。回想下我們的網絡預測是把(b,300,300,3)的輸入經過網絡輸出為(b,8732,33)這樣的矩陣,這也就是我們的y_pred,那麼相應的label資料也就是我們的y_true或者說是batchY也需要是同樣的(b,8732,33)的大小,這一步就是關于這部分如何轉換的。
這裡引入一個題外話,個人覺得SSD其實網絡部分就是一個密集采樣,效果還不錯的主要原因就在于資料的編碼部分,loss的分類計算部分以及預測後的decoder部分三個共同影響的結果,這幾個部分從某種意義上替代了two stages的RPN網絡。
a.生成傳回矩陣回想一下模型部分的priorbox,就像那部分最後說的,先驗框的部分其實是不需要計算可以直接生成的,是以priorbox隻要知道了資料的資訊,如feature map的大小,先驗框數量和長寬比等,就可以直接生成。
這裡我們就直接先生成預設的傳回矩陣,大小和y_pred一樣(b,8732,33),這裡的33為21+4+8,其中21為類别,4為label的坐标,8為先驗框的坐标和variance。
然後我們預設所有的先驗框都為背景類,也就是第0維為1。label的坐标部分我們先用先驗框的坐标作為預設值,其實這裡并不影響,因為到時候計算loss的時候坐标隻會計算正例也就是有非背景類的坐标,最後8個值為先驗框的坐标和variance值。
b.周遊batch并轉換資料格式我們目前的batchY為(b,n_objects,5),這裡對batch進行周遊,然後得到的label是(n_objects,5)這樣的形式。
然後我們目前雖然在resize部分進行了坐标的轉換,不過目前依然是相對于全圖的坐标,而我們的y_pred的坐标是歸一化後的坐标,是以這裡我們也先對資料進行歸一化,也就是相應的xmin,xmax除以原圖的w也就是300,ymin,ymax除以原圖的h也就是300.
在這之後,由于目前的坐标為xmin,ymin,xmax,ymax,但是我們無論計算IOU還是和y_pred計算偏移都需要cx,cy,w,h的形式,是以這裡進行相應的轉換。這個轉換其實也非常簡單,cx也就等于(xmax-xmin)//2,cy等于(ymax-ymin)//2,w等于xmax-xmin,h等于ymax-ymin這樣通過numpy的廣播可以一下就完成轉換。
c.計算IOU生成IOU矩陣iou其實翻譯過來就是交并比,如上圖這樣,把先驗框和label框的交集,除以兩個的并集。這裡注意,我們計算的iou是label和先驗框的iou,而不是預測出來的框。
另外,這裡會通過numpy的廣播一下計算一個label框和所有的8732個先驗框的iou值,代碼裡也叫similarity矩陣,其實是一個東西。
iou矩陣在資料處理編碼中主要提供了一個選擇的根據,之後的label框的生成都是根據這裡iou矩陣中不同框的iou值來進行選擇的。
這裡計算的方法是一樣的,是以下邊就以簡單的兩個框進行說明。
i.轉換我們現在有label(cx,cy,w,h),bbox(cx,cy,w,h),首先為了計算iou我們需要把表示方法從中心表示法轉換為xmin,ymin,xmax,ymax的表示法,也非常簡單就是xmin=cx-w/2,xmax=cx+w/2,ymin=cy-h/2,ymax=cy+h/2。
這樣我們就得到了變換後的label(xmin,xmax,ymin,ymax)和bbox(xmin,xmax,ymin,ymax)
ii.交集首先我們來計算交集
這是兩個框的情況,從上圖可以得出,交集的部分,也就是要求出中間的小矩形的坐标位置,然後就可以得到交集的面積。而小矩形的坐标位置的xmin,ymin其實就是兩個框中xmin,ymin的較大的那個,而小矩形的坐标位置的xmax,ymax是兩個框中xmax,ymax較小的那個,也就是:
xmin_inter=max(xmin1, xmin2)
ymin_inter=max(ymin1, ymin2)
xmax_inter=min(xmax1, xmax2)
ymax_inter=min(ymax1, ymax2)
然後交集的面積就等于(xmax_inter-xmin_inter) * (ymax_inter-ymin_inter)
那麼這是一個框的情況,具體到我們的網絡部分,我們有label(n_objects, 4),
bbox(8732, 4)(這裡相當于把先驗框的坐标部分提取出來),那麼首先就是對兩邊進行拓展,變成同樣的大小,對于label進行tile後,變為label(n_objects, 8732, 4),這裡的tile操作可以了解為複制。然後對于bbox也進行tile,變為(n_objects, 8732, 4)。
注意這裡的4為xmin,ymin,xmax,ymax于是先對label,bbox的第0,1維求max得到xmin_inter,ymin_inter,然後對label,bbox的第2,3維求min得到xmax_inter,ymax_inter。再讓第2維減第0維得到xmax_inter-xmin_inter,用第3維減第1維得到ymax_inter-ymin_inter,最後兩個結果相乘就得到了intersection部分(n_objects, 8732)。
iii.并集并集的計算相對于交集就簡單許多,并集相當于box1的面積加上box2的面積減去交集的面積也就是box1_area+box2_area-intersection。
box1的面積為(xmax1-xmin1) * (ymax1-ymin1)
box2的面積為(xmax2-xmin2) * (ymax2-ymin2)
那麼對于我們的矩陣label(n_objects, 4)和bbox(8732, 4)計算如下:
由于numpy可以友善的廣播特性,是以這裡可以先分别對label和bbox使用上邊的次元計算,得到label(n_objects)和bbox(8732),這兩個現在相當于是label每個框的面積,bbox每個框的面積。然後使用使用tile把label拓展為(n_objects, 8732),把bbox拓展為(n_objects, 8732),直接進行label+bbox-intersection就可以得到并集union(n_objects, 8732)
最後根據公式iou=intersection/union,就可以得到了iou矩陣。
通過numpy廣播對label矩陣(n_objects, 5)計算iou得到全部的iou矩陣
這裡很有意思的是這是一個矩陣,大小為(n_objects, 8732),其實就代表着,每個object的label框和8732所有的先驗框的iou值。
d.貪心二分比對這個名字聽起來雖然唬人,但是其實目的很單純,就是要找出每個label的框最比對的那個bbox。而這裡的難點在于,如果兩個label框都和一個bbox比對,需要找出最大的框,然後再比對剩下的。
整體流程是找出我們剛才的iou矩陣(n_objects, 8732)中整個矩陣最比對的也就是iou值最高的位置,然後把再在iou矩陣除了這個label框和這個bbox框剩下的矩陣中找尋最比對的,直到找完所有label框最比對的,而這裡的trick是複制一個iou矩陣,因為之後還有繼續使用iou矩陣,把複制後的iou矩陣中最比對的找出來,然後橫向把這個label一行都置零,縱向把這個bbox框都置零,就是一個大十字的零,再找下一個最大的,再置零,可以想象,這樣一共會重複n_objects次,得到n_objects個框。
具體的流程如下:首先複制iou矩陣 -> 生成label的索引數也就是從0到n_objects -> 周遊n_objects次 -> 找到各label最大的iou索引 -> 比較各label最大的iou值得到最大的iou的label的索引 -> 儲存iou值和label的相關資訊到matches清單 -> 置零這個label行和這個bbox的列 -> 繼續重複找到iou最大的label與i及它的iou最大索引
最終傳回matches清單 -> 把傳回矩陣相應的bbox填上label的class的one_hot矩陣和label的坐标矩陣,并把iou矩陣相應框的位置置零,注意這裡隻置零單獨的框,也就是一共置零n_objects個框。
e.match multi得到所有大于iou門檻值的label最比對的bbox需要注意的是,這裡是要得到所有大于iou門檻值的label比對的bbox,可能會有人覺得既讓貪心二分比對得到的都是iou最大的,這裡所有大于iou門檻值的的框,那貪心二分比對豈不是多此一舉。其實這裡重要的點在于所有大于門檻值的label,貪心二分比對主要解決的是當所有的框都小于iou門檻值時,我仍會給所有label框找到一個先驗框進行比對,然後進行反向傳播來進行學習,這尤其在最開始訓練時尤其重要。如果沒有貪心二分比對算法的話,那麼可能就一直沒有大于iou門檻值的框,然後就沒有label,就所有loss都為0,就沒有辦法學習了。
整體流程為:生成bbox的索引清單[0,1,...,8731],找到iou矩陣(n_objects, 8732)每個bbox最比對的label索引也就是從列找到最大值的索引(8732,),找到bbox最比對的iou的值這裡是找相應的位置的值(8732,)。這裡的操作主要是為了把bbox比對到iou值最大的label,這樣先從列上找出每個bbox應該比對那個label,然後在把相應的iou值都提取出來好進行iou值門檻值處理。
然後找到所有iou大于門檻值的bbox索引
找到這些iou大于門檻值的label索引,這裡得到label的索引主要是為了之後得到這些label的class的one_hot矩陣
根據bbox的索引,在傳回矩陣中的相應bbox位置填入label的class的one_hot矩陣和label的坐标位置。
把bbox的索引在iou矩陣中的相應位置置零
f.去除iou過高的反例上邊的時候iou門檻值一般會設定為0.5,那麼我們有理由相信,當iou門檻值在0.3到0.5之間的先驗框有可能會幹擾我們的正例學習,是以這裡會找到超過(去除iou的門檻值)也就是0.3并小于(match multi的iou門檻值)也就是0.5之間的先驗框。
不過理論上其實一般不會影響太大,是以真正代碼這裡的去除iou門檻值也為0.5,是以并沒有真正的使用。
g.對batchY的坐标編碼在這之前我們已經選出了我們要的框,最後一步就是對所有的batchY進行編碼。我們的y_encoded為(b,8732,21+4+8),其中這裡的4+4+4分别時(cx,cy,w,h)gt,(cx,cy,w,h)anchor以及variacne的v0,v1,v2,v3。這裡的gt部分也就是ground truth也就是label部分,除了我們找出來的相應的和label對應上的框外,其他也就是背景也就是反例的gt這裡和anchor是一樣的,但是這沒有什麼問題,因為在計算loss的時候,坐标回歸部分我們隻會計算正例的部分。
坐标這裡就像我們在模型部分所說的那樣,我們的網絡并不能直接cx,cy,w,h,是以我們需要對這些換算成相對坐标和相對大小,也就是lx,ly,lw,lh。
簡單的了解我們的編碼,就是要得到真實的框的中心點,相對于先驗框的平移的距離,然後真是的框的長寬相對于先驗框的縮放比例。
另外,這裡的編碼方法其實和faster rcnn如出一轍。
lx=(cx(gt)-cx(anchor))/w(anchor),這裡的意思的就是真實label的中心點cx相對于先驗框的中心點cx的偏移量,相對于先驗框的w的比例大小。
這裡加上variance後,最終的lx=(cx(gt)-cx(anchor))/w(anchor)/v0,這裡v0為0.1,也就是相當于擴大了10倍,這樣在計算loss的時候也相當于擴大了10倍,反向傳播的時候梯度也擴大了10倍,就可以讓網絡更好的進行學習。
相對的ly=(cy(gt)-cy(anchor))/h(anchor)/v1
對于lw和lh的計算公式和cx,cy稍有不同
lw=(log(w(gt)/w(anchor)))/v2,抛開後邊為了增大梯度的v2,前邊的lw相應于label對bbox的寬的比值,這裡使用log主要是兩個原因:
首先我們考慮下log的曲線
我們這裡a是大于1的,可以看出随着x的增加曲線越加的平緩,也就是說如果label和anchor的比例過大是可以起到平緩的作用,這對于不同目标的大小尺寸有一定的作用,考慮下小目标的長寬比就相對較小,而大目标的長寬比就會相對較大,而使用了log之後,可以平衡這中間的差别。這就類似yolo網絡中,編碼部分使用的是w**0.5是類似的道理。
第二個是當我們預測時,需要把這裡的編碼變成解碼,我們預測的w=exp(lw x v2)x w(anchor),那麼我們看下exp函數的樣子
這裡可以看出exp有一個我們在預測寬度時非常需要的一個特性,也就是非負性。因為我們預測的寬度不可能是一個負數。
同理,這裡有lh=(log(h(gt)/h(anchor)))/v3
這裡代碼部分直接使用numpy選擇相應的次元進行運算即可,最終傳回y_encoded(b,8732,21+4+8)