jeudi 5 janvier 2012

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();}

Aucun commentaire:

Enregistrer un commentaire