Introduction#

Dans ce chapitre d’introduction, nous allons présenter un premier réseau neuronal appelé le Perceptron. Ce modèle est un réseau neuronal constitué d’un seul neurone, et nous l’utiliserons ici pour introduire des concepts-clés que nous détaillerons plus tard dans le cours.

Hide code cell content

%config InlineBackend.figure_format = 'svg'
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
from notebook_utils import prepare_notebook_graphics
prepare_notebook_graphics()

Un premier modèle : le perceptron#

Dans la terminologie des réseaux de neurones, un neurone est une fonction paramétrée qui prend un vecteur \(\mathbf{x}\) en entrée et sort une valeur unique \(a\) comme suit :

\[ a = \varphi(\underbrace{\mathbf{w} \mathbf{x} + b}_{o}) , \]

où les paramètres du neurone sont ses poids stockés dans \(\mathbf{w}\). et un terme de biais \(b\), et \(\varphi\) est une fonction d’activation qui est choisie a priori (nous y reviendrons plus en détail plus tard dans le cours) :

Figure made with TikZ

Un modèle constitué d’un seul neurone est appelé perceptron.

Optimisation#

Les modèles présentés dans ce document ont pour but de résoudre des problèmes de prédiction dans lesquels l’objectif est de trouver des valeurs de paramètres « suffisamment bonnes » pour le modèle en jeu compte tenu de données observées.

Le problème de la recherche de telles valeurs de paramètres est appelé optimisation. L’apprentissage profond (ou deep learning) fait un usage intensif d’une famille spécifique de stratégies d’optimisation appelée descente gradiente.

Descente de gradient#

Pour se faire une idée de la descente de gradient, supposons que l’on nous donne le jeu de données suivant sur les prix de l’immobilier :

import pandas as pd

boston = pd.read_csv("../data/boston.csv")[["RM", "PRICE"]]
boston
RM PRICE
0 6.575 24.0
1 6.421 21.6
2 7.185 34.7
3 6.998 33.4
4 7.147 36.2
... ... ...
501 6.593 22.4
502 6.120 20.6
503 6.976 23.9
504 6.794 22.0
505 6.030 11.9

506 rows × 2 columns

Dans notre cas, nous essaierons (pour commencer) de prédire la valeur cible "PRICE" de ce jeu de données, qui est la valeur médiane des maisons occupées par leur propriétaire en milliers de dollars en fonction du nombre moyen de pièces par logement "RM" :

sns.scatterplot(data=boston, x="RM", y="PRICE");
../../_images/0b2f4614a6a4c6340d8245d7ab1812e1f98f366e8bf3967a0d3b8a9e2a8307d8.svg

Supposons que nous ayons une approche naïve dans laquelle notre modèle de prédiction est linéaire sans biais, c’est-à-dire que pour une entrée donnée \(x_i\) la sortie prédite est calculée comme suit :

\[ \hat{y_i} = w x_i \]

\(w\) est le seul paramètre de notre modèle.

Supposons en outre que la quantité que nous cherchons à minimiser (notre objectif, également appelé fonction de perte) est :

\[ \mathcal{L}(w) = \sum_i \left(\hat{y_i} - y_i\right)^2 \]

\(y_i\) est la valeur cible associée au \(i\)-ème échantillon de jeu de données.

Examinons cette quantité en fonction de \(w\) :

import numpy as np

def loss(w, x, y):
    w = np.array(w)
    return np.sum(
        (w[:, None] * x.to_numpy()[None, :] - y.to_numpy()[None, :]) ** 2,
        axis=1
    )

w = np.linspace(-2, 10, num=100)

x = boston["RM"]
y = boston["PRICE"]
plt.plot(w, loss(w, x, y), "r-");
../../_images/0b51074e8926174f1d652d21cb600e209da2919fd467b5f8c2ef28b2478b80df.svg

Ici, il semble qu’une valeur de \(w\) autour de 4 devrait être un bon choix. Cette méthode (générer de nombreuses valeurs pour le paramètre et calculer la perte pour chaque valeur) ne peut pas s’adapter aux modèles qui ont beaucoup de paramètres, donc nous allons donc essayer autre chose.

Supposons que nous ayons accès, à chaque fois que nous choisissons une valeur candidate pour \(w\), à la fois à la perte \(\mathcal{L}\) et aux informations sur la façon dont \(\mathcal{L}\) varie, localement. Nous pourrions, dans ce cas, calculer une nouvelle valeur candidate pour \(w\) en nous déplaçant à partir de la valeur candidate précédente dans la direction de la descente la plus raide. C’est l’idée de base de l’algorithme de descente du gradient qui, à partir d’un candidat initial \(w_0\), calcule itérativement de nouveaux candidats comme :

\[ w_{t+1} = w_t - \rho \left. \frac{\partial \mathcal{L}}{\partial w} \right|_{w=w_t} \]

\(\rho\) est un hyper-paramètre (appelé taux d’apprentissage) qui contrôle la taille des pas à effectuer, et \(\left. \frac{\partial \mathcal{L}}{\partial w} \right|_{w=w_t}\) est le gradient de \(\mathcal{L}\) par rapport à \(w\), évalué en \(w=w_t\). Comme vous pouvez le voir, la direction de la descente la plus raide est l’opposé de la direction indiquée par le gradient (et cela vaut aussi pour les paramètres vectoriels).

Ce processus est répété jusqu’à la convergence, comme l’illustre la figure suivante :

rho = 1e-5

def grad_loss(w_t, x, y):
    return np.sum(
        2 * (w_t * x - y) * x
    )


ww = np.linspace(-2, 10, num=100)
plt.plot(ww, loss(ww, x, y), "r-", alpha=.5);

w = [0.]
for t in range(10):
    w_update = w[t] - rho * grad_loss(w[t], x, y)
    w.append(w_update)

plt.plot(w, loss(w, x, y), "ko-")
plt.text(x=w[0]+.1, y=loss([w[0]], x, y), s="$w_{0}$")
plt.text(x=w[10]+.1, y=loss([w[10]], x, y), s="$w_{10}$");
../../_images/338d6b0bcd2f2caf497cd94113c1b79901b5288b75ef9de6fff0ee9b5dbf939a.svg

Qu’obtiendrions-nous si nous utilisions un taux d’apprentissage plus faible ?

rho = 1e-6

ww = np.linspace(-2, 10, num=100)
plt.plot(ww, loss(ww, x, y), "r-", alpha=.5);

w = [0.]
for t in range(10):
    w_update = w[t] - rho * grad_loss(w[t], x, y)
    w.append(w_update)

plt.plot(w, loss(w, x, y), "ko-")
plt.text(x=w[0]+.1, y=loss([w[0]], x, y), s="$w_{0}$")
plt.text(x=w[10]+.1, y=loss([w[10]], x, y), s="$w_{10}$");
../../_images/02611a7b6446426cfaabbea94c7a0693cda29da9457cc25f918926360d53e9d8.svg

Cela prendrait certainement plus de temps pour converger. Mais attention, un taux d’apprentissage plus élevé n’est pas toujours une bonne idée :

rho = 5e-5

ww = np.linspace(-2, 10, num=100)
plt.plot(ww, loss(ww, x, y), "r-", alpha=.5);

w = [0.]
for t in range(10):
    w_update = w[t] - rho * grad_loss(w[t], x, y)
    w.append(w_update)

plt.plot(w, loss(w, x, y), "ko-")
plt.text(x=w[0]-1., y=loss([w[0]], x, y), s="$w_{0}$")
plt.text(x=w[10]-1., y=loss([w[10]], x, y), s="$w_{10}$");
../../_images/101adcf305137fdad959ebcb80e30cb0cd195996ed6034b37f54857b0929c327.svg

Vous voyez comment nous divergeons lentement parce que nos pas sont trop grands ?

Récapitulatif#

Dans cette section, nous avons introduit :