Thématiques principales

samedi 28 juillet 2018

IA: DataSet et Concours

Aujourd'hui, un article court que je nommerai utilitaire afin de ne pas voir a courir a travers tout le net pour trouver des données ou chercher un concours pour s'exercer.

Donc tout d'abord une petite liste de datasets et de pages associées afin de les récupérer. En effet, si on veut faire de l'IA, le nerf de la guerre c'est les datas... et comme on dit, pour entraîner un bon modèle, il vaut toujours mieux une bonne base de données qu'un bon algorithme....

Ensuite des concours, oui car les concours en IA sont une manière d'apprendre, comprendre et s’améliorer (je parle du programmer de l'IA, pas de l'IA.... quoi qu'il serait intéressant de faire de voir si une IA, ne produirai pas mieux que nous une IA....)

Enfin bref, voila une première liste que je tenterai de compléter au fil de l'eau et des propositions:

Les DataSets

  • Machine Learning Repository : https://archive.ics.uci.edu/ml/index.php
  • AWS: https://registry.opendata.aws/
  • Kaggle : https://www.kaggle.com/uciml
  • AI-Wiki : https://skymind.ai/wiki/open-datasets
  • Les DataSets les plus connus: https://www.analyticsvidhya.com/blog/2018/03/comprehensive-collection-deep-learning-datasets/
  • DataPortals : http://dataportals.org
  • DataMonitor: http://opendatamonitor.eu
  • QuandL: http://quandl.com
  • Des datasets sur GitHub : https://www.kdnuggets.com/2016/05/top-10-datasets-github.html
  • Une liste de resources complementaire: https://blog.bigml.com/list-of-public-data-sources-fit-for-machine-learning/
  • Liste Wikipedia https://en.wikipedia.org/wiki/List_of_datasets_for_machine_learning_research
  • Quoras : https://www.quora.com/Where-can-I-find-large-datasets-open-to-the-public
  • Reddit: www.reddit.com/r/datasets
  • Paris : https://opendata.paris.fr/explore/?sort=modified

Les Concours

  • Kaggle : https://www.kaggle.com/competitions
  • ChallengeData : https://challengedata.ens.fr/fr/season/4/challenge_data_2018.html
  • Bee: http://bee-o-diversity-challenge.strikingly.com/
  • Chalearn: http://www.chalearn.org/
  • CodaLab: https://competitions.codalab.org/
  • Des exercices: https://www.hackerrank.com/domains/ai

Voila, ce n'est pas exhaustif et honnêtement je n'ai pas regarder dans le détails, mais bon cela suffira pour les premiers besoins en données et exercices.

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

samedi 14 juillet 2018

IA : Neuromimétique, la classification linéaire

Dans un article précédent [1], nous avons abordé la structure d'un neurone. Nous n'avions pas vu d'exemple pour illustrer son fonctionnement. Dans ce présent article, je vous propose de le compléter  en traitant d'un exemple de classification. Cela nous permettra d'explorer d'une part le fonctionnement intime du neurone et, d'autre part les grandes lignes du processus de mise en oeuvre d'un modèle au travers d'un exemple simple, celui du tri (binaire) de fruits: des ananas et des pastèques.

Le problème


Imaginons donc que l’on cherche à différencier des pastèques et des ananas. Ces fruits sont assez particuliers donc a priori on pourra facilement mettre en oeuvre un moyen de les discerner. Entre autre en les observant, on peut positionner 4 caractéristiques qui vont nous servir de critères suivant les ensemble suivant :
  • la rugosité -> 0 lisse a 1 rugeux
  • la couleur -> 0 bleu a 1 rouge
  • la forme -> 0 rond a 1 alongé
  • le poid -> 0 (20gr) à 1 (2000gr)
Ainsi pour chacun de fruits, des caractéristiques seront extraites et injecter dans les entrées de notre neurone (le vecteur X) tel que 


Sur cette base, nous pouvons constater une chose importante le neurone s'accommode bien de la nature des données, il est possible de mélanger des choux et des carottes… A l'inverse, du fait de la nature de poids, il est important d'avoir des données normalisés (de même ordre de grandeur) afin de ne pas biaiser le système en faveur d'une caractéristique plutôt qu'une autre. C’est pour cela que l’on place toutes les valeurs entre 0 et 1 (on aurait pu aussi se placer entre -1 et 1).

Visualiser les données

Nous avons pas vraiment de fruits alors pour l'exercice, je vous propose de nous donner un profil de fruits a partir duquel nous produirons deux ensembles nous permettant de travailler. Pour cela, exécutons le code python suivant.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from random import randint, seed
import random
from mpl_toolkits.mplot3d import axes3d

import matplotlib.pyplot as plt
import numpy as np


def generateSet(prototype,nbrEchantillon,coef):
    rand_value=np.random.randn(len(prototype),len(prototype[0]))/coef
    #print(rand_value)
    rand_set=prototype+rand_value
    if nbrEchantillon == 0 :
        return prototype
    else:
        return np.concatenate((rand_set,generateSet(prototype,nbrEchantillon-1,coef)))

1
2
3
4
pasteque=np.array([[0.2, 0.3, 0.2, 0.95]])
anana=np.array([[0.8, 0.65, 0.6, 0.8]])
pasteques=generateSet(pasteque,1999,4)# -> pour separer les ensembles
ananas=generateSet(anana,1999,4)

Ensuite visualisons nos données selon plusieurs angles

 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
27
28
x=np.linspace(0,1,10)

fig = plt.figure(1,figsize=(15,15))
ax = plt.subplot(221, projection='3d')# equivalent a fig.addsubplot

ax.scatter(pasteques.T[0],pasteques.T[1],pasteques.T[2], c='b')
ax.scatter(ananas.T[0],ananas.T[1],ananas.T[2],c="r")
plt.xlabel('rugausité')
plt.ylabel('couleur')
ax.set_zlabel('forme')

ax = plt.subplot(222)
plt.plot(pasteques.T[0],pasteques.T[1],"b.")
plt.plot(ananas.T[0],ananas.T[1],"r.")
plt.xlabel('rugausité')
plt.ylabel('couleur')

ax = plt.subplot(223)
plt.plot(pasteques.T[0],pasteques.T[2],"b.")
plt.plot(ananas.T[0],ananas.T[2],"r.")
plt.xlabel('rugausité')
plt.ylabel('forme')

ax = plt.subplot(224)
plt.plot(pasteques.T[0],pasteques.T[3],"b.")
plt.plot(ananas.T[0],ananas.T[3],"r.")
plt.xlabel('rugausité')
plt.ylabel('poid')


Ainsi une pastèque aura le profil moyen autour des coordonnées [0.2, 0.3, 0.2, 0.95] et un ananas [0.8, 0.65, 0.6, 0.8] 

Bien sur l’exemple est simple et on se rend vite compte que d'une part les données sont déjà très séparées et que d'autre part certains critères seront plus utile que d’autre a la construction du paramétrage du neurone.

Paramétrage

Pour définir les paramètres du neurone, il faut avant tout choisir une fonction d’activation. Plusieurs choix se présente à nous, nous les avions vu dans l'article [1]: nous n'utiliserons pas une fonction linéaire car nous ne souhaitons pas prédire la taille des fruits mais les classer (nous reviendrons sur les prédictions dans un autre article sur la régression linéaire) donc il faut choisir entre une limiteur et une sigmoide. 

Commençons par le limiteur (nous regarderons plus tard ce que l'utilisation du sigmoïde apporte). Le limiteur à pour intérêt de fournir une réponse franche à notre problème de classification en nous donnant pour réponse soit 1 (on va dire une ananas) soit 0 on va dire une pastèque. A noter que si on voulait trier plus de type de fruits, ça ne serait pas possible et il faudrait considérer un nombre plus important de neurones mais faisons déjà avec un car maintenant que nous avons choisi une fonction d’activation, il nous reste encore à le paramétrer le neurone (les poids et le biais).

De façon empirique, la détermination des paramètres est la configuration permettant de discriminer les entrées pertinentes pour séparer les deux ensembles (ananas et pastèques) Ainsi, intuitivement et analytiquement on se rend compte que le plus simple est de s'appuyer sur les paramètres discriminant, et d’annuler ceux qui ne le sont pas. Pour le biais, tentons une moyenne des paramétrés choisis:

W=[1;1;1;0] avec un biais 1,5. Testons de facon analytique:

  • limiteur((Wt.pasteque)-biais)=limiteur( 0.4- 1.5) =limiteur(-1.1 )= 0
  • limiteur((Wt.anana)-biais)=limiteur( 2.35- 1.5) =limiteur(0.85 )= 1

Vérifions en le codant: (on implémente avant quelques fonctions permettant de calculer la sortie du neurone)

1
2
3
4
5
6
def neuroneLim(entre,W,biais):
    a=np.dot(entre,W.T)-biais
    #print("a neurone:",a)
    if a > 0:
        return 1
    return 0

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

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

Ok ça marche c’est cool mais on a paramétré un peu au pif (enfin pas tout à fait) mais essayons de comprendre pourquoi ça marche.

Interprétation

Pour comprendre pourquoi ça fonctionne, il faut s’intéresser au sens mathématique de l’opération de combinaison linéaire de l'entrée et des poids du neurone qui consiste en un produit scalaire. Ce produit va favoriser numériquement les vecteurs ayant une orientation perpendiculaire à une droite séparatrice qui modélisera la démarcation entre nos deux types de fruit. En utilisant le biais pour correctement ajuster la position de cette droite sur selon les ordonnées. 

Ainsi, si l’on représente W sur nos ensembles sous la forme de vecteur, on se rend compte, que orientation de celui ci suit un groupe et rejette l’autre:

 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
27
28
29
30
31
32
33
34
35
x=np.linspace(0,1,10)

fig = plt.figure(1,figsize=(15,15))
ax = plt.subplot(221, projection='3d')# equivalent a fig.addsubplot

ax.scatter(pasteques.T[0],pasteques.T[1],pasteques.T[2], c='b')
ax.scatter(ananas.T[0],ananas.T[1],ananas.T[2],c="r")
plt.xlabel('rugausité')
plt.ylabel('couleur')
ax.set_zlabel('forme')


ax = plt.subplot(222)
plt.plot(pasteques.T[0],pasteques.T[1],"b.")
plt.plot(ananas.T[0],ananas.T[1],"r.")
plt.xlabel('rugausité')
plt.ylabel('couleur')
plt.quiver(0.5,0.5,1,1,scale=5)
plt.plot(x,1-x)

ax = plt.subplot(223)
plt.plot(pasteques.T[0],pasteques.T[2],"b.")
plt.plot(ananas.T[0],ananas.T[2],"r.")
plt.xlabel('rugausité')
plt.ylabel('forme')
plt.quiver(0.5,0.5,1,1,scale=5)
plt.plot(x,1-x)

ax = plt.subplot(224)
plt.plot(pasteques.T[0],pasteques.T[3],"b.")
plt.plot(ananas.T[0],ananas.T[3],"r.")
plt.xlabel('rugausité')
plt.ylabel('poid')
plt.quiver(0.5,0.75,1,0,scale=5)
plt.plot(0.5+x*0,x+0.2)
Donc en fait regarder si un fruit est un ananas, c’est regarder le signe du produit scalaire des paramètres de ce fruit avec les paramètres du neurone…. cool! On a compris comment le neurone fonctionne pour séparer des classes mais…. À ce stade plein de questions devraient vous venir ! comme:
  • Comment mesurer l’efficacité du neurone? est il possible de faire des erreurs pourquoi et comment?
  • Si effectivement il y a des erreurs, alors comment déterminer le séparateur optimalement (ie déterminer de façon formelle les paramètres de W sans y aller à la louche)?

Efficacité et performance

Pour répondre la première question, c'est à dire mesurer l’efficacité d’un RN il faut aller interroger quelques algorithmes et concepts du machine learning. Dans le cadre de la classification, on pourrait se dire, bien il faut donner au neurone des fruits à trier et en connaissant les réponses, faire la différence des bonnes et mauvaises réponses. 

Effectivement c'est une idée, je vous invite a le faire mais sans surprise, nous avons que des 1 pour les ananas et que des 0 pour les pastèques. Cool en même temps ce n’est pas forcement très étonnant puisque nos ensemble sont bien linéairement séparable même en temps.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
resultP=[]
for val in pasteques:
    resultP.append(neuroneLim(val,W,biais))
   
resultA=[]
for val in ananas:
    resultA.append(neuroneLim(val,W,biais))

print("pasteques:",sum(resultP)," somme:", (2000-sum(resultP))*100/1999)
print("ananas:", sum(resultA),"somme:", sum(resultA)*100/1999)
    

1
2
pasteques: 71  somme: 96.49824912456228
ananas: 1815 somme: 90.79539769884943
Tiens pas complètement... Pourquoi?, avons nous négliger quelque chose? Pourtant nous avons construit nos ensembles nous même et on peut légitimement croire que nos données sont bonnes.

En fait, bien que séparable linéairement, nous avons produit nos données selon une loi normale. Il y a donc toujours une possibilité de superposition des composantes qui bien que peu marqué introduit une potentielle erreur. On le voit d’ailleurs sur certains graphes précédent. 

En fait tout n’est pas toujours aussi simple. On pourrait avoir une superposition des composantes? Que faire alors les éléments ne sont pas linéairement séparable?

Étudions cette nouvelle problématique en changeant un peu le coef de génération de familles de fruits rendant certains critères moins facile à discriminer. Reprenons nos scripts précédent et changeons le paramètre 10 par 4 (deuxième bloc de code).



Nos résultats, comme démontré par le schéma, ne sont plus les même car il existe des ananas ayant des caractéristiques proches de ceux des pastèques et réciproquement du coup le taux de capacité à les différencier passe à 97% et 90%... ça reste honnête malgré tout mais on sent bien que l’utilisation seule de ces taux n’est pas vraiment suffisant pour nous faire une idée de la qualité de notre configuration (surtout si elle est produite à la main) et puis pourquoi en avons nous deux? Il faut une autre approche.

La matrice de confusion

Pour évaluer correctement notre modèle de neurone, il existe en Machine learning un outil pour discriminer correctement les limites du classifier: la matrice de confusion.

Le matrice de confusion est un recueil statistique de la performance du modèle de classification en s'intéressant à la nature des prédictions en fonction des valeurs réelles.

Elle se composent ainsi d’une ligne par classe réelle et une colonne par classe prédite (une classe étant soit un type de classe où tout bêtement l’absence où la présence de la classe).

Dans notre cas, nous avons deux classes en exclusion mutuelle (2 types de fruits et si c’est pas l’un c’est l’autre) nous pouvons donc soit établir une matrice avec les deux fruits soit avec un seul fruit en considérant les classes présent et absent (qui facilite souvent la compréhension de la matrice).

Donc sur notre échantillon de 4000 fruits composé des 2000 pastèques et 2000 ananas, nous avons la matrice suivante:


  • 1938 Vrai Positif (c’est à dire les pastèques détectées comme tel)
  • 1798 Vrai Négatif (c’est à dire ananas détectés comme tel)
  • 62 Faux Négatif (c’est à dire une pastèque interprétées comme un ananas)
  • 202 Faux Positif (c’est à dire un ananas détectés comme une pastèque)
On se rend compte que nous avons une préférence pour les pastèques (enfin nous non, mais notre modèle oui!)  Pour affiner la matrice, nous allons ensuite définir deux critères supplémentaire:
  • la précision : VP/(VP+FP)= 1938/(1938+202) = 0.90 capacité à détecter des pastèques en présence d’ananas (0.90 de chance que le modèle réponde que le fruit est un ananas)
  • le rappel ou sensibilité : VP/(VP+FN)= 1938/(1938+62) =0.97 capacité à réellement détecter une pastèque dans un ensemble que de pastèques.
Ce qui signifie que quand le modèle déclare avoir une pastèque, il n’a raison que 90% du temps (ça veut dire que ça peut être aussi un ananas) et il n’est capable que d'en détecter que 97% parmi les seules pastèques. 

Sur la base de la précision et du rappel, on peut calculer F1 ou la moyenne harmonique qui permet de fournir une métrique combinant les deux taux:


L'analyse de ce taux permet de guider dans l'optimisation du modèle, cependant il faut garder a l'esprit précision et rappel sont deux facettes d'une même pièce et que l'augmentation de l'un amènera forcement a un diminution du résultat de l'autre mais selon le contexte, cela peut se justifier....

L'apprentissage

Maintenant que nous avons les moyens d’évaluer le modèle, il est nécessaire d'avoir une approche pour la définition des paramètres du neurone.

En effet, a l’aide de la matrice de confusion, on voit bien qu’il y a un problème, nous qui étions si confiant sur notre analyse et notre paramétrage. Sur un exemple aussi simple…. juste avec un neurone … qu’est ce qui se passé? Il faut croire que décidément, le gâteau est un mensonge et qu’il n’y a pas de repas gratuit!

Mais alors comment definir le parametrage?

En machine learning, pour optimiser le paramétrage des modèles, il existe des procédures d’entrainement. Elles se distinguent en 3 formes:
  • supervisés: on présente au modèle des entrées d'entrainement dont on connait la réponse (donc nos données d’apprentissages sont le couple (data,étiquette). En fonction de l’écart de réponse, on change le paramétrage selon un algorithme spécifique. 
  • non supervisé, le modèle va affiner par lui même les paramètres at runtime en détectant lui même les ensemble intéressant (approche par clustering)
  • semi-supervisé, où les deux approches précédentes sont associées
Allons au plus simple, considérons un apprentissage supervisé (nous aurons l'occasion d'explorer les autres) tel que:
  • si etiquete - sortie > 0 alors W=W+data 
  • si etiquete - sortie < 0 alors W=W-data
  • si etiquete - sortie = 0 alors W
Que l'on peut simplifier par l'algo suivant:

1
2
3
4
def majW(W, sortie, etiquette,entree):
    #print(W, sortie, etiquette,entree)
    return W+(etiquette-sortie)*entree
    

Ainsi l’algo d’apprentissage consiste a exposer le modèle à valeurs d'entraînements permettant la mise à jour de W en suivant les règles precedentes

Pour cela, il faut des données d’entrainement. On a déjà des données pour chaque fruit. on va donc étiqueter nos données et ensuite les mélanger afin d'éviter d’introduire un biais d’apprentissage en favorisant un fruit avant l’autre.

1
2
3
4
5
def addEtiquette(entreeSet,etiquette):
    dataEtiquete=[]
    for val in entreeSet:
        dataEtiquete.append([val,etiquette])
    return dataEtiquete

1
2
3
pasteques=addEtiquette(pasteques,0)
ananas=addEtiquette(ananas,1)
W=np.array([[1, 1, 1, 0]])


1
2
3
4
5
6
datas=pasteques+ananas
random.shuffle(datas)
print(len(datas))
datasApprentissage=datas[:3000]
datasTest=datas[3000:]
print(len(datasApprentissage)+len(datasTest))

Il nous faut penser à évaluer le modèle, donc de cet ensemble de données on ne va en prendre qu’une partie pour l’apprentissage et une autre pour évaluer la qualité de l’apprentissage en le testant.

Première phase: apprentissage supervisé


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
print("W initial:",W)    
for (val,etiquete) in datasApprentissage:
    sortie=neuroneLim(val,W,biais)
    W=majW(W, sortie, etiquete,val)

print("W final:",W)    
erreur=[0]


for (val,etiquete) in datasTest:
    sortie=neuroneLim(val,W,biais)
    #print(sortie,etiquete)
    if sortie != etiquete:
        erreur.append(erreur[len(erreur)-1]+1)
    else:
        erreur.append(erreur[len(erreur)-1])

Nous étions partie d'un W=[1 1 1 0] et nous avons maintenant W=[ 3.40784321 2.44027133 2.17143355 -1.26320997]

Deuxième phase test: 


1
2
3
4
5
6
erreur=[1]+[erreur[i]*100/i for i in range(1,1001)]
print(len(erreur))
fig = plt.figure(1,figsize=(15,15))
ax = plt.subplot(111)
index=np.linspace(0,1,1001)
plt.plot(index,erreur)

Avec ce diagramme, on visualise l’émergence de l'erreur au fur et a mesure que la quantité de données de test valide ou invalide le résultat de l’apprentissage. (donc même si on a le sentiment que l'erreur varie, intrinsèquement, celle du neurone est constante, c'est son évaluation qui s'affine).

Si on interprète le résultat en fonction de notre problématique de classification, on se rend compte que la superposition des classes pose un vrai problème pour le neurone qui reste incapable de séparer les deux ensembles. Il s'agit la d'une limitation classique de ce type de modèle.

Conclusion

Voila, nous sommes au bout de cet exemple qui complète l'article plus théorique sur la structure du neurone. Nous avons suivi un processus complet de traitement des données à classifier (en passant pas l'analyse, le POC et la visualisation) et réaliser des choix argumentés (a peu prêt) afin d’affiner et rendre plus pertinent notre modèle. Nous avons aussi utilisé des outils d’évaluation de notre modèle et considérer un algorithme simple d'apprentissage.

Bien sur nous devions également voir le cas de l'utilisation d'une fonction d'activation de type sigmoïde mais l'article est il me semble déjà assez long.... Je vous propose donc de revenir sur ces petits détails dans de futur (et pas trop lointain) articles.

Merci d'avance de ne pas hésitez a me donner des retours, qu'ils soient sur le fond comme sur la forme!

Références

[1] https://un-est-tout-et-tout-est-un.blogspot.com/2018/07/ia-approche-neuromimetique-le-neurone.html

mercredi 11 juillet 2018

IA : Neuromimétique, le neurone

Aujourd'hui un article sur l'approche neuromimétique et plus particulièrement, son élément fondamental qu'est le neurone. L'objectif de l'article est d'introduire de façon simple les éléments constitutifs du neurone afin de mieux appréhender cet outil et également introduire les articles qui viendront sur les réseaux de neurones en générales.

Histoire

Le neurone est avant toute chose qu'un outil mathématique datant du début du siècle dernier, formalisé par McCULLOCH et PITTS. Basé sur ce modèle, de nombreux types de neurones ont été proposé et de nombreuses architectures conçues. Il serait trop long de traiter de tous ces points ici dans cet article mais nous reviendrons sur chacun d'entre eux. En attendant les détails voici les points marquant de l'histoire des réseaux neuronaux:

  • 1940 : Alan TURING : Machine de Turing
  • 1943 : Warren McCULLOCH & Walter PITTS Modèle formel de neurone.
  • 1948 : Non Neuman : Les réseaux d’automates
  • 1949 : Donald HEBB : Mémoire associative, premières règles d'apprentissage.
  • 1960 : Franck ROSENBLATT et Bernard WIDROW, Perceptron et Adaline.
  • 1969 : Marvin MINSKY Mise en évidence des limitations du perception
  • 1980 : Stephen GROSSBERG et Teuvo KOHONEN Auto-organisation des réseaux et adaptation
  • 1982 : John HOPFIELD : Approche Statistique.
  • 1986 : Paul Smolenski : Machine de BOLTZMANN 
  • 1997: Deep Blue
  • 2011 : Watson
  • 2014 : LeCun Deep Learning 
  • 2015 : Alpha Go
  • 2018 : Alexa

En biologie

La définition du neurone part de l'observation et de l'imitation du vivant [1]. Pour rappel, un neurone biologique est un cellule particulière dans le vivant puisqu'il participe au traitement de l'information et à la réponse biologique. (Bien sur bien d'autres mécanismes existent et peuvent être considérer comme participant au traitement de l'information comme le codage génétique ou la réponse hormonale, mais ce n'est pas le propos de traité de ces sujets la bien que l'approche traitant des algorithme génétique est un autre sujet de l'IA qu'il sera intéressant de traiter...)

Source de l'image : probablement ici mais pas sur...

Ainsi un neurone biologique est constitué:
  • d'un noyau : le cœur de la cellule neuronale
  • de dendrites permettant d’agréger les informations entrantes venant des synapses
  • d'axones fournissant la réponse neuronale
  • de synapses : interconnexion entre les axones et les dendrites permettant le transfert de l’influx nerveux 
Pour ne pas douter de l'efficacité des réseaux de neurones, il suffit de regarder un peu autour de nous et de regarder tous ces êtres vivant se déplaçant, communicant et interagissant entre eux, qu'ils soient humains ou animal.

Dans le cadre de l'etre humain, on parle de:
  • 100 Milliards de neurones
  • 10000 Synapses par neurone 
  • 10^15 Synapses dans le cerveau humain
dont les objectifs sont de facon non exhaustives:
  • Mémoire et persistance des données dans le temps
  • Réflexion, élaboration des idées, associer des concepts et des stratégies 
  • Sens, Analyse des données, traitements des sons, des images, du touché
  • Construction d'une réponse moteur, l’équilibre, l'orientation, la marche, dextérité
Ainsi on comprend qu'il serait bien dommage ne pas essayer de tenter de faire pareil!

Neurone Formel

Le neurone formel est un outil mathématique plutôt simple [2]. Il propose le calcul de la somme des entrées du neurone pondérées par différents coefficient représentant  la "conductance" des dendrites d'entrée. La réponse est alors le traitement de cette somme au travers d'un algorithme permettant de lui donner sens.



Ainsi mathématiquement on définit le neurone tel que:



avec  :

  • a la sortie du neurone
  • xi, le signal d'entré du neurone représenté par un vecteur 
  • wi,  le poid de ponderation des entrées du neurone représenté par un vecteur 
  • biais, une constante de pondération du neurone (à noter que si l’on supprime le biais, on part dans le machine learning, nous y reviendrons aussi)
  • somme, l’opérateur de sommation des entrées pondéré des poids 
  • f, la fonction d’activation du neurone, c’est à dire une fonction permettant de délivrer une sortie spécifique en fonction du résultat de la pondération des entrées

Bien sur dans la littérature nous trouverons la notation matriciel [3] qui permettra de généraliser l'exercice a des réseaux a n neurone:



On notera que W^T correspond à la transposé du vecteur.

Interprétation

Faire un parallèle avec le modèle biologique est très hasardeux cependant on peut interpréter le fonctionnement du modèle mathématique selon les équivalences suivantes:
  • Le vecteur X est le stimulus d’entrée
  • Ce stimulus est reçu par le neurone au travers des dendrites qui vont pondérer les différents messages nerveux.
  • La somme des messages pondérés peut alors être vu comme le niveau d'excitation du neurone 
  • La fonction d’activation sera la manière que le neurone va répondre si celui ci est suffisamment excité 
  • Le biais représente la qualité du message de sortie du neurone permettant de nuancer la capacité du neurone à répondre à l'excitation. 
Bien sur un réseau biologique comporte bien d’autre subtilités non prise en compte dans le modèle mathématique qui du coup est faux (si on voulait vraiment modéliser le comportement d’un cerveau) pourtant ce modèle mathématique est déjà très suffisant pour résoudre bien des problèmes, même avec un seul neurone, la différences d’utilisation se faisant alors sur le type de fonction d’activation qui sera utilisé.

Fonction d'activation

La fonction d’activation du neurone [4,5] est l'élément permettant à celui ci de produire une sortie conforme à l'interprétation des données que l’on souhaite faire produire par le neurone.

Il y a trois types principaux type de fonctions d’activations:
  • de type limiteur
  • de type linéaire
  • de type sigmoïde
Les limiteurs sont des fonctions répondant de façon binaire. Soit 0 ou 1, soit -1 où 1 selon le besoin. Il permettent généralement de produire des classes d’association



Les fonctions linéaire à l’inverse ont pour vocation de retourner le résultat de la somme des entrées pondérées par d'éventuels coefficients de normalisation et considérant d'éventuelle saturation. Elle s’utilise généralement lorsque la nature de la commande de sortie se doit d’être réelle (dans le cadre de prédictions ou de production d'une loi de commande) 

Enfin les fonctions de type sigmoïde sont produite à base d’exponentielle, de log où d’arc-tangente et permettent non pas d’être un compromis entre une fonction limiteur et une fonction lineaire mais de produire des sorties avec une interprétation statistique. Elles seront alors utilisées tout aussi bien en classification qu’en prédiction. 



Bien sur il existe de nombreuses variantes possibles comme les linéaires saturés ou même des hystérésis. Voici le code python pour reproduire ces graphes:


1
2
3
4
5
6
7
8
import matplotlib.pyplot as plt
import numpy as np
import math

l=np.linspace(-20,20,200)
#plt.plot(l,l)
plt.plot(l,[ 0 if val < 0 else 1 for val in l])
plt.plot(l,[ 1 / (1 + math.exp(-val)) for val in l])

Conclusion:

Ceci n'est que la première étape pour comprendre les réseaux de neurones. Il reste de nombreuses choses a voir, entre autres des exemples pour mieux cerner les mécanismes, en particulier l'apprentissage. Nous y viendrons justement dans les prochains articles qui se baseront sur celui ci afin de voir ce qu'il est possible de faire avec déjà juste un neurone! (certains n'auront plus d'excuses)

A noter qu'initialement, je prévoyais d’intégrer dans ce même article les capacités de classification et de régression linéaire qu'offre un seul neurone. Cependant, il s'est avéré que le temps me manque ces derniers temps et en plus j'ai remarqué que mes contenus devenait de plus en plus gros... du coup je prend le partie de découper un peu mes publications afin de publier plus souvent. Je pense que ça augmentera la lisibilité du blog... (et psychologiquement j'aurai pas l'impression d'en faire moins)

Références:

[1] https://www.college-de-france.fr/site/yann-lecun/course-2016-02-12-14h30.htm
[2] https://fr.wikipedia.org/wiki/Neurone_formel
[3] https://un-est-tout-et-tout-est-un.blogspot.com/2018/03/notions-de-mathematiques-elementaires.html
[4] http://www-connex.lip6.fr/~denoyer/wordpress/wp-content/uploads/2016/01/coursml2.pdf
[5] http://www.sylbarth.com/nn.php