Affichage des articles dont le libellé est uima. Afficher tous les articles
Affichage des articles dont le libellé est uima. Afficher tous les articles

mercredi 11 septembre 2013

Installer Apache UIMA Java (environnement d'exécution et de développement)

Il existe 3 implémentations concurrentes d'UIMA

  • UIMA Java (uimaj) (à préférer pour les débutants)
  • UIMA-AS (implémentation java à privilégier pour de l'asynchronous scaleout)
  • UIMA CPP (à préférer pour développer autrement qu'en Java)
Les outils Eclipse ne sont pas indispensables mais facilitent une activité de développement.

Installation le framework Apache UIMA Java et ses outils de développements

  1. Télécharger une archive d'un binaire de "UIMA Java framework & SDK" dans la section de téléchargement de UIMA (contient le nécessaire pour exécuter et développer des composants, des workflows et des applications UIMA en java)
  2. Désarchiver dans le répertoire d'installation de votre choix
  3. Fixer les variables d'environnement. Pour cela je suis les consignes du fichier README présent dans l'archive.
    1. Set JAVA_HOME to the directory of your JRE installation you would like to use for UIMA.
    2. Set UIMA_HOME to the apache-uima directory of your unpacked Apache UIMA distribution
      • export UIMA_HOME=/my/applications/apache-uimaj
    3. Afin d'exécuter les outils UIMA en ligne de commande, ajouter UIMA_HOME/bin to your PATH 
      • export PATH=${UIMA_HOME}/bin:$PATH
    4. Facultatif : afin de faciliter la configuration ultérieure des exemples, ajuster les exemples à votre répertoire d'installation (via l'exécution du script UIMA_HOME/bin/adjustExamplePaths.sh)
  4. Facultatif : Un test d'installation Afin de réaliser ce test consulter la page "Comment exécuter des chaînes de traitement avec UIMA " notamment la section "Exécution d'une chaîne de traitement en ligne de commande".

Installer les outils Eclipse pour assister le développement (via plugins)

  1. Indiquer à Eclipse, le site où récupérer des plugins liés à UIMA 
    • Help > "Software updates | Install New software" > Add 
    • Name : "Apache UIMA" ; URL http://www.apache.org/dist/uima/eclipse-update-site
  2. Indiquer lui ensuite le site avec lequel vous voulez travailler. Attendez quelques instants et le système vous proposera une liste proche de celle-ci
    • Apache UIMA Eclipse tooling 
    • Apache UIMA Ruta 
    • Apache UIMA-AS (Asynchronous Scaleout) Eclipse tooling
  3. Cocher les plugins que vous voulez ajouter puis "Next". Certains peuvent ne pas vouloir s'installer. Les Eclipse tooling sont la priorité. UIMA-AS tooling seulement si vous voulez faire du AS. Ruta si vous savez que vous voulez faire du Ruta.
    • Si vous rencontrez des problèmes : Commence une étape de résolution de dépendances, il est possible qu'il faille cocher plus de sites afin de permettre cette résolution, il est possible qu'il faille faire des install un à un des sites, et il est possible qu'il faille commencer par ceux natifs d'eclipse. Quelques refresh.
  4. Accept the licence
  5. Il se peut qu'il vous signale que vous voulez installer un "Unsigned content", accepter...

Configurer son environnement au sein d'Eclipse


  1. Définir la variable UIMA_HOME au sein d'Eclipse
    • Cliquer onglet Window > Preferences > Java > Build Path > Classpath Variables > Faire New (Name: UIMA_HOME et Path: Folder... > vers "applications/apache-uima") > Ok > Ok > Yes (rebuild)
  2. Importer le répertoire de codes exemples UIMA_HOME/examples au sein d'Eclipse
    • Cliquer onglet File > Import ; Sélectionner "General/Existing Project into Workspace" ; Cliquer "Next" ; Cliquer "Browse" et naviguer jusqu'au répertoire UIMA_HOME
  3. Facultatif : Un test d'installation au sein d'Eclipse Afin de réaliser ce test consulter la page "Comment exécuter des chaînes de traitement avec UIMA " notamment la section "Exécuter une chaîne de traitement au sein d'Eclipse". 

Installer les composants et les outils tiers présents dans la sandbox d'UIMA (addons et tools)


Références



Liens externes 

mercredi 19 décembre 2012

Apache UIMA HMM Tagger FR Models

Download here: Models for the Apache UIMA Hidden Markov Model Tagger Annotator [1] (from the sandbox UIMA Addons)

The models concern the following tasks:
* Part of speech tagging (POS)
* Grammatical subcategorization (Subcat)
* Morphological inflection analysis (Mph) 
* Lemmatization(canonical form) 
* Ee analysis (POS + Subcat + Mph)

Models have been built with the addon's version 2.4 using the French Treebank corpus [2] (version 2010). The ftb licence does not prevent to distribute analysis results under whatever licence but it mentions that the ftb should be used only for research purpose.Consequently we restrict the use of these models only for research purposes.

To get the '.dat', unzip and have a look to the '/HMMTrainerTagger/french/' dir

[1] http://uima.apache.org/sandbox.html#tagger.annotator
[2] For more on the French Treebank, see Abeille, A., L. Clement, and F. Toussenel. 2003. `Building a treebank for French', in A. Abeille (ed) Treebanks , Kluwer, Dordrecht. http://www.llf.cnrs.fr/Gens/Abeille/French-Treebank-fr.php

Apache OpenNLP FR Models


Download here: the last version of the models for processing several common Natural Language Processing tasks in French with Apache OpenNLP  [1]

The models concern the following tasks: Sentence segmentation, Word tokenization, Part-of-Speech Tagging, Morphological inflection analysis*, Lemmatization*, Chunking, Person|Organization|Location Name Entity recognition**

Except for Named Entity models, models have been built using the French Treebank corpus [2] (version 2010). Its licence does not prevent to distribute its analysis results under whatever licence but it mentions that the ftb should be used only for research purpose. Consequently we restrict the use of these models only for research purposes. 


* To be used with the tagger 

** Named Finder Models have been built by Olivier Grisel. See for more detail [3]. 

To get the '.bin' files, unzip and have a loot at the '/opennlp/models/fr/' dir. 

[2] For more on the French Treebank, see Abeille, A., L. Clement, and F. Toussenel. 2003. `Building a treebank for French', in A. Abeille (ed) Treebanks , Kluwer, Dordrecht. http://www.llf.cnrs.fr/Gens/Abeille/French-Treebank-fr.php 

UIMA AS (Asynchronous Scaleout) Tutorial?


At the bottom of the Getting Started: Apache UIMA Asynchronous Scaleout [1] page, you find the following mention
See the README file [3] in the top level directory for instructions on deploying and testing standard UIMA example annotators as UIMA AS services.
Download for example the binary version of UIMA AS [2] and you fill find out the README in the top level directory. It contains various information for Installation and Setup, and examples for Starting the ActiveMQ Broker,  Deploying an Analysis Engine as a UIMA AS Asynchronous Service, Calling a UIMA AS Asynchronous Service, Migration from CPM to UIMA-AS...

Again the direct link to this README file [3].

[1]: http://uima.apache.org/doc-uimaas-what.html
[2]: http://uima.apache.org/downloads.cgi
[3]: http://svn.apache.org/repos/asf/uima/uima-as/trunk/README

mercredi 21 novembre 2012

Accéder et gérer des ressources externes au sein d'un UIMA Analysis Engine

Ce post s'appuie notamment sur la section 1.5.4 du guide utilisateur [1] (Managing and accessing External Resources).
Des exemples de descripteurs et de code Java d'analysis engine mettant en oeuvre l'accès et la gestion à des ressources externes sont fournis dans l'exemple 6 du tutoriel de uima-examples.

De quoi parle-t-on ?

On exploite régulièrement des ressources au sein de composants (analysis engine) de traitement de la langue. Il peut s'agir par exemples de dictionnaires que l'on accède en lecture ou bien d'objets que l'on accède en écriture (et lecture) pour stocker des informations sur le sujet d'analyse du moment. 
Concernant ce deuxième cas, cela peut par exemple consister en le nombre de fois où un élément des données (une instance) avec un label x est classée avec un label y, ce qui permettra par la suite de construire une matrice de contingence pour évaluer les résultats d'un processus de classification. On peut imaginer plusieurs instances d'un même traitement qui comptent en parallèle différents documents... Un autre exemple peut consister en compter les occurrences de formes candidates (e.g. ngrams) en vue de calculer ultérieurement le tf.idf de ces formes pour un extracteur de termes par exemple. Dans ces deux exemples le calcul ultérieur peut être envisagé au sein de la méthode collectionProcessComplete()d'un analysis engine. 
Pour l'exemple sur le comptage de la classification des instances on peut imaginer qu'un objet ayant un attribut de type Map String to Integer pourrait parfaitement nous convenir. Les clés string seraient "truePositive", "trueNegative", "falsePositive" et "falseNegative". Remarquons qu'une Map String to String pourrait très bien convenir aussi pour un dictionnaire qui associe une forme lexicale avec son lemme.

Qu'entend-t-on par gestion et accès à une ressource ?

La gestion d'une ressource est souvent plus "facile" et plus "rapide" à mettre en oeuvre lorsque la définition de celle-ci est au sein même du composant que l'on développe.  Elle nécessite généralement que la connaissance de l'API du langage de programmation (Java) et du modèle de données de la ressource (induit du format du dictionnaire ou bien du phénomène que l'on observe) pour la manipuler. 
Externaliser les ressources de type dictionnaire et spécifier le chemin (e.g. sur le système de fichier ou sur le web) pour y accéder par la définition de paramètres des composants, sont un début de bonne pratique. Il en coûte quelques lignes de codes supplémentaires (gestion de fichiers et du parcours du format de stockage).  C'est un premier pas pour faciliter la maintenance et le partage de de ressources en lecture entre plusieurs composants.
Le chemin peut même désigner une ressource publique ou un chemin relatif plus facilement portable.
Néanmoins cette solution ne répond pas au problème de multiplication des instances de ressources en mémoire (une pour chaque instance de composant qui souhaite accéder à la ressource) ni au besoin d'accès concurrent en écriture sur la ressource. Pour cela il faut se plonger davantage dans la maîtrise du langage de programmation et écrire encore "quelques" lignes de codes supplémentaires.
Par ailleurs il est souvent intéressant de découpler l'API d'accès d'un objet de son implémentation afin de ne pas rendre dépendant le composant à l'implémentation.

UIMA met à disposition ce qu'il appelle un RessourceManager pour permettre de 
  • partager la même instance d'un objet entre plusieurs composants (soit des composant distincts soit plusieurs instances d'un même composant)
  • éventuellement initialiser au démarrage les objets à partir du contenu d'un fichier pointé par une URL (ou éventuellement un chemin sur le système de fichiers local)
  • d'externaliser l'implémentation de l'API de manipulation de l'objet en en fournissant une de base pour traiter l'objet comme un flux et en offrant la possibilité d'en spécifier de plus ad hoc et d'en fournir une implémentation

Comment déclarer l'identifiant utilisé dans le code du composant pour désigner la ressource externe (on parle aussi de clé) et comment spécifier l'interface qui déclare les manipulations que l'on peut réaliser sur l'objet ?  

Pour ce faire, on utilise le "component descriptor editor", onglet "Resources", section de gauche "Resource Dependencies" (correspond à externalResourceDependency dans le code source).

La valeur de la clé permet au code du composant d'identifier cette ressource. Elle est par conséquent unique au niveau de ce composant. Par contre d'autres composants peuvent utiliser cette valeur pour signifier d'autres ressources.

La spécification d'une interface est optionnelle. Pour rappel l'interface permet de dissocier l'implémentation de la manipulation de la ressource de son interface afin de pouvoir faire évoluer indépendamment le code du composant et celui de la gestion de la ressource. Sans nom d'interface spécifié, il sera possible d'accéder directement au contenu de la ressource par le biais d'une ImputStream (obtenu via une URL (éventuellement un chemin sur le système de fichiers local)). 
Cela signifie que le parsing de la ressource ne sera pas externalisé. Mais parfois si le format est simple, il n'est pas utile de définir une interface particulière.

Comment accède t on à la ressource au sein du code du composant ?

L'accès à la ressource ne peut que se faire si on a lié (binding) la clé que l'on a déclaré avec une définition de ressources externes, laquelle spécifiera l'URL pour accéder à la ressource. Cette opération sera vue ultérieurement.

Au sein du code on accède à la ressource à l'initialisation de l'analysis engine, c'est-à-dire que l'on accède à la ressource via le UimaContext fourni disponible au sein de la méthode initialize.

Lorsqu'aucune interface n'est définie lors de la déclaration de la clé désignant la ressource (onglet "Ressources" section "Resource Dependencies" ), on peut accéder à la ressource comme cela :
InputStream stream = getContext().getResourceAsStream("MaCleDesignantLaRessource");
ou bien comme cela si l'on souhaite déterminer la localisation du fichier ressource : 
URI uri = getContext().getResourceURI("MaCleDesignantLaRessource");

Lorsqu'une interface est définie on utilisera la méthode getResourceObject 
qui retourne un objet qui implémentera l'interface déclarée dans la section "Resource Dependencies" (onglet "Ressources" ) :
MyResourceTypeImpl mMyResourceTypeImpl = (MyResourceType)getContext().getResourceObject("MaCleDesignantLaRessource");

Comment définir une nouvelle ressource externe ?

Globalement une définition de ressource consiste à
  1. déclarer un nom de ressource pour l'identifier ultérieurement (afin de la lier avec des clés qui permettent aux composants d'identifier une ressource particulière au sein du code)
  2. indiquer une url pour trouver la ressource 
  3. et optionnellement spécifier une implémentation de l'interface pour accéder à la ressource lorsqu'une interface a été déclarée
La définition s'opère à l'aide du "component descriptor editor", onglet "Resources", section de gauche "Resources needs, Definition and Binding" (correspond à resourceManagerConfiguration dans le code source).

Indiquer dans le champ name (e.g. SharedResourceName) un nom servant à identifier cette définition de ressource. Lorsque vous lierait la ressource avec une clé utilisée par le code du composant, le nom sera automatiquement repris.

Indiquer dans le premier champ URL un chemin valide vers la ressource à charger. Si la ressource est locale indiquer un chemin relatif plutôt qu'un chemin absolu. Il n'y a pas de lieu de stockage spécifique requis pour une ressource. 
Personnellement je les place dans le répertoire resources de mon projet courant et j'ajoute le répertoire dans mon build path afin que les chemins relatifs soient accessibles dans celui-ci.

Dans le cas où votre ressource n'est pas de type dictionnaire et qu'il s'agit d'un objet partagé créé au fur et à mesure de l'analyse des documents traités, il faut tout de même spécifier un chemin vers une ressource existante. Créer par exemple un fichier vide defaultSharedResource.tmp dans un sous répertoire (external) (package) du répertoire resources du projet (n'oubliez pas d'ajouter ce répertoire dans le build path). Le chemin sera alors external/defaultSharedResource.tmp

Lorsqu'une interface a été spécifiée lors de la déclaration de clé ("Resource Dependencies" ), c'est sous cette section ("Resources needs, Definition and Binding") que l'on déclare la classe qui implémente l'interface.

If no implementation class is specified, then the getResource method returns a DataResource object, from which each annotator instance can obtain their own (non-shared) input stream; so threading is not an issue in this case.

Comment associer une clé à une définition d'une ressource externe ?

A l'aide du "component descriptor editor", onglet "Resources", 
  1. cliquer ensuite la clé que vous souhaitez dans la section de droite  ("Resource Dependencies" )
  2. cliquer sur la ressource définie que vous souhaitez dans la section de gauche ("Resources needs, Definition and Binding")
  3. puis cliquer sur "bind"

Lorsque l'on définit une interface d'accès à la ressource, que doit contenir la classe qui l'implémente ?

La classe qui implémenter l'interface de manipulation de la ressource doit implémenter aussi l'interface org.apache.uima.resource.SharedResourceObject. Concrètement cela signifie que l'on doit surcharger la méthode load(DataResource data) qui permet d'avoir accès à la ressource et qui va se charger de la parser pour initialiser l'instance de l'objet souhaité à retourner.
Cela doit globalement ressembler à cela
public class MyResourceTypeImpl implements MyResourceType_interface, SharedResourceObject {

public MyResourceTypeImpl() {
  super();
}

@Override
public void load(DataResource data) throws ResourceInitializationException {
}

}
Si il n'y a pas de ressources à charger, il suffit de laisser vide le code de la méthode.

Pour être instancié par le framework, l'implémentation doit être public et doit avoir un 0-argument constructor.

Comment partager une ressource entre plusieurs composants (ou comment bénéficier d'une seule définition de la ressource) ?


Les descripteurs de chaque composant doivent contenir la déclaration d'une dépendance pour la ressource partagée avec une valeur de clé qui leur est propre.
Seul l'un des composants spécifie une définition de la ressource (en donnant un nom et une URL pour accéder à la ressource). Ce composant là peut lier cette ressource à sa clé. 
Il n'est pas possible d'opérer le binding au sein des descripteurs des autres composants. Si la ressource est requise pour ces composants, ceux-ci ne pourront fonctionner normalement.

Pour partager la ressource il faut utiliser un descripteur aggregate qui va réunir les descripteurs de différents composants. Dans la section Resources, celui-ci spécifiera le binding entre la ressource souhaitée et les clefs affichés des composants.

Qu'en est il de la sûreté d'accès à la ressource quand celle-ci est partagée ?

La documentation indique que "If an implementation class is specified in the external resource, only one instance of that implementation class is created for a given binding, and is shared among all annotators. Because of this, the implementation of that shared instance must be written to be thread-safe - that is, to operate correctly when called at arbitrary times by multiple threads."

Le contrôle d'accès à des méthodes sensibles peut être une solution pour permettre un accès concurrentiel en écriture à la ressource.
public synchronized int increment() {
        return nextValue++;
    }

If no implementation class is specified, then the getResource method returns a DataResource object, from which each annotator instance can obtain their own (non-shared) input stream; so threading is not an issue in this case.

vendredi 27 juillet 2012

Example uima-connectors CAS to CSV

Using uima-connectors to serialize some CAS annotations into CSV-formatted files.
  1. This is performed in two steps: First, create a text view formatted in CSV with the information you wish serialize. 
  2. Second, write the views on the file system. The second step is optional, you do whatever you want with your view.


The first step is performed by the CAS2CSVAE. Each line of the '_CSVView', created at the first step, will contain some feature values of n annotation whose type have been configured via parameter. Columns of the lines contain the values of the features of the annotation type. They are also configured via parameters. 
The second step is performed by the ViewWriterAE. This AE requires the presence of org.apache.uima.examples.SourceDocumentInformation annotations in each CAS which results from using the FileSystemCollectionReader or using any tools from the apache uima SDK (like the DocumentAnalyzer). This is used to name the exported views in the file system.


Requirement
uima-connectors library is available here http://code.google.com/p/uima-connectors.
It requires the uima-common lib http://code.google.com/p/uima-common/.


Example of use
In the example eclipse project you will see how to turn TokenAnnotations with coveredText, posTag, stem which result from apache uima  addons analysis, into CSV formatted files.
See the descriptor desc/xample-apacheAddons-uimaConnectors/example-apacheAddons-uimaConnectors-CAS2CSV-ViewWriter-AAE.xml
In particular have a look at the aggregate and the parameter settings tabs.
The example project assumes you have installed the apache-uima and apache-uima-addons binaries. Check the build path before using it.

mercredi 20 juin 2012

Extraire le texte de documents HTML en créant des annotations à la place des balises

Traiter du SGML (e.g. HTML) ou du XML (i.e. SGML davantage contraint) est une activité commune dans nos chaînes de traitement TAL (Traitement Automatique des Langues). Par ailleurs, souvent les traitements que nous souhaitons réaliser (segmentation en phrases, en mots, étiquetage morpho-syntaxique, reconnaissance d'entités nommées...) ne fonctionnent que sur du texte simple (plain text).

1. Tika MarkupAnnotator vs. uima-connectors XML2CAS

A ma connaissance, il y a au moins deux composants UIMA "librement" disponibles pour 
  1. extraire le texte d'un document SGML sans en modifier ses offsets 
  2. transformer les balises d'un document SGML en annotations UIMA au dessus des zones de texte couvertes.
Il s'agit du Tika MarkupAnnotator [1] et du uima-connectors XML2CAS [3]. 
  • Pour chacun d'eux l'on peut spécifier la vue à analyser et la vue dans laquelle il faut produire le texte.
  • Chacun d'eux produit des annotations correspondantes aux balises. Le système de types est évidement différent dans sa forme même si l'on retrouve globalement dans le fond la même information. 
  • XML2CAS travaille seulement avec de l'XML et vous autorise à spécifier les noms des éléments que vous voulez transformer en annotations afin de restreindre la taille du CAS
  • MarkupAnnotator offre la possibilité de traiter de l'XML mal formée (i.e. du SGML et en particulier HTML). Cela relativement simplement en ajoutant la bibliothèque du TagSoup parser dans le classpath [5].
Le premier est disponible dans les Annotator addons de Apache UIMA [2] et le second requiert de récupérer la bibliothèque uima-common pour fonctionner (au final [3] et [4]).


2. Quelques détails sur mon test
Personnellement j'ai testé les deux au sein d'un projet java Eclipse en déclarant les dépendances  suivantes dans mon classpath (en plus de celles de UIMA):
  • UIMA_HOME/addons/annotator/TikaAnnotator/lib/tika-core-0.7.jar
  • UIMA_HOME/addons/annotator/TikaAnnotator/lib/tika-parsers-0.7.jar
  • UIMA_HOME/addons/annotator/TikaAnnotator/lib/uima-an-tika.jar
  • chemin/vers/votre/lib/uima-common-v120111.jar
  • chemin/vers/votre/lib/uima-connectors-v111205.jar
  • chemin/vers/votre/lib/tagsoup-1.2.1.jar
J'ai ajouté la UIMA nature au projet et ajouté le répertoire desc aux build path pour me faciliter la tâche. J'ai créé un descripteur d'Analysis Engine que j'ai changé en aggregate afin de rajouter, les composants que je souhaitais tester, dans ma pipeline (section aggregate du descripteur) : 
MarkupAnnotator et connectors.xml.XML2CASAE.
J'ai surchagé les paramètres des deux composants (section parameter) et n'ai pas oublié de les configurer (section parameter settings). 
Puis j'ai déclaré les types en sortie (section capabilities).
Enfin j'ai lancé ma chaîne à l'aide du Document Analyser. Lorsque j'ai testé sur du HTML je n'avais pas retiré le composant XML2CAS de ma pipeline.



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


samedi 10 décembre 2011

Mise en correspondance de vues via un Aggregate pour interconnecter des composants UIMA (view et sofa name mapping)

Après avoir rappelé quelques notions (que le lecteur averti peut ne pas lire), le post expose brièvement le problème et présente avec un exemple la mise en oeuvre d'une solution de mise en correspondance de vues au sein d'un Aggregate.

Bref rappel de quelques notions
Les chapitres 5 et 6 de la documentation UIMA tutorials_and_users_guides [1;2] rappellent les définitions des notions d'Artifact, Sofa et View.
Brièvement l'Artifact est la chose non structurée que l'on souhaite analyser. Le Sofa (Subject of Analysis) est une représentation de l'Artifact (e.g. HTML). Une annotation metadata est de l'information associée à un Sofa particulier pour en décrire une sous région. La notion de View simplifie les choses quand plusieurs versions de l'Artifact sont nécessaire à différentes étapes de l'analyse (e.g. une version sans balise d'un document XML). Les notions de Sofa et de CAS View sont liées. Dans l'implémentation Apache actuelle (2.4), à chaque Sofa est associé une seule View et à chaque View un seul Sofa. Le nom qui désigne une vue particulière est Sofa name pour des raisons historiques mais cela s'applique indifféremment à View aussi.
L'analyse d'un composant UIMA (AE - Analysis Engine) porte toujours sur une vue. Il peut y en avoir plusieurs. L'AE peut en créer. Les résultats d'analyse peuvent être associés sur les vues traitées ou bien sur les vues créées.
Un AE qui travaille seulement sur une seule vue est appelée "single-view component" et dans le cas contraire "multi-view/multi-sofa component".
Par défaut, si rien n'est spécifié il y a qu'une seule view et celle-ci a le sofa name de "_InitialView".

Problème d'interopérabilité pour interconnecter des composants développés indépendamment
Le développeur d'AE est libre de choisir le nom qu'il souhaite pour désigner ses vues d'entrée et ses vues de sortie.
Cela conduit naturellement à quelques petits soucis lorsque l'on veut interconnecter des composants ayant des noms de vue différents.

Le mécanisme de mise en correspondance des vues au niveau d'un Aggregate
La section 6.4 de la documentation explique et présente différentes situations de mises en correspondance : au sein d'un Aggregate, d'un CPE, d'une application UIMA, pour des services distants. Je rapporte et illustre la solution au sein d'un AE Aggregate.

Pour cela, j'utilise deux composants : l'XmlDetagger présent dans les exemples fournis avec Apache UIMA [4] et le Whitespace Tokenizer des addons de la Sandbox d'Apache UIMA [5].

L'XmlDetagger est un composant multiple vues qui retire les balises d'un document XML. Il lit l'XML d'une vue input nommée "xmlDocument". Le contenu textuel est écrit dans une nouvelle vue output appelée "plainTextDocument".
Le Whitespace Tokenizer est un composant simple vue qui travaille sur la vue par défaut (i.e.  "_InitialView") et qui ajoute à celle-ci des annotations de phrases et de tokens.

Rapidement, je mets en place un projet Java sous Eclipse. J'y ajoute la UIMA Nature, déclare les répertoires desc et resources dans le build path ainsi que les bibliothèques de UIMA [6]. Je rajoute au  le build path les bibliothèques uima-examples [10] et addon whitespace tokenizer [9] qui vont servir pour notre chaîne.

Je crée un descripteur d'AE Aggregate [7] et j'y ajoute by name en pipeline : d'abord l'XmlDetagger  puis le Whitespace Tokenizer (je déclare les bons types à produire en output dans les capabilities).

A la sauvegarde le message suivant s'affiche
Error in AE Descriptor
The Descriptor is invalid for the following reason:
ResourceInitializationException: The output Sofa "plainTextDocument" in component "XmlDetagger" is not mapped to any output Sofa in its containing aggregate, "demoSofaNameMappingAAE". (Descriptor path ...) 

Ce n'est pas réellement une erreur si l'on sait ce que l'on fait. Une vue qui n'est pas utilisée par exemple n'a pas besoin d'être mise en correspondance dans l'Aggregate.
En l'état si on exécute cet AE avec le DocumentAnalyzer [8] sur un document XML quelconque, on obtiendra l'erreur suivante :
org.apache.uima.analysis_engine.AnalysisEngineProcessException: Annotator processing failed. Caused by: org.apache.uima.cas.CASRuntimeException: No sofaFS with name xmlDocument found. 
Vraissemblablement ici il y a des vues qui requièrent d'être mise en correspondance et cela va se faire au niveau de l'Aggregate.

La première chose à faire est d'indiquer que la vue sur laquelle travaille en entrée XmlDetagger, à savoir xmlDocument, correspond à _InitialView dans l'Aggregate. Autrement dit, la vue d'entrée par défaut de l'Aggregate,  _InitialView, doit être mise en correspondance avec la vue d'entrée, xmlDocument, de l'XmlDetagger.
Au sein du descripteur de l'Aggregate, c'est dans la section capabilities que l'on déclare les vues sur lesquelles on travaille (sous section component capabilities) et celles entre lesquelles on réalise des mises en correspondance (sous section sofaMappings).
On ajoute d'abord la vue xmlDocument comme input (entrée) en faisant un add sofa dans la component capabilities.
Ensuite on indique dans la sous section sofaMappings que pour le composant (componentKeyXmlDetagger sa vue (componentSofaNamexmlDocument correspond à la vue dans l'aggregate (aggregateSofaName_InitialView.
Par les éléments componentKey et componentSofaName on désigne le composant et la vue souhaitée chez celui-ci comme devant intervenir dans un mapping. L'élément aggregateSofaName est la clé partagée entre les éléments sofaMapping.
Cette dernière opération doit se faire pour partie à la main en éditant le source (personnellement je n'y arrive pas via l'éditeur du formulaire). Cela donne
<capability>
...
  <inputSofas>
    <sofaName>xmlDocument</sofaName>
  </inputSofas>
  ...
et
  <sofaMappings>
    <sofaMapping>
      <componentKey>XmlDetagger</componentKey>
      <componentSofaName>xmlDocument</componentSofaName>
      <aggregateSofaName>_InitialView</aggregateSofaName>
    </sofaMapping>
  </sofaMappings>
Si on réexécute l'AE on se rend compte que ce n'est pas encore l'idéal car le Whitespace Tokenizer travaille sur l'_InitialView par défaut et traite donc ce qui correspond à l'xmlDocument au lieu du plainTextDocument.
On rajoute dans l'élément inputSofas
<sofaName>_InitialView</sofaName>
et l'élément suivant
  <sofaMapping>
      <componentKey>WhitespaceTokenizer</componentKey>
      <componentSofaName>_InitialView</componentSofaName>
      <aggregateSofaName>plainTextDocument</aggregateSofaName>
    </sofaMapping>
La vue plainTextDocument du XmlDetagger n'a pas été redéfinie dans l'Aggregate, il n'y a pas d'ambiguité. Ce que produit le XmlDetagger au sein de sa vue output plainTextDocument est accessible sous ce même nom au sein de l'Aggregate qui le met en correspondance avec la vue input du
WhitespaceTokenizer.   
L'exécution fonctionne comme attendu finalement.  

Quelques commentaires supplémentaires

Au niveau de l'Aggregate après mise en correspondance, le nom de la vue du componentSofaName
sera renommé en le nom de la vue déclarée dans aggregateSofaName.
Les vues avec leur nom précédent ne seront plus accessibles. Une fois sérialisée en XMI, on constatera sela en cherchant la valeur de l'attribut sofaID.
Les composants d'Apache OpenNLP sont des composants multiples vues et requièrent en général l'usage de mises en correspondance.


Liens

[1] 5. Annotations, Artifacts & Sofas
[2] 6. Multiple CAS Views
[3] 6.4. Sofa Name Mapping
[4] UIMA_HOME/examples/descriptors/analysis_engine/XmlDetagger.xml
[5] http://uima.apache.org/sandbox.html#whitespace.tokenizer
[6] Créer un projet Eclipse pour le développement d'un composant UIMA
[7] Construire une chaîne de traitement UIMA à partir de composants existants
[8] Exécuter un traitement ou une chaîne de traitement sous UIMA (avec UIMAJ et en local)
[9] UIMA_HOME/addons/annotator/WhitespaceTokenizer/lib/uima-an-wst.jar
[10] UIMA_HOME/lib/uima-examples.jar