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 ustawieniapersistent=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')
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.Operation
są zarejestrowane 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)