Boîte à Outils : série 2

"Easy things should be easy, and hard things should be possible."

pluriTAL  M1
  • Accueil
  • BaO 1
  • BaO 2
  • BàO 3
  • BaO 4
  • Analyse
  • A Propos

BàO 2 : Etiquetage

I. Spécification

Boîte à Outils série 2

Etiquetage des données textuelles extraites dans l'arborescence des fils RSS du Monde.

Données

  • Cordial Entrée : Toute l'arborescence du Fils RSS du Monde 2017 (fichiers txt)
  • Cordial Sortie : Les textes bruts extraits et étiquetés via Cordial (1 fichier txt par rubrique). Dans chaque fichier, chaque ligne comporte le forme, le lemme et la catégorie grammaticale (séparés par tabulation) d'un token du texte
  • TreeTagger Entrée : Toute l'arborescence du Fils RSS du Monde 2017 (fichiers XML)
  • TreeTagger Sortie : Les textes étiquetés via Treetagger dans la BàO2 (1 fichier XML par rubrique). Les donées sont stockées au format XML

II. Outils et Méthodes

2.1 Outils d'étiquetage

2.1.1 Cordial (Version 6 - Universités)

Cet outil permet d'étiqueter nos contenus textuels extraits dans la BàO1, et stocker les résultats en trois colonnes : forme, lemme et catégorie gramaticale.

Selon le manuel, cette version dispose de deux options supplémentaires d'importance : l'étiquetage de texte et l'extraction de phrases sur occurrences et co-occurrences.

jeu d'étiquettes ADJMS Adjectif Masculin Singulier
ADJMP Adjectif Masculin Pluriel
ADJFS Adjectif Féminin Singulier
ADJFP Adjectif Féminin Pluriel
ADJMIN Adjectif Masculin Invariant en Nombre
ADJFIN Adjectif Féminin Invariant en Nombre
ADJSIG Adjectif Singulier Invariant en Genre
ADJPIG Adjectif Pluriel Invariant en Genre
ADJI Adjectif Invariant en Nombre et en Genre
DETDEM Adjectif Démonstratif
ADJNUM Adjectif Numérique Cardinal
ADJORD Adjectif Numérique Ordinal
DETDMS Article Défini Masculin Singulier
DETDFS Article Défini Féminin Singulier
DETIMS Article Indéfini Masculin Singulier
DETIFS Article Indéfini Féminin Singulier
NCMIN Nom commun Masculin Invariant en Nombre
NCFIN Nom commun Féminin Invariant en Nombre
NCSIG Nom commun Singulier Invariant en Genre
NCPIG Nom commun Pluriel Invariant en Genre
...

2.1.2 TreeTagger

Un etiqueteur de morphosyntaxiques.
url : http://www.cis.uni-muenchen.de/%7Eschmid/tools/TreeTagger/
jeu d'étiquettes : http://www.cis.uni-muenchen.de/~schmid/tools/TreeTagger/data/french-tagset.html
fichier de paramètres pour le tree-tagger-french :
french-oral-utf-8.par
french-utf8.par
Nous utilisons les deux dans le but d'examiner l'influence de paramètres sur des résultats d'un même traitement.

2.2 Méthodes (Script Perl + TreeTagger)

Maintenant que l'extraction des contenus textuels et le nettoyage sont effectués, nous entrons dans la deuxième phase : étiquetage.

BàO 2 hérite le script de BàO 1 et y intègre l'outil d'étiquetage (Treetagger). Le programme rappelle successivement le tokenizeur, tree-tagger et le script de formatage qui transforme la sortie de tree-tagger en XML.

III. Problème à résoudre

A cette étape, le problème principal réside en le temps d'exécution épouvantable (on dit que le traitement intégrant TreeTagger dure environ 5h). La chaîne de traitements nécessite d'importantes améliorations.

Easy things should be easy, and hard things should be possible.

Pour améliorer la performance de BàO 2, il convient d'analyse d'abord ce qui rend le programme si lourd. Après une brève analyse, nous faisons l'hypothèse que le ralentissement du programme est causé par :

  1. lecture et écriture de fichiers temporaires
  2. appel de Shell (notamment celui dans la boucle)

3.1 Eviter la lecture & écriture de fichiers.

Tout d'abord, nous tentons d'éviter toutes les lectures & écritures de fichiers. Dans notre projet, pour utiliser TreeTagger avec Perl script, nous lançons des processus fils comme suivant :

                                
system("perl ./tokenise-utf8.pl chaine_tmp.txt | tree-tagger ~/treetagger/lib/french-utf8.par -lemma -token -no-unknown > chaine_tmp_tag.txt");
system("perl treetagger2xml-utf8.pl chaine_tmp_tag.txt utf-8");
                                
                            

Il existe grosso modo trois méthodes pour lancer des processus dans Perl :

  • system(),
  • exec(),
  • les backquotes`...`.

Dans notre travail, l'emploi de system() necessite une série de lectures & écritures de fichiers temporaires, puisque le processus fils utilise STDIN et STDOUT, la fonction ne permet pas de récupérer la sortie dans le script qui lance le processus. Donc, pour accéder aux donées produites, il faut rediriger la sortie vers un fichier. Mais, si l'écriture sur disque n'est pas indispensable, on préférera d'autres solutions.

Si l'on veut utiliser une commande pour disposer de sa sortie sous la forme d'une chaîne de caractères, la manière la plus simple est d'employer les backquotes. Ainsi, nous remplaçons la fonction system() par `...`.

Pour éviter le deuxième appel de system(), nous changeons le mode d'entrée et sortie du script treetagger2xml en STDIN et STDOUT.

De plus, nous remarquons que nous pouvons simplifier la chaîne de traitements en remplaçant tree-tagger par tree-tagger-french.
Le tokenizeur, le fichier de paramètres et les options sont configurés dans le fichier de la commande comme suivant :

                                
#!/bin/sh

# Set these paths appropriately

BIN=/usr/lib/tree-tagger/bin
CMD=/usr/lib/tree-tagger/cmd
LIB=/usr/lib/tree-tagger/lib

OPTIONS="-token -lemma -sgml -no-unknown"

TOKENIZER=${CMD}/utf8-tokenize.perl
TAGGER=${BIN}/tree-tagger
ABBR_LIST=${LIB}/french-abbreviations-utf8
PARFILE=${LIB}/french-oral-utf-8.par
#PARFILE=${LIB}/french-utf-8.par

$TOKENIZER -f -a $ABBR_LIST $* |
$TAGGER $OPTIONS $PARFILE
                                
                            
telecharger

Le pipeline est désormais plus simple : ( nous avons modifié l'entrée et sortie du script treetagger2xml )

                                
$$contenu=`echo "$$contenu" | tree-tagger-french | perl treetagger2xml-utf8.pl`;
                                
                            

Attention, la présence d'apostrophe (single quote) est fatale aux commandes echo et printf. Nous ne pouvons pas remplacer "’" (U+2019 RIGHT SINGLE QUOTATION MARK) par l'apostrophe avant d'appel d'echo. Donc, on utilise sed après echo dans la commande (au lieu de normaliser cette ponctuation dans la fonction nettoyer()).

Comme uniquement un petit morceau du script treetagger2xml-utf8.pl est utile pour notre projet, nous avançons un pas plus loin en transplantant son coeur dans notre script.

Maintenant, le noyau de BàO 2 est comme suivant :

                                
sub etiqueter
{
    my $contenu=$_[0];
    $$contenu=`echo "$$contenu" | sed "s/\’/\'/g" | tree-tagger-french `;
    traitement($contenu);
}

sub traitement 
{
    my $texte="";
    my $contenu=$_[0];
    my @Lignes=split('\n',$$contenu);
    while (my $Ligne=shift(@Lignes)) 
    {
	if ($Ligne!~/\ô\¯\:\\ô\¯\:\\/) 
	{
	    $Ligne=~s/\"/<![CDATA[\"]]>/g;
	    $Ligne=~s/([^\t]*)\t([^\t]*)\t(.*)/<element><data type=\"type\">$2<\/data><data type=\"lemma\">$3<\/data><data type=\"string\">$1<\/data><\/element>/;
	    $Ligne=~s/<unknown>/unknown/g;
	    $texte.=$Ligne."\n";
	}
    }
    $$contenu=$texte;
}
                                
                            

Juste un petit rappel :

  1. Le processus fils hérite STDIN et STDOUT du processus père, pour utiliser echo sans provoquer des erreurs d'encodage, il faut déclarer l'encodage de STDIN et de STDOUT. Nous pouvons utiliser binmode() ;
  2. Il faut déclarer aussi l'encodage du script (use utf8 ) au début du script pour que sed reconnaisse "’";
  3. Si le script implique d'autres fichiers intermédiares, il convient d'ajouter la déclaration use open IO => ':encoding(UTF-8)'.
  4. Au lieu de déclarer l'encodage dans le script, on peut également exécuter le script avec les options : -C[=-CSDL] ou -CD[=-Cio].
    Voir le détail
    http://search.cpan.org/~shay/perl-5.26.2/pod/perlrun.pod#-C
    https://perldoc.perl.org/perlunicode.html
    https://perldoc.perl.org/utf8.html

3.2 Réduire le nombre d'appels de processus fils

Une fois tous les fichiers temporaires sont évidés, le temps d'exécution est réduit à moins d'une heure (le temps réel varie selon la performance de machine). Mais, cela est loin d'être suffisant. En fait, l'écriture & lecture de fichiers n'est pas le cause principale de pauvre performance de Perl. La majorité de temps est occupée par l'appel de shell. Au terme du temps d'exécution, l'appel de processus fils est une opération très coûteuse pour un script Perl ; la situation est encore pire quand on l'emboîte dans la boucle. Ainsi, nous proposons trois pistes pour améliorer le script.

  1. Eviter l'appel de shell en utilisant le module Lingua :: TreeTagger ;
  2. Réduire le nombre d'appel de shell
  3. Déplacer la fonction etiqueter() en dehors de la boucle ;

Nous avons essayé les deux premières :

  1. Malheureusement, Lingua :: TreeTagger n'est pas bien documenté. CPAN fournit peu d'appui pour ce module et nous n'avons pas trouvé les fichiers paramètres pour le français.
  2. Nous avons enchaîné les chaînes de caractères avant d'appeler la fonction etiqueter() ; ainsi, on lance une seule fois le processus pour chaque fichier. Enfin, le temps d'exécution de BàO 2 est réduit à 2~3 min pour la rubrique 3208 de l'ensemble du corpus.

Le script est toujours perfectible...

IV. Solution

                                
#!/usr/bin/perl
use utf8;
use strict;
use warnings;
use XML::RSS;
use XML::XPath;
use open IO => ':encoding(UTF-8)'; #perl -CD/-C24/-Cio
binmode(STDIN, ':encoding(UTF-8)');
binmode(STDOUT, ':encoding(UTF-8)'); #perl -C3
binmode(STDERR, ':encoding(UTF-8)'); #perl -C7 (assurer bon encodage d'impression de DOCUMENTATION)

my $MODIF="2018-05-15";
my $DOC=<<DOCUMENTATION;
    ____________________________________________________________________________

    NOM :   Boîte à Outils 2      
    MODIFICATION :
            $MODIF
    AUTEURS :  
            XU Yizhou, JIANG Chunyang
    USAGE : 
            perl Bao_2.pl REPERTOIRE-A-PARCOURIR RUBRIQUE-A-EXTRAIRE
    DESCRIPTION:
            Le programme prend en entrée le nom du répertoire contenant les 
            fichiers à traiter
            Le programme construit en sortie un fichier structuré contenant
            sur chaque ligne les contenus textuels étiquetés            
    ____________________________________________________________________________

DOCUMENTATION

if (@ARGV!=2) {
    die $DOC;
}

my $repertoire=$ARGV[0];
my $rubrique=$ARGV[1];
my %redondance;
my $cmptItem=0;
my $fileid=0;

#-----------------------------------
#normaliser le nom du répertoire
#-----------------------------------
$repertoire=~ s/[\/]$//;


open(my $FHXML,">","$rubrique-tagged.xml");
print $FHXML "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
print $FHXML "<base rubrique=\"$rubrique\" type=\"POStagged\">\n<entete>\n<auteur>XU Yizhou</auteur>\n<auteur>JIANG Chunyang</auteur>\n</entete>\n<etiquetage>\n";
#------------------------------------------------------------------
parcourirRecursion($repertoire);
# parcourirPile($repertoire);
#------------------------------------------------------------------
print $FHXML "</etiquetage>\n</base>\n";
close($FHXML);
exit 0;
#------------------------------------------------------------------


#------------------------------------------------------------------
sub parcourirRecursion
{
    my ($path)=@_;
    opendir(my $dir, $path) or die "ERR : Echec d'ouverture de $path: $!\n";
    my @files=readdir($dir);
    closedir($dir);
    
    foreach my $file (@files)
    {
        next if $file =~ /^\.\.?$/;
	$file=$path."/".$file;
	if ( -d $file ) { parcourirRecursion($file); }
        if ( -f $file and $file=~ m/-$rubrique.+\.xml$/ )
        {
            $fileid++;
# trois moyens d'extraction
            extraireXPath($file);
#             extraireRSS($file);
#             extraireRegex($file);
        }
    }
}

#------------------------------------------------------------------
sub parcourirPile
{
    my ($path)=@_;
    my @dirs=($path.'/');
    
    while(my $dir=pop(@dirs))
    {
        my $DH;
        unless(opendir($DH, $dir))
        {
            warn "ERR : échec d'ouverture de $dir: $!\n";
            next;
        }
        foreach my $file (readdir($DH))
        {
            next if $file =~ /^\.\.?$/;
            $file=$dir."/".$file;
            if ( -d $file ) { push(@dirs, $file); }
            if ( -f $file and $file=~ m/-$rubrique.+\.xml$/ )
            {
                $fileid++;
# trois moyens d'extraction
#             extraireXPath($file);
                extraireRSS($file);
#             extraireRegex($file);
            }
        }
        closedir($DH);
    }
}

sub extraireRSS
{
    my ($file)=@_;
    my $rss=new XML::RSS( encoding => 'utf-8' );
    eval { $rss->parsefile($file); };
    if ($@) {
        warn "ERR: échec d'analyse du fichier $file : $@\n";
    }
    else
    {
        print $FHXML "<fichier id=\"$fileid\" nom=\"$file\">\n";
        my $contenu="";
        foreach my $item (@{$rss->{'items'}})
        {
            my $titre=$item->{'title'};
            my $description=$item->{'description'};
            #---------------------------------
            #éliminer des doublons
            #---------------------------------
            if(not exists $redondance{$titre})
            {
                $cmptItem++;
                $redondance{$titre}=1;
                nettoyer(\$titre);
                if( not $titre=~ m/[?!.]$/ ) { $titre.='.'; }
                $contenu.=$titre." ";
                if( $description )
                {
                    nettoyer(\$description);
                    $contenu.=$description." ";
                } 
            }            
        }
        etiqueter(\$contenu);
        print $FHXML "$contenu";
        print $FHXML "</fichier>\n";
    }
}

sub extraireXPath
{
    my ($file)=@_;
    print $FHXML "<fichier id=\"$fileid\" nom=\"$file\">\n";
    
    my $xp=XML::XPath->new( filename => $file );
    my $contenu="";
    foreach my $node ($xp->find('/rss/channel/item')->get_nodelist)
    {
        my $titre=$node->find('title')->string_value;
        my $description=$node->find('description')->string_value;
            if(not exists $redondance{$titre})
            {
                $cmptItem++;
                $redondance{$titre}=1;
                nettoyer(\$titre);
                if( not $titre=~ m/[?!.]$/ ) { $titre.='.'; }
                $contenu.=$titre." ";
                if( $description )
                {
                    nettoyer(\$description);
                    $contenu.=$description." ";
                }
            }
    }
    etiqueter(\$contenu);
    print $FHXML "$contenu";
    print $FHXML "</fichier>\n";
}

sub extraireRegex 
{
    my ($file)=@_;
    print $FHXML "<fichier id=\"$fileid\" nom=\"$file\">\n";
    
    open (my $FH, "<", $file);
    my $texte="";
    while (my $ligne=<$FH>)
    {
        chomp $ligne;
        $ligne=~ s/\r//g;
        $texte.=$ligne;
    }
    close($FH);
    my $contenu="";
    $texte=~ s/>\s+</></g;
    while ($texte=~ m/<item>.+?<title>([^<]*)<\/title>[^<]*<description>([^<]*)<\/description>.+?<\/item>/g)
    {
        my $titre=$1;
        my $description=$2;
        if(not exists $redondance{$titre})
        {
            $cmptItem++;
            $redondance{$titre}=1;
            nettoyer(\$titre);
            if( not $titre=~ m/[?!.]$/ ) { $titre.='.'; }
            $contenu.=$titre." ";
            if( $description )
            {
                nettoyer(\$description);
                $contenu.=$description." ";
            }
        }
    }
    etiqueter(\$contenu);
    print $FHXML "$contenu";
    print $FHXML "</fichier>\n";
}

sub nettoyer
{
    my $contenu=$_[0];
    $$contenu =~ s/<[^>]+>//g;
    $$contenu =~ s/<.+>//g;
    $$contenu =~ s/&(#38;)?#39;/'/g;
    $$contenu =~ s/&(#38;)?#34;/"/g;
    $$contenu =~ s/&(amp;)?/et/g;
#     $$contenu =~ s/\x{2019}/\'/g;
}

sub etiqueter
{
    my $contenu=$_[0];
    $$contenu=`echo "$$contenu" | sed "s/\’/\'/g" | tree-tagger-french `;
    traitement($contenu);
}

#--------------------------------------------------------------
#   treetagger2xml
#   entree: référence à la chaîne de caractères contenant 
#           le texte étiqueté et lemmatisé par tree-tagger
#   sortie: le même texte au format xml
#--------------------------------------------------------------
sub traitement 
{
    my $texte="";
    my $contenu=$_[0];
    my @Lignes=split('\n',$$contenu);
    while (my $Ligne=shift(@Lignes)) 
    {
	if ($Ligne!~/\ô\¯\:\\ô\¯\:\\/) 
	{
	    $Ligne=~s/\"/<![CDATA[\"]]>/g;
	    $Ligne=~s/([^\t]*)\t([^\t]*)\t(.*)/<element><data type=\"type\">$2<\/data><data type=\"lemma\">$3<\/data><data type=\"string\">$1<\/data><\/element>/;
	    $Ligne=~s/<unknown>/unknown/g;
	    $texte.=$Ligne."\n";
	}
    }
    $$contenu=$texte;
}

                                
                            
telecharger

V. Résultats

5.1 CORDIAL

Exemple

823353 3208

Téléchargement

3208 3210 3214 3224 3232
3236 3242 3244 3246 3260
3476 3546 651865 823353
TOUT

5.2 TREE-TAGGER[french-oral-utf-8.par]

Exemple Réfléchissez avant de cliquer le bouton ...

823353 3208

Téléchargement

3208 3210 3214 3224 3232
3236 3242 3244 3246 3260
3476 3546 651865 823353
TOUT

5.3 TREE-TAGGER[french-utf8.par]

Exemple Réfléchissez avant de cliquer le bouton ...

823353 3208

Téléchargement

3208 3210 3214 3224 3232
3236 3242 3244 3246 3260
3476 3546 651865 823353
TOUT

© JIANG Chunyang & XU Yizhou. All rights reserved. | Design by TEMPLATED.