Erste Schritte mit Mikrocontrollern

In diesem Dokument wird erläutert, wie Sie ein Modell trainieren und Inferenzen mithilfe eines Mikrocontrollers ausführen.

Hello World-Beispiel

Das Beispiel Hello World soll die absoluten Grundlagen der Verwendung von TensorFlow Lite für Mikrocontroller demonstrieren. Wir trainieren und führen ein Modell aus, das eine Sinusfunktion repliziert. Das heißt, es verwendet eine einzelne Zahl als Eingabe und gibt den Sinuswert der Zahl aus. Bei der Bereitstellung auf dem Mikrocontroller werden die Vorhersagen entweder dazu verwendet, LEDs zu blinken oder eine Animation zu steuern.

Der End-to-End-Workflow umfasst die folgenden Schritte:

  1. Modell trainieren (in Python): Eine Python-Datei zum Trainieren, Konvertieren und Optimieren eines Modells für die Verwendung auf dem Gerät.
  2. Inferenz ausführen (in C++ 17): Ein End-to-End-Unittest, der mithilfe der C++-Bibliothek eine Inferenz für das Modell ausführt.

Unterstütztes Gerät kaufen

Die Beispielanwendung, die wir verwenden werden, wurde auf den folgenden Geräten getestet:

Weitere Informationen zu unterstützten Plattformen finden Sie unter TensorFlow Lite für Mikrocontroller.

Modell trainieren

Verwenden Sie train.py für das Hello World-Modelltraining für die Sinwave-Erkennung

Ausführen: 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/

Inferenz ausführen

Um das Modell auf deinem Gerät auszuführen, folgen wir der Anleitung in der README.md:

Hello World README.md

In den folgenden Abschnitten wird der Einheitentest evaluate_test.cc aus diesem Beispiel beschrieben, der zeigt, wie Inferenzen mit TensorFlow Lite für Mikrocontroller ausgeführt werden. Das Modell wird geladen und die Inferenz wird mehrmals ausgeführt.

1. Bibliotheks-Header einschließen

Zur Verwendung der TensorFlow Lite for Microcontrollers-Bibliothek müssen die folgenden Header-Dateien hinzugefügt werden:

#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. Modellheader einschließen

Der Interpreter für TensorFlow Lite for Microcontrollers erwartet, dass das Modell als C++-Array bereitgestellt wird. Das Modell ist in den Dateien model.h und model.cc definiert. Der Header ist in der folgenden Zeile enthalten:

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

3. Framework-Header für Einheitentest einfügen

Um einen Einheitentest zu erstellen, fügen wir das Framework für TensorFlow Lite for Microcontrollers-Einheitentests hinzu, indem wir die folgende Zeile einfügen:

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

Der Test wird mithilfe der folgenden Makros definiert:

TF_LITE_MICRO_TESTS_BEGIN

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

TF_LITE_MICRO_TESTS_END

Wir besprechen nun den Code, der im obigen Makro enthalten ist.

4. Logging einrichten

Zum Einrichten von Logging wird ein tflite::ErrorReporter-Zeiger mit einem Zeiger auf eine tflite::MicroErrorReporter-Instanz erstellt:

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

Diese Variable wird an den Interpreter übergeben, der ihm das Schreiben von Logs ermöglicht. Da Mikrocontroller oft eine Vielzahl von Mechanismen für das Logging haben, ist die Implementierung von tflite::MicroErrorReporter so konzipiert, dass sie für Ihr jeweiliges Gerät angepasst wird.

5. Modell laden

Im folgenden Code wird das Modell mit Daten aus dem char-Array g_model instanziiert, das in model.h deklariert ist. Anschließend prüfen wir, ob die Schemaversion des Modells mit der von uns verwendeten Version kompatibel ist:

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. Vorgangs-Resolver instanziieren

Es wird eine MicroMutableOpResolver-Instanz deklariert. Diese wird vom Interpreter verwendet, um die vom Modell verwendeten Vorgänge zu registrieren und darauf zuzugreifen:

using HelloWorldOpResolver = tflite::MicroMutableOpResolver<1>;

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

Für MicroMutableOpResolver ist ein Vorlagenparameter erforderlich, der die Anzahl der zu registrierenden Vorgänge angibt. Die Funktion RegisterOps registriert die Vorgänge beim Resolver.

HelloWorldOpResolver op_resolver;
TF_LITE_ENSURE_STATUS(RegisterOps(op_resolver));

7. Arbeitsspeicher zuweisen

Wir müssen eine bestimmte Menge an Speicher für Eingabe-, Ausgabe- und Zwischenarrays vorab zuweisen. Sie wird als uint8_t-Array der Größe tensor_arena_size bereitgestellt:

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

Die erforderliche Größe hängt vom verwendeten Modell ab und muss möglicherweise durch Experimente bestimmt werden.

8. Dolmetscher instanziieren

Wir erstellen eine tflite::MicroInterpreter-Instanz und übergeben die zuvor erstellten Variablen:

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

9. Tensoren zuweisen

Wir weisen den Interpreter an, Speicher aus dem tensor_arena für die Tensoren des Modells zuzuweisen:

interpreter.AllocateTensors();

10. Eingabeform validieren

Die MicroInterpreter-Instanz kann durch Aufrufen von .input(0) einen Zeiger auf den Eingabetensor des Modells bereitstellen, wobei 0 den ersten (und einzigen) Eingabetensor darstellt:

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

Anschließend prüfen wir, ob Form und Typ des Tensors den Erwartungen entsprechen:

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

Der ENUM-Wert kTfLiteFloat32 verweist auf einen der TensorFlow Lite-Datentypen und ist in common.h definiert.

11. Eingabewert angeben

Um eine Eingabe für das Modell bereitzustellen, legen wir den Inhalt des Eingabetensors so fest:

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

In diesem Fall geben wir einen Gleitkommawert ein, der 0 darstellt.

12. Modell ausführen

Zum Ausführen des Modells können Sie Invoke() in der tflite::MicroInterpreter-Instanz aufrufen:

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

Wir können den Rückgabewert (TfLiteStatus) prüfen, um festzustellen, ob die Ausführung erfolgreich war. Die in common.h definierten Werte für TfLiteStatus sind kTfLiteOk und kTfLiteError.

Der folgende Code bestätigt, dass der Wert kTfLiteOk ist, was bedeutet, dass die Inferenz erfolgreich ausgeführt wurde.

TF_LITE_MICRO_EXPECT_EQ(kTfLiteOk, invoke_status);

13. Ausgabe abrufen

Sie können den Ausgabetensor des Modells abrufen, indem Sie output(0) für tflite::MicroInterpreter aufrufen, wobei 0 den ersten (und einzigen) Ausgabetensor darstellt.

In diesem Beispiel ist die Ausgabe des Modells ein einzelner Gleitkommawert, der in einem 2D-Tensor enthalten ist:

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

Wir können den Wert direkt aus dem Ausgabetensor lesen und bestätigen, dass er unseren Erwartungen entspricht:

// 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. Inferenz noch einmal ausführen

Im Rest des Codes werden mehrere Male Inferenzen ausgeführt. In jeder Instanz weisen wir dem Eingabetensor einen Wert zu, rufen den Interpreter auf und lesen das Ergebnis aus dem Ausgabetensor:

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