Exécution de Snowball

L'utilisation de la librairie java libstemmer de Snowball est assez simple. Il faut instancier un SnowballStemmer correspondant au langage à traiter, le frenchStemmer par exemple. Ensuite, on passe chaque mot à l'instance par le biais de sa méthode setCurrent(String mot), on applique une ou plusieurs itération de racinisation avec la méthode stem() et on récupère finalement le résultat à l'aide de la méthode getCurrent() :

import org.tartarus.snowball.ext.frenchStemmer;
...
frenchStemmer myStemmer = new frenchStemmer();
myStemmer.setCurrent("répliqua");
myStemmer.stem("répliqua");
String myStem = myStemmer.getCurrent();

Effectuer plusieurs itérations de racinisation peut permettre d'obtenir une racine plus générique dans le cas de mots fortement déclinés, anticonstitutionnellement par exemple. Cependant être trop agressif sur l'obtention de la racine peut également amener à une racine éloignée du concept auquel est rattaché le mot original !

Descripteur

Je commence par le descripteur XML du composant, rien de particulier, il suffit de spécifier :

  • Implementation Language : Java
  • Engine Type : Primitive
  • Name : aeUIMASnowballStemmer

Il faut préciser que le moteur d'analyse (analysis engine) met à jour le CAS et qu'il est possible de déployer le composant pour des traitements parallèles. Finalement, la classe du composant dérivant du JCasAnnotator_ImplBase s'appelle net.atlanstic.lina.snowball.UIMASnowballStemmer.

Paramètres

Deux paramètres vont permettre de rendre l'utilisation du composant plus appréciable :

  • un paramètre permettant de préciser le type de l'annotation mot dans le CAS ;
  • le langage du document afin de sélectionner le racinisateur le plus approprié.

Je choisis d'appeler le premier paramètre WordAnnotationType et lui donner le type String. Le second s'appelle Language et est également de type String.

Je renseigne les valeurs par défaut org.apache.uima.TokenAnnotation et french pour respectivement les paramètres WordAnnotationType et Language, ce qui correspond à l'utilisation du composant à la suite du WhitespaceTokenizer sur des documents en français.

Type system

Le racinisateur ne retourne pas énormément d'information, juste la racine qu'il a calculé du mot. Il serait possible d'enregistrer cette information dans une annotation Mot ou Token. Cependant l'utilisation de types d'annotation sur lequel le composant n'a pas de contrôle pose des problèmes de maintenance et d'interopérabilité, ce que cherche à résoudre UIMA. Au sein de l'équipe TALN du LINA, nous avons défini plusieurs bonnes pratiques pour la création des type systems. L'un d'eux spécifie que les composants indépendants, comme notre futur racinisateur, doivent au maximum être indépendant d'un point de vue de leur type system. Le premier choix qui va dans le sens de cette règle de conduite est la possibilité de préciser quel type rechercher pour l'annotation Mot. Le deuxième choix sera celui de créer une annotation propre au type.

Nous allons donc créer une annotation net.atlanstic.lina.snowball.Stem qui dérive du super type uima.tcas.Annotation, soit une annotation sur du matériel textuel. Ceci permettra notamment aux annotation d'avoir accès à la méthode getCoveredText(). Cette annotation aura un unique attribut stem de type uima.cas.String qui conservera la racine calculée.

Initialisation du composant

Maintenant que tout est en place pour l'implémentation, passons-y !

Notre classe java va étendre la classe JCasAnnotator_ImplBase, ce qui est assez classique pour un composant de type moteur d'analyse (analysis engine). Nous allons surcharger deux méthodes de cette classe : initialize(UimaContext context) et process(JCas cas)''. La première permet de mettre en place le composant, le second exécute le code métier sur un CAS.

Nous allons avant définir plusieurs constantes et variables de classe : les constantes définissant les noms des paramètres dans le descripteur, une variable par valeur de paramètre et une variable pour le SnowballStemmer :

	/** Token annotation to consider for stemming */
	private final static String PARAM_TYPE = "WordAnnotationType";
	private String theWordTypeStr;
	/** Language to choose for the current text */
	private final static String PARAM_LANG = "Language";
	private String theLanguage;
	/** The stemmer instance for the specified language */
	private SnowballStemmer theStemmer;

Nous allons ensuite utiliser la méthode initialize pour récupérer la valeur des paramètres et instancier le SnowballStemmer le plus approprié au langage spécifié. Pour ce faire nous allons utiliser une petite astuce d'introspection java qui permet d'instancier une classe dont on connaît le nom mais à laquelle on n'a pas accès directement. Il faut cependant faire attention lorsque l'on manipule ce genre d'astuces à bien prendre en compte les cas où ladite classe ne serait pas dans le classpath ou que l'on n'ait pas l'autorisation de l'instancier :

	@Override
	public void initialize(UimaContext context)
			throws ResourceInitializationException {
		super.initialize(context);
		// Obtain the name of the word annotation type
		theWordTypeStr = (String) context.getConfigParameterValue(PARAM_TYPE);
		// Obtain the language and prepare the stemmer
		theLanguage      = (String) context.getConfigParameterValue(PARAM_LANG);
		String className =  "org.tartarus.snowball.ext." + 
			theLanguage.toLowerCase() + "Stemmer";
		try {
			Class stemClass = Class.forName(className);
			theStemmer = (SnowballStemmer) stemClass.newInstance();
		} catch (ClassNotFoundException e) {
			throw new ResourceInitializationException(e);
		} catch (InstantiationException e) {
			// Cannot instanciate the stemmer
			throw new ResourceInitializationException(e);
		} catch (IllegalAccessException e) {
			// Cannot access the stemmer class
			throw new ResourceInitializationException(e);
		}
		// now we are ready to stem...
	}

Finalement l'algorithme métier pour raciniser les mots est assez simple : pour chaque token mot du texte on récupère le texte couvert, on en calcule la racine et on place une annotation Stem au-dessus de l'annotation token mot avec la racine pour attribut. La seule difficulté réside dans l'obtention de la liste de ces tokens mots dont on ne connaît pas à priori le type. Nous allons donc récupérer, par introspection encore une fois, ce dernier dans le type system du CAS comme le décrit un billet précédent.

	@Override
	public void process(JCas cas) throws AnalysisEngineProcessException {
		// Get the annotation type from the CAS type system, and check it exists
		Type mWordType = cas.getTypeSystem().getType(theWordTypeStr);
		if (  (mWordType == null) ) {
			String errmsg = 
				"The word type " + theWordTypeStr + " does not exist in the CAS" +
						"type system !";
			throw new AnalysisEngineProcessException(errmsg, new Object[]{mWordType});
		}
		// Now we browse for all those word annotations...
		AnnotationIndex idxWord = (AnnotationIndex) cas.getAnnotationIndex(mWordType);
		FSIterator itWord       = idxWord.iterator();
		while (itWord.hasNext()) {
			// Stem the current word
			Annotation mWord = (Annotation) itWord.next();
			theStemmer.setCurrent( mWord.getCoveredText() );
			theStemmer.stem(); // FIXME: the stem operation can be applied several times
			// and annotate the stem into the CAS
			Stem mStemAnnot = new Stem(cas);
			mStemAnnot.setBegin( mWord.getBegin() );
			mStemAnnot.setEnd( mWord.getEnd() );
			mStemAnnot.setStem( theStemmer.getCurrent() );
			mStemAnnot.addToIndexes();
		}
	}

Et voilà...

Code complet et téléchargement

Le code source complet ainsi que sa version compilée et les descripteurs sont disponibles dans le jar mis à disposition ici. Bien entendu pour que le composant fonctionne, il faut que les classes de Snowball soient accessibles à partir de votre classpath !