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.
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 |
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
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")
É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.