Dzień Społeczności ML jest 9 listopada! Dołącz do nas na aktualizacje z TensorFlow Jax i więcej Dowiedz się więcej

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 rozróżnianie jest przydatny dla realizacji maszynę algorytmy uczenia się, takich jak wstecznej propagacji błędów do szkolenia sieci neuronowych.

W tym przewodniku będzie badać sposoby obliczyć gradienty z TensorFlow, szczególnie chętnie wykonania .

Ustawiać

import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf

Obliczanie gradientów

Aby odróżnić automatycznie, TensorFlow musi pamiętać, co stało operacje w jakiej kolejności podczas piłkę z przodu. Następnie w kierunku do tyłu podaniu, TensorFlow przechodzi ten wykaz czynności w odwrotnej kolejności gradientów obliczeniowych.

Taśmy gradientowe

TensorFlow zapewnia tf.GradientTape API automatycznego różnicowania; czyli obliczania gradientu obliczeniach w odniesieniu do niektórych wejść, zwykle tf.Variable S. TensorFlow „Rejestry” odpowiednie operacje wykonywane wewnątrz kontekście tf.GradientTape wychodzą z „taśmy”. TensorFlow następnie wykorzystuje taśmę obliczenia gradienty o „rejestrowane” obliczeń z wykorzystaniem zróżnicowania Zanegowana .

Oto prosty przykład:

x = tf.Variable(3.0)

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

Gdy już zapisane pewne operacje, należy GradientTape.gradient(target, sources) obliczyć gradient jakimś celu (często straty) w stosunku do jakiegoś ź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 skalary, ale tf.GradientTape działa równie dobrze na każdej tensora:

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żna przejść zarówno jako źródeł do gradient metody. Taśma jest elastyczna, jak źródła są przekazywane i ponosi zagnieżdżony kombinacji list lub słowników i powrócić gradient strukturalnego w ten sam sposób (patrz 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

To wspólne zebrać tf.Variables w tf.Module lub jednej z jej podklas ( layers.Layer , keras.Model ) dla punktów kontrolnych i eksportu .

W większości przypadków będziesz chciał obliczyć gradienty w odniesieniu do trenowalnych zmiennych modelu. Ponieważ wszystkie podklasy tf.Module agregować swoje zmienne w Module.trainable_variables nieruchomości, można obliczyć te gradienty w kilku linii 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 wejściu nadającego tf.Variable . Powody tego to:

  • 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, co następuje nie obliczy gradient ponieważ tf.Tensor nie „patrzył” domyślnie, a tf.Variable nie jest wyszkolić:

# 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żna wymienić zmienne są obserwowani przez taśmy z wykorzystaniem GradientTape.watched_variables metodę:

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

tf.GradientTape zapewnia haki, które dają użytkownikowi kontrolę nad tym, co jest lub nie jest obserwowany.

Aby nagrać gradientów w odniesieniu do tf.Tensor , trzeba zadzwonić 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

Odwrotnie, aby wyłączyć domyślne zachowanie oglądając wszystkie tf.Variables , ustawiać watch_accessed_variables=False podczas tworzenia taśmę gradientu. 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)

Od GradientTape.watch nie padł na x0 , bez gradientu jest obliczana w odniesieniu do niej:

# 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ż poprosić gradienty wyjścia w odniesieniu do wartości pośrednich obliczanych wewnątrz tf.GradientTape kontekście.

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 posiadane przez GradientTape są uwalniane, gdy tylko GradientTape.gradient wywoływana jest metoda. Aby obliczyć wiele wzniesień w tym samym obliczeniu utworzyć taśmę gradientu z persistent=True . Pozwala to na wielokrotne połączenia do gradient sposobu jak zasoby są uwalniane, gdy obiekt jest taśma śmieci zebrane. 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.

    Dla zwiększenia efektywności niektóre ops (jak ReLU ) nie trzeba zachować ich wyników pośrednich i są usuwane podczas piłkę z przodu. Jednakże, jeśli używasz persistent=True na taśmie, nic nie jest odrzucany a zużycie pamięci szczyt będzie wyższa.

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 będzie następujący:

  • 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 potrzebny jest oddzielny gradientu dla każdej pozycji, patrz Jacobians .

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 gradientu rejestruje operacje, jak są one wykonywane, kontrola przepływu Python jest naturalnie obsługiwane (na przykład, if i while sprawozdania).

Oto inna zmienna jest stosowana na 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śmy albo rejestry result = v0 lub result = v1**2 . Nachylenie w zakresie x jest zawsze None .

dx = tape.gradient(result, x)

print(dx)
None

Pierwsze gradient None

Gdy tarcza nie jest podłączony do źródła dostaniesz 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 nie jest oczywiście połączony z x , ale istnieje wiele mniej oczywisty sposób, że nachylenie może być odłączony.

1. Zamieniłem zmienną na tensor

W części poświęconej „kontrolowanie co ogląda taśmę” obejrzałeś że taśma zostanie automatycznie oglądać tf.Variable ale nie tf.Tensor .

Jednym z typowych błędów jest nieumyślnie zastąpić tf.Variable z tf.Tensor , zamiast korzystania Variable.assign zaktualizować 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 spodziewa się, ciągi być różniczkowalne, ale łatwo jest przypadkowo utworzyć int stałej lub zmiennej, jeśli nie określić 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 niezmienna. Nie możesz zmienić tensora po jego utworzeniu. Ma wartość, ale nie państwo. Bezpaństwowcem są również wszystkie operacje omówione do tej pory: wyjście z tf.matmul zależy tylko od jego wejścia.

tf.Variable posiada wewnętrzną państwowości jego wartość. Gdy 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 tf.data.Dataset iteratory i tf.queue S są stanową i zatrzymuje wszystkie gradienty na tensorów, które przechodzą przez nie.

Nie zarejestrowano gradientu

Niektóre tf.Operation s są zarejestrowane jako non-różniczkowalna i zwróci None . Inni nie gradientu zarejestrowany.

tf.raw_ops stronie wyświetlone które ops niskopoziomowe mają gradienty zarejestrowanych.

Jeśli spróbujesz wziąć gradient przez op pływaka, który nie ma gradientu zarejestrowała taśma wygeneruje błąd zamiast cicho powrocie None . W ten sposób wiesz, że coś poszło nie tak.

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

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 trzeba odróżnić dzięki tej op, to albo trzeba zaimplementować gradientu i zarejestrować go (używając tf.RegisterGradient ) lub ponowne wdrożenie funkcji przy użyciu innych ops.

Zera zamiast Brak

W niektórych przypadkach byłoby wygodne, aby dostać 0 zamiast None dla niezwiązanych gradientów. Można zdecydować, co do powrotu, gdy masz niezwiązanych gradienty używając unconnected_gradients argumentu:

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)