Обман системы распознавания лиц с помощью состязательных атак с помощью GAN.

Это часть серии статей, которые я пишу об обмане систем распознавания лиц с помощью состязательных атак с помощью GAN.

Однако, прежде чем обманывать классификатор распознавания лиц, нам нужно создать такой классификатор для обмана. Я лично хочу построить такой, который узнает мое собственное лицо. Вместо того, чтобы обучать нейронную сеть с нуля, я могу начать с предварительно обученной сети, а затем настроить ее для распознавания моего лица. Тонкая настройка очень полезна, поскольку мы можем начать с весов модели, уже обученных в крупномасштабной базе данных лиц, а затем обновить некоторые из них, чтобы отразить новые задачи, которые мы хотим, чтобы она выполнялась. Эти веса уже умеют распознавать лица, но с той лишь разницей, что они не знают моего лица. Таким образом, обучить этой предварительно обученной модели мое собственное лицо намного проще, поскольку веса модели уже содержат большую часть необходимой информации для выполнения задачи.

Структура каталогов

project
│   README.md
│   AGN.ipynb  
│
└───data
│   │   files_sample.csv
│   └───eyeglasses
│   │
│   └───test_me
│       └───train
|           └───Adrien_Brody
|           ...
|           └───Michael_Chaykowsky
|           ...
|           └───Venus_Williams
│       └───val
|           └───Adrien_Brody
|           ...
|           └───Michael_Chaykowsky
|           ...
|           └───Venus_Williams
│   
└───models
│   │   inception_resnet_v1.py
│   │   mtcnn.py
│   └───utils

Каталог models взят из реализации Facenet PyTorch, основанной на реализации Tensorflow, указанной выше.

└───models
│   │   inception_resnet_v1.py
│   │   mtcnn.py
│   └───utils

В этот inception_resnet_v1.py файл мы будем использовать предварительно обученную модель. Модель Inception Resnet V1 предварительно обучена на VGGFace2, где VGGFace2 - это крупномасштабный набор данных для распознавания лиц, разработанный на основе поиска изображений в Google и имеет большие различия в позе, возрасте, освещении, этнической принадлежности и профессии.

Веса каждого слоя в модели имеют атрибут requires_grad, который может быть установлен на True или False. Когда вы запускаете loss.backward() в цикле обучения, эти веса обновляются, и это то, что содержит всю информацию, необходимую для выполнения прогнозов. При точной настройке сети мы замораживаем все слои до последнего сверточного блока, устанавливая для атрибутов requires_grad значение False, а затем обновляем веса только на оставшихся слоях - что интуитивно вы можете представить себе более ранние слои как содержащий информацию базового уровня, необходимую для распознавания атрибутов лица и характеристик базового уровня, поэтому мы сохраняем всю эту производительность при обновлении последних слоев, чтобы включить другое лицо (мое).

Во всех train каталогах есть 11 или 12 изображений каждого человека, а во всех val каталогах есть 4 или 5 изображений каждого человека. Michael_Chaykowsky - это каталог моего лица, где я использовал разные позы, освещение и ракурсы. Чтобы собрать эти изображения, я снимал видео с помощью стандартного iPhone в различных пространствах, а затем преобразовывал эти видео в изображения и использовал MTCNN для каждого, чтобы выполнить выравнивание лица и обрезку до подходящего размера (160 x 160 пикселей).

Импорт

from torch import nn, optim, as_tensor
from torch.utils.data import Dataset, DataLoader
import torch.nn.functional as F
from torch.optim import lr_scheduler
from torch.nn.init import *
from torchvision import transforms, utils, datasets, models
from models.inception_resnet_v1 import InceptionResnetV1
import cv2
from PIL import Image
from pdb import set_trace
import time
import copy
from pathlib import Path
import os
import sys
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from skimage import io, transform
from tqdm import trange, tqdm
import csv
import glob
import dlib
import pandas as pd
import numpy as np

Многозадачная каскадная сверточная нейронная сеть (MTCNN) для выравнивания лиц

from IPython.display import Video
Video("data/IMG_2411.MOV", width=200, height=350)

Захватите кадры видео как .png файлы и поверните / обрезайте / выровняйте.

vidcap = cv2.VideoCapture('IMG_2411.MOV')
success,image = vidcap.read()
count = 0
success = True
while success:
    cv2.imwrite(f"./Michael_Chaykowsky/Michael_Chaykowsky_{
                  format(count, '04d') }.png", image)
    success,image = vidcap.read()
    print('Read a new frame: ', success)
    count += 1

Изображения приходят повернутыми, поэтому я использую imagemagick, чтобы расположить их правой стороной вверх. Обязательно сначала brew install imagemagick. Я думаю, что есть другой способ установить библиотеку, но если я помню, это был кошмар - так что определенно рекомендую brew install.

%%!
for szFile in ./Michael_Chaykowsky/*.png
do 
    magick mogrify -rotate 90 ./Michael_Chaykowsky/"$(basename "$szFile")" ; 
done

! pip install autocrop

У Autocrop есть приятная функция, в которой они немного изменяют размер изображений лиц, и вы можете указать процентное соотношение лица. Вы можете отказаться от этого, если используете полный метод MTCNN (prefferred), но если нет, вы можете сделать это, что намного проще.

! autocrop -i ./me_orig/Michael_Chaykowsky -o ./me/Michael_Chaykowsky160 -w 720 -H 720 --facePercent 80

! pip install tensorflow==1.13.0rc1

! pip install scipy==1.1.0

Теперь, используя сценарий align_dataset_mtcnn.py из реализации Tensorflow Дэвида Сэндберга Facenet, мы можем применить его к каталогу изображений лиц.

%%!
for N in {1..4}; do \
python ~/Adversarial/data/align/align_dataset_mtcnn.py \ # tensorflow script
~/Adversarial/data/me/ \ # current directory
~/Adversarial/data/me160/ \ # new directory
--image_size 160 \
--margin 32 \
--random_order \
--gpu_memory_fraction 0.25 \
& done

Теперь у вас есть каталог со всеми вашими лицами, выровненными и обрезанными для моделирования.

Загрузить данные

Когда мы загружаем данные, мы выполняем некоторые случайные преобразования изображений для улучшения обучения. Можно попробовать разные преобразования, и вы можете попробовать разные, например

Здесь я использую случайный переворот по горизонтали. Все эти преобразования делают модель более универсальной и предотвращают переобучение.

data_transforms = {
    'train': transforms.Compose([
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'val': transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}
data_dir = 'data/test_me'
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
                                          data_transforms[x])
                  for x in ['train', 'val']}
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x],
                                              batch_size=8, 
                                             shuffle=True)
              for x in ['train', 'val']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train','val']}
class_names = image_datasets['train'].classes
class_names

Выход [1]:

['Adrien_Brody','Alejandro_Toledo','Angelina_Jolie','Arnold_Schwarzenegger','Carlos_Moya','Charles_Moose','James_Blake','Jennifer_Lopez','Michael_Chaykowsky','Roh_Moo-hyun','Venus_Williams']

def imshow(inp, title=None):
    """Imshow for Tensor."""
    inp = inp.numpy().transpose((1, 2, 0))
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    inp = std * inp + mean
    inp = np.clip(inp, 0, 1)
    plt.imshow(inp)
    if title is not None:
        plt.title(title)
    plt.pause(0.001)  # pause a bit so that plots are updated
# Get a batch of training data
inputs, classes = next(iter(dataloaders['train']))
# Make a grid from batch
out = utils.make_grid(inputs)
imshow(out, title=[class_names[x] for x in classes])

Получите предварительно обученный ResNet на наборе данных VGGFace2

from models.inception_resnet_v1 import InceptionResnetV1
print('Running on device: {}'.format(device))
model_ft = InceptionResnetV1(pretrained='vggface2', classify=False, num_classes = len(class_names))

Заморозить ранние слои

Напомним, ранее я упоминал, что мы остановимся на последнем блоке conv. Чтобы найти, где это, мы можем перебирать этот список, используя -n, -n-1, ..., пока не найдем блок.

list(model_ft.children())[-6:]

Выход [2]:

[Block8(
   (branch0): BasicConv2d(
     (conv): Conv2d(1792, 192, kernel_size=(1, 1), stride=(1, 1), bias=False)
     (bn): BatchNorm2d(192, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
     (relu): ReLU()
   )
   (branch1): Sequential(
     (0): BasicConv2d(
       (conv): Conv2d(1792, 192, kernel_size=(1, 1), stride=(1, 1), bias=False)
       (bn): BatchNorm2d(192, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
       (relu): ReLU()
     )
     (1): BasicConv2d(
       (conv): Conv2d(192, 192, kernel_size=(1, 3), stride=(1, 1), padding=(0, 1), bias=False)
       (bn): BatchNorm2d(192, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
       (relu): ReLU()
     )
     (2): BasicConv2d(
       (conv): Conv2d(192, 192, kernel_size=(3, 1), stride=(1, 1), padding=(1, 0), bias=False)
       (bn): BatchNorm2d(192, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
       (relu): ReLU()
     )
   )
   (conv2d): Conv2d(384, 1792, kernel_size=(1, 1), stride=(1, 1))
 ),
 AdaptiveAvgPool2d(output_size=1),
 Linear(in_features=1792, out_features=512, bias=False),
 BatchNorm1d(512, eps=0.001, momentum=0.1, affine=True, track_running_stats=True),
 Linear(in_features=512, out_features=8631, bias=True),
 Softmax(dim=1)]

Удалите последние слои после блока conv и поместите в layer_list.

layer_list = list(model_ft.children())[-5:] # all final layers
layer_list

Выход [3]:

[AdaptiveAvgPool2d(output_size=1),
 Linear(in_features=1792, out_features=512, bias=False),
 BatchNorm1d(512, eps=0.001, momentum=0.1, affine=True, track_running_stats=True),
 Linear(in_features=512, out_features=8631, bias=True),
 Softmax(dim=1)]

Поместите все начальные слои в nn.Sequential. model_ft теперь является моделью резака, но без окончательного линейного, объединяющего, батчнормального и сигмовидного слоев.

model_ft = nn.Sequential(*list(model_ft.children())[:-5])

Если тренировать только последние слои:

for param in model_ft.parameters():
    param.requires_grad = False

Повторно прикрепите последние 5 слоев, что автоматически установит requires_grad = True.

Этот линейный слой Linear(in_features=1792, out_features=512, bias=False) на самом деле требует написания двух пользовательских классов, что не совсем очевидно, если посмотреть на него, но если вы посмотрите на ввод / вывод данных, вы увидите, что внутри слоя есть классы Flatten и normalize. Проверить реализацию реснета на предмет изменения формы в last_linear слое.

class Flatten(nn.Module):
    def __init__(self):
        super(Flatten, self).__init__()
        
    def forward(self, x):
        x = x.view(x.size(0), -1)
        return x
class normalize(nn.Module):
    def __init__(self):
        super(normalize, self).__init__()
        
    def forward(self, x):
        x = F.normalize(x, p=2, dim=1)
        return x

Затем вы можете применить последние слои обратно к новой последовательной модели.

model_ft.avgpool_1a = nn.AdaptiveAvgPool2d(output_size=1)
model_ft.last_linear = nn.Sequential(
    Flatten(),
    nn.Linear(in_features=1792, out_features=512, bias=False),
    normalize()
)
model_ft.logits = nn.Linear(layer_list[3].in_features, len(class_names))
model_ft.softmax = nn.Softmax(dim=1)
model_ft = model_ft.to(device)
criterion = nn.CrossEntropyLoss()
# Observe that all parameters are being optimized
optimizer_ft = optim.SGD(model_ft.parameters(), lr=1e-2, momentum=0.9)
# Decay LR by a factor of *gamma* every *step_size* epochs
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)

Тренироваться

def train_model(model, criterion, optimizer, scheduler,
                num_epochs=25):
    since = time.time()
    FT_losses = []
    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0
    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)
    # Each epoch has a training and validation phase
        for phase in ['train', 'val']:
            if phase == 'train':
                model.train()  # Set model to training mode
            else:
                model.eval()   # Set model to evaluate mode
            running_loss = 0.0
            running_corrects = 0
            # Iterate over data.
            for inputs, labels in dataloaders[phase]:
                inputs = inputs.to(device)
                labels = labels.to(device)
                # zero the parameter gradients
                optimizer.zero_grad()
                # forward
                # track history if only in train
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)
                    _, preds = torch.max(outputs, 1)
                    loss = criterion(outputs, labels)
                    # backward + optimize only if in training phase
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()
                        scheduler.step()
                
                FT_losses.append(loss.item())
                # statistics
                running_loss += loss.item() * inputs.size(0)
                running_corrects += torch.sum(preds == labels.data)
            epoch_loss = running_loss / dataset_sizes[phase]
            epoch_acc = running_corrects.double() /
                         dataset_sizes[phase]
            print('{} Loss: {:.4f} Acc: {:.4f}'.format(
                phase, epoch_loss, epoch_acc))
            # deep copy the model
            if phase == 'val' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())
    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))
    print('Best val Acc: {:4f}'.format(best_acc))
    # load best model weights
    model.load_state_dict(best_model_wts)
    return model, FT_losses

Оценивать

model_ft, FT_losses = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler, num_epochs=500)
plt.figure(figsize=(10,5))
plt.title("FRT Loss During Training")
plt.plot(FT_losses, label="FT loss")
plt.xlabel("iterations")
plt.ylabel("Loss")
plt.legend()
plt.show()

Еще не все

Следите за новостями из этой серии, где я опишу, как обмануть этот классификатор, используя состязательные атаки с GAN.