En las últimas décadas, las Redes Neuronales Artificiales (RNA) se han convertido en una de las herramientas más potentes del aprendizaje supervisado. Inspiradas en el funcionamiento del cerebro humano, las RNA están diseñadas para reconocer patrones complejos y realizar tareas como clasificación, regresión, reconocimiento de imágenes y procesamiento de lenguaje natural.
En esencia, una red neuronal es un modelo computacional compuesto por capas de neuronas artificiales, también conocidas como nodos. Cada neurona recibe una serie de entradas, las procesa mediante una función de activación, y transmite una salida que se propaga a la siguiente capa.
Este tipo de modelos son especialmente útiles cuando trabajamos con conjuntos de datos grandes y complejos, donde los modelos tradicionales de clasificación pueden no capturar adecuadamente las relaciones no lineales entre las variables.
Durante los últimos años, el interés y la aplicación de este tipo de modelos han experimentado tal expansión que se ha convertido en una disciplina por sí misma (Deep learning). Si bien entender bien sus fundamentos requiere de una cantidad notable de tiempo y práctica, esto no significa que se necesiten adquirir todos ellos para empezar a sacarles partido.
Existen dos tipos de modelos basados en redes neuronales:
Modelos de redes simples (perceptrón simple): estos modelos se caracterizan por tener arquitecturas relativamente sencillas por lo que los requerimientos computacionales no son elevados.
Deep learning: son modelos más complejos (perceptrón multicapa, redes convolucionales, redes recurrentes, entre otras) cuyos requerimientos computacionales hacen necesario el uso de muy optimizado de CPUs.
Por objetivos de la clase, trabajaremos con ejemplos de modelos de perceptrón simples.
Las redes neuronales son modelos creados al ordenar operaciones matemáticas siguiendo una determinada estructura. La forma más común de representar la estructura de una red neuronal es mediante el uso de capas, formadas a su vez por neuronas. Cada neurona, realiza una operación sencilla y está conectada a las neuronas de la capa anterior y de la capa siguiente mediante pesos, cuya función es regular la información que se propaga de una neurona a otra.
La primera capa de la red neuronal (color verde) se conoce como capa de entrada y recibe los datos en bruto. La capa intermedia (color azul), conocida como capa oculta, recibe los valores de la capa de entrada, ponderados por los pesos. La última capa, llamada output, combina los valores que salen de la capa intermedia para generar la predicción.
Para facilitar la comprensión de la estructura de las redes, es útil representar una red equivalente a un modelo de regresión lineal.
\[y = w_1x_1+\dots+w_dx_d+e \]Cada neurona de la capa de entrada representa el valor de uno de los predictores. Las flechas representan los coeficientes de regresión, que en términos de redes se llaman pesos, y la neurona de salida representa el valor predicho.
Un ejemplo de modelo neuronal con \(n\) entradas, consta de:
Un conjunto de entradas \(x_1,\dots,x_n\).
Los pesos \(w_1,\dots,w_n\), correspondientes a cada entrada.
Una función de activación, \(f\).
Una salida, \(y\).
Las entradas son el estímulo que la neurona artificial recibe del entorno que la rodea, y la salida es la respuesta a tal estímulo. La neurona puede adaptarse al medio circundante y aprender de él modificando el valor de sus pesos, y por ello son conocidos como los parámetros libres del modelo, ya que pueden ser modificados y adaptados para realizar una tarea determinada.
En este modelo, la salida neuronal \(y\) está dada por:
\[ y =f\left(\sum_{i=1}^n w_ix_i \right) \]
La función de activación se elige de acuerdo a la tarea realizada por la neurona. Entre las más comunes dentro del campo de RNA podemos destacar:
Función | Ecuación | Rango | Gráfica |
---|---|---|---|
Identidad | \(y = x\) | \((-\infty, +\infty)\) |
|
Escalón | \(y = H(x)\) | \(\{0, 1\}\) |
|
Lineal a tramos | \(\begin{cases} -1, & x < -1 \\ x, & -1 \leq x \leq 1 \\ 1, & x > 1 \end{cases}\) | \([-1, 1]\) |
|
Sigmoidea | \(y = \frac{1}{1 + e^{-x}}\) | \([0, 1]\) |
|
Gaussiana | \(y = Ae^{-x^2}\) | \([0, 1]\) |
|
Sinusoidal | \(y = \sin(2\pi x)\) | \([-1, 1]\) |
|
En R, existe la función neuralnet()
, de la librería que tiene el mismo nombre.
Supongamos que eres parte del equipo de análisis de una tienda online. Tienes datos históricos de clientes donde se registra:
Nivel de interés en el producto (escala de 1 a 10).
Nivel de ingresos (en miles de dólares).
Si el cliente compró (1) o no compró (0).
El objetivo es construir un modelo con una Red Neuronal Artificial que prediga si un nuevo cliente comprará, en base a estas dos variables.
Los datos se encuentran en la siguiente figura.
Veamos como funciona la red neuronal.
modelo <- neuralnet(formula = compra ~ interes + ingreso,data = datos,
hidden = 3,linear.output = FALSE, stepmax = 1e6)
plot(modelo, rep = "best")
El parámetro hidden
es opcional. Con él indicamos el
número de neuronas que existirá en cada una de las capas ocultas de la
RNA.
Una vez entrenamos una red neuronal, obtenemos una figura como la anterior. Esta visualización representa cómo la red ha aprendido a tomar decisiones, en este caso, si un cliente comprará o no comprará un producto según su nivel de interés y su ingreso mensual.
La red neuronal está compuesta por tres tipos de capas:
Capa de entrada: En la parte izquierda vemos las entradas del modelo son las variables: interes e ingreso. Estas variables se conectan con cada una de las neuronas en la siguiente capa (oculta) mediante flechas con valores numéricos. Estos valores son conocidos como pesos y determinan cuánta influencia tiene cada variable sobre la activación de una neurona.
Capa oculta: En este ejemplo, la red tiene una capa oculta con tres neuronas. Cada neurona:
Suma las entradas recibidas, multiplicadas por sus respectivos pesos.
Suma un sesgo (bias), representado por las flechas azules desde el nodo 1.
Aplica una función de activación, que transforma esa suma en un valor entre 0 y 1.
El resultado se transmite a la neurona de salida.
Capa de salida: La neurona de salida realiza un proceso similar:
Recibe las salidas de la capa oculta, multiplicadas por nuevos pesos.
Suma un nuevo sesgo.
Aplica otra función de activación para obtener una probabilidad final de que el cliente compre.
Este valor final se interpreta para clasificar así:
Si la salida es mayor a 0.5: el modelo predice que sí comprará.
Si es menor o igual a 0.5: el modelo predice que no comprará.
Ahora veamos la predicción:
pred <- compute(modelo, nuevo_cliente)
probabilidad <- pred$net.result
cat("Probabilidad de compra del nuevo cliente:", round(probabilidad, 3))
## Probabilidad de compra del nuevo cliente: 0.889
interes
o ingreso
)
parece tener mayor influencia sobre la salida?
interes
son más grandes que los de
ingreso
, entonces interes
tiene mayor
influencia en la activación de las neuronas y, por tanto, en la salida
final.
1
?
ingreso
de un cliente aumenta, pero su
interes
disminuye, ¿cómo afectaría eso a la predicción?
ingreso
tiene un peso
positivo fuerte, puede compensar una disminución en
interes
. La red combina ambos efectos según la estructura
interna de pesos y funciones de activación.
Para este ejemplo, trabajaremos con el conjunto de datos iris.
Preparación inicial y limpieza de los datos:
library(plotly)
fig1 <- plot_ly(iris,x = ~Petal.Length, y = ~Petal.Width, z = ~Sepal.Length,
color = ~Species, type = "scatter3d", mode = "markers",
colors = c("red", "green", "blue"))
fig1
fig2 <- plot_ly(iris,x = ~Sepal.Length, y = ~Sepal.Width, z = ~Petal.Width,
color = ~Species, type = "scatter3d", mode = "markers",
colors = c("red", "green", "blue"))
fig2
Dividir los datos en conjunto de entrenamiento y prueba
Mediante un muestreo aleatorio, separamos el conjunto de entrenamiento y en conjunto de prueba. Supongamos en 5 grupos, 4 para entrenamiento y 1 para prueba.
library(caret)
set.seed(1234)
folds <- createFolds(iris$Species, k = 5)
entrenamiento <- iris[-folds[[5]],]
prueba <- iris[folds[[5]],]
entrenamiento_oh <- entrenamiento %>%
mutate(setosa = ifelse(Species == "setosa", 1, 0),
versicolor = ifelse(Species == "versicolor", 1, 0),
virginica = ifelse(Species == "virginica", 1, 0))
Aplicar el método RNA
Construimos la red para los datos de entrenamiento.
library(neuralnet)
formula <- setosa + versicolor + virginica ~ Sepal.Length + Sepal.Width + Petal.Length + Petal.Width
RNA1 <- neuralnet(formula,entrenamiento_oh, hidden = 5)
Podemos explorar las conexiones entre las neuronas incluyendo los pesos asignados a cada una de las conexiones entre ellas.
plot(RNA1, rep = "best")
Aquí vemos cómo la red neuronal aprende a tomar decisiones. A partir
de las cuatro características de cada flor, la red activa una de las
tres salidas: setosa
, versicolor
o
virginica
. Cada línea representa un peso aprendido, y la
combinación de esos pesos es lo que le permite a la red identificar
patrones no lineales entre las clases. El nodo que se activa con mayor
fuerza indica cuál es la predicción final.
Validar la estabilidad del modelo
No se aplico en este ejemplo, pero se podría hacer la validación haciendo 5 replicas del proceso, tomando en cada iteración como conjunto de entrenamiento cada una de las particiones creadas.
Interpretación de los resultados finales
Evaluamos el modelo con los datos de prueba.
output <- compute(RNA1, prueba[,-5])
# Obtener la matriz de salida (probabilidades)
predicciones <- output$net.result
pred <- apply(predicciones, 1, which.max)
# Convertir a factor con nombres de especie
pred <- factor(pred,levels = c(1, 2, 3),labels = c("setosa", "versicolor", "virginica"))
confusion <- table(Real=prueba$Species,Predicho=pred)
confusion
## Predicho
## Real setosa versicolor virginica
## setosa 10 0 0
## versicolor 0 10 0
## virginica 0 3 7
Exactitud <- sum(diag(confusion)) / sum(confusion)
paste0("Exactitud: ", round(Exactitud * 100, 2),"%",sep="")
## [1] "Exactitud: 90%"
Con este segundo ejemplo, probaremos la aseveración que cualquier RNA puede aprender por sí misma virtualmente cualquier función. Probaremos con la hipotenusa de un triángulo a partir de las medidas de lo catetos, pero sin facilitarle la fórmula exacta para hacerlo.
Preparación inicial y limpieza de los datos:
Los valores de los catetos serán simulados.
set.seed(1234)
data <- data.frame(
Cat1 = round(runif(100, min = 1, max = 10)),
Cat2 = round(runif(100, min = 1, max = 10)))
data$Hyp <- sqrt(data$Cat1^2 + data$Cat2^2)
head(data)
## Cat1 Cat2 Hyp
## 1 2 1 2.236068
## 2 7 6 9.219544
## 3 6 4 7.211103
## 4 7 3 7.615773
## 5 9 2 9.219544
## 6 7 4 8.062258
Dividir los datos en conjunto de entrenamiento y prueba
Mediante un muestreo aleatorio, separamos el conjunto de entrenamiento y en cojunto de prueba. Supongamos en 4 grupos, 3 para entrenamiento y 1 para prueba.
set.seed(1234)
library(caret)
folds <- createFolds(data$Hyp, k = 4)
entrenamiento <- data[-folds[[4]],]
prueba <- data[folds[[4]],]
Aplicar el método RNA
Teniendo preparados los datos, estamos en disposición de entrenar nuestra red neuronal.
library(neuralnet)
RNA2 <- neuralnet(Hyp ~ Cat1 + Cat2,entrenamiento, hidden = 6, rep = 3)
Por defecto, la función
neuralnet() efectúa una sola vez el proceso de
entrenamiento, pero con el parámetro rep
se puede cambiar
el número de repeticiones (en el proceso hay una componente aleatoria
que provoca que cada red obtenida tras el entrenamiento sea distinta),
con el objetivo de obtener la mejor RNA posible.
Podemos explorar las conexiones entre las neuronas incluyendo los pesos asignados a cada una de las conexiones entre ellas.
plot(RNA2, rep = "best")
Validar la estabilidad del modelo
Teniendo la red ya entrenada, podemos entregarle nuevas entradas, no con el objetivo de que continúe aprendiendo, sino para obtener una predicción de cual debería ser el valor resultante de la función aprendida.
# Guardar la exactitud de cada fold
Error <- matrix(0,ncol=4,nrow=25)
for(i in 1:4){
# Definir conjuntos entrenamiento y prueba según el fold actual
prueba <- data[folds[[i]],]
entrenamiento <- data[-folds[[i]],]
RNA2 <- neuralnet(Hyp ~ Cat1 + Cat2,entrenamiento, hidden = 6)
output <- compute(RNA2, prueba[,-3], rep = 1)
pred1 <- round(output$net.result,3)
Real <- round(prueba$Hyp,3)
Error[,i] <- round(abs(Real - pred1)/Real,3)
}
Resultado <- data.frame(Real,pred1)
Resultado
## Real pred1
## 1 2.236 2.156
## 5 9.220 9.223
## 7 2.236 2.240
## 11 9.899 9.906
## 13 10.770 10.779
## 19 3.606 3.600
## 23 10.198 10.122
## 24 9.055 9.011
## 25 5.831 5.823
## 27 6.708 6.717
## 30 4.123 4.072
## 32 7.616 7.602
## 34 7.810 7.805
## 41 8.485 8.480
## 42 12.207 12.179
## 43 5.657 5.662
## 47 10.630 10.639
## 61 10.296 10.260
## 67 6.403 6.394
## 69 8.062 8.070
## 79 4.472 4.442
## 81 10.296 10.260
## 86 9.055 9.071
## 88 8.062 8.045
## 97 9.849 9.858
# Error promedio de validación cruzada
Error_promedio <- mean(apply(Error,2,mean))
paste("Error_promedio: ",Error_promedio,"%",sep="")
## [1] "Error_promedio: 0.0032%"
Tenemos, por tanto, una RNA que ha aprendido la fórmula de cálculo de la hipotenusa a partir de un conjunto de casos, con capacidad para calcularla con una precisión bastante buena.
Probemos si una RNA puede calcular probabilidades, sin saber la fórmula de como hacerlo. Por ejemplo, simula probabilidades de alguna distribución normal o distribución Binomial y analiza que tan cerca puede estimar sus probabilidades.
Ejemplo de datos normales simulados.
set.seed(1234)
data <- data.frame(
x <- round(seq(-3,3,length=100),1),
y <- round(pnorm(x),4))
Nota importante: averigua como agregar más de una capa oculta en la red neuronal.