In questo tutorial realizzerai lo sketch finale che implementerà la Rete Neurale già addestrata sulla scheda Arduino Nano 33 BLE.
Lo sketch ricalcherà in parte il codice già scritto per la lettura dei dati dell’IMU a fronte di una gesture, con la differenza che, anziché inviare al terminale seriale la sequenza di 714 valori (6 numeri per 119 campionamenti), questi valori saranno forniti in input alla Rete Neurale e sul terminale seriale sarà inviato il risultato della classificazione.
Il file model.h, che hai generato nel tutorial precedente, contiene una singola istruzione Arduino, cioè una singola istruzione in linguaggio C/C++ che dichiara e inizializza un array di numeri interi (lunghi un byte ciascuno).
Il contenuto di questo array di byte è esattamente la sequenza di numeri binari che costituiscono la nostra rete, rappresentata nel formato TensorFlow Lite a seguito dell’operazione di conversione del modello TensorFlow Keras, effettuata negli ultimi passaggi eseguiti nel corso del precedente tutorial.
Google offre una libreria sviluppata in C++ per implementare sui microcontrollori un “interprete” TensorFlow Lite in grado di eseguire le operazioni di “inferenza” di una rete neurale salvata in formato binario TensorFlow Lite.
Grazie a questa libreria possiamo fornire semplicemente il nostro modello di rete neurale sotto forma di aggregato di numeri binari senza preoccuparci delle effettive operazioni di algebra tensoriale che verranno effettuate durante l’esecuzione dell’algoritmo di inferenza che fornirà la classificazione delle gesture.
Anche quando abbiamo realizzato e addestrato la rete mediante la libreria Keras di TensorFlow sul Jupyter Notebook Colab abbiamo ragionato e scritto codice ad alto livello senza preoccuparci delle effettive operazioni matematiche. In quel caso, una volta addestrata la rete ci eravamo limitati ad invocare una funzione chiamata model.predict() a cui avevamo passato il tensore (un array) di dati da classificare.
La libreria TensorFlow conteneva l’implementazione di tutti i dettagli.
Grazie alla libreria C++ TensorFlow Lite Micro faremo qualcosa di molto simile nel nostro sketch, perché tutti i dettagli sulla sequenza di operazioni da eseguire saranno contenuti nella libreria stessa e nel modello convertito in formato TensorFlow Lite che incorporeremo nello sketch.
TensorFlow Lite è un progetto Google che ha l’obiettivo di portare una versione più leggera della piattaforma TensorFlow su dispositivi che dispongono di risorse limitate come smartphone, tablet, single-board computer e microcontrollori.
Nella realizzazione di questa piattaforma derivata, l’obiettivo progettuale è stato quello di privilegiare l’efficienza e l’ottimizzazione di risorse, sacrificando la chiarezza del codice e la facilità d’uso.
Ciò è evidente anche dal fatto che mentre la libreria madre TensorFlow offre delle API in linguaggio Python, eleganti, intuitive e di alto livello, le API della versione TensorFlowLite sono più tecniche e di difficile comprensione, sviluppate nei linguaggi nativi delle diverse piattaforme di destinazione (Java/Android, Swift/iOs, C++).
La versione destinata ai microcontrollori, che si chiama TensorFlow Lite Micro, è la più limitata di tutte e offre esclusivamente API in C/C++ dall’impronta molto tecnica.
Questa libreria è una collezione di file .h e .cpp che non è indirizzata a nessun modello di microcontrollore in particolare.
È compito dei singoli progetti adattare questa libreria alla propria piattaforma derivando dal repository principale che si trova a questo link la libreria effettiva da includere nel progetto.
Quando fu lanciata la piattaforma TFLite Micro, tra il 2018 e il 2019, il codice fu sperimentato utilizzando proprio l’Arduino Nano 33 BLE e il suo microcontrollore ad architettura ARM Cortex M4.
Il frutto di questo primo “esempio” di utilizzo fu una libreria compatibile con l’IDE Arduino, battezzata Arduino TensorFlowLite Example, il cui repository è ancora oggi raggiungibile a questo indirizzo. Questa libreria, nata come esempio e banco di prova del software, fu inserita nel registro ufficiale delle librerie Arduino (quello che consente l’installazione automatica dall’IDE) con il nome di Arduino_TensorFlowLite e mantenuta allineata per qualche tempo.
Quando nell’ottobre 2019 la libreria raggiunse la versione 2.4.0 ALPHA (l’indicazione ALPHA è sempre rimasta poiché si è sempre trattato di un lavoro ancora sperimentale), i brillanti Sandeep Mistry e Dominic Pajak realizzarono con tale versione il workshop originale da cui è derivata la sequenza di tutorial che stai seguendo, e ne pubblicarono le istruzioni sul Blog TensorFlow.
Una versione di questo workshop è stata inclusa come tutorial di livello “intermedio” nella documentazione ufficiale relativa alla scheda Nano 33 BLE, sul sito Arduino (Get Started With Machine Learning on Arduino).
Nel frattempo, TFLite Micro e TFLite è stato sperimentato e consolidato su numerose piattaforme, sia ad architettura ARM che ESP 32.
Fino al 2022, il repository della libreria Arduino TensorFlowLite Micro Example su GitHub è stato mantenuto automaticamente sincronizzato con il repository generale TensorFlow Lite Micro e le modifiche apportate alla libreria generale sono state propagate nel repository della libreria derivata, ma senza curare l’allineamento delle informazioni richieste dall’IDE Arduino, come ad esempio l’indicazione della versione, che ha continuato ad essere la 2.4.0-ALPHA anche se erano oramai state apportate modifiche significative.
Scaricando la libreria ARDUINO_TensorFlowLite dall’IDE Arduino, veniva installata l’effettiva originaria versione 2.3.0-ALPHA senza le ultime modifiche. Il codice allegato ai tutorial ufficiali di Arduino funziona solo con tale versione.
Il processo di sincronizzazione automatica di GitHub ha dei limiti e quando viene realizzato da un gruppo di progetto un adattamento del software, come è avvenuto nella definizione della libreria per Arduino, è necessario che venga costantemente garantito l’allineamento effettivo del codice.
Non sappiamo se ad un certo punto persone di Arduino o di TensorFlow si siano accorti del disallineamento tra l’evoluzione della versione Micro della piattaforma e la libreria Arduino, ma ciò che è certo è che un referente di Google TensorFlow ha chiesto di rimuovere la libreria dal registro dell’IDE Arduino e contemporaneamente il sync automatico del repository GitHub è stato sospeso.
Dal punto di vista di Google ciò è perfettamente in linea con la loro strategia generale. Google non mantiene nessun porting ufficiale della piattaforma TFLite Micro verso schede hardware specifiche, demandando questa attività ai singoli progetti.
Arduino ha aggiornato i testi dei turorial che facevano riferimento a questa libreria segnalando che essa non è più scaricabile dall’IDE in modo automatico, ma deve essere recuperata dal repository GitHub di TensorFlow, e installata secondo la procedura convenzionale.
Purtroppo la versione scaricabile dal repository, non è più quella originaria e non è compatibile con i tutorial ufficiali Arduino.
Per questo motivo, in occasione della predisposizione del nostro corso, abbiamo realizzato una nuova versione della libreria per Arduino e sviluppato lo sketch che vedrai nel video usando la versione aggiornata a giugno 2024, della libreria TensorFlowLite Micro.
La libreria CHIRALE_TensorFlowLite, è stata inserita nel repository ufficiale Arduino e viene installata automaticamente dall’IDE come qualunque altra libreria supportata.
La libreria Chirale_TensorFlowLite è stata registrata nel repository ufficiale Arduino poco dopo la registrazione del video.
Per installare la libreria puoi quindi procedere come per qualunque altra libreria supportata. Apri il “gestore librerie” dall’IDE e cerca “Chirale_TensorFlowLite“. Clicca quindi sul pulsante per installarla.

Come detto nel video, e come noterai eseguendo l’operazione di compilazione dall’IDE Arduino, la generazione del firmware che incorpora la libreria TensorFlow, richiede un tempo considerevole.
La prima volta che chiedi di effettuare la verifica, o il caricamento dello sketch su Arduino, l’intera libreria deve essere compilata. La libreria è molto articolata e di grandi dimensioni e l’operazione di compilazione, sui PC meno potenti può richiedere tempi superiori alle 2 ore!
Su un MacBook Pro recente, su un PC da “gamer” oppure sull’IDE Arduino WEB, il processo dura circa 6/7 minuti.
Dopo aver compilato lo sketch per la prima volta, l’IDE Arduino è sufficientemente smart da riutilizzare le parti della libreria già compilati, e il processo durerà tempi molto più accettabili.
Se il tuo PC non è potente, e non vuoi attendere tempi troppo lunghi, valuta l’opzione di usare l’IDE via WEB offerto dal cloud Arduino.
Il codice dello sketch è riportato nei riquadri seguenti, nelle due versioni per Arduino Nano 33 BLE Rev.1 e Rev.2.
/*
IMU Classifier
Questo sketch legge i valori di accelerazione lineare e angolare campionati dal sensore IMU
e utilizza il modello TensorFlow Lite importato tramite il file model.h
per identificare la gesture.
Il codice è originariamente sviluppato da Don Coleman e Sandeep Mistry, con contributi da
Dominic Pajak e incluso in diversi tutorial.
Questa versione è stata modificata da Spazio Chirale per adattare il codice
alla versione corrente della libreria TensorFloWLite Micro, mantenuta da Chirale
*** Questo è lo sketch per la versione Rev. 1 delle schede
- Arduino Nano 33 BLE or Arduino Nano 33 BLE Sense board.
***
This example code is in the public domain.
*/
#include <Arduino_LSM9DS1.h> // IMU usato nella Rev. 1 delle board Nano 33 BLE
#include <TensorFlowLite.h>
#include <tensorflow/lite/micro/all_ops_resolver.h>
#include <tensorflow/lite/micro/micro_interpreter.h>
#include <tensorflow/lite/schema/schema_generated.h>
#include "model.h"
const float accelerationThreshold = 2.5; // Soglia espressa in valori G
const int numSamples = 119; // numero di campioni in 1 secondo (l'IMU viene campionato di default a 119 Hz.)
int samplesRead; // variabile per tenere traccia del conteggio di campionamento
// inseriamo nel grafo di elaborazione tutti gli operatori previsti da tflite
// conoscendo in dettaglio il grafo di elaborazione, potremmo includere selettivamente
// solo gli operatori che effettivamente utiliuzziamo, riducendo l'occupazione di memoria
tflite::AllOpsResolver tflOpsResolver;
// dichiaro e inizializzo i puntatori al modello, all'interprete
// e ai tensori di ingresso e di uscita
const tflite::Model* tflModel = nullptr;
tflite::MicroInterpreter* tflInterpreter = nullptr;
TfLiteTensor* tflInputTensor = nullptr;
TfLiteTensor* tflOutputTensor = nullptr;
// Creo un buffer di memoria statico per il TensorFlow Lite Micro
// la dimensione è stata stimata. Solitamente si procede a tentativi per
// definire questo parametro, si parte con un valore grande e successivamente
// si prova e diminuire la dimensione finché il modello sembra funzionare bene...
constexpr int tensorArenaSize = 8 * 1024;
byte tensorArena[tensorArenaSize];
// array per mappare i nomi delle gesture
const char* GESTURES[] = {
"punch",
"flex"
};
#define NUM_GESTURES (sizeof(GESTURES) / sizeof(GESTURES[0]))
void setup() {
Serial.begin(9600);
while (!Serial);
// inizializzo l'IMU
if (!IMU.begin()) {
Serial.println("Inizializzazione del sensore IMU fallita!");
while (1);
}
// stampo i valori di configurazione dell'IMU
Serial.print("Frequenza di campionamento Acceleromero = ");
Serial.print(IMU.accelerationSampleRate());
Serial.println(" Hz");
Serial.print("Frequenza di campionamento Gyroscopio = ");
Serial.print(IMU.gyroscopeSampleRate());
Serial.println(" Hz");
Serial.println();
// recupero la rappresentazione come array di byte del modello TensorFlow Lite
// model è l'array costante definito nel file model.h
tflModel = tflite::GetModel(model);
// verifico che il modello fornito tramite l'array model abbia la stessa
// versione dello schema TensorFlow Lite della libreria che stiamo usando
// lo schema potrebbe essere stato generato con una versione di TensorFlow non
// più compatibile con la libreria TensorFlowLite utilizzata per questo sketch
if (tflModel->version() != TFLITE_SCHEMA_VERSION) {
Serial.println("Gli schemi del modello e della libreria hanno versioni diverse!");
while (1);
}
// Creo l'oggetto interprete per eseguire il modello
tflInterpreter = new tflite::MicroInterpreter(tflModel, tflOpsResolver, tensorArena, tensorArenaSize);
// Alloco la memoria per contenere i tensori di input e di output
tflInterpreter->AllocateTensors();
// Assegno i puntatori ai tensori di input e output del modello
tflInputTensor = tflInterpreter->input(0);
tflOutputTensor = tflInterpreter->output(0);
}
void loop() {
float aX, aY, aZ, gX, gY, gZ;
// se è la prima esecuzione di loop oppure ho già completato un ciclo
// di campionamento, attendo un movimento significativo
while (true) {
if (IMU.accelerationAvailable()) {
// leggo i valori dell'accelerazione
IMU.readAcceleration(aX, aY, aZ);
// calcolo la somma dei valori assoluti
float aSum = fabs(aX) + fabs(aY) + fabs(aZ);
// controllo se il valore assoluto dell'accelerazione complessiva
// sia superiore alla soglia
if (aSum >= accelerationThreshold) {
// inizializzo il conteggio dei campioni
samplesRead = 0;
break; // esco dal ciclo di attesa
}
}
}
// ciclo per leggere tutti i campionamenti relativi alla gesture in corso
while (samplesRead < numSamples) {
// verifico se i dati siano stati campionati
if (IMU.accelerationAvailable() && IMU.gyroscopeAvailable()) {
// leggo i valori
IMU.readAcceleration(aX, aY, aZ);
IMU.readGyroscope(gX, gY, gZ);
// normalizzo i valori letti nell'intervallo tra 0 e 1
// e li inserisco nel tensore di input
tflInputTensor->data.f[samplesRead * 6 + 0] = (aX + 4.0) / 8.0;
tflInputTensor->data.f[samplesRead * 6 + 1] = (aY + 4.0) / 8.0;
tflInputTensor->data.f[samplesRead * 6 + 2] = (aZ + 4.0) / 8.0;
tflInputTensor->data.f[samplesRead * 6 + 3] = (gX + 2000.0) / 4000.0;
tflInputTensor->data.f[samplesRead * 6 + 4] = (gY + 2000.0) / 4000.0;
tflInputTensor->data.f[samplesRead * 6 + 5] = (gZ + 2000.0) / 4000.0;
samplesRead++;
// se ho letto tutti i campionamenti, eseguo l'inferenza
// invocando l'interprete
if (samplesRead == numSamples) {
TfLiteStatus invokeStatus = tflInterpreter->Invoke();
if (invokeStatus != kTfLiteOk) {
Serial.println("Invoke fallito!");
return;
}
// Ciclo per visualizzare i valori in output
for (int i = 0; i < NUM_GESTURES; i++) {
Serial.print(GESTURES[i]);
Serial.print(": ");
Serial.print(tflOutputTensor->data.f[i]*100, 2);
Serial.println("%");
}
Serial.println();
}
}
}
}
/*
IMU Classifier
Questo sketch legge i valori di accelerazione lineare e angolare campionati dal sensore IMU
e utilizza il modello TensorFlow Lite importato tramite il file model.h
per identificare la gesture.
Il codice è originariamente sviluppato da Don Coleman e Sandeep Mistry, con contributi da
Dominic Pajak e incluso in diversi tutorial.
Questa versione è stata modificata da Spazio Chirale per adattare il codice
alla versione corrente della libreria TensorFloWLite Micro, mantenuta da Chirale
*** Questo è lo sketch per la versione Rev. 1 delle schede
- Arduino Nano 33 BLE or Arduino Nano 33 BLE Sense board.
***
This example code is in the public domain.
*/
#include <Arduino_BMI270_BMM150.h> // IMU usato nella Rev. 2 delle board Nano 33 BLE
#include <TensorFlowLite.h>
#include <tensorflow/lite/micro/all_ops_resolver.h>
#include <tensorflow/lite/micro/micro_interpreter.h>
#include <tensorflow/lite/schema/schema_generated.h>
#include "model.h"
const float accelerationThreshold = 2.5; // Soglia espressa in valori G
const int numSamples = 119; // numero di campioni in 1 secondo (l'IMU viene campionato di default a 119 Hz.)
int samplesRead; // variabile per tenere traccia del conteggio di campionamento
// inseriamo nel grafo di elaborazione tutti gli operatori previsti da tflite
// conoscendo in dettaglio il grafo di elaborazione, potremmo includere selettivamente
// solo gli operatori che effettivamente utiliuzziamo, riducendo l'occupazione di memoria
tflite::AllOpsResolver tflOpsResolver;
// dichiaro e inizializzo i puntatori al modello, all'interprete
// e ai tensori di ingresso e di uscita
const tflite::Model* tflModel = nullptr;
tflite::MicroInterpreter* tflInterpreter = nullptr;
TfLiteTensor* tflInputTensor = nullptr;
TfLiteTensor* tflOutputTensor = nullptr;
// Creo un buffer di memoria statico per il TensorFlow Lite Micro
// la dimensione è stata stimata. Solitamente si procede a tentativi per
// definire questo parametro, si parte con un valore grande e successivamente
// si prova e diminuire la dimensione finché il modello sembra funzionare bene...
constexpr int tensorArenaSize = 8 * 1024;
byte tensorArena[tensorArenaSize];
// array per mappare i nomi delle gesture
const char* GESTURES[] = {
"punch",
"flex"
};
#define NUM_GESTURES (sizeof(GESTURES) / sizeof(GESTURES[0]))
void setup() {
Serial.begin(9600);
while (!Serial);
// inizializzo l'IMU
if (!IMU.begin()) {
Serial.println("Inizializzazione del sensore IMU fallita!");
while (1);
}
// stampo i valori di configurazione dell'IMU
Serial.print("Frequenza di campionamento Acceleromero = ");
Serial.print(IMU.accelerationSampleRate());
Serial.println(" Hz");
Serial.print("Frequenza di campionamento Gyroscopio = ");
Serial.print(IMU.gyroscopeSampleRate());
Serial.println(" Hz");
Serial.println();
// recupero la rappresentazione come array di byte del modello TensorFlow Lite
// model è l'array costante definito nel file model.h
tflModel = tflite::GetModel(model);
// verifico che il modello fornito tramite l'array model abbia la stessa
// versione dello schema TensorFlow Lite della libreria che stiamo usando
// lo schema potrebbe essere stato generato con una versione di TensorFlow non
// più compatibile con la libreria TensorFlowLite utilizzata per questo sketch
if (tflModel->version() != TFLITE_SCHEMA_VERSION) {
Serial.println("Gli schemi del modello e della libreria hanno versioni diverse!");
while (1);
}
// Creo l'oggetto interprete per eseguire il modello
tflInterpreter = new tflite::MicroInterpreter(tflModel, tflOpsResolver, tensorArena, tensorArenaSize);
// Alloco la memoria per contenere i tensori di input e di output
tflInterpreter->AllocateTensors();
// Assegno i puntatori ai tensori di input e output del modello
tflInputTensor = tflInterpreter->input(0);
tflOutputTensor = tflInterpreter->output(0);
}
void loop() {
float aX, aY, aZ, gX, gY, gZ;
// se è la prima esecuzione di loop oppure ho già completato un ciclo
// di campionamento, attendo un movimento significativo
while (true) {
if (IMU.accelerationAvailable()) {
// leggo i valori dell'accelerazione
IMU.readAcceleration(aX, aY, aZ);
// calcolo la somma dei valori assoluti
float aSum = fabs(aX) + fabs(aY) + fabs(aZ);
// controllo se il valore assoluto dell'accelerazione complessiva
// sia superiore alla soglia
if (aSum >= accelerationThreshold) {
// inizializzo il conteggio dei campioni
samplesRead = 0;
break; // esco dal ciclo di attesa
}
}
}
// ciclo per leggere tutti i campionamenti relativi alla gesture in corso
while (samplesRead < numSamples) {
// verifico se i dati siano stati campionati
if (IMU.accelerationAvailable() && IMU.gyroscopeAvailable()) {
// leggo i valori
IMU.readAcceleration(aX, aY, aZ);
IMU.readGyroscope(gX, gY, gZ);
// normalizzo i valori letti nell'intervallo tra 0 e 1
// e li inserisco nel tensore di input
tflInputTensor->data.f[samplesRead * 6 + 0] = (aX + 4.0) / 8.0;
tflInputTensor->data.f[samplesRead * 6 + 1] = (aY + 4.0) / 8.0;
tflInputTensor->data.f[samplesRead * 6 + 2] = (aZ + 4.0) / 8.0;
tflInputTensor->data.f[samplesRead * 6 + 3] = (gX + 2000.0) / 4000.0;
tflInputTensor->data.f[samplesRead * 6 + 4] = (gY + 2000.0) / 4000.0;
tflInputTensor->data.f[samplesRead * 6 + 5] = (gZ + 2000.0) / 4000.0;
samplesRead++;
// se ho letto tutti i campionamenti, eseguo l'inferenza
// invocando l'interprete
if (samplesRead == numSamples) {
TfLiteStatus invokeStatus = tflInterpreter->Invoke();
if (invokeStatus != kTfLiteOk) {
Serial.println("Invoke fallito!");
return;
}
// Ciclo per visualizzare i valori in output
for (int i = 0; i < NUM_GESTURES; i++) {
Serial.print(GESTURES[i]);
Serial.print(": ");
Serial.print(tflOutputTensor->data.f[i]*100, 2);
Serial.println("%");
}
Serial.println();
}
}
}
}
Il codice ha la stessa struttura generale dello sketch che hai utilizzato per registrare i dati per l’addestramento della rete.
Dopo aver effettuato le operazioni di inizializzazione nella funzione setup(), viene richiamata la funzione loop(). Come prima cosa si entra in un ciclo while che termina solo se viene rilevata una accelerazione assoluta complessiva superiore alla soglia impostata.
Se questo accade, il programma entra in un altro ciclo while dove viene effettuata la lettura del sensore IMU per ricavare un totale di 119 gruppi di 6 valori (ax, ay, az, gx, gy, gz).
Questa volta, però, i valori non vengono visualizzati sul monitor seriale ma copiati all’interno dell’area di memoria che costituisce il tensore di input alla rete neurale.
Ogni esecuzione del ciclo while, i 6 valori float letti dall’IMU vengono inseriti nella posizione appropriata nel tensore.
Terminato il ciclo di lettura, il tensore di input è completamente valorizzato, per cui viene invocata l’esecuzione dell’operazione di inferenza.
Il risultato della classificazione è quindi contenuto nel tensore di output, sotto forma di una coppia di numeri float di valore compreso tra 0 e 1. Tali valori vengono letti, moltiplicati per 100 e visualizzati sul monitor seriale.
Come avrai notato, la sintassi utilizzata nelle istruzioni che riguardano il TensorFlow Lite non è la tipica sintassi semplificata in stile Arduino.
Si tratta della sintassi nativa del linguaggio C++ e soprattutto vengono utilizzati costrutti sintattici come i famigerati “puntatori”, il cui uso è fortemente sconsigliato dagli “Arduinisti” puri.
Questo è dovuto al fatto che la libreria TensorFlow Lite Micro è stata semplicemente adattata alla piattaforma Arduino senza scrivere una libreria “wrapper” che permetta di offrire API, cioè funzioni, nello stile Arduino, mascherando l’effettiva sintassi per la chiamata alle funzioni del TensorFlow Lite.
Le librerie Arduino sono solitamente scritte per interfacciare librerie standard, le cui API hanno stili molto più tecnici.
In questo caso, il progetto è ancora in una fase sperimentale, e nessuno ha scritto ancora una libreria in stile Arduino. Negli ultimi due anni il TensorFlow Lite sembra essere stabilizzato e probabilmente è giunto il momento che qualcuno effettui un’operazione del genere.
Nel frattempo, non farti spaventare dalle notazioni inusuali per un cultore di Arduino e procediamo ad analizzare in maggiore dettaglio il codice, poiché costituisce un buon esempio di come può essere utilizzata la libreria C++ TensorFlow Lite Micro.
Per implementare il modello di rete neurale su Arduino, per prima cosa devono essere inclusi i seguenti file:
#include <TensorFlowLite.h>
#include <tensorflow/lite/micro/all_ops_resolver.h>
#include <tensorflow/lite/micro/micro_interpreter.h>
#include <tensorflow/lite/schema/schema_generated.h>
Si tratta di file header che sono all’interno della libreria. Perché dobbiamo includere tutti questi file, mentre solitamente effettuiamo l'”include” solo per un unico file .h che ha il nome della libreria?
La risposta è che in realtà, la libreria TensorFlow Lite Micro non viene distribuita come libreria monolitica, ma come collezione strutturata di file sorgenti. Il compilatore specifico della piattaforma hardware utilizzata lavorerà su questi file e seguirà la struttura del codice per generare i file eseguibili che poi saranno assemblati, assieme al nostro codice, nel file binario che costituisce il firmware.
Uno dei motivi del lungo tempo di compilazione, risiede in questa struttura del codice della libreria, che, da un lato è molto flessibile e adattabile facilmente a qualunque microcontrollore, ma dall’altro è molto complessa.
Una volta inclusi i file header della libreria TFLite, deve essere incluso anche il file .h che contiene il nostro modello.
#include "model.h"
Sulle piattaforme dotate di fyle system, come gli smartphone o la RaspberryPi, il modello TFLite viene memorizzato come file binario sul disco virtuale del dispositivo (ricordi il file chiamato gesture_model.tflite che era stato creato su Google Colab dopo la conversione del modello Tensor Flow?) e poi caricato in memoria durante la fase di inizializzazione del dispositivo.
Arduino Nano 33 BLE non ha un fyle system esterno, tipo una SD Card, per cui non abbiamo potuto seguire questa strada.
Per questo motivo, nella sequenza di operazioni fatta attraverso il Jupyter Notebook eseguito su Google Colab, dopo aver generato il file binario gesture_model.tflite, abbiamo generato un file di testo, quindi in formato ASCII, chiamato model.h, abbiamo inserito una prima riga di testo con l’istruzione parziale: “const unsigned char model[] = {” e a seguire lo abbiamo riempito con il “dump” esadecimale del file gesture_model.tflite generato dall’utility xdd ed infine abbiamo aggiunto il completamento dell’istruzione scrivendo la stringa “};“
Il risultato è stato quello di creare un file .h contenente una singola istruzione C++ che definisce un array di byte inizializzato con una sfilza di numeri che sono esattamente il contenuto del file binario del modello.
In questo modo, includendo model.h nel nostro sketch, il compilatore si trova una struttura già pronta che sarà collocata nella memoria del microcontrollore all’atto del caricamento del firmware. Il nostro firmware, in altre parole, incorpora i dati del modello senza la necessità di leggere un file esterno in fase di setup.
L’istruzione che segue crea un oggetto contenente l’elenco degli operatori utilizzati dal nostro modello.
// inseriamo nel grafo di elaborazione tutti gli operatori previsti da tflite
// conoscendo in dettaglio il grafo di elaborazione, potremmo includere selettivamente
// solo gli operatori che effettivamente utiliuzziamo, riducendo l'occupazione di memoria
tflite::AllOpsResolver tflOpsResolver;
Nel nostro caso non siamo stati ad analizzare quali operatori sono effettivamente necessari per eseguire le operazioni di inferenza della nostra rete, per cui abbiamo creato un oggetto di tipo AllOpsResolver che si riferisce alla totalità degli operatori previsti dalla piattaforma TensorFlow Lite.
Avendo una conoscenza approfondita del funzionamento a basso livello della piattaforma TensorFlow Lite avremmo potuto costruire un elenco dei soli operatori effettivamente utilizzati e risparmiato un po’ di memoria in più.
Non siamo così specializzati nel TensorFlow da fare questa finezza e non abbiamo problemi di memoria con il nostro modello, per cui abbiamo proceduto con il default.
Per poter utilizzare il TensorFlow Lite, occorre gestire direttamente l’accesso alle aree di memoria dove risiede il nostro modello. Le righe seguenti dichiarano alcune variabili di tipo “puntatore” che serviranno a questo scopo.
// dichiaro e inizializzo i puntatori al modello, all'interprete
// e ai tensori di ingresso e di uscita
const tflite::Model* tflModel = nullptr;
tflite::MicroInterpreter* tflInterpreter = nullptr;
TfLiteTensor* tflInputTensor = nullptr;
TfLiteTensor* tflOutputTensor = nullptr;
tflModel sarà il puntatore all’area di memoria dove risiede il contenuto del’array model.h, per ora è inizializzato come puntatore a nulla.
tflInterpreter sarà il puntatore all’area di memoria dove risiederà il codice della funzione “interprete” che leggerà il contenuto del “modello” e avvierà l’esecuzione delle operazioni per effettuare il processo di calcolo chiamato “inferenza”. In pratica l’esecuzione della classificazione dell’input.
tflInputTensor sarà il puntatore all’area di memoria, chiamata tensore di input, in pratica l’area in cui dovrà essere collocato l’insieme dei dati che costituiscono l’input alla rete, nel nostro caso i 714 valori del campionamento di una gesture.
tflOutputTensor sarà il puntatore all’area di memoria, chiamata tensore di output, in pratica l’area dove troveremo il nostro risultato.
// Creo un buffer di memoria statico per il TensorFlow Lite Micro
// la dimensione è stata stimata. Solitamente si procede a tentativi per
// definire questo parametro, si parte con un valore grande e successivamente
// si prova e diminuire la dimensione finché il modello sembra funzionare bene...
constexpr int tensorArenaSize = 8 * 1024;
byte tensorArena[tensorArenaSize];
Queste istruzioni allocano un array di 8X1024 byte. Si tratta dell’area chiamata Tensor Arena, dove verranno collocati i tensori, cioè gli array multidimensionali che contengono i dati intermedi delle operazioni di inferenza.
La dimensione di quest’area viene solitamente stimata a tentativi. Nel nostro caso, con il valore 8X1024 = 8.192, ci siamo tenuti un po’ larghi confidando sulla generosa disponibilità di memoria del nostro Arduino.
Passiamo ad analizzare ciò che viene fatto nella funzione setup().
// recupero la rappresentazione come array di byte del modello TensorFlow Lite
// model è l'array costante definito nel file model.h
tflModel = tflite::GetModel(model);
Con questa istruzione viene inizializzato il puntatore tflModel all’area di memoria dove viene collocato il contenuto in binario del nostro modello.
Nei programmi TensorFlowLite tipici delle piattaforme che hanno il fyle system esterno, in questa fase viene letto il file binario relativo al modello e viene caricato in un array che viene poi passato alla funzione GetModel della libreria tflite. Nel nostro caso l’array è già pronto e precaricato in memoria grazie all’inclusione del file model.h nel nostro firmware.
// verifico che il modello fornito tramite l'array model abbia la stessa
// versione dello schema TensorFlow Lite della libreria che stiamo usando
// lo schema potrebbe essere stato generato con una versione di TensorFlow non
// più compatibile con la libreria TensorFlowLite utilizzata per questo sketch
if (tflModel->version() != TFLITE_SCHEMA_VERSION) {
Serial.println("Gli schemi del modello e della libreria hanno versioni diverse!");
while (1);
}
Questa istruzione verifica che il modello che abbiamo acquisito sia compatibile con la versione della libreria TensorFlow che stiamo usando.
Il modello che abbiamo generato sul Jupyter Notebook con la versione 2.15.0 del TensorFlow, come hai visto nel video, è compatibile con la libreria CHIRALE_TensorFlowLite che è stata usata per questo sketch, per cui se dovessi ottenere in fase di esecuzione un errore in questo controllo, ti sarà sufficiente modificare il Jupyter Notebook in modo da richiedere la versione 2.15 di TensorFlow. Secondo le nostre previsioni tutte le versioni 2.x, quindi anche le successive alla 2.15 dovrebbero essere compatibili.
// Creo l'oggetto interprete per eseguire il modello
tflInterpreter = new tflite::MicroInterpreter(tflModel, tflOpsResolver, tensorArena, tensorArenaSize);
In questa istruzione viene creato un oggetto di tipo MicroInterpreter attraverso la chiamata ad una funzione “costruttore” a cui vengono passati i puntatori al modello, al catalogo degli operatori previsti e all’area TensorArena, della quale viene anche passata la dimensione.
// Alloco la memoria per contenere i tensori di input e di output
tflInterpreter->AllocateTensors();
Il commento riportato in questa istruzione descrive perfettamente quello che viene fatto.
// Assegno i puntatori ai tensori di input e output del modello
tflInputTensor = tflInterpreter->input(0);
tflOutputTensor = tflInterpreter->output(0);
Dopo aver allocato la memoria relativa ai tensori di input e di output, inizializzo, con queste due istruzioni, i puntatori ai due tensori. tflInputTensor da questo momento in poi punta al primo byte dell’area riservata al tensore di input (tflInterpreter->input(0)) e tflOutputTensor punta al primo byte dell’area riservata al tensore di output (tflInterpreter->output(0)).
La funzione setup() è terminata e in fase di inizializzazione sono state quindi allocate tutte le aree di memoria riservate alla rete neurale e ai suoi operatori e sono state riempite con i rispettivi valori ricavati dal nostro formato binario del modello. Nella RAM del microcontrollore a questo punto c’è sia il codice eseguibile che implementa l’esecuzione degli algoritmi di inferenza, sia la rappresentazione della rete neurale con i suoi parametri già addestrati.
Nella funzione loop() le istruzioni da spiegare sono le seguenti.
// normalizzo i valori letti nell'intervallo tra 0 e 1
// e li inserisco nel tensore di input
tflInputTensor->data.f[samplesRead * 6 + 0] = (aX + 4.0) / 8.0;
tflInputTensor->data.f[samplesRead * 6 + 1] = (aY + 4.0) / 8.0;
tflInputTensor->data.f[samplesRead * 6 + 2] = (aZ + 4.0) / 8.0;
tflInputTensor->data.f[samplesRead * 6 + 3] = (gX + 2000.0) / 4000.0;
tflInputTensor->data.f[samplesRead * 6 + 4] = (gY + 2000.0) / 4000.0;
tflInputTensor->data.f[samplesRead * 6 + 5] = (gZ + 2000.0) / 4000.0;
Con queste istruzioni, vengono aggiunti, ad ogni ciclo di campionamento, i sei valori di tipo float, letti dal sensore IMU, nella posizione opportuna all’interno dell’area di memoria riservata al tensore di input. In particolare, l’indice tra parentesi quadre definisce la posizione in cui inserire il numero float. I valori vengono normalizzati tra zero e 1.
Se, ad esempio abbiamo appena letto la terza sestina di valori, la variabile samplesRead avrà valore 2 (la prima volta ha valore 0). Le prime 2X6 = 12 posizioni saranno già occupate dalle prime due sestine di valori, per cui il valore normalizzato di ax dovrà essere inserito nella posizione il cui indice è 6X2+0 = 12 (gli indici partono da zero), il valore normalizzato in ay andrà inserito nella posizione il cui indice è 6X2+1 = 13, e così via…
Una volta terminata la raccolta delle 119 sestine di valori, il tensore di input è completamente valorizzato con i 714 valori e può essere invocata l’operazione di inferenza.
// se ho letto tutti i campionamenti, eseguo l'inferenza
// invocando l'interprete
if (samplesRead == numSamples) {
TfLiteStatus invokeStatus = tflInterpreter->Invoke();
if (invokeStatus != kTfLiteOk) {
Serial.println("Invoke fallito!");
return;
}
A questo punto posso recuperare i valori presenti nel tensore di output e inviarli al terminale seriale dopo averli moltiplicati per 100 in modo da mostrare un valore percentuale più chiaro.
// Ciclo per visualizzare i valori in output
for (int i = 0; i < NUM_GESTURES; i++) {
Serial.print(GESTURES[i]);
Serial.print(": ");
Serial.print(tflOutputTensor->data.f[i]*100, 2);
Serial.println("%");
}