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.
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.