Pomoc chronić Wielkiej Rafy Koralowej z TensorFlow na Kaggle Dołącz Wyzwanie

Zaawansowane automatyczne różnicowanie

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

Wprowadzenie do gradienty i automatycznego różnicowania przewodnik zawiera wszystko, co niezbędne do gradientów Obliczyć TensorFlow. Podręcznik ten skupia się na głębsze, mniej typowych cech tf.GradientTape API.

Ustawiać

import tensorflow as tf

import matplotlib as mpl
import matplotlib.pyplot as plt

mpl.rcParams['figure.figsize'] = (8, 6)

Kontrolowanie nagrywania gradientu

W automatycznym przewodnikiem różnicowania obejrzałeś jak kontrolować, które zmienne i tensory oglądają taśmę budując obliczenia gradientu.

Taśma ma również metody manipulacji nagraniem.

Zatrzymaj nagrywanie

Jeśli chcesz, aby zatrzymać nagrywanie gradienty, można użyć tf.GradientTape.stop_recording tymczasowo wstrzymać nagrywanie.

Może to być przydatne w celu zmniejszenia narzutu, jeśli nie chcesz rozróżniać skomplikowanej operacji w środku modelu. Może to obejmować obliczenie metryki lub wyniku pośredniego:

x = tf.Variable(2.0)
y = tf.Variable(3.0)

with tf.GradientTape() as t:
  x_sq = x * x
  with t.stop_recording():
    y_sq = y * y
  z = x_sq + y_sq

grad = t.gradient(z, {'x': x, 'y': y})

print('dz/dx:', grad['x'])  # 2*x => 4
print('dz/dy:', grad['y'])
dz/dx: tf.Tensor(4.0, shape=(), dtype=float32)
dz/dy: None

Zresetuj/rozpocznij nagrywanie od zera

Jeśli chcesz zacząć od początku do końca, należy tf.GradientTape.reset . Wystarczy wyjściu bloku taśmy gradientu i ponownym uruchomieniu jest zwykle łatwiejsze do odczytania, ale można użyć reset metodę przy wyjściu bloku taśma jest trudne lub niemożliwe.

x = tf.Variable(2.0)
y = tf.Variable(3.0)
reset = True

with tf.GradientTape() as t:
  y_sq = y * y
  if reset:
    # Throw out all the tape recorded so far.
    t.reset()
  z = x * x + y_sq

grad = t.gradient(z, {'x': x, 'y': y})

print('dz/dx:', grad['x'])  # 2*x => 4
print('dz/dy:', grad['y'])
dz/dx: tf.Tensor(4.0, shape=(), dtype=float32)
dz/dy: None

Zatrzymaj przepływ gradientu z precyzją

W przeciwieństwie do globalnej kontroli taśmowych przedstawiono powyżej, tf.stop_gradient funkcja jest znacznie bardziej precyzyjne. Może być używany do zatrzymania gradientów płynących po określonej ścieżce, bez konieczności dostępu do samej taśmy:

x = tf.Variable(2.0)
y = tf.Variable(3.0)

with tf.GradientTape() as t:
  y_sq = y**2
  z = x**2 + tf.stop_gradient(y_sq)

grad = t.gradient(z, {'x': x, 'y': y})

print('dz/dx:', grad['x'])  # 2*x => 4
print('dz/dy:', grad['y'])
dz/dx: tf.Tensor(4.0, shape=(), dtype=float32)
dz/dy: None

Gradienty niestandardowe

W niektórych przypadkach możesz chcieć dokładnie kontrolować sposób obliczania gradientów, zamiast używać wartości domyślnych. Sytuacje te obejmują:

  1. Nie ma zdefiniowanego gradientu dla nowej operacji, którą piszesz.
  2. Obliczenia domyślne są numerycznie niestabilne.
  3. Chcesz buforować kosztowne obliczenia z przejścia w przód.
  4. Chcesz zmodyfikować wartość (na przykład za pomocą tf.clip_by_value lub tf.math.round ) bez modyfikowania gradient.

W pierwszym przypadku, aby napisać nowy op można użyć tf.RegisterGradient założyć własną rękę (patrz docs API dla szczegółów). (Pamiętaj, że rejestr gradientów jest globalny, więc zmieniaj go ostrożnie).

Dla tych trzech przypadkach można użyć tf.custom_gradient .

Poniżej przedstawiono przykład, w którym stosuje się tf.clip_by_norm do pośredniego gradient:

# Establish an identity operation, but clip during the gradient pass.
@tf.custom_gradient
def clip_gradients(y):
  def backward(dy):
    return tf.clip_by_norm(dy, 0.5)
  return y, backward

v = tf.Variable(2.0)
with tf.GradientTape() as t:
  output = clip_gradients(v * v)
print(t.gradient(output, v))  # calls "backward", which clips 4 to 2
tf.Tensor(2.0, shape=(), dtype=float32)

Odnoszą się do tf.custom_gradient docs dekorator API więcej szczegółów.

Gradienty niestandardowe w SavedModel

Gradienty niestandardowe mogą być zapisywane SavedModel za pomocą opcji tf.saved_model.SaveOptions(experimental_custom_gradients=True) .

Zostać zapisane w SavedModel funkcja gradientu muszą być identyfikowalne (aby dowiedzieć się więcej, sprawdź wydajność lepiej tf.function przewodnika).

class MyModule(tf.Module):

  @tf.function(input_signature=[tf.TensorSpec(None)])
  def call_custom_grad(self, x):
    return clip_gradients(x)

model = MyModule()
tf.saved_model.save(
    model,
    'saved_model',
    options=tf.saved_model.SaveOptions(experimental_custom_gradients=True))

# The loaded gradients will be the same as the above example.
v = tf.Variable(2.0)
loaded = tf.saved_model.load('saved_model')
with tf.GradientTape() as t:
  output = loaded.call_custom_grad(v * v)
print(t.gradient(output, v))
INFO:tensorflow:Assets written to: saved_model/assets
tf.Tensor(2.0, shape=(), dtype=float32)

Uwaga na temat powyższego przykładu: Jeśli spróbuj wymienić powyższy kod z tf.saved_model.SaveOptions(experimental_custom_gradients=False) , gradient będzie nadal produkować ten sam wynik na załadunek. Powodem jest to, że rejestr gradientu nadal zawiera niestandardową gradientu używany w funkcji call_custom_op . Jednak po ponownym uruchomieniu czas pracy po zapisaniu bez niestandardowych gradientów, uruchamiając załadowany modelu pod tf.GradientTape rzuci błąd: LookupError: No gradient defined for operation 'IdentityN' (op type: IdentityN) .

Wiele taśm

Wiele taśm współpracuje bezproblemowo.

Na przykład tutaj każda taśma ogląda inny zestaw tensorów:

x0 = tf.constant(0.0)
x1 = tf.constant(0.0)

with tf.GradientTape() as tape0, tf.GradientTape() as tape1:
  tape0.watch(x0)
  tape1.watch(x1)

  y0 = tf.math.sin(x0)
  y1 = tf.nn.sigmoid(x1)

  y = y0 + y1

  ys = tf.reduce_sum(y)
tape0.gradient(ys, x0).numpy()   # cos(x) => 1.0
1.0
tape1.gradient(ys, x1).numpy()   # sigmoid(x1)*(1-sigmoid(x1)) => 0.25
0.25

Gradienty wyższego rzędu

Operacje wewnątrz z tf.GradientTape kierownika kontekstowego zapisywane są do automatycznego różnicowania. Jeśli gradienty są obliczane w tym kontekście, obliczanie gradientu jest również rejestrowane. W rezultacie dokładnie to samo API działa również dla gradientów wyższego rzędu.

Na przykład:

x = tf.Variable(1.0)  # Create a Tensorflow variable initialized to 1.0

with tf.GradientTape() as t2:
  with tf.GradientTape() as t1:
    y = x * x * x

  # Compute the gradient inside the outer `t2` context manager
  # which means the gradient computation is differentiable as well.
  dy_dx = t1.gradient(y, x)
d2y_dx2 = t2.gradient(dy_dx, x)

print('dy_dx:', dy_dx.numpy())  # 3 * x**2 => 3.0
print('d2y_dx2:', d2y_dx2.numpy())  # 6 * x => 6.0
dy_dx: 3.0
d2y_dx2: 6.0

Mimo, że nie daje drugą pochodną funkcji skalarnej ten schemat nie uogólnić do wytworzenia Hessian matrycy, ponieważ tf.GradientTape.gradient oblicza tylko gradient skalarnej. Skonstruować Hessian matrycy , należy przejść do Heskiego przykład pod sekcji Jacobiego .

„Zagnieżdżone wywołania tf.GradientTape.gradient ” to dobry wzór podczas obliczania skalarne z gradientem, a następnie uzyskany skalar działa jako źródło drugiego obliczania gradientu, jak w poniższym przykładzie.

Przykład: regularyzacja gradientu wejściowego

Wiele modeli jest podatnych na „przykłady kontradyktoryjne”. Ten zbiór technik modyfikuje dane wejściowe modelu, aby pomylić dane wyjściowe modelu. Najprostszym implementacji takich jak kontradyktoryjności przykład stosując Fast Gradient Signed atak Method -takes jeden krok wzdłuż gradientu wyjścia w odniesieniu do wejścia; „gradient wejściowy”.

Jedną z technik w celu zwiększenia odporności na antagonistycznych przykładów jest wejście regularyzacji gradientu (Finlay i Oberman, 2019), co próby zminimalizowania wielkości gradientu sygnału. Jeśli gradient wejściowy jest mały, to zmiana wyjścia również powinna być niewielka.

Poniżej znajduje się naiwna implementacja regularyzacji gradientu wejściowego. Realizacja to:

  1. Oblicz gradient wyjścia w stosunku do wejścia za pomocą wewnętrznej taśmy.
  2. Oblicz wielkość tego gradientu wejściowego.
  3. Oblicz gradient tej wielkości w odniesieniu do modelu.
x = tf.random.normal([7, 5])

layer = tf.keras.layers.Dense(10, activation=tf.nn.relu)
with tf.GradientTape() as t2:
  # The inner tape only takes the gradient with respect to the input,
  # not the variables.
  with tf.GradientTape(watch_accessed_variables=False) as t1:
    t1.watch(x)
    y = layer(x)
    out = tf.reduce_sum(layer(x)**2)
  # 1. Calculate the input gradient.
  g1 = t1.gradient(out, x)
  # 2. Calculate the magnitude of the input gradient.
  g1_mag = tf.norm(g1)

# 3. Calculate the gradient of the magnitude with respect to the model.
dg1_mag = t2.gradient(g1_mag, layer.trainable_variables)
[var.shape for var in dg1_mag]
[TensorShape([5, 10]), TensorShape([10])]

Jakobian

We wszystkich poprzednich przykładach wzięto gradienty celu skalarnego w odniesieniu do niektórych tensorów źródłowych.

Jakobian macierzy reprezentuje gradienty funkcji wektora cenione. Każdy wiersz zawiera gradient jednego z elementów wektora.

tf.GradientTape.jacobian metoda pozwala skutecznie obliczyć macierz Jacobiego.

Zwróć uwagę, że:

  • Jak gradient : THE sources argument może być tensora lub pojemnik tensorów.
  • W przeciwieństwie do gradient : the target tensor musi być pojedynczym tensor.

Źródło skalarne

Jako pierwszy przykład, oto jakobian celu wektorowego w odniesieniu do źródła skalarnego.

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

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

dy_dx = tape.jacobian(y, delta)

Jeśli wziąć Jacobiego w odniesieniu do skalara wynik ma kształt tarczy, i daje gradient każdego elementu w stosunku do źródła:

print(y.shape)
print(dy_dx.shape)
(201,)
(201,)
plt.plot(x.numpy(), y, label='y')
plt.plot(x.numpy(), dy_dx, label='dy/dx')
plt.legend()
_ = plt.xlabel('x')

png

Źródło tensora

Czy wkład jest skalar lub tensora tf.GradientTape.jacobian skutecznie oblicza nachylenie każdego elementu źródła w odniesieniu do każdego elementu docelowego (ych).

Na przykład, to wyjście tej warstwy ma kształt (10, 7) :

x = tf.random.normal([7, 5])
layer = tf.keras.layers.Dense(10, activation=tf.nn.relu)

with tf.GradientTape(persistent=True) as tape:
  y = layer(x)

y.shape
TensorShape([7, 10])

I kształt warstwy w jądrze jest (5, 10) :

layer.kernel.shape
TensorShape([5, 10])

Kształt jakobianu wyjścia w odniesieniu do jądra to te dwa połączone ze sobą kształty:

j = tape.jacobian(y, layer.kernel)
j.shape
TensorShape([7, 10, 5, 10])

Jeśli suma ponad wymiary docelowego, jesteś w lewo z gradientem kwoty, która zostałaby obliczona przez tf.GradientTape.gradient :

g = tape.gradient(y, layer.kernel)
print('g.shape:', g.shape)

j_sum = tf.reduce_sum(j, axis=[0, 1])
delta = tf.reduce_max(abs(g - j_sum)).numpy()
assert delta < 1e-3
print('delta:', delta)
g.shape: (5, 10)
delta: 2.3841858e-07

Przykład: Heski

Choć tf.GradientTape nie daje jednoznacznej metody konstruowania Macierz Hessego to możliwe, aby zbudować jeden pomocą tf.GradientTape.jacobian metody.

x = tf.random.normal([7, 5])
layer1 = tf.keras.layers.Dense(8, activation=tf.nn.relu)
layer2 = tf.keras.layers.Dense(6, activation=tf.nn.relu)

with tf.GradientTape() as t2:
  with tf.GradientTape() as t1:
    x = layer1(x)
    x = layer2(x)
    loss = tf.reduce_mean(x**2)

  g = t1.gradient(loss, layer1.kernel)

h = t2.jacobian(g, layer1.kernel)
print(f'layer.kernel.shape: {layer1.kernel.shape}')
print(f'h.shape: {h.shape}')
layer.kernel.shape: (5, 8)
h.shape: (5, 8, 5, 8)

Aby skorzystać z tej Hesji Przez metodę Newtona kroku, byś najpierw spłaszczyć swoje osie do matrycy i wygładzić gradient do wektora:

n_params = tf.reduce_prod(layer1.kernel.shape)

g_vec = tf.reshape(g, [n_params, 1])
h_mat = tf.reshape(h, [n_params, n_params])

Macierz Hesja powinna być symetryczna:

def imshow_zero_center(image, **kwargs):
  lim = tf.reduce_max(abs(image))
  plt.imshow(image, vmin=-lim, vmax=lim, cmap='seismic', **kwargs)
  plt.colorbar()
imshow_zero_center(h_mat)

png

Krok aktualizacji metody Newtona pokazano poniżej:

eps = 1e-3
eye_eps = tf.eye(h_mat.shape[0])*eps
# X(k+1) = X(k) - (∇²f(X(k)))^-1 @ ∇f(X(k))
# h_mat = ∇²f(X(k))
# g_vec = ∇f(X(k))
update = tf.linalg.solve(h_mat + eye_eps, g_vec)

# Reshape the update and apply it to the variable.
_ = layer1.kernel.assign_sub(tf.reshape(update, layer1.kernel.shape))

Chociaż jest to stosunkowo proste dla pojedynczego tf.Variable , stosując do tego nietrywialne modelu wymaga starannego konkatenacji i krojenia w celu uzyskania pełnej Hesji w wielu zmiennych.

Partia Jakobian

W niektórych przypadkach chcesz wziąć jakobian każdego ze stosu celów w odniesieniu do stosu źródeł, gdzie jakobiany dla każdej pary cel-źródło są niezależne.

Na przykład tutaj wprowadzić x ma kształt (batch, ins) , a wyjście y ma kształt (batch, outs) :

x = tf.random.normal([7, 5])

layer1 = tf.keras.layers.Dense(8, activation=tf.nn.elu)
layer2 = tf.keras.layers.Dense(6, activation=tf.nn.elu)

with tf.GradientTape(persistent=True, watch_accessed_variables=False) as tape:
  tape.watch(x)
  y = layer1(x)
  y = layer2(y)

y.shape
TensorShape([7, 6])

Pełne Jacobiego z y względem x ma kształt (batch, ins, batch, outs) , nawet jeśli tylko chcesz (batch, ins, outs) :

j = tape.jacobian(y, x)
j.shape
TensorShape([7, 6, 7, 5])

Jeśli nachylenie każdej pozycji w stosie są niezależne, to każda (batch, batch) kawałek tego tensora jest macierzą diagonalną:

imshow_zero_center(j[:, 0, :, 0])
_ = plt.title('A (batch, batch) slice')

png

def plot_as_patches(j):
  # Reorder axes so the diagonals will each form a contiguous patch.
  j = tf.transpose(j, [1, 0, 3, 2])
  # Pad in between each patch.
  lim = tf.reduce_max(abs(j))
  j = tf.pad(j, [[0, 0], [1, 1], [0, 0], [1, 1]],
             constant_values=-lim)
  # Reshape to form a single image.
  s = j.shape
  j = tf.reshape(j, [s[0]*s[1], s[2]*s[3]])
  imshow_zero_center(j, extent=[-0.5, s[2]-0.5, s[0]-0.5, -0.5])

plot_as_patches(j)
_ = plt.title('All (batch, batch) slices are diagonal')

png

Aby uzyskać pożądany efekt, można podsumować ponad duplikat batch wymiar, albo wybrać przekątnych użyciu tf.einsum :

j_sum = tf.reduce_sum(j, axis=2)
print(j_sum.shape)
j_select = tf.einsum('bxby->bxy', j)
print(j_select.shape)
(7, 6, 5)
(7, 6, 5)

O wiele bardziej efektywne byłoby wykonanie obliczeń bez dodatkowego wymiaru. tf.GradientTape.batch_jacobian metoda robi dokładnie to:

jb = tape.batch_jacobian(y, x)
jb.shape
WARNING:tensorflow:5 out of the last 5 calls to <function pfor.<locals>.f at 0x7f7d601250e0> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has experimental_relax_shapes=True option that relaxes argument shapes that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for  more details.
TensorShape([7, 6, 5])
error = tf.reduce_max(abs(jb - j_sum))
assert error < 1e-3
print(error.numpy())
0.0
x = tf.random.normal([7, 5])

layer1 = tf.keras.layers.Dense(8, activation=tf.nn.elu)
bn = tf.keras.layers.BatchNormalization()
layer2 = tf.keras.layers.Dense(6, activation=tf.nn.elu)

with tf.GradientTape(persistent=True, watch_accessed_variables=False) as tape:
  tape.watch(x)
  y = layer1(x)
  y = bn(y, training=True)
  y = layer2(y)

j = tape.jacobian(y, x)
print(f'j.shape: {j.shape}')
WARNING:tensorflow:6 out of the last 6 calls to <function pfor.<locals>.f at 0x7f7cf062fa70> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has experimental_relax_shapes=True option that relaxes argument shapes that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for  more details.
j.shape: (7, 6, 7, 5)
plot_as_patches(j)

_ = plt.title('These slices are not diagonal')
_ = plt.xlabel("Don't use `batch_jacobian`")

png

W tym przypadku, batch_jacobian nadal trwa i powraca coś z oczekiwanego kształtu, ale jego zawartość mają niejasne znaczenie:

jb = tape.batch_jacobian(y, x)
print(f'jb.shape: {jb.shape}')
jb.shape: (7, 6, 5)