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}\) |
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:
## [[1]]
## [[1]][[1]]
## [,1]
## [1,] -5.247587
## [2,] 3.425105
## [3,] 3.425185
##
## Error final (SSE):
## error
## 0.03341951
##
## Iteraciones hasta convergencia:
## 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
##
## Accuracy AND: 1
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\)
## 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:
## [[1]]
## [[1]][[1]]
## [,1]
## [1,] -1.553320
## [2,] 3.689546
## [3,] 3.696853
##
## Error final (SSE):
## error
## 0.02633237
##
## Iteraciones hasta convergencia:
## 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
##
## Accuracy OR: 1
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).
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
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
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.
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
##
## Distribución de clases:
##
## 0 1
## 5 11
## 5 sanas (0 o 1 síntoma) — 11 enfermas (2 o más síntomas)
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):
## [[1]]
## [[1]][[1]]
## [,1]
## [1,] -6.518616
## [2,] 4.472182
## [3,] 4.472182
## [4,] 4.472182
## [5,] 4.472182
##
## SSE final:
## error
## 0.04597895
##
## Iteraciones:
## 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
##
## Accuracy 4-1: 1
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:
## [[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
##
## SSE final:
## error
## 0.01825004
##
## Iteraciones:
## 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
##
## Accuracy 4-2-1: 1
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\)
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.
## 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