PyTorch C ++ VS Python (édition 2019)

Il existe de nombreux types de frameworks Deep Learning tels que PyTorch, Tensorflow et Keras. Cette fois, je ferai attention à ** PyTorch **, que j'utilise souvent!

Saviez-vous que la ** version C ++ a été publiée ** ainsi que la version PyTorch et Python? Cela facilite l'intégration si vous souhaitez utiliser le Deep Learning dans le cadre du traitement de votre programme C ++!

Une telle version C ++ de PyTorch, je me demandais ** "C ++ est un langage compilé, alors peut-être est-il plus rapide que la version Python?" **.

Donc, cette fois, j'ai en fait étudié ** "Quelle est la différence de vitesse entre C ++ et Python?" </ Font> **! De plus, j'étais préoccupé par la précision, alors je l'ai vérifié.

Quoi utiliser pour les expériences comparatives

1. Cadre

Cette fois, comme le titre l'indique, nous utiliserons la version C ++ de "PyTorch". Vous pouvez le télécharger sur le site suivant, alors essayez-le!

Officiel PyTorch: https://pytorch.org/ libtorch.png

Je l'ai téléchargé avec les paramètres ci-dessus. La «version Preview (Nightly)» contient toujours les derniers fichiers. Cependant, il est toujours en développement, donc si vous souhaitez utiliser la version stable, sélectionnez "Stable (1.4)".

De plus, «Exécuter cette commande» en bas est assez important, et si vous avez une version de construction de CXX de 11 ou plus, nous vous recommandons de sélectionner celle du bas. Actuellement, c'est presque CXX17, donc je pense que ça va ci-dessous. Si vous sélectionnez ce qui précède, des erreurs de lien d'autres bibliothèques se produiront et ce sera beaucoup de problèmes.

2. Modèle

Cette fois, nous utiliserons ** autoencoder convolutif </ font> ** (Convolutional Autoencoder). Disponible sur mon GitHub → https://github.com/koba-jon/pytorch_cpp

Ce modèle mappe ** l'image d'entrée (haute dimension) ** à ** l'espace latent (faible dimension) **, et cette fois en fonction de cette ** variable latente (faible dimension) ** ** image ( Le but est de générer de grandes dimensions) ** et de minimiser l'erreur entre celle-ci et l'image d'entrée. Après la formation, ce modèle peut générer à nouveau une image de grande dimension à partir d'une image de grande dimension à travers un espace de faible dimension, de sorte que ** un espace latent qui caractérise davantage l'image de formation ** puisse être obtenu. En d'autres termes, elle a le rôle de compression de dimension et peut être appelée analyse en composantes principales non linéaire. Ceci est très pratique car il a diverses utilisations telles que ** élimination de la malédiction dimensionnelle **, ** apprentissage par transfert ** et ** détection d'anomalie **.

Maintenant, je vais vous expliquer la structure du modèle à utiliser.

  • La taille de l'image est divisée par deux avec une convolution et double avec une convolution inversée
  • Stabilisation des apprentissages et accélération de la convergence
  • La plage possible de variables latentes est (-∞, + ∞)
  • La plage de valeurs de pixel possible est [-1, +1]

Dans l'attente de ces effets, nous avons construit le réseau suivant.

Operation Kernel Size Stride Padding Bias Feature Map BN Activation
Input Output
1 Convolution 4 2 1 False 3 64 ReLU
2 64 128 True ReLU
3 128 256 True ReLU
4 256 512 True ReLU
5 512 512 True ReLU
6 512 512
7 Transposed Convolution 512 512 True ReLU
8 512 512 True ReLU
9 512 256 True ReLU
10 256 128 True ReLU
11 128 64 True ReLU
12 64 3 tanh

3. Ensemble de données

  • Jeu de données CelebA (attributs CelebFaces à grande échelle)
    http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html

Cette fois, nous utiliserons l'ensemble de données CelebA, qui est une collection de 202599 images de visages de célébrités (couleur). La taille de l'image est de 178 x 218 [pixel], ce qui cause des inconvénients lors du pliage inversé, donc cette fois je l'ai redimensionnée en ** 64 x 64 [pixel] **. Parmi ceux-ci, ** 90% (182 340) ont été utilisés pour l'apprentissage d'images **, et ** 10% (20 259) ont été utilisés pour des images de test **.

Lorsqu'il est entré dans le modèle précédent, l'espace latent devient (C, H, W) = (512,1,1). Si vous entrez une image de 128 x 128 [pixels] ou plus, la couche intermédiaire devient un espace latent spatial.

Comparaison

Cette fois, je vais principalement étudier ** "Quelle est la différence de vitesse entre C ++ et Python" **, mais j'aimerais comparer la vitesse et les performances dans les 5 types d'environnements suivants. ..

  • Fonctionnement principal du processeur
    • Python
    • C++
  • Fonctionnement principal du GPU
    • Python
  • Non déterministe --Décisif
    • C++

1. Différence dans l'unité principale (CPU ou GPU)

  • CPU
    Bonne gestion des instructions "série" et "complexes"
  • GPU
    Bon pour traiter des instructions «parallèles» et «simples»

Comme le montrent les fonctionnalités ci-dessus, dans le Deep Learning qui gère les images, le GPU est extrêmement avantageux en termes de vitesse de calcul.

(1) Implémentation par Python

  • Lors de l'utilisation du processeur

CPU.py


device = torch.device('cpu')  #Utiliser le processeur

model.to(device)  #Déplacer le modèle vers le processeur
image = image.to(device)  #Déplacer les données vers le processeur
  • Lors de l'utilisation du GPU

GPU.py


device = torch.device('cuda')    #Utiliser le GPU par défaut
device = torch.device('cuda:0')  #Utilisez le premier GPU
device = torch.device('cuda:1')  #Utilisez le deuxième GPU

model.to(device)  #Déplacer le modèle vers le GPU
image = image.to(device)  #Déplacer les données vers le GPU

(2) Implémentation par C ++

  • Lors de l'utilisation du processeur

CPU.cpp


torch::Device device(torch::kCPU);  //Utiliser le processeur

model->to(device);  //Déplacer le modèle vers le processeur
image = image.to(device);  //Déplacer les données vers le processeur
  • Lors de l'utilisation du GPU

GPU.cpp


torch::Device device(torch::kCUDA);     //Utiliser le GPU par défaut
torch::Device device(torch::kCUDA, 0);  //Utilisez le premier GPU
torch::Device device(torch::kCUDA, 1);  //Utilisez le deuxième GPU

model->to(device);  //Déplacer le modèle vers le GPU
image = image.to(device);  //Déplacer les données vers le GPU

2. Différence entre déterministe et non déterministe (opération principale GPU et Python uniquement)

Dans la version Python de PyTorch, dans le cas de l'apprentissage par GPU, cuDNN est utilisé pour ** améliorer la vitesse d'apprentissage **.

Cependant, contrairement au C ++, ce n'est pas parce que la vitesse d'apprentissage est améliorée que la même situation peut être reproduite en tournant à nouveau l'apprentissage.

Par conséquent, la formule PyTorch indique que pour assurer la reproductibilité, il est nécessaire de rendre le comportement de cuDNN déterministe comme suit, et en même temps, la vitesse diminue.

https://pytorch.org/docs/stable/notes/randomness.html

Deterministic mode can have a performance impact, depending on your model. This means that due to the deterministic nature of the model, the processing speed (i.e. processed batch items per second) can be lower than when the model is non-deterministic.

Du point de vue de l'ingénieur, je l'ai inclus dans cette comparaison de vitesse parce que je peux être préoccupé par la présence ou l'absence de reproductibilité, et parce que la vitesse change en fonction de la présence ou de l'absence de reproductibilité.

Contrairement à la fonction "rand" de C ++, si vous ne définissez aucune valeur initiale du nombre aléatoire, ce sera aléatoire, donc afin d'assurer la reproductibilité en Python, ** explicitement ** la valeur initiale du nombre aléatoire Vous devez le définir. (La définition de la valeur initiale du nombre aléatoire n'affecte pas la vitesse.)

La mise en œuvre est la suivante.

  • Cas définitif

deterministic.py


seed = 0
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
np.random.seed(seed)
random.seed(seed)
torch.backends.cudnn.deterministic = True  #Définitif au lieu de ralentir
torch.backends.cudnn.benchmark = False     #Définitif au lieu de ralentir
  • Cas non déterministe

non_deterministic.py


torch.backends.cudnn.deterministic = False  #Plus rapide que non déterministe
torch.backends.cudnn.benchmark = True       #Accélérez lorsque la taille de l'image ne change pas

3. Différences de mise en œuvre entre les langages de programmation

Même si le contenu que vous souhaitez implémenter est le même, si le langage de programmation change, ** la notation et les règles ** peuvent changer et les ** bibliothèques requises ** peuvent changer. Comme Python et C ++ sont des langages orientés objet, les concepts eux-mêmes sont similaires, mais surtout, puisque Python est un type d'interpréteur et C ++ est un type de compilation, il doit être implémenté étant donné que le typage dynamique ne fonctionne pas pour C ++. Il faut également tenir compte du fait que certaines fonctionnalités ne sont pas disponibles car l'API C ++ de PyTorch est actuellement en cours de développement.

Sur la base de ces points, je présenterai la différence d'implémentation entre Python et C ++, et le programme que j'ai implémenté.

(1) Utilisation de la bibliothèque

En plus de l'état d'utilisation de la bibliothèque Python qui est généralement écrite à l'heure actuelle, la bibliothèque recommandée pour l'implémentation en C ++ et l'état d'utilisation de la bibliothèque du programme que j'ai réellement écrit sont également décrits.

Python (recommandé) C++(Recommandation) C++(fait maison)
Gestion des arguments de ligne de commande argparse boost::program_options boost::program_options
Conception de modèle torch.nn torch::nn torch::nn
Prétraitement (transformation) torchvision.transforms torch::data::transformations (pour divers pré-traitements avant exécution)
or
Self-made (lors de divers prétraitements après l'exécution)
Fait soi-même (en utilisant OpenCV)
Obtenir des ensembles de données (ensembles de données) torchvision.ensembles de données (utilisant Pillow) Fait soi-même (en utilisant OpenCV) Fait soi-même (en utilisant OpenCV)
Chargeur de données torch.utils.data.DataLoader torch::data::make_data_chargeur (pour classification)
or
Fabrication artisanale (autre que classification)
Fait soi-même (en utilisant OpenMP)
Fonction de perte (perte) torch.nn torch::nn torch::nn
Méthode d'optimisation (optimiseur) torch.optim torch::optim torch::optim
Méthode de propagation de l'erreur arrière (arrière) torch.Tensor.backward() torch::Tensor::backward() torch::Tensor::backward()
barre de progression tqdm boost fait maison

** Pour le moment (2020/03/24) </ font> **, cela ressemble à ce qui précède.

Lors de l'utilisation de la bibliothèque PyTorch en C ++, les noms de classe et les noms de fonction sont presque les mêmes que Python. Cela semble être dû à la considération de l'utilisateur du côté du producteur. Je suis très reconnaissant!

Ensuite, je décrirai les points auxquels vous devez faire particulièrement attention lors de l'écriture de programmes PyTorch en C ++.

(2) Conception du modèle

Ce qui suit est un extrait d'une partie du programme que j'ai écrit.

networks.hpp (extrait partiel)


using namespace torch;
namespace po = boost::program_options;

struct ConvolutionalAutoEncoderImpl : nn::Module{
private:
    nn::Sequential encoder, decoder;
public:
    ConvolutionalAutoEncoderImpl(po::Variables_map &vm);
    torch::Tensor forward(torch::Tensor x);
}

TORCH_MODULE(ConvolutionalAutoEncoder);

Lors de la conception d'un modèle, utilisez la classe "torch :: nn" comme en Python. De plus, lors de la création d'un modèle, utilisez une structure. (Il existe aussi une version classe, mais cela semble un peu compliqué) À ce stade, il convient de noter que ** nn :: Module est hérité </ font> ** comme en Python. C'est la même chose que la façon d'écrire Python.

La prochaine chose importante est de nommer la ** structure "[model name] Impl" </ font> ** et sous la structure ** Ajoutez "TORCH_MODULE ([nom du modèle])" </ font> **. Si vous ne le faites pas, vous ne pourrez pas enregistrer ni charger le modèle. De plus, en définissant "TORCH_MODULE ([nom du modèle])", la structure ordinaire "ConvolutionalAutoEncoderImpl" peut être déclarée comme structure du modèle "ConvolutionalAutoEncoder", mais probablement en héritant de la classe en interne. Êtes-vous là? (Attendu) Par conséquent, lors de l'accès aux variables membres, telles que "model-> to (device)", ** "->" (opérateur de flèche) </ font> ** Veuillez noter que vous devez utiliser.

Ensuite, concernant les points ci-dessus, nous expliquerons les points à noter lors de l'utilisation du module de classe nn. Vous pouvez utiliser "nn :: Sequential" comme en Python. Pour ajouter un module à "nn :: Sequential" en C ++, utilisez ** "push_back" </ font> ** comme le type de vecteur. Notez que vous devez utiliser ** "->" (opérateur de flèche) </ font> ** pour appeler la fonction "push_back". L'exemple d'implémentation ressemble à ce qui suit.

networks.cpp (extrait partiel / modification)


nn::Sequential sq;
sq->push_back(nn::Conv2d(nn::Conv2dOptions(3, 64, /*kernel_size=*/4).stride(2).padding(1).bias(false)));
sq->push_back(nn::BatchNorm2d(64));
sq->push_back(nn::ReLU(nn::ReLUOptions().inplace(true)));

(3) Transformation / jeux de données / chargeur de données

Lors de la création de la transformation, des ensembles de données et du chargeur de données par vous-même, ** ".clone ()" est utilisé pour transmettre ** lors du passage de données de type tensoriel à d'autres variables. J'étais accro ici. Le type de tenseur est-il lié à la manipulation des graphiques de calcul? (Attendu), la valeur du tenseur peut changer si elle n'est pas définie de cette manière.

transforms.cpp (extrait partiel)


void transforms::Normalize::forward(torch::Tensor &data_in, torch::Tensor &data_out){
    torch::Tensor data_out_src = (data_in - this->mean) / this->std;
    data_out = data_out_src.clone();
    return;
}

(4) Autres programmes

D'autres programmes sont presque les mêmes que la version Python, et il n'y a rien de particulièrement addictif. De plus, j'ai créé ma propre classe que je trouvais un peu difficile à utiliser car elle était différente de la version Python. Veuillez vous référer au GitHub suivant pour le programme spécifique. https://github.com/koba-jon/pytorch_cpp/tree/master/ConvAE

Je vais peut-être écrire un article de commentaire sur le code source. Si vous pensez que "c'est étrange", nous vous souhaitons la bienvenue, alors veuillez commenter.

Éléments unifiés parmi les langages de programmation

En gros, on peut penser que c'est presque la même chose sauf pour la partie qui ne peut pas être aidée, comme la bibliothèque qui existe en Python n'existe pas en C ++. En outre, vous pouvez penser que vous n'avez pas changé depuis le programme GitHub.

Plus précisément, les contenus suivants ont été unifiés lors de la comparaison de la version Python et de la version C ++.

  • Taille de l'image (64 x 64 x 3)
  • Type d'image (groupe d'images de la méthode d'apprentissage A = groupe d'images de la méthode d'apprentissage B)
  • Taille du lot (16)
  • Taille de l'espace latent (1 x 1 x 512)
  • Méthode d'optimisation (Adam, taux d'apprentissage = 0,0001, β1 = 0,5, β2 = 0,999)
  • Structure du modèle
  • Comment initialiser le modèle
  • Couche pliante, couche de pliage inversée: moyenne 0,0, écart type 0,02
  • Normalisation des lots: moyenne 1,0, écart type 0,02
  • Comment charger des données
  • Lorsque la classe "datasets" est initialisée, seul le chemin est acquis, et quand il est réellement utilisé, l'image est lue en fonction du chemin pour la première fois.
  • Lorsque la classe "datasets" est en cours d'exécution, un seul ensemble de données (une image et un chemin) est lu.
  • Exécutez "transform" lorsque la classe "datasets" est en cours d'exécution.
  • Lorsque la classe "DataLoader" est en cours d'exécution, les données du mini-batch sont lues en parallèle à partir de la classe "datasets".
  • Comment mélanger le jeu de données
  • Mélange pendant l'apprentissage, mais ne mélange pas pendant l'inférence.
  • Pour chaque époque, mélangez au tout début de la saisie des données.

Résultat expérimental

Pour chaque objet à comparer, 182 340 images 64x64 de celebA ont été utilisées, et un apprentissage en mini-lot d'un modèle d'auto-encodeur avec convolution 1 [d'époque] a été réalisé afin de minimiser l'erreur L1. ** "Time per [epoch]" </ font> ** et ** "GPU memory usage" </ font> J'ai vérifié **.

Ici, «temps par [époque]» inclut le temps de traitement de tqdm et des fonctions auto-créées. J'ai inclus cela parce que cela n'avait presque aucun effet sur le temps de traitement total, et parce qu'il est plus pratique d'avoir une visualisation lors de l'utilisation réelle de PyTorch, de nombreuses personnes l'utilisent.

De plus, en utilisant le modèle entraîné, 20 259 images de test ont été entrées une par une dans le modèle et testées. À ce moment-là ** "Vitesse moyenne de propagation avant" </ font> ** et ** "Erreur L1 entre l'image d'entrée et l'image de sortie" </ font> J'ai également vérifié> **.

Ensuite, j'ai appris et testé sans lancer autre chose que le "fichier d'exécution" et "nvidia-smi" (celui qui fonctionnait depuis le début quand Ubuntu a été lancé).

CPU(Core i7-8700) GPU(GeForce GTX 1070)
Python C++ Python C++
Non déterministe Définitif
Apprentissage Heure [heure / époque] 1 heure 04 minutes 49 secondes 1 heure 03:00 5 minutes 53 secondes 7 minutes 42 secondes 17 minutes 36 secondes
Mémoire GPU [Mio] 2 9 933 913 2941
Test Vitesse [secondes / données] 0.01189 0.01477 0.00102 0.00101 0.00101
Erreur L1 (MAE) 0.12621 0.12958 0.12325 0.12104 0.13158

C ++ est un langage compilé. Par conséquent, je pensais que je battrais le langage d'interprétation Python ... mais ** les deux étaient de bons matchs **.

En termes de temps d'apprentissage, nous avons constaté que le CPU est presque le même et que le GPU est plus de deux fois plus lent que C ++ que Python. (Pourquoi?) Quant à ce résultat, le CPU est à peu près le même, mais il ne diffère grandement que lorsqu'il s'agit de GPU.

  • Il est possible que le traitement dans la version PyTorch C ++ du GPU ne soit pas parfait et que la propagation avant et arrière par le GPU ne soit pas optimisée.
  • Le transfert des données du mini-lot obtenues du CPU vers le GPU peut prendre un certain temps.

Est susceptible d'être mentionné. Alors que les personnes suivantes expérimentent, il semble qu'il ne fait aucun doute que ** Python est plus rapide </ font> ** lorsque le GPU est en cours d'exécution. https://www.noconote.work/entry/2019/01/11/151624

De plus, la vitesse et les performances de l'inférence (tests) sont presque les mêmes que celles de Python, donc ** Actuellement, Python peut être meilleur </ font> **.

L'utilisation de la mémoire du GPU est également importante pour une raison quelconque. (Même si la place de ReLU est définie sur True ...)

C'est le résultat de Python (GPU) déterministe et non déterministe, mais comme la formule l'indique clairement, déterministe est plus lent. Après tout, l'heure va changer ici.

Conclusion

  • Vitesse d'apprentissage

  • 1ère place: version Python (non déterministe, opération principale GPU)

  • 2ème place: version Python (déterministe, opération principale GPU)

  • 3e place: version C ++ (opération principale du GPU)

  • 4ème place: opération principale du processeur (version Python, version C ++ similaire)

  • Vitesse de raisonnement

  • 1ère place: opération principale du GPU (version Python, version C ++ similaire)

  • 2ème place: opération principale du CPU (même niveau que la version Python et la version C ++)

  • Performance

  • Tous sont à peu près pareils

en conclusion

Cette fois, j'ai comparé la vitesse et les performances de PyTorch pour Python et C ++.

En conséquence, Python et C ++ sont presque les mêmes en termes de performances, j'ai donc pensé qu'il n'y aurait aucun problème à utiliser PyTorch de ** C ++ **. Cependant, ** À ce stade, il peut ne pas être recommandé de faire C ++ PyTorch pour la vitesse </ font> **.

Peut-être que l'API C ++ est toujours en évolution et pourrait être considérablement améliorée à l'avenir! A partir de maintenant, c'est mon attente!

URL de référence

  • https://pytorch.org/cppdocs/
  • https://orizuru.io/blog/deep-learning/pytorch-cpp_01/
  • https://www.noconote.work/entry/2019/01/08/200120
  • https://www.noconote.work/entry/2019/01/11/151624
  • https://github.com/pytorch/examples/tree/master/cpp

Recommended Posts