Implémentation orientée objets

Dans notre introduction à la régression linéaire, nous avons passé en revue divers composants, notamment les données, le modèle, la fonction de perte et l'algorithme d'optimisation. En effet, la régression linéaire est l'un des modèles d'apprentissage automatique les plus simples. Son apprentissage fait toutefois appel à de nombreux composants identiques à ceux des autres modèles présentés dans cet ouvrage. Par conséquent, avant de plonger dans les détails de la mise en œuvre, il est utile de concevoir certaines des API utilisées tout au long de cet ouvrage. En traitant les composants de l'apprentissage profond comme des objets, nous pouvons commencer par définir des classes pour ces objets et leurs interactions. Cette conception orientée objet pour la mise en œuvre simplifiera grandement la présentation et vous pourriez même vouloir l'utiliser dans vos projets.

Inspiré par des bibliothèques open-source telles que PyTorch Lightning, à un niveau élevé, nous souhaitons avoir trois classes :

La plupart du code de ce chapitre adapte Module et DataModule. Nous n'aborderons la classe Trainer que lorsque nous parlerons des GPU, des CPU, de l'entraînement parallèle et des algorithmes d'optimisation.

Utilitaires

Nous avons besoin de quelques utilitaires pour simplifier la programmation orientée objet. L'un des défis est que les définitions de classes ont tendance à être des blocs de code assez longs. La lisibilité des notebooks exige des fragments de code courts, entrecoupés d'explications, une exigence incompatible avec le style de programmation commun aux bibliothèques Python. La première fonction utilitaire nous permet d'enregistrer des fonctions comme méthodes dans une classe after la création de cette dernière. En fait, nous pouvons le faire even after avoir créé des instances de la classe ! Elle nous permet de diviser l'implémentation d'une classe en plusieurs blocs de code.

Voyons rapidement comment l'utiliser. Nous prévoyons d'implémenter une classe A avec une méthode do. Au lieu d'avoir le code pour A et do dans le même bloc de code, nous pouvons d'abord déclarer la classe A et créer une instance a.

Ensuite, nous définissons la méthode do comme nous le ferions normalement, mais pas dans la portée de la classe A. Au lieu de cela, nous décorons cette méthode par add_to_class avec la classe A comme argument. Ce faisant, la méthode peut accéder aux variables membres de A comme nous l'aurions souhaité si elle avait été définie dans le cadre de la définition de A. Voyons ce qui se passe lorsque nous l'invoquons pour l'instance a.

La seconde est une classe utilitaire qui enregistre tous les arguments de la méthode __init__ d'une classe comme attributs de classe. Cela nous permet d'étendre les signatures d'appel des constructeurs de manière implicite sans code supplémentaire.

Pour l'utiliser, nous définissons notre classe qui hérite de HyperParameters et appelle save_hyperparameters dans la méthode __init__.

Le dernier utilitaire nous permet de tracer la progression de l'expérience de manière interactive pendant qu'elle se déroule. En référence au TensorBoard, beaucoup plus puissant (et complexe), nous l'appelons ProgressBoard. Pour l'instant, nous allons simplement le voir en action.

La fonction draw trace un point (x, y) dans la figure, avec label spécifié dans la légende. L'option every_n lisse la ligne en ne montrant que $1/n$ points dans la figure. Leurs valeurs sont moyennées à partir des $n$ points voisins dans la figure originale.

Dans l'exemple suivant, nous dessinons sin et cos avec un lissage différent. Si vous exécutez ce bloc de code, vous verrez les lignes grandir en animation.

Modèles

La classe Module est la classe de base de tous les modèles que nous allons implémenter. Au minimum, nous devons définir trois méthodes. La méthode __init__ stocke les paramètres apprenables, la méthode training_step accepte un lot de données pour retourner la valeur de perte, la méthode configure_optimizers retourne la méthode d'optimisation, ou une liste de celles-ci, qui est utilisée pour mettre à jour les paramètres apprenables. Optionnellement, nous pouvons définir validation_step pour rapporter les mesures d'évaluation. Parfois, nous mettons le code pour calculer la sortie dans une méthode séparée forward pour le rendre plus réutilisable.

Vous pouvez remarquer que Module est une sous-classe de nn.Module, la classe de base des réseaux de neurones dans PyTorch. Elle fournit des fonctionnalités pratiques pour manipuler les réseaux de neurones. Par exemple, si on définit une méthode forward, comme forward(self, X), alors pour une instance a on peut invoquer cette fonction par a(X). Cela fonctionne puisque la méthode forward est appelée dans la méthode intégrée __call__.

Données

La classe DataModule est la classe de base pour les données. Très souvent, la méthode __init__ est utilisée pour préparer les données. Cela inclut le téléchargement et le prétraitement si nécessaire. La méthode train_dataloader retourne le chargeur de données pour le jeu de données d'entraînement. Un chargeur de données est un générateur (Python) qui produit un lot de données à chaque fois qu'il est utilisé. Ce lot est ensuite introduit dans la méthode training_step de Module pour calculer la perte. Il y a une option val_dataloader pour retourner le chargeur de données de validation. Il se comporte de la même manière, sauf qu'il produit des lots de données pour la méthode validation_step dans Module.

Training (apprentissage)

La classe Trainer entraîne les paramètres apprenables dans la classe Module avec les données spécifiées dans DataModule. La méthode clé est fit, qui accepte deux arguments : model, une instance de Module, et data, une instance de DataModule. Il itère ensuite sur l'ensemble des données max_epochs fois pour entraîner le modèle.