Un point sur la tokenisation

La tokenisation est l'opération de segmenter un acte langagier en unités "atomiques" : les tokens. Les tokenisations les plus courantes sont le découpage en mots ou bien en phrases. Ainsi la tokenisation en mots de la phrase Tout le monde attendait une véritable furia rouge. nous donnerait les composants mots : Tout, le, monde, attendait, une, véritable, furia, rouge, ..

En traitement automatique des langues (TAL), la tokenisation est surtout utilisée lors des prétraitements afin d'identifier des unités de "haut niveau" sur lesquelles porteront l'analyse. De plus, le terme "tokenisation" est couramment associé à l'idée de "découpage en mots", soit une analyse morphologique des actes langagiers.

Nous nous intéressons par la suite aux outils disponibles au sein de NLTK pour réaliser une segmentation en mots.

Tokenisation avec NLTK

Nous allons utiliser le langage Python, et notamment le package NLTK, pour tokeniser en mots. Avant toute chose, il est nécessaire d'ouvrir une console python et d'importer nltk à l'aide de la commande import nltk :

Python 2.5.1 (r251:54863, Oct  5 2007, 13:36:32) 
[GCC 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import nltk

Si vous obtenez un message d'erreur comme ci-dessous, c'est que le paquet nltk n'est pas installé ou bien que Python n'arrive pas à le trouver. Cette page devrait vous permettre de résoudre le problème.

>>> import nltk
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named nltk

Nous allons travailler sur deux textes, le premier en anglais et le second en français. Les textes en anglais posent assez peu de problèmes alors que ceux en français nous confrontent à certaines subtilités. Ces deux textes sont issus de wikipédia et peuvent donc être utilisés, diffusés ou modifiés librement en accord avec la licence GFDL.

Stockons le texte en français dans la variable txt_fr :

>>> txt_fr = u"""
Rapidement, ils ont été fondus avec un alliage de plomb (80 %), d’antimoine (5 %) 
et d'étain (15 %) dans des matrices. L’ouvrier typographe se servait d’un composteur 
sur lequel il alignait les caractères, lus à l’envers, de gauche à droite, piochés dans 
une boîte appelée « casse ». Les caractères du haut de la casse étaient appelés les 
capitales (majuscules) et ceux du bas — les minuscules — les bas-de-casse."""

... et celui en anglais dans la variable txt_en :

>>> txt_en = u"""
Monty Python, or The Pythons, is the collective name of the creators of Monty Python's 
Flying Circus, a British television comedy sketch show that first aired on the BBC on 5 
October 1969. A total of 45 episodes were made over four series. The Python phenomenon 
developed from the original television series into something much larger in scope and impact."""

Découpage naïf avec split

Dans le formalisme Python, les deux chaînes unicodes précédentes sont en réalité des instances de la classe unicode. Cette classe possède une méthode split() qui permet d'éclater la chaîne en une liste de composants à partir d'un caractère séparateur passé en paramètre. Si jamais aucun séparateur n'est spécifié, les caractères correspondant à des espaces sont utilisés comme séparateurs.

>>> txt_fr.split()
[u'Rapidement,', u'ils', u'ont', ...]

>>> txt_en.split()
[u'Monty', u'Python,', u'or', u'The', ...]

Cette approche naïve de découpage à l'aide des séparateurs espaces fonctionne globalement plutôt bien, excepté lorsque des éléments ponctuatifs sont accolés aux mots. Pour la suite, il est nécessaire de définir des mesures d'évaluation de la qualité de la tokenisation en mots. Nous utilisons des mesures régulièrement en TALN :

  • la précision : proportion, parmi les mots identifiés, de mots corrects ;
  • le rappel : proportion, parmi les mots attendus, de mots identifiés.

Étant donné que nous manipulons des listes, nous pouvons adopter une approche ensembliste dans la définition des mesures de précision et de rappel. Ainsi, avec W le résultat attendu de la segmentation en mots et T le résultat calculé, on pose :

  • Précision(T, W) = |T ⋂ W| / |T|
  • Rappel(T, W) = |T ⋂ W| / |W|

Ce qui nous donne, en tenant compte du fait que nous manipulons des listes et non des ensembles, l'implémentation ci-dessous. Notez toutefois que le calcul des mesures est approximative, mais suffisamment précise pour nos besoins. Une version exacte devrait être utilisé pour une évaluation scientifiquement correcte et précise.

def metrics(lstcomp, lstref) :
    card_intersec = 0.0 # force à utiliser la division non entière
    for t in set(lstcomp) :
        card_intersec += min(lstref.count(t), lstcomp.count(t))
    precision = card_intersec/len(lstcomp)
    rappel = card_intersec/len(lstref)
    return (precision, rappel)

Le calcul de ces mesures nécessite d'avoir une tokenisation de référence. Afin de vous éviter le fastidieux travail de tokenisation à la main, voici ci-dessous les deux tokenisations de référence que j'ai utilisé par la suite :

  • Pour le texte en français :
>>> ref_toks_fr = [u'Rapidement', u',', u'ils', u'ont', u'\xe9t\xe9', u'fondus', u'avec', u'un', u'alliage', u'de', 
u'plomb', u'(', u'80 %', u')', u',', u'd\u2019', u'antimoine', u'(', u'5 %', u')', u'et', u'd\'', u'\xe9tain', u'(', u'15 %', u')',
u'dans', u'des', u'matrices', u'.', u'L\u2019', u'ouvrier', u'typographe', u'se', u'servait', u'd\u2019', u'un',
u'composteur', u'sur', u'lequel', u'il', u'alignait', u'les', u'caract\xe8res', u',', u'lus', u'\xe0', u'l\u2019', 
u'envers', u',', u'de', u'gauche', u'\xe0', u'droite', u',', u'pioch\xe9s', u'dans', u'une', u'bo\xeete', u'appel\xe9e',
u'\xab', u'casse', u'\xbb', u'.', u'Les', u'caract\xe8res', u'du', u'haut', u'de', u'la', u'casse', u'\xe9taient', 
u'appel\xe9s', u'les', u'capitales', u'(', u'majuscules', u')', u'et', u'ceux', u'du', u'bas', u'\u2014', u'les', 
u'minuscules', u'\u2014', u'les', u'bas', u'-', u'de', u'-', u'casse', u'.']
  • ... et pour le texte en anglais :
>>> ref_toks_en = [u'Monty', u'Python', u',', u'or', u'The', u'Pythons', u',', u'is', u'the', u'collective', u'name',
u'of', u'the', u'creators', u'of', u'Monty', u'Python', u'\'s', u'Flying', u'Circus', u',', u'a', u'British', u'television',
u'comedy', u'sketch', u'show', u'that', u'first', u'aired', u'on', u'the', u'BBC', u'on', u'5', u'October', u'1969',
u'.', u'A', u'total', u'of', u'45', u'episodes', u'were', u'made', u'over', u'four', u'series', u'.', u'The', u'Python', 
u'phenomenon', u'developed', u'from', u'the', u'original', u'television', u'series', u'into', u'something', 
u'much', u'larger', u'in', u'scope', u'and', u'impact', u'.']

L'application de nos mesures montre clairement que l'utilisation de split est plus efficace sur l'anglais que sur le français, la différence entre les deux tokenisations étant importantes autant en terme de précision (74% pour le français, contre 88% pour l'anglais), que de rappel (57% pour le français, contre 79% pour l'anglais). Notons toutefois que le texte en français est un peu plus compliqué que celui en anglais. L'utilisation d'un corpus parallèle nous permettrait une comparaison plus exacte des résultats.

>>> metrics(txt_fr.split(), ref_toks_fr)
(0.74647887323943662, 0.56989247311827962)

>>> metrics(txt_en.split(), ref_toks_en)
(0.8833333333333333, 0.79104477611940294)

Les résultats de l'algorithme naïf, bien qu'encourageant, peuvent être améliorés, notamment en employant des automates pour repérer des structures particulières telles que les données numériques, les sigles, ...

Utilisation d'expressions régulières

Si les langues naturelles ne sont pas des langages réguliers, les mots les composant peuvent l'être. NLTK offre une classe RegexpTokenizer qui permet d'effectuer une tokenisation à partir d'une description des tokens par le biais d'une expression régulière. Dans la théorie des automates, les expressions régulières sont capables de reconnaître les langages réguliers, tout comme les automates déterministe à états finis.

À partir de l'expérimentation précédente, nous allons utiliser un expression régulière qui définit plusieurs types de mots :

  • les mots composés uniquement de lettres ;
  • les contractions des mots outils (l', d', ...) ;
  • les pourcentages ;
  • les nombres ;
  • les éléments ponctuatifs et signes diacritiques (tout ce qui n'est ni espace, ni alphanumérique).

Cette catégorisation dépend bien entendu de la langue, et n'est pas exhaustive (prise en compte des monnaies, sigles, ...).

Nous utilisons la possibilité dans Python de structurer les expressions régulières de manière plus compréhensible à l'aide de l'instruction (?x). Pour en savoir plus sur les expressions régulières dans Python, se référer à la documentation de référence.

>>> tok2 = nltk.RegexpTokenizer(r'''(?x)
          \d+(\.\d+)?\s*%   # les pourcentages
        | \w'               # les contractions d', l', ...
        | \w+               # les mots pleins
        | [^\w\s]           # les ponctuations
        ''')

La simple utilisation d'une typologie des mots et l'écriture de règles en conséquence nous permet d'augmenter significativement les performances, notamment en terme de rappel, pour chacune des langues. Ainsi la précision est de 92% pour le français (97% pour l'anglais) et la précision de 96% (99% pour l'anglais).

>>> metrics(tok2.tokenize(txt_fr), ref_toks_fr)
(0.91752577319587625, 0.956989247311828)

>>> metrics(tok2.tokenize(txt_en), ref_toks_en)
(0.97058823529411764, 0.9850746268656716)

Intéressons nous aux cas problématiques, i.e. les mots qui ne sont pas correctement identifiés. Pour l'anglais il s'agit des marques d'appartenance en 's. En effet, ces dernières sont spécifiques à l'anglais et n'ont pas été prises en compte dans la mise au point de notre expression régulière plus particulièrement adaptée au français. Pour le français, le problème provient des apostrophes unicodes.

>>> set(ref_toks_en).difference(tok2.tokenize(txt_en))
set([u"'s"])

>>> set(ref_toks_fr).difference(tok2.tokenize(txt_fr))
set([u'd\u2019', u'L\u2019', u'l\u2019'])

Le tokeniseur ci-dessous prend en compte les résultats de notre expérimentation précédente. L'introduction de caractères unicodes complique largement l'écriture de l'expression régulière.

>>> reg_words = r'''(?x)
          \d+(\.\d+)?\s*%   # les pourcentages
        | 's                # l'appartenance anglaise 's
        | \w'               # les contractions d', l', j', t', s'
        '''
>>> reg_words += u"| \w\u2019"      # version unicode
>>> reg_words += u"|\w+|[^\w\s]" # avec les antislashs
>>> tok3 = nltk.RegexpTokenizer(reg_words)

>>> metrics(tok3.tokenize(txt_en), ref_toks_en)
(1.0, 1.0)

>>> metrics(tok3.tokenize(txt_fr), ref_toks_fr)
(1.0, 1.0)

Nous obtenons ainsi une tokénisation parfaite. Bien entendu, cette performance est à relativiser par rapport à la taille de notre échantillon. Si vous souhaitez obtenir une évaluation plus intéressante - et que vous avez une quelques cycles calculs à dépenser - vous pouvez tester le nouveau tokenizer sur le Brown Corpus qui est intégré à NLTK :

>>> metrics(tok3.tokenize(nltk.corpus.brown.raw()),
            nltk.corpus.brown.words())