Zacznij korzystać z mikrokontrolerów

W tym dokumencie wyjaśniono, jak trenować model i uruchamiać wnioskowanie za pomocą mikrokontrolera.

Przykład Hello World

Przykład Hello World ma na celu zademonstrowanie absolutnych podstaw używania TensorFlow Lite dla mikrokontrolerów. Uczymy i uruchamiamy model, który replikuje funkcję sinus, tj. przyjmuje na wejściu pojedynczą liczbę i wyprowadza wartość sinusową tej liczby. Po wdrożeniu w mikrokontrolerze jego przewidywania służą do migania diod LED lub sterowania animacją.

Kompleksowy przepływ pracy obejmuje następujące kroki:

  1. Trenuj model (w Pythonie): plik Pythona do uczenia, konwertowania i optymalizowania modelu do użytku na urządzeniu.
  2. Uruchom wnioskowanie (w C++ 17): kompleksowy test jednostkowy, który uruchamia wnioskowanie na modelu przy użyciu biblioteki C++ .

Uzyskaj obsługiwane urządzenie

Przykładowa aplikacja, z której będziemy korzystać, została przetestowana na następujących urządzeniach:

Dowiedz się więcej o obsługiwanych platformach w TensorFlow Lite dla mikrokontrolerów .

Trenuj modelkę

Użyj train.py do szkolenia modelu Hello World w celu rozpoznawania fali sinusoidalnej

Uruchom: 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 swoim urządzeniu, przejdziemy przez instrukcje zawarte w pliku README.md :

Witaj, świecie README.md

W poniższych sekcjach omówiono przykładowy test jednostkowy evaluate_test.cc , który demonstruje, jak uruchamiać wnioskowanie przy użyciu TensorFlow Lite dla mikrokontrolerów. Ładuje model i kilkakrotnie uruchamia wnioskowanie.

1. Dołącz nagłówki biblioteki

Aby skorzystać z biblioteki TensorFlow Lite for Microcontrollers, musimy dołączyć następujące pliki nagłówkowe:

#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 TensorFlow Lite dla mikrokontrolerów oczekuje, że model zostanie dostarczony jako tablica C++. Model jest zdefiniowany w plikach model.h i model.cc . Nagłówek jest dołączony do następującej linii:

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

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

Aby utworzyć test jednostkowy, dołączamy platformę testów jednostkowych TensorFlow Lite dla mikrokontrolerów, dołączając następujący wiersz:

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

Test definiuje się za pomocą następujących 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 powyższym makrze.

4. Skonfiguruj rejestrowanie

Aby skonfigurować rejestrowanie, tworzony jest wskaźnik tflite::ErrorReporter przy użyciu wskaźnika do instancji tflite::MicroErrorReporter :

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

Zmienna ta zostanie przekazana do interpretera, który umożliwi mu zapisywanie logów. Ponieważ mikrokontrolery często mają różne mechanizmy rejestrowania, implementacja tflite::MicroErrorReporter została zaprojektowana tak, aby można ją było dostosować do konkretnego urządzenia.

5. Załaduj model

W poniższym kodzie tworzona jest instancja modelu przy użyciu danych z tablicy char g_model , która jest zadeklarowana w model.h . Następnie sprawdzamy model, aby upewnić się, że jego wersja schematu jest zgodna z wersją, której używamy:

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. Narzędzie do rozpoznawania operacji instancji

Zadeklarowano instancję MicroMutableOpResolver . Będzie to wykorzystane przez interpreter do zarejestrowania i uzyskania dostępu do operacji używanych przez model:

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 programie rozpoznawania nazw.

HelloWorldOpResolver op_resolver;
TF_LITE_ENSURE_STATUS(RegisterOps(op_resolver));

7. Przydziel pamięć

Musimy wstępnie przydzielić pewną ilość pamięci dla tablic wejściowych, wyjściowych i pośrednich. Jest to dostarczane jako tablica uint8_t o rozmiarze tensor_arena_size :

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

Wymagany rozmiar będzie zależał od używanego modelu i może wymagać ustalenia na podstawie eksperymentów.

8. Tłumacz instancji

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

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

9. Przydzielaj tensory

Mówimy interpreterowi, aby przydzielił pamięć z tensor_arena dla tensorów modelu:

interpreter.AllocateTensors();

10. Sprawdź kształt wejściowy

Instancja MicroInterpreter może dostarczyć nam wskaźnik do tensora wejściowego modelu, wywołując funkcję .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ą takie, jakich oczekujemy:

// 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ść wyliczeniowa kTfLiteFloat32 jest odniesieniem do jednego z typów danych TensorFlow Lite i jest zdefiniowana w common.h .

11. Podaj wartość wejściową

Aby zapewnić dane wejściowe do modelu, ustawiamy zawartość tensora wejściowego w następujący sposób:

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

W tym przypadku wprowadzamy wartość zmiennoprzecinkową reprezentującą 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ć wartość zwracaną, TfLiteStatus , aby określić, czy uruchomienie się powiodło. Możliwe wartości TfLiteStatus zdefiniowane w common.h to kTfLiteOk i kTfLiteError .

Poniższy kod potwierdza, że ​​wartość to kTfLiteOk , co oznacza, że ​​wnioskowanie zostało pomyślnie przeprowadzone.

TF_LITE_MICRO_EXPECT_EQ(kTfLiteOk, invoke_status);

13. Uzyskaj wynik

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

W przykładzie wyjściem modelu jest pojedyncza 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 stwierdzić, że jest to, czego oczekujemy:

// 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. Uruchom ponownie wnioskowanie

Pozostała część kodu uruchamia wnioskowanie jeszcze kilka razy. W każdym przypadku przypisujemy wartość tensorowi wejściowemu, 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);