PyTorch練手項目四:孿生網絡(Siamese Network)


本文目的:展示基於PyTorch,如何利用孿生網絡進行人臉驗證的過程。

1 孿生網絡(Siamese Network)

孿生網絡主要用來衡量兩個輸入的相似程度。孿生神經網絡有兩個輸入(Input1 and Input2),將兩個輸入feed進入兩個神經網絡(Network1 and Network2),這兩個神經網絡分別將輸入映射到新的空間,形成輸入在新的空間中的表示(Representation)。通過Loss的計算,評價兩個輸入的相似度。具體可參考

孿生網絡實際上相當於只有一個網絡,因為兩個神經網絡(Network1 and Network2)結構權值均相同。如果兩個結構或權值不同,就叫偽孿生神經網絡(pseudo-siamese network)。

孿生網絡的loss有多種選擇:

  • Contrastive Loss(傳統的Siamese使用);
  • Triplet loss(詳見 Deep metric learning using Triplet network);
  • Softmax loss:將問題轉換成二分類問題,即將兩個輸出的絕對差值映射到一個結點上;
  • 其他損失,比如cosine loss,exp function,歐氏距離等等。

2 項目代碼

本文基於ORL人臉數據集,利用孿生網絡來進行人臉驗證(Face Verification)。

項目代碼主要可以分為四塊內容:

  • 前奏:導入相關庫,定義一些參數以及輔助函數;
  • 准備數據:准備數據集,並包裝成dataset以及dataloader;
  • 准備模型:構建模型,自定義損失函數;
  • 訓練:訓練並繪制結果;
  • 測試:直觀查看模型效果。

2.1 前奏

先導入相關庫,並定義相關函數和參數。

import torch
import torchvision
import torch.nn as nn
from torch import optim
import torch.nn.functional as F
import torchvision.transforms as transforms
from torch.utils.data import DataLoader,Dataset
import matplotlib.pyplot as plt
import torchvision.utils
import numpy as np
import random
from PIL import Image
import PIL.ImageOps 
print(torch.__version__)  #1.1.0
print(torchvision.__version__)  #0.3.0


#定義一些超參
train_batch_size = 32        #訓練時batch_size
train_number_epochs = 50     #訓練的epoch

def imshow(img,text=None,should_save=False): 
    #展示一幅tensor圖像,輸入是(C,H,W)
    npimg = img.numpy() #將tensor轉為ndarray
    plt.axis("off")
    if text:
        plt.text(75, 8, text, style='italic',fontweight='bold',
            bbox={'facecolor':'white', 'alpha':0.8, 'pad':10})
    plt.imshow(np.transpose(npimg, (1, 2, 0))) #轉換為(H,W,C)
    plt.show()    

def show_plot(iteration,loss):
    #繪制損失變化圖
    plt.plot(iteration,loss)
    plt.show()

2.2 准備數據

2.2.1 ORL人臉數據集

ORL人臉數據集共包含40個不同人的400張圖像,是在1992年4月至1994年4月期間由英國劍橋的Olivetti研究實驗室創建。此數據集下包含40個目錄,每個目錄下有10張圖像,每個目錄表示一個不同的人。所有的圖像是以PGM格式存儲,灰度圖,圖像大小寬度為92,高度為112。對每一個目錄下的圖像,這些圖像是在不同的時間、不同的光照、不同的面部表情(睜眼/閉眼,微笑/不微笑)和面部細節(戴眼鏡/不戴眼鏡)環境下采集的。所有的圖像是在較暗的均勻背景下拍攝的,拍攝的是正臉(有些帶有略微的側偏)。

下載的數據集中training文件夾包含37個人物的圖像,其余3個人的圖像放在testing文件夾中,留給后續測試時候使用。

2.2.2 自定義Dataset和DataLoader

自定義的Dataset需要實現 __ getitem __ 和 __ len __ 函數。每次讀取一對圖像,標簽表示差異度,0表示同一個人,1表示不是同一人。

#自定義Dataset類,__getitem__(self,index)每次返回(img1, img2, 0/1)
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) #37個類別中任選一個
        should_get_same_class = random.randint(0,1) #保證同類樣本約占一半
        if should_get_same_class:
            while True:
                #直到找到同一類別
                img1_tuple = random.choice(self.imageFolderDataset.imgs) 
                if img0_tuple[1]==img1_tuple[1]:
                    break
        else:
            while True:
                #直到找到非同一類別
                img1_tuple = random.choice(self.imageFolderDataset.imgs) 
                if img0_tuple[1] !=img1_tuple[1]:
                    break

        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)
    
    
    
#定義文件dataset
training_dir = "./data/faces/training/"  #訓練集地址
folder_dataset = torchvision.datasets.ImageFolder(root=training_dir)

#定義圖像dataset
transform = transforms.Compose([transforms.Resize((100,100)), #有坑,傳入int和tuple有區別
                                transforms.ToTensor()])
siamese_dataset = SiameseNetworkDataset(imageFolderDataset=folder_dataset,
                                        transform=transform,
                                        should_invert=False)

#定義圖像dataloader
train_dataloader = DataLoader(siamese_dataset,
                            shuffle=True,
                            batch_size=train_batch_size)

2.2.3 可視化數據集

當然,我們通常要先看看數據具體啥樣。實際運行中這一段代碼可省略。

vis_dataloader = DataLoader(siamese_dataset,
                        shuffle=True,
                        batch_size=8)
example_batch = next(iter(vis_dataloader)) #生成一批圖像
#其中example_batch[0] 維度為torch.Size([8, 1, 100, 100])
concatenated = torch.cat((example_batch[0],example_batch[1]),0) 
imshow(torchvision.utils.make_grid(concatenated, nrow=8))
print(example_batch[2].numpy())

注意torchvision.utils.make_grid用法:將若干幅圖像拼成一幅圖像。內部機制是鋪成網格狀的tensor,其中輸入tensor必須是四維(B,C,H,W)。后續還需要調用numpy()和transpose(),再用plt顯示。

# https://pytorch.org/docs/stable/_modules/torchvision/utils.html#make_grid
torchvision.utils.make_grid(tensor, nrow=8, padding=2, normalize=False, range=None, scale_each=False, pad_value=0)

#示例
t = torchvision.utils.make_grid(concatenated, nrow=8)
concatenated.size()  #torch.Size([16, 1, 100, 100])
t.size() #torch.Size([3, 206, 818]) 對於(batch,1,H,W)的tensor,重復三個channel,詳見官網文檔源碼

2.3 准備模型

自定義模型和損失函數。

#搭建模型
class SiameseNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        self.cnn1 = nn.Sequential(
            nn.ReflectionPad2d(1),
            nn.Conv2d(1, 4, kernel_size=3),
            nn.ReLU(inplace=True),
            nn.BatchNorm2d(4),
            
            nn.ReflectionPad2d(1),
            nn.Conv2d(4, 8, kernel_size=3),
            nn.ReLU(inplace=True),
            nn.BatchNorm2d(8),

            nn.ReflectionPad2d(1),
            nn.Conv2d(8, 8, kernel_size=3),
            nn.ReLU(inplace=True),
            nn.BatchNorm2d(8),
        )

        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
    
    
#自定義ContrastiveLoss
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, 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

2.4 訓練

net = SiameseNetwork().cuda() #定義模型且移至GPU
criterion = ContrastiveLoss() #定義損失函數
optimizer = optim.Adam(net.parameters(), lr = 0.0005) #定義優化器

counter = []
loss_history = [] 
iteration_number = 0


#開始訓練
for epoch in range(0, train_number_epochs):
    for i, data in enumerate(train_dataloader, 0):
        img0, img1 , label = data
        #img0維度為torch.Size([32, 1, 100, 100]),32是batch,label為torch.Size([32, 1])
        img0, img1 , label = img0.cuda(), img1.cuda(), label.cuda() #數據移至GPU
        optimizer.zero_grad()
        output1,output2 = net(img0, img1)
        loss_contrastive = criterion(output1, output2, label)
        loss_contrastive.backward()
        optimizer.step()
        if i % 10 == 0 :
            iteration_number +=10
            counter.append(iteration_number)
            loss_history.append(loss_contrastive.item())
    print("Epoch number: {} , Current loss: {:.4f}\n".format(epoch,loss_contrastive.item()))
    
show_plot(counter, loss_history)

2.5 測試

現在用testing文件夾中3個人物的圖像進行測試,注意:模型從未見過這3個人的圖像。

#定義測試的dataset和dataloader

#定義文件dataset
testing_dir = "./data/faces/testing/"  #測試集地址
folder_dataset_test = torchvision.datasets.ImageFolder(root=testing_dir)

#定義圖像dataset
transform_test = transforms.Compose([transforms.Resize((100,100)), 
                                     transforms.ToTensor()])
siamese_dataset_test = SiameseNetworkDataset(imageFolderDataset=folder_dataset_test,
                                        transform=transform_test,
                                        should_invert=False)

#定義圖像dataloader
test_dataloader = DataLoader(siamese_dataset_test,
                            shuffle=True,
                            batch_size=1)


#生成對比圖像
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(x0.cuda(),x1.cuda())
    euclidean_distance = F.pairwise_distance(output1, output2)
    imshow(torchvision.utils.make_grid(concatenated),'Dissimilarity: {:.2f}'.format(euclidean_distance.item()))



2.6 小結:

  • 孿生網絡的本質、Loss和應用場景
  • 如何使用torchvision.datasets.ImageFolder來自定義dataset
  • transforms.Resize()函數有坑,傳入int和tuple,處理方法不一樣
  • torchvision.utils.make_grid()處理機制

Reference


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM