Argumentacja za uczeniem maszynowym na brzegu sieci była kiedyś machaniem ręką: niższe opóźnienia, lepsza prywatność, mniejsze zużycie pasma. Dziś jest to karta specyfikacji. ESP32-S3 z instrukcjami wektorowymi i 8 MB pamięci PSRAM potrafi uruchomić skwantyzowany model splotowy w kilkudziesięciu milisekundach, pobierając średnio jednocyfrowe wartości miliamperów. To zmienia to, co można umieścić w czujniku zasilanym baterią, w panelu kabiny jachtu czy w breloku dostępowym hotelu. Ten artykuł to przewodnik praktykującego inżyniera po wdrażaniu TensorFlow Lite Micro na ESP32-S3, z dwoma realnymi przykładami – wykrywaniem słów kluczowych i detekcją anomalii z akcelerometru – oraz benchmarkami, do których można się porównać.
Dlaczego ML na brzegu sieci ma teraz znaczenie
Cztery ograniczenia przesuwają wnioskowanie na brzeg sieci. Opóźnienie, bo odsyłanie dźwięku lub danych o drganiach do modelu w chmurze dodaje 200–2000 ms, na które nie można sobie pozwolić przy sterowaniu w pętli zamkniętej. Prywatność, bo najczystszym sposobem na zgodność z RODO lub normami danych w hotelarstwie jest w ogóle nieprzenoszenie surowego sygnału. Pasmo, bo flota dziesięciu tysięcy czujników drgań przesyłających surowe dane IMU z częstotliwością 1 kHz nasyci każdą rozsądną sieć transmisyjną. Oraz praca offline, bo łodzie, fabryki i odległe instalacje nie mogą zakładać łączności.
Przeciwwagą zawsze była moc obliczeniowa. Klasyczny ESP32 radził sobie z zabawkowymi modelami. ESP32-S3 zmienił rachunek. S3 dodał zestaw instrukcji wektorowych (PIE – Processor Instruction Extension), który przyspiesza operacje mnożenia z akumulacją leżące u podstaw każdej sieci neuronowej, oraz obsługuje do 8 MB oktalnej PSRAM na przechowywanie modelu i areny tensorów. Rutynowo wdrażamy teraz modele o wielkości 200–500 kB, które na oryginalnym ESP32 byłyby śmieszne, i uruchamiamy je na tyle szybko, że dominującym poborem mocy jest radio, a nie obliczenia.
Możliwości ESP32-S3, które warto znać
- Dwa rdzenie Xtensa LX7 przy 240 MHz, z jednostką wektorową PIE na każdym z nich.
- Do 8 MB oktalnej PSRAM przy 80 MHz, adresowalnej na arenę tensorów i wagi modelu.
- Do 16 MB pamięci flash na przechowywanie modelu i partycje OTA.
- USB-OTG do bezpośredniego połączenia z hostem bez zewnętrznego mostka – przydatne w rozwoju oraz przy dostarczaniu produktów działających jako urządzenia peryferyjne USB.
- Sprzętowo przyspieszane SHA, AES, RSA i ECC – te same silniki, które zabezpieczają Twoje sesje TLS, można ponownie wykorzystać do kontroli integralności modelu.
Dwa rdzenie mają znaczenie. Zwykle przypinamy wnioskowanie TFLite Micro do rdzenia 1, a rdzeń 0 zostawiamy na sieć, próbkowanie czujników i system operacyjny. To rozdzielenie zapobiega temu, by drgania (jitter) wnioskowania psuły uzgadnianie TLS lub podtrzymywanie połączeń MQTT. Jeśli potrzebujesz odświeżenia wiedzy o wyborze między członkami rodziny ESP32 a STM32, nasz przewodnik ESP32 kontra STM32 szczegółowo omawia kompromisy.
Wybór frameworka: TFLite Micro, Edge Impulse, ONNX
Trzy poważne opcje dla urządzeń klasy ESP32. Nie wykluczają się wzajemnie – dostarczaliśmy produkty, które używają Edge Impulse do rozwoju, a TFLite Micro do wdrożenia produkcyjnego.
- TensorFlow Lite Micro to opcja najniższego poziomu. Dostarczasz model
.tflite, linkujesz środowisko uruchomieniowe, rejestrujesz swój resolver operacji i wywołujeszInvoke(). Największa kontrola, najwięcej kodu szablonowego, najmniejszy ślad pamięciowy. Tego używamy produkcyjnie w ścieżkach krytycznych pod względem wydajności. - Edge Impulse opakowuje TFLite Micro (i inne backendy) w kompletny przepływ MLOps – zbieranie danych, etykietowanie, trenowanie, wdrożenie jako biblioteka C++. Doskonały do przejścia od czujnika do modelu w dni, a nie tygodnie. Nieco większy ślad binariów i narzucona struktura.
- ONNX Runtime dla systemów wbudowanych to najnowszy gracz na tej klasie sprzętu. Przydatny, jeśli Twój zespół data science żyje w PyTorch i eksportuje ONNX. Pokrycie operatorów jest wciąż węższe niż w TFLite na Xtensa.
Dla nowych projektów z wewnętrznymi kompetencjami ML domyślnie wybieramy TFLite Micro. Dla zespołów bez dedykowanych inżynierów ML Edge Impulse zwraca się z nawiązką. Artefaktem wdrożeniowym w obu przypadkach jest biblioteka statyczna, którą linkujesz do swojego firmware'u tak samo, jak każdy inny komponent w projekcie urządzeń podłączonych.
Budżety rozmiaru modelu
Trzy liczby decydują o tym, co możesz dostarczyć: flash na model, RAM na arenę tensorów i milisekundy na wnioskowanie. Realistyczne budżety na ESP32-S3 z PSRAM:
- Wagi modelu: 50–500 kB we flashu dla modeli INT8, więcej, jeśli masz zapasowe partycje.
- Arena tensorów: 30–300 kB w PSRAM na tensory aktywacji. Użyj
MicroInterpreter::arena_used_bytes()po pierwszymInvoke(), aby precyzyjnie ją zwymiarować. - Czas wnioskowania: 5–150 ms w zależności od architektury modelu. Cokolwiek powyżej 200 ms sugeruje, że powinieneś przemyśleć topologię modelu, a nie sprzęt.
Arena tensorów to zmienna, która zaskakuje ludzi. Alokuj ją w PSRAM, a nie w wewnętrznym SRAM, chyba że czas wnioskowania jest zdominowany przez przepustowość pamięci i to zmierzyłeś. Dostęp do PSRAM jest wolniejszy niż do SRAM, ale znacznie tańszy pod względem dostępnej pojemności. Kara jest realna, ale przewidywalna, i niemal zawsze przegrywa z wyczerpaniem wewnętrznego RAM-u w najgorszym możliwym momencie.
Kwantyzacja: INT8 to domyślny wybór, INT4 to nowa granica
Modele Float32 nie nadają się do wdrożenia na tej klasie sprzętu. Kwantyzacja INT8, wykonana na danych reprezentatywnych, kosztuje zwykle 1–3 punkty procentowe dokładności w zamian za 4-krotnie mniejsze modele i 2–4-krotnie szybsze wnioskowanie. Używamy kwantyzacji potreningowej TensorFlow z reprezentatywnym zbiorem danych liczącym 100–500 próbek pobranych z faktycznego rozkładu wdrożeniowego.
# Python: post-training INT8 quantization with representative data
converter = tf.lite.TFLiteConverter.from_saved_model("saved_model")
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = lambda: (
[sample.astype(np.float32)] for sample in calibration_samples
)
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
tflite_model = converter.convert()
open("model_int8.tflite", "wb").write(tflite_model)
Kwantyzacja INT4 jest realna i wdrażana w 2026 roku, ale pokrycie operatorów w TFLite Micro jest wciąż częściowe. INT4 stosujemy selektywnie dla bardzo dużych modeli, gdzie zysk na rozmiarze wart jest kosztu inżynieryjnego. Dla poniższego detektora słów kluczowych i detektora anomalii właściwą odpowiedzią jest INT8.
Przykład pierwszy: wykrywanie słów kluczowych
Wykrywanie słowa wybudzającego to kanoniczne demo TinyML, bo uruchamia cały potok – przechwytywanie dźwięku, ekstrakcję cech, sieć neuronową, eliminację drgań – w śladzie pamięciowym, który mieści się wszędzie. Nasza referencyjna architektura krążka sterowania głosowego w pokoju hotelowym:
- Mikrofon I2S próbkujący przy 16 kHz, 16-bit mono.
- Okno przesuwne o długości 1 sekundy, przeskakujące co 200 ms.
- Ekstrakcja cech MFCC: 40 pasm mel, 49 ramek na okno.
- Mała sieć CNN: 4 bloki splotu głębokościowo-separowalnego, ~80 kB INT8.
- Wygładzanie posterioru po 3 oknach, próg 0,85.
Samo wywołanie wnioskowania jest proste, gdy model i operacje są już zarejestrowane:
// Setup, called once
static constexpr int kArenaSize = 96 * 1024;
static uint8_t *arena = (uint8_t*)heap_caps_malloc(kArenaSize, MALLOC_CAP_SPIRAM);
const tflite::Model* model = tflite::GetModel(g_kws_model);
static tflite::MicroMutableOpResolver<8> resolver;
resolver.AddDepthwiseConv2D();
resolver.AddConv2D();
resolver.AddFullyConnected();
resolver.AddSoftmax();
resolver.AddReshape();
resolver.AddAveragePool2D();
resolver.AddQuantize();
resolver.AddDequantize();
static tflite::MicroInterpreter interp(model, resolver, arena, kArenaSize);
interp.AllocateTensors();
TfLiteTensor* input = interp.input(0);
TfLiteTensor* output = interp.output(0);
// Hot loop, called every 200 ms
void run_inference(const int8_t* mfcc) {
memcpy(input->data.int8, mfcc, input->bytes);
int64_t t0 = esp_timer_get_time();
if (interp.Invoke() != kTfLiteOk) return;
int64_t t1 = esp_timer_get_time();
ESP_LOGD(TAG, "infer %lld us", t1 - t0);
int8_t score = output->data.int8[KEYWORD_INDEX];
if (score > kThresholdInt8) on_keyword_detected();
}
Zmierzone na ESP32-S3 przy 240 MHz z PSRAM przy 80 MHz: wnioskowanie zajmuje 18–22 ms na okno, ekstrakcja MFCC kolejne 4–6 ms, pozostawiając ponad 170 ms zapasu na każdy przeskok dla radia, logiki aplikacji i narzutu systemu operacyjnego. Średni prąd jest zdominowany przez mikrofon i wzmacniacz, a nie przez obliczenia.
Przykład drugi: detekcja anomalii z akcelerometru
Dla predykcyjnego utrzymania ruchu pompy wody na jachcie lub agregatu chłodniczego w hotelu kanonicznym modelem jest jednowymiarowy splotowy autoenkoder wytrenowany na sygnaturach drgań zdrowego stanu. Anomalie objawiają się podwyższonym błędem rekonstrukcji. Wdrożenie jest podobne do wykrywania słów kluczowych, ale mniejsze i szybsze.
- 3-osiowy akcelerometr przy 800 Hz przez SPI.
- Okno przesuwne 256 próbek na oś (320 ms), z 50% nakładaniem.
- Normalizacja per oś do zerowej średniej i jednostkowej wariancji.
- Jednowymiarowy autoenkoder splotowy, 4 warstwy, ~35 kB INT8, wymiar latentny 16.
- Błąd MSE rekonstrukcji porównywany z toczącą się linią bazową; alarm, jeśli błąd przekroczy wyuczony próg przez 5 kolejnych okien.
Wnioskowanie mieści się w okolicy 6–8 ms na okno. Cały detektor – próbkowanie, normalizacja, wnioskowanie, logika progowa – zużywa poniżej 5% jednego rdzenia, pozostawiając zapas na to, by urządzenie mogło również publikować surową telemetrię do chmury na potrzeby ponownego trenowania. Wykryte anomalie wyzwalają bogatszy ładunek: ślad błędu rekonstrukcji, surowe okno i stempel wersji modelu, aby zespół chmurowy mógł retrospektywnie ocenić prawdziwe pozytywy. Ta pętla sprzężenia zwrotnego zamienia jednorazowe wdrożenie w system, który się poprawia; to część powodu, dla którego traktujemy przetwarzanie na brzegu sieci jako nierozłączne ze strategią chmurową, a nie jej substytut.
Przepływ wdrożeniowy
Dostarczenie modelu TinyML nie jest aktem jednorazowym. To wersjonowany artefakt jak każdy inny. Nasz standardowy przepływ:
- Wytrenuj i skwantyzuj w Pythonie; wygeneruj
model_int8.tfliteoraz JSON z metadanymi zawierający kształt wejścia, klasy i próg. - Skonwertuj do tablicy C za pomocą
xxd -ilub przepływu osadzania modelu w ESP-IDF. - Wkompiluj w firmware ze stałą wersji modelu; CI dołącza metadane do wydania.
- Dostarcz przez OTA, korzystając z tej samej strategii partycji co firmware. Oddzielamy partycje modelu od partycji kodu, aby aktualizacja modelu nie wymagała pełnego wgrania firmware'u.
- Obserwuj: każde wnioskowanie loguje wersję modelu, wynik i podjętą akcję. Ta telemetria zasila kolejny cykl trenowania.
Traktowanie modelu jako danych, a nie kodu, to klucz do szybkiej iteracji. W niektórych wdrożeniach wypychamy nowe partycje modelu w cyklu tygodniowym, podczas gdy leżący pod spodem firmware porusza się we własnym rytmie kwartalnym. To rozdzielenie pozwala też zespołowi data science dostarczać bez czekania na pociąg wydawniczy firmware'u.
Benchmarki, do których warto się porównać
Liczby z naszych referencyjnych benchmarków na ESP32-S3 przy 240 MHz, PSRAM przy 80 MHz, modele INT8, pojedynczy rdzeń:
- Detektor słów kluczowych (80 kB, wejście 49x40x1): 19 ms wnioskowania, 64 kB areny.
- Autoenkoder akcelerometru (35 kB, wejście 256×3): 7 ms wnioskowania, 22 kB areny.
- MobileNetV1 0.25x dla obrazu 96×96 w skali szarości (220 kB): 145 ms wnioskowania, 180 kB areny.
- Wykrywanie osoby (250 kB, 96×96): 175 ms wnioskowania, 200 kB areny.
Jeśli Twój model trwa znacznie dłużej, niż sugerują te liczby, zwykłymi winowajcami są nieobsługiwane operacje wracające do implementacji referencyjnych, arena umieszczona w wolnej pamięci lub topologia modelu, która pokonuje jednostkę wektorową (mnóstwo drobnych operacji, liczby kanałów niewyrównane do 8 lub 16). Profiluj, zanim zoptymalizujesz, i zawsze weryfikuj, które operacje są faktycznie przyspieszane.
Sprawdzenie zdrowego rozsądku budżetu mocy
ML na brzegu sieci jest przydatny tylko wtedy, gdy urządzenie nadal spełnia swój cel bateryjny. Dobra wiadomość: przy wyłączonym WiFi i radiu bramkowanym wynikami wnioskowania, ESP32-S3 większość czasu spędza w lekkim śnie przy około 0,8 mA, budzi się na okno mikrofonu, wykonuje MFCC plus wnioskowanie w około 25 ms przy mniej więcej 35 mA i wraca do snu. Dla detektora słów kluczowych próbkującego ciągle z przeskokiem 200 ms średni prąd wypada w okolicy 12–18 mA w zależności od cyklu pracy. Ogniwo 2000 mAh wystarczy na mniej więcej cztery miesiące ciągłego nasłuchiwania, dłużej, jeśli najpierw wybudzasz się na energię akustyczną.
Detektor akcelerometru jest jeszcze przyjaźniejszy. Zwykle wybudzamy się na progu ruchu, wykonujemy okno 320 ms i śpimy do następnego zaplanowanego sprawdzenia. Średni prąd mieści się w jednocyfrowych miliamperach, a ogniwo pierwotne CR123A ciągnie urządzenie przez wieloletnie wdrożenie. Zawsze mierz prawdziwym licznikiem kulombów na produkcyjnym PCB; symulacja to punkt wyjścia, a nie odpowiedź.
Czego ML na brzegu sieci nie rozwiązuje
Wnioskowanie na brzegu sieci nie eliminuje chmury. Nadal potrzebujesz scentralizowanego trenowania, wersjonowania modeli, wykrywania dryfu i pętli sprzężenia zwrotnego, która przechwytuje przypadki, w których model brzegowy się mylił. Chmura pozostaje też właściwym miejscem na każde rozumowanie wymagające kontekstu wielu urządzeń – jedno urządzenie widzi jedną maszynę, chmura widzi całą flotę. Nasza szersza praca nad platformą AI domyślnie zakłada podejście hybrydowe i przesuwa wnioskowanie na tę stronę łącza, która ma sens dla danego przypadku użycia.
Jeśli szacujesz wdrożenie TinyML – czy to czujnik w kabinie jachtu, urządzenie dostępowe w hotelu, czy przemysłowy węzeł drgań – budujemy takie rozwiązania kompleksowo na ESP32-S3 i pokrewnym krzemie. Zacznij od naszej usługi urządzeń podłączonych lub przejrzyj szersze możliwości IoT, które oferujemy.