web-dev-qa-db-fra.com

Que signifie le paramètre retenue_graphe dans la méthode backward () de la variable?

Je passe par le tutoriel de transfert de neurones pytorch et je suis confus quant à l'utilisation de retain_variable (obsolète, désormais appelé retain_graph). L'exemple de code montre:

class ContentLoss(nn.Module):

    def __init__(self, target, weight):
        super(ContentLoss, self).__init__()
        self.target = target.detach() * weight
        self.weight = weight
        self.criterion = nn.MSELoss()

    def forward(self, input):
        self.loss = self.criterion(input * self.weight, self.target)
        self.output = input
        return self.output

    def backward(self, retain_variables=True):
        #Why is retain_variables True??
        self.loss.backward(retain_variables=retain_variables)
        return self.loss

De la documentation

keep_graph (bool, facultatif) - Si False, le graphique utilisé pour calculer le grad sera libéré. Notez que dans presque tous les cas, la définition de cette option sur True n'est pas nécessaire et peut souvent être contournée de manière beaucoup plus efficace. Par défaut, la valeur de create_graph.

Donc, en définissant retain_graph= True, nous ne libérons pas la mémoire allouée au graphique lors de la passe arrière. Quel est l'avantage de conserver cette mémoire, pourquoi en avons-nous besoin?

21
jvans

@cleros est assez sur le point de l'utilisation de retain_graph=True. En substance, il conservera toutes les informations nécessaires pour calculer une certaine variable, afin que nous puissions la transmettre à l'envers.

Un exemple illustratif

enter image description here

Supposons que nous ayons un graphique de calcul illustré ci-dessus. La variable d et e est la sortie, et a est l'entrée. Par exemple,

import torch
from torch.autograd import Variable
a = Variable(torch.Rand(1, 4), requires_grad=True)
b = a**2
c = b*2
d = c.mean()
e = c.sum()

quand nous faisons d.backward(), c'est très bien. Après ce calcul, la partie du graphe qui calcule d sera libérée par défaut pour économiser de la mémoire. Donc, si nous faisons e.backward(), le message d'erreur apparaîtra. Pour faire e.backward(), nous devons définir le paramètre retain_graph Sur True dans d.backward(), c'est-à-dire,

d.backward(retain_graph=True)

Tant que vous utilisez retain_graph=True Dans votre méthode en arrière, vous pouvez faire en arrière à tout moment:

d.backward(retain_graph=True) # fine
e.backward(retain_graph=True) # fine
d.backward() # also fine
e.backward() # error will occur!

Une discussion plus utile peut être trouvée ici .

Un vrai cas d'utilisation

À l'heure actuelle, un véritable cas d'utilisation est l'apprentissage multitâche où vous avez plusieurs pertes qui peuvent être sur différentes couches. Supposons que vous ayez 2 pertes: loss1 Et loss2 Et qu'ils résident dans différentes couches. Afin de rétrograder le gradient de loss1 Et loss2 W.r.t au poids apprenable de votre réseau indépendamment. Vous devez utiliser retain_graph=True Dans la méthode backward() lors de la première perte propagée en retour.

# suppose you first back-propagate loss1, then loss2 (you can also do the reverse)
loss1.backward(retain_graph=True)
loss2.backward() # now the graph is freed, and next process of batch gradient descent is ready
optimizer.step() # update the network parameters
32
jdhao

Il s'agit d'une fonctionnalité très utile lorsque vous disposez de plusieurs sorties d'un réseau. Voici un exemple complètement inventé: imaginez que vous voulez construire un réseau convolutionnel aléatoire auquel vous pouvez poser deux questions: l'image d'entrée contient-elle un chat et l'image contient-elle une voiture?

Une façon de le faire est d'avoir un réseau qui partage les couches convolutionnelles, mais qui a deux couches de classification parallèles suivantes (pardonnez mon terrible graphique ASCII graphique, mais cela est censé être trois convlayers, suivis par trois couches entièrement connectées, une pour les chats et une pour les voitures):

_                    -- FC - FC - FC - cat?
Conv - Conv - Conv -|
                    -- FC - FC - FC - car?
_

Étant donné une image sur laquelle nous voulons exécuter les deux branches, lors de la formation du réseau, nous pouvons le faire de plusieurs manières. Tout d'abord (ce qui serait probablement la meilleure chose ici, illustrant la gravité de l'exemple), nous calculons simplement une perte sur les deux évaluations et additionnons la perte, puis rétropropagonsons.

Cependant, il existe un autre scénario - dans lequel nous souhaitons procéder de manière séquentielle. D'abord, nous voulons backprop à travers une branche, puis à travers l'autre (j'ai eu ce cas d'utilisation avant, donc il n'est pas complètement inventé). Dans ce cas, l'exécution de .backward() sur un graphique détruira également toutes les informations de gradient dans les couches convolutionnelles, et les calculs convolutionnels de la deuxième branche (puisque ce sont les seuls partagés avec l'autre branche) ne contiendront pas de graphique plus! Cela signifie que lorsque nous essayons de rétrograder à travers la deuxième branche, Pytorch lancera une erreur car il ne peut pas trouver de graphique reliant l'entrée à la sortie! Dans ces cas, nous pouvons résoudre le problème en conservant simplement le graphique lors de la première passe en arrière. Le graphique ne sera alors pas consommé, mais seulement consommé par la première passe en arrière qui ne nécessite pas de le conserver.

EDIT: Si vous conservez le graphique à toutes les passes en arrière, les définitions de graphique implicites attachées aux variables de sortie ne seront jamais libérées. Il y a peut-être un cas d'utilisation ici aussi, mais je ne peux pas y penser. Donc, en général, vous devez vous assurer que la dernière passe en arrière libère la mémoire en ne conservant pas les informations du graphique.

Quant à ce qui se passe pour plusieurs passes en arrière: Comme vous l'avez deviné, pytorch accumule des gradients en les ajoutant sur place (à la variable/paramètres _.grad_ propriété). Cela peut être très utile, car cela signifie que faire une boucle sur un lot et le traiter une fois à la fois, accumuler les dégradés à la fin, fera la même étape d'optimisation que de faire une mise à jour par lot complète (qui résume uniquement tous les dégradés comme bien). Alors qu'une mise à jour entièrement par lots peut être davantage parallélisée, et est donc généralement préférable, il existe des cas où le calcul par lots est soit très, très difficile à mettre en œuvre, soit tout simplement impossible. En utilisant cette accumulation, cependant, nous pouvons toujours compter sur certaines des propriétés stabilisantes de Nice qu'apporte le traitement par lots. (Si ce n'est pas sur le gain de performance)

11
cleros