Introduction aux dégradés et à la différenciation automatique

Voir sur TensorFlow.org Exécuter dans Google Colab Voir la source sur GitHub Télécharger le cahier

Différenciation automatique et gradients

La différenciation automatique est utile pour la mise en œuvre des algorithmes d' apprentissage machine , telles que rétropropagation pour la formation de réseaux de neurones.

Dans ce guide, vous découvrirez des façons de calculer des gradients avec tensorflow, en particulier dans l' exécution avide .

Installer

import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf

Calcul des gradients

Pour différencier automatiquement, tensorflow a besoin de se rappeler ce que les opérations se produisent dans quel ordre au cours de la passe en avant. Puis, au cours de la passe arrière, tensorflow parcourt cette liste des opérations dans l' ordre inverse à des gradients de calcul.

Rubans dégradés

Tensorflow fournit la tf.GradientTape API de différenciation automatique; qui est, le calcul du gradient d'un calcul par rapport à des entrées, généralement tf.Variable s. Tensorflow « enregistre » opérations pertinentes exécutées dans le cadre d'un tf.GradientTape sur une « bande ». Tensorflow utilise ensuite cette bande pour calculer les gradients d'un calcul « enregistré » en utilisant une différenciation de mode inverse .

Voici un exemple simple :

x = tf.Variable(3.0)

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

Une fois que vous avez enregistré certaines opérations, utilisez GradientTape.gradient(target, sources) pour calculer le gradient de certaines cibles (souvent une perte) par rapport à une source (souvent des variables de modèle):

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

L'exemple ci - dessus utilise scalaires, mais tf.GradientTape fonctionne aussi facilement sur tout tenseur:

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)

Pour obtenir le gradient de loss par rapport aux deux variables, vous pouvez passer à la fois en tant que sources au gradient méthode. La bande est flexible sur la façon dont les sources sont passées et acceptera toute combinaison de listes imbriquées ou des dictionnaires et retourner le gradient structuré de la même manière (voir tf.nest ).

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

Le gradient par rapport à chaque source a la forme de la source :

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

Voici à nouveau le calcul du gradient, en passant cette fois un dictionnaire de variables :

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

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

Dégradés par rapport à un modèle

Il est courant de recueillir tf.Variables dans un tf.Module ou l' un de ses sous - classes ( layers.Layer , keras.Model ) pour checkpointing et l' exportation .

Dans la plupart des cas, vous souhaiterez calculer des gradients par rapport aux variables d'apprentissage d'un modèle. Étant donné que toutes les sous - classes de tf.Module agrègent leurs variables dans la Module.trainable_variables propriété, vous pouvez calculer ces gradients dans quelques lignes de code:

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

Contrôler ce que la bande regarde

Le comportement par défaut est d'enregistrer toutes les opérations après l' accès à un trainable tf.Variable . Les raisons à cela sont :

  • La bande a besoin de savoir quelles opérations enregistrer dans la passe avant pour calculer les gradients dans la passe arrière.
  • La bande contient des références aux sorties intermédiaires, vous ne voulez donc pas enregistrer d'opérations inutiles.
  • Le cas d'utilisation le plus courant consiste à calculer le gradient d'une perte par rapport à toutes les variables d'apprentissage d'un modèle.

Par exemple, ce qui suit ne calcule pas un gradient parce que le tf.Tensor est pas « surveillé » par défaut, et le tf.Variable n'est pas trainable:

# 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

Vous pouvez lister les variables surveillés par la bande en utilisant la GradientTape.watched_variables méthode:

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

tf.GradientTape fournit des crochets qui donnent le contrôle de l' utilisateur sur ce qui est ou non surveillé.

Pour enregistrer des gradients par rapport à un tf.Tensor , vous devez appeler 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

A l' inverse, pour désactiver le comportement par défaut de regarder tous tf.Variables , ensemble watch_accessed_variables=False lors de la création de la bande de gradient. Ce calcul utilise deux variables, mais ne relie le gradient que pour l'une des variables :

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)

Depuis GradientTape.watch n'a pas été appelé x0 , pas de gradient est calculé par rapport à lui:

# 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

Résultats intermédiaires

Vous pouvez également demander des gradients de la sortie par rapport à des valeurs intermédiaires calculées à l' intérieur du tf.GradientTape contexte.

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

Par défaut, les ressources détenues par un GradientTape sont libérés dès que la GradientTape.gradient méthode est appelée. Pour calculer plusieurs gradients sur le même calcul, créer une bande de gradient avec persistent=True . Cela permet à plusieurs appels au gradient méthode que les ressources sont libérées lorsque l'objet de bande est détruite. Par exemple:

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

Remarques sur les performances

  • Il y a un petit surcoût associé à l'exécution d'opérations dans un contexte de bande de dégradé. Pour l'exécution la plus rapide, ce ne sera pas un coût notable, mais vous devez toujours utiliser le contexte de bande autour des zones uniquement où cela est nécessaire.

  • Les bandes de gradient utilisent la mémoire pour stocker les résultats intermédiaires, y compris les entrées et les sorties, à utiliser pendant le passage en arrière.

    Pour plus d' efficacité, certaines opérations (comme ReLU ) ne ont pas besoin de garder leurs résultats intermédiaires et ils sont élagués au cours de la passe en avant. Toutefois, si vous utilisez persistent=True sur la bande, rien est mis au rebut et votre utilisation de la mémoire de pointe sera plus élevé.

Gradients des cibles non scalaires

Un gradient est fondamentalement une opération sur un scalaire.

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

Ainsi, si vous demandez le gradient de plusieurs cibles, le résultat pour chaque source est :

  • Le gradient de la somme des cibles, ou de manière équivalente
  • La somme des gradients de chaque cible.
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

De même, si la ou les cibles ne sont pas scalaires, le gradient de la somme est calculé :

x = tf.Variable(2.)

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

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

Cela rend simple de prendre le gradient de la somme d'une collection de pertes, ou le gradient de la somme d'un calcul de perte par élément.

Si vous avez besoin d' un gradient séparé pour chaque élément, reportez - vous à jacobiens .

Dans certains cas, vous pouvez sauter le jacobien. Pour un calcul élément par élément, le gradient de la somme donne la dérivée de chaque élément par rapport à son élément d'entrée, puisque chaque élément est indépendant :

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

Flux de contrôle

Du fait d' une bande de gradient des opérations enregistre comme elles sont exécutées, le flux de commande Python est géré naturellement (par exemple, if et while déclarations).

Ici , une autre variable est utilisée sur chaque branche d'un if . Le dégradé ne se connecte qu'à la variable qui a été utilisée :

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

Rappelez-vous simplement que les instructions de contrôle elles-mêmes ne sont pas différenciables, elles sont donc invisibles pour les optimiseurs basés sur le gradient.

En fonction de la valeur de x dans l'exemple ci - dessus, la bande soit des enregistrements result = v0 ou result = v1**2 . Le gradient par rapport à x est toujours None .

dx = tape.gradient(result, x)

print(dx)
None

Obtenir un gradient de None

Lorsqu'une cible est pas connecté à une source , vous obtiendrez un gradient de None .

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

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

Ici z est évidemment pas connecté à x , mais il y a plusieurs façons moins évidentes qu'un gradient peut être déconnecté.

1. Remplacement d'une variable par un tenseur

Dans la section sur « ce que le tf.Variable tf.Tensor contrôle de la bande montres » vous avez vu que la bande regardera automatiquement un tf.Variable mais pas un tf.Tensor .

Une erreur courante consiste à remplacer , par inadvertance , un tf.Variable avec un tf.Tensor , au lieu d'utiliser Variable.assign mettre à jour le tf.Variable . Voici un exemple:

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. A fait des calculs en dehors de TensorFlow

La bande ne peut pas enregistrer le chemin du dégradé si le calcul quitte TensorFlow. Par exemple:

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. A pris des dégradés à travers un entier ou une chaîne

Les entiers et les chaînes ne sont pas différenciables. Si un chemin de calcul utilise ces types de données, il n'y aura pas de gradient.

Personne ne s'attend à cordes différentiable, mais il est facile de créer accidentellement un int constant ou variable si vous ne spécifiez pas le 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 ne transpose pas automatiquement entre les types, donc, en pratique, vous obtiendrez souvent une erreur de type au lieu d'un dégradé manquant.

4. A pris des dégradés à travers un objet avec état

L'état arrête les gradients. Lorsque vous lisez à partir d'un objet avec état, la bande ne peut observer que l'état actuel, pas l'historique qui y a conduit.

Un tf.Tensor est immuable. Vous ne pouvez pas modifier un tenseur une fois qu'il est créé. Il a une valeur, mais pas d' état. Toutes les opérations examinées jusqu'à présent sont également sans état: la sortie d'un tf.matmul ne dépend que de ses entrées.

Un tf.Variable a l' état de sa valeur interne. Lorsque vous utilisez la variable, l'état est lu. Il est normal de calculer un gradient par rapport à une variable, mais l'état de la variable empêche les calculs de gradient de remonter plus loin. Par exemple:

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

De même, tf.data.Dataset itérateurs et tf.queue s sont stateful, et arrêtera tous les gradients de tenseurs qui passent à travers eux.

Aucun dégradé enregistré

Certains tf.Operation s sont enregistrés comme étant non différentiables et retourneront None . D' autres ont pas de gradient enregistré.

La tf.raw_ops page montre qui ops bas niveau ont des gradients enregistrés.

Si vous essayez de prendre un gradient par un op flotteur qui n'a pas enregistré la bande gradient renvoie une erreur au lieu de retourner en silence None . De cette façon, vous savez que quelque chose s'est mal passé.

Par exemple, la tf.image.adjust_contrast enveloppe fonction raw_ops.AdjustContrastv2 , ce qui pourrait avoir un gradient mais le gradient est pas mis en œuvre:

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

Si vous avez besoin de faire la différence grâce à cette op, vous devrez soit mettre en œuvre le gradient et l' enregistrer ( en utilisant tf.RegisterGradient ) ou re-mettre en œuvre la fonction à l' aide d' autres opérations.

Des zéros au lieu de Aucun

Dans certains cas , il serait pratique d'obtenir 0 au lieu de None pour les gradients non connectés. Vous pouvez décider de revenir quand vous avez des gradients non connectés à l' aide de l' unconnected_gradients argument:

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)