paint-brush
Curieux de connaître les modèles ML plus rapides ? Découvrez la quantification de modèles avec PyTorch !by@chinmayjog
356
356

Curieux de connaître les modèles ML plus rapides ? Découvrez la quantification de modèles avec PyTorch !

Chinmay Jog12m2024/02/08
Read on Terminal Reader

Découvrez comment la quantification peut aider vos modèles entraînés à fonctionner environ 4 fois plus rapidement tout en conservant la précision à l'aide de Pytorch.
featured image - Curieux de connaître les modèles ML plus rapides ? Découvrez la quantification de modèles avec PyTorch !
Chinmay Jog HackerNoon profile picture
0-item
1-item


Saviez-vous que dans le monde de l'apprentissage automatique, l'efficacité des modèles de Deep Learning (DL) peut être considérablement améliorée par une technique appelée quantification ? Imaginez réduire la charge de calcul de votre réseau neuronal sans sacrifier ses performances. Tout comme la compression d'un fichier volumineux sans perdre son essence, la quantification de modèle vous permet de rendre vos modèles plus petits et plus rapides. Plongeons dans le concept fascinant de la quantification et dévoilons les secrets de l'optimisation de vos réseaux neuronaux pour un déploiement dans le monde réel.


Avant de plonger dans le vif du sujet, les lecteurs doivent se familiariser avec les réseaux de neurones et le concept de base de la quantification, notamment les termes échelle (S) et point zéro (ZP). Pour les lecteurs qui souhaitent un rappel, cet article et cet article expliquent le concept général et les types de quantification.


Dans ce guide, j'expliquerai brièvement pourquoi la quantification est importante et comment la mettre en œuvre à l'aide de Pytorch. Je me concentrerai principalement sur le type de quantification appelé « quantification statique post-formation », qui entraîne une empreinte mémoire 4 fois inférieure du modèle ML et rend l'inférence jusqu'à 4 fois plus rapide.

Concepts

Pourquoi la quantification est-elle importante ?

Les calculs de réseaux de neurones sont le plus souvent effectués avec des nombres à virgule flottante de 32 bits. Un seul nombre à virgule flottante de 32 bits (FP32) nécessite 4 octets de mémoire. En comparaison, un seul nombre entier de 8 bits (INT8) ne nécessite que 1 octet de mémoire. De plus, les ordinateurs traitent l’arithmétique des nombres entiers beaucoup plus rapidement que les opérations flottantes. Tout de suite, vous pouvez voir que la quantification d'un modèle ML de FP32 à INT8 entraînera 4 fois moins de mémoire. De plus, cela accélérera également l’inférence jusqu’à 4x ! Alors que les grands modèles font fureur en ce moment, il est important que les praticiens soient capables d'optimiser les modèles entraînés en termes de mémoire et de vitesse pour l'inférence en temps réel.


Source- Tenor.com


Mots clés

  • Poids- Poids du réseau neuronal formé.


  • Activations- En termes de quantification, les activations ne sont pas les fonctions d'activation comme Sigmoid ou ReLU. Par activations, j'entends les sorties de la carte de caractéristiques des couches intermédiaires, qui sont des entrées pour les couches suivantes.


Quantification statique après la formation

La quantification statique post-formation signifie que nous n'avons pas besoin de former ou d'affiner le modèle pour la quantification après avoir entraîné le modèle d'origine. Nous n’avons pas non plus besoin de quantifier les entrées des couches intermédiaires, appelées activations à la volée. Dans ce mode de quantification, les poids sont directement quantifiés en calculant l'échelle et le point zéro de chaque couche. Cependant, pour les activations, à mesure que l'entrée dans le modèle change, les activations changeront également. Nous ne connaissons pas la plage de chaque entrée que le modèle rencontrera lors de l'inférence. Alors, comment pouvons-nous calculer l’échelle et le point zéro de toutes les activations du réseau ?


Nous pouvons le faire en calibrant le modèle, en utilisant un bon ensemble de données représentatif. Nous observons ensuite la plage de valeurs d'activation pour l'ensemble d'étalonnage, puis utilisons ces statistiques pour calculer l'échelle et le point zéro. Cela se fait en insérant des observateurs dans le modèle, qui collectent des statistiques de données lors de l'étalonnage. Après avoir préparé le modèle (insertion des observateurs), nous exécutons la passe avant du modèle sur l'ensemble de données d'étalonnage. Les observateurs utilisent ces données d'étalonnage pour calculer l'échelle et le point zéro des activations. Désormais, l'inférence consiste simplement à appliquer la transformation linéaire à toutes les couches avec leur échelle et leurs points zéro respectifs.

Alors que l'intégralité de l'inférence est effectuée dans INT8, la sortie finale du modèle est déquantifiée (de INT8 à FP32).


Pourquoi les activations doivent-elles être quantifiées si les poids d'entrée et de réseau sont déjà quantifiés ?

Ceci est une excellente question. Alors que l'entrée réseau et les poids sont effectivement déjà des valeurs INT8, la sortie de la couche est stockée sous la forme INT32, pour éviter tout débordement. Pour réduire la complexité du traitement de la couche suivante, les activations sont quantifiées de INT32 à INT8.


Une fois les concepts clairs, plongeons dans le code et voyons comment il fonctionne !


Pour cet exemple, j'utiliserai un modèle resnet18 affiné sur le jeu de données Flowers102, disponible directement dans Pytorch. Cependant, le code fonctionnera pour n’importe quel CNN formé, avec l’ensemble de données d’étalonnage approprié. Ce tutoriel étant axé sur la quantification, je ne couvrirai pas la partie formation et réglage fin. Cependant, tout le code peut être trouvé ici . Plongeons-nous !


Code de quantification

Importons les bibliothèques nécessaires à la quantification et chargeons le modèle affiné.

 import torch import torchvision import torchvision.transforms as transforms from torchvision.models import resnet18 import torch.nn as nn from torch.ao.quantization import get_default_qconfig from torch.ao.quantization.quantize_fx import prepare_fx, convert_fx from torch.ao.quantization import QConfigMapping import warnings warnings.filterwarnings('ignore')


Ensuite, définissons quelques paramètres, définissons les transformations de données et les chargeurs de données, et chargeons le modèle affiné.

 model_path = 'flowers_model.pth' quantized_model_save_path = 'quantized_flowers_model.pth' batch_size = 10 num_classes = 102 # Define data transforms transform = transforms.Compose( [transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize( (0.485, 0.465, 0.406), (0.229, 0.224, 0.225))] ) # Define train data loader, for using as calibration set trainset = torchvision.datasets.Flowers102(root='./data', split="train", download=True, transform=transform) trainLoader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True, num_workers=2) # Load the finetuned resnet model model_to_quantize = resnet18(weights=None) num_features = model_to_quantize.fc.in_features model_to_quantize.fc = nn.Linear(num_features, num_classes) model_to_quantize.load_state_dict(torch.load(model_path)) model_to_quantize.eval() print('Loaded fine-tuned model')

Pour cet exemple, j'utiliserai quelques échantillons d'entraînement comme ensemble d'étalonnage.

Définissons maintenant la configuration utilisée pour quantifier le modèle.

 # Define quantization parameters config for the correct platform, # "x86" for x86 devices or "qnnpack" for arm devices qconfig = get_default_qconfig("x86") qconfig_mapping = QConfigMapping().set_global(qconfig)

Dans l'extrait ci-dessus, j'ai utilisé la configuration par défaut, mais la classe QConfig de Pytorch est utilisée pour décrire comment le modèle, ou une partie du modèle, doit être quantifié. Nous pouvons le faire en spécifiant le type de classes d’observateurs à utiliser pour les pondérations et les activations.


Nous sommes maintenant prêts à préparer le modèle pour la quantification

 # Fuse conv-> relu, conv -> bn -> relu layer blocks and insert observers model_prep = prepare_fx(model=model_to_quantize, qconfig_mapping=qconfig_mapping, example_inputs=torch.randn((1,3,224,224)))

La fonction prepare_fx insère les observateurs dans le modèle et fusionne également les modules conv→relu et conv→bn→relu. Cela entraîne moins d'opérations et une bande passante mémoire inférieure car il n'est pas nécessaire de stocker les résultats intermédiaires de ces modules.


Calibrez le modèle en transmettant les données d'étalonnage

 # Run calibration for 10 batches (100 random samples in total) print('Running calibration') with torch.no_grad(): for i, data in enumerate(trainLoader): samples, labels = data _ = model_prep(samples) if i == 10: break

Nous n'avons pas besoin d'effectuer un calibrage sur l'ensemble de l'ensemble d'entraînement ! Dans cet exemple, j'utilise 100 échantillons aléatoires, mais en pratique, vous devez choisir un ensemble de données représentatif de ce que le modèle verra lors du déploiement.


Quantifiez le modèle et enregistrez les poids quantifiés !

 # Quantize calibrated model quantized_model = convert_fx(model_prep) print('Quantized model!') # Save quantized torch.save(quantized_model.state_dict(), quantized_model_save_path) print('Saved quantized model weights to disk')

Et c'est tout! Voyons maintenant comment charger un modèle quantifié, puis comparons la précision, la vitesse et l'empreinte mémoire des modèles d'origine et quantifiés.


Charger un modèle quantifié

Un graphe modèle quantifié n’est pas tout à fait identique au modèle original, même si les deux ont les mêmes couches.

L'impression de la première couche ( conv1 ) des deux modèles montre la différence.

 print('\nPrinting conv1 layer of fp32 and quantized model') print(f'fp32 model: {model_to_quantize.conv1}') print(f'quantized model: {quantized_model.conv1}') 

1ère couche du modèle fp32 et du modèle quantifié


Vous remarquerez qu'en plus des différentes classes, la couche conv1 du modèle quantifié contient également les paramètres d'échelle et de point zéro.


Ainsi, ce que nous devons faire, c'est suivre le processus de quantification (sans calibrage) pour créer le graphe modèle, puis charger les poids quantifiés. Bien sûr, si nous enregistrons le modèle quantifié au format onnx, nous pouvons le charger comme n'importe quel autre modèle onnx, sans exécuter les fonctions de quantification à chaque fois.

En attendant, définissons une fonction pour charger le modèle quantifié et enregistrons-la dans inference_utils.py .

 import torch from torch.ao.quantization import get_default_qconfig from torch.ao.quantization.quantize_fx import prepare_fx, convert_fx from torch.ao.quantization import QConfigMapping def load_quantized_model(model_to_quantize, weights_path): ''' Model only needs to be calibrated for the first time. Next time onwards, to load the quantized model, you still need to prepare and convert the model without calibrating it. After that, load the state dict as usual. ''' model_to_quantize.eval() qconfig = get_default_qconfig("x86") qconfig_mapping = QConfigMapping().set_global(qconfig) model_prep = prepare_fx(model_to_quantize, qconfig_mapping, torch.randn((1,3,224,224))) quantized_model = convert_fx(model_prep) quantized_model.load_state_dict(torch.load(weights_path)) return quantized_model


Définir des fonctions pour mesurer la précision et la vitesse

Mesurer la précision

 import torch def test_accuracy(model, testLoader): model.eval() running_acc = 0 num_samples = 0 with torch.no_grad(): for i, data in enumerate(testLoader): samples, labels = data outputs = model(samples) preds = torch.argmax(outputs, 1) running_acc += torch.sum(preds == labels) num_samples += samples.size(0) return running_acc / num_samples

Il s'agit d'un code Pytorch assez simple.


Mesurer la vitesse d'inférence en millisecondes (ms)

 import torch from time import time def test_speed(model): dummy_sample = torch.randn((1,3,224,224)) # Average out inference speed over multiple iterations # to get a true estimate num_iterations = 100 start = time() for _ in range(num_iterations): _ = model(dummy_sample) end = time() return (end-start)/num_iterations * 1000


Ajoutez ces deux fonctions dans inference_utils.py . Nous sommes maintenant prêts à comparer les modèles. Passons en revue le code.


Comparez les modèles pour la précision, la vitesse et la taille

Importons d'abord les bibliothèques nécessaires, définissons les paramètres, les transformations de données et le chargeur de données de test.

 import os import torch import torch.nn as nn import torchvision from torchvision.models import resnet18 import torchvision.transforms as transforms from inference_utils import test_accuracy, test_speed, load_quantized_model import copy import warnings warnings.filterwarnings('ignore') model_weights_path = 'flowers_model.pth' quantized_model_weights_path = 'quantized_flowers_model.pth' batch_size = 10 num_classes = 102 # Define data transforms transform = transforms.Compose( [transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize( (0.485, 0.465, 0.406), (0.229, 0.224, 0.225))] ) testset = torchvision.datasets.Flowers102(root='./data', split="test", download=True, transform=transform) testLoader = torch.utils.data.DataLoader(testset, batch_size=batch_size, shuffle=False, num_workers=2)


Charger les deux modèles

 # Load the finetuned resnet model and the quantized model model = resnet18(weights=None) num_features = model.fc.in_features model.fc = nn.Linear(num_features, num_classes) model.load_state_dict(torch.load(model_weights_path)) model.eval() model_to_quantize = copy.deepcopy(model) quantized_model = load_quantized_model(model_to_quantize, quantized_model_weights_path)


Comparez les modèles

 # Compare accuracy fp32_accuracy = test_accuracy(model, testLoader) accuracy = test_accuracy(quantized_model, testLoader) print(f'Original model accuracy: {fp32_accuracy:.3f}') print(f'Quantized model accuracy: {accuracy:.3f}\n') # Compare speed fp32_speed = test_speed(model) quantized_speed = test_speed(quantized_model) print(f'Inference time for original model: {fp32_speed:.3f} ms') print(f'Inference time for quantized model: {quantized_speed:.3f} ms\n') # Compare file size fp32_size = os.path.getsize(model_weights_path)/10**6 quantized_size = os.path.getsize(quantized_model_weights_path)/10**6 print(f'Original model file size: {fp32_size:.3f} MB') print(f'Quantized model file size: {quantized_size:.3f} MB')


Résultats

Comparaison du modèle fp32 par rapport au modèle quantifié


Comme vous pouvez le constater, la précision du modèle quantifié sur les données de test est presque aussi grande que la précision du modèle original ! L'inférence avec le modèle quantifié est environ 3,6 fois plus rapide (!) et le modèle quantifié nécessite environ 4 fois moins de mémoire que le modèle d'origine !


Conclusion

Dans cet article, nous avons compris le concept général de quantification du modèle ML et un type de quantification appelé quantification statique post-entraînement. Nous avons également examiné pourquoi la quantification est importante et constitue un outil puissant à l’ère des grands modèles. Enfin, nous avons parcouru un exemple de code pour quantifier un modèle entraîné à l'aide de Pytorch et examiné les résultats. Comme les résultats l'ont montré, la quantification du modèle d'origine n'a pas eu d'impact sur les performances, tout en réduisant la vitesse d'inférence d'environ 3,6 fois et l'empreinte mémoire d'environ 4 fois !


Quelques points à noter : la quantification statique fonctionne bien pour les CNN, mais la quantification dynamique est la méthode préférée pour les modèles de séquence. De plus, si la quantification a un impact considérable sur les performances du modèle, la précision peut être retrouvée grâce à une technique appelée Quantization Aware Training (QAT).


Comment fonctionnent la quantification dynamique et le QAT ? Ce sont des messages pour une autre fois. J'espère qu'avec ce guide, vous disposerez des connaissances nécessaires pour effectuer une quantification statique sur vos propres modèles Pytorch.


Les références