Premiers pas avec les microcontrôleurs

Ce document explique comment entraîner un modèle et exécuter une inférence à l'aide d'un microcontrôleur.

Exemple "Hello World"

L'exemple Hello World est conçu pour illustrer les principes de base de l'utilisation de TensorFlow Lite for Microcontrollers. Nous entraînons et exécutons un modèle qui réplique une fonction sinus, c'est-à-dire qu'il prend un seul nombre en entrée et génère la valeur sinus de ce nombre. Lorsqu'elles sont déployées sur le microcontrôleur, ses prédictions sont utilisées pour faire clignoter les LED ou contrôler une animation.

Le flux de travail de bout en bout comprend les étapes suivantes:

  1. Entraîner un modèle (en Python): un fichier Python permettant d'entraîner, de convertir et d'optimiser un modèle pour une utilisation sur un appareil.
  2. Exécuter l'inférence (en C++ 17): test unitaire de bout en bout qui exécute l'inférence sur le modèle à l'aide de la bibliothèque C++.

Obtenir un appareil compatible

L'exemple d'application que nous allons utiliser a été testé sur les appareils suivants:

Apprenez-en davantage sur les plates-formes compatibles dans TensorFlow Lite for Microcontrollers.

Entraîner un modèle

Utilisez train.py pour entraîner le modèle Hello World afin de reconnaître la sirène.

Exécuter: bazel build tensorflow/lite/micro/examples/hello_world:train bazel-bin/tensorflow/lite/micro/examples/hello_world/train --save_tf_model --save_dir=/tmp/model_created/

Exécuter une inférence

Pour exécuter le modèle sur votre appareil, suivez les instructions fournies dans le fichier README.md:

Hello World README.md

Les sections suivantes présentent la méthode evaluate_test.cc de l'exemple, un test unitaire qui montre comment exécuter une inférence à l'aide de TensorFlow Lite for Microcontrollers. Elle charge le modèle et exécute des inférences plusieurs fois.

1. Inclure les en-têtes de bibliothèque

Pour utiliser la bibliothèque TensorFlow Lite for Microcontrollers, nous devons inclure les fichiers d'en-tête suivants:

#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/micro/micro_error_reporter.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/schema/schema_generated.h"
#include "tensorflow/lite/version.h"

2. Inclure l'en-tête du modèle

L'interpréteur TensorFlow Lite for Microcontrollers s'attend à ce que le modèle soit fourni sous forme de tableau C++. Le modèle est défini dans les fichiers model.h et model.cc. L'en-tête est inclus avec la ligne suivante:

#include "tensorflow/lite/micro/examples/hello_world/model.h"

3. Inclure l'en-tête du framework de test unitaire

Afin de créer un test unitaire, nous incluons le framework de test unitaire TensorFlow Lite pour les microcontrôleurs en incluant la ligne suivante:

#include "tensorflow/lite/micro/testing/micro_test.h"

Le test est défini à l'aide des macros suivantes:

TF_LITE_MICRO_TESTS_BEGIN

TF_LITE_MICRO_TEST(LoadModelAndPerformInference) {
  . // add code here
  .
}

TF_LITE_MICRO_TESTS_END

Examinons maintenant le code inclus dans la macro ci-dessus.

4. Configurer la journalisation

Pour configurer la journalisation, un pointeur tflite::ErrorReporter est créé à l'aide d'un pointeur vers une instance tflite::MicroErrorReporter:

tflite::MicroErrorReporter micro_error_reporter;
tflite::ErrorReporter* error_reporter = &micro_error_reporter;

Cette variable sera transmise à l'interpréteur, ce qui lui permettra d'écrire des journaux. Étant donné que les microcontrôleurs possèdent souvent divers mécanismes de journalisation, l'implémentation de tflite::MicroErrorReporter est conçue pour être personnalisée en fonction de votre appareil.

5. Charger un modèle

Dans le code suivant, le modèle est instancié à l'aide des données d'un tableau char, g_model, qui est déclaré dans model.h. Nous vérifions ensuite le modèle pour nous assurer que sa version de schéma est compatible avec celle que nous utilisons:

const tflite::Model* model = ::tflite::GetModel(g_model);
if (model->version() != TFLITE_SCHEMA_VERSION) {
  TF_LITE_REPORT_ERROR(error_reporter,
      "Model provided is schema version %d not equal "
      "to supported version %d.\n",
      model->version(), TFLITE_SCHEMA_VERSION);
}

6. Résolveur d'instanciations d'opérations

Une instance MicroMutableOpResolver est déclarée. L'interpréteur se servira de cette adresse pour enregistrer les opérations utilisées par le modèle et y accéder:

using HelloWorldOpResolver = tflite::MicroMutableOpResolver<1>;

TfLiteStatus RegisterOps(HelloWorldOpResolver& op_resolver) {
  TF_LITE_ENSURE_STATUS(op_resolver.AddFullyConnected());
  return kTfLiteOk;

MicroMutableOpResolver nécessite un paramètre de modèle indiquant le nombre d'opérations qui seront enregistrées. La fonction RegisterOps enregistre les opérations avec le résolveur.

HelloWorldOpResolver op_resolver;
TF_LITE_ENSURE_STATUS(RegisterOps(op_resolver));

7. Allouer de la mémoire

Nous devons préallouer une certaine quantité de mémoire pour les tableaux d'entrée, de sortie et intermédiaires. Ces informations sont fournies sous la forme d'un tableau uint8_t de taille tensor_arena_size:

const int tensor_arena_size = 2 * 1024;
uint8_t tensor_arena[tensor_arena_size];

La taille requise dépend du modèle que vous utilisez et devra peut-être être déterminée par des tests.

8. Instancier l'interpréteur

Nous créons une instance tflite::MicroInterpreter en transmettant les variables créées précédemment:

tflite::MicroInterpreter interpreter(model, resolver, tensor_arena,
                                     tensor_arena_size, error_reporter);

9. Allouer des Tensors

Nous indiquons à l'interpréteur d'allouer de la mémoire à partir du tensor_arena pour les Tensors du modèle:

interpreter.AllocateTensors();

10. Valider la forme de saisie

L'instance MicroInterpreter peut nous fournir un pointeur vers le Tensor d'entrée du modèle en appelant .input(0), où 0 représente le premier (et le seul) Tensor d'entrée:

  // Obtain a pointer to the model's input tensor
  TfLiteTensor* input = interpreter.input(0);

Nous inspectons ensuite ce Tensor pour vérifier que sa forme et son type correspondent à nos attentes:

// Make sure the input has the properties we expect
TF_LITE_MICRO_EXPECT_NE(nullptr, input);
// The property "dims" tells us the tensor's shape. It has one element for
// each dimension. Our input is a 2D tensor containing 1 element, so "dims"
// should have size 2.
TF_LITE_MICRO_EXPECT_EQ(2, input->dims->size);
// The value of each element gives the length of the corresponding tensor.
// We should expect two single element tensors (one is contained within the
// other).
TF_LITE_MICRO_EXPECT_EQ(1, input->dims->data[0]);
TF_LITE_MICRO_EXPECT_EQ(1, input->dims->data[1]);
// The input is a 32 bit floating point value
TF_LITE_MICRO_EXPECT_EQ(kTfLiteFloat32, input->type);

La valeur d'énumération kTfLiteFloat32 est une référence à l'un des types de données TensorFlow Lite et est définie dans common.h.

11. Saisissez une valeur d'entrée

Pour fournir une entrée au modèle, nous définissons le contenu du Tensor d'entrée comme suit:

input->data.f[0] = 0.;

Dans le cas présent, nous saisissons une valeur à virgule flottante représentant 0.

12. Exécuter le modèle

Pour exécuter le modèle, nous pouvons appeler Invoke() sur notre instance tflite::MicroInterpreter:

TfLiteStatus invoke_status = interpreter.Invoke();
if (invoke_status != kTfLiteOk) {
  TF_LITE_REPORT_ERROR(error_reporter, "Invoke failed\n");
}

Nous pouvons vérifier la valeur renvoyée, TfLiteStatus, pour déterminer si l'exécution a réussi. Les valeurs possibles de TfLiteStatus, définies dans common.h, sont kTfLiteOk et kTfLiteError.

Le code suivant affirme que la valeur est kTfLiteOk, ce qui signifie que l'inférence a bien été exécutée.

TF_LITE_MICRO_EXPECT_EQ(kTfLiteOk, invoke_status);

13. Obtenir la sortie

Le Tensor de sortie du modèle peut être obtenu en appelant output(0) sur le tflite::MicroInterpreter, où 0 représente le premier (et le seul) Tensor de sortie.

Dans cet exemple, la sortie du modèle est une seule valeur à virgule flottante contenue dans un Tensor 2D:

TfLiteTensor* output = interpreter.output(0);
TF_LITE_MICRO_EXPECT_EQ(2, output->dims->size);
TF_LITE_MICRO_EXPECT_EQ(1, input->dims->data[0]);
TF_LITE_MICRO_EXPECT_EQ(1, input->dims->data[1]);
TF_LITE_MICRO_EXPECT_EQ(kTfLiteFloat32, output->type);

Nous pouvons lire la valeur directement à partir du Tensor de sortie et confirmer qu'elle correspond à nos attentes:

// Obtain the output value from the tensor
float value = output->data.f[0];
// Check that the output value is within 0.05 of the expected value
TF_LITE_MICRO_EXPECT_NEAR(0., value, 0.05);

14. Exécuter à nouveau l'inférence

Le reste du code exécute des inférences à plusieurs reprises. Dans chaque instance, nous attribuons une valeur au Tensor d'entrée, appelons l'interpréteur et lisons le résultat à partir du Tensor de sortie:

input->data.f[0] = 1.;
interpreter.Invoke();
value = output->data.f[0];
TF_LITE_MICRO_EXPECT_NEAR(0.841, value, 0.05);

input->data.f[0] = 3.;
interpreter.Invoke();
value = output->data.f[0];
TF_LITE_MICRO_EXPECT_NEAR(0.141, value, 0.05);

input->data.f[0] = 5.;
interpreter.Invoke();
value = output->data.f[0];
TF_LITE_MICRO_EXPECT_NEAR(-0.959, value, 0.05);