Apprentissage des réseaux de neurones : principes fondamentaux et implémentation avec Python

L'apprentissage dans un réseau de neurones désigne le processus automatisé permettant d'ajuster les paramètres de poids optimaux à partir d'un jeu de données d'entraînement. L'objectif principal est de minimiser une fonction de perte, qui mesure l'écart entre les prédictions du modèle et la réalité.

L'apprentissage basé sur les données

Contrairement aux approches de programmation classique, l'apprentissage automatique (Machine Learning) cherche à extraire des motifs directement des données. En vision par ordinateur, on extrayait autrefois manuellement des vecteurs de caractéristiques (features) avant de les fournir à un algorithme. Le Deep Learning se distingue par une approche "bout en bout" (end-to-end), où le réseau apprend lui-même les caractéristiques pertinentes directement à partir des pixels bruts.

Pour évaluer la performance d'un modèle, on divise les données en deux ensembles :

  • Données d'entraînement : utilisées pour ajuster les poids du modèle.
  • Données de test : utilisées pour évaluer la capacité de généralisation sur des données inconnues.

Un modèle qui réussit parfaitement sur l'entraînement mais échoue sur le test est en état de surapprentissage (overfitting).

Fonctions de perte

La fonction de perte quantifie l'imprécision du réseau. Elle sert de boussole pour orienter l'ajustement des paramètres.

Erreur Quadratique Moyenne (MSE)

Souvent utilisée pour les problèmes de régression, elle se calcule ainsi :

\[ E = \frac{1}{2}\sum_k(y_k - t_k)^2 \]

import numpy as np

def compute_mse(prediction, target):
    return 0.5 * np.sum((prediction - target) ** 2)

Entropie croisée (Cross-Entropy)

Idéale pour la classification, elle pénalise fortement les prédictions erronées avec une certitude élevée :

\[ E = -\sum_k{t_k\ \log y_k} \]

def compute_cross_entropy(y_pred, y_true):
    epsilon = 1e-8
    return -np.sum(y_true * np.log(y_pred + epsilon))

Apprentissage par mini-batch

Calculer la perte sur l'intégralité d'un dataset massif est coûteux en ressources. On utilise donc des mini-batchs : un sous-ensemble aléatoire de données qui sert d'approximation pour calculer le gradient et mettre à jour les poids.

def batch_cross_entropy(preds, labels):
    if preds.ndim == 1:
        labels = labels.reshape(1, labels.size)
        preds = preds.reshape(1, preds.size)
        
    batch_count = preds.shape[0]
    # Gestion des étiquettes au format index ou one-hot
    if labels.size == preds.size:
        return -np.sum(labels * np.log(preds + 1e-7)) / batch_count
    else:
        return -np.sum(np.log(preds[np.arange(batch_count), labels] + 1e-7)) / batch_count

Calcul du gradient et descente de gradient

Le gradient représente la direction dans laquelle la fonction de perte augmente le plus rapidement. Pour minimiser la perte, nous nous déplaçons dans la direction opposée au gradient.

Différenciation numérique

Pour calculer la dérivée d'une fonction numériquement, on utilise la méthode de la différence centrale pour plus de précision :

def estimate_gradient(func, params):
    delta = 1e-4
    grad = np.zeros_like(params)
    
    it = np.nditer(params, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        original_val = params[idx]
        
        params[idx] = original_val + delta
        upper_val = func(params)
        
        params[idx] = original_val - delta
        lower_val = func(params)
        
        grad[idx] = (upper_val - lower_val) / (2 * delta)
        params[idx] = original_val 
        it.iternext()
        
    return grad

Optimisation par descente de gradient

La mise à jour des paramètres se fait selon la formule suivante, où \(\eta\) est le taux d'apprentissage (learning rate) :

\[ W = W - \eta \frac{\partial L}{\partial W} \]

def optimize_gradient(f, initial_params, lr=0.01, epochs=100):
    p = initial_params
    for _ in range(epochs):
        g = estimate_gradient(f, p)
        p -= lr * g
    return p

Implémentation d'un réseau à deux couches

Voici une structure simplifiée d'un réseau de neurones complet intégrant ces concepts.

from common.functions import sigmoid, softmax

class SimpleNeuralNet:
    def __init__(self, input_dim, hidden_dim, output_dim, weight_scale=0.01):
        self.params = {
            'W1': weight_scale * np.random.randn(input_dim, hidden_dim),
            'b1': np.zeros(hidden_dim),
            'W2': weight_scale * np.random.randn(hidden_dim, output_dim),
            'b2': np.zeros(output_dim)
        }

    def forward(self, x):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        
        layer1 = sigmoid(np.dot(x, W1) + b1)
        out = softmax(np.dot(layer1, W2) + b2)
        return out

    def get_loss(self, x, t):
        y = self.forward(x)
        return batch_cross_entropy(y, t)

    def calculate_grads(self, x, t):
        loss_wrapper = lambda W: self.get_loss(x, t)
        
        grads = {
            'W1': estimate_gradient(loss_wrapper, self.params['W1']),
            'b1': estimate_gradient(loss_wrapper, self.params['b1']),
            'W2': estimate_gradient(loss_wrapper, self.params['W2']),
            'b2': estimate_gradient(loss_wrapper, self.params['b2'])
        }
        return grads

Le cycle d'apprantissage suit une boucle répétitive : sélection d'un mini-batch, calcul du gradient de la perte par rapport aux paramètres, et mise à jour des poids via la descente de gradient. Ce processus est répété sur plusieurs époques jusqu'à ce que la performance sur les données de test soit jugée satifsaisante.

Étiquettes: deep learning Python réseaux de neurones NumPy Gradient Descent

Publié le 19 juin à 18h55