Grokay PyTorch

Bonjour, Habr!

Nous avons en pré-commande un livre tant attendu sur la bibliothèque PyTorch .



Puisque vous apprendrez tout le matériel de base nécessaire sur PyTorch dans ce livre, nous vous rappelons les avantages d'un processus appelé «grokking» ou «compréhension approfondie» du sujet que vous souhaitez apprendre. Dans le post d'aujourd'hui, nous vous expliquerons comment Kai Arulkumaran a critiqué PyTorch (pas d'image). Bienvenue au chat.

PyTorch est un cadre d'apprentissage en profondeur flexible qui distingue automatiquement les objets utilisant des réseaux de neurones dynamiques (c'est-à-dire les réseaux utilisant le contrôle de flux dynamique, comme les if et les boucles while ). PyTorch prend en charge l'accélération GPU, la formation distribuée , divers types d' optimisation et de nombreuses autres fonctionnalités intéressantes. Ici, je présente quelques réflexions sur la façon, à mon avis, d'utiliser PyTorch; tous les aspects de la bibliothèque et les pratiques recommandées ne sont pas traités ici, mais j'espère que ce texte vous sera utile.

Les réseaux de neurones sont une sous-classe de graphiques de calcul. Les graphiques de calcul reçoivent des données en entrée, puis ces données sont acheminées (et peuvent être converties) aux nœuds où elles sont traitées. En apprentissage profond, les neurones (nœuds) transforment généralement les données en leur appliquant des paramètres et des fonctions différenciables, de sorte que les paramètres peuvent être optimisés pour minimiser les pertes par la méthode de descente de gradient. Dans un sens plus large, je note que les fonctions peuvent être stochastiques et un graphique dynamique. Ainsi, alors que les réseaux de neurones correspondent bien au paradigme de programmation de flux de données, l'API PyTorch se concentre sur le paradigme de programmation impératif , et cette façon d'interpréter les programmes créés est beaucoup plus familière. C'est pourquoi le code PyTorch est plus facile à lire, il est plus facile de juger de la conception de programmes complexes, qui, cependant, ne nécessitent pas de compromis sérieux sur les performances: en fait, PyTorch est assez rapide et fournit de nombreuses optimisations dont vous, en tant qu'utilisateur final, ne pouvez pas vous inquiéter du tout (cependant, si vous êtes vraiment intéressé par eux, vous pouvez creuser un peu plus et apprendre à les connaître).

Le reste de cet article est une analyse de l' exemple officiel du jeu de données MNIST . Ici, nous jouons PyTorch, donc je recommande de comprendre l'article uniquement après avoir pris connaissance des manuels officiels du débutant . Pour plus de commodité, le code est présenté sous la forme de petits fragments équipés de commentaires, c'est-à-dire qu'il n'est pas distribué dans des fonctions / fichiers séparés que vous avez l'habitude de voir dans du code modulaire pur.

Importations


 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 

Tous ces éléments sont des importations tout à fait standard, à l'exception des modules torchvision , qui sont particulièrement utilisés pour résoudre des tâches liées à la vision par ordinateur.

Personnalisation


 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 est la manière standard de gérer les arguments de ligne de commande en Python.

Si vous devez écrire du code conçu pour fonctionner sur différents appareils (en utilisant l'accélération GPU, quand il est disponible, mais s'il n'est pas restauré aux calculs sur le CPU), sélectionnez et enregistrez le torch.device approprié, avec lequel vous pouvez déterminer où vous devez les tenseurs sont stockés. Voir la documentation officielle pour plus de détails sur la création d'un tel code. L'approche de PyTorch est d'amener la sélection des appareils au contrôle de l'utilisateur, ce qui peut sembler indésirable dans des exemples simples. Cependant, cette approche simplifie considérablement le travail lorsque vous devez gérer des tenseurs, ce qui a) est pratique pour le débogage b) vous permet d'utiliser efficacement les périphériques manuellement.

Pour la reproductibilité des expériences, vous devez définir des valeurs initiales aléatoires pour tous les composants qui utilisent la génération de nombres aléatoires (y compris random ou numpy , si vous les utilisez numpy ). Remarque: cuDNN utilise des algorithmes non déterministes et est facultativement désactivé à l'aide de torch.backends.cudnn.enabled = False .

Les données


 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) 


Étant torchvision modèles torchvision stockés sous ~/.torch/models/ , je préfère stocker les jeux de torchvision torchvision sous ~/.torch/datasets . Ceci est mon accord de copyright, mais il est très pratique à utiliser dans des projets développés sur la base de MNIST, CIFAR-10, etc. En général, les ensembles de données doivent être stockés séparément du code si vous avez l'intention de réutiliser plusieurs ensembles de données.

torchvision.transforms contient de nombreuses options de conversion pratiques pour des images individuelles, telles que le recadrage et la normalisation.

Il existe de nombreuses options dans le batch_size , mais en plus de batch_size et shuffle , vous devez également garder à l'esprit num_workers et pin_memory , ils aident à augmenter l'efficacité. num_workers > 0 utilise des sous-processus pour le chargement asynchrone des données et ne bloque pas le processus principal pour cela. Un cas d'utilisation typique consiste à charger des données (par exemple, des images) à partir d'un disque et, éventuellement, à les convertir; tout cela peut être fait en parallèle, avec le traitement des données réseau. Le degré de traitement peut devoir être ajusté afin de a) minimiser le nombre de travailleurs et, par conséquent, la quantité de CPU et de RAM utilisée (chaque travailleur charge un lot séparé, plutôt que des échantillons individuels inclus dans le lot) b) minimiser la durée pendant laquelle les données attendent sur le réseau. pin_memory utilise la mémoire épinglée (par opposition à la pagination) pour accélérer toutes les opérations de transfert de données de la RAM vers le GPU (et ne fait rien avec le code spécifique au CPU).

Modèle


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

L'initialisation du réseau s'étend généralement aux variables membres, aux couches qui contiennent des paramètres d'apprentissage et, éventuellement, aux paramètres d'apprentissage individuels et aux tampons non formés. Ensuite, avec un passage direct, ils sont utilisés en combinaison avec des fonctions de F qui sont purement fonctionnelles et ne contiennent pas de paramètres. Certaines personnes aiment travailler avec des réseaux purement fonctionnels (par exemple, conserver les paramètres et utiliser F.conv2d au lieu de nn.Conv2d ) ou des réseaux entièrement constitués de couches (par exemple, nn.ReLU au lieu de F.relu ).

.to(device) est un moyen pratique d'envoyer des paramètres de périphérique (et des tampons) au GPU si le device défini sur GPU, car sinon (si le périphérique est défini sur CPU), rien ne sera fait. Il est important de transférer les paramètres du périphérique vers le périphérique approprié avant de les transmettre à l'optimiseur; sinon, l'optimiseur ne pourra pas suivre correctement les paramètres!

Les réseaux de neurones ( nn.Module ) et les optimiseurs ( optim.Optimizer ) peuvent enregistrer et charger leur état interne, et il est recommandé de le faire avec .load_state_dict(state_dict) - il est nécessaire de recharger l'état des deux afin de reprendre la formation sur la base des dictionnaires précédemment enregistrés états. L'enregistrement de l'objet entier peut être lourd d'erreurs . Si vous avez enregistré les tenseurs sur le GPU et que vous souhaitez les charger sur le CPU ou un autre GPU, alors le moyen le plus simple est de les charger directement sur le CPU en utilisant l' option map_location , par exemple torch.load('model.pth' , map_location='cpu' ).

Voici quelques autres points qui ne sont pas présentés ici, mais qui méritent d'être mentionnés, que vous pouvez utiliser le flux de contrôle avec un passage direct (par exemple, l'exécution de l' if peut dépendre de la variable membre ou des données elles-mêmes. En outre, elle est parfaitement valide au milieu du processus à afficher. ( print ) tensors, ce qui simplifie grandement le débogage. Enfin, avec un passage direct, beaucoup d'arguments peuvent être utilisés. J'illustrerai ce point avec une courte liste qui n'est liée à aucune idée particulière:

 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 

La formation


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

Les modules réseau sont mis en mode de formation par défaut - ce qui affecte dans une certaine mesure le fonctionnement des modules, surtout - l'amincissement et la normalisation des lots. D'une manière ou d'une autre, il est préférable de définir ces éléments manuellement à l'aide de .train() , qui filtre l'indicateur "training" sur tous les modules enfants.

Ici, la méthode .to() accepte non seulement le périphérique, mais définit également non_blocking=True , garantissant ainsi la copie asynchrone des données vers le GPU à partir de la mémoire non_blocking=True , permettant au CPU de rester opérationnel pendant le transfert de données; sinon, non_blocking=True tout simplement pas une option.

Avant de créer un nouvel ensemble de dégradés à l'aide de loss.backward() et de optimiser.step() en optimiser.step() aide d' optimiser.step() , vous devez réinitialiser manuellement les dégradés des paramètres à optimiser à l'aide d' optimiser.zero_grad() . Par défaut, PyTorch accumule les dégradés, ce qui est très pratique si vous ne disposez pas de suffisamment de ressources pour calculer tous les dégradés dont vous avez besoin en une seule passe.

PyTorch utilise un système de «bandes» de dégradés automatiques - il collecte des informations sur les opérations et l'ordre dans lesquels les tenseurs ont été effectués, puis les lit dans le sens opposé pour effectuer la différenciation dans l'ordre inverse (différenciation en mode inverse). C'est pourquoi il est si super flexible et permet des graphes de calcul arbitraires. Si aucun de ces tenseurs ne nécessite de dégradés (vous devez définir requires_grad=True , en créant un tenseur à cet effet), alors aucun graphique n'est enregistré! Cependant, les réseaux ont généralement des paramètres qui nécessitent des gradients, donc tous les calculs effectués sur la base de la sortie du réseau seront stockés dans le graphique. Donc, si vous souhaitez enregistrer les données résultant de cette étape, vous devrez désactiver manuellement les dégradés ou (une approche plus courante), enregistrer ces informations sous la forme d'un numéro Python (en utilisant .item() dans le scalaire PyTorch) ou un tableau numpy . En savoir plus sur autograd dans la documentation officielle .

Une façon de raccourcir le graphe de calcul consiste à utiliser .detach() lorsque l'état masqué est passé lors de l'apprentissage de RNN avec une version tronquée de rétropropagation dans le temps. Il est également pratique pour la différenciation des pertes, lorsque l'un des composants est la sortie d'un autre réseau, mais cet autre réseau ne doit pas être optimisé en ce qui concerne les pertes. À titre d'exemple, j'enseignerai la partie discriminatoire sur le matériel de sortie généré lors de la collaboration avec le GAN, ou la formation politique à l'algorithme acteur-critique en utilisant la fonction objectif comme fonction de base (par exemple, A2C). Une autre technique qui empêche le calcul des gradients est efficace dans la formation du GAN (formation de la partie génératrice sur un matériau discriminant) et typique dans le réglage fin est l'énumération cyclique des paramètres de réseau pour lesquels param.requires_grad = False .

Il est important non seulement d'enregistrer les résultats dans le fichier console / journal, mais également de définir des points de contrôle dans les paramètres du modèle (et l'état de l'optimiseur) au cas où. Vous pouvez également utiliser torch.save() pour enregistrer des objets Python normaux, ou utiliser une autre solution standard - le pickle intégré.

Test


 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) 

En réponse à .train() réseaux doivent être explicitement mis en mode d'évaluation à l'aide de .eval() .

Comme mentionné ci-dessus, lors de l'utilisation d'un réseau, un graphe de calcul est généralement compilé. Pour éviter cela, utilisez le no_grad contexte no_grad avec with torch.no_grad() .

Un peu plus


Ceci est une section supplémentaire dans laquelle j'ai fait quelques digressions plus utiles.
Voici la documentation officielle expliquant l'utilisation de la mémoire.

Erreurs CUDA? Il est difficile de les corriger, et ils sont généralement liés à des incohérences logiques, selon lesquelles des messages d'erreur plus sensibles sont affichés sur le processeur que sur le GPU. Mieux encore, si vous prévoyez de travailler avec le GPU, vous pouvez rapidement basculer entre le CPU et le GPU. Une astuce de développement plus générale consiste à organiser le code afin qu'il puisse être rapidement vérifié avant de commencer une tâche à part entière. Par exemple, préparez un petit ensemble de données ou synthétique, exécutez un train de l'époque + test, etc. Si le problème est une erreur CUDA ou si vous ne pouvez pas du tout basculer vers le CPU, définissez CUDA_LAUNCH_BLOCKING = 1. Cela rendra le lancement du noyau CUDA synchrone et vous recevrez des messages d'erreur plus précis.

Une note sur torch.multiprocessing ou simplement exécuter plusieurs scripts PyTorch en même temps. Étant donné que PyTorch utilise des bibliothèques BLAS multithread pour accélérer les calculs d'algèbre linéaire sur le processeur, plusieurs cœurs sont généralement impliqués. Si vous souhaitez faire plusieurs choses en même temps, en utilisant un traitement multithread ou plusieurs scripts, il peut être conseillé de réduire manuellement leur nombre en définissant la variable d'environnement OMP_NUM_THREADS sur 1 ou une autre valeur faible. Ainsi, la probabilité de glissement du processeur est réduite. La documentation officielle contient d'autres commentaires concernant le traitement multithread.

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


All Articles