{"paragraphs": [{"config": {"enabled": true, "editorHide": true}, "text": "%md\n\n", "results": {"code": "SUCCESS", "msg": [{"type": "HTML", "data": "
"}]}}, {"config": {"enabled": true, "editorHide": true}, "text": "%md\n# Travaux pratiques - Classification automatique avec *k-means*\n\n([la version pr\u00e9c\u00e9dente de cette s\u00e9ance, utilisant l\u2019API RDD](tpClassificationAutomatiqueRDD.html))\n\nR\u00e9f\u00e9rences externes utiles :\n\n> - [Documentation Spark](https://spark.apache.org/docs/latest/) \n- [Documentation API Spark en Scala](https://spark.apache.org/docs/latest/api/scala/org/apache/spark/index.html) \n- [Documentation Scala](http://docs.scala-lang.org) \n\n\n\n**L\u2019objectif** de cette s\u00e9ance de TP est de pr\u00e9senter l\u2019utilisation dans Spark de la m\u00e9thode *k-means* de classification automatique (avec une initialisation par *k-means||*), ainsi que la d\u00e9finition et l\u2019utilisation de *pipelines* (encha\u00eenements de traitements successifs) dans Spark.\n\nNous travaillerons lors de cette s\u00e9ance de travaux pratiques d\u2019abord sur des donn\u00e9es g\u00e9n\u00e9r\u00e9es pendant la s\u00e9ance.\nVous aurez \u00e0 traiter ensuite les donn\u00e9es [Spambase Data Set issues de l\u2019archive de l\u2019UCI](https://archive.ics.uci.edu/ml/datasets/Spambase).\nNous vous incitons \u00e0 **regarder sur le site de l\u2019UCI** des explications plus d\u00e9taill\u00e9es concernant ces donn\u00e9es, notamment la **signification des variables** et les classes auxquelles appartiennent les donn\u00e9es.\n\nJupyter\n\n> Nous allons pour cette s\u00e9ance utiliser Vegas-viz comme outil de visualisation. Avec la gestion dynamique des d\u00e9pendances pour Jupyter/Toree, il suffit d\u2019utiliser la [commande magique](https://github.com/apache/incubator-toree/blob/master/etc/examples/notebooks/magic-tutorial.ipynb) `%AddDeps` pour importer les paquets ad\u00e9quats :", "results": {"code": "SUCCESS", "msg": [{"type": "HTML", "data": "(la version pr\u00e9c\u00e9dente de cette s\u00e9ance, utilisant l\u2019API RDD)
\n\nR\u00e9f\u00e9rences externes utiles :
\n\n\n \n\n\n
L\u2019objectif de cette s\u00e9ance de TP est de pr\u00e9senter l\u2019utilisation dans Spark de la m\u00e9thode k-means de classification automatique (avec une initialisation par k-means||), ainsi que la d\u00e9finition et l\u2019utilisation de pipelines (encha\u00eenements de traitements successifs) dans Spark.
\n\nNous travaillerons lors de cette s\u00e9ance de travaux pratiques d\u2019abord sur des donn\u00e9es g\u00e9n\u00e9r\u00e9es pendant la s\u00e9ance.\nVous aurez \u00e0 traiter ensuite les donn\u00e9es Spambase Data Set issues de l\u2019archive de l\u2019UCI.\nNous vous incitons \u00e0 regarder sur le site de l\u2019UCI des explications plus d\u00e9taill\u00e9es concernant ces donn\u00e9es, notamment la signification des variables et les classes auxquelles appartiennent les donn\u00e9es.
\n\nJupyter
\n\n\n\nNous allons pour cette s\u00e9ance utiliser Vegas-viz comme outil de visualisation. Avec la gestion dynamique des d\u00e9pendances pour Jupyter/Toree, il suffit d\u2019utiliser la commande magique
\n%AddDeps
pour importer les paquets ad\u00e9quats :
```scala\n// Vegas v0.3.11 pour Scala 2.11\n// L'option --transitive permet de t\u00e9l\u00e9charger \u00e9galement les d\u00e9pendances de Vegas
\n\n%AddDeps org.vegas-viz vegas2.11 0.3.11 --transitive\n%AddDeps org.vegas-viz vegas-spark2.11 0.3.11 --transitive\n```
\nNous nous servirons de KMeansDataGenerator.generateKMeansRDD
pour g\u00e9n\u00e9rer un jeu de donn\u00e9es synth\u00e9tique sur lequel nous appliquerons le clustering.\nCes donn\u00e9es sont produites d\u2019abord sous la forme d\u2019un RDD qui est ensuite transform\u00e9 en DataFrame.\nCette m\u00e9thode choisit d\u2019abord k centres de groupes \u00e0 partir d\u2019une loi normale $ d $-dimensionnelle, d\u2019\u00e9cart-type $ r $ et ensuite cr\u00e9e autour de chaque centre un groupe \u00e0 partir d\u2019une loi normale $ d $-dimensionnelle d\u2019\u00e9cart-type 1. Les param\u00e8tres d\u2019appel sont :
sc
: le SparkContext
employ\u00e9 (ici, celui par d\u00e9faut de spark-shell
) ; numPoints
: le nombre total de donn\u00e9es g\u00e9n\u00e9r\u00e9es ; k
: le nombre de centres (donc de groupes dans les donn\u00e9es g\u00e9n\u00e9r\u00e9es) ; d
: la dimension des donn\u00e9es ; r
: \u00e9cart-type pour la loi normale qui g\u00e9n\u00e8re les centres ; numPartitions
: nombre de partitions pour le RDD g\u00e9n\u00e9r\u00e9 (2 par d\u00e9faut, ici nous utiliserons 1 seule). (se r\u00e9f\u00e9rer \u00e0 la documentation pour plus de d\u00e9tails)
\n\nNous g\u00e9n\u00e9rerons 1000 donn\u00e9es bidimensionnelles dans 5 groupes en utilisant :
\n```scala\nimport org.apache.spark.sql.Row\nimport org.apache.spark.ml.linalg.Vectors\nimport org.apache.spark.mllib.util.KMeansDataGenerator
\n\n// G\u00e9n\u00e9rer les donn\u00e9es\nval donneesGenerees = KMeansDataGenerator.generateKMeansRDD(sc, 1000, 5, 2, 5, 1)\n .map(l => Row(l(0), l(1), Vectors.dense(l)))\ndonneesGenerees.take(2)\n```
\nLes donn\u00e9es obtenues sont bien des vecteurs \u00e0 deux coordonn\u00e9es.\nNous pouvons maintenant transformer ce RDD en DataFrame contenant une seule colonne features
de type Vector
:
```scala\n// Construction d'un DataFrame \u00e0 partir du RDD\nimport org.apache.spark.sql.types._\nimport org.apache.spark.ml.linalg.SQLDataTypes.VectorType
\n\nval schemaVecteurs = StructType(Seq(StructField(\"x\", DoubleType, true), StructField(\"y\", DoubleType, true), StructField(\"features\", VectorType, true)))\nval vecteursGroupesDF = spark.createDataFrame(donneesGenerees, schemaVecteurs).cache()\nvecteursGroupesDF.show(false)\n```
\nNous pouvons visualiser les points comme dans les TP pr\u00e9c\u00e9dents en s\u2019appuyant sur Vegas :
\n```scala\n// Import des biblioth\u00e8ques de Vegas\nimport vegas._\nimplicit val render = vegas.render.ShowHTML(kernel.display.content(\"text/html\", _))
\n\nimport vegas.data.External._\nimport vegas.sparkExt._
\n\n// Construction du nuage de points\nval points = vecteursGroupesDF\nVegas(\"Donn\u00e9es initiales\").withDataFrame(points).mark(Point).encodeX(\"x\", Quant).encodeY(\"y\", Quant).show\n```
\nNous pouvons v\u00e9rifier que les donn\u00e9es ont bien \u00e9t\u00e9 g\u00e9n\u00e9r\u00e9es al\u00e9atoirement par cinq gaussiennes mod\u00e9r\u00e9ment bien s\u00e9par\u00e9es les unes des autres.
\nLa MLlib de Spark propose une impl\u00e9mentation de l\u2019algorithme de classification automatique k-means.\nL\u2019initialisation des centres est r\u00e9alis\u00e9e par l\u2019algorithme parall\u00e8le k-means||, vu aussi en cours, avec 5 it\u00e9rations (initSteps: 5
, peut \u00eatre modifi\u00e9 avec KMeans.setInitSteps
).
La classification est obtenue en appelant KMeans.fit
. Diff\u00e9rents param\u00e8tres peuvent \u00eatre d\u00e9finis avec les m\u00e9thodes suivantes (\u00e0 appeler pour l\u2019instance de KMeans
cr\u00e9\u00e9e) :
setFeaturesCol(value: String)
: nom de la colonne qui contient les donn\u00e9es \u00e0 classifier, sous forme de vecteurs (par d\u00e9faut \"features\"
) ; setK(value: Int)
: nombre de groupes ou clusters \u00e0 obtenir (par d\u00e9faut 2) ; setMaxIter(value: Int)
: nombre maximal d\u2019it\u00e9rations ; arr\u00eat de l\u2019ex\u00e9cution lorsque les centres sont stabilis\u00e9s (param\u00e8tre tol
, voir setTol
ci-dessous) ou maxIter
est atteint ; setPredictionCol(value: String)
: nom de la colonne qui contient les \u00ab pr\u00e9dictions \u00bb, c\u2019est \u00e0 dire le num\u00e9ro de groupe pour chaque observation (par d\u00e9faut \"prediction\"
) ; setSeed(value: long)
: valeur d\u2019initialisation du g\u00e9n\u00e9rateur de nombres al\u00e9atoires (la pr\u00e9ciser permet de reproduire les r\u00e9sultats ult\u00e9rieurement) ; setTol(value: Double)
: d\u00e9finition de la valeur de la tol\u00e9rance (tol
) pour l\u2019\u00e9valuation de la convergence de l\u2019algorithme ; setInitMode(value: String)
: m\u00e9thode d\u2019initialisation, soit \"random\"
, soit \"k-means||\"
(par d\u00e9faut \"k-means||\"
) ; setInitSteps(value: Int)
: nombre de pas pour l\u2019initialisation par k-means||
(2 par d\u00e9faut). Se r\u00e9f\u00e9rer \u00e0 la documentation pour plus de d\u00e9tails.
\n\nVoici un exemple de code Scala pour Spark permettant de r\u00e9aliser la classification automatique (clustering) avec KMeans. Commen\u00e7ons par appliquer k-means \u00e0 notre DataFrame :
\n```scala\nimport org.apache.spark.ml.clustering.KMeans
\n\n// Appliquer k-means\nval kmeans = new KMeans().setK(5).setMaxIter(200).setSeed(1L)\nval modele = kmeans.fit(vecteursGroupesDF)\n```
\nUne fois ceci fait, nous pouvons calculer l\u2019inertie globale de cette classification et afficher les coordonn\u00e9es des centres des groupes trouv\u00e9s.
\nscala\n// \u00c9valuer la classification par la somme des inerties intra-classe\n// Attention, d\u00e9pr\u00e9ci\u00e9 pour Spark >= 3.0, utiliser plut\u00f4t ClusteringEvaluator()\nval wsse = modele.computeCost(vecteursGroupesDF)\n
scala\n// Afficher les centres des groupes\nmodele.clusterCenters.foreach(println)\n
Pour obtenir les indices des groupes et r\u00e9aliser la classification \u00e0 proprement parler, il suffit d\u2019appliquer la m\u00e9thode transform
au DataFrame souhait\u00e9 :
scala\n// Trouver l'indice de groupe pour chaque donn\u00e9e\nval resultat = modele.transform(vecteursGroupesDF)\nresultat.show(5)\n
Le DataFrame resultat
contient alors une colonne prediction
qui, pour chaque observation du jeu de donn\u00e9es initial, correspond au num\u00e9ro du groupe (cluster) dans lequel l\u2019algorithme des k-moyennes l\u2019a affect\u00e9e.
La visualisation avec Vegas se fait en ajoutant l\u2019option encodeColor
en utilisant la valeur de la colonne prediction
:
scala\nval points = resultat\nVegas(\"K-Means\").withDataFrame(points).mark(Point).encodeX(\"x\", Quant).encodeY(\"y\", Quant).encodeColor(\"prediction\", Nom).show\n
R\u00e9alisez une nouvelle fois la classification des m\u00eames donn\u00e9es avec k-means en 5 groupes mais une valeur diff\u00e9rente dans .setSeed()
. Visualisez \u00e0 nouveau les r\u00e9sultats (mais en gardant les graphes initiaux ouverts pour pouvoir comparer). Que constatez-vous ?
R\u00e9alisez deux fois la classification des m\u00eames donn\u00e9es avec k-means en 5 groupes mais avec une initialisation random
plut\u00f4t que k-means||
et des valeurs diff\u00e9rentes dans .setSeed()
.
Que constatez-vous ?
\n```scala
\n\n```
\nR\u00e9alisez la classification des m\u00eames donn\u00e9es avec k-means et une initialisation k-means||
en 4 groupes et ensuite en 6 groupes. Visualisez \u00e0 chaque fois les r\u00e9sultats. Que constatez-vous ?
```scala
\n\n```
\nMultipliez par 3 les donn\u00e9es sur une des dimensions initiales en vous servant de ElementwiseProduct. R\u00e9alisez la classification des nouvelles donn\u00e9es avec k-means (et initialisation par k-means||
) en 5 groupes. Visualisez les r\u00e9sultats. Que constatez-vous ?
R\u00e9alisez la classification des donn\u00e9es Spambase Data Set issues de l\u2019archive de l\u2019UCI. Comment pr\u00e9-traiter les donn\u00e9es et pourquoi ? Combien de groupes rechercher ? Visualisez ensuite leurs projections sur des groupes de 3 variables.
\n```scala
\n\n```
\nSpark permet de d\u00e9finir des flux de travail (ou workflows, ou ML pipelines) qui encha\u00eenent diff\u00e9rentes \u00e9tapes de traitement des donn\u00e9es se trouvant dans des DataFrames. Les composants des pipelines sont les transformers (transformateurs) et les estimators (estimateurs).
\n\nUn transformer est un algorithme qui transforme un DataFrame en un autre DataFrame, en g\u00e9n\u00e9ral par ajout d\u2019une ou plusieurs colonnes. La transformation peut \u00eatre, par exemple, le centrage et la r\u00e9duction des variables num\u00e9riques (transformer chaque variable pour qu\u2019elle soit de moyenne nulle et \u00e9cart-type \u00e9gal \u00e0 1) qui d\u00e9crivent les observations ou la transformation de textes en vecteurs. Chaque transformer poss\u00e8de une m\u00e9thode .transform()
qui, en g\u00e9n\u00e9ral, cr\u00e9e une ou plusieurs colonnes et les ajoute au DataFrame re\u00e7u en entr\u00e9e.
Un estimator est un algorithme qui permet de construire un mod\u00e8le \u00e0 partir de donn\u00e9es d\u2019un DataFrame. Une fois construit, ce mod\u00e8le sera un transformer. Chaque estimator poss\u00e8de une m\u00e9thode .fit()
. Par exemple, pour centrer et r\u00e9duire les variables num\u00e9riques d\u00e9crivant des observations il est n\u00e9cessaire d\u2019utiliser un estimator qui d\u00e9termine les moyennes et les \u00e9cart-types \u00e0 partir des donn\u00e9es ; ces moyennes et \u00e9cart-types constituent le mod\u00e8le qui, appliqu\u00e9 comme un transformer, permet ensuite d\u2019obtenir le centratge et la r\u00e9duction pour les donn\u00e9es sur lesquelles il a \u00e9t\u00e9 obtenu (avec .fit()
) mais aussi sur d\u2019autres donn\u00e9es (par exemple, de nouvelles observations concernant le m\u00eame probl\u00e8me). Nous avons d\u00e9j\u00e0 vu la construction d\u2019un mod\u00e8le avec .fit()
et son utilisation ult\u00e9rieure avec .transform()
dans le TP pr\u00e9c\u00e9dent lorsque nous avons centr\u00e9 et r\u00e9duit les variables avant application de l\u2019ACP. Dans cette s\u00e9ance de TP nous examinerons la construction d\u2019un mod\u00e8le descriptif par classification automatique, r\u00e9alis\u00e9e par un estimator. Le mod\u00e8le r\u00e9sultant est un transformer qui, appliqu\u00e9 aux donn\u00e9es d\u2019un DataFrame, fournit pour chaque observation le num\u00e9ro du centre de groupe le plus proche (donc le num\u00e9ro du groupe auquel cette observation est \u00ab affect\u00e9e \u00bb).
Les transformers et les estimators emploient une interface commune parameter pour sp\u00e9cifier les param\u00e8tres utilis\u00e9s.
\n\nUn pipeline permet d\u2019encha\u00eener des composants qui, au d\u00e9part, peuvent \u00eatre des estimators et, apr\u00e8s estimation, sont employ\u00e9s comme des transformers. Le pipeline dispose ainsi
\n\n.fit()
qui permet d\u2019encha\u00eener les op\u00e9rations d\u2019estimation pour tous les estimators composant le pipeline, .transform()
qui encha\u00eene les traitements des donn\u00e9es par les diff\u00e9rents mod\u00e8les estim\u00e9s, devenus des transformers. Dans la documentation de Spark, ce processus est illustr\u00e9 dans cet exemple. Nous aborderons un autre exemple de pipeline dans la suite de cette s\u00e9ance.
\nAfin de centrer et r\u00e9duire les donn\u00e9es Spambase Data Set et d\u2019appliquer ensuite la classification automatique, construisons un pipeline qui regroupe ces deux op\u00e9rations :
\n```scala\nimport org.apache.spark.ml.{Pipeline, PipelineModel}\nimport org.apache.spark.ml.feature.StandardScaler\nimport org.apache.spark.ml.clustering.KMeans\nimport org.apache.spark.ml.feature.VectorAssembler
\n\n// Construction de DataFrame avec les donn\u00e9es de Spambase\ndef genSpamFields(from: Int, to: Int) = for (i <- from until to)\n yield StructField(\"val\"+i.toString, DoubleType, true)\nval spamFields = genSpamFields(0, 57)\nval spamSchema = StructType(spamFields).add(\"label\", DoubleType, true)\nval spamDF = spark.read.format(\"csv\").schema(spamSchema).load(\"data/spambase.data\")\nval colsEntree = {for (i <- 0 until 57) yield \"val\"+i.toString}.toArray\nval assembleur = new VectorAssembler().setInputCols(colsEntree).setOutputCol(\"features\")\nval spamDFA = assembleur.transform(spamDF).cache()\n// Partitionnement en donn\u00e9es de train (80%) et donn\u00e9es de test (20%)\nval spamSplits = spamDFA.randomSplit(Array(0.8, 0.2))
\n\n// Construction d'un pipeline\nval scaler = new StandardScaler().setInputCol(\"features\")\n .setOutputCol(\"scaledFeatures\")\n .setWithStd(true)\n .setWithMean(true)\nval kmeansNS = new KMeans().setFeaturesCol(scaler.getOutputCol)\n .setPredictionCol(\"predictionNS\")\n .setK(5)\n .setMaxIter(200)\n .setSeed(1L)\nval pipeline = new Pipeline().setStages(Array(scaler, kmeansNS))\n```
\nL\u2019objet pipeline
contient d\u00e9sormais deux op\u00e9rations : la normalisation (scaler
) et la classification (kmeansNS
). Ces deux op\u00e9rations (stages) seront appliqu\u00e9es successivement sur les donn\u00e9es pass\u00e9es aux m\u00e9thodes fit
et transform
du pipeline. Par exemple, nous pouvons r\u00e9aliser l\u2019apprentissage du mod\u00e8le KMeans avec normalisation de la fa\u00e7on suivante :
scala\n// Estimation des mod\u00e8les du pipeline (sur donn\u00e9es *train*):\n// le premier mod\u00e8le (centrage et r\u00e9duction) est estim\u00e9 en premier,\n// ensuite il est utilis\u00e9 comme transformer pour modifier les donn\u00e9es\n// et c'est le second mod\u00e8le qui est estim\u00e9 (clustering)\n//\nval modeleKMNS = pipeline.fit(spamSplits(0))\n
Et l\u2019ensemble peut s\u2019appliquer en inf\u00e9rence sur de nouvelles donn\u00e9es comme suit :
\nscala\n// Application du mod\u00e8le pour pr\u00e9dire les groupes des donn\u00e9es de test\n//\nval indicesSpamTest = modeleKMNS.transform(spamSplits(1)).select(\"scaledFeatures\",\"predictionNS\")\nindicesSpamTest.show(5)\n
Un pipeline (ou n\u2019importe lequel de ses sous-mod\u00e8les) peut \u00eatre sauvegard\u00e9 pour \u00eatre r\u00e9utilis\u00e9 plus tard :
\n```scala\n// Un pipeline peut \u00eatre sauvegard\u00e9 (avant estimation)\npipeline.write.overwrite().save(\"spark-pipeline-normEtKMeans\")
\n\n// Un mod\u00e8le issu d'un pipeline (apr\u00e8s estimation) peut \u00eatre sauvegard\u00e9\nmodeleKMNS.write.overwrite().save(\"spark-modele-clustering-spam\")
\n\n// Un mod\u00e8le sauvegard\u00e9 peut \u00eatre charg\u00e9 pour \u00eatre employ\u00e9\nval memeModele = PipelineModel.load(\"spark-modele-clustering-spam\")\n```
\nR\u00e9aliser la visualisation avec Vegas de la classification automatique sur le jeu de donn\u00e9es Spambase.\nCette visualisation est faite sur les deux premi\u00e8res dimensions. Quelles que soient les deux dimensions choisies (parmi les 57 de d\u00e9part) pour visualiser les groupes, elles seront probablement peu repr\u00e9sentatives de cette s\u00e9paration en groupes. Quelle m\u00e9thode employer afin d\u2019am\u00e9liorer la visualisation ?
\n