Este documento desarrolla paso a paso el entrenamiento manual de una red neuronal artificial para aprender la función XOR (OR exclusivo). Se implementa desde cero en R, sin ninguna librería de aprendizaje automático, mostrando todos los cálculos intermedios de manera explícita.
La función XOR produce 1 cuando sus entradas son diferentes, y 0 cuando son iguales. Matemáticamente:
| \(x_1\) | \(x_2\) | \(y\) | Interpretación |
|---|---|---|---|
| 0 | 0 | 0 | Mismas entradas → 0 |
| 0 | 1 | 1 | Entradas distintas → 1 |
| 1 | 0 | 1 | Entradas distintas → 1 |
| 1 | 1 | 0 | Mismas entradas → 0 |
¿Por qué es importante? XOR no es linealmente separable, es decir, ninguna línea recta puede separar ambas clases. Por ello, una red de una sola capa no puede aprenderlo: se necesita al menos una capa oculta.
La red tiene:
\[\sigma(z) = \frac{1}{1 + e^{-z}}\]
Su derivada es:
\[\sigma'(z) = \sigma(z)\,(1 - \sigma(z))\]
Esta propiedad hace que el cálculo de gradientes sea muy eficiente.
Para una sola observación usamos:
\[E = \frac{1}{2}(y - \hat{y})^2\]
El factor \(\frac{1}{2}\) simplifica la derivada:
\[\frac{\partial E}{\partial \hat{y}} = \hat{y} - y\]
# ============================================================
# Funciones fundamentales de la red neuronal
# ============================================================
# Función sigmoide
sigmoid <- function(z) {
1 / (1 + exp(-z))
}
# Derivada de la sigmoide (expresada en términos de h = sigma(z))
sigmoid_deriv <- function(h) {
h * (1 - h)
}
# Función de pérdida: error cuadrático medio (para 1 observación)
loss <- function(y, y_hat) {
0.5 * (y - y_hat)^2
}
# Gradiente de la pérdida respecto a y_hat
grad_loss <- function(y, y_hat) {
y_hat - y
}Nota: La derivada de la sigmoide se calcula directamente desde la activación \(h\), no desde \(z\). Esto evita recalcular la exponencial y hace el código más eficiente.
Una época (epoch) es una pasada completa por el conjunto de entrenamiento: primero se calcula la predicción (forward pass), luego se propaga el error hacia atrás (backpropagation) y se actualizan los pesos.
Estos valores son los obtenidos al final de la época 2 (reportados en las notas de clase):
# Observación de entrenamiento
x1 <- 0; x2 <- 1; y_target <- 1
# Tasa de aprendizaje
alpha <- 0.25
# Pesos actualizados al final de la época 2
w1 <- 0.10000 # x1 → h1
w2 <- 0.50000 # x1 → h2
w3 <- -0.69764 # x2 → h1
w4 <- 0.30517 # x2 → h2
w5 <- 0.21724 # h1 → y_hat
w6 <- 0.42985 # h2 → y_hat
# Sesgos (todos en 0 en este ejemplo)
b1 <- 0; b2 <- 0; b3 <- 0
cat("=== Parámetros al inicio de la Época 3 ===\n")## === Parámetros al inicio de la Época 3 ===
## w1 = 0.10000 w2 = 0.50000
## w3 = -0.69764 w4 = 0.30517
## w5 = 0.21724 w6 = 0.42985
## b1 = 0 b2 = 0 b3 = 0
\[z_1 = w_1 x_1 + w_3 x_2 + b_1\] \[h_1 = \sigma(z_1)\]
z1 <- w1*x1 + w3*x2 + b1
h1 <- sigmoid(z1)
cat(sprintf("z1 = (%.5f)(0) + (%.5f)(1) + 0 = %.5f\n", w1, w3, z1))## z1 = (0.10000)(0) + (-0.69764)(1) + 0 = -0.69764
## h1 = sigma(-0.69764) = 0.33234
\[z_2 = w_2 x_1 + w_4 x_2 + b_2\] \[h_2 = \sigma(z_2)\]
z2 <- w2*x1 + w4*x2 + b2
h2 <- sigmoid(z2)
cat(sprintf("z2 = (%.5f)(0) + (%.5f)(1) + 0 = %.5f\n", w2, w4, z2))## z2 = (0.50000)(0) + (0.30517)(1) + 0 = 0.30517
## h2 = sigma(0.30517) = 0.57571
\[z_3 = w_5 h_1 + w_6 h_2 + b_3\] \[\hat{y} = \sigma(z_3)\]
z3 <- w5*h1 + w6*h2 + b3
yhat <- sigmoid(z3)
E <- loss(y_target, yhat)
cat(sprintf("z3 = (%.5f)(%.5f) + (%.5f)(%.5f) + 0 = %.5f\n",
w5, h1, w6, h2, z3))## z3 = (0.21724)(0.33234) + (0.42985)(0.57571) + 0 = 0.31966
## yhat = sigma(0.31966) = 0.57924
## E = 0.5*(1 - 0.57924)^2 = 0.08852
Análisis La predicción sube de \(\hat{y} \approx 0.5764\) (época 2) a un valor ligeramente mayor, acercándose al objetivo \(y = 1\). El error continúa disminuyendo, confirmando que el gradiente apunta en la dirección correcta.
El error se propaga hacia atrás usando la regla de la cadena. Para cada peso \(w_i\), calculamos:
\[\frac{\partial E}{\partial w_i} = \frac{\partial E}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z_3} \cdot \frac{\partial z_3}{\partial w_i}\]
El error local en la capa de salida es:
\[\delta_3 = (\hat{y} - y) \cdot \hat{y}(1 - \hat{y})\]
delta3 <- grad_loss(y_target, yhat) * sigmoid_deriv(yhat)
dE_dw5 <- delta3 * h1
dE_dw6 <- delta3 * h2
# NOTA: los sesgos b1=b2=b3=0 se mantienen fijos en este ejemplo,
# siguiendo exactamente el documento de clase (LLinas, 2026),
# que no actualiza sesgos. La función automatizada (Parte 2)
# tampoco los actualiza para mantener consistencia.
cat(sprintf("delta3 = (%.5f - 1) * %.5f * (1 - %.5f) = %.5f\n",
yhat, yhat, yhat, delta3))## delta3 = (0.57924 - 1) * 0.57924 * (1 - 0.57924) = -0.10255
## dE/dw5 = delta3 * h1 = (-0.10255)(0.33234) = -0.03408
## dE/dw6 = delta3 * h2 = (-0.10255)(0.57571) = -0.05904
# Actualización pesos de salida (sesgos permanecen en 0)
w5_new <- w5 - alpha * dE_dw5
w6_new <- w6 - alpha * dE_dw6
cat(sprintf("\nw5_nuevo = %.5f - 0.25*(%.5f) = %.5f\n", w5, dE_dw5, w5_new))##
## w5_nuevo = 0.21724 - 0.25*(-0.03408) = 0.22576
## w6_nuevo = 0.42985 - 0.25*(-0.05904) = 0.44461
El error local se propaga hacia atrás multiplicando por el peso correspondiente:
\[\delta_1 = \delta_3 \cdot w_5 \cdot h_1(1 - h_1)\]
delta1 <- delta3 * w5 * sigmoid_deriv(h1)
dE_dw1 <- delta1 * x1
dE_dw3 <- delta1 * x2
dE_db1 <- delta1
cat(sprintf("delta1 = %.5f * %.5f * %.5f*(1-%.5f) = %.5f\n",
delta3, w5, h1, h1, delta1))## delta1 = -0.10255 * 0.21724 * 0.33234*(1-0.33234) = -0.00494
## dE/dw1 = delta1 * x1 = (-0.00494)(0) = -0.00000
## dE/dw3 = delta1 * x2 = (-0.00494)(1) = -0.00494
w1_new <- w1 - alpha * dE_dw1
w3_new <- w3 - alpha * dE_dw3
b1_new <- b1 - alpha * dE_db1
cat(sprintf("\nw1_nuevo = %.5f - 0.25*(%.5f) = %.5f\n", w1, dE_dw1, w1_new))##
## w1_nuevo = 0.10000 - 0.25*(-0.00000) = 0.10000
## w3_nuevo = -0.69764 - 0.25*(-0.00494) = -0.69640
\[\delta_2 = \delta_3 \cdot w_6 \cdot h_2(1 - h_2)\]
delta2 <- delta3 * w6 * sigmoid_deriv(h2)
dE_dw2 <- delta2 * x1
dE_dw4 <- delta2 * x2
dE_db2 <- delta2
cat(sprintf("delta2 = %.5f * %.5f * %.5f*(1-%.5f) = %.5f\n",
delta3, w6, h2, h2, delta2))## delta2 = -0.10255 * 0.42985 * 0.57571*(1-0.57571) = -0.01077
## dE/dw2 = delta2 * x1 = (-0.01077)(0) = -0.00000
## dE/dw4 = delta2 * x2 = (-0.01077)(1) = -0.01077
w2_new <- w2 - alpha * dE_dw2
w4_new <- w4 - alpha * dE_dw4
b2_new <- b2 - alpha * dE_db2
cat(sprintf("\nw2_nuevo = %.5f - 0.25*(%.5f) = %.5f\n", w2, dE_dw2, w2_new))##
## w2_nuevo = 0.50000 - 0.25*(-0.00000) = 0.50000
## w4_nuevo = 0.30517 - 0.25*(-0.01077) = 0.30786
df_comparacion <- data.frame(
Epoca = 1:3,
y_hat = c(0.5735, 0.5764, round(yhat, 5)),
Error_abs = c(abs(1 - 0.5735), abs(1 - 0.5764), abs(1 - yhat)),
Costo = c(0.09095, 0.08973, round(E, 5)),
w3 = c(-0.70000, -0.69884, -0.69764),
w4 = c(0.30000, 0.30255, 0.30517),
w5 = c(0.20000, 0.20865, 0.21724),
w6 = c(0.40000, 0.41498, 0.42985)
)
knitr::kable(df_comparacion,
col.names = c("Época", "ŷ", "|y - ŷ|", "Costo", "w3", "w4", "w5", "w6"),
digits = 5,
caption = "Comparación de resultados en las tres primeras épocas")| Época | ŷ | |y - ŷ| | Costo | w3 | w4 | w5 | w6 |
|---|---|---|---|---|---|---|---|
| 1 | 0.57350 | 0.42650 | 0.09095 | -0.70000 | 0.30000 | 0.20000 | 0.40000 |
| 2 | 0.57640 | 0.42360 | 0.08973 | -0.69884 | 0.30255 | 0.20865 | 0.41498 |
| 3 | 0.57924 | 0.42076 | 0.08852 | -0.69764 | 0.30517 | 0.21724 | 0.42985 |
Análisis En cada época, la predicción \(\hat{y}\) se acerca más al objetivo \(y = 1\), el costo disminuye y los pesos asociados a \(x_2\) (que en esta observación vale 1) se ajustan. Los pesos asociados a \(x_1\) no cambian porque \(x_1 = 0\), y el gradiente respecto a ellos es cero.
Repetir los cálculos manuales 20 veces sería tedioso y propenso a errores. La automatización permite:
# ============================================================
# ENTRENAMIENTO AUTOMATIZADO — SIN LIBRERÍAS DE ML
# ============================================================
entrenar_xor_1obs <- function(
x1_in, x2_in, y_in, # Observación de entrenamiento
w1_0, w2_0, w3_0, w4_0, # Pesos iniciales (capa oculta)
w5_0, w6_0, # Pesos iniciales (capa salida)
b1_0 = 0, b2_0 = 0, b3_0 = 0, # Sesgos iniciales
alpha = 0.25, # Tasa de aprendizaje
n_epocas = 20 # Número de épocas
) {
# Inicializar pesos
w1 <- w1_0; w2 <- w2_0; w3 <- w3_0; w4 <- w4_0
w5 <- w5_0; w6 <- w6_0
b1 <- b1_0; b2 <- b2_0; b3 <- b3_0
# Almacenar resultados
historial <- data.frame()
for (ep in 1:n_epocas) {
# --- FORWARD PASS ---
z1 <- w1*x1_in + w3*x2_in + b1
z2 <- w2*x1_in + w4*x2_in + b2
h1 <- sigmoid(z1)
h2 <- sigmoid(z2)
z3 <- w5*h1 + w6*h2 + b3
yhat <- sigmoid(z3)
E <- loss(y_in, yhat)
# --- BACKPROPAGATION ---
# Error local en la capa de salida
delta3 <- grad_loss(y_in, yhat) * sigmoid_deriv(yhat)
# Gradientes de la capa de salida
dE_dw5 <- delta3 * h1
dE_dw6 <- delta3 * h2
dE_db3 <- delta3
# Error local en la capa oculta
delta1 <- delta3 * w5 * sigmoid_deriv(h1)
delta2 <- delta3 * w6 * sigmoid_deriv(h2)
# Gradientes de la capa oculta
dE_dw1 <- delta1 * x1_in
dE_dw3 <- delta1 * x2_in
dE_dw2 <- delta2 * x1_in
dE_dw4 <- delta2 * x2_in
dE_db1 <- delta1
dE_db2 <- delta2
# Magnitud del gradiente total
grad_mag <- sqrt(dE_dw1^2 + dE_dw2^2 + dE_dw3^2 + dE_dw4^2 +
dE_dw5^2 + dE_dw6^2)
# --- ACTUALIZACIÓN DE PARÁMETROS ---
w1 <- w1 - alpha * dE_dw1
w2 <- w2 - alpha * dE_dw2
w3 <- w3 - alpha * dE_dw3
w4 <- w4 - alpha * dE_dw4
w5 <- w5 - alpha * dE_dw5
w6 <- w6 - alpha * dE_dw6
b1 <- b1 - alpha * dE_db1
b2 <- b2 - alpha * dE_db2
b3 <- b3 - alpha * dE_db3
# Guardar fila del historial
historial <- rbind(historial, data.frame(
Epoca = ep,
z1 = round(z1, 5),
z2 = round(z2, 5),
z3 = round(z3, 5),
h1 = round(h1, 5),
h2 = round(h2, 5),
y_hat = round(yhat, 5),
y = y_in,
Err_abs = round(abs(y_in - yhat), 5),
Costo = round(E, 5),
Grad_mag = round(grad_mag, 6),
w1 = round(w1, 5), w2 = round(w2, 5),
w3 = round(w3, 5), w4 = round(w4, 5),
w5 = round(w5, 5), w6 = round(w6, 5),
b1 = round(b1, 5), b2 = round(b2, 5),
b3 = round(b3, 5)
))
}
return(historial)
}
# Ejecutar con los parámetros del ejemplo de clase
set.seed(42)
hist_orig <- entrenar_xor_1obs(
x1_in = 0, x2_in = 1, y_in = 1,
w1_0 = 0.1, w2_0 = 0.5, w3_0 = -0.7, w4_0 = 0.3,
w5_0 = 0.2, w6_0 = 0.4,
alpha = 0.25, n_epocas = 20
)knitr::kable(
hist_orig[, c("Epoca","z1","z2","z3","h1","h2",
"y_hat","y","Err_abs","Costo","Grad_mag")],
col.names = c("Época","z1","z2","z3","h1","h2",
"ŷ","y","|y-ŷ|","Costo","||∇E||"),
digits = 5,
caption = "Tabla completa: 20 épocas de entrenamiento con una observación XOR")| Época | z1 | z2 | z3 | h1 | h2 | ŷ | y | |y-ŷ| | Costo | ||∇E|| |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | -0.70000 | 0.30000 | 0.29614 | 0.33181 | 0.57444 | 0.57350 | 1 | 0.42650 | 0.09095 | 0.07011 |
| 2 | -0.69769 | 0.30510 | 0.33432 | 0.33233 | 0.57569 | 0.58281 | 1 | 0.41719 | 0.08702 | 0.06837 |
| 3 | -0.69534 | 0.31024 | 0.37154 | 0.33285 | 0.57694 | 0.59183 | 1 | 0.40817 | 0.08330 | 0.06665 |
| 4 | -0.69296 | 0.31541 | 0.40780 | 0.33337 | 0.57821 | 0.60056 | 1 | 0.39944 | 0.07978 | 0.06497 |
| 5 | -0.69056 | 0.32060 | 0.44313 | 0.33391 | 0.57947 | 0.60901 | 1 | 0.39099 | 0.07644 | 0.06331 |
| 6 | -0.68815 | 0.32579 | 0.47754 | 0.33445 | 0.58073 | 0.61717 | 1 | 0.38283 | 0.07328 | 0.06169 |
| 7 | -0.68572 | 0.33098 | 0.51106 | 0.33499 | 0.58200 | 0.62505 | 1 | 0.37495 | 0.07029 | 0.06011 |
| 8 | -0.68329 | 0.33615 | 0.54370 | 0.33553 | 0.58326 | 0.63267 | 1 | 0.36733 | 0.06746 | 0.05856 |
| 9 | -0.68085 | 0.34131 | 0.57548 | 0.33607 | 0.58451 | 0.64003 | 1 | 0.35997 | 0.06479 | 0.05706 |
| 10 | -0.67842 | 0.34644 | 0.60644 | 0.33661 | 0.58575 | 0.64713 | 1 | 0.35287 | 0.06226 | 0.05560 |
| 11 | -0.67599 | 0.35154 | 0.63659 | 0.33716 | 0.58699 | 0.65398 | 1 | 0.34602 | 0.05986 | 0.05418 |
| 12 | -0.67357 | 0.35660 | 0.66596 | 0.33770 | 0.58822 | 0.66060 | 1 | 0.33940 | 0.05760 | 0.05280 |
| 13 | -0.67115 | 0.36162 | 0.69456 | 0.33824 | 0.58943 | 0.66698 | 1 | 0.33302 | 0.05545 | 0.05147 |
| 14 | -0.66875 | 0.36660 | 0.72244 | 0.33878 | 0.59064 | 0.67314 | 1 | 0.32686 | 0.05342 | 0.05018 |
| 15 | -0.66637 | 0.37153 | 0.74960 | 0.33931 | 0.59183 | 0.67909 | 1 | 0.32091 | 0.05149 | 0.04893 |
| 16 | -0.66400 | 0.37641 | 0.77608 | 0.33984 | 0.59301 | 0.68483 | 1 | 0.31517 | 0.04966 | 0.04772 |
| 17 | -0.66165 | 0.38124 | 0.80189 | 0.34037 | 0.59417 | 0.69038 | 1 | 0.30962 | 0.04793 | 0.04655 |
| 18 | -0.65931 | 0.38601 | 0.82706 | 0.34089 | 0.59532 | 0.69573 | 1 | 0.30427 | 0.04629 | 0.04542 |
| 19 | -0.65700 | 0.39073 | 0.85161 | 0.34141 | 0.59646 | 0.70091 | 1 | 0.29909 | 0.04473 | 0.04433 |
| 20 | -0.65471 | 0.39539 | 0.87557 | 0.34193 | 0.59758 | 0.70590 | 1 | 0.29410 | 0.04325 | 0.04327 |
Análisis La predicción \(\hat{y}\) aumenta monótonamente desde ~0.57 hacia 1, el costo disminuye en cada época y la magnitud del gradiente también decrece, lo que indica que el algoritmo converge correctamente. La tasa de mejora es pequeña por época porque la tasa de aprendizaje \(\alpha = 0.25\) es conservadora.
Las gráficas permiten observar visualmente cómo evoluciona el aprendizaje:
par(mfrow = c(2, 2), mar = c(4, 4, 3, 1))
# --- (a) Costo por época ---
plot(hist_orig$Epoca, hist_orig$Costo,
type = "b", pch = 16, col = "#2E86C1",
xlab = "Época", ylab = "Costo E",
main = "(a) Función de pérdida por época",
lwd = 2)
grid()
# --- (b) Error absoluto por época ---
plot(hist_orig$Epoca, hist_orig$Err_abs,
type = "b", pch = 16, col = "#C0392B",
xlab = "Época", ylab = "|y - ŷ|",
main = "(b) Error absoluto por época",
lwd = 2)
grid()
# --- (c) Magnitud del gradiente ---
plot(hist_orig$Epoca, hist_orig$Grad_mag,
type = "b", pch = 16, col = "#27AE60",
xlab = "Época", ylab = "||∇E||",
main = "(c) Magnitud del gradiente por época",
lwd = 2)
grid()
# --- (d) Evolución de pesos seleccionados ---
plot(hist_orig$Epoca, hist_orig$w3,
type = "b", pch = 16, col = "#8E44AD",
xlab = "Época", ylab = "Valor del peso",
main = "(d) Evolución de pesos seleccionados",
ylim = range(c(hist_orig$w3, hist_orig$w4,
hist_orig$w5, hist_orig$w6)),
lwd = 2)
lines(hist_orig$Epoca, hist_orig$w4, type="b", pch=17, col="#E67E22", lwd=2)
lines(hist_orig$Epoca, hist_orig$w5, type="b", pch=15, col="#2E86C1", lwd=2)
lines(hist_orig$Epoca, hist_orig$w6, type="b", pch=18, col="#C0392B", lwd=2)
legend("topright",
legend = c("w3 (x2→h1)", "w4 (x2→h2)", "w5 (h1→ŷ)", "w6 (h2→ŷ)"),
col = c("#8E44AD","#E67E22","#2E86C1","#C0392B"),
pch = c(16,17,15,18), lwd = 2, cex = 0.85)
grid()Análisis de las gráficas:
(a) Costo: Disminuye en cada época, con mayor rapidez al principio y más lentamente al final (curva convexa). Esto es típico del descenso de gradiente: los pasos grandes se dan cuando el gradiente es grande.
(b) Error absoluto: Sigue el mismo patrón que el costo, reduciéndose de ~0.43 a un valor más pequeño. Como usamos la misma observación siempre, la reducción es suave y monótona.
(c) Magnitud del gradiente: Decrece con las épocas, lo que confirma que nos acercamos a un mínimo. Si el gradiente llegara a cero, los pesos dejarían de cambiar (convergencia).
(d) Pesos: Los pesos \(w_3\) y \(w_4\) (conectados a \(x_2 = 1\)) cambian significativamente. Los pesos \(w_5\) y \(w_6\) también evolucionan porque \(h_1\) y \(h_2\) son distintos de cero. Los pesos \(w_1\) y \(w_2\) (conectados a \(x_1 = 0\)) no aparecen porque no cambian.
Los pesos iniciales determinan el punto de partida en el espacio de pérdida. Diferentes inicializaciones pueden llevar a:
par(mfrow = c(2, 2), mar = c(4, 4, 3, 1))
# --- (a) Predicción ---
plot(hist_orig$Epoca, hist_orig$y_hat,
type = "b", pch = 16, col = "#2E86C1", lwd = 2,
xlab = "Época", ylab = "ŷ",
main = "(a) Predicción por época",
ylim = range(c(hist_orig$y_hat, hist_alt$y_hat)))
lines(hist_alt$Epoca, hist_alt$y_hat,
type = "b", pch = 17, col = "#C0392B", lwd = 2)
abline(h = 1, lty = 2, col = "gray50")
legend("bottomright",
legend = c("Original", "Alternativa"),
col = c("#2E86C1","#C0392B"), pch = c(16,17), lwd = 2)
grid()
# --- (b) Costo ---
plot(hist_orig$Epoca, hist_orig$Costo,
type = "b", pch = 16, col = "#2E86C1", lwd = 2,
xlab = "Época", ylab = "Costo",
main = "(b) Costo por época",
ylim = range(c(hist_orig$Costo, hist_alt$Costo)))
lines(hist_alt$Epoca, hist_alt$Costo,
type = "b", pch = 17, col = "#C0392B", lwd = 2)
legend("topright",
legend = c("Original", "Alternativa"),
col = c("#2E86C1","#C0392B"), pch = c(16,17), lwd = 2)
grid()
# --- (c) Error absoluto ---
plot(hist_orig$Epoca, hist_orig$Err_abs,
type = "b", pch = 16, col = "#2E86C1", lwd = 2,
xlab = "Época", ylab = "|y - ŷ|",
main = "(c) Error absoluto por época",
ylim = range(c(hist_orig$Err_abs, hist_alt$Err_abs)))
lines(hist_alt$Epoca, hist_alt$Err_abs,
type = "b", pch = 17, col = "#C0392B", lwd = 2)
legend("topright",
legend = c("Original", "Alternativa"),
col = c("#2E86C1","#C0392B"), pch = c(16,17), lwd = 2)
grid()
# --- (d) Magnitud del gradiente ---
plot(hist_orig$Epoca, hist_orig$Grad_mag,
type = "b", pch = 16, col = "#2E86C1", lwd = 2,
xlab = "Época", ylab = "||∇E||",
main = "(d) Magnitud del gradiente",
ylim = range(c(hist_orig$Grad_mag, hist_alt$Grad_mag)))
lines(hist_alt$Epoca, hist_alt$Grad_mag,
type = "b", pch = 17, col = "#C0392B", lwd = 2)
legend("topright",
legend = c("Original", "Alternativa"),
col = c("#2E86C1","#C0392B"), pch = c(16,17), lwd = 2)
grid()epocas_clave <- c(1, 5, 10, 20)
df_comp <- data.frame(
Epoca = epocas_clave,
yhat_orig = hist_orig$y_hat[epocas_clave],
costo_orig = hist_orig$Costo[epocas_clave],
yhat_alt = hist_alt$y_hat[epocas_clave],
costo_alt = hist_alt$Costo[epocas_clave]
)
knitr::kable(df_comp,
col.names = c("Época", "ŷ (Original)", "Costo (Orig.)",
"ŷ (Alternativa)", "Costo (Alt.)"),
digits = 5,
caption = "Comparación de inicializaciones en épocas clave")| Época | ŷ (Original) | Costo (Orig.) | ŷ (Alternativa) | Costo (Alt.) |
|---|---|---|---|---|
| 1 | 0.57350 | 0.09095 | 0.49681 | 0.12660 |
| 5 | 0.60901 | 0.07644 | 0.54188 | 0.10494 |
| 10 | 0.64713 | 0.06226 | 0.59111 | 0.08360 |
| 20 | 0.70590 | 0.04325 | 0.66699 | 0.05545 |
Análisis: La inicialización alternativa parte de una predicción diferente porque los pesos iniciales producen una activación distinta. Dependiendo de qué tan cerca estén los pesos iniciales de una buena solución, la convergencia puede ser más rápida o más lenta. Inicializaciones con pesos grandes pueden causar gradientes más inestables al principio. En la práctica, técnicas como Xavier o He inicializan los pesos de forma inteligente para acelerar la convergencia.
Cuando entrenamos con múltiples observaciones a la vez (batch), las operaciones escalares se reemplazan por multiplicaciones matriciales. Esto es computacionalmente eficiente porque R (y Python) están optimizados para álgebra lineal.
Con dos observaciones, la entrada se organiza como:
\[X = \begin{pmatrix} x_{1}^{(1)} & x_{2}^{(1)} \\ x_{1}^{(2)} & x_{2}^{(2)} \end{pmatrix} = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}, \quad y = \begin{pmatrix} 1 \\ 1 \end{pmatrix}\]
Las matrices de pesos son:
\[W^{(0,1)} = \begin{pmatrix} w_1 & w_2 \\ w_3 & w_4 \end{pmatrix} \in \mathbb{R}^{2 \times 2}, \quad W^{(1,2)} = \begin{pmatrix} w_5 \\ w_6 \end{pmatrix} \in \mathbb{R}^{2 \times 1}\]
# ============================================================
# ENTRENAMIENTO MATRICIAL CON DOS OBSERVACIONES
# ============================================================
entrenar_xor_matriz <- function(
X, y, # Matriz de entradas y vector objetivo
W1_0, W2_0, # Matrices de pesos iniciales
b1_0, b2_0, # Vectores de sesgos
alpha = 0.25,
n_epocas = 20
) {
W1 <- W1_0 # 2x2: pesos capa oculta
W2 <- W2_0 # 2x1: pesos capa salida
b1 <- b1_0 # 1x2
b2 <- b2_0 # escalar
n <- nrow(X) # número de observaciones
historial <- data.frame()
for (ep in 1:n_epocas) {
# === FORWARD PASS (matricial) ===
# Capa oculta: Z1 = X %*% W1 + b1 (dim: n x 2)
Z1 <- sweep(X %*% W1, 2, b1, "+")
H1 <- sigmoid(Z1) # activaciones ocultas (n x 2)
# Capa de salida: Z2 = H1 %*% W2 + b2 (dim: n x 1)
Z2 <- H1 %*% W2 + b2
Yhat <- sigmoid(Z2) # predicciones (n x 1)
# Costo promedio sobre las n observaciones
E <- mean(0.5 * (y - Yhat)^2)
# === BACKPROPAGATION (matricial) ===
# Delta de la capa de salida (n x 1)
delta2 <- (Yhat - y) * sigmoid_deriv(Yhat)
# Gradientes de la capa de salida
dW2 <- t(H1) %*% delta2 / n # 2x1
db2 <- mean(delta2)
# Propagar error a la capa oculta (n x 2)
delta1 <- (delta2 %*% t(W2)) * sigmoid_deriv(H1)
# Gradientes de la capa oculta
dW1 <- t(X) %*% delta1 / n # 2x2
db1 <- colMeans(delta1)
# === ACTUALIZACIÓN ===
W1 <- W1 - alpha * dW1
W2 <- W2 - alpha * dW2
b1 <- b1 - alpha * db1
b2 <- b2 - alpha * db2
# Magnitud del gradiente
grad_mag <- sqrt(sum(dW1^2) + sum(dW2^2))
historial <- rbind(historial, data.frame(
Epoca = ep,
yhat1 = round(Yhat[1], 5),
yhat2 = round(Yhat[2], 5),
Costo = round(E, 5),
Err_abs1 = round(abs(y[1] - Yhat[1]), 5),
Err_abs2 = round(abs(y[2] - Yhat[2]), 5),
Grad_mag = round(grad_mag, 6)
))
}
return(historial)
}
# Datos: dos observaciones XOR
X_mat <- matrix(c(0,1, 1,0), nrow=2, byrow=TRUE)
y_mat <- matrix(c(1, 1), nrow=2)
cat("Matriz de entradas X:\n"); print(X_mat)## Matriz de entradas X:
## [,1] [,2]
## [1,] 0 1
## [2,] 1 0
## Vector objetivo y:
## [,1]
## [1,] 1
## [2,] 1
# Pesos iniciales (mismos del ejemplo original)
W1_init <- matrix(c(0.1, -0.7, 0.5, 0.3), nrow=2, byrow=FALSE)
W2_init <- matrix(c(0.2, 0.4), nrow=2)
b1_init <- c(0, 0)
b2_init <- 0
cat("\nMatriz de pesos W1 (capa oculta):\n"); print(W1_init)##
## Matriz de pesos W1 (capa oculta):
## [,1] [,2]
## [1,] 0.1 0.5
## [2,] -0.7 0.3
## Vector de pesos W2 (capa salida):
## [,1]
## [1,] 0.2
## [2,] 0.4
knitr::kable(hist_mat,
col.names = c("Época", "ŷ₁", "ŷ₂", "Costo",
"|y₁-ŷ₁|", "|y₂-ŷ₂|", "||∇E||"),
digits = 5,
caption = "Entrenamiento matricial con dos observaciones XOR")| Época | ŷ₁ | ŷ₂ | Costo | |y₁-ŷ₁| | |y₂-ŷ₂| | ||∇E|| |
|---|---|---|---|---|---|---|
| 1 | 0.57350 | 0.58758 | 0.08800 | 0.42650 | 0.41242 | 0.07540 |
| 2 | 0.58286 | 0.59753 | 0.08400 | 0.41714 | 0.40247 | 0.07328 |
| 3 | 0.59190 | 0.60712 | 0.08023 | 0.40810 | 0.39288 | 0.07120 |
| 4 | 0.60063 | 0.61636 | 0.07667 | 0.39937 | 0.38364 | 0.06917 |
| 5 | 0.60905 | 0.62527 | 0.07332 | 0.39095 | 0.37473 | 0.06719 |
| 6 | 0.61717 | 0.63383 | 0.07016 | 0.38283 | 0.36617 | 0.06526 |
| 7 | 0.62500 | 0.64208 | 0.06718 | 0.37500 | 0.35792 | 0.06339 |
| 8 | 0.63254 | 0.65001 | 0.06438 | 0.36746 | 0.34999 | 0.06158 |
| 9 | 0.63981 | 0.65763 | 0.06174 | 0.36019 | 0.34237 | 0.05983 |
| 10 | 0.64681 | 0.66496 | 0.05925 | 0.35319 | 0.33504 | 0.05814 |
| 11 | 0.65356 | 0.67201 | 0.05690 | 0.34644 | 0.32799 | 0.05651 |
| 12 | 0.66006 | 0.67879 | 0.05468 | 0.33994 | 0.32121 | 0.05493 |
| 13 | 0.66633 | 0.68532 | 0.05259 | 0.33367 | 0.31468 | 0.05341 |
| 14 | 0.67237 | 0.69159 | 0.05062 | 0.32763 | 0.30841 | 0.05195 |
| 15 | 0.67819 | 0.69762 | 0.04875 | 0.32181 | 0.30238 | 0.05054 |
| 16 | 0.68380 | 0.70343 | 0.04698 | 0.31620 | 0.29657 | 0.04919 |
| 17 | 0.68921 | 0.70902 | 0.04532 | 0.31079 | 0.29098 | 0.04788 |
| 18 | 0.69443 | 0.71440 | 0.04374 | 0.30557 | 0.28560 | 0.04663 |
| 19 | 0.69947 | 0.71958 | 0.04224 | 0.30053 | 0.28042 | 0.04542 |
| 20 | 0.70433 | 0.72458 | 0.04082 | 0.29567 | 0.27542 | 0.04426 |
par(mfrow = c(2, 2), mar = c(4, 4, 3, 1))
# (a) Predicciones por época
plot(hist_mat$Epoca, hist_mat$yhat1,
type = "b", pch = 16, col = "#2E86C1", lwd = 2,
ylim = range(c(hist_mat$yhat1, hist_mat$yhat2, 0.4, 1)),
xlab = "Época", ylab = "ŷ",
main = "(a) Predicciones por época")
lines(hist_mat$Epoca, hist_mat$yhat2,
type = "b", pch = 17, col = "#C0392B", lwd = 2)
abline(h = 1, lty = 2, col = "gray50")
legend("bottomright",
legend = c("ŷ₁ (obs. 1)", "ŷ₂ (obs. 2)"),
col = c("#2E86C1","#C0392B"), pch = c(16,17), lwd = 2)
grid()
# (b) Costo
plot(hist_mat$Epoca, hist_mat$Costo,
type = "b", pch = 16, col = "#27AE60", lwd = 2,
xlab = "Época", ylab = "Costo promedio",
main = "(b) Costo promedio por época")
grid()
# (c) Error absoluto
plot(hist_mat$Epoca, hist_mat$Err_abs1,
type = "b", pch = 16, col = "#2E86C1", lwd = 2,
ylim = range(c(hist_mat$Err_abs1, hist_mat$Err_abs2)),
xlab = "Época", ylab = "|y - ŷ|",
main = "(c) Error absoluto por observación")
lines(hist_mat$Epoca, hist_mat$Err_abs2,
type = "b", pch = 17, col = "#C0392B", lwd = 2)
legend("topright",
legend = c("|y₁-ŷ₁|", "|y₂-ŷ₂|"),
col = c("#2E86C1","#C0392B"), pch = c(16,17), lwd = 2)
grid()
# (d) Magnitud del gradiente
plot(hist_mat$Epoca, hist_mat$Grad_mag,
type = "b", pch = 16, col = "#8E44AD", lwd = 2,
xlab = "Época", ylab = "||∇E||",
main = "(d) Magnitud del gradiente")
grid()Análisis: Con dos observaciones (batch de tamaño 2), el gradiente se promedia sobre ambas, lo que produce actualizaciones más estables que con una sola observación. Ambas predicciones mejoran simultáneamente hacia el objetivo \(y = 1\).
df_final <- data.frame(
Escenario = c("1 obs. (original)", "1 obs. (alt. init.)",
"2 obs. (matricial)"),
yhat_ep1 = c(hist_orig$y_hat[1], hist_alt$y_hat[1],
(hist_mat$yhat1[1] + hist_mat$yhat2[1])/2),
costo_ep1 = c(hist_orig$Costo[1], hist_alt$Costo[1],
hist_mat$Costo[1]),
yhat_ep20 = c(hist_orig$y_hat[20], hist_alt$y_hat[20],
(hist_mat$yhat1[20] + hist_mat$yhat2[20])/2),
costo_ep20 = c(hist_orig$Costo[20], hist_alt$Costo[20],
hist_mat$Costo[20])
)
knitr::kable(df_final,
col.names = c("Escenario", "ŷ inicial", "Costo inicial",
"ŷ final", "Costo final"),
digits = 5,
caption = "Comparación de los tres escenarios de entrenamiento")| Escenario | ŷ inicial | Costo inicial | ŷ final | Costo final |
|---|---|---|---|---|
| 1 obs. (original) | 0.57350 | 0.09095 | 0.70590 | 0.04325 |
| 1 obs. (alt. init.) | 0.49681 | 0.12660 | 0.66699 | 0.05545 |
| 2 obs. (matricial) | 0.58054 | 0.08800 | 0.71446 | 0.04082 |
Discusión:
Número de observaciones: Usar más observaciones por actualización proporciona un gradiente más representativo de la función de pérdida global, lo que suele producir convergencia más estable.
Inicialización: Diferentes puntos de partida pueden llevar a trayectorias distintas. En redes pequeñas como esta, generalmente se alcanza la misma región del espacio de parámetros, pero la velocidad puede variar.
¿Por qué se necesita la capa oculta? Sin la capa oculta, solo podemos aprender funciones lineales. XOR requiere una frontera no lineal que solo puede construirse combinando múltiples transformaciones lineales con funciones de activación no lineales.
Limitación de entrenar con solo 1 o 2 observaciones: La red se especializa en esas observaciones y puede no generalizar bien a las otras. Esto se llama sobreajuste parcial. Para aprender XOR completo se necesitan las 4 observaciones.
El proceso de aprendizaje de una red neuronal puede entenderse como un ciclo iterativo compuesto por varias etapas fundamentales.
Entrada: Se introduce el vector de características \((x_1, x_2)\).
Propagación hacia adelante (Forward pass):
La red procesa la información a través de sus capas y genera una
predicción \(\hat{y}\).
Cálculo del error:
Se determina la diferencia entre el valor real y la predicción:
\[ error = y - \hat{y} \]
Función de pérdida:
Se cuantifica el error mediante la función:
\[ E = \frac{1}{2}(y - \hat{y})^2 \]
Retropropagación (Backpropagation):
Se calculan los gradientes de la función de pérdida respecto a cada
parámetro.
Actualización de parámetros:
Los pesos se ajustan mediante descenso de gradiente:
\[ w_i \leftarrow w_i - \alpha \frac{\partial E}{\partial w_i} \]
Iteración:
El proceso se repite durante múltiples épocas, mejorando progresivamente
la predicción.
La red neuronal aprende a partir de sus errores mediante los siguientes mecanismos:
Predicción (\(\hat{y}\)):
Inicialmente, la red produce estimaciones alejadas del valor real debido
a pesos aleatorios.
Error:
La diferencia \((y - \hat{y})\) indica
la magnitud y dirección del ajuste necesario.
Función de pérdida:
Penaliza más los errores grandes, guiando el proceso de
optimización.
Gradiente:
Indica cómo debe modificarse cada peso para reducir la pérdida.
Retropropagación:
Distribuye el error a través de la red, asignando responsabilidad a cada
parámetro.
Actualización:
Los pesos se ajustan en la dirección que minimiza el error.
Convergencia:
Con suficientes iteraciones, el modelo mejora sus predicciones y se
aproxima al comportamiento esperado.
Desde una perspectiva didáctica, este proceso permite comprender que el aprendizaje en redes neuronales no es inmediato >ni garantizado, sino el resultado de múltiples ajustes graduales.
Cada iteración representa una mejora incremental basada en el error previo, lo que evidencia la importancia de:
- La tasa de aprendizaje
- La inicialización de los pesos
- La arquitectura del modelo
En conjunto, estos elementos determinan la capacidad de la red para aprender patrones complejos como la función XOR.
Para que la red aprenda correctamente la función XOR, debe entrenarse con todas las combinaciones posibles:
\((0,0) \to 0\), \((0,1) \to 1\), \((1,0) \to 1\), \((1,1) \to 0\).
En esta sección se entrena durante 500 épocas para observar la convergencia del modelo.
# ============================================================
# ENTRENAMIENTO CON LAS 4 OBSERVACIONES XOR
# ============================================================
entrenar_xor_completo <- function(
X, y,
W1_0, W2_0, b1_0 = c(0,0), b2_0 = 0,
alpha = 0.5, n_epocas = 500
) {
W1 <- W1_0; W2 <- W2_0; b1 <- b1_0; b2 <- b2_0
n <- nrow(X)
historial <- data.frame()
for (ep in 1:n_epocas) {
# Forward
Z1 <- sweep(X %*% W1, 2, b1, "+")
H1 <- sigmoid(Z1)
Z2 <- H1 %*% W2 + b2
Yhat <- sigmoid(Z2)
E <- mean(0.5 * (y - Yhat)^2)
# Backprop
delta2 <- (Yhat - y) * sigmoid_deriv(Yhat)
dW2 <- t(H1) %*% delta2 / n
db2 <- mean(delta2)
delta1 <- (delta2 %*% t(W2)) * sigmoid_deriv(H1)
dW1 <- t(X) %*% delta1 / n
db1 <- colMeans(delta1)
# Actualización
W1 <- W1 - alpha * dW1
W2 <- W2 - alpha * dW2
b1 <- b1 - alpha * db1
b2 <- b2 - alpha * db2
if (ep %% 50 == 0 || ep == 1) {
historial <- rbind(historial, data.frame(
Epoca = ep,
Costo = round(E, 6),
yhat_00 = round(Yhat[1], 4),
yhat_01 = round(Yhat[2], 4),
yhat_10 = round(Yhat[3], 4),
yhat_11 = round(Yhat[4], 4)
))
}
}
list(historial = historial, W1 = W1, W2 = W2,
b1 = b1, b2 = b2,
Yhat_final = Yhat)
}
# Las 4 observaciones XOR
X_full <- matrix(c(0,0, 0,1, 1,0, 1,1), nrow=4, byrow=TRUE)
y_full <- matrix(c(0, 1, 1, 0), nrow=4)
cat("Tabla XOR completa:\n")## Tabla XOR completa:
## x1 x2 y
## 0 0 0
## 0 1 1
## 1 0 1
## 1 1 0
knitr::kable(resultado$historial,
col.names = c("Época", "Costo", "ŷ(0,0)", "ŷ(0,1)", "ŷ(1,0)", "ŷ(1,1)"),
digits = 5,
caption = "Entrenamiento XOR completo: evolución cada 50 épocas")| Época | Costo | ŷ(0,0) | ŷ(0,1) | ŷ(1,0) | ŷ(1,1) |
|---|---|---|---|---|---|
| 1 | 0.13382 | 0.6133 | 0.6146 | 0.6492 | 0.6503 |
| 50 | 0.12517 | 0.4953 | 0.4969 | 0.5266 | 0.5281 |
| 100 | 0.12509 | 0.4847 | 0.4869 | 0.5148 | 0.5169 |
| 150 | 0.12508 | 0.4838 | 0.4866 | 0.5135 | 0.5160 |
| 200 | 0.12506 | 0.4838 | 0.4872 | 0.5129 | 0.5160 |
| 250 | 0.12505 | 0.4838 | 0.4877 | 0.5125 | 0.5160 |
| 300 | 0.12504 | 0.4838 | 0.4883 | 0.5121 | 0.5161 |
| 350 | 0.12502 | 0.4837 | 0.4889 | 0.5117 | 0.5162 |
| 400 | 0.12501 | 0.4837 | 0.4894 | 0.5114 | 0.5164 |
| 450 | 0.12499 | 0.4836 | 0.4899 | 0.5110 | 0.5165 |
| 500 | 0.12498 | 0.4835 | 0.4904 | 0.5107 | 0.5167 |
Yhat_final <- resultado$Yhat_final
pred_clase <- ifelse(Yhat_final >= 0.5, 1, 0)
df_pred <- data.frame(
x1 = X_full[,1],
x2 = X_full[,2],
y_real = as.integer(y_full),
y_hat = round(Yhat_final, 4),
clase_pred = pred_clase,
correcto = ifelse(pred_clase == y_full, "✓", "✗")
)
knitr::kable(df_pred,
col.names = c("x₁","x₂","y real","ŷ","Clase predicha","¿Correcto?"),
caption = "Predicciones finales de la red XOR completa")| x₁ | x₂ | y real | ŷ | Clase predicha | ¿Correcto? |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0.4835 | 0 | ✓ |
| 0 | 1 | 1 | 0.4904 | 0 | ✗ |
| 1 | 0 | 1 | 0.5107 | 1 | ✓ |
| 1 | 1 | 0 | 0.5167 | 1 | ✗ |
##
## Precisión final: 50.0%
par(mfrow = c(2, 2), mar = c(4, 4, 3, 1))
hist_c <- resultado$historial
# (a) Costo
plot(hist_c$Epoca, hist_c$Costo,
type = "b", pch = 16, col = "#2E86C1", lwd = 2,
xlab = "Época", ylab = "Costo promedio",
main = "(a) Costo — XOR completo (4 obs.)")
grid()
# (b) Predicciones para cada observación
colores <- c("#C0392B","#2E86C1","#27AE60","#8E44AD")
obs_names <- c("ŷ(0,0)","ŷ(0,1)","ŷ(1,0)","ŷ(1,1)")
y_reales <- c(0, 1, 1, 0)
plot(hist_c$Epoca, hist_c$yhat_00,
type = "b", pch = 16, col = colores[1], lwd = 2,
ylim = c(0, 1),
xlab = "Época", ylab = "ŷ",
main = "(b) Predicciones por observación")
lines(hist_c$Epoca, hist_c$yhat_01, type="b", pch=17, col=colores[2], lwd=2)
lines(hist_c$Epoca, hist_c$yhat_10, type="b", pch=15, col=colores[3], lwd=2)
lines(hist_c$Epoca, hist_c$yhat_11, type="b", pch=18, col=colores[4], lwd=2)
abline(h=0.5, lty=2, col="gray50")
legend("right",
legend = obs_names,
col = colores, pch = c(16,17,15,18), lwd = 2, cex = 0.8)
grid()
# (c) Error absoluto por observación
err_00 <- abs(0 - hist_c$yhat_00)
err_01 <- abs(1 - hist_c$yhat_01)
err_10 <- abs(1 - hist_c$yhat_10)
err_11 <- abs(0 - hist_c$yhat_11)
plot(hist_c$Epoca, err_00,
type = "b", pch = 16, col = colores[1], lwd = 2,
ylim = range(c(err_00,err_01,err_10,err_11)),
xlab = "Época", ylab = "|y - ŷ|",
main = "(c) Error absoluto por observación")
lines(hist_c$Epoca, err_01, type="b", pch=17, col=colores[2], lwd=2)
lines(hist_c$Epoca, err_10, type="b", pch=15, col=colores[3], lwd=2)
lines(hist_c$Epoca, err_11, type="b", pch=18, col=colores[4], lwd=2)
legend("topright",
legend = obs_names,
col = colores, pch = c(16,17,15,18), lwd = 2, cex = 0.8)
grid()
# (d) Predicciones finales vs objetivo
barplot(
rbind(as.numeric(y_full), as.numeric(Yhat_final)),
beside = TRUE,
names.arg = c("(0,0)","(0,1)","(1,0)","(1,1)"),
col = c("#C0392B","#2E86C1"),
legend = c("y real", "ŷ final"),
xlab = "Observación",
ylab = "Valor",
main = "(d) Comparación: predicción final vs real",
ylim = c(0, 1.2)
)
abline(h = 0.5, lty = 2, col = "gray30")
grid()Análisis del entrenamiento XOR completo:
(a) Costo:
El costo desciende progresivamente durante 500 épocas. Dado que se utilizan las cuatro observaciones, el problema > > > >requiere más iteraciones para alcanzar convergencia.(b) Predicciones:
Las predicciones asociadas a \(y=1\) (observaciones \((0,1)\) y \((1,0)\)) tienden hacia 1, mientras que las correspondientes >a \(y=0\) (\((0,0)\) y \((1,1)\)) tienden hacia 0. Esto indica que la red logra capturar el patrón XOR.(c) Error absoluto:
Los errores disminuyen en todas las observaciones, aunque a distintas velocidades debido a la dinámica del gradiente.(d) Comparación final:
Cuando el modelo aprende correctamente, las predicciones superan el umbral de 0.5 para las clases positivas y permanecen >por debajo para las negativas..
El experimento desarrollado permite evidenciar de manera explícita cómo una red neuronal con una sola capa oculta es capaz de aproximar funciones no lineales como XOR.
La implementación manual del algoritmo de retropropagación facilita la comprensión detallada del proceso de aprendizaje, resaltando el papel fundamental del gradiente en la optimización de los parámetros del modelo.
## [1] TRUE
## R version 4.6.0 (2026-04-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 10 x64 (build 19045)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=Spanish_Colombia.utf8 LC_CTYPE=Spanish_Colombia.utf8
## [3] LC_MONETARY=Spanish_Colombia.utf8 LC_NUMERIC=C
## [5] LC_TIME=Spanish_Colombia.utf8
##
## time zone: America/Bogota
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## loaded via a namespace (and not attached):
## [1] digest_0.6.39 R6_2.6.1 fastmap_1.2.0 pingr_2.0.5
## [5] xfun_0.57 cachem_1.1.0 knitr_1.51 htmltools_0.5.9
## [9] rmarkdown_2.31 lifecycle_1.0.5 cli_3.6.6 processx_3.9.0
## [13] sass_0.4.10 jquerylib_0.1.4 compiler_4.6.0 rstudioapi_0.18.0
## [17] tools_4.6.0 evaluate_1.0.5 bslib_0.10.0 yaml_2.3.12
## [21] rlang_1.2.0 jsonlite_2.0.0
Documento generado automáticamente. Todos los cálculos fueron realizados desde cero en R, sin librerías de redes neuronales o diferenciación automática.