Démarrage rapide 🤗 Transformateurs#

Construisons un système d’apprentissage fédéré à l’aide des transformateurs Hugging Face et de Flower !

Nous nous appuierons sur Hugging Face pour fédérer l’entraînement de modèles de langage sur plusieurs clients à l’aide de Flower. Plus précisément, nous mettrons au point un modèle Transformer pré-entraîné (distilBERT) pour la classification de séquences sur un ensemble de données d’évaluations IMDB. L’objectif final est de détecter si l’évaluation d’un film est positive ou négative.

Dépendances#

Pour suivre ce tutoriel, tu devras installer les paquets suivants : datasets, evaluate, flwr, torch, et transformers. Cela peut être fait en utilisant pip :

$ pip install datasets evaluate flwr torch transformers

Flux de travail standard pour le visage#

Traitement des données#

Pour récupérer le jeu de données IMDB, nous utiliserons la bibliothèque datasets de Hugging Face. Nous devons ensuite tokeniser les données et créer des PyTorch dataloaders, ce qui est fait dans la fonction load_data :

import random
import torch
from datasets import load_dataset
from torch.utils.data import DataLoader
from transformers import AutoTokenizer, DataCollatorWithPadding

DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
CHECKPOINT = "distilbert-base-uncased"

def load_data():
    """Load IMDB data (training and eval)"""
    raw_datasets = load_dataset("imdb")
    raw_datasets = raw_datasets.shuffle(seed=42)
    # remove unnecessary data split
    del raw_datasets["unsupervised"]
    tokenizer = AutoTokenizer.from_pretrained(CHECKPOINT)
    def tokenize_function(examples):
        return tokenizer(examples["text"], truncation=True)
    # We will take a small sample in order to reduce the compute time, this is optional
    train_population = random.sample(range(len(raw_datasets["train"])), 100)
    test_population = random.sample(range(len(raw_datasets["test"])), 100)
    tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
    tokenized_datasets["train"] = tokenized_datasets["train"].select(train_population)
    tokenized_datasets["test"] = tokenized_datasets["test"].select(test_population)
    tokenized_datasets = tokenized_datasets.remove_columns("text")
    tokenized_datasets = tokenized_datasets.rename_column("label", "labels")
    data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
    trainloader = DataLoader(
        tokenized_datasets["train"],
        shuffle=True,
        batch_size=32,
        collate_fn=data_collator,
    )
    testloader = DataLoader(
        tokenized_datasets["test"], batch_size=32, collate_fn=data_collator
    )
    return trainloader, testloader

Former et tester le modèle#

Une fois que nous avons trouvé un moyen de créer notre trainloader et notre testloader, nous pouvons nous occuper de l’entraînement et du test. C’est très similaire à n’importe quelle boucle d’entraînement ou de test PyTorch :

from evaluate import load as load_metric
from transformers import AdamW

def train(net, trainloader, epochs):
    optimizer = AdamW(net.parameters(), lr=5e-5)
    net.train()
    for _ in range(epochs):
        for batch in trainloader:
            batch = {k: v.to(DEVICE) for k, v in batch.items()}
            outputs = net(**batch)
            loss = outputs.loss
            loss.backward()
            optimizer.step()
            optimizer.zero_grad()
def test(net, testloader):
    metric = load_metric("accuracy")
    loss = 0
    net.eval()
    for batch in testloader:
        batch = {k: v.to(DEVICE) for k, v in batch.items()}
        with torch.no_grad():
            outputs = net(**batch)
        logits = outputs.logits
        loss += outputs.loss.item()
        predictions = torch.argmax(logits, dim=-1)
        metric.add_batch(predictions=predictions, references=batch["labels"])
    loss /= len(testloader.dataset)
    accuracy = metric.compute()["accuracy"]
    return loss, accuracy

Créer le modèle lui-même#

Pour créer le modèle lui-même, nous allons simplement charger le modèle distillBERT pré-entraîné en utilisant le AutoModelForSequenceClassification de Hugging Face :

from transformers import AutoModelForSequenceClassification

net = AutoModelForSequenceClassification.from_pretrained(
        CHECKPOINT, num_labels=2
    ).to(DEVICE)

Fédérer l’exemple#

Création du client IMDBC#

Pour fédérer notre exemple à plusieurs clients, nous devons d’abord écrire notre classe de client Flower (héritant de flwr.client.NumPyClient). C’est très facile, car notre modèle est un modèle PyTorch standard :

from collections import OrderedDict
import flwr as fl

class IMDBClient(fl.client.NumPyClient):
        def get_parameters(self, config):
            return [val.cpu().numpy() for _, val in net.state_dict().items()]
        def set_parameters(self, parameters):
            params_dict = zip(net.state_dict().keys(), parameters)
            state_dict = OrderedDict({k: torch.Tensor(v) for k, v in params_dict})
            net.load_state_dict(state_dict, strict=True)
        def fit(self, parameters, config):
            self.set_parameters(parameters)
            print("Training Started...")
            train(net, trainloader, epochs=1)
            print("Training Finished.")
            return self.get_parameters(config={}), len(trainloader), {}
        def evaluate(self, parameters, config):
            self.set_parameters(parameters)
            loss, accuracy = test(net, testloader)
            return float(loss), len(testloader), {"accuracy": float(accuracy)}

La fonction get_parameters permet au serveur d’obtenir les paramètres du client. Inversement, la fonction set_parameters permet au serveur d’envoyer ses paramètres au client. Enfin, la fonction fit forme le modèle localement pour le client, et la fonction evaluate teste le modèle localement et renvoie les mesures correspondantes.

Démarrer le serveur#

Maintenant que nous avons un moyen d’instancier les clients, nous devons créer notre serveur afin d’agréger les résultats. Avec Flower, cela peut être fait très facilement en choisissant d’abord une stratégie (ici, nous utilisons FedAvg, qui définira les poids globaux comme la moyenne des poids de tous les clients à chaque tour) et en utilisant ensuite la fonction flwr.server.start_server :

def weighted_average(metrics):
    accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics]
    losses = [num_examples * m["loss"] for num_examples, m in metrics]
    examples = [num_examples for num_examples, _ in metrics]
    return {"accuracy": sum(accuracies) / sum(examples), "loss": sum(losses) / sum(examples)}

# Define strategy
strategy = fl.server.strategy.FedAvg(
    fraction_fit=1.0,
    fraction_evaluate=1.0,
    evaluate_metrics_aggregation_fn=weighted_average,
)

# Start server
fl.server.start_server(
    server_address="0.0.0.0:8080",
    config=fl.server.ServerConfig(num_rounds=3),
    strategy=strategy,
)

La fonction weighted_average est là pour fournir un moyen d’agréger les mesures réparties entre les clients (en gros, cela nous permet d’afficher une belle moyenne de précision et de perte pour chaque tour).

Tout assembler#

Nous pouvons maintenant démarrer des instances de clients en utilisant :

fl.client.start_client(
    server_address="127.0.0.1:8080",
    client=IMDBClient().to_client()
)

Et ils pourront se connecter au serveur et démarrer la formation fédérée.

If you want to check out everything put together, you should check out the full code example: [https://github.com/adap/flower/tree/main/examples/quickstart-huggingface](https://github.com/adap/flower/tree/main/examples/quickstart-huggingface).

Bien sûr, c’est un exemple très basique, et beaucoup de choses peuvent être ajoutées ou modifiées, il s’agissait juste de montrer avec quelle simplicité on pouvait fédérer un flux de travail Hugging Face à l’aide de Flower.

Notez que dans cet exemple, nous avons utilisé PyTorch, mais nous aurions très bien pu utiliser TensorFlow.