TensorFlow 1.x vs TensorFlow 2 - Comportamenti e API

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza su GitHub Scarica quaderno

Sotto il cofano, TensorFlow 2 segue un paradigma di programmazione fondamentalmente diverso da TF1.x.

Questa guida descrive le differenze fondamentali tra TF1.x e TF2 in termini di comportamenti e API e in che modo sono tutte correlate al tuo percorso di migrazione.

Riepilogo ad alto livello dei principali cambiamenti

Fondamentalmente, TF1.xe TF2 utilizzano un diverso insieme di comportamenti di runtime attorno all'esecuzione (desideroso in TF2), variabili, flusso di controllo, forme tensoriali e confronti di uguaglianza dei tensori. Per essere compatibile con TF2, il tuo codice deve essere compatibile con il set completo di comportamenti TF2. Durante la migrazione, puoi abilitare o disabilitare la maggior parte di questi comportamenti individualmente tramite le tf.compat.v1.enable_* o tf.compat.v1.disable_* . L'unica eccezione è la rimozione delle raccolte, che è un effetto collaterale dell'abilitazione/disabilitazione dell'esecuzione ansiosa.

Ad alto livello, TensorFlow 2:

  • Rimuove le API ridondanti .
  • Rende le API più coerenti, ad esempio, Unified RNN e Unified Optimizer .
  • Preferisce le funzioni alle sessioni e si integra meglio con il runtime Python con l'esecuzione di Eager abilitata per impostazione predefinita insieme a tf.function che fornisce dipendenze di controllo automatico per grafici e compilazione.
  • Depreca le raccolte di grafici globali.
  • Modifica la semantica della concorrenza delle variabili usando ResourceVariables su ReferenceVariables .
  • Supporta il flusso di controllo basato su funzioni e differenziabile (Control Flow v2).
  • Semplifica l'API TensorShape per contenere int s invece di oggetti tf.compat.v1.Dimension .
  • Aggiorna la meccanica dell'uguaglianza dei tensori. In TF1.x l'operatore == su tensori e variabili verifica l'uguaglianza dei riferimenti agli oggetti. In TF2 verifica l'uguaglianza dei valori. Inoltre, tensori/variabili non sono più hashable, ma è possibile ottenere riferimenti a oggetti hashable tramite var.ref() se è necessario utilizzarli in insiemi o come chiavi dict .

Le sezioni seguenti forniscono un po' di contesto in più sulle differenze tra TF1.x e TF2. Per saperne di più sul processo di progettazione alla base di TF2, leggi le RFC e i documenti di progettazione .

Pulizia dell'API

Molte API sono sparite o spostate in TF2. Alcune delle modifiche principali includono la rimozione di tf.app , tf.flags e tf.logging a favore di absl-py ora open source , il rehoming dei progetti che vivevano in tf.contrib e la pulizia dello spazio dei nomi tf.* principale tramite spostare le funzioni meno utilizzate in sottopacchetti come tf.math . Alcune API sono state sostituite con i loro equivalenti TF2: tf.summary , tf.keras.metrics e tf.keras.optimizers .

tf.compat.v1 : Endpoint API legacy e compatibilità

I simboli negli spazi dei nomi tf.compat e tf.compat.v1 non sono considerati API TF2. Questi spazi dei nomi espongono un mix di simboli di compatibilità, nonché endpoint API legacy di TF 1.x. Questi hanno lo scopo di aiutare la migrazione da TF1.x a TF2. Tuttavia, poiché nessuna di queste API compat.v1 sono API TF2 idiomatiche, non usarle per scrivere codice TF2 nuovo di zecca.

I singoli simboli tf.compat.v1 possono essere compatibili con TF2 perché continuano a funzionare anche con i comportamenti TF2 abilitati (come tf.compat.v1.losses.mean_squared_error ), mentre altri sono incompatibili con TF2 (come tf.compat.v1.metrics.accuracy ). Molti simboli compat.v1 (anche se non tutti) contengono informazioni sulla migrazione dedicate nella documentazione che spiega il loro grado di compatibilità con i comportamenti di TF2, nonché come migrarli alle API di TF2.

Lo script di aggiornamento TF2 può mappare molti simboli API compat.v1 su API TF2 equivalenti nel caso in cui siano alias o abbiano gli stessi argomenti ma con un ordine diverso. Puoi anche utilizzare lo script di aggiornamento per rinominare automaticamente le API TF1.x.

API di falsi amici

Ci sono una serie di simboli "falsi amici" trovati nello spazio dei nomi TF2 tf (non sotto compat.v1 ) che in realtà ignorano i comportamenti TF2 nascosti e/o non sono completamente compatibili con l'insieme completo dei comportamenti TF2. Pertanto, è probabile che queste API si comportino in modo anomalo con il codice TF2, potenzialmente in modo silenzioso.

  • tf.estimator.* : Gli estimatori creano e utilizzano grafici e sessioni sotto il cofano. In quanto tali, questi non dovrebbero essere considerati compatibili con TF2. Se il codice esegue estimatori, non utilizza comportamenti TF2.
  • keras.Model.model_to_estimator(...) : Questo crea uno stimatore sotto il cofano, che come menzionato sopra non è compatibile con TF2.
  • tf.Graph().as_default() : inserisce i comportamenti del grafico TF1.x e non segue i comportamenti tf.function standard compatibili con TF2. Il codice che inserisce grafici come questo li eseguirà generalmente tramite Sessions e non dovrebbe essere considerato compatibile con TF2.
  • tf.feature_column.* Le API della colonna delle funzioni generalmente si basano sulla creazione di variabili tf.compat.v1.get_variable in stile TF1 e presuppongono che le variabili create saranno accessibili tramite raccolte globali. Poiché TF2 non supporta le raccolte, le API potrebbero non funzionare correttamente durante l'esecuzione con i comportamenti TF2 abilitati.

Altre modifiche alle API

  • TF2 presenta miglioramenti significativi agli algoritmi di posizionamento del dispositivo che rendono superfluo l'utilizzo di tf.colocate_with . Se la rimozione provoca un peggioramento delle prestazioni, segnalare un bug .

  • Sostituisci tutto l'utilizzo di tf.v1.ConfigProto con funzioni equivalenti da tf.config .

Esecuzione impaziente

TF1.x richiedeva di unire manualmente un albero di sintassi astratto (il grafico) effettuando chiamate API tf.* e quindi compilare manualmente l'albero di sintassi astratto passando un insieme di tensori di output e tensori di input a una chiamata session.run . TF2 viene eseguito avidamente (come fa normalmente Python) e rende i grafici e le sessioni come dettagli di implementazione.

Un notevole sottoprodotto dell'esecuzione desiderosa è che tf.control_dependencies non è più richiesto, poiché tutte le righe di codice vengono eseguite in ordine (all'interno di una tf.function , il codice con effetti collaterali viene eseguito nell'ordine scritto).

Niente più globali

TF1.x faceva molto affidamento su spazi dei nomi e raccolte globali impliciti. Quando hai chiamato tf.Variable , sarebbe stato inserito in una raccolta nel grafico predefinito e rimarrebbe lì, anche se hai perso traccia della variabile Python che punta ad essa. Potresti quindi recuperare quella tf.Variable , ma solo se conoscessi il nome con cui era stata creata. Questo era difficile da fare se non avevi il controllo della creazione della variabile. Di conseguenza, sono proliferati tutti i tipi di meccanismi per cercare di aiutarti a ritrovare le variabili e per i framework di trovare variabili create dall'utente. Alcuni di questi includono: ambiti di variabili, raccolte globali, metodi di supporto come tf.get_global_step e tf.global_variables_initializer , ottimizzatori che calcolano implicitamente i gradienti su tutte le variabili addestrabili e così via. TF2 elimina tutti questi meccanismi ( Variabili 2.0 RFC ) a favore del meccanismo predefinito: tieni traccia delle tue variabili. Se perdi le tracce di una tf.Variable , viene raccolta la spazzatura.

Il requisito di tenere traccia delle variabili crea un po' di lavoro extra, ma con strumenti come gli spessori di modellazione e comportamenti come raccolte di variabili orientate agli oggetti implicite in tf.Module s e tf.keras.layers.Layer s , il carico è ridotto al minimo.

Funzioni, non sessioni

Una chiamata session.run è quasi come una chiamata di funzione: specifichi gli input e la funzione da chiamare e ottieni una serie di output. In TF2, puoi decorare una funzione Python usando tf.function per contrassegnarla per la compilazione JIT in modo che TensorFlow la esegua come un singolo grafico ( Functions 2.0 RFC ). Questo meccanismo consente a TF2 di ottenere tutti i vantaggi della modalità grafico:

  • Prestazioni: la funzione può essere ottimizzata (potatura dei nodi, fusione del kernel, ecc.)
  • Portabilità: la funzione può essere esportata/reimportata ( SavedModel 2.0 RFC ), consentendo di riutilizzare e condividere le funzioni TensorFlow modulari.
# TF1.x
outputs = session.run(f(placeholder), feed_dict={placeholder: input})
# TF2
outputs = f(input)

Con il potere di alternare liberamente il codice Python e TensorFlow, puoi sfruttare l'espressività di Python. Tuttavia, TensorFlow portatile viene eseguito in contesti senza un interprete Python, come mobile, C++ e JavaScript. Per evitare di riscrivere il codice quando aggiungi tf.function , usa AutoGraph per convertire un sottoinsieme di costrutti Python nei loro equivalenti TensorFlow:

  • for / while -> tf.while_loop ( break e continue sono supportati)
  • if -> tf.cond
  • for _ in dataset -> dataset.reduce

AutoGraph supporta annidamenti arbitrari del flusso di controllo, il che rende possibile implementare in modo efficiente e conciso molti programmi ML complessi come modelli di sequenza, apprendimento per rinforzo, cicli di formazione personalizzati e altro ancora.

Adattamento alle modifiche al comportamento di TF 2.x

La migrazione a TF2 è completa solo dopo la migrazione al set completo di comportamenti TF2. Il set completo di comportamenti può essere abilitato o disabilitato tramite tf.compat.v1.enable_v2_behaviors e tf.compat.v1.disable_v2_behaviors . Le sezioni seguenti discutono in dettaglio ogni importante cambiamento di comportamento.

Utilizzando tf.function s

È probabile che le modifiche più importanti ai programmi durante la migrazione provengano dal cambiamento fondamentale del paradigma del modello di programmazione da grafici e sessioni all'esecuzione ansiosa e tf.function . Fare riferimento alle guide alla migrazione di TF2 per ulteriori informazioni sul passaggio da API incompatibili con l'esecuzione ansiosa e tf.function ad API compatibili con esse.

Di seguito sono riportati alcuni modelli di programma comuni non legati a nessuna API che potrebbe causare problemi quando si passa da tf.Graph se tf.compat.v1.Session s all'esecuzione desiderosa con tf.function s.

Pattern 1: la manipolazione di oggetti Python e la creazione di variabili destinate a essere eseguite solo una volta vengono eseguite più volte

Nei programmi TF1.x che si basano su grafici e sessioni, l'aspettativa è solitamente che tutta la logica Python nel tuo programma verrà eseguita solo una volta. Tuttavia, con l'esecuzione desiderosa e tf.function è lecito aspettarsi che la logica Python venga eseguita almeno una volta, ma possibilmente più volte (più volte con entusiasmo o più volte su diverse tracce tf.function ). A volte, tf.function traccia anche due volte sullo stesso input, causando comportamenti imprevisti (vedi Esempio 1 e 2). Fare riferimento alla guida alla tf.function per maggiori dettagli.

Esempio 1: creazione di variabili

Considera l'esempio seguente, in cui la funzione crea una variabile quando viene chiamata:

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)

Tuttavia, non è consentito eseguire il wrapping ingenuamente della funzione precedente che contiene la creazione di variabili con tf.function . tf.function supporta solo la creazione di variabili singleton alla prima chiamata . Per imporre ciò, quando tf.function rileva la creazione di variabili nella prima chiamata, tenterà di tracciare nuovamente e genererà un errore se è presente la creazione di variabili nella seconda traccia.

@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 soluzione è memorizzare nella cache e riutilizzare la variabile dopo che è stata creata nella prima chiamata.

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

Esempio 2: Tensori fuori campo dovuti al ritracciamento di tf.function

Come dimostrato nell'Esempio 1, tf.function quando rileva la creazione di variabili nella prima chiamata. Ciò può causare ulteriore confusione, poiché i due tracciati creeranno due grafici. Quando il secondo grafico del ritracciamento tenta di accedere a un tensore dal grafico generato durante il primo tracciamento, Tensorflow genererà un errore lamentando che il tensore è fuori dall'ambito. Per illustrare lo scenario, il codice seguente crea un set di dati sulla prima chiamata tf.function . Questo funzionerebbe come previsto.

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

Tuttavia, se proviamo anche a creare una variabile sulla prima chiamata tf.function , il codice genererà un errore lamentando che il set di dati è fuori dall'ambito. Questo perché il set di dati si trova nel primo grafico, mentre anche il secondo grafico sta tentando di accedervi.

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 soluzione più semplice è garantire che la creazione della variabile e la creazione del set di dati siano entrambe al di fuori della chiamata tf.funciton . Per esempio:

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

Tuttavia, a volte non è evitabile creare variabili in tf.function (come le variabili slot in alcuni ottimizzatori keras TF ). Tuttavia, possiamo semplicemente spostare la creazione del set di dati al di fuori della chiamata tf.function . Il motivo per cui possiamo fare affidamento su questo è perché tf.function riceverà il set di dati come input implicito ed entrambi i grafici possono accedervi correttamente.

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

Esempio 3: ricreazioni impreviste di oggetti Tensorflow dovute all'utilizzo di dict

tf.function ha un supporto molto scarso per gli effetti collaterali di Python come l'aggiunta a un elenco o il controllo/aggiunta a un dizionario. Maggiori dettagli sono in "Prestazioni migliori con tf.function" . Nell'esempio seguente, il codice usa dizionari per memorizzare nella cache set di dati e iteratori. Per la stessa chiave, ogni chiamata al modello restituirà lo stesso iteratore del set di dati.

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

Tuttavia, il modello sopra non funzionerà come previsto in tf.function . Durante il tracciamento, tf.function ignorerà l'effetto collaterale python dell'aggiunta ai dizionari. Invece, ricorda solo la creazione di un nuovo set di dati e iteratore. Di conseguenza, ogni chiamata al modello restituirà sempre un nuovo iteratore. Questo problema è difficile da notare a meno che i risultati numerici o le prestazioni non siano sufficientemente significativi. Pertanto, consigliamo agli utenti di pensare attentamente al codice prima di avvolgere ingenuamente tf.function nel codice 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

Possiamo usare tf.init_scope per sollevare il set di dati e la creazione dell'iteratore al di fuori del grafico, per ottenere il comportamento previsto:

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 regola generale è evitare di fare affidamento sugli effetti collaterali di Python nella logica e usarli solo per eseguire il debug delle tracce.

Esempio 4: manipolazione di un elenco Python globale

Il codice TF1.x seguente utilizza un elenco globale delle perdite che utilizza solo per mantenere l'elenco delle perdite generate dalla fase di addestramento corrente. Si noti che la logica Python che aggiunge le perdite all'elenco verrà chiamata solo una volta, indipendentemente dal numero di passaggi di addestramento per i quali viene eseguita la sessione.

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

Tuttavia, se questa logica Python è mappata ingenuamente a TF2 con un'esecuzione ansiosa, l'elenco globale delle perdite avrà nuovi valori aggiunti in ogni fase di addestramento. Ciò significa che il codice della fase di addestramento che in precedenza prevedeva che l'elenco contenesse solo le perdite della fase di addestramento corrente ora vede effettivamente l'elenco delle perdite di tutte le fasi di addestramento eseguite fino a quel momento. Si tratta di una modifica del comportamento non intenzionale e l'elenco dovrà essere cancellato all'inizio di ogni passaggio o reso locale al passaggio di addestramento.

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

Schema 2: un tensore simbolico destinato a essere ricalcolato ad ogni passaggio in TF1.x viene accidentalmente memorizzato nella cache con il valore iniziale quando si passa a desideroso.

Questo modello di solito fa sì che il codice si comporti in modo anomalo durante l'esecuzione ansiosa al di fuori di tf.functions, ma genera un InaccessibleTensorError se la memorizzazione nella cache del valore iniziale si verifica all'interno di un tf.function . Tuttavia, tieni presente che per evitare il modello 1 sopra, spesso strutturerai inavvertitamente il tuo codice in modo tale che questa memorizzazione nella cache del valore iniziale avvenga al di fuori di qualsiasi tf.function che potrebbe generare un errore. Quindi, presta particolare attenzione se sai che il tuo programma potrebbe essere suscettibile a questo schema.

La soluzione generale a questo modello è ristrutturare il codice o utilizzare callable Python, se necessario, per assicurarsi che il valore venga ricalcolato ogni volta invece di essere accidentalmente memorizzato nella cache.

Esempio 1: tasso di apprendimento/iperparametro/ecc. programmi che dipendono dal passo globale

Nel frammento di codice seguente, l'aspettativa è che ogni volta che viene eseguita la sessione verrà letto il valore global_step più recente e verrà calcolata una nuova velocità di apprendimento.

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

Tuttavia, quando provi a passare a desideroso, fai attenzione a finire con il calcolo del tasso di apprendimento solo una volta e poi riutilizzato, piuttosto che seguire il programma 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)
  ...

Poiché questo esempio specifico è un modello comune e gli ottimizzatori devono essere inizializzati solo una volta anziché in ogni fase di addestramento, gli ottimizzatori TF2 supportano le pianificazioni tf.keras.optimizers.schedules.LearningRateSchedule o i callable Python come argomenti per la velocità di apprendimento e altri iperparametri.

Esempio 2: le inizializzazioni simboliche di numeri casuali assegnate come attributi dell'oggetto e poi riutilizzate tramite il puntatore vengono accidentalmente memorizzate nella cache quando si passa a desideroso

Considera il seguente modulo 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

Usarlo come segue in TF1.x calcolerà un nuovo tensore di rumore casuale ogni volta che viene eseguita la sessione:

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

Tuttavia, in TF2 l'inizializzazione di noise_adder all'inizio causerà il calcolo di noise_distribution solo una volta e verrà bloccato per tutti i passaggi di addestramento:

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

Per risolvere questo problema, rifattorizzare NoiseAdder per chiamare tf.random.normal ogni volta che è necessario un nuovo tensore casuale, invece di fare riferimento allo stesso oggetto tensore ogni volta.

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

Pattern 3: il codice TF1.x si basa direttamente e cerca i tensori per nome

È comune per i test del codice TF1.x fare affidamento sul controllo di quali tensori o operazioni sono presenti in un grafico. In alcuni rari casi, il codice di modellazione si baserà anche su queste ricerche per nome.

I nomi dei tensori non vengono generati quando si esegue avidamente al di fuori di tf.function , quindi tutti gli usi di tf.Tensor.name devono avvenire all'interno di una tf.function . Tieni presente che è molto probabile che i nomi effettivamente generati differiscano tra TF1.x e TF2 anche all'interno della stessa tf.function e le garanzie API non garantiscono la stabilità dei nomi generati tra le versioni TF.

Pattern 4: la sessione TF1.x esegue selettivamente solo una parte del grafico generato

In TF1.x, puoi costruire un grafico e quindi scegliere di eseguirne selettivamente solo un sottoinsieme con una sessione, scegliendo un insieme di input e output che non richiedono l'esecuzione di tutte le operazioni nel grafico.

Ad esempio, potresti avere sia un generatore che un discriminatore all'interno di un singolo grafico e utilizzare chiamate tf.compat.v1.Session.run separate per alternare solo l'addestramento del discriminatore o solo l'addestramento del generatore.

In TF2, a causa delle dipendenze del controllo automatico in tf.function e dell'esecuzione ansiosa, non vi è alcuna potatura selettiva delle tracce di tf.function . Verrebbe eseguito un grafico completo contenente tutti gli aggiornamenti delle variabili anche se, ad esempio, viene emesso solo l'output del discriminatore o del generatore da tf.function .

Quindi, dovresti usare più tf.function s contenenti diverse parti del programma, o un argomento condizionale alla tf.function su cui ti rami in modo da eseguire solo le cose che vuoi effettivamente eseguire.

Rimozione raccolte

Quando l'esecuzione desiderosa è abilitata, le API compat.v1 relative alla raccolta di grafici (incluse quelle che leggono o scrivono in raccolte nascoste come tf.compat.v1.trainable_variables ) non sono più disponibili. Alcuni possono generare ValueError s, mentre altri possono restituire silenziosamente elenchi vuoti.

L'utilizzo più standard delle raccolte in TF1.x consiste nel mantenere gli inizializzatori, il passaggio globale, i pesi, le perdite di regolarizzazione, le perdite di output del modello e gli aggiornamenti delle variabili che devono essere eseguiti, ad esempio dai livelli di BatchNormalization .

Per gestire ciascuno di questi usi standard:

  1. Inizializzatori - Ignora. L'inizializzazione manuale delle variabili non è richiesta con l'esecuzione desiderosa abilitata.
  2. Passaggio globale: consultare la documentazione di tf.compat.v1.train.get_or_create_global_step per le istruzioni di migrazione.
  3. Pesi: mappa i tuoi modelli su tf.Module s/ tf.keras.layers.Layer s/ tf.keras.Model s seguendo le indicazioni nella guida alla mappatura del modello e quindi utilizza i rispettivi meccanismi di rilevamento del peso come tf.module.trainable_variables .
  4. Perdite di regolarizzazione: mappa i tuoi modelli su tf.Module s/ tf.keras.layers.Layer s/ tf.keras.Model s seguendo le indicazioni nella guida alla mappatura dei modelli e quindi usa tf.keras.losses . In alternativa, puoi anche monitorare manualmente le tue perdite di regolarizzazione.
  5. Modella le perdite di output: utilizza i meccanismi di gestione delle perdite tf.keras.Model o tieni traccia separatamente delle perdite senza utilizzare le raccolte.
  6. Aggiornamenti del peso: ignora questa raccolta. L'esecuzione desiderosa e tf.function . (con autografo e dipendenze di controllo automatico) significano che tutti gli aggiornamenti delle variabili verranno eseguiti automaticamente. Quindi, non dovrai eseguire esplicitamente tutti gli aggiornamenti di peso alla fine, ma tieni presente che ciò significa che gli aggiornamenti di peso potrebbero avvenire in un momento diverso rispetto a quello che hanno fatto nel tuo codice TF1.x, a seconda di come stavi usando le dipendenze di controllo.
  7. Riepiloghi: fare riferimento alla guida all'API di riepilogo della migrazione .

L'utilizzo di raccolte più complesse (come l'utilizzo di raccolte personalizzate) potrebbe richiedere il refactoring del codice per mantenere i propri negozi globali o per non fare affidamento sugli archivi globali.

ResourceVariables invece di ReferenceVariables

ResourceVariables ha garanzie di coerenza in lettura/scrittura più solide rispetto a ReferenceVariables . Ciò porta a una semantica più prevedibile e più facile da ragionare sull'osservazione o meno del risultato di una scrittura precedente quando si utilizzano le variabili. È estremamente improbabile che questa modifica provochi la generazione di errori o l'interruzione silenziosa del codice esistente.

Tuttavia, è possibile, anche se improbabile , che queste garanzie di coerenza più forti possano aumentare l'utilizzo della memoria del programma specifico. Si prega di presentare un problema se si ritiene che questo sia il caso. Inoltre, se disponi di unit test che si basano su confronti esatti di stringhe con i nomi degli operatori in un grafico corrispondente alle letture di variabili, tieni presente che l'abilitazione delle variabili di risorsa può modificare leggermente il nome di questi operatori.

Per isolare l'impatto di questa modifica del comportamento sul codice, se l'esecuzione ansiosa è disabilitata è possibile utilizzare tf.compat.v1.disable_resource_variables() e tf.compat.v1.enable_resource_variables() per disabilitare o abilitare globalmente questa modifica del comportamento. ResourceVariables verrà sempre utilizzato se l'esecuzione desiderosa è abilitata.

Flusso di controllo v2

In TF1.x, operazioni di flusso di controllo come tf.cond e tf.while_loop di basso livello in linea come Switch , Merge ecc. TF2 fornisce operazioni di flusso di controllo funzionali migliorate implementate con tracce tf.function separate per ogni ramo e supporto differenziazione di ordine superiore.

Per isolare l'impatto di questa modifica del comportamento sul codice, se l'esecuzione ansiosa è disabilitata è possibile utilizzare tf.compat.v1.disable_control_flow_v2() e tf.compat.v1.enable_control_flow_v2() per disabilitare o abilitare globalmente questa modifica del comportamento. Tuttavia, puoi disabilitare il flusso di controllo v2 solo se anche l'esecuzione desiderosa è disabilitata. Se è abilitato, verrà sempre utilizzato il flusso di controllo v2.

Questa modifica del comportamento può cambiare drasticamente la struttura dei programmi TF generati che utilizzano il flusso di controllo, poiché conterranno diverse tracce di funzioni nidificate anziché un grafico piatto. Pertanto, qualsiasi codice fortemente dipendente dalla semantica esatta delle tracce prodotte potrebbe richiedere alcune modifiche. Ciò comprende:

  • Codice basato su nomi di operatori e tensori
  • Codice che fa riferimento ai tensori creati all'interno di un ramo di flusso di controllo TensorFlow dall'esterno di quel ramo. È probabile che questo produca un InaccessibleTensorError

Questa modifica del comportamento deve essere da neutra a positiva, ma se riscontri un problema in cui il flusso di controllo v2 ha prestazioni peggiori per te rispetto al flusso di controllo TF1.x, segnala un problema con i passaggi di riproduzione.

Il comportamento dell'API TensorShape cambia

La classe TensorShape è stata semplificata per contenere oggetti int s, anziché tf.compat.v1.Dimension . Quindi non è necessario chiamare .value per ottenere un int .

I singoli oggetti tf.compat.v1.Dimension sono ancora accessibili da tf.TensorShape.dims .

Per isolare l'impatto di questa modifica del comportamento sul codice, puoi utilizzare tf.compat.v1.disable_v2_tensorshape() e tf.compat.v1.enable_v2_tensorshape() per disabilitare o abilitare globalmente questa modifica del comportamento.

Di seguito vengono illustrate le differenze tra TF1.x e 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 avevi questo in TF1.x:

value = shape[i].value

Quindi fallo in TF2:

value = shape[i]
value
16

Se avevi questo in TF1.x:

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

Quindi, fallo in TF2:

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

Se lo avevi in ​​TF1.x (o usavi qualsiasi altro metodo di dimensione):

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

Quindi fallo in 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

Il valore booleano di un tf.TensorShape è True se il rango è noto, False in 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

Potenziali errori dovuti a modifiche di TensorShape

È improbabile che le modifiche al comportamento di TensorShape interrompano silenziosamente il codice. Tuttavia, potresti vedere che il codice relativo alla forma inizia a generare AttributeError s poiché int s e None s non hanno gli stessi attributi di tf.compat.v1.Dimension s. Di seguito sono riportati alcuni esempi di questi 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'

Uguaglianza tensoriale per valore

Gli operatori binari == e != su variabili e tensori sono stati modificati per confrontare in base al valore in TF2 anziché in base al riferimento all'oggetto come in TF1.x. Inoltre, tensori e variabili non sono più direttamente utilizzabili come hash o utilizzabili in set o chiavi dict, perché potrebbe non essere possibile eseguire l'hashing per valore. Invece, espongono un metodo .ref() che puoi usare per ottenere un riferimento hashable al tensore o alla variabile.

Per isolare l'impatto di questa modifica del comportamento, puoi utilizzare tf.compat.v1.disable_tensor_equality() e tf.compat.v1.enable_tensor_equality() per disabilitare o abilitare globalmente questa modifica del comportamento.

Ad esempio, in TF1.x, due variabili con lo stesso valore restituiranno false quando si utilizza l'operatore == :

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

x == y
False

Mentre in TF2 con i controlli di uguaglianza del tensore abilitati, x == y restituirà 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>

Quindi, in TF2, se hai bisogno di confrontare per riferimento a un oggetto assicurati di usare is e is not

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

x is y
False

Tensori e variabili di hashing

Con i comportamenti di TF1.x potevi aggiungere direttamente variabili e tensori a strutture di dati che richiedono l'hashing, come le chiavi 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>}

Tuttavia, in TF2 con l'uguaglianza del tensore abilitata, i tensori e le variabili sono resi non hash a causa della semantica dell'operatore == e != che cambia in controlli di uguaglianza dei valori.

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.

Quindi, in TF2 se è necessario utilizzare oggetti tensore o variabili come chiavi o set contenuti, è possibile utilizzare tensor.ref() per ottenere un riferimento hashable che può essere utilizzato come chiave:

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 necessario, puoi anche ottenere il tensore o la variabile dal riferimento 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>

Risorse e ulteriori letture

  • Visita la sezione Migrazione a TF2 per ulteriori informazioni sulla migrazione a TF2 da TF1.x.
  • Leggi la guida alla mappatura dei modelli per saperne di più sulla mappatura dei tuoi modelli TF1.x per lavorare direttamente in TF2.