[PYTHON] Schneiden Sie das PyTorch-Berechnungsdiagramm aus

Was möchten Sie tun?

Lassen Sie uns das Berechnungsdiagramm aufbrechen und die ursprüngliche Differenzierung definieren

Was ist diesmal zu tun?

Ich denke, dass es verschiedene Situationen gibt, in denen das Berechnungsdiagramm abgeschnitten ist, aber ich weiß nicht, ob es übereinstimmt oder nicht, wenn es sich um eine komplizierte Funktion handelt. Deshalb habe ich dieses Mal eine Funktion vorbereitet, die mit eval ausgewertet wird, indem eine einfache Funktion in eine Zeichenfolge konvertiert wird. Machen. Unabhängig davon, wie einfach die Berechnung ist, kann PyTorch sich nicht darum kümmern, sodass das Berechnungsdiagramm abgeschnitten wird.

Wenn Sie dies tun, müssen Sie die Differenzierung selbst definieren, diesmal jedoch die Vorwärtsdifferenzmethode $\frac{\partial f(x, w)}{\partial w} = \frac{f(x, w + \Delta w) - f(x, w)}{\Delta w}$ Ich werde benützen.

Nachteil

Code wird dadurch kompliziert, dass keine automatische Differenzierung verwendet wird. Dies ist einer der Gründe für die Verwendung von PyTorch. In diesem Fall müssen Sie dies nicht tun und die Geschwindigkeit wird langsamer.

Vorteil

Diesmal ist es kein Vorteil. Wenn Sie das Berechnungsdiagramm jedoch wirklich ausschneiden möchten, gibt es einen unersetzlichen Vorteil, dass Sie dies vorerst tun können. Auf der anderen Seite, wenn es absolut unmöglich ist, denke ich, dass es so sein wird, als würde man das Modell überdenken und überlegen, ob man es wirklich mit PyTorch machen muss, aber da man es vorerst tun kann, wird man die Gelegenheit verlieren, es zu überdenken. ..

Wenn Sie es trotzdem tun möchten, bitte. (Ich musste es noch tun)

** Machen wir das **

Betrachten Sie die folgende Funktion

Betrachten Sie etwas wahnsinnig Einfaches.

Betrachten Sie eine solche Funktion


#Beide sind gleich, aber f_str ist so, PyTorch kann sich nicht um Differenzierung kümmern.
#Eingabe ist x,Es wird angenommen, dass sowohl w als auch PyTorch vom Tensortyp sind.
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" ist genau das, was ich gesehen habe. f_str ist dasselbe wie f, wird jedoch berechnet, indem es in eine Zeichenkette konvertiert und mit eval in einen Python-Ausdruck neu interpretiert wird. In Anbetracht der Möglichkeit, dass die Eingabe "x" in einem Stapel eingeht, wird im Fall von "f_str" der Inhalt einmal getrennt und der Tensor neu erstellt.

intelligente automatische Differenzierung der Taschenlampe

f kann automatisch unterschieden werden


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 ist intelligent, sodass Sie alles sehen können, was Sie mit f gemacht haben, und w.grad wird automatisch nachy.backward ()ausgeführt. Ich denke, dies ist einer der Gründe für die Verwendung von Frameworks für maschinelles Lernen wie PyTorch.

Dumme Funktion, die nicht automatisch unterschieden werden kann

Sie erinnern sich vielleicht, dass ich f_str früher sehr dumm gemacht habe. In diesem Fall werden Sie der folgenden Trauer gegenüberstehen.

f_str kann nicht automatisch unterschieden werden


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

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

w.grad # => None

Wenn ich denke, dass es kein grad_fn gibt, erhalte ich einen Fehler, wenn ich rückwärts gehe, und grad ist nicht gesetzt und es ist schrecklich.

Lassen Sie uns zuerst die Person lernen, die es normal gemacht hat

Machen wir Daten

Datenerstellung


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)

Stellen Sie den Wert entsprechend auf true ein. W. Geben Sie dann eine Zufallszahl an, um ein Paar aus "x" und "f (x, wahres w)" zu bilden. Es wäre schön, eine Gaußsche Zufallszahl darauf zu setzen, aber es ist mühsam, also werde ich das diesmal nicht tun. Dies ist ein Overkill für die Verwendung von PyTorch. Ich denke, dass scipy.minimize.optimize oder so etwas ausreicht.

Lass uns normal lernen

Lass uns lernen 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)

Das model.weight ist schließlich 1.1996, -3.3987. Da ich "1.2, -3.4" als wahren Wert eingestellt habe, ist es fast dasselbe. Der Verlust während des Trainings und der Verlust der zur Validierung vorbereiteten Daten (erfolgreich, wenn alle fast 0 sind)

Screenshot_20200624_235842.png

Es wurde wie. Hört sich ziemlich gut an.

Versuchen Sie, f in f_str zu ändern

Jetzt ändern wir f in die verdammte Funktion f_str.

Screenshot_20200625_000201.png

Es ist ein ziemlich verwirrender Fehler. Es ist leicht zu sagen. Da der Berechnungsgraph durch "f_str" abgeschnitten wurde und eine automatische Differenzierung unmöglich wurde, ist er Moos durch "rückwärts". Sie müssen die Differenzierung also selbst definieren.

Lassen Sie uns nur für Sie ein Differential machen

Die offizielle Dokumentation zu diesen Fällen finden Sie hier [https://pytorch.org/docs/stable/notes/extending.html#extending-torch-autograd]. Als ich es jedoch tatsächlich ausprobierte, konnte ich den juckenden Ort nicht erreichen, daher werde ich es hier und da vorstellen.

Erst der Code, dann der Kommentar

Definieren Sie die Differenzierung durch Vorwärtsdifferenz für allgemeine Funktionen


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 #Tatsächlich können Sie etwas in ctx speichern und rückwärts verwenden
        return ys
        
    @staticmethod
    def backward(ctx, grad_output):
        xs, ys, weight = ctx.saved_tensors
        f = ctx.f
        dw = 0.001
        diff = []
        weight = weight.detach() #Abnehmen, um zu vermeiden, dass zusätzliche Berechnungshistorien im Gewicht verbleiben.
        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

Erstellen Sie eine Klasse, die torch.autograd.Function erbt, definieren Sie forward und backward mit @ staticmethod und setzen Sie das erste Argument von jeder auf ctx (ein anderer Name ist ebenfalls akzeptabel). Es ist jedoch eine gute Idee, dies usw. wie in der Dokumentation zu befolgen.

Andere Daten als Tensor speichern

Die Dokumentation besagt, dass Sie den Tensor mit ctx.save_for_backward speichern können, aber diese Methode speichert nur torch.Tensor.

Aber dieses Mal möchte ich "f_str" als Argument für "vorwärts" übergeben und es für "rückwärts" speichern. Tatsächlich kann dies in Form von "ctx. Nachara = ..." gespeichert werden, was in "rückwärts" verwendbar zu sein scheint. Es wird auch in Pytorch verwendet, daher denke ich, dass es wahrscheinlich in Ordnung ist, es zu verwenden. Ich werde.

Was soll rückwärts zurückkehren?

Der von "rückwärts" zurückgegebene Wert entspricht dem Argument "vorwärts". Gibt das Differentialergebnis des Arguments von "forward" minus "ctx" zurück.

Wenn es etwas entspricht, das keine Differenzierung erfordert (Nicht-Tensor oder Tensor, der nicht "required_grad = True" ist), können Sie None zurückgeben. Dieses Mal muss nur "w" differenziert werden.

Wenn die Eingabe Tensor $ {\ bf w} = [w_0, w_1, ..., w_ {n-1}] $ ist, ist der zurückgegebene Wert

[\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}}]

Es wird sein. $ \ Sum_i $ sagt jedoch, dass, wenn die Eingabe x in einem Mini-Batch $ [x_0, x_1, ...] $ kommt, die Ergebnisse von jedem addiert werden. Die Dimension von "grad_output" entspricht der Größe des Mini-Batches. Multiplizieren Sie das Ergebnis also wie folgt.

Schließlich können auch diejenigen lernen, die f_str verwenden

f_Lernen eines Modells mit 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):
        #Es ist etwas nervig zu schreiben.
        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)

Beachten Sie, dass die erstellte Funktion in Form von .apply aufgerufen wird.

Screenshot_20200625_001619.png

Eine Grafik ähnlich der vorherigen kam so oft heraus, dass ich dachte, ich hätte einen Fehler gemacht. Die endgültigen Parameter sind ebenfalls "1.1996, -3.3987", was ebenfalls fast den wahren Werten "1.2, -3.4" entspricht. Nun, ich verwende keine Zufallszahlen, ich mache die gleichen Daten mit den gleichen Parameteranfangswerten, also ist das wahrscheinlich der Fall. Ich weiß es nicht.

Übrigens habe ich versucht, den Verlust übereinander zu ziehen und die Differenz zwischen den vorhergesagten Validierungswerten zu ziehen.

Screenshot_20200625_001748.png

Es ist fast das gleiche. Ich habe eine schlampige Differenzierung verwendet. Wenn Sie also glauben, dass es einen kleinen Unterschied geben wird, scheint es keinen großen Unterschied zu geben. Ich bin froh.

Zusammenfassung

Ich habe gesehen, wie man PyTorch zwangsweise verwendet, indem man selbst ein Differential für eine seltsame Funktion definiert, die ein Berechnungsdiagramm durchschneidet. Ich will es nicht mehr machen.

Ich werde dieses Notizbuch hier ablegen [https://gist.github.com/gyu-don/f5cc025139312ccfd39e48400018118d)

Recommended Posts

Schneiden Sie das PyTorch-Berechnungsdiagramm aus
[PyTorch] Beispiel ⑨ ~ Dynamischer Graph ~
Verknüpfte Komponenten des Diagramms
Lernen Sie mit PyTorch Graph Convolutional Networks
Anrufdiagramm mit PyCallGraph ausgeben
Schneiden wir das Gesicht aus dem Bild