Comment se faire des amis PyTorch et C ++. Utilisation de TorchScript

Il y a environ un an, les développeurs de PyTorch ont présenté la communauté TorchScript , un outil qui vous permet de créer une solution aliénable à partir d'un pipeline en python en quelques clics de souris qui peuvent être intégrés dans un système C ++. Ci-dessous, je partage l'expérience de son utilisation et j'essaie de décrire les pièges rencontrés le long de ce chemin. Je porterai une attention particulière à la mise en œuvre du projet sur Windows, car bien que la recherche en ML soit généralement effectuée sur Ubuntu, la solution finale est souvent (soudainement!) Requise sous les «fenêtres».


Un exemple de code pour exporter un modèle et un projet C ++ à l'aide du modèle se trouve dans le référentiel sur GitHub .




Les développeurs de PyTorch ne sont pas dupes. Le nouvel outil vous permet vraiment de transformer un projet de recherche dans PyTorch en code intégré dans un système C ++ en quelques jours ouvrables, et avec un peu de compétence plus rapidement.


TorchScript est apparu dans PyTorch version 1.0 et continue d'évoluer et de changer. Si la première version il y a un an était pleine de bogues et était plus expérimentale, alors la version actuelle 1.3 au moins sur le deuxième point est sensiblement différente: vous ne pouvez plus l'appeler expérimentale, elle est tout à fait appropriée pour une utilisation pratique. Je vais me concentrer sur elle.


Au cœur de TorchScript se trouve son propre compilateur autonome (sans Python) d'un langage de type python, ainsi que des outils pour convertir un programme écrit en Python et PyTorch, des méthodes pour enregistrer et charger les modules résultants, et une bibliothèque pour les utiliser en C ++. Pour travailler, vous devrez ajouter plusieurs DLL au projet avec un poids total d'environ 70 Mo (pour Windows) pour travailler sur le CPU et 300 Mo pour la version GPU. TorchScript prend en charge la plupart des fonctionnalités de PyTorch et les principales fonctionnalités du langage python. Mais les bibliothèques tierces, comme OpenCV ou NumPy, devront être oubliées. Heureusement, de nombreuses fonctions de NumPy ont un analogue dans PyTorch.


Convertir le pipeline en modèle PyTorch sur TorchScript


TorchScript propose deux façons de convertir le code Python dans son format interne: le traçage et le script (traçage et script). Pourquoi deux? Non, c'est clair, bien sûr, que deux valent mieux qu'un ...



Mais dans le cas de ces méthodes, il s'avère, comme dans l'aphorisme bien connu, des déviations gauche et droite: les deux sont pires. Eh bien, le monde n'est pas parfait. Dans une situation spécifique, vous devez choisir celle qui convient le mieux.


La méthode de traçage est très simple. Un échantillon de données est prélevé (généralement initialisé par des nombres aléatoires), envoyé à la fonction ou à la méthode de la classe qui nous intéresse, et PyTorch construit et stocke le graphique de calcul de la même manière que d'habitude lors de la formation d'un réseau neuronal. Voila - le script est prêt:


import torch import torchvision model = torchvision.models.resnet34(pretrained = True) model.eval() sample = torch.rand(1, 3, 224, 224) scripted_model = torch.jit.trace(model, sample) 

L'exemple ci-dessus produit un objet de la classe ScriptModule. Il peut être enregistré


 scripted_model.save('my_script.pth') 

puis chargez-le dans un programme C ++ (plus d'informations ci - dessous ) ou dans du code Python au lieu de l'objet d'origine:


Exemple de code Python à l'aide d'un modèle enregistré
 import cv2 from torchvision.transforms import Compose, ToTensor, Normalize transforms = Compose([ToTensor(), Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])]) img = cv2.resize(cv2.imread('pics/cat.jpg'), (224,224)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) x = transforms(img).unsqueeze(0) # add batch dimension scripted_model = torch.jit.load('my_script.pth') y = scripted_model(x) print(y[0].argmax(), y[0][y[0].argmax()]) 

 tensor(282) tensor(12.8130, grad_fn=<SelectBackward>) 

Le ScriptModule résultant peut apparaître n'importe où nn.Module est couramment utilisé.


De la manière décrite, vous pouvez tracer des instances de la classe et des fonctions nn.Module (dans ce dernier cas, une instance de la classe torch._C.Function est torch._C.Function ).


Cette méthode (traçage) a un avantage important: de cette façon, vous pouvez convertir presque n'importe quel code Python qui n'utilise pas de bibliothèques externes. Mais il y a un inconvénient tout aussi important: pour toutes les branches, seule la branche qui a été exécutée sur les données de test sera mémorisée:


 def my_abs(x): if x.max() >= 0: return x else: return -x my_abs_traced = torch.jit.trace(my_abs, torch.tensor(0)) print(my_abs_traced(torch.tensor(1)), my_abs_traced(torch.tensor(-1))) 

 c:\miniconda3\lib\site-packages\ipykernel_launcher.py:2: TracerWarning: Converting a tensor to a Python boolean might cause the trace to be incorrect. We can't record the data flow of Python values, so this value will be treated as a constant in the future. This means that the trace might not generalize to other inputs! tensor(1) tensor(-1) 

Oups! Cela ne semble pas être ce que nous aimerions, non? Il est bon qu’au moins un message d’avertissement (TracerWarning) soit émis. Il convient de prêter attention à ces messages.


Ici, la deuxième méthode vient à notre aide - le script:


 my_abs_script = torch.jit.script(my_abs) print(my_abs_script(torch.tensor(1)), my_abs_script(torch.tensor(-1))) 

 tensor(1) tensor(1) 

Hourra, le résultat attendu est reçu! Le script analyse récursivement le code Python et le convertit en code dans son propre langage. En sortie, nous obtenons également la classe ScriptModule (pour les modules) ou torch._C.Function (pour les fonctions). Il semblerait, le voici, le bonheur! Mais un autre problème se pose: le langage interne de TorchScript est fortement typé, contrairement à Python. Le type de chaque variable est déterminé par la première affectation, le type des arguments de fonction par défaut est Tensor . Par conséquent, par exemple, un modèle familier


 def my_func(x): y = None if x.max() > 0: y = x return y my_func = torch.jit.script(my_func) 

Le traçage échouera.


Une erreur de trace ressemble à ceci
 RuntimeError Traceback (most recent call last) <ipython-input-9-25414183a687> in <module>() ----> 1 my_func = torch.jit.script(my_func) d:\programming\3rd_party\pytorch\pytorch_ovod_1.3.0a0_de394b6\torch\jit\__init__.py in script(obj, optimize, _frames_up, _rcb) 1224 if _rcb is None: 1225 _rcb = _gen_rcb(obj, _frames_up) -> 1226 fn = torch._C._jit_script_compile(qualified_name, ast, _rcb, get_default_args(obj)) 1227 # Forward docstrings 1228 fn.__doc__ = obj.__doc__ RuntimeError: Variable 'y' previously has type None but is now being assigned to a value of type Tensor : at <ipython-input-8-75677614fca6>:4:8 def my_func(x): y = None if x.max() > 0: y = x ~ <--- HERE return y 

Il est à noter que bien qu'une erreur se produise lors de l' torch.jit.script , l'endroit qui l'a provoquée dans le code scripté est également indiqué.


Même les points après que les constantes commencent à jouer un rôle:


 def my_func(x): if x.max() > 0: y = 1.25 else: y = 0 return y my_func = torch.jit.script(my_func) 

donnera une erreur
 RuntimeError Traceback (most recent call last) <ipython-input-10-0a5f18586763> in <module>() 5 y = 0 6 return y ----> 7 my_func = torch.jit.script(my_func) d:\programming\3rd_party\pytorch\pytorch_ovod_1.3.0a0_de394b6\torch\jit\__init__.py in script(obj, optimize, _frames_up, _rcb) 1224 if _rcb is None: 1225 _rcb = _gen_rcb(obj, _frames_up) -> 1226 fn = torch._C._jit_script_compile(qualified_name, ast, _rcb, get_default_args(obj)) 1227 # Forward docstrings 1228 fn.__doc__ = obj.__doc__ d:\programming\3rd_party\pytorch\pytorch_ovod_1.3.0a0_de394b6\torch\jit\__init__.py in _rcb(name) 1240 # closure rcb fails 1241 result = closure_rcb(name) -> 1242 if result: 1243 return result 1244 return stack_rcb(name) RuntimeError: bool value of Tensor with more than one value is ambiguous 

Parce qu'il faut écrire non pas 0 , mais 0. pour que le type dans les deux branches soit le même! Gâté, vous savez, avec votre python!


Ce n'est que le début de la liste des modifications que vous devez apporter au code python afin qu'il puisse être transformé avec succès en un module TorchScript. Je vais énumérer les cas les plus typiques plus en détail plus tard . En principe, il n'y a pas de science fusée ici et votre code peut être corrigé en conséquence. Mais le plus souvent, je ne veux pas réparer les modules tiers, y compris les modules standard de torchvision , et comme d'habitude, ils ne conviennent généralement pas aux scripts.


Heureusement, les deux technologies peuvent être combinées: ce qui est écrit est écrit et ce qui ne l'est pas est en train de tracer:


 class MyModule(torch.nn.Module): def __init__(self): super(MyModule, self).__init__() self.resnet = torchvision.models.resnet34(pretrained = True) #       torch.jit.script(my_module) #    -   resnet34. #     self.resnet  ScriptModule. self.resnet.eval() # NB:     !  -  ! self.resnet = torch.jit.trace(self.resnet, torch.rand((1,3,224,224), dtype=torch.float)) def forward(self, x): if x.shape[2] < 224 or x.shape[3] < 224: return torch.tensor(0) else: return self.resnet(x) my_module = MyModule() my_module = torch.jit.script(my_module) 

Dans l'exemple ci-dessus, le traçage est utilisé pour inclure un module qui n'est pas scriptable dans un module où il n'y a pas suffisamment de trace et un script est nécessaire. Il y a une situation inverse. Par exemple, si nous devons télécharger un modèle sur ONNX, le traçage est utilisé. Mais le modèle tracé peut inclure des fonctions TorchScript, donc la logique qui nécessite des branches et des boucles peut y être implémentée! Un exemple est donné dans la documentation officielle de torch.onnx .


Les fonctionnalités fournies par PyTorch pour créer des modules TorchScript sont décrites plus en détail dans la documentation officielle et le torch.jit . En particulier, je n'ai pas mentionné de moyen pratique d'utiliser torch.jit.trace et torch.jit.script sous la forme de décorateurs, sur les particularités du débogage du code scripté. Ceci et bien plus est dans la documentation.


Nous incluons le modèle dans un projet C ++


Malheureusement, la documentation officielle se limite à des exemples de la forme "ajouter 2 tenseurs générés à l'aide de torch.ones ". J'ai préparé un exemple de projet plus proche de la réalité qui envoie une image d'OpenCV au réseau neuronal et reçoit les résultats sous la forme d'un tenseur de réponse, un tuple de variables, une image avec des résultats de segmentation.


Pour que l'exemple fonctionne, vous avez besoin de scripts de classification enregistrés à l'aide de ResNet34 et d'une segmentation à l'aide de DeepLabV3. Pour préparer ces scripts, vous devez exécuter ce bloc-notes jupyter .


Nous avons besoin de la bibliothèque torchlib . Vous pouvez l'obtenir de plusieurs manières:


  1. Si PyTorch est déjà installé à l'aide de pip install , vous pouvez le trouver dans le répertoire Python: <Miniconda3>\Lib\site-packages\torch ;
  2. Si vous avez compilé PyTorch à partir des sources, il est là: <My Pytorch repo>\build\lib.win-amd64-3.6\torch ;
  3. Enfin, vous pouvez télécharger la bibliothèque séparément de pytorch.org en choisissant Language = C ++, et décompressez l'archive.

Le code C ++ est assez simple. Il faut:


  1. Inclure le fichier d'en-tête
     #include <torch/script.h> 
  2. Télécharger le modèle
     torch::jit::script::Module module = torch::jit::load("../resnet34_infer.pth"); 
  3. Préparer les données
     torch::Tensor tensor = torch::from_blob(img.data, { img.rows, img.cols, 3 }, torch::kByte); 
  4. Fonction de forward appel et obtenir le résultat
     auto output = module.forward( { tensor } ) 
  5. Obtenez des données du résultat. La façon de procéder dépend de ce que le réseau de neurones retourne. Soit dit en passant, dans le cas général, il peut également accepter non seulement une image, il est donc préférable de regarder le code source de l'exemple entier, il existe différentes options. Par exemple, pour obtenir des données à partir d'un tenseur unidimensionnel de type float:
     float* data = static_cast<float*>(output.toTensor().data_ptr()); 
  6. Il y a encore une subtilité. N'oubliez pas d'insérer l'analogue with torch.no_grad() dans le code afin de ne pas gaspiller des ressources sur le calcul et le stockage des gradients dont nous n'avons pas besoin. Malheureusement, cette commande ne peut pas être incluse dans le script, vous devez donc l'ajouter au code C ++:
     torch::NoGradGuard no_grad; 

Comment construire un projet en utilisant CMake est décrit dans le guide officiel . Mais le sujet du projet dans Visual Studio n'y est pas divulgué, je vais donc le décrire plus en détail. Vous devrez modifier manuellement les paramètres du projet:


  1. J'ai testé sur Visual Studio 2017. Je ne peux pas en dire plus sur les autres versions.
  2. Le jeu d'outils v14.11 v141 doit être installé (cochez "VC++ 2017 version 15.4 v14.11 toolset" dans le programme d'installation VS).
  3. La plateforme doit être x64 .
  4. Dans General → Platform Toolset sélectionnez v141(Visual Studio 2017)
  5. Dans C/C++ → General → Additional Include Directories <libtorch dir>\include C/C++ → General → Additional Include Directories ajoutez <libtorch dir>\include
  6. Dans l' <libtorch dir>\lib Linker → General → Additional Library Directories ajoutez <libtorch dir>\lib
  7. Dans Linker → Input → Additional Dependencies ajoutez torch.lib; c10.lib torch.lib; c10.lib . Sur Internet, ils écrivent que caffe2.lib peut encore être nécessaire, et pour le GPU et quelque chose d'autre de <libtorch dir>\lib , mais dans la version actuelle, l'ajout de ces deux bibliothèques me suffisait. Il s'agit peut-être d'informations périmées.
  8. Ils écrivent également que vous devez définir C/C++ → Language → Conformance Mode = No , mais je n'ai pas vu la différence.

De plus, la variable __cplusplus ne doit PAS être déclarée dans le projet. Tenter d'ajouter l' /Zc:__cplusplus entraînera des erreurs de compilation dans le fichier ivalue.h .


Dans le projet joint, les paramètres de chemin (non seulement pour TorchLib, mais aussi pour OpenCV et CUDA) sont extraits dans le fichier d'accessoires , avant l'assemblage, vous devez les enregistrer là-bas conformément à votre configuration locale. En fait, c'est tout.


Quoi d'autre à garder à l'esprit


Si le processus décrit vous a paru trop simple, votre intuition ne vous a pas trompé. Il y a un certain nombre de nuances à prendre en compte pour convertir un modèle PyTorch écrit en Python en TorchScript. Je vais énumérer ci-dessous ceux que j'ai dû affronter. J'en ai déjà mentionné quelques-uns, mais je répète de rassembler tout en un seul endroit.



  • Le type de variables passées à la fonction est Tensor par défaut. Si dans certains cas (très fréquents) cela est inacceptable, vous devrez déclarer les types manuellement à l'aide d'annotations de type MyPy, quelque chose comme ceci:

 def calc_letter_statistics(self, cls_preds: List[Tensor], cls_thresh: float)->Tuple[int, Tuple[Tensor, Tensor, Tensor]] 

ou alors:


 def calc_letter_statistics(self, cls_preds, cls_thresh): # type: (List[Tensor], float)->Tuple[int, Tuple[Tensor, Tensor, Tensor]] 

  • Les variables sont fortement typées et le type, s'il n'est pas spécifié explicitement, est déterminé par la première affectation. Constructions familières de la forme x=[]; for ...: x.append(y) x=[]; for ...: x.append(y) devra être modifié, car au moment d'affecter [] compilateur ne peut pas déterminer quel type sera dans la liste. Par conséquent, vous devez spécifier le type explicitement, par exemple:

 from typing import List x: List[float] = [] 

ou (un autre "par exemple")


 from torch import Tensor from typing import Dict, Tuple, List x: Dict[int: Tuple[float, List[Tensor], List[List[int]]]] = {} 

  • Dans l'exemple ci-dessus, ce sont les noms qui doivent être importés, car ces noms sont cousus dans le code TorchScript. Approche alternative, apparemment légale

 import torch import typing x: typing.List[torch.Tensor] = [] 

entraînera une erreur de typage du constructeur de type inconnu.


  • Une autre conception familière avec laquelle vous devez vous séparer:

 x = None if smth: x = torch.tensor([1,2,3]) 

Il y a deux options. Ou attribuez Tensor les deux fois (le fait qu'il soit de dimensions différentes n'est pas effrayant):


 x = torch.tensor(0) if smth: x = torch.tensor([1,2,3]) 

et n'oubliez pas de chercher ce qui se cassera après un tel remplacement. Ou essayez d'écrire honnêtement:


 x: Optional[Tensor] = None if smth: x = torch.tensor([1,2,3]) 

mais alors avec une utilisation supplémentaire de x où le tenseur est attendu, nous obtiendrons très probablement une erreur: Attendu une valeur de type 'Tensor' pour l'argument 'x' mais trouvé à la place le type 'Optional [Tensor]'.


  • N'oubliez pas d'écrire, par exemple, x=0. lors de la première affectation x=0. au lieu de l'habituel x=0 , etc., si la variable x doit être de type float .


  • Si quelque part nous avons utilisé l'initialisation à l'ancienne du tenseur via x = torch.Tensor(...) , vous devrez vous en séparer et le remplacer par une version plus récente avec une petite lettre x = torch.tensor(...) . Sinon, pendant le script, il volera: Opération interne inconnue: aten :: Tensor. Voici quelques suggestions: aten :: tensor . Il semble qu'ils expliquent même le problème et il est clair ce qui doit être fait. Cependant, il est clair si vous connaissez déjà la bonne réponse.


  • Le code est scripté dans le contexte du module où torch.jit.script appelé. Par conséquent, si quelque part, dans les entrailles de la classe ou de la fonction scriptée, par exemple, math.pow , vous devrez ajouter import math au module de compilation. Et il est préférable de scripter la classe où elle est déclarée: soit en utilisant le décorateur @torch.jit.script , soit en déclarant une fonction supplémentaire à côté d'elle qui en fait ScriptModule. Sinon, nous obtenons un message d'erreur mathématique de valeur non définie lorsque nous essayons de compiler une classe à partir d'un module dans lequel, apparemment, l'importation math été effectuée.


  • Si quelque part vous avez une construction de la forme my_tensor[my_tensor < 10] = 0 ou similaire, vous obtiendrez une erreur cryptique lors de l'écriture de scripts:


     *aten::index_put_(Tensor(a!) self, Tensor?[] indices, Tensor values, bool accumulate=False) -> (Tensor(a!)):* *Expected a value of type 'Tensor' for argument 'values' but instead found type 'int'.* *aten::index_put_(Tensor(a!) self, Tensor[] indices, Tensor values, bool accumulate=False) -> (Tensor(a!)):* *Expected a value of type 'List[Tensor]' for argument 'indices' but instead found type 'List[Optional[Tensor]]'.* 

    Ce dont vous avez besoin est de remplacer le nombre par le tenseur: my_tensor[my_tensor < 10] = torch.tensor(0.).to(my_tensor.device) . Et n'oubliez pas a) la correspondance des types my_tensor et du tenseur créé (dans ce cas, float) et b) about .to(my_tensor.device) . Si vous oubliez la seconde, tout sera scripté, mais déjà en train de travailler avec le GPU, vous serez contrarié, qui ressemblera aux mots cryptiques Erreur CUDA: un accès mémoire illégal a été rencontré , sans indiquer où l'erreur s'est produite!


  • N'oubliez pas que par défaut nn.Module et, par conséquent, les modèles de torchvision sont créés en "mode train" (vous ne le croirez pas, mais il s'avère qu'il existe un tel mode ). Dans ce cas, Dropout et d'autres astuces du mode train sont utilisées, ce qui rompt la trace ou conduit à des résultats inadéquats lors de l'exécution. N'oubliez pas d'appeler model.eval() avant de model.eval() script ou un suivi.


  • Pour les fonctions et les classes ordinaires, vous devez scripter le type, pour nn.Module - une instance


  • Tentative dans une méthode scriptée d'accéder à une variable globale



 cls_thresh = 0.3 class MyModule(torch.nn.Module): ... x = r < cls_thresh ... 

entraînera une erreur de script de la forme python. La valeur de type 'float' ne peut pas être utilisée comme valeur . Il est nécessaire de faire de la variable un attribut dans le constructeur:


 cls_thresh = 0.3 class MyModule(torch.nn.Module): def __init__(self): ... self.cls_thresh = cls_thresh ... x = r < self.cls_thresh ... 

  • Une autre subtilité apparaît si l'attribut de classe est utilisé comme paramètre de tranche:

 class FPN(nn.Module): def __init__(self, block, num_blocks, num_layers =5): ... self.num_layers = num_layers def forward(self, x): ... return (p3, p4, p5, p6, p7)[:self.num_layers] 

provoque des erreurs de script, les indices de tranche de tuple doivent être des constantes entières . Il est nécessaire d'indiquer que l'attribut num_layers est constant et ne changera pas:


 class FPN(nn.Module): num_layers: torch.jit.Final[int] def __init__(self, block, num_blocks, num_layers =5): ... 

  • Dans certains cas, où le tenseur s'adaptait normalement, vous devez explicitement transmettre le nombre:

 xx1 = x1.clamp(min=x1[i]) 

renvoie une erreur lors de l'écriture d' Expected a value of type 'Optional[number]' for argument 'min' but instead found type 'Tensor'. script. Expected a value of type 'Optional[number]' for argument 'min' but instead found type 'Tensor'. . Eh bien, ici, à partir du message d'erreur, il est clair que faire:


 xx1 = x1.clamp(min=x1[i].item()) 

Les problèmes ci-dessus se produisent lors du traçage. C'est à cause d'eux qu'il n'est généralement pas possible de simplement compiler des solutions prêtes à l'emploi dans TorchScript, et vous devez soit masser le code source pendant une longue période (si le code source est approprié à modifier), soit utiliser le traçage. Mais la trace a ses propres nuances:


  • Les constructions du formulaire ne fonctionnent pas dans la trace

 tensor_a.to(tensor_b.device) 

Le dispositif sur lequel le tenseur est chargé est fixé au moment du traçage et ne change pas lors de l'exécution. Ce problème peut être partiellement surmonté en déclarant le tenseur membre de nn.Module type Parameter . Ensuite, lors du chargement du modèle, il démarrera sur le périphérique spécifié dans la fonction torch.jit.load .


Épilogue


Tout ce qui précède, bien sûr, crée des problèmes. Mais TorchScript vous permet de combiner et d'envoyer à la solution dans son ensemble le modèle lui-même et le code Python qui fournit le pré et le post-traitement. Oui, et le temps de préparer la solution pour la compilation, même malgré les difficultés ci-dessus, est incomparablement inférieur au coût de création d'une solution, mais ici PyTorch offre de grands avantages, donc le jeu en vaut la chandelle.


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


All Articles