Recursos didácticos de ML: Redes neuronales para evitar obstáculos en un vehículo
En este ejemplo se describe cómo se puede adiestrar un robot fabricado bajo un entorno tipo Arduino para dotarle de un modelo autónomo. Se tratan varias partes que se pueden saltar, en función de lo que más interese y cuyo índice es el siguiente:
Parte I: Hardware y coste
Parte II: Conexiones
Parte III: Datos de entrada. El manual de conducción
Parte IV: Aprendiendo a conducir. El entrenamiento neuronal
Parte V: El código conductor
Conclusiones
Parte I: Hardware y coste.
Se describen los materiales y los costes aproximados que se necesitarían para este robot con indicaciones de donde se podrían adquirir:
Arduino Nano: Es el microcontrolador de todo, tiene un coste unitario de unos 4€ – 5€ y es fácil de encontrar en Aliexpress o Amazon
Placa soporte, yo opté por una circular de metal ya hecha que me pillé por Aliexpress a unos 2’6€ en https://es.aliexpress.com/item/1005008163755685.html?spm=a2g0o.order_list.order_list_main.138.43af194d9Se4ye&gatewayAdapt=glo2esp
Un controlador L298N que son fáciles de encontrar con un precio entre 2€ - 3€
Un mando a distancia con un receptor IR de infrarrojos, lo hay de varios tipos y casi todos funcionan igual, yo tomé uno de la marca elegoo, si se cambia de marca hay que reconfigurar o buscar los botones adecuados, en todo caso se puede eludir esto si se quiere un robot sólo de IA y se ahorraría también un montón de código. El coste ronda los 3€
Una protoboard pequeña que se pueden encontrar fácilmente por 3€
Tornillos, tuercas de unos 3mm y 4mm y ángulos de 90º para generar soporte tal y como se muestran en las fotos de más abajo, en total puede haber un coste de lo usado de no más de 5€ (comprando en lote es fácil de encontrar)
2 ruedas con sus motores (yo tomé unas caras, pero dado que no controlo velocidades, se puede optar por motores más baratos sin cuadraturas), teniendo un coste de uno más de 4€ por Ali-express
Un servo (de 180 grados suficiente) que ronda unos 2.40€
Un sensor de ultrasonidos que rondará unos 2.5€ y es fácil de encontrar
Caja de 2 baterías lipo de 3.7V a 2.5V
Finalmente 3 leds, con 3 resistencias de 220V más cableado donde estimo un coste de unos 2€ máximo sabiendo que son materiales que se compran a granell
En total bajo un coste máximo aproximado de unos 40€ sin contar las baterías, yo cogí una de 5V para Arduino que tenía en casa y 2 pilas lipo recargables pueden costar unos 10€ si se busca bien se tendría algo parecido a:

Nótese que a pesar de que 40€ puedan parecer mucho, todas las piezas son reutilizables para otros futuros artefactos que nos pueda interesar crear, de hecho, muchas de dichas piezas las tengo de otros anteriores por lo que he tenido que comprarlas de nuevo.
Para finalizar esta sesión y dado que este post está enfocado al detalle de cómo incorporar Inteligencia Artificial al anterior artefacto, no ofrezco detalle de su montaje en la práctica, aunque tal y como se observa, no cuidé demasiado los detalles y marcha bien.
Parte II: Conexiones.
En todo caso, no se explica como montar físicamente el robot a nivel de apretar tornillos y demás, sino que en cambio se detalla cómo deben estar conectadas las componentes a muy alto nivel para que posteriormente se entienda cómo va a ser la incorporación de la “inteligencia” al artefacto:

Parte III: Datos de entrada. El manual de conducción
Tras montar el robot, hay que dar las instrucciones adecuadas al microcontrolador parra que:
En modo manual, que no se analiza aquí, el robot avance, retroceda y gire
En modo IA, el robot evite obstáculos cuando los detecte a una determinada distancia
Antes de dar el paso 2 que es el que se describe aquí, se construye un programa denominado SimulaRed.py que va a contener tanto simulaciones como los pesos que tomará el microcontrolador para generar un comportamiento adecuado, acorde al código.
Por tanto, esta parte trata cómo son los datos con los que se entrena el vehículo que están bajo la función generar_dataset, de la que se generan 4000 muestras siguiendo la siguiente regla:
for _ in range(n):
dist = random.uniform(0, 40)
angle = random.uniform(60, 120)
if dist < SAFE_STOP_CM:
if angle < 85:
label = 2
elif angle > 95:
label = 1
else:
label = 3
else:
if dist > OBSTACLE_LIMIT_CM:
label = 0
else:
if angle < 85:
label = 2
elif angle > 95:
label = 1
else:
label = 0
Que gráficamente responde al siguiente esquema:

Nótese que aquí está la clave, la red neuronal a entrenar va a aprender estas regiones del espacio y aunque el gráfico tiene cierta sencillez y podría aproximarse con un modelo tipo múltiple tree decisional, obviamente las reglas dadas anteriormente podrían ser refinadas con decisiones no lineales y en consecuencia el método no permitiría una “escala en inteligencia” que sí lo permitiría la red.
Por tanto, la red a diseñar tendrá que aprender los datos aquí presentados que básicamente es un manual de qué hacer cuando te encuentras un obstáculo a una determinada distancia bajo un determinado ángulo dado por el sensor de ultrasonidos que controla la distancia y el servo que controla el ángulo.
Parte IV: Aprendiendo a conducir. El entrenamiento neuronal
Los datos anteriores son la entrada de la función entrenar(), básicamente esta función entrena una red neuronal de una única capa oculta de 8 neuronas y una capa de salida de 4 neuronas siendo todas de tipo tangente hiperbólico y obedeciendo a 2 únicas entradas que son las de distancia y ángulo obtenidos de la combinación de los sensores anteriormente citados. Por tanto, se tiene la siguiente arquitectura de red:

Que como se puede observar (si se cuentan las flechitas) se tendría un total de unos 2 · 8 + 8 · 4 = 48 pesos neuronales que van a intentar, tras ejecutar esta función y aplicar unas 80 iteraciones o epochs replicar el anterior espacio.
Como puede observarse en el código, se entrena todo desde el scratch sin hacer uso de la librería sklearn, siendo clave las siguientes líneas:
h = [math.tanh(sum(xi[j] * W1[j][i] for j in range(2))) for i in range(hidden)]
o = [math.tanh(sum(h[j] * W2[j][i] for j in range(hidden))) for i in range(out)]
Que básicamente resumen la dinámica de la red donde aplican primeramente el movimiento forward o hacia delante de la red para finalmente concluir con una variante tipo backpropagation tratando de minimizar una función de error tal como:
sample_loss += (target[i] - o[i])**2
La opción elegida y aunque no es la óptima, teóricamente ofrece una rápida caída del error y al final alguna muestra de overtraining tal y como se observa en la salida gráfica tras ejecutar la función:

Cabe preguntarse por tanto ¿Y si se hubiera hecho un backpropagation estricto en vez de estas simplificaciones que buscan evitar el derivar? ¿Y no cabría usar la librería de redes de sklearn? Todo esto serían interesantes ejercicios que darían finalmente una serie de pesos a implementar en alguna red neuronal que puede ser como ésta, de sólo tanh y una única capa oculta con 48 parámetros o algo más elaborado.
Además, el código .py que genera tanto los siguientes pesos:

Como una posible simulación de una trayectoria para testear que el robot no se queda atascado tal como sigue:

Tiene la suficiente versatilidad para que una vez visto, que genera un movimiento relativamente estable, se pueda extraer la función de la red con los pesos entrenados, este código puede encontrarse en el git:
Parte V: El código conductor
El código final, que va al microcontrolador es un código .ino que se encuentra junto al fichero .py en la ruta anterior.
Este código, aparte de conectar entre sí las componentes hardware, permite integrar la salida de la red a través de un área donde se “cortan y pegan” los pesos obtenidos del código .py:

Y dichos pesos son resueltos mediante la función decidirAcción() donde cabe destacar la parte donde se aplica la red neuronal quedando todo bastante resumido gracias al uso de las operaciones vectoriales adecuadamente utilizadas:

Conclusiones
Con lo construido hasta aquí se tiene una aplicación práctica y directa de un red neuronal aplicada al movimiento autónomo de un robot.
Se ha tomado este caso sencillo con claro objetivo didáctico, pero cabe hacer ejemplos más elaborados e incluso variantes que pueden resultar bastante óptimas, si bien, la opción aquí escogida permite tener la red totalmente descompuesta en sus elementos y por tanto pueden ser manipulados frente a una función de una librería que puede quedar más ocultos o difíciles de descifrar ya que Arduino no acepta Python y por tanto todo debe ser pasado a C y claramente, una fórmula matemática, aunque pueda ser larga, es perfectamente resoluble tal y como se puede comprobar.
Finalmente, un vídeo con el resultado se tiene también en la dirección de git siguiente:



Comentarios