Questa pagina è stata tradotta dall'API Cloud Translation.
Switch to English

Differenziazione automatica avanzata

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza sorgente su GitHubScarica notebook

La guida alla differenziazione automatica include tutto il necessario per calcolare i gradienti. Questa guida si concentra sulle caratteristiche più profonde e meno comunitf.GradientTape .

Impostare

import tensorflow as tf

import matplotlib as mpl
import matplotlib.pyplot as plt

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

Controllo della registrazione del gradiente

Nella guida alla differenziazione automatica hai visto come controllare quali variabili e tensori vengono osservati dal nastro durante la costruzione del calcolo del gradiente.

Il nastro ha anche metodi per manipolare la registrazione.

Se desideri interrompere la registrazione dei gradienti, puoi utilizzare GradientTape.stop_recording() per sospendere temporaneamente la registrazione.

Questo può essere utile per ridurre l'overhead se non desideri differenziare un'operazione complicata nel mezzo del tuo modello. Ciò potrebbe includere il calcolo di una metrica o di un risultato intermedio:

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

Se desideri ricominciare da capo, usa reset() . La semplice uscita dal blocco del nastro sfumato e il riavvio sono generalmente più facili da leggere, ma è possibile utilizzare il reset quando l'uscita dal blocco del nastro è difficile o impossibile.

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

Interrompi il gradiente

In contrasto con i controlli globali del nastro sopra, la funzione tf.stop_gradient è molto più precisa. Può essere utilizzato per impedire ai gradienti di scorrere lungo un percorso particolare, senza dover accedere al nastro stesso:

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

Gradienti personalizzati

In alcuni casi, potresti voler controllare esattamente come vengono calcolati i gradienti piuttosto che usare l'impostazione predefinita. Queste situazioni includono:

  • Non esiste un gradiente definito per una nuova operazione che stai scrivendo.
  • I calcoli predefiniti sono numericamente instabili.
  • Desideri memorizzare nella cache un calcolo costoso dal passaggio in avanti.
  • Si desidera modificare un valore (ad esempio utilizzando: tf.clip_by_value , tf.math.round ) senza modificare il gradiente.

Per scrivere un nuovo op, puoi usare tf.RegisterGradient per impostarne uno tuo. Vedi quella pagina per i dettagli. (Tieni presente che il registro del gradiente è globale, quindi modificalo con cautela.)

Per gli ultimi tre casi, puoi usare tf.custom_gradient .

Ecco un esempio che applicatf.clip_by_norm al gradiente intermedio.

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

Vedi il decoratore tf.custom_gradient per maggiori dettagli.

Nastri multipli

Più nastri interagiscono perfettamente. Ad esempio, qui ogni nastro guarda un diverso insieme di tensori:

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

Gradienti di ordine superiore

Le operazioni all'interno del gestore di contesto GradientTape vengono registrate per la differenziazione automatica. Se i gradienti vengono calcolati in quel contesto, viene registrato anche il calcolo del gradiente. Di conseguenza, la stessa identica API funziona anche per gradienti di ordine superiore. Per esempio:

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

Anche se questo ti dà la derivata seconda di una funzione scalare , questo modello non generalizza per produrre una matrice hessiana, poiché GradientTape.gradient calcola solo il gradiente di uno scalare. Per costruire un'Assia, vedere l' esempio dell'Assia nella sezione Jacobiana .

"Chiamate annidate a GradientTape.gradient " è un buon modello quando si calcola uno scalare da un gradiente, quindi lo scalare risultante funge da origine per un secondo calcolo del gradiente, come nell'esempio seguente.

Esempio: regolarizzazione del gradiente di input

Molti modelli sono suscettibili di "esempi contraddittori". Questa raccolta di tecniche modifica l'input del modello per confondere l'output del modello. L' implementazione più semplice prevede un unico passaggio lungo il gradiente dell'output rispetto all'input; il "gradiente di input".

Una tecnica per aumentare la robustezza degli esempi contraddittori è la regolarizzazione del gradiente di input , che tenta di ridurre al minimo l'entità del gradiente di input. Se il gradiente di input è piccolo, anche il cambiamento nell'output dovrebbe essere piccolo.

Di seguito è riportata un'implementazione ingenua della regolarizzazione del gradiente di input. L'implementazione è:

  1. Calcola il gradiente dell'uscita rispetto all'ingresso utilizzando un nastro interno.
  2. Calcola l'ampiezza di quel gradiente di input.
  3. Calcola il gradiente di quella grandezza rispetto al modello.
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])]

Jacobiani

Tutti gli esempi precedenti hanno preso i gradienti di un target scalare rispetto ad alcuni tensori sorgente.

La matrice Jacobiana rappresenta i gradienti di una funzione a valori vettoriali. Ogni riga contiene il gradiente di uno degli elementi del vettore.

Il metodo GradientTape.jacobian consente di calcolare in modo efficiente una matrice Jacobiana.

Nota che:

  • gradient simile: l'argomento delle sources può essere un tensore o un contenitore di tensori.
  • A differenza del gradient : il tensore target deve essere un singolo tensore.

Fonte scalare

Come primo esempio, ecco lo Jacobiano di un vettore-obiettivo rispetto a una sorgente scalare.

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)

Quando prendi lo Jacobiano rispetto a uno scalare il risultato ha la forma del bersaglio , e dà il gradiente di ogni elemento rispetto alla sorgente:

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

Fonte tensoriale

Indipendentemente dal fatto che l'input sia scalare o tensore, GradientTape.jacobian calcola in modo efficiente il gradiente di ciascun elemento della sorgente rispetto a ciascun elemento della destinazione.

Ad esempio, l'output di questo livello ha una forma di (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])

E la forma del kernel del livello è (5, 10) :

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

La forma dello Jacobiano dell'output rispetto al kernel sono quelle due forme concatenate insieme:

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

Se si sommano le dimensioni del target, resta il gradiente della somma che sarebbe stata calcolata da 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: 4.7683716e-07

Esempio: iuta

Sebbenetf.GradientTape non fornisca un metodo esplicito per costruire una matricetf.GradientTape , è possibiletf.GradientTape una utilizzando il metodo GradientTape.jacobian .

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)

Per usare questo hessiano per il passaggio del metodo di Newton, devi prima appiattire i suoi assi in una matrice e appiattire il gradiente in un vettore:

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

La matrice dell'Assia dovrebbe essere simmetrica:

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

La fase di aggiornamento del metodo di Newton è mostrata di seguito.

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

Sebbene ciò sia relativamente semplice per una singola tf.Variable , applicarlo a un modello non banale richiederebbe un'attenta concatenazione e tf.Variable per produrre un Hessiano completo su più variabili.

Lotto Jacobian

In alcuni casi, si desidera prendere lo Jacobiano di ciascuno di una pila di bersagli rispetto a una pila di sorgenti, dove i Jacobiani per ciascuna coppia di destinazione-sorgente sono indipendenti.

Ad esempio, qui l'ingresso x è a forma di (batch, ins) e l'output y è a forma di (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])

Lo Jacobiano completo di y rispetto a x ha una forma di (batch, ins, batch, outs) , anche se si desidera solo (batch, ins, outs) .

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

Se i gradienti di ogni elemento nella pila sono indipendenti, allora ogni fetta (batch, batch) di questo tensore è una matrice diagonale:

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

Per ottenere il risultato desiderato è possibile sommare la dimensione del batch duplicato, oppure selezionare le diagonali utilizzando 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)

Sarebbe molto più efficiente eseguire il calcolo senza la dimensione extra in primo luogo. Il metodo GradientTape.batch_jacobian fa esattamente questo.

jb = tape.batch_jacobian(y, x)
jb.shape
WARNING:tensorflow:5 out of the last 5 calls to <function pfor.<locals>.f at 0x7f9a400e8620> 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/tutorials/customization/performance#python_or_tensor_args 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 0x7f9a401090d0> 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/tutorials/customization/performance#python_or_tensor_args 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 questo caso batch_jacobian viene ancora eseguito e restituisce qualcosa con la forma prevista, ma i suoi contenuti hanno un significato poco chiaro.

jb = tape.batch_jacobian(y, x)
print(f'jb.shape: {jb.shape}')
WARNING:tensorflow:7 out of the last 7 calls to <function pfor.<locals>.f at 0x7f9a4c0637b8> 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/tutorials/customization/performance#python_or_tensor_args and https://www.tensorflow.org/api_docs/python/tf/function for  more details.
jb.shape: (7, 6, 5)