\( \newcommand{\R}{\mathbb{R}} \newcommand{\Rp}{\mathbb{R_+}} \newcommand{\Z}{\mathbb{Z}} \newcommand{\Rn}{\mathbb{R}^n} \newcommand{\N}{\mathbb{N}} \renewcommand{\to}{\rightarrow} \newcommand{\from}{\colon} \renewcommand{\Vec}{\mathsf{vec}} \newcommand{\Ker}{\mathrm{Ker}} \renewcommand{\Im}{\mathrm{Im}} \newcommand{\rang}{\mathrm{rang}} \newcommand{\O}{\mathcal{O}} \newcommand{\C}{\mathcal{C}} \newcommand{\U}{\mathcal{U}} \newcommand{\x}{\mathbf{x}} \newcommand{\X}{\mathbf{X}} \newcommand{\z}{\mathbf{z}} \newcommand{\w}{\mathbf{w}} \newcommand{\W}{\mathbf{W}} \newcommand{\a}{\mathbf{a}} \newcommand{\b}{\mathbf{b}} \newcommand{\B}{\mathbf{B}} \newcommand{\dpartial}[2]{{\partial #1 \over \partial #2}} \newcommand{\d}{\mathbf{d}} \)

TP 6 : Backpropagation

Programmation pour l’IA

L’objectif de cet exercice est d’implémenter une petite classe pour faire des réseaux de neurones par couche et de les entraîner en utilisant l’algorithme de backpropagation. On réutilise la classe Derivable du TP précédent :

import numpy as np

class Derivable:
    def __init__(self, f, df):
        self.f = f
        self.d = df
    def __call__(self, x):
        return self.f(x)

On définit aussi :

Sigmoid = Derivable(lambda x: 1 / (1+np.exp(-x)), lambda x: np.exp(-x)/(1+np.exp(-x))**2)
ReLU = Derivable(lambda x: np.maximum(0,x), lambda x: (x>0)*1.0)
  1. On veut définir une classe permettant de gérer les réseaux de neurones. On veut pouvoir initialiser un réseau de neurones en ne donnant qu’une taille d’entrée et une liste décrivant chaque couche par sa taille et sa fonction d’activation (de type Derivable). Par exemple, on pourra créer le réseau utiliser comme exemple dans les slides par R = NeuralNet(3, [(4,ReLU), (3,Sigmoid), (5,ReLU), (1, Sigmoid)]). On pourra évaluer sa valeur en fait R(np.array([1,2,3])).

Compléter le code suivant pour implémenter le constructeur et la méthode __call__.

class NeuralNet:
    def __init__(self, insize : int, layers : list[tuple[int, Derivable]]):
        self.W = []
        self.activation = []
        self.b = []
        # initialisation des poids (uniformément entre 0 et 1) avec les bonnes tailles.
        # initialisation des biais Ă  0.
        
    def __call__(self,X):
        ...
  1. Même si techniquement on peut le faire pour n’importe quelle erreur, on va se concentrer sur l’erreur quadratique. On veut implémenter une méthode def grad(self,x,y) qui calcule le gradient de l’erreur quadratique sur un exemple \((\x,y)\), pour les poids self.W actuels. On va appliquer la formule donnée dans les slides. La fonction devra retourner (DW,DB) où DW est un tableau numpy tel que DW[i] est la matrice \(\Delta W^{i}\) et DB[i] est \(\Delta \b^i\) vue dans les slides. Dans le cas de l’erreur quadratique, on rappelle que \(\dpartial{e}{a^L}(a^L,y)\) vaut \(2(a^L - y)\).

  2. Définir une méthode train(self,X,Y,step,epoch) qui entraîne le réseau de neurones par descente de gradient stochastique sur les données X,Y où step est le pas de gradiant et epoch le nombre d’epochs (remarque : vu la question précédente, on se place dans le cadre d’une descente de gradient stochastique avec des mini-batches de taille 1).

  3. Définir un réseau de neurones avec une couche d’entrée de taille \(2\), une couche cachée ReLU de taille \(3\) et une couche sigmoïde de sortie de taille \(1\). Entraîner votre réseau sur 80% des données du jeu mystery.csv (déjà étudiée au TP 4). Quel erreur de test obtenez-vous à la fin ?

Refaire la question précédente avec Pytorch : on utilisera une architecture avec une couche cachée ayant 3 sigmoïdes et un biais et une sigmoïde sans biais pour la sortie. Implémentez une descente de gradient avec une erreur log-vraissemblance (\(y\log f(\x) +(1-y)\log(1-f(\x))\).