Introduzione a moduli, livelli e modelli

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza l'origine su GitHub Scarica quaderno

Per eseguire l'apprendimento automatico in TensorFlow, è probabile che tu debba definire, salvare e ripristinare un modello.

Un modello è, astrattamente:

  • Una funzione che calcola qualcosa sui tensori (un passaggio in avanti )
  • Alcune variabili che possono essere aggiornate in risposta alla formazione

In questa guida, andrai sotto la superficie di Keras per vedere come vengono definiti i modelli TensorFlow. Questo esamina come TensorFlow raccoglie variabili e modelli, nonché come vengono salvati e ripristinati.

Impostare

import tensorflow as tf
from datetime import datetime

%load_ext tensorboard

Definizione di modelli e livelli in TensorFlow

La maggior parte dei modelli è composta da strati. I livelli sono funzioni con una struttura matematica nota che possono essere riutilizzate e hanno variabili addestrabili. In TensorFlow, la maggior parte delle implementazioni di alto livello di livelli e modelli, come Keras o Sonnet , sono basate sulla stessa classe fondamentale: tf.Module .

Ecco un esempio di un tf.Module molto semplice che opera su un tensore scalare:

class SimpleModule(tf.Module):
  def __init__(self, name=None):
    super().__init__(name=name)
    self.a_variable = tf.Variable(5.0, name="train_me")
    self.non_trainable_variable = tf.Variable(5.0, trainable=False, name="do_not_train_me")
  def __call__(self, x):
    return self.a_variable * x + self.non_trainable_variable

simple_module = SimpleModule(name="simple")

simple_module(tf.constant(5.0))
<tf.Tensor: shape=(), dtype=float32, numpy=30.0>

I moduli e, per estensione, i livelli sono una terminologia di deep learning per "oggetti": hanno uno stato interno e metodi che utilizzano quello stato.

Non c'è niente di speciale in __call__ tranne che per agire come un richiamabile Python ; puoi invocare i tuoi modelli con qualsiasi funzione desideri.

È possibile attivare e disattivare l'addestrabilità delle variabili per qualsiasi motivo, incluso il congelamento dei livelli e delle variabili durante la messa a punto.

Sottoclasse tf.Module , tutte le istanze tf.Variable o tf.Module assegnate alle proprietà di questo oggetto vengono raccolte automaticamente. Ciò consente di salvare e caricare variabili e anche di creare raccolte di tf.Module s.

# All trainable variables
print("trainable variables:", simple_module.trainable_variables)
# Every variable
print("all variables:", simple_module.variables)
trainable variables: (<tf.Variable 'train_me:0' shape=() dtype=float32, numpy=5.0>,)
all variables: (<tf.Variable 'train_me:0' shape=() dtype=float32, numpy=5.0>, <tf.Variable 'do_not_train_me:0' shape=() dtype=float32, numpy=5.0>)
2021-10-26 01:29:45.284549: W tensorflow/python/util/util.cc:348] Sets are not currently considered sequences, but this may change in the future, so consider avoiding using them.

Questo è un esempio di un modello di livello lineare a due strati composto da moduli.

Prima uno strato denso (lineare):

class Dense(tf.Module):
  def __init__(self, in_features, out_features, name=None):
    super().__init__(name=name)
    self.w = tf.Variable(
      tf.random.normal([in_features, out_features]), name='w')
    self.b = tf.Variable(tf.zeros([out_features]), name='b')
  def __call__(self, x):
    y = tf.matmul(x, self.w) + self.b
    return tf.nn.relu(y)

E poi il modello completo, che crea due istanze di livello e le applica:

class SequentialModule(tf.Module):
  def __init__(self, name=None):
    super().__init__(name=name)

    self.dense_1 = Dense(in_features=3, out_features=3)
    self.dense_2 = Dense(in_features=3, out_features=2)

  def __call__(self, x):
    x = self.dense_1(x)
    return self.dense_2(x)

# You have made a model!
my_model = SequentialModule(name="the_model")

# Call it, with random results
print("Model results:", my_model(tf.constant([[2.0, 2.0, 2.0]])))
Model results: tf.Tensor([[7.706234  3.0919805]], shape=(1, 2), dtype=float32)

Le istanze tf.Module raccoglieranno automaticamente, in modo ricorsivo, tutte le istanze tf.Variable o tf.Module ad esse assegnate. Ciò consente di gestire raccolte di tf.Module con una singola istanza di modello e di salvare e caricare interi modelli.

print("Submodules:", my_model.submodules)
Submodules: (<__main__.Dense object at 0x7f7ab2391290>, <__main__.Dense object at 0x7f7b6869ea10>)
for var in my_model.variables:
  print(var, "\n")
<tf.Variable 'b:0' shape=(3,) dtype=float32, numpy=array([0., 0., 0.], dtype=float32)> 

<tf.Variable 'w:0' shape=(3, 3) dtype=float32, numpy=
array([[ 0.05711935,  0.22440144,  0.6370985 ],
       [ 0.3136791 , -1.7006774 ,  0.7256515 ],
       [ 0.16120772, -0.8412193 ,  0.5250952 ]], dtype=float32)> 

<tf.Variable 'b:0' shape=(2,) dtype=float32, numpy=array([0., 0.], dtype=float32)> 

<tf.Variable 'w:0' shape=(3, 2) dtype=float32, numpy=
array([[-0.5353216 ,  1.2815404 ],
       [ 0.62764466,  0.47087234],
       [ 2.19187   ,  0.45777202]], dtype=float32)>

In attesa di creare variabili

Potresti aver notato qui che devi definire le dimensioni di input e output per il livello. In questo modo la variabile w ha una forma nota e può essere allocata.

Rinviando la creazione della variabile alla prima volta che il modulo viene chiamato con una forma di input specifica, non è necessario specificare la dimensione dell'input in anticipo.

class FlexibleDenseModule(tf.Module):
  # Note: No need for `in_features`
  def __init__(self, out_features, name=None):
    super().__init__(name=name)
    self.is_built = False
    self.out_features = out_features

  def __call__(self, x):
    # Create variables on first call.
    if not self.is_built:
      self.w = tf.Variable(
        tf.random.normal([x.shape[-1], self.out_features]), name='w')
      self.b = tf.Variable(tf.zeros([self.out_features]), name='b')
      self.is_built = True

    y = tf.matmul(x, self.w) + self.b
    return tf.nn.relu(y)
# Used in a module
class MySequentialModule(tf.Module):
  def __init__(self, name=None):
    super().__init__(name=name)

    self.dense_1 = FlexibleDenseModule(out_features=3)
    self.dense_2 = FlexibleDenseModule(out_features=2)

  def __call__(self, x):
    x = self.dense_1(x)
    return self.dense_2(x)

my_model = MySequentialModule(name="the_model")
print("Model results:", my_model(tf.constant([[2.0, 2.0, 2.0]])))
Model results: tf.Tensor([[4.0598335 0.       ]], shape=(1, 2), dtype=float32)

Questa flessibilità è il motivo per cui i livelli di TensorFlow spesso devono solo specificare la forma dei loro output, ad esempio in tf.keras.layers.Dense , piuttosto che la dimensione sia dell'input che dell'output.

Risparmio di pesi

Puoi salvare un tf.Module sia come checkpoint che come SavedModel .

I checkpoint sono solo i pesi (ovvero i valori dell'insieme di variabili all'interno del modulo e dei suoi sottomoduli):

chkp_path = "my_checkpoint"
checkpoint = tf.train.Checkpoint(model=my_model)
checkpoint.write(chkp_path)
'my_checkpoint'

I checkpoint sono costituiti da due tipi di file: i dati stessi e un file di indice per i metadati. Il file di indice tiene traccia di ciò che viene effettivamente salvato e la numerazione dei checkpoint, mentre i dati del checkpoint contengono i valori delle variabili e i relativi percorsi di ricerca degli attributi.

ls my_checkpoint*
my_checkpoint.data-00000-of-00001  my_checkpoint.index

Puoi guardare all'interno di un checkpoint per assicurarti che l'intera raccolta di variabili sia salvata, ordinata in base all'oggetto Python che le contiene.

tf.train.list_variables(chkp_path)
[('_CHECKPOINTABLE_OBJECT_GRAPH', []),
 ('model/dense_1/b/.ATTRIBUTES/VARIABLE_VALUE', [3]),
 ('model/dense_1/w/.ATTRIBUTES/VARIABLE_VALUE', [3, 3]),
 ('model/dense_2/b/.ATTRIBUTES/VARIABLE_VALUE', [2]),
 ('model/dense_2/w/.ATTRIBUTES/VARIABLE_VALUE', [3, 2])]

Durante l'addestramento distribuito (multi-macchina) possono essere partizionati, motivo per cui sono numerati (ad esempio, '00000-of-00001'). In questo caso, però, c'è solo uno shard.

Quando carichi nuovamente i modelli, sovrascrivi i valori nel tuo oggetto Python.

new_model = MySequentialModule()
new_checkpoint = tf.train.Checkpoint(model=new_model)
new_checkpoint.restore("my_checkpoint")

# Should be the same result as above
new_model(tf.constant([[2.0, 2.0, 2.0]]))
<tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[4.0598335, 0.       ]], dtype=float32)>

Funzioni di salvataggio

TensorFlow può eseguire modelli senza gli oggetti Python originali, come dimostrato da TensorFlow Serving e TensorFlow Lite , anche quando scarichi un modello addestrato da TensorFlow Hub .

TensorFlow deve sapere come eseguire i calcoli descritti in Python, ma senza il codice originale . Per fare ciò, puoi creare un grafico , che è descritto nella Guida introduttiva ai grafici e alle funzioni .

Questo grafico contiene operazioni, o ops , che implementano la funzione.

Puoi definire un grafico nel modello sopra aggiungendo il decoratore @tf.function per indicare che questo codice deve essere eseguito come grafico.

class MySequentialModule(tf.Module):
  def __init__(self, name=None):
    super().__init__(name=name)

    self.dense_1 = Dense(in_features=3, out_features=3)
    self.dense_2 = Dense(in_features=3, out_features=2)

  @tf.function
  def __call__(self, x):
    x = self.dense_1(x)
    return self.dense_2(x)

# You have made a model with a graph!
my_model = MySequentialModule(name="the_model")

Il modulo che hai creato funziona esattamente come prima. Ogni firma univoca passata alla funzione crea un grafico separato. Per i dettagli, consultare la Guida introduttiva ai grafici e alle funzioni .

print(my_model([[2.0, 2.0, 2.0]]))
print(my_model([[[2.0, 2.0, 2.0], [2.0, 2.0, 2.0]]]))
tf.Tensor([[0.62891716 0.        ]], shape=(1, 2), dtype=float32)
tf.Tensor(
[[[0.62891716 0.        ]
  [0.62891716 0.        ]]], shape=(1, 2, 2), dtype=float32)

Puoi visualizzare il grafico tracciandolo all'interno di un riepilogo TensorBoard.

# Set up logging.
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
logdir = "logs/func/%s" % stamp
writer = tf.summary.create_file_writer(logdir)

# Create a new model to get a fresh trace
# Otherwise the summary will not see the graph.
new_model = MySequentialModule()

# Bracket the function call with
# tf.summary.trace_on() and tf.summary.trace_export().
tf.summary.trace_on(graph=True)
tf.profiler.experimental.start(logdir)
# Call only one tf.function when tracing.
z = print(new_model(tf.constant([[2.0, 2.0, 2.0]])))
with writer.as_default():
  tf.summary.trace_export(
      name="my_func_trace",
      step=0,
      profiler_outdir=logdir)
tf.Tensor([[0.         0.01750386]], shape=(1, 2), dtype=float32)

Avvia TensorBoard per visualizzare la traccia risultante:

#docs_infra: no_execute
%tensorboard --logdir logs/func

Uno screenshot del grafico in TensorBoard

Creazione di un SavedModel

Il modo consigliato per condividere modelli completamente addestrati consiste nell'usare SavedModel . SavedModel contiene sia una raccolta di funzioni che una raccolta di pesi.

Puoi salvare il modello che hai appena addestrato come segue:

tf.saved_model.save(my_model, "the_saved_model")
INFO:tensorflow:Assets written to: the_saved_model/assets
# Inspect the SavedModel in the directory
ls -l the_saved_model
total 24
drwxr-sr-x 2 kbuilder kokoro  4096 Oct 26 01:29 assets
-rw-rw-r-- 1 kbuilder kokoro 14702 Oct 26 01:29 saved_model.pb
drwxr-sr-x 2 kbuilder kokoro  4096 Oct 26 01:29 variables
# The variables/ directory contains a checkpoint of the variables
ls -l the_saved_model/variables
total 8
-rw-rw-r-- 1 kbuilder kokoro 408 Oct 26 01:29 variables.data-00000-of-00001
-rw-rw-r-- 1 kbuilder kokoro 356 Oct 26 01:29 variables.index

Il file saved_model.pb è un buffer di protocollo che descrive il funzionale tf.Graph .

Modelli e livelli possono essere caricati da questa rappresentazione senza creare effettivamente un'istanza della classe che l'ha creata. Ciò è auspicabile in situazioni in cui non si dispone (o si desidera) un interprete Python, come servire su larga scala o su un dispositivo perimetrale, o in situazioni in cui il codice Python originale non è disponibile o pratico da usare.

Puoi caricare il modello come nuovo oggetto:

new_model = tf.saved_model.load("the_saved_model")

new_model , creato dal caricamento di un modello salvato, è un oggetto utente TensorFlow interno senza alcuna conoscenza della classe. Non è di tipo SequentialModule .

isinstance(new_model, SequentialModule)
False

Questo nuovo modello funziona sulle firme di input già definite. Non puoi aggiungere più firme a un modello ripristinato in questo modo.

print(my_model([[2.0, 2.0, 2.0]]))
print(my_model([[[2.0, 2.0, 2.0], [2.0, 2.0, 2.0]]]))
tf.Tensor([[0.62891716 0.        ]], shape=(1, 2), dtype=float32)
tf.Tensor(
[[[0.62891716 0.        ]
  [0.62891716 0.        ]]], shape=(1, 2, 2), dtype=float32)

Pertanto, utilizzando SavedModel , è possibile salvare pesi e grafici TensorFlow utilizzando tf.Module , quindi caricarli di nuovo.

Modelli e strati Keras

Nota che fino a questo punto non si fa menzione di Keras. Puoi creare la tua API di alto livello su tf.Module e le persone lo hanno fatto.

In questa sezione, esaminerai come Keras utilizza tf.Module . Una guida utente completa ai modelli Keras è disponibile nella guida Keras .

Strati di Keras

tf.keras.layers.Layer è la classe base di tutti i livelli Keras ed eredita da tf.Module .

Puoi convertire un modulo in un livello Keras semplicemente sostituendo il genitore e quindi cambiando __call__ in call :

class MyDense(tf.keras.layers.Layer):
  # Adding **kwargs to support base Keras layer arguments
  def __init__(self, in_features, out_features, **kwargs):
    super().__init__(**kwargs)

    # This will soon move to the build step; see below
    self.w = tf.Variable(
      tf.random.normal([in_features, out_features]), name='w')
    self.b = tf.Variable(tf.zeros([out_features]), name='b')
  def call(self, x):
    y = tf.matmul(x, self.w) + self.b
    return tf.nn.relu(y)

simple_layer = MyDense(name="simple", in_features=3, out_features=3)

I livelli Keras hanno il loro __call__ che fa alcune operazioni di contabilità descritte nella sezione successiva e quindi chiama call() . Non dovresti notare alcun cambiamento nella funzionalità.

simple_layer([[2.0, 2.0, 2.0]])
<tf.Tensor: shape=(1, 3), dtype=float32, numpy=array([[0.      , 0.179402, 0.      ]], dtype=float32)>

La fase build

Come notato, in molti casi è conveniente attendere per creare variabili finché non si è sicuri della forma di input.

I livelli Keras sono dotati di un passaggio aggiuntivo del ciclo di vita che ti consente una maggiore flessibilità nel modo in cui definisci i tuoi livelli. Questo è definito nella funzione build .

build viene chiamato esattamente una volta e viene chiamato con la forma dell'input. Di solito viene utilizzato per creare variabili (pesi).

Puoi riscrivere il livello MyDense sopra per essere flessibile alle dimensioni dei suoi input:

class FlexibleDense(tf.keras.layers.Layer):
  # Note the added `**kwargs`, as Keras supports many arguments
  def __init__(self, out_features, **kwargs):
    super().__init__(**kwargs)
    self.out_features = out_features

  def build(self, input_shape):  # Create the state of the layer (weights)
    self.w = tf.Variable(
      tf.random.normal([input_shape[-1], self.out_features]), name='w')
    self.b = tf.Variable(tf.zeros([self.out_features]), name='b')

  def call(self, inputs):  # Defines the computation from inputs to outputs
    return tf.matmul(inputs, self.w) + self.b

# Create the instance of the layer
flexible_dense = FlexibleDense(out_features=3)

A questo punto il modello non è stato costruito, quindi non ci sono variabili:

flexible_dense.variables
[]

La chiamata alla funzione alloca variabili di dimensioni appropriate:

# Call it, with predictably random results
print("Model results:", flexible_dense(tf.constant([[2.0, 2.0, 2.0], [3.0, 3.0, 3.0]])))
Model results: tf.Tensor(
[[-1.6998017  1.6444504 -1.3103955]
 [-2.5497022  2.4666753 -1.9655929]], shape=(2, 3), dtype=float32)
flexible_dense.variables
[<tf.Variable 'flexible_dense/w:0' shape=(3, 3) dtype=float32, numpy=
 array([[ 1.277462  ,  0.5399406 , -0.301957  ],
        [-1.6277349 ,  0.7374014 , -1.7651852 ],
        [-0.49962795, -0.45511687,  1.4119445 ]], dtype=float32)>,
 <tf.Variable 'flexible_dense/b:0' shape=(3,) dtype=float32, numpy=array([0., 0., 0.], dtype=float32)>]

Poiché build viene chiamato solo una volta, gli input verranno rifiutati se la forma dell'input non è compatibile con le variabili del livello:

try:
  print("Model results:", flexible_dense(tf.constant([[2.0, 2.0, 2.0, 2.0]])))
except tf.errors.InvalidArgumentError as e:
  print("Failed:", e)
Failed: In[0] mismatch In[1] shape: 4 vs. 3: [1,4] [3,3] 0 0 [Op:MatMul]

I livelli Keras hanno molte più funzionalità extra tra cui:

  • Perdite facoltative
  • Supporto per le metriche
  • Supporto integrato per un argomento di training facoltativo per distinguere tra utilizzo di addestramento e inferenza
  • get_config e from_config metodi che consentono di archiviare accuratamente le configurazioni per consentire la clonazione del modello in Python

Leggi di loro nella guida completa ai livelli e ai modelli personalizzati.

Modelli Keras

Puoi definire il tuo modello come livelli Keras nidificati.

Tuttavia, Keras fornisce anche una classe modello completa denominata tf.keras.Model . Eredita da tf.keras.layers.Layer , quindi un modello Keras può essere utilizzato, nidificato e salvato allo stesso modo dei livelli Keras. I modelli Keras sono dotati di funzionalità extra che li rendono facili da addestrare, valutare, caricare, salvare e persino addestrare su più macchine.

Puoi definire il SequentialModule dall'alto con un codice quasi identico, convertendo ancora __call__ in call() e cambiando il genitore:

class MySequentialModel(tf.keras.Model):
  def __init__(self, name=None, **kwargs):
    super().__init__(**kwargs)

    self.dense_1 = FlexibleDense(out_features=3)
    self.dense_2 = FlexibleDense(out_features=2)
  def call(self, x):
    x = self.dense_1(x)
    return self.dense_2(x)

# You have made a Keras model!
my_sequential_model = MySequentialModel(name="the_model")

# Call it on a tensor, with random results
print("Model results:", my_sequential_model(tf.constant([[2.0, 2.0, 2.0]])))
Model results: tf.Tensor([[5.5604653 3.3511646]], shape=(1, 2), dtype=float32)

Sono disponibili tutte le stesse funzionalità, comprese le variabili di tracciamento e i sottomoduli.

my_sequential_model.variables
[<tf.Variable 'my_sequential_model/flexible_dense_1/w:0' shape=(3, 3) dtype=float32, numpy=
 array([[ 0.05627853, -0.9386015 , -0.77410126],
        [ 0.63149   ,  1.0802224 , -0.37785745],
        [-0.24788402, -1.1076807 , -0.5956209 ]], dtype=float32)>,
 <tf.Variable 'my_sequential_model/flexible_dense_1/b:0' shape=(3,) dtype=float32, numpy=array([0., 0., 0.], dtype=float32)>,
 <tf.Variable 'my_sequential_model/flexible_dense_2/w:0' shape=(3, 2) dtype=float32, numpy=
 array([[-0.93912166,  0.77979285],
        [ 1.4049559 , -1.9380962 ],
        [-2.6039495 ,  0.30885765]], dtype=float32)>,
 <tf.Variable 'my_sequential_model/flexible_dense_2/b:0' shape=(2,) dtype=float32, numpy=array([0., 0.], dtype=float32)>]
my_sequential_model.submodules
(<__main__.FlexibleDense at 0x7f7b48525550>,
 <__main__.FlexibleDense at 0x7f7b48508d10>)

L'override tf.keras.Model è un approccio molto Pythonico alla creazione di modelli TensorFlow. Se stai migrando modelli da altri framework, questo può essere molto semplice.

Se stai costruendo modelli che sono semplici assemblaggi di livelli e input esistenti, puoi risparmiare tempo e spazio utilizzando l' API funzionale , che include funzionalità aggiuntive relative alla ricostruzione e all'architettura del modello.

Ecco lo stesso modello con l'API funzionale:

inputs = tf.keras.Input(shape=[3,])

x = FlexibleDense(3)(inputs)
x = FlexibleDense(2)(x)

my_functional_model = tf.keras.Model(inputs=inputs, outputs=x)

my_functional_model.summary()
Model: "model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         [(None, 3)]               0         
_________________________________________________________________
flexible_dense_3 (FlexibleDe (None, 3)                 12        
_________________________________________________________________
flexible_dense_4 (FlexibleDe (None, 2)                 8         
=================================================================
Total params: 20
Trainable params: 20
Non-trainable params: 0
_________________________________________________________________
my_functional_model(tf.constant([[2.0, 2.0, 2.0]]))
<tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[8.219393, 4.511119]], dtype=float32)>

La differenza principale qui è che la forma di input è specificata in anticipo come parte del processo di costruzione funzionale. L'argomento input_shape in questo caso non deve essere specificato completamente; puoi lasciare alcune dimensioni come None .

Salvataggio dei modelli Keras

I modelli Keras possono essere sottoposti a checkpoint e sarà lo stesso di tf.Module .

I modelli Keras possono anche essere salvati con tf.saved_model.save() , poiché sono moduli. Tuttavia, i modelli Keras hanno metodi pratici e altre funzionalità:

my_sequential_model.save("exname_of_file")
INFO:tensorflow:Assets written to: exname_of_file/assets

Altrettanto facilmente, possono essere ricaricati in:

reconstructed_model = tf.keras.models.load_model("exname_of_file")
WARNING:tensorflow:No training configuration found in save file, so the model was *not* compiled. Compile it manually.

Keras SavedModels salva anche gli stati di metrica, perdita e ottimizzatore.

Questo modello ricostruito può essere utilizzato e produrrà lo stesso risultato quando richiamato sugli stessi dati:

reconstructed_model(tf.constant([[2.0, 2.0, 2.0]]))
<tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[5.5604653, 3.3511646]], dtype=float32)>

C'è altro da sapere sul salvataggio e la serializzazione dei modelli Keras, inclusa la fornitura di metodi di configurazione per livelli personalizzati per il supporto delle funzionalità. Consulta la guida al salvataggio e alla serializzazione .

Qual è il prossimo

Se vuoi conoscere maggiori dettagli su Keras, puoi seguire le guide Keras esistenti qui .

Un altro esempio di API di alto livello costruita su tf.module è Sonnet di DeepMind, che è trattato sul loro sito .