Thématiques principales

mardi 17 juillet 2018

IA : Neuromimétique : classification sigmoide

Nous avons traité dans un article précédent de la classification de fruits (ananas et pastèques) avec un neurone en employant une fonction d'activation linéaire [1]

Nous avions évoqué la possible utilisation dans ce genre de problématique de fonctions d'activation de type sigmoide. Je vous propose donc de reprendre l'article précédent en appliquant ce type de fonctions a notre problème et comprendre leur fonctionnement, leur interprétation possible.

Considérons donc une autre manière d’implémenter nos neurones en vous proposant le rework suivant:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import math

def neuroneCore(entre,W,biais):
    return np.dot(entre,W.T)-biais
    
def limiter(a):
    if a > 0:
        return 1
    return 0

def sigmoid(a):
    return 1 / (1 + math.exp(-a))
    
def neuroneLim(entre,W,biais):
    a=neuroneCore(entre,W,biais)
    return limiter(a)

def neuroneSig(entre,W,biais):
    a=neuroneCore(entre,W,biais)
    return sigmoid(a)

Avec ce rework, on sépare d'un coté la fonction calculant la pondération des entrées par les dendrites et ensuite on applique dessus soit la fonction limiter soit sigmoïde. On a du coup deux types de neurones. Ce sera ici le seconde type qui nous intéressera.

Appliquons lui les poids initiaux que nous avions appliqués avec en entrée nos deux profils de fruits.

1
2
3
4
5
6
7
8
W=np.array([[1, 1, 1, 0]])
biais=1.5

pasteque=np.array([[0.2, 0.3, 0.2, 0.95]])
anana=np.array([[0.8, 0.65, 0.6, 0.8]])

print(neuroneSig(pasteque,W,biais))
print(neuroneSig(anana,W,biais))

1
2
0.31002551887238755
0.6341355910108007

Bon ceci signifie donc que la pastèque est a 31% un ananas et un ananas est un ananas a 63%. Oui n'oublions pas que avec le limiter, l'ananas valait 1 et la pastèque 0. Donc c'est assez logique donc que d'un coté le taux de similarité d'un pastèque avec un ananas soit prêt de seulement 30%... par contre il faut avouer que pour un ananas, on se serait attendu a être un peu plus ressemblant a un ananas!

N'oublions pas que ces taux sont issus d'un modèle produit a l'intuition. Essayons avec les poids issus de l'apprentissage en mode linaire pour rappel (W=[ 3.40784321 2.44027133 2.17143355 -1.26320997] )

1
2
3
W=np.array([[3.40784321, 2.44027133, 2.17143355, -1.26320997]])
0.12462296460187255
0.7340302487024424

Et bien, c'est évident que c'est mieux, 12% et 73% ! mais ce ne sont que des résultats obtenu sur les profils. Il faut regarder ce que nous donne ce modèle sur l'ensemble des données de test (oui car techniquement avec ce W on a déjà utilisé les 3/4 des données pour l'entrainement).

Mais comment évaluer l'efficacité du modèle? Comment évaluer la pertinence de ces différents taux ?

Ce que l'on peut imaginer pour valider ce nouveau type de neurone, faire c'est de construire un algorithme qui selon la valeur de la sortie du neurone,  classe en ananas ou en pastèque le fruit testé avec pour restriction qu'entre par exemple 40 et 60%, il faut considérer que le fruit est indiscernable et donc mis dans une autre catégorie erreur.

Mais attendez, on avait vu avec la matrice de confusion, qu'il était aussi possible que parfois lorsque l'on détecte un ananas, en fait c'est une pastèque et inversement! Il va nous falloir donc aussi des données étiquetées que nous confronterons avec la valeur prédite par le neurone quand celle ci est soit inférieur a 40% soit supérieur a 60%

Construisons d'abord nos données étiquetés, données d'entrainement et données de tests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
pasteques=generateSet(pasteque,1999,4)
ananas=generateSet(anana,1999,4)
pasteques2=addEtiquette(pasteques,0)
ananas2=addEtiquette(ananas,1)

datas=pasteques2+ananas2
random.shuffle(datas)
print(len(datas))
datasApprentissage=datas[:3000]
datasTest=datas[3000:]
print(len(datasApprentissage)+len(datasTest))

La bien sur vous allez dire mais on a déjà un W, pourquoi refaire des données d'entrainement? Ba effectivement on va re-procéder a une phase d'entrainement de notre modèle car si nous avions trouvé un modèle intéressant dans l'article précédent, ici, il ne faut pas oublié que nous utilisons une fonction d’activation de type sigmoïde pour laquelle nous avons décidé que toute réponse comprise entre 40% et 60% serait ignoré. Ainsi pendant la phase d'entrainement, il semble pertinent de procéder a la même règle.

A noter de plus que sans nous en apercevoir, dans notre phase d'apprentissage avec un limiter, nous avions un peu mis de coté les corrections sur le biais! Corrigeons ça:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
biaisFactor=np.array([[1, 1, 1, 1]])
marjMin=0.25
marjMax=0.75

print("W initial:",W,biais)    
for (val,etiquete) in datasApprentissage:
    sortie=neuroneSig(val,W,biais)
    if(sortie < marjMin) or (sortie > marjMax):
        W=majW(W, sortie, etiquete,val)
        biais=(np.dot(W,biaisFactor.T))/2
    
print("W final:",W,biais)  

1
2
W initial: [[1 1 1 0]] 1.5
W final: [[16.20492332 10.44986249 12.34447655  2.96069606]] [[20.97997921]]

Par curiosité, que se passe t il si on applique ces paramètres sur nos profils? Regardons.

1
2
print(neuroneSig(pasteque,W,biais))
print(neuroneSig(anana,W,biais))

1
2
8.938402177973581e-05
0.9998068041389231

Wahou! Presque 0% et 99%! C'est prometteur!

Calculons notre matrice de confusion car c'est probablement trop beau.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
erreur=[]
patequesTestIsP=[]
patequesTestIsA=[]
ananasTestIsP=[]
ananasTestIsA=[]
#Revoir comment construire le taux d'erreur
for (val,etiquete) in datasTest:
    sortie=neuroneSig(val,W,biais)
    if (sortie > marjMin) and (sortie < marjMax):
        erreur.append(val)
    if (sortie < marjMin) :
        if etiquete == 0:
            patequesTestIsP.append(val)
        if etiquete == 1:
            patequesTestIsA.append(val)
    if (sortie > marjMax):
        if etiquete == 0:
            ananasTestIsP.append(val)
        if etiquete == 1:
            ananasTestIsA.append(val)

print(len(erreur),":",len(datasTest))
print(len(patequesTestIsP),":",len(datasTest))
print(len(patequesTestIsA),":",len(datasTest))
print(len(ananasTestIsP),":",len(datasTest))
print(len(ananasTestIsA),":",len(datasTest))

1
2
3
4
5
15 : 1000
471 : 1000
40 : 1000
22 : 1000
452 : 1000

Donc nous avons comme matrice de confusion les données suivantes:
  • 15 données indécidables
  • 471 pasteques qui sont bien des pasquetes!
  • 452 ananas qui sont bien des ananas!
  • 40 pasteques qui se prennent pour des ananas
  • 22 ananas qui se prennent pour des pastèques
Donc nous avons 471/(471+22)=0.95 de précision et 471/(471+40)=0.92 de rappel (nous ne comptons pas les données exclu puisque c'est justement le but)

C'est effectivement mieux que pour la classification avec une fonction d'activation limiter.

Maintenant essayons de comprendre comment ça marche. Visualisons un peu nos données.

1
2
3
4
5
6
7
8
9
fig = plt.figure(1,figsize=(12,12))

plt.plot(patequesTestIsP.T[0],patequesTestIsP.T[1],"b.")
plt.plot(ananasTestIsA.T[0],ananasTestIsA.T[1],"b.")
plt.plot(patequesTestIsA.T[0],patequesTestIsA.T[1],"r.")
plt.plot(ananasTestIsP.T[0],ananasTestIsP.T[1],"y.")
plt.plot(erreur.T[0],erreur.T[1],"g.")
plt.xlabel('rugausité')
plt.ylabel('couleur')

Produisant le graphe suivant:


Il faut avouer que ce n'est pas très parlant même si l'on devine une section de données jaune quasi verticales.

Il ne faut pas oublier que nous traitons de données selon 4 dimensions ainsi, dans cette projection, l'hyperplan séparateur des deux classes de fruit n'est probablement pas perpendiculaire au plan du graphique. Ainsi, il est difficile de vraiment visualiser le résultat (ou il faudrait identifier une transformation des données dans un base adéquat... vous savez, matrice de passages, etc... je laisse ça aux motivés mais ça serait intéressant de s'y attarder quand même un de ces quatre).

Simplification

Pour comprendre malgré tout notre problème, je vous propose de le simplifier et de travailler seulement en deux dimensions des le départ.

Reprenons donc nos données et recalculons nos ensembles:

1
2
3
4
5
6
7
8
W=np.array([[1, 1]])#, 1, 0]])
biais=1.5

pasteque=np.array([[0.2, 0.3]])#, 0.2, 0.95]])
anana=np.array([[0.8, 0.65]])#, 0.6, 0.8]])

print(neuroneSig(pasteque,W,biais))
print(neuroneSig(anana,W,biais))

On adapte la procédure d’apprentissage

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#On utilise l'algo en limiter

biaisFactor=np.array([[1, 1]])#, 1, 1]])
marjMin=0.25
marjMax=0.75

print("W initial:",W,biais)    
for (val,etiquete) in datasApprentissage:
    sortie=neuroneSig(val,W,biais)
    if(sortie < marjMin) or (sortie > marjMax):
        W=majW(W, sortie, etiquete,val)
        biais=(np.dot(W,biaisFactor.T))/2
    
print("W final:",W,biais)        

Ensuite on fait passer nos données de tests dans notre algorithme d’évaluation (voir précédent celui ci n'a pas changé) et on finit par afficher les données en adaptant forcement au fait que nous sommes maintenant en deux dimensions (au passage on utilise les paramètres du neurone pour dessiner la droite séparatrice).


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fig = plt.figure(1,figsize=(12,12))

plt.plot(patequesTestIsP.T[0],patequesTestIsP.T[1],"b.")
plt.plot(ananasTestIsA.T[0],ananasTestIsA.T[1],"b.")
plt.plot(patequesTestIsA.T[0],patequesTestIsA.T[1],"r.")
plt.plot(ananasTestIsP.T[0],ananasTestIsP.T[1],"y.")
plt.plot(erreur.T[0],erreur.T[1],"g.")
plt.xlabel('rugausité')
plt.ylabel('couleur')

x=np.linspace(0,1,10)
plt.plot(x,(biais[0][0]-W[0][0]*x)/W[0][1])

Voyons donc ce que cela donne:


Ainsi, tout devient plus clair avec cette simplification. Pour rappel, en bleu nous avons les données correctement classées, en vert, les données écartées, en rouge et jaune, les faux positifs et les vrais négatifs.

Avec ce graphe on comprend tout de suite mieux l’intérêt de la fonction sigmoïde. Elle permet de construire une zone autour de la droite séparatrice dans laquelle on pousse le modèle a ne pas prendre de décision (ou du moins donner aux prédictions une crédibilité minimale) Bien sur en faisant de la sorte, on sacrifie potentiellement des prédictions justes, cependant, et c'est le contexte de métier qui l'impose, il est parfois préférable de ne pas décider plutôt que de faire une erreur.

Ainsi en utilisant une fonction d'activation de type sigmoïde, on améliore la qualité de la classification en se permettant de rejeter certaines prise de décisions.

Conclusion

Voila, nous avons fait un bon tour de la notion de classification a l'aide de modèle linéaire (ici un seul neurone) Dans les prochains articles je vous propose, maintenant que nous avons acquis les bases des concepts de la classification , d'une part de découvrir quelques outils et framework permettant un travail similaire, et d'autre part d’élargir nos types de modèles. A très bientôt.

Références

[1] https://un-est-tout-et-tout-est-un.blogspot.com/2018/07/ai-approche-neuromimetique-la.html

Aucun commentaire:

Enregistrer un commentaire