Guida alla scrittura di callback TensorFlow/Keras personalizzati

Introduzione

Supponiamo di volere che il tuo modello Keras abbia un comportamento specifico durante l'addestramento, la valutazione o la previsione. Ad esempio, potresti voler salvare il tuo modello in ogni epoca di addestramento. Un modo per farlo è usare i callback.

In generale, i callback sono funzioni che vengono chiamate quando si verifica un evento e vengono passate come argomenti ad altre funzioni. Nel caso di Keras, sono uno strumento per personalizzare il comportamento del tuo modello, sia durante l'allenamento, la valutazione o l'inferenza. Alcune applicazioni sono la registrazione, la persistenza del modello, l'arresto anticipato o la modifica della velocità di apprendimento. Questo viene fatto passando un elenco di Callback come argomenti per keras.Model.fit(),keras.Model.evaluate() or keras.Model.predict().

Alcuni casi d'uso comuni per le richiamate sono la modifica della velocità di apprendimento, la registrazione, il monitoraggio e l'interruzione anticipata della formazione. Keras ha una serie di callback integrate, dettagliate
nella documentazione
.

Tuttavia, alcune applicazioni più specifiche potrebbero richiedere una richiamata personalizzata. Per esempio, implementare il riscaldamento della frequenza di apprendimento con un decadimento del coseno dopo un periodo di attesa non è attualmente integrato, ma è ampiamente utilizzato e adottato come utilità di pianificazione.

Classe di callback e relativi metodi

Keras ha una classe di callback specifica, keras.callbacks.Callback, con metodi che possono essere richiamati durante l'addestramento, il test e l'inferenza a livello globale, batch o epoca. In modo da creare richiamate personalizzate, dobbiamo creare una sottoclasse e sovrascrivere questi metodi.

I keras.callbacks.Callback class ha tre tipi di metodi:

  • metodi globali: chiamati all'inizio o alla fine di fit(), evaluate() ed predict().
  • metodi a livello di batch: richiamati all'inizio o alla fine dell'elaborazione di un batch.
  • metodi a livello di epoca: richiamati all'inizio o alla fine di un batch di addestramento.

Nota: Ogni metodo ha accesso a un dict chiamato logs. Le chiavi ei valori di logs sono contestuali: dipendono dall'evento che chiama il metodo. Inoltre, abbiamo accesso al modello all'interno di ogni metodo tramite il self.model attributo.

Diamo un'occhiata a tre esempi di callback personalizzati: uno per la formazione, uno per la valutazione e uno per la previsione. Ognuno stamperà in ogni fase ciò che il nostro modello sta facendo e a quali registri abbiamo accesso. Ciò è utile per comprendere cosa è possibile fare con i callback personalizzati in ogni fase.

Iniziamo definendo un modello giocattolo:

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

model = keras.Sequential()
model.add(keras.layers.Dense(10, input_dim = 1, activation='relu'))
model.add(keras.layers.Dense(10, activation='relu'))
model.add(keras.layers.Dense(1))
model.compile(
    optimizer=keras.optimizers.RMSprop(learning_rate=0.1),
    loss = "mean_squared_error",
    metrics = ["mean_absolute_error"]
)

x = np.random.uniform(low = 0, high = 10, size = 1000)
y = x**2
x_train, x_test = (x[:900],x[900:])
y_train, y_test = (y[:900],y[900:])

Richiamata formazione personalizzata

Il nostro primo richiamo è essere richiamati durante l'allenamento. Sottoclassiamo il Callback classe:

class TrainingCallback(keras.callbacks.Callback):
    def __init__(self):
        self.tabulation = {"train":"", 'batch': " "*8, 'epoch':" "*4}
    def on_train_begin(self, logs=None):
        tab = self.tabulation['train']
        print(f"{tab}Training!")
        print(f"{tab}available logs: {logs}")

    def on_train_batch_begin(self, batch, logs=None):
        tab = self.tabulation['batch']
        print(f"{tab}Batch {batch}")
        print(f"{tab}available logs: {logs}")

    def on_train_batch_end(self, batch, logs=None):
        tab = self.tabulation['batch']
        print(f"{tab}End of Batch {batch}")
        print(f"{tab}available logs: {logs}")

    def on_epoch_begin(self, epoch, logs=None):
        tab = self.tabulation['epoch']
        print(f"{tab}Epoch {epoch} of training")
        print(f"{tab}available logs: {logs}")

    def on_epoch_end(self, epoch, logs=None):
        tab = self.tabulation['epoch']
        print(f"{tab}End of Epoch {epoch} of training")
        print(f"{tab}available logs: {logs}")

    def on_train_end(self, logs=None):
        tab = self.tabulation['train']
        print(f"{tab}Finishing training!")
        print(f"{tab}available logs: {logs}")

Se uno di questi metodi non viene sovrascritto, il comportamento predefinito continuerà come prima. Nel nostro esempio, stampiamo semplicemente i registri disponibili e il livello a cui viene applicata la richiamata, con un'indentazione adeguata.

Diamo un'occhiata alle uscite:

model.fit(
    x_train,
    y_train,
    batch_size=500,
    epochs=2,
    verbose=0,
    callbacks=[TrainingCallback()],
)
Training!
available logs: {}
    Epoch 0 of training
    available logs: {}
        Batch 0
        available logs: {}
        End of Batch 0
        available logs: {'loss': 2172.373291015625, 'mean_absolute_error': 34.79669952392578}
        Batch 1
        available logs: {}
        End of Batch 1
        available logs: {'loss': 2030.1309814453125, 'mean_absolute_error': 33.30256271362305}
    End of Epoch 0 of training
    available logs: {'loss': 2030.1309814453125, 'mean_absolute_error': 33.30256271362305}
    Epoch 1 of training
    available logs: {}
        Batch 0
        available logs: {}
        End of Batch 0
        available logs: {'loss': 1746.2772216796875, 'mean_absolute_error': 30.268001556396484}
        Batch 1
        available logs: {}
        End of Batch 1
        available logs: {'loss': 1467.36376953125, 'mean_absolute_error': 27.10252571105957}
    End of Epoch 1 of training
    available logs: {'loss': 1467.36376953125, 'mean_absolute_error': 27.10252571105957}
Finishing training!
available logs: {'loss': 1467.36376953125, 'mean_absolute_error': 27.10252571105957}


Tieni presente che possiamo seguire in ogni passaggio cosa sta facendo il modello e a quali metriche abbiamo accesso. Alla fine di ogni batch ed epoca, abbiamo accesso alla funzione di perdita nel campione e alle metriche del nostro modello.

Richiamata di valutazione personalizzata

Ora, chiamiamo il Model.evaluate() metodo. Possiamo vedere che alla fine di un batch abbiamo accesso alla funzione di perdita e alle metriche al momento, e alla fine della valutazione abbiamo accesso alla perdita e alle metriche complessive:

class TestingCallback(keras.callbacks.Callback):
    def __init__(self):
          self.tabulation = {"test":"", 'batch': " "*8}
      
    def on_test_begin(self, logs=None):
        tab = self.tabulation['test']
        print(f'{tab}Evaluating!')
        print(f'{tab}available logs: {logs}')

    def on_test_end(self, logs=None):
        tab = self.tabulation['test']
        print(f'{tab}Finishing evaluation!')
        print(f'{tab}available logs: {logs}')

    def on_test_batch_begin(self, batch, logs=None):
        tab = self.tabulation['batch']
        print(f"{tab}Batch {batch}")
        print(f"{tab}available logs: {logs}")

    def on_test_batch_end(self, batch, logs=None):
        tab = self.tabulation['batch']
        print(f"{tab}End of batch {batch}")
        print(f"{tab}available logs: {logs}")
res = model.evaluate(
    x_test, y_test, batch_size=100, verbose=0, callbacks=[TestingCallback()]
)
Evaluating!
available logs: {}
        Batch 0
        available logs: {}
        End of batch 0
        available logs: {'loss': 382.2723083496094, 'mean_absolute_error': 14.069927215576172}
Finishing evaluation!
available logs: {'loss': 382.2723083496094, 'mean_absolute_error': 14.069927215576172}

Richiamata di previsione personalizzata

Infine, chiamiamo il Model.predict() metodo. Si noti che alla fine di ogni batch abbiamo accesso agli output previsti del nostro modello:

class PredictionCallback(keras.callbacks.Callback):
    def __init__(self):
        self.tabulation = {"prediction":"", 'batch': " "*8}

    def on_predict_begin(self, logs=None):
        tab = self.tabulation['prediction']
        print(f"{tab}Predicting!")
        print(f"{tab}available logs: {logs}")

    def on_predict_end(self, logs=None):
        tab = self.tabulation['prediction']
        print(f"{tab}End of Prediction!")
        print(f"{tab}available logs: {logs}")

    def on_predict_batch_begin(self, batch, logs=None):
        tab = self.tabulation['batch']
        print(f"{tab}batch {batch}")
        print(f"{tab}available logs: {logs}")

    def on_predict_batch_end(self, batch, logs=None):
        tab = self.tabulation['batch']
        print(f"{tab}End of batch {batch}")
        print(f"{tab}available logs:n {logs}")
res = model.predict(x_test[:10],
                    verbose = 0, 
                    callbacks=[PredictionCallback()])

Dai un'occhiata alla nostra guida pratica e pratica per l'apprendimento di Git, con le migliori pratiche, gli standard accettati dal settore e il cheat sheet incluso. Smetti di cercare su Google i comandi Git e in realtà imparare esso!

Predicting!
available logs: {}
        batch 0
        available logs: {}
        End of batch 0
        available logs:
 {'outputs': array([[ 7.743822],
       [27.748264],
       [33.082104],
       [26.530678],
       [27.939169],
       [18.414223],
       [42.610645],
       [36.69335 ],
       [13.096557],
       [37.120853]], dtype=float32)}
End of Prediction!
available logs: {}

Con questi è possibile personalizzare il comportamento, impostare il monitoraggio o alterare in altro modo i processi di formazione, valutazione o inferenza. Un'alternativa al sublcassing consiste nell'usare il LambdaCallback.

Utilizzo di LambaCallback

Uno dei callback integrati in Keras è il LambdaCallback classe. Questo callback accetta una funzione che definisce come si comporta e cosa fa! In un certo senso, ti consente di utilizzare qualsiasi funzione arbitraria come callback, consentendoti così di creare callback personalizzate.

La classe ha i parametri opzionali:
-on_epoch_begin

  • on_epoch_end
  • on_batch_begin
  • on_batch_end
  • on_train_begin
  • on_train_end

Ogni parametro accetta una funzione che viene chiamato nel rispettivo evento del modello. Ad esempio, effettuiamo una richiamata per inviare un'e-mail al termine dell'addestramento del modello:

import smtplib
from email.message import EmailMessage

def send_email(logs): 
    msg = EmailMessage()
    content = f"""The model has finished training."""
    for key, value in logs.items():
      content = content + f"n{key}:{value:.2f}"
    msg.set_content(content)
    msg['Subject'] = f'Training report'
    msg['From'] = '[email protected]'
    msg['To'] = 'receiver-email'

    s = smtplib.SMTP('smtp.gmail.com', 587)
    s.starttls()
    s.login("[email protected]", "your-gmail-app-password")
    s.send_message(msg)
    s.quit()

lambda_send_email = lambda logs : send_email(logs)

email_callback = keras.callbacks.LambdaCallback(on_train_end = lambda_send_email)

model.fit(
    x_train,
    y_train,
    batch_size=100,
    epochs=1,
    verbose=0,
    callbacks=[email_callback],
)

Per effettuare la nostra richiamata personalizzata utilizzando LambdaCallback, dobbiamo solo implementare la funzione che vogliamo essere chiamata, avvolgerla come a lambda funzione e passarlo al
LambdaCallback classe come parametro.

Un richiamo per visualizzare l'addestramento del modello

In questa sezione, forniremo un esempio di callback personalizzato che crea un'animazione del miglioramento delle prestazioni del nostro modello durante l'allenamento. Per fare ciò, memorizziamo i valori dei log alla fine di ogni batch. Quindi, alla fine del ciclo di formazione, creiamo un'animazione utilizzando matplotlib.

Per migliorare la visualizzazione, la perdita e le metriche verranno tracciate in scala logaritmica:

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation
from IPython import display

class TrainingAnimationCallback(keras.callbacks.Callback):
    def __init__(self, duration = 40, fps = 1000/25):
        self.duration = duration
        self.fps = fps
        self.logs_history = []

    def set_plot(self):   
        self.figure = plt.figure()
        
        plt.xticks(
            range(0,self.params['steps']*self.params['epochs'], self.params['steps']),
            range(0,self.params['epochs']))
        plt.xlabel('Epoch')
        plt.ylabel('Loss & Metrics ($Log_{10}$ scale)')

        self.plot = {}
        for metric in self.model.metrics_names:
          self.plot[metric], = plt.plot([],[], label = metric)
          
        max_y = [max(log.values()) for log in self.logs_history]
        
        self.title = plt.title(f'batches:0')
        plt.xlim(0,len(self.logs_history)) 
        plt.ylim(0,max(max_y))

           
        plt.legend(loc='upper right')
  
    def animation_function(self,frame):
        batch = frame % self.params['steps']
        self.title.set_text(f'batch:{batch}')
        x = list(range(frame))
        
        for metric in self.model.metrics_names:
            y = [log[metric] for log in self.logs_history[:frame]]
            self.plot[metric].set_data(x,y)
        
    def on_train_batch_end(self, batch, logs=None):
        logarithm_transform = lambda item: (item[0], np.log(item[1]))
        logs = dict(map(logarithm_transform,logs.items()))
        self.logs_history.append(logs)
       
    def on_train_end(self, logs=None):
        self.set_plot()
        num_frames = int(self.duration*self.fps)
        num_batches = self.params['steps']*self.params['epochs']
        selected_batches = range(0, num_batches , num_batches//num_frames )
        interval = 1000*(1/self.fps)
        anim_created = FuncAnimation(self.figure, 
                                     self.animation_function,
                                     frames=selected_batches,
                                     interval=interval)
        video = anim_created.to_html5_video()
        
        html = display.HTML(video)
        display.display(html)
        plt.close()

Utilizzeremo lo stesso modello di prima, ma con più campioni di addestramento:

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

model = keras.Sequential()
model.add(keras.layers.Dense(10, input_dim = 1, activation='relu'))
model.add(keras.layers.Dense(10, activation='relu'))
model.add(keras.layers.Dense(1))
model.compile(
    optimizer=keras.optimizers.RMSprop(learning_rate=0.1),
    loss = "mean_squared_error",
    metrics = ["mean_absolute_error"]
)

def create_sample(sample_size, train_test_proportion = 0.9):
    x = np.random.uniform(low = 0, high = 10, size = sample_size)
    y = x**2
    train_test_split = int(sample_size*train_test_proportion)
    x_train, x_test = (x[:train_test_split],x[train_test_split:])
    y_train, y_test = (y[:train_test_split],y[train_test_split:])
    return (x_train,x_test,y_train,y_test)

x_train,x_test,y_train,y_test = create_sample(35200)


model.fit(
    x_train,
    y_train,
    batch_size=32,
    epochs=2,
    verbose=0,
    callbacks=[TrainingAnimationCallback()],
)

Il nostro output è un'animazione delle metriche e della funzione di perdita mentre cambiano durante il processo di formazione:

Il tuo browser non supporta i video HTML.

Conclusione

In questa guida, abbiamo esaminato l'implementazione dei callback personalizzati in Keras.
Esistono due opzioni per l'implementazione di callback personalizzate: tramite la sottoclasse di keras.callbacks.Callback classe, o utilizzando il keras.callbacks.LambdaCallback classe.

Abbiamo visto un esempio pratico usando LambdaCallbackper l'invio di un'e-mail alla fine del ciclo di addestramento e un esempio per la sottoclasse di Callback classe che crea un'animazione del ciclo di addestramento.

Sebbene Keras abbia molti callback integrati, sapere come implementare un callback personalizzato può essere utile per applicazioni più specifiche.

Timestamp:

Di più da Impilamento