-
Notifications
You must be signed in to change notification settings - Fork 13
2.2 Règles d'annotation
Pendant très longtemps seuls les corpus annotés manuellement avaient droit de cité: l'utilisation des règles étaient proscrite du fait de leur origine sulfureuse dans le monde ancien des approches symboliques. Or, quiconque a eu le rare bonheur d'annoter des corpus, une tâche qui remplace avantageusement la flagellation comme pénitence, sait à quel point il peut être frustrant d'annoter encore et encore le même motif qu'une simple règle aurait pu identifier quasiment à coup sûr. Pourtant, au début le mélange des genres était acceptable. Les arbres du Penn Tree Bank, par exemple, furent d'abord produit avec un "shallow parser" puis corrigé à la main par des armées d'étudiants. Or, si l'on réfléchit un instant, prétendre qu'une annotation humaine manuelle est supérieure à quelques règles revient à affirmer que la meilleure façon de décrire ℕ est de lister tous les éléments qui lui appartiennent. De la même façon, si l'on doit annoter chacun des "et" dans un texte comme étant une conjonction de coordination, la tâche risque de devenir rapidement répétitive et ennuyeuse. La solution que nous proposons n'a pas pour vocation de relancer le vieux combat depuis longtemps oublié entre les systèmes à base de règle et l'apprentissage machine. La messe est dite et la guerre depuis longtemps perdue. Ce que nous affirmons en revanche, c'est que l'utilisation de règles permet d'automatiser l'annotation de documents, en particulier les éléments récurrents, et par conséquent d'alléger la tâche de l'annotateur pour lui permettre de se concentrer sur les éléments les plus difficiles à analyser.
Tamgu offre un formalisme particulier de règles, proche des expressions régulières, qui se fond discrètement dans vos programmes. Ces règles se combinent avec des lexiques généraux ou utilisateurs pour détecter la présence et la position d'expressions récurrentes.
De plus, ces règles peuvent intégrer des capsules autrement dit des appels à des modules externes tels que des "word embeddings" ou des classificateurs.
Tamgu peut à la fois manipuler des lexiques généraux combinés à des lexiques utilisateurs. Ces lexiques sont en particulier utilisés pour effectuer une segmentation du texte en mots ou en expressions à mots multiples.
Les règles lexicales sont composées d'une étiquette que l'on associe à un mot ou une expression régulière. Une règle lexicale commence systématiquement par une "@".
@bouffe <- viande.
@bouffe <- "marrons glacés".
@bouffe <- "escalope milanaise".
@bouffe <- "poisson(s)".
@bouffe <- repas.
Dans l'exemple ci-dessus, il faut comprendre la règle "poisson(s)" comme une règle qui peut s'appliquer aussi bien à "poisson" qu'à "poissons".
L'ensemble de ces mots et expressions régulières est alors compilé à la volée sous la forme d'un objet transducer. C'est ce même type qui permet aussi de compiler des lexiques généraux de la langue.
Les règles s'écrivent directement dans le code ou dans une chaine de caractères au choix et selon les besoins de l'utilisateur. Une règle est composée d'une étiquette associée à une expression régulière complexe dont chaque élément est séparé par une virgule.
labouffe <- {du,"l{ea}",des}, #bouffe.
Remarquons tout de suite plusieurs petites choses.
- Les accolades introduisent une disjonction entre éléments
- Les étiquettes définies dans le lexique sont précédées d'un "#"
- Les mots sont écrits tel quel ou sous la forme d'une expression régulière
Les règles ont été directement écrites dans le code. Il suffit de déclarer un objet annotator pour y avoir accès:
annotator r;
ustring u="La dame mange des marrons glacés après un repas avec du poisson et de la viande.";
vector v = r.parse(u);
Pour appliquer nos règles, nous allons nous servir de la méthode: parse.
Cette méthode va dans un premier temps se servir du lexique pour découper le texte en mots, en fonction du lexique utilisateur. Ainsi, marrons glacés sera reconnu comme un seul élément, alors que le reste des mots sera découpé le long des espaces et des ponctuations.
ustring u=@"
La dame mange des marrons glacés après un repas avec du poisson et de la viande.
"@;
@bouffe <- viande.
@bouffe <- "marrons glacés".
@bouffe <- "escalope milanaise".
@bouffe <- "poisson(s)".
@bouffe <- repas.
labouffe <- {du,la,des}, #bouffe.
annotator r;
vector v = r.parse(u);
//On affiche le vecteur
println(v);
//Puis chacune des sections détectées.
for (self e in v)
println(u[e[1][0]: e[-1][-1]]);
Ce qui après exécution nous donnera:
[['labouffe',[15,18],[19,33]],['labouffe',[54,56],[57,64]],['labouffe',[71,73],[74,80]]]
des marrons glacés
du poisson
la viande
On peut rajouter un lexique du français à cet exemple, de façon par exemple à rendre la règle plus générale. Dans ce cas, on utilisera aussi "#" pour faire référence à un trait ou à une catégorie détectée par le lexique.
//On remplace notre disjonction par une vérification de la catégorie du mot avant
labouffe <- #Det, #bouffe.
//On rajoute un lexique
transducer lex(_current+"french.tra");
annotator r;
//On le charge dans notre annotateur
r.lexicon(lex);
vector v = r.parse(u);
L'exécution nous donnera un résultat légèrement différent de précédemment:
[['labouffe',[15,18],[19,33]],['labouffe',[40,42],[43,48]],['labouffe',[54,56],[57,64]],['labouffe',[71,73],[74,80]]]
des marrons glacés
un repas //repas est en plus, car "un" est un déterminant...
du poisson
la viande
Evidemment, nous pouvons intégrer autant de règles que nous voulons. Lorsqu'une règle s'applique, le curseur se déplace après le dernier mot consommé par cette règle. On applique alors de nouveaux toutes les règles à partir de cette nouvelle position. En revanche, si une règle échoue, on passe à la règle suivante, toujours à partir de la position courante dans la phrase. Si toutes les règles échouent, on avance le curseur d'un mot et on applique toute la grammaire à nouveau.
Il est aussi possible de compiler la grammaire à partir d'une chaine de caractères, ce qui permet en particulier de disposer de plusieurs annotateurs.
//Notre règle n'est plus déclarée dans le code
string règle = "labouffe <- {du,la,des}, #bouffe.";
transducer lex(_current+"french.tra");
annotator r;
//On compile notre règle
r.lexicon(lex);
r.compile(règle);
string autre = "laboisson <- #Det, #boisson.";
annotator rr;
rr.compile(autre);
//On applique notre grammaire sur "u"
r.parse(u);
//Puis on prend ce résultat toujours dans "r" et on applique "rr" dessus
rr.apply(r);
Dans l'exemple ci-dessus, la grammaire n'est plus définie dans le code mais sous la forme d'une chaine de caractères, ce qui permet de disposer de plusieurs annotateurs. Ceux-ci peuvent même s'enchainer. En effet, le résultat de l'application de "r" sur "u" est toujours conservé dans "r". On peut donc appliquer la second grammaire sur cette structure et faire par exemple référence aux étiquettes produites par la première grammaire. Notons que dans un contexte multi-thread, il peux y avoir plusieurs exécutions en parallèle de "r", chacune ayant accès à son propre environnement.
En fait, la mise en place d'une telle grammaire peut se faire graduellement. Chaque nouveau motif récurrent peut ainsi être traduit sous la forme d'une règle, dès que le besoin se fait sentir.
Nous avons aussi utilisé ce mécanisme de règles dans le bruitage des documents servant à l'entrainement de systèmes de traduction automatique (https://www.aclweb.org/anthology/D19-5617.pdf). Nous avons isolé grâce à ces règles les mots que nous voulions remplacer par une version bruitée dans les documents. Grâce à ces règles, la détection et le remplacement se font en deux lignes de code.