Passer au contenu principal
Utilisez W&B pour le suivi des expériences de machine learning, la gestion des versions des jeux de données et la collaboration autour des projets.
Avantages de l’utilisation de W&B

Ce que vous trouverez dans ce notebook

Nous vous montrons comment intégrer W&B à votre code PyTorch pour ajouter le suivi des expériences à votre pipeline.
Schéma de l’intégration entre PyTorch et W&B
# importer la bibliothèque
import wandb

# définir un dictionnaire d'hyperparamètres avec la configuration
config = {
    "learning_rate": 0.001,
    "epochs": 100,
    "batch_size": 128
}

# démarrer une nouvelle expérience
with wandb.init(project="new-sota-model", config=config) as run:

    # configurer le modèle et les données
    model, dataloader = get_model(), get_data()

    # facultatif : suivre les gradients
    run.watch(model)

    for batch in dataloader:
    metrics = model.training_step()
    # consigner des métriques dans votre boucle d'entraînement pour visualiser les performances du modèle
    run.log(metrics)

    # facultatif : enregistrer le modèle à la fin
    model.to_onnx()
    run.save("model.onnx")
Suivez ce tutoriel vidéo. Remarque : les sections commençant par Étape suffisent pour intégrer W&B à un pipeline existant. Le reste se limite à charger les données et à définir un modèle.

Installer, importer et se connecter

import os
import random

import numpy as np
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
from tqdm.auto import tqdm

# Assurer un comportement déterministe
torch.backends.cudnn.deterministic = True
random.seed(hash("setting random seeds") % 2**32 - 1)
np.random.seed(hash("improves reproducibility") % 2**32 - 1)
torch.manual_seed(hash("by removing stochasticity") % 2**32 - 1)
torch.cuda.manual_seed_all(hash("so runs are repeatable") % 2**32 - 1)

# Configuration du périphérique
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# supprimer le miroir lent de la liste des miroirs MNIST
torchvision.datasets.MNIST.mirrors = [mirror for mirror in torchvision.datasets.MNIST.mirrors
                                      if not mirror.startswith("http://yann.lecun.com")]

Étape 0 : Installer W&B

Pour commencer, vous devez installer la bibliothèque. wandb s’installe facilement avec pip.
!pip install wandb onnx -Uq

Étape 1 : Importer W&B et vous connecter

Pour journaliser des données sur notre service web, vous devrez vous connecter. Si c’est la première fois que vous utilisez W&B, vous devrez créer un compte gratuit via le lien qui s’affiche.
import wandb

wandb.login()

Définir l’expérience et le pipeline

Suivre les métadonnées et les hyperparamètres avec wandb.init

Dans le code, la première chose à faire est de définir notre expérience : quels sont les hyperparamètres ? quelles métadonnées sont associées à ce run ? Il est très courant de stocker ces informations dans un dictionnaire config (ou un objet similaire) puis d’y accéder au besoin. Dans cet exemple, nous ne faisons varier que quelques hyperparamètres et codons le reste à la main. Mais n’importe quelle partie de votre modèle peut faire partie de config. Nous incluons également quelques métadonnées : nous utilisons le jeu de données MNIST et une architecture convolutionnelle. Si, plus tard, nous travaillons par exemple avec des architectures entièrement connectées sur CIFAR dans le même projet, cela nous aidera à différencier nos runs.
config = dict(
    epochs=5,
    classes=10,
    kernels=[16, 32],
    batch_size=128,
    learning_rate=0.005,
    dataset="MNIST",
    architecture="CNN")
Maintenant, définissons le pipeline global, ce qui est assez typique pour l’entraînement d’un modèle :
  1. nous commençons par make un modèle, ainsi que les données et l’optimiseur associés, puis
  2. nous train le modèle en conséquence et enfin
  3. nous le test pour voir comment l’entraînement s’est déroulé.
Nous implémenterons ces fonctions ci-dessous.
def model_pipeline(hyperparameters):

    # indiquer à wandb de démarrer
    with wandb.init(project="pytorch-demo", config=hyperparameters) as run:
        # accéder à tous les HPs via run.config, pour que la journalisation corresponde à l'exécution.
        config = run.config

        # créer le modèle, les données et le problème d'optimisation
        model, train_loader, test_loader, criterion, optimizer = make(config)
        print(model)

        # et les utiliser pour entraîner le modèle
        train(model, train_loader, criterion, optimizer, config)

        # et tester ses performances finales
        test(model, test_loader)

    return model
La seule différence ici par rapport à un pipeline standard est que tout se passe dans le contexte de wandb.init. L’appel à cette fonction établit un canal de communication entre votre code et nos serveurs. Le fait de passer le dictionnaire config à wandb.init enregistre immédiatement toutes ces informations, afin que vous sachiez toujours quelles valeurs d’hyperparamètres vous avez choisi d’utiliser pour votre expérience. Pour vous assurer que les valeurs que vous avez choisies et enregistrées sont toujours bien celles qui sont utilisées dans votre modèle, nous vous recommandons d’utiliser la copie run.config de votre objet. Consultez la définition de make ci-dessous pour voir quelques exemples.
Remarque : nous veillons à exécuter notre code dans des processus séparés, afin que tout problème de notre côté (par exemple, si un énorme monstre marin attaque nos centres de données) ne fasse pas planter votre code. Une fois le problème résolu, par exemple lorsque le Kraken retourne dans les profondeurs, vous pouvez synchroniser les données avec wandb sync.
def make(config):
    # Préparer les données
    train, test = get_data(train=True), get_data(train=False)
    train_loader = make_loader(train, batch_size=config.batch_size)
    test_loader = make_loader(test, batch_size=config.batch_size)

    # Créer le modèle
    model = ConvNet(config.kernels, config.classes).to(device)

    # Définir la perte et l'optimiseur
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(
        model.parameters(), lr=config.learning_rate)
    
    return model, train_loader, test_loader, criterion, optimizer

Définir le chargement des données et le modèle

Nous devons maintenant préciser comment les données sont chargées et à quoi ressemble le modèle. Cette partie est très importante, mais elle n’est pas différente de ce qu’elle serait sans wandb, donc nous ne nous y attarderons pas.
def get_data(slice=5, train=True):
    full_dataset = torchvision.datasets.MNIST(root=".",
                                              train=train, 
                                              transform=transforms.ToTensor(),
                                              download=True)
    #  équivalent au découpage avec [::slice] 
    sub_dataset = torch.utils.data.Subset(
      full_dataset, indices=range(0, len(full_dataset), slice))
    
    return sub_dataset


def make_loader(dataset, batch_size):
    loader = torch.utils.data.DataLoader(dataset=dataset,
                                         batch_size=batch_size, 
                                         shuffle=True,
                                         pin_memory=True, num_workers=2)
    return loader
Définir le modèle est normalement la partie la plus intéressante. Mais avec wandb, rien ne change, donc on va s’en tenir à une architecture ConvNet standard. N’hésitez pas à faire quelques essais et à tester différentes variantes — tous vos résultats seront enregistrés sur wandb.ai.
# Réseau de neurones conventionnel et convolutif

class ConvNet(nn.Module):
    def __init__(self, kernels, classes=10):
        super(ConvNet, self).__init__()
        
        self.layer1 = nn.Sequential(
            nn.Conv2d(1, kernels[0], kernel_size=5, stride=1, padding=2),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2))
        self.layer2 = nn.Sequential(
            nn.Conv2d(16, kernels[1], kernel_size=5, stride=1, padding=2),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2))
        self.fc = nn.Linear(7 * 7 * kernels[-1], classes)
        
    def forward(self, x):
        out = self.layer1(x)
        out = self.layer2(out)
        out = out.reshape(out.size(0), -1)
        out = self.fc(out)
        return out

Définir la logique d’entraînement

En poursuivant notre model_pipeline, il est temps de préciser comment nous entraînons. Deux fonctions wandb entrent en jeu ici : watch et log.

Suivre les gradients avec run.watch() et tout le reste avec run.log()

run.watch journalisera les gradients et les paramètres de votre modèle toutes les log_freq étapes de l’entraînement. Il vous suffit de l’appeler avant de commencer l’entraînement. Le reste du code d’entraînement reste le même : nous parcourons les époques et les lots, en effectuant des passes avant et arrière et en appliquant notre optimizer.
def train(model, loader, criterion, optimizer, config):
    # Indique à wandb de surveiller le modèle : gradients, poids, et plus encore.
    run = wandb.init(project="pytorch-demo", config=config)
    run.watch(model, criterion, log="all", log_freq=10)

    # Lancer l'entraînement et le suivre avec wandb
    total_batches = len(loader) * config.epochs
    example_ct = 0  # nombre d'exemples traités
    batch_ct = 0
    for epoch in tqdm(range(config.epochs)):
        for _, (images, labels) in enumerate(loader):

            loss = train_batch(images, labels, model, optimizer, criterion)
            example_ct +=  len(images)
            batch_ct += 1

            # Enregistrer les métriques tous les 25 lots
            if ((batch_ct + 1) % 25) == 0:
                train_log(loss, example_ct, epoch)


def train_batch(images, labels, model, optimizer, criterion):
    images, labels = images.to(device), labels.to(device)
    
    # Passe avant ➡
    outputs = model(images)
    loss = criterion(outputs, labels)
    
    # Passe arrière ⬅
    optimizer.zero_grad()
    loss.backward()

    # Mise à jour avec l'optimiseur
    optimizer.step()

    return loss
La seule différence réside dans le code de journalisation : alors qu’auparavant vous pouviez consigner des métriques en les affichant dans le terminal, vous transmettez maintenant les mêmes informations à run.log(). run.log() attend un dictionnaire dont les clés sont des chaînes de caractères. Ces chaînes identifient les objets enregistrés, qui constituent les valeurs. Vous pouvez aussi indiquer, de façon facultative, à quelle step d’entraînement vous en êtes.
Remarque : j’aime utiliser le nombre d’exemples que le modèle a vus, car cela facilite la comparaison entre différentes tailles de batch, mais vous pouvez utiliser les étapes brutes ou le nombre de batches. Pour des runs d’entraînement plus longs, il peut aussi être pertinent de journaliser par epoch.
def train_log(loss, example_ct, epoch):
    with wandb.init(project="pytorch-demo") as run:
        # Enregistrer la perte et le numéro d'époque
        # C'est ici que nous enregistrons les métriques dans W&B
        run.log({"epoch": epoch, "loss": loss}, step=example_ct)
        print(f"Loss after {str(example_ct).zfill(5)} examples: {loss:.3f}")

Définir la logique de test

Une fois l’entraînement du modèle terminé, nous voulons le tester : l’exécuter sur de nouvelles données issues de la Production, par exemple, ou l’appliquer à des exemples soigneusement sélectionnés à la main.

(Facultatif) Appelez run.save()

C’est aussi le bon moment pour enregistrer l’architecture du modèle et les paramètres finaux sur le disque. Pour une compatibilité maximale, nous allons exporter notre modèle au format Open Neural Network eXchange (ONNX). En passant ce nom de fichier à run.save(), vous vous assurez que les paramètres du modèle sont enregistrés sur les serveurs de W&B : fini les pertes de repères pour savoir quel fichier .h5 ou .pb correspond à quel run d’entraînement. Pour des fonctionnalités wandb plus avancées pour le stockage, la gestion des versions et la distribution de modèles, consultez nos outils Artifacts.
def test(model, test_loader):
    model.eval()

    with wandb.init(project="pytorch-demo") as run:
        # Exécuter le modèle sur quelques exemples de test
        with torch.no_grad():
            correct, total = 0, 0
            for images, labels in test_loader:
                images, labels = images.to(device), labels.to(device)
                outputs = model(images)
                _, predicted = torch.max(outputs.data, 1)
                total += labels.size(0)
                correct += (predicted == labels).sum().item()

            print(f"Accuracy of the model on the {total} " +
                f"test images: {correct / total:%}")
            
            run.log({"test_accuracy": correct / total})

        # Enregistrer le modèle au format ONNX interchangeable
        torch.onnx.export(model, images, "model.onnx")
        run.save("model.onnx")

Exécutez l’entraînement et suivez vos métriques en direct sur wandb.ai

Maintenant que nous avons défini l’ensemble du pipeline et ajouté ces quelques lignes de code W&B, nous sommes prêts à lancer notre expérience avec un suivi complet. Nous vous fournirons quelques liens : notre documentation, la page du projet, qui organise tous les runs d’un projet, et la page du run, où les résultats de ce run seront stockés. Accédez à la page du run et consultez ces onglets :
  1. Charts, où les gradients du modèle, les valeurs des paramètres et la perte sont enregistrés tout au long de l’entraînement
  2. System, qui contient diverses métriques système, notamment l’utilisation des E/S du disque, les métriques CPU et GPU (regardez cette température grimper), et bien plus encore
  3. Logs, qui contient une copie de tout ce qui a été envoyé vers la sortie standard pendant l’entraînement
  4. Files, où, une fois l’entraînement terminé, vous pouvez cliquer sur model.onnx pour visualiser notre réseau avec le visualiseur de modèles Netron.
Une fois le run terminé, à la sortie du bloc with wandb.init, nous afficherons également un résumé des résultats dans la sortie de la cellule.
# Construire, entraîner et analyser le modèle avec le pipeline
model = model_pipeline(config)

Tester des hyperparamètres avec Sweeps

Dans cet exemple, nous n’avons examiné qu’un seul ensemble d’hyperparamètres. Or, une part importante de la plupart des flux de travail de ML consiste à itérer sur plusieurs hyperparamètres. Vous pouvez utiliser W&B Sweeps pour automatiser les tests d’hyperparamètres et explorer l’espace des modèles possibles ainsi que les stratégies d’optimisation. Consultez un notebook Colab montrant l’optimisation des hyperparamètres avec W&B Sweeps. Exécuter un balayage d’hyperparamètres avec W&B est très simple. Il n’y a que 3 étapes :
  1. Définir le balayage : pour cela, créez un dictionnaire ou un fichier YAML qui spécifie les paramètres à explorer, la stratégie de recherche, la métrique d’optimisation, etc.
  2. Initialiser le balayage : sweep_id = wandb.sweep(sweep_config)
  3. Exécuter l’agent de balayage : wandb.agent(sweep_id, function=train)
C’est tout ce qu’il faut pour exécuter un balayage d’hyperparamètres.
Tableau de bord d’entraînement PyTorch
Explorez des exemples de projets suivis et visualisés avec W&B dans notre Galerie →.

Configuration avancée

  1. Variables d’environnement : définissez des clés API dans les variables d’environnement pour pouvoir lancer des entraînements sur un cluster géré.
  2. Mode hors ligne : utilisez le mode dryrun pour entraîner hors ligne et synchroniser les résultats plus tard.
  3. Sur site : installez W&B dans un cloud privé ou sur des serveurs isolés du réseau au sein de votre propre infrastructure. Nous proposons des déploiements locaux pour tous les profils, du monde universitaire aux équipes d’entreprise.
  4. Sweeps : configurez rapidement une recherche d’hyperparamètres avec notre outil léger de réglage.