Introducción a las gráficas y la función tf.

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.

Un gráfico simple de TensorFlow

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:

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:

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 .