Fabien POULARD
Jérôme Rocheteau
Laurent Audibert
Matthieu Vernier
Nicolas Hernandez
Nicolas Hernandez
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 :
Le développement d'UIMA AS cherche à répondre à ces limitations.
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.
Lorsqu'une chaîne de traitement UIMA est déployée par le CPM, la configuration suivante se met en place :
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) :
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 :
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.
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...
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 :
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
Le déploiement d'un service UIMA AS nécessite deux descripteurs :
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.
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.
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.
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.
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 :
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) ;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 :
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 !"); } } }
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.
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
}
}
AnnotationIndex<annotation> aSentenceAnnotationAnnotationIndex = aJCas.getAnnotationIndex(SentenceAnnotation.type);
Iterator<annotation> aSentenceAnnotationIterator = aSentenceAnnotationAnnotationIndex.iterator();
while (aSentenceAnnotationIterator.hasNext()) {
SentenceAnnotation aSentenceAnnotation = (SentenceAnnotation) aSentenceAnnotationIterator.next();
// Faire quelque chose
}
FSIterator<annotation> anySubSentenceAnnotationFSIterator = anAnnotationIndex.subiterator(aSentenceAnnotation);
while (anySubSentenceAnnotationFSIterator.hasNext()) {
Annotation aSubSentenceAnnotation = (Annotation) anySubSentenceAnnotationFSIterator.next();
// Faire quelque chose
}
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++;
}
boolean containsAVerb = false;
boolean containsAKeyword = false;
int wordCounter = 0;
if ((containsAKeyword) && (containsAVerb)) { Opinion aOpinion = new Opinion(aJCas); aOpinion.setBegin(aSentenceAnnotation.getBegin()); aOpinion.setEnd(aSentenceAnnotation.getEnd()); aOpinion.setLength(wordCounter); aOpinion.addToIndexes();}
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.
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.
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 :
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 :
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.
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 ...)
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.
<capability>et
...
<inputSofas>
<sofaName>xmlDocument</sofaName>
</inputSofas>
...
<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.
<sofaMapping>
<componentKey>XmlDetagger</componentKey>
<componentSofaName>xmlDocument</componentSofaName>
<aggregateSofaName>_InitialView</aggregateSofaName>
</sofaMapping>
</sofaMappings>
<sofaName>_InitialView</sofaName>et l'élément suivant
<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
<componentKey>WhitespaceTokenizer</componentKey>
<componentSofaName>_InitialView</componentSofaName>
<aggregateSofaName>plainTextDocument</aggregateSofaName>
</sofaMapping>
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; }
<w cat="ADV" ee="ADV" ei="ADV" lemma="à tout prix"> <w catint="P">à</w> <w catint="D">tout</w> <w catint="N">prix</w> </w>echo '<?xml version="1.0" encoding="UTF-8"?>' > frenchTreebank.xml ; echo "<ftb>" >> frenchTreebank.xml ; for f in `ls lmf*.xml`; do echo $f; export HEAD=`head -1 $f`; if [ "$HEAD" == '<?xml version="1.0" encoding="UTF-8"?><text <text>' ]; then echo " > " >> frenchTreebank.xml " >> frenchTreebank.xml; fi; cat $f |grep -v "xml version" >> frenchTreebank.xml; done ; echo "</ftb>