En esta sesión práctica de programación en R, haremos una introducción básica a las Redes Neuronales Artificiales, e implementaremos con ellas un clasificador sobre el conjunto de datos Predicting a Pulsar Star, tal y como se utilizó anteriormente en la sesión práctica Introducción a los Modelos de Clasificación en R. Tanto éste como muchos otros conjuntos de datos reales y útiles se encuentran diponibles en el portal Kaggle.
Las Redes Neuronales Artificiales son modelos computacionales inspirados en las neuronas biológicas, y que están conformadas por un conjunto de unidades de cómputo básico (neuronas) las cuales están conectadas entre ellas de múltiples maneras. Estas conexiones estarán definidas por unos pesos los cuales determinarán la fuerza o importancia de dichas conexiones, y durante el proceso de aprendizaje o entrenamiento de la red, serán estos pesos los que se ajustarán con el fin de producir la salida adecuada según la entrada que se aplique a la red. En términos generales, las redes neuronales artificiales están compuestas por una capa de entrada, una o más capas ocultas, y una capa de salida como se observa en la siguiente figura:
Por su parte, en cada neurona se realiza el cálculo matemático de la suma ponderada de las entradas con los pesos de la red, y luego se aplica sobre dicho resultado una función de activación, que producirá la salida final de cada neurona y que servirán como valores de entrada de la siguiente capa de la red neuronal.
El proceso de aprendizaje de la red se logra gracias al algoritmo de Propagación hacia atrás o Backpropagation, que consiste en aplicar las entradas a la red, obtener una salida final en la última capa, comparar este resultado con el resultado esperado (aprendizaje supervisado), y luego ir modificando hacia atrás los pesos de la capa de salida, oculta y de entrada a fin de minimizar (a partir del Descenso del Gradiente) el error entre la salida deseada y la salida generada por la red neuronal.
En este ejercicio práctico nos concentraremos en los detalles de la implementación de la red neuronal en R para un problema real, por lo que a efectos de una explicación más detallada del tema, recomiendo leer el artículo Introduction to Neural Networks de Ben Gorman.
Como se mencionó, para este ejercicio haremos uso del conjunto de datos de Predicting a Pulsar Star de Kaggle, por lo que, en primer lugar, carguemos dicho dataset:
dataset <- read.csv("pulsar_stars.csv")
Haciendo uso de la función str, podemos explorar la estructura del dataframe que contiene el conjunto de datos:
str(dataset)
## 'data.frame': 17898 obs. of 9 variables:
## $ Mean.of.the.integrated.profile : num 140.6 102.5 103 136.8 88.7 ...
## $ Standard.deviation.of.the.integrated.profile: num 55.7 58.9 39.3 57.2 40.7 ...
## $ Excess.kurtosis.of.the.integrated.profile : num -0.2346 0.4653 0.3233 -0.0684 0.6009 ...
## $ Skewness.of.the.integrated.profile : num -0.7 -0.515 1.051 -0.636 1.123 ...
## $ Mean.of.the.DM.SNR.curve : num 3.2 1.68 3.12 3.64 1.18 ...
## $ Standard.deviation.of.the.DM.SNR.curve : num 19.1 14.9 21.7 21 11.5 ...
## $ Excess.kurtosis.of.the.DM.SNR.curve : num 7.98 10.58 7.74 6.9 14.27 ...
## $ Skewness.of.the.DM.SNR.curve : num 74.2 127.4 63.2 53.6 252.6 ...
## $ target_class : int 0 0 0 0 0 0 0 0 0 0 ...
Como podemos observar, el conjunto de datos tiene 17898 observaciones y 9 variables en cada una. Específicamente, este dataset contiene información estadística de estrellas pulsares obtenidos por el trabajo High Time Resolution Universe Survey, y cuyas variables son:
En este caso, la clase especifica con 1 si se trata de una estrella pulsar, y con 0 si no lo es.
Nuestro objetivo en esta práctica será hacer uso de todas estas variables para construir una red neurona artificial capaz de predecir si una estrella es o no un Pulsar.
A partir de la tabla anterior, también podemos ver que, a excepción de la variable target_class, todos los demás registros son numéricos. Sin embargo, ya que se trata de un problema de clasificación, es conveniente que nuestra variable a predecir, es decir la variable dependiente target_class, sea un Factor o variable categórica. Vamos entonces a convertir esta variable, así como asignarle el nombre TipoDeEstrella a la columna:
colnames(dataset)[9] <- "TipoEstrella"
dataset$TipoEstrella <- factor(dataset$TipoEstrella, levels = c("0", "1"), labels = c("NoPulsar", "Pulsar"))
Si ahora hacemos uso de la función summary podemos tener un resumen estadístico de las variables del dataset:
summary(dataset)
## Mean.of.the.integrated.profile
## Min. : 5.812
## 1st Qu.:100.930
## Median :115.078
## Mean :111.080
## 3rd Qu.:127.086
## Max. :192.617
## Standard.deviation.of.the.integrated.profile
## Min. :24.77
## 1st Qu.:42.38
## Median :46.95
## Mean :46.55
## 3rd Qu.:51.02
## Max. :98.78
## Excess.kurtosis.of.the.integrated.profile
## Min. :-1.8760
## 1st Qu.: 0.0271
## Median : 0.2232
## Mean : 0.4779
## 3rd Qu.: 0.4733
## Max. : 8.0695
## Skewness.of.the.integrated.profile Mean.of.the.DM.SNR.curve
## Min. :-1.7919 Min. : 0.2132
## 1st Qu.:-0.1886 1st Qu.: 1.9231
## Median : 0.1987 Median : 2.8018
## Mean : 1.7703 Mean : 12.6144
## 3rd Qu.: 0.9278 3rd Qu.: 5.4643
## Max. :68.1016 Max. :223.3921
## Standard.deviation.of.the.DM.SNR.curve
## Min. : 7.37
## 1st Qu.: 14.44
## Median : 18.46
## Mean : 26.33
## 3rd Qu.: 28.43
## Max. :110.64
## Excess.kurtosis.of.the.DM.SNR.curve Skewness.of.the.DM.SNR.curve
## Min. :-3.139 Min. : -1.977
## 1st Qu.: 5.782 1st Qu.: 34.961
## Median : 8.434 Median : 83.065
## Mean : 8.304 Mean : 104.858
## 3rd Qu.:10.703 3rd Qu.: 139.309
## Max. :34.540 Max. :1191.001
## TipoEstrella
## NoPulsar:16259
## Pulsar : 1639
##
##
##
##
A partir del resumen, podemos ver que el conjunto de datos incluye registros de 16259 estrellas que no son pulsares, mientras que 1639 de ellas sí lo son.
Por otro lado, se observa también que existe cierta variabilidad y diferencias de escala entre los valores mínimos y máximos de los registros medidos. En este sentido, la aplicación de las redes neuronales artificiales exige que las variables de entrada estén todas rescaladas en el mismo rango, por lo que haremos uso de la función scale para cumplir con esta condición:
dataset[, c(1:8)] <- scale(dataset[, c(1:8)])
summary(dataset)
## Mean.of.the.integrated.profile
## Min. :-4.1035
## 1st Qu.:-0.3957
## Median : 0.1559
## Mean : 0.0000
## 3rd Qu.: 0.6239
## Max. : 3.1785
## Standard.deviation.of.the.integrated.profile
## Min. :-3.18236
## 1st Qu.:-0.60988
## Median : 0.05815
## Mean : 0.00000
## 3rd Qu.: 0.65374
## Max. : 7.63232
## Excess.kurtosis.of.the.integrated.profile
## Min. :-2.212200
## 1st Qu.:-0.423630
## Median :-0.239293
## Mean : 0.000000
## 3rd Qu.:-0.004259
## Max. : 7.134757
## Skewness.of.the.integrated.profile Mean.of.the.DM.SNR.curve
## Min. :-0.5775 Min. :-0.4208
## 1st Qu.:-0.3176 1st Qu.:-0.3628
## Median :-0.2548 Median :-0.3329
## Mean : 0.0000 Mean : 0.0000
## 3rd Qu.:-0.1366 3rd Qu.:-0.2426
## Max. :10.7543 Max. : 7.1516
## Standard.deviation.of.the.DM.SNR.curve
## Min. :-0.9736
## 1st Qu.:-0.6106
## Median :-0.4040
## Mean : 0.0000
## 3rd Qu.: 0.1079
## Max. : 4.3304
## Excess.kurtosis.of.the.DM.SNR.curve Skewness.of.the.DM.SNR.curve
## Min. :-2.53941 Min. :-1.0030
## 1st Qu.:-0.55970 1st Qu.:-0.6562
## Median : 0.02884 Median :-0.2046
## Mean : 0.00000 Mean : 0.0000
## 3rd Qu.: 0.53248 3rd Qu.: 0.3234
## Max. : 5.82240 Max. :10.1971
## TipoEstrella
## NoPulsar:16259
## Pulsar : 1639
##
##
##
##
Por último, vamos a crear un conjunto de entrenamiento (80%) y un conjunto de validación (20%) a partir de nuestro dataset original, esto con la intención de validar la calidad de la clasificación producida por la red neuronal. Para esto, hagamos uso de la función sample.split del paquete caTools:
library(caTools)
set.seed(1234)
split <- sample.split(dataset$TipoEstrella, SplitRatio = 0.80)
training_set <- subset(dataset, split == TRUE)
test_set <- subset(dataset, split == FALSE)
A fin de asegurarnos de que la proporción de estrellas pulsares y no pulsares es aproximadamente la misma en ambos conjuntos de datos, veamos la distribución de los tipos de estrella en cada uno:
table(training_set$TipoEstrella)
##
## NoPulsar Pulsar
## 13007 1311
table(test_set$TipoEstrella)
##
## NoPulsar Pulsar
## 3252 328
En efecto, vemos que para ambos conjuntos, la relación NoPulsar/Pulsar es alrededor de 9.9, de modo que la proporción se mantiene.
Ya que nuestro conjunto de datos está listo y preparado, podemos comenzar a construir nuestra red neuronal artificial.
La implementación de la red neuronal la haremos haciendo uso del paquete H2O, el cual es una librería para análisis predictivo y machine learning que incorpora funciones para crear redes neuronales artificiales así como modelos de deep learning.
En primer lugar, es necesario entonces cargar la librería e inicializarla:
library(h2o)
h2o.init(nthreads = -1)
## Connection successful!
##
## R is connected to the H2O cluster:
## H2O cluster uptime: 2 hours 3 seconds
## H2O cluster timezone: America/Caracas
## H2O data parsing timezone: UTC
## H2O cluster version: 3.20.0.2
## H2O cluster version age: 22 days
## H2O cluster name: H2O_started_from_R_Administrator_mlr302
## H2O cluster total nodes: 1
## H2O cluster total memory: 3.34 GB
## H2O cluster total cores: 4
## H2O cluster allowed cores: 4
## H2O cluster healthy: TRUE
## H2O Connection ip: localhost
## H2O Connection port: 54321
## H2O Connection proxy: NA
## H2O Internal Security: FALSE
## H2O API Extensions: Algos, AutoML, Core V3, Core V4
## R Version: R version 3.4.4 (2018-03-15)
Cuando se inicializa h2o, se generará un mensaje que hace referencia a la conexión exitosa de la librería al cluster que realizará los cálculos. El parámetro nthreads = -1 se refiere a que se harán uso de todos los CPU disponibles del equipo para realizarlos.
Una vez inicializado, podemos crear nuestro objeto clasificador haciendo uso de la función h2o.deeplearning (veremos el progreso del entrenamiento mientras se ejecuta):
classifier = h2o.deeplearning(y = 'TipoEstrella',
training_frame = as.h2o(training_set),
activation = 'Rectifier',
hidden = c(5, 5),
epochs = 100,
train_samples_per_iteration = -2)
##
|
| | 0%
|
|=================================================================| 100%
##
|
| | 0%
|
|=========================== | 42%
|
|================================================================ | 98%
|
|=================================================================| 100%
En esta función, el parámetro activation se refiere a la función de activación que se empleará en cada neurona, que en este caso es el Rectificador, mientras que el parámetro hidden establece la cantidad de capas ocultas así como neuronas en cada una de ellas. En este caso, la red neuronal creada tendrá dos capas ocultas, cada una de ellas con 6 neuronas. Por último, el parámetro epochs establece la cantidad de veces que se pasarán los datos de etrenamiento a fin de aplicar el algoritmo de aprendizaje. Todos estos parámetros se pueden afinar y, dependiendo de su valor, se podrán obtener mejores o peores resultados de la clasificación. Sin embargo, en este ejercicio no abordaremos los detalles de dicho procedimiento de afinación de hiperparámetros.
Una vez finalizado el entrenamiento, podemos realizar predicciones sobre el conjunto de validación de la siguiente manera:
prob_pred <- h2o.predict(classifier, newdata = as.h2o(test_set))
##
|
| | 0%
|
|=================================================================| 100%
##
|
| | 0%
|
|=================================================================| 100%
y_pred <- as.vector(ifelse(prob_pred$predict == 'NoPulsar', 0, 1))
y_test_set <- ifelse(test_set$TipoEstrella == 'NoPulsar', 0, 1)
En las dos últimas líneas convertimos las predicciones en un vector de ceros y unos, así como la columna TipoEstrella del conjunto de prueba, a fin de realizar luego la matriz de confusión de los resultados:
cm <- table(y_test_set, y_pred)
cm
## y_pred
## y_test_set 0 1
## 0 3224 28
## 1 50 278
En este caso, tenemos un total de 3502 predicciones correctas, lo que nos genera una precisión en la predicción del 97.82%, y un error del 2.18%.
Haciendo uso del paquete ROCR podemos graficar la curva ROC, la cual nos da una idea de la calidad del modelo a partir de las relaciones entre Falsos Positivos (False Positives) y Verdaderos Positivos (True Positives) obtenidos sobre el conjunto de validación:
library(ROCR)
pred1 <- prediction(as.numeric(y_pred), as.numeric(y_test_set))
perf1 <- performance(pred1, "tpr", "fpr")
plot(perf1)
Como podemos ver, la clasificación producida por la red neuronal genera una línea resultante que está bastante alejada de la diagonal que, para este tipo de curvas, representa la selección completamente al azar de las clases, lo que refleja entonces un buen desempeño del clasificador para este conjunto de datos.
Por supuesto, es posible mejorar la calidad de la clasificación si se ajustan los parámetros de la red neuronal como la cantidad de capas ocultas, la cantidad de neuronas o la función de activación.
Una vez finalizada la ejecución del algoritmo, es necesario finalizar la sesión de h2o, por lo que debe escribirse:
h2o.shutdown()
## Are you sure you want to shutdown the H2O instance running at http://localhost:54321/ (Y/N)?
Y seleccionar ‘Y’.
En esta sesión práctica realizamos una implementación sencilla de una red neuronal artificial para clasificación haciendo uso del paquete H2O. Como pudimos ver, en general se obtuvieron precisiones superiores al 97.5%, con relaciones aceptables de Falsos Positivos y Verdaderos Positivos, y esto partiendo de una arquitectura básica de la red neuronal sin detenernos en afinar sus parámetros. Resulta claro que haciendo un análisis más profundo, no solo de la red y su arquitectura sino de los datos de entrada, podría mejorarse el resultado (haciendo ingeniería de atributos o feature engineering, por ejemplo), además de aplicando técnicas de evaluación más elaboradas como la Validación Cruzada, por lo que invito al lector a probar tales alternativas sobre este mismo conjunto de datos a fin de observar sus efectos sobre el resultado final.
Así, concluimos esta práctica introductoria a las Redes Neuronales Artificiales en R.