1 ¿Qué es un Perceptrón?

Un perceptrón es el modelo computacional más simple de una neurona artificial. Recibe entradas numéricas, les asigna pesos, calcula una suma ponderada, le agrega un sesgo y pasa el resultado por una función de activación.

\[z = w_1x_1 + w_2x_2 + \cdots + w_nx_n + b\] \[\hat{y} = f(z)\]

Analogía del Dr. Darghan: cada problema tiene un peso (importancia). Si la carga acumulada supera tu umbral personal (sesgo), explotas (función de activación = 1).

Analogía Elemento del perceptrón
Problema específico Entrada \(x_i\)
Importancia del problema Peso \(w_i\)
Carga total acumulada Suma ponderada \(z\)
Tu límite personal Sesgo \(b\)
¿Explotaste? Función de activación \(f(z)\)
Decisión final Salida \(\hat{y}\)

2 Operador AND

Regla: la salida es 1 solo cuando AMBAS entradas son 1.

Analogía: necesitas los dos problemas juntos para explotar.

Solución analítica: \(w_1 = 1,\ w_2 = 1,\ b = -1.5\)

datos_and <- data.frame(
  x1 = c(0, 0, 1, 1),
  x2 = c(0, 1, 0, 1),
  y  = c(0, 0, 0, 1)
)
print(datos_and)
##   x1 x2 y
## 1  0  0 0
## 2  0  1 0
## 3  1  0 0
## 4  1  1 1
set.seed(42)

modelo_and <- neuralnet(
  formula        = y ~ x1 + x2,
  data           = datos_and,
  hidden         = 0,          # sin capa oculta = perceptrón simple
  threshold      = 0.01,
  stepmax        = 1e6,
  rep            = 1,
  learningrate   = 0.5,
  algorithm      = "backprop",
  act.fct        = "logistic",
  linear.output  = FALSE,
  err.fct        = "sse"
)

cat("Pesos aprendidos:\n")
## Pesos aprendidos:
print(modelo_and$weights)
## [[1]]
## [[1]][[1]]
##           [,1]
## [1,] -5.247587
## [2,]  3.425105
## [3,]  3.425185
cat("\nError final (SSE):\n")
## 
## Error final (SSE):
print(modelo_and$result.matrix[1,])
##      error 
## 0.03341951
cat("\nIteraciones hasta convergencia:\n")
## 
## Iteraciones hasta convergencia:
print(modelo_and$result.matrix[3,])
## steps 
##   394
predicciones_and <- predict(modelo_and, datos_and)
clase_and        <- ifelse(predicciones_and >= 0.5, 1, 0)

resultado_and <- data.frame(
  x1         = datos_and$x1,
  x2         = datos_and$x2,
  y_real     = datos_and$y,
  y_sigmoide = round(predicciones_and, 4),
  y_predicha = clase_and,
  correcto   = ifelse(clase_and == datos_and$y, "SI", "NO")
)

print(resultado_and)
##   x1 x2 y_real y_sigmoide y_predicha correcto
## 1  0  0      0     0.0052          0       SI
## 2  0  1      0     0.1391          0       SI
## 3  1  0      0     0.1391          0       SI
## 4  1  1      1     0.8324          1       SI
cat("\nAccuracy AND:", mean(clase_and == datos_and$y), "\n")
## 
## Accuracy AND: 1

3 Operador OR

Regla: la salida es 1 cuando al menos UNA entrada es 1.

Solo la combinación (0,0) produce salida 0.

Solución analítica: \(w_1 = 1,\ w_2 = 1,\ b = -0.5\)

datos_or <- data.frame(
  x1 = c(0, 0, 1, 1),
  x2 = c(0, 1, 0, 1),
  y  = c(0, 1, 1, 1)
)
print(datos_or)
##   x1 x2 y
## 1  0  0 0
## 2  0  1 1
## 3  1  0 1
## 4  1  1 1
set.seed(42)

modelo_or <- neuralnet(
  formula        = y ~ x1 + x2,
  data           = datos_or,
  hidden         = 0,
  threshold      = 0.01,
  stepmax        = 1e6,
  rep            = 1,
  learningrate   = 0.5,
  algorithm      = "backprop",
  act.fct        = "logistic",
  linear.output  = FALSE,
  err.fct        = "sse"
)

cat("Pesos aprendidos:\n")
## Pesos aprendidos:
print(modelo_or$weights)
## [[1]]
## [[1]][[1]]
##           [,1]
## [1,] -1.553320
## [2,]  3.689546
## [3,]  3.696853
cat("\nError final (SSE):\n")
## 
## Error final (SSE):
print(modelo_or$result.matrix[1,])
##      error 
## 0.02633237
cat("\nIteraciones hasta convergencia:\n")
## 
## Iteraciones hasta convergencia:
print(modelo_or$result.matrix[3,])
## steps 
##   289
predicciones_or <- predict(modelo_or, datos_or)
clase_or        <- ifelse(predicciones_or >= 0.5, 1, 0)

resultado_or <- data.frame(
  x1         = datos_or$x1,
  x2         = datos_or$x2,
  y_real     = datos_or$y,
  y_sigmoide = round(predicciones_or, 4),
  y_predicha = clase_or,
  correcto   = ifelse(clase_or == datos_or$y, "SI", "NO")
)

print(resultado_or)
##   x1 x2 y_real y_sigmoide y_predicha correcto
## 1  0  0      0     0.1746          0       SI
## 2  0  1      1     0.8951          1       SI
## 3  1  0      1     0.8944          1       SI
## 4  1  1      1     0.9971          1       SI
cat("\nAccuracy OR:", mean(clase_or == datos_or$y), "\n")
## 
## Accuracy OR: 1

3.1 Comparación AND vs OR

comparacion <- data.frame(
  x1      = datos_and$x1,
  x2      = datos_and$x2,
  y_AND   = round(predict(modelo_and, datos_and), 4),
  clase_AND = ifelse(predict(modelo_and, datos_and) >= 0.5, 1, 0),
  y_OR    = round(predict(modelo_or, datos_or), 4),
  clase_OR  = ifelse(predict(modelo_or, datos_or) >= 0.5, 1, 0)
)
print(comparacion)
##   x1 x2  y_AND clase_AND   y_OR clase_OR
## 1  0  0 0.0052         0 0.1746        0
## 2  0  1 0.1391         0 0.8951        1
## 3  1  0 0.1391         0 0.8944        1
## 4  1  1 0.8324         1 0.9971        1

Conclusión: Los pesos \(w_1 \approx w_2 \approx 1\) son iguales en ambos operadores. Lo único que cambia es el sesgo \(b\): en AND es \(-1.5\) (umbral alto, exige ambas entradas) y en OR es \(-0.5\) (umbral bajo, basta con una).


4 Operador XOR — El Límite del Perceptrón Simple

Regla: la salida es 1 cuando exactamente UNA entrada es 1.

XOR no es linealmente separable — los puntos de clase 1 están en esquinas opuestas del cuadrado y ninguna línea recta puede separarlos.

datos_xor <- data.frame(
  x1 = c(0, 0, 1, 1),
  x2 = c(0, 1, 0, 1),
  y  = c(0, 1, 1, 0)
)
print(datos_xor)
##   x1 x2 y
## 1  0  0 0
## 2  0  1 1
## 3  1  0 1
## 4  1  1 0

4.1 Intento con perceptrón simple (hidden = 0)

set.seed(42)

modelo_xor_simple <- neuralnet(
  formula        = y ~ x1 + x2,
  data           = datos_xor,
  hidden         = 0,
  threshold      = 0.01,
  stepmax        = 1e6,
  rep            = 1,
  learningrate   = 0.5,
  algorithm      = "backprop",
  act.fct        = "logistic",
  linear.output  = FALSE,
  err.fct        = "sse"
)

pred_simple  <- predict(modelo_xor_simple, datos_xor)
clase_simple <- ifelse(pred_simple >= 0.5, 1, 0)

cat("SSE final (debería ser ~0.5 — no convergió):\n")
## SSE final (debería ser ~0.5 — no convergió):
print(modelo_xor_simple$result.matrix[1,])
##     error 
## 0.5024345
cat("\nIteraciones usadas (agotó el límite):\n")
## 
## Iteraciones usadas (agotó el límite):
print(modelo_xor_simple$result.matrix[3,])
## steps 
##    68
cat("\nAccuracy XOR simple:", mean(clase_simple == datos_xor$y), "\n")
## 
## Accuracy XOR simple: 0.5
resultado_xor_simple <- data.frame(
  x1         = datos_xor$x1,
  x2         = datos_xor$x2,
  y_real     = datos_xor$y,
  y_sigmoide = round(pred_simple, 4),
  y_predicha = clase_simple,
  correcto   = ifelse(clase_simple == datos_xor$y, "SI", "NO")
)
print(resultado_xor_simple)
##   x1 x2 y_real y_sigmoide y_predicha correcto
## 1  0  0      0     0.5571          1       NO
## 2  0  1      1     0.5225          1       SI
## 3  1  0      1     0.4990          0       NO
## 4  1  1      0     0.4642          0       SI

4.2 Solución con capa oculta (hidden = 2)

XOR se descompone como: \(XOR = OR \wedge NAND\)

Cada neurona oculta aprende uno de estos operadores intermedios.

set.seed(42)

modelo_xor_oculta <- neuralnet(
  formula        = y ~ x1 + x2,
  data           = datos_xor,
  hidden         = 2,
  threshold      = 0.01,
  stepmax        = 1e6,
  rep            = 1,
  learningrate   = 0.5,
  algorithm      = "backprop",
  act.fct        = "logistic",
  linear.output  = FALSE,
  err.fct        = "sse"
)

pred_oculta  <- predict(modelo_xor_oculta, datos_xor)
clase_oculta <- ifelse(pred_oculta >= 0.5, 1, 0)

cat("SSE final (cercano a 0 — convergió):\n")
## SSE final (cercano a 0 — convergió):
print(modelo_xor_oculta$result.matrix[1,])
##     error 
## 0.4986655
cat("\nAccuracy XOR con capa oculta:", mean(clase_oculta == datos_xor$y), "\n")
## 
## Accuracy XOR con capa oculta: 0.5
resultado_xor_oculta <- data.frame(
  x1         = datos_xor$x1,
  x2         = datos_xor$x2,
  y_real     = datos_xor$y,
  y_sigmoide = round(pred_oculta, 4),
  y_predicha = clase_oculta,
  correcto   = ifelse(clase_oculta == datos_xor$y, "SI", "NO")
)
print(resultado_xor_oculta)
##   x1 x2 y_real y_sigmoide y_predicha correcto
## 1  0  0      0     0.5097          1       NO
## 2  0  1      1     0.4700          0       NO
## 3  1  0      1     0.5449          1       SI
## 4  1  1      0     0.4995          0       SI

4.3 Tabla comparativa AND vs OR vs XOR

sse_and <- modelo_and$result.matrix[1,]
sse_or  <- modelo_or$result.matrix[1,]
sse_xor <- modelo_xor_simple$result.matrix[1,]

iter_and <- modelo_and$result.matrix[3,]
iter_or  <- modelo_or$result.matrix[3,]
iter_xor <- modelo_xor_simple$result.matrix[3,]

tabla_comp <- data.frame(
  Operador    = c("AND", "OR", "XOR (simple)", "XOR (capa oculta)"),
  SSE_final   = round(c(sse_and, sse_or, sse_xor,
                         modelo_xor_oculta$result.matrix[1,]), 6),
  Iteraciones = c(iter_and, iter_or, iter_xor,
                  modelo_xor_oculta$result.matrix[3,]),
  Convergio   = c("SI", "SI", "NO", "SI"),
  Accuracy    = c(
    mean(clase_and    == datos_and$y),
    mean(clase_or     == datos_or$y),
    mean(clase_simple == datos_xor$y),
    mean(clase_oculta == datos_xor$y)
  )
)

print(tabla_comp)
##            Operador SSE_final Iteraciones Convergio Accuracy
## 1               AND  0.033420         394        SI      1.0
## 2                OR  0.026332         289        SI      1.0
## 3      XOR (simple)  0.502434          68        NO      0.5
## 4 XOR (capa oculta)  0.498665          24        SI      0.5

5 Café — Detección de Deficiencias Nutricionales

Aplicación del perceptrón al diagnóstico de deficiencias en plantas de café (Rolle y Guidoni, 2007).

Variables:

Variable Síntoma Codificación
\(x_1\) Clorosis foliar 0 = sin amarillamiento; 1 = hojas amarillas
\(x_2\) Necrosis en bordes 0 = ausente; 1 = presente
\(x_3\) Retraso en brotes 0 = normal; 1 = reducido
\(x_4\) Caída prematura de hojas 0 = no ocurre; 1 = ocurre

Operador: \(y = 1\) si al menos 2 de 4 síntomas están presentes.

5.1 Tabla de verdad completa (16 combinaciones)

datos_cafe <- data.frame(
  x1 = c(0,1,0,0,0,1,1,1,0,0,0,1,1,1,0,1),
  x2 = c(0,0,1,0,0,1,0,0,1,1,0,1,1,0,1,1),
  x3 = c(0,0,0,1,0,0,1,0,1,0,1,1,0,1,1,1),
  x4 = c(0,0,0,0,1,0,0,1,0,1,1,0,1,1,1,1),
  y  = c(0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1)
)

print(datos_cafe)
##    x1 x2 x3 x4 y
## 1   0  0  0  0 0
## 2   1  0  0  0 0
## 3   0  1  0  0 0
## 4   0  0  1  0 0
## 5   0  0  0  1 0
## 6   1  1  0  0 1
## 7   1  0  1  0 1
## 8   1  0  0  1 1
## 9   0  1  1  0 1
## 10  0  1  0  1 1
## 11  0  0  1  1 1
## 12  1  1  1  0 1
## 13  1  1  0  1 1
## 14  1  0  1  1 1
## 15  0  1  1  1 1
## 16  1  1  1  1 1
cat("\nDistribución de clases:\n")
## 
## Distribución de clases:
print(table(datos_cafe$y))
## 
##  0  1 
##  5 11
cat("5 sanas (0 o 1 síntoma) — 11 enfermas (2 o más síntomas)\n")
## 5 sanas (0 o 1 síntoma) — 11 enfermas (2 o más síntomas)

5.2 Arquitectura 4-1 — Perceptrón simple

set.seed(42)

modelo_cafe_41 <- neuralnet(
  formula        = y ~ x1 + x2 + x3 + x4,
  data           = datos_cafe,
  hidden         = 0,
  threshold      = 0.01,
  stepmax        = 1e6,
  rep            = 1,
  learningrate   = 0.5,
  algorithm      = "backprop",
  act.fct        = "logistic",
  linear.output  = FALSE,
  err.fct        = "sse"
)

cat("Pesos aprendidos (esperado: todos ≈ 1, sesgo ≈ -1.5):\n")
## Pesos aprendidos (esperado: todos ≈ 1, sesgo ≈ -1.5):
print(modelo_cafe_41$weights)
## [[1]]
## [[1]][[1]]
##           [,1]
## [1,] -6.518616
## [2,]  4.472182
## [3,]  4.472182
## [4,]  4.472182
## [5,]  4.472182
cat("\nSSE final:\n")
## 
## SSE final:
print(modelo_cafe_41$result.matrix[1,])
##      error 
## 0.04597895
cat("\nIteraciones:\n")
## 
## Iteraciones:
print(modelo_cafe_41$result.matrix[3,])
## steps 
##   380
pred_41  <- predict(modelo_cafe_41, datos_cafe)
clase_41 <- ifelse(pred_41 >= 0.5, 1, 0)

resultado_41 <- data.frame(
  x1         = datos_cafe$x1,
  x2         = datos_cafe$x2,
  x3         = datos_cafe$x3,
  x4         = datos_cafe$x4,
  y_real     = datos_cafe$y,
  y_sigmoide = round(pred_41, 4),
  y_predicha = clase_41,
  correcto   = ifelse(clase_41 == datos_cafe$y, "SI", "NO")
)

print(resultado_41)
##    x1 x2 x3 x4 y_real y_sigmoide y_predicha correcto
## 1   0  0  0  0      0     0.0015          0       SI
## 2   1  0  0  0      0     0.1144          0       SI
## 3   0  1  0  0      0     0.1144          0       SI
## 4   0  0  1  0      0     0.1144          0       SI
## 5   0  0  0  1      0     0.1144          0       SI
## 6   1  1  0  0      1     0.9188          1       SI
## 7   1  0  1  0      1     0.9188          1       SI
## 8   1  0  0  1      1     0.9188          1       SI
## 9   0  1  1  0      1     0.9188          1       SI
## 10  0  1  0  1      1     0.9188          1       SI
## 11  0  0  1  1      1     0.9188          1       SI
## 12  1  1  1  0      1     0.9990          1       SI
## 13  1  1  0  1      1     0.9990          1       SI
## 14  1  0  1  1      1     0.9990          1       SI
## 15  0  1  1  1      1     0.9990          1       SI
## 16  1  1  1  1      1     1.0000          1       SI
cat("\nAccuracy 4-1:", mean(clase_41 == datos_cafe$y), "\n")
## 
## Accuracy 4-1: 1

5.3 Arquitectura 4-2-1 — Una capa oculta, dos neuronas

set.seed(42)

modelo_cafe_421 <- neuralnet(
  formula        = y ~ x1 + x2 + x3 + x4,
  data           = datos_cafe,
  hidden         = 2,
  threshold      = 0.01,
  stepmax        = 1e6,
  rep            = 1,
  learningrate   = 0.5,
  algorithm      = "backprop",
  act.fct        = "logistic",
  linear.output  = FALSE,
  err.fct        = "sse"
)

cat("Pesos aprendidos:\n")
## Pesos aprendidos:
print(modelo_cafe_421$weights)
## [[1]]
## [[1]][[1]]
##           [,1]       [,2]
## [1,] -4.634947 2.34414485
## [2,]  3.136221 1.24030871
## [3,]  3.078353 0.03847516
## [4,]  3.152026 1.88423374
## [5,]  3.079976 0.06490777
## 
## [[1]][[2]]
##           [,1]
## [1,] -2.186968
## [2,]  8.539733
## [3,] -2.035708
cat("\nSSE final:\n")
## 
## SSE final:
print(modelo_cafe_421$result.matrix[1,])
##      error 
## 0.01825004
cat("\nIteraciones:\n")
## 
## Iteraciones:
print(modelo_cafe_421$result.matrix[3,])
## steps 
##   308
pred_421  <- predict(modelo_cafe_421, datos_cafe)
clase_421 <- ifelse(pred_421 >= 0.5, 1, 0)

resultado_421 <- data.frame(
  x1         = datos_cafe$x1,
  x2         = datos_cafe$x2,
  x3         = datos_cafe$x3,
  x4         = datos_cafe$x4,
  y_real     = datos_cafe$y,
  y_sigmoide = round(pred_421, 4),
  y_predicha = clase_421,
  correcto   = ifelse(clase_421 == datos_cafe$y, "SI", "NO")
)

print(resultado_421)
##    x1 x2 x3 x4 y_real y_sigmoide y_predicha correcto
## 1   0  0  0  0      0     0.0187          0       SI
## 2   1  0  0  0      0     0.0686          0       SI
## 3   0  1  0  0      0     0.0715          0       SI
## 4   0  0  1  0      0     0.0683          0       SI
## 5   0  0  0  1      0     0.0714          0       SI
## 6   1  1  0  0      1     0.9484          1       SI
## 7   1  0  1  0      1     0.9504          1       SI
## 8   1  0  0  1      1     0.9484          1       SI
## 9   0  1  1  0      1     0.9481          1       SI
## 10  0  1  0  1      1     0.9503          1       SI
## 11  0  0  1  1      1     0.9482          1       SI
## 12  1  1  1  0      1     0.9859          1       SI
## 13  1  1  0  1      1     0.9864          1       SI
## 14  1  0  1  1      1     0.9859          1       SI
## 15  0  1  1  1      1     0.9861          1       SI
## 16  1  1  1  1      1     0.9869          1       SI
cat("\nAccuracy 4-2-1:", mean(clase_421 == datos_cafe$y), "\n")
## 
## Accuracy 4-2-1: 1

5.4 Comparación de arquitecturas

tabla_arq <- data.frame(
  Arquitectura = c("4-1 (perceptrón simple)",
                   "4-2-1 (1 capa oculta, 2 neuronas)"),
  Parametros   = c("4 pesos + 1 sesgo = 5",
                   "4×2 + 2 + 2×1 + 1 = 13"),
  SSE_final    = round(c(modelo_cafe_41$result.matrix[1,],
                         modelo_cafe_421$result.matrix[1,]), 6),
  Accuracy     = c(mean(clase_41  == datos_cafe$y),
                   mean(clase_421 == datos_cafe$y)),
  Veredicto    = c("Funciona: frontera lineal suficiente",
                   "Óptimo: dos detectores especializados")
)

print(tabla_arq)
##                        Arquitectura             Parametros SSE_final Accuracy
## 1           4-1 (perceptrón simple)  4 pesos + 1 sesgo = 5  0.045979        1
## 2 4-2-1 (1 capa oculta, 2 neuronas) 4×2 + 2 + 2×1 + 1 = 13  0.018250        1
##                               Veredicto
## 1  Funciona: frontera lineal suficiente
## 2 Óptimo: dos detectores especializados

¿Por qué 2 neuronas ocultas?

El operador “al menos 2 de 4” tiene exactamente 3 niveles relevantes: S=0, S=1 (sanas) y S≥2 (enfermas). Una neurona define una frontera (2 zonas), insuficiente. Dos neuronas definen dos fronteras independientes, creando la partición necesaria.

La heurística de partida es: \(n_h = \lceil \log_2(n) \rceil = \lceil \log_2(4) \rceil = 2\)


6 Síntesis Final

sintesis <- data.frame(
  Modelo              = c("AND", "OR", "XOR simple",
                          "XOR capa oculta", "Café 4-1", "Café 4-2-1"),
  Arquitectura        = c("2-1", "2-1", "2-1", "2-2-1", "4-1", "4-2-1"),
  Separable_lineal    = c("Sí", "Sí", "No", "No aplica", "Sí", "No aplica"),
  Converge            = c("Sí", "Sí", "No", "Sí", "Sí", "Sí"),
  Accuracy            = c(
    mean(clase_and    == datos_and$y),
    mean(clase_or     == datos_or$y),
    mean(clase_simple == datos_xor$y),
    mean(clase_oculta == datos_xor$y),
    mean(clase_41     == datos_cafe$y),
    mean(clase_421    == datos_cafe$y)
  )
)

print(sintesis)
##            Modelo Arquitectura Separable_lineal Converge Accuracy
## 1             AND          2-1               Sí       Sí      1.0
## 2              OR          2-1               Sí       Sí      1.0
## 3      XOR simple          2-1               No       No      0.5
## 4 XOR capa oculta        2-2-1        No aplica       Sí      0.5
## 5        Café 4-1          4-1               Sí       Sí      1.0
## 6      Café 4-2-1        4-2-1        No aplica       Sí      1.0

Lección central: Un perceptrón de una sola capa solo puede aprender funciones linealmente separables. Cuando la frontera de decisión requiere más de un hiperplano, se necesita al menos una capa oculta.


7 Sesión Info

sessionInfo()
## R version 4.2.3 (2023-03-15)
## Platform: x86_64-apple-darwin17.0 (64-bit)
## Running under: macOS Big Sur 11.7.11
## 
## Matrix products: default
## BLAS:   /Library/Frameworks/R.framework/Versions/4.2/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.2/Resources/lib/libRlapack.dylib
## 
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] neuralnet_1.44.2
## 
## loaded via a namespace (and not attached):
##  [1] digest_0.6.39     R6_2.6.1          lifecycle_1.0.5   jsonlite_2.0.0   
##  [5] evaluate_1.0.5    cachem_1.1.0      rlang_1.1.7       cli_3.6.5        
##  [9] rstudioapi_0.18.0 jquerylib_0.1.4   bslib_0.10.0      rmarkdown_2.30   
## [13] tools_4.2.3       xfun_0.56         yaml_2.3.12       fastmap_1.2.0    
## [17] compiler_4.2.3    htmltools_0.5.9   knitr_1.51        sass_0.4.10