Introducción

RNA
Si alguna vez has buscado información sobre las redes neuronales artificiales, lo más seguro es que pasaste mucho tiempo leyendo articulos que al final de cuentas solo te dan una idea muy general de que son y para que se utilizan, pero no van más allá de definiciones, en este tutorial voy a omitir todas la definiciones y me voy a enfocar en como implementarlas.
No quiero decir que no sea importante conocer la historia y las definiciones de las redes neuronales y mucho menos que sea un gurú sobre el tema, pero ten por seguro que a comparación de los articulos que rondan por internet, este va a ser el tutorial más completo que puedas encontrar, ¿por que tan seguro?, pues fácil, yo voy a poner un código escrito en c++ para que lo puedas analizar y despues lo puedas implementar por ti mismo, a verdad, a que no te esperabas esa.
Antes de seguir adelante, para este tutorial es necesario que tengas conocimientos de matematicas (solo si lo quieres entender), cualquier lenguaje de programación (para que lo implementes), y software capaz de resolver ecuaciones (para que lo compruebes), como ya es costumbre lo voy a poner en negrillas porque ya casí nadie lee la introducción.

IMPORTANTE:

Para este proyecto es necesario un lenguaje de programación de alto nivel y software matemático.


Teoria


En la introducción mencione que no me hiba a entretener en cuestiones de definiciones, ni tampoco la historia de la redes neuronales artificiales, para ello existen muchos sitios en internet donde puedes encontrar información al respecto.
Lo importante a mencionar seria que las redes neuronales artificiales (RNA por sus siglas en español) o artificial neural network (ANN en inglés), es que se basan en como funciona el cerebro humano, un par de psicologos por los años ochenta propusieron el siguiente modelo:

Modelo perceptron • xj = Entradas.
• wij = Pesos sinapticos.
• yi = Salida de la red.

Es el modelo más simple de red neuronal, basicamente el modelo nos dice que la salida va a ser el resultado de la sumatoria de los productos aplicada a una función no lineal, algunos libros proponen un umbral en lugar de la función no lineal, y como estamos trabajando con sistemas discretos resulta ser mas conveniente.
Este modelo se utiliza SI y SOLO SI el problema es linealmente separable, y esto es debido a que la respuesta generaliza de la salida es de la siguiente forma:

yi=f(w1j*x1+w2j*x2+w3j*x3+....+wnj*xn)

Voy a poner un ejemplo para que esto quede mas claro, supongamos que quieres hacer una compuerta lógica utilizándo este modelo, y como yo soy el que esta escribiendo elíjo la compuerta AND.

Tabla de verdad compuerta AND
X1 X0 y
0 0 0
0 1 0
1 0 0
1 1 1

Gráficamente la tabla de verdad nos queda de la siguiente manera:

Grafica Tabla de Verdad
Los circulos corresponden a un cero, mientras que la cruz (tache) a un uno.

Aplicando el modelo:

Es hora de aplicar el modelo general, para encontrar la ecuación de la compuerta AND.

yi=f(w1j*x1+w2j*x2+w3j*x3+....+wnj*xn)

Como tenemos dos bits de entrada necesitamos dos variables independientes y un bias, por lo tanto el modelo matemático nos quedaría de la siguiente manera:

y = w0*x0+w1*x1+w2*x2

¿Ya viste la forma lineal de la ecuación?, ¿nooooooooo?, bueno entonces dejame cambiar un poco las cosas:

y = w0*x0+w1*x1+Θ : Θ = w2*x2

Ya esta mejor ¿no es cierto?, bueno en dado caso de que no sea así pongamoslo de la siguiente forma:

Explicacion ecuacion Para encontrar las ordenadas a los ejes respecto a las entradas, nos quedarían las siguientes ecuaciones:

x0*w0+x1*w1+b = 0 : x0*w0 = 0
x1*w1+b=0
x1 = -b/w1

x0*w0+x1*w1+b = 0 : x1*w1 = 0
x0*w0+b=0
x0 = -b/w0

Gráficamente tenemos:

Interseccion con los ejes Resolviendo x0 y x1 para la compuerta AND:

Para simplificar aún más los calculos igualamos las variables:

w0 = w1
w0 = w1 = 1

El intervalo del umbral es (1,2], voy a utilizar 1.5.

-b/w0 = -b/w1 = 1.5 : w0 = w1 = 1 ∴ b = -1.5

Ya solo nos falta definir la función:

y es 1 si x0+x1-1.5 ≥ 0
y es 0 si x0+x1-1.5 < 0

O si lo prefieren:

y es 1 si x0+x1 ≥ 1.5
y es 0 si x0+x1 < 1.5

Comprobando la salida:

• 0+0 = 0 como es menos que 1.5 el resultado es 0
• 0+1 = 1 como es menos que 1.5 el resultado es 0
• 1+0 = 1 como es menos que 1.5 el resultado es 0
• 1+1 = 2 como es mayor que 1.5 el resultado es 1

Comprobación gráfica:
Comprobacion grafica
En la gráfica se puede observar como la recta separa (divide) las dos posibles salidas.

Acaban de crear una compuerta lógica utilizando matematicas, tal vez ahorita no parezca la gran cosa y hasta para algunos puede que la solución fuera demasiado obvia, pero les aseguro que es la base de metodos más sofisticados.
Esta técnica la pueden utilizar para compuertas NAND, OR, NOR, y NOT, teniendo en cuenta que todo circuito lógico se puede replicar con compuertas NAND y NOR no esta por demás tener en mente este metodo.

Ahora bien, para el caso de la compuerta XOR la cosa cambia y esto es debido a su comportamiento lógico, la tabla de verdad es la siguiente:

Tabla de verdad compuerta XOR
X1 X0 y
0 0 0
0 1 1
1 0 1
1 1 0

Gráficamente la tabla de verdad es la siguiente: Tabla XOR Con una sola recta no es posible separar la solución como fue el caso de la compuerta AND, una posible solución sería añadir un nuevo umbral:

Umbral 1: (1,2]
Umbral 2: (0,1)
Solucion XOR Con dos rectas es posible resolver este tipo de problemas, pero ¿que pasa si agregamos 1 bit más?, ya estariamos hablando de un espacio, en un espacio todavia podemos ser capacez de visualizar las rectas solución, con 4 bits ya estamos hablando de hyper-espacios cuyas curvas de nivel tendrían que ser tridimencionales, ahora imaginate 1 byte, geometricamente ya no tengo la más minima idea de siquiera que pudiera ser una recta octodimensional, aparte resulta poco eficiente tener que estar calculando las rectas solución y es por eso que se tuvieron que crear nuevas topologias de red, y nuevos algoritmos de aprendizaje.

Perceptrón multicapa

En este tutorial voy a utilizar perceptrones multicapa topología feed forward con entrenamiento supervisado, para el aprendizaje voy a utilizar el algortimo de propagación inversa y la técnica de momentum para que converja más rápido.

Ya vimos que se pueden encontrar las rectas solución de problemas que se encuentren en un plano y tal vez hasta en un espacio, pero para más de 3 bits es prácticamente imposible lograrlo, para ello se utilizan las redes neuronales artificiales las cuales calculan las soluciones, se dice que debido a su paralelismo generalizan las respuestas, actualmente estoy trabajando en un robot cuya única función es navegar libremente utilizando sensores y redes neuronales, debo de admitir que a ciencia cierta no entiendo como es capaz de razonar, mi teoria es que como se trata de un ente matematico pueden darse peculiaridades, pero en fin ya me estoy saliendo del tema por ahora no mas robot asesino y más algoritmos.

Por parte de la aplicacion vamos a crear una red neuronal capaz de simular la compuerta lógica XOR de 2 bits.

Actualmente no existe formula algúna para encontrar la topologia adecuada de la red, lo que sabemos con certeza es que una sola neurona no es capaz de lograrlo, por lo que es necesario añadir una neurona extra, nuestra topologia va a quedar de la siguiente manera:

2:2:1 (Entradas:Neuronas capa oculta:Salida).

Vamos a definir las secciones de la red de la siguiente forma:
Red FeedForwad • i = Entradas
• j = Capa oculta
• k = Capa de salida

Entre la capa de entrada y la capa oculta vamos a utilizar dos parametros:

• wij = Pesos psinapticos
• b = Ordenada al origen de la entrada a la capa oculta

Entre la capa oculta y la capa de salida vamos a utilizar dos parametros:

• kjk = Pesos psinapticos
• v = Ordenada al origen de la capa oculta a la salida

Cuando veas:
w21 estoy haciendo referencia al peso de la entrada dos y la neurona uno de la capa oculta .
k31 estoy haciendo referencia al peso de la capa oculta 3 que va a la capa de salida 1.

Propagación Inversa:

El primer paso es definir la función, podemos elegir entre tres opciones:

• Sigmoide tangencial: Se utiliza cuando el sistema es bipolar, el 1 corresponde a Vdd y el 0 corresponde a -Vdd.
• Sigmoide logaritmica: Se utiliza más en sistemas analógicos, pero se puede usar en sistemas digitales.
• Recta: Se utiliza en sistemas digitales, el 1 corresponde a Vdd y el 0 corresponde a Vss

Voy a utilizar la sigmoide logaritmica para que sirva de ejemplo, como el algoritmo utiliza la derivada de la función de una vez lo hacemos:

Derivada sigmoide
Por lo tanto la derivada la podemos definir como:

Derivada sigmoide
Si han tomado clases de métodos numericos sabran que la mayória de los métodos se basan en gradientes descencientes, el algortimo de propagación inversa no es diferente, lo que trata de hacer es minimizar el error.

Error medio cuadratico Voy a redifinir los parametros:

• Ok = Salida de la red
• tk = Valor deseado

El error queda de la siguiente forma:
Error medio cuadratico
Listo ya tenemos todo lo necesario, anteriormente mencioné que el algoritmo de propagación inversa o Backpropagation usa el metodo de gradiente decendiente, por lo tanto todo lo que se necesita es derivar parcialmente el error de la salida hacia la entrada (por eso se llama propagación inversa):

Capa de salida:

Error de la capa de salida
Nota: σ(xk) corresponde a la función aplicada a los pesos de la capa de salida. Error de la capa de salida
Nota: Se usa la regla de la cadena, no se les olvide que Ok = fsalida((fcapa oculta(xij))) Error de la capa de salida
Nota: σ(xk) es el valor de la salida y la parcial de xk es la salida de la capa oculta.
Resultado de la capa de salida

Capa oculta:

Error capa oculta
Nota: Omití varios pasos.

Resultado de la capa oculta

Ordena al origen de salida:
Resultado ordenada al origen
De echo el bias u offset u ordenada al origen para la capa oculta y de salida resulta ser lo mismo por lo tanto podemos definirla de la siguiente manera:

Resultado ordenada al origen

Momentum:

La ecuación es la siguiente:
Momentum

Se utiliza por dos razones:

• Ayuda al algoritmo a converger más rápido.
• Permite salir de mínimos locales.

El momentum se aplica a todos los pesos de la red, aquellos que van de la entrada a la capa oculta, y de la capa oculta a la salida.
α es el factor de correspondencia, su intervalo va de [0,1], el valor depende del factor de aprendizaje, solo recuerda, si subes uno baja el otro, es cosa de jugar con los parametros.

XOR

Por fin hemos terminamos, nadie dijo que esto iba a ser fácil, si quedaron dudas respecto a la topologia o al algoritmo, no se preocupen, los voy a llevar de la mano para crear la XOR:

Topologia:

Es tipo feedforwad 2:2:1, esto quiere decir que la red parte de la entrada hacia la salida, tiene 2 entradas, 2 neuronas en la capa oculta, y 1 salida:

Topologia XOR Pues bueno me quedo algo pintoresca, en fin vamos a desmenuzarla para ver como funciona:

• La linea roja corresponden al peso de la entrada numero uno, por lo tanto sus pesos son w11 y w12
• La linea naranja corresponden al peso de la entrada numero dos, por lo tanto sus pesos son w21 y w22
• La linea verde corresponden al peso de la ordenada al origen por lo tanto, sus pesos son b11 y b12
• La linea Azul pálido corresponden a la entrada uno de la capa de salida, por lo tanto su peso es k11
• La linea Azul corresponden a la entrada dos de la capa de salida, por lo tanto su peso es k21
• La linea Morada corresponden al peso de la ordenada al origen por lo tanto su peso es b=v

Recuerden que la función de activación que estoy utilizando es la sigmoide logaritmica:

Funcion sigmoide logaritmica
Y la derivada la definimos como:
Derivada sigmoide
La entrada de Oj1 corresponderia a la sumatoria de los productos, dicho de otra forma es la suma de las entradas por sus pesos:
y1=x0*w11+x1*w21+b11
Por lo tanto la salida de Oj1 es:
Oj1=σ(y1)
Analogamente la entrada de Oj2 es:
y2=x0*w12+x1*w22+b12
Por lo tanto la salida de Oj2 es:
Oj2=σ(y2)
Ahora vamos con la entrada de la capa de salida Ok1:
y3=Oj1*k11+Oj2*k21+v
Por lo tanto la salida de Ok1 es:
Ok1=σ(y3)

Ha llegado el turno de aplicar el algoritmo de propagación inversa, como ya lo mencione va de la salida hacia la entrada, el objetivo es minimizar el error cuadrático médio.
Error medio cuadratico
Capa de salida:

Capa de salida Por lo tanto:
Resultado Capa de salida

Capa oculta:

Capa de oculta Por lo tanto:

Resultado Capa oculta

La ecuación final con momentum α y factor de aprendisaje η es:

Ecuacion final
Hemos terminado ya tenemos todo listo para codificar el algoritmo, nos vemos en la siguiente sección, por cierto no olvides presionar los bótones para recomendar esta página.

Software

Para programar el algoritmo voy a utilizar el software de codeblocks, lo pueden descargar de la siguiente página:.


La versión que voy a utilizar es 13.12, que por lo que veo ya tiene dos años sin actualizarse, pero bueno, de eso al borland c++ que pensaba utilizar creo que esta más actualizado.
Por cierto bajen el que dice mingw, de lo contrario solo es el IDE sin compilador, ya que este instalado le dan en settings+compiler y en la pestaña toolchain executables presionen sobre autodetect, de lo contrario parece que no detecta la ruta del compilador o al menos en mi caso no lo detectaba.
No bajen el que dice mingw-TDM, a menos que sepan lo que estan haciendo, no sé por que con el TDM el programa no compilaba correctamente, como dije, si saben lo que hacen pues bajen uno de esos dos, de lo contrario solo el que dice mingw-setup.

Para comprobar el algoritmo voy a utilizar matlab, la versión es R2013b, se fijan que estoy usando cosas de hace dos años, en fin, pueden optar por octave, maple, o cualquier otro software matematico.

Código C++


Debo admitir que no soy muy bueno programando en alto nivel, dicho esto, el programa del algoritmo es algo rústico pero en parte sirve para que sea mas claro, de todas maneras voy a ir paso a paso.

NOTA: En la sección Resultados voy a dejar un video para que vean como se utilizan estos archivos y el código fuente libre de comentarios.

Por parte de las variables, la primer línea corresponde a las entradas (Xo,X1) y valor deseado(tk), la segunda a los pesos de la red, la tercera y cuarta al momentum, la quinta y sexta es para el algoritmo de propagación inversa, definí la taza de aprendizaje η en 0.1 y el momentum α en 0.9, y la última linea son variables generales.

#include<stdio.h>
#include<math.h>
bool x0,x1,tk;
float w11,w12,w21,w22,w31,w32,v11,v21,v31;
float dw11,dw12,dw21,dw22,dw31,dw32,dv11,dv21,dv31;
float dw11a,dw12a,dw21a,dw22a,dw31a,dw32a,dv11a,dv21a,dv31a;
float fik,fij1,fij2,sum,E,Et;
float y,o1,o2,ok,eta=0.1,alfa=0.9;
int cont, i;

main()
{
/*Inicializando el Error máximo cuadratico en cero*/
Et=0;
/*Inicializando los valores utilizados en alpha*/
dw11a=0;
dw12a=0;
dw21a=0;
dw22a=0;
dw31a=0;
dw32a=0;
dv11a=0;
dv21a=0;
dv31a=0;
/*Se eligen el valor de los pesos aleatoreamente*/
printf("Dame el valor de w11(j):");
scanf("%f",&w11);
printf("Dame el valor de w12(j):");
scanf("%f",&w12);
printf("Dame el valor de w21(j):");
scanf("%f",&w21);
printf("Dame el valor de w22(j):");
scanf("%f",&w22);
printf("Dame el valor de w31(j):");
scanf("%f",&w31);
printf("Dame el valor de w32(j):");
scanf("%f",&w32);
printf("Dame el valor de w11(k):");
scanf("%f",&v11);
printf("Dame el valor de w21(k):");
scanf("%f",&v21);
printf("Dame el valor de w31(k):");
scanf("%f",&v31);
/*Primer set de entrenamiento*/
x0=0;
x1=0;
tk=0;
/*Inicio del programa principal*/
principal:
while(i!=1000000)
{

Aplicamos lo visto en la sección de teoría, si tienen dudas revisen el apartado XOR, el error lo definí en dos partes, E calcula el cuadrado, mientras que Et es el EMC, no sé como se podría hacer esta operación en una sola linea, en fin, supongo que tu computadora tiene al menos 512Mbytes de RAM, así que a derocharla en variables.

/*Calculando la salida de la red*/
y=x0*w11+x1*w21+w31;
o1=1/(1+exp(-y));
y=x0*w12+x1*w22+w32;
o2=1/(1+exp(-y));
y=v11*o1+v21*o2+v31;
ok=1/(1+exp(-y));
/*Calculando el error*/
E=(pow(tk-ok,2));
Et=0.5*(Et+E);

Por si no lo recuerdan:

Resultado Capa de salida Resultado Capa oculta
/*Inicializando algoritmo para ajustar pesos*/
fik=ok*(1-ok)*(tk-ok);
sum=fik*(v11+v21+v31);
fij1=o1*(1-o1)*sum;
fij2=o2*(1-o2)*sum;

Recuerden que el momentum lo definimos como:

Momentum
/*Ajustando pesos en la capa oculta*/
dv11=eta*fik*o1+alfa*dv11a;
dv21=eta*fik*o2+alfa*dv21a;
dv31=eta*fik*1+alfa*dv31a;
/*Ajustando pesos en la capa de entrada*/
dw11=eta*fij1*x0+alfa*dw11a;
dw12=eta*fij2*x1+alfa*dw12a;
dw21=eta*fij1*x0+alfa*dw21a;
dw22=eta*fij2*x1+alfa*dw22a;
dw31=eta*fij1+alfa*dw31a;
dw32=eta*fij2+alfa*dw32a;
/*Actualizacion de pesos usados en alfa*/
dw11a = dw11;
dw12a = dw12;
dw21a = dw21;
dw22a = dw22;
dw31a = dw31;
dw32a = dw32;
dv11a = dv11;
dv21a = dv21;
dv31a = dv31;
/*Actualizando pesos w=w+delta(w)*/
w11=w11+dw11;
w12=w12+dw12;
w21=w21+dw21;
w22=w22+dw22;
w31=w31+dw31;
w32=w32+dw32;
v11=v11+dv11;
v21=v21+dv21;
v31=v31+dv31;
cont = cont++;

Como es entrenamiento supervisado por lotes, le ponemos el resto del set de la compuerta XOR y repetimos todo el set hasta que aprenda o hasta que el error sea muy pequeño, lo primero que suceda.

if (cont==1) {x0=1;x1=0;tk=1;goto principal;}
if (cont==2) {x0=0;x1=1;tk=1;goto principal;}
if (cont==3) {x0=1;x1=1;tk=0;goto principal;}
cont=0;
i=i++;
x0=0;
x1=0;
tk=0;
goto principal;
}

Por último imprimimos el valor de los pesos, asi como el error que obtuvimos.

printf("###################################################################\n");
printf("i: %d\n",i);
printf("Et: %f\n",Et);
printf("Valor de w11=%f;\n",w11);
printf("Valor de w12=%f;\n",w12);
printf("Valor de w21=%f;\n",w21);
printf("Valor de w22=%f;\n",w22);
printf("Valor de w31=%f;\n",w31);
printf("Valor de w32=%f;\n",w32);
printf("Valor de v11=%f;\n",v11);
printf("Valor de v21=%f;\n",v21);
printf("Valor de v31=%f;\n",v31);
printf("\007");
printf("###################################################################\n");
printf("http://www.edx-twin3.org\n");
printf("###################################################################\n");
getchar();
}

Con esto terminamos el algoritmo, ahora falta comprobar que los pesos calculados sean válidos, para ello voy a crear un archivo "m" en matlab:

clear;
clc;
%Entradas
x0 = 1
x1 = 1
%Pesos
%Aquí van los pesos que encontro el programa
%1.- Obteniendo la salida de la red
y = x0*w11+x1*w21+w31;
nodo1 = 1/(1+exp(-1*y));
y = x0*w12+x1*w22+w32;
nodo2 = 1/(1+exp(-1*y));
y = nodo1*v11+nodo2*v21+v31;
O = 1/(1+exp(-1*y))

Básicamente se trata de la estructura de la red, por lo tanto no vas a tener ningún problema en utilizar otro software para verificar los pesos.

Resultado y palabras finales

Explicación de como utilizar los archivos.


Ustedes pueden intentar con diferentes valores, existen algunos que hacen aún más pequeño el error, solo tengan en cuenta que debido a que estamos utilizando la sigmoide logaritmica la salida de la red NUNCA va a devolver valores cerrados (enteros), la salida para un 0 = 0.0000000000000001 y para un 1 = 0.9999999999999999, por lo tanto deben redondear los valores a cero y uno
Si quieren que la red de los valores cerrados deben cambiar la función de activación a una recta, y como la derivada de la recta es 1 por lo tanto O1k=y(3).
Aquí les dejo los archivos y una lista de pesos con las cuales estuve probando el algoritmo:

Código en c++: xor.cpp
Ejecutable: xor.zip
Archivo m: xor_comp.m
Lista de pesos: pesos.txt

Hemos llegado al final de este tutorial sobre redes neuronales artificiales, esto solo fue una breve introducción existen algoritmos más complejos, pero para empezar en este mundo de la inteligencia artificial es un buen inicio, ya solo falta que empiezen a crear su versión de humanoide estilo ex-maquina, solo un consejo, pongale pila eveready.
No se olviden de recomendar esta pagina en facebook, twitter, google plus o cualquier otra red social, foro, pagina web, o medio de comunicación, tambien pueden seguirme en twitter @edx_twin3 donde pongo las ultimas noticias sobre la página.
Como siempre cualquier duda, sugerencia, comentario, etc ...., la pueden enviar ya sea mediante la pagina de contacto, mediante correo electrónico edx@edx-twin3.org o utilizando la pestaña de Comentarios de este tutorial.
Por último pero no menos importante cualquier donativo es de gran ayuda para seguír haciendo tutoriales como este, gracias :1.

Comentarios, dudas o sugerencias