探索使用對比損失的孿生網絡進行圖像相似性比較
簡介
在計算機視覺領域,準確地測量圖像相似性是一項關鍵任務,具有廣泛的實際應用。從圖像搜索引擎到人臉識別系統和基于內容的推薦系統,有效比較和查找相似圖像的能力非常重要。Siamese網絡與對比損失結合,為以數據驅動方式學習圖像相似性提供了強大的框架。在這篇博文中,我們將深入了解Siamese網絡的細節,探討對比損失的概念,并探討這兩個組件如何共同工作以創建一個有效的圖像相似性模型。
Siamese神經網絡是一類旨在比較和測量輸入樣本對之間相似性的神經網絡架構。術語“Siamese”來源于網絡體系結構包含兩個相同結構且共享相同權重集的孿生神經網絡的概念。每個網絡處理來自配對的輸入樣本之一,并通過比較它們的輸出來確定兩個輸入之間的相似性或不相似性。
什么是Siamese神經網絡
Siamese網絡的主要動機是學習輸入樣本的有意義的表示,以捕捉它們的相似性比較所需的基本特征。這些網絡在直接使用標記示例進行訓練有限或困難的任務中表現出色,因為它們可以學會區分相似和不相似的實例,而無需顯式類標簽。Siamese網絡的架構通常包括三個主要組件:共享網絡、相似度度量和對比損失函數:
(1) 共享網絡:共享網絡是Siamese架構的核心組件。它負責從輸入樣本中提取有意義的特征表示。共享網絡包含神經單元的層,例如卷積層或全連接層,用于處理輸入數據并生成固定長度的embedding向量。通過在孿生網絡之間共享相同的權重,模型學會為相似的輸入提取相似的特征,從而實現有效的比較。
(2) 相似性度:一旦輸入由共享網絡處理,就會使用相似性度量來比較生成的embedding,并測量兩個輸入之間的相似性或不相似性。相似度度量的選擇取決于特定任務和輸入數據的性質。常見的相似性度量包括歐氏距離、余弦相似度或相關系數。相似性度量量化了embedding之間的距離或相關性,并提供了輸入樣本之間相似性的度量。
(3) 對比損失函數:為了訓練Siamese網絡,采用了對比損失函數。對比損失函數鼓勵網絡為相似的輸入生成距離更近的embedding,而為不相似的輸入生成距離更遠的embedding。當相似對之間的距離超過一定閾值或不相似對之間的距離低于另一個閾值時,對比損失函數對模型進行懲罰。對比損失函數的確切制定取決于所選的相似性度量和相似對與不相似對之間的期望邊際。
在訓練過程中,Siamese網絡學會優化其參數以最小化對比損失,并生成能夠有效捕捉輸入數據的相似性結構的判別性embedding。
對比損失函數
對比損失是Siamese網絡中常用于學習輸入樣本對之間相似性或不相似性的損失函數。它旨在以這樣一種方式優化網絡的參數,即相似的輸入具有在特征空間中更接近的embedding,而不相似的輸入則被推到更遠的位置。通過最小化對比損失,網絡學會生成能夠有效捕捉輸入數據的相似性結構的embedding。
為了詳細了解對比損失函數,讓我們將其分解為其關鍵組件和步驟:
(1) 輸入對:對比損失函數作用于輸入樣本對,其中每對包含一個相似或正例和一個不相似或負例。這些對通常在訓練過程中生成,其中正例對代表相似實例,而負例對代表不相似實例。
(2) embedding:Siamese網絡通過共享網絡處理每個輸入樣本,為配對中的兩個樣本生成embedding向量。這些embedding是固定長度的表示,捕捉輸入樣本的基本特征。
(3) 距離度量:使用距離度量,如歐氏距離或余弦相似度,來衡量生成的embedding之間的不相似性或相似性。距離度量的選擇取決于輸入數據的性質和任務的具體要求。
(4) 對比損失計算:對比損失函數計算每對embedding的損失,鼓勵相似對具有更小的距離,而不相似對具有更大的距離。對比損失的一般公式如下:L = (1 — y) * D2 + y * max(0, m — D)
其中:
- L:對于一對的對比損失。
- D:embedding之間的距離或不相似性。
- y:標簽,指示配對是否相似(相似為0,不相似為1)。
- m:定義不相似性閾值的邊際參數。
損失項 `(1 — y) * D2` 對相似對進行懲罰,如果它們的距離超過邊際(m),則鼓勵網絡減小它們的距離。項 `y * max(0, m — D)2` 對不相似對進行懲罰,如果它們的距離低于邊際,則推動網絡增加它們的距離。
(5) 損失的匯總:為了獲得整個輸入對批次的整體對比損失,通常會對所有對之間的個體損失進行平均或求和。匯總方法的選擇取決于特定的訓練目標和優化策略。
通過通過梯度下降優化方法(例如反向傳播和隨機梯度下降)最小化對比損失,Siamese網絡學會生成能夠有效捕捉輸入數據的相似性結構的判別性embedding。對比損失函數在訓練Siamese網絡中發揮著關鍵作用,使其能夠學習可用于各種任務,如圖像相似性、人臉驗證和文本相似性的有意義表示。對比損失函數的具體制定和參數可以根據數據的特性和任務的要求進行調整。
在 PyTorch 中的孿生神經網絡
1. 數據集創建
我們使用的數據集來自來自 :
http://vision.stanford.edu/aditya86/ImageNetDogs/
def copy_files(source_folder,files_list,des):
for file in files_list:
source_file=os.path.join(source_folder,file)
des_file=os.path.join(des,file)
shutil.copy2(source_file,des_file)
print(f"Copied {file} to {des}")
return
def move_files(source_folder,des):
files_list=os.listdir(source_folder)
for file in files_list:
source_file=os.path.join(source_folder,file)
des_file=os.path.join(des,file)
shutil.move(source_file,des_file)
print(f"Copied {file} to {des}")
return
def rename_file(file_path,new_name):
directory=os.path.dirname(file_path)
new_file_path=os.path.join(directory,new_name)
os.rename(file_path,new_file_path)
print(f"File renamed to {new_file_path}")
return
folder_path=r"C:\Users\sri.karan\Downloads\images1\Images\*"
op_path_similar=r"C:\Users\sri.karan\Downloads\images1\Images\similar_all_images"
tmp=r"C:\Users\sri.karan\Downloads\images1\Images\tmp"
op_path_dissimilar=r"C:\Users\sri.karan\Downloads\images1\Images\dissimilar_all_images"
folders_list=glob.glob(folder_path)
folders_list=list(set(folders_list).difference(set(['C:\\Users\\sri.karan\\Downloads\\images1\\Images\\similar_all_images','C:\\Users\\sri.karan\\Downloads\\images1\\Images\\tmp','C:\\Users\\sri.karan\\Downloads\\images1\\Images\\dissimilar_all_images'])))
l,g=0,0
random.shuffle(folders_list)
for i in glob.glob(folder_path):
if i in ['C:\\Users\\sri.karan\\Downloads\\images1\\Images\\similar_all_images','C:\\Users\\sri.karan\\Downloads\\images1\\Images\\tmp','C:\\Users\\sri.karan\\Downloads\\images1\\Images\\dissimilar_all_images']:
continue
file_name=i.split('\\')[-1].split("-")[1]
picked_files=pick_random_files(i,6)
copy_files(i,picked_files,tmp)
for m in range(3):
rename_file(os.path.join(tmp,picked_files[m*2]),"similar_"+str(g)+"_first.jpg")
rename_file(os.path.join(tmp,picked_files[m*2+1]),"similar_"+str(g)+"_second.jpg")
g+=1
move_files(tmp,op_path_similar)
choice_one,choice_two=random.choice(range(len(folders_list))),random.choice(range(len(folders_list)))
picked_dissimilar_one=pick_random_files(folders_list[choice_one],3)
picked_dissimilar_two=pick_random_files(folders_list[choice_two],3)
copy_files(folders_list[choice_one],picked_dissimilar_one,tmp)
copy_files(folders_list[choice_two],picked_dissimilar_two,tmp)
picked_files_dissimilar=picked_dissimilar_one+picked_dissimilar_two
for m in range(3):
rename_file(os.path.join(tmp,picked_files_dissimilar[m]),"dissimilar_"+str(l)+"_first.jpg")
rename_file(os.path.join(tmp,picked_files_dissimilar[m+3]),"dissimilar_"+str(l)+"_second.jpg")
l+=1
move_files(tmp,op_path_dissimilar)
我們挑選了3對相似圖像(狗品種)和3對不相似圖像(狗品種)來微調模型,為了使負樣本簡單,對于給定的錨定圖像(狗品種),任何除地面實況狗品種以外的其他狗品種都被視為負標簽。
注意: “相似圖像” 意味著來自相同狗品種的圖像被視為正對,而“不相似圖像” 意味著來自不同狗品種的圖像被視為負對。
2.代碼解釋
- 46行:從每個狗圖像文件夾中隨機挑選了6張圖像。
- 47行:選擇的圖像被移動到一個名為 “tmp” 的文件夾中,并且由于它們來自同一狗品種文件夾,因此被重命名為 “similar_images”。
- 55行:完成所有這些后,它們被移動到 “similar_all_images” 文件夾中。
- 56、57行:類似地,為了獲得不相似的圖像對,從兩個不同的狗品種文件夾中選擇了3張圖像。
- 然后重復上述流程,以獲得不相似的圖像對并將它們移動到 “dissimilar_all_images” 文件夾中。
完成所有這些后,我們可以繼續創建數據集對象。
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from PIL import Image
import numpy as np
import random
from torch.utils.data import DataLoader, Dataset
import torch
import torch.nn as nn
from torch import optim
import torch.nn.functional as F
class ImagePairDataset(torch.utils.data.Dataset):
def __init__(self, root_dir):
self.root_dir = root_dir
self.transform = T.Compose(
[
# We first resize the input image to 256x256 and then we take center crop.
transforms.Resize((256,256)),
transforms.ToTensor()
]
)
self.image_pairs = self.load_image_pairs()
def __len__(self):
return len(self.image_pairs)
def __getitem__(self, idx):
image1_path, image2_path, label = self.image_pairs[idx]
image1 = Image.open(image1_path).convert("RGB")
image2 = Image.open(image2_path).convert("RGB")
# Convert the tensor to a PIL image
# image1 = functional.to_pil_image(image1)
# image2 = functional.to_pil_image(image2)
image1 = self.transform(image1)
image2 = self.transform(image2)
# image1 = torch.clamp(image1, 0, 1)
# image2 = torch.clamp(image2, 0, 1)
return image1, image2, label
def load_image_pairs(self):
image_pairs = []
# Assume the directory structure is as follows:
# root_dir
# ├── similar
# │ ├── similar_image1.jpg
# │ ├── similar_image2.jpg
# │ └── ...
# └── dissimilar
# ├── dissimilar_image1.jpg
# ├── dissimilar_image2.jpg
# └── ...
similar_dir = os.path.join(self.root_dir, "similar_all_images")
dissimilar_dir = os.path.join(self.root_dir, "dissimilar_all_images")
# Load similar image pairs with label 1
similar_images = os.listdir(similar_dir)
for i in range(len(similar_images) // 2):
image1_path = os.path.join(similar_dir, f"similar_{i}_first.jpg")
image2_path = os.path.join(similar_dir, f"similar_{i}_second.jpg")
image_pairs.append((image1_path, image2_path, 0))
# Load dissimilar image pairs with label 0
dissimilar_images = os.listdir(dissimilar_dir)
for i in range(len(dissimilar_images) // 2):
image1_path = os.path.join(dissimilar_dir, f"dissimilar_{i}_first.jpg")
image2_path = os.path.join(dissimilar_dir, f"dissimilar_{i}_second.jpg")
image_pairs.append((image1_path, image2_path, 1))
return image_pairs
dataset = ImagePairDataset(r"/home/niq/hcsr2001/data/image_similarity")
train_size = int(0.8 * len(dataset))
test_size = len(dataset) - train_size
train_dataset, test_dataset = torch.utils.data.random_split(dataset, [train_size, test_size])
batch_size = 32
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
在上述代碼的第8到10行:對圖像進行預處理,包括將圖像調整大小為256。我們使用批量大小為32,這取決于您的計算能力和 GPU。
#create the Siamese Neural Network
class SiameseNetwork(nn.Module):
def __init__(self):
super(SiameseNetwork, self).__init__()
# Setting up the Sequential of CNN Layers
# self.cnn1 = nn.Sequential(
# nn.Conv2d(3, 256, kernel_size=11,stride=4),
# nn.ReLU(inplace=True),
# nn.MaxPool2d(3, stride=2),
# nn.Conv2d(256, 256, kernel_size=5, stride=1),
# nn.ReLU(inplace=True),
# nn.MaxPool2d(2, stride=2),
# nn.Conv2d(256, 384, kernel_size=3,stride=1),
# nn.ReLU(inplace=True)
# )
self.cnn1=nn.Conv2d(3, 256, kernel_size=11,stride=4)
self.relu = nn.ReLU()
self.maxpool1=nn.MaxPool2d(3, stride=2)
self.cnn2=nn.Conv2d(256, 256, kernel_size=5,stride=1)
self.maxpool2=nn.MaxPool2d(2, stride=2)
self.cnn3=nn.Conv2d(256, 384, kernel_size=3,stride=1)
self.fc1 =nn.Linear(46464, 1024)
self.fc2=nn.Linear(1024, 256)
self.fc3=nn.Linear(256, 1)
# Setting up the Fully Connected Layers
# self.fc1 = nn.Sequential(
# nn.Linear(384, 1024),
# nn.ReLU(inplace=True),
# nn.Linear(1024, 32*46464),
# nn.ReLU(inplace=True),
# nn.Linear(32*46464,1)
# )
def forward_once(self, x):
# This function will be called for both images
# Its output is used to determine the similiarity
# output = self.cnn1(x)
# print(output.view(output.size()[0], -1).shape)
# output = output.view(output.size()[0], -1)
# output = self.fc1(output)
# print(x.shape)
output= self.cnn1(x)
# print(output.shape)
output=self.relu(output)
# print(output.shape)
output=self.maxpool1(output)
# print(output.shape)
output= self.cnn2(output)
# print(output.shape)
output=self.relu(output)
# print(output.shape)
output=self.maxpool2(output)
# print(output.shape)
output= self.cnn3(output)
output=self.relu(output)
# print(output.shape)
output=output.view(output.size()[0], -1)
# print(output.shape)
output=self.fc1(output)
# print(output.shape)
output=self.fc2(output)
# print(output.shape)
output=self.fc3(output)
return output
def forward(self, input1, input2):
# In this function we pass in both images and obtain both vectors
# which are returned
output1 = self.forward_once(input1)
output2 = self.forward_once(input2)
return output1, output2
我們的網絡稱為 SiameseNetwork,我們可以看到它幾乎與標準 CNN 相同。唯一可以注意到的區別是我們有兩個前向函數(forward_once 和 forward)。為什么呢?
我們提到通過相同網絡傳遞兩個圖像。forward_once 函數在 forward 函數中調用,它將一個圖像作為輸入傳遞到網絡。輸出存儲在 output1 中,而來自第二個圖像的輸出存儲在 output2 中,正如我們在 forward 函數中看到的那樣。通過這種方式,我們設法輸入了兩個圖像并從我們的模型獲得了兩個輸出。
我們已經看到了損失函數應該是什么樣子,現在讓我們來編碼它。我們創建了一個名為 ContrastiveLoss 的類,與模型類一樣,我們將有一個 forward 函數。
class ContrastiveLoss(torch.nn.Module):
def __init__(self, margin=2.0):
super(ContrastiveLoss, self).__init__()
self.margin = margin
def forward(self, output1, output2, label):
# Calculate the euclidean distance and calculate the contrastive loss
euclidean_distance = F.pairwise_distance(output1, output2, keepdim = True)
loss_contrastive = torch.mean((1-label) * torch.pow(euclidean_distance, 2) +
(label) * torch.pow(torch.clamp(self.margin - euclidean_distance, min=0.0), 2))
return loss_contrastive
net = SiameseNetwork().cuda()
criterion = ContrastiveLoss()
optimizer = optim.Adam(net.parameters(), lr = 0.0005 )
按照頂部的流程圖,我們可以開始創建訓練循環。我們迭代100次并提取兩個圖像以及標簽。我們將梯度歸零,將兩個圖像傳遞到網絡中,網絡輸出兩個向量。然后,將兩個向量和標簽饋送到我們定義的 criterion(損失函數)中。我們進行反向傳播和優化。出于一些可視化目的,并查看我們的模型在訓練集上的性能,因此我們將每10批次打印一次損失。
counter = []
loss_history = []
iteration_number= 0
# Iterate throught the epochs
for epoch in range(100):
# Iterate over batches
for i, (img0, img1, label) in enumerate(train_loader, 0):
# Send the images and labels to CUDA
img0, img1, label = img0.cuda(), img1.cuda(), label.cuda()
# Zero the gradients
optimizer.zero_grad()
# Pass in the two images into the network and obtain two outputs
output1, output2 = net(img0, img1)
# Pass the outputs of the networks and label into the loss function
loss_contrastive = criterion(output1, output2, label)
# Calculate the backpropagation
loss_contrastive.backward()
# Optimize
optimizer.step()
# Every 10 batches print out the loss
if i % 10 == 0 :
print(f"Epoch number {epoch}\n Current loss {loss_contrastive.item()}\n")
iteration_number += 10
counter.append(iteration_number)
loss_history.append(loss_contrastive.item())
show_plot(counter, loss_history)
我們現在可以分析結果。我們能看到的第一件事是損失從1.6左右開始,并以接近1的數字結束。看到模型的實際運行情況將是有趣的。現在是我們在模型之前沒見過的圖像上測試我們的模型的部分。與之前一樣,我們使用我們的自定義數據集類創建了一個 Siamese Network 數據集,但現在我們將其指向測試文件夾。
作為接下來的步驟,我們從第一批中提取第一張圖像,并迭代5次以提取接下來5批中的5張圖像,因為我們設置每批包含一張圖像。然后,使用 torch.cat() 水平組合兩個圖像,我們可以清楚地可視化哪個圖像與哪個圖像進行了比較。
我們將兩個圖像傳入模型并獲得兩個向量,然后將這兩個向量傳入 F.pairwise_distance() 函數,這將計算兩個向量之間的歐氏距離。使用這個距離,我們可以作為衡量兩張臉有多不相似的指標。
test_loader_one = DataLoader(test_dataset, batch_size=1, shuffle=False)
dataiter = iter(test_loader_one)
x0, _, _ = next(dataiter)
for i in range(5):
# Iterate over 5 images and test them with the first image (x0)
_, x1, label2 = next(dataiter)
# Concatenate the two images together
concatenated = torch.cat((x0, x1), 0)
output1, output2 = net(x0.cuda(), x1.cuda())
euclidean_distance = F.pairwise_distance(output1, output2)
imshow(torchvision.utils.make_grid(concatenated), f'Dissimilarity: {euclidean_distance.item():.2f}')
view raweval.py hosted with ? by GitHub
總結
Siamese 網絡與對比損失結合,為學習圖像相似性提供了一個強大而有效的框架。通過對相似和不相似圖像進行訓練,這些網絡可以學會提取能夠捕捉基本視覺特征的判別性embedding。對比損失函數通過優化embedding空間進一步增強
了模型準確測量圖像相似性的能力。隨著深度學習和計算機視覺的進步,Siamese 網絡在各個領域都有著巨大的潛力,包括圖像搜索、人臉驗證和推薦系統。通過利用這些技術,我們可以為基于內容的圖像檢索、視覺理解以及視覺領域的智能決策開啟令人興奮的可能性。