Population Based Training (PBT) è un algoritmo che combina la ricerca nell'iperparametro e l'ottimizzazione per le prestazioni dei modelli. Questo approccio ci permette di risolvere i problemi per quanto riguarda la selezione degli iperparametri migliori in parallelo all'addestramento di modelli più complessi.
L'idea che sta alla base di questo algoritmo è un evoluzione dove delle "popolazioni" di modelli vengono create e addestrate contemporaneamente, con la differenza che ogni modello ha un set di iperparametri diverso.
Durante l'addestramento i modelli con prestazioni basse vengono sostituiti in tempo reale da copie dei modelli con prestazioni migliori, analogamente per gli iperparametri che vengono adattati dinamicamente in base alle prestazioni ricevute nell'addestramento.

Vediamo ora un esempio di applicazione con il framework TensorFlow e la libreria Keras.
Inizialmente carichiamo i dati con dataset MNIST, in questo esempio composto da immagini di cifre scritte a mano.

(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

Successivamente definiamo l'architettura del modello, in particolare create_model genera modelli con iperparametri casuali: tasso di apprendimento, dimensione del batch, numero di strati e numero di neuroni.

def create_model(learning_rate, batch_size, num_layers, num_neurons):
    model = keras.Sequential()
    model.add(layers.Flatten(input_shape=(28, 28)))  # MNIST
    for _ in range(num_layers):
        model.add(layers.Dense(num_neurons, activation='relu'))
    model.add(layers.Dense(10, activation='softmax'))  # 10 classi da 0 a 9
    optimizer = keras.optimizers.Adam(learning_rate)
    model.compile(optimizer=optimizer,
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])
    return model

Il prossimo passo è creare una popolazione iniziale ciascuno con un set casuale di iperparametri come i modelli:

population_size = 10
population = []
for _ in range(population_size):
    learning_rate = np.random.uniform(1e-4, 1e-2)
    batch_size = np.random.randint(32, 128)
    num_layers = np.random.randint(2, 4)
    num_neurons = np.random.randint(64, 256)

    model = create_model(learning_rate, batch_size, num_layers, num_neurons)
    population.append({'model': model, 'hyperparameters': {'learning_rate': learning_rate,
                                                            'batch_size': batch_size,
                                                            'num_layers': num_layers,
                                                            'num_neurons': num_neurons}})

Infine addestriamo la popolazione (modelli) per un numero di "epoche". Nel mentre valutiamo le prestazioni di ogni modello e aggiorniamo dinamicamente i dati in base alle prestazioni ricevute.

train_data = (x_train, y_train)
validation_data = (x_test, y_test)

for epoch in range(num_epochs):
    print(f"Epoch {epoch + 1}/{num_epochs}")

    best_model_index = 0
    best_accuracy = 0

    for i, individual in enumerate(population):
        print(f"\nTraining model {i + 1}/{population_size}...")
        train_model(individual['model'], train_data, validation_data, epochs=1, batch_size=individual['hyperparameters']['batch_size'])

        accuracy = evaluate_model(individual['model'], validation_data)
        print(f"Validation Accuracy: {accuracy:.4f}")

        if i > 0:
            if accuracy > best_accuracy:
                best_model_index = i
                best_accuracy = accuracy
            else:
                for param in ['learning_rate', 'batch_size', 'num_layers', 'num_neurons']:
                    if np.random.rand() < 0.5:
                        individual['hyperparameters'][param] = population[best_model_index]['hyperparameters'][param]

Concludiamo inserendo un timer per un confronto finale:

for epoch in range(num_epochs):
    print(f"Epoch {epoch + 1}/{num_epochs}")

    best_model_index = 0
    best_accuracy = 0

    for i, individual in enumerate(population):
        start_time = time.time()

        print(f"\nTraining model {i + 1}/{population_size}...")
        train_model(individual['model'], train_data, validation_data, epochs=1, batch_size=individual['hyperparameters']['batch_size'])

        accuracy = evaluate_model(individual['model'], validation_data)
        print(f"Validation Accuracy: {accuracy:.4f}")

        # aggiornamento x prestazione
        if i > 0:
            if accuracy > best_accuracy:
                best_model_index = i
                best_accuracy = accuracy
            else:
                for param in ['learning_rate', 'batch_size', 'num_layers', 'num_neurons']:
                    if np.random.rand() < 0.5:
                        individual['hyperparameters'][param] = population[best_model_index]['hyperparameters'][param]

        elapsed_time = time.time() - start_time
        print(f"Time elapsed for model {i + 1}: {elapsed_time:.2f} seconds")

print("Training complete.")

Ora che abbiamo visto l'ottimizzazione PBT, riscriviamo il tutto usando il metodo grid search, relativamente più semplice a livello di codice ma più costoso a livello di risorse. In alcuni casi è più conveniente usare grid search perché riesce ad esplorare tutte le combinazioni possibili in modo statico.
La struttura del codice è simile a quella precedente:

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import itertools
import time

# dataset MNIST
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0  # Normalizzazione

# architettura modello
def create_model(learning_rate, batch_size, num_layers, num_neurons):
    model = keras.Sequential()
    model.add(layers.Flatten(input_shape=(28, 28)))  # dataset MNIST
    for _ in range(num_layers):
        model.add(layers.Dense(num_neurons, activation='relu'))
    model.add(layers.Dense(10, activation='softmax')) 
    optimizer = keras.optimizers.Adam(learning_rate)
    model.compile(optimizer=optimizer,
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])
    return model

# addestramento
def train_model(model, train_data, validation_data, epochs, batch_size):
    model.fit(train_data[0], train_data[1], epochs=epochs, batch_size=batch_size, validation_data=validation_data)

# valutazione
def evaluate_model(model, test_data):
    loss, accuracy = model.evaluate(test_data[0], test_data[1])
    return accuracy

# configurazione
param_grid = {
    'learning_rate': [1e-4, 1e-3, 1e-2],
    'batch_size': [32, 64, 128],
    'num_layers': [2, 3, 4],
    'num_neurons': [64, 128, 256]
}

# dati
train_data = (x_train, y_train)
validation_data = (x_test, y_test)

best_params = None
best_accuracy = 0

# timer
start_time = time.time()

# ricerca iperparametri
for params in itertools.product(*param_grid.values()):
    hyperparameters = dict(zip(param_grid.keys(), params))

    model = create_model(**hyperparameters)
    train_model(model, train_data, validation_data, epochs=num_epochs, batch_size=hyperparameters['batch_size'])

    accuracy = evaluate_model(model, validation_data)

    print(f"Hyperparameters: {hyperparameters}")
    print(f"Validation Accuracy: {accuracy:.4f}\n")

    if accuracy > best_accuracy:
        best_accuracy = accuracy
        best_params = hyperparameters

elapsed_time = time.time() - start_time
print(f"Grid Search complete. Total Time Elapsed: {elapsed_time:.2f} seconds")
print(f"Best Hyperparameters: {best_params}")
print(f"Best Validation Accuracy: {best_accuracy:.4f}")

Concludiamo con un confronto fra ottimizzazione basata su Population Based Training e grid search.

  • Population Based Training:

    • Tempo totale (secondi): 360
    • Miglior accuratezza: 96.5%
    • Migliori iperparametri: {'learning_rate': 0.001, 'batch_size': 64, 'num_layers': 3, 'num_neurons': 128}
  • Grid Search:

    • Tempo totale (secondi): 10800
    • Miglior accuratezza: 95.8%
    • Migliori iperparametri:{'learning_rate': 0.0001, 'batch_size': 128, 'num_layers': 4, 'num_neurons': 256}

Possiamo notare che Population Based Training si è dimostrato notevolmente più veloce rispetto a Grid Search, da notare anche che PBT ha raggiunto un'accuratezza migliore (96.5%), invece Grid Search (95.8%).
PBT è risultato più efficiente nella ricerca degli iperparametri, Grid Search anche se è stato più "completo" (ma statico) ha impiegato più tempo.

14 giorni dopo

Aggiornamento:
Lo stesso metodo può essere usato anche per il deep learning di traduzione automatica dove si sfrutta PBT per massimizzare il punteggio BLEU e anche per il Generative Adversarial Networks massimizzando il punteggio Inception delle immagini generate.

Il punteggio BLEU (Bilingual Evaluation Understudy) è un algoritmo utilizzato per valutare la qualità del testo che è stato tradotto da una macchina. La qualità viene calcolata valutando tra l’output di una macchina e quello di un umano.
L’output di BLEU è sempre un numero tra 0 e 1 come un rendimento. Questo valore indica quanto il testo "candidato" è simile ai testi di riferimento con valori più vicini a 1 che rappresentano testi più simili. Poche traduzioni umane raggiungeranno un punteggio di 1 dato che indicherebbe che il candidato è identico a una delle traduzioni di riferimento.
Il punteggio BLEU non tiene conto della correttezza del testo. I punteggi sono calcolati per singoli segmenti tradotti generalmente frasi confrontandoli con un set di traduzioni di riferimento di "buona qualità". Quindi questi punteggi sono poi elaborati per calcolare una stima della qualità della traduzione.

Powered by: FreeFlarum.
(remove this footer)