Pisanie pętli treningowej od podstaw

Zobacz na TensorFlow.org Uruchom w Google Colab Wyświetl źródło na GitHub Pobierz notatnik

Ustawiać

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np

Wstęp

Keras zapewnia domyślny szkolenia i pętle oceny, fit() i evaluate() . Ich użycie jest opisane w przewodniku Training & oceny z wbudowanych metod .

Jeśli chcesz dostosować algorytm uczenia modelu przy jednoczesnym wykorzystaniu wygodę fit() (na przykład, aby wyszkolić GAN użyciu fit() ), można podklasy Model klasy i zaimplementować własną train_step() metodę, która jest wywoływany w fit() . To jest opisane w przewodniku Dostosowywanie co dzieje się w fit() .

Teraz, jeśli chcesz bardzo niskiego poziomu kontroli nad szkoleniem i oceną, powinieneś napisać własne pętle szkolenia i oceny od podstaw. O tym jest ten przewodnik.

Korzystanie z GradientTape : pierwszy end-to-end przykład

Wywołanie modelu Wewnątrz GradientTape zakres pozwala odzyskać gradienty nadający się do szkolenia wagi warstwy w stosunku do wartości strat. Korzystanie instancję optymalizatora, można korzystać z tych gradientów zaktualizować te zmienne (które można pobrać za pomocą model.trainable_weights ).

Rozważmy prosty model MNIST:

inputs = keras.Input(shape=(784,), name="digits")
x1 = layers.Dense(64, activation="relu")(inputs)
x2 = layers.Dense(64, activation="relu")(x1)
outputs = layers.Dense(10, name="predictions")(x2)
model = keras.Model(inputs=inputs, outputs=outputs)

Wytrenujmy go za pomocą gradientu mini-partii z niestandardową pętlą treningową.

Najpierw będziemy potrzebować optymalizatora, funkcji straty i zestawu danych:

# Instantiate an optimizer.
optimizer = keras.optimizers.SGD(learning_rate=1e-3)
# Instantiate a loss function.
loss_fn = keras.losses.SparseCategoricalCrossentropy(from_logits=True)

# Prepare the training dataset.
batch_size = 64
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
x_train = np.reshape(x_train, (-1, 784))
x_test = np.reshape(x_test, (-1, 784))

# Reserve 10,000 samples for validation.
x_val = x_train[-10000:]
y_val = y_train[-10000:]
x_train = x_train[:-10000]
y_train = y_train[:-10000]

# Prepare the training dataset.
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(batch_size)

# Prepare the validation dataset.
val_dataset = tf.data.Dataset.from_tensor_slices((x_val, y_val))
val_dataset = val_dataset.batch(batch_size)
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11493376/11490434 [==============================] - 1s 0us/step
11501568/11490434 [==============================] - 1s 0us/step

Oto nasza pętla treningowa:

  • Otwieramy for pętli, która wykonuje iteracje nad epok
  • Dla każdej epoki, możemy otworzyć for pętli, która wykonuje iteracje nad zbioru danych, w partiach
  • Dla każdej partii, otwieramy GradientTape() zakres
  • Wewnątrz tego zakresu wywołujemy model (przejście do przodu) i obliczamy stratę
  • Poza zakresem pobieramy gradienty wag modelu ze względu na stratę
  • Na koniec używamy optymalizatora do aktualizacji wag modelu na podstawie gradientów
epochs = 2
for epoch in range(epochs):
    print("\nStart of epoch %d" % (epoch,))

    # Iterate over the batches of the dataset.
    for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):

        # Open a GradientTape to record the operations run
        # during the forward pass, which enables auto-differentiation.
        with tf.GradientTape() as tape:

            # Run the forward pass of the layer.
            # The operations that the layer applies
            # to its inputs are going to be recorded
            # on the GradientTape.
            logits = model(x_batch_train, training=True)  # Logits for this minibatch

            # Compute the loss value for this minibatch.
            loss_value = loss_fn(y_batch_train, logits)

        # Use the gradient tape to automatically retrieve
        # the gradients of the trainable variables with respect to the loss.
        grads = tape.gradient(loss_value, model.trainable_weights)

        # Run one step of gradient descent by updating
        # the value of the variables to minimize the loss.
        optimizer.apply_gradients(zip(grads, model.trainable_weights))

        # Log every 200 batches.
        if step % 200 == 0:
            print(
                "Training loss (for one batch) at step %d: %.4f"
                % (step, float(loss_value))
            )
            print("Seen so far: %s samples" % ((step + 1) * batch_size))
Start of epoch 0
Training loss (for one batch) at step 0: 68.7478
Seen so far: 64 samples
Training loss (for one batch) at step 200: 1.9448
Seen so far: 12864 samples
Training loss (for one batch) at step 400: 1.1859
Seen so far: 25664 samples
Training loss (for one batch) at step 600: 0.6914
Seen so far: 38464 samples

Start of epoch 1
Training loss (for one batch) at step 0: 0.9113
Seen so far: 64 samples
Training loss (for one batch) at step 200: 0.9550
Seen so far: 12864 samples
Training loss (for one batch) at step 400: 0.5139
Seen so far: 25664 samples
Training loss (for one batch) at step 600: 0.7227
Seen so far: 38464 samples

Niskopoziomowa obsługa metryk

Dodajmy monitorowanie metryk do tej podstawowej pętli.

Możesz łatwo ponownie użyć wbudowanych metryk (lub niestandardowych, które napisałeś) w takich pętlach treningowych napisanych od podstaw. Oto przepływ:

  • Utwórz wystąpienie metryki na początku pętli
  • Zadzwoń metric.update_state() po każdej partii
  • Zadzwoń metric.result() , gdy trzeba wyświetlić aktualną wartość metryki
  • Zadzwoń metric.reset_states() , gdy trzeba usunąć stan metryka (zazwyczaj pod koniec epoki)

Użyjmy tej wiedzy, aby obliczyć SparseCategoricalAccuracy danych walidacyjnych na koniec każdej epoki:

# Get model
inputs = keras.Input(shape=(784,), name="digits")
x = layers.Dense(64, activation="relu", name="dense_1")(inputs)
x = layers.Dense(64, activation="relu", name="dense_2")(x)
outputs = layers.Dense(10, name="predictions")(x)
model = keras.Model(inputs=inputs, outputs=outputs)

# Instantiate an optimizer to train the model.
optimizer = keras.optimizers.SGD(learning_rate=1e-3)
# Instantiate a loss function.
loss_fn = keras.losses.SparseCategoricalCrossentropy(from_logits=True)

# Prepare the metrics.
train_acc_metric = keras.metrics.SparseCategoricalAccuracy()
val_acc_metric = keras.metrics.SparseCategoricalAccuracy()

Oto nasza pętla szkoleń i oceny:

import time

epochs = 2
for epoch in range(epochs):
    print("\nStart of epoch %d" % (epoch,))
    start_time = time.time()

    # Iterate over the batches of the dataset.
    for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):
        with tf.GradientTape() as tape:
            logits = model(x_batch_train, training=True)
            loss_value = loss_fn(y_batch_train, logits)
        grads = tape.gradient(loss_value, model.trainable_weights)
        optimizer.apply_gradients(zip(grads, model.trainable_weights))

        # Update training metric.
        train_acc_metric.update_state(y_batch_train, logits)

        # Log every 200 batches.
        if step % 200 == 0:
            print(
                "Training loss (for one batch) at step %d: %.4f"
                % (step, float(loss_value))
            )
            print("Seen so far: %d samples" % ((step + 1) * batch_size))

    # Display metrics at the end of each epoch.
    train_acc = train_acc_metric.result()
    print("Training acc over epoch: %.4f" % (float(train_acc),))

    # Reset training metrics at the end of each epoch
    train_acc_metric.reset_states()

    # Run a validation loop at the end of each epoch.
    for x_batch_val, y_batch_val in val_dataset:
        val_logits = model(x_batch_val, training=False)
        # Update val metrics
        val_acc_metric.update_state(y_batch_val, val_logits)
    val_acc = val_acc_metric.result()
    val_acc_metric.reset_states()
    print("Validation acc: %.4f" % (float(val_acc),))
    print("Time taken: %.2fs" % (time.time() - start_time))
Start of epoch 0
Training loss (for one batch) at step 0: 88.9958
Seen so far: 64 samples
Training loss (for one batch) at step 200: 2.2214
Seen so far: 12864 samples
Training loss (for one batch) at step 400: 1.3083
Seen so far: 25664 samples
Training loss (for one batch) at step 600: 0.8282
Seen so far: 38464 samples
Training acc over epoch: 0.7406
Validation acc: 0.8201
Time taken: 6.31s

Start of epoch 1
Training loss (for one batch) at step 0: 0.3276
Seen so far: 64 samples
Training loss (for one batch) at step 200: 0.4819
Seen so far: 12864 samples
Training loss (for one batch) at step 400: 0.5971
Seen so far: 25664 samples
Training loss (for one batch) at step 600: 0.5862
Seen so far: 38464 samples
Training acc over epoch: 0.8474
Validation acc: 0.8676
Time taken: 5.98s

Przyspieszenie swój etap treningowy z tf.function

Domyślną wykonawcze w TensorFlow 2 jest chętny wykonanie . W związku z tym nasza pętla treningowa powyżej wykonuje się chętnie.

Jest to świetne do debugowania, ale kompilacja wykresów ma zdecydowaną przewagę wydajności. Opisanie obliczeń jako statycznego wykresu umożliwia frameworkowi zastosowanie globalnych optymalizacji wydajności. Jest to niemożliwe, gdy framework jest zmuszony do chciwego wykonywania jednej operacji po drugiej, bez wiedzy o tym, co będzie dalej.

Możesz skompilować w statyczny wykres dowolną funkcję, która przyjmuje tensory jako dane wejściowe. Wystarczy dodać @tf.function dekorator na nim tak:

@tf.function
def train_step(x, y):
    with tf.GradientTape() as tape:
        logits = model(x, training=True)
        loss_value = loss_fn(y, logits)
    grads = tape.gradient(loss_value, model.trainable_weights)
    optimizer.apply_gradients(zip(grads, model.trainable_weights))
    train_acc_metric.update_state(y, logits)
    return loss_value

Zróbmy to samo z krokiem oceny:

@tf.function
def test_step(x, y):
    val_logits = model(x, training=False)
    val_acc_metric.update_state(y, val_logits)

Teraz powtórzmy naszą pętlę treningową z tym skompilowanym krokiem treningowym:

import time

epochs = 2
for epoch in range(epochs):
    print("\nStart of epoch %d" % (epoch,))
    start_time = time.time()

    # Iterate over the batches of the dataset.
    for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):
        loss_value = train_step(x_batch_train, y_batch_train)

        # Log every 200 batches.
        if step % 200 == 0:
            print(
                "Training loss (for one batch) at step %d: %.4f"
                % (step, float(loss_value))
            )
            print("Seen so far: %d samples" % ((step + 1) * batch_size))

    # Display metrics at the end of each epoch.
    train_acc = train_acc_metric.result()
    print("Training acc over epoch: %.4f" % (float(train_acc),))

    # Reset training metrics at the end of each epoch
    train_acc_metric.reset_states()

    # Run a validation loop at the end of each epoch.
    for x_batch_val, y_batch_val in val_dataset:
        test_step(x_batch_val, y_batch_val)

    val_acc = val_acc_metric.result()
    val_acc_metric.reset_states()
    print("Validation acc: %.4f" % (float(val_acc),))
    print("Time taken: %.2fs" % (time.time() - start_time))
Start of epoch 0
Training loss (for one batch) at step 0: 0.7921
Seen so far: 64 samples
Training loss (for one batch) at step 200: 0.7755
Seen so far: 12864 samples
Training loss (for one batch) at step 400: 0.1564
Seen so far: 25664 samples
Training loss (for one batch) at step 600: 0.3181
Seen so far: 38464 samples
Training acc over epoch: 0.8788
Validation acc: 0.8866
Time taken: 1.59s

Start of epoch 1
Training loss (for one batch) at step 0: 0.5222
Seen so far: 64 samples
Training loss (for one batch) at step 200: 0.4574
Seen so far: 12864 samples
Training loss (for one batch) at step 400: 0.4035
Seen so far: 25664 samples
Training loss (for one batch) at step 600: 0.7561
Seen so far: 38464 samples
Training acc over epoch: 0.8959
Validation acc: 0.9028
Time taken: 1.27s

Dużo szybciej, prawda?

Niskopoziomowa obsługa strat śledzonych przez model

Warstwy i modele rekurencyjnie śledzić wszelkie straty powstałe podczas przejściu do przodu warstwami tak nazwać self.add_loss(value) . Powstały lista skalarnych wartości strat są dostępne za pośrednictwem właściwości model.losses na koniec przełęczy przodu.

Jeśli chcesz wykorzystać te składniki straty, powinieneś je zsumować i dodać do głównej straty na etapie treningu.

Rozważ tę warstwę, która powoduje utratę regularyzacji aktywności:

class ActivityRegularizationLayer(layers.Layer):
    def call(self, inputs):
        self.add_loss(1e-2 * tf.reduce_sum(inputs))
        return inputs

Zbudujmy naprawdę prosty model, który go wykorzystuje:

inputs = keras.Input(shape=(784,), name="digits")
x = layers.Dense(64, activation="relu")(inputs)
# Insert activity regularization as a layer
x = ActivityRegularizationLayer()(x)
x = layers.Dense(64, activation="relu")(x)
outputs = layers.Dense(10, name="predictions")(x)

model = keras.Model(inputs=inputs, outputs=outputs)

Oto jak powinien teraz wyglądać nasz etap szkolenia:

@tf.function
def train_step(x, y):
    with tf.GradientTape() as tape:
        logits = model(x, training=True)
        loss_value = loss_fn(y, logits)
        # Add any extra losses created during the forward pass.
        loss_value += sum(model.losses)
    grads = tape.gradient(loss_value, model.trainable_weights)
    optimizer.apply_gradients(zip(grads, model.trainable_weights))
    train_acc_metric.update_state(y, logits)
    return loss_value

Streszczenie

Teraz wiesz już wszystko o korzystaniu z wbudowanych pętli treningowych i pisaniu własnych od podstaw.

Podsumowując, oto prosty, kompleksowy przykład, który łączy wszystko, czego nauczyłeś się w tym przewodniku: DCGAN wyszkolony na cyfrach MNIST.

Kompleksowy przykład: pętla treningowa GAN od podstaw

Możesz być zaznajomiony z generatywnymi sieciami kontradyktoryjnymi (GAN). Sieci GAN mogą generować nowe obrazy, które wyglądają prawie jak prawdziwe, poprzez poznanie ukrytej dystrybucji zestawu danych treningowych obrazów ("przestrzeń utajona" obrazów).

GAN składa się z dwóch części: modelu „generatora”, który odwzorowuje punkty w przestrzeni utajonej na punkty w przestrzeni obrazu, modelu „dyskryminatora”, klasyfikatora, który może odróżnić rzeczywiste obrazy (ze zbioru danych treningowych) od fałszywych obrazy (wyjście sieci generatora).

Pętla treningowa GAN wygląda tak:

1) Trenuj dyskryminator. - Wypróbuj partię losowych punktów w ukrytej przestrzeni. - Zamień punkty w fałszywe obrazy za pomocą modelu „generatora”. - Pobierz partię prawdziwych obrazów i połącz je z wygenerowanymi obrazami. - Trenuj model „dyskryminatora”, aby klasyfikować obrazy generowane i rzeczywiste.

2) Trenuj generator. - Próbuj losowe punkty w ukrytej przestrzeni. - Zamień punkty w fałszywe obrazy za pośrednictwem sieci „generatora”. - Pobierz partię prawdziwych obrazów i połącz je z wygenerowanymi obrazami. - Trenuj model „generatora”, aby „oszukiwał” dyskryminator i klasyfikował fałszywe obrazy jako prawdziwe.

O wiele bardziej szczegółowy przegląd sposobów Gans prac, patrz Głębokie Nauka o Pythonie .

Zaimplementujmy tę pętlę treningową. Najpierw stwórz dyskryminator, który ma klasyfikować fałszywe i prawdziwe cyfry:

discriminator = keras.Sequential(
    [
        keras.Input(shape=(28, 28, 1)),
        layers.Conv2D(64, (3, 3), strides=(2, 2), padding="same"),
        layers.LeakyReLU(alpha=0.2),
        layers.Conv2D(128, (3, 3), strides=(2, 2), padding="same"),
        layers.LeakyReLU(alpha=0.2),
        layers.GlobalMaxPooling2D(),
        layers.Dense(1),
    ],
    name="discriminator",
)
discriminator.summary()
Model: "discriminator"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (None, 14, 14, 64)        640       
_________________________________________________________________
leaky_re_lu (LeakyReLU)      (None, 14, 14, 64)        0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 7, 7, 128)         73856     
_________________________________________________________________
leaky_re_lu_1 (LeakyReLU)    (None, 7, 7, 128)         0         
_________________________________________________________________
global_max_pooling2d (Global (None, 128)               0         
_________________________________________________________________
dense_4 (Dense)              (None, 1)                 129       
=================================================================
Total params: 74,625
Trainable params: 74,625
Non-trainable params: 0
_________________________________________________________________

Następnie pozwolić na utworzenie sieci generator, który włącza utajone wektorów do wyjść kształcie (28, 28, 1) (obejmując cyfry MNIST)

latent_dim = 128

generator = keras.Sequential(
    [
        keras.Input(shape=(latent_dim,)),
        # We want to generate 128 coefficients to reshape into a 7x7x128 map
        layers.Dense(7 * 7 * 128),
        layers.LeakyReLU(alpha=0.2),
        layers.Reshape((7, 7, 128)),
        layers.Conv2DTranspose(128, (4, 4), strides=(2, 2), padding="same"),
        layers.LeakyReLU(alpha=0.2),
        layers.Conv2DTranspose(128, (4, 4), strides=(2, 2), padding="same"),
        layers.LeakyReLU(alpha=0.2),
        layers.Conv2D(1, (7, 7), padding="same", activation="sigmoid"),
    ],
    name="generator",
)

Oto kluczowy fragment: pętla treningowa. Jak widać, jest to dość proste. Funkcja kroku treningowego zajmuje tylko 17 linii.

# Instantiate one optimizer for the discriminator and another for the generator.
d_optimizer = keras.optimizers.Adam(learning_rate=0.0003)
g_optimizer = keras.optimizers.Adam(learning_rate=0.0004)

# Instantiate a loss function.
loss_fn = keras.losses.BinaryCrossentropy(from_logits=True)


@tf.function
def train_step(real_images):
    # Sample random points in the latent space
    random_latent_vectors = tf.random.normal(shape=(batch_size, latent_dim))
    # Decode them to fake images
    generated_images = generator(random_latent_vectors)
    # Combine them with real images
    combined_images = tf.concat([generated_images, real_images], axis=0)

    # Assemble labels discriminating real from fake images
    labels = tf.concat(
        [tf.ones((batch_size, 1)), tf.zeros((real_images.shape[0], 1))], axis=0
    )
    # Add random noise to the labels - important trick!
    labels += 0.05 * tf.random.uniform(labels.shape)

    # Train the discriminator
    with tf.GradientTape() as tape:
        predictions = discriminator(combined_images)
        d_loss = loss_fn(labels, predictions)
    grads = tape.gradient(d_loss, discriminator.trainable_weights)
    d_optimizer.apply_gradients(zip(grads, discriminator.trainable_weights))

    # Sample random points in the latent space
    random_latent_vectors = tf.random.normal(shape=(batch_size, latent_dim))
    # Assemble labels that say "all real images"
    misleading_labels = tf.zeros((batch_size, 1))

    # Train the generator (note that we should *not* update the weights
    # of the discriminator)!
    with tf.GradientTape() as tape:
        predictions = discriminator(generator(random_latent_vectors))
        g_loss = loss_fn(misleading_labels, predictions)
    grads = tape.gradient(g_loss, generator.trainable_weights)
    g_optimizer.apply_gradients(zip(grads, generator.trainable_weights))
    return d_loss, g_loss, generated_images

Załóżmy trenować naszą Gan, poprzez wielokrotne wywołanie train_step na partiach obrazu.

Ponieważ nasz dyskryminator i generator są convnetami, będziesz chciał uruchomić ten kod na GPU.

import os

# Prepare the dataset. We use both the training & test MNIST digits.
batch_size = 64
(x_train, _), (x_test, _) = keras.datasets.mnist.load_data()
all_digits = np.concatenate([x_train, x_test])
all_digits = all_digits.astype("float32") / 255.0
all_digits = np.reshape(all_digits, (-1, 28, 28, 1))
dataset = tf.data.Dataset.from_tensor_slices(all_digits)
dataset = dataset.shuffle(buffer_size=1024).batch(batch_size)

epochs = 1  # In practice you need at least 20 epochs to generate nice digits.
save_dir = "./"

for epoch in range(epochs):
    print("\nStart epoch", epoch)

    for step, real_images in enumerate(dataset):
        # Train the discriminator & generator on one batch of real images.
        d_loss, g_loss, generated_images = train_step(real_images)

        # Logging.
        if step % 200 == 0:
            # Print metrics
            print("discriminator loss at step %d: %.2f" % (step, d_loss))
            print("adversarial loss at step %d: %.2f" % (step, g_loss))

            # Save one generated image
            img = tf.keras.preprocessing.image.array_to_img(
                generated_images[0] * 255.0, scale=False
            )
            img.save(os.path.join(save_dir, "generated_img" + str(step) + ".png"))

        # To limit execution time we stop after 10 steps.
        # Remove the lines below to actually train the model!
        if step > 10:
            break
Start epoch 0
discriminator loss at step 0: 0.69
adversarial loss at step 0: 0.69

Otóż ​​to! Otrzymasz ładnie wyglądające fałszywe cyfry MNIST już po około 30 sekundach treningu na procesorze graficznym Colab.