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



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

    Продолжение надо?

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 12
    • +3
      Вот ссылка на диссертацию Иванова Александра Ивановича — начальника «Лаборатории биометрико-нейросетевых технологий» АО ПНИЭИ, д.т.н. А вот статьи и монография на эту тему с места его работы.
      Надеюсь, это будет Вам интересно. Желаю удачи.
      • +1
        нужно выложить дисер Иванова и желательно с исходными кодами ;)
        • 0
          Кстати, да, всегда вымораживало то, что диссертации каким-то образом платные. Это же просто возмутительно…
          • 0
            тогда надо просить Александра лично ;)
      • 0
        Теперь дело за малым — считать отпечаток, или радужную оболочку глаза, или состав испражнений и преобразовать в вектор. Производить аутентификацию с погрешностью вычитать вектора друг из друга вы уже умеете.
        • 0
          Вычитание векторов это самое популярное, да, но у него слишком неприемлемый FRR.

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

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

              Поэтому хранение в виде матрицы весов самое оптимальное, при помощи нее через прямую передачу вектора по нейронной сети и реализуется сравнение. Два зайца одним выстрелом. А есть еще и третий заяц, который мимоходом от страха помирает — можно через изменение количества эпох легко регулировать «чувствительность» к слегка измененным биометрическим данным. Например, палец влажный, или голос хриплый.
              • 0
                давайте по порядку, товарищ, как на экзамене — я спрашиваю а вы отвечаете ;)
                1) «Вычитание векторов дает частый отказ тому кому бы следовало в систему зайти» — какой такой культур-мультур-вычитание? какие вектора вычитаются? почему отказ?
                2) «да и хранение векторов неудачное решение в плане безопасности, стырив эти данные можно сделать сколько-угодно слепков кого угодно» — какой такой вектор и зачем его хранить что м стырить?
                если позволите вас вовлечь в дискуссию по этому поводу то в моей разработке нет ни первого ни второго и мне интересно м что я не понимаю?
                • 0
                  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) Человек выше предложил хранить не модель нейронной сети, а сами вектора. Мы на одном из этапов вычисляли такой вектор для гипотетического сферического коня. Вот можно было и остановиться — тупо сохранить его. А потом когда кому-то надо авторизироваться, как пароль сравнивать через вычитание векторов. Но такое чистое хранение дает риск, что кто-то залезет в базу данных и стырит биометрию всех пользователей, что чревато. Зная, например, вектор с биометрией и то, как он составляется — можно восстановить исходную биометрию, например папиллярный узор или радужную оболочку. В примере с нейронной сетью вектор с биометрией пользователя можно не сохранять, а хранить саму модель. Из нее восстановить биометрию будет очень трудно, или даже невозможно ( тут не берусь судить, но точно не легко )
                  • 0
                    1. Отпечаток пальца в после сканера представляется в виде цифровой последовательности. Это и есть вектор. При каждом считывании вектор немного отличается. Чтобы определить кому он принадлежит, его нас надо сравнить с базой векторов хранящейся у нас.
                    Самый простой способ сравнить два вектора, это вычесть один из другого и посмотреть насколько результат близок к нолю.
                    2. Про что такое вектор написал. Зачем хранить — ну у нас же база пользователей какая то есть. Каждому сопоставлен вектор. Их и храним ))

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

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.