O Google I/O é um embrulho! Fique por dentro das sessões do TensorFlow Ver sessões

TensorFlow 1.x vs TensorFlow 2 - Comportamentos e APIs

Veja no TensorFlow.org Executar no Google Colab Ver no GitHub Baixar caderno

Nos bastidores, o TensorFlow 2 segue um paradigma de programação fundamentalmente diferente do TF1.x.

Este guia descreve as diferenças fundamentais entre o TF1.xe o TF2 em termos de comportamentos e APIs, e como tudo isso se relaciona com sua jornada de migração.

Resumo de alto nível das principais mudanças

Fundamentalmente, TF1.xe TF2 usam um conjunto diferente de comportamentos de tempo de execução em torno da execução (ansioso no TF2), variáveis, fluxo de controle, formas de tensor e comparações de igualdade de tensor. Para ser compatível com o TF2, seu código deve ser compatível com o conjunto completo de comportamentos do TF2. Durante a migração, você pode habilitar ou desabilitar a maioria desses comportamentos individualmente por meio das tf.compat.v1.enable_* ou tf.compat.v1.disable_* . A única exceção é a remoção de coleções, que é um efeito colateral de habilitar/desabilitar a execução antecipada.

Em alto nível, o TensorFlow 2:

  • Remove APIs redundantes .
  • Torna as APIs mais consistentes - por exemplo, Unified RNNs e Unified Optimizers .
  • Prefere funções em vez de sessões e integra-se melhor com o tempo de execução do Python com execução Eager habilitada por padrão junto com tf.function que fornece dependências de controle automático para gráficos e compilação.
  • Desaprova as coleções de gráficos globais.
  • Altera a semântica de simultaneidade da variável usando ResourceVariables sobre ReferenceVariables .
  • Suporta fluxo de controle diferenciável e baseado em função (Control Flow v2).
  • Simplifica a API TensorShape para armazenar int s em vez de objetos tf.compat.v1.Dimension .
  • Atualiza a mecânica de igualdade do tensor. No TF1.x, o operador == em tensores e variáveis ​​verifica a igualdade de referência do objeto. No TF2 ele verifica a igualdade de valor. Além disso, tensores/variáveis ​​não são mais passíveis de hash, mas você pode obter referências de objeto hashable para eles via var.ref() se precisar usá-los em conjuntos ou como chaves dict .

As seções abaixo fornecem mais contexto sobre as diferenças entre TF1.xe TF2. Para saber mais sobre o processo de design por trás do TF2, leia as RFCs e os documentos de design .

Limpeza de API

Muitas APIs desapareceram ou foram movidas no TF2. Algumas das principais mudanças incluem a remoção de tf.app , tf.flags e tf.logging em favor do absl-py agora de código aberto , realocação de projetos que viviam em tf.contrib e limpeza do namespace tf.* principal por movendo funções menos usadas em subpacotes como tf.math . Algumas APIs foram substituídas por seus equivalentes do TF2 - tf.summary , tf.keras.metrics e tf.keras.optimizers .

tf.compat.v1 : endpoints de API legados e de compatibilidade

Símbolos sob os namespaces tf.compat e tf.compat.v1 não são considerados APIs do TF2. Esses namespaces expõem uma combinação de símbolos de compatibilidade, bem como endpoints de API herdados do TF 1.x. Estes destinam-se a ajudar na migração do TF1.x para o TF2. No entanto, como nenhuma dessas APIs compat.v1 são APIs idiomáticas do TF2, não as use para escrever código TF2 totalmente novo.

Símbolos tf.compat.v1 individuais podem ser compatíveis com TF2 porque continuam a funcionar mesmo com comportamentos TF2 habilitados (como tf.compat.v1.losses.mean_squared_error ), enquanto outros são incompatíveis com TF2 (como tf.compat.v1.metrics.accuracy ). Muitos símbolos compat.v1 (embora não todos) contêm informações de migração dedicadas em sua documentação que explica seu grau de compatibilidade com os comportamentos do TF2, bem como como migrá-los para APIs do TF2.

O script de atualização do TF2 pode mapear muitos símbolos de API compat.v1 para APIs TF2 equivalentes no caso de serem aliases ou terem os mesmos argumentos, mas com uma ordenação diferente. Você também pode usar o script de atualização para renomear automaticamente as APIs do TF1.x.

APIs de falsos amigos

Há um conjunto de símbolos "false-friend" encontrados no namespace TF2 tf (não em compat.v1 ) que realmente ignoram os comportamentos do TF2 ocultos e/ou não são totalmente compatíveis com o conjunto completo de comportamentos do TF2. Como tal, é provável que essas APIs se comportem mal com o código TF2, potencialmente de maneira silenciosa.

  • tf.estimator.* : Os estimadores criam e usam gráficos e sessões sob o capô. Como tal, estes não devem ser considerados compatíveis com TF2. Se o seu código estiver executando estimadores, ele não está usando comportamentos do TF2.
  • keras.Model.model_to_estimator(...) : Isso cria um estimador sob o capô, que como mencionado acima não é compatível com TF2.
  • tf.Graph().as_default() : Isso insere comportamentos de gráfico TF1.x e não segue os comportamentos tf.function padrão compatíveis com TF2. O código que insere gráficos como este geralmente os executa por meio de sessões e não deve ser considerado compatível com TF2.
  • tf.feature_column.* As APIs de coluna de recursos geralmente dependem da criação de variável tf.compat.v1.get_variable no estilo TF1 e assumem que as variáveis ​​criadas serão acessadas por meio de coleções globais. Como o TF2 não oferece suporte a coleções, as APIs podem não funcionar corretamente ao executá-las com os comportamentos do TF2 ativados.

Outras alterações da API

  • O TF2 apresenta melhorias significativas nos algoritmos de posicionamento do dispositivo que tornam o uso de tf.colocate_with desnecessário. Se removê-lo causar uma degradação de desempenho , registre um bug .

  • Substitua todo o uso de tf.v1.ConfigProto por funções equivalentes de tf.config .

Execução ansiosa

O TF1.x exigia que você costurasse manualmente uma árvore de sintaxe abstrata (o gráfico) fazendo chamadas de API tf.* e então compilasse manualmente a árvore de sintaxe abstrata passando um conjunto de tensores de saída e tensores de entrada para uma chamada session.run . O TF2 executa avidamente (como o Python normalmente faz) e faz com que gráficos e sessões pareçam detalhes de implementação.

Um subproduto notável da execução rápida é que tf.control_dependencies não é mais necessário, pois todas as linhas de código são executadas em ordem (dentro de um tf.function , o código com efeitos colaterais é executado na ordem escrita).

Não há mais globais

O TF1.x dependia muito de namespaces e coleções globais implícitos. Quando você chamasse tf.Variable , ele seria colocado em uma coleção no gráfico padrão e permaneceria lá, mesmo que você perdesse o controle da variável Python apontando para ele. Você poderia então recuperar esse tf.Variable , mas somente se você soubesse o nome com o qual ele foi criado. Isso era difícil de fazer se você não estivesse no controle da criação da variável. Como resultado, todos os tipos de mecanismos proliferaram para tentar ajudá-lo a encontrar suas variáveis ​​novamente e para os frameworks encontrarem variáveis ​​criadas pelo usuário. Alguns deles incluem: escopos de variáveis, coleções globais, métodos auxiliares como tf.get_global_step e tf.global_variables_initializer , otimizadores que calculam implicitamente gradientes sobre todas as variáveis ​​treináveis ​​e assim por diante. O TF2 elimina todos esses mecanismos ( Variables 2.0 RFC ) em favor do mecanismo padrão - você acompanha suas variáveis. Se você perder o controle de um tf.Variable , ele será coletado como lixo.

A exigência de rastrear variáveis ​​cria algum trabalho extra, mas com ferramentas como os shims de modelagem e comportamentos como coleções implícitas de variáveis ​​orientadas a objetos em tf.Module tf.keras.layers.Layer s , a carga é minimizada.

Funções, não sessões

Uma chamada session.run é quase como uma chamada de função: você especifica as entradas e a função a ser chamada e recebe de volta um conjunto de saídas. No TF2, você pode decorar uma função Python usando tf.function para marcá-la para compilação JIT para que o TensorFlow a execute como um gráfico único ( Functions 2.0 RFC ). Este mecanismo permite que o TF2 obtenha todos os benefícios do modo gráfico:

  • Desempenho: A função pode ser otimizada (poda de nós, fusão de kernel, etc.)
  • Portabilidade: A função pode ser exportada/reimportada ( SavedModel 2.0 RFC ), permitindo reutilizar e compartilhar funções modulares do TensorFlow.
# TF1.x
outputs = session.run(f(placeholder), feed_dict={placeholder: input})
# TF2
outputs = f(input)

Com o poder de intercalar livremente o código Python e TensorFlow, você pode aproveitar a expressividade do Python. No entanto, o TensorFlow portátil é executado em contextos sem um interpretador Python, como dispositivos móveis, C++ e JavaScript. Para ajudar a evitar reescrever seu código ao adicionar tf.function , use o AutoGraph para converter um subconjunto de construções do Python em seus equivalentes do TensorFlow:

  • for / while -> tf.while_loop ( break e continue são suportados)
  • if -> tf.cond
  • for _ in dataset -> conjunto de dataset.reduce

O AutoGraph oferece suporte a aninhamentos arbitrários de fluxo de controle, o que possibilita a implementação de forma eficiente e concisa de muitos programas de ML complexos, como modelos de sequência, aprendizado por reforço, loops de treinamento personalizados e muito mais.

Adaptando-se às Mudanças de Comportamento do TF 2.x

Sua migração para o TF2 só será concluída depois que você migrar para o conjunto completo de comportamentos do TF2. O conjunto completo de comportamentos pode ser habilitado ou desabilitado via tf.compat.v1.enable_v2_behaviors e tf.compat.v1.disable_v2_behaviors . As seções abaixo discutem cada mudança de comportamento importante em detalhes.

Usando tf.function s

As maiores mudanças em seus programas durante a migração provavelmente virão da mudança de paradigma do modelo de programação fundamental de gráficos e sessões para execução rápida e tf.function . Consulte os guias de migração do TF2 para saber mais sobre como mudar de APIs incompatíveis com execução antecipada e tf.function para APIs compatíveis com elas.

Abaixo estão alguns padrões de programa comuns não vinculados a nenhuma API que podem causar problemas ao alternar de tf.Graph se tf.compat.v1.Session s para execução antecipada com tf.function s.

Padrão 1: manipulação de objetos Python e criação de variáveis ​​destinadas a serem feitas apenas uma vez e executadas várias vezes

Em programas TF1.x que dependem de gráficos e sessões, a expectativa geralmente é que toda a lógica Python em seu programa seja executada apenas uma vez. No entanto, com execução antecipada e tf.function , é justo esperar que sua lógica Python seja executada pelo menos uma vez, mas possivelmente mais vezes (ou várias vezes ansiosamente ou várias vezes em diferentes rastreamentos tf.function ). Às vezes, tf.function irá rastrear duas vezes na mesma entrada, causando comportamentos inesperados (veja o Exemplo 1 e 2). Consulte o guia tf.function para obter mais detalhes.

Exemplo 1: criação de variável

Considere o exemplo abaixo, onde a função cria uma variável quando chamada:

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)

No entanto, envolver ingenuamente a função acima que contém a criação de variáveis ​​com tf.function não é permitido. tf.function suporta apenas criações de variáveis ​​singleton na primeira chamada . Para reforçar isso, quando tf.function detectar a criação de variável na primeira chamada, ele tentará rastrear novamente e gerará um erro se houver criação de variável no segundo rastreamento.

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

Uma solução alternativa é armazenar em cache e reutilizar a variável depois que ela é criada na primeira chamada.

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

Exemplo 2: Tensores fora do escopo devido ao tf.function da função tf.

Conforme demonstrado no Exemplo 1, tf.function irá refazer quando detectar a criação da variável na primeira chamada. Isso pode causar confusão extra, porque os dois traçados criarão dois gráficos. Quando o segundo gráfico do retraçamento tenta acessar um tensor do gráfico gerado durante o primeiro rastreamento, o Tensorflow gerará um erro reclamando que o tensor está fora do escopo. Para demonstrar o cenário, o código abaixo cria um conjunto de dados na primeira chamada de tf.function . Isso funcionaria como esperado.

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

No entanto, se também tentarmos criar uma variável na primeira chamada de tf.function , o código gerará um erro reclamando que o conjunto de dados está fora do escopo. Isso ocorre porque o conjunto de dados está no primeiro gráfico, enquanto o segundo gráfico também está tentando acessá-lo.

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.

A solução mais direta é garantir que a criação da variável e a criação do conjunto de dados estejam fora da chamada tf.funciton . Por exemplo:

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

No entanto, às vezes não é evitável criar variáveis ​​em tf.function (como variáveis ​​de slot em alguns otimizadores TF keras ). Ainda assim, podemos simplesmente mover a criação do conjunto de dados para fora da chamada tf.function . A razão pela qual podemos confiar nisso é porque tf.function receberá o conjunto de dados como uma entrada implícita e ambos os gráficos poderão acessá-lo corretamente.

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

Exemplo 3: recriações inesperadas de objetos do Tensorflow devido ao uso de dict

tf.function tem um suporte muito ruim para efeitos colaterais do python, como anexar a uma lista ou verificar/adicionar a um dicionário. Mais detalhes estão em "Melhor desempenho com tf.function" . No exemplo abaixo, o código usa dicionários para armazenar em cache conjuntos de dados e iteradores. Para a mesma chave, cada chamada para o modelo retornará o mesmo iterador do conjunto de dados.

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

No entanto, o padrão acima não funcionará como esperado em tf.function . Durante o rastreamento, tf.function irá ignorar o efeito colateral do python da adição aos dicionários. Em vez disso, ele lembra apenas a criação de um novo conjunto de dados e iterador. Como resultado, cada chamada para o modelo sempre retornará um novo iterador. Esse problema é difícil de perceber, a menos que os resultados numéricos ou o desempenho sejam significativos o suficiente. Portanto, recomendamos que os usuários pensem cuidadosamente no código antes de envolver tf.function ingenuamente no 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 o conjunto de dados e a criação do iterador fora do gráfico, para alcançar o comportamento 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

A regra geral é evitar depender dos efeitos colaterais do Python em sua lógica e usá-los apenas para depurar seus rastreamentos.

Exemplo 4: Manipulando uma lista global do Python

O código TF1.x a seguir usa uma lista global de perdas que ele usa para manter apenas a lista de perdas geradas pela etapa de treinamento atual. Observe que a lógica do Python que acrescenta perdas à lista será chamada apenas uma vez, independentemente de quantas etapas de treinamento a sessão for executada.

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

No entanto, se essa lógica Python for mapeada ingenuamente para TF2 com execução antecipada, a lista global de perdas terá novos valores anexados a ela em cada etapa de treinamento. Isso significa que o código da etapa de treinamento, que anteriormente esperava que a lista continha apenas perdas da etapa de treinamento atual, agora vê a lista de perdas de todas as etapas de treinamento executadas até o momento. Esta é uma mudança de comportamento não intencional, e a lista precisará ser limpa no início de cada etapa ou feita localmente na etapa de treinamento.

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

Padrão 2: Um tensor simbólico destinado a ser recalculado a cada passo no TF1.x é acidentalmente armazenado em cache com o valor inicial ao alternar para ansioso.

Esse padrão geralmente faz com que seu código se comporte silenciosamente mal ao executar avidamente fora de tf.functions, mas gera um InaccessibleTensorError se o cache do valor inicial ocorrer dentro de um tf.function . No entanto, esteja ciente de que, para evitar o Padrão 1 acima, você muitas vezes inadvertidamente estruturará seu código de forma que esse cache de valor inicial aconteça fora de qualquer tf.function que possa gerar um erro. Portanto, tome cuidado extra se souber que seu programa pode ser suscetível a esse padrão.

A solução geral para esse padrão é reestruturar o código ou usar callables do Python, se necessário, para garantir que o valor seja recalculado a cada vez, em vez de ser acidentalmente armazenado em cache.

Exemplo 1: Taxa de aprendizagem/hiperparâmetro/etc. horários que dependem da etapa global

No trecho de código a seguir, a expectativa é que toda vez que a sessão for executada, o valor global_step mais recente seja lido e uma nova taxa de aprendizado seja calculada.

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

No entanto, ao tentar mudar para ansioso, tenha cuidado para não acabar com a taxa de aprendizado sendo calculada apenas uma vez e depois reutilizada, em vez de seguir o cronograma pretendido:

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

Como esse exemplo específico é um padrão comum e os otimizadores devem ser inicializados apenas uma vez em vez de em cada etapa de treinamento, os otimizadores TF2 suportam agendamentos tf.keras.optimizers.schedules.LearningRateSchedule ou callables Python como argumentos para a taxa de aprendizado e outros hiperparâmetros.

Exemplo 2: Inicializações de números aleatórios simbólicos atribuídos como atributos de objeto e reutilizados por meio de ponteiro são acidentalmente armazenados em cache ao alternar para ansioso

Considere o seguinte 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

Usá-lo da seguinte forma no TF1.x calculará um novo tensor de ruído aleatório toda vez que a sessão for executada:

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

No entanto, no TF2, inicializar o noise_adder no início fará com que o noise_distribution seja calculado apenas uma vez e fique congelado para todas as etapas de treinamento:

...
# 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 corrigir isso, NoiseAdder para chamar tf.random.normal toda vez que um novo tensor aleatório for necessário, em vez de se referir ao mesmo objeto tensor a 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

Padrão 3: o código TF1.x depende diretamente e procura tensores por nome

É comum que os testes de código do TF1.x dependam da verificação de quais tensores ou operações estão presentes em um gráfico. Em alguns casos raros, o código de modelagem também contará com essas pesquisas por nome.

Nomes de tensor não são gerados ao executar avidamente fora de tf.function , então todos os usos de tf.Tensor.name devem acontecer dentro de um tf.function . Tenha em mente que os nomes gerados reais são muito provavelmente diferentes entre TF1.xe TF2 mesmo dentro do mesmo tf.function , e as garantias da API não garantem a estabilidade dos nomes gerados nas versões do TF.

Padrão 4: a sessão TF1.x executa seletivamente apenas parte do gráfico gerado

No TF1.x, você pode construir um gráfico e, em seguida, optar por executar seletivamente apenas um subconjunto dele com uma sessão, escolhendo um conjunto de entradas e saídas que não exigem a execução de todas as operações no gráfico.

Por exemplo, você pode ter um gerador e um discriminador dentro de um único gráfico e usar chamadas tf.compat.v1.Session.run separadas para alternar entre treinar apenas o discriminador ou treinar apenas o gerador.

No TF2, devido às dependências de controle automático em tf.function e execução antecipada, não há poda seletiva de rastreamentos de tf.function . Um gráfico completo contendo todas as atualizações de variáveis ​​seria executado mesmo se, por exemplo, apenas a saída do discriminador ou do gerador fosse saída do tf.function .

Portanto, você precisaria usar vários tf.function s contendo diferentes partes do programa, ou um argumento condicional para o tf.function que você ramifica para executar apenas as coisas que você realmente deseja que sejam executadas.

Remoção de coleções

Quando a execução antecipada está habilitada, as APIs compat.v1 relacionadas à coleção de gráficos (incluindo aquelas que lêem ou gravam em coleções ocultas, como tf.compat.v1.trainable_variables ) não estão mais disponíveis. Alguns podem gerar ValueError s, enquanto outros podem retornar silenciosamente listas vazias.

O uso mais padrão de coleções no TF1.x é manter inicializadores, a etapa global, pesos, perdas de regularização, perdas de saída de modelo e atualizações de variáveis ​​que precisam ser executadas, como nas camadas BatchNormalization .

Para lidar com cada um desses usos padrão:

  1. Inicializadores - Ignorar. A inicialização manual da variável não é necessária com a execução antecipada habilitada.
  2. Etapa global - Consulte a documentação de tf.compat.v1.train.get_or_create_global_step para obter instruções de migração.
  3. Pesos - Mapeie seus modelos para tf.Module s/ tf.keras.layers.Layer s/ tf.keras.Model s seguindo as orientações do guia de mapeamento de modelos e, em seguida, use seus respectivos mecanismos de rastreamento de peso, como tf.module.trainable_variables .
  4. Perdas de regularização - Mapeie seus modelos para tf.Module s/ tf.keras.layers.Layer s/ tf.keras.Model s seguindo as orientações no guia de mapeamento de modelos e, em seguida, use tf.keras.losses . Alternativamente, você também pode acompanhar manualmente suas perdas de regularização.
  5. Perdas de saída do modelo - Use os mecanismos de gerenciamento de perda tf.keras.Model ou rastreie separadamente suas perdas sem usar coleções.
  6. Atualizações de peso - Ignore esta coleção. A execução ansiosa e tf.function (com autógrafo e dependências de auto-controle) significa que todas as atualizações de variáveis ​​serão executadas automaticamente. Portanto, você não precisará executar explicitamente todas as atualizações de peso no final, mas observe que isso significa que as atualizações de peso podem ocorrer em um momento diferente do que aconteceram em seu código TF1.x, dependendo de como você estava usando as dependências de controle.
  7. Resumos - consulte o guia de API de resumo de migração .

O uso de coleções mais complexas (como o uso de coleções personalizadas) pode exigir que você refatore seu código para manter seus próprios armazenamentos globais ou para que ele não dependa de armazenamentos globais.

ResourceVariables em vez de ReferenceVariables

ResourceVariables têm garantias de consistência de leitura e gravação mais fortes do que ReferenceVariables . Isso leva a uma semântica mais previsível e fácil de raciocinar sobre se você observará ou não o resultado de uma gravação anterior ao usar suas variáveis. É extremamente improvável que essa alteração faça com que o código existente gere erros ou quebre silenciosamente.

No entanto, é possível, embora improvável , que essas garantias de consistência mais fortes possam aumentar o uso de memória de seu programa específico. Por favor, registre um problema se você achar que esse é o caso. Além disso, se você tiver testes de unidade que dependem de comparações exatas de strings com os nomes dos operadores em um gráfico correspondente a leituras de variáveis, esteja ciente de que habilitar variáveis ​​de recursos pode alterar ligeiramente o nome desses operadores.

Para isolar o impacto dessa mudança de comportamento em seu código, se a execução antecipada estiver desabilitada, você pode usar tf.compat.v1.disable_resource_variables() e tf.compat.v1.enable_resource_variables() para desabilitar ou habilitar globalmente essa mudança de comportamento. ResourceVariables sempre será usado se a execução antecipada estiver habilitada.

Controle de fluxo v2

No tf.function , operações de fluxo de controle, como tf.cond e tf.while_loop , operações de baixo nível em linha, como Switch , Merge , etc. diferenciação de ordem superior.

Para isolar o impacto dessa mudança de comportamento em seu código, se a execução antecipada estiver desabilitada, você pode usar tf.compat.v1.disable_control_flow_v2() e tf.compat.v1.enable_control_flow_v2() para desabilitar ou habilitar globalmente essa mudança de comportamento. No entanto, você só pode desabilitar o fluxo de controle v2 se a execução antecipada também estiver desabilitada. Se estiver habilitado, o fluxo de controle v2 sempre será usado.

Essa mudança de comportamento pode alterar drasticamente a estrutura de programas TF gerados que usam fluxo de controle, pois eles conterão vários traços de função aninhados em vez de um gráfico plano. Assim, qualquer código que seja altamente dependente da semântica exata dos traços produzidos pode exigir alguma modificação. Isso inclui:

  • Código baseado em nomes de operadores e tensores
  • Código referente a tensores criados em uma ramificação de fluxo de controle do TensorFlow de fora dessa ramificação. Isso provavelmente produzirá um InaccessibleTensorError

Essa mudança de comportamento deve ser de desempenho neutro a positivo, mas se você encontrar um problema em que o fluxo de controle v2 funciona pior para você do que o fluxo de controle TF1.x, registre um problema com as etapas de reprodução.

Alterações de comportamento da API do TensorShape

A classe TensorShape foi simplificada para conter int s, em vez de objetos tf.compat.v1.Dimension . Portanto, não há necessidade de chamar .value para obter um int .

Objetos tf.compat.v1.Dimension individuais ainda podem ser acessados ​​em tf.TensorShape.dims .

Para isolar o impacto dessa mudança de comportamento em seu código, você pode usar tf.compat.v1.disable_v2_tensorshape() e tf.compat.v1.enable_v2_tensorshape() para desabilitar ou habilitar globalmente essa mudança de comportamento.

O seguinte demonstra as diferenças entre TF1.xe 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])

Se você tivesse isso no TF1.x:

value = shape[i].value

Então faça isso no TF2:

value = shape[i]
value
16

Se você tivesse isso no TF1.x:

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

Então, faça isso no TF2:

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

Se você tinha isso no TF1.x (ou usou qualquer outro método de dimensão):

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

Então faça isso no 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

O valor booleano de um tf.TensorShape é True se a classificação for conhecida, False caso contrário.

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

Erros potenciais devido a alterações do TensorShape

É improvável que as alterações de comportamento do TensorShape quebrem silenciosamente seu código. No entanto, você pode ver o código relacionado à forma começar a gerar AttributeError s como int se None s não têm os mesmos atributos que tf.compat.v1.Dimension s. Abaixo estão alguns exemplos desses 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'

Igualdade de tensores por valor

Os operadores binários == e != em variáveis ​​e tensores foram alterados para comparar por valor no TF2 em vez de comparar por referência de objeto como no TF1.x. Além disso, tensores e variáveis ​​não são mais diretamente hashable ou utilizáveis ​​em conjuntos ou chaves dict, porque pode não ser possível hash por valor. Em vez disso, eles expõem um método .ref() que você pode usar para obter uma referência hashable para o tensor ou variável.

Para isolar o impacto dessa mudança de comportamento, você pode usar tf.compat.v1.disable_tensor_equality() e tf.compat.v1.enable_tensor_equality() para desabilitar ou habilitar globalmente essa mudança de comportamento.

Por exemplo, em TF1.x, duas variáveis ​​com o mesmo valor retornarão false quando você usar o operador == :

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

x == y
False

Enquanto no TF2 com verificações de igualdade de tensor habilitadas, x == y retornará 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>

Então, no TF2, se você precisar comparar por referência de objeto, certifique-se de usar is e is not

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

x is y
False

Tensores e variáveis ​​de hash

Com os comportamentos do TF1.x, você costumava adicionar variáveis ​​e tensores diretamente a estruturas de dados que requerem hash, como chaves set e 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>}

No entanto, no TF2 com a igualdade de tensor habilitada, tensores e variáveis ​​são tornados não destrutíveis devido à semântica do operador == e != mudando para verificações de igualdade de valor.

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.

Então, no TF2, se você precisar usar objetos tensores ou variáveis ​​como chaves ou set conteúdo, você pode usar tensor.ref() para obter uma referência hashable que pode ser usada como chave:

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

Se necessário, você também pode obter o tensor ou variável da referência 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 e leitura adicional