LIVENESS DETECTION — проверка идентификатора на принадлежность «живому» пользователю

от автора

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

Хотим познакомить вас с технологией liveness detection, в задачу которой входит проверка идентификатора на принадлежность «живому» пользователю.

Датасет можно скачать по ссылке.

Для обучения в датасете  есть 4 подкласса.

  • real — «живое» лицо

  • replay — кадры с видео

  • printed — распечатанная фотография

  • 2dmask — надетая 2d маска

Каждый образец представлен последовательностью из 5 картинок.

Строим модель

Для решения задачи классификации изображений на принадлежность «живому» пользователю будем обучать нейронную сеть, используя фреймворк pytorch.

Решение строится на работе с последовательностью картинок, а не с каждой картинкой отдельно. Используем небольшую претренированную сеть Resnet18 на каждую картинку из последовательности. Затем стакаем полученные фичи и применяем 1d свертку и далее fully connected слой на 1 класс.

Таким образом, наша архитектура выглядит следующим образом:

class Empty(nn.Module):     def __init__(self):         super(Empty, self).__init__()      def forward(self, x):         return x class SpoofModel(nn.Module):     def __init__(self):         super(SpoofModel, self).__init__()         self.encoder = torchvision.models.resnet18()         self.encoder.fc = Empty()         self.conv1d = nn.Conv1d(             in_channels=5,             out_channels=1,             kernel_size=(3),             stride=(2),             padding=(1))         self.fc = nn.Linear(in_features=256, out_features=1)      def forward(self, x):         vectors = []         for i in range(0, x.shape[1]):             v = self.encoder(x[:, i])             v = v.reshape(v.size(0), -1)             vectors.append(v)         vectors = torch.stack(vectors)         vectors = vectors.permute((1, 0, 2))         vectors = self.conv1d(vectors)         x = self.fc(vectors)         return x

Для примера мы будем тренировать нашу модель 5 эпох с батч сайзов 64, что займёт примерно 1 час с учетом валидации на одной 2080TI.

На валидации смотрим  3 метрики: f1, accuracy и f2 score.

Код для валидации:

def eval_metrics(outputs, labels, threshold=0.5):     return {         'f1': f1_score(y_true=labels, y_pred=(outputs > threshold).astype(int), average='macro'),         'accuracy': accuracy_score(y_true=labels, y_pred=(outputs > threshold).astype(int)),         'fbeta 2': fbeta_score(labels,  y_pred=(outputs > threshold).astype(int), beta=2, average='weighted'),         'f1 weighted': f1_score(y_true=labels, y_pred=(outputs > threshold).astype(int), average='weighted')     }  def validation(model, val_loader):     model.eval()     metrics = []     batch_size = val_loader.batch_size     tq = tqdm(total=len(val_loader) * batch_size, position=0, leave=True)     with torch.no_grad():         for i, (inputs, labels) in enumerate(val_loader):             inputs = inputs.cuda()             labels = labels.cuda()             outputs = model(inputs).view(-1)             tq.update(batch_size)             metrics.append(eval_metrics(outputs.cpu().numpy(), labels.cpu().numpy()))         metrics_mean = mean_metrics(metrics)     tq.close()     return metrics_mean

В качестве оптимайзера используем SGD c learning rate = 0.001, а в качестве loss BCEWithLogitsLoss.

Не будем использовать экзотических аугментаций. Делаем только Resize и RandomHorizontalFlip для изображений при обучении.

Полный код функции для тренировки:

Итоговый ход тренировки выглядит так:def train():     path_data = 'data/'     checkpoints_path = 'model'     num_epochs = 5     batch_size = 64     val_batch_size = 32     lr = 0.001     weight_decay = 0.0000001     model = SpoofModel()     model.train()     model = model.cuda()     epoch = 0     if os.path.exists(os.path.join(checkpoints_path, 'model_.pt')):         epoch, model = load_model(model, os.path.join(checkpoints_path, 'model_.pt'))     optimizer = torch.optim.SGD(model.parameters(), lr=lr, weight_decay=weight_decay)     criterion = torch.nn.BCEWithLogitsLoss()     path_images = []      for label in ['2dmask', 'real', 'printed', 'replay']:         videos = os.listdir(os.path.join(path_data, label))         for video in videos:             path_images.append({                 'path': os.path.join(path_data, label, video),                 'label': int(label != 'real'),                 })     split_on = int(len(path_images) * 0.7)     train_paths = path_images[:split_on]     val_paths = path_images[split_on:]     train_transform = torchvision.transforms.Compose([         torchvision.transforms.ToPILImage(),         torchvision.transforms.Resize(224),         torchvision.transforms.RandomHorizontalFlip(),         torchvision.transforms.ToTensor(),         torchvision.transforms.Normalize(             [0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])     val_transform = torchvision.transforms.Compose([         torchvision.transforms.ToPILImage(),         torchvision.transforms.Resize(224),         torchvision.transforms.ToTensor(),         torchvision.transforms.Normalize(             [0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])     train_dataset = AntispoofDataset(paths=train_paths, transform=train_transform)     train_loader = DataLoader(dataset=train_dataset,                               batch_size=batch_size,                               shuffle=True,                               num_workers=8,                               drop_last=True)      val_dataset = AntispoofDataset(paths=val_paths, transform=val_transform)     val_loader = DataLoader(dataset=val_dataset,                             batch_size=val_batch_size,                             shuffle=True,                             num_workers=8,                             drop_last=False)     tq = None     try:         for epoch in range(epoch, num_epochs):             tq = tqdm(total=len(train_loader) * batch_size, position=0, leave=True)             tq.set_description(f'Epoch {epoch}, lr {lr}')             losses = []             for inputs, labels in train_loader:                 inputs = inputs.cuda()                 labels = labels.cuda()                 optimizer.zero_grad()                 with torch.set_grad_enabled(True):                     outputs = model(inputs)                     loss = criterion(outputs.view(-1), labels.float())                     loss.backward()                     optimizer.step()                     optimizer.zero_grad()                     tq.update(batch_size)                     losses.append(loss.item())                 intermediate_mean_loss = np.mean(losses[-10:])                 tq.set_postfix(loss='{:.5f}'.format(intermediate_mean_loss))             epoch_loss = np.mean(losses)             epoch_metrics = validation(model, val_loader=val_loader)             tq.close()             print('\nLoss: {:.4f}\t Metrics: {}'.format(epoch_loss, epoch_metrics))             save_model(model, epoch, checkpoints_path, name_postfix=f'e{epoch}')     except KeyboardInterrupt:         tq.close()         print('\nCtrl+C, saving model...')         save_model(model, epoch, checkpoints_path)

Итоговый ход тренировки выглядит так:

В качестве модели для проверки используем веса с 3 эпохи.

Для проверки у нас есть 10 примеров. Построим confusion matrix:

На 10 примерах мы достигли 100% точности. Конечно, для идеальной проверки модели требуется  данных значительно больше.

Таким образом, в своей статье я предложил один из вариантов реализации liveness detection с помощью классификации изображений нейронной сетью. Полный код размещен по ссылке

ссылка на оригинал статьи https://habr.com/ru/post/539496/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *