{"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 - SVM lin\u00e9aires\n\n([la version pr\u00e9c\u00e9dente de cette s\u00e9ance, utilisant l\u2019API RDD](tpSVMlineairesRDD.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\u2019emploi des SVM lin\u00e9aires dans Spark, y compris la d\u00e9finition de *pipeline* et la recherche de valeurs pour les hyperparam\u00e8tres (*grid search*) en utilisant la validation crois\u00e9e. La m\u00eame m\u00e9thodologie doit \u00eatre employ\u00e9e pour la construction de tout mod\u00e8le d\u00e9cisionnel avec Spark.\n\nLes exemples ci-dessous reprennent, en partie, ceux des documentations en ligne.", "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\u2019emploi des SVM lin\u00e9aires dans Spark, y compris la d\u00e9finition de pipeline et la recherche de valeurs pour les hyperparam\u00e8tres (grid search) en utilisant la validation crois\u00e9e. La m\u00eame m\u00e9thodologie doit \u00eatre employ\u00e9e pour la construction de tout mod\u00e8le d\u00e9cisionnel avec Spark.
\n\nLes exemples ci-dessous reprennent, en partie, ceux des documentations en ligne.
\nNous travaillerons dans cette s\u00e9ance de travaux pratiques sur un d\u00e9coupage apprentissage / test particulier d\u2019une partie des donn\u00e9es Adult de la base UCI. Les donn\u00e9es ont \u00e9t\u00e9 employ\u00e9es pour pr\u00e9dire si le revenu annuel d\u2019un individu d\u00e9passe 50 k\\$ (en 1996). Regardez ce site pour une description des variables pr\u00e9sentes. Les donn\u00e9es que vous r\u00e9cup\u00e9rez ne correspondent pas directement aux 14 variables indiqu\u00e9es dans la description car un pr\u00e9-traitement a \u00e9t\u00e9 effectu\u00e9. Il est d\u00e9crit sur la page LIBSVM Tools : \u00ab les six variables continues sont discr\u00e9tis\u00e9es en quantiles, chaque quantile \u00e9tant ensuite repr\u00e9sent\u00e9 par une variable binaire. Une variable cat\u00e9gorielle \u00e0 m modalit\u00e9s est convertie en m caract\u00e9ristiques binaires \u00bb. Cependant, l\u2019ordre des variables initiales et le nombre de quantiles pour chaque variable initiale n\u2019est pas pr\u00e9cis\u00e9.
\n\nCommen\u00e7ons par t\u00e9l\u00e9charger le fichier contenant les donn\u00e9es d\u00e9crites ci-dessus :
\n```scala\nimport sys.process._
\n\n// Cr\u00e9ation d'un dossier pour ce TP\n\"mkdir -p tpsvm/data\" !
\n\n// T\u00e9l\u00e9chargement des donn\u00e9es\n\"wget -nc http://cedric.cnam.fr/vertigo/Cours/RCP216/docs/a7 -P tpsvm/data/\" !\n```
\nSpark MLlib propose une impl\u00e9mentation des machines \u00e0 vecteurs de support (Support Vector Machines, SVM) avec noyau lin\u00e9aire (adapt\u00e9 aux donn\u00e9es lin\u00e9airement s\u00e9parables), voir la documentation en ligne.
\n\nPour lire les donn\u00e9es et les s\u00e9parer en ensemble d\u2019apprentissage et ensemble de test, dans spark-shell
ou Jupyter, entrez :
scala\n// Lire les donn\u00e9es dans le format appropri\u00e9\nval donnees = spark.read.format(\"libsvm\").load(\"tpsvm/data/a7\")\ndonnees.show(2, false) // visualiser les 2 premi\u00e8res lignes du DataFrame\n
Pour tester la capacit\u00e9 de notre mod\u00e8le pr\u00e9dictif \u00e0 g\u00e9n\u00e9raliser sur des donn\u00e9es non-vues, nous divisons notre jeu de donn\u00e9es en deux: une partie sera utilis\u00e9e pour l\u2019apprentissage, l\u2019autre partie sera conserv\u00e9e pour l\u2019\u00e9valuation.
\nscala\n// Partitionner les donn\u00e9es en apprentissage (50%) et test (50%)\nval partitions = donnees.randomSplit(Array(0.5, 0.5))\nval apprentissage = partitions(0).cache() // conserv\u00e9 en m\u00e9moire\nval test = partitions(1)\n
En g\u00e9n\u00e9ral, diff\u00e9rents traitements pr\u00e9alables sont appliqu\u00e9s aux donn\u00e9es avant la mod\u00e9lisation d\u00e9cisionnelle. Ces traitements peuvent \u00eatre regroup\u00e9s dans un pipeline, comme nous l\u2019avons expliqu\u00e9 et ensuite illustr\u00e9 dans cet exemple.
\n\nNous construisons ici encore un pipeline qui dans un premier temps se r\u00e9duit \u00e0 la SVM et, dans cette section optionnelle inclut en plus une r\u00e9duction de dimension par ACP. L\u2019utilisation de :paste
dans spark-shell
peut vous faciliter les choses pour la suite du TP.
```scala\nimport org.apache.spark.ml.classification.LinearSVC\nimport org.apache.spark.ml.{Pipeline, PipelineModel}
\n\n// D\u00e9finition de l'estimateur SVC lin\u00e9aire\nval linSvc = new LinearSVC().setMaxIter(10)\n .setFeaturesCol(\"features\")\n .setLabelCol(\"label\")
\n\n// D\u00e9finition du pipeline (r\u00e9duit ici au SVM lin\u00e9aire)\nval pipeline = new Pipeline().setStages(Array(linSvc))\n```
\nIl est utile de regarder la documentation de l\u2019API Spark en scala pour les SVM lin\u00e9aires. Un des param\u00e8tres de LinearSVC
est setStandardization()
avec True
par d\u00e9faut, donc le centrage et la r\u00e9duction des variables d\u00e9crivant les observations sont r\u00e9alis\u00e9s par d\u00e9faut. Aussi, vous pouvez noter la pr\u00e9sence d\u2019un param\u00e8tre setWeightCol()
qui permet de pond\u00e9rer des observations.
Un mod\u00e8le d\u00e9cisionnel obtenu comme r\u00e9sultat d\u2019une proc\u00e9dure de mod\u00e9lisation d\u00e9pend des donn\u00e9es (observations) d\u2019apprentissage, mais \u00e9galement de la famille param\u00e9trique dans laquelle il est recherch\u00e9 et des valeurs d\u2019un ou plusieurs hyperparam\u00e8tres (comme par ex. la constante de r\u00e9gularisation pour une SVM). Afin d\u2019obtenir le \u00ab meilleur \u00bb mod\u00e8le (pour un ensemble de donn\u00e9es et dans une famille param\u00e9trique fix\u00e9e, comme les SVM lin\u00e9aires) il est n\u00e9cessaire d\u2019explorer l\u2019espace des valeurs des hyperparam\u00e8tres. Cette exploration fait en g\u00e9n\u00e9ral appel \u00e0 une grille multidimensionnelle (une dimension par hyperparam\u00e8tre) et porte dans ce cas le nom grid-search. Chaque point de la grille correspond \u00e0 une combinaison particuli\u00e8re de valeurs pour les diff\u00e9rents hyperparam\u00e8tres. Chaque combinaison sera caract\u00e9ris\u00e9e par le score de validation crois\u00e9e obtenu pour les mod\u00e8les qui poss\u00e8dent ces valeurs des hyperparam\u00e8tres (avec la validation crois\u00e9e k-fold, chacun de ces mod\u00e8les apprend sur k-1 parties des donn\u00e9es et est \u00e9valu\u00e9 sur la k-\u00e8me). Il est utile de revoir cette section du cours. Les explications de la mise en oeuvre dans Spark se trouvent ici.
\n\nParmi les diff\u00e9rentes combinaisons de la grille sera retenue celle qui pr\u00e9sente le meilleur score de validation crois\u00e9e. Ensuite, un nouveau mod\u00e8le est appris sur la totalit\u00e9 des donn\u00e9es d\u2019apprentissage, en utilisant pour les hyperparam\u00e8tres la meilleure combinaison trouv\u00e9e par grid-search. Les performances de g\u00e9n\u00e9ralisation de ce mod\u00e8le peuvent \u00eatre estim\u00e9es sur des donn\u00e9es de test mises de c\u00f4t\u00e9 au d\u00e9part.
\n\nNous d\u00e9finissons d\u2019abord une grille de recherche pour les hyperparam\u00e8tres en nous servant de ParamGridBuilder()
. Pour chaque hyperparam\u00e8tre explor\u00e9 il est n\u00e9cessaire d\u2019indiquer avec .addGrid()
les valeurs \u00e0 inclure dans la grille.
Est ensuite d\u00e9finie l\u2019instance de CrossValidator()
qui est employ\u00e9e pour exlorer les combinaisons de valeurs de la grille. Il est n\u00e9cessaire d\u2019indiquer \u00e0 quel estimateur l\u2019appliquer (ici c\u2019est au pipeline qui contient seulement une SVM lin\u00e9aire), avec quelles valeurs pour les (hyper)param\u00e8tres (ici la grille paramGrid
), quel type de validation crois\u00e9e employer (ici k-fold avec k = 5) et comment \u00e9valuer les r\u00e9sultats (ici avec une instance de BinaryClassificationEvaluator
qui utilise par d\u00e9faut la m\u00e9trique areaUnderROC
(AUC), l\u2019aire sous la courbe ROC).
```scala\nimport org.apache.spark.ml.tuning.{CrossValidator, ParamGridBuilder}\nimport org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
\n\n// Construction de la grille de (hyper)param\u00e8tres utilis\u00e9e pour grid search\n// Une composante est ajout\u00e9e avec .addGrid() pour chaque (hyper)param\u00e8tre \u00e0 explorer\nval paramGrid = new ParamGridBuilder()\n .addGrid(linSvc.regParam,\n Array(0.02, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5))\n .build()
\n\n// D\u00e9finition de l'instance de CrossValidator : \u00e0 quel estimateur l'appliquer,\n// avec quels (hyper)param\u00e8tres, combien de folds, comment \u00e9valuer les r\u00e9sultats\nval cv = new CrossValidator().setEstimator(pipeline)\n .setEstimatorParamMaps(paramGrid)\n .setNumFolds(5)\n .setEvaluator(new BinaryClassificationEvaluator())
\n\n// Construction et \u00e9valuation par validation crois\u00e9e des mod\u00e8les correspondant\n// \u00e0 toutes les combinaisons de valeurs de (hyper)param\u00e8tres de paramGrid\nval cvSVCmodel = cv.fit(apprentissage)\n```
\nUne fois l\u2019entra\u00eenement termin\u00e9, il est possible de r\u00e9cup\u00e9rer les valeurs d\u2019aire sous la courbe ROC pour tous les mod\u00e8les :
\nscala\n// Afficher les valeurs AUC obtenues pour les combinaisons de la grille\ncvSVCmodel.avgMetrics\n
Et \u00e9galement d\u2019afficher la meilleure combinaison d\u2019hyperparam\u00e8tres :
\nscala\n// Afficher les meilleures valeurs pour les hyperparam\u00e8tres\ncvSVCmodel.getEstimatorParamMaps.zip(cvSVCmodel.avgMetrics).maxBy(_._2)._1\n
Le meilleur mod\u00e8le peut \u00eatre sauvegard\u00e9 pour \u00eatre r\u00e9utilis\u00e9 par la suite :
\nscala\n// Enregistrement du meilleur mod\u00e8le\ncvSVCmodel.write.save(\"cvSVCmodel\")\n
L\u2019affichage avec .avgMetrics
des valeurs AUC obtenues pour toutes les combinaisons de la grille est fait dans l\u2019ordre dans lequel la grille des hyperparam\u00e8tres a \u00e9t\u00e9 d\u00e9clar\u00e9e. Il est possible de voir la valeur la plus \u00e9lev\u00e9e, qui correspond \u00e0 la meilleure combinaison de valeurs pour les hyperparam\u00e8tres.
La description du meilleur mod\u00e8le obtenu et enregistr\u00e9 avec .write.save()
est visible dans cvSVCmodel/bestModel/stages/0_linearsvc_.../metadata/part-00000
(remplacer les ...
par la s\u00e9quence de caract\u00e8re qui compl\u00e8te le nom du r\u00e9pertoire sur votre ordinateur). Il est utile de lire cette description, regarder notamment la valeur de \"regParam\"
.
Le meilleur mod\u00e8le (obtenu avec la meilleure combinaison de valeurs pour les hyperparam\u00e8tres apr\u00e8s un nouvel apprentissage sur la totalit\u00e9 des donn\u00e9es d\u2019apprentissage) est accessible dans cvSVCmodel
. Nous pouvons \u00e9valuer ses performances sur les donn\u00e9es d\u2019apprentissage et sur les donn\u00e9es de test. Pour cela, il est d\u2019abord n\u00e9cessaire de s\u2019en servir avec .transform()
pour obtenir les pr\u00e9dictions de ce mod\u00e8le sur les deux ensembles de donn\u00e9es.
```scala\n// Calculer les pr\u00e9dictions sur les donn\u00e9es d'apprentissage\nval resApp = cvSVCmodel.transform(apprentissage)
\n\n// Calculer les pr\u00e9dictions sur les donn\u00e9es de test\nval resultats = cvSVCmodel.transform(test)
\n\n// Afficher le sch\u00e9ma du DataFrame resultats\nresultats.printSchema()
\n\n// Afficher 10 lignes compl\u00e8tes de r\u00e9sultats (sans la colonne features)\nresultats.select(\"label\", \"rawPrediction\", \"prediction\").show(10, false)\n```
\nPour r\u00e9aliser l\u2019\u00e9valuation des mod\u00e8les, il faut passer par l\u2019objet Evaluator
accessible via getEvaluator
. Cet objet expose simplement une m\u00e9thode evaluate
:
```scala\n// Calculer AUC sur donn\u00e9es d'apprentissage\nprintln(cvSVCmodel.getEvaluator.evaluate(resApp))
\n\n// Calculer AUC sur donn\u00e9es de test\nprintln(cvSVCmodel.getEvaluator.evaluate(resultats))\n```
\nLa valeur de AUC obtenue sur les donn\u00e9es d\u2019apprentissage avec cvSVCmodel.getEvaluator.evaluate(resApp)
est-elle la m\u00eame que la valeur la plus \u00e9lev\u00e9e de la liste issue de .avgMetrics
? Pourquoi ?
R\u00e9duisez la partition d\u2019apprentissage par rapport \u00e0 celle de test et examinez l\u2019impact sur l\u2019aire sous la courbe ROC du meilleur mod\u00e8le sur les donn\u00e9es de test.
\nCette section est optionnelle.
\n\nNous cherchons \u00e0 d\u00e9terminer si une r\u00e9duction de la dimension des donn\u00e9es (quelle est la dimension des donn\u00e9es d\u2019origine ?) obtenue par une analyse en composantes principales (ACP, voir la s\u00e9ance de travaux pratiques sur l\u2019ACP) a un impact sur les r\u00e9sultats. Pour cela nous pouvons inclure l\u2019ACP, pr\u00e9c\u00e9d\u00e9e du centrage et de la r\u00e9duction des donn\u00e9es, dans le pipeline.
\n```scala\nimport org.apache.spark.ml.feature.StandardScaler\nimport org.apache.spark.ml.feature.PCA
\n\napprentissage.printSchema()
\n\n// D\u00e9finition du scaler pour centrer et r\u00e9duire les variables initiales\nval scaler = new StandardScaler().setInputCol(\"features\")\n .setOutputCol(\"scaledFeatures\")\n .setWithStd(true)\n .setWithMean(true)
\n\n// Construction et application d'une nouvelle instance d'estimateur PCA\nval pca = new PCA().setInputCol(\"scaledFeatures\").setOutputCol(\"pcaFeatures\")
\n\n// D\u00e9finition de l'estimateur SVC lin\u00e9aire\nval linSvc = new LinearSVC().setMaxIter(10)\n .setFeaturesCol(\"pcaFeatures\")\n .setLabelCol(\"label\")
\n\n// D\u00e9finition du pipeline (r\u00e9duit ici au SVM lin\u00e9aire)\nval pipelineA = new Pipeline().setStages(Array(scaler, pca, linSvc))\n```
\n\u00c0 quelles op\u00e9rations math\u00e9matiques correspondent les objets scaler
, pca
et linSvc
?
La grille doit \u00eatre modifi\u00e9e afin d\u2019inclure des valeurs pour le nombre de composantes principales \u00e0 retenir :
\n```scala\nval paramGridA = new ParamGridBuilder()\n .addGrid(pca.k, Array(100, 20, 2))\n .addGrid(linSvc.regParam,\n Array(0.05, 0.1, 0.3, 0.5))\n .build()
\n\nval cvA = new CrossValidator().setEstimator(pipelineA)\n .setEstimatorParamMaps(paramGridA)\n .setNumFolds(5)\n .setEvaluator(new BinaryClassificationEvaluator())\n```
\nNous avons r\u00e9duit ici le nombre de valeurs diff\u00e9rentes \u00e0 explorer pour chaque hyperparam\u00e8tre afin d\u2019acc\u00e9l\u00e9rer les calculs durant la s\u00e9ance de TP.
\n\nIl est maintenant possible de refaire la recherche des meilleures valeurs pour les hyperparam\u00e8tres et d\u2019\u00e9valuer le mod\u00e8le obtenu :
\n```scala\nval cvSVCmodelA = cvA.fit(apprentissage)
\n\ncvSVCmodelA.avgMetrics\n```
\nscala\ncvSVCmodelA.getEstimatorParamMaps.zip(cvSVCmodelA.avgMetrics).maxBy(_._2)._1\n
scala\n// Sauvegarde du meilleur mod\u00e8le\ncvSVCmodel.write.save(\"cvSVCmodelA\")\n
```scala\n// Calcul des r\u00e9sultats de test\nval resultatsA = cvSVCmodelA.transform(test)
\n\n// \u00c9valution des m\u00e9triques de test\ncvSVCmodelA.getEvaluator.evaluate(resultatsA)\n```
\nCombien de mod\u00e8les sont appris au total durant cette proc\u00e9dure ?
\nQuel est ici l\u2019impact de la r\u00e9duction de la dimension des donn\u00e9es avec une ACP ?
\n