Нейроаутентификация: введение в биометрическую аутентификацию

в 18:57, , рубрики: deep learning, python, Алгоритмы, аутентификация, защита информации, информационная безопасность, искусственный интеллект, машинное обучение

Нейроаутентификация: введение в биометрическую аутентификацию - 1

Привет. Решил написать о применении нейронных сетей в совсем не традиционной для них сфере: аутентификация. Это лежит вне задач машинного обучения, и то от чего в ML пытаются избавиться — тут поощряется.

Минимум теории — максимум практики.

Заинтересовался? Тогда добро пожаловать под кат.

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

В общем виде биометрическая аутентификация происходит в несколько этапов:

I. Этап обучения.
1) Считывание биометрических данных
2) Преобразование и нормализация данных
3) Обучение модели
II. Этап использования
1) Считывание биометрических данных
2) Преобразование и нормализация данных
3) Классификация входного вектора на два класса: 0 — запретить, 1 — разрешить.

Сама фишка в третьих пунктах. Поэтому, на данный момент опустим считывание биометрических данных ( это задача специальных устройств ) и перейдем к сферическому коню в вакууме:

Пусть сферический конь в вакууме имеет отпечаток в виде строки с длиной в диапазоне от 0 до 255. Наша задача будет в аутентификации сферического коня. Учтем, что отпечаток коня может изменяться в зависимости от разных факторов, так что надо учесть погрешность.

Сводим задачу биометрической аутентификации к задаче бинарной классификации

Да-да, вам не послышалось. Что вообще аутентификация? Это ответ на входные данные либо разрешить авторизацию, либо нет.

Поэтому наша задача сводится к классификации входного вектора значений на 0 (запретить) или 1 ( разрешить ). Напомню, если кто не помнит/не знает — для binary classification идеально подходят Feed-Forward NN с полносвязными слоями.

Использовать я буду Keras, как самое простое для проектирования нейронных сетей. Полный код будет в конце.

Функция создания модели будет выглядеть так:

def build_model(x):
    model = models.Sequential()
    model.add(layers.Dense(64, activation='relu', input_shape=(255,)))

    model.add(layers.Dense(64, activation='relu'))
    model.add(layers.Dense(1, activation='sigmoid'))
    
    opt = optimizers.Adam()
    model.compile(optimizer=opt,
              loss='binary_crossentropy',
              metrics=['accuracy'])
    return model

Мы задали модель, которой на вход подается 2D тензор размерностью (None, 255) ( по нотации TensorFlow). Другими словами, массив векторов с биометрическими характеристиками.

У модели два скрытых слоя с 64 нейронами в каждом и relu-активацией ( relu(x) = max(0,x) )

На выходе 1 нейрон с сигмоидной активацией. В итоге получаем значение [0;1]. Отклонение от 1 и будет нашей погрешностью, которая будет иметь порог после которого значение будем принимать за 0. Оптимизатором выберем адам, просто потому что мне он нравится.

Кто не знаком с принципами нечеткой логикой — там вместо true false используются значения между 0 и 1, а приведение к четкой логике происходит через дефаззификацию — специальные функции

В качетсве loss-функции — binary crossentropy, эта функция возвращает ошибку классификации. Задача оптимизатора адама будет в том, чтобы подстроить веса нейронной сети так, чтобы минимизировать значение loss-функции

Нормализация данных

Итак, мы условились, что отпечаток сферического коня — это строка произвольного размера, меньше 256 символов. Следовательно, нам надо входную строку нормализовать к 255-му вектору. Проще всего взять байты строки и дописать к ним нулевые, чтобы получилось 255 байт. Но из-за специфики обучения это будет не-комильфо, ибо нейронная сеть начнет возвращать для строк «похожей» длины очень близкое значение к 1, из-за поганых нулей. Поэтому мы просто повторим строку n-раз до размера 255, а далее поделим каждый байт на 255.

Зачем делить? ASCII строка кодируется байтами [0;255], нам же надо привести входные данные к [0;1], поэтому и делим на верхнюю границу.

def GetString():
    def f(inp, i):
        if (len(inp) - 1) > i:
            return inp[i]
        else:
            return f(inp, i - len(inp))
    input_value = bytearray()
    input_value.extend(map(ord, str(input("Passphrase> "))))
    assert len(input_value) <= 255, 'Maximum string length exeeded'
    return np.reshape(np.array([f(input_value, i) for i in range(255)], dtype=np.float32) / 255., (1, 255))

Функция возвращает введенную строку нормализованную в 255-мерный вектор, где каждый элемент лежит в диапазоне от 0 до 1. Обратите внимание на (1, 255) — это матрица 1x255 — аналогично обычному вектору, но в numpy имеет особое значение именно такое его представление, так как в будущем мы будем эти вектора объединять в 2D тензор по вертикальному axis.

Обучающая выборка

Как по фразе про объединение вы уже, наверное, догадались, нам нужны еще примеры для обучения, которые не будут являться отпечатками регистрируемого пользователя ( коня ). Для этого мы сгенерим рандомные уже нормализованные последовательности и соединим их с отпечатком.

def GetTrainTensor(input_value):
    x = np.append(np.random.uniform(size=(1000, 255)),
                  input_value,
                  axis=0)
    y = np.append(np.array( [[0] * (x.shape[0] - 1)], dtype=np.float32), [1])
    return x, np.reshape(y, (y.shape[0], 1))

В данном коде мы создаем матрицу размером 1000x255 ( 1000 255-мерных векторов ) с равномерно распределенными значениями [0;1], соединяем с отпечатком и получаем тензор, который передадим в модель для обучения. Она состоит из 1000 векторов не являющимися отпечатком юзера и одним таковым.

В этом же коде мы создаем labels для наших отпечатков размерностью 1001x1, очевидно, первая тысяча — нули и только одна единица.

Обучение

А вот тут то начинаются полные расхождения с принятыми в ML практиками. Дело в том, что обычные классификаторы тренируются на обучающей выборке, валидируются на валидирующей и окончательная проверка на тестовой. Тут будет только обучающая выборка. Да! А самое необычное, что так ненавидимый overfitting в нейронных сетях будет как раз основной нашей фишкой. Нам надо достигнуть такого уровня overfitting при котором модель меморизирует наш отпечаток и будет возвращать значения близкие к нему только при самых незначительных расхождениях. Для этого мы не будем разбивать на batch, возьмем 1к эпох и запустим.

Другими словами — в классическим задачах НС используется для генерализации ( generalization ), а у нас для меморизации ( fitting ). Наша задача достигнуть не высокого уровня генерализации, а заставить нейронку работать как словарь, который сопоставляет ключи со значениями, но принимает неточные ключи. Это и будет нашей аутентификацией с погрешностью, основной в биометрических системах.

train_x, train_y = GetTrainTensor(GetString())
model = build_model(train_x)
model.fit(train_x,
                    train_y,
                    epochs=1000,
                    )

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

Сразу скажу, изредка на некоторых отпечатках НС не сходится, поэтому надо делать динамический подбор глобальных параметров, но об этом в следующих частях, пока сойдет и так. Не смотря на то, что уже после 500-й эпохи у нас accuracy 1, нам нужен наименьший loss, для достаточного overfitting.

Passphrase> password_konyasha_v_vacuume
Epoch 1/1000
1001/1001 [==============================] - 0s - loss: 0.0712 - acc: 0.9870
Epoch 2/1000
1001/1001 [==============================] - 0s - loss: 0.0082 - acc: 0.9990
... а тут уже используем модель обученную

Epoch 998/1000
1001/1001 [==============================] - 0s - loss: 1.0002e-07 - acc: 1.0000
Epoch 999/1000
1001/1001 [==============================] - 0s - loss: 1.0002e-07 - acc: 1.0000
Epoch 1000/1000
1001/1001 [==============================] - 0s - loss: 1.0002e-07 - acc: 1.0000

Passphrase> password_konyasha_v_vacuume
1.0

Passphrase> paSsword_konyasha_v_vacuume
0.999857

Passphrase> password_koNyaSHa_v_vacuume
0.999999

Passphrase> pasSwOrD
3.85486e-16

Passphrase> password_KonAyASha_v_vaaacuuume
4.14147e-15

Passphrase> passw0rd_KoNyAsHa_V_vacuum3
1.0

Passphrase> test test
2.29619e-11

Passphrase> nothing
4.83392e-20

Passphrase> blablabla_konyashka_hehe
2.20884e-21

Видим, что исходное значение возвращает полное соответствие, небольшие мутации отклонения ( иногда 100% уверенность, что тоже самое ). Совершенно чужие отпечатки дают значения близкие к нулю.

Заключение

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

Всем спасибо за внимание.

P.S. А код то забыл ;)

Код

import numpy as np
from keras import models
from keras import layers
from keras import optimizers
import matplotlib.pyplot as plt


def GetString():
    def f(inp, i):
        if (len(inp) - 1) > i:
            return inp[i]
        else:
            return f(inp, i - len(inp))
    input_value = bytearray()
    input_value.extend(map(ord, str(input("Passphrase> "))))
    assert len(input_value) <= 255, 'Maximum string length exeeded'
    return np.reshape(np.array([f(input_value, i) for i in range(255)], dtype=np.float32) / 255., (1, 255))
    
def GetTrainTensor(input_value):
    x = np.append(np.random.uniform(size=(1000, 255)),
                  input_value,
                  axis=0)
    y = np.append(np.array( [[0] * (x.shape[0] - 1)], dtype=np.float32), [1])
    return x, np.reshape(y, (y.shape[0], 1))
                       
                        
train_x, train_y = GetTrainTensor(GetString())

def build_model(x):
    model = models.Sequential()
    model.add(layers.Dense(64, activation='relu', input_shape=(255,)))

    model.add(layers.Dense(64, activation='relu'))
    model.add(layers.Dense(1, activation='sigmoid'))
    
    opt = optimizers.Adam()
    model.compile(optimizer=opt,
              loss='binary_crossentropy',
              metrics=['accuracy'])
    return model

model = build_model(train_x)
model.fit(train_x,
                    train_y,
                    epochs=1000,
                    )

for i in range(20):
    print(model.predict(GetString())[0][0])

Автор: SolidMinus

Источник

Поделиться

* - обязательные к заполнению поля