Crear una Red Neuronal en Python desde cero
Programaremos una red neuronal artificial en Python, sin utilizar librerías de terceros. Entrenaremos el modelo y en pocas lineas el algoritmo podrá conducir por sí mismo un coche robot!.
Para ello, explicaremos brevemente la arquitectura de la red neuronal, explicaremos el concepto Forward Propagation y a continuación el de Backpropagation donde ocurre «la magia» y aprenden las neuronas.
Asumimos que se tienen conocimientos básicos de Redes Neuronales y de Python, les recomiendo repasar algunos conceptos en los artículos:
- Principales Algoritmo de ML: Redes Neuronales
- Aprendizaje Profundo, una guía Rápida
- Una sencilla Red Neuronal con Keras
- Neural Networks Representations
- VIDEO de 1 hora, clase universitaria: «Neural Networks»
- Redes Neuronales a lo largo de la historia
Comencemos con un Ejercicio Práctico
Vamos a crear una red neuronal que conduzca un coche de juguete Arduino que más adelante construiremos y veremos en el «mundo real».
Nuestros datos de entrada serán:
- Sensor de distancia al obstáculo
- si es 0 no hay obstáculos a la vista
- si es 0,5 se acerca a un obstáculo
- si es 1 está demasiado cerca de un obstáculo
- Posición del obstáculo (izquierda,derecha)
- El obstáculo es visto a la izquierda será -1
- visto a la derecha será 1
Las salidas serán
- Girar
- derecha 1 / izquierda -1
- Dirección
- avanzar 1 / retroceder -1
La velocidad del vehículo podría ser una salida más (por ejemplo disminuir la velocidad si nos aproximamos a un objeto) y podríamos usar más sensores como entradas pero por simplificar el modelo y su implementación mantendremos estas 2 entradas y 2 salidas.
Para entrenar la red tendremos las entradas y salidas que se ven en la tabla:
Entrada: Sensor Distancia | Entrada: Posición Obstáculo | Salida: Giro | Salida: Dirección | Acción de la Salida |
---|---|---|---|---|
0 | 0 | 0 | 1 | Avanzar |
0 | 1 | 0 | 1 | Avanzar |
0 | -1 | 0 | 1 | Avanzar |
0.5 | 1 | -1 | 1 | Giro a la izquierda |
0.5 | -1 | 1 | 1 | Giro a la derecha |
0.5 | 0 | 0 | 1 | Avanzar |
1 | 1 | 0 | -1 | Retroceder |
1 | -1 | 0 | -1 | Retroceder |
1 | 0 | 0 | -1 | Retroceder |
-1 | 0 | 0 | 1 | Avanzar |
-1 | -1 | 0 | 1 | Avanzar |
-1 | 1 | 0 | 1 | Avanzar |
Esta será la arquitectura de la red neuronal propuesta:
En la imagen anterior -y durante el ejemplo- usamos la siguiente notación en las neuronas:
- X(i) son las entradas
- a(i) activación en la capa 2
- y(i) son las salidas
Y quedan implícitos, pero sin representación en la gráfica:
- O(j) Los pesos de las conexiones entre neuronas será una matriz que mapea la capa j a la j+1
- Recordemos que utilizamos 1 neurona extra en la capa 1 y una neurona extra en la capa 2 a modo de Bias -no están en la gráfica- para mejorar la precisión de la red neuronal, dandole mayor «libertad algebraica».
Los cálculos para obtener los valores de activación serán:
a(1) = g(OT1X)
a(2) = g(OT2X)
a(3) = g(OT3X)
*Nota: la T indica matriz traspuesta, para poder hacer el producto
En las ecuaciones, la g es una función Sigmoide que refiere al caso especial de función logística y definida por la fórmula:
g(z) = 1/(1+e-z)
Funciones Sigmoide
Una de las razones para utilizar la función sigmoide –función Logística– es por sus propiedades matemáticas, en nuestro caso, sus derivadas. Cuando más adelante la red neuronal haga backpropagation para aprender y actualizar los pesos, haremos uso de su derivada. En esta función puede ser expresada como productos de f y 1-f . Entonces f'(t) = f(t)(1 – f(t)). Por ejemplo la función tangente y su derivada arco-tangente se utilizan normalizadas, donde su pendiente en el origen es 1 y cumplen las propiedades.
Forward Propagation -ó red Feedforward-
Con Feedforward nos referimos al recorrido de «izquierda a derecha» que hace el algoritmo de la red, para calcular el valor de activación de las neuronas desde las entradas hasta obtener los valores de salida.
Si usamos notación matricial, las ecuaciones para obtener las salidas de la red serán:
X = [x0 x1 x2]
zlayer2 = O1X
alayer2 = g(zlayer2)
zlayer3 = O2alayer2
y = g(zlayer3)
Resumiendo: tenemos una red; tenemos 2 entradas, éstas se multiplican por los pesos de las conexiones y cada neurona en la capa oculta suma esos productos y les aplica la función de activación para «emitir» un resultado a la siguiente conexión (concepto conocido en biología como sinapsis química).
Como bien sabemos, los pesos iniciales se asignan con valores entre -1 y 1 de manera aleatoria. El desafío de este algoritmo, será que las neuronas aprendan por sí mismas a ajustar el valor de los pesos para obtener las salidas correctas.
Backpropagation (cómputo del gradiente)
Al hacer backpropagtion es donde el algoritmo itera para aprender! Esta vez iremos de «derecha a izquierda» en la red para mejorar la precisión de las predicciones. El algoritmo de backpropagation se divide en dos Fases: Propagar y Actualizar Pesos.
Fase 1: Propagar
Esta fase implica 2 pasos:
1.1 Hacer forward propagation de un patrón de entrenamiento (recordemos que es este es un algoritmo supervisado, y conocemos las salidas) para generar las activaciones de salida de la red.
1.2 Hacer backward propagation de las salidas (activación obtenida) por la red neuronal usando las salidas «y» reales para generar los Deltas (error) de todas las neuronas de salida y de las neuronas de la capa oculta.
Fase 2: Actualizar Pesos:
Para cada <<sinapsis>> de los pesos:
2.1 Multiplicar su delta de salida por su activación de entrada para obtener el gradiente del peso.
2.2 Substraer un porcentaje del gradiente de ese peso
El porcentaje que utilizaremos en el paso 2.2 tiene gran influencia en la velocidad y calidad del aprendizaje del algoritmo y es llamado «learning rate» ó tasa de aprendizaje. Si es una tasa muy grande, el algoritmo aprende más rápido pero tendremos mayor imprecisión en el resultado. Si es demasiado pequeño, tardará mucho y podría no finalizar nunca.
Deberemos repetir las fases 1 y 2 hasta que la performance de la red neuronal sea satisfactoria.
Si denotamos al error en el layer «l» como d(l) para nuestras neuronas de salida en layer 3 la activación menos el valor actual será (usamos la forma vectorial):
d(3) = alayer3 – y
d(2) = OT2 d(3) . g'(zlayer2)
g'(zlayer2) = alayer2 . (1 – alayer2)
Al fin aparecieron las derivadas! Nótese que no tendremos delta para la capa 1, puesto que son los valores X de entrada y no tienen error asociado.
El valor del costo -que es lo que queremos minimizar- de nuestra red será
J = alayer dlayer + 1
Usamos este valor y lo multiplicamos al learning rate antes de ajustar los pesos. Esto nos asegura que buscamos el gradiente, iteración a iteración «apuntando» hacia el mínimo global.
Nota: el layer en el código es realmente a(l)
El Código Completo de la red Neuronal con Backpropagation
Aquí va el código, recuerden que lo pueden ver y descargar al final del artículo o desde mi cuenta de GitHub.
Primero, declaramos la clase NeuralNetwork
Y ahora creamos una red a nuestra medida, con 2 neuronas de entrada, 3 ocultas y 2 de salida. Deberemos ir ajustando los parámetros de entrenamiento learning rate y la cantidad de iteraciones «epochs» para obtener buenas predicciones.
La salidas obtenidas son: (comparar los valores «y» con los de «Network» )
Como podemos ver son muy buenos resultados.
Aquí podemos ver como el coste de la función se va reduciendo y tiende a cero:
Y podemos ver los pesos obtenidos de las conexiones con nn.print_weights() pues estos valores serán los que usaremos en la red final que en un próximo artículo implementaremos en Arduino para que un coche-robot conduzca sólo evitando obstáculos.
Conclusión
Creamos una red neuronal en pocas líneas de código Python:
- comprendimos cómo funciona una red neuronal «básica»,
- el porqué de las funciones Sigmoides y sus derivadas que …
- nos permiten hacer Backpropagation,
- hallar el gradiente para minimizar el coste,
- reducir el error iterando y obtener las salidas buscadas,
- logrando que la red aprenda por sí misma en base a un conjunto de datos de entrada y sus salidas como «buen» Algoritmo Supervisado que es.
Nos queda como proyecto futuro aplicar esta red que construimos en el mundo real y comprobar si un coche Arduino será capaz de conducir por sí mismo y evitar obstáculos..! (en el próximo artículo lo veremos en acción!)
Suscripción al Blog
Como siempre, te invito a suscribirte al Blog y recibir los artículos cada 15 días.
Puedes hacer más ejercicios Machine Learning en Python en nuestra categoría d Ejercicios paso a paso por ejemplo de Regresión Logística ó clustering K-means ó comprender y crear una Sencilla Red Neuronal
Recursos
Si tienes que armar tu ambiente de Programación Python puedes hacerlo siguiendo los pasos de este artículo: Instalar ambiente de Desarrollo Python con Anaconda
El código utilizado es una adaptación del original del BogoToBogo en donde se enseña la función XOR.
Pueden descargar el código de este artículo en un Jupyter Notebook aquí o visualizar online ó pueden acceder a mi Github.
Puedes ver la continuación de este artículo en donde aplicaremos la red neuronal a un coche arduino! no te lo pierdas!
Lee acerca de la evolución de las redes neuronales desde 1950 hasta la actualidad