Grokay PyTorch

Hallo Habr!

Wir haben ein lang erwartetes Buch über die PyTorch-Bibliothek vorbestellt .



Da Sie in diesem Buch alle erforderlichen Grundmaterialien zu PyTorch kennenlernen, erinnern wir Sie an die Vorteile eines Prozesses namens „Grokking“ oder „tiefgreifendes Verständnis“ des Themas, das Sie lernen möchten. Im heutigen Beitrag werden wir Ihnen erzählen, wie Kai Arulkumaran PyTorch zugeschlagen hat (kein Bild). Willkommen bei Katze.

PyTorch ist ein flexibles Deep-Learning-Framework, das automatisch zwischen Objekten unterscheidet, die dynamische neuronale Netzwerke verwenden ( dh Netzwerke, die eine dynamische Flusssteuerung verwenden, z. B. if und while Schleifen). PyTorch unterstützt GPU-Beschleunigung, verteiltes Training , verschiedene Arten der Optimierung und viele andere nette Funktionen. Hier habe ich einige Gedanken darüber gemacht, wie PyTorch meiner Meinung nach verwendet werden sollte. Alle Aspekte der Bibliothek und empfohlene Vorgehensweisen werden hier nicht behandelt, aber ich hoffe, dieser Text wird Ihnen nützlich sein.

Neuronale Netze sind eine Unterklasse von Rechengraphen. Berechnungsgraphen empfangen Daten als Eingabe, dann werden diese Daten an den Knoten weitergeleitet (und können konvertiert werden), an denen sie verarbeitet werden. Beim Deep Learning transformieren Neuronen (Knoten) normalerweise Daten, indem sie Parameter und differenzierbare Funktionen auf sie anwenden, so dass die Parameter optimiert werden können, um Verluste durch die Gradientenabstiegsmethode zu minimieren. Im weiteren Sinne stelle ich fest, dass Funktionen stochastisch und graphendynamisch sein können. Während neuronale Netze gut zum Paradigma der Datenflussprogrammierung passen, konzentriert sich die PyTorch-API auf das Paradigma der imperativen Programmierung , und diese Art der Interpretation der erstellten Programme ist viel vertrauter. Aus diesem Grund ist PyTorch-Code einfacher zu lesen und das Design komplexer Programme zu beurteilen. Dies erfordert jedoch keine ernsthaften Kompromisse bei der Leistung: PyTorch ist schnell genug und bietet viele Optimierungen, über die Sie sich als Endbenutzer überhaupt keine Sorgen machen können (Wenn Sie sich jedoch wirklich für sie interessieren, können Sie etwas tiefer graben und sie kennenlernen).

Der Rest dieses Artikels ist eine Analyse des offiziellen Beispiels für den MNIST-Datensatz . Hier spielen wir PyTorch, daher empfehle ich, den Artikel erst nach Kenntnis der offiziellen Anfängerhandbücher zu verstehen. Der Einfachheit halber wird der Code in Form kleiner Fragmente mit Kommentaren dargestellt, dh er wird nicht in separate Funktionen / Dateien verteilt, die Sie in rein modularem Code gewohnt sind.

Importe


 import argparse import os import torch from torch import nn, optim from torch.nn import functional as F from torch.utils.data import DataLoader from torchvision import datasets, transforms 

All dies sind Standardimporte, mit Ausnahme von torchvision , die besonders aktiv zur Lösung von Aufgaben im Zusammenhang mit Computer Vision eingesetzt werden.

Anpassung


 parser = argparse.ArgumentParser(description='PyTorch MNIST Example') parser.add_argument('--batch-size', type=int, default=64, metavar='N', help='input batch size for training (default: 64)') parser.add_argument('--epochs', type=int, default=10, metavar='N', help='number of epochs to train (default: 10)') parser.add_argument('--lr', type=float, default=0.01, metavar='LR', help='learning rate (default: 0.01)') parser.add_argument('--momentum', type=float, default=0.5, metavar='M', help='SGD momentum (default: 0.5)') parser.add_argument('--no-cuda', action='store_true', default=False, help='disables CUDA training') parser.add_argument('--seed', type=int, default=1, metavar='S', help='random seed (default: 1)') parser.add_argument('--save-interval', type=int, default=10, metavar='N', help='how many batches to wait before checkpointing') parser.add_argument('--resume', action='store_true', default=False, help='resume training from checkpoint') args = parser.parse_args() use_cuda = torch.cuda.is_available() and not args.no_cuda device = torch.device('cuda' if use_cuda else 'cpu') torch.manual_seed(args.seed) if use_cuda: torch.cuda.manual_seed(args.seed) 

argparse ist die Standardmethode zum argparse Befehlszeilenargumenten in Python.

Wenn Sie Code schreiben müssen, der für die Arbeit auf verschiedenen Geräten ausgelegt ist (mithilfe der GPU-Beschleunigung, sofern verfügbar, aber nicht auf Berechnungen auf der CPU zurückgesetzt), wählen Sie das entsprechende torch.device und speichern torch.device , mit dem Sie festlegen können, wo Sie es verwenden sollen Tensoren werden gespeichert. Weitere Informationen zum Erstellen eines solchen Codes finden Sie in der offiziellen Dokumentation . Der Ansatz von PyTorch besteht darin, die Auswahl der Geräte der Benutzersteuerung zu unterziehen, was in einfachen Beispielen unerwünscht erscheinen kann. Dieser Ansatz vereinfacht jedoch die Arbeit erheblich, wenn Sie sich mit Tensoren befassen müssen. A) ist praktisch für das Debuggen, b) ermöglicht es Ihnen, Geräte effektiv manuell zu verwenden.

Um die Reproduzierbarkeit von Experimenten zu numpy , müssen Sie zufällige Anfangswerte für alle Komponenten festlegen, die eine Zufallszahlengenerierung verwenden (einschließlich random oder numpy , wenn Sie diese numpy ). Bitte beachten Sie: cuDNN verwendet nicht deterministische Algorithmen und ist optional mit torch.backends.cudnn.enabled = False deaktiviert.

Daten


 data_path = os.path.join(os.path.expanduser('~'), '.torch', 'datasets', 'mnist') train_data = datasets.MNIST(data_path, train=True, download=True, transform=transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])) test_data = datasets.MNIST(data_path, train=False, transform=transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])) train_loader = DataLoader(train_data, batch_size=args.batch_size, shuffle=True, num_workers=4, pin_memory=True) test_loader = DataLoader(test_data, batch_size=args.batch_size, num_workers=4, pin_memory=True) 


Da torchvision Modelle unter ~/.torch/models/ gespeichert torchvision , bevorzuge ich es, Torchvision- torchvision unter ~/.torch/datasets zu speichern. Dies ist meine Urheberrechtsvereinbarung, aber es ist sehr praktisch, sie in Projekten zu verwenden, die auf der Grundlage von MNIST, CIFAR-10 usw. entwickelt wurden. Im Allgemeinen sollten Datasets getrennt vom Code gespeichert werden, wenn Sie mehrere Datasets wiederverwenden möchten.

torchvision.transforms enthält viele praktische Konvertierungsoptionen für einzelne Bilder, z. B. Zuschneiden und Normalisieren.

Es gibt viele Optionen im batch_size , aber neben batch_size und shuffle sollten Sie auch num_workers und pin_memory , um die Effizienz zu steigern. num_workers > 0 verwendet num_workers > 0 zum asynchronen Laden von Daten und blockiert den Hauptprozess dafür nicht. Ein typischer Anwendungsfall besteht darin, Daten (z. B. Bilder) von einer Festplatte zu laden und möglicherweise zu konvertieren. All dies kann parallel zur Netzwerkdatenverarbeitung erfolgen. Der Verarbeitungsgrad muss möglicherweise angepasst werden, um a) die Anzahl der Mitarbeiter und folglich die Menge der verwendeten CPU und des verwendeten Arbeitsspeichers zu minimieren (jeder Mitarbeiter lädt einen separaten Stapel anstelle einzelner im Stapel enthaltener Stichproben). B) die Wartezeit der Daten im Netzwerk zu minimieren. pin_memory verwendet pin_memory Speicher (im Gegensatz zu gepumptem), um Datenübertragungsvorgänge vom RAM zur GPU zu beschleunigen (und macht nichts mit CPU-spezifischem Code).

Modell


 class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 10, kernel_size=5) self.conv2 = nn.Conv2d(10, 20, kernel_size=5) self.conv2_drop = nn.Dropout2d() self.fc1 = nn.Linear(320, 50) self.fc2 = nn.Linear(50, 10) def forward(self, x): x = F.relu(F.max_pool2d(self.conv1(x), 2)) x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2)) x = x.view(-1, 320) x = F.relu(self.fc1(x)) x = self.fc2(x) return F.log_softmax(x, dim=1) model = Net().to(device) optimiser = optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum) if args.resume: model.load_state_dict(torch.load('model.pth')) optimiser.load_state_dict(torch.load('optimiser.pth')) 

Die Netzwerkinitialisierung erstreckt sich normalerweise auf Mitgliedsvariablen, Ebenen, die Lernparameter und möglicherweise einzelne Lernparameter und nicht trainierte Puffer enthalten. Bei einem direkten Durchlauf werden sie dann in Kombination mit Funktionen von F , die rein funktional sind und keine Parameter enthalten. Einige Leute arbeiten gerne mit rein funktionalen Netzwerken (z. B. Parameter F.conv2d und F.conv2d anstelle von nn.Conv2d ) oder Netzwerken, die vollständig aus Schichten bestehen (z. B. nn.ReLU anstelle von F.relu ).

.to(device) ist eine bequeme Möglichkeit, Geräteparameter (und Puffer) an die GPU zu senden, wenn das device auf GPU eingestellt ist, da sonst (wenn das Gerät auf CPU eingestellt ist) nichts unternommen wird. Es ist wichtig, die Geräteparameter auf das entsprechende Gerät zu übertragen, bevor Sie sie an den Optimierer übergeben. Andernfalls kann der Optimierer die Parameter nicht korrekt verfolgen!

Sowohl neuronale Netze ( nn.Module ) als auch Optimierer ( optim.Optimizer ) können ihren internen Status speichern und laden. Es wird empfohlen, dies mit .load_state_dict(state_dict) zu tun. Es ist erforderlich, den Status beider .load_state_dict(state_dict) zu laden, um das Training basierend auf zuvor gespeicherten Wörterbüchern .load_state_dict(state_dict) Staaten. Das Speichern des gesamten Objekts kann mit Fehlern behaftet sein . Wenn Sie die Tensoren auf der GPU gespeichert haben und sie auf die CPU oder eine andere GPU laden möchten, können Sie sie am einfachsten mit der Option torch.load('model.pth' direkt auf die CPU map_location , z. B. torch.load('model.pth' , map_location='cpu' ).

Hier sind einige andere Punkte, die hier nicht gezeigt werden, aber erwähnenswert sind, dass Sie den Kontrollfluss mit einem direkten Durchlauf verwenden können (zum Beispiel kann die Ausführung der if von der Mitgliedsvariablen oder von den Daten selbst abhängen. Darüber hinaus ist sie in der Mitte des auszugebenden Prozesses vollkommen gültig ( print ) Tensoren, was das Debuggen erheblich vereinfacht. Schließlich können mit einem direkten Durchlauf viele Argumente verwendet werden. Ich werde diesen Punkt mit einer kurzen Auflistung veranschaulichen, die nicht an eine bestimmte Idee gebunden ist:

 def forward(self, x, hx, drop=False): hx2 = self.rnn(x, hx) print(hx.mean().item(), hx.var().item()) if hx.max.item() > 10 or self.can_drop and drop: return hx else: return hx2 

Schulung


 model.train() train_losses = [] for i, (data, target) in enumerate(train_loader): data = data.to(device=device, non_blocking=True) target = target.to(device=device, non_blocking=True) optimiser.zero_grad() output = model(data) loss = F.nll_loss(output, target) loss.backward() train_losses.append(loss.item()) optimiser.step() if i % 10 == 0: print(i, loss.item()) torch.save(model.state_dict(), 'model.pth') torch.save(optimiser.state_dict(), 'optimiser.pth') torch.save(train_losses, 'train_losses.pth') 

Netzwerkmodule werden standardmäßig in den Trainingsmodus versetzt - was sich in gewissem Maße auf den Betrieb von Modulen auswirkt, vor allem auf das Ausdünnen und die Chargennormalisierung. Auf die eine oder andere Weise ist es besser, solche Dinge manuell mit .train() , wodurch das Flag "Training" für alle .train() Module gefiltert wird.

Hier akzeptiert die .to() -Methode nicht nur das Gerät, sondern setzt auch non_blocking=True , wodurch ein asynchrones Kopieren von Daten aus dem festgeschriebenen Speicher auf die GPU sichergestellt wird, sodass die CPU während der Datenübertragung betriebsbereit bleibt. Andernfalls ist non_blocking=True einfach keine Option.

Bevor Sie mit loss.backward() einen neuen Satz von Verläufen loss.backward() und mit loss.backward() optimiser.step() , müssen Sie die Verläufe der zu optimierenden Parameter manuell mit optimiser.step() zurücksetzen. Standardmäßig sammelt PyTorch Farbverläufe. Dies ist sehr praktisch, wenn Sie nicht über genügend Ressourcen verfügen, um alle benötigten Farbverläufe in einem Durchgang zu berechnen.

PyTorch verwendet ein "Band" -System mit automatischen Verläufen - es sammelt Informationen darüber, welche Operationen und in welcher Reihenfolge an den Tensoren ausgeführt wurden, und spielt sie dann in die entgegengesetzte Richtung ab, um die Differenzierung in umgekehrter Reihenfolge durchzuführen (Differenzierung im umgekehrten Modus). Deshalb ist es so superflexibel und ermöglicht beliebige Rechengraphen. Wenn für keinen dieser Tensoren Farbverläufe erforderlich sind (Sie müssen requires_grad=True festlegen und zu diesem Zweck einen Tensor erstellen), wird kein Diagramm gespeichert! Netzwerke haben jedoch normalerweise Parameter, die Gradienten erfordern, sodass alle Berechnungen, die auf der Grundlage der Netzwerkausgabe durchgeführt werden, in der Grafik gespeichert werden. Wenn Sie also die aus diesem Schritt resultierenden Daten speichern möchten, müssen Sie die Verläufe manuell deaktivieren oder (ein häufigerer Ansatz) diese Informationen als Python-Nummer (mit .item() im PyTorch-Skalar) oder als numpy Array numpy . Lesen Sie mehr über autograd in der offiziellen Dokumentation .

Eine Möglichkeit, den Rechengraphen zu verkürzen, besteht darin, .detach() wenn der verborgene Zustand beim Lernen von RNN mit einer abgeschnittenen Version der Backpropagation-through-Time übergeben wird. Es ist auch praktisch, wenn Verluste unterschieden werden, wenn eine der Komponenten die Ausgabe eines anderen Netzwerks ist, dieses andere Netzwerk jedoch nicht in Bezug auf Verluste optimiert werden sollte. Als Beispiel werde ich den diskriminierenden Teil des bei der Arbeit mit dem GAN erzeugten Ausgabematerials oder das Richtlinientraining im Akteur-Kritiker-Algorithmus unter Verwendung der Zielfunktion als Basisfunktion (z. B. A2C) unterrichten. Eine andere Technik, die die Berechnung von Gradienten verhindert, ist beim Training von GAN (Training des erzeugenden Teils auf diskriminantem Material) effektiv und typisch für die Feinabstimmung ist die zyklische Aufzählung von Netzwerkparametern, für die param.requires_grad = False .

Es ist wichtig, nicht nur die Ergebnisse in der Konsolen- / Protokolldatei aufzuzeichnen, sondern auch Kontrollpunkte in den Modellparametern (und im Optimierungsstatus) für alle Fälle festzulegen. Sie können auch torch.save() , um reguläre Python-Objekte zu speichern, oder eine andere Standardlösung verwenden - die integrierte pickle .

Testen


 model.eval() test_loss, correct = 0, 0 with torch.no_grad(): for data, target in test_loader: data = data.to(device=device, non_blocking=True) target = target.to(device=device, non_blocking=True) output = model(data) test_loss += F.nll_loss(output, target, reduction='sum').item() pred = output.argmax(1, keepdim=True) correct += pred.eq(target.view_as(pred)).sum().item() test_loss /= len(test_data) acc = correct / len(test_data) print(acc, test_loss) 

In Reaktion auf .train() Netzwerke mit .eval() explizit in den Evaluierungsmodus versetzt werden.

Wie oben erwähnt, wird bei Verwendung eines Netzwerks normalerweise ein Berechnungsgraph erstellt. Um dies zu verhindern, verwenden Sie den no_grad with torch.no_grad() .

Noch mehr


Dies ist ein zusätzlicher Abschnitt, in dem ich einige weitere nützliche Abschweifungen vorgenommen habe.
Hier ist die offizielle Dokumentation , die das Arbeiten mit dem Gedächtnis erklärt.

CUDA-Fehler? Es ist schwierig, sie zu beheben, und normalerweise sind sie mit logischen Inkonsistenzen verbunden, nach denen auf der CPU sinnvollere Fehlermeldungen angezeigt werden als auf der GPU. Das Beste ist, wenn Sie mit der GPU arbeiten möchten, können Sie schnell zwischen CPU und GPU wechseln. Ein allgemeinerer Entwicklungstipp besteht darin, den Code so zu organisieren, dass er schnell überprüft werden kann, bevor eine vollständige Aufgabe gestartet wird. Bereiten Sie beispielsweise einen kleinen oder synthetischen Datensatz vor, führen Sie einen Zug + Test für eine Ära aus usw. Wenn es sich um einen CUDA-Fehler handelt oder Sie überhaupt nicht zur CPU wechseln können, setzen Sie CUDA_LAUNCH_BLOCKING = 1. Dadurch wird der CUDA-Kernel synchron gestartet und Sie erhalten genauere Fehlermeldungen.

Ein Hinweis zu torch.multiprocessing oder zum gleichzeitigen Ausführen mehrerer PyTorch-Skripte. Da PyTorch BLAS-Bibliotheken mit mehreren Threads verwendet, um lineare Algebra-Berechnungen auf der CPU zu beschleunigen, sind normalerweise mehrere Kerne beteiligt. Wenn Sie mehrere Dinge gleichzeitig OMP_NUM_THREADS möchten, indem Sie eine Multithread-Verarbeitung oder mehrere Skripte verwenden, kann es ratsam sein, deren Anzahl manuell zu reduzieren, indem Sie die Umgebungsvariable OMP_NUM_THREADS auf 1 oder einen anderen niedrigen Wert setzen. Somit wird die Wahrscheinlichkeit eines Durchrutschens des Prozessors verringert. Die offizielle Dokumentation enthält weitere Kommentare zur Multithread-Verarbeitung.

Source: https://habr.com/ru/post/de471228/


All Articles