TensorFlow 1.x vs TensorFlow 2 - Comportamientos y API

Ver en TensorFlow.org Ejecutar en Google Colab Ver en GitHub Descargar libreta

Debajo del capó, TensorFlow 2 sigue un paradigma de programación fundamentalmente diferente de TF1.x.

Esta guía describe las diferencias fundamentales entre TF1.x y TF2 en términos de comportamientos y API, y cómo todo esto se relaciona con su proceso de migración.

Resumen de alto nivel de los principales cambios

Fundamentalmente, TF1.x y TF2 usan un conjunto diferente de comportamientos de tiempo de ejecución en torno a la ejecución (ansioso en TF2), variables, flujo de control, formas de tensor y comparaciones de igualdad de tensor. Para ser compatible con TF2, su código debe ser compatible con el conjunto completo de comportamientos de TF2. Durante la migración, puede habilitar o deshabilitar la mayoría de estos comportamientos individualmente a través de las tf.compat.v1.enable_* o tf.compat.v1.disable_* . La única excepción es la eliminación de colecciones, que es un efecto secundario de habilitar/deshabilitar la ejecución ansiosa.

En un nivel alto, TensorFlow 2:

  • Elimina las API redundantes .
  • Hace que las API sean más coherentes, por ejemplo, RNN unificados y optimizadores unificados .
  • Prefiere las funciones a las sesiones y se integra mejor con el tiempo de ejecución de Python con la ejecución Eager habilitada de forma predeterminada junto con tf.function que proporciona dependencias de control automático para gráficos y compilación.
  • Retira las colecciones de gráficos globales.
  • Altera la semántica de simultaneidad de variables mediante el uso de ResourceVariables sobre ReferenceVariables .
  • Admite flujo de control diferenciable y basado en funciones (Control Flow v2).
  • Simplifica la API de TensorShape para contener int s en lugar de objetos tf.compat.v1.Dimension .
  • Actualiza la mecánica de igualdad de tensores. En TF1.x, el operador == en tensores y variables verifica la igualdad de referencia de objetos. En TF2 comprueba la igualdad de valores. Además, los tensores/variables ya no se pueden modificar, pero puede obtener referencias de objetos modificables a ellos a través de var.ref() si necesita usarlos en conjuntos o como claves de dict .

Las secciones a continuación brindan más contexto sobre las diferencias entre TF1.x y TF2. Para obtener más información sobre el proceso de diseño detrás de TF2, lea los RFC y los documentos de diseño .

limpieza de API

Muchas API se han ido o se han movido en TF2. Algunos de los cambios principales incluyen la eliminación de tf.app , tf.flags y tf.logging a favor de absl-py ahora de código abierto, la reubicación de proyectos que vivían en tf.contrib y la limpieza del espacio de nombres principal tf.* mover funciones menos usadas a subpaquetes como tf.math . Algunas API se han reemplazado con sus equivalentes de TF2: tf.summary , tf.keras.metrics y tf.keras.optimizers .

tf.compat.v1 : Puntos finales de API heredados y de compatibilidad

Los símbolos bajo los espacios de nombres tf.compat y tf.compat.v1 no se consideran API de TF2. Estos espacios de nombres exponen una combinación de símbolos de compatibilidad, así como puntos finales de API heredados de TF 1.x. Estos están destinados a ayudar en la migración de TF1.x a TF2. Sin embargo, como ninguna de estas API compat.v1 son API TF2 idiomáticas, no las utilice para escribir código TF2 nuevo.

Los símbolos tf.compat.v1 individuales pueden ser compatibles con TF2 porque continúan funcionando incluso con los comportamientos de TF2 habilitados (como tf.compat.v1.losses.mean_squared_error ), mientras que otros son incompatibles con TF2 (como tf.compat.v1.metrics.accuracy ). Muchos símbolos compat.v1 (aunque no todos) contienen información de migración dedicada en su documentación que explica su grado de compatibilidad con los comportamientos de TF2, así como también cómo migrarlos a las API de TF2.

El script de actualización de TF2 puede asignar muchos símbolos API compat.v1 a API TF2 equivalentes en el caso de que sean alias o tengan los mismos argumentos pero con un orden diferente. También puede usar el script de actualización para cambiar automáticamente el nombre de las API de TF1.x.

API de falsos amigos

Hay un conjunto de símbolos de "falso amigo" que se encuentran en el espacio de nombres TF2 tf (no en compat.v1 ) que en realidad ignoran los comportamientos de TF2 ocultos y/o no son totalmente compatibles con el conjunto completo de comportamientos de TF2. Como tal, es probable que estas API se comporten mal con el código TF2, potencialmente de manera silenciosa.

  • tf.estimator.* : Los estimadores crean y usan gráficos y sesiones bajo el capó. Como tales, estos no deben considerarse compatibles con TF2. Si su código está ejecutando estimadores, no está utilizando comportamientos de TF2.
  • keras.Model.model_to_estimator(...) : Esto crea un Estimator debajo del capó, que como se mencionó anteriormente no es compatible con TF2.
  • tf.Graph().as_default() : Esto ingresa los comportamientos gráficos de TF1.x y no sigue los comportamientos estándar de tf.function compatibles con TF2. El código que ingresa gráficos como este generalmente los ejecutará a través de Sesiones y no debe considerarse compatible con TF2.
  • tf.feature_column.* Las API de columnas de características generalmente se basan en la creación de variables tf.compat.v1.get_variable al estilo TF1 y suponen que se accederá a las variables creadas a través de colecciones globales. Como TF2 no admite colecciones, es posible que las API no funcionen correctamente cuando se ejecutan con los comportamientos de TF2 habilitados.

Otros cambios de API

  • TF2 presenta mejoras significativas en los algoritmos de colocación de dispositivos que hacen innecesario el uso de tf.colocate_with . Si eliminarlo provoca una degradación del rendimiento , notifique un error .

  • Reemplace todo el uso de tf.v1.ConfigProto con funciones equivalentes de tf.config .

ejecución ansiosa

TF1.x requería unir manualmente un árbol de sintaxis abstracta (el gráfico) haciendo llamadas a la API tf.* y luego compilar manualmente el árbol de sintaxis abstracta pasando un conjunto de tensores de salida y tensores de entrada a una llamada session.run . TF2 se ejecuta con entusiasmo (como normalmente lo hace Python) y hace que los gráficos y las sesiones se sientan como detalles de implementación.

Un subproducto notable de la ejecución ansiosa es que tf.control_dependencies ya no es necesario, ya que todas las líneas de código se ejecutan en orden (dentro de una tf.function , el código con efectos secundarios se ejecuta en el orden escrito).

No más globales

TF1.x dependía en gran medida de colecciones y espacios de nombres globales implícitos. Cuando llamó a tf.Variable , se colocaría en una colección en el gráfico predeterminado y permanecería allí, incluso si perdiera el rastro de la variable de Python que apuntaba a él. Luego podría recuperar ese tf.Variable , pero solo si supiera el nombre con el que se había creado. Esto era difícil de hacer si no tenía el control de la creación de la variable. Como resultado, proliferaron todo tipo de mecanismos para intentar ayudarlo a encontrar sus variables nuevamente y para que los marcos encontraran variables creadas por el usuario. Algunos de estos incluyen: ámbitos de variables, colecciones globales, métodos auxiliares como tf.get_global_step y tf.global_variables_initializer , optimizadores que calculan implícitamente gradientes sobre todas las variables entrenables, etc. TF2 elimina todos estos mecanismos ( Variables 2.0 RFC ) a favor del mecanismo predeterminado: usted realiza un seguimiento de sus variables. Si pierde el rastro de una tf.Variable , se recolecta basura.

El requisito de realizar un seguimiento de las variables crea algo de trabajo adicional, pero con herramientas como las correcciones de modelado y comportamientos como colecciones de variables implícitas orientadas a objetos en tf.Module s y tf.keras.layers.Layer s , la carga se minimiza.

Funciones, no sesiones

Una llamada session.run es casi como una llamada de función: especifica las entradas y la función que se va a llamar, y obtiene un conjunto de salidas. En TF2, puede decorar una función de Python usando tf.function para marcarla para la compilación JIT para que TensorFlow la ejecute como un solo gráfico ( Funciones 2.0 RFC ). Este mecanismo permite que TF2 obtenga todos los beneficios del modo gráfico:

  • Rendimiento: la función se puede optimizar (poda de nodos, fusión de kernel, etc.)
  • Portabilidad: la función se puede exportar/reimportar ( SavedModel 2.0 RFC ), lo que le permite reutilizar y compartir funciones modulares de TensorFlow.
# TF1.x
outputs = session.run(f(placeholder), feed_dict={placeholder: input})
# TF2
outputs = f(input)

Con el poder de intercalar libremente el código de Python y TensorFlow, puede aprovechar la expresividad de Python. Sin embargo, TensorFlow portátil se ejecuta en contextos sin un intérprete de Python, como dispositivos móviles, C++ y JavaScript. Para ayudar a evitar tener que volver a escribir su código al agregar tf.function , use AutoGraph para convertir un subconjunto de construcciones de Python en sus equivalentes de TensorFlow:

  • for / while -> tf.while_loop (se admiten break y continue )
  • if -> tf.cond
  • for _ in dataset -> conjunto de dataset.reduce

AutoGraph admite anidamientos arbitrarios de flujo de control, lo que hace posible implementar de manera concisa y eficaz muchos programas de aprendizaje automático complejos, como modelos de secuencia, aprendizaje de refuerzo, bucles de entrenamiento personalizados y más.

Adaptación a los cambios de comportamiento de TF 2.x

Su migración a TF2 solo se completa una vez que haya migrado al conjunto completo de comportamientos de TF2. El conjunto completo de comportamientos se puede habilitar o deshabilitar a través tf.compat.v1.enable_v2_behaviors y tf.compat.v1.disable_v2_behaviors . Las siguientes secciones analizan cada cambio de comportamiento importante en detalle.

Uso tf.function s

Es probable que los mayores cambios en sus programas durante la migración provengan del cambio de paradigma del modelo de programación fundamental de gráficos y sesiones a una ejecución entusiasta y tf.function . Consulte las guías de migración de TF2 para obtener más información sobre cómo pasar de las API que son incompatibles con la ejecución entusiasta y tf.function a las API que son compatibles con ellas.

A continuación se muestran algunos patrones de programas comunes que no están vinculados a ninguna API que pueden causar problemas al cambiar de tf.Graph s y tf.compat.v1.Session s a una ejecución entusiasta con tf.function s.

Patrón 1: la manipulación de objetos de Python y la creación de variables destinadas a realizarse solo una vez se ejecutan varias veces

En los programas TF1.x que se basan en gráficos y sesiones, la expectativa suele ser que toda la lógica de Python en su programa solo se ejecute una vez. Sin embargo, con una ejecución ansiosa y tf.function , es justo esperar que su lógica de Python se ejecute al menos una vez, pero posiblemente más veces (ya sea varias veces con entusiasmo o varias veces en diferentes seguimientos de tf.function ). A veces, tf.function incluso rastreará dos veces la misma entrada, lo que provocará comportamientos inesperados (consulte los Ejemplos 1 y 2). Consulte la guía de tf.function para obtener más detalles.

Ejemplo 1: creación de variables

Considere el siguiente ejemplo, donde la función crea una variable cuando se llama:

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)

Sin embargo, no se permite envolver ingenuamente la función anterior que contiene la creación de variables con tf.function . tf.function solo admite creaciones de variables singleton en la primera llamada . Para hacer cumplir esto, cuando tf.function detecta la creación de variables en la primera llamada, intentará realizar un seguimiento nuevamente y generará un error si hay una creación de variables en el segundo seguimiento.

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

Una solución consiste en almacenar en caché y reutilizar la variable después de crearla en la primera llamada.

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

Ejemplo 2: Tensores fuera de alcance debido al tf.function de la función tf.

Como se demostró en el Ejemplo 1, tf.function retrocederá cuando detecte la creación de Variables en la primera llamada. Esto puede causar una confusión adicional, porque los dos trazos crearán dos gráficos. Cuando el segundo gráfico del rastreo intenta acceder a un tensor del gráfico generado durante el primer rastreo, Tensorflow generará un error quejándose de que el tensor está fuera del alcance. Para demostrar el escenario, el siguiente código crea un conjunto de datos en la primera llamada a tf.function . Esto funcionaría como se esperaba.

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

Sin embargo, si también intentamos crear una variable en la primera llamada a tf.function , el código generará un error quejándose de que el conjunto de datos está fuera del alcance. Esto se debe a que el conjunto de datos está en el primer gráfico, mientras que el segundo gráfico también intenta acceder a él.

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 solución más sencilla es asegurarse de que la creación de variables y la creación de conjuntos de datos estén fuera de la llamada tf.funciton . Por ejemplo:

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

Sin embargo, a veces no se puede evitar crear variables en tf.function (como las variables de ranura en algunos optimizadores de keras de TF ). Aún así, podemos simplemente mover la creación del conjunto de datos fuera de la llamada tf.function . La razón por la que podemos confiar en esto es porque tf.function recibirá el conjunto de datos como una entrada implícita y ambos gráficos pueden acceder a él correctamente.

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

Ejemplo 3: recreaciones inesperadas de objetos de Tensorflow debido al uso de dictados

tf.function tiene un soporte muy pobre para los efectos secundarios de python, como agregar a una lista o verificar/agregar a un diccionario. Más detalles están en "Mejor rendimiento con tf.function" . En el siguiente ejemplo, el código usa diccionarios para almacenar en caché conjuntos de datos e iteradores. Para la misma clave, cada llamada al modelo devolverá el mismo iterador del conjunto de datos.

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

Sin embargo, el patrón anterior no funcionará como se esperaba en tf.function . Durante el seguimiento, tf.function ignorará el efecto secundario de Python de la adición a los diccionarios. En cambio, solo recuerda la creación de un nuevo conjunto de datos e iterador. Como resultado, cada llamada al modelo siempre devolverá un nuevo iterador. Este problema es difícil de notar a menos que los resultados numéricos o el rendimiento sean lo suficientemente significativos. Por lo tanto, recomendamos a los usuarios que piensen detenidamente en el código antes de envolver tf.function ingenuamente en el código 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

Podemos usar tf.init_scope para levantar el conjunto de datos y la creación del iterador fuera del gráfico, para lograr el comportamiento esperado:

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 regla general es evitar confiar en los efectos secundarios de Python en su lógica y solo usarlos para depurar sus rastros.

Ejemplo 4: manipular una lista global de Python

El siguiente código TF1.x usa una lista global de pérdidas que usa para mantener solo la lista de pérdidas generada por el paso de entrenamiento actual. Tenga en cuenta que la lógica de Python que agrega pérdidas a la lista solo se llamará una vez, independientemente de cuántos pasos de entrenamiento se ejecuten en la sesión.

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(...)  

Sin embargo, si esta lógica de Python se asigna ingenuamente a TF2 con una ejecución ansiosa, la lista global de pérdidas tendrá nuevos valores agregados en cada paso de entrenamiento. Esto significa que el código del paso de entrenamiento que anteriormente esperaba que la lista solo contuviera pérdidas del paso de entrenamiento actual ahora ve la lista de pérdidas de todos los pasos de entrenamiento ejecutados hasta el momento. Este es un cambio de comportamiento no deseado, y la lista deberá borrarse al comienzo de cada paso o hacerse local para el paso de entrenamiento.

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

Patrón 2: un tensor simbólico destinado a volver a calcularse cada paso en TF1.x se almacena accidentalmente en caché con el valor inicial al cambiar a entusiasta.

Este patrón generalmente hace que su código se comporte mal en silencio cuando se ejecuta con entusiasmo fuera de tf.functions, pero genera un InaccessibleTensorError si el almacenamiento en caché del valor inicial ocurre dentro de una tf.function . Sin embargo, tenga en cuenta que para evitar el Patrón 1 anterior, a menudo estructurará su código sin darse cuenta de tal manera que este almacenamiento en caché del valor inicial ocurrirá fuera de cualquier tf.function que podría generar un error. Por lo tanto, tenga mucho cuidado si sabe que su programa puede ser susceptible a este patrón.

La solución general a este patrón es reestructurar el código o usar llamadas de Python si es necesario para asegurarse de que el valor se vuelva a calcular cada vez en lugar de almacenarse en caché accidentalmente.

Ejemplo 1: Tasa de aprendizaje/hiperparámetro/etc. horarios que dependen del paso global

En el siguiente fragmento de código, se espera que cada vez que se ejecute la sesión se lea el valor global_step más reciente y se calcule una nueva tasa de aprendizaje.

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(...)

Sin embargo, cuando intente cambiar a entusiasta, tenga cuidado de no terminar con la tasa de aprendizaje calculada solo una vez y luego reutilizada, en lugar de seguir el programa previsto:

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

Debido a que este ejemplo específico es un patrón común y los optimizadores solo deben inicializarse una vez en lugar de en cada paso de capacitación, los optimizadores TF2 admiten los cronogramas tf.keras.optimizers.schedules.LearningRateSchedule o Python callables como argumentos para la tasa de aprendizaje y otros hiperparámetros.

Ejemplo 2: las inicializaciones de números aleatorios simbólicos asignadas como atributos de objeto y luego reutilizadas a través del puntero se almacenan accidentalmente en caché al cambiar a ansioso

Considere el siguiente módulo NoiseAdder :

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

Utilizándolo de la siguiente manera en TF1.x calculará un nuevo tensor de ruido aleatorio cada vez que se ejecute la sesión:

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(...)

Sin embargo, en TF2, inicializar el noise_adder de ruido al principio hará que la distribución de noise_distribution solo se calcule una vez y se congele para todos los pasos de entrenamiento:

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

Para solucionar esto, refactorice NoiseAdder para llamar a tf.random.normal cada vez que se necesite un nuevo tensor aleatorio, en lugar de hacer referencia al mismo objeto tensor cada vez.

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

Patrón 3: el código TF1.x se basa directamente y busca tensores por nombre

Es común que las pruebas de código TF1.x se basen en verificar qué tensores u operaciones están presentes en un gráfico. En algunos casos excepcionales, el código de modelado también se basará en estas búsquedas por nombre.

Los nombres de tensor no se generan cuando se ejecuta con entusiasmo fuera de tf.function , por lo que todos los usos de tf.Tensor.name deben ocurrir dentro de tf.function . Tenga en cuenta que es muy probable que los nombres reales generados difieran entre TF1.x y TF2, incluso dentro de la misma tf.function , y las garantías de la API no garantizan la estabilidad de los nombres generados en las versiones de TF.

Patrón 4: la sesión TF1.x ejecuta selectivamente solo una parte del gráfico generado

En TF1.x, puede construir un gráfico y luego elegir ejecutar de forma selectiva solo un subconjunto del mismo con una sesión eligiendo un conjunto de entradas y salidas que no requieren ejecutar cada operación en el gráfico.

Por ejemplo, puede tener tanto un generador como un discriminador dentro de un solo gráfico y usar llamadas tf.compat.v1.Session.run separadas para alternar entre solo entrenar el discriminador o solo entrenar el generador.

En TF2, debido a las dependencias de control automático en tf.function y la ejecución entusiasta, no hay una poda selectiva de los rastros de tf.function . Se ejecutará un gráfico completo que contenga todas las actualizaciones de variables incluso si, por ejemplo, solo se emite la salida del discriminador o el generador desde la tf.function .

Por lo tanto, necesitaría usar múltiples tf.function s que contengan diferentes partes del programa, o un argumento condicional para la tf.function en la que se bifurca para ejecutar solo las cosas que realmente desea que se ejecuten.

Eliminación de colecciones

Cuando la ejecución entusiasta está habilitada, las API compat.v1 relacionadas con la colección de gráficos (incluidas las que leen o escriben en colecciones bajo el capó, como tf.compat.v1.trainable_variables ) ya no están disponibles. Algunos pueden generar ValueError s, mientras que otros pueden devolver silenciosamente listas vacías.

El uso más estándar de las colecciones en TF1.x es mantener inicializadores, el paso global, pesos, pérdidas de regularización, pérdidas de salida del modelo y actualizaciones de variables que deben ejecutarse, como las capas de BatchNormalization .

Para manejar cada uno de estos usos estándar:

  1. Inicializadores - Ignorar. No se requiere la inicialización manual de variables con la ejecución entusiasta habilitada.
  2. Paso global: consulte la documentación de tf.compat.v1.train.get_or_create_global_step para obtener instrucciones sobre la migración.
  3. Pesos: asigne sus modelos a tf.Module s/ tf.keras.layers.Layer s/ tf.keras.Model s siguiendo las instrucciones de la guía de asignación de modelos y luego use sus respectivos mecanismos de seguimiento de peso, como tf.module.trainable_variables .
  4. Pérdidas de regularización: asigne sus modelos a tf.Module s/ tf.keras.layers.Layer s/ tf.keras.Model s siguiendo las instrucciones de la guía de asignación de modelos y luego use tf.keras.losses . Alternativamente, también puede realizar un seguimiento manual de sus pérdidas de regularización.
  5. Modele las pérdidas de producción: utilice los mecanismos de gestión de pérdidas de tf.keras.Model o rastree por separado sus pérdidas sin utilizar colecciones.
  6. Actualizaciones de peso - Ignora esta colección. La ejecución ansiosa y tf.function (con autógrafos y dependencias de control automático) significa que todas las actualizaciones de variables se ejecutarán automáticamente. Por lo tanto, no tendrá que ejecutar explícitamente todas las actualizaciones de peso al final, pero tenga en cuenta que esto significa que las actualizaciones de peso pueden ocurrir en un momento diferente al que ocurrieron en su código TF1.x, dependiendo de cómo estaba usando las dependencias de control.
  7. Resúmenes: consulte la guía API de resumen de migración .

El uso de colecciones más complejas (como el uso de colecciones personalizadas) puede requerir que refactorice su código para mantener sus propias tiendas globales o para que no dependa en absoluto de las tiendas globales.

ResourceVariables en lugar de ReferenceVariables

ResourceVariables tiene garantías de coherencia de lectura y escritura más sólidas que ReferenceVariables . Esto conduce a una semántica más predecible y más fácil de razonar sobre si observará o no el resultado de una escritura anterior al usar sus variables. Es muy poco probable que este cambio provoque que el código existente genere errores o se rompa en silencio.

Sin embargo, es posible, aunque poco probable, que estas garantías de consistencia más fuertes puedan aumentar el uso de memoria de su programa específico. Presente un problema si considera que este es el caso. Además, si tiene pruebas unitarias que se basan en comparaciones de cadenas exactas con los nombres de operadores en un gráfico correspondiente a lecturas de variables, tenga en cuenta que habilitar variables de recursos puede cambiar ligeramente el nombre de estos operadores.

Para aislar el impacto de este cambio de comportamiento en su código, si la ejecución ansiosa está deshabilitada, puede usar tf.compat.v1.disable_resource_variables() y tf.compat.v1.enable_resource_variables() para deshabilitar o habilitar globalmente este cambio de comportamiento. ResourceVariables siempre se usará si la ejecución entusiasta está habilitada.

Flujo de control v2

En TF1.x, operaciones de flujo de control como tf.cond y tf.while_loop de bajo nivel en línea como Switch , Merge , etc. TF2 proporciona operaciones de flujo de control funcional mejoradas que se implementan con seguimientos de tf.function separados para cada rama y soporte diferenciación de orden superior.

Para aislar el impacto de este cambio de comportamiento en su código, si la ejecución ansiosa está deshabilitada, puede usar tf.compat.v1.disable_control_flow_v2() y tf.compat.v1.enable_control_flow_v2() para deshabilitar o habilitar globalmente este cambio de comportamiento. Sin embargo, solo puede deshabilitar el flujo de control v2 si la ejecución ansiosa también está deshabilitada. Si está habilitado, siempre se utilizará el flujo de control v2.

Este cambio de comportamiento puede cambiar drásticamente la estructura de los programas TF generados que utilizan el flujo de control, ya que contendrán varios seguimientos de funciones anidadas en lugar de un gráfico plano. Por lo tanto, cualquier código que dependa en gran medida de la semántica exacta de los rastros producidos puede requerir alguna modificación. Esto incluye:

  • Código que se basa en nombres de operadores y tensores
  • Código que hace referencia a tensores creados dentro de una rama de flujo de control de TensorFlow desde fuera de esa rama. Es probable que esto produzca un InaccessibleTensorError

Este cambio de comportamiento pretende ser de neutral a positivo, pero si se encuentra con un problema en el que el flujo de control v2 funciona peor que el flujo de control TF1.x, presente un problema con los pasos de reproducción.

Cambios en el comportamiento de la API de TensorShape

La clase TensorShape se simplificó para contener int s, en lugar de objetos tf.compat.v1.Dimension . Entonces no hay necesidad de llamar a .value para obtener un int .

Todavía se puede acceder a los objetos tf.compat.v1.Dimension individuales desde tf.TensorShape.dims .

Para aislar el impacto de este cambio de comportamiento en su código, puede usar tf.compat.v1.disable_v2_tensorshape() y tf.compat.v1.enable_v2_tensorshape() para deshabilitar o habilitar globalmente este cambio de comportamiento.

A continuación se muestran las diferencias entre TF1.x y 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 tuvieras esto en TF1.x:

value = shape[i].value

Luego haz esto en TF2:

value = shape[i]
value
16

Si tuvieras esto en TF1.x:

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

Luego, haz esto en TF2:

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

Si tenía esto en TF1.x (o utilizó cualquier otro método de dimensión):

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

Luego haz esto en 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

El valor booleano de tf.TensorShape es True si se conoce el rango, False en caso contrario.

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

Posibles errores debido a cambios en TensorShape

Es poco probable que los cambios de comportamiento de TensorShape rompan silenciosamente su código. Sin embargo, es posible que vea que el código relacionado con la forma comienza a generar AttributeError s como int s y None s no tienen los mismos atributos que tf.compat.v1.Dimension s. A continuación se muestran algunos ejemplos de estos AttributeError s:

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'

Tensor de igualdad por valor

Los operadores binarios == y != en variables y tensores se cambiaron para comparar por valor en TF2 en lugar de comparar por referencia de objeto como en TF1.x. Además, los tensores y las variables ya no se pueden modificar directamente ni se pueden usar en conjuntos o claves de dictado, porque es posible que no sea posible codificarlos por valor. En cambio, exponen un método .ref() que puede usar para obtener una referencia hashable al tensor o variable.

Para aislar el impacto de este cambio de comportamiento, puede usar tf.compat.v1.disable_tensor_equality() y tf.compat.v1.enable_tensor_equality() para deshabilitar o habilitar globalmente este cambio de comportamiento.

Por ejemplo, en TF1.x, dos variables con el mismo valor devolverán falso cuando utilice el operador == :

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

x == y
False

Mientras esté en TF2 con las comprobaciones de igualdad de tensores habilitadas, x == y devolverá 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>

Entonces, en TF2, si necesita comparar por referencia de objeto, asegúrese de usar is y is not

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

x is y
False

Tensores hash y variables

Con los comportamientos de TF1.x, solía poder agregar directamente variables y tensores a las estructuras de datos que requieren hashing, como las claves set y 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>}

Sin embargo, en TF2 con la igualdad de tensores habilitada, los tensores y las variables no se pueden modificar debido a que la semántica de los operadores == y != cambia a comprobaciones de igualdad de valores.

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.

Entonces, en TF2, si necesita usar tensores o objetos variables como claves o set contenidos, puede usar tensor.ref() para obtener una referencia hashable que se puede usar como clave:

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 es necesario, también puede obtener el tensor o la variable de la referencia usando reference.deref() :

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

Recursos y lecturas adicionales

  • Visite la sección Migrar a TF2 para obtener más información sobre cómo migrar a TF2 desde TF1.x.
  • Lea la guía de mapeo de modelos para obtener más información sobre cómo mapear sus modelos TF1.x para que funcionen directamente en TF2.