angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Automatisation de l'optimisation des hyperparamètres de Keras avec Talos

Un modèle Deep Learning n'est PAS une boîte noire. Il faut le régler pour obtenir de bonnes performances.

2 février 2022
post main image
https://www.pexels.com/nl-nl/@monoar-rahman-22660

Dans les deux billets précédents, je vous ai montré mes premiers pas avec Keras. J'ai utilisé des exemples trouvés sur Internet et modifié l'ensemble de données en quelque chose de trivial, c'est-à-dire que j'ai généré les données moi-même et que je connais les valeurs attendues. Mais je vous ai également dit que je n'avais aucune idée de la raison pour laquelle des paramètres comme les neurones, les époques, le batch_size avaient ces valeurs.
Donc, ce que nous avons n'est pas vraiment une boîte noire. À l'extérieur, il y a aussi des interrupteurs et des vis qui nécessitent toute notre attention. Dans ce billet, j'utilise Talos, 'Hyperparameter Optimization for Keras, TensorFlow (tf.keras) and PyTorch', voir les liens ci-dessous, qui est destiné à automatiser le processus de sélection des paramètres optimaux.

A la fin de ce post se trouve le code pour que vous puissiez l'essayer vous-même.

Hyperparamètres

Les paramètres comme les neurones, les époques et la taille des lots sont appelés hyperparamètres, et leur réglage est essentiel pour une bonne performance du modèle. Il existe sur Internet des articles intéressants sur la façon d'ajuster ces paramètres. On peut appeler cela le Saint Graal des réseaux neuronaux : l'optimisation des hyperparamètres. Site Web de Merriam-Webster : Le Saint Graal est un objet ou un but recherché pour sa grande signification. Si vous ne savez pas ce que vous faites, il est facile de sélectionner des paramètres non optimaux, voire totalement erronés.

Fonctions de perte

Pour effectuer une optimisation, nous avons besoin de valeurs qui indiquent les performances de notre modèle. Ces valeurs sont calculées par les fonctions de perte. Nous utilisons différentes fonctions de perte pour la régression et la classification. Voir par exemple l'article 'Overview of loss functions for Machine Learning', voir les liens ci-dessous.

Fonctions de perte de régression :

  • Mean Squared Error (MSE)
  • Mean Absolute Error (MAE)
  • Huber
  • Log-Cosh
  • Quantile

Class Fonctions de perte de qualification :

  • Binary Cross Entropy
  • Multi-Class Cross Entropy

Talos résumé

J'aime bien la phrase de l'article " Hyperparameter Optimization with Keras ", voir les liens ci-dessous :

Ne vous méprenez pas ; MÊME SI NOUS FAISONS GET LE MÉTRIQUE DE PERFORMANCE BIEN (oui je crie), nous devons considérer ce qui se passe dans le processus d'optimisation d'un modèle.

Avec Talos , nous paramétrons notre modèle. Le nombre de combinaisons d'époques, de batch_size, etc. peut être énorme. Talos choisit au hasard un certain nombre de combinaisons et crée le nouveau modèle avec des données d'entraînement et de validation. Cela peut prendre quelques minutes, mais aussi des heures, voire des jours.
Une fois l'analyse terminée, nous pouvons utiliser les scores, modifier les valeurs des paramètres et/ou en ajouter d'autres et recommencer. Pendant ce temps, nous pouvons faire autre chose, comme boire un café, parler à un ami ou, mieux encore, essayer d'en savoir plus sur l'optimisation Machine Learning . Voyons comment cela fonctionne.

Exemple

Je vais exécuter Talos pour un modèle Neural Network très simple basé sur 'Keras 101: A simple (and interpretable) Neural Network model for House Pricing regression', voir les liens ci-dessous.
Le jeu de données est généré avec cette fonction :

# define input sequence
def fx(x0, x1):
    y = x0 + 2*x1
    return y

Le modèle :

model = Sequential()
# add layers
model.add(Dense(100, input_shape=(2,), activation='relu', name='layer_input'))
model.add(Dense(50, activation='relu', name='layer_hidden_1')
model.add(Dense(1, activation='linear', name='layer_output')
# compile
model.compile(optimizer='adam', loss='mse', metrics=['mean_absolute_error'])

Et la fonction d'ajustement :

history = model.fit(X_train, y_train, epochs=100, validation_split=0.05)

Comme je vous l'ai déjà dit, je ne sais pas pourquoi les paramètres ont ces valeurs. Ok, enfin un peu.

Talos

Pour utiliser ceci dans Talos , nous remplaçons les paramètres codés en dur par des variables qui peuvent être contrôlées par Talos. Tout d'abord, nous créons un dictionnaire de paramètres avec les valeurs que nous voulons faire varier. Ne commencez pas avec tous les paramètres que vous voulez modifier. Avant que vous le sachiez, vous attendrez et attendrez encore. Aussi, j'ai commencé par exemple le paramètre batch_size avec deux valeurs éloignées l'une de l'autre pour avoir une idée. Avec les paramètres ci-dessous, Talos a fait 16 exécutions et le temps total sur mon PC (sans GPU) était de 42 secondes.

# parameters
p = dict(
	first_neuron=[24, 192],
	activation=['relu', 'elu'],
	epochs=[50, 200],
	batch_size=[8, 32]
)

Ensuite, nous changeons le modèle pour Talos :

model = Sequential()
# add layers
model.add(Dense(
    params['first_neuron'],
    input_shape=(2,), 
    activation=params['activation'],
    name='layer_input')
)
model.add(Dense(
    50,
    activation=params['activation'],
    name='layer_hidden_1')
)
model.add(Dense(
    1,
    activation='linear',
    name='layer_output')
)
# compile
model.compile(
    optimizer='adam',
    loss='mse',
    metrics=['mean_absolute_error'],
)

Et la fonction d'ajustement devient :

history = model.fit(
    x=x_train, 
    y=y_train,
    validation_data=[x_val, y_val],
    epochs=params['epochs'],
    batch_size=params['batch_size'],
    verbose=0,
)

Exécuter Talos et analyser.

C'est l'heure de l'exécution ! Après l'exécution, j'imprime le tableau complet des résultats.

# perform scan
so = talos.Scan(
	x=X,
	y=y,
	model=dlm.model2scan, 
	params=p,
	experiment_name='model2scan',
	val_split=0.3,
)
print('scan details {}'.format(so.details))
print('analyze ...')
a = talos.Analyze(so)
a_table = a.table('val_loss', sort_by='val_loss', exclude=['start', 'end', 'duration'])
print('a_table = \n{}'.format(a_table))

Voici le tableau. Notez qu'il est trié par val_loss, ce qui signifie que la dernière ligne est la gagnante :

   activation  epochs  round_epochs  first_neuron  val_mean_absolute_error  mean_absolute_error  batch_size   val_loss        loss
4        relu      50            50            24                 7.786795            10.516150          32  94.053894  143.701889
12        elu      50            50            24                 5.021943             2.713378          32  30.090796   10.598367
13        elu      50            50           192                 2.880891             2.392172          32  10.355382    7.829942
5        relu      50            50           192                 2.306647             1.435794          32   8.680595    3.228125
8         elu      50            50            24                 2.205534             1.599179           8   5.929257    3.529320
9         elu      50            50           192                 1.564395             0.991934           8   3.059430    1.329003
1        relu      50            50           192                 0.813473             0.418985           8   1.315141    0.284857
14        elu     200           200            24                 0.934512             0.557581          32   1.210560    0.448432
0        relu      50            50            24                 0.680375             0.463401           8   0.818936    0.343270
15        elu     200           200           192                 0.773358             0.466824          32   0.776512    0.313476
6        relu     200           200            24                 0.510728             0.256091          32   0.515720    0.105524
7        relu     200           200           192                 0.473588             0.219744          32   0.471352    0.076440
2        relu     200           200            24                 0.562183             0.213781           8   0.431688    0.075045
3        relu     200           200           192                 0.261547             0.062857           8   0.179677    0.006133
10        elu     200           200            24                 0.327835             0.207104           8   0.140866    0.063864
11        elu     200           200           192                 0.218479             0.116624           8   0.071611    0.028682

En regardant les données, nous voyons que epochs=200 donne de bien meilleurs résultats que epochs=50. Nous pouvons le changer à 100 pour voir si cela est également bon. Le batch_size=8 donne aussi de meilleurs résultats que le batch_size=32. Nous pouvons le changer en 16 pour voir si c'est également bon. Nous pouvons aussi essayer de réduire first_neuron. Les nouveaux paramètres pour Talos :

p = dict(
	first_neuron=[128, 192],
	activation=['relu', 'elu'],
	epochs=[100, 200],
	batch_size=[8, 16]
)

Faisons un nouvel essai avec ces valeurs. Le résultat :

    batch_size activation  epochs  val_mean_absolute_error  val_loss      loss  round_epochs  first_neuron  mean_absolute_error
13          16        elu     100                 0.884498  1.053809  0.726826           100           192             0.706334
12          16        elu     100                 0.779756  0.834097  0.669265           100           128             0.696979
4           16       relu     100                 0.415210  0.235131  0.124713           100           128             0.291552
9            8        elu     100                 0.395605  0.204896  0.157592           100           192             0.321956
5           16       relu     100                 0.290187  0.109819  0.064319           100           192             0.211642
15          16        elu     200                 0.234876  0.108920  0.070559           200           192             0.220270
14          16        elu     200                 0.274542  0.107709  0.075080           200           128             0.216383
0            8       relu     100                 0.280294  0.104701  0.049495           100           128             0.188951
1            8       relu     100                 0.268457  0.100130  0.041598           100           192             0.160658
6           16       relu     200                 0.228168  0.079410  0.033531           200           128             0.138397
11           8        elu     200                 0.175497  0.070203  0.041988           200           192             0.154247
10           8        elu     200                 0.150712  0.057377  0.021882           200           128             0.108372
8            8        elu     100                 0.205943  0.055572  0.048045           100           128             0.187398
2            8       relu     200                 0.182463  0.046856  0.018731           200           128             0.096890
7           16       relu     200                 0.135524  0.025975  0.010142           200           192             0.073783
3            8       relu     200                 0.078327  0.009304  0.004181           200           192             0.042721

Les meilleurs résultats se sont améliorés mais pas tant que ça. Le epochs=200 est toujours le meilleur ainsi que le batch_size=8. Les paramètres finaux sont :

p = dict(
	first_neuron=192,
	activation='relu',
	epochs=200,
	batch_size=8,
)

Code complet

Voici le code au cas où vous voudriez essayer vous-même. Sélectionnez 'run_optimizer' pour exécuter Talos. L'ensemble de données est divisé en données de formation et en données de test. Les données de formation sont à nouveau divisées en données de formation et en données de validation.

Après avoir exécuté l'optimiseur, vous pouvez introduire les nouvelles valeurs des paramètres dans le modèle, générer des graphiques, exécuter une évaluation par rapport aux données de test et effectuer quelques prédictions.

# optimizing keras hyperparameters with talos
from keras.models import Sequential, load_model
from keras.layers import Dense
import numpy as np
from plotly.subplots import make_subplots
import plotly.graph_objects as go
from sklearn.model_selection import train_test_split
import talos

# use plotly or pyplot
from matplotlib import pyplot

# your input: select talos optimizer or normal operation
run_optimizer = False
#run_optimizer = True

# your input: train model or use saved model
use_saved_model = False
#use_saved_model = True

# create dataset
def fx(x0, x1):
    y = x0 + 2*x1
    return y

X_items = []
y_items = []
for x0 in range(0, 18, 3):
    for x1 in range(2, 27, 3):
        y = fx(x0, x1)
        X_items.append([x0, x1])
        y_items.append(y)

X = np.array(X_items).reshape((-1, 2))
y = np.array(y_items)
print('X = {}'.format(X))
print('y = {}'.format(y))
X_data_shape = X.shape
print('X_data_shape = {}'.format(X_data_shape))

class DLM:
    
    def __init__(
        self,
        model_name='my_model',
    ):
        self.model_name = model_name
        self.layer_input_shape=(2, )
        # your input: final model parameters
        self.params = dict(
            # layers
            first_neuron=192,
            activation='relu',
            # compile
            # fit
            val_split=0.3,
            epochs=200,
            batch_size=8,
            verbose=0,
        )

    def data_split_train_test(
        self,
        X,
        y,
    ):
        self.X_train, self.X_test, self.y_train, self.y_test = train_test_split(X, y, test_size=0.3, random_state=1)
        print('self.X_train = {}'.format(self.X_train))
        print('self.X_test = {}'.format(self.X_test))
        print('self.y_train = {}'.format(self.y_train))
        print('self.y_test = {}'.format(self.y_test))

        print('training data row count = {}'.format(len(self.y_train)))
        print('test data row count = {}'.format(len(self.y_test)))

        X_train_data_shape = self.X_train.shape
        print('X_train_data_shape = {}'.format(X_train_data_shape))

    def get_model(
        self,
    ):
        self.model = self.get_model_for_params(self.params)
        return self.model

    def get_model_for_params(self, params):
        model = Sequential()
        # add layers
        model.add(Dense(
            params['first_neuron'],
            input_shape=self.layer_input_shape, 
            activation=params['activation'],
            name='layer_input')
        )
        model.add(Dense(
            50,
            activation=params['activation'],
            name='layer_hidden_1')
        )
        model.add(Dense(
            1,
            activation='linear',
            name='layer_output')
        )
        # compile
        model.compile(
            optimizer='adam',
            loss='mse',
            metrics=['mean_absolute_error'],
        )
        return model

    def model_summary(
        self,
        model,
    ):
        model.summary()

    def fit(
        self, 
        model,
        plot=False,
    ):
        # split training data 
        X_train, X_val, y_train, y_val = train_test_split(self.X_train, self.y_train, test_size=self.params['val_split'], random_state=1)
        print('X_train = {}'.format(X_train))
        print('y_train = {}'.format(y_train))
        print('X_val = {}'.format(X_val))
        print('y_val = {}'.format(y_val))

        print('training data row count = {}'.format(len(y_train)))
        print('validation data row count = {}'.format(len(y_val)))

        history = self.fit_model(model, X_train, y_train, X_val, y_val, self.params)

        if plot:

            fig = go.Figure()
            fig.add_trace(go.Scattergl(y=history.history['loss'], name='Train'))
            fig.add_trace(go.Scattergl(y=history.history['val_loss'], name='Valid'))
            fig.update_layout(height=500, width=700, xaxis_title='Epoch', yaxis_title='Loss')
            fig.show()

            fig = go.Figure()
            fig.add_trace(go.Scattergl(y=history.history['mean_absolute_error'], name='Train'))
            fig.add_trace(go.Scattergl(y=history.history['val_mean_absolute_error'], name='Valid'))
            fig.update_layout(height=500, width=700, xaxis_title='Epoch', yaxis_title='Mean Absolute Error')
            fig.show() 

            pyplot.plot(history.history['loss'], label='Train')
            pyplot.plot(history.history['val_loss'], label='Valid')
            pyplot.legend()
            #pyplot.show()
            pyplot.savefig('ex_dl_loss.png')

        return history

    def fit_model(self, model, x_train, y_train, x_val, y_val, params):
        history = model.fit(
            x=x_train, 
            y=y_train,
            validation_data=[x_val, y_val],
            epochs=params['epochs'],
            batch_size=params['batch_size'],
            verbose=0,
        )
        return history

    def model2scan(self, x_train, y_train, x_val, y_val, params):
        model = self.get_model_for_params(params)
        history = self.fit_model(model, x_train, y_train, x_val, y_val, params)
        return history, model

    def evaluate(
        self, 
        model,
    ):
        score = model.evaluate(self.X_test, self.y_test)
        print('test data - loss = {}'.format(score[0]))
        print('test data - mean absolute error = {}'.format(score[1]))
        return score

    def predict(
        self,
        model,
        x0,
        x1,
        fx=None,
    ):
        x = np.array([[x0, x1]]).reshape((-1, 2))
        predictions = model.predict(x)
        expected = ''
        if fx is not None:
            expected = ', expected = {}'.format(fx(x0, x1))
        print('for x = {}, predictions = {}{}'.format(x, predictions, expected))
        return predictions

    def save_model(
        self,
        model,
    ):
        model.save(self.model_name)

    def load_saved_model(
        self,
    ):
        self.model = load_model(self.model_name)
        return self.model


dlm = DLM()

if not run_optimizer:
    # create & save or used saved
    if use_saved_model:
        model = dlm.load_saved_model()    
    else:
        dlm.data_split_train_test(X, y)
        model = dlm.get_model()
        # remove plot=True for no plot
        dlm.fit(model, plot=True)
        dlm.evaluate(model)
        dlm.save_model(model)

    # predict
    dlm.predict(model, 4, 17, fx=fx)
    dlm.predict(model, 23, 79, fx=fx)
    dlm.predict(model, 40, 33, fx=fx)
    dlm.predict(model, 140, 68, fx=fx)
else:
    # talos
    # your input: parameters run 1
    p = dict(
        first_neuron=[24, 192],
        activation=['relu', 'elu'],
        epochs=[50, 200],
        batch_size=[8, 32]
    )
    # your input: parameters run 2 (change p2 to p)
    p2 = dict(
        first_neuron=[128, 192],
        activation=['relu', 'elu'],
        epochs=[100, 200],
        batch_size=[8, 16]
    )
    # perform scan
    so = talos.Scan(
        x=X,
        y=y,
        model=dlm.model2scan, 
        params=p,
        experiment_name='model2scan',
        val_split=0.3,
    )
    print('scan details {}'.format(so.details))
    print('analyze ...')
    a = talos.Analyze(so)
    # dump table
    a_table = a.table('val_loss', sort_by='val_loss', exclude=['start', 'end', 'duration'])
    print('a_table = \n{}'.format(a_table))

Quelques réflexions

Convergeons-nous vraiment dans la bonne direction ? Il s'agit d'un problème très complexe. Nous avons un monde à N dimensions avec des hauts et des bas partout. Cela signifie que le point de départ peut être très important. En fin de compte, vous devriez toujours faire un essai avec autant de paramètres que possible. Si vous disposez d'un grand ensemble de données, vous devez commencer par le réduire en choisissant des échantillons aléatoires.

J'espère qu'il est clair que vous devez avoir une très bonne compréhension de ce que vous faites. Mais un système dont la vitesse est optimisée (coûteux GPU) aide aussi beaucoup.

Résumé

Talos est un très bon outil qui fait beaucoup de travail pour vous. La documentation pourrait être améliorée mais je ne me plains pas. Il a beaucoup plus de fonctionnalités mais je ne les ai pas toutes essayées. Je n'ai pas pu faire fonctionner mes exemples (en plusieurs étapes) de univariate LSTM avec cet outil en raison de la nature plus complexe de l'ensemble de données. Je dois examiner cela de plus près.
Pouvons-nous optimiser l'optimiseur ? Bien sûr. Dans une prochaine étape, nous pourrions prendre les résultats du tableau et les faire analyser automatiquement pour faire une nouvelle sélection des paramètres pour la prochaine exécution.

Liens / crédits

10 Hyperparameter optimization frameworks
https://towardsdatascience.com/10-hyperparameter-optimization-frameworks-8bc87bc8b7e3

5 Regression Loss Functions All Machine Learners Should Know
https://heartbeat.comet.ml/5-regression-loss-functions-all-machine-learners-should-know-4fb140e9d4b0

How do I choose the optimal batch size?
https://ai.stackexchange.com/questions/8560/how-do-i-choose-the-optimal-batch-size

How to Develop LSTM Models for Time Series Forecasting
https://machinelearningmastery.com/how-to-develop-lstm-models-for-time-series-forecasting

How to get reproducible results in keras
https://stackoverflow.com/questions/32419510/how-to-get-reproducible-results-in-keras

How to Manually Optimize Machine Learning Model Hyperparameters
https://machinelearningmastery.com/manually-optimize-hyperparameters

How to tune the number of epochs and batch_size in Keras-tuner?
https://kegui.medium.com/how-to-tune-the-number-of-epochs-and-batch-size-in-keras-tuner-c2ab2d40878d

Hyperparameter Optimization for Keras, TensorFlow (tf.keras) and PyTorch
https://github.com/autonomio/talos

Hyperparameter Optimization with Keras
https://towardsdatascience.com/hyperparameter-optimization-with-keras-b82e6364ca53

Keras 101: A simple (and interpretable) Neural Network model for House Pricing regression
https://towardsdatascience.com/keras-101-a-simple-and-interpretable-neural-network-model-for-house-pricing-regression-31b1a77f05ae

Overview of loss functions for Machine Learning
https://medium.com/analytics-vidhya/overview-of-loss-functions-for-machine-learning-61829095fa8a

What is batch size in neural network?
https://stats.stackexchange.com/questions/153531/what-is-batch-size-in-neural-network

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.