Objectif¶

Écrire un programme permettant à l'Ergo Jr de déterminer par apprentissage automatique la meilleure façon de lancer une balle pour atteindre une cible dans une direction fixée. La cible est posée à une distance aléatoire de l'Ergo Jr.

Recherches documentaires¶

Une explication plutôt claire et simple des différentes catégories d'apprentissage

https://makina-corpus.com/blog/metier/2017/initiation-au-machine-learning-avec-python-theorie

La meilleur technique pour l'objectif envisagé semble être l'apprentissage par renforcement

  • Apprentissage en ligne
  • Apprentissage épisodique
  • Monte Carlo, algorithme du bandit
  • Q-learning
  • Réseau de neurones

Des références :

Introduction à Scikit Learn¶

Préparation des données¶

Features en anglais ou variables explicatives en français sont les variables accessibles, que l'on peut mesurer. Elles correspondent aux données que l'on cherche à prédire. Dans notre cas les angles des moteurs pour réaliser un lancé gagnant.

Apprentissage avec Perceptron¶

Soit $f$ la fonction de transfert suivante : $$ \forall x\in\mathbb{R},\ f_H: x\mapsto \left\{ \begin{array}{cccl} 0 & \text{si} & x < 0 \\ 1 & \text{si} & x \geq 0 \\ \end{array}\right. $$

Perceptron : L'objectif est d'implanter un perceptron à seuil (neurone simple à seuil) pour résoudre les problèmes de tri entre deux classes (ensembles d'élement) linéairements séparables.

Implantation du neuronne artificiel¶

Description¶

L'unique fonction de ce neurone est de fournir une sortie égale à $1$ ou $0$, on parle alors de neurone activé pour la valeur $1$ et de neurone non actif pour la valeur $0$.

Pour cela on effectue la somme $S = b + \sum\limits_{i=1}^n x_i\,\omega_i$, avec $b$ le poids associé à l'entrée de biais du neurone artificiel. La valeur de l'entrée de biais est fixée à 1. Connaissant cette somme on décide d'activer ou non notre neuron en fonction d'un seuil d'activation $\theta$, fixé au préalable.

Pour décider de l'activation de notre neurone on utilise la fonction de transfert HeavySide notée $f_H$ de type seuil qui renvoie

heaviside :$\hphantom{000000000000000000000000000}$ Graphique :
$$ y_H=f_H(S)=\left\{\begin{array}{cccl}0 & \text{si} & S < 0 \\1 & \text{si} & S \geq 0 \\ \end{array}\right.$$
$$y_H = f_H(S) = f_H(b + \sum\limits_{i=1}^n x_i\,\omega_i)$$

Remarques :

  • Soient $W$ et $X$ les matrice colonnes associées aux poids et aux valeurs d'entrées alors on peut écrire que : $$S = b + \sum\limits_{i=1}^n x_i\,\omega_i = b + W^TX$$
  • La valeur seuil de la fonction Heaviside est à définir en fonction des besoins
heaviside :$\hphantom{000000000000000000000000}$ Graphique :
$$ y_H=\left\{\begin{array}{cccl}0 & \text{si} & S < \theta \\1 & \text{si} & S \geq \theta \\ \end{array}\right.$$

Apprentissage¶

L'algorithme consiste à donner des valeurs d'apprentissage au perceptron et comparer la sortie de la fonction Heaviside notée $y_H$ à la sortie attendue que l'on peut lire dans les valeurs d'apprentissage notée $y_v$.

La différence $\Delta y = y_v - y_H$, introduit une notion d'erreur permettant par la suite de corriger (ou pas) les poids attribués à chaque valeur d'entrée du neurone

  • Si $\Delta y \neq 0$, on adapte les poids $\omega_i$ et $b$ en les incrémentant ou décrémentant d'un pas d'apprentissage noté $\eta$ fixé arbitrairement ($\eta$ un réel tel que $\eta\in]\,0, 1]$), l'ajustement se fait de la manière suivante :
    • $\omega_{i}= \omega_i + x_i*\eta*\Delta y$
    • $b = b + \eta*\Delta y$
  • On répète ces étapes $n$ fois.
  • Plus $n$ est grand plus le perceptron apprend.

Minimisation de l'erreur : mise à jour des poids¶

Afin de mesurer l’erreur commise sur l’ensemble d’un jeu de données, il est nécessaire de se fixer une fonction de perte qui mesure le coût d’une erreur lors de la prédiction d’un exemple.Pour cela nous allons nous intéresser à la fonction de perte liée à l'erreur quadratique $$E(y_v, f_H(b + \sum\limits_{i=1}^n x_i\,\omega_i)) = \dfrac{1}{2}(y_v-f_H(b + \sum\limits_{i=1}^n x_i\,\omega_i))^2=\dfrac{1}{2}(y_v-y_s)^2$$

La mise à jour des poids revient à minimiser la valeur de sortie de la fonction d'erreur que nous pouvons approcher grâce à la méthode de descente de gradient. Cet ajustement des poids se fera pour chaque n-uplet de valeurs en entrée du perceptron.

$$\Delta \omega = -\eta\nabla E$$
$$\begin{pmatrix} \omega_1\\ \omega_2\\ \dots\\ \omega_n\end{pmatrix}= -\eta \begin{pmatrix} \dfrac{\partial E}{\partial \omega_1}\\ \dfrac{\partial E}{\partial \omega_2}\\ \dots\\ \dfrac{\partial E}{\partial \omega_n}\end{pmatrix}$$

Commençons par évaluer $\dfrac{\partial E}{\partial \omega_i}$ pour une fonction d'activation quelconque de classe $\mathcal{C}^1$ notée $f$, nous reviendrons sur le cas particulier de la fonction HeavySide $f_H$ un peu plus loin.

$E = \dfrac{1}{2}(y_v-y_s)^2 = \dfrac{1}{2}\left(y_v-f\left(\sum\limits_{i=0}^n x_i\,\omega_i\right)\right)^2\quad\text{Pour simplifier les notations le biais est inclu dans la somme des }\omega_ix_i$

$\dfrac{\partial E}{\partial \omega_i} = 2\times\dfrac{1}{2}\left(y_v-f\left(\sum\limits_{i=0}^n x_i\,\omega_i\right)\right)\times\dfrac{\partial }{\partial \omega_i}\left(y_v-f\left(\sum\limits_{i=0}^n x_i\,\omega_i\right)\right)$

$\dfrac{\partial E}{\partial \omega_i} = (y_v-y_s)\times\dfrac{\partial }{\partial \omega_i}\left(f\left(\sum\limits_{i=0}^n x_i\,\omega_i\right)\right) \quad\text{avec}\quad (f\circ g)'(x) = f'(g(x))g'(x)$

$\dfrac{\partial E}{\partial \omega_i} = (y_v - y_s)\ f'\left(\sum\limits_{i=0}^n x_i\omega_i\right)\,x_i$

$\Delta \omega_i = -\eta\,(y_v - y_s)\,x_i\ f'\left(\sum\limits_{i=0}^n x_i\omega_i\right)$

$$\boxed{\Delta \omega_i = \eta\,(y_s - y_v)\,x_i\ f'\left(\sum\limits_{i=0}^n x_i\omega_i\right)}$$

Nous obtenons une relation de mise à jour des poids pour un n-uplet donné en entrée du perceptron. Pour le biais il suffit de fixer le $x_i$ correspondant à $1$ ou $-1$ suivant les cas.

Exemple¶

Exemple d'apprentissage n° 1 : Imaginons une situation ou nous distinguons un territoire avec deux colonnies d'insectes (deux classes d'insectes). L'idée est d'écrire un programme qui va classer automatiquement tout nouvel insecte arrivant sur la carte dans une des colonnies en fonction de sa position.

Commençons par implanter nos colonnies

In [2]:
from random import randint
import numpy as np
import matplotlib.pyplot as plt
In [3]:
x1, y1 = [0.03, 0.14, 0.4], [0.08, 0.36, 0.17]
x2, y2 = [0.67, 0.53], [0.69, 0.67]
plt.grid()
plt.plot(x1, y1, '*')
plt.plot(x2, y2, 'ro')
plt.show()

Pour cet exemple l'idée est de réduire à son strict minimum le nombre de variables explicatives. Cela permet d'observer le fonctionnement du neurone de notre perceptron dans un plan.

La Table d'apprentissage correspondante aux variables explicatives qui serviront en entrées de notre neurone.

\begin{array}{|c|c|c|}\hline x_1 & x_2 & y_v\\\hline 0.03 & 0.08 & 1\\\hline 0.14 & 0.36 & 1\\\hline 0.4 & 0.17 & 1\\\hline 0.67 & 0.53 & 0\\\hline 0.69 & 0.67 & 0\\\hline \end{array}

Soit $b = 0.5$, $\forall i\in\mathbb{N},\, \omega_i=0$ et $\eta = 0.2$

  • On lit un n-uplet
  • On calcul sa sortie
  • On met les poids à jour si nécessaire : $\omega_{i}= \omega_i + \eta*\Delta y*x_i$ et $\quad b = b + \eta*\Delta y$
  • On recommence au début tant que l'apprentissage n'est pas satisfaisant
\begin{array}{|c|c|c|c|c|c|c|c|}\hline \text{Itérations} & \text{n-uplet} & b & \omega_1 & \omega_2 & S = b +\sum\limits_{i=1}^n x_i\,\omega_i & y_H = f_H(S) & Y_v & \Delta y \\\hline 1 & (0.03, 0.08) & 0.5 & 0 & 0 & 0.5 & 1 & 1 & 0\\\hline 2 & (0.14, 0.36) & 0.5 & 0 & 0 & 0.5 & 1 & 1 & 0\\\hline 3 & (0.4, 0.17) & 0.5 & 0 & 0 & 0.5 & 1 & 1 & 0\\\hline 4 & (0.67, 0.53) & 0.5 & 0 & 0 & 0.5 & 1 & 0 & -1\\\hline 5 & (0.69, 0.67) & 0.3 & -0.134 & -0.106 & 0.13652 & 1 & 0 & -1\\\hline \end{array}

Pour les trois premières itérations $\Delta y = 0$ donc rien à faire.

À la quatrième itération, on observe que $\Delta y \neq 0$ donc une mise à jour des poids s'impose.

$$\begin{pmatrix} \omega_1\\ \omega_2\end{pmatrix}=\begin{pmatrix} 0\\ 0\end{pmatrix} + 0.2\times (-1)\times \begin{pmatrix} 0.67\\ 0.53\end{pmatrix} = \begin{pmatrix} -0.134\\ -0.106\end{pmatrix}\quad\quad\text{et}\quad b = 0.5 - 0.2 = 0.3$$

Itération cinq, nouvelle mise à jour des poids

$$\begin{pmatrix} \omega_1\\ \omega_2\end{pmatrix}=\begin{pmatrix} -0.134\\ -0.106\end{pmatrix} + 0.2\times (-1)\times \begin{pmatrix} 0.69\\ 0.67\end{pmatrix} = \begin{pmatrix} -0.272\\ -0.24\end{pmatrix}\quad\quad\text{et}\quad b = 0.3 - 0.2 = 0.1$$

Doit-on s'arréter à ce stade ? La réponse est non, les poids ont été modifiés à la cinquième itération mais sont-ils valables pour toutes les valeurs de la table d'apprentissage ? On recommence tant que ce n'est pas le cas.

\begin{array}{|c|c|c|c|c|c|c|c|}\hline \text{Itérations} & \text{n-uplet} & b & \omega_1 & \omega_2 & S = b +\sum\limits_{i=1}^n x_i\,\omega_i & y_H = f_H(S) & Y_v & \Delta y \\\hline 6 & (0.03, 0.08) & 0.1 & -0.272 & -0.24 & 0.07264 & 1 & 1 & 0\\\hline 7 & (0.14, 0.36) & 0.1 & -0.272 & -0.24 & -0.02448 & 0 & 1 & 1\\\hline 8 & (0.40, 0.17) & 0.3 & -0.244 & -0.168 & 0.174 & 1 & 1 & 0\\\hline 9 & (0.67, 0.53) & 0.3 & -0.244 & -0.168 & 0.0475 & 1 & 0 & -1\\\hline 10 & (0.69, 0.67) & 0.1 & -0.378 & -0.274 & -0.344 & 0 & 0 & 0\\\hline \end{array}

À la septième itération, on observe que $\Delta y \neq 0$ donc une mise à jour des poids s'impose.

$$\begin{pmatrix} \omega_1\\ \omega_2\end{pmatrix}=\begin{pmatrix} -0.272\\ -0.24\end{pmatrix} + 0.2\times \begin{pmatrix} 0.14\\ 0.36\end{pmatrix} = \begin{pmatrix} -0.244\\ -0.168\end{pmatrix}\quad\quad\text{et}\quad b = 0.1 + 0.2 = 0.3$$

À la neuvième itération, on observe que $\Delta y \neq 0$ donc une mise à jour des poids s'impose.

$$\begin{pmatrix} \omega_1\\ \omega_2\end{pmatrix}=\begin{pmatrix} -0.244\\ -0.168\end{pmatrix} + 0.2\times(-1)\times \begin{pmatrix} 0.67\\ 0.53\end{pmatrix} = \begin{pmatrix} -0.378\\ -0.274\end{pmatrix}\quad\quad\text{et}\quad b = 0.3 - 0.2 = 0.1$$

Équation de la séparatrice : $b +\sum\limits_{i=1}^n x_i = 0$

$$\omega_1x_1 + \omega_2x_2 + b = 0 \quad\text{ssi}\quad x_2 = -\frac{1}{\omega_2}(\omega_1x_1 + b)$$

Si on remplace par les valeurs que l'on a trouvé à l'itération 10 (on est pas sûr que ce soit fini) on peut écrire :

$$x_2 = \frac{1}{0.274}(-0.378\,x_1 + 0.1)$$

Essayons maintenant de tracer cette droite sur notre graphique.

In [4]:
def separatrice(x):
    return (1/0.274)*(-0.378*x + 0.1)

x = np.linspace(0,0.6,100)
y = separatrice(x)

plt.grid()
plt.plot(x, y)
plt.plot(x1, y1, '*')
plt.plot(x2, y2, 'ro')
plt.show()

Le travail d'apprentissage n'est pas terminé, écrire toutes les étapes à la main serait bien trop long, il ne reste plus qu'à écrire une fonction Python qui fera le travail à notre place.

Comment classer un nombre de colonnie supérieur à 2 ?

Implantation¶

In [ ]:
from random import choice, uniform
In [5]:
# La fonction de transfert
def heavyside(x):
    if x < 0:
        return 0
    return 1
In [6]:
def sigmoide(x, k=1):
    return 1./(1+np.exp(-k*x))
In [7]:
def perceptron(f, e, n, eta, b, data):
    '''
    f    -> function : fonction de transfert
    e    -> int      : nombre d'entrées du perceptron
    n    -> int      : nombre d'itérations pour l'apprentissage
    eta  -> int      : le pas d'apprentissage (arbitraire : 0 < eta <= 1)
    b    -> int      : poids du biais
    data -> list     : les données que l'on cherche à prédire
    '''
    errors = []                  # liste des erreurs à la prédiction
    w = np.zeros(e)              # Tous les poids à zéro
    w[-1] = b                    # poids du biais
    fin = len(data)
    for i in range(n):
        #x, s = choice(data)     # tirage aléatoire d'une des prédictions
        if i >= fin:
            x, s = data[i%fin]   # lecture circulaire de la table d'apprentissage
        else:
            x, s = data[i]
        result = np.dot(w, x)    # somme pondérée
        error = s - f(result)    # différence entre prédiction et résultat
        errors.append(error)
        w += eta * error * x     # mise à jour des poids (biais compris)
    return w, errors

Revenons à notre Exemple d'apprentissage n° 1 :

In [8]:
# Les données que l'on cherche à prédire
data = [
    (np.array([0.03, 0.08, 1]), 1),
    (np.array([0.14, 0.36, 1]), 1),
    (np.array([0.40, 0.17, 1]), 1),
    (np.array([0.67, 0.53, 1]), 0),
    (np.array([0.69, 0.67, 1]), 0),
]

Les tests

In [9]:
w, errors = perceptron(heavyside, 3, 20, 0.2, 0.5,  data)
print(w)
for x, _ in data:
    result = np.dot(x, w)
    print("{}: {} -> {}".format(x[:], result, heavyside(result)))
[-0.35  -0.202  0.3  ]
[ 0.03  0.08  1.  ]: 0.27334 -> 1
[ 0.14  0.36  1.  ]: 0.17828 -> 1
[ 0.4   0.17  1.  ]: 0.12566 -> 1
[ 0.67  0.53  1.  ]: -0.04156 -> 0
[ 0.69  0.67  1.  ]: -0.07684 -> 0

Évaluation de l'erreur avec l'apprentissage

In [10]:
plt.ylim([-1,1])
plt.plot(errors)
plt.show()
In [11]:
x = np.linspace(min(x1), max(x2), 100)
def separator(x):
    return -(w[0]/w[1]*x + w[2]/w[1])

plt.grid()
#plt.ylim([0,1])
plt.plot(x1, y1, '*')
plt.plot(x2, y2, 'ro')

for n in [10, 15]:
    w, errors = perceptron(heavyside, 3, n, 0.2, 0.5, data)
    y = separator(x)
    plt.plot(x, y, label = str(n))
    
plt.legend(loc=4)   
plt.show()

Arrivée de nouveaux insectes on positionne quelques insectes sur la carte, on calcul $\,f_H(S)\,$ avec les poids liés à l'apprentissage. La sortie indique à quelles catégories appartiennent nos insectes

In [12]:
insects = [np.array([0.30, 0.70, 1]), np.array([0.70, 0.80, 1]), np.array([0.4, 0.7, 1])]
for insect in insects:
    result = np.dot(insect, w)
    print("{}: {} -> {}".format(insect[:], result, heavyside(result)))
[ 0.3  0.7  1. ]: 0.0536 -> 1
[ 0.7  0.8  1. ]: -0.1066 -> 0
[ 0.4  0.7  1. ]: 0.0186 -> 1

Perceptron monocouche : extension à $N$ classes¶

Le réseau de neurones du perceptron mono-couche permet de construire une méthode de classification en $N$ classes en considérant chaque neurone comme un indicateur d’une classe

Regardons maintenant ce qui ce passe quand on possède un entier $N>2$ de colonnies à classer toujours linéairement séparables 2 à 2.

In [13]:
def colonnie(X, Y, n, idc=1):
    return np.array([np.random.uniform(X[0], X[1], n), np.random.uniform(Y[0], Y[1], n), np.ones(n)])

c1 = colonnie((0, 0.30), (0, 0.40), 4)
c2 = colonnie((0.30, 0.70), (0.50, 0.90), 4)
c3 = colonnie((0.60, 0.90), (0, 0.40), 4)

plt.grid()
plt.plot(c1[0], c1[1], 'bo')
plt.plot(c2[0], c2[1], 'ro')
plt.plot(c3[0], c3[1], 'go')
plt.show()

Il nous faut N neurones, le neurone $i\in[1, N]$ sépare la classe $i$ des autres classes

Reformulation du perceptron :

  • $\omega_{ij}= \omega_{ij} + \eta*(y_{vj} - y_{H})*x_i\quad$ avec $\quad y_H = f_H(b + \sum\limits_{i=1}^n x_i\,\omega_{ij})$

Pour un perceptron à 2 neurones, il y aura deux séparatrices

Implantation du perceptron Mono Couche : plusieurs neurones travaillent en parallèle¶

In [14]:
def perceptronMonoCouche(f, n, eta, b, data):
    '''
    f    -> function : fonction de transfert
    e    -> int      : nombre d'entrées du perceptron
    n    -> int      : nombre d'itérations pour l'apprentissage
    eta  -> int      : le pas d'apprentissage (arbitraire : 0 < eta <= 1)
    data -> list     : les données que l'on cherche à prédire
    '''
    errors = []                               # liste des erreurs à la prédiction
    
    entrees = len(data)                       # nombre d'entrées dans un neurone
    neurones = len(data[-1])                  # nombre de neurones dans la couche = nombre prédictions
    w = np.zeros([neurones, len(data[0][0])]) # Tous les poids à zéro
    w[:,-1] = 1                               # poids du biais
    for j in range(neurones):
        e = []
        for i in range(n):
            if i >= entrees:
                x, s = data[i%entrees]   # lecture circulaire de la table d'apprentissage
            else:
                x, s = data[i]
            result = np.dot(w[j], x)
            
            error = s[j] - f(result)
            e.append(error)
            w[j] += eta*error*x
        errors.append(e)
    return w, errors

Mise en forme des données concernant nos insectes

In [15]:
def setData(c,ids):
    transp = np.transpose(c)
    data=[]
    for elt in transp:
        data.append((elt, ids))
    return data
In [16]:
data1 = setData(c1, [0,0])
data2 = setData(c2, [1,0])
data3 = setData(c3, [1,1])
data = data1 + data2 + data3

w, errors = perceptronMonoCouche(heavyside, 300, 0.2, 0.5, data)
print(w)
[[ 0.29973321  0.21135027 -0.2       ]
 [ 0.50870122 -0.35195219 -0.2       ]]

Tracé des séparatrices

In [17]:
x = np.linspace(0, 1, 100)

def separator(x, w):
    return -(w[0]/w[1]*x + w[2]/w[1])


plt.ylim([-0.5,1])
plt.plot(c1[0], c1[1], 'bo')
plt.plot(c2[0], c2[1], 'ro')
plt.plot(c3[0], c3[1], 'go')

y0 = separator(x, w[0])
y1 = separator(x, w[1])
plt.plot(x, y0)
plt.plot(x, y1)

plt.grid()
plt.show()

De nouveaux insectes

In [18]:
nuage = colonnie((0, 1.0), (0, 1.0), 6)
nuage = setData(nuage, [])
for insecte in nuage:
    result = np.dot(insecte[0], w[0])
    print("{}: {} -> {}".format(insecte[:], result, heavyside(result)))
    result = np.dot(insecte[0], w[1])
    print("{}: {} -> {}".format(insecte[:], result, heavyside(result)))
    print
(array([ 0.05709874,  0.83499256,  1.        ]), []): -0.00640970674867 -> 0
(array([ 0.05709874,  0.83499256,  1.        ]), []): -0.464831259836 -> 0

(array([ 0.92778082,  0.6748024 ,  1.        ]), []): 0.220706398767 -> 1
(array([ 0.92778082,  0.6748024 ,  1.        ]), []): 0.0344650622535 -> 1

(array([ 0.10245428,  0.45823596,  1.        ]), []): -0.072442753672 -> 0
(array([ 0.10245428,  0.45823596,  1.        ]), []): -0.309158527783 -> 0

(array([ 0.74296787,  0.31898166,  1.        ]), []): 0.0901090079126 -> 1
(array([ 0.74296787,  0.31898166,  1.        ]), []): 0.0656823718412 -> 1

(array([ 0.27591928,  0.21981433,  1.        ]), []): -0.0708400084359 -> 0
(array([ 0.27591928,  0.21981433,  1.        ]), []): -0.137003659056 -> 0

(array([ 0.88655068,  0.73761705,  1.        ]), []): 0.221624249637 -> 1
(array([ 0.88655068,  0.73761705,  1.        ]), []): -0.00861651387814 -> 0

Il n'y a plus qu'à combiner les deux sorties pour déterminer l'appartenance à une colonnie

  • [0, 0] -> bleu
  • [1, 0] -> rouge
  • [1, 1] -> vert

Exemple d'apprentissage n°2 : les variables explicatives sont plus nombreuses

Considérons un jeu de données définissant deux ensembles de n-uplets linéairements séparables, on aimerait par exemple pouvoir trier des caisses en fonction de la place restante. Les caisses pleines d'un coté et les caises avec encore au moins un emplacement vide de l'autre. Ci-dessous les valeurs des variables explicatives en fonction du remplissage des caisses.

0000 0100 1001 0101 1101

Table d'apprentissage Jeu limité des combinaisons possibles

\begin{array}{|c|c|c|c|c|}\hline x_1 & x_2 & x_3 & x_4 & y_v \\\hline 0 & 0 & 0 & 0 & 1\\\hline 0 & 1 & 0 & 0 & 0\\\hline 1 & 0 & 0 & 1 & 0\\\hline 0 & 1 & 0 & 1 & 0\\\hline 1 & 1 & 0 & 1 & 0\\\hline \end{array}
In [19]:
# Les données que l'on cherche à prédire
data = [
    (np.array([0, 0, 0, 0]), 1), #0
    (np.array([0, 0, 1, 0]), 0), #2
    (np.array([0, 1, 0, 1]), 0), #5
    (np.array([0, 1, 1, 0]), 0), #6
    (np.array([1, 0, 0, 0]), 0), #8
    (np.array([1, 0, 1, 0]), 0), #10
    (np.array([1, 0, 1, 1]), 0), #11
    (np.array([1, 1, 0, 0]), 0), #12
]
In [20]:
w, errors = perceptron(heavyside, 4, 100, 0.2, 0.5, data)
for x, _ in data:
    result = np.dot(x, w)
    print("{}: {} -> {}".format(x[:], result, heavyside(result)))
[0 0 0 0]: 0.0 -> 1
[0 0 1 0]: -0.2 -> 0
[0 1 0 1]: -0.3 -> 0
[0 1 1 0]: -0.6 -> 0
[1 0 0 0]: -0.2 -> 0
[1 0 1 0]: -0.4 -> 0
[1 0 1 1]: -0.3 -> 0
[1 1 0 0]: -0.6 -> 0

De nouvelles caisses

In [21]:
caisses = [np.array([0, 1, 0, 0]), #4
           np.array([1, 1, 0, 1]), #13
           np.array([1, 1, 1, 1]), #15
           np.array([0, 0, 0, 0])] #0
for c in caisses:
    result = np.dot(c, w)
    print("{}: {} -> {}".format(c[:], result, heavyside(result)))
[0 1 0 0]: -0.4 -> 0
[1 1 0 1]: -0.5 -> 0
[1 1 1 1]: -0.7 -> 0
[0 0 0 0]: 0.0 -> 1

Exemple d'apprentissage n°3 : Régression linéaire avec un seul neurone (exemple tiré de la loi de Beer Lambert)

Soit une série de coordonnées, l'objectif est de tracer une régression linéaire liée à cette ensemble de données.

  • Une seule entrée en plus du biais : C
  • Une fonction d'activation linéaire
  • La sortie attendue : A (dans laquelle j'ai ajouté un peu de bruit)
In [22]:
k = 100
C = [np.random.uniform(1e-3, 3e-2, 20), np.ones(20)]
A = k*C[0]+np.random.uniform(-0.3, 0.3, 20)
plt.plot(C[0],A, 'o')
plt.show()

Regréssion linéaire avec numpy

In [34]:
z  =  np.polyfit(C[0], A, 1)
p  =  np.poly1d (z)
print(p)
 
100.2 x + 0.04535

Régression linéaire avec le perceptron

In [23]:
data = []
x, ys = np.transpose(C), A.T
for i in range(len(x)):
    data.append((x[i], ys[i]))
In [38]:
x = np.linspace(min(C[0]), max(C[0]), 100)
def separator(x):
    return (w[0]*x + w[1])

plt.grid()
#plt.ylim([0,1])
plt.plot(C[0], A, 'o')

for n in [10, 1000, 10000, 100000, 1000000]:
    w, errors = perceptron(lambda x: x , 2, n, 0.1, 0.5, data)
    print(str(w[0]) + "*x + "+str(w[1]))
    y = separator(x)
    plt.plot(x, y, label = str(n))
    
plt.legend(loc=4)   
plt.show()
0.0168466333547*x + 1.1224748842
0.71377777675*x + 1.86715469626
6.7342208623*x + 1.75666061356
50.0354621118*x + 0.961946526319
99.4144748139*x + 0.0556862003731

On supperpose numpy et le perceptron

In [39]:
y_numpy = p(x)
plt.grid()
plt.plot(x, y_numpy)
plt.plot(x, y)
plt.show()

L'écart entre les deux

In [41]:
plt.grid()
plt.plot(x, abs(y_numpy-y))
plt.show()

Perceptron multicouche¶

Propagation des données à travers le réseau¶

Prenons pour simplifier les explications et les notations, un réseau de neurones avec 2 couches

  • la première contient deux neurones
  • la deuxième un seul neurone

Tous les neurones ont une fonction de transfert sigmoide du type : $$y_s = f_s(e) = \dfrac{L}{1+ \exp(-ke)}\quad\text{avec}\quad(L, k)\in\mathbb{N}^2\quad\text{et}\quad e = \sum\limits_{i=1}^n x_i\,\omega_i$$

Couche cachée : $k=1, L=1$

  • premier neurone : $y_{3s} = f_s(2.5\times 0.5 + 1\times 1.5) \simeq 0.94$
  • deuxième neurone : $y_{4s} = f_s(2.5\times -1 + 1\times -2) \simeq 0.011$

Sortie

  • neurone de sortie : $y_{5s} = f_s(0.94\times 1 + 0.011\times 3) \simeq 0.73$

Avec des matrices

$$D = \begin{pmatrix} 2.5 & 1\end{pmatrix}\quad W = \begin{pmatrix} 0.5 & -1 & 1\\ 1.5 & -2 & 3\end{pmatrix}$$

Couche cachée

$$S_{\text{caché}} = D\times\ W_{\text{caché}}= \begin{pmatrix} 2.5 & 1\end{pmatrix}\times\begin{pmatrix} 0.5 & -1\\ 1.5 & -2\end{pmatrix} = \begin{pmatrix} 0.94 & 0.011\end{pmatrix}$$

Sortie

$$S_{\text{sortie}} = S_{\text{caché}}\times W_{\text{sortie}} = \begin{pmatrix} 0.94 & 0.011\end{pmatrix}\times\begin{pmatrix} 1 \\ 3\end{pmatrix} \simeq 0.73$$

Implantation de la fonction de propagation¶

In [25]:
def propagation_avant(f, data, neurones, poids):
    '''
    Cette fonction propage les données à travers les couches du réseau. 
    Depuis la première couche cachée jusqu'à la couche de sortie.
    
    f        -> function : fonction de transfert
    data     -> array(1D): les données d'entrée d'une couche
    neurones -> list     : nombre de neurones par couche
    poids    -> array    : table des poids
    '''
    
    entrees = [np.copy(data)]
    couches = len(neurones)
    debut = 0
    
    for i in range(couches):
        fin = debut+neurones[i]                     # colonnes utilisées pour les calculs
        e = len(entrees[i])
        S = np.dot(entrees[i], poids[:e,debut:fin]) # somme pondérée sur une couche
        result = f(S)                               # sortie de la couche i
        debut = fin                                
        entrees.append(result)
    return entrees
 

Utilisation de : $\text{propagation_avant}$¶

In [26]:
D = np.array([2.5, 1])
W = np.array([[0.5, -1, 1],[1.5, -2, 3]])
couches_neurones = [2, 1]
S = propagation_avant(sigmoide, D, couches_neurones, W)
for result in S:
    print result
[ 2.5  1. ]
[ 0.93991335  0.01098694]
[ 0.72569201]

Rétropropagation : mise à jour des poids du réseau¶

$$\omega_{ij}= \omega_{ij} + \eta\times x_i\times \Delta_j$$

Toujours à partir de l'exemple ci-dessus, nous avions comme données d'entrées $D = (2.5\ \ 1)$. Soit $y_v = 1$, la sortie attendue

  • Erreur de sortie à minimiser : $\Delta_5 = y_v-ys = 1-0.73 = 0.27$
  • Erreurs de la couche cachée :
    • $\Delta_3 = f_s(y_{3S})(1-f_s(y_{3S}))\sum \omega_i\Delta_j = 0.94*(1-0.94)*1*0.27 = 0.015$
    • $\Delta_4 = f_s(y_{4S})(1-f_s(y_{4S}))\sum \omega_i\Delta_j = 0.011*(1-0.011)*3*0.27 = 0.0088$
  • Mise à jour des poids : le taux d'apprentissage est fixé à 0.1
    • $\omega_{13} = \omega_{13} + 0.1*2.5*0.015 = 0.50375$
    • $\omega_{14} = \omega_{14} + 0.1*1*0.0088 = 1.50088$
    • $\omega_{23} = \omega_{23} + 0.1*2.5*0.015 = -0.99625$
    • $\omega_{24} = \omega_{24} + 0.1*1*0.0088 = -1.9991$
    • $\omega_{35} = \omega_{35} + 0.1*0.94*0.27 = 1.02538$
    • $\omega_{45} = \omega_{45} + 0.1*0.011*0.27 = 3.0003$

Implantation¶

In [27]:
def retroProp(f, data, neurones, poids):
    x = data[0]
    yv = data[1]
    
    couches = len(neurones)-1
    fin = len(poids[0])
    
    # Erreur de sortie
    ys = propagation_avant(f, x, neurones, poids)
    deltas = [yv - ys[-1]]
    
    # Erreur sur les couches cachées
    for i in range(couches, 0, -1):
        debut = fin - neurones[i]
        produit_scalaire = np.dot(deltas[0], poids[:, debut:fin].T)
        delta = (ys[i] * (1-ys[i]))*produit_scalaire
        deltas.insert(0, delta)
        fin = debut
    return deltas
    
    # Mise à jour des poids

Utilisation de : $\text{retroProp}$¶

In [28]:
D = (np.array([2.5, 1]), 1)
W = np.array([[0.5, -1, 1],[1.5, -2, 3]])
retroProp(sigmoide, D, [2, 1], W)
Out[28]:
[array([ 0.01549189,  0.00894208]), array([ 0.27430799])]

christophe.casseau@ensam.eu