[PYTHON] Couper le graphe de calcul PyTorch

Qu'est-ce que tu veux faire?

Décomposons le graphe de calcul et définissons la différenciation d'origine

Que faire cette fois

Je pense qu'il existe diverses situations dans lesquelles le graphe de calcul est coupé, mais je ne sais pas s'il convient ou non à une fonction compliquée, donc cette fois j'ai préparé une fonction qui évalue avec eval comme une chaîne de caractères d'une fonction simple. Faire. Peu importe la simplicité du calcul, PyTorch ne peut pas s'en occuper, le graphique de calcul sera donc coupé.

De plus, si vous le faites, vous devrez définir vous-même la différenciation, mais cette fois la méthode de la différence avant $\frac{\partial f(x, w)}{\partial w} = \frac{f(x, w + \Delta w) - f(x, w)}{\Delta w}$ J'utiliserai.

Désavantage

Le code est compliqué en n'utilisant pas la différenciation automatique, qui est l'une des raisons d'utiliser PyTorch. Dans ce cas, vous n'êtes pas obligé de le faire et la vitesse sera plus lente.

avantage

Il n'y a aucun avantage à faire cette fois. Cependant, si vous voulez vraiment couper le graphe de calcul, il y a un avantage irremplaçable que vous pouvez le faire pour le moment. Par contre, si c'est absolument impossible, je pense que ce sera comme repenser le modèle, repenser si vous devez vraiment le faire avec PyTorch, mais comme vous pouvez le faire pour le moment, vous perdrez l'occasion de le repenser. ..

Si vous voulez toujours le faire, veuillez. (Je devais encore le faire)

** Faisons le **

Considérez la fonction suivante

Considérez quelque chose d'insensément simple.

Considérez une telle fonction


#Les deux sont les mêmes, mais f_str est comme ça, PyTorch ne peut pas prendre en charge la différenciation.
#L'entrée est x,On suppose que w et PyTorch sont de type Tensor.
def f(x, w):
    return 2 * x * w[0] + x**2 * w[1]

def f_str(x, w):
    return torch.tensor([eval(f'2 * {x_} * {w[0]} + {x_}**2 * {w[1]}') for x_ in x])

«f» est exactement ce que j'ai vu. f_str est identique à f, mais il est calculé en le convertissant en une chaîne de caractères et en le réinterprétant en une expression Python avec eval. Compte tenu de la possibilité que l'entrée «x» arrive dans un lot, dans le cas de «f_str», le contenu est une fois séparé et le tenseur est refait.

différenciation automatique intelligente de la torche

f peut être automatiquement différencié


x = torch.tensor([1.])
w = torch.tensor([1., 1.]).requires_grad_()

f(x, w) # => tensor([3.], grad_fn=<AddBackward0>)

y.backward()
w.grad # => tensor([2., 1.])

PyTorch est intelligent, vous pouvez donc voir tout ce que vous avez fait avec f, et w.grad sera fait automatiquement aprèsy.backward (). Je pense que c'est l'une des raisons d'utiliser des frameworks d'apprentissage automatique tels que PyTorch.

Fonction stupide qui ne peut pas être différenciée automatiquement

Vous vous souvenez peut-être que j'ai rendu f_str très stupide plus tôt. Dans ce cas, vous ferez face au deuil suivant.

f_str ne peut pas être différencié automatiquement


x = torch.tensor([1.])
w = torch.tensor([1., 1.]).requires_grad_()

f_str(x, w) # => tensor([3.]) grad_Il n'y a pas de fn! !! !! !!
y.backward() # RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn

w.grad # => None

Si je pense qu'il n'y a pas de grad_fn, j'obtiens une erreur quand je recule, et grad n'est pas défini et c'est terrible.

Tout d'abord, apprenons la personne qui l'a fait normalement

Faisons des données

Création de données


actual_w = 1.2, -3.4

xs = np.random.rand(200).astype(np.float32)
ys = np.array([f(x, actual_w) for x in xs], dtype=np.float32)
train_d = torch.utils.data.TensorDataset(torch.from_numpy(xs), torch.from_numpy(ys))
train_loader = torch.utils.data.DataLoader(train_d, batch_size=10)

v_xs = np.random.rand(10).astype(np.float32)
v_ys = np.array([f(x, actual_w) for x in v_xs], dtype=np.float32)
valid_d = torch.utils.data.TensorDataset(torch.from_numpy(v_xs), torch.from_numpy(v_ys))
valid_loader = torch.utils.data.DataLoader(valid_d, batch_size=1)

Définissez la valeur de manière appropriée sur true w. Ensuite, donnez un nombre aléatoire pour faire une paire de «x» et «f (x, true w)». Ce serait bien de mettre un nombre aléatoire gaussien là-dessus, mais c'est gênant, donc je ne le ferai pas cette fois. C'est exagéré d'utiliser PyTorch. Je pense que scipy.minimize.optimize ou quelque chose comme ça suffit.

Apprenons normalement

Apprenons f


class Model(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.weight = torch.nn.parameter.Parameter(torch.tensor([0., 0.]))
        
    def forward(self, x):
        return f(x, self.weight)

model = Model()
optimizer = torch.optim.Adam(model.parameters(), lr=0.1)
criterion = torch.nn.MSELoss()
loss_hist = []
model.train()
for epoch in range(20):
    for i, (xs, l) in enumerate(train_loader):
        out = model(xs)
        loss = criterion(out, l)
        loss_hist.append(loss)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    print(epoch, loss, model.weight)

Le "model.weight" est finalement "1.1996, -3.3987". Depuis que j'ai défini 1.2, -3.4 comme valeur vraie, c'est presque la même chose. La perte pendant l'entraînement et la perte des données préparées pour la validation (réussie quand toutes sont presque 0)

Screenshot_20200624_235842.png

C'est devenu comme. Ça sonne plutôt bien.

Essayez de changer f en f_str

Maintenant, changeons f en putain de fonction f_str.

Screenshot_20200625_000201.png

C'est une erreur plutôt déroutante. C'est facile à dire. Puisque le graphe de calcul a été coupé par «f_str» et que la différenciation automatique est devenue impossible, il est moussé par «en arrière». Vous devez donc définir vous-même la différenciation.

Faisons un différentiel uniquement pour vous

La documentation officielle de ces cas peut être trouvée ici [https://pytorch.org/docs/stable/notes/extending.html#extending-torch-autograd). Cependant, lorsque je l'ai essayé, je n'ai pas pu atteindre l'endroit qui me démange, alors je vais le présenter ici et là.

D'abord le code, puis le commentaire

Définir la différenciation par différence directe pour les fonctions générales


class GeneralFunctionWithForwardDifference(torch.autograd.Function):
    @staticmethod
    def forward(ctx, f, xs, weight):
        ys = f(xs, weight)
        ctx.save_for_backward(xs, ys, weight)
        ctx.f = f #En fait, vous pouvez enregistrer quelque chose dans ctx et l'utiliser à l'envers
        return ys
        
    @staticmethod
    def backward(ctx, grad_output):
        xs, ys, weight = ctx.saved_tensors
        f = ctx.f
        dw = 0.001
        diff = []
        weight = weight.detach() #Détachez-le pour éviter de laisser un historique de calcul supplémentaire en poids.
        for i in range(len(weight)):
            weight[i] += dw
            diff.append(torch.sum(grad_output * (f(xs, weight) - ys)))
            weight[i] -= dw
        diff = torch.tensor(diff) / dw
        return None, None, diff

Créez une classe qui hérite de torch.autograd.Function, définissez forward et backward avec @ staticmethod, et définissez le premier argument de chacun sur ctx (un autre nom est également acceptable). Mais c'est une bonne idée de suivre ceci), etc., comme dans la documentation.

Sauvegarder des données autres que le tenseur

La documentation dit que vous pouvez enregistrer le tenseur avec ctx.save_for_backward, mais cette méthode ne peut enregistrer que torch.Tensor.

Mais cette fois, je veux passer f_str comme argument à forward et l'enregistrer pour backward. En fait, cela peut être sauvegardé sous la forme de «ctx. Nachara = ...», qui semble être utilisable en «arrière». Il est également utilisé dans Pytorch, donc je pense que c'est probablement correct de l'utiliser. Je vais.

Que devrait revenir en arrière?

La valeur renvoyée par «backward» correspond à l'argument de «forward». Renvoie le résultat différentiel de l'argument de forward moins ctx.

Si cela correspond à quelque chose qui ne nécessite pas de différenciation (non tenseur ou tenseur qui n'est pas required_grad = True), vous pouvez renvoyer None. Cette fois, seul «w» a besoin d'être différencié.

Si l'entrée est tenseur $ {\ bf w} = [w_0, w_1, ..., w_ {n-1}] $, la valeur renvoyée est

[\sum_i\mathrm{grad\_output}_i\frac{\partial f(x_i, {\bf w})}{\partial w_0}, \sum_i \mathrm{grad\_output}_i\frac{\partial f(x_i, {\bf w})}{\partial w_1}, ... \sum_i\mathrm{grad\_output}_i\frac{\partial f(x_i, {\bf w})}{\partial w_{n-1}}]

Ce sera. Cependant, $ \ sum_i $ dit que si l'entrée x vient dans un mini-lot $ [x_0, x_1, ...] $, les résultats de chacun seront additionnés. La dimension de grad_output correspond à la taille du mini-lot, donc multipliez le résultat comme ceci.

Enfin, ceux qui utilisent f_str peuvent également apprendre

f_Apprendre un modèle à l'aide de str


class Model2(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.weight = torch.nn.parameter.Parameter(torch.tensor([0., 0.]))
        
    def forward(self, x):
        #C'est un peu ennuyeux à écrire.
        return GeneralFunctionWithForwardDifference.apply(f_str, x, self.weight)

model2 = Model2()
optimizer = torch.optim.Adam(model2.parameters(), lr=0.1)
criterion = torch.nn.MSELoss()
loss_hist2 = []
model2.train()
for epoch in range(20):
    for i, (xs, l) in enumerate(train_loader):
        out = model2(xs)
        loss = criterion(out, l)
        loss_hist2.append(loss)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    print(epoch, loss, model2.weight)

Notez que la Fonction créée est appelée sous la forme de .apply.

Screenshot_20200625_001619.png

Un graphique similaire au précédent est tellement ressorti que j'ai cru faire une erreur. Les paramètres finaux sont également "1.1996, -3.3987", qui est également presque identique aux vraies valeurs "1.2, -3.4". Eh bien, je n'utilise pas de nombres aléatoires, je fais les mêmes données avec les mêmes valeurs initiales de paramètres, donc c'est probablement le cas. Je ne sais pas.

Au fait, j'ai essayé de dessiner les pertes les unes sur les autres et de faire la différence entre les valeurs de validation prédites.

Screenshot_20200625_001748.png

C'est presque pareil. J'ai utilisé une différenciation bâclée, donc si vous pensez qu'il y aura une légère différence, il semble qu'il n'y ait pas beaucoup de différence. Je suis contente.

Résumé

J'ai vu comment utiliser PyTorch de manière forcée en définissant vous-même un différentiel pour une fonction étrange qui traverse un graphique de calcul. Je ne veux plus le faire.

Je mettrai ce cahier ici [https://gist.github.com/gyu-don/f5cc025139312ccfd39e48400018118d)

Recommended Posts

Couper le graphe de calcul PyTorch
[PyTorch] Exemple ⑨ ~ Graphique dynamique ~
Composants liés du graphique
Apprenez avec les réseaux convolutifs PyTorch Graph
Graphique d'appel de sortie avec PyCallGraph
Coupons le visage de l'image