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

Комментарии (12)


  1. Kolyuchkin
    22.08.2017 22:26
    +3

    Вот ссылка на диссертацию Иванова Александра Ивановича — начальника «Лаборатории биометрико-нейросетевых технологий» АО ПНИЭИ, д.т.н. А вот статьи и монография на эту тему с места его работы.
    Надеюсь, это будет Вам интересно. Желаю удачи.


    1. vitsch
      23.08.2017 20:06
      +1

      нужно выложить дисер Иванова и желательно с исходными кодами ;)


      1. SolidMinus Автор
        23.08.2017 20:12

        Кстати, да, всегда вымораживало то, что диссертации каким-то образом платные. Это же просто возмутительно…


        1. vitsch
          23.08.2017 21:33

          тогда надо просить Александра лично ;)


  1. Vasyutka
    23.08.2017 14:33

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


    1. SolidMinus Автор
      23.08.2017 18:28

      Вычитание векторов это самое популярное, да, но у него слишком неприемлемый FRR.

      Сравнение векторов обычными методами матстата вроде RMSE имеет ряд недостатков, такие как если хранить отпечатки как вектора, а не матрицами параметров нейросети — незащищенность биометрических данных, стырить легко. Хэшировать биометрию еще не умеют, т.к md5/sha256 etc не подходят из-за высокого уровня коллизий на длинных последовательностях с высоким deviation. А с матрицей весов восстановить исходный отпечаток не зная обучающую выборку очень сложно, считай что матрица параметров это уже хэш-функция. Второе — это возможность регулировать уровень overfitting'а, что позволяет легко настраивать модель на идентификацию биометрии в условиях, когда она может считаться с какими-либо искажениями.


      1. vitsch
        23.08.2017 20:03

        а на человеческом языке можнос?


        1. SolidMinus Автор
          23.08.2017 20:20

          Перевожу:
          Вычитание векторов дает частый отказ тому кому бы следовало в систему зайти, да и хранение векторов неудачное решение в плане безопасности, стырив эти данные можно сделать сколько-угодно слепков кого угодно. Если хранить все в виде матрицы весов нейронной сети, то исходный вектор с биометрией очень сложно восстановить.

          А поскольку шифрование в данном случае векторов не имеет смысла — при взломе можно будет ключ достать, а хэш функций с низким уровнем коллизий ( когда два вектора дают одинаковый хэш, например ввел пароль совершенно другой и тебя пустило на хабр ) для длинных векторов не существует ( md5/sha256 после очевидного числа байт дают коллизии, например у тебя пароль из овер 100500 символов, а он пустил по qwerty ), тем более там где байты не только в пределе печатаемых символов, а какие угодно.

          Поэтому хранение в виде матрицы весов самое оптимальное, при помощи нее через прямую передачу вектора по нейронной сети и реализуется сравнение. Два зайца одним выстрелом. А есть еще и третий заяц, который мимоходом от страха помирает — можно через изменение количества эпох легко регулировать «чувствительность» к слегка измененным биометрическим данным. Например, палец влажный, или голос хриплый.


          1. vitsch
            23.08.2017 21:44

            давайте по порядку, товарищ, как на экзамене — я спрашиваю а вы отвечаете ;)
            1) «Вычитание векторов дает частый отказ тому кому бы следовало в систему зайти» — какой такой культур-мультур-вычитание? какие вектора вычитаются? почему отказ?
            2) «да и хранение векторов неудачное решение в плане безопасности, стырив эти данные можно сделать сколько-угодно слепков кого угодно» — какой такой вектор и зачем его хранить что м стырить?
            если позволите вас вовлечь в дискуссию по этому поводу то в моей разработке нет ни первого ни второго и мне интересно м что я не понимаю?


            1. SolidMinus Автор
              23.08.2017 22:21

              1)

              а) вычитание — например, RMSE — Root mean square error ( среднее значение квадратного корня разницы квадратов каждого элемента. ). По типу обычного вычитания: a(x1, x2); b(y1, y2); a — b = c(x1 — y1; x2 — y2) — как в школе. Для сравнения двух векторов. Два одинаковых вектора при любых видах матстат сравнения дадут 0.

              b) Какие вектора? У меня в статье биометрия представляется как вектор. Оно и логично, любые данные в памяти компьютера — это вектор, даже матрицы. Вопрос в соглашении. Очевидно, под первым вектором тут подразумевается известная биометрия пользователя, и биометрия того кто пытается под ним залогиниться. RMSE используется чтобы численно описать различие двух векторов. То есть, есть вектор a и b, а есть число z которое их различие описывает.

              Пысы. Если еще в школе учитесь, или не помните, напомню, что вектора это не только стрелки ( это их геометрическое представление ), а еще и набор координат. Ближайший родственник — одномерный массив.

              c) Почему отказ? А потому что данные не всегда удается верно считать при помощи биометрических устройств. Например, если взять палец — он может быть влажный, грязный, и т.д. И RMSE между эталонным вектором и считанным даст очень большое значение, из-за чего в авторизации может быть отказано. Т.к это среднее значение, у нас может различие быть очень существенным в одном месте ( мб лаг при считывании ), а может быть в разных местах по чуть-чуть ( ну, просто палец похожий попался ) и RMSE вернет одинаковое число для обеих случаев. И тут вопрос кого он пропустить, а кого послать куда подальше)

              2) Человек выше предложил хранить не модель нейронной сети, а сами вектора. Мы на одном из этапов вычисляли такой вектор для гипотетического сферического коня. Вот можно было и остановиться — тупо сохранить его. А потом когда кому-то надо авторизироваться, как пароль сравнивать через вычитание векторов. Но такое чистое хранение дает риск, что кто-то залезет в базу данных и стырит биометрию всех пользователей, что чревато. Зная, например, вектор с биометрией и то, как он составляется — можно восстановить исходную биометрию, например папиллярный узор или радужную оболочку. В примере с нейронной сетью вектор с биометрией пользователя можно не сохранять, а хранить саму модель. Из нее восстановить биометрию будет очень трудно, или даже невозможно ( тут не берусь судить, но точно не легко )


            1. iStorm
              24.08.2017 12:39

              1. Отпечаток пальца в после сканера представляется в виде цифровой последовательности. Это и есть вектор. При каждом считывании вектор немного отличается. Чтобы определить кому он принадлежит, его нас надо сравнить с базой векторов хранящейся у нас.
              Самый простой способ сравнить два вектора, это вычесть один из другого и посмотреть насколько результат близок к нолю.
              2. Про что такое вектор написал. Зачем хранить — ну у нас же база пользователей какая то есть. Каждому сопоставлен вектор. Их и храним ))

              В случае паролей пользователей, сами пароли не хранятся. Хранится некая хэш-функция от них. md5 или sha например. Это делает невозможным, при краже базы, узнать пароли пользователей. Но хэш-функции дают равномерно распределенный результат. Т.е. для последовательностей 0000 и 0001 результаты могут отличаться не на один символ, а быть вообще разными. Например md5 для приведенных строк будет
              0000 — 4a7d1ed414474e4033ac29ccb8653d9b
              0001 — 25bbdcd06c32d477f7fa1c3e4a91b032
              Для паролей этот метод подходит т.к. юзер вводит одни и те же символы. Для биометрии нет т.к. при считывании всегда возможны расхождения в векторах. А по хэшу определить близость начальных векторов невозможно.
              Из всего этого следует что нам нужно хранить вектора в оригинальном виде. Что в случае утечки базы, позволит злоумышленнику наделать отпечатков близким к нашим векторам. В случае биометрии есть еще одна засада. Если при утечке базы не хэшированных паролей, мы можем поменять все пароли и считать новую базу чистой, то в случае с биометрией пользователи имеют те отпечатки, какие у них есть и сменить их не могут.
              Это все делает защиту биометрических данных сложной. В случае же предложенном автором, информация о биометрии сохраняется в весах нейросети. Не имея обучающей выборки, восстановить их них отпечаток довольно затруднительно.


  1. vitsch
    23.08.2017 18:18

    100% Accurate Recognition of Individual EEG Patterns for Human-Machine Interaction http://dx.doi.org/10.13140/RG.2.2.34715.87849 achieved on 109 subjects from the EEG-MMI benchmark. Advanced students can extend the method to 1,100 subjects from the sleep EEG benchmark http://doi.org/10.6084/m9.figshare.4729840 .