Erweiterte automatische Unterscheidung

Auf TensorFlow.org ansehen In Google Colab ausführen Quelle auf GitHub anzeigen Notizbuch herunterladen

Die Einführung in die Steigungen und die automatische Differenzierung Führung enthält alles , was erforderlich ist, um berechnen Gradienten in TensorFlow. Dieser Leitfaden konzentriert sich auf tiefere, weniger gemeinsame Merkmale der tf.GradientTape API.

Aufstellen

pip uninstall tensorflow keras -y
pip install tf-nightly
import tensorflow as tf

import matplotlib as mpl
import matplotlib.pyplot as plt

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

Steuern der Verlaufsaufzeichnung

In der automatischen Differenzierung Führung sah , wie Sie zu steuern , welche Variablen und Tensoren vom Band beobachtet werden , während die Gradientenberechnungssystems zu bauen.

Das Band verfügt auch über Methoden, um die Aufnahme zu manipulieren.

Höre auf, aufzunehmen

Wenn Sie möchten die Aufnahme Steigungen stoppen, können Sie tf.GradientTape.stop_recording zur Aufnahme vorübergehend auszusetzen.

Dies kann nützlich sein, um den Overhead zu reduzieren, wenn Sie keine komplizierten Operationen in der Mitte Ihres Modells unterscheiden möchten. Dies könnte die Berechnung einer Metrik oder eines Zwischenergebnisses beinhalten:

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

Zurücksetzen/Aufnahme von Grund auf neu starten

Wenn Sie ganz von vorne beginnen möchten, verwenden Sie tf.GradientTape.reset . Einfach austritt , wird das Gefälle Bandblock und den Neustart in der Regel einfacher zu lesen, aber Sie können die Verwendung reset - Methode , wenn der Bandblock ist schwierig oder unmöglich zu verlassen.

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

Stoppen Sie den Gradientenfluss mit Präzision

Im Gegensatz zu den globalen Bandsteuerungen über die tf.stop_gradient ist Funktion viel präziser. Es kann verwendet werden, um zu verhindern, dass Gradienten entlang eines bestimmten Pfads fließen, ohne auf das Band selbst zugreifen zu müssen:

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

Benutzerdefinierte Farbverläufe

In einigen Fällen möchten Sie möglicherweise genau steuern, wie Farbverläufe berechnet werden, anstatt den Standardwert zu verwenden. Zu diesen Situationen gehören:

  1. Es gibt keinen definierten Farbverlauf für eine neue Op, die Sie schreiben.
  2. Die Standardberechnungen sind numerisch instabil.
  3. Sie möchten eine teure Berechnung aus dem Vorwärtsdurchlauf zwischenspeichern.
  4. Sie mögen einen Wert (zum Beispiel unter Verwendung von modifizieren tf.clip_by_value oder tf.math.round ) , ohne die Steigung zu verändern.

Für den ersten Fall, eine neue op schreiben Sie verwenden können tf.RegisterGradient Ihre eigene einrichten (siehe die API - Dokumentation für Details). (Beachten Sie, dass die Verlaufsregistrierung global ist, ändern Sie sie also mit Vorsicht.)

Für die drei letztgenannten Fällen können Sie verwenden tf.custom_gradient .

Hier ist ein Beispiel , das gilt tf.clip_by_norm auf das Zwischen 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)

Siehe die tf.custom_gradient Dekorateur API - Dokumentation für weitere Details.

Benutzerdefinierte Farbverläufe in SavedModel

Benutzerdefinierte Verläufe können mit Hilfe der Option SavedModel gespeichert werden tf.saved_model.SaveOptions(experimental_custom_gradients=True) .

Um in die SavedModel gespeichert zu werden, muss die Gradientenfunktion nachvollziehbar sein (um mehr zu erfahren, besuchen Sie das Bessere Leistung mit tf.function Führer).

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)

Ein Hinweis zu dem obigen Beispiel: Wenn Sie versuchen , den obigen Code mit dem Ersetzen tf.saved_model.SaveOptions(experimental_custom_gradients=False) , wird die Steigung immer noch das gleiche Ergebnis beim Laden produziert. Der Grund dafür ist , dass die Gradient Registrierung , um den benutzerdefinierten Verlauf enthält noch in der Funktion verwendet call_custom_op . Wenn Sie die Laufzeit nach dem Speichern ohne individuelle Steigungen jedoch neu starten, das geladene Modell unter dem laufenden tf.GradientTape den Fehler werfen: LookupError: No gradient defined for operation 'IdentityN' (op type: IdentityN) den LookupError: No gradient defined for operation 'IdentityN' (op type: IdentityN) .

Mehrere Bänder

Mehrere Bänder interagieren nahtlos.

Hier zum Beispiel beobachtet jedes Band einen anderen Satz von Tensoren:

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

Gradienten höherer Ordnung

Operationen innerhalb der tf.GradientTape Kontext - Manager sind für die automatische Differenzierung aufgezeichnet. Wenn in diesem Zusammenhang Gradienten berechnet werden, wird auch die Gradientenberechnung aufgezeichnet. Als Ergebnis funktioniert die exakt gleiche API auch für Gradienten höherer Ordnung.

Zum Beispiel:

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

Während , dass man die zweite Ableitung einer Skalarfunktion nicht geben, dieses Muster verallgemeinern keine Hesse - Matrix herzustellen, da tf.GradientTape.gradient nur den Gradienten einer skalaren berechnet. Um ein Konstrukt Hesse - Matrix , gehen Sie auf das hessische Beispiel unter dem Jacobi - Abschnitt .

„Nested auf Anrufe tf.GradientTape.gradient “ ist ein gutes Muster , wenn man einen skalaren von einem Gradienten werden die Berechnung und dann die sich ergebende skalare wirkt als eine Quelle für eine zweite Gradientenberechnungseinheit, wie in dem folgenden Beispiel.

Beispiel: Regularisierung des Eingabegradienten

Viele Modelle sind anfällig für „konträre Beispiele“. Diese Sammlung von Techniken modifiziert die Eingabe des Modells, um die Ausgabe des Modells zu verwirren. Die einfachste Implementierung wie das Adversarial Beispiel der Fast Gradient Signed Method Angriff unter Verwendung von -takes einen einzigen Schritt entlang des Gradienten des Ausgangssignals in Bezug auf die Eingabe; der "Eingangsgradient".

Eine Technik , die Robustheit zu adversarial Beispiele zu erhöhen , ist Eingangs gradient Regularisierung (Finlay & Oberman 2019), die versucht , die Größe des Eingangs Gradienten zu minimieren. Wenn der Eingangsgradient klein ist, sollte auch die Änderung des Ausgangs klein sein.

Unten ist eine naive Implementierung der Eingabegradienten-Regularisierung. Die Umsetzung ist:

  1. Berechnen Sie die Steigung des Ausgangs in Bezug auf den Eingang mit einem Innenband.
  2. Berechnen Sie die Größe dieses Eingabegradienten.
  3. Berechnen Sie den Gradienten dieser Größe in Bezug auf das Modell.
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])]

Jakobiner

Alle vorherigen Beispiele nahmen die Gradienten eines skalaren Ziels in Bezug auf einen oder mehrere Quelltensoren.

Die Jacobi - Matrix repräsentiert die Gradienten eines Vektors Wertfunktion. Jede Zeile enthält den Gradienten eines der Elemente des Vektors.

Die tf.GradientTape.jacobian Methode ermöglicht es Ihnen, effizient eine Jacobi - Matrix zu berechnen.

Beachten Sie, dass:

  • Wie gradient : Die sources Argument kann ein Tensor oder ein Behälter von Tensoren sein.
  • Im Gegensatz zu gradient : Der target Tensor ein einzelner Tensor sein muss.

Skalare Quelle

Als erstes Beispiel ist hier die Jacobi-Zahl eines Vektor-Targets in Bezug auf eine Skalarquelle.

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)

Wenn Sie die Jacobi in Bezug auf eine skalare nehmen hat das Ergebnis die Form des Ziels, und gibt die Steigung der jedes Element in Bezug auf die Quelle:

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

Tensorquelle

Ob der Eingang Skalar oder Tensor ist, tf.GradientTape.jacobian berechnet effizient , die Gradienten für jedes Element aus der Quelle in Bezug auf jedes Element des Targets (s).

Zum Beispiel hat der Ausgang dieser Schicht eine Form (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])

Und die Form der Schicht des Kernels (5, 10) :

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

Die Form des Jacobi-Wertes der Ausgabe in Bezug auf den Kernel sind diese beiden miteinander verketteten Formen:

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

Wenn Sie das Ziel der Dimensionen summieren, sind Sie mit dem Gradienten der Summe links , die durch berechnet worden wäre 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

Beispiel: Hessisch

Während tf.GradientTape eine explizite Methode nicht gibt eine für die Konstruktion von Hesse - tf.GradientTape.jacobian Matrix ist es möglich , eine mit dem bauen tf.GradientTape.jacobian Methode.

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)

Um diesen hessische Verwendung für ein Newton - Verfahren Schritt, würden Sie zuerst seine Achsen in eine Matrix abflachen und verflachen die Gradienten in einen Vektor:

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

Die Hesse-Matrix sollte symmetrisch sein:

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

Der Aktualisierungsschritt der Newton-Methode ist unten dargestellt:

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

Während dies für eine einzige relativ einfach ist tf.Variable , wobei dies zu einer nicht-trivialen Modell würde vorsichtig Verkettung und Schneiden erfordern eine vollständige hessischen über mehrere Variablen zu erzeugen.

Batch Jacobian

In einigen Fällen möchten Sie den Jacobi-Wert jedes Zielstapels in Bezug auf einen Quellenstapel nehmen, wobei die Jacobi-Werte für jedes Ziel-Quellen-Paar unabhängig sind.

Zum Beispiel sind hier die Eingabe x ist so geformt (batch, ins) und das Ausgangssignal y ist so geformt (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])

Die volle Jacobi von y in Bezug auf x hat eine Form von (batch, ins, batch, outs) , auch wenn Sie nur wollen (batch, ins, outs) :

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

Wenn die Gradienten der einzelnen Elemente in dem Stapel unabhängig voneinander sind, dann ist jede (batch, batch) slice dieser Tensor ist eine Diagonalmatrix:

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

Um das gewünschte Ergebnis zu erhalten, können Sie die doppelte Summe über batch - Dimension, oder wählen Sie die Diagonalen mit 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)

Es wäre viel effizienter, die Berechnung von vornherein ohne die zusätzliche Dimension durchzuführen. Die tf.GradientTape.batch_jacobian Methode tut genau das:

jb = tape.batch_jacobian(y, x)
jb.shape
WARNING:tensorflow:5 out of the last 5 calls to <function pfor.<locals>.f at 0x7f173c6be0e0> 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 0x7f16685c5440> 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

In diesem Fall batch_jacobian läuft noch und kehrt etwas mit der erwarteten Form, aber es ist Inhalt hat eine unklare Bedeutung:

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