Muchas veces en programación nos encontramos con situaciones en las que debemos repetir una misma tarea (un calculo o una serie de calculos) muchas veces, más aún cada ejecución puede requerir mucho tiempo, lo que lleva a que el proceso resulte costoso computacionalmente.
Ahora bien, hoy en día casi todas las computadores son multinúcleos, brindando una posibilidad mayor de incrementar el rendimiento de muchas actividades que se realizan en la cotidianidad de un cientifico de datos.
En este articulo, se brinda una idea sobre el procesamiento en paralelo y algunas recomendaciones que pueden ayudar considerablemente a mejorar los programas que escribamos en R.
Tradicionalmente, se tiende a realizar procesamiento serial, esto es cuando un problema se divide en una serie de instrucciones, que se ejecutan secuencialmente una tras otra, en un solo procesador. Sin embargo, en muchas ocasiones se puede hacer uso de múltiples recursos informáticos para resolver un problema computacional, esto es procesamiento en paralelo. En este paradigma, el problema se divide en partes discretas a las que pueden aplicarse una serie de instrucciones simultaneamente en diferentes procesadores (Barney and others 2010).
Enfoques de procesamiento. Fuente: (Barney and others 2010)
De acuerdo con Matloff (2015) los recursos informáticos pueden ser:
Un sistema multiprocesador, como su nombre lo indica tiene dos o más procesadores (dos o más CPUs), para que dos o más programas puedan ser ejecutados al mismo tiempo. Usualmente contamos con una computadora de dos o más núcleos (sistemas multinúcleos), que también se conocen como un sistema multiprocesador de baja gama. A su vez, estos sistemas multiprocesadores son conocidos como sistemas de memoria compartida, ya que comparten la misma RAM física.
Un clúster, que consiste en múltiples computadoras conectadas en red, cada una ejecutándose de manera independiente, participando en la resolución de un problema numérico.
Muchos procesos son sujetos a paralelizar, sin embargo no siempre es viable realizarlo, a menudo sucede que la versión paralela del problema, termina ejecutandose más lento que la versión en serie. Hay dos principales problemas en la programación en paralelo:
Sobrecarga de comunicaciones: por lo general, los datos deben transferirse a todos los procesos o hilos (también se denomina asi a los núcleos disponibles) y en ocasiones transferirse datos entre los mismos hilos, por lo tanto esto es algo que lleva tiempo y puede afectar el rendimiento. Cuando en los algoritmos se involucran trozos de cálculo lo suficientemente grandes como para que la sobrecarga no sea un problema, se llama paralelización de grano grueso. En R, para problemas más sencillos como operaciones aritméticas, algebraicas, etc. se tienen funciones como apply(), sapply(), lapply(), entre otras, las cuales se encuentran programadas en lenguajes C y C++ que hacen que los cálculos sean significativamente más rápidos.
Tiempo de acceso a memoria: como se menciono antes, en los sistemas multiprocesadores se comparte la memoria RAM entre hilos. Es posible validar en el administrador de tareas el porcentaje (%) de uso de RAM que un solo hilo está presentando, en caso que éste sea muy alto, es altamente probable que no exista beneficio al realizar procesamiento en paralelo.
Balance de carga: Se debe tener cuidado con la forma de asignar el trabajo a los hilos, pues se corre el riesgo de asignar más trabajo a unos que a otros, haciendo que algunos hilos queden improductivos mientras aún queda trabajo por hacer.
Parallel (2013) es una librería que se basa en los paquetes multicore y snow. Está orientado a la paralelización de grano grueso y en problemas donde cada hilo realiza su trabajo de manera independiente, es decir que no necesitan comunicarse de ninguna manera.
En general, todo el trabajo pesado de balanceo de carga, consolidación de los resultados de los hilos, entre otros, ya está contenido en el paquete, es decir que éste realiza todo ese trabajo por nosotros, lo que se resume a que el modelo computacional básico que debamos seguir sea:
En general, hay dos maneras en que nuestro código puede ser paralelizado, estas son los enfoques forking y socket:
El enfoque forking crea una copia del ambiente actual de R, en cada núcleo y realiza la operación indicada, este no tiene mayor dificultad de implementación pero no se encuentra disponible para windows.
El enfoque socket es más general y requiere un poco más de trabajo para implementar, aquí se crea un clúster en la misma máquina (también se puede utilizar para crear un clúster con más máquinas) y se lanza una nueva versión de R a cada core, dado que es un nuevo ambiente, se debe enviar a los hilos cada objeto que necesita para llevar a cabo su tarea, incluso la invocación de librerías, en caso de ser necesarias. Este enfoque se encuentra disponible en cualquier sistema operativo (incluyendo windows).
A continuación se brindará una ilustración del uso de la librería parallel, utilizando el conjunto de datos Saber Pro 11 2019-2 alojado en Datos Abiertos de Colombia. El objetivo es realizar una segmentación por medio del algoritmo kmeans con las variables de los puntajes de cada asignatura evaluada en dicha prueba.
# Nombre de las variables del archivo
cols_df <- colnames(df_saber11)
# Columnas asociadas al puntaje de cada asignatura
pos_cols_interes <- grep('PUNT_', cols_df)
pos_cols_interes <- pos_cols_interes[-length(pos_cols_interes)]
cols_interes <- cols_df[pos_cols_interes]
df_saber11_punt <- df_saber11[, cols_interes]
df_saber11_punt <- na.omit(df_saber11_punt)
dim(df_saber11_punt)
# [1] 546193 5
str(df_saber11_punt)
# 'data.frame': 546193 obs. of 5 variables:
# $ PUNT_LECTURA_CRITICA : int 47 60 66 62 63 49 76 57 62 68 ...
# $ PUNT_MATEMATICAS : int 48 65 57 54 57 29 70 65 62 66 ...
# $ PUNT_C_NATURALES : int 37 54 41 61 55 41 70 63 66 63 ...
# $ PUNT_SOCIALES_CIUDADANAS: int 30 59 74 73 57 41 68 66 39 77 ...
# $ PUNT_INGLES : int 54 63 64 53 52 35 72 60 63 51 ...
# - attr(*, "na.action")= 'omit' Named int 51653 57476 138613 161574 195781 224530 238162 321946 329068 329341 ...
# ..- attr(*, "names")= chr "51653" "57476" "138613" "161574" ...
Una tarea que es potencialmente parelelizable y que puede tomar mucho tiempo dependiendo de la cantidad de datos con la que contamos, es encontrar la cantidad óptima de grupos en nuestro dataset. A continuación se aplica el método del codo y se evalúa hasta 20 posibles grupos.
1. Programación serial
wss <- c()
kvalues <- 2:20 # Valores de k a evaluar
start_time <- Sys.time()
for (k in kvalues) {
set.seed(123)
wss[k] <- kmeans(df_saber11_punt, k)$tot.withinss
}
finish_time <- Sys.time()
#Tiempo de procesamiento
finish_time - start_time
# Time difference of 51.77258 secs
2. Programación en paralelo
Lo primero es instalar el paquete parallel (en caso de no tenerlo instalado) y luego cargarlo.
#install.packages('parallel')
library(parallel)
A continuación, la función detectCores() permite detectar la cantidad de núcleos lógicos en la máquina. Generalmente se utiliza la mitad, para que el sitema operativo y el resto de tareas de nuestra máquina sigan funcionando normal.
# Cantidad de nucleos
n.cores <- round(detectCores() / 2)
n.cores
# [1] 3
La función makeCluster() sirve para crear el cluster, donde se especifica la cantidad de hilos que vamos a utilizar (aquí también se podría especificar las ip de otras máquinas).
# Creacion del cluster
cl <- makeCluster(n.cores)
Como se mencionaba antes, en cada hilo se crea una ambiente limpio de R, por lo que se deben enviar todos los objetos necesarios para ejecutar la tarea.
clusterExport(cl, c('df_saber11_punt', 'kvalues'))
Ahora bien, las funciones parApply(), parSapply() y parLapply() son similares a las conocidas de la base de R, en este caso estas se encargan de todo el trabajo pesado en la parelización.
Finalmente, stopCluster() debe ser ejecutado para cerrar todos los ambientes de R creados en los hilos, si esto no se hace, nuestros procedimientos posteriores pueden presentar problemas.
start_time <- Sys.time()
# busqueda del mejor valor de k
wss <- parSapply(cl = cl, # Cluster
kvalues, # vector a recorrer
function(x){
set.seed(123)
wss_k <- kmeans(df_saber11_punt, x)$tot.withinss
return(wss_k)
})
finish_time <- Sys.time()
# No olvidar Cerrar el cluster!!
stopCluster(cl)
#Tiempo de procesamiento
finish_time - start_time
# Time difference of 27.51692 secs
Se puede observar que la versión paralela presenta un mayor rendimiento. Imaginemos en análisis o tareas más grandes el provecho que le podemos sacar a esta herramienta!
Existen más funcionalidades muy interesantes en esta librería, por ejemplo clusterCall() ejecuta cualquier función con los mismos argumentos sobre cada uno de los núcleos, es útil si queremos invocar una librería en cada uno de los ambientes. Todas las funcionalidades pueden ser consultadas en Parallel (2013).
Ahora bien, el gráfico obtenido a partir de los anteriores datos se ve de la siguiente manera.
A manera de complementar el ejercicio, se selecciona k = 4 pues a partir de éste el cambio presentando en el wss es relativamente pequeño. A continuación se observan los centroides de cada cluster y el centroide global de la base de datos con el fin de comparar descriptivamente el comportamiento de los grupos respecto al comportamiento general de los individuos.
# [1] "Promedios por cluster:"
# PUNT_LECTURA_CRITICA PUNT_MATEMATICAS PUNT_C_NATURALES
# 1 39.82321 36.63493 36.29366
# 2 49.72916 47.92814 44.99045
# 3 58.02529 56.93176 53.79691
# 4 66.67767 67.51740 64.53919
# PUNT_SOCIALES_CIUDADANAS PUNT_INGLES
# 1 33.26395 35.59991
# 2 41.71047 44.65743
# 3 53.01323 53.94880
# 4 64.69498 67.39263
# [1] "Promedio general"
# PUNT_LECTURA_CRITICA PUNT_MATEMATICAS PUNT_C_NATURALES
# 52.15787 50.60696 48.23525
# PUNT_SOCIALES_CIUDADANAS PUNT_INGLES
# 46.22492 48.41689
Por lo general los ciclos (for) en R llevan a que nuestro código sea más lento, así que en la medida de lo posible, si necesitamos repetir un procedimiento de grano fino muchas veces, usar funciones como replicate(), apply(), sapply(), lapply(), entre otras, y trabajar vectorialmente, mejoran significativamente la velocidad de nuestra tarea.
El procesamiento paralelizado genera un beneficio muy grande en la cotidianidad de un cientifico de datos, sin embargo se deben tener en cuenta las recomendaciones presentadas en el presente documento, pues no en todos los procedimientos genera tal beneficio.
Finalmente, la libreria parallel realiza por nosotros todo el trabajo pesado en lo que se refiere a procesamiento paralelizado, además es muy escalable si queremos incluso crear un cluster de computadoras.
Por supuesto también queda el reto de profundizar en el dataset utilizado, y encontrar diferentes insights que nos permitan aportar al desarrollo educativo de Colombia.
Muchas gracias por llegar hasta el final de este tutorial, quedan algunas referencias sobre esta interesante temática en caso que quieras profundizar
Barney, Blaise, and others. 2010. “Introduction to Parallel Computing.” Lawrence Livermore National Laboratory 6 (13): 10.
Matloff, Norman. 2015. Parallel Computing for Data Science: With Examples in R, C++ and Cuda. Vol. 28. CRC Press.
Parallel, Team R Core. 2013. “Package Parallel.” Requires R Version 2 (0).