La posibilidad de ejecutar modelos de inteligencia artificial directamente en hardware de recursos limitados, como los sistemas embebidos, abre un abanico de aplicaciones prácticas que van desde la detección de anomalías en líneas de producción hasta el procesamiento de lenguaje natural en dispositivos portátiles sin conexión a la nube. El principle obstáculo reside en la brecha entre los requisitos computacionales de los modelos de gran escala y las restricciones inherentes a la plataforma embebida.
Preparación del Entorno de Desarrollo
El primer paso consiste en evaluar las capacidades del hardware objetivo y establecer el entorno de software necesario. Un dispositivo ARM Cortex-A53 o superior con al menos 512MB de RAM constituye una base de partida razonable. El sistema operativo debe estar basado en un kernel de Linux reciente.
Es fundamental insatlar las dependencias del sistema. El siguiente conjunto de comandos prepara un entorno basado en Debian:
apt-get update
apt-get install -y python3 python3-pip libatlas3-base
pip3 install numpy torch torchvision --index-url https://download.pytorch.org/whl/cpu
Estrategias de Reducción del Modelo
Para hacer que un modelo grande sea viable, es necesario comprimirlo sin comprometer en exceso su precisión. Esto se logra mediante técnicas como la poda y la cuantización.
Eliminación de Parámetros Redundantes (Poda)
La poda identifica y elimina los pesos de menor impacto en la red neuronal. El siguiente fragmento aplica una poda no estructurada basada en la norma L1 a las capas lineales del modelo.
import torch
from torch.nn.utils import prune
def reducir_parametros_red(red_neuronal, tasa_poda=0.25):
"""Aplica poda L1 a las capas lineales de la red."""
capas_objetivo = [modulo for modulo in red_neuronal.modules() if isinstance(modulo, torch.nn.Linear)]
for capa in capas_objetivo:
prune.l1_unstructured(capa, name='weight', amount=tasa_poda)
# Hacer la poda permanente y eliminar el atributo de máscara
prune.remove(capa, 'weight')
return red_neuronal
Cuantización a Precisión Entera
La cuantización convierte los cálculos de coma flotante a números enteros, acelerando la inferencia y reduciendo el uso de memoria. Se recomienda cuantizar dinámicamente después de la poda.
def cuantizar_red_dinamica(modelo):
"""Cuantiza dinámicamente la red neuronal para CPU."""
modelo_cuantizado = torch.quantization.quantize_dynamic(
modelo,
{torch.nn.Linear, torch.nn.Conv2d}, # Capas a cuantizar
dtype=torch.qint8
)
return modelo_cuantizado
Proceso de Integración en la Plataforma
Una vez optimizado el modelo, se debe empaquetar e instalar en el dispositivo embebido.
Exportación a un Formato Portátil
Exportar el modelo optimizado a ONNX facilita su uso con diferentes motores de inferencia.
def exportar_a_onnx(modelo, tensor_ejemplo, ruta_salida):
"""Exporta el modelo PyTorch a formato ONNX."""
torch.onnx.export(
modelo,
tensor_ejemplo,
ruta_salida,
opset_version=13,
input_names=['entrada'],
output_names=['salida'],
dynamic_axes={'entrada': {0: 'lote'}, 'salida': {0: 'lote'}}
)
Transferencia y Ejecución en el Dispositivo
El script de inferencia en el dispositivo debe gestionar la memoria de forma eficiente y manejar la entrada/salida de los datos.
import onnxruntime as ort
import numpy as np
def ejecutar_inferencia_embebida(ruta_modelo, datos_entrada):
"""Carga el modelo ONNX y ejecuta una inferencia."""
opciones = ort.SessionOptions()
opciones.intra_op_num_threads = 2 # Limitar hilos para controlar uso de CPU
opciones.inter_op_num_threads = 1
sesion = ort.InferenceSession(ruta_modelo, options=opciones)
nombre_entrada = sesion.get_inputs()[0].name
# Asegurar formato correcto y tipo de dato
tensor_entrada = np.array(datos_entrada, dtype=np.float32)
resultado = sesion.run(None, {nombre_entrada: tensor_entrada})
return resultado[0]
Validación y Monitoreo del Rendimiento
Es crucial verificar que la versión optimizada del modelo mantiene un nivel de precisión aceptable y cumple con los requisitos de latencia y consumo de recursos.
import time
def medir_rendimiento(funcion_inferencia, datos_prueba, vueltas=50):
"""Mide el tiempo de inferencia promedio y el consumo de memoria."""
tiempos = []
for _ in range(vueltas):
inicio = time.perf_counter()
funcion_inferencia(datos_prueba)
fin = time.perf_counter()
tiempos.append((fin - inicio) * 1000) # Convertir a milisegundos
promedio = np.mean(tiempos)
desviacion = np.std(tiempos)
print(f"Latencia promedio: {promedio:.2f} ms (+/- {desviacion:.2f} ms)")
print(f"Latencia mínima: {np.min(tiempos):.2f} ms")
print(f"Latencia máxima: {np.max(tiempos):.2f} ms")
Para una validación completa, se debe comparar la salida del modelo optimizado con la del modelo original utilizando un conjunto de datos de prueba representativo, calculando métricas como el error cuadrático medio (MSE) para tareas de regresión o la precisión para clasificación.