Le Machine Learning (2/2) - exemples pratiques

machine learningpython
Paul Denoyes - 30/11/2018 à 17:14:370 commentaire

Pour faire suite à l’article de Hind, illustrons tout ça avec quelques exemples.


Exemple 1 : Détection de la langue (FR ou pas FR) avec un Naïve Bayes multinomial


L'objet de ce paragraphe est de mettre en place un modèle qui permettra de séparer les textes en langue française, des textes dans d'autres langues.


Nous sommes dans un cas d’apprentissage :

- Supervisé. Des phrases, séquences de mots, associées à des catégories – ici la langue de la séquence catégorisée en FR vs. pas FR.

- Probabiliste.

- Offline. Cependant, on pourrait l'utiliser de façon incrémentale au besoin, avec scikit-learn - méthode partial_fit.

- Paramétrique. Car nous utiliserons une distribution multinomiale, définie par des paramètres, et des probabilités conditionnelles, elles-mêmes des paramètres.

- Non linéaire. Dans le cas (le notre) d’une distribution multinomiale (cependant, les attributs coef_ et intercept_ permettent de passer au logarithme pour le rendre linéaire au besoin : Le naïve Bayes multinomial devient linéaire lorsqu'on l'exprime dans l'espace des logarithmes – ln(A x B) = ln(A) + ln(B)).


  • Théorie


Théorème de Bayes :

Notation : Probabilité P d'avoir A sachant B se note P(A|B).

Théorème : Pour A et B indépendants : P(A|B) = P(B|A) * P(A) / P(B)


Dans notre cas :

A : « piocher » une séquence de mots « cette courte phrase ».

B : se trouver dans la catégorie "Français".

B|A : se trouver dans la catégorie "Français" | sachant que l’on a « pioché » "cette courte phrase"

A|B : « piocher » une séquence de mots « cette courte phrase » lorsque l'on prend un élément de la catégorie "Français"


Comment classer dans la bonne catégorie une séquence que l’on n’avait pas dans notre jeu d’entrainement, sachant que dans ce cas P(A) n’est pas évaluable, P(B|A) non plus ?


  • On va considérer (même si c'est faux) qu'il n'y a pas de dépendance entre les mots, du coup on a le droit de dire que :


Probabilité de tirer la séquence "cette courte phrase" = P("cette courte phrase") = P("cette") x P("courte") x P("Phrase")

et

P("cette courte phrase"|"Français") = P("cette"|"Français") x P("courte"|"Français") x P("phrase"|"Français")


  • Maintenant quid de l’estimation pour une séquence dont un ou plusieurs mots de celle-ci ne seraient pas dans le corpus d’apprentissage ?


Si on ne connaît pas un des mots de la phrase, alors P(mot qu’on ne connait pas) = 0 et ça va mettre à 0 les formules ci-dessus.


Il existe alors plusieurs façons de s'affranchir de ce problème et d'estimer cette probabilité à autre chose que zéro, sans pour autant donner une valeur délirante.

Par exemple on pourrait utiliser la correction de Laplace (cf. additive smoothing) avec des variable discrètes, ou utiliser une distribution pour estimer la probabilité d'une variable continue (Imaginez que vous deviez, à l’aveugle donner une estimation de taille d’une personne que vous n'avez jamais vue. Si la taille suit une distribution Gaussienne, et disons que la moyenne est 1m65, hommes/femmes confondus. Sans autre information sur cette personne, vous pourriez approcher de la vérité en disant qu’elle mesure sans doute autours de 1m65.)


  • Un peu de code


import numpy as np
import nltk
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
import csv
from nltk.stem.porter import PorterStemmer
import pickle

reader = csv.reader(open('LanguageClassificationCorpusShuffled-V01.csv'), delimiter=";")
x = list(reader)
result = np.array(x)
#Chargement des données dans un format exploitable (liste ici, np.array souvent pour les dimensions)
X = result[:,0]
Y = result[:,1]
#Jeu d'entrainement
Xtrain = X[2000:-1]
Ytrain = Y[2000:-1]
#Jeu de tests
Xtest = X[0:1999]
Ytest = Y[0:1999]

####### TOKENIZATION

def tokenize(text):
   tokens = nltk.word_tokenize(text)
   stems = stem_tokens(tokens, stemmer)
   return stems

stemmer = PorterStemmer()
def stem_tokens(tokens, stemmer):
   stemmed = []
   for item in tokens:
       stemmed.append(stemmer.stem(item))
   return stemmed

######## 
   
############################################################
##                 APPRENTISSAGE                          ##
############################################################

from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline

text_clf = Pipeline([('vect', CountVectorizer(tokenizer=tokenize)),
                    ('tfidf', TfidfTransformer(norm='l2', use_idf=True, smooth_idf=True, sublinear_tf=False)),
                    ('clf', MultinomialNB(alpha=1.0)),
])

#Entrainement du modèle
text_clf = text_clf.fit(Xtrain, Ytrain)

############################################################
##               SAUVEAGARDE MODELE                       ##
############################################################

with open('modeleMNB-Langue-V01.pkl', 'wb') as f:
   pickle.dump(text_clf, f)

############################################################
##               MESURE PERFORMANCE                       ##
############################################################

predicted = text_clf.predict(Xtest)
prediction = np.mean(predicted == Ytest)
print("")
print("################# MNB MNB MNB #################")
print("Précision globale sur le jeu de test en mode MNB")
print("")
print (prediction)
print("")
print("Rapport de classification")
print("")
from sklearn import metrics
print(metrics.classification_report(Ytest, predicted))

#Courbe ROC
import matplotlib.pyplot as plt
from sklearn.metrics import roc_curve
probabilities = text_clf.predict_proba(Xtest)
Y_test = [int(i) - 1 for i in Ytest]
fpr, tpr, _ = roc_curve(Y_test, probabilities[:, 1])
plt.plot(fpr, tpr)
plt.plot([0, 1], [0, 1], color='grey', lw=1, linestyle='--')
plt.xlabel('Taux de faux positifs'); plt.ylabel('Taux de vrais positifs')


> SORTIE


#################  MNB MNB MNB  #################
Précision globale sur le jeu de test en mode MNB

0.9309654827413707

Rapport de classification

             precision    recall  f1-score   support

          1       0.96      0.94      0.95      1446
          2       0.85      0.91      0.88       553

avg / total       0.93      0.93      0.93      1999


  • Interprétation du résultat


Les images ci-dessous définissent et illustrent la précision, le rappel et quelques autres métriques courants.




- Précision (combien de candidats sélectionnés sont pertinents) : VP / (VP + FP). Note : Precision en anglais.

- Rappel (ou sensibilité ou sélectivité : combien de candidats pertinents sont sélectionnés) : VP / (VP + FN). Note : Recall ou sensitivity en anglais.

- Taux de faux positifs (TFP) : FP / (FP + VN). Note : FPR (pour false positive rate) en anglais.

- Spécificité (probabilité d'obtenir un négatif lorsqu’il s'agir d'un vrai négatif) : VN / (VN + FP). Note : Specificity (ou selectivity, ambigu avec le français) en anglais.

- Exactitude : (VP + VN) / (VP + VN + FP + FN). Note : Accuracy en anglais.

- Score F1 = 2 x (Précision x Rappel) / (Précision + Rappel). Note : F1-score en anglais.


En général, la précision et le rappel ou/et l'exactitude suffisent à déterminer un bon modèle, cependant il existe des cas pour lesquels ils ne sont pas pertinents.


Notez qu'ici, précision et rappel sont proches de 90% dans chaque classe. Le support quand à lui indique la taille de la population sur laquelle se basent les métriques. Plus cette population est importante, plus la mesure est fiable.


Un exemple pour illustrer les cas de figures qui nécessitent d'autres métriques :


Imaginez que l'on veuille entraîner un modèle à diagnostiquer la narcolepsie, en fonction de mesures prises par un électroencéphalogramme.

Sachant que ce trouble ne touche que 0,05 % de la population, si je pose comme simple modèle :


def diagnose(data):
  return 'not narcoleptic'

print(diagnose([['whatever'], ['I', 'pass', 'here']]))


Qui retourne systématiquement le diagnostic : 'not narcoleptic'.

La précision de ce modèle sur la classe "non narcoleptique" sera de 99,95 % - ([9995 / (9995 + 5)] = 0.9995) - ce qui dans l'absolu semble une très bonne précision. Pourtant il est évident que ce classificateur est très mauvais vu qu'il classe tout à "non narcoleptique" !


Nous sommes face à deux problèmes :

1/ Nos données sont très déséquilibrées, et nous avons peu de cas négatifs (c'est à dire qui ne sont PAS "non narcoleptiques).

2/ On voudrait ici détecter TOUS les narcoleptiques, quitte à avoir quelques faux négatifs, on voudra donc pour des raisons métier, ici des raisons de santé publique, favoriser la recherche de vrais négatifs (i.e. de vrais "non [non narcoleptiques]" = "vrais narcoleptiques").


Prenons maintenant tous les autres chiffres. Pour 10.000 personnes :


. Le score F1 est une moyenne qui permet, quand modèle n'est pas clairement meilleur qu'un autre, de comparer deux modèles rapidement et de façon très "macro". ATTENTION, il n'est pas aussi pertinent que ne le laisse croire sa très fréquente utilisation. Il est très utile mais qu'il faut s'en méfier et faire attention aux enjeux fonctionnels/métier, plutôt que de s'en remettre aveuglément à des "moyennes".

Imaginez deux modèles, M1 et M2. M1 a la meilleure précision, M2 le meilleur rappel, et aucune contrainte fonctionnelle ne vous demande de privilégier la précision ou le rappel. Dans ce cas, pour déterminer le meilleur modèle on peut décider de moyenner ces résultats afin de résumer la performance de chaque modèle un seul chiffre, et de dire que le candidat avec la meilleure moyenne est le meilleur candidat tout court.

Pour le score F1, nous utilisons une moyenne harmonique qui donnera plus de poids aux faibles scores, afin de défavoriser les modèles affichant des scores de précision ou rappels extrêmement faibles. Par exemple 100% de précision et 0% de rappel (très mauvais modèle) donne un score F1 de 0%, qui reflète bien la mauvaise qualité du modèle, alors qu'une moyenne arithmétique donnerait 50%.

Au delà de deux classes, on pourra moyenner les précisions et rappels de chaque classe avant d'établir un score F1, ou directement moyenner les scores F1 de chaque classe (moyenne arithmétique, harmonique - macro-F1, weighted-F1 etc...).

Note : Un très bon article qui approfondi les scores F1 macro, dont il existe plusieurs versions.


. L'exactitude et le score F1 sont des moyennes, qui donnent une sorte de "résumé" de la précision et du rappel. On prend souvent ce résumé pour référence de la qualité d'un modèle.

Cependant pour affiner un peu, disons que l'exactitude sera à utiliser quand les vrais positifs et les vrais négatifs sont les plus "importants" niveau métier, c'est à dire qu'ils comptent le plus. Le score F1 quant à lui sera plutôt à utiliser si les faux positifs et les faux négatifs sont les plus cruciaux du point de vue métier.


. Quand les données sont quantitativement déséquilibrées, par exemple beaucoup de cas réellement négatifs et très peu de cas réellement positifs, le weighted-F1 ou F1 pondéré est une variante adaptée du score F1. Il sera en revanche préférable de réserver l'exactitude à des jeux de données plus équilibrées.


. Enfin, dans des cas extrêmes comme le notre ou 1/ Nos données sont vraiment très déséquilibrées, vue la très très faible proportion de narcoleptiques ET 2/ Il est très important de détecter les vrais négatifs qui sont les narcoleptiques, le TFP et la spécificité seront de bonnes mesures pour détecter les mauvais modèles.


Courbe ROC : (ROC pour Receiver operating characteristic)


Il peut aussi être intéressant pour un classificateur binaire, de parcourir les taux de de vrais/faux positifs, le long du seuil de discrimination des classes.

. Le plus souvent pour mesurer la qualité du classificateur : plus l'aire sous la courbe est grande, plus le classificateur est bon.

. Mais aussi par exemple pour décider d'où placer le seuil de discrimination des classes, selon que l'on veut privilégier davantage la précision ou le rappel, en fonction de raisons métier.



Exemple 2 : "Distance" entre les mots (word embeddings - vectorization des mots)


Ce paragraphe décrira un exemple capable de déterminer la proximité sémantique des mots du corpus, afin par exemple de déterminer de quel sujet traite tel ou tel document.


Nous sommes dans un cas d’apprentissage :

- Non supervisé. On fournit simplement un texte et point.

- Géométrique. On va calculer des vecteurs, des distances entre des points de l’espace etc…

- Incrémental.

- Non paramétrique. L’algorithme place des points les uns par rapport aux autres.

- Non linéaire.


  • Le principe


Donner une représentation microscopique de chaque mot du corpus au travers d’une suite de chiffres, qui puisse macroscopiquement dessiner une représentation cohérente de l’ensemble du corpus.


Ces valeurs seront calculées sur la base du contexte de chaque mot (i.e. les mots qui l’entourent). “You get to know a word by the company it keeps.” — Firth, J. R. (1957)


Généralement :

. Soit par un modèle minimisant une fonction coût qui évalue la proximité de deux mots (chacun étant une suite de chiffres) à la probabilité qu’ils ont d'apparaître dans le même contexte (GloVe)

. Soit par un modèle prédictif (réseau de neurones qui va apprendre à prédire un mot cible en sortie en fonction des mots de contexte passés en entrée – Word2Vec)

C’est ce dernier modèle que nous allons illustrer.


  • Illustration


Corpus de texte : Le vif zéphyr jubile sur les kumquats du clown gracieux


Encodons le texte en considérant que chaque mot du corpus (cible) a pour contexte un seul mot à droite et un seul mot à gauche.



Continuous bag of words (CBOW) : On passe ensuite le contexte (i.e. 2 mots dans notre cas) dans un réseau de neurones avec une couche cachée de dimension N (le nombre de dimensions que l’on souhaite pour définir chaque mot).

On optimise afin que l’entrée (contexte) corresponde à la sortie (mot cible).



Skip-gram : idem mais dans l’autre sens, de la cible, prédire le contexte




  • Un peu de code


import unicodedata, re
import gensim

############################################################
##                APPRENTISSAGE                           ##
############################################################
try:
    model = gensim.models.Word2Vec.load("Word2Vec-UnsupervizedSimilarities.model")
except IOError:
    def remove_accents(text):
        nkfd_form = unicodedata.normalize('NFKD', text)
        return u"".join([c for c in nkfd_form if not unicodedata.combining(c)])

    def tokenize(text):
        return re.sub("\s+", " ", re.sub("[^A-Za-z]", " ", remove_accents(text))).split()

    stoplist = set('www com mais dans a le la les \n un une des me ma mes .  l l  l d  d d  h  h h  j cela de ou notre et pour dans qui tres très sont avons nous que de je est au du en à vous ce se sur votre avec tout bien il elle sur il ne par pas on'.split())
    FRENCH_STOP_WORDS = frozenset(['com', 'www', 'ou', '\n', 'notre', 'se', 'cela', 'la', 'me', 'à', 'vous', 'votre', 'j', 'j ', ' j', 'pas', 'ma', 'elle', 'en', 'par', 'est', 'avec', 'et', 'il', 'que', 'l', 'l ', ' l', '.', 'avons', 'un', 'tout', 'mais', 'sur', 'les', 'une', 'on', 'du', 'bien', 'nous', 'h', ' h', 'h ', 'ce', 'des', 'je', 'au', 'mes', 'sont', 'de', 'très', 'a', 'le', 'dans', 'd', ' d', 'd ', 'qui', 'tres', 'pour', 'ne'])

    def removeStopWords(text):
        return str([word for word in text.lower().split() if word not in stoplist])
    #On charge notre corpus 0 nettoyage + tokenization
    NomFichierApprentissage = "Datas-90K"
    ad_data = [removeStopWords(line.strip()) for line in open(NomFichierApprentissage + ".csv", encoding="latin-1")]
    text_data = [tokenize(ad_datum.lower()) for ad_datum in ad_data]
    model = gensim.models.Word2Vec(text_data, size=2000, window = 10, min_count = 5, workers=12)
model.save("Word2Vec-UnsupervizedSimilarities.model")

############################################################
##                  RESULTATS                             ##
############################################################

print("")
print("")
print("")
print("                  ON RESTE SUR UN VOCABLE SPECIALISEE HOTELERIE, POUR RESTER RACCORD AVEC LE CORPUS")

#TOUVER L'INTRUS
print("")
ListeIntrus = ["sejour", "vacances", "villegiature", "conge", "parking"]
print("")
print("")
print("## Trouver l'intrus dans la liste suivante %s" %ListeIntrus)
print("##")
print("##                >> " + model.wv.doesnt_match(ListeIntrus))
print("")

#SIMILARITE ENTRE DEUX MOTS (Entre 0 et 1)
MotsSimilaires = (("bon", "excellent"))
print("")
print("## Quelle similarité entre "'"%s"'", et "'"%s"'"" %(MotsSimilaires[0], MotsSimilaires[1]))
print("##")
print("##                >> " + str(model.wv.similarity(MotsSimilaires[0], MotsSimilaires[1])))
print("##")
print("")
MotsSimilaires = (("bon", "mauvais"))
print("")
print("## Quelle similarité entre "'"%s"'", et "'"%s"'"" %(MotsSimilaires[0], MotsSimilaires[1]))
print("##")
print("##                >> " + str(model.wv.similarity(MotsSimilaires[0], MotsSimilaires[1])))
print("##")
print("")

#DEDUCTIONS
print("")
positive=['chambre', 'terrasse']
negative = ['lit']
print("## LOGIQUE - ANALOGIES")
print("##")
print("## %s est à %s ce que %s est à " %(positive[0], negative[0], positive[1]) + "'" + str(model.wv.most_similar(positive=positive, negative = negative, topn=1)[0][0]) + "' " + "avec un indice de confiance de " + str(model.wv.most_similar(positive=positive, negative = negative, topn=1)[0][1]))
print("##")
positive=['chambre', 'sdb']
negative = ['lit']
print("## %s est à %s ce que %s est à " %(positive[0], negative[0], positive[1]) + "'" + str(model.wv.most_similar(positive=positive, negative = negative, topn=1)[0][0]) + "' " + "avec un indice de confiance de " + str(model.wv.most_similar(positive=positive, negative = negative, topn=1)[0][1]))
print("")

#TROUVER DES SYNONYMES
NombreFeatures = 5
MotSimilaire = ['repas']
similaires = model.wv.most_similar(positive=MotSimilaire, topn=NombreFeatures)
dissemblables = model.wv.most_similar(negative=MotSimilaire, topn=NombreFeatures)
#Graphique avec Matplotlib
import matplotlib.pyplot as plt
import numpy as np
features = []
coefs = []
for i in range (0, NombreFeatures):
    features = np.append(features, similaires[i][0])
    coefs = np.append(coefs, similaires[i][1])
for i in range (0, NombreFeatures):
    features = np.append(features, dissemblables[i][0])
    coefs = np.append(coefs, dissemblables[i][1])
featuresTuple = tuple(features)
coefsList = list(coefs)
y_similarite = np.arange(len(featuresTuple))
plt.figure(figsize=(10, 5))
colors = ['black' if c < 0.6 else 'lightblue' for c in coefsList]
plt.bar(y_similarite, coefs, align='center', alpha=0.9, color = colors)
plt.xticks(y_similarite, features, rotation=60, fontsize= 14)
plt.ylabel('Degré de similarité', fontsize= 20)
plt.title('Plus et moins similaires avec %s' %MotSimilaire, fontsize= 24)
plt.show()


  • Résultats


         ON RESTE SUR UN VOCABLE SPÉCIALISÉ HÔTELLERIE, POUR RESTER RACCORD AVEC LE CORPUS


## Trouver l'intrus dans la liste suivante ['sejour', 'vacances', 'villegiature', 'conge', 'parking']
##
##               >> parking


## Quelle similarité entre "bon", et "excellent"
##
##               >> 0.8612218442783093
##


## Quelle similarité entre "bon", et "mauvais"
##
##               >> 0.4286132022531659
##


## LOGIQUE - ANALOGIES
##
## chambre est à lit ce que terrasse est à 'exterieure' avec un indice de confiance de 0.566259503364563
##
## chambre est à lit ce que sdb est à 'sanitaires' avec un indice de confiance de 0.606020450592041

Graphique illustrant ce que le modèle est capable de trouver, par exemple les 5 mots les plus similaires pour lui au mot repas (en bleu), et les plus éloignés (en noir).

Commentaires :

Aucun commentaires pour le moment


Laissez un commentaire :

Réalisé par
Expaceo