UIMA FR
Communauté francophone autour d'UIMA
  • Accueil
  • Top 10
  • Statistiques
  • Inscription
  • Archives
  • Contact

Informations

BilboPlanet - An Open Source RSS feed aggregator written in PHP.
 

Abonnement

  • feed  Fil de tous les articles
  • feed  Fil des articles populaires

Membres

  • feed Fabien POULARD
  • feed Jérôme Rocheteau
  • feed Laurent Audibert
  • feed Matthieu Vernier
  • feed Nicolas Hernandez
  • feed Nicolas Hernandez

Participer

  • meta  Ajouter votre blog
  • meta  Administration

 

Filtrer les articles :     Articles du jour   -   Articles de la semaine   -   Articles du mois   -   Tous les articles

Accès rapide aux derniers articles de la page

  • 09/01/2012 : Premiers pas avec UIMA AS
  • 05/01/2012 : Installation des plugins UIMA-AS sous Eclipse Indigo
  • 05/06/2011 : Copier une annotation d'une vue à une autre dans UIMA
  • 20/09/2010 : UIMA insights : le doigt dans le workflow
  • 08/09/2010 : Un rapide tokeniseur en mots pour le français
  • 17/07/2010 : Du bon usage des ressources dans UIMA
  • 01/04/2010 : Parcours des annotations couvertes par une autre annotation
  • 16/03/2010 : Tests unitaires pour UIMA avec UUTUC
  • 06/03/2010 : Utilisation du CAS Editor
  • 04/03/2010 : Release du collection reader pour Wikipedia v.0.4
Page suivante »
 

Premiers pas avec UIMA AS

0 vote
Par Fabien POULARD le 09/01/2012 à 10:00 Voir l'article

UIMA offre un cadre de développement structurant pour la mise au point de chaînes de traitement de l'information non structurée. S'il permet simplement de déployer des chaînes complexes et tirer parti de la puissance de calcul des processeurs multicœurs, l'ordonnanceur -- le CPM -- a plusieurs limites :

  • Il n'est pas aisé de déployer une chaîne sur plusieurs machines ;
  • L'affectation de ressources se fait au niveau de la chaîne (CPE) et non au niveau des composants alors que le coût d'exécution de ces derniers est fortement variable ;
  • Les chaînes ne peuvent pas directement traiter un flux continu de données.

Le développement d'UIMA AS cherche à répondre à ces limitations.

Principe général

UIMA AS est développé en remplacement du Collection Processing Manager (CPM) dans le but d'offrir de meilleures capacités de flexibilités et montée en charge. UIMA AS ne remplace nullement UIMA, et les composants développés pour UIMA peuvent aussi bien fonctionner dans une chaîne UIMA AS. AS profite du système d'Aggregate pour organiser les composants en chaînes de traitement, tirant parti au passage des capacités des flow controllers.

L'approche CPM (UIMA classique)

Lorsqu'une chaîne de traitement UIMA est déployée par le CPM, la configuration suivante se met en place :

  • le CPM instancie autant de processing pipelines (PP, combinaison séquentielle de composants) que nécessaire, chaque PP contenant une seule et unique instance de chaque composant ;
  • le Collection Reader (CR) est instancié dans un seul thread (pas de parallélisation possible) ;
  • les CAS produits par le CR sont stockés dans une queue tampon avant d'être distribués vers les différents PP qui ne peuvent qu'en traiter un à la fois : un CAS en sortie d'un composant est directement passé en entrée du composant suivant.

Ce fonctionnement est décrit par le schéma suivant tiré de la documentation d'Apache UIMA si ce n'est que les cas consumers n'existent plus en tant qu'entités particulières mais sont des composants comme les autres (et donc instanciés dans les PP) :

cpe-detail.png

L'approche UIMA AS

Les chaînes dans UIMA AS sont déployées sous forme de services auxquels peuvent se connecter des clients. Les clients envoient des requêtes au service puis attendent le résultat du traitement. L'interface entre les clients et les services se fait au travers d'un système de messagerie asynchrone :

  • les requêtes des clients sont stockées dans une queue tampon en entrée du service similaire à la queue du PP pour le CPM ;
  • les résultats des traitements sont renvoyées dans une queue tampon propre à chaque client.

Ce fonctionnement est décrit par le schéma ci-dessous tiré de la documentation d'Apache UIMA AS. Il permet d'instancier une seule fois une chaîne de traitement et de lui soumettre un flux continu de données à traiter en provenance potentiellement de plusieurs sources.

uima-as-arch.png

Le système de messagerie asynchrone prend en charge la distribution des requêtes entre différentes instances potentielles du service permettant ainsi une montée en charge à chaud. Il suffit de déployer une nouvelle instance du service et lui indiquer de se connecter à la queue tampon d'entrée pour augmenter les capacités de traitement. Le système de messagerie utilisé par défaut, ActiveMQ, ayant la capacité de communiquer au travers du réseau, ledit service peut tout à fait être déployé sur une machine distante.

Une autre force de UIMA AS est que ce système de queue tampons et de routage des CAS au travers d'un système de messagerie asynchrone peut être mis en place au sein même du service entre les différents composants. Pour cette première prise en main évitons tout de même de compliquer les choses...

Mise en place de l'environnement

La mise en place d'une chaîne de traitement UIMA AS comparable à un CPE nécessite trois éléments : le service UIMA AS en charge du traitement, le client qui soumet les données et récupère les résultats du traitement et le système de messagerie coordonne la communication entre ces deux premiers.

La mise en place d'un service UIMA AS nécessite, outre le développement des composants, un descripteur de déploiement (deployment descriptor). Afin de simplifier son écriture dans la prochaine section de ce billet, il est préférable d'installer les plugins Eclipse dédiés.

Un certain nombre de dépendances sont nécessaires pour la compilation et l'exécution d'un service et d'un client UIMA AS, a minima :

  • les bibliothèques UIMA AS ; par simplicité les bibliothèques classiques UIMA (hors addons) sont également packagées dans l'archive UIMA AS. La suite du billet repose sur la version 2.3.1.
  • un certain nombre de bibliothèques Spring :
    • spring-aop
    • spring-asm
    • spring-beans
    • spring-context
    • spring-context-support
    • spring-core
    • spring-expression
    • spring-jms
    • spring-tx
    • spring-web
    • spring-webmvc

Finalement, il est nécessaire d'installer un système de messagerie de type JMS. Le plus simple est d'utiliser celui proposé par défaut : Apache ActiveMQ. Une version est distribuée dans l'archive UIMA AS. Toutefois, si vous êtes sous un environnement Linux, il sera plus aisé et pérenne du paquetage de votre distribution.

Ainsi pour Debian Wheezy, installez le paquet :

sudo apt-get install activemq

puis activez l'instance par défaut :

cd /etc/activemq/instances-enabled/
sudo ln -s ../instances-available/main/

et lancez enfin le système :

sudo /etc/init.d/activemq start

Création et déploiement d'un service

Descripteur de déploiement

Le déploiement d'un service UIMA AS nécessite deux descripteurs :

  • Un descripteur de composant qui définit l'aggregate qui sera déployé ;
  • Un descripteur de déploiement qui décrit comment l'aggregate sera déployé.

Pour l'exercice nous déploierons un composant de prétraitement : découpage en phrases, en mots et étiquetage des rôles grammaticaux. Nous utiliserons pour cela les composants addons distribués avec UIMA : le WhitespaceTokenizer et le HMMTagger. L'écriture de l'aggregate ne pose pas de difficulté particulière, il s'agit d'un composant UIMA classique sans aucune spécificité que nous nommerons POSTagger.xml.

Le descripteur de déploiement est une nouveauté de UIMA AS. Il est disponible dans l'assistant de création d'un nouveau fichier sous la catégorie UIMA aux côtés des autres descripteurs, si tant est que vous avez correctement installé les plugins UIMA AS. Nous le nommerons POSTagger-Service.xml.

Création d'un nouveau descripteur de déploiement UIMA AS

Le descripteur de déploiement se compose de deux onglets : un onglet de configuration générale (Overview) et un onglet de configuration du déploiement (Deployment Configurations). Il faut préciser dans l'onglet de configuration générale le nom de la queue de messagerie sur laquelle le service sera joignable (Name for input queue) et le nom de l'aggregate à déployer comme service (Top analysis engine descriptor). Ces deux champs sont mis en valeur dans la capture d'écran ci-dessous.

Complétion de la configuration générale du déploiement

Par défaut il n'est pas nécessaire d'intervenir sur la configuration du déploiement. Le service sera alors déployé tel qu'il l'aurait été sous forme d'un CPE, c-à-d sans contrôle particulier sur les composants qui composent l'aggregate. Pour l'exercice, nous allons le déployé comme un aggregate UIMA AS, c-à-d que chaque composant de l'aggregate est lui-même déployé comme un service connecté aux autres composants par le système de messageries et de queues.

Ce déploiement nécessite simplement de cocher la case Run as AS aggregate. Une fois coché, les différents composants qui constituent l'aggregate sont accessibles pour une configuration propre.

Configuration du déploiement comme un AS Aggregate

Pour simplifier nous considérerons qu'ils sont tous déployés en local et dans la même JVM. Nous allons simplement demander de déployer trois instances du HMMTagger étant donné que c'est le composant le plus coûteux. Il suffit pour ce faire de sélectionner le composant HMMTagger dans la liste puis de positionner le paramètre Number of replicated instances sur 3.

Configuration du nombre d'instances déployées pour chaque service

Écriture du client asynchrone

Une fois le descripteur de déploiement écrit, il ne reste plus qu'à écrire le code du client asynchrone qui va s'y connecter. La documentation d'UIMA AS décrit en détail un cas d'utilisation duquel ce billet s'inspire.

Tout d'abord, toujours dans le cadre de l'exercice, nous faisons le choix que le service soit déployé par l'application elle-même :

public static final String DD2SPRINGXSLTFILEPATH = 
	"/LIBS-APPS/UIMA/apache-uima-as-2.3.1/bin/dd2spring.xsl";
public static final String SAXONCLASSPATH = 
	"file:/LIBS-APPS/UIMA/apache-uima-as-2.3.1/saxon/saxon8.jar";
public static final String DEPLOYMENTDESCRIPTOR =
	"desc/POSTagger-Service.xml";
...
// Create Asynchronous Client API
BaseUIMAAsynchronousEngine_impl uimaAsEngine = 
		new BaseUIMAAsynchronousEngine_impl();
 
///////////////////////////////////////////////////// SERVICE DEPLOYING
System.out.println("Deploying service");
// Create a Map to hold required parameters
Map<String, Object> servCtxt = new HashMap<String, Object>();
servCtxt.put(UimaAsynchronousEngine.DD2SpringXsltFilePath, 
		DD2SPRINGXSLTFILEPATH);
servCtxt.put(UimaAsynchronousEngine.SaxonClasspath,
		SAXONCLASSPATH);
String serviceId = uimaAsEngine.deploy(DEPLOYMENTDESCRIPTOR, servCtxt);
System.out.println("\\t...Service deployed !");

Une fois le service déployé, nous allons configurer un client asynchrone pour s'y connecter et soumettre des requêtes. Il est important ici que :

  • le paramètre UimaAsynchronousEngine.ServerUri corresponde à l'adresse du gestionnaire de messagerie (broker) spécifié auparavant dans le descripteur de déploiement (nous avons laissé la valeur par défaut ${defaultBrokerURL}, nous verrons après comment la spécifier à l'exécution) ;
  • le paramètre UimaAsynchronousEngine.Endpoint doit correspondre au nom de la queue sur laquelle le service est joignable.
///////////////////////////////////////////////////// SERVICE EXECUTION
System.out.println("Preparing for execution");
// Create map to pass server URI and Endpoint parameters
Map<String, Object> appCtxt = new HashMap<String, Object>();
appCtxt.put(UimaAsynchronousEngine.ServerUri, "tcp://localhost:61616");
appCtxt.put(UimaAsynchronousEngine.Endpoint, "POSTaggerQueue");
appCtxt.put(UimaAsynchronousEngine.CasPoolSize, 2);
// Initialize
uimaAsEngine.initialize(appCtxt);
uimaAsEngine.addStatusCallbackListener(new MyStatusCallbackListener());
// Get an empty CAS from the Cas pool
CAS cas = uimaAsEngine.getCAS();
System.out.println("\\t...CAS retrieved");
// Initialize it with input data
cas.setDocumentText("Un exemple de texte...");
cas.setDocumentLanguage("fr");
// Send CAS to service for processing
uimaAsEngine.sendCAS(cas);
System.out.println("\\t...CAS sent");

Il est intéressant d'observer le fonctionnement du client :

  1. L'initialisation consiste, outre la configuration des différents paramètres précités, à définir l'instance StatusCallbackListener qui sera chargée de consommer les résultats du traitement par le service (toute l'asynchronicité du client réside dans ce fonctionnement) ;
  2. L'envoi d'une demande de traitement passe d'abord par la demande d'un CAS disponible par la méthode getCAS(). Une fois le CAS obtenu, il est initialisé comme le ferait un Collection Reader, puis soumis au système par la méthode sendCAS().

Il ne reste alors plus qu'à implémenter le StatusCallbackListener et plus particulièrement la méthode entityProcessComplete appelée lorsque le traitement d'un CAS est terminé :

static class MyStatusCallbackListener extends UimaAsBaseCallbackListener {
 
	@Override
	public void entityProcessComplete(CAS aCas, EntityProcessStatus aStatus) {
		System.out.println("Entity process complete.");
		// Handle errors
		if ( (aStatus != null) && aStatus.isException() ) {
			List<Exception> exceptions = aStatus.getExceptions();
			for(Exception e: exceptions) {
				e.printStackTrace();
			}
			return;
		}
		// Process CAS
		try {
			System.out.println("Concepts identified:");
			JCas jcas = aCas.getJCas();
			FSIterator<Annotation> it = 
					jcas.getAnnotationIndex(T_Token.type).iterator();
			while( it.hasNext() ) {
				T_Token token = (T_Token) it.next();
				System.out.println("\\t+ POS:" + token.getPos());
			}
		} catch (CASException e) {
			System.out.println("Problem getting a JCas !");
		}
	}
 
}

Exécution

Une fois tout ce travail réalisé, il ne reste plus qu'à exécuter le tout. Il est nécessaire d'ajouter toutes les dépendances nécessaires à l'exécution soit toutes les bibliothèques UIMA et UIMA AS ainsi que les nombreuses dépendances Spring.

Il est également nécessaire de spécifier, par le biais d'une variable de JVM l'adresse du gestionnaire de messagerie que nous avons positionné jusqu'alors à ${defaultBrokerURL}. Pour l'exercice, nous utilisons un broker ActiveMQ local. Le paramètre de la JVM est donc le suivant :

-DdefaultBrokerURL=tcp://localhost:61616

À partir d'ici le service devrait se déployer et le client asynchrone s'y connecter sans accroc. Si vous rencontrez des erreurs du type Connection refused c'est très probablement que votre système de messagerie ne tourne pas ou bien que les noms de queues précisés dans le descripteur et pour le client sont différents.

Retour au sommaire
 

Installation des plugins UIMA-AS sous Eclipse Indigo

0 vote
Par Fabien POULARD le 05/01/2012 à 11:28 Voir l'article

Au sein de Dictanova, nous avons des besoins importants en termes de distribution de la charge de calcul. Le CPM classique d'UIMA (l'organe chargé de l'ordonnancement des traitements) ne répond pas suffisamment à nos besoins et nous nous tournons donc vers UIMA AS (pour UIMA Asynchronous Scaleout) qui offre des possibilités de montées en charge beaucoup plus importantes en permettant notamment de déployer les Analysis Engine dans plusieurs JVM et sur plusieurs machines.

Je décris dans ce billet la procédure que j'ai mise en oeuvre pour installer les plugins UIMA AS sous Eclipse Indigo.

Le problème

Dans le meilleur des mondes, l'installation des plugins UIMA AS devrait se dérouler sans encombre en utilisant le gestionnaire de plugins d'Eclipse. Cependant, si comme moi vous n'aviez jusqu'à présent que les plugins UIMA classiques d'installés et que vous souhaitez installer les plugins UIMA AS, Eclipse vous informera que c'est impossible car une dépendance n'a pas été trouvée.

L'origine de ce problème est la récente release de UIMA 2.4.0, et la mise à jour dans cette version 2.4.0 des plugins alors que les plugins UIMA AS dépendent d'une version 2.3.x. Il est donc nécessaire de revenir à une version 2.3.1 des plugins UIMA avant d'installer les plugins UIMA AS.

Rétrograder les plugins Eclipse Apache UIMA en version 2.3.1 (downgrade) pour installer le plugin UIMA AS

Il n'est pas directement possible de revenir à une version antérieure d'un plugin. Il est nécessaire dans un premier temps de désinstaller le plugin, puis d'installer la version antérieure.

Pour désinstaller le plugin, il faut se rendre dans le menu Aide > À propos d'Eclipse > Détails de l'installation (Help > About Eclipse > Installation Details) comme le montrent les captures d'écran ci-dessous :

eclipse-about-install.png

eclipse-installation-details.png

La fenêtre liste les features actuellement installées. Sélectionnez celles qui correspondent à UIMA puis cliquez sur Désinstaller (Uninstall).

Une fois la désinstallation effectuée et après avoir redémarrer Eclipse, il est possible d'installer la version 2.3.1 des plugins UIMA classiques et dans le même temps le plugin UIMA AS.

Rendez-vous dans le menu Aide > Ajout de nouveaux logiciels (Help > Install new software), indiquez à l'assistant d'utiliser tous les sites à sa disposition et décochez la case indiquant de ne montrer que les dernières versions comme l'illustre la capture d'écran ci-dessous :

eclipse-install-plugins.png

Il suffit ensuite de sélectionner les versions 2.3.1 des différents plugins : UIMA Runtime, UIMA tools et UIMA AS. L'installation devrait alors se dérouler sans accrocs.

Retour au sommaire
 

Copier une annotation d'une vue à une autre dans UIMA

0 vote
Par Fabien POULARD le 05/06/2011 à 00:59 Voir l'article

Lorsque l'on travaille avec plusieurs vues au sein d'un même CAS, on se retrouve rapidement confronté au besoin de recopier certaines annotations d'une vue vers une autre.

Il est possible de recréer l'annotation et de repositionner tous ses traits sur les mêmes valeurs que l'annotation d'origine. Cette approche est fastidieuse lorsque l'annotation est complexe, que l'on n'en connaît pas tous les traits ou bien lorsque l'on veut copier plusieurs annotations de types différentes.

Une autre approche, beaucoup plus souple, consiste à faire une copie profonde de l'objet Java du CAS correspondant à ladite annotation à l'aide de la méthode clone. Il faut alors penser à modifier la valeur du SOFA associée la nouvelle annotation sous peine de se voir refuser l'ajout de l'annotation copiée à l'index de la nouvelle vue.

La difficulté réside dans le fait que le trait contenant le SOFA n'est pas directement accessible. Il faut utiliser la méthode setFeatureValue pour mettre à jour la valeur :

Feature sofaFeature = annotation.getType().getFeatureByBaseName("sofa");
annotation.setFeatureValue(sofaFeature, view.getSofa());

Voici la méthode que j'utilise désormais pour copier mes annotations d'une vue à une autre :

public static Annotation copyAnnotationToView(Annotation a, JCas view) {
	// To copy the annotation we must process in three steps
	// 1- Clone the annotation from the original view
	Annotation a2 = (Annotation) a.clone();
	// 2- Change the Sofa of the cloned annotation
	Feature sofaFeature = a2.getType().getFeatureByBaseName("sofa");
	a2.setFeatureValue(sofaFeature, view.getSofa());
	// 3- Add this annotation to the indexes of the new view
	a2.addToIndexes(view);
	return a2;
}
Retour au sommaire
 

UIMA insights : le doigt dans le workflow

0 vote
Par Fabien POULARD le 20/09/2010 à 00:08 Voir l'article

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.

Mon problème

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

Le workflow d'UIMA

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 :

cpe-detail.png

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 :

  1. Instanciation du composant dans le thread principal du CPE ;
  2. Appel de la méthode initialize() toujours dans le thread principal ;
  3. Création des threads des pipelines et rattachement des composants à ces derniers ;
  4. Appels successifs à process() au sein de ces threads ;

Ma solution

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);
	...
Retour au sommaire
 

Un rapide tokeniseur en mots pour le français

0 vote
Par Fabien POULARD le 08/09/2010 à 00:10 Voir l'article

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.

Idée et objectifs

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 :

  • les articles et pronoms contractés (l', d', j', m', ...)
  • les composés lexicaux à apostrophe (aujourd'hui, ...)
  • les composés lexicaux à traits d'union (arc-en-ciel, peut-être, sauve-qui-peut, ...)
  • les valeurs numériques (14 000, 14,18, 30 %, ...)
  • les acronymes (ASSEDIC, ASCII, ...) et les sigles (C-4, c-à-d, i.e., ...)
  • les unités de mesure (A/m, km/h, ...)

Mise en œuvre

Catégories générales des caractères unicodes

La norme unicode offre sept catégories générales de caractère :

UnicodeJava (getType)
les lettres (L)
Ll LOWERCASE_LETTER
LuUPPERCASE_LETTER
LmMODIFIER_LETTER
LoOTHER_LETTER
LtTITLECASE_LETTER
les marques (M)
McCOMBINING_SPACING_MARK
MeENCLOSING_MARK
MnNON_SPACING_MARK
les nombres (N)
NdDECIMAL_DIGIT_NUMBER
NlLETTER_NUMBER
NoOTHER_NUMBER
les ponctuations (P)
PcCONNECTOR_PUNCTUATION
PdDASH_PUNCTUATION
PeEND_PUNCTUATION
PiINITIAL_QUOTE_PUNCTUATION
PfFINAL_QUOTE_PUNCTUATION
PoOTHER_PUNCTUATION
PsSTART_PUNCTUATION
les symboles (S)
ScCURRENCY_SYMBOL
SmMATH_SYMBOL
SkMODIFIER_SYMBOL
SoOTHER_SYMBOL
les séparateurs (Z)
ZlLINE_SEPARATOR
ZpPARAGRAPH_SEPARATOR
ZsSPACE_SEPARATOR
divers (C)
CcCONTROL
CfFORMAT
CoPRIVATE_USE
CsSURROGATE
CnUNASSIGNED

Le site Fileformat fournit une liste et une description exhaustive des caractères contenus dans ces différentes catégories.

Description des mots

Mots pleins

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 :

fsm-motsimple.png

Le cas de l'apostrophe

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 :

fsm-apostrophe.png

Le cas du trait d'union

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 :

fsm-union.png

Les valeurs numériques particulières

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.

fsm-decimal.png

Algorithmie

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 :

  • Parfois l'espace sépare un mot, parfois il est contenu au sein d'un mot (entre le nombre et la monnaie par exemple) ;
  • Parfois le passage d'un mot à un autre se fait en consommant un ou plusieurs caractères (espace), parfois le caractère de séparation appartient à l'un des deux mots (apostrophe) ;
  • La consommation d'un caractère inattendu ne doit pas interrompre le fonctionnement : une décision doit être prise (couper le mot ou pas).

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 :

  • start_word un nouveau mot commence ;
  • end_word le mot courant se termine, le caractère juste consommé n'en faisant pas parti ;
  • end_words_prev le mot courant se termine au caractère anté-précédent ;
  • switch_word le mot courant se termine sur le caractère consommé, le caractère courant appartient à un nouveau mot ;
  • switch_word_prev le mot courant se termine au caractère anté-précédent, le caractère courant appartient à un nouveau mot ;
  • nop aucune opération

L'automate ci-dessous correspond à ce transducteur :

  • les états en N (orange) correspondent à l'automate de reconnaissance des valeurs numériques ;
  • les états en L (bleu) correspondent à une combinaison des automates de reconnaissance des mots alphabétiques ;
  • les transitions en rouge correspondent aux cas d'erreur où il faut prendre une décision ;
  • les transitions en vert correspondent aux cas attendus de changement de mot.

fsm-complete.png

Implémentation

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}

Distribution

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.

Retour au sommaire
 

Du bon usage des ressources dans UIMA

0 vote
Par Fabien POULARD le 17/07/2010 à 01:46 Voir l'article

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.

L'erreur de Jérôme

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 :

  1. dans la méthode initialize() on prépare la structure qui va nous permettre de collecter les données
  2. dans la méthode process() on compte les termes et on alimente la structure initialisée dans le initialize()
  3. dans la méthode collectionProcessComplete() on fait un bilan de ce que l'on a collecté dans la fameuse structure et on renvoie tout à l'utilisateur.

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.

Du CPE et des Processing pipelines

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 :

Représentation schématique d'un CPE

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 :

Représentation schématique, plus précise, d'un CPE

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.

L'avènement des ressources externes

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) :

  • Elles sont partagées entre toutes les instances du même AE (cf. section 1.5.4.4);
  • Elles peuvent implémenter une interface particulière et ainsi embarquer un comportement ;

Il serait donc préférable dans le cas de Jérôme :

  • soit d'empêcher le déploiement en parallèle de plusieurs instances du même AE ;
  • soit de passer par une ressource (une ArrayList par exemple)

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.

Retour au sommaire
 

Parcours des annotations couvertes par une autre annotation

0 vote
Par Fabien POULARD le 01/04/2010 à 11:11 Voir l'article

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 :

  • récupérer les mots contenus dans une phrases ;
  • récupérer les paragraphes dans un document ;
  • …

Il y a au moins deux approches dans Apache UIMA qui permettent de répondre à ce besoin : le subiterator et le FSMatchConstraint.

Utilisation du subiterator

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 :

  1. On récupère un pointeur sur l'annotation couvrante qui nous intéresse (A:3), à l'aide d'un itérateur par exemple ;
  2. On récupére l'index des annotations couvertes (les B) ;
  3. On appelle la méthode subiterator de l'index des annotations couvertes (B) en passant en paramètre l'annotation couvrante (A:3), la méthode nous retourne un itérateur sur les annotations B couvertes par A:3, soit B:4, B:5 et B:6.

Voici le code correspondant :

  1. // Récupération des index
  2. AnnotationIndex annAIdx = (AnnotationIndex) jcas.getAnnotationIndex(A.type);
  3. AnnotationIndex annBIdx = (AnnotationIndex) jcas.getAnnotationIndex(B.type);
  4. // On recherche ''A:3''
  5. FSIterator annAIt = annAIdx.iterator();
  6. while (annAIt.hasNext()) {
  7. A monA3 = (A) annAIt.next();
  8. // On récupére l'itérateur sur les annotations B couvertes par A3
  9. FSIterator annBSousA3It = annBIdx.subiterator(monA3);
  10. while (annBSousA3It.hasNext()) {
  11. // On récupére successivement B4, B5 et B6
  12. B annB = (B) annBSousA3It.next();
  13. System.out.println("Sous A3 : "+annB);
  14. }
  15. }

Utilisation des contraintes (FSMatchConstraint)

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 :

  1. /**
  2.  *
  3.  * This method provides an iterator over typed annotations that either
  4.  * have an offset embedded in that of a given annotation in a document,
  5.  * or have the same offset as these annotation.
  6.  *
  7.  * @param theDocument the document in which stand the source and
  8.  * target annotations
  9.  * @param theAnnotation the source annotation under which target
  10.  * annotations that have to be drawn out
  11.  * @param theType the type of the target annotations that have
  12.  * to be drawn out from the document under
  13.  * the source annotation
  14.  * @param isStrict the boolean that defines the offset matching,
  15.  * offsets strictly equal if isStrict is true, begin
  16.  * offsets greater or equal and end offsets less
  17.  * or equal otherwise.
  18.  * @return the iterator over the type theType annotations
  19.  * which stand under the annotation theAnnotation
  20.  * in the document theDocument
  21.  *
  22.  * @author Fabien Poulard
  23.  * @author Jérôme Rocheteau
  24.  *
  25.  * @license Apache 2.0
  26.  */
  27. public FSIterator subiterator(JCas theDocument, Annotation theAnnotation,Type theType,boolean isStrict) {
  28. // Ajout: déclaration de la variable type
  29. Type theAnnotationType = theAnnotation.getType();
  30. // On utilise le constraint factory
  31. ConstraintFactory theConstraints = theDocument.getConstraintFactory();
  32. // On définit les contraintes sur le début de l'annotation
  33. FSIntConstraint beginConstraint = theConstraints.createIntConstraint();
  34. if (isStrict) {
  35. beginConstraint.eq(theAnnotation.getBegin());
  36. } else {
  37. beginConstraint.geq(theAnnotation.getBegin());
  38. }
  39. Feature beginFeature = theAnnotationType.getFeatureByBaseName("begin");
  40. FeaturePath beginPath = theDocument.createFeaturePath();
  41. beginPath.addFeature(beginFeature);
  42. FSMatchConstraint begin = theConstraints.embedConstraint(beginPath,beginConstraint);
  43. // ... puis sur la fin de l'annotation
  44. FSIntConstraint endConstraint = theConstraints.createIntConstraint();
  45. if (isStrict) {
  46. endConstraint.eq(theAnnotation.getEnd());
  47. } else {
  48. endConstraint.leq(theAnnotation.getEnd());
  49. }
  50. Feature endFeature = theAnnotationType.getFeatureByBaseName("end");
  51. FeaturePath endPath = theDocument.createFeaturePath();
  52. endPath.addFeature(endFeature);
  53. FSMatchConstraint end = theConstraints.embedConstraint(endPath, endConstraint);
  54. // JR: on définit une contrainte sur le type d'annotation
  55. FSTypeConstraint typeConstraint = theConstraints.createTypeConstraint();
  56. typeConstraint.add(theType); //
  57. FeaturePath typePath = theDocument.createFeaturePath();
  58. FSMatchConstraint type = theConstraints.embedConstraint(typePath, typeConstraint);
  59. // On combine les contraintes
  60. FSMatchConstraint beginAndEnd = theConstraints.and(type,theConstraints.and(begin, end));
  61. // On génère un itérateur respectant ces contraintes
  62. FSIterator filteredIterator =
  63. theDocument.createFilteredIterator(theDocument.getAnnotationIndex().iterator(), beginAndEnd);
  64. return filteredIterator;
  65. }

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.

Retour au sommaire
 

Tests unitaires pour UIMA avec UUTUC

0 vote
Par Fabien POULARD le 16/03/2010 à 15:45 Voir l'article

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).

Principe de UUTUC

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 :

  1. AnalysisEngine engine =
  2. AnalysisEngineFactory.createAnalysisEngineFromPath("descriptors/tutorial/ex1/RoomNumberAnnotator.xml");
  3. JCas jCas =
  4. AnalysisEngineFactory.process(engine, "data/WatsonConferenceRooms.txt");

Le couplage de UUTUC avec JUnit permet de mettre en place un banc de tests unitaires :

  • Afin de tester la conformité de l'implémentation avec les spécifications attendues ;
  • Prévenir les problèmes de régression lors de l'évolution des composants

Écriture 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 :

  1. import org.junit.Test;
  2. import static org.junit.Assert.*;
  3. ...
  4. /**
  5.  * This class defines the tests for the main methods of the Shingle class.
  6.  */
  7. public class ShingleTest {
  8. ...
  9. /**
  10.   * This method just checks that the isComplete method works
  11.   * @throws InvalidShingleException
  12.   * @throws OverloadShingleException
  13.   */
  14. @Test
  15. public void completeness() throws InvalidShingleException, OverloadShingleException {
  16. Shingle s1 = new Shingle(2);
  17. assertFalse( s1.isComplete() ); // before any adding
  18. s1.add( theShingleItems[0] );
  19. assertFalse( s1.isComplete() ); // after a first adding
  20. s1.add( theShingleItems[0] );
  21. assertTrue( s1.isComplete() ); // should be complete by now
  22. }
  23. ...
  24. }

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 :

  1. /** Static data for testing */
  2. private static String CAS_CONTENT =
  3. "Suisse : inauguration d'une nouvelle synagogue, une première depuis 50 ans";
  4. private static Integer[][] CAS_OFFSETS = { {0,6}, {9,21}, {22,24}, {24,27},
  5. {28,36}, {37,46}, {48,51}, {52,60}, {61,67}, {68,74} };
  6. ...
  7. /**
  8.  * This method is used to set up the testing environment, creating the
  9.  * data necessary for the different tests methods.
  10.  */
  11. @Before
  12. public void setUp() throws UIMAException, IOException, ShinglingTestingException {
  13. // Set up a CAS with a couple of shingle items in
  14. TypeSystemDescription tsd = TypeSystemDescriptionFactory
  15. .createTypeSystemDescription("shingling-ts");
  16. theTestingCas = JCasFactory.createJCas(tsd);
  17. theTestingCas.setDocumentText(CAS_CONTENT);
  18. for(Integer[] idx: CAS_OFFSETS) {
  19. AnnotationFactory.createAnnotation(theTestingCas,
  20. idx[0], idx[1], ShingleItem.class) );
  21. }
  22. }

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.

Intégration avec Maven

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 :

  1. <repository>
  2. <snapshots>
  3. <enabled>false</enabled>
  4. </snapshots>
  5. <id>uutuc-googlecode</id>
  6. <name>uutuc Google Code repository</name>
  7. <url>http://uutuc.googlecode.com/svn/repo/</url>
  8. </repository>
  9. ...
  10. <!-- UUTUC for testing -->
  11. <dependency>
  12. <groupId>org.uutuc</groupId>
  13. <artifactId>uutuc</artifactId>
  14. <version>0.9.10</version>
  15. <optional>false</optional>
  16. <scope>test</scope>
  17. </dependency>
  18. <!-- JUnit 4 for testing -->
  19. <dependency>
  20. <groupId>junit</groupId>
  21. <artifactId>junit</artifactId>
  22. <version>4.3.1</version>
  23. <scope>test</scope>
  24. </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 :

  1. <!-- Testing -->
  2. <plugin>
  3. <groupId>org.apache.maven.plugins</groupId>
  4. <artifactId>maven-surefire-plugin</artifactId>
  5. <configuration>
  6. <reportFormat>brief</reportFormat>
  7. <useFile>false</useFile>
  8. </configuration>
  9. </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 ;)

Retour au sommaire
 

Utilisation du CAS Editor

0 vote
Par Fabien POULARD le 06/03/2010 à 19:21 Voir l'article

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 propos

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...

Initialiser un projet

Le fonctionnement du CAS Editor est lié à :

  • une vue CAS Editor
  • un projet CAS Editor

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 :

  1. Créer un projet CAS Editor
  2. Passe en vue CAS Editor afin d'accéder aux fonctionnalités spécifiques
  3. Créer un répertoire corpus
  4. Importer dans ce répertoire corpus des fichiers textes afin de les transformer en CAS
  5. Ouvrir et visualiser le contenu des fichiers (CAS) du corpus

Ajouter des annotations manuellement

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 :

  1. Sélectionner le texte
  2. Sélectionner le type d'annotation dans le Feature View
  3. Appuyer sur Entrée

l'autre permet de choisir le type de chaque nouvelle annotation rajoutée :

  1. Sélectionner le texte
  2. Appuyer sur shift + entrée
  3. Choisir l'annotation à ajouter

La vidéo ci-dessous illustre ces deux méthodes :

Utiliser un AE pour ajouter des annotations

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.

Construction et installation du PEAR

Pour l'exemple je vais prendre le WhitespaceTokenizer, ce dernier a deux avantages pour ce tutoriel :

  • Il est simple d'utilisation (pas de paramètres) et n'a pas de dépendances sur d'autres composants
  • Il fait parti officiellement d'UIMA et distribué sur le site : UIMA Annotator Addons & Simple Server & Pear packaging tools

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 :

  1. ...
  2. <repositories>
  3. <repository>
  4. <id>apache</id>
  5. <name>Apache UIMA</name>
  6. <layout>default</layout>
  7. <url>http://people.apache.org/repo/m2-incubating-repository/</url>
  8. </repository>
  9. </repositories>
  10. <pluginRepositories>
  11. <pluginRepository>
  12. <id>apache</id>
  13. <name>Apache UIMA</name>
  14. <url>http://people.apache.org/repo/m2-incubating-repository/</url>
  15. <layout>default</layout>
  16. </pluginRepository>
  17. </pluginRepositories>
  18. ...

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 :

Visualiser et modifier les annotations

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 :

  • Par le menu contextuel, Mode permet de sélectionner la façon dont on parcours/sélectionne les annotations ;
  • L'onglet Feature Structure View permet de sélectionner les types d'annotation à faire apparaître dans l'onglet Outline, il alors possible dans ce dernier de supprimer des annotations (croix rouge) ;
  • Les onglets Edit View permettent quant à eux de modifier les valeurs des champs de l'annotation sélectionnée.

La vidéo ci-dessous illustre ces différentes manipulations :

Retour au sommaire
 

Release du collection reader pour Wikipedia v.0.4

0 vote
Par Fabien POULARD le 04/03/2010 à 13:39 Voir l'article

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 jar du composant (et ses dépendances : mwdumper et wikimodel.wem)
  • Les sources du composant

Présentation du composant

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 :

  • Pas d'accès répétitifs aux serveurs des projets MediaWiki, préservant la bande passante et le temps de calcul de ces derniers ;
  • Pas besoin de déployer un serveur de base de données en local et d'y importer les données de dump (si ça vous amuse, vous pouvez toujours suivre ce tutoriel) ;
  • Limiter l'espace disque disponible pour stocker les informations en utilisant directement la version compressée des dumps ;
  • Limiter la latence dans l'accès aux données provoquées par les requêtes réseau ou bien le serveur SQL.

Les fonctionnalités de cette version 0.4 sont les suivantes :

  • Chargement à partir d'un dump XML compressé ou non ;
  • Nombreuses options de filtrage (voir plus bas) concernant les pages et révisions à charger dans la chaîne de traitement ;
  • Interprétation de la syntaxe wiki et annotation des Titres, Sections, Paragraphes et Liens (cf. ce billet pour plus d'informations).

Installation

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.

Utilisation

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

Interface du 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.

Interface de configuration du Mediawiki UIMA Loader

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 :

  • Latest Revision Only, si vous cochez cette case seules la dernière révision disponible pour chaque article sera chargée, sinon toutes les révisions (présentes dans le dump) seront chargées ;
  • Ignore Talks, si vous cochez cette case, les pages de type discussion seront ignorées, sinon elles seront également chargées ;
  • Config Namespaces Filter, ce champs permet de spécifier les espaces de nom à considérer lors du chargement. S'il est laissé vide, tous les espaces de nom sont chargés. Pour wikipedia les espaces de nom disponibles sont :
    • -2 : ressources de type média ;
    • -1 : pages spéciales ;
    • 0 : espace de nom principal où l'on trouve les articles de l'encyclopédie ;
    • 1 : discussions à propos des articles ;
    • 2 : pages des utilisateurs ;
    • 3 : discussion à propos des utilisateurs ;
    • 4 : espace Wikipédia (le projet)
    • 5 : espace de discussion autour du projet Wikipédia
    • 6 : fichiers
    • 7 : discussion à propos des fichiers
    • 8 : espace MédiaWiki (le logiciel)
    • 9 : discussion à propos de MédiaWiki
    • 10 : modèles
    • 11 : discussion à propos des modèles
    • 12 : aide
    • 13 : discussion à propos de l'aide
    • 14 : catégories
    • 15 : discussion à propos des catégories
    • 100 : portail
    • 101 : discussion à propos du portail
    • 102 : projets
    • 103 : discussion autour des projets
    • 104 : références
    • 105 : discussion autour des références

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 ;

  • Config Title Match, ce champs permet à l'aide d'une expression rationnelle de filtrer les pages dont le titre valide l'expression rationnelle. Par exemple : A.* pour toutes les pages commençant dont le titre commence par A ;
  • Config List Filter et Config Exact List Filter, ces champs permette d'indiquer en paramètre le chemin d'un fichier contenant un nom de page par ligne. Seules les pages précisées dans ce fichier seront chargées. Si c'est le paramétrage Exact qui est employé, le titre doit correspondre exactement, autrement le filtre vérifie s'il correspond au titre de l'article ou éventuellement de sa page de discussion ;
  • Config Revision List Filter, ce champs permet de renseigner le chemin d'un fichier contenant en paramètre les numéros de révision à charger (une révision par ligne) ;
  • Config Before Timestamp Filter et Config After Timestamp Filter, ces champs permettent de délimiter temporellement les données à importer en indiquant des dates limites de début et de fin au format yyyy-MM-dd'T'HH:mm:ss'Z'.

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.

Retour au sommaire
Page suivante »
Produit par le BilboPlanet CSS - Xhtml valide Produit par le BilboPlanet Retour au début