Retour à l'accueil

Descente de gradient et rétropropagation dans les réseaux de neurones : CUDA/C++

Plongée approfondie dans la descente de gradient et la rétropropagation. Implémentation d'algorithmes d'entraînement de réseaux de neurones à partir de zéro sur CUDA/C++ pour MNIST. Décomposition détaillée des mathématiques et du code.

Descente de gradient et rétropropagation : Entraînement de réseaux de neurones sur CUDA/C++ à partir de zéro
Advertisement 728x90

Descente de Gradient et Rétropropagation : Entraînement de Réseaux Neuronaux de Zéro avec CUDA/C++

Cet article est la quatrième partie d'une exploration approfondie du monde des réseaux de neurones, où nous construisons une architecture Transformer étape par étape, en partant des composants les plus fondamentaux en CUDA. Dans cet épisode, nous passons de l'implémentation de la propagation avant (forward pass) à l'étape cruciale de l'entraînement du modèle. Nous explorerons les mécanismes fondamentaux de la Descente de Gradient et de la Rétropropagation, en les implémentant à bas niveau avec C++ et CUDA pour entraîner notre réseau de neurones à reconnaître des chiffres manuscrits de l'ensemble de données MNIST. L'objectif n'est pas seulement d'utiliser des bibliothèques prêtes à l'emploi, mais de comprendre en profondeur comment ces processus computationnels complexes fonctionnent "sous le capot", ce qui est essentiel pour une compréhension approfondie des modèles modernes d'apprentissage automatique et d'intelligence artificielle.

Fondamentaux de l'Entraînement des Réseaux de Neurones : La Descente de Gradient

Après avoir implémenté avec succès la propagation avant dans la partie précédente, notre modèle est capable de faire des prédictions. Cependant, au stade initial, sans entraînement, ces prédictions seront aléatoires. Pour que le modèle commence à produire des résultats corrects, ses paramètres internes (poids et biais) doivent être ajustés pour minimiser l'erreur entre les prédictions et les étiquettes réelles. Des algorithmes comme la Descente de Gradient et la Rétropropagation sont utilisés précisément à cette fin.

Imaginez que vous êtes au sommet d'une montagne dans un brouillard épais, et votre tâche est de descendre au point le plus bas de la vallée. Vous ne pouvez pas voir l'ensemble du paysage, mais vous pouvez sentir la pente sous vos pieds. Le gradient est un vecteur qui indique la direction de la pente la plus raide. Par conséquent, pour descendre, vous devez vous déplacer dans la direction opposée au gradient. Dans le contexte des réseaux de neurones, la "montagne" est la surface de la fonction d'erreur (ou fonction de perte), et la "vallée" est son minimum global. L'objectif de la descente de gradient est de trouver ce minimum en ajustant itérativement les poids du réseau dans la direction opposée au gradient de la fonction de perte par rapport à ces poids.

Google AdInline article slot

La Rétropropagation est un algorithme qui calcule efficacement ces gradients. Elle fonctionne en propageant l'erreur de la couche de sortie du réseau vers la couche d'entrée, distribuant la responsabilité de l'erreur parmi tous les poids. Mathématiquement, cela est implémenté via la Règle de la Chaîne de dérivation, ce qui nous permet de calculer comment un changement dans chaque poids affecte l'erreur globale du modèle.

Fonction de Perte : Entropie Croisée

Pour mesurer l'inexactitude des prédictions du modèle, une fonction de perte est utilisée. Dans les tâches de classification, telles que la reconnaissance de chiffres manuscrits MNIST, la perte d'entropie croisée est un choix standard. Elle quantifie la divergence entre la distribution de probabilité prédite par le modèle et la distribution réelle (où seule la classe correcte a une probabilité de 1, et les autres sont à 0). Plus cette divergence est grande, plus la valeur de la fonction de perte est élevée.

Pour un seul exemple, l'entropie croisée est calculée à l'aide de la formule :

Google AdInline article slot

![L = -\sum_{i=1}^{K} y_i \log(\hat{y}_i)](./images/image-1.svg)

Où :

  • ![K](./images/image-2.svg) — est le nombre de classes (pour MNIST, c'est 10).
  • ![y_i](./images/image-3.svg) — est la valeur réelle (1 pour la classe correcte, 0 pour les autres).
  • ![

\hat{y}_i](./images/image-4.svg) — est la prédiction du modèle (probabilité obtenue après la couche Softmax).

Google AdInline article slot

Étant donné que l'entraînement se fait généralement sur des lots de données, et non sur des exemples individuels, la fonction de perte totale pour un lot est moyennée sur sa taille (![N](./images/image-5.svg) — taille du lot) :

![L = -\frac{1}{N} \sum_{j=1}^{N} \sum_{i=1}^{K} y_{j,i} \log(\hat{y}_{j,i})](./images/image-6.svg)

Voici le code C++ pour le calcul de l'entropie croisée sur le CPU, ce qui simplifie le débogage et la compréhension :

float cross_entropy_loss_cpu(
  const Tensor& predictions, const std::vector<int>& labels
) {
  int batch_size = predictions.shape()[0];
  int num_classes = predictions.shape()[1];
  float loss = 0.0f;
  for (int i = 0; i < batch_size; i++) {
    for (int j = 0; j < num_classes; j++) {
      float prob = predictions.get({i, j});
      float target = (labels[i] == j) ? 1.0f : 0.0f;
      loss += -std::log(prob) * target;
    }
  }
  return loss / batch_size;
}

Rétropropagation et la Règle de la Chaîne

Pour minimiser la fonction de perte ![L](./images/image-9.svg), nous devons savoir comment sa valeur change avec une petite modification de chaque poids dans le réseau. C'est le gradient de la fonction de perte par rapport aux poids. Par exemple, pour le poids ![W_1](./images/image-10.svg) de la première couche, nous devons trouver ![

\frac{\partial L}{\partial W_1}](./images/image-8.svg).

L'architecture de notre réseau, développée dans les articles précédents, se présente comme suit :

  • Première Couche Linéaire : ![z_1 = W_1 \cdot x + b_1](./images/image-11.svg)
  • Activation (ReLU) : ![a_1 = \sigma(z_1)](./images/image-12.svg)
  • Deuxième Couche Linéaire : ![z_2 = W_2 \cdot a_1 + b_2](./images/image-13.svg)
  • Sortie (Softmax + Perte) : ![L = Loss(Softmax(z_2))](./images/image-14.svg)

Pour calculer le gradient ![

\frac{\partial L}{\partial W_1}](./images/image-16.svg), nous appliquons la Règle de la Chaîne :

![\frac{\partial L}{\partial W_1} = \frac{\partial L}{\partial z_2} \cdot \frac{\partial z_2}{\partial a_1} \cdot \frac{\partial a_1}{\partial z_1} \cdot \frac{\partial z_1}{\partial W_1}](./images/image-17.svg)

Chaque terme de cette chaîne représente un gradient local, qui montre comment la sortie d'une couche ou d'une fonction donnée affecte son entrée. La rétropropagation commence par le calcul du gradient de la fonction de perte par rapport aux sorties de la dernière couche, puis propage séquentiellement ce gradient en arrière à travers toutes les couches du réseau.

Dérivation Détaillée des Gradients pour Softmax et Entropie Croisée

Commençons par la toute fin de la chaîne – la dérivée de la fonction de perte ![L](./images/image-25.svg) par rapport aux sorties de la deuxième couche linéaire ![z_2](./images/image-20.svg) après l'application de Softmax. C'est l'un des gradients les plus importants et les plus fréquemment utilisés. Rappelons comment Softmax se présente pour chaque élément ![i](./images/image-19.svg) dans le vecteur ![z_2](./images/image-20.svg) :

![a_{2, i} = \frac{e^{z_i}}{\sum_{j=1}^{K} e^{z_j}}](./images/image-21.svg)

Où ![z_2](./images/image-22.svg) est la sortie de la deuxième couche linéaire (logits), et ![a_2](./images/image-23.svg) est le résultat de Softmax (probabilités). Le gradient ![

\frac{\partial L}{\partial z_{2,i}}](./images/image-18.svg) peut être décomposé en utilisant la Règle de la Chaîne :

![\frac{\partial L}{\partial z_{2,i}} = \sum_{k=1}^{K} \frac{\partial L}{\partial a_{2,k}} \cdot \frac{\partial a_{2, k}}{\partial z_{2,i}}](./images/image-24.svg)

Tout d'abord, trouvons la dérivée de la fonction de perte ![L](./images/image-27.svg) par rapport à ![a_i](./images/image-26.svg) :

![\frac{\partial L}{\partial a_i} = \frac{\partial}{\partial a_i} \left( -y_i \ln(a_i) \right) = -y_i \cdot \frac{1}{a_i} = -\frac{y_i}{a_i}](./images/image-29.svg)

Passons maintenant à la dérivée de Softmax par rapport à ![z](./images/image-30.svg). Ici, la règle du quotient pour la différenciation sera nécessaire. La formule de Softmax : ![a_i = \frac{e^{z_i}}{\sum_{k=1}^K e^{z_k}}](./images/image-32.svg)

Si ![i=j](./images/image-43.svg), la dérivée est :

![\frac{\partial a_i}{\partial z_i} = a_i (1 - a_i)](./images/image-36.svg)

Si ![i \neq j](./images/image-37.svg), la dérivée est :

![\frac{\partial a_i}{\partial z_j} = -a_i a_j](./images/image-41.svg)

En utilisant le symbole delta de Kronecker ![

\delta_{ij}](./images/image-42.svg), ces deux cas peuvent être combinés :

![\frac{\partial a_i}{\partial z_j} = a_i (\delta_{ij} - a_j)](./images/image-44.svg)

En combinant les dérivées de l'entropie croisée et de Softmax, nous obtenons :

![\frac{\partial L}{\partial z_i} = \sum_{k=1}^{K} \left( -\frac{y_k}{a_k} \right) \cdot \left( a_k(\delta_{ki} - a_i) \right) = \sum_{k=1}^{K} -y_k (\delta_{ki} - a_i)](./images/image-49.svg)

Une simplification supplémentaire, considérant que la somme des probabilités réelles ![

\sum_{k=1}^{K} y_k](./images/image-51.svg) est toujours égale à 1, conduit à une formule étonnamment simple et élégante :

![\frac{\partial L}{\partial z_i} = a_i - y_i](./images/image-52.svg)

Cela signifie que le gradient de la fonction de perte par rapport aux logits (sorties avant Softmax) est simplement la différence entre les probabilités prédites et les étiquettes réelles. Ce résultat est une pierre angulaire pour l'implémentation de la rétropropagation dans les modèles de classification.

Implémentation du Gradient en C++

En appliquant la formule dérivée, nous pouvons implémenter le calcul du gradient pour la dernière couche de notre réseau de neurones. Ce gradient sera ensuite utilisé pour mettre à jour les poids, déplaçant le modèle vers une erreur plus petite. Voici à quoi cela ressemble dans le code C++ :

auto grad = Tensor::create_zeros({ batch_size, num_classes });

for (int i = 0; i < batch_size; ++i) {
  for (int j = 0; j < num_classes; ++j) {
    float prob = fc2.output->get({i, j});
    float target = (targets[i] == j) ? 1.0f : 0.0f;
    float gradient = (prob - target) / batch_size; // Division by batch_size for averaging
    grad->set({i, j}, gradient);
  }
}

Ce code calcule le gradient d'erreur pour chaque élément du lot, puis le moyenne sur la taille du lot. Le tenseur grad résultant contient des informations sur la quantité à modifier les logits ![z_2](./images/image-54.svg) pour réduire l'erreur. Ce gradient sera ensuite propagé à travers les couches précédentes du réseau, permettant aux poids ![W_2](./images/image-13.svg), ![b_2](./images/image-13.svg), ![W_1](./images/image-11.svg) et ![b_1](./images/image-11.svg) d'être ajustés en utilisant les dérivées locales correspondantes.

Points Clés :

  • La Descente de Gradient est une méthode d'optimisation itérative qui ajuste les paramètres du modèle dans la direction opposée au gradient de la fonction de perte pour trouver son minimum.
  • La Rétropropagation est un algorithme qui utilise la règle de la chaîne pour calculer efficacement les gradients de la fonction de perte par rapport à tous les poids d'un réseau de neurones.
  • L'Entropie Croisée est une fonction de perte standard pour les tâches de classification, mesurant la divergence entre les distributions de probabilité prédites et réelles.
  • La dérivée de Softmax + Entropie Croisée par rapport aux logits se simplifie à la différence entre les probabilités prédites et les étiquettes réelles, simplifiant considérablement l'implémentation.
  • L'implémentation à partir de zéro en C++/CUDA permet une compréhension approfondie des mécanismes internes des modèles comme Transformer, sans dépendre d'abstractions de haut niveau.

— Editorial Team

Advertisement 728x90

Lire ensuite