Introducción

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: si los bits son iguales, el resultado es 0; si los bits son diferentes, el resultado es 1.

# Tabla de la función XOR
xor_df <- data.frame(
  x1 = c(0,0,1,1),
  x2 = c(0,1,0,1),
  y  = c(0,1,1,0)
)
xor_df
##   x1 x2 y
## 1  0  0 0
## 2  0  1 1
## 3  1  0 1
## 4  1  1 0

En esta práctica construiremos, entrenaremos y visualizaremos una red neuronal 2–2–1 (dos entradas, dos neuronas ocultas, una salida) para aproximar el comportamiento de XOR usando función logística y retropropagación. Automatizaremos el flujo completo para cualquier número de épocas y pesos.


El modelo

Trabajaremos con una red densa de topología 2–2–1:

Usamos los siguientes pesos iniciales (ajustables desde el chunk de R):

El conjunto de entrenamiento de esta sección es un único patrón: \((x_1,x_2)=(0,1)\) con etiqueta \(y=1\).
Esto nos permite replicar exactamente los números de la primera época y verificar el procedimiento.

Desarrollo automatizado

El bloque de entrenamiento recibe X1, X2, y, n_epocas y tasa_apren, además de init_params, y ejecuta:

  1. Forward pass: obtiene \(\hat y\) y el error de la época.
  2. Backpropagation: calcula gradientes en salida y oculta.
  3. Actualización: ajusta \(w_1..w_6\) con tasa de aprendizaje específica.
  4. Registro: guarda epoch, O1, error y pesos actualizados.
  5. Repite 2–5 hasta completar {n_epocas} época(s).
  6. Tabla gt: muestra el resumen con una fila por época.
  7. Graficar la red inicial con pesos/sesgos de partida, y la final tras la última época.

¿Cómo usarlo? Cambia X1, X2, O1_deseado, n_epocas y tasa_apren; ejecuta el chunk completo. Todo (gráficos y tabla) se recalcula automáticamente.

# ========== XOR 2-2-1 ENTRENAMIENTO ==========

# Entradas
X1 <- 0; X2 <- 1; O1_deseado <- 1
n_epocas <- 2
tasa_apren <- 0.25

# Pesos iniciales 
init_params <- list(
  w1=0.1, w2=0.5, w3=-0.7, w4=0.3, w5=0.2, w6=0.4,
  b1=0,   b2=0,   b3=0
)

# Librerías
suppressPackageStartupMessages({
  library(ggplot2)
  library(grid)
  library(reticulate)
})

# Paquetes de Python
if (!py_module_available("matplotlib")) py_install("matplotlib", pip=TRUE)
if (!py_module_available("networkx"))  py_install("networkx",  pip=TRUE)

# Utilidades
sigmoid <- function(z) 1/(1+exp(-z))
make_params <- function(w1,w2,w3,w4,w5,w6,b1,b2,b3) list(w1=w1,w2=w2,w3=w3,w4=w4,w5=w5,w6=w6,b1=b1,b2=b2,b3=b3)
round_df <- function(df, digits=6){ num <- sapply(df, is.numeric); df[num] <- lapply(df[num], round, digits); df }

forward <- function(x1,x2,y,p){
  z1 <- p$w1*x1 + p$w3*x2 + p$b1; h1 <- sigmoid(z1)
  z2 <- p$w2*x1 + p$w4*x2 + p$b2; h2 <- sigmoid(z2)
  z3 <- p$w5*h1 + p$w6*h2 + p$b3; o1 <- sigmoid(z3)
  loss <- 0.5*(y - o1)^2                      # error de la época (único)
  list(cache=list(x1=x1,x2=x2,z1=z1,h1=h1,z2=z2,h2=h2,z3=z3,o1=o1,y=y), loss=as.numeric(loss))
}

# Gradiente 
backward <- function(cache, p){
  with(cache, {
    dE_dz3 <- (o1 - y) * (o1*(1 - o1))  #corregido 
    g_w5 <- dE_dz3 * h1
    g_w6 <- dE_dz3 * h2
    dE_dz1 <- dE_dz3 * p$w5 * (h1*(1 - h1))
    dE_dz2 <- dE_dz3 * p$w6 * (h2*(1 - h2))
    list(
      w1=dE_dz1*x1, w3=dE_dz1*x2,
      w2=dE_dz2*x1, w4=dE_dz2*x2,
      w5=g_w5,      w6=g_w6
    )
  })
}

step_update <- function(p, g, tasa_apren){
  p$w1 <- p$w1 - tasa_apren*g$w1; p$w2 <- p$w2 - tasa_apren*g$w2
  p$w3 <- p$w3 - tasa_apren*g$w3; p$w4 <- p$w4 - tasa_apren*g$w4
  p$w5 <- p$w5 - tasa_apren*g$w5; p$w6 <- p$w6 - tasa_apren*g$w6
  p
}

train_epocas <- function(x1,x2,y,p,tasa_apren,n_epocas){
  history <- data.frame()
  for (e in seq_len(n_epocas)){
    fwd_before <- forward(x1,x2,y,p)     
    grads <- backward(fwd_before$cache, p)
    p <- step_update(p, grads, tasa_apren)      

    history <- rbind(history, data.frame(
      epoch=e, O1=as.numeric(fwd_before$cache$o1),
      w1=p$w1,w2=p$w2,w3=p$w3,w4=p$w4,w5=p$w5,w6=p$w6,
      b1=p$b1,b2=p$b2,b3=p$b3,
      error=as.numeric(fwd_before$loss)
    ))
  }
  list(params=p, history=history)
}

# Ejecutar entrenamiento
params0 <- do.call(make_params, init_params)
train <- train_epocas(X1, X2, O1_deseado, params0, tasa_apren=tasa_apren, n_epocas=n_epocas)
paramsF <- train$params

# Pasar a Python para graficar (inicial y final)
build_graph_dict <- function(p, x1,x2,y){
  list(
    X1=as.numeric(x1), X2=as.numeric(x2), O1_target=as.numeric(y),
    weights=list(w1=p$w1, w2=p$w2, w3=p$w3, w4=p$w4, w5=p$w5, w6=p$w6),
    biases =list(b1=p$b1, b2=p$b2, b3=p$b3)
  )
}
py$NN_INIT     <- build_graph_dict(params0, X1, X2, O1_deseado)
py$NN_FINAL    <- build_graph_dict(paramsF, X1, X2, O1_deseado)
py$TITLE_INIT  <- sprintf("Red XOR - Inicial", X1, X2, O1_deseado)
py$TITLE_FINAL <- sprintf("Red XOR - Final tras %d epoca(s)", n_epocas, tasa_apren)

# Tabla resumen general
cat("\n=== Resumen general por época ===\n")
## 
## === Resumen general por época ===
print(round_df(train$history, 6), row.names = FALSE)
##  epoch       O1  w1  w2        w3       w4       w5       w6 b1 b2 b3    error
##      1 0.573499 0.1 0.5 -0.698844 0.302550 0.208654 0.414982  0  0  0 0.090952
##      2 0.576380 0.1 0.5 -0.697647 0.305172 0.217241 0.429852  0  0  0 0.089727

A continuación, se presenta el registro de evolución por época condensado en una tabla:

# Tabla 
if (!requireNamespace("gt", quietly = TRUE)) install.packages("gt")
if (!requireNamespace("dplyr", quietly = TRUE)) install.packages("dplyr")

library(gt)
library(dplyr)

pretty_history_multicolor <- function(history, X1, X2, y, tasa_apren, n_epocas){

  # Orden y tipos 
  df <- history %>%
    select(epoch, O1, w1, w2, w3, w4, w5, w6, b1, b2, b3, error) %>%
    mutate(epoch = as.integer(epoch))
  num_cols <- setdiff(names(df), "epoch")
  df[num_cols] <- lapply(df[num_cols], function(x) round(x, 6))

  
  row_cols <- c("#FFF2CC", "#E2F0D9", "#DDEBF7", "#FCE4D6",
                "#EDE7F6", "#E2EFDA", "#F8CECC", "#D9E1F2")
  row_cols <- rep_len(row_cols, nrow(df))

  # Tabla base
  tbl <- gt(df) %>%
    tab_header(
      title = md("**Resumen de entrenamiento XOR**"),
      subtitle = md(sprintf("Inputs: X1=%d, X2=%d, y=%d   •   Tasa de aprendizaje=%.2f   •   épocas=%d",
                            X1, X2, y, tasa_apren, n_epocas))
    ) %>%
    tab_style(style = cell_text(color = "white"), locations = cells_title(groups = "title")) %>%
    tab_style(style = cell_text(color = "white"), locations = cells_title(groups = "subtitle")) %>%
    cols_label(
      epoch = "Época",
      O1    = "O1 (salida)",
      error = "Error total"
    ) %>%
    cols_width(
      epoch ~ px(70),
      O1 ~ px(110),
      c(w1, w2, w3, w4, w5, w6) ~ px(95),
      c(b1, b2, b3) ~ px(70),
      error ~ px(120)
    ) %>%
    # Formato numérico (sin tocar 'epoch', w1,w2,b´s)
    fmt_number(columns = c(O1, w3, w4, w5, w6, error),
               decimals = 6, use_seps = FALSE) %>%
    # Estética general
    tab_options(
      table.border.top.color = "transparent",
      table.border.bottom.color = "transparent",
      heading.background.color = "#111827",
      heading.title.font.size = px(18),
      heading.subtitle.font.size = px(13),
      column_labels.background.color = "#f3f4f6",
      column_labels.font.weight = "bold",
      data_row.padding = px(6)
    ) %>%
    opt_table_font(font = c("Inter","Arial","Verdana","Sans-Serif")) %>%
    tab_source_note(md("Evolución del modelo."))

  # Pintar cada fila con un color distinto
  for (i in seq_len(nrow(df))) {
    tbl <- tab_style(
      tbl,
      style = cell_fill(color = row_cols[i]),
      locations = cells_body(rows = i, columns = everything())
    )
  }

  tbl
}

# Mostrar la tabla
resumen_formal <- pretty_history_multicolor(train$history, X1, X2, O1_deseado, tasa_apren, n_epocas)
resumen_formal
Resumen de entrenamiento XOR
Inputs: X1=0, X2=1, y=1 • Tasa de aprendizaje=0.25 • épocas=2
Época O1 (salida) w1 w2 w3 w4 w5 w6 b1 b2 b3 Error total
1 0.573499 0.1 0.5 −0.698844 0.302550 0.208654 0.414982 0 0 0 0.090952
2 0.576380 0.1 0.5 −0.697647 0.305172 0.217241 0.429852 0 0 0 0.089727
Evolución del modelo.
# FUNCION DE GRAFICO 
import matplotlib.pyplot as plt
import networkx as nx

def plot_nn(graph, title="Red Neuronal XOR"):
    G = nx.DiGraph()

    # nodos y posiciones
    nodos = ["X1", "X2", "h1", "h2", "O1", "b1", "b2", "b3"]
    G.add_nodes_from(nodos)
    pos = {
        "X1": (-2,  1), "X2": (-2, -1),
        "h1": ( 0,  1), "h2": ( 0, -1),
        "O1": ( 2,  0),
        "b1": (-3,  2), "b2": (-3, -2),
        "b3": ( 1,  2)
    }

    w = graph["weights"]; b = graph["biases"]
    x1 = int(graph["X1"]); x2 = int(graph["X2"]); y = int(graph["O1_target"])

    
    edges = [
        ("X1","h1", f"W1={w['w1']:.6f}"),
        ("X1","h2", f"W2={w['w2']:.6f}"),
        ("X2","h1", f"W3={w['w3']:.6f}"),
        ("X2","h2", f"W4={w['w4']:.6f}"),
        ("h1","O1", f"W5={w['w5']:.6f}"),
        ("h2","O1", f"W6={w['w6']:.6f}"),
        ("b1","h1", f"b1={b['b1']:.0f}"),
        ("b2","h2", f"b2={b['b2']:.0f}"),
        ("b3","O1", f"b3={b['b3']:.0f}")
    ]
    G.add_weighted_edges_from([(u, v, 1.0) for u, v, _ in edges])

    plt.figure(figsize=(8, 5))

    # nodos
    nx.draw_networkx_nodes(G, pos,
        nodelist=["X1","X2","h1","h2","O1"], node_color="lightcoral", node_size=1800)
    nx.draw_networkx_nodes(G, pos,
        nodelist=["b1","b2","b3"], node_color="lightgray", node_shape="^", node_size=1300)

   
    labels = {"X1":f"X1\n{x1}", "X2":f"X2\n{x2}",
              "h1":"h1", "h2":"h2", "O1":f"O1\n{y}",
              "b1":"1", "b2":"1", "b3":"1"}
    nx.draw_networkx_labels(G, pos, labels, font_size=12, font_weight="bold")

    # conexiones y etiquetas de aristas
    nx.draw_networkx_edges(G, pos, edgelist=[(u, v) for u, v, _ in edges],
                           arrows=True, arrowstyle="-|>", arrowsize=18)
    edge_labels = {(u, v): w for u, v, w in edges}
    nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_size=10)

    # textos de capas (ASCII con mathtext)
    plt.text(-2, -2.5, r"Input Layer $\in \mathbb{R}^2$",  fontsize=11)
    plt.text( 0, -2.5, r"Hidden Layer $\in \mathbb{R}^2$", fontsize=11)
    plt.text( 2, -2.5, r"Output Layer $\in \mathbb{R}^1$", fontsize=11)

    plt.title(title, fontsize=14, fontweight="bold")
    plt.axis("off")
    plt.tight_layout()
    plt.show()
# Grafico inicial (pesos de partida)
plot_nn(NN_INIT,  TITLE_INIT)

# Grafico final (pesos tras la ultima epoca)
plot_nn(NN_FINAL, TITLE_FINAL)

NOTAS