DSH(CVPR2016)
論文鏈接: Deep Supervised Hashing for Fast Image Retrieval
github上有人實現了這篇論文的代碼: DSH-pytorch,我結合其他深度哈希檢索的算法做了一個baseline的匯總,其中DSH代碼的鏈接:https://github.com/swuxyj/DeepHash-pytorch/blob/master/DSH.py
基本背景
深度哈希檢索的目標是學習出一個深度神經網絡模型。輸入圖片,這個模型可以將這個圖片轉換成“01010101111”這樣的二進制編碼,轉換后,圖像之間的語義相似性能夠通過哈希編碼的相似度來體現。比如左圖自由女神像和中間的拿破侖圖像的語義相似性很小,因此得到的哈希編碼的相似度就很小;中間的拿破侖和右邊的拿破侖圖像語義很接近,因此得到的哈希編碼相似度很大。
哈希編碼的相似度可以使用漢明距離,漢明距離是兩個相同長度的編碼對應位不同的數量,比如1011101 與 1001001 之間的漢明距離是 2,因為有兩位不一樣。如果將編碼使用+1,-1來進行表示,那么漢明距離和內積有一個關系:
$D_h(b_1,b_2)=(K-b_1^Tb_2)/2$
其中K表示編碼長度。顯然當$b_1,b_2$每一位都相等的話,$b_1^Tb_2$的內積為$K$。每有一位不同,內積就會少2,因此有這樣一個對應關系。
哈希編碼的好處是時間復雜度和空間復雜度都很小,比如,用512個浮點數表示圖片,和用48個二進制編碼表示圖片比起來,當然512位浮點數檢索的時候計算起來的開銷就很大,存儲的開銷也很大。
總體說來深度哈希檢索的目標就是讓相似的圖片特征之間盡可能近,不相似的圖片特征之間盡可能遠。當然哈希編碼不一定是針對圖片的,也有論文針對視頻音頻來編碼。相似不相似由標簽信息得到,特征則由圖片輸入進深度神經網絡來得到,最后特征之間的近和遠可以使用余弦相似度、歐氏距離、漢明距離這些來進行衡量。
算法框架
DSH算法的結構如上圖所示,輸入圖片,通過卷積神經網絡,最后得到一個k維的輸出,用這個輸出計算loss,通過loss方向傳播計算梯度,從而更新網絡的參數。中間的網絡是作者自定義的一個網絡結構,為了公平的作為baseline,我在實踐的時候統一使用了AlexNet作為baseline。
對於圖片$x_1,x_2$,數據集給定了它們的相似性y,如果y=0表示它們相似,y=1表示它們不相似。圖片$x_1,x_2$通過卷積神經網絡后,變成了$b_1,b_2$。分兩種情況討論:
- 當y=0時,圖片$x_1,x_2$相似,這個時候$b_1,b_2$之間的漢明距離$D_h(b_1,b_2)$越大,模型的誤差就越大,因此損失為
;
- 當y=1的時候,圖片$x_1,x_2$不相似,這個時候$b_1,b_2$之間的漢明距離$D_h(b_1,b_2)$越小,模型的誤差就越大,作者希望模型的漢明距離大於m,大於m的話誤差為0,小於m的話誤差就是$m-D_h(b_1,b_2)$,可以使用max函數來表示這一想法,即$max(m-D_h(b_1,b_2),0)$
將兩種情況匯總就能得到如下損失函數:
松弛操作
現在的存在一個問題,神經網絡的輸出是連續的實數而不是+1,-1這樣離散的兩個值,因此需要做一個松弛操作,即將漢明距離改成歐氏距離,再加上一個對的L1正則化,因此最終的損失函數如下圖所示:
這里的alpha就是一個超參數,需要調整的,我在實踐中發現取0.1的時候效果最好。
代碼部分
代碼的其他部分,如計算MAP,用dataloader加載數據等部分和其他的深度哈希檢索算法都基本一致,因此本篇博客主要講解損失函數計算部分。
訓練的時候使用了非對稱的思想,有點類似與ADSH那篇文章。
假定batch size=64, 編碼長度bit=12,訓練集中共有10500張圖片,那么卷積神經網絡的輸出的shape就是$64\times12$,訓練集U的shape是10500x12,表示訓練集中所有圖片的特征。論文中的損失公式是要計算兩張圖片特征的歐氏距離,首先需要兩個特征相減,然后平方求和,最后開根號,公式后面有一個平方,因此就不用開根號了,具體如下,推導如下所示:
$||b_1 - b_2||_2^2=(b_1^{(0)}-b_2^{(0)})^2+(b_1^{(1)}-b_2^{(1)})^2+……+(b_1^{(11)}-b_2^{(11)})^2$
$64\times12$的u和$10500\times12$的U要相減需要先改變它們的維度,再利用pytorch的廣播機制進行相減,u.unsqueeze(1)的shape是$64\times 1 \times 12$,U.unsqueeze(0)的shape是$1 \times 10500 \times12$,進行相減后u.unsqueeze(1) - self.U.unsqueeze(0)的shape是$64 \times 10500 \times12$,此時計算dist = (u.unsqueeze(1) - self.U.unsqueeze(0)).pow(2).sum(dim=2)就可以求的歐氏距離^2。
如果有公共的標簽就算做相似,記作y=1。 這里的標簽形如 [1 1 0 0 0 1],表示該圖片屬於第0類、第1類和第5類,假定另外一張圖片的標簽是[1 0 0 0 0 0],讓[1 1 0 0 0 1]和[1 0 0 0 0 0]計算內積,得到$1\times1+1\times0+0\times0+0\times0+0\times0+1\times0=1$,是大於0的,表明有是相似的,y應該為0。只要計算內積再判斷是否大於0,就能判斷兩張圖片是否是相似的,從而得到y值。論文中的y可以通過代碼: (y @ self.Y.t() == 0).float()進行計算。
這篇論文公式最難計算的部分就是上面歐氏距離的計算和y的計算,其他部分和其他深度檢索的代碼都差不多,具體的損失函數代碼計算如下:
class DSHLoss(torch.nn.Module): def __init__(self, config, bit): super(DSHLoss, self).__init__() self.m = 2 * bit self.U = torch.zeros(config["num_train"], bit).float().to(config["device"]) self.Y = torch.zeros(config["num_train"], config["n_class"]).float().to(config["device"])
def forward(self, u, y, ind, config): self.U[ind, :] = u.data self.Y[ind, :] = y.float() dist = (u.unsqueeze(1) - self.U.unsqueeze(0)).pow(2).sum(dim=2) y = (y @ self.Y.t() == 0).float() loss = (1 - y) / 2 * dist + y / 2 * (self.m - dist).clamp(min=0) loss1 = loss.mean() loss2 = config["alpha"] * (1 - u.sign()).abs().mean() return loss1 + loss2
完整的代碼可以參考博客開頭的鏈接。
實驗結果
代碼運行結果如下,在五個數據集上進行了測試,由於骨干網絡用的AlexNet,因此結果肯定比直接用作者原始定義的那個淺層網絡效果要好很多。
dataset | this impl. | paper |
cifar10 | 0.774 | 0.6755 |
nus_wide_21 | 0.766 | 0.5621 |
ms coco | 0.655 | - |
imagenet | 0.576 | - |
mirflickr | 0.735 | - |