Publié par et
Il y a 2 années · 14 minutes · Data

Les outils de la Data Science : Spark MLlib, théorie et concepts (1/2)

Dans deux précédents articles nous vous présentions R et Python et comment ils sont utilisés en Data Science. La limite de ces langages est cependant rapidement atteinte lorsque l’on a affaire à de gros jeux de données qui ne tiennent plus en mémoire. Dans ce cas là, la solution à envisager est de distribuer les calculs sur un cluster de plusieurs machines, avec l’idée fondatrice de porter l’algorithme vers les données et non l’inverse.

La librairie Mahout permettait de faire cela sur un cluster Hadoop en utilisant MapReduce. Cependant, les limites de cette librairie ont vite été atteintes. Par nature, la plupart des algorithmes de Machine Learning sont itératifs: plusieurs passages sur les données sont nécessaires, générant ainsi de nombreux jobs MapReduce et beaucoup de lecture/écriture sur disque. Avec l’apparition de Spark, et de sa librairie de Machine Learning associée MLlib, ces contraintes se sont envolées, améliorant drastiquement les performances obtenues.

Cet article a pour but de présenter et de comprendre MLlib sous un angle plus théorique, et sera suivi d’un second article le présentant sous un aspect plus pratique avec une utilisation des principaux algorithmes de Machine Learning. Nous allons donc voir ici les spécificités de MLlib, à savoir son fonctionnement global, le type de données qu’elle requiert, ainsi que le mode de construction des algorithmes. Toutes ces notions sont importantes pour utiliser MLlib de manière optimale.

Petit rappel de Machine Learning

Pour employer de manière efficace MLlib, il est bien entendu nécessaire d’avoir quelques bases en Machine Learning. Le Machine Learning est une branche de l’Intelligence Artificielle qui permet l’analyse et la construction d’algorithmes capables d’apprendre à partir de données d’entrée. On peut distinguer deux catégories principales d’algorithmes: de type supervisé ou non supervisé.

En apprentissage supervisé, on dispose d’un dataset composé de caractéristiques (features) associées à des labels (target). L’objectif est de construire un estimateur capable de prédire le label d’un objet à partir de ses features. L’algorithme apprend alors à partir de données dont on connait le label et est ensuite capable de faire de la prédiction sur de nouvelles données dont on ne connaît pas le label. On distingue les algorithmes de classification, pour lesquels le label à prédire est une classe (prédire un mail comme étant spam / non spam), de ceux de régression, pour lequels il faut prédire une variable continue (prédire la taille d’une personne en fonction de son poids et de son âge par exemple). En apprentissage non-supervisé, on ne dispose pas de label pour nos données. L’objectif est alors de trouver des similarités entre les objets observés, pour les regrouper au sein de clusters.

On peut de plus citer les algorithmes dédiés aux systèmes de recommandation (collaborative filtering), ainsi que l’apprentissage par renforcement, qui regroupe un ensemble d’algorithmes qui vont faire leurs prédictions en apprenant de leurs erreurs au fur et à mesure, et s’adapteront aux éventuels changements.

Rapide tour d’horizon de Spark

Spark est un framework d’analyse de données né il y a un peu plus de 5 ans à l’AMPLab de l’UC Berkeley. Il est désormais géré par Databricks, entreprise fondée par les développeurs à l’origine du projet. Il est devenu un projet de la fondation Apache en juin 2013 et a obtenu le label “Apache Top-Level Project” en février 2014. Il réunit aujourd’hui plus de 200 contributeurs venant de plus de 50 entreprises telles que Yahoo ! ou Intel. Spark s’est appuyé sur le framework Hadoop, déjà existant et très utilisé, en utilisant le système de fichiers distribués de ce dernier, HDFS, et le gestionnaire de ressources YARN, permettant d’exécuter des programmes Spark sur Hadoop. Cependant, à la différence d’Hadoop, Spark ne se limite pas au paradigme MapReduce et promet des performances jusqu’à 100 fois plus rapide. L’origine de ces performances : la montée en mémoire. Là où Hadoop lit les données sur des disques durs, Spark peut les monter en mémoire et gagner ainsi énormément en rapidité.

Les API et les projets attenants

Spark possède 3 API : en Scala, Python et Java. Pour les deux premiers langages il propose une interface en ligne de commande qui permet une exploration rapide et interactive des données. La version 1.4 de Spark prévue pour Juin 2015 inclura en plus une API R. Plusieurs projets se greffent au dessus de Spark : Spark SQL qui permet d’exécuter des requêtes SQL sur des RDD (Résilient Distributed Datasets) et contient l’API des DataFrames (collection de données organisée en colonnes, très utilisé en Data Science), Spark Streaming pour l’analyse de données en temps réel, GraphX pour l’exécution d’algorithmes de graphes et donc MLlib, la librairie de machine learning.

Les Resilient Distributed Datasets

Le Resilient Distributed Dataset (RDD) est un concept créé par les fondateurs de Spark. C’est sous ce format que sont gérées les données en Spark. Les RDD sont des collections immutables. Par défaut, lors de la lecture d’un fichier, les données sont manipulées sous forme d’un RDD de String où chaque élément correspond à un ligne du fichier. Il est ensuite possible de d’effectuer des opérations sur le RDD. Il en existe deux sortes :

  • Les transformations : elles transforment un RDD en un autre RDD (map, filter, reduceByKey)
  • Les actions : elles transforment un RDD en une valeur (count, collect …)

Il est important de noter que les transformations sont “lazy”, c’est-à-dire que Spark n’exécutera les calculs demandés que si une action est appliquée à un RDD.

rdd.png/>

 

NB: Dans cette présentation, nous allons principalement présenter MLlib via l’API Scala, qui est l’API de base. Cependant, les utilisateurs des autres APIs pourront facilement s’y retrouver car l’utilisation de la librairie est relativement semblable pour tous les langages. Nous considérons de plus que l’utilisateur possède déjà une connaissance minimale de Spark et des RDDs, l’objectif étant de les utiliser dans MLlib.

MLlib: Une librairie optimisée pour le calcul parallélisé

MLlib est la librairie de Machine Learning de Spark. Tous les algorithmes de cette librairie sont conçus de manière à être optimisés pour le calcul en parallèle sur un cluster. Une des conséquences directes à cela est que, pour de petits datasets qui tiennent en mémoire, un algorithme lancé depuis Spark en local sur votre machine mettra beaucoup plus de temps à s’exécuter que le même algorithme lancé depuis Python ou R, qui sont optimisés pour le mode local. En revanche, les performances deviennent extrêmement intéressantes lorsque les volumétries sont très importantes.

MLlib a été conçu pour une utilisation très simple des algorithmes en les appelant sur des RDD dans un format spécifique, quel que soit l’algorithme choisi. L’architecture se rapproche ainsi de ce que l’on trouve dans la librairie scikit-learn de Python, bien qu’il y ait encore des différences notables qui vont être effacées dans les prochaines versions de l’API.

Les algorithmes présents dans MLlib sont, tout comme le reste du framework, développés en Scala, en se basant principalement sur le package d’algèbre linéaire Breeze pour l’implémentation des algorithmes. De plus, pour faire fonctionner MLlib, il est nécessaire d’installer gfortran, ainsi que Numpy si vous utilisez l’API Python.

 Les types de données spécifiques à MLlib

L’une des spécificités de MLlib (et peut-être une de ses faiblesses pour le moment) est qu’il nous contraint à utiliser des RDD aux types spécifiques. Les algorithmes implémentés nécessitent ainsi en entrée des RDD[Vector] (pour des données n’ayant pas de label), des RDD[LabeledPoint] (spécifiques à l’apprentissage supervisé) ou bien des RDD[Rating] (pour les systèmes de recommandation).

Vector

Les Vector sont de simples vecteurs de doubles. MLlib supporte deux types de Vector : dense (chaque entrée doit être spécifiée) et sparse (seules les entrées non nulles, avec leurs positions, doivent être spécifiées).

import org.apache.spark.mllib.linalg.{Vector, Vectors}

// Create a dense vector (1.0, 0.0, 3.0).
val dv: Vector = Vectors.dense(1.0, 0.0, 3.0)
// Create a sparse vector (1.0, 0.0, 3.0) by specifying its indices and values corresponding to nonzero entries.
val sv1: Vector = Vectors.sparse(3, Array(0, 2), Array(1.0, 3.0))

Une remarque importante: Contrairement à ce que son nom peut sembler l’indiquer, l’objet Vector ne donne accès à aucune opération arithmétique quand ils sont utilisés en Scala ou en Java. Il correspond simplement à une représentation particulière de la donnée afin d’uniformiser l’utilisation des algorithmes.

LabeledPoint

Ce type de donnée est spécifique aux algorithmes d’apprentissage supervisé, pour lesquels il est nécessaire de spécifier le label correspondant à chaque vecteur pour la phase d’apprentissage. Un LabeledPoint est composé d’un vecteur, dense ou sparse, associé à un label.

Le label est obligatoirement un double, ce qui permet d’utiliser à la fois des algorithmes de classification ou de régression. Pour la classification, le label doit obligatoirement prendre comme valeurs 0, 1, 2, 3, …, en commençant toujours par 0.

Une fois en présence d’un LabeledPoint, il est possible d’accéder au label correspondant via l’attribut .label, ainsi qu’au Vector via l’attribut .features.

import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.regression.LabeledPoint

// Create a labeled point with a positive label and a dense feature vector.
val pos: LabeledPoint = LabeledPoint(1.0, Vectors.dense(1.0, 0.0, 3.0))

// Create a labeled point with a negative label and a sparse feature vector.
val neg: LabeledPoint = LabeledPoint(0.0, Vectors.sparse(3, Array(0, 2), Array(1.0, 3.0)))
 
// Get the label and the features corresponding to the LabeledPoint
val label: Double = neg.label
val features: Vector = neg.features

Rating

Une donnée de type Rating est exclusivement utilisée dans le cadre du collaborative filtering, qui est un algorithme utilisé classiquement dans les systèmes de recommandation. Un Rating n’est autre qu’un tuple contenant trois éléments :

  • User: Un entier représentant un utilisateur
  • Item: Un entier représentant un item
  • Rating: Un double représentant la note qu’a donné User à Item
import org.apache.spark.mllib.recommendation.Rating
 
// Create a rating corresponding to User 4, which gave to Item 7 a rating of 3
val rating = Rating(4, 7, 3.0) 

 

NB: Dans les prochaines version de Spark, ces contraintes sur les types de données vont être relâchées. Les algorithmes prendront alors en entrée des DataFrames, introduites dans la version 1.3, ce qui les rapprocheront encore plus du fonctionnement rencontré sous Python ou R.

L’architecture des algorithmes

Les algorithmes

Que ce soit pour de la classification, de la régression, du clustering ou autre, tous les algorithmes possèdent leur propre classe (voire même plusieurs selon leur type d’implémentation). La démarche est alors la suivante:

  • Instancier la classe
  • Appeler les setters associés pour modifier les paramètres (qui sont relatifs à l’algorithme en question)
  • Appeler la méthode run() sur un RDD pour entraîner le modèle

Cependant, pour de nombreux algorithmes (souvent les principaux utilisés), il est aussi possible d’utiliser des méthodes statiques au lieu de la classe avec les setters. Il suffit alors d’appeler la méthode train() contenue dans l’Object associé à l’algorithme qui prend comme entrées un RDD, ainsi que tous les paramètres nécessaires. Cette manière de faire est beaucoup plus proche des implémentations classiques en Python ou R, et est donc à prioriser lorsque c’est possible.

Dans tous les cas, une fois que l’algorithme est entraîné, il retourne un objet “Model”.

NB: L’API Java a exactement le même fonctionnement que celle en Scala pour la construction des algorithmes. En Python, on utilise systématiquement la méthode train().

Les classes Model

Chaque algorithme de MLlib, une fois entraîné sur des données, retourne un objet Model, qui va typiquement posséder une méthode predict(). Cette méthode va permettre d’appliquer le modèle à une nouvelle donnée ou un nouveau RDD de données pour prédire une valeur.

Prenons l’exemple de la Régression Logistique, qui est un algorithme de classification binaire qui identifie un hyperplan séparant au mieux les deux classes. L’algorithme va donc prendre en entrée un RDD[LabeledPoint] et retourner un LogisticRegressionModel qui va pouvoir prédire la classe de nouvelles données grâce à sa méthode predict(), comme le montre le schéma ci-dessous.

MLlib_algorithm.jpg

Construire une chaîne de traitement de données en MLlib

En pratique, la démarche globale pour construire une pipeline de traitement de données en MLlib est la suivante:

  • Charger les données à traiter dans un RDD
  • Transformation des données pour obtenir un RDD[Vector] ou un RDD[LabeledPoint] utilisable par un algorithme de MLlib

Cette étape est plus généralement appelée Feature Engineering. Elle regroupe tout le travail de nettoyage, de gestion des outliers et des données manquantes et de création de nouvelles features, puis de transformation au bon format de RDD requis par MLlib. Cette partie est la plus longue et à la fois la plus intéressante de la démarche en Data Science car elle implique une réflexion sur la signification des données et permet, lorsqu’elle est pertinente, d’améliorer grandement les performances des algorithmes. 

  • Sélection et entraînement d’un algorithme à l’aide des méthodes run() ou train() sur le RDD créé
  • Prédictions sur de nouvelles données grâce à la méthode predict() du Model résultant de l’étape précédente

Dans le cas d’application de modèles supervisés (classification ou régression), une étape supplémentaire est fortement recommandée avant l’entraînement de l’algorithme: la séparation du RDD en train et test sets. L’algorithme va alors être entraîné uniquement sur le train set, alors que le test set va être utilisé afin de valider les performances du modèle créé (le test set correspond alors à  des “nouvelles données”, au sens où l’algorithme ne les a pas utilisées pour s’entraîner, dont on connaît la véritable valeur que l’on souhaite prédire). Cela permet notamment de tuner les paramètres pour améliorer les capacités de généralisation de l’algorithme à de nouvelles données.

La figure ci-dessous illustre la démarche complète de création d’une pipeline de traitement de données dans le cas d’un apprentissage supervisé.

MLlib_supervised_pipeline.jpg

Dans un cas non-supervisé (classiquement pour des tâches de clustering), nous n’avons pas de notion de variable à prédire. On utilise donc un RDD de Vector, et l’apprentissage se fait sur toutes les données disponibles, sans l’étape de splitting.

Remarque sur la taille des datasets

Il n’est cependant pas rare que le dataset sur lequel on souhaite entraîner l’algorithme ne soit pas très volumineux. Il peut en effet arriver d’être en possession d’une volumétrie très importante de données brutes qui nécessitent un pré-traitement en Spark (fichiers de logs par exemple), et qu’une fois ce traitement effectué, les données agrégées puissent passer en mémoire. Il est alors recommandé de passer à une librairie optimisée pour le calcul sur un seul noeud.

Il est notamment très facile de jongler de la sorte si vous utilisez l’API Python de Spark, appelée PySpark: Utiliser PySpark pour tous les pré-traitements sur les grosses volumétries jusqu’à obtenir un dataset qui tienne mémoire, puis transformer le RDD en Data Frame Pandas ou en Array Numpy et utiliser scikit-learn. De même, si vous cherchez à faire un grid-search pour tester différents paramètres pour un même algorithme, il peut être intéressant de faire un parallelize() sur la liste de paramètres et d’utiliser une librairie comme scikit-learn sur chaque noeud lorsque la volumétrie le permet. Si cependant la volumétrie après traitement de la donnée reste trop importante, alors MLlib est de loin le plus adapté.

Conclusion

Nous avons maintenant toutes les cartes en main pour appliquer les concepts de MLlib sur des cas concrets. Dans le prochain article, nous présenterons les différents packages présents dans MLlib pour construire des algorithmes de Machine Learning, et donnerons plusieurs exemples pratiques.

Yoann Benoit
Yoann est Data Scientist chez Xebia depuis près de deux ans. Il intervient sur de nombreux sujets autour de la Data Science et du Big Data, allant de la collecte, du traitement et de l'analyse des données jusqu'à la mise en production de pipelines complets de Machine Learning. Speaker et rédacteur à la fois sur les concepts et les technologies liées à la Data Science, il travaille principalement avec Python, Scala et Spark. Il intervient de plus en tant que formateur sur l'Analyse de Données et le Machine Learning sur Spark.
Alban Phelip
Alban est data engineer chez Xebia. Issu d'une formation en statistiques il s'est spécialisé dans l'ingéniérie des données. Ses outils préférés : Spark et R. Speaker et bloggeur il se passionne par tout ce qui touche de près ou de loin au Big Data et à la Data science.

3 réflexions au sujet de « Les outils de la Data Science : Spark MLlib, théorie et concepts (1/2) »

  1. Publié par Ellande, Il y a 2 années

    Très bonne rentrée en matière.
    Vivement le second volet !

  2. Publié par Saad El Fchtali, Il y a 6 mois

    Merci pour cette excellente intro , j’attaque le deuxième volet tout de suite.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *