!pip install d2l==1.0.0-alpha1.post0
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 :
Module
contient les modèles, les pertes et les méthodes d'optimisation ; DataModule
fournit les chargeurs de données pour l'entraînement et la validation ; Trainer
, qui nous permet d'entraîner les modèles sur une variété de plateformes matérielles. 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.
import time
import numpy as np
import torch
from torch import nn
from d2l import torch as d2l
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.
def add_to_class(Class): #@save
def wrapper(obj):
setattr(Class, obj.__name__, obj)
return wrapper
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
.
class A:
def __init__(self):
self.b = 1
a = 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
.
@add_to_class(A)
def do(self):
print('Class attribute "b" is', self.b)
a.do()
Class attribute "b" is 1
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.
class HyperParameters: #@save
def save_hyperparameters(self, ignore=[]):
raise NotImplemented
Pour l'utiliser, nous définissons notre classe qui hérite de HyperParameters
et appelle save_hyperparameters
dans la méthode __init__
.
# Call the fully implemented HyperParameters class saved in d2l
class B(d2l.HyperParameters):
def __init__(self, a, b, c):
self.save_hyperparameters(ignore=['c'])
print('self.a =', self.a, 'self.b =', self.b)
print('There is no self.c =', not hasattr(self, 'c'))
b = B(a=1, b=2, c=3)
self.a = 1 self.b = 2 There is no self.c = True
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.
class ProgressBoard(d2l.HyperParameters): #@save
"""Plot data points in animation."""
def __init__(self, xlabel=None, ylabel=None, xlim=None,
ylim=None, xscale='linear', yscale='linear',
ls=['-', '--', '-.', ':'], colors=['C0', 'C1', 'C2', 'C3'],
fig=None, axes=None, figsize=(3.5, 2.5), display=True):
self.save_hyperparameters()
def draw(self, x, y, label, every_n=1):
raise NotImplemented
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.
board = d2l.ProgressBoard('x')
for x in np.arange(0, 10, 0.1):
board.draw(x, np.sin(x), 'sin', every_n=2)
board.draw(x, np.cos(x), 'cos', every_n=10)
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.
class Module(nn.Module, d2l.HyperParameters): #@save
def __init__(self, plot_train_per_epoch=2, plot_valid_per_epoch=1):
super().__init__()
self.save_hyperparameters()
self.board = ProgressBoard()
def loss(self, y_hat, y):
raise NotImplementedError
def forward(self, X):
assert hasattr(self, 'net'), 'Neural network is defined'
return self.net(X)
def plot(self, key, value, train):
"""Plot a point in animation."""
assert hasattr(self, 'trainer'), 'Trainer is not inited'
self.board.xlabel = 'epoch'
if train:
x = self.trainer.train_batch_idx / \
self.trainer.num_train_batches
n = self.trainer.num_train_batches / \
self.plot_train_per_epoch
else:
x = self.trainer.epoch + 1
n = self.trainer.num_val_batches / \
self.plot_valid_per_epoch
self.board.draw(x, value.to(d2l.cpu()).detach().numpy(),
('train_' if train else 'val_') + key,
every_n=int(n))
def training_step(self, batch):
l = self.loss(self(*batch[:-1]), batch[-1])
self.plot('loss', l, train=True)
return l
def validation_step(self, batch):
l = self.loss(self(*batch[:-1]), batch[-1])
self.plot('loss', l, train=False)
def configure_optimizers(self):
raise NotImplementedError
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__
.
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
.
class DataModule(d2l.HyperParameters): #@save
def __init__(self, root='../data', num_workers=4):
self.save_hyperparameters()
def get_dataloader(self, train):
raise NotImplementedError
def train_dataloader(self):
return self.get_dataloader(train=True)
def val_dataloader(self):
return self.get_dataloader(train=False)
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.
class Trainer(d2l.HyperParameters): #@save
def __init__(self, max_epochs, num_gpus=0, gradient_clip_val=0):
self.save_hyperparameters()
assert num_gpus == 0, 'No GPU support yet'
def prepare_data(self, data):
self.train_dataloader = data.train_dataloader()
self.val_dataloader = data.val_dataloader()
self.num_train_batches = len(self.train_dataloader)
self.num_val_batches = (len(self.val_dataloader)
if self.val_dataloader is not None else 0)
def prepare_model(self, model):
model.trainer = self
model.board.xlim = [0, self.max_epochs]
self.model = model
def fit(self, model, data):
self.prepare_data(data)
self.prepare_model(model)
self.optim = model.configure_optimizers()
self.epoch = 0
self.train_batch_idx = 0
self.val_batch_idx = 0
for self.epoch in range(self.max_epochs):
self.fit_epoch()
def fit_epoch(self):
raise NotImplementedError