mercredi 17 août 2011

Apache UIMA par la pratique

Dans un précédent billet, je vous ai brossé un portrait rapide de Apache UIMA.

Je vous propose aujourd'hui de vous présenter "la bête" un peu plus en détail suite à ma lecture de la documentation. (Bien faite cela dit en passant) Pour ce qui est de la JavaDoc, elle semble également à la hauteur en tous cas à première vue.

Voici donc un exemple de création de composants et leur assemblage.
Pour avoir d'avantage de détails sur les différents points abordés ici, je vous propose de vous référer à la documentation qui se trouve ici  pour l'implémentation "de base" et pour la version AS.


Le but de l'exemple :

Parser une page HTML pour récupérer les tags H1 à Hx pour en faire un sommaire.


Mise en place de l'environement :

Avant de commencer téléchargeons quelques plugins pour notre environnement de développement préféré en utilisant l'URL ci-dessous
http://www.apache.org/dist/uima/eclipse-update-site/

Ces plugins vont nous permettre de créer plus facilement les différents descripteurs XML utilisés par les composants.

Récupérons ensuite les dependances maven de base :
  • uimaj-cpe pour obtenir toutes les dépendances nécessaires au fonctionnement de UIMA (à l'exception de UIMA-AS)
  • uimaj-tools pour disposer comme son nom l'indique d'outils (ex : le DocumentAnalyzer qui permet de valider un AnalysisEngine en lui fournissant soit un fichier soit en saisissant directement un contenu, en sortie les annotations sont mises en évidence)

 <dependencies>
   <dependency>
      <groupId>org.apache.uima</groupId>
      <artifactId>uimaj-tools</artifactId>
      <version>2.3.1</version>
      <type>jar</type>
      <scope>compile</scope>
   </dependency>
   <dependency>
      <groupId>org.apache.uima</groupId>
      <artifactId>uimaj-cpe</artifactId>
      <version>2.3.1</version>
      <type>jar</type>
      <scope>compile</scope>
   </dependency>
  </dependencies>


Lecture des fichiers avec un composant "Collection Reader" :

Les "Collection Reader" permettent de créer des CAS pour chaque ressource (ie: artefact en UIMA). A noter que la création des CAS initiaux n'est pas réservée aux "Collection Reader", en fonction des besoins il est tout à fait possible de créer une chaine de traitement dans notre application et lui passer des CAS que l'on crée au besoin, l'application fait donc office de "Collection Reader".

Pour nous permettre de manipuler un minimum tous les types de composant, nous allons créer notre propre "Collection Reader" qui va lire dans un dossier les fichiers dont l'extension est ".html". (Mais vous pouvez utiliser directement le FileSystemCollectionReader qui est décrit dans
la documentation...)

Un "Collection Reader" doit implémenter l'interface  org.apache.uima.collection.CollectionReader. Comme pour tous les composants UIMA, il ya une classe abstraite qui fournit déjà les implémentations génériques pour les fonctions les plus communes. (org.apache.uima.collection.CollectionReader_ImplBase)
 
Lorsque l'on crée un "Collection Reader" héritant de l'implémentation de base, il nous reste les fonctions suivantes à implémenter :

  • void initialize()

Initialise de composant. En fait cette méthode ne fait rien par défaut, mais pour les besoins de notre exemple nous allons la surcharger. Il existe une autre méthode d'initialisation qu'il est préférable de ne pas surcharger d'après la documentation.

  protected List<File> files = new ArrayList<File>();
    protected Iterator<File> iter;
    protected int count = 0;

    public void initialize() throws ResourceInitializationException {
      String srcDir =  (String)getConfigParameterValue("sourcedirectory");
      File directory = new File(srcDir);
      if (!directory.isDirectory()) {
         // use i18n, first parameter is the path

         // to the properties file containing messages.
         throw new ResourceInitializationException(
                     "messages.properties",
                     "not.directory.error",
                     new String[]{srcDir});
      }

      for (File f : directory.listFiles(new HtmlFileFilter())) {
           files.add(f);
      }
      iter = files.iterator();
   }


  • void getNext(CAS aCAS)

Alimente le CAS fourni en paramètre par le prochain élément à traiter dans la collection. En d'autres termes, on initialise grâce à cette méthode l'artefact du CAS (ou le Subject Of Analysis - SofA). On peut également en profiter pour ajouter des informations sur la resource grâce aux annotations (au sens UIMA du terme) SourceDocumentInformation (taille, URI) et DocumentAnnotation (langue du document). Attention, il faut absolument que les Types que vous utilisez dans le composant soient bien décrit dans le descripteur.

  public void getNext(CAS aCAS) throws IOException, CollectionException {
      JCas jcas;
      try {
        jcas = aCAS.getJCas();
      } catch (CASException e) {
        throw new CollectionException(e);
      }

      // open input stream to file
      File file = (File) iter.next();
      BufferedInputStream fis =  new BufferedInputStream(new FileInputStream(file));

      try {
        byte[] contents = new byte[(int) file.length()];
        fis.read(contents);
        String text = new String(contents);
        // put document in CAS
        jcas.setDocumentText(text);
      } finally {
        if (fis != null)
            fis.close();
      }  

      count++;
      // update source document info annotation
      SourceDocumentInformation srcInfo = new SourceDocumentInformation(jcas);
      SourceDocumentInformation srcInfo = new SourceDocumentInformation(jcas);
      srcInfo.setUri(file.getAbsoluteFile().toURL().toString());
      srcInfo.setLastSegment(!iter.hasNext());
      srcInfo.addToIndexes();
  }

  public class HtmlFileFilter implements FilenameFilter {
      public boolean accept(File dir, String name) {
        return name.endsWith("html");
      }
  }

  • boolean hasNext()

Retourne vrai si il y a encore des ressources à traiter.

   public boolean hasNext() 
   throws IOException, CollectionException {
      return iter.hasNext();
   }

  •  Progress[] getProgress()

Retourne les compteurs sur les ressources traitées par rapport aux ressources total à traiter, les unités de ces compteurs sont laissées
à la discression du developpeur (nombre de fichier, quantité de données... à chaque unité, un objet Progress)
 
   public Progress[] getProgress() {
      return new Progress[]{
                 new ProgressImpl 
                 (count,files.size(),Progress.ENTITIES)};
   }
  •  void close()    

Y a-t-il vraiment besoin de l'expliquer ? :)

        public void close() throws IOException {
            // rien dans notre cas
        }

Vous avez surement constaté que nous accédions à un paramètre de configuration (appel à la méthode getConfigParameterValue). Comme pour
tous les composant UIMA, les paramètres de configuration sont déclarés dans le descripteur du composant. Voici à quoi resemble le descripteur
de note collection reader. Rien de particulier dans notre cas, on ne fait que déclarer la classe que nous allons utiliser en tant
que collection reader et lui attribuer un paramètre de configuration "sourcedirectory" de type String avec comme valeur par défaut le répertoire courant.
Ce composant utilise une annotation de type SourceDocumentInformation qui est disponible en sortie.

Pour plus de facilités lors de la création de ce fichier XML, je vous recommande de passer par les plugins que nous avons téléchargés.
Je vous conseille également la documentation pour plus de précision sur le contenu de ce descripteur.

    <?xml version="1.0" encoding="UTF-8"?>
       <collectionReaderDescription xmlns="http://uima.apache.org/resourceSpecifier">
        <frameworkImplementation>org.apache.uima.java</frameworkImplementation>
        <implementationName>org.apache.uima.test.html.content.FileReader</implementationName>
         <processingResourceMetaData>
           <name>collectionReaderDescriptor</name>
           <description/>
           <version>1.0</version>
           <vendor/>
           <configurationParameters>
             <configurationParameter>
               <name>sourcedirectory</name>
               <type>String</type>
               <multiValued>false</multiValued>
               <mandatory>false</mandatory>
             </configurationParameter>
           </configurationParameters>
           <configurationParameterSettings>
             <nameValuePair>
               <name>sourcedirectory</name>
               <value>
                 <string>.</string>
               </value>
             </nameValuePair>
           </configurationParameterSettings>
           <typeSystemDescription>
             <types>
               <typeDescription>
                <name>org.apache.uima.examples.SourceDocumentInformation</name>
                 <description/>
                 <supertypeName>uima.tcas.Annotation</supertypeName>
                 <features>
                   <featureDescription>
                     <name>uri</name>
                     <description/>
                     <rangeTypeName>uima.cas.String</rangeTypeName>
                   </featureDescription>
                   <featureDescription>
                     <name>lastSegment</name>
                     <description/>
                     <rangeTypeName>uima.cas.Boolean</rangeTypeName>
                   </featureDescription>
                 </features>
               </typeDescription>
             </types>
           </typeSystemDescription>
           <typePriorities/>
           <fsIndexCollection/>
           <capabilities>
             <capability>
               <inputs/>
               <outputs>
                 <type allAnnotatorFeatures="true">org.apache.uima.examples.SourceDocumentInformation</type>
               </outputs>
               <languagesSupported/>
             </capability>
           </capabilities>
           <operationalProperties>
             <modifiesCas>true</modifiesCas>
             <multipleDeploymentAllowed>false</multipleDeploymentAllowed>
             <outputsNewCASes>true</outputsNewCASes>
           </operationalProperties>
         </processingResourceMetaData>
         <resourceManagerConfiguration/>
       </collectionReaderDescription>
  
Remarque : Ce composant n'a pas besoin d'être threadsafe, le framework garantit qu'un seul thread utilise le composant à la fois.


Place à l'analyse du document


Maintenant que nous disposons d'un composant pour lire nos documents, il faut pouvoir les annoter. Passons donc à la réalisation des "Analysis Engines".

Les Analysis Engine sont les unités de traitement des artefacts. Ce sont ces AE qui alimentent les CAS avec les annotations.

Comment créer un Analyse Engine? Tout d'abord, il faut définir la structure de données manipulée par notre AE. Cette structure doit être une sous classe de org.apache.uima.jcas.cas.TOP. (Il existe des types de base déjà disponible String, Long... et une sorte de conteneur FSArray pour "Features Struture Array" qui est en fait un tableau de TOP). Cette structure sera générée par un Annotator. (Sous classe de uima.tas.Annotation qui hérite elle-même de la classe TOP)

Nous définissons donc la structure Heading qui n'est en fait qu'une chaine de caractères. Pour créer notre "Annotator" et la structure associée, nous allons utiliser le plugin eclipse qui manipule les AEDescriptors.

New > Others > UIMA > Analyse Engine Descriptor

On ouvre le fichier xml créé et on active l'onglet "Type System" pour y ajouter notre structure de données. Pour cela, on clique sur "Add Type" et on précise le nom de l'annotation (ie : le nom complet de la classe) en indiquant qu'il
s'agit d'une annotation (uima.tcas.Annotation). Une fois cette annotation créée, il est possible de lui attributer des attributs en la sélectionnant et en cliquant sur le bouton "Add". Dans notre cas, nous lui ajoutons un field title de type uima.cas.String.

Ceci fait, il est possible de générer les classes en cliquant sur JCasGen. (Une erreur apparaît lorsque vous enregistrez, elle indique que votre AE n'a pas de nom. Ce qui est normal puisque nous ne l'avons pas encore créé :D)

Passons donc à la création de l'AnalyseEngine. Comme pour le Collection Reader, il y a une interface et une classe de base pour les AnalysisEngines qui sont respectivement :  org.apache.uima.analysis_component.AnalysisComponent et org.apache.uima.analysis_component.JCasAnnotator_ImplBase.
Il est à noter que notre classe de base n'implémente pas directement l'interface mais étend une série de 2 ou 3 classes. Au final, il ne nous reste plus qu'une méthode à implémenter :

    @Override
    public void process(JCas aJCas) throws AnalysisEngineProcessException {
      // read configuration parameter
      String tag = (String)getContext().getConfigParameterValue("tagName");
      /*
       *  Get Sofa content to analyze.
       *  If the content is an embedded text, you can use the getDocumentText().
       *  Otherwise, you can access the artefact content using the aJCas.getSofaDataxxx method.
       */
      String content = aJCas.getDocumentText();
      /*
       * Define tags to search
       */
      Pattern startTagPattern = Pattern.compile("<" + tag + ">");
      Pattern endTagPattern = Pattern.compile("</" + tag + ">");
      Matcher startMatcher = startTagPattern.matcher(content);
      Matcher endMatcher = endTagPattern.matcher(content);
      JCas tagView;
      try {
        /*
         * Create a view and its underlying Sofa (subject of analysis).
         * The view provides access to the Sofa data and the index repository
         * that contains metadata (annotations and other feature structures)
         * pertaining to that Sofa.
         *
         * Remark : This method creates the underlying Sofa feature structure,
         * but does not set the Sofa data. Setting ths Sofa data must be done
         * by calling setSofaDataArray(FeatureStructure, String),
         * setSofaDataString(String, String) or setSofaDataURI(String, String)
         * on the JCas view returned by this method.
         */
        tagView = aJCas.createView(tag);
      } catch (CASException e) {
        /*
         * By default, UIMA provides a logging system similar to the standard Java Logging system. But it is possible
         * to specify a specific logger implementation on startup using the following option:
         * -Dorg.apache.uima.logger.class=<org.apache.uima.util.impl.Log4jLogger_impl>
         */
        getContext().getLogger().log(Level.WARNING, "The view " + tag + " already exists.");
        throw new AnalysisEngineProcessException("messages.properties", "exist.view.error", new String[] { tag });
      }
      while (startMatcher.find() && endMatcher.find()) {
        int titleStartIndex = startMatcher.end();
        int titleEndIndex = endMatcher.start();
        String title = content.substring(titleStartIndex, titleEndIndex);
        Heading heading = new Heading(tagView, titleStartIndex, titleEndIndex);
        heading.setTitle(title);
        // register the CAS in the view
        heading.addToIndexes();
        heading.addToIndexes();
        // register the CAS in the input CAS is impossible. You have to make a choice
        // heading.addToIndexes(aJCas);
      }
     } 

En dehors de cette méthode, il en existe trois autres qui sont :

  • initialize pour initialiser le composant. Si vous devez surcharger cette méthode, veillez à appeler l'implémentation de la super classe pour garantir que les initialisations nécessaires au bon fonctionnement de l'application soient faites, notamment l'initialisation du contexte qui nous permet de lire la configuration du composant. Cette méthode n'est appelée qu'une seule fois à la création du composant.
  • destroy appelée lorsque l'application se termine afin de libérer les ressources utilisées par le composant.
  • reconfigure qui n'est jamais appelée par le framework UIMA, elle permet de recharger la configuration du composant lorsque l'utilisateur l'appelle. Par défaut, cette méthode appelle un destroy puis un initialize.

Pour accéder à la configuration du composant, il faut passer par le contexte :

String myConf = (String) getContext().getConfigParameterValue("myConf");

La récupération du contenu à traiter se fait (dans notre cas) par la méthode getDocumentText du CAS passé en paramètre. Pour obtenir le document qui n'est pas un texte (une video par exemple), on utilise getSofaAsByStream avec getSofaMimeType pour connaitre le type du contenu. Si la ressource n'est pas accessible en locale, il faut passer par la méthode getSofaDataURI.

Pour la notion de vue, je pense que le commentaire présent dans l'exemple (une copie de la javadoc) est suffisant. Une remarque toute fois, une annotation qui est indexée dans une vue n'est accéssible que dans cette vue.

Maintenant que notre AE est créé, il est possible de l'ajouter au descripteur. Pour ce faire, il faut déclarer le nom complet de la classe dans l'onglet "Overview". Dans cet onglet nous précisons également :
  • que c'est une classe écrite en Java
  • qu'il est autorisé à modifier le CAS
  • que c'est un"AnalysisEngine" de type "Primitive". Le type "Aggregate" est en fait un descripteur qui regroupe plusieurs "AE Primitive" au sein d'un workflow configurable.

En passant dans l'onglet "Paramerters" & "Parameter Settings", nous ajoutons un paramètre de configuration "tagName" avec la vajeur "H1" par défaut.

Pour finir, nous précisons que notre composant accépte en entrée/sortie les annotations de type Heading.

Au final notre descripteur ressemble à celà :


  <?xml version="1.0" encoding="UTF-8"?>
  <analysisEngineDescription xmlns="http://uima.apache.org/resourceSpecifier">
     <frameworkImplementation>org.apache.uima.java</frameworkImplementation>
      <primitive>true</primitive>
    <annotatorImplementationName>
org.apache.uima.test.html.content.HeadingAE
</annotatorImplementationName>
      <analysisEngineMetaData>
      <name>ContentDescriptor</name>
      <description/>
      <version>1.0</version>
      <vendor/>
      <configurationParameters>
        <configurationParameter>
        <name>tagName</name>
        <type>String</type>
        <multiValued>false</multiValued>
        <mandatory>false</mandatory>
        </configurationParameter>
      </configurationParameters>
      <configurationParameterSettings>
        <nameValuePair>
        <name>tagName</name>
        <value>
          <string>H1</string>
        </value>
        </nameValuePair>
      </configurationParameterSettings>
      <typeSystemDescription>
        <types>
        <typeDescription>
          <name>org.apache.uima.test.html.content.Heading</name>
          <description/>
          <supertypeName>uima.tcas.Annotation</supertypeName>
          <features>
          <featureDescription>
            <name>title</name>
            <description/>
            <rangeTypeName>uima.cas.String</rangeTypeName>
          </featureDescription>
          </features>
        </typeDescription>
        </types>
      </typeSystemDescription>
      <typePriorities/>
      <fsIndexCollection/>
      <capabilities>
        <capability>
        <inputs>
          <type allAnnotatorFeatures="true">org.apache.uima.test.html.content.Heading</type>
        </inputs>
        <outputs>
          <type allAnnotatorFeatures="true">org.apache.uima.test.html.content.Heading</type>
        </outputs>
        <languagesSupported/>
        </capability>
      </capabilities>
      <operationalProperties>
        <modifiesCas>true</modifiesCas>
        <multipleDeploymentAllowed>true</multipleDeploymentAllowed>
        <outputsNewCASes>false</outputsNewCASes>
      </operationalProperties>
      </analysisEngineMetaData>
      <resourceManagerConfiguration/>
  </analysisEngineDescription>

A noter que nous pouvons dans ce descripteur définir des chemins vers des ressources accessibles depuis le contexte. Pour plus de détails sur ce point ainsi que ceux non abordés dans cette partie, je vous invite à consulter la documentation.

Si vous voulez tester votre AnalysisEngine avant d'aller plus loin, vous pouvez lancer la classe org.apache.uima.tools.docanalyer.DocumentAnalyzer.
Une fois l'IHM affichée, chargez le descripteur de votre AE et choisissez le mode interactif. Saisissez du texte et vérifiez que les annotations sont correctement générées. Si vous essayez avec le composant que nous venons de réaliser, vous ne verrez rien !

Il ne s'agit pas d'un bug de notre part puisque que nous avons utilisé les vues pour indexer nos différentes annotations. Or, comme je vous l'ai indiqué, les view utilisent leur propre repository d'index, il est donc impossible pour le DocumentAnalyzer de retrouver nos annotations Heading à partir des index du CAS passé en paramètre de la méthode.


Exploitaiton des annotation dans un CASConsumer.


Maintenant que nous disposons d'une liste d'annotations Heading, nous pouvons créer notre CASConsumer pour afficher une table des matières sur la sortie standard. Les CAS Consumer sont les composants de fin de chaîne qui exploitent les différentes annotations réalisées par les AE. La  documentation précise qu'un CasConsumer n'est en réalité qu'un AE pour lequel il y a quelques restrictions au niveau de sa configuration.
(Paramètre multipleDeploymentAllowed à false, pas de modification des CAS, etc... )

Comme pour les composants précédents, il y a une interface avec une implémentation de base, respectivement  org.apache.uima.collection.CasConsumer  et org.apache.uima.collection.CasConsumer_ImplBase.

La seule méthode qu'il nous reste à implémenter suite à notre extension de la classe de base s'appelle "processCas". Voici l'implémentation que je propose pour obtenir les différentes vues que nous avons construites et en afficher le contenu dans un sommaire.

     public void processCas(CAS aCAS) throws ResourceProcessException {
      JCas aJCas;
      try {
       aJCas = aCAS.getJCas();
       String[] tags = (String[])getConfigParameterValue("tagList");
       Content content = new Content();
       content.setTitle("CONTENT");
       List<Content> previousSubContentList = new ArrayList<ContentWriter.Content>();
       previousSubContentList.add(content);
       for(String tagName : tags) {
         
        List<Content> currentSubContentList = new ArrayList<ContentWriter.Content>();
        
        // get tag view
        JCas currentView = aJCas.getView(tagName);

        // access to Heading annotations and iterate on it.
        AnnotationIndex headingIndex = currentView.getAnnotationIndex(Heading.type);
        Iterator<Heading> iterHeading = headingIndex.iterator();
        Heading currentHeading;
        if (iterHeading.hasNext()) {
            currentHeading = iterHeading.next();
            boolean hasNext = false;
            do {
          
             for (int i = 0; i < previousSubContentList.size(); ++i) {
              Content parentContent = previousSubContentList.get(i);
            
              if (parentContent.hasSubTitle(currentHeading)) {
               Content subTitle = new Content();
               subTitle.setTitle(currentHeading.getTitle());
               subTitle.setEnd(currentHeading.getEnd());
             
               hasNext = iterHeading.hasNext();
               if (hasNext) {
                currentHeading = iterHeading.next();
                subTitle.setNextContent(currentHeading.getBegin());
               }
               parentContent.addSubTitle(subTitle);
               currentSubContentList.add(subTitle);
               break;
              }
             }
           
            } while (hasNext);
          
        }
        previousSubContentList = currentSubContentList;
       }
       content.display();
      } catch (CASException e) {
       throw new ResourceProcessException();
      }
     }
     public class Content {
      public int end = 0;
      public int nextContent = Integer.MAX_VALUE;
      public String title;
      public List<Content> subTitles = new ArrayList<ContentWriter.Content>();
      public boolean hasSubTitle(Heading heading) {
       return ((heading.getBegin() > end) && (nextContent > heading.getBegin()));
      }
      public void addSubTitle(Content subTitle) {
       subTitles.add(subTitle);
      }
      public void setTitle(String title) {
       this.title = title;
      }
      public void setEnd(int end) {
       this.end = end;
      }
      public void setNextContent(int nextContent) {
       this.nextContent = nextContent;
      }
      public void display() {
       System.out.println(title);
       for (int i = 0; i < subTitles.size(); ++i) {
        subTitles.get(i).display(""+(i+1));
       }
      }
      private void display(String index) {
       System.out.println(index + " " + title);
       for (int i = 0; i < subTitles.size(); ++i) {
        subTitles.get(i).display(index +"." + (i+1));
       }
      }
   }

Comme pour les autres composant, nous avons la possibilité de surcharge une méthode d'initialisation, de nettoyage... Je vous laisse consulter la JavaDoc du CASConsumer pour plus de détails.

Voici à quoi ressemble le descripteur de ce composant, vous remarquerez que cette fois, j'utilise un tableau de valeurs en parametre et qu'il est obligatoire :

    <?xml version="1.0" encoding="UTF-8"?>
    <casConsumerDescription xmlns="http://uima.apache.org/resourceSpecifier">
     <frameworkImplementation>org.apache.uima.java</frameworkImplementation>
     <implementationName>org.apache.uima.test.html.content.ContentWriter</implementationName>
      <processingResourceMetaData>
        <name>casConsumerDescriptor</name>
        <description/>
        <version>1.0</version>
        <vendor/>
        <configurationParameters>
          <configurationParameter>
            <name>tagList</name>
            <type>String</type>
            <multiValued>true</multiValued>
            <mandatory>true</mandatory>
          </configurationParameter>
        </configurationParameters>
        <configurationParameterSettings>
          <nameValuePair>
            <name>tagList</name>
            <value>
              <array>
                <string>H1</string>
                <string>H2</string>
                <string>H3</string>
              </array>
            </value>
          </nameValuePair>
        </configurationParameterSettings>
        <typeSystemDescription/>
        <typePriorities/>
        <fsIndexCollection/>
        <capabilities>
          <capability>
            <inputs/>
            <outputs/>
            <languagesSupported/>
          </capability>
        </capabilities>
        <operationalProperties>
          <modifiesCas>false</modifiesCas>
          <multipleDeploymentAllowed>false</multipleDeploymentAllowed>
          <outputsNewCASes>false</outputsNewCASes>
        </operationalProperties>
      </processingResourceMetaData>
      <resourceManagerConfiguration/>
    </casConsumerDescription>

Comme pour les autres composants, n'hésitez pas à utiliser l'éditeur de descripteurs proposé dans la liste des plugins UIMA.

Pour nous permettre de tester un enchaînement de composants et en même temps construire le descripteur XML de notre chaine de traitements, le jar uima-tool vous fournit la classe org.apache.uima.tools.CpmFrame. Après exécution, nous disposons d'une IHM qui permet de charger des descripteurs de CollectionReader, d'AE et de CASConsumer.

Dans notre exemple, j'ai ajouté notre FileReader, 3 fois le même descripteur de HeadingAE avec un paramètre de configuration différent et notre
ContentWriter. J'ai placé dans le dossier courant, le fichier test.hmtl suivant :

   <html>
      <H1>T1</H1>
        <H2>ST1</H2>
          <H3>SST1</H3>
        <H2>ST2</H2>
          <H3>SST2</H3>
          <H3>SST3</H3>
      <H1>T2</H1>
        <H2>ST3</H2>
        <H2>ST4</H2>
          <H3>SST4</H3>
     </html>

Et voici ce que j'ai obtenu sur ma console :

    CONTENT
    1 T1
    1.1 ST1
    1.1.1 SST1
    1.2 ST2
    1.2.1 SST2
    1.2.2 SST3
    2 T2
    2.1 ST3
    2.2 ST4
    2.2.1 SST4

J'ai l'impression que ça fonctionne !

Nous pouvons sauvegarder notre CPEDescriptor, il ressemble à ceci :

    <?xml version="1.0" encoding="UTF-8"?>
    <cpeDescription xmlns="http://uima.apache.org/resourceSpecifier">
        <collectionReader>
            <collectionIterator>
                <descriptor>
                    <import location="file:///home/eric/developpement/workspace/HtmlContent/desc/collectionReaderDescriptor.xml"/>
                </descriptor>
            </collectionIterator>
        </collectionReader>
        <casProcessors casPoolSize="3" processingUnitThreadCount="1">
            <casProcessor deployment="integrated" name="ContentDescriptor">
                <descriptor>
                    <import location="file:///home/eric/developpement/workspace/HtmlContent/desc/ContentDescriptor.xml"/>
                </descriptor>
                <deploymentParameters/>
                <errorHandling>
                    <errorRateThreshold action="terminate" value="0/1000"/>
                    <maxConsecutiveRestarts action="terminate" value="30"/>
                    <timeout max="100000" default="-1"/>
                </errorHandling>
                <checkpoint batch="10000" time="1000ms"/>
            </casProcessor>
           <casProcessor deployment="integrated" name="ContentDescriptor 2">
                <descriptor>
                    <import location="file:///home/eric/developpement/workspace/HtmlContent/desc/ContentDescriptor.xml"/>
                </descriptor>
                <deploymentParameters/>
                <errorHandling>
                    <errorRateThreshold action="terminate" value="0/1000"/>
                    <maxConsecutiveRestarts action="terminate" value="30"/>
                    <timeout max="100000" default="-1"/>
                </errorHandling>
                <checkpoint batch="10000" time="1000ms"/>
                <configurationParameterSettings>
                    <nameValuePair>
                        <name>tagName</name>
                        <value>
                            <string>H2</string>
                        </value>
                    </nameValuePair>
                </configurationParameterSettings>
            </casProcessor>
           <casProcessor deployment="integrated" name="ContentDescriptor 3">
                <descriptor>
                    <import location="ContentDescriptor.xml"/>
                </descriptor>
                <deploymentParameters/>
                <errorHandling>
                    <errorRateThreshold action="terminate" value="0/1000"/>
                    <maxConsecutiveRestarts action="terminate" value="30"/>
                    <timeout max="100000" default="-1"/>
                </errorHandling>
                <checkpoint batch="10000" time="1000ms"/>
                <configurationParameterSettings>
                    <nameValuePair>
                        <name>tagName</name>
                        <value>
                            <string>H3</string>
                        </value>
                    </nameValuePair>
                </configurationParameterSettings>
            </casProcessor>
            <casProcessor deployment="integrated" name="casConsumerDescriptor">
                <descriptor>
                    <import location="file:///home/eric/developpement/workspace/HtmlContent/desc/casConsumerDescriptor.xmlimport location="file:///home/eric/developpement/workspace/HtmlContent/desc/casConsumerDescriptor.xml"/>
                </descriptor>
                <deploymentParameters/>
                <errorHandling>
                    <errorRateThreshold action="terminate" value="0/1000"/>
                    <maxConsecutiveRestarts action="terminate" value="30"/>
                    <timeout max="100000" default="-1"/>
                </errorHandling>
                <checkpoint batch="10000" time="1000ms"/>
            </casProcessor>
        </casProcessors>
        <cpeConfig>
            <numToProcess>-1</numToProcess>
            <deployAs>immediate</deployAs>
            <checkpoint batch="0" time="30000ms"/>
            <timerImpl/>
        </cpeConfig>
    </cpeDescription>

Mais qu'est ce qu'un CPE au juste ?
Non, il ne s'agit pas de "Conseiller principale d'éducation" ni du "Contrat première embauche". Ca signifie "Collection Processing Engine", il s'agit d'un élément qui assemble un groupe de composants UIMA (AE, CollectionReader...). Un CPE est exécuté par un CPM (ou "Collection Processing Manager") qui va permettre de gérer le déploiement des  composants du CPE, la gestion des erreurs...

Dans un descripteur de CPE nous avons trois balises principales :
  • collectionReader : Cette balise permet de déclarer le CollectionReader qui sera utilisé par le CPE pour générer des CAS. Il permet de déclarer également un ensemble de CASInitializer, mais l'utilisation de ces composants n'est plus conseillée et c'est pourquoi je n'en ai pas parlé jusque là. Lorsqu'il y a un nombre consécutif d'exceptions lancées par le CollectionReader, le CPM s'arrête. Par défaut, le seuil est fixé à 100 exceptions. (Configurable par la variable -DMaxCRErrorThreshold sur votre JVM)
  • casProcessors : Cet élément contient la déclarations des différents AEs et CASConsumers utilisés pour traiter les Artefacts. Il accepte trois attributs dont 2 obligatoires. Le casPoolSize (obligatoire) qui définit combien de CAS sont mis en queue en attente de traitement par les AEs & CASConsumer. Le processingUnitThreadCount (obligatoire) qui définit le nombre de pipelines de traitement gérés en parallèle. (pipeline = Chaîne de traitements composées des différents AE & CasConsumer). Le dernier paramètre, optionnel celui-ci, dropCasOnException est un boolean qui permet de définir ce qui arrive au CAS si un casProcessor lance une exception. Si la valeur  est positionnée à "false" (la valeur par défaut), les CAS ne sont plus autorisés à être traités par les différents composant. Sinon, ils sont nettoyés puis remis dans le pool pour pouvoir être rejoués plus tard. Ensuite pour chaque casProcessor, il est possible de redéfinir les valeurs des variables de configuration des AEs, de définir des options de déploiement lorsque les AEs ne sont pas déployées en mode "integrated" (i.e.: tous les composants sont gérés dans la même instance de JVM). Il est également possible de configurer le comportement sur les exceptions. (nombre de tentatives, comportement à adopter lorsqu'un seuil trop important d'erreurs est dépassé.)
  • cpeConfig : Cette balise permet de configurer le CPE. Il est possible entre autre de définir le mode de démarrage (interactif, immediat...),un nombre de ressources à traiter, les intervalles de checkpoints... (Le checkpoint sauvegarde visiblement des états et des stats dans un fichier, mais je ne sais pas encore quel type d'inforamtion ni dans quel cas ces données sont utilisées)
  • Il esxiste une quatirème balise optionnelle resourceManagerConfiguration qui propose de redéfinir les ressources qui peuvent avoir été déclarées dans les différents AEs utilisés par le CPE.


Pour plus de détails sur les possibilités de configuration, je vous redirige  une nouvelle fois vers la documentation.

Maintenant que nous savons que nos composants fonctionnent ensemble, voyons comment les intégrer dans une application JAVA.


Creation du CPE/CPM



Lorsque l'on souhaite réaliser une application UIMA, le point d'entrée est la classe org.apache.uima.UIMAFramework. Elle permet de lire les fichiers descripteurs et à partir de ces fichiers construire des instances de CollectionReader, d'analysis engine...

Dans notre cas, cela nous donne quelque chose comme ça :

  XMLParser uimaXmlParser = UIMAFramework.getXMLParser();
  CpeDescription cpeDesc = uimaXmlParser.parseCpeDescription(new   XMLInputSource("desc/cpeDescriptor.xml"));
  CollectionProcessingEngine cpe = UIMAFramework.produceCollectionProcessingEngine(cpeDesc);

  System.out.println("Process simple HTML H tags in other thread...");
  cpe.process();
  System.out.println("This message is called just after le process method...");


Avec la sortie console :

     Process simple HTML H tags in other thread...
     This message is called just after le process method...
     CONTENT
     1 T1
     1.1 ST1
     1.1.1 SST1
     1.2 ST2
     1.2.1 SST2
     1.2.2 SST3
     2 T2
     2.1 ST3
     2.2 ST4
     2.2.1 SST4

Conclusion.

Bravo aux courageux qui ont tenu jusqu'au bout !
J'espére vous avoir donné envie d'approfondir le sujet suite à la lecture de ce post largement inspiré de la documentation et du tutorial Apache UIMA.

La rédaction de ce post a été pour moi l'occasion de créer mes premiers composants UIMA, il y a encore beaucoup de chose à découvrir !
Des posts vont donc suivre avec le système de packaging des composants UIMA (les PEAR), et IUMA-AS...

Aucun commentaire:

Enregistrer un commentaire