Introduzione ai grafici e tf.function

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza la fonte su GitHub Scarica taccuino

Panoramica

Questa guida va sotto la superficie di TensorFlow e Keras per dimostrare come funziona TensorFlow. Se invece vuoi iniziare subito con Keras, dai un'occhiata alla raccolta di guide Keras .

In questa guida imparerai come TensorFlow ti consente di apportare semplici modifiche al tuo codice per ottenere grafici, come i grafici sono archiviati e rappresentati e come puoi usarli per accelerare i tuoi modelli.

Questa è una panoramica generale che tf.function come tf.function ti consente di passare dall'esecuzione ansiosa all'esecuzione del grafico. Per una specifica più completa di tf.function , vai alla guida tf.function .

Cosa sono i grafici?

Nelle tre guide precedenti, hai eseguito TensorFlow con entusiasmo . Ciò significa che le operazioni TensorFlow vengono eseguite da Python, operazione per operazione, e restituiscono i risultati a Python.

Sebbene l'esecuzione desiderosa abbia diversi vantaggi unici, l'esecuzione del grafico consente la portabilità al di fuori di Python e tende a offrire prestazioni migliori. L'esecuzione del grafico significa che i calcoli del tensore vengono eseguiti come un grafico TensorFlow , a volte indicato come tf.Graph o semplicemente un "grafico".

I grafici sono strutture dati che contengono un insieme di oggetti tf.Operation , che rappresentano unità di calcolo; e tf.Tensor , che rappresentano le unità di dati che scorrono tra le operazioni. Sono definiti in un contesto tf.Graph . Poiché questi grafici sono strutture di dati, possono essere salvati, eseguiti e ripristinati senza il codice Python originale.

Questo è l'aspetto di un grafico TensorFlow che rappresenta una rete neurale a due livelli quando viene visualizzato in TensorBoard.

Un semplice grafico TensorFlow

I vantaggi dei grafici

Con un grafico, hai una grande flessibilità. Puoi usare il tuo grafico TensorFlow in ambienti che non dispongono di un interprete Python, come applicazioni mobili, dispositivi incorporati e server di backend. TensorFlow usa i grafici come formato per i modelli salvati quando li esporta da Python.

I grafici sono anche facilmente ottimizzati, consentendo al compilatore di eseguire trasformazioni come:

  • Deduci staticamente il valore dei tensori piegando i nodi costanti nel tuo calcolo ("ripiegamento costante") .
  • Separare le sottoparti di un calcolo che sono indipendenti e suddividerle tra thread o dispositivi.
  • Semplifica le operazioni aritmetiche eliminando le sottoespressioni comuni.

C'è un intero sistema di ottimizzazione, Grappler , per eseguire questo e altri acceleratori.

In breve, i grafici sono estremamente utili e consentono a TensorFlow di funzionare velocemente , in parallelo e in modo efficiente su più dispositivi .

Tuttavia, vuoi comunque definire i tuoi modelli di apprendimento automatico (o altri calcoli) in Python per comodità e quindi costruire automaticamente i grafici quando ne hai bisogno.

Sfruttando i grafici

tf.function ed esegui un grafico in TensorFlow usando tf.function , come chiamata diretta o come decoratore. tf.function accetta una normale funzione come input e restituisce una Function . Una Function è un chiamabile Python che crea grafici TensorFlow dalla funzione Python. Usi una Function allo stesso modo del suo equivalente Python.

import tensorflow as tf
import timeit
from datetime import datetime
# Define a Python function.
def a_regular_function(x, y, b):
  x = tf.matmul(x, y)
  x = x + b
  return x

# `a_function_that_uses_a_graph` is a TensorFlow `Function`.
a_function_that_uses_a_graph = tf.function(a_regular_function)

# Make some tensors.
x1 = tf.constant([[1.0, 2.0]])
y1 = tf.constant([[2.0], [3.0]])
b1 = tf.constant(4.0)

orig_value = a_regular_function(x1, y1, b1).numpy()
# Call a `Function` like a Python function.
tf_function_value = a_function_that_uses_a_graph(x1, y1, b1).numpy()
assert(orig_value == tf_function_value)

All'esterno, una Function sembra una normale funzione che scrivi usando le operazioni TensorFlow. Sotto , invece, è molto diverso . Una Function incapsula diversi tf.Graph dietro un'API . È così che Function è in grado di darti i vantaggi dell'esecuzione del grafico , come velocità e distribuibilità.

tf.function applica a una funzione e a tutte le altre funzioni che chiama :

def inner_function(x, y, b):
  x = tf.matmul(x, y)
  x = x + b
  return x

# Use the decorator to make `outer_function` a `Function`.
@tf.function
def outer_function(x):
  y = tf.constant([[2.0], [3.0]])
  b = tf.constant(4.0)

  return inner_function(x, y, b)

# Note that the callable will create a graph that
# includes `inner_function` as well as `outer_function`.
outer_function(tf.constant([[1.0, 2.0]])).numpy()
array([[12.]], dtype=float32)

Se hai utilizzato TensorFlow 1.x, noterai che non hai mai avuto bisogno di definire un Placeholder o tf.Session .

Conversione di funzioni Python in grafici

Qualsiasi funzione che scrivi con TensorFlow conterrà una combinazione di operazioni TF integrate e logica Python, come clausole if-then , loop, break , return , continue e altro. Sebbene le operazioni di TensorFlow siano facilmente catturate da un tf.Graph , la logica specifica di Python deve essere sottoposta a un passaggio aggiuntivo per diventare parte del grafico. tf.function utilizza una libreria chiamata AutoGraph ( tf.autograph ) per convertire il codice Python in codice generatore di grafici.

def simple_relu(x):
  if tf.greater(x, 0):
    return x
  else:
    return 0

# `tf_simple_relu` is a TensorFlow `Function` that wraps `simple_relu`.
tf_simple_relu = tf.function(simple_relu)

print("First branch, with graph:", tf_simple_relu(tf.constant(1)).numpy())
print("Second branch, with graph:", tf_simple_relu(tf.constant(-1)).numpy())
First branch, with graph: 1
Second branch, with graph: 0

Sebbene sia improbabile che sia necessario visualizzare direttamente i grafici, è possibile ispezionare gli output per verificare i risultati esatti. Questi non sono facili da leggere, quindi non c'è bisogno di guardare troppo attentamente!

# This is the graph-generating output of AutoGraph.
print(tf.autograph.to_code(simple_relu))
def tf__simple_relu(x):
    with ag__.FunctionScope('simple_relu', 'fscope', ag__.ConversionOptions(recursive=True, user_requested=True, optional_features=(), internal_convert_user_code=True)) as fscope:
        do_return = False
        retval_ = ag__.UndefinedReturnValue()

        def get_state():
            return (do_return, retval_)

        def set_state(vars_):
            nonlocal do_return, retval_
            (do_return, retval_) = vars_

        def if_body():
            nonlocal do_return, retval_
            try:
                do_return = True
                retval_ = ag__.ld(x)
            except:
                do_return = False
                raise

        def else_body():
            nonlocal do_return, retval_
            try:
                do_return = True
                retval_ = 0
            except:
                do_return = False
                raise
        ag__.if_stmt(ag__.converted_call(ag__.ld(tf).greater, (ag__.ld(x), 0), None, fscope), if_body, else_body, get_state, set_state, ('do_return', 'retval_'), 2)
        return fscope.ret(retval_, do_return)
# This is the graph itself.
print(tf_simple_relu.get_concrete_function(tf.constant(1)).graph.as_graph_def())
node {
  name: "x"
  op: "Placeholder"
  attr {
    key: "_user_specified_name"
    value {
      s: "x"
    }
  }
  attr {
    key: "dtype"
    value {
      type: DT_INT32
    }
  }
  attr {
    key: "shape"
    value {
      shape {
      }
    }
  }
}
node {
  name: "Greater/y"
  op: "Const"
  attr {
    key: "dtype"
    value {
      type: DT_INT32
    }
  }
  attr {
    key: "value"
    value {
      tensor {
        dtype: DT_INT32
        tensor_shape {
        }
        int_val: 0
      }
    }
  }
}
node {
  name: "Greater"
  op: "Greater"
  input: "x"
  input: "Greater/y"
  attr {
    key: "T"
    value {
      type: DT_INT32
    }
  }
}
node {
  name: "cond"
  op: "StatelessIf"
  input: "Greater"
  input: "x"
  attr {
    key: "Tcond"
    value {
      type: DT_BOOL
    }
  }
  attr {
    key: "Tin"
    value {
      list {
        type: DT_INT32
      }
    }
  }
  attr {
    key: "Tout"
    value {
      list {
        type: DT_BOOL
        type: DT_INT32
      }
    }
  }
  attr {
    key: "_lower_using_switch_merge"
    value {
      b: true
    }
  }
  attr {
    key: "_read_only_resource_inputs"
    value {
      list {
      }
    }
  }
  attr {
    key: "else_branch"
    value {
      func {
        name: "cond_false_34"
      }
    }
  }
  attr {
    key: "output_shapes"
    value {
      list {
        shape {
        }
        shape {
        }
      }
    }
  }
  attr {
    key: "then_branch"
    value {
      func {
        name: "cond_true_33"
      }
    }
  }
}
node {
  name: "cond/Identity"
  op: "Identity"
  input: "cond"
  attr {
    key: "T"
    value {
      type: DT_BOOL
    }
  }
}
node {
  name: "cond/Identity_1"
  op: "Identity"
  input: "cond:1"
  attr {
    key: "T"
    value {
      type: DT_INT32
    }
  }
}
node {
  name: "Identity"
  op: "Identity"
  input: "cond/Identity_1"
  attr {
    key: "T"
    value {
      type: DT_INT32
    }
  }
}
library {
  function {
    signature {
      name: "cond_false_34"
      input_arg {
        name: "cond_placeholder"
        type: DT_INT32
      }
      output_arg {
        name: "cond_identity"
        type: DT_BOOL
      }
      output_arg {
        name: "cond_identity_1"
        type: DT_INT32
      }
    }
    node_def {
      name: "cond/Const"
      op: "Const"
      attr {
        key: "dtype"
        value {
          type: DT_BOOL
        }
      }
      attr {
        key: "value"
        value {
          tensor {
            dtype: DT_BOOL
            tensor_shape {
            }
            bool_val: true
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const"
      }
    }
    node_def {
      name: "cond/Const_1"
      op: "Const"
      attr {
        key: "dtype"
        value {
          type: DT_BOOL
        }
      }
      attr {
        key: "value"
        value {
          tensor {
            dtype: DT_BOOL
            tensor_shape {
            }
            bool_val: true
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const_1"
      }
    }
    node_def {
      name: "cond/Const_2"
      op: "Const"
      attr {
        key: "dtype"
        value {
          type: DT_INT32
        }
      }
      attr {
        key: "value"
        value {
          tensor {
            dtype: DT_INT32
            tensor_shape {
            }
            int_val: 0
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const_2"
      }
    }
    node_def {
      name: "cond/Const_3"
      op: "Const"
      attr {
        key: "dtype"
        value {
          type: DT_BOOL
        }
      }
      attr {
        key: "value"
        value {
          tensor {
            dtype: DT_BOOL
            tensor_shape {
            }
            bool_val: true
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const_3"
      }
    }
    node_def {
      name: "cond/Identity"
      op: "Identity"
      input: "cond/Const_3:output:0"
      attr {
        key: "T"
        value {
          type: DT_BOOL
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Identity"
      }
    }
    node_def {
      name: "cond/Const_4"
      op: "Const"
      attr {
        key: "dtype"
        value {
          type: DT_INT32
        }
      }
      attr {
        key: "value"
        value {
          tensor {
            dtype: DT_INT32
            tensor_shape {
            }
            int_val: 0
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const_4"
      }
    }
    node_def {
      name: "cond/Identity_1"
      op: "Identity"
      input: "cond/Const_4:output:0"
      attr {
        key: "T"
        value {
          type: DT_INT32
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Identity_1"
      }
    }
    ret {
      key: "cond_identity"
      value: "cond/Identity:output:0"
    }
    ret {
      key: "cond_identity_1"
      value: "cond/Identity_1:output:0"
    }
    attr {
      key: "_construction_context"
      value {
        s: "kEagerRuntime"
      }
    }
    arg_attr {
      key: 0
      value {
        attr {
          key: "_output_shapes"
          value {
            list {
              shape {
              }
            }
          }
        }
      }
    }
  }
  function {
    signature {
      name: "cond_true_33"
      input_arg {
        name: "cond_identity_1_x"
        type: DT_INT32
      }
      output_arg {
        name: "cond_identity"
        type: DT_BOOL
      }
      output_arg {
        name: "cond_identity_1"
        type: DT_INT32
      }
    }
    node_def {
      name: "cond/Const"
      op: "Const"
      attr {
        key: "dtype"
        value {
          type: DT_BOOL
        }
      }
      attr {
        key: "value"
        value {
          tensor {
            dtype: DT_BOOL
            tensor_shape {
            }
            bool_val: true
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const"
      }
    }
    node_def {
      name: "cond/Identity"
      op: "Identity"
      input: "cond/Const:output:0"
      attr {
        key: "T"
        value {
          type: DT_BOOL
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Identity"
      }
    }
    node_def {
      name: "cond/Identity_1"
      op: "Identity"
      input: "cond_identity_1_x"
      attr {
        key: "T"
        value {
          type: DT_INT32
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Identity_1"
      }
    }
    ret {
      key: "cond_identity"
      value: "cond/Identity:output:0"
    }
    ret {
      key: "cond_identity_1"
      value: "cond/Identity_1:output:0"
    }
    attr {
      key: "_construction_context"
      value {
        s: "kEagerRuntime"
      }
    }
    arg_attr {
      key: 0
      value {
        attr {
          key: "_output_shapes"
          value {
            list {
              shape {
              }
            }
          }
        }
      }
    }
  }
}
versions {
  producer: 716
  min_consumer: 12
}

La maggior parte delle volte, tf.function funzionerà senza particolari considerazioni. Tuttavia, ci sono alcuni avvertimenti e la guida tf.function può aiutare qui, così come il riferimento completo di AutoGraph

Polimorfismo: una Function , molti grafici

Un tf.Graph è specializzato in un tipo specifico di input (ad esempio, tensori con undtype specifico o oggetti con lo stesso id() ).

Ogni volta che invochi una Function con nuovi dtypes e forme nei suoi argomenti, la Function crea un nuovo tf.Graph per i nuovi argomenti. I dtypes e le forme degli tf.Graph di un tf.Graph sono conosciuti come una firma di input o semplicemente una firma .

La Function memorizza il tf.Graph corrispondente a quella firma in una ConcreteFunction . Una ConcreteFunction è un wrapper attorno a un tf.Graph .

@tf.function
def my_relu(x):
  return tf.maximum(0., x)

# `my_relu` creates new graphs as it observes more signatures.
print(my_relu(tf.constant(5.5)))
print(my_relu([1, -1]))
print(my_relu(tf.constant([3., -3.])))
tf.Tensor(5.5, shape=(), dtype=float32)
tf.Tensor([1. 0.], shape=(2,), dtype=float32)
tf.Tensor([3. 0.], shape=(2,), dtype=float32)

Se la Function è già stata chiamata con quella firma, la Function non crea un nuovo tf.Graph .

# These two calls do *not* create new graphs.
print(my_relu(tf.constant(-2.5))) # Signature matches `tf.constant(5.5)`.
print(my_relu(tf.constant([-1., 1.]))) # Signature matches `tf.constant([3., -3.])`.
tf.Tensor(0.0, shape=(), dtype=float32)
tf.Tensor([0. 1.], shape=(2,), dtype=float32)

Poiché è supportata da più grafici, una Function è polimorfica . Ciò gli consente di supportare più tipi di input di quelli che un singolo tf.Graph potrebbe rappresentare, nonché di ottimizzare ogni tf.Graph per prestazioni migliori.

# There are three `ConcreteFunction`s (one for each graph) in `my_relu`.
# The `ConcreteFunction` also knows the return type and shape!
print(my_relu.pretty_printed_concrete_signatures())
my_relu(x)
  Args:
    x: float32 Tensor, shape=()
  Returns:
    float32 Tensor, shape=()

my_relu(x=[1, -1])
  Returns:
    float32 Tensor, shape=(2,)

my_relu(x)
  Args:
    x: float32 Tensor, shape=(2,)
  Returns:
    float32 Tensor, shape=(2,)

Utilizzo di tf.function

Finora, hai imparato come convertire una funzione Python in un grafico semplicemente usando tf.function come decoratore o wrapper. Ma in pratica, far funzionare correttamente tf.function può essere complicato! Nelle sezioni seguenti, imparerai come puoi far funzionare il tuo codice come previsto con tf.function .

Esecuzione del grafico vs. esecuzione ansiosa

Il codice in una Function può essere eseguito sia avidamente che come grafico. Per impostazione predefinita, Function esegue il suo codice come un grafico:

@tf.function
def get_MSE(y_true, y_pred):
  sq_diff = tf.pow(y_true - y_pred, 2)
  return tf.reduce_mean(sq_diff)
y_true = tf.random.uniform([5], maxval=10, dtype=tf.int32)
y_pred = tf.random.uniform([5], maxval=10, dtype=tf.int32)
print(y_true)
print(y_pred)
tf.Tensor([1 1 0 0 5], shape=(5,), dtype=int32)
tf.Tensor([9 1 4 9 0], shape=(5,), dtype=int32)
get_MSE(y_true, y_pred)
<tf.Tensor: shape=(), dtype=int32, numpy=37>

Per verificare che il grafico della tua Function stia eseguendo lo stesso calcolo della sua funzione Python equivalente, puoi farlo eseguire con entusiasmo con tf.config.run_functions_eagerly(True) . Questo è un interruttore che disattiva la capacità di Function di creare ed eseguire grafici , invece di eseguire il codice normalmente.

tf.config.run_functions_eagerly(True)
get_MSE(y_true, y_pred)
<tf.Tensor: shape=(), dtype=int32, numpy=37>
# Don't forget to set it back when you are done.
tf.config.run_functions_eagerly(False)

Tuttavia, la Function può comportarsi in modo diverso sotto il grafico e l'esecuzione desiderosa. La funzione di print Python è un esempio di come queste due modalità differiscono. Diamo un'occhiata a cosa succede quando inserisci un'istruzione print nella tua funzione e la chiami ripetutamente.

@tf.function
def get_MSE(y_true, y_pred):
  print("Calculating MSE!")
  sq_diff = tf.pow(y_true - y_pred, 2)
  return tf.reduce_mean(sq_diff)

Osserva cosa viene stampato:

error = get_MSE(y_true, y_pred)
error = get_MSE(y_true, y_pred)
error = get_MSE(y_true, y_pred)
Calculating MSE!

L'output è sorprendente? get_MSE stato stampato solo una volta anche se è stato chiamato tre volte.

Per spiegare, l'istruzione print viene eseguita quando Function esegue il codice originale per creare il grafico in un processo noto come "tracing" . La traccia acquisisce le operazioni TensorFlow in un grafico e la print non viene acquisita nel grafico. Quel grafico viene quindi eseguito per tutte e tre le chiamate senza mai eseguire nuovamente il codice Python .

Come controllo di integrità, disattiviamo l'esecuzione del grafico per confrontare:

# Now, globally set everything to run eagerly to force eager execution.
tf.config.run_functions_eagerly(True)
# Observe what is printed below.
error = get_MSE(y_true, y_pred)
error = get_MSE(y_true, y_pred)
error = get_MSE(y_true, y_pred)
Calculating MSE!
Calculating MSE!
Calculating MSE!
tf.config.run_functions_eagerly(False)

print è un effetto collaterale di Python e ci sono altre differenze di cui dovresti essere a conoscenza quando converti una funzione in una Function .

tf.function best practice

Potrebbe volerci del tempo per abituarsi al comportamento di Function . Per iniziare rapidamente, gli utenti alle prime @tf.function dovrebbero giocare con le funzioni di decorazione dei giocattoli con @tf.function per fare esperienza con il passaggio dall'esecuzione ansiosa a quella grafica.

Progettare per tf.function può essere la soluzione migliore per scrivere programmi TensorFlow compatibili con i grafici. Ecco alcuni suggerimenti:

  • Alterna tra l'esecuzione ansiosa e l'esecuzione del grafico in anticipo e spesso con tf.config.run_functions_eagerly per individuare se/quando le due modalità divergono.
  • Crea tf.Variable all'esterno della funzione Python e modificale all'interno. Lo stesso vale per gli oggetti che utilizzano tf.Variable , comekeras.layers , keras.Model tf.optimizers .
  • Evita di scrivere funzioni che dipendono da variabili Python esterne , escludendo tf.Variable s e oggetti Keras.
  • Preferisci scrivere funzioni che accettano tensori e altri tipi di TensorFlow come input. Puoi passare altri tipi di oggetti ma fai attenzione !
  • Includere quanti più calcoli possibile in una tf.function per massimizzare il guadagno in termini di prestazioni. Ad esempio, decora un'intera fase di allenamento o l'intero ciclo di allenamento.

Vedendo l'accelerazione

tf.function solito migliora le prestazioni del tuo codice, ma la quantità di accelerazione dipende dal tipo di calcolo che esegui. I piccoli calcoli possono essere dominati dal sovraccarico di chiamare un grafico. Puoi misurare la differenza di prestazioni in questo modo:

x = tf.random.uniform(shape=[10, 10], minval=-1, maxval=2, dtype=tf.dtypes.int32)

def power(x, y):
  result = tf.eye(10, dtype=tf.dtypes.int32)
  for _ in range(y):
    result = tf.matmul(x, result)
  return result
print("Eager execution:", timeit.timeit(lambda: power(x, 100), number=1000))
Eager execution: 1.8956256799999665
power_as_graph = tf.function(power)
print("Graph execution:", timeit.timeit(lambda: power_as_graph(x, 100), number=1000))
Graph execution: 0.5566363650000312

tf.function è comunemente usato per velocizzare i cicli di allenamento e puoi saperne di più in Scrivere un ciclo di allenamento da zero con Keras.

Prestazioni e compromessi

I grafici possono velocizzare il codice, ma il processo di creazione ha un sovraccarico. Per alcune funzioni, la creazione del grafico richiede più tempo dell'esecuzione del grafico. Questo investimento viene in genere rapidamente ripagato con l'aumento delle prestazioni delle esecuzioni successive, ma è importante essere consapevoli che i primi passaggi di qualsiasi addestramento di modelli di grandi dimensioni possono essere più lenti a causa della traccia.

Non importa quanto sia grande il tuo modello, vuoi evitare di tracciare frequentemente. La guida tf.function illustra come impostare le specifiche di input e utilizzare gli argomenti del tensore per evitare il ritracciamento. Se ti accorgi che stai ottenendo prestazioni insolitamente scarse, è una buona idea controllare se stai ritracciando accidentalmente.

Quando viene tracciata una Function ?

Per capire quando la tua Function sta tracciando, aggiungi un'istruzione print al suo codice. Come regola generale, Function eseguirà l'istruzione print ogni volta che traccia.

@tf.function
def a_function_with_python_side_effect(x):
  print("Tracing!") # An eager-only side effect.
  return x * x + tf.constant(2)

# This is traced the first time.
print(a_function_with_python_side_effect(tf.constant(2)))
# The second time through, you won't see the side effect.
print(a_function_with_python_side_effect(tf.constant(3)))
Tracing!
tf.Tensor(6, shape=(), dtype=int32)
tf.Tensor(11, shape=(), dtype=int32)
# This retraces each time the Python argument changes,
# as a Python argument could be an epoch count or other
# hyperparameter.
print(a_function_with_python_side_effect(2))
print(a_function_with_python_side_effect(3))
Tracing!
tf.Tensor(6, shape=(), dtype=int32)
Tracing!
tf.Tensor(11, shape=(), dtype=int32)

I nuovi argomenti Python attivano sempre la creazione di un nuovo grafico, quindi la traccia aggiuntiva.

Prossimi passi

Puoi saperne di più su tf.function nella pagina di riferimento API e seguendo la guida Better performance with tf.function .