IoT e IA

TensorFlow Lite Micro: primeros pasos en un microcontrolador con salida MQTT

Una introducción práctica a TensorFlow Lite for Microcontrollers: el modelo, el op resolver, la tensor arena y el intérprete explicados, un ejemplo completo en una placa ESP32-S3 y un puente de serie a MQTT que funciona con cualquier placa.

Ilustración de primeros pasos con TensorFlow Lite Micro: un modelo cuantizado cargado en un microcontrolador y predicciones enviadas a un broker MQTT
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

TensorFlow Lite Micro es el runtime que usan la mayoría de los proyectos TinyML para ejecutar redes neuronales en microcontroladores, y su API resulta pequeña en cuanto entiendes cuatro objetos: el modelo, el op resolver, la tensor arena y el intérprete. Esta guía de primeros pasos explica cada uno de ellos, recorre un ejemplo completo en una placa ESP32-S3 y muestra cómo enviar las predicciones a un broker MQTT, ya sea directamente por Wi-Fi o mediante un puente serie para placas sin conectividad de red.

Qué es TensorFlow Lite Micro

TensorFlow Lite for Microcontrollers (a menudo abreviado como TFLM y ahora parte de la familia LiteRT de Google) es un motor de inferencia en C++ diseñado para dispositivos sin sistema operativo, sin asignación dinámica de memoria durante la inferencia y con muy poca RAM. Lee el mismo formato flatbuffer .tflite que TensorFlow Lite en los móviles, pero admite un conjunto más reducido de operadores y está pensado para compilarse directamente dentro de tu firmware.

TFLM solo hace inferencia. El entrenamiento se realiza en TensorFlow en un ordenador; el microcontrolador recibe un modelo convertido y, normalmente, cuantizado a int8. Si quieres entender en conjunto por qué alguien ejecuta ML en chips tan pequeños, consulta qué es TinyML.

Los cuatro conceptos clave

Objeto Qué es Qué puede fallar
tflite::Model El flatbuffer, incrustado en la flash como array de bytes Versión de esquema incompatible con la librería
MicroMutableOpResolver<N> La lista de kernels de operadores enlazados en el firmware Falta un operador en tiempo de ejecución; la plantilla N es demasiado pequeña
Tensor arena Un búfer de bytes estático para entradas, salidas y tensores intermedios AllocateTensors() falla cuando es demasiado pequeña
MicroInterpreter Une el modelo, el resolver y la arena y ejecuta Invoke() Entrada escrita en float cuando el modelo espera int8

Un programa típico hace lo siguiente una sola vez al arrancar: mapear el modelo, registrar los operadores que usa, construir el intérprete y reservar los tensores. Después, en cada inferencia: escribir el tensor de entrada, llamar a Invoke() y leer el tensor de salida. Esa es toda la API del runtime que necesitas para la mayoría de los proyectos.

Paso 1: entrenar y convertir un modelo

El primer modelo clásico de TFLM aprende sin(x). Es pequeño, fácil de verificar a simple vista y pone a prueba la entrada y la salida cuantizadas. Este script lo entrena, lo convierte a int8 completo y escribe archivos fuente C++ para el 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")

Antes de escribir el firmware, revisa los operadores del modelo convertido. Abrir el archivo .tflite en un visor de modelos como Netron o usar tf.lite.experimental.Analyzer.analyze(model_content=tfl) muestra que este modelo solo contiene operadores FULLY_CONNECTED. Esa lista es exactamente lo que registras en el resolver.

Por qué cuantizar para TensorFlow Lite Micro

Los microcontroladores son mucho más rápidos con aritmética entera que con coma flotante, y muchos ni siquiera tienen unidad de coma flotante. La cuantización int8 completa guarda cada peso en un byte en lugar de cuatro y ejecuta todas las capas con kernels enteros, lo que reduce el uso de flash, reduce la tensor arena y permite que librerías optimizadas como CMSIS-NN o los kernels de Espressif hagan su trabajo. El coste es una pequeña pérdida de precisión y el paso extra de convertir entradas y salidas con el scale y el zero_point de cada tensor, algo que el firmware siguiente hace de forma explícita.

Flash y RAM: dónde vive el modelo

El array del modelo se declara const, así que el enlazador lo coloca en la flash y no consume RAM. La tensor arena es el coste en RAM: entradas, salidas, activaciones intermedias y parte de la gestión interna del intérprete. En la familia ESP32, la RAM interna es la opción rápida; las arenas muy grandes pueden ir a la PSRAM en placas que la tengan, con cierta penalización de velocidad. Para este modelo del seno, la arena necesita bastante menos de los 4 KB reservados más abajo, algo que puedes confirmar con la línea de log que imprime el firmware.

Paso 2: preparar el proyecto de la placa

Esta guía usa una placa de desarrollo ESP32-S3, que también se puede programar desde el Arduino IDE, pero compila con ESP-IDF porque el componente esp-tflite-micro de Espressif sigue la API original de TFLM y añade kernels optimizados. En otras placas, el código C++ del siguiente paso es el mismo; solo cambia el sistema de compilación.

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"

Paso 3: ejecutar la inferencia con TensorFlow Lite Micro

El firmware recorre x de 0 a 2π, cuantiza cada valor con la escala y el punto cero del tensor de entrada, ejecuta el modelo e imprime una línea JSON por predicción en la consola serie.

// 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. Mapear el modelo
  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. Registrar solo los operadores que usa el modelo
  static tflite::MicroMutableOpResolver<3> resolver;
  resolver.AddFullyConnected();
  resolver.AddQuantize();
  resolver.AddDequantize();

  // 3. Construir el intérprete sobre una arena estática y reservar los tensores
  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. Cuantizar la entrada: 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. Descuantizar la salida: 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

El monitor muestra líneas como {"x":1.571,"y":0.98,"ref":1.000,"us":...}. La y predicha debería seguir de cerca a ref, con pequeños errores debidos a la cuantización. Si todas las salidas tienen el mismo valor, es casi seguro que la entrada se está escribiendo sin cuantizar.

Paso 4: enviar las predicciones por MQTT

Hay dos formas de llevar estas predicciones a un broker.

Directamente por Wi-Fi. En placas con Wi-Fi como el ESP32-S3, añade el cliente MQTT de ESP-IDF y publica cada línea JSON en lugar de imprimirla. El tutorial de TinyML en ESP32 contiene un firmware completo que hace exactamente eso, incluidos el Last Will y la lógica de publicar solo al cambiar.

Puente serie. Muchas placas populares para TFLM, como las placas Cortex-M con sensores integrados, no tienen Wi-Fi. Pueden imprimir JSON por el puerto serie USB y un ordenador o una Raspberry Pi lo reenvía. Este puente funciona con el firmware anterior sin cambios:

pip install pyserial "paho-mqtt>=2.0"
# serial_to_mqtt.py  uso: python serial_to_mqtt.py COM5   (o /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                      # omitir líneas de arranque y de log
        try:
            json.loads(line)              # reenviar solo JSON válido
        except json.JSONDecodeError:
            continue
        client.publish(topic, line, qos=0)
        print(line)

Cierra antes idf.py monitor, ya que solo un programa puede ocupar el puerto serie. Después observa el flujo desde cualquier máquina:

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

El servidor MQTT público gratuito sirve para estas pruebas, siempre que recuerdes que cualquiera puede leer el área pública y que no es para datos de producción. Los dispositivos de producción deben usar credenciales privadas y TLS en el puerto 8883; en el puente, eso significa llamar a client.tls_set() y client.username_pw_set() con los valores de tu broker de ConnectMQ y usar el client ID emitido.

El patrón de puente tiene un efecto secundario útil: el firmware de la placa queda libre de código de red, certificados y credenciales, así que puedes cambiar de broker, añadir TLS o cambiar los topics en el gateway sin volver a flashear nada. La contrapartida es un dispositivo más que mantener en marcha. Para un puñado de placas de laboratorio conectadas a un ordenador es ideal; para una flota sobre el terreno, las placas con Wi-Fi integrado que publican directamente suelen ser más sencillas de operar.

Elegir qué publicar

Un modelo de regresión como este produce un valor en cada llamada, pero a la mayoría de las aplicaciones reales les interesan los eventos: un valor que supera un límite, una clase que cambia o una confianza que cae. Decide en el dispositivo o en el puente qué cuenta como evento y publica esos eventos, más un latido lento. Así el tráfico queda holgadamente dentro de los límites de frecuencia por cliente y los consumidores posteriores son más sencillos.

Solución de problemas de TensorFlow Lite Micro

  1. Didn't find op for builtin opcode: registra el operador que falta y aumenta el contador de la plantilla del resolver.
  2. AllocateTensors() falla: amplía la arena y, cuando funcione, redúcela a arena_used_bytes() más algo de margen.
  3. Hard fault o bloqueo en Invoke(): el array del modelo no está alineado o la arena se solapa con otra memoria. Mantén alignas(16) en ambos.
  4. Precisión mucho peor que en el PC: el conjunto de datos representativo no cubría el rango real de entradas, así que la cuantización recortó valores. Usa muestras reales en representative_dataset.
  5. Errores de compilación sobre MicroErrorReporter o AllOpsResolver: estás mezclando código de ejemplo antiguo con una versión más reciente de TFLM. Las versiones actuales usan el constructor mostrado arriba y MicroMutableOpResolver.

Hacia dónde seguir

Sustituye el modelo del seno por un clasificador alimentado con características de sensores reales, como en TinyML en ESP32, o deja que una plataforma genere el pipeline por ti con el tutorial de Edge Impulse con MQTT. El repositorio oficial de TensorFlow Lite Micro documenta los operadores soportados y la adaptación a nuevas placas. Para el contexto de arquitectura, la guía de IA e IoT explica cómo encajan los modelos en microcontroladores junto a la analítica en la nube.

Empieza con ConnectMQ

Tanto si las predicciones salen de tu placa por Wi-Fi como a través de un puente, necesitan un destino fiable. ConnectMQ es un broker MQTT gestionado con TLS, MQTT 3.1.1 y 5.0 y un cliente MQTT en el navegador para comprobar los mensajes a medida que llegan. Crea un broker con el plan Serverless gratuito en app.connectmq.com, sin tarjeta de crédito.

Preguntas frecuentes

¿Qué diferencia hay entre TensorFlow Lite y TensorFlow Lite Micro?

TensorFlow Lite está pensado para móviles y dispositivos Linux con sistema operativo y memoria dinámica. TensorFlow Lite Micro es un runtime reducido para microcontroladores que usa una arena estática, un conjunto limitado de operadores y no depende de ningún sistema operativo.

¿Cómo elijo el tamaño de la tensor arena?

Empieza con un tamaño generoso, llama a AllocateTensors(), lee después arena_used_bytes() y fija la arena en ese valor más un margen de seguridad. La necesidad depende de los tensores intermedios más grandes del modelo.

¿TensorFlow Lite Micro admite modelos en coma flotante?

Sí, muchos operadores tienen kernels en coma flotante, pero los modelos int8 son más pequeños y mucho más rápidos en la mayoría de los microcontroladores. La cuantización entera completa es la opción habitual en producción.

¿Puede TensorFlow Lite Micro enviar datos por MQTT por sí mismo?

No. TFLM solo realiza la inferencia. Los resultados se publican con un cliente MQTT independiente, como el cliente MQTT de ESP-IDF, PubSubClient o un puente serie en un gateway.

Artículos relacionados

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.