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.
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.
El bloque de entrenamiento recibe X1, X2,
y, n_epocas y tasa_apren, además
de init_params, y ejecuta:
epoch,
O1, error y pesos actualizados.gt: muestra el resumen con una
fila por é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)