Wprowadzenie do gradientów i automatycznego różniczkowania

Zobacz na TensorFlow.org Uruchom w Google Colab Wyświetl źródło na GitHub Pobierz notatnik

Automatyczne różnicowanie i gradienty

Automatyczne różnicowanie jest przydatne do implementacji algorytmów uczenia maszynowego, takich jak wsteczna propagacja , do uczenia sieci neuronowych.

W tym przewodniku poznasz sposoby obliczania gradientów za pomocą TensorFlow, zwłaszcza w przypadku szybkiego wykonywania .

Ustawiać

import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf

Obliczanie gradientów

Aby rozróżniać automatycznie, TensorFlow musi pamiętać, jakie operacje mają miejsce w jakiej kolejności podczas przejścia do przodu . Następnie, podczas przechodzenia wstecznego , TensorFlow przemierza tę listę operacji w odwrotnej kolejności, aby obliczyć gradienty.

Taśmy gradientowe

TensorFlow udostępnia interfejs API tf.GradientTape do automatycznego różnicowania; czyli obliczanie gradientu obliczeń w odniesieniu do niektórych danych wejściowych, zwykle tf.Variable s. TensorFlow „zapisuje” odpowiednie operacje wykonywane w kontekście tf.GradientTape na „taśmie”. TensorFlow używa następnie tej taśmy do obliczenia gradientów „zarejestrowanego” obliczenia przy użyciu różnicowania w trybie odwrotnym .

Oto prosty przykład:

x = tf.Variable(3.0)

with tf.GradientTape() as tape:
  y = x**2

Po zarejestrowaniu niektórych operacji użyj GradientTape.gradient(target, sources) aby obliczyć gradient pewnego celu (często straty) względem pewnego źródła (często zmiennych modelu):

# dy = 2x * dx
dy_dx = tape.gradient(y, x)
dy_dx.numpy()
6.0

Powyższy przykład używa skalarów, ale tf.GradientTape działa równie łatwo na dowolnym tensorze:

w = tf.Variable(tf.random.normal((3, 2)), name='w')
b = tf.Variable(tf.zeros(2, dtype=tf.float32), name='b')
x = [[1., 2., 3.]]

with tf.GradientTape(persistent=True) as tape:
  y = x @ w + b
  loss = tf.reduce_mean(y**2)

Aby uzyskać gradient loss w odniesieniu do obu zmiennych, możesz przekazać obie jako źródła do metody gradient . Taśma jest elastyczna jeśli chodzi o sposób przekazywania źródeł i zaakceptuje każdą zagnieżdżoną kombinację list lub słowników i zwróci gradient o takiej samej strukturze (zobacz tf.nest ).

[dl_dw, dl_db] = tape.gradient(loss, [w, b])

Gradient w odniesieniu do każdego źródła ma kształt źródła:

print(w.shape)
print(dl_dw.shape)
(3, 2)
(3, 2)

Oto znowu obliczanie gradientu, tym razem z podaniem słownika zmiennych:

my_vars = {
    'w': w,
    'b': b
}

grad = tape.gradient(loss, my_vars)
grad['b']
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([-1.6920902, -3.2363236], dtype=float32)>

Gradienty względem modelu

Powszechne jest zbieranie tf.Variables do tf.Module lub jednej z jej podklas ( layers.Layer , keras.Model ) w celu wskazywania punktów kontrolnych i eksportowania .

W większości przypadków będziesz chciał obliczyć gradienty w odniesieniu do trenowalnych zmiennych modelu. Ponieważ wszystkie podklasy tf.Module agregują swoje zmienne we właściwości Module.trainable_variables , możesz obliczyć te gradienty w kilku linijkach kodu:

layer = tf.keras.layers.Dense(2, activation='relu')
x = tf.constant([[1., 2., 3.]])

with tf.GradientTape() as tape:
  # Forward pass
  y = layer(x)
  loss = tf.reduce_mean(y**2)

# Calculate gradients with respect to every trainable variable
grad = tape.gradient(loss, layer.trainable_variables)
for var, g in zip(layer.trainable_variables, grad):
  print(f'{var.name}, shape: {g.shape}')
dense/kernel:0, shape: (3, 2)
dense/bias:0, shape: (2,)

Kontrolowanie tego, co ogląda taśma

Domyślnym zachowaniem jest rejestrowanie wszystkich operacji po uzyskaniu dostępu do możliwej do trenowania tf.Variable . Powody tego są następujące:

  • Taśma musi wiedzieć, jakie operacje zapisać w przejściu do przodu, aby obliczyć gradienty w przejściu do tyłu.
  • Na taśmie znajdują się odniesienia do wyjść pośrednich, więc nie chcesz rejestrować niepotrzebnych operacji.
  • Najczęstszym przypadkiem użycia jest obliczenie gradientu straty w odniesieniu do wszystkich możliwych do trenowania zmiennych modelu.

Na przykład w poniższym przykładzie nie można obliczyć gradientu, ponieważ tf.Tensor nie jest domyślnie „obserwowany”, a tf.Variable nie można wytrenować:

# A trainable variable
x0 = tf.Variable(3.0, name='x0')
# Not trainable
x1 = tf.Variable(3.0, name='x1', trainable=False)
# Not a Variable: A variable + tensor returns a tensor.
x2 = tf.Variable(2.0, name='x2') + 1.0
# Not a variable
x3 = tf.constant(3.0, name='x3')

with tf.GradientTape() as tape:
  y = (x0**2) + (x1**2) + (x2**2)

grad = tape.gradient(y, [x0, x1, x2, x3])

for g in grad:
  print(g)
tf.Tensor(6.0, shape=(), dtype=float32)
None
None
None

Możesz wyświetlić listę zmiennych obserwowanych przez taśmę za pomocą metody GradientTape.watched_variables :

[var.name for var in tape.watched_variables()]
['x0:0']

tf.GradientTape udostępnia punkty zaczepienia, które dają użytkownikowi kontrolę nad tym, co jest oglądane, a co nie.

Aby zarejestrować gradienty względem tf.Tensor , musisz wywołać GradientTape.watch(x) :

x = tf.constant(3.0)
with tf.GradientTape() as tape:
  tape.watch(x)
  y = x**2

# dy = 2x * dx
dy_dx = tape.gradient(y, x)
print(dy_dx.numpy())
6.0

I odwrotnie, aby wyłączyć domyślne zachowanie oglądania wszystkich tf.Variables , ustaw watch_accessed_variables=False podczas tworzenia taśmy gradientowej. To obliczenie wykorzystuje dwie zmienne, ale łączy gradient tylko dla jednej ze zmiennych:

x0 = tf.Variable(0.0)
x1 = tf.Variable(10.0)

with tf.GradientTape(watch_accessed_variables=False) as tape:
  tape.watch(x1)
  y0 = tf.math.sin(x0)
  y1 = tf.nn.softplus(x1)
  y = y0 + y1
  ys = tf.reduce_sum(y)

Ponieważ GradientTape.watch nie został wywołany na x0 , nie jest obliczany gradient względem niego:

# dys/dx1 = exp(x1) / (1 + exp(x1)) = sigmoid(x1)
grad = tape.gradient(ys, {'x0': x0, 'x1': x1})

print('dy/dx0:', grad['x0'])
print('dy/dx1:', grad['x1'].numpy())
dy/dx0: None
dy/dx1: 0.9999546

Wyniki pośrednie

Można również zażądać gradientów danych wyjściowych w odniesieniu do wartości pośrednich obliczonych w kontekście tf.GradientTape .

x = tf.constant(3.0)

with tf.GradientTape() as tape:
  tape.watch(x)
  y = x * x
  z = y * y

# Use the tape to compute the gradient of z with respect to the
# intermediate value y.
# dz_dy = 2 * y and y = x ** 2 = 9
print(tape.gradient(z, y).numpy())
18.0

Domyślnie zasoby przechowywane przez GradientTape są zwalniane zaraz po wywołaniu metody GradientTape.gradient . Aby obliczyć wiele gradientów w ramach tego samego obliczenia, utwórz taśmę gradientu z ustawieniem persistent=True . Pozwala to na wiele wywołań metody gradient , gdy zasoby są zwalniane, gdy obiekt taśmy jest zbierany bezużytecznie. Na przykład:

x = tf.constant([1, 3.0])
with tf.GradientTape(persistent=True) as tape:
  tape.watch(x)
  y = x * x
  z = y * y

print(tape.gradient(z, x).numpy())  # [4.0, 108.0] (4 * x**3 at x = [1.0, 3.0])
print(tape.gradient(y, x).numpy())  # [2.0, 6.0] (2 * x at x = [1.0, 3.0])
[  4. 108.]
[2. 6.]
del tape   # Drop the reference to the tape

Uwagi dotyczące wydajności

  • Wykonywanie operacji w kontekście taśmy gradientowej wiąże się z niewielkim obciążeniem. Dla większości chętnych do wykonania nie będzie to zauważalny koszt, ale nadal powinieneś używać kontekstu taśmy wokół obszarów tylko tam, gdzie jest to wymagane.

  • Taśmy gradientowe wykorzystują pamięć do przechowywania wyników pośrednich, w tym danych wejściowych i wyjściowych, do wykorzystania podczas przejścia wstecznego.

    Aby zwiększyć wydajność, niektóre operacje (takie jak ReLU ) nie muszą zachowywać swoich wyników pośrednich i są przycinane podczas podania do przodu. Jeśli jednak użyjesz na taśmie ustawienia persistent=True , nic nie zostanie odrzucone , a szczytowe użycie pamięci będzie wyższe.

Gradienty celów nieskalarnych

Gradient jest zasadniczo operacją na skalarze.

x = tf.Variable(2.0)
with tf.GradientTape(persistent=True) as tape:
  y0 = x**2
  y1 = 1 / x

print(tape.gradient(y0, x).numpy())
print(tape.gradient(y1, x).numpy())
4.0
-0.25

Tak więc, jeśli poprosisz o gradient wielu celów, wynik dla każdego źródła to:

  • Gradient sumy celów lub równoważnie
  • Suma gradientów każdego celu.
x = tf.Variable(2.0)
with tf.GradientTape() as tape:
  y0 = x**2
  y1 = 1 / x

print(tape.gradient({'y0': y0, 'y1': y1}, x).numpy())
3.75

Podobnie, jeśli cele nie są skalarne, obliczany jest gradient sumy:

x = tf.Variable(2.)

with tf.GradientTape() as tape:
  y = x * [3., 4.]

print(tape.gradient(y, x).numpy())
7.0

Ułatwia to obliczenie gradientu sumy zbioru strat lub gradientu sumy obliczeń strat z uwzględnieniem elementów.

Jeśli potrzebujesz osobnego gradientu dla każdego elementu, zapoznaj się z Jakobianami .

W niektórych przypadkach możesz pominąć jakobian. W obliczeniach uwzględniających elementy, gradient sumy daje pochodną każdego elementu w odniesieniu do jego elementu wejściowego, ponieważ każdy element jest niezależny:

x = tf.linspace(-10.0, 10.0, 200+1)

with tf.GradientTape() as tape:
  tape.watch(x)
  y = tf.nn.sigmoid(x)

dy_dx = tape.gradient(y, x)
plt.plot(x, y, label='y')
plt.plot(x, dy_dx, label='dy/dx')
plt.legend()
_ = plt.xlabel('x')

png

Kontrola przepływu

Ponieważ taśma gradientowa rejestruje operacje podczas ich wykonywania, przepływ sterowania w Pythonie jest naturalnie obsługiwany (na przykład instrukcje if i while ).

Tutaj inna zmienna jest używana w każdej gałęzi if . Gradient łączy się tylko ze zmienną, która została użyta:

x = tf.constant(1.0)

v0 = tf.Variable(2.0)
v1 = tf.Variable(2.0)

with tf.GradientTape(persistent=True) as tape:
  tape.watch(x)
  if x > 0.0:
    result = v0
  else:
    result = v1**2 

dv0, dv1 = tape.gradient(result, [v0, v1])

print(dv0)
print(dv1)
tf.Tensor(1.0, shape=(), dtype=float32)
None

Pamiętaj tylko, że same instrukcje sterujące nie są różniczkowalne, więc są niewidoczne dla optymalizatorów gradientowych.

W zależności od wartości x w powyższym przykładzie taśma zapisuje result = v0 lub result = v1**2 . Gradient względem x to zawsze None .

dx = tape.gradient(result, x)

print(dx)
None

Uzyskanie gradientu None

Kiedy cel nie jest połączony ze źródłem otrzymasz gradient None .

x = tf.Variable(2.)
y = tf.Variable(3.)

with tf.GradientTape() as tape:
  z = y * y
print(tape.gradient(z, x))
None

Tutaj z oczywiście nie jest połączone z x , ale istnieje kilka mniej oczywistych sposobów rozłączenia gradientu.

1. Zamieniłem zmienną na tensor

W sekcji "sterowanie tym, co ogląda taśma" widziałeś, że taśma będzie automatycznie oglądać tf.Variable ale nie tf.Tensor .

Jednym z typowych błędów jest nieumyślne zastąpienie tf.Variable tf.Tensor , zamiast używania Variable.assign do aktualizacji tf.Variable . Oto przykład:

x = tf.Variable(2.0)

for epoch in range(2):
  with tf.GradientTape() as tape:
    y = x+1

  print(type(x).__name__, ":", tape.gradient(y, x))
  x = x + 1   # This should be `x.assign_add(1)`
ResourceVariable : tf.Tensor(1.0, shape=(), dtype=float32)
EagerTensor : None

2. Czy obliczenia poza TensorFlow

Taśma nie może zarejestrować ścieżki gradientu, jeśli obliczenia wychodzą z TensorFlow. Na przykład:

x = tf.Variable([[1.0, 2.0],
                 [3.0, 4.0]], dtype=tf.float32)

with tf.GradientTape() as tape:
  x2 = x**2

  # This step is calculated with NumPy
  y = np.mean(x2, axis=0)

  # Like most ops, reduce_mean will cast the NumPy array to a constant tensor
  # using `tf.convert_to_tensor`.
  y = tf.reduce_mean(y, axis=0)

print(tape.gradient(y, x))
None

3. Wziął gradienty przez liczbę całkowitą lub ciąg

Liczby całkowite i łańcuchy nie są rozróżnialne. Jeśli ścieżka obliczeniowa wykorzystuje te typy danych, nie będzie gradientu.

Nikt nie oczekuje, że łańcuchy będą różniczkowalne, ale łatwo jest przypadkowo utworzyć stałą lub zmienną int , jeśli nie określisz dtype .

x = tf.constant(10)

with tf.GradientTape() as g:
  g.watch(x)
  y = x * x

print(g.gradient(y, x))
WARNING:tensorflow:The dtype of the watched tensor must be floating (e.g. tf.float32), got tf.int32
WARNING:tensorflow:The dtype of the target tensor must be floating (e.g. tf.float32) when calling GradientTape.gradient, got tf.int32
WARNING:tensorflow:The dtype of the source tensor must be floating (e.g. tf.float32) when calling GradientTape.gradient, got tf.int32
None

TensorFlow nie rzutuje automatycznie między typami, więc w praktyce często otrzymujesz błąd typu zamiast brakującego gradientu.

4. Wziął gradienty przez obiekt stanowy

Stan zatrzymuje gradienty. Kiedy czytasz z obiektu stanowego, taśma może obserwować tylko bieżący stan, a nie historię, która do niego prowadzi.

tf.Tensor jest niezmienny. Nie możesz zmienić tensora po jego utworzeniu. Ma wartość , ale nie ma stanu . Wszystkie omówione do tej pory operacje są również bezstanowe: dane wyjściowe tf.matmul zależą tylko od jego danych wejściowych.

tf.Variable ma stan wewnętrzny — swoją wartość. Kiedy używasz zmiennej, odczytywany jest stan. Normalne jest obliczanie gradientu w odniesieniu do zmiennej, ale stan zmiennej blokuje obliczenia gradientu przed cofnięciem się dalej. Na przykład:

x0 = tf.Variable(3.0)
x1 = tf.Variable(0.0)

with tf.GradientTape() as tape:
  # Update x1 = x1 + x0.
  x1.assign_add(x0)
  # The tape starts recording from x1.
  y = x1**2   # y = (x1 + x0)**2

# This doesn't work.
print(tape.gradient(y, x0))   #dy/dx0 = 2*(x1 + x0)
None

Podobnie iteratory tf.data.Dataset i tf.queue są stanowe i zatrzymują wszystkie gradienty na tensorach, które przez nie przechodzą.

Nie zarejestrowano gradientu

Niektóre tf.Operationzarejestrowane jako nieróżnicowalne i zwrócą None . Inne nie mają zarejestrowanego gradientu .

Strona tf.raw_ops pokazuje, które operacje niskiego poziomu mają zarejestrowane gradienty.

Jeśli spróbujesz pobrać gradient przez operację float, która nie ma zarejestrowanego gradientu, taśma zgłosi błąd zamiast po cichu zwracać None . W ten sposób wiesz, że coś poszło nie tak.

Na przykład funkcja tf.image.adjust_contrast raw_ops.AdjustContrastv2 , który może mieć gradient, ale gradient nie jest zaimplementowany:

image = tf.Variable([[[0.5, 0.0, 0.0]]])
delta = tf.Variable(0.1)

with tf.GradientTape() as tape:
  new_image = tf.image.adjust_contrast(image, delta)

try:
  print(tape.gradient(new_image, [image, delta]))
  assert False   # This should not happen.
except LookupError as e:
  print(f'{type(e).__name__}: {e}')
LookupError: gradient registry has no entry for: AdjustContrastv2

Jeśli potrzebujesz zróżnicować tę operację, będziesz musiał albo zaimplementować gradient i zarejestrować go (używając tf.RegisterGradient ) albo ponownie zaimplementować funkcję przy użyciu innych operacji.

Zera zamiast Brak

W niektórych przypadkach wygodnie byłoby uzyskać 0 zamiast None dla niepołączonych gradientów. Możesz zdecydować, co zwrócić, gdy masz niepołączone gradienty, używając argumentu unconnected_gradients :

x = tf.Variable([2., 2.])
y = tf.Variable(3.)

with tf.GradientTape() as tape:
  z = y**2
print(tape.gradient(z, x, unconnected_gradients=tf.UnconnectedGradients.ZERO))
tf.Tensor([0. 0.], shape=(2,), dtype=float32)