Pytorch
TensorFlow

#
Fine-tuning eine Modells mit der Trainer API

🤗 Transformers stellt eine `Trainer`-Klasse bereit, mit der du Modelle auf deinen Datensätzen fein-tunen kannst. Nachdem die Datenverarbeitung im letzten Abschnitt abgeschlossen ist, bleiben nur noch wenige Schritte, um den `Trainer` zu definieren. Der schwierigste Teil ist die Vorbereitung der Umgebung um `Trainer.train()` auszuführen, da dies auf einer CPU sehr langsam läuft. Wenn keine GPU verfügbar ist, kannst du bei [Google Colab] (https://colab.research.google.com/) auf kostenlose GPUs oder TPUs zugreifen.

In den folgenden Code-Beispielen wird davon ausgegangen, dass du die Beispiele aus dem vorherigen Abschnitt bereits ausgeführt hast. Hier ist eine kurze Zusammenfassung, die dir zeigt, was erwartet wird:

    from datasets import load_dataset
    from transformers import AutoTokenizer, DataCollatorWithPadding

    raw_datasets = load_dataset("glue", "mrpc")
    checkpoint = "bert-base-uncased"
    tokenizer = AutoTokenizer.from_pretrained(checkpoint)

    def tokenize_function(example):
        return tokenizer(example["sentence1"], example["sentence2"], truncation=True)

    tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
    data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

###
Training

Als erstes müssen wir eine Klasse `TrainingArguments` definieren, die alle Hyperparameter enthält, die der `Trainer` für das Training und die Evaluation verwendet. Das einzige Argument das hier angegeben werden muss, ist ein Verzeichnis in dem das trainierte Modell sowie die Checkpoints gespeichert werden. Für alles andere können die Standardeinstellungen verwendet werden. Diese sollten für ein grundlegendes Fein-tunen ausreichen.

    from transformers import TrainingArguments

    training_args = TrainingArguments("test-trainer")

💡 Wenn du dein Modell während des Trainings automatisch in das Hub hochladen möchtest, kann in `TrainingArguments` das Argument `push_to_hub=True` angegeben werden. Darüber erfahren wir in Kapitel 4 mehr.

Der zweite Schritt ist die Definition unseres Modells. Wie im vorherigen Kapitel verwenden wir die Klasse `AutoModelForSequenceClassification` mit zwei Labels:

    from transformers import AutoModelForSequenceClassification

    model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)

Du wirst feststellen, dass du im Gegensatz zu Kapitel 2 eine Warnung erhältst, nachdem du dieses vortrainierte Modell instanziiert hast. Der Grund dafür ist, dass BERT nicht auf die Klassifizierung von Satzpaaren vortrainiert wurde. Deshalb wurde der Kopf des vortrainierten Modells verworfen und stattdessen ein neuer Kopf hinzugefügt, der für die Klassifizierung von Sequenzen geeignet ist. Diese Warnungen weisen darauf hin, dass Teile der Gewichtung nicht verwendet wurden (die Gewichte für den verworfenen Kopf) und dass einige andere zufällig initialisiert wurden (die Gewichte für den neuen Kopf). Abschließend werden wir aufgefordert, das Modell zu trainieren, und genau das werden wir jetzt tun.

Sobald wir unser Modell haben, können wir einen `Trainer` definieren, indem wir alle bisher erstellten Objekte übergeben - das `Modell`, die `training_args`, die Trainings- und Validierungsdaten, unseren `data_collator` und unseren `tokenizer`:

    from transformers import Trainer

    trainer = Trainer(
        model,
        training_args,
        train_dataset=tokenized_datasets["train"],
        eval_dataset=tokenized_datasets["validation"],
        data_collator=data_collator,
        tokenizer=tokenizer,
    )

Merke: Wenn der `tokenizer` übergeben wird, wie wir es hier getan haben, wird der vom `Trainer` verwendete `data_collator` standardmäßig ein `DataCollatorWithPadding` sein, wie er zuvor definiert wurde. Deshalb kannst du die Zeile `data_collator=data_collator` in diesem Aufruf weglassen. Unabhängig davon war es trotzdem wichtig, diesen Teil der Verarbeitung in Abschnitt 2 zu zeigen!

Um das Modell auf unserem Datensatz fein-tunen zu können, müssen wir nur die Methode `train()` unseres `Trainers` aufrufen:

    trainer.train()

Dadurch wird das Fein-tunen gestartet (was auf einer GPU ein paar Minuten dauern sollte) und der Trainingsverlust wird alle 500 Schritte gemeldet. Es wird jedoch nicht zurückgegeben, wie gut (oder schlecht) das Modell funktioniert. Dies liegt an folgenden Punkten:

1. Wir haben dem `Trainer` nicht mitgeteilt die Performance in der Trainingsschleife auszuwerten, indem wir `evaluation_strategy` entweder auf `"steps"` (alle `eval_steps` auswerten) oder `"epoch"` (am Ende jeder Epoche evaluieren) gesetzt haben.
2. Wir haben dem `Trainer` keine Funktion `compute_metrics()` zur Verfügung gestellt, um während der Evaluation eine Metrik zu berechnen (sonst hätte die Evaluation nur den Verlust ausgegeben, was keine sehr intuitive Zahl ist).

###
Evaluation

Im Folgenden wird gezeigt, wie wir eine `compute_metrics()`-Funktion erstellen und sie beim nächsten Training verwenden können. Die Funktion muss ein `EvalPrediction`-Objekt (ein bennantes Tupel mit einem `predictions`-Feld und einem `label_ids`-Feld) annehmen und ein Dictionary zurückgeben, das Strings auf Floats abbildet (die Strings sind die Namen der zurückgegebenen Metriken und die Floats ihre zugehörigen Werte). Um Vorhersagen von unserem Modell zu erhalten, können wir den Befehl “Trainer.predict()” verwenden:

    predictions = trainer.predict(tokenized_datasets["validation"])
    print(predictions.predictions.shape, predictions.label_ids.shape)

    (408, 2) (408,)

Die Ausgabe der `predict()`-Methode ist ein weiteres benanntes Tupel mit drei Feldern: `predictions`, `label_ids` und `metrics`. Das Feld `metrics` enthält den Verlust des übergebenen Datensatzes sowie Zeitangaben dazu, wie lange die Vorhersage insgesamt und im Durchschnitt gedauert hat. Sobald wir unsere Funktion `compute_metrics()` fertiggestellt haben und sie an den `Trainer` übergeben, enthält dieses Feld auch die von der `compute_metrics()`-Funktion zurückgegebenen Metriken.

Die Vorhersagen in `predictions` sind ein zweidimensionales Array mit der Form 408 x 2 (408 ist die Anzahl der Elemente unseres Datensatzes). Das sind die Logits für jedes Element des Datensatzes, das wir an `predict()` übergeben haben (siehe vorheriges Kapitel dass alle Transformer Modelle Logits zurückgeben). Um diese in Vorhersagen umzuwandeln, die wir mit den Labels vergleichen können, müssen wir den Index mit dem höchsten Wert auf der zweiten Achse nehmen:

    import numpy as np

    preds = np.argmax(predictions.predictions, axis=-1)

Jetzt können wir diese Vorhersagen in `preds` mit den Labels vergleichen. Wir greifen auf die Metriken aus der 🤗 Bibliothek Evaluate zurück, um unsere Funktion `compute_metric()` zu erstellen. Die mit dem MRPC-Datensatz verbundenen Metriken können genauso einfach geladen werden, wie wir den Datensatz geladen haben, diesmal mit der Funktion `evaluate.load()`. Das zurückgegebene Objekt verfügt über eine Berechnungsmethode, mit der wir die Metrik auswerten können:

    import evaluate

    metric = evaluate.load("glue", "mrpc")
    metric.compute(predictions=preds, references=predictions.label_ids)

    {'accuracy': 0.8578431372549019, 'f1': 0.8996539792387542}

Die genauen Ergebnisse können variieren, da die zufällige Initialisierung des Modellkopfes den Optimierungsverlauf und damit die Metriken verändern kann. Hier hat das Modell eine Genauigkeit von 85,78 % über die Validierungsdaten und eine F1-Maß von 89,97 erreicht hat. Dies sind die beiden Kennzahlen, die zur Bewertung der Ergebnisse des MRPC-Datensatzes für den GLUE-Benchmark verwendet werden. In der Tabelle im [BERT-Paper] (https://arxiv.org/pdf/1810.04805.pdf) wird für das Basismodell ein F1-Maß von 88,9 angegeben. Das Paper hat das `uncased` Modell verwendet, während wir derzeit das `cased` Modell verwenden, was das bessere Ergebnis erklärt.

Zusammenfassend ergibt das unsere Funktion `compute_metrics()`:

    def compute_metrics(eval_preds):
        metric = evaluate.load("glue", "mrpc")
        logits, labels = eval_preds
        predictions = np.argmax(logits, axis=-1)
        return metric.compute(predictions=predictions, references=labels)

Um diese Funktion in Aktion zu sehen, definieren wir einen neuen `Trainer` mit der Funktion “compute_metrics()”, um am Ende jeder Epoche Metriken zu melden:

    training_args = TrainingArguments("test-trainer", evaluation_strategy="epoch")
    model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)

    trainer = Trainer(
        model,
        training_args,
        train_dataset=tokenized_datasets["train"],
        eval_dataset=tokenized_datasets["validation"],
        data_collator=data_collator,
        tokenizer=tokenizer,
        compute_metrics=compute_metrics,
    )

Hier ein Hinweis, dass wir ein neues `TrainingArguments` errstellen, dessen `evaluation_strategy` auf `"epoch"` gesetzt ist, und ein neues Modell definieren - andernfalls würden wir nur das Training des momentanen Modells fortführen, das wir bereits trainiert haben. Um einen neuen Trainingslauf zu starten, führen wir folgendes aus:

    trainer.train()

Nun werden am Ende jeder Epoche zusätzlich zu den Trainingsverlusten auch die Validierungsverluste und -metriken gemeldet. Auch hier kann die Genauigkeit/F1-Maß aufgrund der zufälligen Initialisierung des Modells zu unserem Beispiel variieren, aber sie sollte in etwa gleich sein.

Der `Trainer` funktioniert sofort auf mehreren GPUs oder TPUs und bietet zahlreiche Optionen, wie z. B. Training mit gemischter Genauigkeit (verwende `fp16 = True` in deinen Trainingsargumenten). In Kapitel 10 gehen wir auf alle Funktionen ein, die die `Trainer`-Klasse bereitstellt.

Damit ist die Einführung in das Fein-tunen mit der `Trainer` API abgeschlossen. Beispiele für die gängigsten CL-Aufgaben werden in Kapitel 7 gegeben, aber jetzt schauen wir uns erst einmal an, wie man das Gleiche in PyTorch bewerkstelligen kann.

✏️ Probier es aus! Fein-tune ein Modell mit dem GLUE SST-2 Datensatz, indem du die Datenverarbeitung aus Abschnitt 2 verwendest.