A partir des fichiers XML étiquetés avec TreeTagger, nous allons extraire des patrons morphosyntaxiques.
Nous allons également utiliser les fichiers étiquetés avec UDpipe et reformatés en XML pour extraire des relations de dépendance car ce dernier, contrairement au fichier TreeTagger, indique la position du gouverneur et la relation syntaxique qui lie les deux tokens.
Pour chacune des deux extractions, nous allons utiliser quatre méthodes différentes :
- une avec Perl
- une avec Python
- une avec XQuery (via BaxeX)
- une avec des feuilles de styles XSLT.
LANGAGE INFORMATIQUE | METHODE | MODE D'EMPLOI DU LANCEMENT DU SCRIPT DANS LE TERMINAL | TELECHARGEMENT DU SCRIPT ENTIER COMMENTE |
---|---|---|---|
Perl | Expressions régulières | perl bao3_extract_patron.pl fichier-tt.xml PATRON | BAO3_EXTRACT_PATRON.PL |
Python | Lecture globale + expressions régulières | python3 bao3_extract_patron_v3.py fichier-tt.xml PATRON1 -- PATRON2 -- PATRON3 -- ... -- PATRONn |
bao3_extract_patron_v3.py |
Python | Lecture avec un buffer + expressions régulières | python3 bao3_extract_patron_v2.py fichier-tt.xml PATRON1 -- PATRON2 -- PATRON3 -- ... -- PATRONn | bao3_extract_patron_v2.py |
XSLT | Feuille de style | xsltproc feuille_style.xsl nom_corpus.xml > nom_fichier.txt | NOM-PREP-NOM-PRP-TT.xsl |
XQuery | Requête | Utilisation de BaseX | NOM-PRP-NOM-PRP.xq |
LANGAGE INFORMATIQUE | MODE D'EMPLOI DU LANCEMENT DU SCRIPT DANS LE TERMINAL | TELECHARGEMENT DU SCRIPT ENTIER COMMENTE |
---|---|---|
Perl | perl bao3-extract-relation-udpipe.pl fichier-ud.xml nom_relation > nom_fichier_sortie.txt | BAO3-EXTRACT-RELATION-UDPIPE.PL |
Python | python3 bao3_extract_relation_v2.py fichier-ud.xml nom_relation |
bao3_extract_relation_v2.py |
XSLT | xsltproc feuille_style.xsl fichier-ud.xml > nom_fichier.txt | relation-OBJ-UD-xslt.xsl |
XQuery | Utilisation de BaseX | extract-OBJ-udpipe.xq |
On prend en entrée le fichier XML étiqueté par TreeTagger que l'on a sorti dans la BAO 2. On rappelle que la structure de ce dernier ressemble à ça :
<element><data type="type">NAM</data><data type="lemma">Brexit</data><data type="string">Brexit</data></element>
<element><data type="type">PUN</data><data type="lemma">:</data><data type="string">:</data></element>
<element><data type="type">ADV</data><data type="lemma">comment</data><data type="string">comment</data></element>
On lit le texte ligne par ligne. Si à un moment on rencontre un élément dont le contenu de la balise <data type="type"> est celui du premier élément de notre liste de patron, on regarde les lignes qui suivent.
Tant que la POS correspond au reste du patron, on ajoute la forme de l'élément dans notre string "terme".
A la fin du parcours, si la longueur que nous avons parcouru dans la liste des lignes est égale à celle de notre patron, cela signifie que l'on a bien affaire au patron que nous recherchions.
Notre string "terme" devient une clé de notre dictionnaire et sa valeur correspond à son nombre d'occurrences dans le corpus. On vide notre string "terme" pour continuer à chercher nos patrons.
Dans le fichier de sortie, on trie le dictionnaire selon les valeurs et on écrit le nombre d'occurrences du terme suivi d'une tabulation et du terme en question. Ci-dessous se trouve un extrait :
16708 éléments trouvés
480 l’Union européenne
190 Commission européenne
83 élections législatives
71 Banque centrale
66 ministre britannique
65 affaires étrangères
Le nom du fichier de sortie contient le numéro de la rubrique ainsi que le nom du patron extrait.
On prend en entrée notre fichier étiqueté par UDpipe et reformaté en XML et le nom d'une relation. On rappelle que ce fichier est structuré comme suit. Pour la lisibilité, chaque balise a été mise sur une ligne séparée mais dans le fichier de base, on a une balise <item> par ligne.
<item>
<a>1</a>
<a>Brexit</a>
<a>Brexit</a>
<a>NOUN</a>
<a>_</a>
<a>Gender=Masc|Number=Sing</a>
<a>6</a>
<a>nsubj</a>
<a>_</a>
<a>SpacesAfter= </a>
</item>
On crée un dictionnaire dans lequel les clés seront les couples (forme du gouverneur - forme du dépendant) et les valeurs seront le nombre d'occurrences du couple en question.
On change la valeur par défaut de $/ qui contient normalement un retour à la ligne. On le définit par </p>. Grâce à cela, on pourra parcourir le fichier phrase par phrase car nous ne voulons pas relever les patrons qui commencent dans une phrase mais se terminent dans une autre.
Pour chaque phrase, on met la liste des lignes dans @LIGNES. Chaque ligne correspond à l'annotation d'un seul token. Dans la première balise <a> du token, on retrouve un nombre : sa position dans la phrase. Dans la septième balise <a>, on retrouve un autre nombre : la position du gouverneur de ce token dans la phrase. Dans le deuxième <a>, on a la forme du dépendant.
Si le contenu de la huitième balise <a> contient le nom de la relation entrée, on garde le contenu de ces balises dans des variables respectives.
On regarde si la position du gouverneur est inférieur à la position du dépendant. Si oui, cela signifie que le gouverneur se trouve avant le dépendant. Dans ce cas-là, on utilise un indice k qui commencera à 0 et ira jusqu'au nombre de l'indice de i (c'est-à-dire le token que l'on est actuellement en train de regarder). On récupère la forme du gouverneur et on ajoute la forme du gouverneur et du dépendant dans notre dictionnaire et on ajoute 1 à sa valeur pour compter le nombre d'occurrences.
Si la position du gouverneur est supérieur à la position du dépendant, cela signifie que le gouverneur se trouve après le dépendant. On utilise donc un incide k qui ira de i+1 (le token jusqte après celui où on est) jusqu'à la fin de la liste de lignes. Ici aussi on récupère la forme et on met le couple dans le dictionnaire.
Une fois tout le fichier parcourru, on trie notre dictionnaire sur le nombre d'occurrences et on imprime sur le terminal la forme du gouverneur et du dépendant suivis d'une tabulation et du nombre d'occurrences.
Pour avoir les résultats dans un fichier TXT, il suffit de rediriger à l'aide d'un > la sortie du terminal vers un fichier .txt.
Le programme prend environ une seconde pour le traitement d'une seule relation.
Notre programme prend en entrée :
- un fichier XML étiqueté avec TreeTagger
- un ou plusieurs patrons séparés par des " -- "
Le programme prend d'abord l'intégralité des patrons (depuis le deuxième item de la liste d'arguments jusqu'à la fin) et les met dans une liste. Chaque item, y compris les "--" sont donc un élément de la liste. Par exemple pour 3 patrons rentrés (NOM ADJ -- NOM PRP NOM -- ADJ NOM), la liste va ressemble à ça :
["NOM","ADJ","--","NOM","PRP","NOM","--","ADJ","NOM"]
On fait passer cette liste dans une fonction appelée "segmentation_patrons()". Elle va segmenter en sous-liste chaque patron. Pour cela, elle va tout simplement parcourir la liste en entrée et ajouter dans une liste tampon chaque item tant qu'elle ne rencontre pas d'item "--". Quand c'est le cas, ça signifie qu'un nouveau patron commence. Elle ajoute la liste tampon à la liste des listes de patrons et elle vide la liste tampon. La fonction continue ainsi jusqu'à ce que tous les patrons aient été segmentés en sous-listes. Cette liste de listes, si on reprend l'exemple du dessus, va ressembler à ça :
[["NOM","ADJ"],["NOM","PRP","NOM"],["ADJ","NOM"]]
Pour pouvoir nommer les fichiers de résultat, on récupère :
1. le numéro de la rubrique qui se trouve dans le nom du fichier XML
2. le patron.
Pour que le nommage des fichiers soient jolis, on fait passer la liste des listes de patrons dans une fonction appelée "pretty_names()". Elle va renvoyer une liste avec autant d'items qu'il y a de patrons et chaque item aura été formaté comme suit :
["NOM-ADJ","NOM-PRP-NOM","ADJ-NOM"]
Une fois tout ceci effectué, on commence une boucle qui fera autant de tours qu'il y a de patrons. Dans cette boucle, on ouvre un fichier en écriture, qui contiendra dans son nom le numéro de la rubrique, tt (pour TreeTagger) et le nom du patron (qui provient de la liste renvoyée par pretty_names()).
On lance la fonction "extract_patron()" sur le corpus et le patron.
Cette fonction fait tout d'abord une copie du corpus dans "corpus_". En effet, la méthode que nous allons utiliser pour parcourir le fichier va supprimer la première ligne du fichier lorsqu'elle a fini de l'examiner. Cette méthode est pratique car elle permet de ne pas avoir à gérer indépendamment l'indice de l'élément du patron qu'on cherche à matcher et l'indice de la ligne dans laquelle on veut matcher cet élément : le premier élément du patron sera toujours la première ligne du corpus, le deuxième élément toujours la deuxième ligne, etc.
On fait donc une copie pour ne jamais avoir à modifier le corpus original. Si on modifiait le corpus original, le deuxième patron de notre liste de patrons ne pourraient rien matcher car le corpus est vide.
On initialise un booléen.
On initialise une string vide dans laquelle on va mettre la forme de l'élément de la ligne observée si sa POS match avec le patron. Si la POS ne match plus, le booléen deviendra "False" et on sortira de la boucle. Comme ça, si le début et la fin de notre patron match avec le corpus, mais que le milieu du patron ne match pas, on ne va pas le retenir. On ne va, par ailleurs, pas inutilement continuer à parcourir le patron si dès le début, la ligne ne match pas avec le premier élément du patron.
Si on finit de parcourir tout le patron et que le booléen est toujours "True", ça veut dire que tout le patron a matché et on ajoute notre string à notre fichier de résultat. On la vide ensuite pour pouvoir passer à la ligne suivante.
On recommence la même chose pour tous les patrons.
On aura en sortie un fichier TXT par patrons.
Avec la commande time du terminal, on peut savoir que ce programme prend environ 1 minute pour extraire 6 patrons.
La deuxième version du programme en Python prend les mêmes éléments en entrée et les fichiers de sortie sont les mêmes également.
La seule chose qui change est la façon dont on parcourt le fichier. Dans la première version, on lisait le fichier dans sa globalité et on retirait petit à petit les lignes.
Dans la deuxième version, on ne charge pas le fichier en entier. On lit le corpus ligne par ligne. On rajoute dans un buffer de liste de tuples la POS et la forme de l'élément de chaque ligne. Le buffer fait la longueur de notre patron. On regarde ensuite si les POS contenues dans le buffer correspondent à notre patron.
Si c'est le cas, on écrit les formes contenues dans le buffer dans notre fichier. On vide ensuite le buffer et on recommence.
Cette méthode est beaucoup plus rapide que la première. Pour rechercher 6 patrons d'un seul coup, le programme prend environ 4 secondes. On peut savoir ça en utilisant la commande "time" dans le terminal. Alors que le premier prend plus d'une minute.
Le programme en Python pour l'extraction de dépendance prend en entrée un fichier étiqueté avec UDpipe reformaté en XML et le nom d'une relation.
Le programme va utiliser deux buffers :
- un buffer pour la phrase
- un buffer pour les couples (lemmes du dépendant - position du gouverneur dans la phrase)
Il va parcourir notre fichier ligne par ligne. Pour chaque ligne, on va assigner au contenu qui se trouve entre les balises <a>...</a> les noms de variables suivants :
1. idx (pour l'index du mot dans la phrase)
2. word (pour la forme du token)
3. lemma (pour le lemme du token)
4. tag (pour la Part of Speech du token)
5. _ (information qui ne nous intéresse pas)
6. _ (information qui ne nous intéresse pas)
7. position_gouv (pour la position du gouverneur dans la phrase)
8. rel (pour le nom de la relation qu'entretient ce token avec son gouverneur.
fields=re.findall("<a>([^<]+)</a>", line)
idx, word, lemma, tag, _, _, position_gouv, rel, _, _ = fields
A chaque mot, on rentre dans notre buffer de phrase (un dictionnaire) l'index du mot pour clé avec comme valeur son lemme.
sent_buf[idx] = lemma
Si le nom de la relation du mot de la ligne est celui qui nous intéresse, on met dans notre buffer de relation le couple (lemme du dépendant - position du gouverneur dans la phrase).
Dans la variable "couples" qui est un ensemble, on met le lemme du dépendant et le lemme du gouverneur qu'on a pu récupérer grâce à notre buffer de phrase et à sa position conservé dans le buffer de relation.
On écrit tous ces couples dans notre fichier en précisiant la relation.
Ce programme prend environ 1 seconde pour une relation.
XQuery est une langage qui permet de faire des requêtes sur des bases de données XML.
Avant de pouvoir l'utiliser, il faut régler un problème présent dans les fichiers XML de TreeTagger. En effet, les entités nommées "&" sont écrites sans le point-virgule. Parfois, on peut même trouver des "&amp". On écrit un petit script en Python pour pouvoir régler ce problème.
Dans l'exemple à télécharger plus haut (pour le patron NOM PRP NOM PRP), on va effectuer la même instruction pour tous les noeuds "element". Le double slash permet d'indiquer qu'on veut trouver ce noeud peu importe où il se trouve dans l'arboresence. Ca nous évite d'avoir à écrire tout le chemin.
for $element in collection(corpus-annotation-tt-3214_correction_entite)//element
"let" permet d'attribuer une valeur à une variable.
La variable "frere1" est le premier frère suivant de "element", "frere2" le deuxième frère suivant et ainsi de suite pour la longueur de tout le patron.
let $frere:=$element/following-sibling::element[1]
let $frere2:=$element/following-sibling::element[2]
let $frere3:=$element/following-sibling::element[3]
Dans l'arborescence, comme montré plus haut, les noeuds "element" ont plusieurs noeuds-fils "data". Si le premier noeud-fils "data", la POS, de l'élément sélectionné est NOM, que le premier noeud-fils "data" de "frere1" est "PRP" et ainsi de suite, on sélectionne le troisième fils "data", la forme, de chaque élément et on les joint avec un espace.
Pour trouver un autre patron morphosyntaxique, il suffit d'ajuster le nombre de "frère" à la longueur du patron et de remplacer les XX dans les "contains(text(),"XX"))".
Pour chaque noeud <item> qui contient dans son huitième noeud-fils <a> (relation) "obj", son deuxième noeud-fils <a> (forme) est mis dans la variable $depforme. Sa position (son premier noeud-fils <a>) est mis dans $positionSource. Et la position du gouverneur (septième noeud-fils <a>) est mis dans la variable $positionCible.
for $item in collection(corpus-annotation-ud-3214.udpipe)//item
where contains($item/a[8]/text(),'obj')
let $depforme:=$item/a[2]/text()
let $positionSource:=$item/a[1]
let $positionCible:=$item/a[7]
Si le gouverneur se trouve avant le dépendant, alors on cherche le gouverneur dans les frères précédents (preceding-sibling) de l'item. Sinon, dans ses frères suivants (following-sibling). On garde la forme du gouverneur dans la variable $noeudC.
On renvoie la forme du gouverneur suivi d'une flèche et de la forme du dépendant.
Pour les autres relations, il suffit de changer le "obj" de la ligne 2 de la requête par le nom d'une autre relation.
Une feuille de style XSLT est un fichier XML bien formé qui permet de transformer un autre fichier XML au travers de règles.
Dans l'exemple téléchargeable plus haut, on crée un fichier TXT dans lequel se trouve les séquences NOM-PRP-NOM-PRP présentes dans un fichier XML étiqueté par TreeTagger. Ici également on passe d'abord notre fichier XML par le programme de nettoyage des entités.
Comme dans la requête XQuery, on part du noeud "element" et on vérifie si ses frères suivants (following-sibling) correspondent au patron que l'on cherche.
Si oui, on les affiche en précisant bien à l'aide des éléments <xsl:text> que l'on veut un espace entre chaque noeud textuel.
Pour pouvoir lancer la transformation, on utilise le processeur XSLTPROC sur le terminal.
Le principe de la feuille de style XSLT est relativement le même que celui de la requête XQuery.
On paramètre un paramètre nommé "Relation" dans lequel se trouve "obj".
<xsl:param name="Relation">obj</xsl:param>
Pour étudier une autre relation, il suffit de changer ce qui se trouve dans la balise <xsl:param>.
Des quatre méthodes, les programmes en Python semblent être les plus rapides et les plus maléables. Il est très facile de changer la relation de dépendance ou le patron morphosyntaxique que l'on veut étudier, contrairement à XSLT ou XQuery où il faut réécrire la feuille de style/requête. Par ailleurs, l'automatisation du nommage des fichiers de sortie et leur type est beaucoup plus facile à gérer et nous pouvons rechercher plusieurs patrons à la fois. C'est pourquoi c'est la méthode que j'ai conservée pour les résultats suivants, bien qu'au final, les quatre méthodes donnent des résultats très similaires, voire complètement identiques en termes de données.