Je considère comme acquis le découpage en phrases et en mots (si ce n'est pas le cas, vous pouvez vous rafraîchir la mémoire ici). Nous allons dans cette partie directement travailler sur une version du corpus des discours de N. Sarkozy déjà découpée en phrases et en mots.

La fonction ci-dessous permet de charger la liste des phrases du corpus et pour chaque phrase la liste des mots qui la compose :

def load_preprocessed_corpus(finput="Discours-Sarkozy-v20111025.preproc.txt"):
	"""
	Load the content of a preprocessed file as a list of sentences which happen
	to be a list of words.
	"""
	fh = codecs.open(finput, "r", "utf-8")
	corpus = fh.read()
	fh.close()
	sents = []
	for sent in corpus.split("\n"):
		sents.append( sent.split() )
	return sents

Exemple d'utilisation :

>>> sentences = load_preprocessed_corpus("Discours-Sarkozy-v20111025.preproc.txt")
>>> "Nombre de phrases  : %d" % len(sentences)
'Nombre de phrases  : 84325'
>>> "Nombre de mots : %d" % len([w for s in sentences for w in s])
'Nombre de mots : 2017309'

Morphologie : des expressions rationnelles à l'algorithme de Porter

Nous allons avoir besoin d'un outil puissant pour exprimer par intention les formes textuelles que nous allons manipuler : les expressions rationnelles. Celles-ci permettent de décrire un langage régulier pour lequel il sera possible de générer un automate acceptant. La plupart des langages informatiques récents offrent des bibliothèques permettant de manipuler ces expressions et les automates en découlant. Python ne fait pas exception à la règle.

Les expressions rationnelles sont gérés sous Python par le module re. Il serait trop long de présenter l'utilisation des expressions rationnelles avec Python ici, et d'autres le font beaucoup mieux que moi.

France, Français, Française, Françaises...

Le nuage de mots généré précédemment contient des mots très similaires, c'est le cas notamment de français et française. Les expressions rationnelles peuvent nous permettre de les considérer ensemble :

>>> import re
>>> len(re.findall(u"\\bfrançais\\b", text))
1031
>>> len(re.findall(u"\\bfrançaise\\b", text))
1286
>>> len(re.findall(u"\\bfran[cç]ais\\w\\b", text))
2661
>>> tous_francais = set()
>>> for m in re.finditer(u"\\bfran[cç]ais\\w*\\b", text):
...     tous_francais.add( m.group(0) )
>>> tous_francais
set([u'fran\xe7aise', u'fran\xe7ais', u'fran\xe7aises'])

Généralisation : vers l'algorithme de Porter

L'exemple précédent nous montre que nous pourrions profiter d'une normalisation morphologique des mots collectés afin de rendre mieux compte de la distribution lexicale. Après tout français, française et françaises correspondent tous trois au même mot français tel qu'on le trouve dans le dictionnaire.

En effet, française et françaises sont des flexions de l'adjectif français. En linguistique, on oppose flexions et dérivations :

  • La flexion consiste à ajouter un affixe au mot afin de refléter un changement au niveau grammatical, du genre, du nombre, de la personne, etc., sans que cette modification morphologique n'altère le sens du lexème d'origine ;
  • La dérivation consiste à créer un nouveau lexème, c-à-d un nouveau mot avec un nouveau sens, par l'ajout d'un affixe.

Les règles flexionnelles en français semblent assez régulières : ajout d'un "e" pour marquer le féminin, d'un "s" pour le pluriel... Ne pourrait-on pas définir un algorithme capable de déconstruire ces flexions afin de retrouver le lexème d'origine, au masculin singulier ?

Petit exercice :

  • Tentez de produire une liste exhaustive des suffixes utilisés pour marquer le genre et le nombre en français
  • Recherchez toutes les formes textuelles qui utilisent ces suffixes

Proposition de corrigé :

all_words = set([w.lower() for w in words])
 
def evaluation_suffix(suffix):
	global raw_text
	regexp = re.compile(r"\b(\w+)%s\b"%suffix, re.I|re.U)
	map = {}
	# Collecter les mots avec le suffixe
	for m in regexp.finditer(raw_text):
		avec_suffixe = m.group(0)
		sans_suffixe = m.group(1)
		if not map.has_key(avec_suffixe):
			map[avec_suffixe] = {
				"sans_suffixe": sans_suffixe,
				"sans_suffixe_existe": False}
	# Chercher les occurrences sans suffixe
	for k in map.keys():
		sans_suffixe = map[k]["sans_suffixe"].lower()
		map[k]["sans_suffixe_existe"] = (sans_suffixe in all_words)
	# Rapport
	nb_est_suffixe = len([k for k in map.keys() if map[k]["sans_suffixe_existe"]])
	nb_est_pas_suffixe = len(map) - nb_est_suffixe
	if nb_est_suffixe > nb_est_pas_suffixe:
		print "%s est un suffixe (%d/%d)" % (suffix, nb_est_suffixe, nb_est_pas_suffixe)
	else:
		print "%s n'est PAS un suffixe (%d/%d)" % (suffix, nb_est_suffixe, nb_est_pas_suffixe)
 
suffixes = ["ous", "aux", "s", "e", "ais", "ives", "ent", "es", "ai", "ons", "ez", "t", "ait", "ions", "iez", "aient", "ant", "le", "les", "ne", "nes", "x"]
 
for suffix in suffixes:
	evaluation_suffix(suffix)

Algorithme de Porter

L'algorithme de Porter offre un cadre algorithmique pour la désuffixation des mots. Le fonctionnement de l'algorithme est décrit en de nombreux endroits sur le Web, notamment ici ou sur mon propre blog. S'il a été mis au point pour l'anglais, il est tout à fait envisageable de l'utiliser pour le français (d'ailleurs certains l'ont fait).

L'idée de l'algorithme de Porter est de déconstruire les flexions pour retrouver la forme canonique des mots : le lemme. Par exemple : retirer le suffixe "aux" de chevaux et le remplacer par "al" pour obtenir le singulier. Les règles de Porter sont déclenchées lorsque deux conditions sont réunies :

  1. le mot se termine par un suffixe particulier
  2. le radical (mot privé dudit suffixe) respecte un motif particulier, le plus généralement un certain nombre de pseudo-syllabes

Le code python ci-dessous est une proposition d'implémentation de la règle de Porter pour normaliser journaux et chevaux en respectivement journal et cheval :

def compute_m(w):
     pseudosyllabs = ""
     for c in w:
             if c in ['a', 'e', 'i', 'o', 'u', 'y']:
                     if len(pseudosyllabs)==0 or pseudosyllabs[-1]=="C":
                             pseudosyllabs += "V"
             else:
                     if len(pseudosyllabs)==0 or pseudosyllabs[-1]=="V":
                             pseudosyllabs += "C"
     return pseudosyllabs.count("VC")
 
# Règle de Porter : (m=1 && *c) aux -> al
# Le mot se termine par aux, a un radical composé d'une pseudo-syllabe et qui se termine par une consomne
def regle_porter_aux(w):
     if w.endswith("aux"):
          radical = w[:-3]
          if not radical[-1] in ['a', 'e', 'i', 'o', 'u', 'y']:
               return radical + "al"
     return w
 
>>> regle_porter_aux("journaux")
'journal'
>>> regle_porter_aux("chevaux")
'cheval'
>>> regle_porter_aux("agneaux")
'agneaux'

Petit exercice :