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:

Equazione 1

E abbiamo definito Dw (distanza Euclidea) come:

Equazione 1.1

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.

Campioni da diverse classi
Tutti i campioni di un soggetto

 

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:

  1. La prima immagine della prima coppia di immagini viene data in input alla rete
  2. La seconda immagine della prima coppia viene data in input alla rete
  3. La funzione di perdita viene calcolata utilizzando gli output dei punti 1 e 2
  4. La funzione di perdita viene propagata all’indietro (backpropagation) per calcolare i gradienti
  5. 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:

Figura 2: funzione di perdita nel tempo. L’asse x rappresenta il numero di iterazioni.

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]))
Figura 3: Alcuni output del modello. Valori bassi indicano maggiore somiglianza, mentre valori alti indicano una somiglianza minore.

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

Share:

Contenuti
Torna in alto