W niniejszym artykule zaprezentuję przykład wykorzystania modelu sieci neuronowej utworzonej w oparciu o framework TensorFlow Lite na platformę bazującą na mikrokontrolerze z rodziny STM32. Mam nadzieję, że pozwoli to w prosty i przystępny sposób pokazać, w jaki sposób można rozpocząć swoją przygodę z uczeniem maszynowym na mikrokontrolerach. Poznanie i zrozumienie podstaw działania frameworku TensorFlow Lite umożliwi nie tylko orientację w trendach w dziedzinie sztucznej inteligencji, lecz także może zaowocować utworzeniem zarówno użytecznych jak i ciekawych projektów na urządzeniach wbudowanych.
TinyML – co to jest?
TinyML to stosunkowo nowa dziedzina sztucznej inteligencji. Dotyczy głównie przetwarzania sieci głębokiej bezpośrednio na urządzeniu wbudowanym. Urządzenia wbudowane z reguły nie nadają się do treningu modeli sieci głębokich ze względu na ograniczone zasoby pamięciowe oraz obliczeniowe. Jednak w przypadku uruchomienia gotowego modelu wymagania sprzętowe zwykle są mniej restrykcyjne. Gotowy model, zwany inferencyjnym, może być zoptymalizowany, np. pod względem architektury sieci, jej parametrów czy użytych typów danych. Podział na trening i inferencje, które mogą być realizowane niezależnie od platformy sprzętowej, daje możliwość wykorzystania sztucznej inteligencji w mikrokontrolerach.
Pierwsze kroki
Platforma sprzętowa
W ramach demonstracji zaprezentowany zostanie prosty przykład dotyczący klasyfikacji cyfr na podstawie dostarczonego obrazu. W tym celu będzie nam potrzebny zestaw uruchomieniowy STM32F429I-DISC1 oparty na mikrokontrolerze STM32F429ZIT6, do którego dostarczony jest dotykowy ekran LCD. Obraz uzyskany podczas rysowania na ekranie posłuży jako wejście do modelu sieci neuronowej. Uzyskany z modelu rezultat klasyfikacji zostanie zaprezentowany na ekranie.
Model
Do uzyskania modelu sieci potrzebujemy zestawu danych treningowych, walidacyjnych i testowych. Aby zaopiekować przedstawiony problem powinna wystarczyć w zupełności baza odręcznie zapisanych cyfr bazy MNIST. Każdy rekord w bazie zawiera zestaw składający się z obrazu przedstawiającego cyfrę oraz etykiety oznaczającej kategorię, do której należy obraz. W przypadku MNIST mamy 10 kategorii, które odpowiadają cyfrom 0-9.
Proces tworzenia, uczenia i przygotowania modelu do wdrożenia na mikrokontroler przeprowadzimy przy pomocy frameworku TensorFlow Lite. Framework ten posiada zestaw narzędzi, które umożliwiają uruchamianie modeli na urządzeniach mobilnych, wbudowanych oraz IoT. Aktualną listę wspieranych platform sprzętowych można znaleźć na stronie.
W przypadku mikrokontrolerów opartych na procesorze Cortex-M (jak w przypadku użytej tu platformy sprzętowej), TensorFlow Lite wykorzystuje bibliotekę CMSIS NN. Biblioteka ta zawiera listę operacji wykorzystywanych w sieciach neuronowych zoptymalizowanych pod Cortex-M. Dzięki temu działanie modelu może być efektywniejsze.
Model możemy utworzyć samodzielnie, bądź zaimportować z formatu TensorFlow (gotowych modeli w tym formacie jest znacznie więcej) i dokonać konwersji z wykorzystaniem narzędzia TensorFlow Lite Converter. Podczas konwersji można użyć szeregu optymalizacji pozwalających przykładowo na zmniejszenie rozmiaru modelu czy ilości wykorzystywanej pamięci podczas działania. Po konwersji, za pomocą narzędzia TensorFlow Lite Interpreter, gotowy model zostanie załadowany do pamięci mikrokontrolera.
Uproszczony schemat działania TensorFlow Lite został zaprezentowany poniżej.
W przypadku treningu tak prostego modelu, jak w tym przykładzie, kompilacja TensorFlow Lite ze źródeł i implementacja z użyciem C++ nie będą nam potrzebne. Tworzenie modelu oraz trening przeprowadzimy z użyciem tzw. Google Colab, który w odróżnieniu od Jupyter notebook, działa z wykorzystaniem serwerów Google’a oferując darmowy dostęp do GPU oraz TPU. Dzięki temu proces uczenia może trwać krócej.
Tworzenie modelu, trening i konwersja
Model utworzymy od podstaw, wykorzystując format TensorFlow. Następnie wykonamy konwersję na format TensorFlow Lite. W ramach optymalizacji model może zostać dodatkowo poddany kwantyzacji. W takim przypadku, po kwantyzacji każda operacja, która oryginalnie bazowała na zapisie zmiennoprzecinkowym zostanie zmieniona na zapis stałoprzecinkowy. Ma to dodatkową zaletę w przypadku, gdy docelowa architektura nie wspiera operacji zmiennoprzecinkowych. Instrukcje do tworzenia modelu i jego uczenia można znaleźć na stronie.
Tworzenie modelu
Do utworzenia modelu potrzebujemy TensorFlow przynajmniej w wersji 2.3. Oprócz importu samego TensorFlow, zaimportujmy bibliotekę NumPy, zaprojektowaną do wykonywania obliczeń na wielowymiarowych macierzach. Dodajmy też logowanie, które może przydać się podczas treningu:
Kolejnym krokiem będzie utworzenie samego modelu. Nie ma potrzeby, aby samodzielnie pobierać i manipulować bazą MNIST. TensorFlow umożliwia wygodny dostęp zarówno do zestawu danych treningowych, jak i testowych.
Zanim przystąpimy do projektowania warstw sieci, zajmijmy się obróbką danych wejściowych, tzw. feature preprocessing. Pozwala ona na przekształcenie danych wejściowych w taki sposób, aby były dopasowane do architektury sieci np. poprzez redukcję wymiarów.
W ramach obróbki danych zostanie przeprowadzony tzw. feature scaling, który polega na normalizacji danych wejściowych. Powinno to przyspieszyć proces uczenia. Obrazy zawierające cyfry są reprezentowane w 8-bitowej skali odcieni szarości o wymiarach 28×28. Minimalną wartością jest 0, zaś maksymalną 255, do uzyskania zakresu [0,1] wystarczy więc, że dane podzielimy przez 255.
Architektura sieci
Kolejny krok to określenie architektury sieci. W tym przypadku problem rozpoznawania cyfr jest problemem z dziedziny klasyfikacji, zatem architekturę można podzielić na dwie części:
- ekstrakcję cech (tzw. feature extraction),
- klasyfikację.
W przypadku ekstrakcji cech algorytmy próbują wydobyć charakterystyczne cechy jak np. ilość linii czy ich nachylenie. W przykładzie wejściem są obrazy, więc efektywnym sposobem wydobycia cech jest stosowanie m.in. naprzemiennie warstw konwolucyjnej i warstwy zbiorczej (tzw. pooling).
Pierwsza z nich stosuje operację konwolucji. Zapewnia ona kompozycyjność (tzw. compositionality). Kompozycyjność pozwala sieci na rozpoznawanie coraz bardziej złożonych kształtów. Przykładowo, pierwsza warstwa konwolucyjna pozwala wydobyć krawędzie z pikseli, kolejna warstwa konwolucyjna kształty a kolejna obiekty na podstawie kształtów. Klasyfikację obiektu niezależnie od miejsca (tzw. local invariance) zapewnia nam warstwa zbiorcza.
W analizowanym przykładzie potrzebujemy 10 wyjść. Każde z nich odpowiadać będzie prawdopodobieństwu, z jakim dana cyfra wystąpiła na obrazie. Aby uzyskać 10-elementowe wyjście potrzebujemy warstwy, która połączy naszą ostatnią warstwę zbiorczą z wyjściową.
Metryką, pozwalającą określić jakość procesu uczenia, będzie dokładność pomiędzy prawdziwym rezultatem a rezultatem uzyskanym na wyjściu sieci. Następnie wykorzystamy funkcję błędu (tzw. error function) oraz algorytm próbujący ten błąd zminimalizować (tzw. optimizer). Jako funkcji błędu użyjemy entropii krzyżowej przeznaczonej dla problemu klasyfikacji. Jako algorytmu optymalizacji użyjemy algorytmu Adam (tzw. Adaptive Moment Estimation). Wyniki procesu uczenia można zobaczyć na poniższym rysunku.
Kolejny etap to konwersja modelu na format wykorzystywany przez TensorFlow Lite. W tym celu dodamy następujące instrukcje:
Ostatnim krokiem przed wdrożeniem jest pobranie utworzonego modelu. W tym celu najpierw zapiszemy model w formacie .tflite:
Niestety, w przypadku użytej platformy sprzętowej, sam plik nie wystarczy. Standardowo, TensorFlow Lite Interpreter działa na modelu, który dostarczany jest przy użyciu określonego systemu plików. Aby załadować model do pamięci mikrokontrolera, dokonamy konwersji modelu na postać pliku źródłowego. Plik ten potem dołączymy do aplikacji, a następnie skompilujemy dla docelowej platformy. Do konwersji mnist_model.tflite do mnist_model.cc wykorzystamy program xxd:
Po utworzeniu pliku mnist_model.cc można go pobrać na lokalny komputer. Będzie on nam później potrzebny.
Aplikacja i wdrożenie modelu
Po utworzeniu modelu przejdziemy do przygotowania aplikacji. Będzie ona pobierała dane wejściowe od użytkownika, dokonywała predykcji za pomocą zintegrowanego modelu a następnie prezentowała uzyskany rezultat.
Przygotowanie aplikacji
Pierwszy krok to przygotowanie aplikacji, która umożliwi użytkownikowi rysowanie. Aby nie odkrywać koła na nowo, w ramach artykułu, jako podstawę wykorzystamy gotowy przykład. W kolejnych etapach będziemy go przerabiać do naszych potrzeb.
Naszą bazę, przykład LTDC_Paint (Ryc. 4), wygenerujemy za pomocą STM32CubeMX. Po konfiguracji z domyślnymi opcjami, przystąpimy do kompilacji jego pierwszego uruchomienia, w moim przypadku, za pomocą STM32CubeIDE.
Kolejnym krokiem jest import do projektu TensorFlow Lite jako niezbędnej biblioteki. Niestety, twórcy TensorFlow Lite nie oferują bezpośrednio dostępu do binarnej wersji pod mikrokontrolery. W dokumentacji można jedynie znaleźć instrukcję jak wygenerować za pomocą narzędzia Make szereg przykładowych projektów i na ich podstawie rozwijać swoje własne.
To podejście nie sprawdzi się w naszym przypadku, ponieważ bazowy projekt, LTDC_Paint, już istnieje. Najlepszym więc wyjściem wydaje się ręczny import wszystkich niezbędnych plików do projektu i kompilacja TensorFlow Lite ze źródeł. Pomocną instrukcję do importu TensorFlow Lite można znaleźć na stronie. Przed przystąpieniem do budowania i kompilacji projektu z TensorFlow Lite warto upewnić się, że:
- Projekt z aplikacją wykorzystuje docelowo język C++ a nie C. W STM32CubeIDE istnieje opcja konwersji z C na C++.
- Należy upewnić się, że włączone pliki TensorFlowLite są dostępne z poziomu kompilatora C++ a nie tylko C.
- Katalog examples w tensorflow_lite/tensorflow/lite/micro jest usunięty (zawiera plik main.c, którego nie będziemy używać)
Przetwarzanie danych
W ramach modelu wymagane jest przygotowanie obrazów wejściowych, w skali odcieni szarości, o rozmiarach 28×28. Należy dokonać obróbki danych przed dostarczeniem ich do sieci. W ramach przetwarzania danych wykorzystamy trzy operacje:
- konwersja do odcieni szarości,
- interpolacja dwuliniowa,
- normalizacja.
Pierwsza konwersja będzie polegać na zamianie kolorowych obrazów na obrazy w skali odcieni szarości. Każdy 32-bitowy piksel formacie ARGB zostanie zamieniony na 8-bitowy pixel:
Następnie obraz należy przeskalować do rozmiaru 28×28. W ramach przekształcenia rozmiaru wykonamy interpolację dwuliniową:
Ostatnia operacja polega na znormalizowaniu danych do wartości z zakresu [0,1]:
Sieć
Ostatnim etapem jest dostarczenie przerobionych danych do modelu. Do uruchomienia modelu dołączymy niezbędne nagłówki oraz zmienne, które będą przechowywać potrzebne obiekty TensorFlow Lite. Klasa MicroErrorReporter udostępnia mechanizm rejestrowania informacji podczas inferencji. Klasa ta przyda się do raportowania napotkanych błędów.
Po dodaniu wskaźników na model, interpreter oraz tensory wejściowe i wyjściowe, następną rzeczą, którą należy zrobić, jest przydzielenie obszaru pamięci roboczej, którego model będzie potrzebował podczas działania (tzw. arena tensorowa). Ten obszar pamięci będzie używany do przechowywania tensorów wejściowych, wyjściowych i pośrednich modelu.
Jak duża powinna być arena tensorowa? Niestety, nie ma konkretnej odpowiedzi, gdyż wszystko zależy od architektury modelu. Wartość można ustalić metodą prób i błędów, rozpoczynając od takiej, która pozwala na uruchomienie modelu, a następnie stopniowo ją redukując.
W funkcji setup() uruchomimy logowanie, załadujemy model, skonfigurujemy interpreter i zaalokujemy pamięć. Zamiast klasy AllOpsResolver, ograniczymy się do użycia klasy MicroMutableOpResolver, gdyż potrzeba tylko czterech operacji:
- conv 2D,
- max pooling 2D,
- fully connected,
- reshape.
Następnie wypełnimy tensor wejściowy danymi uzyskanymi po przeskalowaniu obrazu wejściowego i uruchomimy model:
Do uzyskania cyfry, dla której prawdopodobieństwo jest największe skorzystamy z:
Uruchomienie
Z przykładowym działaniem aplikacji możecie zapoznać się na przygotowanym przeze mnie filmie.
Podsumowanie
Powyższy przykład przedstawił kroki, za pomocą których można stworzyć od podstaw swój własny model sieci neuronowej i dostarczyć go do aplikacji przeznaczonej na platformę wbudowaną. Raz wdrożony model może działać samodzielnie, bez dostępu do sieci. Dzięki temu podobne podejście można zastosować dodatkowo w przypadku, kiedy istotne jest zachowanie prywatności danych.
Źródła
- TensorFlow Lite
- TinyLM: Machine Learning with TensorFlow Lite on Arduino and Ultra-Low-Power Microcontrollers, Daniel Situnayake, Pete Warden, 2020
Świetny artykuł, złożone zagadnienie opisane w przystępny sposób 🙂
Ciekawy artykuł w przystępnej formie.
Portal Forbot przyzwyczaił mnie jednak do formy przedstawiania kodu programów w pokrojonych fragmentach, a następnie powtórzenie ich w zespolonej formie.
Przykładowo pod tekstem mówiącym o nagłówkach, można by było umieść wyłącznie fragment kodu z nagłówkami, itp. A następnie z takich fragmentów przedstawić ich zestawienie. Wówczas mniej doświadczony czytelnik (jak ja) nie przeraża się bardziej obszernego kodu (i go pomija), gdyż zaznajomił się chwile wcześniej z jego fragmentami.