TensorFlow 1.x vs TensorFlow 2 – Comportements et API

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

Sous le capot, TensorFlow 2 suit un paradigme de programmation fondamentalement différent de TF1.x.

Ce guide décrit les différences fondamentales entre TF1.x et TF2 en termes de comportements et d'API, et leur lien avec votre parcours de migration.

Résumé de haut niveau des principaux changements

Fondamentalement, TF1.x et TF2 utilisent un ensemble différent de comportements d'exécution autour de l'exécution (impatient dans TF2), des variables, du flux de contrôle, des formes de tenseur et des comparaisons d'égalité de tenseur. Pour être compatible avec TF2, votre code doit être compatible avec l'ensemble complet des comportements TF2. Pendant la migration, vous pouvez activer ou désactiver la plupart de ces comportements individuellement via les tf.compat.v1.enable_* ou tf.compat.v1.disable_* . La seule exception est la suppression des collections, qui est un effet secondaire de l'activation/désactivation de l'exécution hâtive.

À un niveau élevé, TensorFlow 2 :

  • Supprime les API redondantes .
  • Rend les API plus cohérentes - par exemple, les RNN unifiés et les optimiseurs unifiés .
  • Préfère les fonctions aux sessions et s'intègre mieux au runtime Python avec l'exécution Eager activée par défaut avec tf.function qui fournit des dépendances de contrôle automatique pour les graphiques et la compilation.
  • Obsolète les collections de graphes globales .
  • Modifie la sémantique de concurrence variable en utilisant ResourceVariables sur ReferenceVariables .
  • Prend en charge le flux de contrôle basé sur les fonctions et différenciable (Control Flow v2).
  • Simplifie l'API TensorShape pour contenir int s au lieu des objets tf.compat.v1.Dimension .
  • Met à jour la mécanique de l'égalité des tenseurs. Dans TF1.x, l'opérateur == sur les tenseurs et les variables vérifie l'égalité des références d'objet. Dans TF2, il vérifie l'égalité des valeurs. De plus, les tenseurs/variables ne sont plus hachables, mais vous pouvez obtenir des références d'objet hachables via var.ref() si vous avez besoin de les utiliser dans des ensembles ou comme clés dict .

Les sections ci-dessous fournissent un peu plus de contexte sur les différences entre TF1.x et TF2. Pour en savoir plus sur le processus de conception de TF2, lisez les RFC et les docs de conception .

Nettoyage des API

De nombreuses API ont disparu ou ont été déplacées dans TF2. Certains des changements majeurs incluent la suppression de tf.app , tf.flags et tf.logging en faveur de l ' absl-py désormais open source , la relocalisation des projets qui vivaient dans tf.contrib et le nettoyage de l'espace de noms principal tf.* par déplacer les fonctions les moins utilisées dans des sous-paquetages comme tf.math . Certaines API ont été remplacées par leurs équivalents TF2 - tf.summary , tf.keras.metrics et tf.keras.optimizers .

tf.compat.v1 : points de terminaison de l'API héritée et de compatibilité

Les symboles sous les espaces de noms tf.compat et tf.compat.v1 ne sont pas considérés comme des API TF2. Ces espaces de noms exposent un mélange de symboles de compatibilité, ainsi que des points de terminaison d'API hérités de TF 1.x. Celles-ci sont destinées à faciliter la migration de TF1.x vers TF2. Cependant, comme aucune de ces API compat.v1 n'est une API TF2 idiomatique, ne les utilisez pas pour écrire du tout nouveau code TF2.

Les symboles tf.compat.v1 individuels peuvent être compatibles avec TF2 car ils continuent de fonctionner même avec les comportements TF2 activés (tels que tf.compat.v1.losses.mean_squared_error ), tandis que d'autres sont incompatibles avec TF2 (tels que tf.compat.v1.metrics.accuracy ). De nombreux symboles compat.v1 (mais pas tous) contiennent des informations de migration dédiées dans leur documentation qui expliquent leur degré de compatibilité avec les comportements TF2, ainsi que la façon de les migrer vers les API TF2.

Le script de mise à niveau TF2 peut mapper de nombreux symboles d'API compat.v1 vers des API TF2 équivalentes dans le cas où ils sont des alias ou ont les mêmes arguments mais avec un ordre différent. Vous pouvez également utiliser le script de mise à niveau pour renommer automatiquement les API TF1.x.

API de faux amis

Il existe un ensemble de symboles "faux amis" trouvés dans l'espace de noms TF2 tf (pas sous compat.v1 ) qui ignorent en fait les comportements TF2 sous le capot et/ou ne sont pas entièrement compatibles avec l'ensemble complet des comportements TF2. En tant que telles, ces API sont susceptibles de mal se comporter avec le code TF2, potentiellement de manière silencieuse.

  • tf.estimator.* : Les estimateurs créent et utilisent des graphiques et des sessions sous le capot. En tant que tels, ceux-ci ne doivent pas être considérés comme compatibles avec TF2. Si votre code exécute des estimateurs, il n'utilise pas les comportements TF2.
  • keras.Model.model_to_estimator(...) : Cela crée un estimateur sous le capot, qui, comme mentionné ci-dessus, n'est pas compatible avec TF2.
  • tf.Graph().as_default() : Cela entre dans les comportements de graphe TF1.x et ne suit pas les comportements standard de tf.function compatibles avec TF2. Le code qui entre dans des graphiques comme celui-ci les exécutera généralement via des sessions et ne doit pas être considéré comme compatible avec TF2.
  • tf.feature_column.* Les API de colonne de fonctionnalités reposent généralement sur la création de variables tf.compat.v1.get_variable de style TF1 et supposent que les variables créées seront accessibles via des collections globales. Comme TF2 ne prend pas en charge les collections, les API peuvent ne pas fonctionner correctement lors de leur exécution avec les comportements TF2 activés.

Autres modifications de l'API

  • TF2 présente des améliorations significatives des algorithmes de placement d'appareils, ce qui rend inutile l'utilisation de tf.colocate_with . Si sa suppression entraîne une dégradation des performances , veuillez signaler un bogue .

  • Remplacez toute utilisation de tf.v1.ConfigProto par des fonctions équivalentes de tf.config .

Exécution impatiente

TF1.x vous obligeait à assembler manuellement un arbre de syntaxe abstraite (le graphe) en effectuant des appels d'API tf.* , puis à compiler manuellement l'arbre de syntaxe abstraite en transmettant un ensemble de tenseurs de sortie et de tenseurs d'entrée à un appel session.run . TF2 s'exécute avec impatience (comme Python le fait normalement) et donne aux graphiques et aux sessions l'impression d'être des détails d'implémentation.

Un sous-produit notable de l'exécution hâtive est que tf.control_dependencies n'est plus nécessaire, car toutes les lignes de code s'exécutent dans l'ordre (dans un tf.function , le code avec des effets secondaires s'exécute dans l'ordre écrit).

Plus de globals

TF1.x s'appuyait fortement sur des collections et des espaces de noms globaux implicites. Lorsque vous tf.Variable , elle était placée dans une collection du graphe par défaut et y restait, même si vous perdiez la trace de la variable Python pointant vers elle. Vous pourriez alors récupérer ce tf.Variable , mais seulement si vous connaissiez le nom avec lequel il avait été créé. C'était difficile à faire si vous ne contrôliez pas la création de la variable. En conséquence, toutes sortes de mécanismes ont proliféré pour tenter de vous aider à retrouver vos variables, et pour les frameworks de trouver des variables créées par l'utilisateur. Certains d'entre eux incluent : les étendues de variables, les collections globales, les méthodes d'assistance telles que tf.get_global_step et tf.global_variables_initializer , les optimiseurs calculant implicitement les gradients sur toutes les variables pouvant être formées, etc. TF2 élimine tous ces mécanismes ( Variables 2.0 RFC ) au profit du mécanisme par défaut - vous gardez une trace de vos variables. Si vous perdez la trace d'un tf.Variable , il est ramassé.

L'obligation de suivre les variables crée un travail supplémentaire, mais avec des outils tels que les shims de modélisation et des comportements tels que les collections de variables implicites orientées objet dans tf.Module s et tf.keras.layers.Layer s , la charge est minimisée.

Des fonctions, pas des sessions

Un appel session.run est presque comme un appel de fonction : vous spécifiez les entrées et la fonction à appeler, et vous récupérez un ensemble de sorties. Dans TF2, vous pouvez décorer une fonction Python à l'aide de tf.function pour la marquer pour la compilation JIT afin que TensorFlow l'exécute comme un seul graphique ( Functions 2.0 RFC ). Ce mécanisme permet à TF2 de bénéficier de tous les avantages du mode graphique :

  • Performance : La fonction peut être optimisée (node ​​pruning, kernel fusion, etc.)
  • Portabilité : La fonction peut être exportée/réimportée ( RFC SavedModel 2.0 ), ce qui vous permet de réutiliser et de partager des fonctions TensorFlow modulaires.
# TF1.x
outputs = session.run(f(placeholder), feed_dict={placeholder: input})
# TF2
outputs = f(input)

Avec le pouvoir d'intercaler librement le code Python et TensorFlow, vous pouvez tirer parti de l'expressivité de Python. Cependant, TensorFlow portable s'exécute dans des contextes sans interpréteur Python, tels que mobile, C++ et JavaScript. Pour éviter de réécrire votre code lors de l'ajout tf.function , utilisez AutoGraph pour convertir un sous-ensemble de constructions Python en leurs équivalents TensorFlow :

  • for / while -> tf.while_loop ( break et continue sont pris en charge)
  • if -> tf.cond
  • for _ in dataset -> dataset.reduce

AutoGraph prend en charge les imbrications arbitraires de flux de contrôle, ce qui permet d'implémenter de manière performante et concise de nombreux programmes ML complexes tels que des modèles de séquence, l'apprentissage par renforcement, des boucles de formation personnalisées, etc.

Adaptation aux changements de comportement TF 2.x

Votre migration vers TF2 n'est terminée qu'une fois que vous avez migré vers l'ensemble complet des comportements TF2. L'ensemble complet de comportements peut être activé ou désactivé via tf.compat.v1.enable_v2_behaviors et tf.compat.v1.disable_v2_behaviors . Les sections ci-dessous traitent en détail de chaque changement de comportement majeur.

Utilisation tf.function s

Les changements les plus importants apportés à vos programmes pendant la migration proviendront probablement du changement de paradigme du modèle de programmation fondamental des graphes et des sessions vers une exécution rapide et tf.function . Reportez-vous aux guides de migration TF2 pour en savoir plus sur le passage d'API incompatibles avec l'exécution hâtive et tf.function à des API compatibles avec eux.

Vous trouverez ci-dessous quelques modèles de programme courants qui ne sont liés à aucune API et qui peuvent causer des problèmes lors du passage de tf.Graph s et tf.compat.v1.Session s à une exécution rapide avec tf.function s.

Modèle 1 : la manipulation d'objets Python et la création de variables destinées à être effectuées une seule fois sont exécutées plusieurs fois

Dans les programmes TF1.x qui reposent sur des graphes et des sessions, on s'attend généralement à ce que toute la logique Python de votre programme ne s'exécute qu'une seule fois. Cependant, avec une exécution rapide et tf.function , il est juste de s'attendre à ce que votre logique Python soit exécutée au moins une fois, mais peut-être plusieurs fois (soit plusieurs fois avec impatience, soit plusieurs fois sur différentes traces tf.function ). Parfois, tf.function va même tracer deux fois sur la même entrée, provoquant des comportements inattendus (voir Exemples 1 et 2). Reportez-vous au guide des tf.function pour plus de détails.

Exemple 1 : création de variables

Prenons l'exemple ci-dessous, où la fonction crée une variable lorsqu'elle est appelée :

def f():
  v = tf.Variable(1.0)
  return v

with tf.Graph().as_default():
  with tf.compat.v1.Session() as sess:
    res = f()
    sess.run(tf.compat.v1.global_variables_initializer())
    sess.run(res)

Cependant, l'encapsulation naïve de la fonction ci-dessus qui contient la création de variable avec tf.function n'est pas autorisée. tf.function ne prend en charge que les créations de variables singleton lors du premier appel . Pour appliquer cela, lorsque tf.function détecte la création de variable dans le premier appel, il tentera à nouveau de tracer et générera une erreur s'il y a création de variable dans la deuxième trace.

@tf.function
def f():
  print("trace") # This will print twice because the python body is run twice
  v = tf.Variable(1.0)
  return v

try:
  f()
except ValueError as e:
  print(e)

Une solution de contournement consiste à mettre en cache et à réutiliser la variable après sa création lors du premier appel.

class Model(tf.Module):
  def __init__(self):
    self.v = None

  @tf.function
  def __call__(self):
    print("trace") # This will print twice because the python body is run twice
    if self.v is None:
      self.v = tf.Variable(0)
    return self.v

m = Model()
m()

Exemple 2 : Tenseurs hors champ en raison du retraçage de tf.function

Comme démontré dans l'exemple 1, tf.function retracera lorsqu'il détectera la création de variable lors du premier appel. Cela peut entraîner une confusion supplémentaire, car les deux tracés créeront deux graphiques. Lorsque le deuxième graphe du retraçage tente d'accéder à un tenseur à partir du graphe généré lors du premier traçage, Tensorflow génère une erreur se plaignant que le tenseur est hors de portée. Pour illustrer le scénario, le code ci-dessous crée un ensemble de données lors du premier appel tf.function . Cela fonctionnerait comme prévu.

class Model(tf.Module):
  def __init__(self):
    self.dataset = None

  @tf.function
  def __call__(self):
    print("trace") # This will print once: only traced once
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])
    it = iter(self.dataset)
    return next(it)

m = Model()
m()

Cependant, si nous essayons également de créer une variable lors du premier appel à tf.function , le code générera une erreur se plaignant que l'ensemble de données est hors de portée. En effet, le jeu de données se trouve dans le premier graphique, tandis que le deuxième graphique tente également d'y accéder.

class Model(tf.Module):
  def __init__(self):
    self.v = None
    self.dataset = None

  @tf.function
  def __call__(self):
    print("trace") # This will print twice because the python body is run twice
    if self.v is None:
      self.v = tf.Variable(0)
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])
    it = iter(self.dataset)
    return [self.v, next(it)]

m = Model()
try:
  m()
except TypeError as e:
  print(e) # <tf.Tensor ...> is out of scope and cannot be used here.

La solution la plus simple consiste à s'assurer que la création de la variable et la création de l'ensemble de données sont toutes deux en dehors de l'appel tf.funciton . Par example:

class Model(tf.Module):
  def __init__(self):
    self.v = None
    self.dataset = None

  def initialize(self):
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])
    if self.v is None:
      self.v = tf.Variable(0)

  @tf.function
  def __call__(self):
    it = iter(self.dataset)
    return [self.v, next(it)]

m = Model()
m.initialize()
m()

Cependant, il est parfois impossible d'éviter de créer des variables dans tf.function (telles que des variables d'emplacement dans certains optimiseurs TF keras ). Pourtant, nous pouvons simplement déplacer la création de l'ensemble de données en dehors de l'appel tf.function . La raison pour laquelle nous pouvons nous fier à cela est que tf.function recevra l'ensemble de données en tant qu'entrée implicite et que les deux graphiques pourront y accéder correctement.

class Model(tf.Module):
  def __init__(self):
    self.v = None
    self.dataset = None

  def initialize(self):
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])

  @tf.function
  def __call__(self):
    if self.v is None:
      self.v = tf.Variable(0)
    it = iter(self.dataset)
    return [self.v, next(it)]

m = Model()
m.initialize()
m()

Exemple 3 : recréations d'objets Tensorflow inattendues en raison de l'utilisation de dict

tf.function prend très mal en charge les effets secondaires de python tels que l'ajout à une liste ou la vérification/l'ajout à un dictionnaire. Plus de détails sont dans "De meilleures performances avec tf.function" . Dans l'exemple ci-dessous, le code utilise des dictionnaires pour mettre en cache les jeux de données et les itérateurs. Pour une même clé, chaque appel au modèle renverra le même itérateur du jeu de données.

class Model(tf.Module):
  def __init__(self):
    self.datasets = {}
    self.iterators = {}

  def __call__(self, key):
    if key not in self.datasets:
      self.datasets[key] = tf.compat.v1.data.Dataset.from_tensor_slices([1, 2, 3])
      self.iterators[key] = self.datasets[key].make_initializable_iterator()
    return self.iterators[key]

with tf.Graph().as_default():
  with tf.compat.v1.Session() as sess:
    m = Model()
    it = m('a')
    sess.run(it.initializer)
    for _ in range(3):
      print(sess.run(it.get_next())) # prints 1, 2, 3

Cependant, le modèle ci-dessus ne fonctionnera pas comme prévu dans tf.function . Pendant le traçage, tf.function ignorera l'effet secondaire python de l'ajout aux dictionnaires. Au lieu de cela, il se souvient uniquement de la création d'un nouvel ensemble de données et d'un itérateur. Par conséquent, chaque appel au modèle renverra toujours un nouvel itérateur. Ce problème est difficile à remarquer à moins que les résultats numériques ou les performances ne soient suffisamment significatifs. Par conséquent, nous recommandons aux utilisateurs de bien réfléchir au code avant d'envelopper naïvement tf.function dans le code python.

class Model(tf.Module):
  def __init__(self):
    self.datasets = {}
    self.iterators = {}

  @tf.function
  def __call__(self, key):
    if key not in self.datasets:
      self.datasets[key] = tf.data.Dataset.from_tensor_slices([1, 2, 3])
      self.iterators[key] = iter(self.datasets[key])
    return self.iterators[key]

m = Model()
for _ in range(3):
  print(next(m('a'))) # prints 1, 1, 1

Nous pouvons utiliser tf.init_scope pour soulever l'ensemble de données et la création de l'itérateur en dehors du graphique, pour obtenir le comportement attendu :

class Model(tf.Module):
  def __init__(self):
    self.datasets = {}
    self.iterators = {}

  @tf.function
  def __call__(self, key):
    if key not in self.datasets:
      # Lifts ops out of function-building graphs
      with tf.init_scope():
        self.datasets[key] = tf.data.Dataset.from_tensor_slices([1, 2, 3])
        self.iterators[key] = iter(self.datasets[key])
    return self.iterators[key]

m = Model()
for _ in range(3):
  print(next(m('a'))) # prints 1, 2, 3

La règle générale est d'éviter de s'appuyer sur les effets secondaires de Python dans votre logique et de ne les utiliser que pour déboguer vos traces.

Exemple 4 : Manipulation d'une liste Python globale

Le code TF1.x suivant utilise une liste globale de pertes qu'il utilise pour ne maintenir que la liste des pertes générées par l'étape d'apprentissage en cours. Notez que la logique Python qui ajoute les pertes à la liste ne sera appelée qu'une seule fois, quel que soit le nombre d'étapes d'entraînement pour lesquelles la session est exécutée.

all_losses = []

class Model():
  def __call__(...):
    ...
    all_losses.append(regularization_loss)
    all_losses.append(label_loss_a)
    all_losses.append(label_loss_b)
    ...

g = tf.Graph()
with g.as_default():
  ...
  # initialize all objects
  model = Model()
  optimizer = ...
  ...
  # train step
  model(...)
  total_loss = tf.reduce_sum(all_losses)
  optimizer.minimize(total_loss)
  ...
...
sess = tf.compat.v1.Session(graph=g)
sess.run(...)  

Cependant, si cette logique Python est naïvement mappée à TF2 avec une exécution rapide, la liste globale des pertes aura de nouvelles valeurs ajoutées à chaque étape de formation. Cela signifie que le code d'étape d'entraînement qui s'attendait auparavant à ce que la liste ne contienne que les pertes de l'étape d'entraînement actuelle voit désormais la liste des pertes de toutes les étapes d'entraînement exécutées jusqu'à présent. Il s'agit d'un changement de comportement involontaire, et la liste devra soit être effacée au début de chaque étape, soit rendue locale à l'étape de formation.

all_losses = []

class Model():
  def __call__(...):
    ...
    all_losses.append(regularization_loss)
    all_losses.append(label_loss_a)
    all_losses.append(label_loss_b)
    ...

# initialize all objects
model = Model()
optimizer = ...

def train_step(...)
  ...
  model(...)
  total_loss = tf.reduce_sum(all_losses) # global list is never cleared,
  # Accidentally accumulates sum loss across all training steps
  optimizer.minimize(total_loss)
  ...

Modèle 2 : Un tenseur symbolique destiné à être recalculé à chaque étape dans TF1.x est accidentellement mis en cache avec la valeur initiale lors du passage à impatient.

Ce modèle provoque généralement un mauvais comportement silencieux de votre code lors de l'exécution avec impatience en dehors de tf.functions, mais déclenche une InaccessibleTensorError si la mise en cache de la valeur initiale se produit à l'intérieur d'un tf.function . Cependant, sachez que pour éviter le modèle 1 ci-dessus, vous structurerez souvent par inadvertance votre code de manière à ce que cette mise en cache de la valeur initiale se produise en dehors de toute tf.function qui pourrait générer une erreur. Faites donc très attention si vous savez que votre programme peut être sensible à ce modèle.

La solution générale à ce modèle est de restructurer le code ou d'utiliser des callables Python si nécessaire pour s'assurer que la valeur est recalculée à chaque fois au lieu d'être accidentellement mise en cache.

Exemple 1 : Taux d'apprentissage/hyperparamètre/etc. horaires qui dépendent de l'étape globale

Dans l'extrait de code suivant, on s'attend à ce qu'à chaque exécution de la session, la valeur global_step la plus récente soit lue et qu'un nouveau taux d'apprentissage soit calculé.

g = tf.Graph()
with g.as_default():
  ...
  global_step = tf.Variable(0)
  learning_rate = 1.0 / global_step
  opt = tf.compat.v1.train.GradientDescentOptimizer(learning_rate)
  ...
  global_step.assign_add(1)
...
sess = tf.compat.v1.Session(graph=g)
sess.run(...)

Cependant, lorsque vous essayez de passer à impatient, méfiez-vous du fait que le taux d'apprentissage ne soit calculé qu'une seule fois puis réutilisé, plutôt que de suivre le calendrier prévu :

global_step = tf.Variable(0)
learning_rate = 1.0 / global_step # Wrong! Only computed once!
opt = tf.keras.optimizers.SGD(learning_rate)

def train_step(...):
  ...
  opt.apply_gradients(...)
  global_step.assign_add(1)
  ...

Étant donné que cet exemple spécifique est un modèle courant et que les optimiseurs ne doivent être initialisés qu'une seule fois plutôt qu'à chaque étape de formation, les optimiseurs TF2 prennent en charge les planifications tf.keras.optimizers.schedules.LearningRateSchedule ou les callables Python comme arguments pour le taux d'apprentissage et d'autres hyperparamètres.

Exemple 2 : les initialisations de nombres aléatoires symboliques affectées en tant qu'attributs d'objet puis réutilisées via un pointeur sont accidentellement mises en cache lors du passage à désireux

Considérez le module NoiseAdder suivant :

class NoiseAdder(tf.Module):
  def __init__(shape, mean):
    self.noise_distribution = tf.random.normal(shape=shape, mean=mean)
    self.trainable_scale = tf.Variable(1.0, trainable=True)

  def add_noise(input):
    return (self.noise_distribution + input) * self.trainable_scale

L'utiliser comme suit dans TF1.x calculera un nouveau tenseur de bruit aléatoire à chaque exécution de la session :

g = tf.Graph()
with g.as_default():
  ...
  # initialize all variable-containing objects
  noise_adder = NoiseAdder(shape, mean)
  ...
  # computation pass
  x_with_noise = noise_adder.add_noise(x)
  ...
...
sess = tf.compat.v1.Session(graph=g)
sess.run(...)

Cependant, dans TF2, l'initialisation de noise_adder au début entraînera le calcul de noise_distribution une seule fois et le gel de toutes les étapes d'apprentissage :

...
# initialize all variable-containing objects
noise_adder = NoiseAdder(shape, mean) # Freezes `self.noise_distribution`!
...
# computation pass
x_with_noise = noise_adder.add_noise(x)
...

Pour résoudre ce problème, refactorisez NoiseAdder pour appeler tf.random.normal chaque fois qu'un nouveau tenseur aléatoire est nécessaire, au lieu de faire référence au même objet tenseur à chaque fois.

class NoiseAdder(tf.Module):
  def __init__(shape, mean):
    self.noise_distribution = lambda: tf.random.normal(shape=shape, mean=mean)
    self.trainable_scale = tf.Variable(1.0, trainable=True)

  def add_noise(input):
    return (self.noise_distribution() + input) * self.trainable_scale

Modèle 3 : le code TF1.x s'appuie directement sur les tenseurs et les recherche par leur nom

Il est courant que les tests de code TF1.x reposent sur la vérification des tenseurs ou des opérations présents dans un graphe. Dans de rares cas, le code de modélisation s'appuiera également sur ces recherches par nom.

Les noms de tenseur ne sont pas générés du tout lors de l'exécution avec impatience en dehors de tf.function , donc toutes les utilisations de tf.Tensor.name doivent se produire à l'intérieur d'un tf.function . Gardez à l'esprit que les noms générés réels sont très susceptibles de différer entre TF1.x et TF2, même au sein du même tf.function , et les garanties de l'API n'assurent pas la stabilité des noms générés entre les versions de TF.

Modèle 4 : la session TF1.x n'exécute de manière sélective qu'une partie du graphique généré

Dans TF1.x, vous pouvez construire un graphe, puis choisir de n'en exécuter de manière sélective qu'un sous-ensemble avec une session en choisissant un ensemble d'entrées et de sorties qui ne nécessitent pas l'exécution de chaque opération du graphe.

Par exemple, vous pouvez avoir à la fois un générateur et un discriminateur à l'intérieur d'un même graphique, et utiliser des appels tf.compat.v1.Session.run séparés pour alterner entre uniquement l'entraînement du discriminateur ou uniquement l'entraînement du générateur.

Dans TF2, en raison des dépendances de contrôle automatique dans tf.function et de l'exécution rapide, il n'y a pas d'élagage sélectif des traces de tf.function . Un graphique complet contenant toutes les mises à jour de variables serait exécuté même si, par exemple, seule la sortie du discriminateur ou du générateur est sortie de la tf.function .

Ainsi, vous devrez soit utiliser plusieurs tf.function s contenant différentes parties du programme, soit un argument conditionnel à la tf.function sur laquelle vous vous branchez afin d'exécuter uniquement les choses que vous voulez réellement exécuter.

Suppression des collections

Lorsque l'exécution hâtive est activée, les API compat.v1 liées à la collection de graphes (y compris celles qui lisent ou écrivent dans des collections sous le capot telles que tf.compat.v1.trainable_variables ) ne sont plus disponibles. Certains peuvent déclencher des ValueError s, tandis que d'autres peuvent renvoyer silencieusement des listes vides.

L'utilisation la plus standard des collections dans TF1.x consiste à gérer les initialiseurs, l'étape globale, les poids, les pertes de régularisation, les pertes de sortie du modèle et les mises à jour de variables qui doivent être exécutées, par exemple à partir des couches BatchNormalization .

Pour gérer chacune de ces utilisations standard :

  1. Initialiseurs - Ignorer. L'initialisation manuelle des variables n'est pas nécessaire lorsque l'exécution hâtive est activée.
  2. Étape globale - Voir la documentation de tf.compat.v1.train.get_or_create_global_step pour les instructions de migration.
  3. Poids - Mappez vos modèles sur tf.Module s/ tf.keras.layers.Layer s/ tf.keras.Model s en suivant les instructions du guide de mappage de modèle , puis utilisez leurs mécanismes de suivi de poids respectifs tels que tf.module.trainable_variables .
  4. Pertes de régularisation - Mappez vos modèles sur tf.Module s/ tf.keras.layers.Layer s/ tf.keras.Model s en suivant les instructions du guide de mappage de modèle , puis utilisez tf.keras.losses . Alternativement, vous pouvez également suivre manuellement vos pertes de régularisation.
  5. Modélisez les pertes de sortie - Utilisez les mécanismes de gestion des pertes tf.keras.Model ou suivez séparément vos pertes sans utiliser de collections.
  6. Mises à jour de poids - Ignorez cette collection. Une exécution rapide et tf.function (avec les dépendances d'autographe et de contrôle automatique) signifient que toutes les mises à jour de variables seront exécutées automatiquement. Ainsi, vous n'aurez pas à exécuter explicitement toutes les mises à jour de poids à la fin, mais notez que cela signifie que les mises à jour de poids peuvent se produire à un moment différent de celui dans votre code TF1.x, selon la façon dont vous utilisiez les dépendances de contrôle.
  7. Résumés : reportez-vous au guide de l'API de résumé de migration .

Une utilisation plus complexe des collections (telle que l'utilisation de collections personnalisées) peut vous obliger à refactoriser votre code pour maintenir vos propres magasins globaux ou pour qu'il ne repose pas du tout sur les magasins globaux.

ResourceVariables au lieu de ReferenceVariables

ResourceVariables ont des garanties de cohérence en lecture-écriture plus fortes que ReferenceVariables . Cela conduit à une sémantique plus prévisible et plus facile à raisonner pour savoir si vous observerez ou non le résultat d'une écriture précédente lors de l'utilisation de vos variables. Il est extrêmement peu probable que ce changement entraîne le code existant à générer des erreurs ou à se casser silencieusement.

Cependant, il est possible, bien que peu probable , que ces garanties de cohérence renforcées augmentent l'utilisation de la mémoire de votre programme spécifique. Veuillez signaler un problème si vous trouvez que c'est le cas. De plus, si vous avez des tests unitaires reposant sur des comparaisons de chaînes exactes avec les noms d'opérateurs dans un graphique correspondant à des lectures de variables, sachez que l'activation des variables de ressource peut légèrement modifier le nom de ces opérateurs.

Pour isoler l'impact de ce changement de comportement sur votre code, si l'exécution hâtive est désactivée, vous pouvez utiliser tf.compat.v1.disable_resource_variables() et tf.compat.v1.enable_resource_variables() pour désactiver ou activer globalement ce changement de comportement. ResourceVariables sera toujours utilisé si l'exécution hâtive est activée.

Flux de contrôle v2

Dans TF1.x, contrôlez les opérations de flux telles que tf.cond et tf.while_loop les opérations de bas niveau en ligne telles que Switch , Merge etc. TF2 fournit des opérations de flux de contrôle fonctionnelles améliorées qui sont implémentées avec des traces tf.function distinctes pour chaque branche et support différenciation d'ordre supérieur.

Pour isoler l'impact de ce changement de comportement sur votre code, si l'exécution hâtive est désactivée, vous pouvez utiliser tf.compat.v1.disable_control_flow_v2() et tf.compat.v1.enable_control_flow_v2() pour désactiver ou activer globalement ce changement de comportement. Cependant, vous ne pouvez désactiver le flux de contrôle v2 que si l'exécution hâtive est également désactivée. S'il est activé, le flux de contrôle v2 sera toujours utilisé.

Ce changement de comportement peut modifier considérablement la structure des programmes TF générés qui utilisent le flux de contrôle, car ils contiendront plusieurs traces de fonctions imbriquées plutôt qu'un seul graphique plat. Ainsi, tout code fortement dépendant de la sémantique exacte des traces produites peut nécessiter quelques modifications. Ceci comprend:

  • Code reposant sur les noms d'opérateurs et de tenseurs
  • Code faisant référence aux Tensors créés dans une branche de flux de contrôle TensorFlow depuis l'extérieur de cette branche. Cela est susceptible de produire une InaccessibleTensorError

Ce changement de comportement est destiné à être de performance neutre à positive, mais si vous rencontrez un problème où le flux de contrôle v2 fonctionne moins bien pour vous que le flux de contrôle TF1.x, veuillez signaler un problème avec les étapes de reproduction.

Modifications du comportement de l'API TensorShape

La classe TensorShape a été simplifiée pour contenir int s, au lieu des objets tf.compat.v1.Dimension . Il n'est donc pas nécessaire d'appeler .value pour obtenir un int .

Les objets tf.compat.v1.Dimension individuels sont toujours accessibles à partir de tf.TensorShape.dims .

Pour isoler l'impact de ce changement de comportement sur votre code, vous pouvez utiliser tf.compat.v1.disable_v2_tensorshape() et tf.compat.v1.enable_v2_tensorshape() pour désactiver ou activer globalement ce changement de comportement.

Les éléments suivants illustrent les différences entre TF1.x et TF2.

import tensorflow as tf
# Create a shape and choose an index
i = 0
shape = tf.TensorShape([16, None, 256])
shape
TensorShape([16, None, 256])

Si vous aviez ceci dans TF1.x :

value = shape[i].value

Ensuite, faites ceci dans TF2 :

value = shape[i]
value
16

Si vous aviez ceci dans TF1.x :

for dim in shape:
    value = dim.value
    print(value)

Ensuite, faites ceci dans TF2 :

for value in shape:
  print(value)
16
None
256

Si vous aviez ceci dans TF1.x (ou utilisé toute autre méthode de dimension):

dim = shape[i]
dim.assert_is_compatible_with(other_dim)

Ensuite, faites ceci dans TF2 :

other_dim = 16
Dimension = tf.compat.v1.Dimension

if shape.rank is None:
  dim = Dimension(None)
else:
  dim = shape.dims[i]
dim.is_compatible_with(other_dim) # or any other dimension method
True
shape = tf.TensorShape(None)

if shape:
  dim = shape.dims[i]
  dim.is_compatible_with(other_dim) # or any other dimension method

La valeur booléenne d'un tf.TensorShape est True si le rang est connu, False sinon.

print(bool(tf.TensorShape([])))      # Scalar
print(bool(tf.TensorShape([0])))     # 0-length vector
print(bool(tf.TensorShape([1])))     # 1-length vector
print(bool(tf.TensorShape([None])))  # Unknown-length vector
print(bool(tf.TensorShape([1, 10, 100])))       # 3D tensor
print(bool(tf.TensorShape([None, None, None]))) # 3D tensor with no known dimensions
print()
print(bool(tf.TensorShape(None)))  # A tensor with unknown rank.
True
True
True
True
True
True

False

Erreurs potentielles dues aux modifications de TensorShape

Il est peu probable que les changements de comportement de TensorShape cassent silencieusement votre code. Cependant, vous pouvez voir le code lié à la forme commencer à générer AttributeError s car int s et None s n'ont pas les mêmes attributs que tf.compat.v1.Dimension s do. Vous trouverez ci-dessous quelques exemples de ces AttributeError :

try:
  # Create a shape and choose an index
  shape = tf.TensorShape([16, None, 256])
  value = shape[0].value
except AttributeError as e:
  # 'int' object has no attribute 'value'
  print(e)
'int' object has no attribute 'value'
try:
  # Create a shape and choose an index
  shape = tf.TensorShape([16, None, 256])
  dim = shape[1]
  other_dim = shape[2]
  dim.assert_is_compatible_with(other_dim)
except AttributeError as e:
  # 'NoneType' object has no attribute 'assert_is_compatible_with'
  print(e)
'NoneType' object has no attribute 'assert_is_compatible_with'

Égalité du tenseur par valeur

Les opérateurs binaires == et != sur les variables et les tenseurs ont été modifiés pour comparer par valeur dans TF2 plutôt que par référence d'objet comme dans TF1.x. De plus, les tenseurs et les variables ne sont plus directement hachables ou utilisables dans des ensembles ou des clés dict, car il peut ne pas être possible de les hacher par valeur. Au lieu de cela, ils exposent une méthode .ref() que vous pouvez utiliser pour obtenir une référence hachable au tenseur ou à la variable.

Pour isoler l'impact de ce changement de comportement, vous pouvez utiliser tf.compat.v1.disable_tensor_equality() et tf.compat.v1.enable_tensor_equality() pour désactiver ou activer globalement ce changement de comportement.

Par exemple, dans TF1.x, deux variables de même valeur renverront faux lorsque vous utiliserez l'opérateur == :

tf.compat.v1.disable_tensor_equality()
x = tf.Variable(0.0)
y = tf.Variable(0.0)

x == y
False

Alors que dans TF2 avec les vérifications d'égalité de tenseur activées, x == y renverra True .

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)
y = tf.Variable(0.0)

x == y
<tf.Tensor: shape=(), dtype=bool, numpy=True>

Ainsi, dans TF2, si vous avez besoin de comparer par référence d'objet, assurez-vous d'utiliser is et is not

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)
y = tf.Variable(0.0)

x is y
False

Tenseurs et variables de hachage

Avec les comportements TF1.x, vous pouviez ajouter directement des variables et des tenseurs aux structures de données nécessitant un hachage, telles que les clés set et dict .

tf.compat.v1.disable_tensor_equality()
x = tf.Variable(0.0)
set([x, tf.constant(2.0)])
{<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>,
 <tf.Tensor: shape=(), dtype=float32, numpy=2.0>}

Cependant, dans TF2 avec l'égalité des tenseurs activée, les tenseurs et les variables sont rendus non hachables en raison de la sémantique des opérateurs == et != passant aux contrôles d'égalité des valeurs.

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)

try:
  set([x, tf.constant(2.0)])
except TypeError as e:
  # TypeError: Variable is unhashable. Instead, use tensor.ref() as the key.
  print(e)
Variable is unhashable. Instead, use tensor.ref() as the key.

Ainsi, dans TF2, si vous devez utiliser des objets tenseurs ou variables comme clés ou set des contenus, vous pouvez utiliser tensor.ref() pour obtenir une référence hachable pouvant être utilisée comme clé :

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)

tensor_set = set([x.ref(), tf.constant(2.0).ref()])
assert x.ref() in tensor_set

tensor_set
{<Reference wrapping <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>>,
 <Reference wrapping <tf.Tensor: shape=(), dtype=float32, numpy=2.0>>}

Si nécessaire, vous pouvez également obtenir le tenseur ou la variable de la référence en utilisant reference.deref() :

referenced_var = x.ref().deref()
assert referenced_var is x
referenced_var
<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>

Ressources et lectures complémentaires

  • Visitez la section Migrer vers TF2 pour en savoir plus sur la migration vers TF2 depuis TF1.x.
  • Lisez le guide de mappage de modèles pour en savoir plus sur le mappage de vos modèles TF1.x pour qu'ils fonctionnent directement dans TF2.