Travaux pratiques - Analyse de textes avec Spark NLP

Références externes utiles :

Spark NLP est une bibliothèque développée pour Spark par la société John Snow Labs NLP et permettant de réaliser un assez grand nombre d’opérations de traitement automatique pour plusieurs langues. Cette bibliothèque est aujourd’hui bien positionnée par rapport à ses concurrents, voir par exemple ce comparatif. L’objectif de cette séance de TP est de faire une rapide introduction à l’utilisation de Spark NLP.

Exécutez les commandes suivantes pour créer l’environnement nécessaire. D’abord, il faut créer le répertoire de travail et télécharger les données :

import sys.process._

// Récupération des données
"wget -nc http://cedric.cnam.fr/vertigo/Cours/RCP216/docs/texteEn.txt -P tpnlp" !

"wget -nc http://cedric.cnam.fr/vertigo/Cours/RCP216/docs/texteFr.txt -P tpnlp" !

Afin d’utiliser Spark NLP, nous devons importer le module. Comme il s’agit d’une dépendance externe (c’est-à-dire un module qui n’est pas inclus de base dans Spark), nous devons récupérer le .jar depuis les dépôts en ligne. De façon analogue à Vegas dans les TP précédents, nous pouvons importer SparkNLP dans Jupyter via la commande magique %AddDeps fournie par Apache Toree:

%AddDeps com.johnsnowlabs.nlp spark-nlp_2.11 2.7.5 --transitive

Cette commande permet de télécharger automatiquement et d’utiliser par la suite dans Spark la bibliothèque SparkNLP ainsi que toutes ses dépendances (option –transitive). Nous utilisons la version 2.7.5 de la bibliothèque compilée pour Scala 2.11 (qui est la version compatible avec Spark 2.4.x, que nous utilisons pour les TP).

Pour d’autres versions de Spark veuillez regarder la documentation de Spark NLP.

Note

Si vous souhaitez utiliser Spark NLP hors connexion, l’alternative est de créer un .jar local contenant cette bibliothèque et ensuite d’utiliser le .jar. Pour obtenir le .jar il faut entrer les commandes suivantes (étant positionné dans le répertoire tpnlp) si une version 2.4 de Spark est employée :

git clone https://github.com/JohnSnowLabs/spark-nlp
cd spark-nlp
sbt -Dis_spark24=true assembly

Une fois créé le .jar, il faut lancer spark-shell avec

spark-shell --jars spark-nlp/target/scala-2.11/spark-nlp-spark24-assembly-3.3.0.jar

Ou bien dans Jupyter :

%AddDeps file:/chemin/vers/spark-nlp/target/scala-2.11/spark-nlp-spark24-assembly-3.3.0.jar

Traitement d’un fichier texte en anglais

Nous appliquerons d’abord certains traitements au fichier en anglais texteEn.txt (le contenu textuel d’une page Wikipedia). Dans Spark, il faut donc lire ce fichier dans un DataFrame.

// enlever tpnlp/ si spark-shell ou Zeppelin sont lancés dans le répertoire tpnlp
val texteEnTout = spark.read.textFile("tpnlp/texteEn.txt").toDF("text")

Une fois ce travail effectué, nous allons éliminer les lignes de texte qui sont vides :

import org.apache.spark.sql.functions.length

val texteEn = texteEnTout.where(length($"text") > 0)

Le fichier texte a été lu dans un DataFrame qui possède une seule colonne appelée text (les traitements prédéfinis que nous regarderons dans la suite attendent une colonne ayant ce nom comme colonne d’entrée). Chaque paragraphe (terminé par un saut de ligne) est lu dans un item du DataFrame. Avec .where(length($"value") > 0) nous éliminons les items vides (correspondant aux paragraphes vides entre deux sauts de lignes successifs).

Question

À l’aide de Spark, déterminer combien le fichier textEn.txt comporte de lignes non-vides.

Correction

println(texteEn.count)

Spark NLP propose aussi bien des traitements séparés (les annotateurs) que des regroupements prédéfinis de plusieurs traitements (les pipelines). La documentation disponible pourrait sans doute être plus détaillée.

Commençons par importer les modules utiles :

import org.apache.spark.ml.Pipeline
import com.johnsnowlabs.nlp.pretrained.PretrainedPipeline
import com.johnsnowlabs.nlp.Finisher

Nous appliquons d’abord des traitements prédéfinis dans un pipeline Spark NLP, explain_document_ml : découpage en phrases, découpage en tokens (mots), lemmatisation, étiquetage morpho-syntaxique. Les résultats sont ensuite traités par un annotateur (en terminologie Spark NLP) appelé finisher qui rend les résultats plus facilement lisibles par un humain. Pour appliquer le pipeline Spark NLP et ensuite le finisher aux résultats, nous intégrons les deux dans un pipeline Spark :

// Charge le modèle pré-entraîné "explain_document_ml")
val explainPipelineModel = PretrainedPipeline("explain_document_ml").model
// Le Finisher permet d'annoter les résultats de façon lisible par un humain
val finisherExplainEn = new Finisher().setInputCols("token", "lemmas", "pos")
// Création d'un Pipeline Spark qui applique le modèle puis l'annotateur
val pipelineExplainEn = new Pipeline().setStages(Array(explainPipelineModel,finisherExplainEn))

Le traitement est ensuite appliqué aux données et les résultats sont affichés, vous pouvez les examiner :

val modelExplainEn = pipelineExplainEn.fit(texteEn)
val annoteTexteEn = modelExplainEn.transform(texteEn).cache()
annoteTexteEn.show()

Question

Quelles sont les différentes colonnes accessibles dans le DataFrame annoteTexteEn ? À quoi correspondent-elles ?

Afficher les 10 premières lignes pour:

  • la colonne correspondant à la tokenisation;

  • la colonne correspondant à la lemmatisation;

  • la colonne correspondent à l’étiquetage morpho-syntaxique (part-of-speech tagging ou POS).

annoteTexteEn.select("finished_token").show(truncate=false)
annoteTexteEn.select("finished_lemmas").show(false)
annoteTexteEn.select("finished_pos").show(false)

Expérimentation avec l’analyse des sentiments

Spark NLP propose également d’autres pipelines prédéfinis, voir la documentation, dont un qui a pour objectif d’associer à chaque phrase une étiquette indiquant si les sentiments transmis par la phrase sont positifs ou négatifs. Nous faisons ci-dessous un essai de ce pipeline sur des données bien plus courtes, écrites directement dans l’interface de Spark et que vous pouvez modifier à loisir :

val sentimentPipelineModel = PretrainedPipeline("analyze_sentiment").model
val finisherSentiment = new Finisher().setInputCols("document","sentiment")
val pipelineSentiment = new Pipeline().setStages(Array(sentimentPipelineModel,finisherSentiment))

Nous pouvons l’appliquer sur un DataFrame contenant des données de test, pour expérimenter :

val testSentimentData = Seq("The movie is great. But the cinema is quite dirty.").toDF("text")
val modelSentiment = pipelineSentiment.fit(testSentimentData)
val sentimentTestSentimentData = modelSentiment.transform(testSentimentData)
sentimentTestSentimentData.show(false)

Question

À quoi correspondent les valeurs de la colonne finished_sentiment du DataFrame sentimentTestSentimentData ?

Modifier la phrase de test de sorte à n’avoir que des sentiments positifs.

Traitement d’un fichier texte en français

Enfin, examinons maintenant un autre pipeline, explain_document_md, appliquée au document en français texteFr.txt (l’équivalent plus court en français de la page Wikipedia en anglais considérée plus haut). Ce pipeline inclut également l’identification d’entités nommées (Named Entity Recognition, NER, colonne ner) et leur extraction (colonne entities).

Comme précédemment, commençons par charger le texte français dans un DataFrame en éliminant les lignes vides.

val texteFr = spark.read.textFile("tpnlp/texteFr.txt").toDF("text").where(length($"text") > 0)
println(texteFr.count)

Spark NPL propose un modèle d’explication de documents pour la langue française, que nous pouvons charger de façon analogue au PretrainedPipeline utilisé plus haut :

val explainPipelineModel = PretrainedPipeline("explain_document_md","fr").model
val finisherExplainFr = new Finisher().setInputCols("token", "lemma", "pos", "ner", "entities")
val pipelineExplainFr = new Pipeline().setStages(Array(explainPipelineModel,finisherExplainFr))

val modelExplainFr = pipelineExplainFr.fit(texteFr)
val annoteTexteFr = modelExplainFr.transform(texteFr)

annoteTexteFr.show(5, false)

Vous pouvez examiner le résultat des opérations de lemmatisation (qui ont pour objectif de remplacer chaque mot par sa forme « canonique ») et l’identification des entités nommées.

Question

Examinez les entités nommées de la première phrase du document (c’est-à-dire la première ligne du DataFrame). Quelles sont-elles ? De quelles types sont-elles ?

// Afficher la première ligne
texteFr.show(1, false)
// À compléter
// ...
annoteTexteFr.select("finished_ner").show(1, false)
annoteTexteFr.select("finished_entities").show(1, false)

Pour terminer, notons que Spark NLP permet d’afficher directement en Scala la liste des ressources disponibles, et notamment des modèles d’analyse de texte qui sont disponibles. Par exemple, pour afficher tous les Pipelines disponibles pour l’analyse de texte en français (lang=fr) :

import com.johnsnowlabs.nlp.pretrained.ResourceDownloader

ResourceDownloader.showPublicPipelines(lang="fr")

Notez que certains Pipelines ne sont disponibles qu’à partir de la version 3.0 de Spark NLP.

Nous nous servirons de Spark NLP dans une séance de TP suivante afin produire des représentations vectorielles de textes à classer.