Perceptron Multicouche

Jérémie Decock (www.jdhp.org)

Attention:

Veuillez noter que ce document est en cours de rédaction. Dans son état actuel, il n'est pas destiné à être lu par d'autres personnes que ses auteurs.

Principales implémentations en Python

Notes du livre Dunod

Notations

Les notations suivantes sont détaillées au fil du document:

$\newcommand{\cur}{i}$ $\cur$: couche courante

$\newcommand{\prev}{j}$ $\newcommand{\prevcur}{{\cur\prev}}$ $\prev$: couche immédiatement en amont de la courche courrante (i.e. vers la couche d'entrée du réseau)

$\newcommand{\next}{k}$ $\newcommand{\curnext}{{\next\cur}}$ $\next$: couche immédiatement en aval de la courche courrante (i.e. vers la couche de sortie du réseau)

$\newcommand{\ex}{\eta}$ $\ex$: exemple (sample ou feature) courant (i.e. le vecteur des entrées courantes du réseau)

$\newcommand{\pot}{x}$ $\pot_\cur$: Potentiel d'activation du neurone $i$ pour l'exemple courant

$\newcommand{\weight}{w}$ $\newcommand{\wcur}{{\weight_{\cur\prev}}}$ $\wcur$: Poids de la connexion entre le neurone $j$ et le neurone $i$

$\newcommand{\activthres}{\theta}$ $\activthres_\cur$: Seuil d'activation du neurone $i$

$\newcommand{\activfunc}{f}$ $\activfunc_\cur$: Fonction d'activation du neurone $i$

$\newcommand{\errfunc}{E}$ $\errfunc$: Fonction objectif ou fonction d'erreur

$\newcommand{\learnrate}{\epsilon}$ $\learnrate$: Pas d'apprentissage ou Taux d'apprentissage

$\newcommand{\learnit}{n}$ $\learnit$: Numéro d'itération (ou cycle ou époque) du processus d'apprentissage

$\newcommand{\sigout}{y}$ $\sigout_\cur$: Signal de sortie du neurone $i$ pour l'exemple courant

$\newcommand{\sigoutdes}{d}$ $\sigoutdes_\cur$: Sortie désirée (étiquette) du neurone $i$ pour l'exemple courant

$\newcommand{\weights}{\boldsymbol{W}}$ $\weights$: Matrice des poids du réseau (en réalité il y a une matrice de taille potentiellement différente par couche)

$\newcommand{\errsig}{\Delta}$ $\errsig_i$: Signal d'erreur du neurone $i$ pour l'exemple courant

$$ \pot_\cur = \sum_\prev \wcur \sigout\prev $$$$ \sigout\cur = \activfunc(\pot_\cur) $$$$ \weights = \begin{pmatrix} \weight_{11} & \cdots & \weight_{1m} \\ \vdots & \ddots & \vdots \\ \weight_{n1} & \cdots & \weight_{nm} \end{pmatrix} $$

Divers

Le PMC peut approximer n'importe quelle fonction continue avec une précision arbitraire suivant le nombre de neurones présents sur la couche cachée.

Initialisation des poids: généralement des petites valeurs aléatoires

Fonction objectif (ou fonction d'erreur)

Fonction objectif: $\errfunc \left( \weights \left( \learnit \right) \right)$

$\learnit$: itération courante de l'apprentissage $(1, 2, ...)$

Typiquement, la fonction objectif (fonction d'erreur) est la somme du carré de l'erreur de chaque neurone de sortie.

$$ \errfunc = \frac12 \sum_{\cur \in \Omega} \left[ \sigout_\cur - \sigoutdes_\cur \right]^2 $$

$\Omega$: l'ensemble des neurones de sortie

Le $\frac12$, c'est juste pour simplifier les calculs de la dérivée ?

Apprentissage

Mise à jours des poids

$$ \weights(\learnit + 1) = \weights(\learnit) \underbrace{- \learnrate \nabla \errfunc \left( \weights(\learnit) \right)} $$

$- \learnrate \nabla \errfunc \left( \weights(\learnit) \right)$: descend dans la direction opposée au gradient (plus forte pente)

avec $\nabla \errfunc \left( \weights(\learnit) \right)$: gradient de la fonction objectif au point $\weights$

$\learnrate > 0$: pas (ou taux) d'apprentissage

$$ \begin{align} \delta_{\wcur} & = \wcur(\learnit + 1) - \wcur(\learnit) \\ & = - \learnrate \frac{\partial \errfunc}{\partial \wcur} \end{align} $$$$ \Leftrightarrow \wcur(\learnit + 1) = \wcur(\learnit) - \learnrate \frac{\partial \errfunc}{\partial \wcur} $$

Chaque présentation de l'ensemble des exemples = un cycle (ou une époque) d'apprentissage

Critère d'arrêt de l'apprentissage: quand la valeur de la fonction objectif se stabilise (ou que le problème est résolu avec la précision souhaitée)

  • "généralement il n'y a qu'un seul minimum local" (preuve ???)
  • "dans le cas contraire, le plus simple est de recommencer plusieurs fois l'apprentissage avec des poids initiaux différents et de conserver la meilleure matrice $\weights$ (celle qui minimise $\errfunc$)"

Rétropropagation du gradient

Rétropropagation du gradient: une méthode pour calculer efficacement le gradient de la fonction objectif $\errfunc$.

Intuition: La rétropropagation du gradient n'est qu'une méthode parmis d'autre pour résoudre le probème d'optimisation des poids $\weight$. On pourrait très bien résoudre ce problème d'optimisation avec des algorithmes évolutionnistes par exemple. En fait, l'intérêt de la méthode de la rétropropagation du gradient (et ce qui explique sa notoriété) est qu'elle formule le problème d'optimisation des poids avec une écriture analytique particulièrement efficace qui élimine astucieusement un grand nombre de calculs redondants (un peu à la manière de ce qui se fait en programmation dynamique): quand on decide d'optimiser les poids via une descente de gradient, certains termes (les signaux d'erreurs $\errsig$) apparaissent un grand nombre de fois dans l'écriture analytique complète du gradient. La méthode de la retropropagation du gradient fait en sorte que ces termes ne soient calculés qu'une seule fois. À noter qu'on aurrait très bien pu résoudre le problème avec une descente de gradient oú le gradient $\frac{\partial \errfunc}{\partial\wcur(\learnit)}$ serait calculé via une approximation numérique (méthode des différences finies par exemple) mais ce serait beaucoup plus lent et beaucoup moins efficace...

Principe: on modifie les poids à l'aide des signaux d'erreur $\errsig$.

$$ \wcur(\learnit + 1) = \wcur(\learnit) \underbrace{- \learnrate \frac{\partial \errfunc}{\partial \wcur(\learnit)}}_{\delta_\prevcur} $$$$ \begin{align} \delta_\prevcur & = - \learnrate \frac{\partial \errfunc}{\partial \wcur(\learnit)} \\ & = - \learnrate \errsig_\cur \sigout\prev \end{align} $$
  • Dans le cas de l'apprentissage différé (batch), on calcule pour chaque exemple l'erreur correspondante. Leur contribution individuelle aux modifications des poids sont additionnées
  • L'apprentissage suppervisé fonctionne mieux avec des neurones de sortie linéaires (fonction d'activation $\activfunc$ = fonction identitée) "car les signaux d'erreurs se transmettent mieux".
  • Des données d'entrée binaires doivent être choisies dans $\{-1,1\}$ plutôt que $\{0,1\}$ car un signal nul ne contribu pas à l'apprentissage.

Voc:

  • erreur marginale: TODO

Signaux d'erreur $\errsig_\cur$ pour les neurones de sortie $(\cur \in \Omega)$

$$ \errsig_\cur = \activfunc'(\pot_\cur)[\sigout_\cur - \sigoutdes_\cur] $$

Signaux d'erreur $\errsig_\cur$ pour les neurones cachés $(\cur \not\in \Omega)$

$$ \errsig_\cur = \activfunc'(\pot_\cur) \sum_\next \weight_\curnext \errsig_\next $$

Plus de détail : calcul de $\errsig_\cur$

Dans l'exemple suivant on ne s'intéresse qu'aux poids $\weight_1$, $\weight_2$, $\weight_3$, $\weight_4$ et $\weight_5$ pour simplifier la demonstration.

Attention: $\weight_1$ influe $\pot_2$ et $\pot_3$ en plus de $\pot_1$ et $\pot_o$.

Calcul de $\frac{\partial \errfunc}{\partial \weight_4}$

rappel:

$$ \begin{align} \errfunc &= \frac12 \left( \sigout_o - \sigoutdes_o \right)^2 \tag{1} \\ \sigout_o &= \activfunc(\pot_o) \tag{2} \\ \pot_o &= \sigout_2 \weight_4 + \sigout_3 \weight_5 \tag{3} \\ \end{align} $$

c'est à dire:

$$ \errfunc = \frac12 \left( \activfunc \left( \sigout_2 \weight_4 + \sigout_3 \weight_5 \right) - \sigoutdes_o \right)^2 $$

donc, en appliquant les règles de derivation de fonctions composées, on a:

$$ \frac{\partial \errfunc}{\partial \weight_4} = \frac{\partial \pot_o}{\partial \weight_4} \underbrace{ \frac{\partial \sigout_o}{\partial \pot_o} \frac{\partial \errfunc}{\partial \sigout_o} }_{\errsig_o} $$

de (1), (2) et (3) on déduit:

$$ \begin{align} \frac{\partial \pot_o}{\partial \weight_4} &= \sigout_2 \\ \frac{\partial \sigout_o}{\partial \pot_o} &= \activfunc'(\pot_o) \\ \frac{\partial \errfunc}{\partial \sigout_o} &= \sigout_o - \sigoutdes_o \\ \end{align} $$

le signal d'erreur s'écrit donc:

$$ \begin{align} \errsig_o &= \frac{\partial \sigout_o}{\partial \pot_o} \frac{\partial \errfunc}{\partial \sigout_o} \\ &= \activfunc'(\pot_o) [\sigout_o - \sigoutdes_o] \end{align} $$

Calcul de $\frac{\partial \errfunc}{\partial \weight_5}$

$$ \frac{\partial \errfunc}{\partial \weight_5} = \frac{\partial \pot_o}{\partial \weight_5} \underbrace{ \frac{\partial \sigout_o}{\partial \pot_o} \frac{\partial \errfunc}{\partial \sigout_o} }_{\errsig_o} $$

avec:

$$ \begin{align} \frac{\partial \pot_o}{\partial \weight_5} &= \sigout_3 \\ \frac{\partial \sigout_o}{\partial \pot_o} &= \activfunc'(\pot_o) \\ \frac{\partial \errfunc}{\partial \sigout_o} &= \sigout_o - \sigoutdes_o \\ \errsig_o &= \frac{\partial \sigout_o}{\partial \pot_o} \frac{\partial \errfunc}{\partial \sigout_o} \\ &= \activfunc'(\pot_o) [\sigout_o - \sigoutdes_o] \end{align} $$

Calcul de $\frac{\partial \errfunc}{\partial \weight_2}$

$$ \frac{\partial \errfunc}{\partial \weight_2} = \frac{\partial \pot_2}{\partial \weight_2} % \underbrace{ \frac{\partial \sigout_2}{\partial \pot_2} \frac{\partial \pot_o}{\partial \sigout_2} \underbrace{ \frac{\partial \sigout_o}{\partial \pot_o} \frac{\partial \errfunc}{\partial \sigout_o} }_{\errsig_o} }_{\errsig_2} $$

avec:

$$ \begin{align} \frac{\partial \pot_2}{\partial \weight_2} &= \sigout_1 \\ \frac{\partial \sigout_2}{\partial \pot_2} &= \activfunc'(\pot_2) \\ \frac{\partial \pot_o}{\partial \sigout_2} &= \weight_4 \\ \errsig_2 &= \frac{\partial \sigout_2}{\partial \pot_2} \frac{\partial \pot_o}{\partial \sigout_2} \errsig_o \\ &= \activfunc'(\pot_2) \weight_4 \errsig_o \end{align} $$

Calcul de $\frac{\partial \errfunc}{\partial \weight_3}$

$$ \frac{\partial \errfunc}{\partial \weight_3} = \frac{\partial \pot_3}{\partial \weight_3} % \underbrace{ \frac{\partial \sigout_3}{\partial \pot_3} \frac{\partial \pot_o}{\partial \sigout_3} \underbrace{ \frac{\partial \sigout_o}{\partial \pot_o} \frac{\partial \errfunc}{\partial \sigout_o} }_{\errsig_o} }_{\errsig_3} $$

avec:

$$ \begin{align} \frac{\partial \pot_3}{\partial \weight_3} &= \sigout_1 \\ \frac{\partial \sigout_3}{\partial \pot_3} &= \activfunc'(\pot_3) \\ \frac{\partial \pot_o}{\partial \sigout_3} &= \weight_5 \\ \errsig_3 &= \frac{\partial \sigout_3}{\partial \pot_3} \frac{\partial \pot_o}{\partial \sigout_3} \errsig_o \\ &= \activfunc'(\pot_3) \weight_5 \errsig_o \end{align} $$

Calcul de $\frac{\partial \errfunc}{\partial \weight_1}$

$$ \frac{\partial \errfunc}{\partial \weight_1} = \frac{\partial \pot_1}{\partial \weight_1} % \underbrace{ \frac{\partial \sigout_1}{\partial \pot_1} \left( \frac{\partial \pot_2}{\partial \sigout_1} % err? \underbrace{ \frac{\partial \sigout_2}{\partial \pot_2} \frac{\partial \pot_o}{\partial \sigout_2} \underbrace{ \frac{\partial \sigout_o}{\partial \pot_o} \frac{\partial \errfunc}{\partial \sigout_o} }_{\errsig_o} }_{\errsig_2} + \frac{\partial \pot_3}{\partial \sigout_1} % err? \underbrace{ \frac{\partial \sigout_3}{\partial \pot_3} \frac{\partial \pot_o}{\partial \sigout_3} \underbrace{ \frac{\partial \sigout_o}{\partial \pot_o} \frac{\partial \errfunc}{\partial \sigout_o} }_{\errsig_o} }_{\errsig_3} \right) }_{\errsig_1} $$

avec:

$$ \begin{align} \frac{\partial \pot_1}{\partial \weight_1} &= \sigout_i \\ \frac{\partial \sigout_1}{\partial \pot_1} &= \activfunc'(\pot_1) \\ \frac{\partial \pot_2}{\partial \sigout_1} &= \weight_2 \\ \frac{\partial \pot_3}{\partial \sigout_1} &= \weight_3 \\ \errsig_1 &= \frac{\partial \sigout_1}{\partial \pot_1} \left( \frac{\partial \pot_2}{\partial \sigout_1} \errsig_2 + \frac{\partial \pot_3}{\partial \sigout_1} \errsig_3 \right) \\ &= \activfunc'(\pot_1) \left( \weight_2 \errsig_2 + \weight_3 \errsig_3 \right) \end{align} $$

Fonctions d'activation : fonctions sigmoides (en forme de "S")

La fonction sigmoïde (en forme de "S") est définie par :

$$f(x) = \frac{1}{1 + e^{-x}}$$

pour tout réel $x$.

On peut la généraliser à toute fonction dont l'expression est :

$$f(x) = \frac{1}{1 + e^{-\lambda x}}$$
In [7]:
def sigmoid(x, _lambda=1.):
    y = 1. / (1. + np.exp(-_lambda * x))
    return y

Fonction dérivée :

$$ f'(x) = \frac{\lambda e^{-\lambda x}}{(1+e^{-\lambda x})^{2}} $$

qui peut aussi être défini par

$$ \frac{\mathrm{d} y}{\mathrm{d} x} = \lambda y (1-y) $$

où $y$ varie de 0 à 1.

In [9]:
def d_sigmoid(x, _lambda=1.):
    e = np.exp(-_lambda * x)
    y = _lambda * e / np.power(1 + e, 2)
    return y

Tangente hyperbolique

In [11]:
def tanh(x):
    y = np.tanh(x)
    return y

Dérivée :

$$ \tanh '= \frac{1}{\cosh^{2}} = 1-\tanh^{2} $$
In [13]:
def d_tanh(x):
    y = 1. - np.power(np.tanh(x), 2)
    return y

Fonction logistique

Fonctions ayant pour expression

$$ f(t) = K \frac{1}{1+ae^{-rt}} $$

où $K$ et $r$ sont des réels positifs et $a$ un réel quelconque.

Les fonctions sigmoïdes sont un cas particulier de fonctions logistique avec $a > 0$.

Python implementation

In [15]:
# Define the activation function and its derivative
activation_function = tanh
d_activation_function = d_tanh
In [16]:
def init_weights(num_input_cells, num_output_cells, num_cell_per_hidden_layer, num_hidden_layers=1):
    """
    The returned `weights` object is a list of weight matrices,
    where weight matrix at index $i$ represents the weights between
    layer $i$ and layer $i+1$.
    
    Numpy array shapes for e.g. num_input_cells=2, num_output_cells=2,
    num_cell_per_hidden_layer=3 (without taking account bias):
    - in:        (2,)
    - in+bias:   (3,)
    - w[0]:      (3,3)
    - w[0]+bias: (3,4)
    - w[1]:      (3,2)
    - w[1]+bias: (4,2)
    - out:       (2,)
    """
    
    # TODO:
    # - faut-il que wij soit positif ?
    # - loi normale plus appropriée que loi uniforme ?
    # - quel sigma conseillé ?
    
    W = []
    
    # Weights between the input layer and the first hidden layer
    W.append(np.random.uniform(low=0., high=1., size=(num_input_cells + 1, num_cell_per_hidden_layer + 1)))
    
    # Weights between hidden layers (if there are more than one hidden layer)
    for layer in range(num_hidden_layers - 1):
        W.append(np.random.uniform(low=0., high=1., size=(num_cell_per_hidden_layer + 1, num_cell_per_hidden_layer + 1)))
    
    # Weights between the last hidden layer and the output layer
    W.append(np.random.uniform(low=0., high=1., size=(num_cell_per_hidden_layer + 1, num_output_cells)))
    
    return W
In [17]:
def evaluate_network(weights, input_signal):      # TODO: find a better name
    
    # Add the bias on the input layer
    input_signal = np.concatenate([input_signal, [-1]])
    
    assert input_signal.ndim == 1
    assert input_signal.shape[0] == weights[0].shape[0]
    
    # Compute the output of the first hidden layer
    p = np.dot(input_signal, weights[0])
    output_hidden_layer = activation_function(p)
    
    # Compute the output of the intermediate hidden layers
    # TODO: check this
    num_layers = len(weights)
    for n in range(num_layers - 2):
        p = np.dot(output_hidden_layer, weights[n + 1])
        output_hidden_layer = activation_function(p)
    
    # Compute the output of the output layer
    p = np.dot(output_hidden_layer, weights[-1])
    output_signal = activation_function(p)
    
    return output_signal
In [18]:
def compute_gradient():
    # TODO
    pass
In [19]:
weights = init_weights(num_input_cells=2, num_output_cells=2, num_cell_per_hidden_layer=3, num_hidden_layers=1)
print(weights)
#print(weights[0].shape)
#print(weights[1].shape)
[array([[ 0.8168422 ,  0.20315285,  0.081432  ,  0.82639322],
       [ 0.69934567,  0.05970217,  0.28393276,  0.20037798],
       [ 0.9480069 ,  0.51893179,  0.05693284,  0.45467443]]), array([[ 0.37976881,  0.60952467],
       [ 0.9091477 ,  0.67505368],
       [ 0.9193108 ,  0.80397798],
       [ 0.10581638,  0.53331891]])]
In [20]:
input_signal = np.array([.1, .2])
input_signal
Out[20]:
array([ 0.1,  0.2])
In [21]:
evaluate_network(weights, input_signal)
Out[21]:
array([-0.58687255, -0.68984899])

Notes de la documentation sklearn

  • features : les données d'entrée du réseau (i.e. les entrées de la 1ere couche du réseau)
    • "nombre de features" = taille du vecteur d'entrées
  • loss function: fonction objectif (ou fonction d'erreur)
  • fitting: processus d'apprentissage (training)
  • sample: exemple

Les biais sont stockés dans une liste de vecteurs plutôt qu'une liste de scalaires... pourquoi ???

Avantages des PMC:

  • capables d'apprendre des modèles non linéaires
  • capables d'apprendre des modèles en temps réel (apprentissage on-line)

Inconvenients des PMC:

  • les PMC avec une ou plusieurs couches cachées ont une fonction objectif non-convexe avec des minimas locaux. Par conséquent, le résultat du processus d'apprentissage peut varier d'une execution à l'autre suivant la valeur des poids initiaux et l'obtention d'un réseau optimal n'est pas garanti
  • pour obtenir un résultat satisfaisant, il est souvant nécessaire de régler (plus ou moins empiriquement) de nombreux meta-paramètres (nombres de couches cachées, nombre de neurones sur les couches cachées, nombres d'itérations, ...)
  • une mauvaise normalisation des données d'entrée a un impact très négatif sur la qualité du résultat ("mal conditionné" ???)

Cross-Entropy Loss Function: ...

Softmax: ...

Multi-label classification: ... modèle de classifieur qui permet a un exemple d'appartenir à plusieurs classes