lundi 16 janvier 2012

Animation des rencontres "Outils et instruments logiciels pour le TAL" au sein de l'équipe TALN

Depuis septembre 2010, j'anime au sein de l'équipe des rencontres "Outils et instruments  logiciels pour le TAL" connues aussi sous le nom de "réunions logiciels".

J'utilise ce post pour rappel du principe et les commentaires pour présenter les différentes interventions qui ont lieu.

La motivation de ces rencontres sont l'échange de connaissances
et l'entraide pour la prise en main d'un nouveau logiciel.

L'objectif pour le présentateur est simple : faire connaître un
logiciel et donner un aperçu de l'utilisation de celui-ci. La présentation prend la forme d'une
démonstration d'un cas d'utilisation. Suivant le logiciel, cela prend
15 à 30 minutes questions comprises.

Il faut prendre logiciel (au sens très large) [2] : cela peut concerner un
développement personnel, une bibliothèque, une application, un analyseur
particulier de données...

Je vous invite à vous proposer pour présenter un logiciel que vous pensez être
pertinent pour l'équipe (même si vous n'en connaissez que quelques fonctions),
ainsi qu'à émettre des demandes de démonstration. Je me propose de
coordonner pour trouver un intervenant dans le cas de demandes. On pourra
discuter plus tard sur les modalités d'ouverture de ces réunions à des
participants hors équipe.

Les rencontres sont au rythme d'une demi-heure tous les mois voire toutes les 6 semaines.
Elles sont planifiées sur l'agenda de l'équipe (aussi consultable en ligne sur [1]).

[1] Agenda des réunions de l'équipe TALN
[2] Instrument, outil, utilitaire et ressource
http://www.revue-texto.net/Corpus/Publications/Habert/Habert_Portrait.html#2.1.

jeudi 5 janvier 2012

Définition d'un descripteur primitif d'Analysis Engine



La dernière étape consister à créer un descripteur indiquant au framework UIMA comment utiliser le composant. En particulier il définit la classe métier de l'Annotator, le Type System manipulé, les Types qui sont utilisés comme input et ceux qui seront des output. C'est aussi ici que sont déclarés les paramètres et les ressources partagées utilisés si il y a lieu.

La création du descripteur du composant est facilitée à travers Eclipse et les plugins installés. Dans le répertoire desc/opinionRecognizer du projet (ou seulement desc si vous n'avez pas packagé votre projet) :
  • Créer le fichier descripteur du composant, opinionRecognizerAE.xml en cliquant dessus avec le bouton droit et New - Other - UIMA - Analysis Engine Descriptor File.
  • Sur la première page (onglet Overview accessible au bas du cadre) spécifier le nom de la classe qui implémente votre code métier (i.e. opinionRecognizer.OpinionRecognizerAE).
  • Sous l'onglet Type System, ajouter (Add) par nom (by name) le type system défini pour votre composant (i.e. desc/opinionRecognizer/opinionRecognizerTS.xml).
  • Enfin sous l'onglet Capabilities, spécifiez les types des annotations qui doivent apparaître en sortie (i.e. SentenceAnnotation, TokenAnnotation et Opinion).
Si en cliquant sur ce fichier pour l'ouvrir, vous n'accédez qu'à son contenu XML, alors demandez d'ouvrir avec le Component Descriptor Editor en cliquant sur le fichier avec le bouton droit.

Implémenter le code métier d'un composant Analysis Engine


La seconde étape correspond au développement du code métier du composant.

Le code métier d'un AE est à minima constitué d'une classe dite Annotator qui étend la classe JCasAnnotator_ImplBase du framework UIMA. En particulier, on trouve le code métier dans une méthode surchargée appelée process qui est automatiquement appelée à l'exécution pour chaque CAS traitée. La méthode process a accès au CAS et via l'API d'UIMA elle peut manipuler l'artifact ou les (index d') annotations qui ont été précédemment ajoutées ou bien en ajouter de nouvelles.

La classe JCasAnnotator_ImplBase est une implémentation par défaut de la classe AnalysisComponent. Cette implémentation par défaut implémente toutes les méthodes exceptée la méthode process. En général on travaille directement à partir de celle-ci quitte à surcharger les méthodes déjà implémentées. 
La classe AnalysisComponent a plusieurs méthodes dont les plus importantes sont initialize, process et collectionProcessComplete. La méthode initialize est appelée une fois par le framework UIMA à la création de la première instance de la classe Annotator ; elle sert par exemple à récupérer la valeur de paramètres ou à charger des ressources qui seront partagées par les différentes instances de la classe. La méthode process est donc appélée une fois par item traité. La méthode collectionProcessComplete est appelée quand l'entière collection a été traitée et sert à produire des résultats relatifs à toute la collection.

Dans cet exercice, vous n'aurez besoin que d'implémenter la méthode process.

Création de la classe Annotator
  1. Dans le répertoire src, créer le package opinionRecognizer.
  2. Au sein du package, créer la classe OpinionRecognizerAE. Par convention les classes Annotator se termineront par le suffixe AE.
  3. Faire étendre la classe de JCasAnnotator_ImplBase
  4. et surcharger la méthode process.
Cela doit donner quelque chose comme :
package opinionRecognizer;
import
org.apache.uima.analysis_component.JCasAnnotator_ImplBase;
import org.apache.uima.jcas.JCas;
import opinionRecognizer.types.*;
public class OpinionRecognizerAE extends JCasAnnotator_ImplBase{
    public void process(JCas aJCas)  {
        // Faire quelque chose
    }
}
Création du code métier au sein de la méthode process

La méthode process reçoit en argument une instance de JCas laquelle constitue le document analysé ainsi que toutes les annotations qui y ont été associées lors d'éventuelles précédentes analyses. Le JCas fournit une approche JNI (Java Native Interface) pour la manipulation des objets CAS et de leur propriétés (i.e.~avec des new, des getter/setter, ... Le framework UIMA se charge de passer le CAS d'un composant à un autre.

Pour cet exercice, l'analyseur devra ajouter une nouvelle annotation Opinion délimitée aux offsets (begin et end) de toutes les phrases SentenceAnnotation contenant des mots TokenAnnotation qui sont des verbes (posTag débutant par "vb" égal à "bez") ou dont la forme de surface coveredText est "we" ou "our". On ajoutera aussi le nombre de mots contenu dans l'opinion length.

L'algorithme que je propose d'implémenter est le suivant :
  1. récupération d'un index de phrases SentenceAnnotation
  2. parcourir l'index de phrases et pour chacune, 
    1. récupérer un sous index de mots TokenAnnotation
    2. parcourir l'index de mots et pour chaque mot
      1. définir un booléen à vrai si un mot a son trait posTag débutant par "vb" égal à "bez"
      2. définir un booléen à vrai si un mot a son trait forme de surface coveredText est "we" ou "our".
      3. incrémenter un compteur de mots
    3. si les deux booléens sont vrais alors
      1. créer une nouvelle annotation Opinion aux offsets (begin et end) de la phrase courante
      2. initialiser les valeurs begin, end et length de cette annotation
La récupération d'un index de phrases SentenceAnnotation peut se réaliser à l'aide de la méthode getAnnotationIndex appliquée au JCas et qui prend en argument le type d'annotation souhaité. Le parcourir de l'index de phrases peut se réaliser à l'aider d'un Iterator
Cela doit donner quelque chose comme :
AnnotationIndex<annotation>  aSentenceAnnotationAnnotationIndex = aJCas.getAnnotationIndex(SentenceAnnotation.type);
Iterator<annotation> aSentenceAnnotationIterator = aSentenceAnnotationAnnotationIndex.iterator(); 
while (aSentenceAnnotationIterator.hasNext()) { 
    SentenceAnnotation aSentenceAnnotation = (SentenceAnnotation) aSentenceAnnotationIterator.next();
    // Faire quelque chose
}
L'ajout du précédent code lève des erreurs de dépendances non importées (AnnotationIndex, Iterator, SentenceAnnotation).  Elles se résolvent très facilement par un left-click sur la petite croix rouge dans la marge de gauche au niveau de chaque erreur détectée et en optant pour le bon import.
Cette action sera à reproduire avec le code à venir.

La récupération d'un index d'annotations couvertes par une autre annotation peut se faire à l'aide de la méthode subiterator qui s'applique sur un index d'annotations et qui prend en argument l'annotation recouvrante.
Elle requiert la création d'un index d'annotations. Pour notre besoin, un index de mots TokenAnnotation peut suffire. Il se construit comme l'index de phrases vu ci-dessus. On décide de récupérer l'index de toutes les annotations pour varier les plaisirs. Il faudra alors ne considérer que les annotations mots qui nous intéressent.
L'instruction suivante réalise cela. On la place au même niveau que la construction d'index de phrases.

AnnotationIndex<annotation> anAnnotationIndex = aJCas.getAnnotationIndex();


A l'intérieur de la boucle, pour chaque phrase on récupère un FSIterator (qui est un iterator un peu spécial sur ce type de structure) sur les annotations couvertes par la phrase courant aSentenceAnnotation.
Cela donne 
FSIterator<annotation> anySubSentenceAnnotationFSIterator = anAnnotationIndex.subiterator(aSentenceAnnotation);
while (anySubSentenceAnnotationFSIterator.hasNext()) {
   Annotation aSubSentenceAnnotation = (Annotation) anySubSentenceAnnotationFSIterator.next();
   // Faire quelque chose
}
Si l'annotation couverte courante aSubSentenceAnnotation est un TokenAnnotation alors on teste si un mot a son trait posTag débutant par "vb" égal à "bez" et si un mot a son trait forme de surface coveredText est "we" ou "our" et l'on compte le mot.
Cela correspond au code ci-dessous

if (aSubSentenceAnnotation.getClass().getName().equalsIgnoreCase("org.apache.uima.TokenAnnotation")) {
    TokenAnnotation aWord = (TokenAnnotation) aSubSentenceAnnotation;
    if (aWord.getPosTag().toLowerCase().startsWith("vb") || aWord.getPosTag().equalsIgnoreCase("bez")) {
        containsAVerb = true;
    }
    if (aWord.getCoveredText().equalsIgnoreCase("our") || aWord.getCoveredText().equalsIgnoreCase("we")) {
        containsAKeyword = true;
    }
    wordCounter++;
}

Pour que ce code fonctionne il faut rajouter quelques déclarations de variables avant la boucle de parcours des annotations couvertes par l'annotation phrase courante.

boolean containsAVerb = false;
boolean containsAKeyword = false;
int wordCounter = 0;
Pour chaque phrase aSentenceAnnotation parcourue on teste si les présences d'un mot clef et d'un verbe sont confirmées. Dans la positive, on créer une nouvelle annotation Opinion. On définit ses traits begin et end en fonction des begin et end de la phrase courante. On définit aussi la longueur length de l'opinion. Et au final on ajoute l'annotation ainsi créée à l'index des annotations à l'aide de la méthode addToIndexes.
Ce qui donne le code suivant :
if ((containsAKeyword) && (containsAVerb)) {    Opinion aOpinion = new Opinion(aJCas);    aOpinion.setBegin(aSentenceAnnotation.getBegin());    aOpinion.setEnd(aSentenceAnnotation.getEnd());    aOpinion.setLength(wordCounter);    aOpinion.addToIndexes();}

Définir les types de données (type system) à manipuler au sein d'un composant UIMA


La première étape pour développer un composant est de définir les types de données qu'il va manipuler. 

Création du descripteur et des types de données à manipuler 

En lien avec notre exercice, nous allons définir le type d'annotation opinionRecognizer.types.Opinion qui correspondra au type des annotations produites par notre composant d'analyse. On lui ajoutera un trait (featurelength qui indiquera le nombre de mots contenu dans l'annotation Opinion courante. Par simplicité nous ne rappellerons pas par la suite le nom de package (opinionRecognizer.types) qui précède le nom du type.

D'abord créons le fichier descripteur :
Les fichiers descripteurs de systèmes de types et d'AE se placent dans le répertoire desc. Pour s'y retrouver ultérieurement parmi les descripteurs qui seront disponibles dans le CLASSPATH, je vous conseille de créer un sous répertoire portant le nom de votre projet (en respectant les conventions de nommage Java), par exemple ici : opinionRecognizer.
  1. Faire un clic-droit sur le répertoire desc (ou sur le sous-répertoire que vous venez de créer si vous avez suivi mon conseil).
  2. Choisir le menu 'New'-'Other...'-'UIMA'-'Type System Descriptor File'.
  3. A l'écran suivant, donner un nom au fichier descripteur de types par exemple opinionRecognizerTS.xml. Là encore par convention et pour s'y retrouver plus tard je vous conseille d'utiliser le suffixe TS pour vos noms de fichier descripteur de systèmes de types.
Ensuite, une fois le fichier créé, ajoutons le type désiré  opinionRecognizer.types.Opinion : 
  1. Dans l'onglet Type System du descripteur, ajouter un type avec le bouton Add Type.
  2. Donner lui le nom de opinionRecognizer.types.Opinion
  3. Le faire hériter du type uima.tcas.Annotation défini par le framework UIMA et qui permet d'hériter des traits beginend et coveredText. Le trait coveredText est un trait un peu particulier parce que l'on ne peut y accéder qu'en lecture. Il retourne le texte de l'artifact couvert entre les offsets délimités par les valeurs entières begin et end.
Ajoutons aussi une feature au type :
  1. En sélectionnant le type, cliquer sur le bouton Add...
  2. Donner lui le nom de length qui indiquera le nombre de mots contenu
  3. Faire hériter du type simple Integer.
Puisque nous manipulons aussi les types mot, org.apache.uima.TokenAnnotation, et phrase, org.apache.uima.SentenceAnnotation, qui sont des uima.tcas.Annotation et le trait posTag (String) du type TokenAnnotation dont les annotations auront été produits par les composants de prétraitement, il nous faut aussi ajouter ces types. Le faire.

Génération automatique des l'API JAVA du système de types

Une fois le fichier de descripteur de systèmes de type créé et sauvé, une API java permettant de manipuler les types (et les traits) est automatiquement générée. 

Si jamais vous craignez que cette API ne soit pas générer ou que vous vouliez forcer sa re-génération, cliquer sur le bouton JCasGen présent dans l'éditeur du fichier Système de Type.

Les fichiers Opinion.java et Opinion_Type.java sont générés dans le sous répertoire  opinionRecognizer.types du répertoire src du projet. 
Le fichier  Opinion.java  contient le constructeur de type. Noter la présence d'accesseurs pour modifier la valeur des attributs du type Opinion (par exemple les méthodes getLength() et  setLength(int v)).

mercredi 4 janvier 2012

Développer son premier composant UIMA : l'Analysis Engine (AE)

Objectifs 
Ce post présente ce qu'est le composant élémentaire de toute chaîne de traitement d'analyse UIMA à savoir l'Analysis Engine (AE). Il explique de quoi il se compose techniquement, et comment développer ses différentes parties.
Il s'agit entre autres de prendre en main l'API et les outils UIMA pour réaliser des opérations classiques : telles que définir des types d'annotation, générer automatiquement l'API pour manipuler les types d'annotations définis, récupérer des annotations posées par d'autres annotateurs, consulter et définir les valeurs de traits d'une annotation, créer une nouvelle annotation, créer un fichier XML descripteur et tester un composant.
Pour illustrer cet exercice de développement on cherchera à développer la fonction de reconnaissance de phrases qui expriment une opinion. On posera l'hypothèse qu'une phrase qui contient des verbes et des mots dont la forme de surface comme "we" ou "our" est une expression d'opinion... On utilisera pour cela quelques AE disponibles dans les Apache Uima Addons. On testera l'AE sur les textes présents dans le répertoire UIMA_HOME/examples/data notamment Apache_UIMA.txt. De fait nous travaillerons sur l'anglais pour cet exercice.


Prérequis

Il est nécessaire d'avoir réaliser les tutoriels suivant :
Tel que l'explique ces précédents posts, il faudra avoir au préalable construit et avoir à disposition une chaîne que nous appellerons de pré-traitement et qui sera composée des Apache UIMA addons suivant : 
  • WhiteSpaceTokenizer qui découpe en mots, TokenAnnotation, et phrases, SentenceAnnotation, un texte fourni en entrée
  • Tagger qui rajoute un trait posTag aux TokenAnnotation
Qu'est ce qu'un composant UIMA de type Analysis Engine et à quoi cela sert ?
Un Analysis Engine (AE) est le composant élémentaire d'une chaîne de traitements UIMA. Il a généralement pour objet une tâche d'analyse (par exemple reconnaître si une phrase est une expression d'une opinion) sur ce que l'on appelle un artifact (par exemple un texte). Il reçoit les résultats d'analyse de précédents composants (par exemple un découpage en phrases, en mots, la reconnaissance des étiquettes grammaticales (nom, verbe, adjectif...)) qui lui servent de base pour ses traitements afin qu'il puisse à son tour associer à l'artifact le résultat d'analyse qu'il produit (par exemple les opinions). On appelle les résultats d'analyse, des meta-données ou des annotations. L'ensemble formé par l'artifact et les méta-données constitue la structure commune d'analyse (CAS ou Common Analysis Structure) et est LA structure de donnée qui transite d'un composant à un autre.

De quoi est constitué un composant UIMA ?
Un composant UIMA est logiquement constitué de trois éléments :
  • la définition des types des données que le composant manipule (lit et crée), on appelle cela le système de types (TS ou type system),
  • le code métier qui réalise la fonction souhaitée,
  • un descripteur indiquant au framework UIMA comment utiliser le composant.
Le système de types et le descripteur du composant sont des fichiers XML que l'on peut éditer avec des interfaces graphiques via les plugins Eclipse de UIMA. La définition du système de type entraîne la génération automatique d'une API pour manipuler les types de données définis au sein du code métier.

L'ensemble de ces éléments doit se trouver accessible via le CLASSPATH de l'application qui exécute. En général, on encapsule ces éléments au sein d'une même archive Jar.

Préparer un projet Java sous Eclipse pour accueillir le développement de votre composant

Il vous faut un projet Java sous Eclipse pour accueillir le développement de votre composant. Différentes options : 
  • vous pouvez utiliser celui précédemment créé pour la chaîne de prétraitement 
  • ou bien vous créez un nouveau projet comme indiquer ici mais il vous faudra alors déclarer une dépendance vers soit le projet qui contient votre chaîne de prétraitement soit vers le jar que vous aurez exporté à partir de celle-ci. Pour déclarer la dépendance au projet, Click droit sur le projet, Build path, Configure build path, Onglet Projects, Add les projets, Ok, OK...

Exécuter votre AE au sein d'un Aggregate

Au sein du répertoire desc/opinionRecognizer créer un descripteur aggregate qui ajoute en pipeline le descripteur de pretraitement et le descripteur de votre AE. N'oubliez pas de changer les capabilities.
Et exécutez le.


Pour aller plus loin
  • la création et la récupération de vues,
  • la définition des paramètres au sein du descripteur et leur accès au sein du code, 
  • la déclaration et la manipulation de ressources partagées par plusieurs instances d'un même AE,
  • le parcours d'index d'annotations à l'aide de contraintes
Remerciement
Le contenu décrit sur cette page s'inspire, adapte, étend le support créé pour le tutoriel qui a eu lieu à UIMA@RMLL'09 ainsi que le tutoriel Getting started de la documentation d'Apache UIMA.
Je vous invite à consulter ces différentes sources pour des informations complémentaires