![]() | ![]() | ![]() | ![]() |
Visión general
Esta guía analiza la superficie de TensorFlow y Keras para ver cómo funciona TensorFlow. Si, en cambio, desea comenzar de inmediato con Keras, consulte nuestra colección de guías de Keras .
En esta guía, verá el núcleo de 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 general que cubre cómo tf.function
permite cambiar de una ejecución ávida a una ejecución gráfica. Para obtener una especificación más completa de tf.function
, consulte la guía de tf.function
.
¿Qué son las gráficas?
En las tres guías anteriores, has visto a TensorFlow funcionando 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 ávida 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 tensores 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, tiene 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 backend. 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 al compilador realizar transformaciones como:
- Infiera estáticamente el valor de los tensores plegando nodos constantes en su cálculo ("plegado constante") .
- Separe las subpartes de un cálculo que sean independientes y divídalas entre subprocesos o dispositivos.
- Simplifique las operaciones aritméticas eliminando subexpresiones comunes.
Hay un sistema de optimización completo, Grappler , para realizar esta y otras aceleraciones.
En resumen, los gráficos son extremadamente útiles y permiten que TensorFlow se ejecute rápidamente , se ejecute en paralelo y se ejecute de manera eficiente en varios dispositivos .
Sin embargo, aún desea definir nuestros modelos de aprendizaje automático (u otros cálculos) en Python por conveniencia, y luego construir gráficos automáticamente cuando los necesite.
Aprovechando las gráficas
Puedes crear y ejecutar un gráfico en TensorFlow mediante tf.function
, ya sea como una llamada directa o como un decorador. tf.function
toma una función regular como entrada y devuelve una Function
. Una Function
es un Python invocable 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.
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)
En el exterior, una Function
parece a una función normal que escribes con las operaciones de TensorFlow. 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
aplica a una función y a todas las demás funciones 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 utilizado TensorFlow 1.x, se dará cuenta de que en ningún momento es necesario definir un Placeholder
o tf.Session
.
Conversión de funciones de Python en gráficos
Cualquier función que escriba con TensorFlow contendrá una combinación de operaciones TF nativas 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 mediante un tf.Graph
, la lógica específica de Python debe someterse a 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 ver los resultados exactos. Estos no son fáciles de leer, por lo que no es necesario mirar con demasiada atención.
# 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 (retval_, do_return) def set_state(vars_): nonlocal retval_, do_return (retval_, do_return) = 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, ('retval_', 'do_return'), 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_INT32 type: DT_BOOL } } } 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_INT32 } } } node { name: "cond/Identity_1" op: "Identity" input: "cond:1" attr { key: "T" value { type: DT_BOOL } } } node { name: "Identity" op: "Identity" input: "cond/Identity" 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_INT32 } output_arg { name: "cond_identity_1" type: DT_BOOL } } node_def { name: "cond/Const" 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" } } 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_BOOL } } attr { key: "value" value { tensor { dtype: DT_BOOL tensor_shape { } bool_val: true } } } experimental_debug_info { original_node_names: "cond/Const_2" } } node_def { name: "cond/Const_3" 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_3" } } node_def { name: "cond/Identity" op: "Identity" input: "cond/Const_3:output:0" attr { key: "T" value { type: DT_INT32 } } experimental_debug_info { original_node_names: "cond/Identity" } } node_def { name: "cond/Const_4" 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_4" } } node_def { name: "cond/Identity_1" op: "Identity" input: "cond/Const_4:output:0" attr { key: "T" value { type: DT_BOOL } } 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" } arg_attr { key: 0 value { attr { key: "_output_shapes" value { list { shape { } } } } } } } function { signature { name: "cond_true_33" input_arg { name: "cond_identity_x" type: DT_INT32 } output_arg { name: "cond_identity" type: DT_INT32 } output_arg { name: "cond_identity_1" type: DT_BOOL } } node_def { name: "cond/Identity" op: "Identity" input: "cond_identity_x" attr { key: "T" value { type: DT_INT32 } } experimental_debug_info { original_node_names: "cond/Identity" } } 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_1" op: "Identity" input: "cond/Const:output:0" attr { key: "T" value { type: DT_BOOL } } 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" } arg_attr { key: 0 value { attr { key: "_output_shapes" value { list { shape { } } } } } } } } versions { producer: 561 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 tipodtype
específico u objetos con el mismo id()
).
Cada vez que invoca una Function
con nuevos 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
. Un 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 sees 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 la Function
ya ha sido llamada con esa firma, la 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á respaldado por múltiples gráficos, podemos decir que 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=[1, -1]) Returns: float32 Tensor, shape=(2,) my_relu(x) Args: x: float32 Tensor, shape=(2,) Returns: float32 Tensor, shape=(2,) my_relu(x) Args: x: float32 Tensor, shape=() Returns: float32 Tensor, shape=()
Usando tf.function
Hasta ahora, ha visto cómo puede 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 hacer que su código funcione como se espera con tf.function
.
Ejecución de gráficos versus ejecución ávida
El código de una Function
se puede ejecutar con entusiasmo y como gráfico. De forma predeterminada, 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 8 6 3 8], shape=(5,), dtype=int32) tf.Tensor([9 8 3 6 1], shape=(5,), dtype=int32)
get_MSE(y_true, y_pred)
<tf.Tensor: shape=(), dtype=int32, numpy=26>
Para verificar que el gráfico de su Function
está haciendo el mismo cálculo que su función Python equivalente, podemos 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=26>
# Don't forget to set it back when you are done.
tf.config.run_functions_eagerly(False)
Sin embargo, la Function
puede comportarse de manera diferente bajo el gráfico y la ejecución ansiosa. La función de print
Python es un ejemplo de cómo se diferencian estos dos modos. Veamos qué sucede cuando insertamos una declaración de print
en nuestra función y la llamamos 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!
¿Es sorprendente la salida? get_MSE
solo se imprimió una vez a pesar de que 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 "rastreo" . El seguimiento captura las operaciones de TensorFlow en un gráfico y la 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 Python .
Como prueba 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
.
tf.function
mejores prácticas
Puede llevar algún tiempo acostumbrarse al comportamiento de Function
. Para comenzar rápidamente, los usuarios principiantes deben jugar con las funciones de decoración de juguetes con @tf.function
para obtener experiencia al pasar de la ejecución ansiosa a la ejecución de gráficos.
Diseñar para tf.function
puede ser su mejortf.function
para escribir programas de TensorFlow compatibles con gráficos. A continuación se ofrecen algunos consejos:
- Alterne entre ejecución ávida y gráfica temprana y frecuentemente con
tf.config.run_functions_eagerly
para identificar si / cuando los dos modos divergen. - Cree
tf.Variable
s fuera de la función de Python ytf.Variable
en el interior. Lo mismo ocurre con los objetos que usantf.Variable
, comokeras.layers
,keras.Model
sytf.optimizers
. - Evite escribir funciones que dependan de variables externas de Python , excluyendo
tf.Variables
y objetos Keras. - Prefiere escribir funciones que tomen 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 función
tf.function
para maximizar la ganancia de rendimiento. Por ejemplo, decore un paso de entrenamiento completo o todo el ciclo 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: 1.9166935040000226
power_as_graph = tf.function(power)
print("Graph execution:", timeit.timeit(lambda: power_as_graph(x, 100), number=1000))
Graph execution: 0.5355543040000157
tf.function
se usa comúnmente para acelerar los ciclos de entrenamiento, como puede ver aquí 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 amortiza rápidamente con el aumento de rendimiento de las ejecuciones posteriores, pero es importante tener en cuenta que los primeros pasos de cualquier entrenamiento de modelo grande pueden ser más lentos debido al rastreo.
No importa cuán grande sea su modelo, desea evitar el rastreo con frecuencia. La guía tf.function
analiza cómo establecer especificaciones de entrada y usar argumentos tensoriales para evitar el retroceso. Si encuentra que está obteniendo un rendimiento inusualmente bajo, es una buena idea verificar si está retrocediendo accidentalmente.
¿Cuándo se realiza el seguimiento de 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 rastree.
@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)
Aquí, verá un seguimiento adicional porque los nuevos argumentos de Python siempre activan la creación de un nuevo gráfico.
Próximos pasos
Puede leer una discusión más detallada tanto en la página de referencia de la API de tf.function
como en la guía .