mercredi 21 novembre 2012

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

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

De quoi parle-t-on ?

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

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

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

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

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

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

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

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

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

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

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

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

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

Comment définir une nouvelle ressource externe ?

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

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

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

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

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

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

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

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

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

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

public MyResourceTypeImpl() {
  super();
}

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

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

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

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


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

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

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

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

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

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