Привет. Решил написать о применении нейронных сетей в совсем не традиционной для них сфере: аутентификация. Это лежит вне задач машинного обучения, и то от чего в 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