Hai una domanda? Connettiti con la community al forum TensorFlow Visita il forum

Introduzione ai gradienti e differenziazione automatica

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza sorgente su GitHub Scarica taccuino

Differenziazione automatica e gradienti

La differenziazione automatica è utile per implementare algoritmi di apprendimento automatico come backpropagation per l'addestramento di reti neurali.

In questa guida esplorerai i modi per calcolare i gradienti con TensorFlow, specialmente nell'esecuzione impaziente .

Impostare

import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf

Calcolo dei gradienti

Per differenziarsi automaticamente, TensorFlow deve ricordare quali operazioni avvengono in quale ordine durante il passaggio in avanti . Quindi, durante il passaggio all'indietro , TensorFlow attraversa questo elenco di operazioni in ordine inverso per calcolare i gradienti.

Nastri sfumati

TensorFlow fornisce l'APItf.GradientTape per la differenziazione automatica; ovvero, calcolare il gradiente di un calcolo rispetto ad alcuni input, solitamente tf.Variable s. TensorFlow "registra" le operazioni rilevanti eseguite nel contesto di untf.GradientTape su un "nastro". TensorFlow utilizza quindi quel nastro per calcolare i gradienti di un calcolo "registrato" utilizzando la differenziazione in modalità inversa .

Qui c'è un semplice esempio:

x = tf.Variable(3.0)

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

Dopo aver registrato alcune operazioni, usa GradientTape.gradient(target, sources) per calcolare il gradiente di un target (spesso una perdita) relativo a qualche sorgente (spesso le variabili del modello):

# dy = 2x * dx
dy_dx = tape.gradient(y, x)
dy_dx.numpy()
6.0

L'esempio precedente utilizza gli scalari, matf.GradientTape funziona altrettanto facilmente su qualsiasi tensore:

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)

Per ottenere il gradiente di loss rispetto ad entrambe le variabili, è possibile passare entrambe come sorgenti al metodo del gradient . Il nastro è flessibile su come le fonti vengono passate e accetterà qualsiasi combinazione annidata di elenchi o dizionari e restituirà il gradiente strutturato allo stesso modo (vedere tf.nest ).

[dl_dw, dl_db] = tape.gradient(loss, [w, b])

Il gradiente rispetto a ciascuna sorgente ha la forma della sorgente:

print(w.shape)
print(dl_dw.shape)
(3, 2)
(3, 2)

Ecco di nuovo il calcolo del gradiente, questa volta passando un dizionario di variabili:

my_vars = {
    'w': w,
    'b': b
}

grad = tape.gradient(loss, my_vars)
grad['b']
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([ 2.5342524, -3.8607523], dtype=float32)>

Gradienti rispetto a un modello

È comune raccogliere tf.Variables in un tf.Module o in una delle sue sottoclassi ( layers.Layer , keras.Model ) per il checkpoint e l' esportazione .

Nella maggior parte dei casi, vorrai calcolare i gradienti rispetto alle variabili allenabili di un modello. Poiché tutte le sottoclassi di tf.Module aggregano le loro variabili nella proprietà Module.trainable_variables , puoi calcolare questi gradienti in poche righe di codice:

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

Controllo di ciò che guarda il nastro

Il comportamento predefinito è registrare tutte le operazioni dopo l'accesso a una tf.Variable . Le ragioni di ciò sono:

  • Il nastro deve sapere quali operazioni registrare nella passata in avanti per calcolare i gradienti nella passata all'indietro.
  • Il nastro contiene riferimenti a uscite intermedie, quindi non si desidera registrare operazioni non necessarie.
  • Il caso d'uso più comune prevede il calcolo del gradiente di una perdita rispetto a tutte le variabili addestrabili di un modello.

Ad esempio, quanto segue non riesce a calcolare un gradiente perché tf.Tensor non è "controllato" per impostazione predefinita e tf.Variable non è tf.Variable :

# 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

È possibile elencare le variabili guardate dal nastro utilizzando il metodo GradientTape.watched_variables :

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

tf.GradientTape fornisce hook che danno all'utente il controllo su ciò che è o non viene guardato.

Per registrare i gradienti rispetto a un tf.Tensor , è necessario chiamare 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

Al contrario, per disabilitare il comportamento predefinito di guardare tutte le tf.Variables , impostare watch_accessed_variables=False durante la creazione del nastro sfumato. Questo calcolo utilizza due variabili, ma collega solo il gradiente per una delle variabili:

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)

Poiché GradientTape.watch non è stato chiamato su x0 , non viene calcolato alcun gradiente rispetto ad esso:

# 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

Risultati intermedi

È inoltre possibile richiedere gradienti dell'output rispetto a valori intermedi calcolati all'interno del contestotf.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

Per impostazione predefinita, le risorse detenute da un GradientTape vengono rilasciate non appena viene chiamato il metodo GradientTape.gradient . Per calcolare più gradienti sullo stesso calcolo, creare un nastro gradiente con persistent=True . Ciò consente più chiamate al metodo gradient quando le risorse vengono rilasciate quando l'oggetto nastro viene sottoposto a garbage collection. Per esempio:

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())  # 108.0 (4 * x**3 at x = 3)
print(tape.gradient(y, x).numpy())  # 6.0 (2 * x)
[  4. 108.]
[2. 6.]
del tape   # Drop the reference to the tape

Note sulle prestazioni

  • Esiste un piccolo sovraccarico associato all'esecuzione di operazioni all'interno di un contesto di nastro sfumato. Per l'esecuzione più entusiasta questo non sarà un costo notevole, ma dovresti comunque utilizzare il contesto del nastro intorno alle aree solo dove è richiesto.

  • I nastri sfumati utilizzano la memoria per memorizzare i risultati intermedi, inclusi input e output, da utilizzare durante il passaggio all'indietro.

    Per l'efficienza, alcune operazioni (come ReLU ) non hanno bisogno di mantenere i loro risultati intermedi e vengono potate durante il passaggio in avanti. Tuttavia, se si utilizza persistent=True sul nastro, nulla viene scartato e il picco di utilizzo della memoria sarà maggiore.

Gradienti di target non scalari

Un gradiente è fondamentalmente un'operazione su uno scalare.

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

Pertanto, se chiedi il gradiente di più target, il risultato per ciascuna sorgente è:

  • Il gradiente della somma dei target o equivalentemente
  • La somma dei gradienti di ogni obiettivo.
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

Allo stesso modo, se i target non sono scalari, viene calcolato il gradiente della somma:

x = tf.Variable(2.)

with tf.GradientTape() as tape:
  y = x * [3., 4.]

print(tape.gradient(y, x).numpy())
7.0

Ciò semplifica il calcolo del gradiente della somma di una raccolta di perdite o del gradiente della somma di un calcolo di perdita per elemento.

Se hai bisogno di un gradiente separato per ogni oggetto, fai riferimento ai giacobiani .

In alcuni casi puoi saltare lo Jacobiano. Per un calcolo basato sugli elementi, il gradiente della somma fornisce la derivata di ogni elemento rispetto al suo elemento di input, poiché ogni elemento è indipendente:

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

Flusso di controllo

Poiché un nastro a gradiente registra le operazioni mentre vengono eseguite, il flusso di controllo Python viene gestito naturalmente (ad esempio, istruzioni if e while ).

Qui viene utilizzata una variabile diversa su ogni ramo di un if . Il gradiente si collega solo alla variabile che è stata utilizzata:

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

Ricorda solo che le istruzioni di controllo stesse non sono differenziabili, quindi sono invisibili agli ottimizzatori basati su gradiente.

A seconda del valore di x nell'esempio precedente, il nastro registra result = v0 o result = v1**2 . Il gradiente rispetto a x è sempre None .

dx = tape.gradient(result, x)

print(dx)
None

Ottenere un gradiente di None

Quando una destinazione non è collegata a una sorgente, otterrai un gradiente di None .

x = tf.Variable(2.)
y = tf.Variable(3.)

with tf.GradientTape() as tape:
  z = y * y
print(tape.gradient(z, x))
None

Qui z ovviamente non è connesso a x , ma ci sono molti modi meno ovvi in ​​cui un gradiente può essere disconnesso.

1. Sostituita una variabile con un tensore

Nella sezione sul "controllo di ciò che guarda il nastro" hai visto che il nastro guarderà automaticamente un tf.Variable ma non un tf.Tensor .

Un errore comune è sostituire inavvertitamente tf.Variable con tf.Tensor , invece di utilizzare Variable.assign per aggiornare tf.Variable . Ecco un esempio:

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. Calcoli al di fuori di TensorFlow

Il nastro non può registrare il percorso del gradiente se il calcolo esce da TensorFlow. Per esempio:

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. Ha preso i gradienti attraverso un numero intero o una stringa

I numeri interi e le stringhe non sono differenziabili. Se un percorso di calcolo utilizza questi tipi di dati, non ci sarà alcun gradiente.

Nessuno si aspetta che le stringhe siano differenziabili, ma è facile creare accidentalmente una costante o una variabile int se non si specifica il 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 non esegue automaticamente il cast tra i tipi, quindi, in pratica, riceverai spesso un errore di tipo invece di un gradiente mancante.

4. Ha preso gradienti attraverso un oggetto con stato

Lo stato ferma le pendenze. Quando leggi da un oggetto con stato, il nastro può solo osservare lo stato corrente, non la cronologia che lo ha portato.

Un tf.Tensor è immutabile. Non puoi cambiare un tensore una volta che è stato creato. Ha un valore , ma nessuno stato . Tutte le operazioni discusse finora sono anche senza stato: l'output di un tf.matmul dipende solo dai suoi input.

Una tf.Variable ha uno stato interno, il suo valore. Quando si utilizza la variabile, viene letto lo stato. È normale calcolare un gradiente rispetto a una variabile, ma lo stato della variabile impedisce ai calcoli del gradiente di andare più indietro. Per esempio:

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

Allo stesso modo,tf.data.Dataset iteratoritf.data.Dataset e tf.queue s sono stateful e fermeranno tutti i gradienti sui tensori che li attraversano.

Nessuna pendenza registrata

Alcuni tf.Operation sono registrati come non differenziabili e restituiranno None . Altri non hanno registrato gradiente .

La pagina tf.raw_ops mostra quali operazioni di basso livello hanno gradienti registrati.

Se si tenta di prendere un gradiente attraverso un'operazione float che non ha gradiente registrato, il nastro genererà un errore invece di restituire silenziosamente None . In questo modo sai che qualcosa è andato storto.

Ad esempio, la funzione tf.image.adjust_contrast avvolge raw_ops.AdjustContrastv2 , che potrebbe avere un gradiente ma il gradiente non è implementato:

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

Se devi differenziare attraverso questa operazione, dovrai implementare il gradiente e registrarlo (usando tf.RegisterGradient ) o reimplementare la funzione usando altre operazioni.

Zeri invece di Nessuno

In alcuni casi sarebbe conveniente ottenere 0 invece di None per gradienti non collegati. Puoi decidere cosa restituire quando hai gradienti non connessi utilizzando l'argomento 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)