Travaux pratiques - SVM linéaires

(la nouvelle version de cette séance, utilisant l’API DataFrame)

Références externes utiles :

Les exemples ci-dessous reprennent, en partie, ceux des documentations en ligne.

Récupération des fichiers nécessaires

Ouvrez une fenêtre terminal et récupérez les fichiers imports.scala (qui regroupe toutes les commandes import) et linSvmData.txt (qui contient les données dans un format adapté) :

[cloudera@quickstart ~]$ mkdir -p tpsvm/data
[cloudera@quickstart ~]$ export PATH="$PATH:/usr/lib/spark/bin"
[cloudera@quickstart ~]$ cd tpsvm
[cloudera@quickstart tpsvm]$ wget http://cedric.cnam.fr/~crucianm/src/imports.scala
[cloudera@quickstart tpsvm]$ cd data
[cloudera@quickstart data]$ wget http://cedric.cnam.fr/~crucianm/src/linSvmData.txt

SVM linéaires : utilisation des données d’origine

Positionnez-vous dans le répertoire /home/cloudera/tpsvm (pour cela, entrez dans une fenêtre terminal cd /home/cloudera/tpsvm) et lancez spark-shell. Ensuite entrez

scala> :load imports.scala

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 faire l’apprentissage, la prédiction et l’évaluation, entrez dans spark-shell :

// Lire les données en format LabeledPoint (regardez ce format dans la documentation)
scala> val donnees = MLUtils.loadLabeledPoints(sc, "file:///home/cloudera/tpsvm/data/linSvmData.txt")
scala> donnees.take(2)  // visualisez (le début des) 2 premières lignes du RDD

// Partitionner les données en apprentissage (50%) et test (50%)
scala> val partitions = donnees.randomSplit(Array(0.5, 0.5))
scala> val apprentissage = partitions(0).cache()  // conservé en mémoire
scala> val test = partitions(1)

// Faire l'apprentissage pour obtenir le modèle
scala> val numIterations = 100
scala> val modele = SVMWithSGD.train(apprentissage, numIterations)

// Supprimer le seuillage pour pouvoir obtenir les courbes ROC
scala> modele.clearThreshold()

// Faire les prévisions sur les données de test
scala> val predictionsEtEtiquettes = test.map(point => {val prediction = modele.predict(point.features);
                                                        (prediction, point.label)})

// Calculer l'aire sous la courbe ROC (Receiver Operating Characteristic)
scala> val metrics = new BinaryClassificationMetrics(predictionsEtEtiquettes)
scala> val aireSousROC = metrics.areaUnderROC()

Question :

Comment intérpréter ce résultat ?

Question :

Réduisez la partition d’apprentissage afin d’obtenir une aire nettement inférieure à 1 sous la courbe ROC.

A partir de la version 1.3.0 de Spark, le modèle (la liste des vecteurs de support, leurs pondérations et le seuil de la fonction de décision) peut être enregistré après apprentissage (attention, un bug de la version 1.3.0 de Spark ne permet pas d’enregistrement dans le système de fichiers local, avec file://, mais seulement dans HDFS, avec hdfs://) de la façon suivante :

scala> modele.save(sc, "file:///home/cloudera/tpsvm/modele.svm")

et relu lorsqu’il est nécessaire de faire d’autres prédictions en se servant de ce modele :

scala> val modeleSVM = SVMModel.load(sc, "file:///home/cloudera/tpsvm/modele.svm")

Question :

Quelle est, par défaut, la valeur du paramètre de régularisation utilisé ? Que pondère ce paramètre dans Spark ?

Par défaut, l’optimisation nécessaire pour trouver les paramètres du modèle SVM (les vecteurs de support, leurs pondérations et le seuil de la fonction de décision) est faite par une descente de gradient stochastique (SGD) avec une régularisation L2. Il est possible de modifier le paramètre de régularisation en configurant directement l’optimiseur :

scala> import org.apache.spark.mllib.optimization.GradientDescent

scala> val svmAlgo = new SVMWithSGD()
scala> svmAlgo.optimizer.setNumIterations(numIterations).setRegParam(0.2)

// Utiliser dans ce cas svmAlgo.run plutôt que SVMWithSGD.train :
scala> val modele = svmAlgo.run(apprentissage)

Enfin, il est possible d’employer une régularisation L1 qui privilégie les solutions « creuses », en configurant directement l’optimiseur (il n’est pas nécessaire de tester immédiatement cela) :

scala> import org.apache.spark.mllib.optimization.L1Updater

scala> val svmAlgo = new SVMWithSGD()
scala> svmAlgo.optimizer.setNumIterations(200).setRegParam(0.1).setUpdater(new L1Updater)
scala> val modeleL1 = svmAlgo.run(apprentissage)

Un autre algorithme d’optimisation, Limited-memory Broyden–Fletcher–Goldfarb–Shanno (L-BFGS), plus efficace que SGD, a été implémenté dans MLlib pour d’autres méthodes (par exemple pour la régression logistique).

Apprentissage et prévision après réduction de la dimension des données

Nous cherchons à savoir 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 réduirons la dimension des données d’abord à 100 et ensuite à 2.

Pour appliquer l’ACP avec une réduction à 100 de la dimension des données, il faut tenir compte du format utilisé pour les données d’apprentissage et de test, RDD[LabeledPoint] (regardez-le dans la documentation). Il est possible de copier et coller dans spark-shell des instructions multi-lignes en entrant d’abord :paste. Entrez dans spark-shell :

// Créer la matrice de données
scala> val matLignes = new RowMatrix(donnees.map(point => point.features))
scala> matLignes.numCols()     // afficher le nombre de dimensions des données initiales
scala> matLignes.rows.take(2)    // visualiser 2 lignes de cette RowMatrix

// Réaliser l'ACP avec k = 100, représenter la matrice de projection sous forme de DenseMatrix
scala> val k = 100    // 100 premières composantes principales
scala> val matCompPrincipales = matLignes.computePrincipalComponents(k)
scala> val denseMatCompPrincipales = new DenseMatrix(matLignes.numCols().toInt,k,matCompPrincipales.toArray)

// Obtenir une représentation sous forme de RDD[LabeledPoint] pour les données projetées
scala> val donneesk = donnees.map(point => {
           val pm = Matrices.dense(1,matLignes.numCols()
                            .toInt,point.features.toArray);
           LabeledPoint(point.label,Vectors.dense(pm.multiply(denseMatCompPrincipales).values))
       })

Expliquez les opérations réalisées dans cette dernière ligne.

Il est maintenant possible de refaire l’apprentissage et la prédiction sur cette nouvelle représentation des données (de dimension réduite). Pour cela, entrez dans spark-shell :

// Partitionner les données en apprentissage (50%) et test (50%)
scala> val partitions = donneesk.randomSplit(Array(0.5, 0.5))
scala> val apprentissage = partitions(0).cache()  // conservé en mémoire
scala> val test = partitions(1)

// Faire l'apprentissage pour obtenir le modèle
scala> val numIterations = 100
scala> val modele = SVMWithSGD.train(apprentissage, numIterations)

// Supprimer le seuillage pour pouvoir obtenir les courbes ROC
scala> modele.clearThreshold()

// Faire les prévisions sur les données de test
scala> val predictionsEtEtiquettes = test.map(point => {
           val prediction = modele.predict(point.features);
           (prediction, point.label)
       })

// Calculer l'aire sous la courbe ROC (Receiver Operating Characteristic)
scala> val metrics = new BinaryClassificationMetrics(predictionsEtEtiquettes)
scala> val aireSousROC = metrics.areaUnderROC()

Question :

Comment intérpréter ce résultat par rapport au résultat précédent ?

Réduisons la dimension des données à 2 et examinons les résultats. Pour cela, il faut reprendre les opérations de l’ACP, avec k = 2 :

// Réaliser l'ACP avec k = 2, représenter la matrice de projection sous forme de DenseMatrix
scala> val k = 2    // 2 premières composantes principales
scala> val matCompPrincipales = matLignes.computePrincipalComponents(k)
scala> val denseMatCompPrincipales = new DenseMatrix(matLignes.numCols().toInt,k,matCompPrincipales.toArray)

// Obtenir une représentation sous forme de RDD[LabeledPoint] pour les données projetées
scala> val donneesk = donnees.map(point => {
           val pm = Matrices.dense(1,matLignes.numCols().toInt,point.features.toArray);
           LabeledPoint(point.label,Vectors.dense(pm.multiply(denseMatCompPrincipales).values))
       })

Ensuite, il faut refaire l’apprentissage et la prédiction sur cette nouvelle représentation des données (de dimension très réduite) en reprenant la séquence d’opérations indiquées plus haut.

Question :

Comment intérpréter ce nouveau résultat par rapport aux résultats précédents ?

Question :

Visualisez avec gnuplot (commande plot et non splot, car ce sont des données 2D) les données 2D de test (issues de la dernière réduction), avec leurs étiquettes de classe et/ou les prévisions faites par SVM.