Neuroauthentication: An Introduction to Biometric Authentication



    Hi, Habr. I decided to write about the use of neural networks in a completely non-traditional sphere: authentication. This is beyond the scope of machine learning, and what ML is trying to get rid of is encouraged.

    The minimum of theory is the maximum of practice.

    Interested in? Then welcome to cat.

    I must say right away that for many terms I will write their English version, since I think that most, like me, are very new to their Russian versions.

    In general, biometric authentication takes place in several stages:

    I. Stage of training.
    1) Reading biometric data
    2) Converting and normalizing data
    3) Training model
    II. Stage of use
    1) Read biometric data
    2) Conversion and normalization of data
    3) Classification of the input vector into two classes: 0 - disable, 1 - enable.

    The chip itself in the third paragraphs. Therefore, at the moment, we omit the reading of biometric data (this is the task of special devices) and move on to the spherical horse in vacuum:

    Let the spherical horse in vacuum have an imprint in the form of a string with a length in the range from 0 to 255. Our task will be to authenticate the spherical horse. Consider that the imprint of the horse may vary depending on various factors, so we must take into account the error.

    We reduce the biometric authentication problem to the binary classification problem


    Yes, yes, you did not hear. What is authentication in general? This is a response to the input either allow authorization or not.

    Therefore, our task is to classify the input vector of values ​​by 0 (disable) or 1 (enable). Let me remind you, if someone doesn’t remember / don’t know, Feed-Forward NN with fully connected layers is ideal for binary classification.

    I will use Keras as the simplest for the design of neural networks. The full code will be at the end.

    The model creation function will look like this:

    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

    We specified a model that receives a 2D tensor of dimension (None, 255) at the input (according to the TensorFlow notation). In other words, an array of vectors with biometric characteristics.

    The model has two hidden layers with 64 neurons in each and relu activation (relu (x) = max (0, x))

    At the output, 1 neuron with sigmoid activation. As a result, we get the value [0; 1]. Deviation from 1 will be our error, which will have a threshold after which we will take the value as 0. We will select Adam as the optimizer, simply because I like it.

    Those who are not familiar with the principles of fuzzy logic - there, instead of true false, values ​​between 0 and 1 are used, and the reduction to clear logic occurs through defuzzification - special functions

    As a loss function, binary crossentropy, this function returns a classification error. The task of the adam optimizer will be to adjust the weights of the neural network so as to minimize the value of the loss function

    Data normalization


    So, we agreed that the imprint of a spherical horse is a string of arbitrary size, less than 256 characters. Therefore, we need to normalize the input string to the 255th vector. The easiest way is to take the bytes of the string and append them to zero to get 255 bytes. But due to the specifics of training, this will be non-comme il faut, because the neural network will begin to return a very close value to 1 for strings of “similar” length, due to filthy zeros. Therefore, we simply repeat the string n-times to size 255, and then divide each byte by 255.

    Why divide? ASCII string is encoded with bytes [0; 255], but we need to bring the input data to [0; 1], therefore, divide by the upper limit.

    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))
    

    The function returns the entered string normalized to a 255-dimensional vector, where each element lies in the range from 0 to 1. Pay attention to (1, 255) - this is a 1x255 matrix - similar to a regular vector, but in numpy this representation is of particular importance, since in the future we will combine these vectors into a 2D tensor along the vertical axis.

    Training sample


    As you probably guessed by the phrase about the association, we need more training examples that will not be fingerprints of the registered user (horse). To do this, we will generate randomized normalized sequences and combine them with the fingerprint.

    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))
    

    In this code, we create a matrix with a size of 1000x255 (1000 255-dimensional vectors) with uniformly distributed values ​​[0; 1], connect it with the fingerprint and get a tensor that we will pass to the model for training. It consists of 1000 vectors that are not the fingerprint of the user and one.

    In the same code, we create labels for our prints with a dimension of 1001x1, obviously, the first thousand are zeros and only one unit.

    Training


    And here then the complete discrepancy begins with the practices adopted in ML . The fact is that ordinary classifiers train on a training sample, validate on a validating and final test on a test. There will be only a training sample. Yes! And the most unusual thing is that so hated overfitting in neural networks will be just our main chip. We need to achieve a level of overfitting at which the model memorizes our fingerprint and will return values ​​close to it only with the most insignificant discrepancies. To do this, we will not break into batch, take 1k eras and run.

    In other words, in classical problems, the NS is used for generalization (generalization), and for us in memorization (fitting). Our task is not to achieve a high level of generalization, but to make the neuron work like a dictionary that maps keys to values, but accepts inaccurate keys. This will be our authentication with an error, basic in biometric systems.

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

    Testing


    I must say right away that sometimes the NS does not converge on some fingerprints, so you need to make a dynamic selection of global parameters, but more on that in the following parts, until it does. Despite the fact that after the 500th era we have accuracy 1, we need the smallest loss, for sufficient overfitting. We see that the initial value returns full compliance, small deviation mutations (sometimes 100% certainty that the same thing). Absolutely alien prints give values ​​close to zero.

    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




    Conclusion


    The time has come for the conclusion. Today we have implemented almost all the steps except collecting prints. Now it’s up to the small thing - to count the imprint, or the iris of the eye, or the composition of the stool, and convert it into a vector. You already know how to authenticate with an error.

    Thank you all for your attention.

    PS But I forgot the code;)

    The code
    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])

    Only registered users can participate in the survey. Please come in.

    Need to continue?

    • 85.7% Yes 48
    • 14.2% No 8

    Also popular now: