eXclusive OR (XOR) es una operación lógica booleana muy utilizada en criptografía y en la generación de bits de paridad para la comprobación de errores y la tolerancia a fallos. XOR compara dos bits de entrada y genera un bit de salida. La lógica es sencilla. Si los bits son iguales, el resultado es 0. Si los bits son diferentes, el resultado es 1.

El ejemplo que se trabajo en clase se trata de una red neuronal con dos inputs, dos neuronas ocultas y una neurona de salida, con la función Sigmoide como función de activación. Este código fue desarrollado con el objetivo de ejecutar automaticamente n-iteraciones del proceso de retropropagación que busca el entrenamiento del algoritmo optimizando los pesos para que la red neuronal pueda aprender a asignar entradas arbitrarias con su respecctiva salida.

Se utilizaron las librerías ‘igraph’, ‘ggraph’ y ‘ggplot2’ para la graficación de la red neuronal, y la librería Deriv para las derivadas parciales que se requieren para el proceso de retropropagación.

Inicialmente se asignan los pesos iniciales de cada aristas y los sesgos, para el ejemplo se trabajo con los valores iniciales que se pueden observar, y con 0 sesgos:

#Ingrese el valor de cada arista
w1 <<- 0.1
w2 <<- 0.5
w3 <<- -0.7
w4 <<- 0.3
w5 <<- 0.2
w6 <<- 0.4

#Ingrese el valor de los sesgos  
b1 <<- 0
b2 <<- 0
b3 <<- 0


k <<- 1

#Función de activación Sigmoide
f_activacion <- function(x) {
  1/(1 + exp(-x))
} 

#Derivada de la función de activación
df_activacion <- Deriv(f_activacion, "x" ) 

#Función que ejecuta una época
f_epoca <- function(x1,x2) {
  
  #h1
  z1 = w1*x1 + w3*x2 + b1
  h1 = f_activacion(z1)
  #h2
  z2 = w2*x1 + w4*x2 + b2
  h2 = f_activacion(z2)
  #o1
  z3 = w5*h1 + w6*h2 + b3
  o1 = f_activacion(z3)
  
  return(list(z1=z1, h1=h1, z2=z2, h2=h2, z3=z3, o1=o1))
  
} 

#Función error total
f_error_total <- function(o_esp, o1) {
  sum(0.5 * (o_esp - o1)^2)
} 

#Derivada del error total 
df_error_total <- Deriv(f_error_total, "o1") 

#Función que genera al grafo
f_grafico <- function(x1,x2,x3,x4,x5,x6){
  #Nodos
  nodes <- data.frame(
    name = c("X1","X2","H1","H2","O1","B1","B2"),
    label = c("X1\n0","X2\n1","H1","H2","O1\n1","B1","B2"),
    type = c("input","input","hidden","hidden","output","bias","bias"),
    x = c(0,0,3.5,3.5,7,2,5),
    y = c(3,0,3,0,1.5,4,4)
  )
  
  #Aristas
  edges <- data.frame(
    from = c("X1","X1","X2","X2","H1","H2","B1","B1","B2"),
    to   = c("H1","H2","H1","H2","O1","O1","H1","H2","O1"),
    label= c(paste("W1 = ",x1),paste("W2 = ",x2),paste("W3 = ",x3)
             ,paste("W4 = ",x4),paste("W5 = ",x5),paste("W6 = ",x6),
             "B1 = 0","B2 = 0","B3 = 0"),
    color= c("black","black","black","black","black","black","green","green","green")
  )
  
  #Creación del grafo
  g <- graph_from_data_frame(edges, vertices = nodes)
  
  #Graficación
  
  ggraph(g, layout = "manual", x = nodes$x, y = nodes$y) +
    
    #Aristas
    geom_edge_link(aes(label = label, color = I(color)),
                   angle_calc = 'along',
                   label_dodge = unit(2.5, 'mm'),
                   label_size = 3,
                   label_pos = 0.3,
                   arrow = arrow(length = unit(3, 'mm'), type = "closed"),
                   end_cap = circle(16, 'pt'))+
    
    #Nodos
    geom_node_point(aes(shape = type, color = type, fill = type), size = 15, stroke = 1) +
    
    geom_node_text(aes(label = label, x = x, y = y), color="black", size=4, fontface="bold") +
    
    scale_shape_manual(values = c(
      input = 21, hidden = 21, output = 21, bias = 17  #21 es circulo, 17 es triangulo
    )) +
    
    scale_fill_manual(values=c("input"="#F52754","hidden"="#F52754","output"="#F52754","bias"="black")) +
    
    scale_color_manual(values = c(
      input = "black", hidden = "black", output = "black", bias = "grey"
    )) +
    
    guides(shape = "none", color = "none", fill = "none")+
    
    theme_void()
} 

Al momento de ejecutar el código, se asignan los valores de entrada x1 y x2, el valor que se espera obtener al ejecutar la función y el número de épocas que se quieran observar, luego de esto el código ejecuta un ciclo iterativo en el cual se ejecuta cada época. En cada época se genera inicialmente la red neuronal con los pesos actuales, luego de eso se muestra la salida que se obtuvo y el error total, luego de esto internamente se realiza el proceso de retropropagación y se actualizan los pesos de la red neuronal para dar paso a la siguiente epoca. Para este ejemplo se realizara un prueba con 10 épocas:

num1 <- 0 #Digite x1
num2 <- 1 #Digite x2
num3 <- 1 #Digite el valor esperado
num4 <- 10 #Digite el número de épocas que quiere ver
tasa_de_aprendizaje <- 0.25 #Digite la tasa de aprendizaje

#Este ciclo se encarga de todas las operaciones de la retropropagación
while(k <= num4){
  
  #grafico de la epoca  
  print(f_grafico(w1,w2,w3,w4,w5,w6))  
  
  
  salida <- f_epoca(num1,num2) 
  print(paste("La salida de la época ", k, " Es la siguiente: ", salida$o1 ))
  
  error <- f_error_total(num3,salida$o1)
  print(paste("El error respecto al valor esperado en la época ", k, "fue de: ", error))
  
  
  #Retropropagacion
  #Output layer
  dfz3 = df_error_total(num3, salida$o1)*df_activacion(salida$z3)
  
  dfw5 = dfz3*salida$h1
  dfw6 = dfz3*salida$h2
  
  actualizacion5 = w5 - tasa_de_aprendizaje*dfw5
  actualizacion6 = w6 - tasa_de_aprendizaje*dfw6 
  
  #Hidden layer
  dfz1 = dfz3*w5*df_activacion(salida$z1)
  dfz2 = dfz3*w6*df_activacion(salida$z2)
  
  #neurona h1
  dfw1 = dfz1*num1
  dfw3 = dfz1*num2
  
  actualizacion1 = w1 - tasa_de_aprendizaje*dfw1
  actualizacion3 = w3 - tasa_de_aprendizaje*dfw3
  
  #neurona h2
  dfw2 = dfz2*num1
  dfw4 = dfz2*num2
  
  actualizacion2 = w2 - tasa_de_aprendizaje*dfw2
  actualizacion4 = w4 -tasa_de_aprendizaje*dfw4
  
  #reemplazamos los w
  paste("los pesos actualizados son los siguientes: ")
  w1 = actualizacion1
  w2 = actualizacion2
  w3 = actualizacion3
  w4 = actualizacion4
  w5 = actualizacion5
  w6 = actualizacion6
  
  k <- k+1
}

## [1] "La salida de la época  1  Es la siguiente:  0.573498503710098"
## [1] "El error respecto al valor esperado en la época  1 fue de:  0.0909517631687627"

## [1] "La salida de la época  2  Es la siguiente:  0.576379711212853"
## [1] "El error respecto al valor esperado en la época  2 fue de:  0.0897270745360528"

## [1] "La salida de la época  3  Es la siguiente:  0.579242611989336"
## [1] "El error respecto al valor esperado en la época  3 fue de:  0.0885183897827782"

## [1] "La salida de la época  4  Es la siguiente:  0.582087260829031"
## [1] "El error respecto al valor esperado en la época  4 fue de:  0.0873255287806913"

## [1] "La salida de la época  5  Es la siguiente:  0.584913705115242"
## [1] "El error respecto al valor esperado en la época  5 fue de:  0.0861483161005782"

## [1] "La salida de la época  6  Es la siguiente:  0.587721985240695"
## [1] "El error respecto al valor esperado en la época  6 fue de:  0.084986580726937"

## [1] "La salida de la época  7  Es la siguiente:  0.590512135029322"
## [1] "El error respecto al valor esperado en la época  7 fue de:  0.0838401557791221"

## [1] "La salida de la época  8  Es la siguiente:  0.593284182162425"
## [1] "El error respecto al valor esperado en la época  8 fue de:  0.0827088782396437"

## [1] "La salida de la época  9  Es la siguiente:  0.596038148607473"
## [1] "El error respecto al valor esperado en la época  9 fue de:  0.081592588690239"

## [1] "La salida de la época  10  Es la siguiente:  0.598774051047897"
## [1] "El error respecto al valor esperado en la época  10 fue de:  0.0804911310562579"

Como se puede observar, conforme se va ejecutando cada época, el valor de salida se va acercando más y más al valor esperado (que en este caso es 1), y el error total va disminuyendo, lo cual permite identificar la efectividad del algoritmo de aprendizaje.