{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Fingerprint Authentication\n", "\n", "This tutorial demonstrates how to use a machine learning model to generate a unique signature from a grayscale image of a fingerprint. The generated signature can then be compared against previously generated signatures stored in flash memory to authenticate users." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Demo Video\n", "\n", "The following is a video of the demo described in this tutorial:\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Quick Links\n", "\n", "- [GitHub Source](https://github.com/SiliconLabs/mltk/blob/master/mltk/tutorials/fingerprint_authentication.ipynb) - View this tutorial on Github\n", "- [Run on Colab](https://colab.research.google.com/github/siliconlabs/mltk/blob/master/mltk/tutorials/fingerprint_authentication.ipynb) - Run this tutorial on Google Colab\n", "- [C++ Example Application](../../docs/cpp_development/examples/fingerprint_authenticator.md) - View this tutorial's associated C++ example application\n", "- [Machine Learning Model](../../docs/python_api/models/siliconlabs/fingerprint_signature_generator.md) - View this tutorial's associated machine learning model" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Overview\n", "\n", "### Objectives\n", "\n", "After completing this tutorial, you will have:\n", "1. A better understanding of how machine learning may be used to generate unique signatures\n", "2. The tools needed to create a fingerprint dataset\n", "3. All of the tools needed to develop your own signature generation machine learning model\n", "4. A working demo to authenticate fingerprints\n", "\n", "### Content\n", "\n", "This tutorial is divided into the following sections:\n", "1. [Overview of how to generate a unique signature using machine learning](#signature-generation-machine-learning-model-overview)\n", "2. [Creating the dataset](#creating-the-dataset)\n", "3. [Creating the model](#creating-the-model)\n", "4. [Evaluating the model](#evaluating-the-model)\n", "5. [Running the model](#running-the-model)\n", "\n", "### Running this tutorial from a notebook\n", "\n", "For documentation purposes, this tutorial was designed to run within a [Jupyter Notebook](https://jupyter.org). \n", "The notebook can either run locally on your PC _or_ on a remote server like [Google Colab](https://colab.research.google.com/notebooks/welcome.ipynb). \n", "\n", "- Refer to the [Notebook Examples Guide](../../docs/guides/notebook_examples_guide.md) for more details\n", "- Click here: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/siliconlabs/mltk/blob/master/mltk/tutorials/fingerprint_authentication.ipynb) to run this tutorial interactively in your browser\n", "\n", "__NOTE:__ Some of the following sections require this tutorial to be running locally with a supported embedded platform connected.\n", "\n", "\n", "### Running this tutorial from the command-line\n", "\n", "While this tutorial uses a [Jupyter Notebook](https://jupyter.org), \n", "the recommended approach is to use your favorite text editor and standard command terminal, no Jupyter Notebook required. \n", "\n", "See the [Standard Python Package Installation](https://siliconlabs.github.io/mltk/docs/installation.html#standard-python-package) guide for more details on how to enable the `mltk` command in your local terminal.\n", "\n", "In this mode, when you encounter a `!mltk` command in this tutorial, the command should actually run in your local terminal (excluding the `!`)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Required Hardware\n", "\n", "Some parts of the tutorial requires a supported development board and the [R503 Fingerprint Module](https://www.adafruit.com/product/4651).\n", "\n", "See the [Hardware Setup](https://siliconlabs.github.io/mltk/docs/cpp_development/examples/fingerprint_authenticator.html#hardware-setup) section of the Fingerprint Authenticator C++ application for details on how to connect the fingerprint module to the development board. \n", "\n", "__NOTE:__ Only the fingerprint module needs to be connected to the development board. You do _not_ need to build the C++ application from source for this tutorial." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Install MLTK Python Package\n", "\n", "Before using the MLTK, it must first be installed. \n", "See the [Installation Guide](../../docs/installation.md) for more details." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!pip install --upgrade silabs-mltk" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "All MLTK modeling operations are accessible via the `mltk` command. \n", "Run the command `mltk --help` to ensure it is working. \n", "__NOTE:__ The exclamation point `!` tells the Notebook to run a shell command, it is not required in a [standard terminal](https://siliconlabs.github.io/mltk/docs/installation.html#standard-python-package)" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Usage: mltk [OPTIONS] COMMAND [ARGS]...\n", "\n", " Silicon Labs Machine Learning Toolkit\n", "\n", " This is a Python package with command-line utilities and scripts to aid the\n", " development of machine learning models for Silicon Lab's embedded platforms.\n", "\n", "Options:\n", " --version Display the version of this mltk package and\n", " exit\n", " --install-completion [bash|zsh|fish|powershell|pwsh]\n", " Install completion for the specified shell.\n", " --show-completion [bash|zsh|fish|powershell|pwsh]\n", " Show completion for the specified shell, to\n", " copy it or customize the installation.\n", " --help Show this message and exit.\n", "\n", "Commands:\n", " build MLTK build commands\n", " classify_audio Classify keywords/events detected in a...\n", " classify_image Classify images detected by a camera...\n", " commander Silab's Commander Utility\n", " compile Compile a model for the specified...\n", " custom Custom Model Operations\n", " evaluate Evaluate a trained ML model\n", " fingerprint_reader View/save fingerprints captured by the...\n", " profile Profile a model\n", " quantize Quantize a model into a .tflite file\n", " run_model_profiler_benchmarks Build and run the model profiler...\n", " summarize Generate a summary of a model\n", " train Train an ML model\n", " tse_compress Perform compression of all weights in a...\n", " update_params Update the parameters of a previously...\n", " utest Run the all unit tests\n", " view View an interactive graph of the given...\n", " view_audio View the spectrograms generated by the...\n" ] } ], "source": [ "!mltk --help" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Signature Generation Machine Learning Model Overview\n", "\n", "Before continuing with this tutorial, it is recommended to review the [MLTK Overview](../../docs/overview.md), which provides an overview of the core concepts used by the this tutorial.\n", "\n", "While classification (e.g. predicting if an image contains a cat, dog, or goat) is a common usecase for embedded machine learning, another useful application of machine learning is signature generation.\n", "For example, given a grayscale image of someone's fingerprint, generate a sequence of numbers that are unique to the fingerprint; different images of the same fingerprint should generate a nearly identical sequence of numbers while a different person's fingerprint should generate a different sequence of numbers. The sequence of numbers is called the __signature__ and machine learning is used to create the signature generator.\n", "Two signatures are considered similar if their [euclidean distance](https://en.wikipedia.org/wiki/Euclidean_distance) is below a certain threshold.\n", "\n", "This is illustrated as follows: \n", "![](../../docs/img/fingerprint_signature_overview.png)\n", "\n", "\n", "__NOTE:__ While this tutorial uses grayscale images of fingerprints, many other sample types (audio, accelerometer, etc.) could theoretically be used as well.\n", "\n", "\n", "### Siamese Networks\n", "\n", "The [Siamese Network](https://en.wikipedia.org/wiki/Siamese_neural_network) machine learning model architecture is used to generate the signatures.\n", "\n", "> Siamese Networks are neural networks which share weights between two or more sister networks,\n", "each producing embedding vectors of its respective inputs. In supervised similarity learning, the networks are then trained to maximize the contrast (distance) between embeddings of inputs of different classes, \n", "while minimizing the distance between embeddings of similar classes, resulting in embedding spaces that reflect the class segmentation of the training inputs. [[1]](https://keras.io/examples/vision/siamese_contrastive/)\n", "\n", "A siamese network can be illustrated as follows: \n", "![](https://miro.medium.com/max/700/1*0E9104t29iMBmtvq7G1G6Q.png) \n", "[Siamese network used in Signet](https://arxiv.org/abs/1707.02131)\n", "\n", "There are several things to note about this diagram: \n", "- The top and bottom blocks of the diagram share the _same_ weights and parameters\n", "- The top and bottom blocks are called a \"tower\" (so the model has two towers)\n", "- Only one of the towers is needed to generate the signature\n", "- The last layer of the tower is a __fully connected__ layer, the output of this layer is the __signature__ generated from the model input\n", "- The number of units (aka neurons) in the last fully connected layer determines the since of the generated signature\n", "\n", "\n", "Refer to the following links for additional information about siamese networks:\n", "- [Image similarity estimation using a Siamese Network with a contrastive loss](https://keras.io/examples/vision/siamese_contrastive/)\n", "- [A friendly introduction to Siamese Networks](https://towardsdatascience.com/a-friendly-introduction-to-siamese-networks-85ab17522942)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Creating the dataset \n", "\n", "Before training the model, a dataset is required. The dataset should be a collection of fingerprint images captured by the [R503 Fingerprint Module](https://www.adafruit.com/product/4651).\n", "\n", "__NOTE:__ Due to privacy concerns, no dataset is provided by this tutorial. You must generate your own dataset using the instructions below.\n", "\n", "Recall that the goal of ML model is to generate a sequence of numbers that are similar for the images of the same fingerprint and different for different fingerprints.\n", "Thus, the dataset should have many images of the __same__ fingerprint.\n", "\n", "The structure of the dataset might look something like:\n", "\n", "```\n", "abs/left/index/1.jpg - Person \"abc\", left hand, index finger, image 1\n", "abs/left/index/2.jpg - Person \"abc\", left hand, index finger, image 2\n", "abs/left/index/3.jpg - Person \"abc\", left hand, index finger, image 3\n", "...\n", "abs/left/thumb/1.jpg - Person \"abc\", left hand, thumb, image 1\n", "abs/left/thumb/2.jpg - Person \"abc\", left hand, thumb, image 2\n", "abs/left/thumb/3.jpg - Person \"abc\", left hand, thumb, image 3\n", "...\n", "```\n", "\n", "The goal is to have has many different people and fingerprints as possible. However, it is critical that there are multiple images of the _same_ fingerprint. This way, the ML model can learn the features that make fingerprints similar and different." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Generating the dataset\n", "\n", "To aid the generation of the dataset, the MLTK provides the command: \n", "\n", "```shell\n", "mltk fingerprint_reader --generate-dataset\n", "```\n", "\n", "__NOTE:__ To use this command, you must have a locally connected development board with the R503 fingerprint module connected. \n", "Refer to [Hardware Setup](https://siliconlabs.github.io/mltk/docs/cpp_development/examples/fingerprint_authenticator.html#hardware-setup) for more details.\n", "\n", "\n", "With the hardware setup, issue the command:\n", "\n", "```shell\n", "mltk fingerprint_reader fingerprint_signature_generator --generate-dataset\n", "```\n", "\n", "This will guide you through the process of capturing your fingerprints and saving them to your local PC.\n", "After the command completes, repeat the command with as many other peoples' fingers as possible. The larger your dataset, the better your trained model will perform.\n", "\n", "__NOTE:__ The command above uses the pre-built model [fingerprint_signature_generator](../../docs/python_api/models/siliconlabs/fingerprint_signature_generator.md). This argument is effectively ignored when using the `--generate-dataset` option.\n", "\n", "__WARNING:__ Be sure to backup your dataset directory after adding new samples!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Data preprocessing\n", "\n", "The [R503 Fingerprint Module](https://www.adafruit.com/product/4651) generates 192x192 grayscale images. \n", "Additional preprocessing is applied to the raw images to help the ML model learn the important features of the images.\n", "\n", "The preprocessing algorithm source may be found in [fingerprint_signature_generator_dataset.py](https://github.com/SiliconLabs/mltk/blob/master/mltk/models/siliconlabs/fingerprint_signature_generator_dataset.py)\n", "\n", "The following algorithms are used: \n", "- __Color space balancing__ - This uses simple statistical centering and removes outliers\n", "- __Sharpening__ - This applies 2D convolution using a harpening filter: `original + (original ? blurred) × amount`\n", "- __Quality rejection__ - Using simple heuristics, if the image is found to be too blurry it is dropped\n", "\n", "__NOTE:__ These algorithms are used to preprocess the training dataset _and_ on the embedded device at runtime (see [data_preprocessor.cc](https://github.com/SiliconLabs/mltk/blob/master/cpp/shared/apps/fingerprint_authenticator/data_preprocessor.cc))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Generating fingerprint pairs\n", "\n", "To train a [Siamese Network](https://en.wikipedia.org/wiki/Siamese_neural_network), __pairs__ of fingerprint images are supplied as inputs to the model.\n", "\n", "The image pairs are grouped into two classes: \n", "- __match__ - Images are of the _same_ fingerprint\n", "- __no-match__ - Images are of _different_ fingerprints\n", "\n", "The [fingerprint_signature_generator_dataset.py](https://github.com/SiliconLabs/mltk/blob/master/mltk/models/siliconlabs/fingerprint_signature_generator_dataset.py) script is used to generate the image pairs from the fingerprint dataset. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Creating the Model\n", "\n", "The [model specification](../../docs/guides/model_specification.md) used by this tutorial may be found on Github: [fingerprint_signature_generator.py](https://github.com/siliconlabs/mltk/blob/master/mltk/models/siliconlabs/fingerprint_signature_generator.py)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Dataset\n", "\n", "Due to privacy concerns, no dataset is provided by this tutorial. You must generate your own dataset using the instructions in this tutorial.\n", "\n", "Once the dataset is generated, update the model specification script:\n", "\n", "```python\n", "# NOTE: For privacy purposes, no dataset is provided for this model.\n", "# As such, you must generate your own dataset to train this model.\n", "# Refer to this model's corresponding tutorial for how to generate the dataset.\n", "DATASET_ARCHIVE_URL = 'your-fingerprint-dataset-directory-or-download-url'\n", "#DATASET_ARCHIVE_URL = '~/.mltk/fingerprint_reader/dataset'\n", "```\n", "\n", "And modify `DATASET_ARCHIVE_URL` point to your dataset directory or download URL." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Loss Function\n", "\n", "As per the Keras tutorial: [Image similarity estimation using a Siamese Network with a contrastive loss](https://keras.io/examples/vision/siamese_contrastive/), a __contrastive loss__ function is used for model training.\n", "\n", "The source code for the custom loss function may be found on Github: [mltk/core/keras/losses.py](https://github.com/siliconlabs/mltk/blob/master/mltk/core/keras/losses.py)\n", "\n", "The basic formula for contrastive loss is: \n", "```\n", "Contrastive loss = mean( (1-true_value) * square(prediction) + true_value * square( max(margin-prediction, 0) ))\n", "```\n", "\n", "Where `margin` defines the baseline for distance for which pairs should be classified as dissimilar." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Model Parameters\n", "\n", "Recall that any preprocessing that is done to the data at training time must also be done at runtime on the embedded device.\n", "So, the __exact__ parameters and algorithms used for color balancing and image sharpening must also be used on the embedded device.\n", "\n", "To aid with this, the MLTK allows for embedding [model parameters](../../docs/guides/model_parameters.md) into the generated `.tflite` model file that is programmed onto the embedded device.\n", "\n", "These parameters are set by the model python script, e.g.:\n", "\n", "```python\n", "# The maximum \"distance\" between two signature vectors to be considered\n", "# the same fingerprint\n", "# Refer to the /eval/h5/threshold_vs_accuracy.png\n", "# to get an idea of what this valid should be\n", "my_model.model_parameters['threshold'] = 0.22\n", "\n", "# Also add the preprocessing settings to the model parameters\n", "preprocess_params = my_model.dataset.preprocess_params\n", "my_model.model_parameters['sharpen_filter'] = my_model.dataset.sharpen_filter.flatten().tobytes()\n", "my_model.model_parameters['sharpen_filter_width'] = my_model.dataset.sharpen_filter.shape[1]\n", "my_model.model_parameters['sharpen_filter_height'] = my_model.dataset.sharpen_filter.shape[0]\n", "my_model.model_parameters['sharpen_gain'] = my_model.dataset.sharpen_gain\n", "my_model.model_parameters['balance_threshold_max'] = preprocess_params['balance_threshold_max']\n", "my_model.model_parameters['balance_threshold_min'] = preprocess_params['balance_threshold_min']\n", "my_model.model_parameters['border'] = preprocess_params['border']\n", "my_model.model_parameters['verify_imin'] = preprocess_params['verify_imin']\n", "my_model.model_parameters['verify_imax'] = preprocess_params['verify_imax']\n", "my_model.model_parameters['verify_full_threshold'] = preprocess_params['verify_full_threshold']\n", "my_model.model_parameters['verify_center_threshold'] = preprocess_params['verify_center_threshold']\n", "```\n", "\n", "And then read by the firmware application at runtime: [data_preprocessor.cc](https://github.com/SiliconLabs/mltk/blob/master/cpp/shared/apps/fingerprint_authenticator/data_preprocessor.cc)\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Saving the model\n", "\n", "The trained Siamese network contains two \"towers\", however, only one of the towers is required to generate the signature.\n", "\n", "Thus, after model training, but before the model is saved, the model is modified so that only one of the towers is saved.\n", "This is done using the `on_save_keras_model` [TrainMixin](../../docs/python_api/mltk_model/train_mixin.md) property.\n", "\n", "```python\n", "def my_keras_model_saver(\n", " mltk_model:MyModel,\n", " keras_model:KerasModel,\n", " logger:logging.Logger\n", ") -> KerasModel:\n", " \"\"\"This is invoked after training successfully completes\n", " \n", " Here want to just save one of the \"towers\"\n", " as that is what is used to generate the fingerprint signature\n", " on the device\n", " \"\"\"\n", " # The given keras_model contains the full siamese network\n", " # Save it to the model's log dir\n", " h5_path = mltk_model.h5_log_dir_path\n", " siamese_network_h5_path = h5_path[:-len('.h5')] + '.siamese.h5'\n", " logger.debug(f'Saving {siamese_network_h5_path}')\n", " keras_model.save(siamese_network_h5_path, save_format='tf')\n", "\n", " # Extract the embedding network from the siamese network\n", " embedding_network = None\n", " for layer in keras_model.layers:\n", " if layer.name == 'model':\n", " embedding_network = layer\n", " break\n", " if embedding_network is None:\n", " raise RuntimeError('Failed to find embedding model in siamese network model, does the embedding model have the name \"model\" ?')\n", "\n", " # Save the tower as the .h5 model file for this model\n", " logger.debug(f'Saving {h5_path}')\n", " embedding_network.save(h5_path, save_format='tf')\n", "\n", " # Return the keras model\n", " return embedding_network\n", "\n", "my_model.on_save_keras_model = my_keras_model_saver\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Train the model\n", "\n", "With the dataset and model specification script ready, it's time to train the model.\n", "\n", "This can be done with the command:\n", "\n", "```\n", "mltk train fingerprint_signature_generator\n", "```\n", "\n", "__NOTE:__ Replace `fingerprint_signature_generator` with the name of your model." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Evaluating the model\n", "\n", "After training completes, the model is automatically evaluated. This is done using a custom evaluation function:\n", "\n", "```python\n", "def my_model_evaluator(\n", " mltk_model:MyModel, \n", " built_model:Union[KerasModel, TfliteModel],\n", " eval_dir:str,\n", " logger:logging.Logger,\n", " show:bool\n", ") -> EvaluationResults:\n", " \"\"\"Custom callback to evaluate the trained model\n", " \n", " The model is effectively a classifier, but we need to do\n", " a special step to compare the signatures in the dataset.\n", " \"\"\"\n", " results = ClassifierEvaluationResults(\n", " name=mltk_model.name,\n", " classes=mltk_model.classes\n", " ) \n", "\n", " threshold = my_model.model_parameters['threshold']\n", " logger.error(f'Using model threshold: {threshold}')\n", "\n", " y_pred, y_label, y_dis = generate_predictions( \n", " mltk_model,\n", " built_model,\n", " threshold\n", " )\n", "\n", " results.calculate(\n", " y=y_label,\n", " y_pred=y_pred,\n", " )\n", "\n", " results.generate_plots(\n", " logger=logger, \n", " output_dir=eval_dir, \n", " show=show\n", " )\n", "\n", " match_dis = []\n", " nomatch_dis = []\n", "\n", " for y, dis in zip(y_label, y_dis):\n", " if y == 0:\n", " match_dis.append(dis)\n", " else:\n", " nomatch_dis.append(dis)\n", "\n", " match_dis = sorted(match_dis)\n", " match_dis_x = [i for i in range(len(match_dis))]\n", " nomatch_dis = sorted(nomatch_dis)\n", " nomatch_dis_x = [i for i in range(len(nomatch_dis))]\n", "\n", " step = (match_dis[-1] - match_dis[0]) / 100\n", " thresholds = np.arange(match_dis[0], match_dis[-1], step)\n", "\n", " match_acc = []\n", " nomatch_acc = []\n", "\n", " for thres in thresholds:\n", " valid_count = sum(x < thres for x in match_dis)\n", " match_acc.append(valid_count / len(match_dis))\n", " valid_count = sum(x > thres for x in nomatch_dis)\n", " nomatch_acc.append(valid_count / len(nomatch_dis))\n", "\n", " fig = plt.figure('Threshold vs Accuracy')\n", "\n", " plt.plot(match_acc, thresholds, label='Match')\n", " plt.plot(nomatch_acc, thresholds, label='Non-match')\n", "\n", " #plt.ylim([0.0, 0.01])\n", " plt.legend(loc=\"lower right\")\n", " plt.xlabel('Accuracy')\n", " plt.ylabel('Threshold')\n", " plt.title('Threshold vs Accuracy')\n", " plt.grid(which='major')\n", "\n", " if eval_dir:\n", " output_path = f'{eval_dir}/threshold_vs_accuracy.png'\n", " plt.savefig(output_path)\n", " logger.info(f'Generated {output_path}')\n", " if show:\n", " plt.show(block=False)\n", " else:\n", " fig.clear()\n", " plt.close(fig)\n", " \n", "\n", " fig = plt.figure('Euclidean Distance')\n", "\n", " plt.plot(match_dis_x, match_dis, label='Match')\n", " plt.plot(nomatch_dis_x, nomatch_dis, label='Non-match')\n", "\n", " plt.legend(loc=\"lower right\")\n", " plt.xlabel('Index')\n", " plt.ylabel('Distance')\n", " plt.title('Euclidean Distance')\n", " plt.grid(which='major')\n", "\n", " if eval_dir:\n", " output_path = f'{eval_dir}/eclidean_distance.png'\n", " plt.savefig(output_path)\n", " logger.info(f'Generated {output_path}')\n", " if show:\n", " plt.show(block=False)\n", " else:\n", " fig.clear()\n", " plt.close(fig)\n", "\n", " return results\n", "\n", "\n", "my_model.eval_custom_function = my_model_evaluator\n", "```\n", "\n", "This uses the model's validation dataset to generate pairs of matching and non-matching fingerprint images.\n", "Each image from the pair is given to the trained model (recall that only one \"tower\" of the Siamese network is saved, thus the model only has one input) and its corresponding signature is generated.\n", "\n", "The euclidean distance is then calculated between the two signatures. If the distances is less than a threshold (which is specified in the [model parameters](#model-parameters)) then the two images are considered a match, otherwise they are considered a non-match.\n", "\n", "The model predictions are then compared against the actual values to generate the various classification evaluation metrics.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Determining the threshold\n", "\n", "The model threshold parameter must be determined before we can deploy this model.\n", "The model threshold is effectively the maximum euclidean distance between two signatures for them to be considered the same. e.g.:\n", "\n", "```\n", "IF distance(signature1, signature2) < threshold THEN\n", " Signatures are from the same fingers\n", "ELSE\n", " Signatures are from different fingers\n", "```\n", "\n", "The MLTK evaluation scripts allow for determining this value.\n", "\n", "After the evaluation completes, various diagrams are generated in the model's log directory (the actual directory path is printed to the console, e.g.: `~/.mltk/models/fingerprint_signature_generator/eval/h5/`).\n", "\n", "One such diagram is:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8qNh9FAAAACXBIWXMAAA9hAAAPYQGoP6dpAABnqUlEQVR4nO3dd3gU5d7G8e9uei+kkRgIvUMQhCNFUFAERLBRVEAEO+oLx6OoR0H0CHZERRQLHkUpFvQIIhhBBVEECSK9hx4ChDSSbLLz/rGyuoYSINlJdu/Pde2VZOaZmd8+CcnNMzPPWAzDMBARERERr2E1uwARERERcS8FQBEREREvowAoIiIi4mUUAEVERES8jAKgiIiIiJdRABQRERHxMgqAIiIiIl5GAVBERETEyygAioiIiHgZBUARERERL6MAKCIiIuJlFABFREREvIwCoIiIiIiXUQAUERER8TIKgCIiIiJeRgFQRERExMsoAIqIiIh4GQVAERERES+jACgiIiLiZRQARURERLyMAqCIiIiIl1EAFBEREfEyCoAiIiIiXkYBUERERMTLKACKiIiIeBkFQBEREREvowAoIiIi4mUUAEVERES8jAKgiIiIiJdRABQRERHxMgqAIiIiIl5GAVBERETEyygAioiIiHgZBUARERERL6MAKOJhlixZgsVi4eOPPza7FKBy6hk3bhwWi6VcbS0WC+PGjauwY4uIeAIFQJFqwGKxlOu1ZMkSs0uVM5g/fz4Wi4XExETsdrvZ5YiIl/I1uwARObP333/f5ev//ve/LFq0qMzyJk2asGHDBneWJmdpxowZpKSksHPnTr799lu6d+9udkki4oUUAEWqgZtvvtnl659++olFixaVWQ6cdwAsKCggODj4vPYhJ5efn8/nn3/OhAkTePfdd5kxY0aVDYD5+fmEhISYXYaIVBKdAhbxUHa7nf/85z9ccMEFBAYG0q1bN7Zu3erSpmvXrjRv3pxVq1ZxySWXEBwczCOPPAJAUVERY8eOpX79+gQEBJCcnMyDDz5IUVGRyz4WLVpEp06diIyMJDQ0lEaNGjn3cbb1AMyZM4c2bdoQFBRETEwMN998M3v37j3j+y0qKmLUqFHExsYSFhbG1VdfzZ49e8643cGDB/H19eWJJ54os27Tpk1YLBZeffVVAGw2G0888QQNGjQgMDCQGjVq0KlTJxYtWnTG4wB89tlnHD9+nBtuuIGBAwfy6aefUlhYWKZdYWEh48aNo2HDhgQGBlKzZk2uvfZatm3b5mxjt9t5+eWXadGiBYGBgcTGxnLllVeycuVKAHbu3InFYmH69Oll9v/36yJPXFO5fv16brzxRqKioujUqRMAv/32G7fccgt169YlMDCQhIQEbr31Vg4fPlxmv3v37mX48OEkJiYSEBBAnTp1uOuuuyguLmb79u1YLBZeeumlMtv9+OOPWCwWPvroo3L1o4icP40AinioiRMnYrVaeeCBBzh27BjPPvssN910Ez///LNLu8OHD9OzZ08GDhzIzTffTHx8PHa7nauvvpqlS5dy++2306RJE9auXctLL73E5s2bmTt3LgDr1q3jqquuomXLlowfP56AgAC2bt3KsmXLzqme6dOnM2zYMC666CImTJjAwYMHefnll1m2bBmrV68mMjLylO93xIgRfPDBB9x444106NCBb7/9lt69e5+xn+Lj4+nSpQuzZ89m7NixLutmzZqFj48PN9xwA+AIShMmTGDEiBG0a9eOnJwcVq5cya+//srll19+xmPNmDGDSy+9lISEBAYOHMiYMWP43//+59w/QGlpKVdddRVpaWkMHDiQ+++/n9zcXBYtWsTvv/9OvXr1ABg+fDjTp0+nZ8+ejBgxgpKSEn744Qd++ukn2rZte8ZaTuaGG26gQYMGPP300xiGATgC/vbt2xk2bBgJCQmsW7eON998k3Xr1vHTTz85b8bZt28f7dq1Izs7m9tvv53GjRuzd+9ePv74YwoKCqhbty4dO3ZkxowZjBo1qky/hIWF0bdv33OqW0TOgSEi1c4999xjnOqf7+LFiw3AaNKkiVFUVORc/vLLLxuAsXbtWueyLl26GIAxdepUl328//77htVqNX744QeX5VOnTjUAY9myZYZhGMZLL71kAMahQ4dOWWt56ykuLjbi4uKM5s2bG8ePH3e2+/LLLw3AePzxx53Lxo4d6/L+09PTDcC4++67XY594403GoAxduzYU9ZnGIbxxhtvlOkbwzCMpk2bGpdddpnz61atWhm9e/c+7b5O5eDBg4avr68xbdo057IOHToYffv2dWn3zjvvGIDx4osvltmH3W43DMMwvv32WwMw7rvvvlO22bFjhwEY7777bpk2f++TE/05aNCgMm0LCgrKLPvoo48M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tfuor91Hfe0+6uuKpZtARERERLyMRgBFREREvIwCoIiIiIiXUQAUERER8TIKgCIiIiJeRgHQJK+99hopKSkEBgbSvn17VqxYcdr2c+bMoXHjxgQGBtKiRQvmz5/vpkqrv7Pp62nTptG5c2eioqKIioqie/fuZ/zeyJ/O9uf6hJkzZ2KxWOjXr1/lFuhBzravs7Ozueeee6hZsyYBAQE0bNhQv0fK6Wz7etKkSTRq1IigoCCSk5MZNWoUhYWFbqq2evr+++/p06cPiYmJWCwW5s6de8ZtlixZwoUXXkhAQAD169dn+vTplV6nRzHE7WbOnGn4+/sb77zzjrFu3TrjtttuMyIjI42DBw+etP2yZcsMHx8f49lnnzXWr19v/Pvf/zb8/PyMtWvXurny6uds+/rGG280XnvtNWP16tXGhg0bjFtuucWIiIgw9uzZ4+bKq5+z7esTduzYYSQlJRmdO3c2+vbt655iq7mz7euioiKjbdu2Rq9evYylS5caO3bsMJYsWWKkp6e7ufLq52z7esaMGUZAQIAxY8YMY8eOHcbXX39t1KxZ0xg1apSbK69e5s+fbzz66KPGp59+agDGZ599dtr227dvN4KDg43Ro0cb69evN1555RXDx8fHWLBggXsK9gAKgCZo166dcc899zi/Li0tNRITE40JEyactH3//v2N3r17uyxr3769cccdd1RqnZ7gbPv670pKSoywsDDjvffeq6wSPca59HVJSYnRoUMH46233jKGDh2qAFhOZ9vXr7/+ulG3bl2juLjYXSV6jLPt63vuuce47LLLXJaNHj3a6NixY6XW6UnKEwAffPBBo1mzZi7LBgwYYPTo0aMSK/MsOgXsZsXFxaxatYru3bs7l1mtVrp3787y5ctPus3y5ctd2gP06NHjlO3F4Vz6+u8KCgqw2WxER0dXVpke4Vz7evz48cTFxTF8+HB3lOkRzqWvv/jiCy6++GLuuece4uPjad68OU8//TSlpaXuKrtaOpe+7tChA6tWrXKeJt6+fTvz58+nV69ebqnZW+jv4vnzNbsAb5OVlUVpaSnx8fEuy+Pj49m4ceNJtzlw4MBJ2x84cKDS6vQE59LXf/fQQw+RmJhY5heNuDqXvl66dClvv/026enpbqjQc5xLX2/fvp1vv/2Wm266ifnz57N161buvvtubDYbY8eOdUfZ1dK5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z3cuO3GN3+kcPnyYTZs28e9//5tu3brRpEkTjh496tKmZcuWpKenc+TIkVPuZ8SIEXzzzTdMmTKFkpISrr322jMeW0SkImgEUESqrC+//JKjR48yfPhwIiIiXNZdd911vP322zz33HN069aNevXqMXDgQEpKSpg/fz4PPfQQKSkpDB06lFtvvZXJkyfTqlUrdu3aRWZmJv3796d9+/YEBwfzyCOPcN999/Hzzz+XucP4ZKKioqhRowZvvvkmNWvWJCMjgzFjxri0GTRoEE8//TT9+vVjwoQJ1KxZk9WrV5OYmMjFF18MQJMmTfjHP/7BQw89xK233nrGUUMRkYqiEUARqbLefvttunfvXib8gSMArly5kujoaObMmcMXX3xBamoql112GStWrHC2e/3117n++uu5++67ady4MbfddptzxC86OpoPPviA+fPn06JFCz766CPGjRt3xrqsViszZ85k1apVNG/enFGjRvHcc8+5tPH392fhwoXExcXRq1cvWrRowcSJE/Hx8XFpN3z4cIqLi7n11lvPoYdERM6NJoIWETHRk08+yZw5c/jtt9/MLkVEvIhGAEVETJCXl8fvv//Oq6++yr333mt2OSLiZRQARURMMHLkSNq0aUPXrl11+ldE3E6ngEVERES8jEYARURERLyMAqCIiIiIl1EAFBEREfEyCoAiIiIiXkYBUERERMTLKACKiIiIeBkFQBEREREvowAoIiIi4mUUAEVERES8zP8DhTjuPJrzsqQAAAAASUVORK5CYII=", "text/plain": [ "" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from IPython.display import Image\n", "from mltk.utils.path import fullpath\n", "\n", "Image(filename=fullpath('~/.mltk/models/fingerprint_signature_generator/eval/h5/threshold_vs_accuracy.png')) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This diagram compares the threshold versus the model's accuracy for each class.\n", "\n", "__NOTE:__ Different diagrams will be generated for different fingerprint datasets and model parameters.\n", "\n", "So, from the diagram, if the threshold was set to 0.2, then the model would: \n", "- Correctly identify two matching fingerprints about 91% of the time\n", "- Correctly identify two non-matching fingerprints about 97% of the time\n", "\n", "If the threshold was set to 0.1, then the model would: \n", "- Correctly identify two matching fingerprints about 45% of the time\n", "- Correctly identify two non-matching fingerprints about 98% of the time\n", "\n", "Since the intent of this application is to authenticate users, we want the non-match accuracy to be as high as possible,\n", "while at the same time have a reasonably high match accuracy:\n", "- __Non-match accuracy__ - The higher this is, the better the application rejects hackers from spoofing fingerprints\n", "- __Match accuracy__ - The higher this is, the better the user-experience when using the application\n", "\n", "\n", "The threshold value is set in the model specification python script, e.g.:\n", "\n", "```python\n", "# The maximum \"distance\" between two signature vectors to be considered\n", "# the same fingerprint\n", "# Refer to the /eval/h5/threshold_vs_accuracy.png\n", "# to get an idea of what this valid should be\n", "my_model.model_parameters['threshold'] = 0.22\n", "```\n", "\n", "After updating the threshold, re-run the model evaluation with the command:\n", "\n", "```\n", "mltk evaluate fingerprint_signature_generator\n", "\n", "Name: fingerprint_signature_generator\n", "Model Type: classification\n", "Overall accuracy: 95.469%\n", "Class accuracies:\n", "- no-match = 97.465%\n", "- match = 92.982%\n", "Average ROC AUC: 96.277%\n", "Class ROC AUC:\n", "- no-match = 97.308%\n", "- match = 95.245%\n", "```\n", "\n", "__NOTE:__ Replace `fingerprint_signature_generator` with the name of your model." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Running the model\n", "\n", "Now that we have a trained model, it is time to run it in on an embedded device.\n", "\n", "There are several different ways this can be done:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Using the command-line\n", "\n", "The MLTK features the command: \n", "```\n", "mltk fingerprint_reader --help\n", "```\n", "Which will load the trained fingerprint model and execute it on the embedded device.\n", "\n", "__NOTE:__ Additional hardware is required to run this command, see [Hardware Setup](https://siliconlabs.github.io/mltk/docs/cpp_development/examples/fingerprint_authenticator.html#hardware-setup)\n", "\n", "\n", "To run program your model to an embedded device, issue the command: \n", "```\n", "mltk fingerprint_reader fingerprint_signature_generator --accelerator MVP\n", "```\n", "\n", "__NOTE:__ Replace `fingerprint_signature_generator` with the name of your model.\n", "\n", "Which will program the [fingerprint_authenticator](../../docs/cpp_development/examples/fingerprint_authenticator.md) application and your model to the embedded device and run.\n", "\n", "This command will also display images of the fingerprints captured from the fingerprint module." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Building the C++ example application\n", "\n", "The MLTK supports building [C++ Applications](../../docs/cpp_development/index.md).\n", "\n", "It also features an [fingerprint_authenticator](../../docs/cpp_development/examples/fingerprint_authenticator.md) C++ application\n", "which can be built using: \n", "- [Visual Studio Code](../../docs/cpp_development/vscode.md) \n", "- [Simplicity Studio](../../docs/cpp_development/simplicity_studio.md)\n", "- [Command Line](../../docs/cpp_development/command_line.md)\n", "\n", "Refer to the [fingerprint_authenticator](../../docs/cpp_development/examples/fingerprint_authenticator.md) application's documentation\n", "for how include your model into the built application." ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.8 (tags/v3.10.8:aaaf517, Oct 11 2022, 16:50:30) [MSC v.1933 64 bit (AMD64)]" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "1b794eb47024974fee893fdb7015f3d322c4012087fc39c73069299b7c169399" } } }, "nbformat": 4, "nbformat_minor": 2 }