La dernière fois que j’ai abordé ce sujet, je vous ai présenté une technique vraiment simple pour changer vos appels lapply en leur équivalents parallèles mclapply. Mais bien que ce soit une modification extrêmement simple à implémenter et qui donne d’excellent gains en performance, celle-ci nécessitait toutefois que votre code fasse déjà usage de lapply. Alors explorons une autre techinque simple pour introduire du traitement parallèle dans votre code source existant à l’aide des librairies foreach et doMC.

La librairie foreach est ici essentielle parce qu’elle implémente un type de boucle qui fait abstraction de toute forme de compteur, la rendant ainsi utilisable dans un contexte d’éxecution en parallèle. Et, histoire de faire plaisir à tous les fans du langage Python, foreach vous donnera quelque peu l’impression de travailler avec les list comprehensions 🙂

Explorons donc un bout de code utilisant la librairie RandomForest puisqu’elle nous permettra de toucher certains aspects clés de la boucle foreach.

Tel qu’observé dans le manuel de référence de randomForest à propos du paramètre ntree:

ntree: Nombre d’arbres à faire croître.
Ce nombre ne devrait pas être trop petit afin d’assurer que chaque
ligne d’entrée sera prédite au moins quelques fois

Bien entendu, attribuer une valeur importante à ntree résultera irrémédiablement en un temps de traitement plus long… Mais avec l’aide de foreach et doMC, nous serons en mesure de répartir ce traitement sur plusieurs CPUs.

Ce bout de code:

library("randomForest")

... # génération de votre ensemble d'entrainement

rf <- randomForest(x=x, y=y, ntree=1000)

Devient:

library("randomForest")
library(foreach)
library(doMC)
registerDoMC(4)

... # génération des votre ensemble d'entrainement

rf <-  foreach(ntree=rep(250, 4), .combine=combine, .packages='randomForest') %dopar% {
    randomForest(x=x, y=y, ntree=ntree)
}

Voici les différences clés à observer dans le seconde version:

  • Nous initialisons les librairies foreach et doMC
  • Nous précisons le nombre de CPUs disponible à doMC # N’oubliez surtout pas cette étape sinon l’exécution se fera de manière séquentielle!
  • Nous spécifions que la librairie randomForest doit être initialisée dans les contextes d’exécution parallèles à l’aide de l’option .packages de foreach
  • Nous fusionnons les résultats des différents traitements parallèles en passant la méthode combine de randomForest à l’option .combine de foreach.

Plutôt simple, non ? Et maintenant nous sommes assurés de pouvoir faire croitre un nombre d’arbre suffisant à une saine exploration de notre jeu de données tout en demeurant dans un délai computationnel raisonnable !

La fusion des résultats des traitement individuels de foreach, bien que non nécessaire per se puisque par défaut foreach retournera une liste de résultats, peut se faire à l’aide de bon nombre de fonctions R communes telles que c (concat), cbind/rbind ou encore à l’aide d’une fonction sur mesure que nous aurons préalablement définie.

Si vous n’êtes toujours pas convaincus, laissez moi ajouter que la librairie doMC peut aisément être remplacée par une autre implémentation do* en fonction du type de distribution des tâches qui vous intéresse. Par exemple: de très légères modifications au code ci-haut permettraient à doMPI ou encore doSNOW de distribuer vos tâches sur une grappe de calcul. Pas mal !

Comme lecture de chevet, vous pourriez vous pencher sur l’imbrication des boucles foreach et/ou l’usage de l’opérateur when afin de prévenir l’évaluation d’un sous ensemble de votre jeu de données… Cet argument rappelera certainement le if des list comprehensions de Python.

Alors allez-y, épargner du temps réel d’exécution en distribuant vos tâches adéquatement et profitez du temps sauvé pour explorer votre jeu de données plus à fond !