Teaching - TAL

Page principale de l'UE


Traitement Automatique de la Langue

POS tagging, analyse des phrases

Nous travaillerons en python, comme en MAPSI, en sortant parfois pour aller vers des logiciels dédiés

Corpus d'analyse (issu de la conférence CoNLL 2000 lien):

Téléchargement : lien

Chargement des données

def load(filename):
    listeDoc = list()
    with open(filename, "r") as f:
        doc = list()
        for ligne in f:
            #print "l : ",len(ligne)," ",ligne
            if len(ligne) < 2: # fin de doc
                listeDoc.append(doc)
                doc = list()
                continue
            mots = ligne.split(" ")
            doc.append((mots[0],mots[1]))
    return listeDoc

# =============== chargement ============
filename = "data/wapiti/chtrain.txt" # a modifier
filenameT = "data/wapiti/chtest.txt" # a modifier

alldocs = load(filename)
alldocsT = load(filenameT)

print len(alldocs)," docs read"
print len(alldocsT)," docs (T) read"

Dans certaines fonctions (et pour ceux qui sont encore en python 2), il faut convertir les textes en UTF-8 avec l'astuce suivante à la ligne 12:

doc.append((u""+mots[0],u""+mots[1]))

Approche à base de dictionnaire

Nous proposons de coder une méthode de référence à base de dictionnaire.

Algorithme proposé :

  • Parcourir tous les documents : d
    • Parcourir tous les mots de d et leur clé : m,c
      • Attribuer la cle c au mot m

Note: en cas d'ambiguïté, le dernier mot de la liste impose son choix

Evaluer les performances sur la base de test

Documentation

Pour utiliser un dictionnaire python dans le cas où certaines clés sont inconnues:

dico = dict()
# ... init + ...
# remplacer
dico[cle] # qui plante en cas de clé inconnue
# par
dico.setdefault(cle, valeurParDefaut)

Validation: j'ai trouvé 1433 bonnes réponses sur 1896

Raffinements possibles

  1. afficher les erreurs / afficher les mots du corpus d'apprentissage possédant plusieurs tags
    • Afficher le pourcentage d'erreur en apprentissage (quantification des mots possédant plusieurs tags)
    • Comprendre les erreurs
    • Afficher le pourcentage d'erreur sur les mots inconnus en test
  2. Regarder l'impact des pré-traitements (minuscules...)
  3. Analyser les erreurs : matrice de confusion
  4. Optimiser les performances sur les mots inconnus
    • Etiquettes les plus probables

Méthodes séquentielles

Nous allons retravailler sur les HMM, à partir des corrections de code MAPSI.

# allx: liste de séquences d'observations
# allq: liste de séquences d'états
# N: nb états
# K: nb observation

def learnHMM(allx, allq, N, K, initTo1=True):
    if initTo1:
        eps = 1e-5
        A = np.ones((N,N))*eps
        B = np.ones((N,K))*eps
        Pi = np.ones(N)*eps
    else:
        A = np.zeros((N,N))
        B = np.zeros((N,K))
        Pi = np.zeros(N)
    for x,q in zip(allx,allq):
        Pi[int(q[0])] += 1
        for i in range(len(q)-1):
            A[int(q[i]),int(q[i+1])] += 1
            B[int(q[i]),int(x[i])] += 1
        B[int(q[-1]),int(x[-1])] += 1 # derniere transition
    A = A/np.maximum(A.sum(1).reshape(N,1),1) # normalisation
    B = B/np.maximum(B.sum(1).reshape(N,1),1) # normalisation
    Pi = Pi/Pi.sum()
    return Pi , A, B

def viterbi(x,Pi,A,B):
    T = len(x)
    N = len(Pi)
    logA = np.log(A)
    logB = np.log(B)
    logdelta = np.zeros((N,T))
    psi = np.zeros((N,T), dtype=int)
    S = np.zeros(T)
    logdelta[:,0] = np.log(Pi) + logB[:,x[0]]
    #forward
    for t in range(1,T):
        logdelta[:,t] = (logdelta[:,t-1].reshape(N,1) + logA).max(0) + logB[:,x[t]]
        psi[:,t] = (logdelta[:,t-1].reshape(N,1) + logA).argmax(0)
    # backward
    logp = logdelta[:,-1].max()
    S[T-1] = logdelta[:,-1].argmax()
    for i in range(2,T+1):
        S[T-i] = psi[S[T-i+1],T-i+1]
    return S, logp #, delta, psi
 

Mise en forme des données

Il faut maintenant travailler avec des index de mots (et la même chose pour les clés)

  The cat is in the garden => 1 2 3 4 1 5

Proposition de code pour la mise en forme:

import numpy as np
# alldocs etant issu du chargement des données

buf = [[m for m,c in d ] for d in alldocs]
mots = []
[mots.extend(b) for b in buf]
mots = np.unique(np.array(mots))
nMots = len(mots)+1 # mot inconnu

mots2ind = dict(zip(mots,range(len(mots))))
mots2ind["UUUUUUUU"] = len(mots)

buf2 = [[c for m,c in d ] for d in alldocs]
cles = []
[cles.extend(b) for b in buf2]
cles = np.unique(np.array(cles))
cles2ind = dict(zip(cles,range(len(cles))))

nCles = len(cles)

print(nMots,nCles," in the dictionary")

# mise en forme des données
allx  = [[mots2ind[m] for m,c in d] for d in alldocs]
allxT = [[mots2ind.setdefault(m,len(mots)) for m,c in d] for d in alldocsT]

allq  = [[cles2ind[c] for m,c in d] for d in alldocs]
allqT = [[cles2ind.setdefault(c,len(cles)) for m,c in d] for d in alldocsT]

Application

Appliquer l'algorithme d'apprentissage puis évaluer les performances en test

Validation: j'ai trouvé 1536 bonnes réponses sur 1896

Pour aller plus loin

1. Visualisation des matrices de transition (pour mieux comprendre le modèle de langage)

Quelques fonctions utiles:

    plt.figure()
    plt.imshow(A, interpolation='nearest')
    localLabs = ... # liste des POS-TAG
    plt.yticks(range(len(localLabs)),localLabs) # affichage sur l'image
    if filename != None:
        plt.savefig(filename)
 

2. Visualisation des matrices de confusion (pour comprendre les erreurs de nos modèles)

3. Introspection de Viterbi : construire une vidéo du remplissage de la matrice delta, puis du back-tracking dans Psi. Analyser les déviation de la prédiction en cas d'erreur (par exemple en début de back-tracking).

Une autre forme d'introspection simple: visualiser la matrice psi:

  • si les colonnes sont constantes, les enchainements ont peu d'impact... Dès que les colonnes ne sont pas homogènes, on peut voir l'intérêt de la modélisation séquentielle.

lien pour faire une vidéo (fin du TME)

4. Analyser les variations de performances en fonction

  • du pre-processing des textes (majuscules ou pas, ponctuation ou pas, ajout d'un tag début de phrase...)
  • des paramètres d'optimisation (initialisation des matrices Pi, A, B)
  • du nombre de documents présents en apprentissage (faire varier la taille de la base et tracer la courbe de performances associée)

5. Quelles erreurs sont corrigées par rapport à une approche mot à mot? Trouver des exemples et expliquer.

Comparaison avec le marché (TreeTagger)

Dans le domaine du POS-Tagging, treetagger fait figure de vétéran... Mais il est rapide, simple d'usage et bien pratique!

Comparer vos performances: lien vers TreeTagger

Approches possibles:

  • [la plus simple] utiliser directement l'executable et analyser le fichier de résultats en python
  • [la plus élégante] charger un wrapper python (en bas de la page de treetagger) et faire toutes les expériences en python

Expériences dans nltk

Jusqu'ici, nous avons tout fait à la main. Il est possible d'utiliser des outils avancé pour aller plus vite. L'outil le plus polyvalent actuellement est nltk.

CRF

1. Trouver la bonne entrée:

from nltk.tag.crf           import CRFTagger

Ce package est en fait un wrapper sur CRFSuite... Il faudra sans doute l'installer en salle machine, en prenant en compte la configuration locale:

  • installation en local
  • configuration du proxy

Commande générale pour un paquet:

  pip install paquet --user --proxy=proxy.ufr-info-p6.jussieu.fr:3128 

En particulier:

  pip install python-crfsuite

Il est aussi possible qu'il faille télécharger des ressources nltk: ce package repose sur beaucoup de ressources externes. Après avoir lancer python

  >> import nltk
  >> nltk.download()

2. Créer un objet classifieur, l'entrainer

tagger = CRFTagger()
tagger.train(alldocs, u'crf.model') # donner en plus le fichier de stockage du calcul des features

3. Appliquer sur des données

tagger.tag(['Je suis à la maison'])

4. Un CRF repose essentiellement sur des caractéristiques, l'apprentissage est une pondération de ces caractéristiques.

  • Il faut comprendre qu'est ce qui est calculé:
print tagger._get_features([u"Je"], 0)
  • On peut ensuite chercher à définir nos propres caractéristiques, par exemple par héritage en définissant une classe fille de l'objet précédent. Voir lien.

Classifieur simple, Perceptron

Si tout ce passe dans l'extraction des caractéristiques, a-t-on encore besoin de la modélisation séquentielle??? Testons un classifieurs très simple, le perceptron:

from nltk.tag.perceptron    import PerceptronTagger
tagger = PerceptronTagger(load=False)
tagger.train(alldocs)

Comparons ce qui ce passe si on applique le perceptron sur les phrases... Ou indépendamment au niveau des mots:

# adT_seq: liste de liste de mots (=liste de phrase)
allpred_smart  = [[t for w,t in tagger.tag(adT_seq[i])] for i in range(len(adT_seq))]
allpred_stupid = [[tagger.tag([w])[0][1] for w in adT_seq[i]] for i in range(len(adT_seq))]

Il existe un pos-tagger pré-entrainé dans nltk

Comparer les performances avec l'outil proposé par nltk au niveau de la page lien

Et sur d'autres applications? NER, ...

Il suffit d'avoir des bases pour entrainer les modèles! Le plus dur est donc maintenant de trouver les bonnes ressources (éventuellement dans une autre langue que l'anglais)

Lien vers les bases nltk

Pour attaquer facilement la tâche NER, le dataset CoLNN 2002 est parfait (les datasets correspondant aux années suivantes sont corrects également).

Lien: https://www.clips.uantwerpen.be/conll2002/ner/

Comparaison avec les CRF (Wapiti)

Comparer vos performances par rapport à Wapiti

Comparaison avec la bibliothèque de Stanford NLP

Lien vers le tuto (un peu sommaire)