.. _chap-tpSVMlineaires: ################################# Travaux pratiques - SVM linéaires ################################# (`la version précédente de cette séance, utilisant l'API RDD `_) .. only:: html .. container:: notebook .. image:: _static/zeppelin_classic_logo.png :class: svg-inline `Cahier Zeppelin `_ Références externes utiles : * `Documentation Spark `_ * `Documentation API Spark en Scala `_ * `Documentation Scala `_ **L'objectif** de cette séance de TP est de présenter l'emploi des SVM linéaires dans Spark, y compris la définition de *pipeline* et la recherche de valeurs pour les hyperparamètres (*grid search*) en utilisant la validation croisée. La même méthodologie doit être employée pour la construction de tout modèle décisionnel avec Spark. Les exemples ci-dessous reprennent, en partie, ceux des documentations en ligne. Récupération des fichiers nécessaires ====================================== Nous travaillerons dans cette séance de travaux pratiques sur un découpage apprentissage / test particulier d'une partie des données `Adult de la base UCI `_. Les données ont été employées pour prédire si le revenu annuel d'un individu dépasse 50 k$ (en 1996). Regardez ce site pour une description des variables présentes. Les données que vous récupérez ne correspondent pas directement aux 14 variables indiquées dans la description car un pré-traitement a été effectué. Il est décrit sur la page `LIBSVM Tools `_ : « les six variables continues sont discrétisées en quantiles, chaque quantile étant ensuite représenté par une variable binaire. Une variable catégorielle à *m* modalités est convertie en *m* caractéristiques binaires ». Cependant, l'ordre des variables initiales et le nombre de quantiles pour chaque variable initiale n'est pas précisé. Ouvrez une fenêtre terminal et récupérez les données dans un format adapté : .. code-block:: bash %%bash mkdir -p tpsvm/data cd tpsvm/data wget -nc http://cedric.cnam.fr/vertigo/Cours/RCP216/docs/a7.zip unzip a7.zip SVM linéaires : apprentissage et évaluation =========================================== .. Positionnez-vous dans le répertoire ``~/tpsvm`` (pour cela, entrez dans une fenêtre terminal ``cd ~/tpsvm`` si ce répertoire est dans votre répertoire ``$HOME``) et lancez ``spark-shell``. Si la commande ``spark-shell`` n'est pas trouvée, entrez d'abord ``export PATH="$PATH:/opt/spark/bin"`` (si vous êtes sur un ordinateur de la salle de travaux pratiques) et ensuite ``spark-shell``. Spark MLlib propose une implémentation des machines à vecteurs de support (*Support Vector Machines*, SVM) avec noyau linéaire (adapté aux données linéairement séparables), voir `la documentation en ligne `_. Pour lire les données et les séparer en ensemble d'apprentissage et ensemble de test, dans ``spark-shell`` ou Zeppelin, entrez : .. code-block:: scala // Lire les données dans le format approprié val donnees = spark.read.format("libsvm").load("tpsvm/data/a7") donnees.show(2, false) // visualiser les 2 premières lignes du DataFrame // Partitionner les données en apprentissage (50%) et test (50%) val partitions = donnees.randomSplit(Array(0.5, 0.5)) val apprentissage = partitions(0).cache() // conservé en mémoire val test = partitions(1) Construction de *pipeline* -------------------------- En général, différents traitements préalables sont appliqués aux données avant la modélisation décisionnelle. Ces traitements peuvent être regroupés dans un *pipeline*, `comme nous l'avons expliqué `_ et ensuite `illustré dans cet exemple `_. Nous construisons ici encore un *pipeline* qui dans un premier temps se réduit à la SVM et, dans `cette section optionnelle `_ inclut en plus une réduction de dimension par ACP. L'utilisation de ``:paste`` dans ``spark-shell`` peut vous faciliter les choses pour la suite du TP. .. code-block:: scala import org.apache.spark.ml.classification.LinearSVC import org.apache.spark.ml.{Pipeline, PipelineModel} // Définition de l'estimateur SVC linéaire val linSvc = new LinearSVC().setMaxIter(10) .setFeaturesCol("features") .setLabelCol("label") // Définition du pipeline (réduit ici au SVM linéaire) val pipeline = new Pipeline().setStages(Array(linSvc)) Il est utile de regarder `la documentation de l'API Spark en scala pour les SVM linéaires `_. Un des paramètres de ``LinearSVC`` est ``setStandardization()`` avec ``True`` par défaut, donc le centrage et la réduction des variables décrivant les observations sont réalisés par défaut. Aussi, vous pouvez noter la présence d'un paramètre ``setWeightCol()`` qui permet de **pondérer** des observations. Recherche des valeurs optimales des hyperparamètres --------------------------------------------------- Un modèle décisionnel obtenu comme résultat d'une procédure de modélisation dépend des données (observations) d'apprentissage, mais également de la famille paramétrique dans laquelle il est recherché et des valeurs d'un ou plusieurs hyperparamètres (comme par ex. la constante de régularisation pour une SVM). Afin d'obtenir le « meilleur » modèle (pour un ensemble de données et dans une famille paramétrique fixée, comme les SVM linéaires) il est nécessaire d'explorer l'espace des valeurs des hyperparamètres. Cette exploration fait en général appel à une grille multidimensionnelle (une dimension par hyperparamètre) et porte dans ce cas le nom *grid-search*. Chaque point de la grille correspond à une combinaison particulière de valeurs pour les différents hyperparamètres. Chaque combinaison sera caractérisée par le score de validation croisée obtenu pour les modèles qui possèdent ces valeurs des hyperparamètres (avec la validation croisée *k-fold*, chacun de ces modèles apprend sur *k-1* parties des données et est évalué sur la *k*-ème). Il est utile de revoir `cette section du cours `_. Les explications de la mise en oeuvre dans Spark `se trouvent ici `_. Parmi les différentes combinaisons de la grille sera retenue celle qui présente le meilleur score de validation croisée. Ensuite, un nouveau modèle est appris sur la totalité des données d'apprentissage, en utilisant pour les hyperparamètres la meilleure combinaison trouvée par *grid-search*. Les performances de généralisation de ce modèle peuvent être estimées sur des données de test mises de côté au départ. Nous définissons d'abord une grille de recherche pour les hyperparamètres en nous servant de ``ParamGridBuilder()``. Pour chaque hyperparamètre exploré il est nécessaire d'indiquer avec ``.addGrid()`` les valeurs à inclure dans la grille. Est ensuite définie l'instance de ``CrossValidator()`` qui est employée pour exlorer les combinaisons de valeurs de la grille. Il est nécessaire d'indiquer à quel estimateur l'appliquer (ici c'est au *pipeline* qui contient seulement une SVM linéaire), avec quelles valeurs pour les (hyper)paramètres (ici la grille ``paramGrid``), quel type de validation croisée employer (ici *k-fold* avec *k* = 5) et comment évaluer les résultats (ici avec une instance de ``BinaryClassificationEvaluator`` qui utilise par défaut la métrique ``areaUnderROC`` (AUC), `l'aire sous la courbe ROC `_). .. code-block:: scala import org.apache.spark.ml.tuning.{CrossValidator, ParamGridBuilder} import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator // Construction de la grille de (hyper)paramètres utilisée pour grid search // Une composante est ajoutée avec .addGrid() pour chaque (hyper)paramètre à explorer val paramGrid = new ParamGridBuilder() .addGrid(linSvc.regParam, Array(0.02, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5)) .build() // Définition de l'instance de CrossValidator : à quel estimateur l'appliquer, // avec quels (hyper)paramètres, combien de folds, comment évaluer les résultats val cv = new CrossValidator().setEstimator(pipeline) .setEstimatorParamMaps(paramGrid) .setNumFolds(5) .setEvaluator(new BinaryClassificationEvaluator()) // Construction et évaluation par validation croisée des modèles correspondant // à toutes les combinaisons de valeurs de (hyper)paramètres de paramGrid val cvSVCmodel = cv.fit(apprentissage) Une fois l'entraînement terminé, il est possible de récupérer les valeurs d'aire sous la courbe ROC pour tous les modèles : .. code-block:: scala // Afficher les valeurs AUC obtenues pour les combinaisons de la grille cvSVCmodel.avgMetrics Et également d'afficher la meilleure combinaison d'hyperparamètres : .. code-block:: scala // Afficher les meilleures valeurs pour les hyperparamètres cvSVCmodel.getEstimatorParamMaps.zip(cvSVCmodel.avgMetrics).maxBy(_._2)._1 Le meilleur modèle peut être sauvegardé pour être réutilisé par la suite : .. code-block:: scala // Enregistrement du meilleur modèle cvSVCmodel.write.save("cvSVCmodel") L'affichage avec ``.avgMetrics`` des valeurs AUC obtenues pour toutes les combinaisons de la grille est fait dans l'ordre dans lequel la grille des hyperparamètres a été déclarée. Il est possible de voir la valeur la plus élevée, qui correspond à la meilleure combinaison de valeurs pour les hyperparamètres. La description du meilleur modèle obtenu et enregistré avec ``.write.save()`` est visible dans ``cvSVCmodel/bestModel/stages/0_linearsvc_.../metadata/part-00000`` (remplacer les ``...`` par la séquence de caractère qui complète le nom du répertoire sur votre ordinateur). Il est utile de lire cette description, regarder notamment la valeur de ``"regParam"``. Évaluation du modèle obtenu avec les valeurs optimales des hyper-paramètres --------------------------------------------------------------------------- Le meilleur modèle (obtenu avec la meilleure combinaison de valeurs pour les hyperparamètres après un nouvel apprentissage sur la totalité des données d'apprentissage) est accessible dans ``cvSVCmodel``. Nous pouvons évaluer ses performances sur les données d'apprentissage et sur les données de test. Pour cela, il est d'abord nécessaire de s'en servir avec ``.transform()`` pour obtenir les prédictions de ce modèle sur les deux ensembles de données. .. code-block:: scala // Calculer les prédictions sur les données d'apprentissage val resApp = cvSVCmodel.transform(apprentissage) // Calculer les prédictions sur les données de test val resultats = cvSVCmodel.transform(test) // Afficher le schéma du DataFrame resultats resultats.printSchema() // Afficher 10 lignes complètes de résultats (sans la colonne features) resultats.select("label", "rawPrediction", "prediction").show(10, false) Pour réaliser l'évaluation des modèles, il faut passer par l'objet ``Evaluator`` accessible via ``getEvaluator``. Cet objet expose simplement une méthode ``evaluate`` : .. code-block:: scala // Calculer AUC sur données d'apprentissage println(cvSVCmodel.getEvaluator.evaluate(resApp)) // Calculer AUC sur données de test println(cvSVCmodel.getEvaluator.evaluate(resultats)) .. comment est effectivement réalisée l'évaluation de AUC à partir de la colonne rawPrediction ? .. admonition:: Question : La valeur de AUC obtenue sur les données d'apprentissage avec ``cvSVCmodel.getEvaluator.evaluate(resApp)`` est-elle la même que la valeur la plus élevée de la liste issue de ``.avgMetrics`` ? Pourquoi ? .. ifconfig:: tpscala in ('public') .. admonition:: Correction : La liste issue de ``.avgMetrics`` indique les valeurs AUC obtenues pour toutes les combinaisons de la grille. Pour chaque point de la grille, il s'agit d'une moyenne entre les valeurs obtenues par les *k* modèles (validation croisée *k-fold*) sur leurs fractions de test. La valeur de AUC obtenue sur les données d'apprentissage avec ``cvSVCmodel.getEvaluator.evaluate(resApp)`` est celle du meilleur modèle sur la totalité des données d'apprentissage. Elle sera donc en général différente de la valeur la plus élevée de la liste issue de ``.avgMetrics``. .. admonition:: Question : Réduisez la partition d'apprentissage par rapport à celle de test et examinez l'impact sur l'aire sous la courbe ROC du meilleur modèle sur les données de test. .. ifconfig:: tpscala in ('public') .. admonition:: Correction : Il est nécessaire de reprendre l'expérience précédente avec (bien) moins de 50% de données pour l'apprentissage et le reste pour le test. Il faut modifier ``val partitions = donnees.randomSplit(Array(0.5, 0.5))`` avec par ex. ``Array(0.2, 0.8)`` ou même ``Array(0.05, 0.95)``. Apprentissage et prédiction après réduction de la dimension des données ======================================================================= Cette section est optionnelle. Nous cherchons à déterminer si une réduction de la dimension des données (quelle est la dimension des données d'origine ?) obtenue par une analyse en composantes principales (ACP, voir la `séance de travaux pratiques sur l'ACP `_) a un impact sur les résultats. Pour cela nous pouvons inclure l'ACP, précédée du centrage et de la réduction des données, dans le *pipeline*. .. code-block:: scala import org.apache.spark.ml.feature.StandardScaler import org.apache.spark.ml.feature.PCA apprentissage.printSchema() // Définition du scaler pour centrer et réduire les variables initiales val scaler = new StandardScaler().setInputCol("features") .setOutputCol("scaledFeatures") .setWithStd(true) .setWithMean(true) // Construction et application d'une nouvelle instance d'estimateur PCA val pca = new PCA().setInputCol("scaledFeatures").setOutputCol("pcaFeatures") // Définition de l'estimateur SVC linéaire val linSvc = new LinearSVC().setMaxIter(10) .setFeaturesCol("pcaFeatures") .setLabelCol("label") // Définition du pipeline (réduit ici au SVM linéaire) val pipelineA = new Pipeline().setStages(Array(scaler, pca, linSvc)) .. admonition:: Question À quelles opérations mathématiques correspondent les objets ``scaler``, ``pca`` et ``linSvc`` ? .. ifconfig:: tpscala in ('public') .. admonition:: Correction ``scaler`` réalise l'opération de normalisation des données (centrage et réduction), ``pca`` réalise une analyse en composantes principales (réduction de dimension) et ``linSvc`` est une machine à vecteurs de support comme vu ci-dessus. La grille doit être modifiée afin d'inclure des valeurs pour le nombre de composantes principales à retenir : .. code-block:: scala val paramGridA = new ParamGridBuilder() .addGrid(pca.k, Array(100, 20, 2)) .addGrid(linSvc.regParam, Array(0.05, 0.1, 0.3, 0.5)) .build() val cvA = new CrossValidator().setEstimator(pipelineA) .setEstimatorParamMaps(paramGridA) .setNumFolds(5) .setEvaluator(new BinaryClassificationEvaluator()) Nous avons réduit ici le nombre de valeurs différentes à explorer pour chaque hyperparamètre afin d'accélérer les calculs durant la séance de TP. Il est maintenant possible de refaire la recherche des meilleures valeurs pour les hyperparamètres et d'évaluer le modèle obtenu : .. code-block:: scala val cvSVCmodelA = cvA.fit(apprentissage) cvSVCmodelA.avgMetrics .. code-block:: scala cvSVCmodelA.getEstimatorParamMaps.zip(cvSVCmodelA.avgMetrics).maxBy(_._2)._1 .. code-block:: scala // Sauvegarde du meilleur modèle cvSVCmodel.write.save("cvSVCmodelA") .. code-block:: scala // Calcul des résultats de test val resultatsA = cvSVCmodelA.transform(test) // Évalution des métriques de test cvSVCmodelA.getEvaluator.evaluate(resultatsA) .. admonition:: Question : Combien de modèles sont appris au total durant cette procédure ? .. ifconfig:: tpscala in ('public') .. admonition:: Correction : La grille contient deux hyperparamètres, le nombre de composantes principales à retenir lors de l'ACP (avec 3 valeurs différentes) et la constante de régularisation de la SVM (avec 4 valeurs différentes). Le nombre de points de la grille est donc de 3 x 4 = 12. Pour chaque point de la grille, la validation croisée *5-fold* construit et évalue 5 modèles. Au total sont donc appris (et évalués) 12 x 5 = 60 modèles. .. admonition:: Question : Quel est ici l'impact de la réduction de la dimension des données avec une ACP ? .. ifconfig:: tpscala in ('public') .. admonition:: Correction : En examinant les résultats obtenus avec ``cvSVCmodelA.avgMetrics`` on constate que l'impact de la réduction de dimension par ACP est négatif dans ce cas, mais reste limité même pour une réduction drastique de 125 à 2.