Introduction aux graphes et tf.function

Voir sur TensorFlow.org Exécuter dans Google Colab Voir la source sur GitHub Télécharger le cahier

Aperçu

Ce guide va sous la surface de TensorFlow et Keras pour montrer comment fonctionne TensorFlow. Si vous souhaitez plutôt commencer immédiatement avec Keras, consultez la collection de guides Keras .

Dans ce guide, vous apprendrez comment TensorFlow vous permet d'apporter des modifications simples à votre code pour obtenir des graphiques, comment les graphiques sont stockés et représentés, et comment vous pouvez les utiliser pour accélérer vos modèles.

Ceci est une vue d'ensemble qui explique comment tf.function vous permet de passer d'une exécution tf.function à une exécution graphique. Pour une spécification plus complète de tf.function , consultez le guide tf.function .

Que sont les graphiques ?

Dans les trois guides précédents, vous avez exécuté TensorFlow avec impatience . Cela signifie que les opérations TensorFlow sont exécutées par Python, opération par opération, et renvoient les résultats à Python.

Alors que l'exécution rapide présente plusieurs avantages uniques, l'exécution graphique permet la portabilité en dehors de Python et a tendance à offrir de meilleures performances. L'exécution d'un graphique signifie que les calculs de tenseur sont exécutés sous la forme d'un graphique TensorFlow , parfois appelé tf.Graph ou simplement un « graphe ».

Les graphes sont des structures de données qui contiennent un ensemble d'objets tf.Operation , qui représentent des unités de calcul ; et les objets tf.Tensor , qui représentent les unités de données qui circulent entre les opérations. Ils sont définis dans un contexte tf.Graph . Étant donné que ces graphiques sont des structures de données, ils peuvent être enregistrés, exécutés et restaurés sans le code Python d'origine.

Voici à quoi ressemble un graphique TensorFlow représentant un réseau de neurones à deux couches lorsqu'il est visualisé dans TensorBoard.

Un simple graphique TensorFlow

Les avantages des graphiques

Avec un graphique, vous avez une grande flexibilité. Vous pouvez utiliser votre graphique TensorFlow dans des environnements qui n'ont pas d'interpréteur Python, comme les applications mobiles, les appareils intégrés et les serveurs principaux. TensorFlow utilise des graphiques comme format pour les modèles enregistrés lorsqu'il les exporte depuis Python.

Les graphiques sont également facilement optimisés, permettant au compilateur d'effectuer des transformations telles que :

  • Déduire statiquement la valeur des tenseurs en pliant les nœuds constants dans votre calcul ("pliage constant") .
  • Séparez les sous-parties d'un calcul qui sont indépendantes et répartissez-les entre des threads ou des périphériques.
  • Simplifiez les opérations arithmétiques en éliminant les sous-expressions courantes.

Il existe un système d'optimisation complet, Grappler , pour effectuer cette accélération et d'autres.

En bref, les graphiques sont extrêmement utiles et permettent à votre TensorFlow de s'exécuter rapidement , de fonctionner en parallèle et de s'exécuter efficacement sur plusieurs appareils .

Cependant, vous souhaitez toujours définir vos modèles d'apprentissage automatique (ou d'autres calculs) en Python pour plus de commodité, puis construire automatiquement des graphiques lorsque vous en avez besoin.

Tirer parti des graphiques

Vous créez et exécutez un graphique dans TensorFlow à l'aide de tf.function , soit en tant tf.function , soit en tant que décorateur. tf.function prend une fonction normale en entrée et renvoie une Function . Une Function est un appelable Python qui crée des graphiques TensorFlow à partir de la fonction Python. Vous utilisez une Function de la même manière que son équivalent 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)

À l'extérieur, une Function ressemble à une fonction normale que vous écrivez à l'aide d'opérations TensorFlow. En dessous , cependant, c'est très différent . Une Function encapsule plusieurs tf.Graph derrière une API . C'est ainsi que Function est en mesure de vous offrir les avantages de l'exécution graphique , comme la vitesse et la déployabilité.

tf.function s'applique à une fonction et à toutes les autres fonctions qu'elle appelle :

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 vous avez utilisé TensorFlow 1.x, vous remarquerez qu'à aucun moment vous n'avez eu besoin de définir un Placeholder ou tf.Session .

Conversion de fonctions Python en graphiques

Toute fonction que vous écrivez avec TensorFlow contiendra un mélange d'opérations TF intégrées et de logique Python, telles que if-then clauses if-then , des boucles, break , return , continue , etc. Alors que les opérations TensorFlow sont facilement capturées par un tf.Graph , la logique spécifique à Python doit subir une étape supplémentaire afin de faire partie du graphique. tf.function utilise une bibliothèque appelée AutoGraph ( tf.autograph ) pour convertir le code Python en code générant des graphiques.

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

Bien qu'il soit peu probable que vous ayez besoin de visualiser les graphiques directement, vous pouvez inspecter les sorties pour vérifier les résultats exacts. Ce ne sont pas faciles à lire, donc pas besoin de trop regarder !

# 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 plupart du temps, tf.function fonctionnera sans considérations particulières. Cependant, il y a quelques mises en garde, et le guide tf.function peut aider ici, ainsi que la référence complète d'AutoGraph

Polymorphisme : une Function , plusieurs graphes

Un tf.Graph est spécialisé dans un type spécifique d'entrées (par exemple, des tenseurs avec undtype spécifique ou des objets avec le même id() ).

Chaque fois que vous invoquez une Function avec de nouveaux dtypes et formes dans ses arguments, Function crée un nouveau tf.Graph pour les nouveaux arguments. Les dtypes et formes des entrées d'un tf.Graph sont appelés signature d'entrée ou simplement signature .

La Function stocke le tf.Graph correspondant à cette signature dans une ConcreteFunction . Une ConcreteFunction est un wrapper autour d'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 la Function a déjà été appelée avec cette signature, Function ne crée pas de nouveau 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)

Parce qu'elle est soutenue par plusieurs graphiques, une Function est polymorphe . Cela lui permet de prendre en charge plus de types d'entrée qu'un seul tf.Graph ne pourrait en représenter, ainsi que d'optimiser chaque tf.Graph pour de meilleures performances.

# 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,)

Utilisation de tf.function

Jusqu'à présent, vous avez appris à convertir une fonction Python en un graphique simplement en utilisant tf.function comme décorateur ou wrapper. Mais en pratique, faire fonctionner correctement tf.function peut être délicat ! Dans les sections suivantes, vous apprendrez comment faire fonctionner votre code comme prévu avec tf.function .

Exécution graphique vs exécution avide

Le code d'une Function peut être exécuté à la fois avec impatience et sous forme de graphique. Par défaut, Function exécute son code sous forme de graphique :

@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>

Pour vérifier que le graphe de votre Function effectue le même calcul que sa fonction Python équivalente, vous pouvez le faire exécuter avec impatience avec tf.config.run_functions_eagerly(True) . Il s'agit d'un commutateur qui désactive la capacité de Function à créer et à exécuter des graphiques , au lieu d'exécuter le code normalement.

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)

Cependant, Function peut se comporter différemment sous graphe et exécution rapide. La fonction d' print Python est un exemple de la différence entre ces deux modes. Voyons ce qui se passe lorsque vous insérez une instruction print dans votre fonction et que vous l'appelez à plusieurs reprises.

@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)

Observez ce qui est imprimé :

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

Le rendu est-il surprenant ? get_MSE n'est imprimé qu'une seule fois même s'il a été appelé trois fois.

Pour expliquer, l'instruction print est exécutée lorsque Function exécute le code d'origine afin de créer le graphique dans un processus appelé "tracing" . Le traçage capture les opérations TensorFlow dans un graphique, et l' print n'est pas capturée dans le graphique. Ce graphique est ensuite exécuté pour les trois appels sans jamais réexécuter le code Python .

Pour vérifier l'intégrité, désactivons l'exécution du graphique pour comparer :

# 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 est un effet secondaire de Python , et il existe d' autres différences que vous devez connaître lors de la conversion d'une fonction en Function .

Meilleures pratiques tf.function

Cela peut prendre un certain temps pour s'habituer au comportement de Function . Pour commencer rapidement, les utilisateurs @tf.function doivent jouer avec la décoration de fonctions de jouets avec @tf.function pour acquérir de l'expérience en passant de l'exécution avide à l'exécution graphique.

Concevoir pour tf.function peut être votre meilleurtf.function pour écrire des programmes TensorFlow compatibles avec les graphes. Voici quelques conseils:

Vu l'accélération

tf.function améliore généralement les performances de votre code, mais la quantité d'accélération dépend du type de calcul que vous exécutez. Les petits calculs peuvent être dominés par le surcoût lié à l'appel d'un graphe. Vous pouvez mesurer la différence de performance comme suit :

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 est couramment utilisé pour accélérer les boucles d'entraînement, et vous pouvez en savoir plus à ce sujet dans Écrire une boucle d'entraînement à partir de zéro avec Keras.

Performances et compromis

Les graphiques peuvent accélérer votre code, mais le processus de création de ceux-ci a une certaine surcharge. Pour certaines fonctions, la création du graphe prend plus de temps que l'exécution du graphe. Cet investissement est généralement rapidement remboursé grâce à l'amélioration des performances des exécutions ultérieures, mais il est important de savoir que les premières étapes de toute formation sur un grand modèle peuvent être plus lentes en raison du traçage.

Quelle que soit la taille de votre modèle, vous voulez éviter de tracer fréquemment. Le guide tf.function explique comment définir des spécifications d'entrée et utiliser des arguments de tenseur pour éviter de revenir en arrière. Si vous constatez que vous obtenez des performances inhabituellement médiocres, il est judicieux de vérifier si vous revenez accidentellement.

Quand une Function tracée ?

Pour savoir quand votre Function traçage, ajoutez une instruction print à son code. En règle générale, Function exécutera l'instruction print à chaque fois qu'elle tracera.

@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)

Les nouveaux arguments Python déclenchent toujours la création d'un nouveau graphe, d'où le traçage supplémentaire.

Prochaines étapes

Vous pouvez en savoir plus sur tf.function sur la page de référence de l'API et en suivant le guide Meilleures performances avec tf.function .