Fabien POULARD
Jérôme Rocheteau
Laurent Audibert
Matthieu Vernier
Nicolas Hernandez
Nicolas Hernandez
Les opérations logicielles en traitement automatique des langues sont souvent très importantes, très coûteuse, mais également parallélisables. C'est l'un des avantages indéniables d'UIMA, par le choix d'une architecture par composants, de permettre de paralléliser les traitements assez simplement.
La programmation d'un composant UIMA doit donc se faire avec en tête l'idée qu'il pourrait être déployé pour un traitement parallélisé. Lorsque comme moi on n'a pas cette habitude, on se heurte rapidement à des petits obstacles techniques. Dans le cas présent, il s'agissait de développer un composant qui exportait les résultats d'un calcul dans un fichier CSV.
La chaîne de traitement que j'utilise pour ma thèse calcule des scores de similarité entre des documents. Afin de simplifier l'évaluation de mon système, je souhaite réunir tous ces scores de similarité dans un même fichier CSV.
La solution naïve qui fonctionne presque consiste à ce que le composant ouvre un fichier et y écrive au fur et à mesure. Les systèmes ne rechignent pas à ce que plusieurs processus écrivent en même temps dans le même fichier... Malheureusement ils ne s'assurent pas non plus d'ordonnancer les requêtes d'écriture au risque que chacun écrive sur la copie de son voisin !
Voici le résultat que j'attends :
source-document00003.txt;suspicious-document00016.txt;not-derivative;0.0016992353 source-document00003.txt;suspicious-document00040.txt;not-derivative;0.23280424 source-document00267.txt;suspicious-document00055.txt;derivative;0.14148398
et voici ce que ça peut donner quand les différents processus décident d'écrire en même temps :
source-document00003.txt;suspicious-docsource-document00003.txt;suspicious-source-document00267.txt;suspicious-document00055.txt;derivative;0.14148398 ument00016.txt;not-derivative;0.0016992353 document00040.txt;not-derivative;0.23280424
J'avais déjà rapidement présenté le workflow d'UIMA dans mon billet sur les ressources. Un petit résumé des épisodes précédents ne sera peut-être pas de trop.
Comme d'habitude, pour simplifier, on va considérer qu'on utilise uniquement le flow controler par défaut qui fait s'enchaîner séquentiellement les composants. Cela revient à considérer que la sortie d'un composant alimente directement l'entrée du composant suivant, à la manière des pipes dans le monde Unix.
Dans cette configuration, UIMA regroupe les chaînes de CAS processors (les composants qui manipulent les CAS) dans autant de processing pipelines que l'attribut processingUnitThreadCount du CPE le demande, comme l'illustre le schéma ci-dessous :
l est nécessaire d'instancier les composants utilisés autant de fois qu'il y a de processing pipelines. Jusque-là, rien de bien nouveau en réalité. Mais ce que j'ai découvert récemment, douloureusement bien entendu, c'est que cette instanciation ne se faisait pas au sein des pipelines, mais au sein du thread principal du CPE. Soit la procédure suivante, pour chaque composant :
J'aurais certes pu faire le choix d'utiliser le système des ressources pour mon composant, comme je le préconise dans mon billet précédent. Toutefois, je n'ai pas eu le temps de manipuler le système des ressources et il est grand temps que j'en finisse avec ma thèse :) Je me suis donc tourné vers quelque chose de plus direct.
Mon choix s'est donc porté sur la création d'autant de fichiers qu'il y a d'instances de mon composant. Je suffixe tout simplement le nom du fichier par l'identifiant du thread courant. Cette méthode fonctionne suffisamment bien, mais nécessite bien entendu de concaténer l'ensemble des fichiers à la fin du traitement. De plus elle ne permet pas directement de garder une trace de l’ordonnancement des écritures ! Dans mon cas ce n'était pas un prérequis.
Pour la petite histoire, j'ai découvert que la méthode initialize() était appelée dans le thread principal car tous mes fichiers étaient suffixés par l'identifiant 1. J'ai donc simplement déplacé l'ouverture des fichiers dans la méthode process() en ajoutant un test pour s'assurer que cette initialisation n'avait pas déjà eu lieu.
Voici le code (synthétique) correspondant, toute l'astuce réside dans le Thread.currentThread().getId() :
public void process(JCas aJCas) throws AnalysisEngineProcessException { // Check the streams have been initialized if (theStream == null) { Long tid = Thread.currentThread().getId(); theStream =new PrintStream( new File( theExportSuffix + ".__" + tid), "utf-8"); } // Select the right view JCas view = aJCas.getView(theViewName); ...
Parmi les addons distribués avec UIMA, le composant WhitespaceTokenizer permet de découper les textes en "mots" en s'appuyant sur les espaces et autres blancs. Malheureusement si cette approche est acceptable pour l'anglais, elle est beaucoup plus problématique pour le français. Le découpage en mots n'est pas une tâche forcément évidente et qui nécessiterait un réel travail. Partagé entre la nécessité d'une tokenisation correcte et le peu de temps qui me reste pour terminer ma thèse, voici une proposition suffisamment acceptable pour mes besoins.
Mon idée est d'exploiter la catégorie générale des caractère telles que définies dans la norme Unicode pour détecter les frontières des mots. Une rupture dans la catégorie des caractères indiquant vraisemblablement un changement de mot.
Je souhaite que mon composant soit en mesure d'identifier correctement tous les mots détectés par le WhitespaceTokenizer (c'est un minimum), ainsi que les cas problématiques tels que :
La norme unicode offre sept catégories générales de caractère :
| Unicode | Java (getType) |
|---|---|
| les lettres (L) | |
| Ll | LOWERCASE_LETTER |
| Lu | UPPERCASE_LETTER |
| Lm | MODIFIER_LETTER |
| Lo | OTHER_LETTER |
| Lt | TITLECASE_LETTER |
| les marques (M) | |
| Mc | COMBINING_SPACING_MARK |
| Me | ENCLOSING_MARK |
| Mn | NON_SPACING_MARK |
| les nombres (N) | |
| Nd | DECIMAL_DIGIT_NUMBER |
| Nl | LETTER_NUMBER |
| No | OTHER_NUMBER |
| les ponctuations (P) | |
| Pc | CONNECTOR_PUNCTUATION |
| Pd | DASH_PUNCTUATION |
| Pe | END_PUNCTUATION |
| Pi | INITIAL_QUOTE_PUNCTUATION |
| Pf | FINAL_QUOTE_PUNCTUATION |
| Po | OTHER_PUNCTUATION |
| Ps | START_PUNCTUATION |
| les symboles (S) | |
| Sc | CURRENCY_SYMBOL |
| Sm | MATH_SYMBOL |
| Sk | MODIFIER_SYMBOL |
| So | OTHER_SYMBOL |
| les séparateurs (Z) | |
| Zl | LINE_SEPARATOR |
| Zp | PARAGRAPH_SEPARATOR |
| Zs | SPACE_SEPARATOR |
| divers (C) | |
| Cc | CONTROL |
| Cf | FORMAT |
| Co | PRIVATE_USE |
| Cs | SURROGATE |
| Cn | UNASSIGNED |
Le site Fileformat fournit une liste et une description exhaustive des caractères contenus dans ces différentes catégories.
Les mots pleins sont ceux qui se composent uniquement d'une suite de caractères alphanumériques, on peut les reconnaître à l'aide de l'automate suivant :
Les articles et pronoms contractés précédent un autre mot, ils se composent d'une seule lettre et d'un apostrophe. Les apostrophes appartiennent à la catégorie Po qui contient également le point d'exclamation, le dièse, ... ou bien à Lm. Il est préférable d'établir une liste des caractères correspondant. L'apostrophe (U+0027) et le modifieur apostrophe (U+02BC) sont les deux caractères qui semblent correspondre pour le français.
Les composés lexicaux à apostrophe sont quant à eux formés de deux séquences alphabétiques connectées par un apostrophe (aujourd'hui, ...).
L'automate permettant de reconnaître ces deux formes de mots :
Le trait d'union est utilisé dans plusieurs configurations : pour les composés lexicaux (arc-en-ciel, peut-être, sauve-qui-peut, ...) et certains sigles sigles (C-4, c-à-d, ...). D'une manière générale certaines ponctuations sont employées au sein des mots : le point pour les abréviations (i.e., M., ...), ou le slash pour les unités de mesure (A/m, km/h, ...).
Les traits d'union sont regroupés dans la catégorie ''Punctuation, dash'' (''Pd''), les autres ponctuations (slash et point) sont placées dans ''Punctuation, other'' (''Po''). L'important c'est que ladite ponctuation soit placée entre deux séquences de lettres :
Finalement le dernier cas particulier concerne les valeurs numériques complexes (14 000, 14,18, 30 %, -3, ...). La catégorie ''Symbol, Currency'' (''Sc'') nous intéresse particulièrement. Le symbole ''%'' se trouve dans la catégorie ''Punctuation, other' (''Po''), tandis que le point et la virgule décimale correspondent respectivement à U+002E et U+002C.
La tokenisation s'effectue sur un flux continue de caractères. L'utilisation d'automates classiques tels que présentés précédemment n'est pas forcément possible car l'utilisation commune ne correspond pas forcément à une grammaire régulière et ils sont non déterministes. En gros nous pouvons nous retrouver dans ces cas de figure :
Ma proposition consiste à mettre en œuvre une sorte de transducteur à pile. La pile permet uniquement de conserver un historique des derniers caractères consommés afin de découper les mots à une position antérieure si nécessaire (symbole de monnaie non trouvé mais espace consommé par exemple). L'utilisation d'un transducteur permet d'enovyer des signaux indiquant si une coupure de mot doit avoir lieu ou non.
Les signaux envoyés par le système peuvent être les suivants :
L'automate ci-dessous correspond à ce transducteur :
J'ai recherché une bibliothèque Java pour manipuler des transducteurs... mais je n'ai rien trouvé. J'ai donc tout implémenté manuellement ce qui n'est pas formidable pour le maintient du code. J'ai donc implémenté également plusieurs tests unitaires.
Au final, les cas où les symboles monnétaires sont espacés des nombres par un espace pose problème, mais j'ai choisi de laisser tel quel car je ne vois pas de solution pratique (je ne vois plus grand chose en fait).
Chose intéressante, mon implémentation à base de transducteur est plus rapide de près de 40% que le composant WhitespaceTokenizer sur un petit corpus de test, mais d'autres tests donnent les deux annotateurs ex-aequo. La qualité du résultat est sans comparaison, comme l'illustre l'exemple ci-dessous :
"Le Roi a reçu en audience en début d’après-midi au Château
Résultat du WhitespaceTokenizer : {", Le, Roi, a, reçu, en, audience, en, début, d, ’, après, -, midi, au, Château}
Résultat de notre composant : {", Le, Roi, a, reçu, en, audience, en, début, d’, après-midi, au, Château}
Les jar avec les sources et le descripteur sont disponibles ici.
J'ai également ouvert un dépôt sur github pour ceux qui voudraient contribuer.
Les ressources sont un aspect de UIMA que j'ai peu abordé et que j'ai très peu utilisé, sauf récemment lorsque l'on a entraîné un modèle français HMM pour le HMM Tagger (il faut que j'écrive un billet à ce sujet d'ailleurs). Pourtant les ressources, et leur rôle, sont assez mal compris au sein d'UIMA.
Je m'excuse par avance auprès de Jérôme pour construire toute mon argumentation et ce billet sur une erreur qu'il a faite :) Pour information, Jérôme travaille en tant qu'ingénieur de recherche au sein de notre équipe de recherche, et est très certainement la personne de l'équipe la plus impliquée et la plus compétente sur UIMA. Voilà pour la mise en garde, je peux désormais mettre en place l'inquisition et prononcer l'auto da fé de sa méthode !
L'erreur dont je parle est celle présentée dans ce billet. En gros, Jérôme nous explique dans ce billet que pour compter le nombre de mots dans tous les documents d'une collection, à l'aide d'un AE, on procède en en trois temps :
D'une manière générale, cela revient à dire que l'on peut définir une structure persistante dans un AE qui nous permette de stocker des informations relevant de tous les CAS. Ce raisonnement est entièrement faux, même s'il fonctionne en général.
Tout d'abord il est peut-être nécessaire de rappeler ce qu'est un CPE. Il s'agit de l'ensemble des composants (collection reader, analysis engines et CAS consumers) agencés pour produire et traiter les différents CAS. On peut en quelque sorte considérer qu'un CPE est une chaîne de traitement instanciée.
Un CPE est orchestré par un CPM, ce dernier va se charger de la mise en œuvre de la chaîne : instanciation des composants, appel des méthodes de l'API, distribution des CAS, suivi des erreurs, collecte d'informations statistiques sur le déroulement du traitement.
Voilà donc pour la théorie générale, qui peut se résumer à ce schéma que j'emprunte gentiment aux gens d'Apache :
Au regard de ce schéma on pourrait penser que la méthode décrite par Jérôme fonctionne puisqu'on a l'impression que les CAS produits ne passent au travers que d'une seule instance des AE. Si tel était le cas, l'intérêt de UIMA en termes de déploiement serait limité puisque cela reviendrait à transmettre la sortie d'un composant à l'entrée du composant suivant dans la chaîne. Une sorte pipe entre les différentes briques logicielles.
Il faut se pencher un peu plus profondément dans la configuration du CPE pour se rendre compte que le fonctionnement est bien mieux pensé que cela. Ainsi, on trouve dans la section ''casProcessors'' des descripteurs (XML) de CPE, un attribut d'intérêt : processingUnitThreadCount.
L'attribut processingUnitThreadCount spécifie le nombre de Processing pipelines (trad: chaînes de traitement ?) répliquées. Une processing pipeline est composée de la séquence d'AE définissant le traitement à opérer. Ces séquences (et donc les AE associés) sont dupliqués autant de fois que le nombre précisé par l'attribut processingUnitThreadCount, chacune s'exécutant dans son propre thread indépendamment des autres.
Un petit schéma, encore une fois gentiment emprunté aux gens d'Apache, vaut mieux qu'un long discours :
Le CPM prélève les CAS produit par le collection reader, et stockées dans la queue d'entrée (de taille casPoolSize), et les distribue dans les différentes processing pipelines. Les CAS étant distribuées entre les différentes chaînes, un CAS donné ne passera que par une seule de ces chaînes : c'est ici que la méthode de Jérôme échoue ! En effet, chaque AE dupliqué ne traite qu'un sous ensemble des CAS produits et alimente sa propre structure interne, sans connaissance des informations stockées dans les structures des autres instances de ce même AE.
Au sein de l'équipe nous avons déjà discuté brièvement des ressources externes. Cependant le contenu des discussions se résumait plus au mois à les ressources on ne sait pas trop à quoi ça sert, par contre c'est ennuyeux car il faut éditer les descripteurs pour les préciser, donc on préfère les paramètres. Nous sommes passés à côté de l'essence même de ces dernières.
La section 1.5.4 de la documentation ''Tutorials and users guides'' décrit le fonctionnement et l'utilisation des ressources externes.
Sometimes you may want an annotator to read from an external file – for example, a long list of keys and values that you are going to build into a HashMap. You could, of course, just introduce a configuration parameter that holds the absolute path to this resource file, and build the HashMap in your annotator's initialize method. However, this is not the best solution for three reasons:
Les ressources externes ont deux avantages majeurs (à mon avis) :
Il serait donc préférable dans le cas de Jérôme :
J'ai la lâcheté de ne pas proposer de correction, mais l'objectif de ce billet était simplement d'expliquer pourquoi l'utilisation de ressources externes pouvait se justifier et pourquoi la combinaison de structures internes et d'appel à la méthode collectionProcessComplete n'était pas toujours suffisant.
Lorsque l'on travaille avec Apache UIMA et que l'on ajoute un nombre important d'annotations, il arrive un moment où l'on va vouloir filtrer certaines de ces annotations en fonction d'autres. Ainsi, assez couramment on éprouve le besoin de devoir récupérer des annotations qui couvrent la même zone de texte qu'une autre. Par exemple :
Il y a au moins deux approches dans Apache UIMA qui permettent de répondre à ce besoin : le subiterator et le FSMatchConstraint.
L'approche basée sur le subiterator ne peut fonctionner que si les types que l'on cherche à accéder sont couverts par le type couvrant au sens de UIMA, c-à-d en terme de priorité des types (cf. [la javadoc de TypePriorities] ou cet email).
Considérons une annotation A qui couvre des annotations B de la manière suivante :
Il y a du texte et les annotations sont sur ce texte ... [-----A:1-----] [---A:2---] [--------A:3--------] [B:1] [B:2] [B:3] [B:4] [B:5] [B:6]
Dans l'exemple ci-dessus, nous sommes intéressés par les annotations B couvertes par l'annotation A:3, en d'autres termes les annotations B:4, B:5 et B:6.
La méthode est la suivante :
Voici le code correspondant :
// Récupération des index AnnotationIndex annAIdx = (AnnotationIndex) jcas.getAnnotationIndex(A.type); AnnotationIndex annBIdx = (AnnotationIndex) jcas.getAnnotationIndex(B.type); // On recherche ''A:3'' FSIterator annAIt = annAIdx.iterator(); while (annAIt.hasNext()) { A monA3 = (A) annAIt.next(); // On récupére l'itérateur sur les annotations B couvertes par A3 FSIterator annBSousA3It = annBIdx.subiterator(monA3); while (annBSousA3It.hasNext()) { // On récupére successivement B4, B5 et B6 B annB = (B) annBSousA3It.next(); System.out.println("Sous A3 : "+annB); } }
Lorsque l'on ne connaît pas les priorités des types ou bien qu'elles ne correspondent pas à ce que l'on souhaite faire, il est nécessaire de passer par un mécanisme plus complexe (mais beaucoup plus puissant) : le système de contraintes d'index.
Dans le cas présent, nous allons définir une contrainte imposant que les attributs begin et end d'une annotation d'un type donné correspondent à une certaine valeur : celle de l'annotation couvrante. Puis nous pourrons générer un itérateur qui retournera les annotations de l'index qui respectent cette contrainte.
Voici l'implémentation d'une méthode qui fait cela :
/** * * This method provides an iterator over typed annotations that either * have an offset embedded in that of a given annotation in a document, * or have the same offset as these annotation. * * @param theDocument the document in which stand the source and * target annotations * @param theAnnotation the source annotation under which target * annotations that have to be drawn out * @param theType the type of the target annotations that have * to be drawn out from the document under * the source annotation * @param isStrict the boolean that defines the offset matching, * offsets strictly equal if isStrict is true, begin * offsets greater or equal and end offsets less * or equal otherwise. * @return the iterator over the type theType annotations * which stand under the annotation theAnnotation * in the document theDocument * * @author Fabien Poulard * @author Jérôme Rocheteau * * @license Apache 2.0 */ public FSIterator subiterator(JCas theDocument, Annotation theAnnotation,Type theType,boolean isStrict) { // Ajout: déclaration de la variable type Type theAnnotationType = theAnnotation.getType(); // On utilise le constraint factory ConstraintFactory theConstraints = theDocument.getConstraintFactory(); // On définit les contraintes sur le début de l'annotation FSIntConstraint beginConstraint = theConstraints.createIntConstraint(); if (isStrict) { beginConstraint.eq(theAnnotation.getBegin()); } else { beginConstraint.geq(theAnnotation.getBegin()); } Feature beginFeature = theAnnotationType.getFeatureByBaseName("begin"); FeaturePath beginPath = theDocument.createFeaturePath(); beginPath.addFeature(beginFeature); FSMatchConstraint begin = theConstraints.embedConstraint(beginPath,beginConstraint); // ... puis sur la fin de l'annotation FSIntConstraint endConstraint = theConstraints.createIntConstraint(); if (isStrict) { endConstraint.eq(theAnnotation.getEnd()); } else { endConstraint.leq(theAnnotation.getEnd()); } Feature endFeature = theAnnotationType.getFeatureByBaseName("end"); FeaturePath endPath = theDocument.createFeaturePath(); endPath.addFeature(endFeature); FSMatchConstraint end = theConstraints.embedConstraint(endPath, endConstraint); // JR: on définit une contrainte sur le type d'annotation FSTypeConstraint typeConstraint = theConstraints.createTypeConstraint(); typeConstraint.add(theType); // FeaturePath typePath = theDocument.createFeaturePath(); FSMatchConstraint type = theConstraints.embedConstraint(typePath, typeConstraint); // On combine les contraintes FSMatchConstraint beginAndEnd = theConstraints.and(type,theConstraints.and(begin, end)); // On génère un itérateur respectant ces contraintes FSIterator filteredIterator = theDocument.createFilteredIterator(theDocument.getAnnotationIndex().iterator(), beginAndEnd); return filteredIterator; }
Cette méthode prend en paramètre le JCas dans lequel travailler, l'annotation couvrante (l'annotation A3 dans l'exemple précédent), le type d'annotation qui nous intéresse (le type B pour reprendre l'exemple précédent) et un booléen qui permet de préciser si l'on souhaite une correspondance exacte ou approximative des frontières.
La qualité du code développé dans le cadre des activités de recherche scientifique n'est pas toujours aussi bon qu'on pourrait l'espérer. Outre la nécessité (évidente à mes yeux) d'ouvrir le codes des activités scientifiques financées par l'État et les collectivités territoriales, il est également nécessaire de suivre de bonnes pratiques de programmation. L'écriture de tests unitaires et leur exécution régulière est une de ces bonnes pratiques.
Je présente dans ce billet un cas d'utilisation de la bibliothèque UUTUC, présentée lors du Workshop sur l'Ingénierie Logiciel, les Tests et l'Assurance Qualité pour le Traitement des Langues Naturelles (SETQA-NLP 2009), pour tester l'implémentation d'une bibliothèque développée et utilisée dans le cadre de ma thèse (tddts-uima-shingling).
UUTUC est une bibliothèque offrant un certain nombre de méthodes facilitant le processus de test des composants UIMA. On y trouve notamment un certain nombre de classes de type Factory qui facilitent la mise en place de chaînes de traitement simples pour expérimenter les composants.
À l'aide de ces classes, l'exécution d'un AE sur un simple fichier texte se résume à ces quelques lignes :
AnalysisEngine engine = AnalysisEngineFactory.createAnalysisEngineFromPath("descriptors/tutorial/ex1/RoomNumberAnnotator.xml"); JCas jCas = AnalysisEngineFactory.process(engine, "data/WatsonConferenceRooms.txt");
Le couplage de UUTUC avec JUnit permet de mettre en place un banc de tests unitaires :
J'utilise le framework JUnit 4 pour les tests unitaires. Il suffit de faire précéder les méthodes considérées comme des tests par @Test pour qu'elles soient reconnues comme telles par JUnit. Exemple :
import org.junit.Test; import static org.junit.Assert.*; ... /** * This class defines the tests for the main methods of the Shingle class. */ public class ShingleTest { ... /** * This method just checks that the isComplete method works * @throws InvalidShingleException * @throws OverloadShingleException */ @Test public void completeness() throws InvalidShingleException, OverloadShingleException { Shingle s1 = new Shingle(2); assertFalse( s1.isComplete() ); // before any adding s1.add( theShingleItems[0] ); assertFalse( s1.isComplete() ); // after a first adding s1.add( theShingleItems[0] ); assertTrue( s1.isComplete() ); // should be complete by now } ... }
Combiné à UUTUC, il permet de mettre en place un environnement UIMA assez simplement. Ainsi dans l'exemple ci-dessous, nous définissons une méthode à exécuter avant chaque test (@Before) qui crée un JCas et y ajoute quelques annotations à l'aide des Factory de UUTUC :
/** Static data for testing */ private static String CAS_CONTENT = "Suisse : inauguration d'une nouvelle synagogue, une première depuis 50 ans"; private static Integer[][] CAS_OFFSETS = { {0,6}, {9,21}, {22,24}, {24,27}, {28,36}, {37,46}, {48,51}, {52,60}, {61,67}, {68,74} }; ... /** * This method is used to set up the testing environment, creating the * data necessary for the different tests methods. */ @Before public void setUp() throws UIMAException, IOException, ShinglingTestingException { // Set up a CAS with a couple of shingle items in TypeSystemDescription tsd = TypeSystemDescriptionFactory .createTypeSystemDescription("shingling-ts"); theTestingCas = JCasFactory.createJCas(tsd); theTestingCas.setDocumentText(CAS_CONTENT); for(Integer[] idx: CAS_OFFSETS) { AnnotationFactory.createAnnotation(theTestingCas, idx[0], idx[1], ShingleItem.class) ); } }
Malheureusement il y a assez peu de documentation concernant UUTUC. Il est ainsi régulièrement nécessaire d'aller jeter un œil au code source qui heureusement est très bien écrit.
Maven modélisant toutes les étapes du cycle de développement, il intègre une étape test entre le compile et le package. La gestion des tests unitaires se faisant quant à eux au travers du plugin maven-surefire-plugin.
Il faut tout d'abord rajouter dans le pom.xml les informations de dépendance sur UUTUC et JUnit :
<repository> <snapshots> <enabled>false</enabled> </snapshots> <id>uutuc-googlecode</id> <name>uutuc Google Code repository</name> <url>http://uutuc.googlecode.com/svn/repo/</url> </repository> ... <!-- UUTUC for testing --> <dependency> <groupId>org.uutuc</groupId> <artifactId>uutuc</artifactId> <version>0.9.10</version> <optional>false</optional> <scope>test</scope> </dependency> <!-- JUnit 4 for testing --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.3.1</version> <scope>test</scope> </dependency>
Il suffit ensuite de faire appel au plugin maven-surefire-plugin qui prend en charge tout ce qui concerne les tests, sous réserve que ces derniers soient bien présents dans src/test/java :
<!-- Testing --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <reportFormat>brief</reportFormat> <useFile>false</useFile> </configuration> </plugin>
Il est alors possible de lancer l'exécution des tests avec Maven :
$ mvn test ... ------------------------------------------------------- T E S T S ------------------------------------------------------- Running tddts.uima.shingling.ShingleTest Tests run: 16, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.992 sec Results : Tests run: 16, Failures: 0, Errors: 0, Skipped: 0 ...
Plus d'excuse pour ne pas tester votre code maintenant ! L'excuse de faire du prototypage pour la recherche n'en est pas une bonne dès que les résultats que vous publiez dépendent de la qualité dudit code. C'est votre intégrité et honnêteté scientifique qui est en jeux ;)
Il y a quelques temps j'avais écrit une documentation en interne dans mon laboratoire sur l'utilisation du ''CAS Editor''. À l'époque c'était assez éprouvant car ce dernier se présentait sous la forme d'une application RCP Eclipse.
Depuis la version 2.3.0, le CAS Editor a été intégré sous la forme d'un plugin Eclipse. L'occasion de remettre la doc au goût du jour et de la partager avec le reste du monde.
Avant toute chose il est nécessaire d'installer les plugins Eclipse de UIMA. Pour ce faire, il faut ajouter le dépôt Eclipse : http://www.apache.org/dist/incubator/uima/eclipse-update-site/ dans l'outil de gestion des plugins. Ensuite il suffit de rechercher UIMA et d'installer tous les plugins associés.
À noter que les personnes ayant installé les plugins des versions antérieures doivent simplement faire une mise-à-jour afin de faire apparaître le plugin CAS Editor qui n'était pas présent auparavant.
Un petit redémarrage d'Eclipse et vous devriez être paré pour la suite...
Le fonctionnement du CAS Editor est lié à :
Ceci est spécifique à la version 2.3 d'UIMA et devrait évoluer dans les prochaines versions.
La vidéo ci-dessous illustre les étapes nécessaires à l'initialisation d'un projet et l'importation d'un corpus :
Il n'est possible d'ajouter des annotations que si le type d'annotation à ajouter est présent dans le Type System. Si toutefois vous ajoutez des types d'annotation à un Type System qui est déjà utilisé par le projet CAS Editor, les nouveaux types ne vont pas apparaître. Il est nécessaire de fermer puis réouvrir le projet.
Il y a deux façons d'ajouter une annotation dans un CAS, l'une permet d'ajouter la même annotation par un simple appuie sur Entrée :
l'autre permet de choisir le type de chaque nouvelle annotation rajoutée :
La vidéo ci-dessous illustre ces deux méthodes :
Il est tout à fait possible d'utiliser directement un Analysis Engine directement dans le CAS Editor afin d'ajouter des annotations aux CAS. La procédure est toutefois un peu contraignante et surtout peu intuitive. Je vais décrire l'approche qui consiste à utiliser un composant empaqueté dans un PEAR.
Pour l'exemple je vais prendre le WhitespaceTokenizer, ce dernier a deux avantages pour ce tutoriel :
Il nous faut la version source du paquet UIMA Annotator Addons & Simple Server & Pear packaging tools. Une fois téléchargée, décompressez là quelque part et placez-vous dans le répertoire uimaj-annotator-addons-2.3.0-incubating/WhitespaceTokenizer/.
Il nous faut modifier un peu le pom.xml afin d'y ajouter les dépôts qui sont normalement déclarés dans le pom parent :
... <repositories> <repository> <id>apache</id> <name>Apache UIMA</name> <layout>default</layout> <url>http://people.apache.org/repo/m2-incubating-repository/</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>apache</id> <name>Apache UIMA</name> <url>http://people.apache.org/repo/m2-incubating-repository/</url> <layout>default</layout> </pluginRepository> </pluginRepositories> ...
Il est alors possible de créer le PEAR avec maven en lançant dans le répertoire du WhitespaceTokenizer :
$ mvn package
Le pear est alors créé dans le répertoire target/ sous le nom WhitespaceTokenizer.pear. Il faut l'installer à l'aide du PearInstaller.
Une fois le PEAR installé, il faut créer un répertoire processing dans le projet du CAS Editor, y importer le descripteur PEAR, l'intégrer à un composant Agregate. On peut alors le faire tourner sur une partie du corpus.
La vidéo ci-dessous présente ces dernières phases :
Le but du CAS Editor est tout de même de pouvoir visualiser et manipuler les annotations, ce qui se fait dans l'éditeur.
La visualisation des annotations est configurable par le menu contextuel Show annotations où l'on sélectionne les annotations à afficher. Le mode de mise en valeur de ces dernières se configure dans les propriétés du projet.
Le parcours des annotations s'opère de plusieurs manières :
La vidéo ci-dessous illustre ces différentes manipulations :
Wikipedia est une incroyable source d’information, de données et plus généralement d’actes langagiers (utilisation du langage). C'est une ressource sans équivalent pour les chercheurs en traitement automatique des langues (TAL).
Le MediaWiki UIMA Loader est un composant UIMA, de type collection reader, permettant de tirer parti de Wikipédia pour la construction de corpus. La version 0.4 est la première release officiellement annoncée du composant.
Pour les impatients :
Le composant MediaWiki UIMA Loader est un collection reader permettant de charger des données issues d'un MediaWiki, notamment Wikipédia et ses projets dérivés ...
Le composant est distribué sous licence Apache 2. Vous pouvez donc l'utiliser dans le cadre d'un travail académique ou commercial. Dans les deux cas, si vous trouvez le composant utile, n'hésitez pas à me dire ce que vous en pensez, si vous souhaitez de nouvelles fonctionnalités ou si vous rencontrez des bugs.
Contrairement à plusieurs projets existant, le composant n'attaque pas directement les sites Wikipedia. Il ne nécessite pas non plus de créer un miroir local de la base de données MediaWiki. Il travaille directement à partir des dumps XML, ce qui présente les avantages suivant :
Les fonctionnalités de cette version 0.4 sont les suivantes :
Avant d'installer et d'utiliser le composant, il est nécessaire d'avoir un environnement UIMA installé. Si ce n'est pas le cas, se référer à ce tutoriel.
Le plus simple est de récupérer le jar du composant dans l'espace de téléchargement de uima-fr, ainsi que les dépendances : mwdumper et wikimodel.wem.
Si vous souhaitez reconstruire le jar vous mêmes, il vous faut télécharger les sources du composant, toujours dans l'espace de téléchargement de uima-fr,et les compiler à l'aide de maven :
$ tar -xzvf mediawiki-uima-loader-0.4.1.tar.gz ... $ cd mediawiki-uima-loader-0.4.1 $ mvn package ...
Le jar devrait être créé dans le répertoire target/, les dépendances quant à elles auront été téléchargées dans votre dépôt maven local.
Vous pouvez utiliser le composant dans n'importe quelle chaîne de traitement UIMA, de la même façon que vous utilisez un composant classique de type collection reader. La démarche ci-dessous concerne l'utilisation de l'outil cpeGui, mais elle devrait être similaire pour les autres outils du même type.
Le cpeGui n'est pas capable en l'état de charger un descripteur xml depuis un jar. Avant tout, il est donc nécessaire d'extraire le descripteur du composant du jar afin de le rendre accessible. Si vous avez compilé le composant vous même, le descripteur est présent dans le répertoire desc. Sinon, il suffit de l'extraire du jar :
$ jar -x wikipedia-cr.xml -f mediawiki-uima-loader-0.4.1.jar
Il est nécessaire de rajouter le jar du composant et de ses dépendances dans le UIMA_CLASSPATH, avant de lancer le cpeGui en ligne de commande. Pour l'exemple, nous considérerons que le jar du composant est dans le répertoire courant et que les dépendances sont dans le dépôt maven local :
$ export UIMA_CLASSPATH=$UIMA_CLASSPATH:~/.m2/repository/org/wikimedia/mwdumper/1.16/mwdumper-1.16.jar:~/.m2/repository/org/wikimodel/org.wikimodel.wem/2.0.7-SNAPSHOT/org.wikimodel.wem-2.0.7-SNAPSHOT.jar:mediawiki-uima-loader-0.4.1.jar $ cpeGui
Dans la partie de l'interface dédiée au Collection Reader, cliquez sur Browse et allez sélectionner le descripteur du composant que nous avons extrait du jar (wikipedia-cr.xml). L'interface se modifie afin d'offrir les champs de paramétrage du composant.
Le seul paramètre obligatoire est le champs Input Xml Dump. Vous devez renseigner dans ce dernier le chemin menant au dump XML de Wikipedia (ou tout autre dump MediaWiki) que vous souhaitez charger. Par exemple : ~/frwiki-20100111-pages-meta-history.xml.bz2. Le composant est capable de lire un dump, qu'il soit compressé ou non.
Les autres paramètres concernent le filtrage à mettre en place lors du chargement des données :
Par exemple pour prendre en considération uniquement toutes les pages de discussion : 1,3,5,7,9,11,13,15,101,103,105, ou bien pour prendre en compte tous les espaces de nom excepté celui des catégories : !14 ;
Une fois le composant paramétré, il suffit de renseigner les autres composants de la chaîne comme vous le faites habituellement et de lancer l'exécution.
Attention, si vous exportez le contenu traité par le composant, à l'aide du composant XmiWriter par exemple, à partir d'un dump compressé, prenez en compte que le volume de données risque d'être 20 à 100 fois supérieures à la taille originale du dump. Ainsi, il faut compter une vingtaine de Go minimum pour la version française de Wikipédia en ne considérant que les dernières révisions des articles.
La création de composants UIMA permettant d'accéder et tirer parti de Wikipédia offrirait de nouvelles perspectives au traitement des langues en offrant un accès aisé à cette formidable ressource que représente l'encyclopédie libre. Je compte m'atteler à la création de tels composants et vais tâcher de publier plusieurs billets décrivant ma démarche en cours.
Voici le cinquième billet, plus orienté technique de développement, qui discute de la gestion du projet avec Maven, permettant notamment de gérer automatiquement les dépendances à MWDumper et à Wikimodel.
Maven est une sorte de super-gestionnaire de projet qui peut se charger d'à peu près tout : dépendances, compilation, packaging, lancement des tests unitaires, ... L'outil est disponible sur la plupart des distribution, pour ma part sous Ubuntu :
$ sudo aptitude install maven2
L'objectif de ce billet est d'expliquer comment, à l'aide de notre génial ingénieur de recherche, j'ai pu utilisé maven pour gérer la construction du collection reader pour Wikipedia.
La première complication lorsque l'on souhaite compiler le collection reader ce sont les dépendances, il y en a trois :
Par chance tous ces projets sont déjà gérés par maven ce qui va fortement nous faciliter la tâche. Les étapes à suivre sont les suivantes :
Ainsi pour mwdumper :
$ svn co http://svn.wikimedia.org/svnroot/mediawiki/trunk/mwdumper ... $ cd mwdumper $ mvn compile ... $ mvn install ...
Une fois ces étapes terminées, un nouveau dossier doit apparaître dans votre dépôt local : ~/.m2/repository/org/wikimedia/mwdumper/ ; il doit contenir un dossier correspondant à la version compilée (1.16 pour moi) et dans ce dossier les fichiers suivants :
Il faut procéder de la même manière pour WikiModel :
$ svn co http://wikimodel.googlecode.com/svn/trunk/org.wikimodel.wem ... $ cd org.wikimodel.wem $ mvn compile ... $ mvn install ...
Vous devriez de la même manière voir apparaître dans votre dépôt local : ~/.m2/repository/org/wikimodel/org.wikimodel.wem/ ; il doit contenir une structure similaire.
Ces étapes sont nécessaires pour la suite car elles placent les dépendances dans le dépôt local de maven, là où il ira les chercher lors de la compilation.
Revenons maintenant au composant UIMA. L'intelligence de Maven se configure dans un fichier à la racine du projet et nommé pom.xml.
Dans un premier temps, il faut définir le projet en lui donnant :
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd" > <modelVersion>4.0.0</modelVersion> <groupId>uima.wikipedia</groupId> <artifactId>uima-mediawiki-loader</artifactId> <version>0.4</version> <packaging>jar</packaging> <name>MediaWiki UIMA Loader</name> <description>This is a UIMA Collection Reader for the MediaWiki dumps (Wikipedia & co).</description> <licenses> <license> <name>Apache 2</name> <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> </license> </licenses> ... </project>
Dans un second temps, nous définissons les dépendances nécessaires à la construction de notre composant. Comme nous l'avons vu précédemment, il y en a plusieurs :
Les dépendances se déclarent entre les tags <dependencies>, en indiquant notamment la version nécessaire.
... <dependencies> <!-- Required to generate the Java classes --> <dependency> <groupId>org.apache.uima</groupId> <artifactId>uimaj-core</artifactId> <version>2.3.0-incubating</version> <scope>compile</scope> </dependency> <!-- DocumentAnnotation UIMA Type --> <dependency> <groupId>org.apache.uima</groupId> <artifactId>uimaj-document-annotation</artifactId> <version>2.3.0-incubating</version> <scope>compile</scope> </dependency> <!-- WikiModel --> <dependency> <groupId>org.wikimodel</groupId> <artifactId>org.wikimodel.wem</artifactId> <version>2.0.7-SNAPSHOT</version> </dependency> <!-- WikiMedia Dumper --> <dependency> <groupId>org.wikimedia</groupId> <artifactId>mwdumper</artifactId> <version>1.16</version> </dependency> </dependencies> ...
Comme vous l'avez certainement remarqué, on a précisé que le projet dépendait d'UIMA, mais contrairement aux dépendances sur MWDumper et WikiModel, nous n'avons pas installé ces dernières dans le dépôt maven local. En fait Maven va être capable d'aller chercher tout seul ces dépendances grâce au dépôt maven mis en ligne par Apache UIMA. Il suffit de préciser l'existence de ce dépôt :
... <repositories> <!-- Apache UIMA repository --> <repository> <id>apache</id> <name>Apache</name> <url>http://people.apache.org/repo/m2-incubating-repository</url> </repository> </repositories> ...
Finalement, on précise le processus de construction du composant entre les tags <build>. Dans nos cas cela revient tout simplement à préciser où aller chercher les sources, où placer les fichiers compilés et préciser ce qui doit être considérés comme des ressources et donc placé dans le Jar en plus des classes.
On utilisera de plus le plugin maven-compiler-plugin afin de préciser la version de Java que l'on souhaite pour la compilation.
... <build> <plugins> <!-- Java Compiler --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.0.2</version> <configuration> <source>1.5</source> <target>1.5</target> </configuration> </plugin> </plugins> <sourceDirectory>src</sourceDirectory> <outputDirectory>bin</outputDirectory> <resources> <resource> <directory>desc</directory> </resource> </resources> </build> ...
Le pom.xml est suffisant à cette étape pour permettre de lancer la compilation du projet et le packaging.
$ mvn compile ...
Vous devriez constater que le dossier bin s'est peuplé des classes compilées.
$ mvn package ...
Vous devriez maintenant constater l'apparition d'un dossier target dans lequel vous trouverez notamment un Jar nommé : uima-mediawiki-loader-0.4.jar. Et voilà, dans l'état il est possible d'obtenir un Jar du projet à partir des sources sans trop de problèmes. Toutefois la première étape de récupération des dépendances me paraît trop contraignantes. Il est possible, à l'instar d'Apache UIMA, de mettre en place un dépôt contenant des versions compilées des dépendances afin que maven aille directement les chercher.
Un dépôt maven ce n'est ni plus ni moins qu'un système de fichiers respectant une certaine structure et accessible par http (par exemple).
J'ai créé un dossier sur mon serveur que j'ai rendu accessible par http à l'aide d'Apache, puis j'y ai collé l'arborescence concernant mwdumper et wikimodel quiu était présente dans mon dépôt local :
Il y a une petite nuance tout de même, lorsque le dépôt est distant, les fichiers doivent être accompagnés de leurs checksums afin de vérifier que le téléchargement s'est bien déroulé. Le plus simple pour générer ces fichiers de checksums est de réitérer l'installation dans le dépôt maven local avec une option supplémentaire :
$ mvn install -DcreateChecksum=true ...
Vous trouverez alors dans le dépôt local les fichiers en *.md5 et *.sha1 qu'il faut également transférer sur le serveur.
Une fois que le dépôt distant est mis en place, il suffit de le déclarer dans le pom.xml :
... <repository> <id>uima-fr.org</id> <name>UIMA Fr</name> <url>http://www.uima-fr.org/m2-repo/</url> </repository> ...
Il est maintenant possible de compiler le composant sans avoir à récupérer les dépendances en amont. Tout se fait automatiquement et de manière transparente pour l'utilisateur... c'est assez plaisant.
Comme je suis un fainéant, je trouve que ce serait cool de pouvoir déployer automatiquement mon composant sur mon dépôt, c'est tellement pratique les dépôts !
J'ai choisi de pouvoir déployer mon composant sur le dépôt par ssh, pour des questions de sécurité. Mais il est également possible de le faire par ftp. Le déploiement par ssh nécessite tout d'abord de pouvoir se connecter automatiquement au serveur par ssh à l'aide d'un échange de clé, puis il suffit de renseigner dans le pom.xml l'adresse du dépôt et dans ~/.m2/settings.xml de préciser les modalités de connexion au dépôt.
Je ne vais pas détailler ici comment déployer un serveur ssh et faire tourner un ssh-agent en local, il y a tout un tas de tutoriels disponibles sur internet pour ça. Ce qu'il faut juste retenir, c'est que maven ne se connectera au serveur ssh si les deux conditions suivantes sont remplies :
Le plus simple pour s'assurer de tout cela est de se connecter directement manuellement au serveur :
Copiez donc le contenu de votre clé publique que vous trouverez dans le fichier ~/.ssh/id_dsa.pub ou bien ~/.ssh/id_rsa.pub. Elle doit ressembler à quelque chose comme ceci :
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzqedhFIi8hy743U7pEvLMQvCEeAo/CxmLjF4jF2WguguN+U/4GsJrONvgoWMYXRn0zVMoHNpCEXQ+BT80ZTnv+MILu5elgFsE18bFA+7qjd454LwuZpoIoJOsCNyJKyGjy7ER5cZGN/z8G6cmSJTGauc270W7WJQELqKM3rfqPJH4FXPF9+WDP4UK/o7k54g36/3hHeBmqW++mpyEwkm0eT+GlBRlmP4NjVJACMoyYwl2S1Ep/m85aYR+95m3neHFZpUPmEyN52/Sod7ak28AHZ0M5oE/nRoUr1AAc0LzJw7BM327fAO6o7iHcfoIdo7pix2KLoteqT8tQIRQUmzxQ== grdscarabe@grdscarabe-desktop
Attention, cette clé se trouve sur le poste client à partir duquel maven se connaîtra !
Copiez donc cette clé dans le fichier ~/.ssh/authorized_keys, si ce fichier n'existe pas créez-le. Faites bien attention à ce qu'il soit dans le répertoire personnel de l'utilisateur que maven utilisera pour se connecter (le votre très certainement).
Une fois cette opération effectuée, vous devriez pouvoir vous connecter au serveur sans que ce dernier ne vous demande de mot de passe. Il se peut que si vous ayez protégé votre clé privée par un mot de passe, le ssh-agent vous le demande. Dans ce cas vous êtes de mon point de vue suffisamment au courant du problème pour ne pas lire cette section. Sinon vous êtes bien malin d'avoir mis un mot de passe :)
Il suffit de déclarer dans le dépôt dans le pom.xml, nous l'appellerons ici uimafr-repository. Il est également nécessaire de charger l'extension wagon-ssh-external qui permet à maven de déployer par ssh :
... <extensions> <!-- Enabling the use of SSH --> <extension> <groupId>org.apache.maven.wagon</groupId> <artifactId>wagon-ssh-external</artifactId> <version>1.0-beta-6</version> </extension> </extensions> <distributionManagement> <repository> <id>uimafr-repository</id> <url>scpexe://www.uima-fr.org//home/www-data/org_uima-fr_www/m2-repo</url> </repository> </distributionManagement> ...
Enfin, il faut localement configurer maven pour reconnaître le dépôt uimafr-repository. Cette configuration se fait par le fichier ~/.m2/settings.xml. S'il n'existe pas, créez-le puis copiez-collez y le contenu suivant :
<settings> <servers> <server> <id>uimafr-repository</id> <configuration> <sshExecutable>ssh</sshExecutable> <scpExecutable>scp</scpExecutable> </configuration> </server> </servers> </settings>
En gros ce dernier permet de préciser quels sont les outils à utiliser pour la connexion ssh. Sous linux, nous utiliserons les outils ssh pour la connexion et scp pour le transfert de fichier.
Et voilà, maintenant pour déployer mon composant sur le dépôt, il me suffit de faire :
$ mvn deploy ...
Elle est pas belle la vie ?
Il y a bien des extensions possibles pour rendre maven encore plus pratique pour la gestion de ce projet. J'en vois notamment deux :
Dans le premier cas, la solution au problème doit certainement se trouver du côté du ''exec-maven-plugin''. Dans le second cas, il faudrait aller voir du côté de ''maven-assembly-plugin''.
Et je conclus ce looong billet en distribuant la nouvelle version, estampillée 0.4, du collection reader pour Wikipedia : par ici ! Vous trouverez les dépendances nécessaires à son fonctionnement dans le dépôt maven de uima-fr.
Vous pouvez tester le composant à l'aide du cpeGui :
$ UIMA_CLASSPATH=~/.m2/repository/uima/wikipedia/mwuima-loader/0.4/mwuima-loader-0.4.jar:~/.m2/repository/org/wikimedia/mwdumper/1.16/mwdumper-1.16.jar:~/.m2/repository/org/wikimodel/org.wikimodel.wem/2.0.7-SNAPSHOT/org.wikimodel.wem-2.0.7-SNAPSHOT.jar cpeGui
Bien sûr, si vous n'avez pas installé le composant avec maven, il faudra modifier en conséquence les chemins du UIMA_CLASSPATH.
Wikipedia est une incroyable source d'information, de données et plus généralement d'actes langagiers (utilisation du langage). Ce dernier point est très important pour nous autres chercheurs en traitement automatique des langues. En effet, nous avons besoin d'observer de très nombreux exemples d'utilisation du langage, que ce soit de manière automatique ou manuelle. Pour ce faire, nous compilons de vastes exemples d'utilisation du langage sous forme de corpus.
La création de composants UIMA permettant d'accéder et tirer parti de Wikipédia offrirait de nouvelles perspectives au traitement des langues en offrant un accès aisé à cette formidable ressource que représente l'encyclopédie libre. Je compte m'atteler à la création de tels composants et vais tâcher de publier plusieurs billets décrivant ma démarche en cours.
Voici le quatrième billet qui discute de l'analyse syntaxique du contenu des pages wiki pour la suppression des balises Wiki.
Il existe plusieurs initiatives de programmes permettant d'interpréter la syntaxe de MediaWiki. Il y a le code PHP utilisé par MediaWiki et qui sert de référence, mais également plusieurs autres initiatives notamment en Java.
La plupart des initiatives Java ont pour objectif de produire des version XHTML ou PDF des pages en syntaxe wiki et ne donnent pas accès à l'arbre syntaxique de la page. Un programme en C basé sur Flex/Bison et développé dans le cadre du projet MediaWiki offre cette possibilité.
Parmi les initiatives en Java qui semblent intéressantes pour le collection reader UIMA :
J'ai tout d'abord essayé Bliki pour traiter la syntaxe de MediaWiki. Ce dernier s'est toutefois avéré assez complexe, notamment au niveau de son architecture, de telle façon que je n'ai pas réussi à obtenir ce que je voulais. Je me suis alors tourné vers WikiModel qui offre un analyseur syntaxique (Wiki Event Model (WEM)) au fonctionnement proche de SAX.
Le gros avantage de WikModel est qu'il offre directement un parseur pour MediaWiki : org.wikimodel.wem.mediawiki.MediaWikiParser. Le parseur analyse du code Wiki brute et, comme un parseur SAX classique, lance des évènements à une instance implémentant l'interface org.wikimodel.wem.IWemListener. J'ai choisi de créer une classe MediawikiCasConverter implémentant cette interface et gérant elle même le lancement du parseur sur les textes brutes extraits des révisions.
import org.wikimodel.wem.mediawiki.MediaWikiParser import org.wikimodel.wem.IWemListener; ... public class MediawikiCasConverter implements IWemListener { public MediawikiCasConverter() { theParser = new MediaWikiParser(); } public void runParser(String rawWikiText) throws WikiParserException { // We use a string reader to parse the raw wiki texte StringReader reader = new StringReader(rawWikiText); // Parsing theParser.parse(reader, this); } // IWemListener methods below ... }
Les méthodes du IWemListener sont de deux types. Les premières sont des méthodes one shot, ie elles sont appelées une seule fois lorsqu'un élément est rencontré. Ce sont notamment celles qui concernent le texte et leur mise en forme :
Les secondes sont des méthodes à la SAX en deux temps : begin et end. Ce sont notamment celles qui concernent la structuration du texte :
La plupart de ces méthodes ne font que des appels à une fonction de plus haut niveau qui collecte les données textuelles de la page et qui maintient l'index du CAS : addToContent. C'est notamment le cas pour la méthode onWord qui est appelée lorsque le parseur rencontre un nouveau mot :
/** * This method adds the string in parameter into the collected content * and then increment the offset by the size of this string. * * @param str the string to be added to the content */ protected void addToContent(String str) { if ( str != null ) { theTextContent.append(str); theOffset += str.length(); } } ... /** Called when a word is encountered */ public void onWord(String str) { addToContent(str); }
J'ai choisi de transposer une partie des informations contenues dans les pages de Wikipedia sous la forme d'annotations. J'ai ainsi ajouté les types :
En ce qui concerne les liens, j'ai choisi pour le moment d'ignorer les images qui me semblent apporter plus de bruit qu'autre chose.
La mise en place de ces annotations se fait assez facilement :
Cette approche permet de dédier toute la partie analyse à une classe. Si jamais l'on veut ignorer certaines annotations (ou toutes), il suffit de les filtrer au moment de leur récupérer par getAnnotations. L'extrait de code ci-dessous illustre ce fonctionnement pour les titres :
/** * When we encounter a new header, we create an annotation for it. */ public void beginHeader(int headerLevel, WikiParameters params) { // Jump a line addToContent(" "); // Create the annotation Header header = new Header(theCas); header.setLevel(headerLevel); header.setBegin( theOffset ); // Add it to the list theHeadersAnnotations.add( header ); // Add it as header of the last unclosed section if ( theUnclosedSections.size() > 0 ) { Section section = theUnclosedSections.get( theUnclosedSections.size()-1 ); section.setTitle(header); } } /** * Add the ending value of the last started header. */ public void endHeader(int headerLevel, WikiParameters params) { // Retrieve the last header Header header = theHeadersAnnotations.get( theHeadersAnnotations.size()-1 ); // Update its ending value header.setEnd( theOffset ); // Jump a line addToContent(" "); }
Je me suis rendu compte que le parseur posait des problèmes (heap overflow) sur des pages de certains espaces de noms. J'ai donc choisi de n'appliquer l'analyse syntaxique qu'aux pages de l'espace de nom 0 (espace de nom principal). Ceci est toutefois facilement modifiable dans la méthode uima.wikipedia.mwdumper.ThreadedMWDumper.writeStartPage.
Il reste pas mal d'améliorations possibles : prendre en compte toutes les mises en formes/structurations, prendre en charge les macros, rendre l'analyse syntaxique configurable. Toutefois, le composant est dans l'état actuel aussi complet et performant que je le souhaitais à l'origine. Il ne reste plus qu'à le tester de manière intensive. J'ai d'ailleurs deux trois cobayes en tête pour ça.
Pas de nouvelle version du composant pour l'instant. Étant donné que les dépendances se sont multipliées, et que le composant est désormais suffisamment fonctionnel pour se voir estampillé 1.0 ou quelque chose du genre, je vais travailler un peu plus son packaging.
Wikipedia est une incroyable source d'information, de données et plus généralement d'actes langagiers (utilisation du langage). Ce dernier point est très important pour nous autres chercheurs en traitement automatique des langues. En effet, nous avons besoin d'observer de très nombreux exemples d'utilisation du langage, que ce soit de manière automatique ou manuelle. Pour ce faire, nous compilons de vastes exemples d'utilisation du langage sous forme de corpus.
La création de composants UIMA permettant d'accéder et tirer parti de Wikipédia offrirait de nouvelles perspectives au traitement des langues en offrant un accès aisé à cette formidable ressource que représente l'encyclopédie libre. Je compte m'atteler à la création de tels composants et vais tâcher de publier plusieurs billets décrivant ma démarche en cours.
Voici le troisième billet qui discute du filtrage des données à charger : articles, révision, ...
L'outil mwdumper inclut un certain nombre de filtres commandables à partir de la ligne de commande :
Voir le billet Insérer plusieurs copies locales de Wikipedia dans une base PostgreSQL pour un cas d'utilisation de ces filtres. La combinaison de ces filtres permet de contrôler avec précision les données à charger, nous allons donc nous en contenter.
On peut classer les filtres en deux familles : ceux qui nécessitent des paramètres et ceux qui n'en nécessitent pas. J'ai donc choisi pour le moment d'utiliser des paramètres booléens pour les filtres sans paramètre, et des paramètres chaînes de caractères pour les autres.
L'activation des paramètres booléens suffit à activer les filtres correspondant : IgnoreTalks et LatestRevisionOnly.
Pour activer les autres filtres il suffit de les configurer en renseignant les champs dédiés. Si les champs restent non renseignés alors les filtres ne sont pas activés : ConfigNamespacesFilter, ConfigTitleMatch, ConfigListFilter, ConfigExactListFilter, ConfigRevisionListFilter, ConfigBeforeTimestampFilter, ConfigAfterTimestampFilter.
Voici une nouvelle version du collection reader pour Wikipédia, estampillée 0.2, qui ne prend toujours pas en compte la syntaxe, mais qui permet de filtrer le type de contenu à charger à partir du dump XML. Pour utiliser le composant, il est nécessaire d'avoir le jar de mwdumper dans le classpath.