IoT et IA

TensorFlow Lite Micro : premiers pas sur un microcontrôleur avec sortie MQTT

Une introduction pratique à TensorFlow Lite for Microcontrollers : le modèle, l'op resolver, l'arène de tenseurs et l'interpréteur expliqués, un exemple complet sur une carte ESP32-S3 et un pont série vers MQTT qui fonctionne avec n'importe quelle carte.

Illustration de prise en main de TensorFlow Lite Micro montrant un modèle quantifié chargé dans un microcontrôleur et des prédictions envoyées vers un broker MQTT
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

TensorFlow Lite Micro est le runtime utilisé par la plupart des projets TinyML pour exécuter des réseaux de neurones sur des microcontrôleurs, et son API est réduite une fois que l'on comprend quatre objets : le modèle, l'op resolver, l'arène de tenseurs et l'interpréteur. Ce guide de prise en main explique chacun d'eux, déroule un exemple complet sur une carte ESP32-S3 et montre comment envoyer les prédictions vers un broker MQTT, soit directement en Wi-Fi, soit via un pont série pour les cartes sans connectivité réseau.

Qu'est-ce que TensorFlow Lite Micro

TensorFlow Lite for Microcontrollers (souvent abrégé TFLM, et désormais intégré à la famille LiteRT de Google) est un moteur d'inférence C++ conçu pour des appareils sans système d'exploitation, sans allocation dynamique de mémoire pendant l'inférence et avec très peu de RAM. Il lit le même format flatbuffer .tflite que TensorFlow Lite sur téléphone, mais prend en charge un ensemble plus restreint d'opérateurs et se compile directement dans votre firmware.

TFLM ne fait que de l'inférence. L'entraînement a lieu dans TensorFlow sur un ordinateur ; le microcontrôleur reçoit un modèle converti, généralement quantifié en int8. Pour comprendre plus largement pourquoi on exécute du ML sur des puces aussi petites, consultez ce qu'est le TinyML.

Les quatre concepts clés

Objet Ce que c'est Ce qui peut mal tourner
tflite::Model Le flatbuffer, intégré en flash sous forme de tableau d'octets Version de schéma incompatible avec la bibliothèque
MicroMutableOpResolver<N> La liste des noyaux d'opérateurs liés au firmware Opérateur manquant à l'exécution ; paramètre de template N trop petit
Arène de tenseurs Un tampon d'octets statique pour les entrées, sorties et tenseurs intermédiaires AllocateTensors() échoue lorsqu'elle est trop petite
MicroInterpreter Relie le modèle, le resolver et l'arène, et exécute Invoke() Entrée écrite en flottant alors que le modèle attend de l'int8

Un programme typique effectue les opérations suivantes une seule fois au démarrage : mapper le modèle, enregistrer les opérateurs qu'il utilise, construire l'interpréteur, allouer les tenseurs. Puis, pour chaque inférence : écrire le tenseur d'entrée, appeler Invoke(), lire le tenseur de sortie. C'est toute l'API du runtime dont vous aurez besoin pour la plupart des projets.

Étape 1 : entraîner et convertir un modèle

Le premier modèle TFLM classique apprend sin(x). Il est petit, facile à vérifier à l'œil et met en jeu une entrée et une sortie quantifiées. Ce script l'entraîne, le convertit entièrement en int8 et écrit les fichiers source C++ pour le firmware.

pip install tensorflow numpy
# train_sine.py
import numpy as np
import tensorflow as tf

rng = np.random.default_rng(1)
x = rng.uniform(0, 2 * np.pi, 3000).astype(np.float32)
y = (np.sin(x) + 0.05 * rng.standard_normal(x.size)).astype(np.float32)

model = tf.keras.Sequential([
    tf.keras.Input(shape=(1,)),
    tf.keras.layers.Dense(16, activation="relu"),
    tf.keras.layers.Dense(16, activation="relu"),
    tf.keras.layers.Dense(1),
])
model.compile(optimizer="adam", loss="mse")
model.fit(x, y, epochs=200, batch_size=64, validation_split=0.2, verbose=0)
print("val mse:", model.evaluate(x[:500], y[:500], verbose=0))


def rep():
    for v in np.linspace(0, 2 * np.pi, 300, dtype=np.float32):
        yield [np.array([[v]], dtype=np.float32)]


conv = tf.lite.TFLiteConverter.from_keras_model(model)
conv.optimizations = [tf.lite.Optimize.DEFAULT]
conv.representative_dataset = rep
conv.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
conv.inference_input_type = tf.int8
conv.inference_output_type = tf.int8
tfl = conv.convert()
print("model bytes:", len(tfl))

with open("tflm_sine/main/model_data.cc", "w") as f:
    f.write('#include "model_data.h"\n')
    f.write("alignas(16) const unsigned char g_model[] = {%s};\n" % ", ".join(f"0x{b:02x}" for b in tfl))
    f.write(f"const unsigned int g_model_len = {len(tfl)};\n")
with open("tflm_sine/main/model_data.h", "w") as f:
    f.write("#pragma once\nextern const unsigned char g_model[];\nextern const unsigned int g_model_len;\n")

Avant d'écrire le firmware, inspectez les opérateurs du modèle converti. Ouvrir le fichier .tflite dans un visualiseur de modèles comme Netron ou utiliser tf.lite.experimental.Analyzer.analyze(model_content=tfl) montre que ce modèle ne contient que des opérateurs FULLY_CONNECTED. Cette liste correspond exactement à ce que vous enregistrez dans le resolver.

Pourquoi quantifier pour TensorFlow Lite Micro

Les microcontrôleurs sont bien plus rapides en arithmétique entière qu'en virgule flottante, et beaucoup n'ont même pas d'unité flottante. La quantification entièrement int8 stocke chaque poids sur un octet au lieu de quatre et exécute chaque couche avec des noyaux entiers, ce qui réduit l'occupation de la flash, réduit l'arène de tenseurs et permet aux bibliothèques optimisées comme CMSIS-NN ou les noyaux d'Espressif de faire leur travail. Le prix à payer est une légère perte de précision et l'étape supplémentaire de conversion des entrées et sorties avec le scale et le zero_point de chaque tenseur, ce que le firmware ci-dessous fait explicitement.

Flash et RAM : où réside le modèle

Le tableau du modèle est déclaré const : l'éditeur de liens le place donc en flash et il ne coûte aucune RAM. Le coût en RAM, c'est l'arène de tenseurs : entrées, sorties, activations intermédiaires et une partie de la gestion interne de l'interpréteur. Sur la famille ESP32, la RAM interne est l'option rapide ; les très grandes arènes peuvent aller en PSRAM sur les cartes qui en sont équipées, au prix d'une certaine perte de vitesse. Pour ce modèle sinus, l'arène nécessite bien moins que les 4 Ko réservés ci-dessous, ce que vous pouvez confirmer avec la ligne de journal affichée par le firmware.

Étape 2 : configurer le projet de la carte

Ce guide utilise une carte de développement ESP32-S3, également programmable depuis l'Arduino IDE, mais compile avec ESP-IDF, car le composant esp-tflite-micro d'Espressif suit l'API TFLM amont et ajoute des noyaux optimisés. Sur d'autres cartes, le code C++ de l'étape suivante reste identique ; seul le système de compilation change.

tflm_sine/
  CMakeLists.txt
  main/
    CMakeLists.txt
    main.cc
    model_data.cc   (generated)
    model_data.h    (generated)
# tflm_sine/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(tflm_sine)
# tflm_sine/main/CMakeLists.txt
idf_component_register(SRCS "main.cc" "model_data.cc" INCLUDE_DIRS ".")
cd tflm_sine
idf.py set-target esp32s3
idf.py add-dependency "espressif/esp-tflite-micro"

Étape 3 : exécuter l'inférence avec TensorFlow Lite Micro

Le firmware parcourt x de 0 à 2π, quantifie chaque valeur avec l'échelle et le point zéro du tenseur d'entrée, exécute le modèle et affiche une ligne JSON par prédiction sur la console série.

// tflm_sine/main/main.cc
#include <algorithm>
#include <cmath>
#include <cstdio>

#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/schema/schema_generated.h"

#include "model_data.h"

static const char *TAG = "tflm";
static constexpr int kArenaSize = 4 * 1024;
alignas(16) static uint8_t tensor_arena[kArenaSize];

extern "C" void app_main(void) {
  // 1. Map the model
  const tflite::Model *model = tflite::GetModel(g_model);
  if (model->version() != TFLITE_SCHEMA_VERSION) {
    ESP_LOGE(TAG, "model schema %lu != runtime %d", (unsigned long)model->version(), TFLITE_SCHEMA_VERSION);
    return;
  }

  // 2. Register only the operators the model uses
  static tflite::MicroMutableOpResolver<3> resolver;
  resolver.AddFullyConnected();
  resolver.AddQuantize();
  resolver.AddDequantize();

  // 3. Build the interpreter over a static arena and allocate tensors
  static tflite::MicroInterpreter interpreter(model, resolver, tensor_arena, kArenaSize);
  if (interpreter.AllocateTensors() != kTfLiteOk) {
    ESP_LOGE(TAG, "AllocateTensors failed, increase kArenaSize");
    return;
  }
  ESP_LOGI(TAG, "arena used %u of %d bytes", (unsigned)interpreter.arena_used_bytes(), kArenaSize);

  TfLiteTensor *input = interpreter.input(0);
  TfLiteTensor *output = interpreter.output(0);
  const float two_pi = 2.0f * (float)M_PI;
  int step = 0;

  while (true) {
    float x = two_pi * (step % 40) / 40.0f;

    // 4. Quantize the input: q = x / scale + zero_point
    int q = (int)lroundf(x / input->params.scale) + input->params.zero_point;
    input->data.int8[0] = (int8_t)std::min(127, std::max(-128, q));

    int64_t t0 = esp_timer_get_time();
    if (interpreter.Invoke() != kTfLiteOk) {
      ESP_LOGE(TAG, "Invoke failed");
      break;
    }
    int us = (int)(esp_timer_get_time() - t0);

    // 5. Dequantize the output: y = (q - zero_point) * scale
    float y = (output->data.int8[0] - output->params.zero_point) * output->params.scale;
    printf("{\"x\":%.3f,\"y\":%.3f,\"ref\":%.3f,\"us\":%d}\n", x, y, sinf(x), us);

    step++;
    vTaskDelay(pdMS_TO_TICKS(500));
  }
}
python train_sine.py
cd tflm_sine
idf.py build flash monitor

Le moniteur affiche des lignes comme {"x":1.571,"y":0.98,"ref":1.000,"us":...}. La valeur y prédite doit suivre de près ref, avec de petites erreurs dues à la quantification. Si toutes les sorties ont la même valeur, l'entrée est presque certainement écrite sans quantification.

Étape 4 : envoyer les prédictions via MQTT

Il existe deux façons d'acheminer ces prédictions vers un broker.

Directement en Wi-Fi. Sur les cartes Wi-Fi comme l'ESP32-S3, ajoutez le client MQTT d'ESP-IDF et publiez chaque ligne JSON au lieu de l'afficher. Le tutoriel TinyML sur ESP32 contient un firmware complet qui fait exactement cela, y compris le Last Will et la logique de publication sur changement.

Pont série. De nombreuses cartes TFLM populaires, comme les cartes Cortex-M dotées de capteurs embarqués, n'ont pas de Wi-Fi. Elles peuvent afficher du JSON sur le port série USB, et un ordinateur ou un Raspberry Pi le relaie. Ce pont fonctionne avec le firmware ci-dessus sans modification :

pip install pyserial "paho-mqtt>=2.0"
# serial_to_mqtt.py  usage: python serial_to_mqtt.py COM5   (or /dev/ttyACM0)
import json
import sys
import uuid

import paho.mqtt.client as mqtt
import serial

port = sys.argv[1]
run = uuid.uuid4().hex[:8]
topic = f"connectmq/demo/{run}/lab/tflm-board/inference"

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"tflm-bridge-{run}")
client.will_set(f"connectmq/demo/{run}/lab/tflm-board/status", "offline", qos=1, retain=True)
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.loop_start()
client.publish(f"connectmq/demo/{run}/lab/tflm-board/status", "online", qos=1, retain=True)
print("publishing to", topic)

with serial.Serial(port, 115200, timeout=2) as ser:
    while True:
        line = ser.readline().decode("utf-8", errors="ignore").strip()
        if not line.startswith("{"):
            continue                      # skip boot and log lines
        try:
            json.loads(line)              # forward only valid JSON
        except json.JSONDecodeError:
            continue
        client.publish(topic, line, qos=0)
        print(line)

Fermez d'abord idf.py monitor, car un seul programme peut occuper le port série. Suivez ensuite le flux depuis n'importe quelle machine :

mosquitto_sub -h broker.connectmq.com -p 1883 -i tflm-viewer-12 -t 'connectmq/demo/+/lab/tflm-board/#' -v

Le serveur MQTT public gratuit broker.connectmq.com convient parfaitement ici, à condition de garder à l'esprit que n'importe qui peut lire la zone publique. Les appareils de production doivent utiliser des identifiants privés et TLS sur le port 8883 ; dans le pont, cela signifie appeler client.tls_set() et client.username_pw_set() avec les valeurs de votre broker ConnectMQ et utiliser le client ID attribué.

Le schéma du pont a un effet secondaire utile : le firmware de la carte reste exempt de code réseau, de certificats et d'identifiants, si bien que vous pouvez changer de broker, ajouter TLS ou modifier les topics sur la passerelle sans rien reflasher. La contrepartie est un appareil de plus à maintenir en fonctionnement. Pour quelques cartes de laboratoire sur un ordinateur, c'est idéal ; pour un parc sur le terrain, des cartes avec Wi-Fi intégré qui publient directement sont généralement plus simples à exploiter.

Choisir ce qu'il faut publier

Un modèle de régression comme celui-ci produit une valeur à chaque appel, mais la plupart des applications réelles s'intéressent aux événements : une valeur qui franchit une limite, une classe qui change ou une confiance qui chute. Décidez sur l'appareil ou sur le pont de ce qui constitue un événement et publiez-les, avec un heartbeat lent. Cela maintient le trafic bien en deçà des limites de débit par client et simplifie les consommateurs en aval.

Dépannage de TensorFlow Lite Micro

  1. Didn't find op for builtin opcode : enregistrez l'opérateur manquant et augmentez le paramètre de template du resolver.
  2. AllocateTensors() échoue : agrandissez l'arène, puis réduisez-la à arena_used_bytes() plus une marge une fois que tout fonctionne.
  3. Hard fault ou plantage dans Invoke() : le tableau du modèle n'est pas aligné, ou l'arène chevauche une autre zone mémoire. Conservez alignas(16) sur les deux.
  4. Précision bien inférieure à celle obtenue sur PC : le jeu de données représentatif ne couvrait pas la plage d'entrée réelle, et la quantification a écrêté des valeurs. Utilisez de vrais échantillons dans representative_dataset.
  5. Erreurs de compilation concernant MicroErrorReporter ou AllOpsResolver : vous mélangez d'anciens exemples de code avec une version plus récente de TFLM. Les versions actuelles utilisent le constructeur présenté ci-dessus et MicroMutableOpResolver.

Pour aller plus loin

Remplacez le modèle sinus par un classificateur alimenté par de vraies caractéristiques de capteurs, comme dans TinyML sur ESP32, ou laissez une plateforme générer le pipeline pour vous avec le tutoriel Edge Impulse MQTT. Le dépôt officiel TensorFlow Lite Micro documente les opérateurs pris en charge et le portage vers de nouvelles cartes. Pour le contexte architectural, le guide IA et IoT explique comment les modèles sur microcontrôleur s'articulent avec l'analytique cloud.

Démarrer avec ConnectMQ

Que les prédictions quittent votre carte en Wi-Fi ou via un pont, elles ont besoin d'une destination fiable. ConnectMQ est un broker MQTT managé avec TLS, MQTT 3.1.1 et 5.0, et un client MQTT intégré au navigateur pour vérifier les messages dès leur arrivée. Créez un broker avec l'offre gratuite Serverless sur app.connectmq.com, sans carte bancaire.

Questions fréquentes

Quelle est la différence entre TensorFlow Lite et TensorFlow Lite Micro ?

TensorFlow Lite cible les téléphones et les appareils Linux dotés d'un système d'exploitation et de mémoire dynamique. TensorFlow Lite Micro est un runtime allégé pour microcontrôleurs qui utilise une arène statique, un ensemble d'opérateurs réduit et aucune dépendance à un OS.

Comment choisir la taille de l'arène de tenseurs ?

Commencez avec une taille généreuse, appelez AllocateTensors(), puis lisez arena_used_bytes() et réglez l'arène sur cette valeur plus une marge de sécurité. Le besoin dépend des plus grands tenseurs intermédiaires du modèle.

TensorFlow Lite Micro prend-il en charge les modèles flottants ?

Oui, de nombreux opérateurs disposent de noyaux flottants, mais les modèles int8 sont plus petits et beaucoup plus rapides sur la plupart des microcontrôleurs. La quantification entière complète est le choix normal en production.

TensorFlow Lite Micro peut-il envoyer lui-même des données via MQTT ?

Non. TFLM ne fait que de l'inférence. Vous publiez les résultats avec un client MQTT séparé, comme le client MQTT d'ESP-IDF, PubSubClient ou un pont série sur une passerelle.

Articles liés

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.