Similarità facciale con le Reti Siamesi in PyTorch
Articolo in lingua originale di Harshvardhan Gupta
Questa è la seconda parte di un articolo. Prima di continuare, dovreste leggere la parte 1.
Nella parte 1 abbiamo parlato della classe di problemi che il one shot learning mira a risolvere e come le reti siamesi siano un buon candidato per questo tipo di problemi. Abbiamo analizzato una funzione di perdita particolare che calcola la somiglianza tra una coppia di immagini.
Potete trovare tutto il codice in formato Jupyter Notebook al termine di questo articolo.
L’architettura
Utilizzeremo un’architettura standard di rete neurale convoluzionale. Utilizziamo la batch normalization dopo ciascun layer convoluzionale, seguita dal dropout.
class SiameseNetwork(nn.Module): def __init__(self): super(SiameseNetwork, self).__init__() self.cnn1 = nn.Sequential( nn.ReflectionPad2d(1), nn.Conv2d(1, 4, kernel_size=3), nn.ReLU(inplace=True), nn.BatchNorm2d(4), nn.Dropout2d(p=.2), nn.ReflectionPad2d(1), nn.Conv2d(4, 8, kernel_size=3), nn.ReLU(inplace=True), nn.BatchNorm2d(8), nn.Dropout2d(p=.2), nn.ReflectionPad2d(1), nn.Conv2d(8, 8, kernel_size=3), nn.ReLU(inplace=True), nn.BatchNorm2d(8), nn.Dropout2d(p=.2), ) self.fc1 = nn.Sequential( nn.Linear(8*100*100, 500), nn.ReLU(inplace=True), nn.Linear(500, 500), nn.ReLU(inplace=True), nn.Linear(500, 5) ) def forward_once(self, x): output = self.cnn1(x) output = output.view(output.size()[0], -1) output = self.fc1(output) return output def forward(self, input1, input2): output1 = self.forward_once(input1) output2 = self.forward_once(input2) return output1, output2
Non c’è niente di speciale in questa rete. Accetta un input di 100×100 pixel e ha 3 layer fully connected dopo i layer convoluzionali.
E dov’è l’altro Siamese?
Nell’articolo precedente vi ho mostrato una coppia di reti che processa ciascun immagine di una coppia. Tuttavia, ora vi presento una sola rete. Questo perché i pesi sono limitati ad essere identici tra le due reti, quindi utilizziamo un unico modello a cui diamo in input due immagini in successione. Dopodiché calcoliamo il valore della funzione di perdita utilizzando entrambe le immagini e proseguiamo con la backpropagation. Questo permette di risparmiare molta memoria senza alcun impatto su altre metriche, come l’accuratezza.
Funzione di perdita di paragone
Abbiamo definito la funzione di perdita come:
E abbiamo definito Dw (distanza Euclidea) come:
Gw è l’output generato dalla nostra rete per un’immagine,
La funzione di perdita di paragone in PyTorch è come segue:
Class ContrastiveLoss(torch.nn.Module): """ Contrastive loss function. Based on: http://yann.lecun.com/exdb/publis/pdf/hadsell-chopra-lecun-06.pdf """ def __init__(self, margin=2.0): super(ContrastiveLoss, self).__init__() self.margin = margin def forward(self, output1, output2, label): euclidean_distance = F.pairwise_distance(output1, output2) 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
Il dataset
Nel post precedente volevo utilizzare MNIST, tuttavia alcuni lettori mi hanno suggerito di utilizzare un esempio di somiglianza facciale di cui avevo parlato. Perciò sono passato da MNIST/OmniGlot al dataset di facce AT&T. Il dataset contiene immagini di 40 soggetti fotografati da diverse angolazioni. Ho messo da parte gli ultimi 3 soggetti per utilizzarli come test per il modello.
Caricamento dei dati
La nostra architettura richiede in input una coppia ed un’etichetta: simili/non-simili.
Perciò ho creato il mio caricatore di dati personalizzato in grado di eseguire questo task: utilizza la cartella delle immagini da cui leggere le immagini. Questo significa che potete utilizzare il dataset che preferite.
class SiameseNetworkDataset(Dataset): def __init__(self,imageFolderDataset,transform=None,should_invert=True): self.imageFolderDataset = imageFolderDataset self.transform = transform self.should_invert = should_invert def __getitem__(self,index): img0_tuple = random.choice(self.imageFolderDataset.imgs) #we need to make sure approx 50% of images are in the same class should_get_same_class = random.randint(0,1) if should_get_same_class: while True: #keep looping till the same class image is found img1_tuple = random.choice(self.imageFolderDataset.imgs) if img0_tuple[1]==img1_tuple[1]: break else: img1_tuple = random.choice(self.imageFolderDataset.imgs) img0 = Image.open(img0_tuple[0]) img1 = Image.open(img1_tuple[0]) img0 = img0.convert("L") img1 = img1.convert("L") if self.should_invert: img0 = PIL.ImageOps.invert(img0) img1 = PIL.ImageOps.invert(img1) if self.transform is not None: img0 = self.transform(img0) img1 = self.transform(img1) return img0, img1 , torch.from_numpy(np.array([int(img1_tuple[1]!=img0_tuple[1])],dtype=np.float32)) def __len__(self): return len(self.imageFolderDataset.imgs)
Il dataset delle reti siamesi genera una coppia di immagini, insieme alla propria etichetta di somiglianza (0 se TRUE e 1 se FALSE). Per evitare sbilanciamenti, mi sono assicurato che almeno la metà delle immagini fossero della stessa classe e che l’altra metà non lo fosse.
Addestrare le reti Siamesi
Il processo di addestramento segue questi step:
- La prima immagine della prima coppia di immagini viene data in input alla rete
- La seconda immagine della prima coppia viene data in input alla rete
- La funzione di perdita viene calcolata utilizzando gli output dei punti 1 e 2
- La funzione di perdita viene propagata all’indietro (backpropagation) per calcolare i gradienti
- I pesi vengono aggiornati utilizzando un ottimizzatore, Adam in questo esempio.
class SiameseNetworkDataset(Dataset): def __init__(self,imageFolderDataset,transform=None,should_invert=True): self.imageFolderDataset = imageFolderDataset self.transform = transform self.should_invert = should_invert def __getitem__(self,index): img0_tuple = random.choice(self.imageFolderDataset.imgs) #we need to make sure approx 50% of images are in the same class should_get_same_class = random.randint(0,1) if should_get_same_class: while True: #keep looping till the same class image is found img1_tuple = random.choice(self.imageFolderDataset.imgs) if img0_tuple[1]==img1_tuple[1]: break else: img1_tuple = random.choice(self.imageFolderDataset.imgs) img0 = Image.open(img0_tuple[0]) img1 = Image.open(img1_tuple[0]) img0 = img0.convert("L") img1 = img1.convert("L") if self.should_invert: img0 = PIL.ImageOps.invert(img0) img1 = PIL.ImageOps.invert(img1) if self.transform is not None: img0 = self.transform(img0) img1 = self.transform(img1) return img0, img1 , torch.from_numpy(np.array([int(img1_tuple[1]!=img0_tuple[1])],dtype=np.float32)) def __len__(self): return len(self.imageFolderDataset.imgs)
La rete è stata addestrata per 100 epoche, utilizzando Adam e un learning rate di 0.0005. Il grafico della funzione di perdita nel tempo è mostrato qua sotto:
Testare la rete
Abbiamo tenuto da parte 3 soggetti in modo da avere un test set che verrà utilizzato per valutare le performance del nostro modello. Per calcolare la somiglianza calcoliamo il Dw (equazione 1.1). La distanza corrisponde direttamente alla non-somiglianza tra le immagini di una coppia. Un valore alto di Dw indica un’alta non-somiglianza.
folder_dataset_test = dset.ImageFolder(root=Config.testing_dir) siamese_dataset = SiameseNetworkDataset(imageFolderDataset=folder_dataset_test, transform=transforms.Compose([transforms.Scale((100,100)), transforms.ToTensor() ]) ,should_invert=False) test_dataloader = DataLoader(siamese_dataset,num_workers=6,batch_size=1,shuffle=True) dataiter = iter(test_dataloader) x0,_,_ = next(dataiter) for i in range(10): _,x1,label2 = next(dataiter) concatenated = torch.cat((x0,x1),0) output1,output2 = net(Variable(x0).cuda(),Variable(x1).cuda()) euclidean_distance = F.pairwise_distance(output1, output2) imshow(torchvision.utils.make_grid(concatenated),'Dissimilarity: {:.2f}'.format(euclidean_distance.cpu().data.numpy()[0][0]))
I risultati sono abbastanza buoni. La rete è in grado di riconoscere la stessa persona anche da angolazioni diverse. Fa anche un buon lavoro nel discriminare immagini non-simili.
Conclusione
Abbiamo discusso e implementato una rete siamese per discriminare tra coppie di facce per il riconoscimento facciale. Questo è utile quando ci sono pochi (o un solo) campioni per ciascun soggetto da utilizzare come training set. Abbiamo utilizzato la funzione di perdita discriminativa in grado di addestrare questo tipo di reti neurali.
Potete trovare il codice nel mio repo: https://github.com/harveyslash/Facial-Similarity-with-Siamese-Networks-in-Pytorch