Ver en TensorFlow.org | Ejecutar en Google Colab | Ver fuente en GitHub | Descargar libreta |
Descripción general
Esta guía profundiza en TensorFlow y Keras para demostrar cómo funciona TensorFlow. Si, en cambio, desea comenzar de inmediato con Keras, consulte la colección de guías de Keras .
En esta guía, aprenderá cómo TensorFlow le permite realizar cambios simples en su código para obtener gráficos, cómo se almacenan y representan los gráficos y cómo puede usarlos para acelerar sus modelos.
Esta es una descripción general que cubre cómo tf.function
le permite cambiar de una ejecución ansiosa a una ejecución gráfica. Para una especificación más completa de tf.function
, vaya a la guía de tf.function
.
¿Qué son los gráficos?
En las tres guías anteriores, ejecutó TensorFlow con entusiasmo . Esto significa que Python ejecuta las operaciones de TensorFlow, operación por operación, y devuelve los resultados a Python.
Si bien la ejecución entusiasta tiene varias ventajas únicas, la ejecución de gráficos permite la portabilidad fuera de Python y tiende a ofrecer un mejor rendimiento. La ejecución de gráficos significa que los cálculos de tensor se ejecutan como un gráfico de TensorFlow , a veces denominado tf.Graph
o simplemente "gráfico".
Los gráficos son estructuras de datos que contienen un conjunto de objetos tf.Operation
, que representan unidades de cálculo; y objetos tf.Tensor
, que representan las unidades de datos que fluyen entre operaciones. Se definen en un contexto tf.Graph
. Dado que estos gráficos son estructuras de datos, se pueden guardar, ejecutar y restaurar sin el código Python original.
Así es como se ve un gráfico de TensorFlow que representa una red neuronal de dos capas cuando se visualiza en TensorBoard.
Los beneficios de los gráficos.
Con un gráfico, tienes una gran flexibilidad. Puede usar su gráfico de TensorFlow en entornos que no tienen un intérprete de Python, como aplicaciones móviles, dispositivos integrados y servidores back-end. TensorFlow usa gráficos como formato para los modelos guardados cuando los exporta desde Python.
Los gráficos también se optimizan fácilmente, lo que permite que el compilador realice transformaciones como:
- Infiera estáticamente el valor de los tensores plegando nodos constantes en su cálculo ("plegamiento constante") .
- Separe las subpartes de un cálculo que son independientes y divídalas entre subprocesos o dispositivos.
- Simplifique las operaciones aritméticas eliminando subexpresiones comunes.
Existe todo un sistema de optimización, Grappler , para realizar esta y otras aceleraciones.
En resumen, los gráficos son extremadamente útiles y permiten que su TensorFlow funcione rápido , en paralelo y de manera eficiente en varios dispositivos .
Sin embargo, aún desea definir sus modelos de aprendizaje automático (u otros cálculos) en Python por conveniencia y luego construir gráficos automáticamente cuando los necesite.
Configuración
import tensorflow as tf
import timeit
from datetime import datetime
Aprovechando los gráficos
Creas y ejecutas un gráfico en TensorFlow usando tf.function
, ya sea como una llamada directa o como decorador. tf.function
toma una función normal como entrada y devuelve una Function
. Una Function
es una llamada de Python que crea gráficos de TensorFlow a partir de la función de Python. Utiliza una Function
de la misma manera que su equivalente en Python.
# 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)
En el exterior, una Function
parece una función normal que escribes usando operaciones de TensorFlow. Por debajo , sin embargo, es muy diferente . Una Function
encapsula varios tf.Graph
s detrás de una API . Así es como Function
puede brindarle los beneficios de la ejecución de gráficos , como la velocidad y la capacidad de implementación.
tf.function
se aplica a una función y a todas las demás funciones a las que llama :
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)
Si ha usado TensorFlow 1.x, notará que en ningún momento necesitó definir un tf.Session
Placeholder
Convertir funciones de Python en gráficos
Cualquier función que escriba con TensorFlow contendrá una combinación de operaciones TF integradas y lógica de Python, como cláusulas if-then
, bucles, break
, return
, continue
y más. Si bien las operaciones de TensorFlow se capturan fácilmente con un tf.Graph
, la lógica específica de Python debe realizar un paso adicional para convertirse en parte del gráfico. tf.function
usa una biblioteca llamada AutoGraph ( tf.autograph
) para convertir código Python en código generador de gráficos.
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
Aunque es poco probable que necesite ver los gráficos directamente, puede inspeccionar las salidas para comprobar los resultados exactos. Estos no son fáciles de leer, ¡así que no es necesario mirar con demasiado cuidado!
# 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 retval_, do_return (do_return, retval_) = vars_ def if_body(): nonlocal retval_, do_return try: do_return = True retval_ = ag__.ld(x) except: do_return = False raise def else_body(): nonlocal retval_, do_return 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 } } } } 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 } } } } 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 } } } } 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 } } } } node_def { name: "cond/Identity" op: "Identity" input: "cond/Const_3:output:0" attr { key: "T" value { type: DT_BOOL } } } 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 } } } } node_def { name: "cond/Identity_1" op: "Identity" input: "cond/Const_4:output:0" attr { key: "T" value { type: DT_INT32 } } } 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 } } } } node_def { name: "cond/Identity" op: "Identity" input: "cond/Const:output:0" attr { key: "T" value { type: DT_BOOL } } } node_def { name: "cond/Identity_1" op: "Identity" input: "cond_identity_1_x" attr { key: "T" value { type: DT_INT32 } } } 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: 898 min_consumer: 12 }
La mayoría de las veces, tf.function
funcionará sin consideraciones especiales. Sin embargo, hay algunas advertencias, y la guía tf.function puede ayudar aquí, así como la referencia completa de AutoGraph
Polimorfismo: una Function
, muchos gráficos
Un tf.Graph
está especializado en un tipo específico de entradas (por ejemplo, tensores con un dtype
específico u objetos con el mismo id()
).
Cada vez que invoque una Function
con nuevos tipos de dtypes
y formas en sus argumentos, Function
crea un nuevo tf.Graph
para los nuevos argumentos. Los dtypes
y formas de las entradas de un tf.Graph
se conocen como una firma de entrada o simplemente una firma .
La Function
almacena el tf.Graph
correspondiente a esa firma en una función ConcreteFunction
. Una ConcreteFunction
es un envoltorio alrededor de 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)
Si ya se ha llamado a la Function
con esa firma, Function
no crea un nuevo 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)
Debido a que está respaldada por varios gráficos, una Function
es polimórfica . Eso le permite admitir más tipos de entrada de los que podría representar un solo tf.Graph
, así como optimizar cada tf.Graph
para un mejor rendimiento.
# 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,)
Uso tf.function
Hasta ahora, ha aprendido cómo convertir una función de Python en un gráfico simplemente usando tf.function
como decorador o envoltorio. ¡Pero en la práctica, hacer que tf.function
funcione correctamente puede ser complicado! En las siguientes secciones, aprenderá cómo puede hacer que su código funcione como se espera con tf.function
.
Ejecución de gráficos frente a ejecución ansiosa
El código en una Function
se puede ejecutar tanto con entusiasmo como con un gráfico. Por defecto, Function
ejecuta su código como un gráfico:
@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 0 4 4 7], shape=(5,), dtype=int32) tf.Tensor([3 6 3 0 6], shape=(5,), dtype=int32)
get_MSE(y_true, y_pred)
<tf.Tensor: shape=(), dtype=int32, numpy=11>
Para verificar que el gráfico de su Function
está haciendo el mismo cálculo que su función de Python equivalente, puede hacer que se ejecute con entusiasmo con tf.config.run_functions_eagerly(True)
. Este es un interruptor que desactiva la capacidad de Function
para crear y ejecutar gráficos , en lugar de ejecutar el código normalmente.
tf.config.run_functions_eagerly(True)
get_MSE(y_true, y_pred)
<tf.Tensor: shape=(), dtype=int32, numpy=11>
# Don't forget to set it back when you are done.
tf.config.run_functions_eagerly(False)
Sin embargo, Function
puede comportarse de manera diferente bajo el gráfico y la ejecución ansiosa. La función de print
de Python es un ejemplo de cómo estos dos modos difieren. Veamos qué sucede cuando inserta una declaración de print
en su función y la llama repetidamente.
@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)
Observa lo que está impreso:
error = get_MSE(y_true, y_pred)
error = get_MSE(y_true, y_pred)
error = get_MSE(y_true, y_pred)
Calculating MSE!
¿La salida es sorprendente? get_MSE
solo se imprimió una vez, aunque se llamó tres veces.
Para explicarlo, la declaración de print
se ejecuta cuando Function
ejecuta el código original para crear el gráfico en un proceso conocido como "seguimiento" . El seguimiento captura las operaciones de TensorFlow en un gráfico y print
no se captura en el gráfico. Luego, ese gráfico se ejecuta para las tres llamadas sin volver a ejecutar el código de Python .
Como control de cordura, apaguemos la ejecución del gráfico para comparar:
# 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
es un efecto secundario de Python , y hay otras diferencias que debe tener en cuenta al convertir una función en una Function
. Obtenga más información en la sección Limitaciones de la guía Mejor rendimiento con tf.function .
Ejecución no estricta
La ejecución de gráficos solo ejecuta las operaciones necesarias para producir los efectos observables, que incluyen:
- El valor de retorno de la función.
- Efectos secundarios bien conocidos documentados, tales como:
- Operaciones de entrada/salida, como
tf.print
- Operaciones de depuración, como las funciones de aserción en
tf.debugging
- Mutaciones de
tf.Variable
- Operaciones de entrada/salida, como
Este comportamiento generalmente se conoce como "ejecución no estricta" y difiere de la ejecución ansiosa, que recorre todas las operaciones del programa, sean necesarias o no.
En particular, la verificación de errores en tiempo de ejecución no cuenta como un efecto observable. Si se omite una operación porque no es necesaria, no puede generar ningún error de tiempo de ejecución.
En el siguiente ejemplo, la operación "innecesaria" tf.gather
se omite durante la ejecución del gráfico, por lo que el error de tiempo de ejecución InvalidArgumentError
no se genera como lo haría en una ejecución ansiosa. No confíe en que se genere un error al ejecutar un gráfico.
def unused_return_eager(x):
# Get index 1 will fail when `len(x) == 1`
tf.gather(x, [1]) # unused
return x
try:
print(unused_return_eager(tf.constant([0.0])))
except tf.errors.InvalidArgumentError as e:
# All operations are run during eager execution so an error is raised.
print(f'{type(e).__name__}: {e}')
tf.Tensor([0.], shape=(1,), dtype=float32)
@tf.function
def unused_return_graph(x):
tf.gather(x, [1]) # unused
return x
# Only needed operations are run during graph exection. The error is not raised.
print(unused_return_graph(tf.constant([0.0])))
tf.Tensor([0.], shape=(1,), dtype=float32)
mejores prácticas tf.function
Puede llevar algún tiempo acostumbrarse al comportamiento de Function
. Para comenzar rápidamente, los usuarios primerizos deben jugar decorando funciones de juguete con @tf.function
para obtener experiencia al pasar de la ejecución ansiosa a la gráfica.
Diseñar para tf.function
puede ser su mejor opción para escribir programas TensorFlow compatibles con gráficos. Aquí hay algunos consejos:
- Alterne entre la ejecución ansiosa y gráfica de manera temprana y frecuente con
tf.config.run_functions_eagerly
para identificar si los dos modos divergen o cuándo. - Cree
tf.Variable
s fuera de la función de Python y modifíquelos en el interior. Lo mismo ocurre con los objetos que usantf.Variable
, comokeras.layers
,keras.Model
s ytf.optimizers
. - Evite escribir funciones que dependan de variables externas de Python , excluyendo
tf.Variable
sy objetos Keras. - Prefiere escribir funciones que toman tensores y otros tipos de TensorFlow como entrada. Puede pasar otros tipos de objetos, ¡pero tenga cuidado !
- Incluya tantos cálculos como sea posible en una
tf.function
para maximizar la ganancia de rendimiento. Por ejemplo, decora un paso de entrenamiento completo o todo el bucle de entrenamiento.
Viendo la aceleración
tf.function
generalmente mejora el rendimiento de su código, pero la cantidad de aceleración depende del tipo de cálculo que ejecute. Los cálculos pequeños pueden estar dominados por la sobrecarga de llamar a un gráfico. Puede medir la diferencia en el rendimiento de la siguiente manera:
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: 2.5637862179974036
power_as_graph = tf.function(power)
print("Graph execution:", timeit.timeit(lambda: power_as_graph(x, 100), number=1000))
Graph execution: 0.6832536700021592
tf.function
se usa comúnmente para acelerar los bucles de entrenamiento, y puede obtener más información al respecto en Escribir un bucle de entrenamiento desde cero con Keras.
Rendimiento y compensaciones
Los gráficos pueden acelerar su código, pero el proceso de creación tiene algunos gastos generales. Para algunas funciones, la creación del gráfico lleva más tiempo que la ejecución del gráfico. Esta inversión generalmente se recupera rápidamente con el aumento del rendimiento de las ejecuciones posteriores, pero es importante tener en cuenta que los primeros pasos de cualquier entrenamiento de modelos grandes pueden ser más lentos debido al seguimiento.
No importa cuán grande sea su modelo, desea evitar el seguimiento frecuente. La guía tf.function
analiza cómo establecer especificaciones de entrada y usar argumentos de tensor para evitar el retroceso. Si descubre que está obteniendo un rendimiento inusualmente bajo, es una buena idea verificar si está retrocediendo accidentalmente.
¿Cuándo se rastrea una Function
?
Para averiguar cuándo se está rastreando su Function
, agregue una declaración de print
a su código. Como regla general, Function
ejecutará la declaración de print
cada vez que realice un seguimiento.
@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)
Los nuevos argumentos de Python siempre desencadenan la creación de un nuevo gráfico, de ahí el seguimiento adicional.
Próximos pasos
Puede obtener más información sobre tf.function
en la página de referencia de la API y siguiendo la guía Mejor rendimiento con tf.function
.