Wprowadzenie do mikrokontrolerów

Ten dokument wyjaśnia, jak wytrenować model i uruchomić wnioskowanie za pomocą mikrokontrolera.

Przykład Hello World

Przykład Hello World ma na celu zademonstrowanie absolutnych podstaw korzystania z TensorFlow Lite na potrzeby mikrokontrolerów. Trenujemy i uruchamiamy model, który replikuje funkcję sinus, czyli wejściową wartość przyjmuje 1 liczbę i na wyjściu generuje jej wartość sinus. Wskaźniki te są wdrażane w mikrokontrolerze i wykorzystują swoje prognozy do migania diod LED lub sterowania animacjami.

Cały proces składa się z tych kroków:

  1. Wytrenuj model (w Pythonie): plik Pythona do trenowania, konwertowania i optymalizacji modelu pod kątem użycia na urządzeniu.
  2. Uruchom wnioskowanie (w C++ 17): kompleksowy test jednostkowy, który polega na wnioskowaniu na podstawie modelu przy użyciu biblioteki C++.

Kup obsługiwane urządzenie

Przykładowa aplikacja, której użyjemy, została przetestowana na tych urządzeniach:

Więcej informacji o obsługiwanych platformach znajdziesz w artykule o TensorFlow Lite dla mikrokontrolerów.

Trenowanie modelu

Użyj train.py do trenowania modelu hello World na potrzeby rozpoznawania sinwave

Uruchomienie: 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/

Uruchom wnioskowanie

Aby uruchomić model na urządzeniu, wykonajmy instrukcje podane w README.md:

Hello World README.md

W kolejnych sekcjach omówimy test jednostkowy evaluate_test.cc w przykładzie, który pokazuje, jak uruchomić wnioskowanie za pomocą TensorFlow Lite dla mikrokontrolerów. Wczytuje model i kilka razy uruchamia wnioskowanie.

1. Dołącz nagłówki biblioteki

Aby użyć biblioteki TensorFlow Lite dla mikrokontrolerów, musimy dołączyć te pliki nagłówka:

#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. Dołącz nagłówek modelu

Interpreter interpretacji TensorFlow Lite dla mikrokontrolerów oczekuje, że model będzie udostępniany w postaci tablicy C++. Model jest zdefiniowany w plikach model.h i model.cc. Nagłówek pojawia się w tym wierszu:

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

3. Dołącz nagłówek platformy do testów jednostkowych

Aby utworzyć test jednostkowy, uwzględnimy platformę do testowania jednostkowego TensorFlow Lite dla mikrokontrolerów, dodając ten wiersz:

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

Test jest definiowany za pomocą tych makr:

TF_LITE_MICRO_TESTS_BEGIN

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

TF_LITE_MICRO_TESTS_END

Omówimy teraz kod zawarty w makrze powyżej.

4. Skonfiguruj logowanie

Aby można było skonfigurować logowanie, tworzony jest wskaźnik tflite::ErrorReporter za pomocą wskaźnika do instancji tflite::MicroErrorReporter:

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

Ta zmienna będzie przekazywana do interpretera, który umożliwia zapisywanie logów. Mikrokontrolery często mają różne mechanizmy logowania, więc implementacja tflite::MicroErrorReporter została zaprojektowana w taki sposób, aby można było dostosować je do konkretnego urządzenia.

5. Wczytywanie modelu

W poniższym kodzie utworzony jest model z wykorzystaniem danych z tablicy char, g_model, która jest zadeklarowana w komponencie model.h. Następnie sprawdzamy model, aby upewnić się, że jego wersja schematu jest zgodna z używaną przez nas wersją:

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. Program do rozpoznawania operacji w instancji

Instancja MicroMutableOpResolver jest zadeklarowana. Będzie on używany przez tłumacza do rejestrowania operacji używanych przez model i uzyskiwania do nich dostępu:

using HelloWorldOpResolver = tflite::MicroMutableOpResolver<1>;

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

MicroMutableOpResolver wymaga parametru szablonu wskazującego liczbę operacji, które zostaną zarejestrowane. Funkcja RegisterOps rejestruje operacje w resolverze.

HelloWorldOpResolver op_resolver;
TF_LITE_ENSURE_STATUS(RegisterOps(op_resolver));

7. Przydziel pamięć

Musimy wstępnie przydzielić pewną ilość pamięci na tablice wejściowe, wyjściowe i pośrednie. Wartość ta jest podawana w postaci tablicy uint8_t o rozmiarze tensor_arena_size:

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

Wymagany rozmiar zależy od używanego modelu i może być wymagany w ramach eksperymentów.

8. Utwórz instancję tłumaczenia

Tworzymy instancję tflite::MicroInterpreter, przekazując utworzone wcześniej zmienne:

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

9. Przydziel tensory

Informujemy tłumacza, aby przydzielił pamięć z tensor_arena dla tensorów modelu:

interpreter.AllocateTensors();

10. Zweryfikuj kształt danych wejściowych

Instancja MicroInterpreter może dostarczyć nam wskaźnik do tensora wejściowego modelu, wywołując metodę .input(0), gdzie 0 reprezentuje pierwszy (i jedyny) tensor wejściowy:

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

Następnie sprawdzamy tensor, aby potwierdzić, że jego kształt i typ są zgodne z oczekiwaniami:

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

Wartość wyliczenia kTfLiteFloat32 jest odniesieniem do jednego z typów danych TensorFlow Lite i jest zdefiniowana w common.h.

11. Podaj wartość wejściową

Aby podać dane wejściowe do modelu, ustawiamy zawartość tensora wejściowego w ten sposób:

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

W tym przypadku podajemy liczbę zmiennoprzecinkową reprezentującą element 0.

12. Uruchom model

Aby uruchomić model, możemy wywołać Invoke() w naszej instancji tflite::MicroInterpreter:

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

Możemy sprawdzić zwracaną wartość TfLiteStatus, aby określić, czy uruchomienie zakończyło się powodzeniem. Możliwe wartości TfLiteStatus zdefiniowane w common.h to kTfLiteOk i kTfLiteError.

Ten kod potwierdza, że wartość to kTfLiteOk, co oznacza, że wnioskowanie zostało wykonane.

TF_LITE_MICRO_EXPECT_EQ(kTfLiteOk, invoke_status);

13. Uzyskiwanie danych wyjściowych

Tensor wyjściowy modelu można uzyskać, wywołując funkcję output(0) w metodzie tflite::MicroInterpreter, gdzie 0 to pierwszy (i jedyny) tensor wyjściowy.

W tym przykładzie dane wyjściowe modelu to jedna wartość zmiennoprzecinkowa zawarta w tensorze 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);

Możemy odczytać wartość bezpośrednio z tensora wyjściowego i zapewnić, że jest ona zgodna z oczekiwaniami:

// 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. Ponownie uruchom wnioskowanie

Pozostała część kodu uruchamia wnioskowanie kilka razy. W każdej instancji przypisujemy wartość do tensora wejściowego, wywołujemy interpreter i odczytujemy wynik z tensora wyjściowego:

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