TensorFlow 1.x a TensorFlow 2 — zachowania i interfejsy API

Zobacz na TensorFlow.org Uruchom w Google Colab Zobacz na GitHub Pobierz notatnik

Pod maską TensorFlow 2 podąża za zasadniczo innym paradygmatem programowania niż TF1.x.

W tym przewodniku opisano podstawowe różnice między programami TF1.x i TF2 pod względem zachowań i interfejsów API oraz ich związek z podróżą migracji.

Ogólne podsumowanie głównych zmian

Zasadniczo TF1.x i TF2 używają innego zestawu zachowań środowiska uruchomieniowego wokół wykonywania (chętne w TF2), zmiennych, przepływu sterowania, kształtów tensorów i porównań równości tensorów. Aby był zgodny z TF2, Twój kod musi być zgodny z pełnym zestawem zachowań TF2. Podczas migracji większość z tych zachowań można włączać lub wyłączać pojedynczo za pomocą interfejsów API tf.compat.v1.enable_* lub tf.compat.v1.disable_* . Jedynym wyjątkiem jest usuwanie kolekcji, co jest efektem ubocznym włączania/wyłączania gorliwego wykonywania.

Na wysokim poziomie TensorFlow 2:

  • Usuwa nadmiarowe interfejsy API .
  • Sprawia, że ​​interfejsy API są bardziej spójne — na przykład Unified RNN i Unified Optimizers .
  • Preferuje funkcje niż sesje i lepiej integruje się ze środowiskiem wykonawczym Python z domyślnie włączonym wykonywaniem Eager wraz z tf.function , który zapewnia automatyczne zależności sterowania dla wykresów i kompilacji.
  • Wycofuje globalne kolekcje wykresów .
  • Zmienia semantykę współbieżności zmiennych przy użyciu ResourceVariables przez ReferenceVariables .
  • Obsługuje oparty na funkcjach i różniczkowy przepływ sterowania (Control Flow v2).
  • Upraszcza interfejs API TensorShape do przechowywania obiektów typu int zamiast obiektów tf.compat.v1.Dimension .
  • Aktualizuje mechanikę równości tensorów. W TF1.x operator == na tensorach i zmiennych sprawdza równość referencji obiektów. W TF2 sprawdza równość wartości. Dodatkowo tensory/zmienne nie są już haszowalne, ale możesz uzyskać do nich haszujące odniesienia do obiektów za pomocą var.ref() , jeśli chcesz ich używać w zestawach lub jako klawisze dict .

Poniższe sekcje zawierają więcej kontekstu na temat różnic między TF1.x i TF2. Aby dowiedzieć się więcej o procesie projektowania stojącym za TF2, przeczytaj dokumenty RFC i dokumentację projektową .

Oczyszczanie API

Wiele interfejsów API zniknęło lub zostało przeniesionych w TF2. Niektóre z głównych zmian obejmują usunięcie tf.app , tf.flags i tf.logging na rzecz absl-py o otwartym kodzie źródłowym , zmianę lokalizacji projektów, które znajdowały się w tf.contrib , oraz oczyszczenie głównej przestrzeni nazw tf.* przez przenoszenie mniej używanych funkcji do podpakietów, takich jak tf.math . Niektóre interfejsy API zostały zastąpione ich odpowiednikami w TF2 — tf.summary , tf.keras.metrics i tf.keras.optimizers .

tf.compat.v1 : starsze i zgodne punkty końcowe interfejsu API

Symbole w przestrzeniach nazw tf.compat i tf.compat.v1 nie są uznawane za interfejsy API TF2. Te przestrzenie nazw uwidaczniają mieszankę symboli zgodności, a także starsze punkty końcowe interfejsu API z TF 1.x. Mają one na celu ułatwienie migracji z TF1.x do TF2. Ponieważ jednak żaden z tych interfejsów API compat.v1 nie jest idiomatycznymi interfejsami API TF2, nie używaj ich do pisania zupełnie nowego kodu TF2.

Poszczególne symbole tf.compat.v1 mogą być zgodne z TF2, ponieważ nadal działają nawet przy włączonych zachowaniach TF2 (takich jak tf.compat.v1.losses.mean_squared_error ), podczas gdy inne są niezgodne z TF2 (takie jak tf.compat.v1.metrics.accuracy ). Wiele symboli compat.v1 (choć nie wszystkie) zawiera w swojej dokumentacji dedykowane informacje dotyczące migracji, które wyjaśniają ich stopień zgodności z zachowaniami TF2, a także sposób migracji ich do interfejsów API TF2.

Skrypt aktualizacji TF2 może mapować wiele symboli interfejsu API compat.v1 na równoważne interfejsy API TF2 w przypadku, gdy są one aliasami lub mają te same argumenty, ale o innej kolejności. Możesz również użyć skryptu aktualizacji, aby automatycznie zmienić nazwy interfejsów API TF1.x.

Interfejsy API fałszywych znajomych

Istnieje zestaw symboli „fałszywych przyjaciół” znalezionych w przestrzeni nazw TF2 tf (nie w compat.v1 ), które faktycznie ignorują ukryte zachowania TF2 i/lub nie są w pełni kompatybilne z pełnym zestawem zachowań TF2. W związku z tym te interfejsy API mogą źle działać z kodem TF2, potencjalnie w cichy sposób.

  • tf.estimator.* : Estymatory tworzą i wykorzystują wykresy i sesje pod maską. W związku z tym nie należy ich uważać za kompatybilne z TF2. Jeśli w kodzie działają estymatory, nie używa on zachowań TF2.
  • keras.Model.model_to_estimator(...) : Tworzy estymator pod maską, który, jak wspomniano powyżej, nie jest kompatybilny z TF2.
  • tf.Graph().as_default() : Wprowadza zachowanie wykresu TF1.x i nie jest zgodne ze standardowymi zachowaniami tf.function zgodnymi z TF2. Kod, który wprowadzi takie wykresy, zazwyczaj uruchamia je za pośrednictwem sesji i nie powinien być uważany za zgodny z TF2.
  • tf.feature_column.* Interfejsy API kolumn funkcji generalnie opierają się na tworzeniu zmiennych tf.compat.v1.get_variable w stylu TF1 i zakładają, że do utworzonych zmiennych będzie można uzyskać dostęp za pośrednictwem kolekcji globalnych. Ponieważ TF2 nie obsługuje kolekcji, interfejsy API mogą nie działać poprawnie podczas ich uruchamiania z włączonymi zachowaniami TF2.

Inne zmiany API

  • TF2 zawiera znaczące ulepszenia algorytmów umieszczania urządzeń, które sprawiają, że korzystanie z tf.colocate_with niepotrzebne. Jeśli usunięcie go spowoduje spadek wydajności , zgłoś błąd .

  • Zastąp całe użycie tf.v1.ConfigProto równoważnymi funkcjami z tf.config .

Chętna egzekucja

TF1.x wymagał ręcznego połączenia abstrakcyjnego drzewa składni (wykresu) przez wywołanie interfejsu API tf.* , a następnie ręcznego skompilowania abstrakcyjnego drzewa składni poprzez przekazanie zestawu tensorów wyjściowych i wejściowych do wywołania session.run . TF2 wykonuje się chętnie (jak zwykle robi to Python) i sprawia, że ​​wykresy i sesje przypominają szczegóły implementacji.

Jednym z godnych uwagi produktów ubocznych szybkiego wykonywania jest to, że tf.control_dependencies nie jest już wymagane, ponieważ wszystkie wiersze kodu są wykonywane w kolejności (w ramach tf.function kod z efektami ubocznymi jest wykonywany w kolejności zapisanej).

Nigdy więcej globalnych

TF1.x w dużym stopniu opierał się na niejawnych globalnych przestrzeniach nazw i kolekcjach. Kiedy tf.Variable , zostanie on umieszczony w kolekcji w domyślnym wykresie i pozostanie tam, nawet jeśli stracisz orientację wskazującą na nią zmienną Pythona. Możesz wtedy odzyskać tę tf.Variable , ale tylko wtedy, gdy znasz nazwę, pod którą została utworzona. Było to trudne do zrobienia, jeśli nie kontrolowałeś tworzenia zmiennej. W rezultacie mnożyły się wszelkiego rodzaju mechanizmy, które próbowały pomóc w ponownym odnalezieniu zmiennych, a frameworki w celu znalezienia zmiennych utworzonych przez użytkownika. Niektóre z nich obejmują: zakresy zmiennych, kolekcje globalne, metody pomocnicze, takie jak tf.get_global_step i tf.global_variables_initializer , optymalizatory niejawnie obliczające gradienty na wszystkich możliwych do trenowania zmiennych i tak dalej. TF2 eliminuje wszystkie te mechanizmy ( Variables 2.0 RFC ) na rzecz mechanizmu domyślnego - śledzisz swoje zmienne. Jeśli zgubisz tf.Variable , zbierane są śmieci.

Wymóg śledzenia zmiennych wymaga dodatkowej pracy, ale dzięki narzędziom takim jak podkładki modelujące i zachowania, takie jak niejawne kolekcje zmiennych obiektowych w tf.Module s i tf.keras.layers.Layer s , obciążenie jest zminimalizowane.

Funkcje, a nie sesje

Wywołanie session.run jest prawie jak wywołanie funkcji: określasz dane wejściowe i funkcję, która ma być wywołana, i otrzymujesz zestaw wyników. W TF2 możesz udekorować funkcję Pythona za pomocą tf.function do oznaczenia jej do kompilacji JIT, tak aby TensorFlow uruchomił ją jako pojedynczy wykres ( Functions 2.0 RFC ). Ten mechanizm pozwala TF2 na uzyskanie wszystkich zalet trybu wykresu:

  • Wydajność: funkcję można zoptymalizować (przycinanie węzłów, fuzja jądra itp.)
  • Przenośność: funkcję można eksportować/ponownie importować ( SavedModel 2.0 RFC ), co pozwala na ponowne wykorzystanie i udostępnianie modułowych funkcji TensorFlow.
# TF1.x
outputs = session.run(f(placeholder), feed_dict={placeholder: input})
# TF2
outputs = f(input)

Dzięki możliwości swobodnego przeplatania kodu Pythona i TensorFlow możesz wykorzystać ekspresję Pythona. Jednak przenośny TensorFlow wykonuje się w kontekstach bez interpretera Pythona, takiego jak mobile, C++ i JavaScript. Aby uniknąć przepisywania kodu podczas dodawania tf.function , użyj AutoGraph , aby przekonwertować podzbiór konstrukcji Pythona na ich odpowiedniki TensorFlow:

  • for / while -> tf.while_loop (obsługiwane są break i continue )
  • if -> tf.cond
  • for _ in dataset -> dataset.reduce

AutoGraph obsługuje dowolne zagnieżdżenia przepływu sterowania, co umożliwia wydajną i zwięzłą implementację wielu złożonych programów ML, takich jak modele sekwencji, uczenie się ze wzmocnieniem, niestandardowe pętle szkoleniowe i wiele innych.

Adaptacja do zmian w zachowaniu TF 2.x

Migracja do TF2 jest zakończona dopiero po migracji do pełnego zestawu zachowań TF2. Pełny zestaw zachowań można włączyć lub wyłączyć za pomocą tf.compat.v1.enable_v2_behaviors i tf.compat.v1.disable_v2_behaviors . Poniższe sekcje szczegółowo omawiają każdą poważną zmianę zachowania.

Korzystanie tf.function s

Największe zmiany w programach podczas migracji prawdopodobnie wynikają z przesunięcia podstawowego paradygmatu modelu programowania z wykresów i sesji do szybkiego wykonywania i tf.function . Zapoznaj się z przewodnikami migracji TF2, aby dowiedzieć się więcej o przechodzeniu z interfejsów API, które są niezgodne z szybkim wykonywaniem i tf.function do interfejsów API, które są z nimi zgodne.

Poniżej znajduje się kilka typowych wzorców programów niezwiązanych z żadnym API, które mogą powodować problemy podczas przełączania z tf.Graph s i tf.compat.v1.Session s do szybkiego wykonywania za pomocą tf.function s.

Wzorzec 1: Manipulacja obiektami Pythona i tworzenie zmiennych, które mają być wykonywane tylko raz, są uruchamiane wiele razy

W programach TF1.x, które opierają się na wykresach i sesjach, zwykle oczekuje się, że cała logika Pythona w twoim programie zostanie uruchomiona tylko raz. Jednak w przypadku szybkiego wykonywania i tf.function można oczekiwać, że logika Pythona zostanie uruchomiona co najmniej raz, ale prawdopodobnie więcej razy (albo wielokrotnie z chęcią, albo wielokrotnie na różnych śladach tf.function ). Czasami tf.function będzie nawet śledzić dwa razy na tym samym wejściu, powodując nieoczekiwane zachowania (patrz Przykład 1 i 2). Więcej informacji można znaleźć w przewodniku tf.function guide .

Przykład 1: Tworzenie zmiennej

Rozważmy poniższy przykład, w którym funkcja tworzy zmienną po wywołaniu:

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)

Jednak naiwne zawijanie powyższej funkcji, która zawiera tworzenie zmiennych, za pomocą tf.function jest niedozwolone. tf.function obsługuje tworzenie zmiennych singleton tylko przy pierwszym wywołaniu . Aby to wymusić, gdy tf.function wykryje tworzenie zmiennej w pierwszym wywołaniu, spróbuje ponownie śledzić i zgłosi błąd, jeśli w drugim śledzeniu wystąpi tworzenie zmiennej.

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

Obejście to buforowanie i ponowne użycie zmiennej po jej utworzeniu w pierwszym wywołaniu.

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

Przykład 2: Tensory poza zakresem z powodu tf.function

Jak pokazano w przykładzie 1, tf.function się, gdy wykryje utworzenie zmiennej w pierwszym wywołaniu. Może to spowodować dodatkowe zamieszanie, ponieważ dwa obrysy utworzą dwa wykresy. Gdy drugi wykres z retracingu próbuje uzyskać dostęp do Tensora z wykresu wygenerowanego podczas pierwszego śledzenia, Tensorflow zgłosi błąd, skarżąc się, że Tensor jest poza zakresem. Aby zademonstrować scenariusz, poniższy kod tworzy zestaw danych przy pierwszym wywołaniu tf.function . To działałoby zgodnie z oczekiwaniami.

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

Jeśli jednak spróbujemy również utworzyć zmienną przy pierwszym wywołaniu tf.function , kod zgłosi błąd, narzekając, że zestaw danych jest poza zakresem. Dzieje się tak, ponieważ zbiór danych znajduje się na pierwszym wykresie, podczas gdy drugi wykres również próbuje uzyskać do niego dostęp.

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.

Najprostszym rozwiązaniem jest zapewnienie, że tworzenie zmiennych i tworzenie zestawu danych znajdują się poza wywołaniem tf.funciton . Na przykład:

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

Jednak czasami nie da się uniknąć tworzenia zmiennych w tf.function (takich jak zmienne slotów w niektórych optymalizatorach TF Keras ). Mimo to możemy po prostu przenieść tworzenie zestawu danych poza wywołanie tf.function . Powodem, dla którego możemy na tym polegać, jest to, że tf.function odbierze zbiór danych jako niejawne dane wejściowe i oba wykresy będą miały do ​​niego prawidłowy dostęp.

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

Przykład 3: Nieoczekiwane odtworzenie obiektu Tensorflow z powodu użycia dict

tf.function ma bardzo słabą obsługę efektów ubocznych Pythona, takich jak dołączanie do listy lub sprawdzanie/dodawanie do słownika. Więcej szczegółów znajduje się w „Lepsza wydajność dzięki tf.function” . W poniższym przykładzie kod używa słowników do buforowania zestawów danych i iteratorów. Dla tego samego klucza każde wywołanie modelu zwróci ten sam iterator zestawu danych.

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

Jednak powyższy wzór nie będzie działał zgodnie z oczekiwaniami w tf.function . Podczas śledzenia tf.function zignoruje efekt uboczny Pythona związany z dodawaniem do słowników. Zamiast tego zapamiętuje tylko utworzenie nowego zestawu danych i iteratora. W rezultacie każde wywołanie modelu zawsze zwróci nowy iterator. Ten problem jest trudny do zauważenia, chyba że wyniki liczbowe lub wydajność są wystarczająco znaczące. Dlatego zalecamy użytkownikom dokładne przemyślenie kodu przed naiwnym zawinięciem tf.function na kod Pythona.

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

Możemy użyć tf.init_scope , aby przenieść zestaw danych i tworzenie iteratorów poza wykres, aby osiągnąć oczekiwane zachowanie:

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

Ogólną zasadą jest unikanie polegania na efektach ubocznych Pythona w swojej logice i używanie ich tylko do debugowania śladów.

Przykład 4: Manipulowanie globalną listą Pythona

Poniższy kod TF1.x używa globalnej listy strat, której używa tylko do utrzymywania listy strat wygenerowanych przez bieżący krok uczenia. Zwróć uwagę, że logika Pythona, która dodaje straty do listy, zostanie wywołana tylko raz, niezależnie od tego, dla ilu etapów szkolenia jest uruchamiana sesja.

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

Jeśli jednak ta logika Pythona zostanie naiwnie zmapowana do TF2 z gorliwym wykonaniem, globalna lista strat będzie miała dodawane nowe wartości w każdym kroku uczenia. Oznacza to, że kod etapu szkolenia, który wcześniej oczekiwał, że lista będzie zawierała tylko straty z bieżącego etapu szkolenia, teraz faktycznie widzi listę strat ze wszystkich dotychczas wykonanych etapów szkolenia. Jest to niezamierzona zmiana zachowania i lista będzie musiała zostać wyczyszczona na początku każdego kroku lub umieszczona lokalnie na etapie szkolenia.

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

Wzorzec 2: Symboliczny tensor, który ma być przeliczany na każdym kroku w TF1.x, jest przypadkowo buforowany z początkową wartością podczas przełączania na eager.

Ten wzorzec zwykle powoduje, że twój kod zachowuje się dyskretnie, gdy jest wykonywany z niecierpliwością poza tf.functions, ale wywołuje InaccessibleTensorError , jeśli buforowanie wartości początkowej występuje wewnątrz tf.function . Należy jednak pamiętać, że aby uniknąć powyższego wzorca 1 , często nieumyślnie ustrukturyzujesz swój kod w taki sposób, aby buforowanie wartości początkowej miało miejsce poza jakąkolwiek tf.function , która byłaby w stanie zgłosić błąd. Zachowaj więc szczególną ostrożność, jeśli wiesz, że Twój program może być podatny na ten wzorzec.

Ogólnym rozwiązaniem tego wzorca jest restrukturyzacja kodu lub użycie funkcji wywoływalnych Pythona, jeśli to konieczne, aby upewnić się, że wartość jest przeliczana za każdym razem, a nie przypadkowo buforowana.

Przykład 1: Szybkość uczenia/hiperparametr/itd. harmonogramy zależne od kroku globalnego

W poniższym fragmencie kodu oczekuje się, że przy każdym uruchomieniu sesji zostanie odczytana najnowsza wartość global_step i obliczona zostanie nowa szybkość uczenia się.

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

Jednak próbując przełączyć się na gorliwy, uważaj, aby nie skończyć z szybkością uczenia się obliczaną tylko raz, a następnie ponownie wykorzystaną, zamiast postępować zgodnie z zamierzonym harmonogramem:

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

Ponieważ ten konkretny przykład jest typowym wzorcem, a optymalizatory powinny być inicjowane tylko raz, a nie na każdym etapie uczenia, optymalizatory TF2 obsługują harmonogramy tf.keras.optimizers.schedules.LearningRateSchedule lub wywołania Pythona jako argumenty dla szybkości uczenia się i innych hiperparametrów.

Przykład 2: Symboliczne inicjalizacje liczb losowych przypisane jako atrybuty obiektu, a następnie ponownie użyte za pomocą wskaźnika są przypadkowo buforowane po przełączeniu na eager

Rozważ następujący moduł 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

Użycie go w następujący sposób w TF1.x obliczy nowy tensor losowego szumu za każdym razem, gdy uruchamiana jest sesja:

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

Jednak w TF2 inicjalizacja noise_adder na początku spowoduje, że noise_distribution zostanie obliczona tylko raz i zostanie zamrożona dla wszystkich kroków szkoleniowych:

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

Aby to naprawić, zrefaktoruj NoiseAdder , aby wywoływał tf.random.normal każdym razem, gdy potrzebny jest nowy losowy tensor, zamiast za każdym razem odnosić się do tego samego obiektu tensora.

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

Wzorzec 3: Kod TF1.x bezpośrednio opiera się na i wyszukuje tensory według nazwy

Często testy kodu TF1.x polegają na sprawdzeniu, jakie tensory lub operacje są obecne na grafie. W niektórych rzadkich przypadkach kod modelowania będzie również opierał się na tych wyszukiwaniach według nazwy.

Nazwy tensorów w ogóle nie są generowane podczas gorliwego wykonywania poza tf.function , więc wszystkie tf.Tensor.name muszą mieć miejsce wewnątrz tf.function . Należy pamiętać, że rzeczywiste wygenerowane nazwy najprawdopodobniej będą się różnić między TF1.xi TF2 nawet w ramach tej samej tf.function , a gwarancje API nie zapewniają stabilności generowanych nazw w różnych wersjach TF.

Wzorzec 4: Sesja TF1.x selektywnie uruchamia tylko część wygenerowanego wykresu

W TF1.x można skonstruować wykres, a następnie wybrać tylko selektywne uruchamianie tylko jego podzbioru w sesji, wybierając zestaw danych wejściowych i wyjściowych, które nie wymagają uruchamiania każdej operacji na wykresie.

Na przykład możesz mieć zarówno generator, jak i dyskryminator w jednym wykresie i używać oddzielnych wywołań tf.compat.v1.Session.run , aby naprzemiennie trenować tylko dyskryminator lub tylko trenować generator.

W TF2 ze względu na zależności automatycznego sterowania w tf.function i szybkie wykonywanie, nie ma selektywnego czyszczenia śladów tf.function . Pełny wykres zawierający wszystkie aktualizacje zmiennych zostanie uruchomiony, nawet jeśli na przykład z funkcji tf.function zostanie wyprowadzony tylko wynik dyskryminatora lub generatora.

Tak więc musiałbyś użyć wielu tf.function zawierających różne części programu lub warunkowego argumentu do tf.function , na którym się rozgałęzisz, aby wykonać tylko to, co faktycznie chcesz uruchomić.

Usuwanie kolekcji

Gdy włączone jest szybkie wykonywanie, interfejsy API compat.v1 związane z kolekcją wykresów (w tym te, które odczytują lub zapisują kolekcje pod maską, takie jak tf.compat.v1.trainable_variables ) nie są już dostępne. Niektóre mogą zgłaszać ValueError s, podczas gdy inne mogą po cichu zwracać puste listy.

Najbardziej standardowym zastosowaniem kolekcji w TF1.x jest utrzymanie inicjatorów, kroku globalnego, wag, strat regularyzacji, strat danych wyjściowych modelu i aktualizacji zmiennych, które należy uruchomić, na przykład z warstw BatchNormalization .

Aby obsłużyć każde z tych standardowych zastosowań:

  1. Inicjatory — ignoruj. Ręczna inicjalizacja zmiennych nie jest wymagana przy włączonym przyspieszonym wykonywaniu.
  2. Krok globalny — instrukcje dotyczące migracji można znaleźć w dokumentacji tf.compat.v1.train.get_or_create_global_step .
  3. Wagi — mapuj swoje modele na tf.Module s/ tf.keras.layers.Layer s/ tf.keras.Model s, postępując zgodnie ze wskazówkami w przewodniku mapowania modeli, a następnie użyj odpowiednich mechanizmów śledzenia wagi, takich jak tf.module.trainable_variables .
  4. Straty przy regularyzacji — przypisz swoje modele do tf.Module s/ tf.keras.layers.Layer s/ tf.keras.Model s, postępując zgodnie ze wskazówkami w przewodniku mapowania modeli, a następnie użyj tf.keras.losses . Alternatywnie możesz również ręcznie śledzić straty związane z regularyzacją.
  5. Modelowe straty wyjściowe — użyj mechanizmów zarządzania stratami tf.keras.Model lub oddzielnie śledź swoje straty bez korzystania z windykacji.
  6. Aktualizacje wagi — zignoruj ​​tę kolekcję. Chętne wykonanie i tf.function (z autografem i zależnościami auto-kontroli) oznaczają, że wszystkie aktualizacje zmiennych zostaną uruchomione automatycznie. Tak więc nie będziesz musiał jawnie uruchamiać wszystkich aktualizacji wagi na końcu, ale pamiętaj, że oznacza to, że aktualizacje wagi mogą nastąpić w innym czasie niż w kodzie TF1.x, w zależności od tego, jak używałeś zależności kontrolnych.
  7. Podsumowania — zapoznaj się z przewodnikiem po migracji podsumowania interfejsu API .

Bardziej złożone użycie kolekcji (takie jak używanie kolekcji niestandardowych) może wymagać refaktoryzacji kodu w celu utrzymania własnych sklepów globalnych lub w ogóle nie polegania na sklepach globalnych.

ResourceVariables zamiast ReferenceVariables

ResourceVariables mają silniejsze gwarancje spójności odczytu i zapisu niż ReferenceVariables . Prowadzi to do bardziej przewidywalnych, łatwiejszych do uzasadnienia w semantyce tego, czy będziesz obserwować wynik poprzedniego zapisu podczas używania zmiennych. Ta zmiana jest bardzo mało prawdopodobna, aby spowodować, że istniejący kod będzie zgłaszał błędy lub dyskretnie się zepsuje.

Jednak jest możliwe, choć mało prawdopodobne , że te silniejsze gwarancje spójności mogą zwiększyć użycie pamięci przez określony program. Zgłoś problem , jeśli tak jest. Ponadto, jeśli masz testy jednostkowe oparte na dokładnych porównaniach ciągów z nazwami operatorów na wykresie odpowiadającym odczytom zmiennych, pamiętaj, że włączenie zmiennych zasobów może nieznacznie zmienić nazwy tych operatorów.

Aby wyizolować wpływ tej zmiany zachowania na kod, jeśli szybkie wykonywanie jest wyłączone, możesz użyć tf.compat.v1.disable_resource_variables() i tf.compat.v1.enable_resource_variables() , aby globalnie wyłączyć lub włączyć tę zmianę zachowania. ResourceVariables będą zawsze używane, jeśli włączone jest szybkie wykonywanie.

Sterowanie przepływem v2

W TF1.x, operacje przepływu sterowania, takie jak tf.cond i tf.while_loop , wbudowane niskopoziomowe operacje, takie jak Switch , Merge itp. TF2 zapewnia ulepszone operacje przepływu sterowania funkcjonalne, które są zaimplementowane z oddzielnymi śladami tf.function dla każdej gałęzi i wsparcia zróżnicowanie wyższego rzędu.

Aby wyizolować wpływ tej zmiany zachowania na kod, jeśli szybkie wykonywanie jest wyłączone, możesz użyć tf.compat.v1.disable_control_flow_v2() i tf.compat.v1.enable_control_flow_v2() , aby globalnie wyłączyć lub włączyć tę zmianę zachowania. Przepływ sterowania v2 można jednak wyłączyć tylko wtedy, gdy przyspieszone wykonywanie jest również wyłączone. Jeśli jest włączona, zawsze będzie używany przepływ sterowania v2.

Ta zmiana zachowania może radykalnie zmienić strukturę generowanych programów TF, które używają przepływu sterowania, ponieważ będą one zawierać kilka zagnieżdżonych śladów funkcji zamiast jednego płaskiego wykresu. Tak więc każdy kod, który jest wysoce zależny od dokładnej semantyki produkowanych śladów, może wymagać pewnej modyfikacji. To zawiera:

  • Kod oparty na nazwach operatorów i tensorów
  • Kod odwołujący się do tensorów utworzonych w gałęzi przepływu sterowania TensorFlow spoza tej gałęzi. Może to spowodować InaccessibleTensorError

Ta zmiana zachowania ma na celu zapewnienie wydajności neutralnej lub pozytywnej, ale jeśli napotkasz problem, w którym przepływ sterowania v2 działa gorzej niż przepływ sterowania TF1.x, zgłoś problem z etapami odtwarzania.

Zmiany w zachowaniu API TensorShape

Klasa TensorShape została uproszczona do przechowywania obiektów typu int zamiast obiektów tf.compat.v1.Dimension . Nie ma więc potrzeby wywoływania .value , aby uzyskać int .

Poszczególne obiekty tf.compat.v1.Dimension są nadal dostępne z tf.TensorShape.dims .

Aby wyizolować wpływ tej zmiany zachowania na kod, możesz użyć tf.compat.v1.disable_v2_tensorshape() i tf.compat.v1.enable_v2_tensorshape() , aby globalnie wyłączyć lub włączyć tę zmianę zachowania.

Poniżej przedstawiono różnice między TF1.x i 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])

Gdybyś miał to w TF1.x:

value = shape[i].value

Następnie zrób to w TF2:

value = shape[i]
value
16

Gdybyś miał to w TF1.x:

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

Następnie zrób to w TF2:

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

Jeśli miałeś to w TF1.x (lub użyłeś innej metody wymiarowania):

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

Następnie zrób to w 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

Wartość logiczna tf.TensorShape to True , jeśli ranga jest znana, False w przeciwnym razie.

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

Potencjalne błędy spowodowane zmianami TensorShape

Zmiany zachowania TensorShape raczej nie spowodują dyskretnego złamania kodu. Jednak możesz zobaczyć, że kod związany z kształtem zaczyna podnosić AttributeError s, ponieważ int i None nie mają tych samych atrybutów, co tf.compat.v1.Dimension . Poniżej znajduje się kilka przykładów takich 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'

Równość tensora według wartości

Binarne operatory == i != na zmiennych i tensorach zostały zmienione tak, aby porównywać według wartości w TF2 zamiast porównywania według odniesienia do obiektu, jak w TF1.x. Ponadto tensory i zmienne nie są już bezpośrednio haszowane ani używane w zestawach lub kluczach dyktujących, ponieważ może nie być możliwe ich haszowanie według wartości. Zamiast tego udostępniają metodę .ref() , której można użyć, aby uzyskać haszujące odwołanie do tensora lub zmiennej.

Aby wyizolować wpływ tej zmiany zachowania, możesz użyć tf.compat.v1.disable_tensor_equality() i tf.compat.v1.enable_tensor_equality() , aby globalnie wyłączyć lub włączyć tę zmianę zachowania.

Na przykład w TF1.x dwie zmienne o tej samej wartości zwrócą fałsz, gdy użyjesz operatora == :

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

x == y
False

Podczas gdy w TF2 z włączonym sprawdzaniem równości tensorów, x == y zwróci 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>

Tak więc w TF2, jeśli chcesz porównać według odniesienia do obiektu, upewnij się, że używasz i is not is

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

x is y
False

Tensory i zmienne haszujące

Zachowania TF1.x umożliwiały bezpośrednie dodawanie zmiennych i tensorów do struktur danych wymagających mieszania, takich jak klawisze set i 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>}

Jednak w TF2 z włączoną równością tensorów, tensory i zmienne stają się niemożliwe do zaszyfrowania ze względu na semantykę operatorów == i != zmieniającą się na sprawdzanie równości wartości.

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.

Tak więc w TF2, jeśli potrzebujesz użyć obiektów tensor lub zmiennych jako kluczy lub set zawartość, możesz użyć tensor.ref() , aby uzyskać haszowalną referencję, która może być użyta jako klucz:

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

W razie potrzeby można również uzyskać tensor lub zmienną z referencji za pomocą reference.deref() :

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

Zasoby i dalsze czytanie

  • Odwiedź sekcję Migracja do TF2 , aby dowiedzieć się więcej o migracji do TF2 z TF1.x.
  • Przeczytaj przewodnik po mapowaniu modeli, aby dowiedzieć się więcej o mapowaniu modeli TF1.x do bezpośredniej pracy w TF2.