《CNN Image Retrieval in PyTorch: Training and evaluati-ng CNNs for Image Retrieval in PyTorch》程式碼思路解讀

真語發表於2021-06-18

       這是一個基於微調卷積神經網路的影像檢索的程式碼實現,這裡我就基於程式碼做一個實現思路的個人解讀,如果有不對的地方或者不夠詳細的地方,歡迎大家指出。

程式碼的GitHub地址:filipradenovic/cnnimageretrieval-pytorch (Commit c340540)

相關論文地址:

Fine-tuning CNN Image Retrieval with No Human Annotation,  Radenović F., Tolias G., Chum O., TPAMI 2018 [arXiv]

CNN Image Retrieval Learns from BoW: Unsupervised Fine-Tuning with Hard Examples,  Radenović F., Tolias G., Chum O., ECCV 2016 [arXiv]

 

寫在前面

        我是在2020年的4月份,為了我的本科畢設,研讀學習這份程式碼。但到現在才有空閒來把我學習的成果整理到部落格上,時間上是有些延遲的。作者在2020年12月份又更新了程式碼,釋出了新的版本(V1.2),新增了億點點細節。但這不影響我發表這篇部落格,因為這個程式碼的整體思路還是不變的,這也是這篇部落格的重點。如果這篇部落格涉及到了程式碼的細節,那就是基於它V1.1的版本,更準確地說,是基於Commit c340540的版本。

        關於我為什麼是基於程式碼的解讀,而不是基於論文的解讀。原因是我做本科的畢設時,閱讀論文的水平有限,把握不住實現的思路與過程。求助導師時,他說:可以閱讀程式碼,閱讀程式碼比閱讀論文更容易。在此,也感謝導師的指導。

       再次說明:對於資料集的一些更精妙的設計,以及對影像的白化處理等,這裡由於水平的限制,都避開不談。想要了解的同學,可以深入閱讀論文,這篇部落格並沒有對這些內容進行解讀。這篇部落格只是梳理了非常基本的程式碼思路。

       溫馨提示:這篇部落格有點長,點選右下角魔法陣上的【顯示目錄】,可以更方便導航。

 

前置知識

        想要了解這個檢索實現的思路,需要先了解一些相關的知識,有助於後續整體思路的把握。由於篇幅的關係,這個章節只會對這些知識做簡要的介紹。如果已經瞭解,可跳過這一章節。這些前置知識有:孿生網路Contrastive LossTriple Loss

孿生網路

       簡單來說,孿生網路可用來衡量兩個輸入的相似度。孿生網路的結構如下圖所示,兩個輸入經過完全一樣的神經網路,輸出為各自的高維向量表徵。得到輸入的向量特徵後,可以通過餘弦相似度計算兩個輸入的相似度,也可以計算特徵間的距離,如歐氏距離,通過loss計算,來評價模型的特徵表示效果。

       既然提到loss方程,接下來就要介紹兩個ranking loss,用於學習相對距離,相關關係,常被用於孿生網路中。

圖1  孿生網路結構

Contrastive Loss

        中文稱為【對比損失函式】,表示式如下所示。其中,d表示兩個向量的距離,例如一般是歐氏距離;y表示兩個輸入是否相似,如果相似則為1,如果不相似為0;margin是設定好的閾值,當兩個樣本的向量距離超過一定值,也就是margin,就表示這兩個樣本不相似了。

$ L=\frac{1}{2 N} \sum_{n=1}^{N} y d^{2}+(1-y) \max (\operatorname{margin}-d, 0)^{2}$

       從式子上我們可以發現,如果兩個輸入相似(即y=1),則式中只剩下  $ d^{2} $。這符合我們的直觀感受:如果兩個輸入相似,向量的距離越大,則損失越大。如果兩個輸入不相似(即y=0),則式中只剩下 $ \max (\operatorname{margin}-d, 0)^{2} $。這裡應該理解為:當兩個輸入不相似時,若向量的距離大於margin,則損失為0;若向量的距離小於margin,且距離越小,損失越大。於是優化的方向為讓相似樣本的向量特徵距離變小,讓不相似樣本的向量特徵距離超過閾值

       這裡有張經典的圖,如下圖所示,紅色虛線為相似時的曲線,藍色實線為不相似情況的曲線,橫座標為樣本間的特徵距離,橫座標上有個特殊的點是margin值,縱座標是損失值。從圖上我們也發現,相似情況下(即紅色曲線)損失值隨距離的增大而增大;不相似的情況下(即藍色曲線)損失值隨距離的縮小而減小,且距離大於等於margin時,損失為0。(這兩個曲線圈起來,後面要考的

圖2  Contrastive Loss 損失與距離的關係圖

Triple Loss 

     中文稱為【三元損失函式】,顧名思義,計算一次loss要同時輸入三元:錨點樣本(anchor),正樣本(positive),負樣本(negative),分別用$a,p,n$表示,損失函式的表示式如下所示。其中,$r_{a}, r_{p}, r_{n}$分別表示錨點樣本,正樣本,負樣本的高維向量表徵。而$\mathrm{d}(r_{a}, r_{p}), \mathrm{d}(r_{a}, r_{n}) $表示$<a,p>$之間的距離和$<a,n>$之間的距離。同樣的,這裡的m也是margin,是設定好的閾值,表示希望的$\mathrm{d}(r_{a}, r_{n})$ 與 $\mathrm{d}(r_{a}, r_{p}) $的差距。

$ L\left(r_{a}, r_{p}, r_{n}\right)=\max \left(0, m+\mathrm{d}\left(r_{a}, r_{p}\right)-\mathrm{d}\left(r_{a}, r_{n}\right)\right) $

     從上面的式子中我們可以發現,當$\mathrm{d}(r_{a}, r_{n}) $比$\mathrm{d}(r_{a}, r_{p})$大,損失就小;且差距越大,損失越小;直到差值大於margin,損失為0。相反,如果差距越小,損失越大;甚至$\mathrm{d}(r_{a}, r_{p})$比$\mathrm{d}(r_{a}, r_{n}) $還大時,損失值就很大了。

 

整體思路

訓練過程

       瞭解了前面介紹的前置知識之後,再看影像檢索的訓練過程,就會理解他的用意。

       下圖是作者在GitHub和論文中都展示的圖片,很好地表示了其核心思想。這裡我對圖中的文字做了本土化,如果想看原本的表達,再次指路GitHub或者論文。示意圖中的$ \overline{\mathbf{f}}( ) $表示影像的高維特徵向量。該圖中的上半部分描述了原始影像轉化為高維向量特徵的過程:影像經過卷積層(也即卷積神經網路,如ResNet等去掉最後一層【全連線層】),再經過池化層和$\ell_{2}$歸一化操作(即向量單位化),最終形成一個影像的固定維度的向量表示。示意圖的下半部分描述了訓練時,使用對比損失函式的情況。示意圖中的兩條Loss-dist曲線就是圖2中的拆分。

影像檢索訓練流程圖(對比損失)

圖3  影像檢索訓練示意圖

檢索過程

       對於檢索的過程,我自己畫瞭如下圖所示的示意圖。檢索過程如下:

  1. 圖片池裡的圖片轉換為列向量特徵,多個列向量特徵再拼在一起組成矩陣;
  2. 將查詢物件轉換為列向量特徵,如果有多個查詢物件同時查詢,則將它們的列向量特徵拼成矩陣。
  3. 將圖片池的特徵矩陣轉置後與查詢物件的向量特徵(即計算餘弦相似度)得到相似度的結果。這個結果中第i行,第j列元素表示的是第i個圖片池中的圖片與第j個查詢物件的相似度

影像檢索過程示意圖

圖4 影像檢索過程示意圖

歐氏距離與餘弦相似度

       看到這裡,不知道大家有沒有疑問:訓練過程,loss方程用的是Contrastive Loss或者Triple Loss 本質都是讓相似樣本的距離更近,不相似樣本的距離更遠。這裡的距離用的是歐氏距離。但實際檢索時,不是用樣本間的歐氏距離排名,而是用餘弦相似度排名。誠然,餘弦相似度計算更簡單,只要矩陣乘法運算,不需要像歐氏距離一樣計算平方。但是,從理論上來說,這樣是可行的嗎?難道向量間的歐氏距離越近,餘弦相似度越高?如果沒有這個疑問的小夥伴就跳過這part吧!

       回到我們的疑問:難道向量間的歐氏距離越近,餘弦相似度越高?這當然不是絕對的,我們可以很輕鬆舉出反例。那難度作者錯了嗎?並沒有,這個結論在一定條件下是可以成立的,那就是當向量的模長一定時,這個結論是成立的!而作者早在圖3中,就保證了這關鍵的一步,是的,就是$\ell_{2}$歸一化操作(即向量單位化)。這些向量特徵都是單位向量!

      下面是以上結論的一個證明:其實,向量間的歐氏距離和餘弦相似度由餘弦公式建立聯絡的,設兩個向量分別為 $\boldsymbol{a}$, $\boldsymbol{b}$ ,則有以下的關係。其中,$ d_{<\boldsymbol{a}, \boldsymbol{b}>} $ 表示兩個向量間的歐氏距離。由這個公式我們可以得知,兩個向量模長一定時,歐氏距離越近,餘弦相似度越高。

$ d_{<\boldsymbol{a}, \boldsymbol{b}>}^{2} = \left |\boldsymbol{a}   \right |^{2} + \left |\boldsymbol{b}  \right |^{2} - 2\boldsymbol{a}\cdot \boldsymbol{b} $

 

資料集介紹

      作者使用了 retrieval-SfM-120k 作為訓練集和驗證集。使用Oxford5kParis6kROxford5k,RParis6k作為測試集。接下來對這幾個資料集做個介紹,會涉及到具體的檔案細節

retrieval-SfM-120k

       retrieval-SfM-120k是若干張建築物的圖片,它們已經分好了簇,相似圖片在一個簇裡,不同簇的圖片即為不相似。還有若干對q-p圖片,q表示查詢,p表示相似的圖片。

文件結構

       retrieval-SfM-120k 下載解壓後目錄結構包含ims資料夾,retrieval-SfM-120k.pkl 和retrieval-SfM-120k-whiten.pkl。其中 ims 資料夾 存放圖片 。 retrieval-SfM-120k.pkl 存 放 圖 片相關資訊。retrieval-SfM-120k-whiten.pkl 的內容還不瞭解,這裡不做解釋,不影響程式碼整體的理解。

       ims 下還有三級目錄。其中圖片的檔名(程式碼中稱為 cid)的逆序的前六位決定了圖片的路徑,如某圖的 cid 逆序前六位為 123456,則其路徑為./ims/12/34/56。

       retrieval-SfM-120k.pkl 包含了這個 database 的相關資訊。其字典結構圖如圖 5 所示。其中按mode分為train和val。train和val又是兩個字典,分別包含cids,cluster,qidxs, pidxs這四個關鍵字,這四個key對應的value都是列表。其中 cids 是所有圖片的檔名列表;cluster 是由 cids 中對應圖片的 clusterID 組成的列表;qidxs 和 pidxs 是一一對應的,組成一對對 q-p對,其中 q 表示查詢物件在 cids 中的下標,p 表示與查詢物件匹配的影像在 cids的下標。而圖中的數字為該列表的元素個數。

圖5 retrieval-SfM-120k.pkl字典結構

Oxford5k, Paris6k,ROxford5k,RParis6k

       Oxford5k 由 5062 張已被人工標註的圖片組成,其中有 55 張是查詢物件,是11 個地標的五種不同拍攝條件下的圖片。類似地,Paris6k 資料集由 6412 個影像和 55 個查詢組成。而ROxford5k和RParis6k是Radenovi 等人,重新整理了Oxford5k,Paris6k這兩個資料集而成的。所做的改動如下:每個資料集新增了 15 個查詢;修改標註錯誤和資料集大小;還根據答案集的不同,設定了挑戰級別:Easy,Medium,Hard。

文件結構

        程式碼中會自動下載對應的測試集,這四個資料集下載後的文件結構比較像,以Oxford5k為例,有一個名為jpg的資料夾和一個gnd_oxford5k.pkl的檔案。

        其中,jpg的資料夾裡面存放的就是圖片,gnd_oxford5k.pkl檔案存放的是圖片的檢索資訊。

       接下來具體介紹各個gnd_xxx.pkl檔案,其中xxx是對應的資料集名稱,如oxford5k,paris6k等。這些gnd_xxx.pkl檔案存放一個了dict,其字典結構如圖6所示。其中,imlist 和 qimlist 都是列表,儲存著每張圖片或查詢物件的檔名,下面的數字是列表長度。而 gnd 也是列表,和 qimlist 列表是一一對應的,儲存了對應的查詢影像的檢索資訊。而每個gnd元素都是字典,都包含若干項關鍵字,包含的關鍵字如圖6所示。其中 bbx 內有 4 個元素,為int型別:x1,y1,x2,y2, 表示了查詢圖片的具體查詢區域。而ok,junk是與查詢物件匹配與不匹配的圖片在 imlist 的索引列表,easy,hard,junk也是針對查詢物件的匹配程度分出來的圖片在 imlist 的索引列表,根據這三個列表,可以把檢索分為三個難度:Easy,Medium,Hard。

圖6  gnd_xxx.pkl 字典結構

正負集劃分

        如果是 Oxford5k 和 Paris6k,正類就是ok列表,負類就是junk列表。而對於 ROxford5k 和 RParis6k,會分成三個難度:Easy(E),Medium(M),Hard(H),這三個難度下的正負類劃分不太一樣,具體見表 1。

表1  ROxford5k,RParis6k正負類的劃分表
  E M H
正類 easy easy, hard hard
負類 hard,  junk junk junk, easy

 

具體實現細節

       這一章節會涉及到具體的實現細節。這一章節提到的過程,都是預設引數下的過程,減少了許多可選操作的說明,如白化操作等。

  • 模型                        AlexNet,Vgg16,ResNet101等經典模型去掉全連線層作為卷積層,再加上一層池化操作和$\ell_{2}$正則化操作。其中池化可以是最大值池化,平均值池化和廣義平均值池化(數學上,廣義平均值也就是p次冪平均)。
  • 資料庫的選擇          訓練集:retrieval-SfM-120k['train'],驗證集:retrieval-SfM-120k['val'],測試集:Oxford5k, Paris6k, ROxford5k, RParis6k。
  • 訓練時模型的輸入   訓練集中的圖片通過模型變成特徵向量。從中選取qsize(q-p對的個數)個元組。每個元組共有(1+1+nnum)個特徵向量,分別是查詢物件q,正類p和nnum個負類n1,n2....查詢和正類是由q-p對直接給出。負類是q由當前模型的在圖片池中的查詢結果,按照查詢順序從上到下依次選取nnum個與q在不同簇的圖片,且這nnum個圖片也在不同的簇中。(注:這些元組在不同的epoch就會更新一次,因為模型更新了。)這裡呼應了論文標題中的No Human Annotation(不需要人工標註)。
  • 訓練時模型的輸出   每個元組經過模型的向量特徵組成的矩陣(矩陣維度:特徵維度*(1+1+nnum) )
  • 訓練的標籤             [-1,1,0,0,...,0]。-1代表查詢物件,1表示匹配,0表示不匹配。與輸入的元組的每個特徵向量一一對應。
  • 損失函式                 如果是Contrastive Loss,每個元組的loss是nnum個負類與查詢物件的Contrastive Loss 和 nnum個相同的正類與查詢物件的Contrastive Loss 的和;如果是Triple Loss,每個元組的loss是nnum個(查詢物件,正類,其中一個負類)的Triple Loss 的和。他們的d都是用向量的歐氏距離定義的。
  • 訓練的優化             採用Adam演算法優化,學習率隨著epoch指數衰減,公式為: $ l r=l r_{0} \times \gamma^{\text {epoch }} $
  • 測試時模型的輸入  測試集中相簿的圖片和查詢物件的圖片
  • 測試時模型的輸出  查詢物件的特徵矩陣(所有查詢物件的特徵向量組成的矩陣)和相簿圖片特徵矩陣(相簿圖片所有的特徵向量組成的矩陣)
  • 測試的檢索排名      相簿圖片特徵矩陣與查詢物件特徵矩陣的點乘,得到的是scores矩陣(維度:相簿圖片數量* 查詢數量),其中第i行,第j列表示圖片池中的第i個圖片與第j個查詢物件的相似度得分。ranks是scores的按列排序的索引值,即得分高的圖片的索引排在前面,是最終的檢索結果。
  • 檢索的評價指標      Oxford5k 和 Paris6k 的檢索結果的指標是 mAP(mean Average Precision),AP 是單個查詢結果的平均準確率,mAP 是所有查詢結果 AP 的平均。ROxford5k 和 RPairs6k 的檢索指標比較豐富。除了mAP 用於評價整體的檢索質量外,新增了 mP@k,是結果列表中 top-k 檢索結果的準確率指標,反映了搜尋引擎的質量。匹配的圖片排的越前面得分會越高,不匹配的圖片越排在匹配的後面得分會越高。

 

檔案目錄

     程式碼的檔案結構及其說明如下所示。

.
│  LICENSE
│  README.md
│  
└─cirtorch                                                         
    │  __init__.py
    │  
    ├─datasets                                                          資料集載入和處理
    │      datahelpers.py                                                   圖片處理方法
    │      genericdataset.py                                              定義通過檔名列表載入圖片的方法
    │      testdataset.py                                                    定義生成測試集的方法
    │      traindataset.py                                                   定義生成訓練集和驗證集的方法
    │      __init__.py
    │      
    ├─examples                                                        包含所有可執行的檔案               
    │      test.py                                                                 測試模型(相比e2e版本,增加許多可選操作)
    │      test_e2e.py                                                          端到端測試模型
    │      train.py                                                                訓練模型
    │      __init__.py
    │      
    ├─layers                                                             定義神經網路裡的層操作方法
    │      functional.py                                                      定義以下三個檔案要用的函式
    │      loss.py                                                                定義損失函式
    │      normalization.py                                                定義正則化方法
    │      pooling.py                                                          定義池化方法
    │      __init__.py
    │      
    ├─networks                                                        定義所用的模型 
    │      imageretrievalnet.py                                          定義模型,初始化模型,定義通過模型生成特徵的方法                  
    │      __init__.py
    │      
    └─utils                                                                包含所有工具檔案
            download.py                                                       下載各個資料集
            download_win.py                                                未知,沒怎麼用到
            evaluate.py                                                          定義計算檢索評價指標的方法
            general.py                                                            定義通用的工具方法
            whiten.py                                                             定義白化方法
            __init__.py

 

後記

        關於訓練和測試的程式碼執行,GitHub上都有相關的指導,這裡就不多贅述。這篇文章旨在解讀程式碼,幫助初入門的同學快速掌握基本思想。對於資料集的一些更精妙的設計,以及對影像的白化處理等,這裡由於水平的限制,都避開不談,想要了解的同學,可以深入閱讀論文,這篇部落格並沒有對這些內容進行解讀。如果文中有錯誤的地方,歡迎大傢俬信或評論。

相關文章