INTRODUCCIÓN

En este ejercicio, el objetivo es “automatizar” o “generalizar” el problema propuesto en RPubs sobre redes neuronales, trabajando específicamente con la segunda fila de los datos del problema XOR. Los datos se organizaron en una matriz mediante la función matrix, y de la misma manera se definieron los pesos y sesgos.


SOLUCIÓN

En este primer paso, los datos se organizan en dos matrices llamadas X y Y, que representan las variables \(x_1\), \(x_2\) y \(y\), respectivamente. Esta forma de estructuración se eligió para facilitar los cálculos posteriores. A continuación, se muestran en un data frame:

X <- matrix(c(0,0, 0,1, 1,1, 1,0), ncol = 2, byrow = TRUE)
Y <- matrix(c(0, 1, 0, 1), byrow = TRUE)

x1 <- c(0, 0, 1, 1)
x2 <- c(0, 1, 0, 1)
y <- c(0, 1, 0, 1)

logic <- data.frame(x1, x2, y)

knitr::kable(logic)
x1 x2 y
0 0 0
0 1 1
1 0 0
1 1 1

Modelo

Para visualizar la red neuronal, se emplearon dos librerías de Python: networkx y matplotlib.pyplot. La primera se utiliza para trabajar con grafos y la segunda para realizar gráficos.

import networkx as nx   #Permite trabajar con grafos en Python.
import matplotlib.pyplot as plt #Se usa para graficar.

G = nx.DiGraph() #Creamos un grafo dirigido


# Nodos
G.add_node('X1', pos=(0,0.7)) #Añadimos un nodo con la etiqueta X1, en la posición (0,1)
G.add_node('X2', pos=(0,0.3))
G.add_node('H1', pos=(1,0.7))
G.add_node('H2', pos=(1,0.3))
G.add_node('O1', pos=(2,0.5))

#Sesgos
G.add_node('bH', pos = (0.4, 1.1))
G.add_node('bO', pos = (1.4, 1.0))

# Aristas
Aristas = [('X1','H1'), ('X1','H2'), ('X2','H1'), ('X2','H2'), ('H1','O1'), ('H2','O1')] #Cada tupla (origen, destino) representa una flecha de entrada a salida.
G.add_edges_from(Aristas) #Añade esas conexiones al grafo.

# Aristas de sesgo
G.add_edge('bH','H1')
G.add_edge('bH','H2')
G.add_edge('bO','O1')
Aristas_Sesgos = [('bH','H1'), ('bH','H2'), ('bO','O1')]


w1 = 0.1; w2 = 0.5
w3 = -0.7; w4 = 0.3
b1 = 0; b2 = 0

w5 = 0.2; w6 = 0.4
b3 = 0

W_Ocultos = [[w1, w2],
             [w3, w4]]
W_Salida = [w5, w6]

b_Ocultos = [[b1, b2]]
b_Salida = b3

edge_labels = {
    ('X1','H1'): f"w1 = {W_Ocultos[0][0]:.1f}", #Indica que es la arista que va de la neurona X1 a H1. Usa fstring para convertir el valor del peso W_Ocultos[0][0] a un string con 1 decimal.
    ('X1','H2'): f"w2 = {W_Ocultos[0][1]:.1f}",
    ('X2','H1'): f"w3 = {W_Ocultos[1][0]:.1f}",
    ('X2','H2'): f"w4 = {W_Ocultos[1][1]:.1f}",
    ('H1','O1'): f"w5 = {W_Salida[0]:.1f}",
    ('H2','O1'): f"w6 = {W_Salida[1]:.1f}"
}

sesgos_edge_labels = {
    ('bH','H1'): r"$\mathbf{b_1=0.1}$",
    ('bH','H2'): r"$\mathbf{b_2=-0.7}$",
    ('bO','O1'): r"$\mathbf{b_3=0.2}$"
}


# Posiciones
pos = nx.get_node_attributes(G, 'pos') #Diccionario de posiciones de cada nodo. Se usa para dibujar la red en esas coordenadas específicas.

nodos = ['X1','X2','H1','H2','O1']

# Dibujar
nx.draw(G, pos, with_labels=True, nodelist=nodos, node_size=2000, node_color=['red','red','red','red','red'], font_weight='bold', edgecolors='black', linewidths=2, labels = {
    'X1': r'$\mathbf{X_1}$',
    'X2': r'$\mathbf{X_2}$',
    'H1': r'$\mathbf{h_1}$',
    'H2': r'$\mathbf{h_2}$',
    'O1': r'$\mathbf{O_1}$'
}); #nx.draw dibuja el grafo (pos: Coordenadas de los nodos; with_labels=True: Muestra el nombre de cada nodo; node_size: El tamaño de los nodos; node_color: El color de cada nodo)

#Dibujar sesgos
sesgos = ['bH','bO']
_ = nx.draw_networkx_nodes(
    G,
    pos,
    nodelist=sesgos,
    node_shape='^',  # triángulo
    node_color=['gray','gray'],
    node_size=1500, edgecolors='black', linewidths=2
);

sesgos_node_labels = {
    'bH': r'$\mathbf{1}$',
    'bO': r'$\mathbf{1}$'
}
_=nx.draw_networkx_labels(G, pos, labels=sesgos_node_labels, font_size=12);

_=nx.draw_networkx_edges(
    G, pos,
    edgelist=Aristas_Sesgos,          # Qué aristas dibujar
    edge_color='green',           # Color de la flecha
    arrows=True,                # Asegura que se dibujen flechas
    width=2                     # Grosor de la flecha
);

_=nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, label_pos=0.7, font_weight='bold', font_size = 10); #Dibuja etiquetas sobre las aristas de un grafo. edge_labels: Un diccionario que dice que mostrar en cada arista.
_ = nx.draw_networkx_edge_labels(G, pos, edge_labels=sesgos_edge_labels, font_size=10, label_pos = 0.65, font_color='green');

plt.show() #Muestra el grafo


Automatización

Procedemos a realizar los cálculos mediante funciones y ciclos. Dado que en el ejemplo original se consideran dos épocas, aquí también se trabajará con el mismo número de iteraciones.

Al finalizar las dos épocas, los resultados obtenidos son los siguientes:

Sigmoide <- function(x){
  1 / (1 + exp(-x))
}

Dev_Sigmoide <- Deriv(Sigmoide, "x")

w1 <- 0.1; w2 <- 0.5
w3 <- -0.7; w4 <- 0.3
b1 <- 0; b2 <- 0

w5 <- 0.2; w6 <- 0.4
b3 <- 0

#Fijamos los pesos
W_Ocultos <- matrix(c(w1, w3, w2, w4), nrow = 2, byrow = TRUE)
W_Salida <- matrix(c(w5, w6), nrow = 1, byrow = TRUE)

#Fijamos los sesgos
b_Ocultos <- c(b1, b2)
b_Salida <- b3


epocas = 2
Tasa_Aprendizaje = 0.25
Entradas <- matrix(X[2, ], ncol = 1)
Objetivo <- matrix(Y[2, ], ncol = 1)
resultados <- list()

for (epoca in 1:epocas){
  #Cálculo
  
  #Neurona h1 y h2
  Z_Ocultos <- W_Ocultos %*% Entradas + b_Ocultos  #%*% Producto punto entre matrices o vectores
  f_Ocultos <- Sigmoide(Z_Ocultos)
  
  #Neurona O1 (Salida)
  Z_Salida <- W_Salida %*% f_Ocultos + b_Salida
  f_Salida <- Sigmoide(Z_Salida)
  
  
  #Error
  Error <- 0.5 * (Objetivo - f_Salida) ^ 2
  
  #BACKPROPAGATION
  #Output Layer
  #Gradiente para capa salida
  DError_Df <- -(Objetivo - f_Salida)
  Df_Dz <- Dev_Sigmoide(Z_Salida)
  DError_Dz   <- DError_Df * Df_Dz
  
  #Gradiente de capa de salida
  DError_Dw_Salida <- DError_Dz %*% t(f_Ocultos)
  DError_Dw_Salida
  
  #Hidden Layer
  #Gradiente para capa oculta (h1, h2)
  DError_Dz_Oculto <- (t(W_Salida) %*% DError_Dz) * Dev_Sigmoide(Z_Ocultos)
  
  #Gradiente de capa oculta
  DError_Dw_Oculto <- DError_Dz_Oculto %*% t(Entradas)
  
  #Nuevos Pesos
  W_Ocultos <- W_Ocultos - Tasa_Aprendizaje * DError_Dw_Oculto
  W_Salida  <- W_Salida  - Tasa_Aprendizaje * DError_Dw_Salida
  
  resultados[[epoca]] <- c(
    Epoca = epoca,
    as.vector(W_Ocultos),
    W_Salida,
    f_Salida = f_Salida,
    Error = Error
  )
  
}

tabla_resultados <- do.call(rbind, resultados)
colnames(tabla_resultados) <- c(
  "Época",
  "$\\boldsymbol{w_1}$", "$\\boldsymbol{w_2}$", "$\\boldsymbol{w_3}$", "$\\boldsymbol{w_4}$",
  "$\\boldsymbol{w_5}$", "$\\boldsymbol{w_6}$",
  "$\\boldsymbol{O_1}$", "$\\boldsymbol{E_{total}}$"
)

tabla_resultados %>%
  kable(
    format = "html",              
    caption = "Evolución de los pesos y error por época",
    digits = 5,                   # Número de decimales
    align = "c"                   # Centrar columnas
  ) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE,
    position = "center"
  ) %>%
  row_spec(1, bold = TRUE, color = "white", background = "steelblue") %>%  
  row_spec(2, italic = TRUE, background = "lightyellow") %>%               
  column_spec(1, bold = TRUE, color = "white", background = "darkred")     
Evolución de los pesos y error por época
Época \(\boldsymbol{w_1}\) \(\boldsymbol{w_2}\) \(\boldsymbol{w_3}\) \(\boldsymbol{w_4}\) \(\boldsymbol{w_5}\) \(\boldsymbol{w_6}\) \(\boldsymbol{O_1}\) \(\boldsymbol{E_{total}}\)
1 0.1 0.5 -0.69884 0.30255 0.20865 0.41498 0.57350 0.09095
2 0.1 0.5 -0.69765 0.30517 0.21724 0.42985 0.57638 0.08973


Observamos que entre la época 1 y la época 2 no hay una diferencia significativa, lo que indica que serán necesarias muchas más épocas para alcanzar el resultado esperado.