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:
- Wytrenuj model (w Pythonie): plik Pythona do trenowania, konwertowania i optymalizacji modelu pod kątem użycia na urządzeniu.
- 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:
- Arduino Nano 33 BLE Sense (z użyciem Arduino IDE)
- SparkFun Edge (kompilacja bezpośrednio ze źródła)
- Zestaw STM32F746 Discovery (korzystając z Mbed)
- Adafruit EdgeBadge (z użyciem Arduino IDE)
- Zestaw Adafruit TensorFlow Lite dla mikrokontrolerów (z użyciem Arduino IDE)
- Adafruit Circuit Playground Bluefruit (z użyciem Arduino IDE)
- Espressif ESP32-DevKitC (z użyciem ESP IDF)
- Espressif ESP-EYE (z użyciem ESP IDF)
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
:
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"
micro_mutable_op_resolver.h
udostępnia operacje używane przez interpreter do uruchamiania modelu.micro_error_reporter.h
podaje dane debugowania.micro_interpreter.h
zawiera kod do wczytywania i uruchamiania modeli.schema_generated.h
zawiera schemat formatu pliku modelu TensorFlow LiteFlatBuffer
.version.h
udostępnia informacje o obsłudze wersji schematu TensorFlow Lite.
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 = µ_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);