En el mundo de la inteligencia artificial y el aprendizaje automático, las redes neuronales se han convertido en la herramienta por excelencia para resolver problemas complejos que involucran patrones y datos no lineales. Sin embargo, uno de los grandes retos en la actualidad es implementar modelos efectivos en entornos con recursos limitados, tales como microcontroladores con capacidad de memoria y procesamiento restringidos. Aprender a crear lo que algunos denominan la red neuronal más adorable o «cutest neural network» implica enfrentarse a limitaciones técnicas y conceptuales, con el objetivo de llevar la potencia del aprendizaje automático a sistemas embebidos de bajo consumo. El punto de partida de esta exploración fue la necesidad de estimar la pose de un objeto —es decir, su ubicación y orientación en el espacio— a partir de las lecturas obtenidas de seis sensores diferentes. Dichas lecturas presentaban características no lineales y estaban acopladas, lo que hizo inviable el uso de una solución analítica explícita.
Ante esta dificultad, la alternativa fue emplear una red neuronal simple, compuesta por unas pocas capas densas y con un número reducido de parámetros, capaz de aproximar la función inversa entre las lecturas de sensores y la pose. Para lograr este propósito se generó un conjunto de datos de entrenamiento mediante simulaciones directas que traducían poses a lecturas sensoras y luego, usando esta información, se entrenó la red para que hiciera la operación inversa. La red resultante se quería desplegar en un microcontrolador Cortex-M0 con tan solo 16 kB de RAM y 32 kB de memoria flash, un entorno extremadamente limitado en términos de memoria y procesamiento. Un desafío primordial fue el hecho de que este microcontrolador no cuenta con soporte de hardware para representar números en punto flotante, lo que generalmente es un requisito para las operaciones matemáticas que realizan las redes neuronales más comunes. Este obstáculo condujo a la necesidad de trabajar con redes neuronales cuantizadas que utilizaran únicamente aritmética entera, reduciendo la dependencia en cálculos costosos y complejos.
Se recurrió a TensorFlow para realizar un entrenamiento consciente de la cuantización, es decir, un proceso en el que la red aprende a funcionar dentro de las limitaciones de los tipos de datos enteros, minimizando así la pérdida de precisión. El modelo entrenado se guardó en un formato .tflite, el cual es un archivo optimizado para inferencia en dispositivos de baja potencia, y empleando la librería microflow-rs, se generó código Rust que implementa la inferencia utilizando la eficiencia de la biblioteca nalgebra para operaciones matriciales. Sin embargo, la situación no era tan sencilla como parecía. Aunque la mayoría de las técnicas modernas avanzan hacia inferencias eficientes, aún dependen en gran medida de operaciones en coma flotante para la escala de activaciones, la calibración de salidas y la multiplicación con constantes en tiempo real.
El código generado y las APIs están diseñados para recibir y devolver valores en formato de punto flotante, lo que implica que detrás del telón ocurre una cantidad importante de cálculos que no pueden prescindir del uso de números flotantes. Este detalle importante incrementa tanto el tamaño del código como la latencia de la inferencia, factores que son inaceptables en un dispositivo con recursos tan limitados. Como contraste, la idea de una red verdaderamente adorable pasa por la eliminación total del punto flotante, apostando por una ejecución íntegramente en enteros que utilice solamente multiplicaciones, sumas y desplazamientos binarios para realizar los cálculos necesarios. Para entender esta problemática a fondo, es fundamental repasar los conceptos básicos de redes neuronales y la cuantización. Una red neuronal utiliza capas que transforman un vector de entrada en un vector de salida, usando multiplicaciones matriciales y funciones de activación no lineales, como la ReLU.
Los pesos y sesgos son los parámetros entrenables que determinan la transformación y se ajustan para minimizar el error en la predicción. La cuantización consiste en limitar el rango y la precisión de estos parámetros y de los datos intermedios, tratando de representar los valores con un número menor de bits, como 8 o incluso 4 bits. Esto reduce el tamaño del modelo y la cantidad de memoria y ancho de banda necesarios durante la inferencia. Pero en la práctica, ejecutar estas operaciones directamente en enteros presenta retos en el mantenimiento de la precisión y en la implementación de la función de activación y el escalado de activaciones entre capas. Existe una técnica llamada multiplicador cuantizado, que permite realizar el escalado de activaciones con multiplicaciones en punto fijo y desplazamientos aritméticos, logrando una aproximación bastante cercana a las operaciones en coma flotante, pero utilizando únicamente operaciones enteras.
Esto es especialmente útil en ambientes donde no se dispone de hardware para cálculo en punto flotante, como el microcontrolador Cortex-M0 mencionado. En cuanto al entrenamiento, dos opciones principales se presentan: la cuantización posterior al entrenamiento y el entrenamiento consciente de la cuantización (quantization-aware training). La primera consiste en entrenar la red con números en punto flotante y luego simplemente aproximar los pesos a enteros, lo cual puede ocasionar pérdida significativa de precisión. La segunda opción, aunque más compleja, incorpora la cuantización durante el proceso de entrenamiento, ayudando a que el modelo se adapte a las limitaciones y resultando en una mejor precisión después de la cuantización. Dado que las bibliotecas convencionales como TensorFlow y microflow no permiten eliminar completamente las operaciones en punto flotante y cuentan con APIs rígidas, la solución más efectiva puede ser diseñar y entrenar la red desde cero usando herramientas más flexibles como JAX.
Este framework permite definir funciones de cuantización con gradientes personalizados, haciendo posible que la red aprenda a convivir con las limitaciones impuestas por la aritmética entera y el escalado fijo. El ejemplo proporcionado en código Python hace uso de la función custom_vjp para definir la cuantización y permitir que durante la propagación hacia atrás —el cálculo de gradientes necesario para el entrenamiento— se ignore la función no diferenciable de cuantización, simulando un comportamiento que facilita la actualización de los parámetros. Con este método se puede entrenar una red muy simple, observar que el error disminuye y obtener pesos y sesgos ya cuantizados, listos para ser implementados sobre el microcontrolador. Además, este entrenamiento personalizado permite ajustar finamente el escalado de activación para maximizar la precisión durante la inferencia. La implementación final de la inferencia en el microcontrolador debería consistir en un código escrito manualmente que realice las operaciones matriciales con los parámetros cuantizados almacenados en variables constantes.
Estos valores pueden incluso generarse directamente desde el cuaderno de entrenamiento en Python, haciendo que el proceso sea reproducible y que el firmware resulte extremadamente compacto y eficiente. La ventaja de esta aproximación es doble. Por un lado, se comprende en detalle todo el proceso y las operaciones involucradas, lo que aporta seguridad y control absoluto sobre el comportamiento del sistema. Por otro lado, se obtiene una red neuronal que es realmente adorable para entornos embebidos: mínima en consumo de recursos, rápida y sin dependencias innecesarias o código inflado. Aunque existen otras alternativas y librerías que prometen soluciones para redes neuronales en dispositivos de bajo consumo, como CMSIS-NN, IREE, MicroTVM, uTensor o TinyEngine, la realidad es que muchas de estas opciones son demasiado complejas, están pensadas para arquitecturas específicas o requieren infraestructuras que superan las capacidades del hardware en cuestión.
Además, la documentación muchas veces es escasa o está orientada a desarrolladores con experiencia considerable, lo que puede desalentar a quienes buscan una solución sencilla y ligera para tareas específicas y con redes simples. En referencia a la preparación del entorno y los toolkits recomendados, el autor original señala las dificultades encontradas con las dependencias y herramientas de compilación asociadas a TensorFlow Lite Micro. Estos inconvenientes incluyen incompatibilidades de versiones, scripts que no funcionan correctamente y un tamaño de código final prohibitivo para microcontroladores de escasos recursos. Por el contrario, la estrategia basada en definir y entrenar la red manualmente, aprovechar la potencia de JAX para el entrenamiento y luego traducir los resultados a código Rust optimizado representa una ruta pragmática y elegante, que puede ser aplicada en diferentes contextos similares donde la simplicidad, el control y la eficiencia son prioritarios. En conclusión, la búsqueda por construir la red neuronal más adorable no solo consiste en hacer un modelo pequeño, sino también en llevarlo a la práctica con total control sobre las operaciones de aritmética, evitando dependencias innecesarias y optimizando al máximo la ejecución para entornos con restricciones severas.
La combinación de un entrenamiento consciente de la cuantización, herramientas modernas y la implementación manual de la inferencia pueden marcar la diferencia para habilitar aplicaciones inteligentes en los sistemas embebidos del futuro.