logo

Introducción

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:

Por objetivos de la clase, trabajaremos con ejemplos de modelos de perceptrón simples.

¿Cómo funciona RNA?

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:

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.

Ejemplo de motivación

Supongamos que eres parte del equipo de análisis de una tienda online. Tienes datos históricos de clientes donde se registra:

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:

El resultado se transmite a la neurona de salida.

Este valor final se interpreta para clasificar así:

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

Preguntas para reflexionar:

¿Qué significa que un peso sea negativo entre una entrada y una neurona?
respuesta: Un peso negativo indica que un aumento en esa variable reduce la activación de la neurona. Es decir, actúa como una señal “inhibidora”. Cuanto más alto es el valor de entrada, menor será el efecto neto en esa neurona.


¿Cuál de las dos variables (interes o ingreso) parece tener mayor influencia sobre la salida?
Respuesta: Puedes observar qué variable tiene mayores pesos en magnitud. Si los pesos asociados a 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.


¿Qué rol cumplen los nodos azules con el número 1?
Respuesta: Son los bias (sesgos). Actúan como entradas constantes que permiten a las neuronas activar incluso si todas las demás entradas son cero. Funcionan como un desplazamiento para ajustar la función de activación.


¿Qué pasaría si agregamos más neuronas ocultas?
Respuesta: Más neuronas permiten que la red aprenda patrones más complejos y no lineales. Sin embargo, también puede aumentar el riesgo de sobreajuste, especialmente si el conjunto de datos es pequeño.


Si el ingreso de un cliente aumenta, pero su interes disminuye, ¿cómo afectaría eso a la predicción?
Respuesta: Dependerá de los pesos aprendidos. Si 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.


¿El modelo que hemos entrenado es lineal o no lineal? ¿Por qué?
Respuesta: Es un modelo no lineal, ya que aunque combina entradas linealmente, pasa esos valores por funciones de activación no lineales. Esto permite generar fronteras de decisión curvas y más complejas.

Ejemplos en R

Ejemplo 1:

Para este ejemplo, trabajaremos con el conjunto de datos iris.

Solución paso a paso:

Paso 1

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

Paso 2

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))

Paso 3

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.

Paso 4

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.

Paso 5

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%"

Ejemplo 2:

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.

Solución paso a paso:

Paso 1

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

Paso 2

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]],]

Paso 3

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")

Paso 4

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.

Ejercicio de clase:

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.