Обман системы распознавания лиц с помощью состязательных атак с помощью 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.