Améliorer les scripts Python avec Cython
Introduction
Python pourrait être l'un des langages de programmation les plus populaires aujourd'hui, mais ce n'est certainement pas le plus efficace. Dans le monde de l'apprentissage automatique en particulier, les praticiens sacrifient l'efficacité pour la facilité d'utilisation qu'offre Python.
Cela ne signifie pas que vous ne pouvez pas accélérer les choses d'autres manières. Cython est un moyen facile de réduire considérablement le temps de calcul des scripts Python, sans sacrifier la fonctionnalité facilement obtenue avec Python.
Ce tutoriel vous présentera l'utilisation de Cython pour accélérer les scripts Python. Nous examinerons une tâche simple mais coûteuse en calcul : créer une boucle for qui itère à travers une liste Python d'un milliard de nombres et les additionne. Comme le temps est particulièrement important lors de l'exécution de code sur des dispositifs à ressources limitées, nous mettrons cette question en contexte en considérant comment implémenter du code Python en Cython sur Raspberry Pi (RPi). Cython apporte un changement significatif dans la vitesse de calcul. Pensez-y comme à un paresseux comparé à un guépard.
Prérequis pour améliorer les scripts Python avec Cython
- Connaissances de base en Python : Familiarité avec la syntaxe Python, les fonctions, les types de données et les modules.
- Compréhension des bases de C/C++ : Compréhension de base des concepts C ou C++ tels que les pointeurs, les types de données et les structures de contrôle.
- Environnement de développement Python : Python installé (de préférence Python 3.x), avec un gestionnaire de paquets comme
pip. - Installation de Cython : Installez Cython en utilisant
pip install cython. - Familiarité avec le terminal/l'invite de commande : Capacité de base à naviguer et à exécuter des commandes dans le terminal ou l'invite de commande.
Ces prérequis m'aideront à être prêt à commencer à optimiser le code Python en utilisant Cython.
Image de paresseux provenant de IndiaToday | Image de guépard provenant de Pixabay
Les sections couvertes dans ce tutoriel sont les suivantes :
- Python, CPython et Cython
- Cythonisation de code Python simple
- Cythoniser une boucle for
- Attribution des types de données C aux variables
- Utiliser Cython sur Raspberry Pi
Commençons.
Python et CPython
De nombreuses personnes ne sont pas conscientes du fait que des langages comme Python sont en réalité implémentés dans d'autres langages. Par exemple, l'implémentation en C de Python s'appelle CPython. Notez qu'il ne s'agit pas de Cython. Pour plus d'informations sur les différentes implémentations de Python, vous pouvez lire cet article.
L'implémentation par défaut et la plus populaire de Python est CPython. Il y a un avantage important à l'utiliser. C est un langage compilé et son code est converti en code machine, qui est exécuté directement par l'unité centrale de traitement (CPU). Vous vous demandez peut-être, si C est un langage compilé, cela signifie-t-il que Python l'est aussi ?
L'implémentation de Python en C (CPython) n'est pas 100 % compilée et n'est pas non plus 100 % interprétée. Il y a à la fois compilation et interprétation dans le processus d'exécution d'un script Python. Pour clarifier cela, voyons les étapes de l'exécution d'un script Python :
- Compiler le code source en utilisant CPython pour générer du bytecode
- Interpréter le bytecode dans un interpréteur CPython
- Exécuter la sortie de l'interprète CPython dans une machine virtuelle CPython
La compilation a lieu lorsque CPython compile le code source (.py) pour générer le bytecode CPython (.pyc). Le bytecode CPython (.pyc) est ensuite interprété à l'aide d'un interprète CPython, et la sortie s'exécute dans une machine virtuelle CPython. Selon les étapes ci-dessus, le processus d'exécution d'un script Python implique à la fois la compilation et l'interprétation.
Le compilateur CPython génère le bytecode une seule fois, mais l'interpréteur est appelé chaque fois que le code s'exécute. En général, l'interprétation du bytecode prend beaucoup de temps. Si l'utilisation d'un interpréteur ralentit l'exécution, pourquoi l'utiliser du tout ? La grande raison est qu'il aide à rendre Python multiplateforme. Étant donné que le bytecode s'exécute dans une machine virtuelle CPython au-dessus du CPU, il est indépendant de la machine sur laquelle il s'exécute. En conséquence, le bytecode peut s'exécuter sur différentes machines sans changement.
S'il n'y a pas d'interprète utilisé, alors le compilateur CPython générera du code machine qui s'exécute directement sur le CPU. Parce que les différentes plateformes ont des instructions différentes, le code ne sera pas multi-plateforme.
En résumé, l'utilisation d'un compilateur accélère le processus mais un interpréteur rend le code multiplateforme. Ainsi, une des raisons pour lesquelles Python est plus lent que C est qu'un interpréteur est utilisé. Rappelez-vous que le compilateur s'exécute une seule fois mais que l'interpréteur s'exécute chaque fois que le code est exécuté.
Python est beaucoup plus lent que C, mais de nombreux programmeurs le préfèrent tout de même car il est beaucoup plus facile à utiliser. Python cache de nombreux détails au programmeur, ce qui peut aider à éviter des débogages frustrants. Par exemple, puisque Python est un langage à typage dynamique, vous n'avez pas à spécifier explicitement le type de chaque variable dans votre code – Python le déduira automatiquement. En revanche, avec les langages à typage statique (comme C, C++ ou Java), vous devez spécifier les types des variables, comme indiqué ci-dessous.
int x = 10
string s = "Hello"
Comparez cela à l'implémentation ci-dessous en Python. Le typage dynamique rend le codage plus facile, mais ajoute beaucoup plus de charge à la machine pour trouver le type de données approprié. Cela rend le processus plus lent.
x = 10
s = "Hello"
En général, les langages de « niveau supérieur » comme Python sont beaucoup plus faciles à utiliser pour les développeurs. Cependant, lorsque le code est exécuté, il doit être converti en instructions de bas niveau. Cette conversion prend plus de temps, ce qui est un sacrifice fait au profit de la facilité d'utilisation.
Si le temps est un facteur important, alors vous devez utiliser les instructions de bas niveau. Ainsi, plutôt que de taper le code en utilisant Python, qui est l'interface, vous pouvez l'écrire en utilisant CPython qui est le backend de Python implémenté en C. Cependant, si vous le faites, vous aurez l'impression de programmer en C, pas en Python.
CPython est beaucoup plus complexe. Dans CPython, tout est implémenté en C. Il n'y a aucun moyen d'échapper à la complexité de C dans le code. C'est pourquoi de nombreux développeurs choisissent plutôt Cython. Mais en quoi Cython est-il différent de CPython ?
Comment Cython est différent
Selon la documentation de Cython, Cython est Python avec des types de données C. Une autre définition tirée du document de tutoriel Cython de 2009 clarifie :
Cython est un langage de programmation basé sur Python avec une syntaxe supplémentaire pour fournir des déclarations de types statiques. Cela profite des avantages de Python tout en permettant d'atteindre la vitesse de C.
Selon les définitions ci-dessus, Cython est un langage qui vous permet d'avoir le meilleur des deux mondes : la vitesse et la facilité d'utilisation. Vous pouvez toujours écrire du code régulier en Python, mais pour accélérer les choses à l'exécution, Cython vous permet de remplacer certains morceaux de code Python par du C. Ainsi, vous finissez par mélanger les deux langages dans un seul fichier. Notez que vous pouvez imaginer que tout ce qui est valide en Python l'est aussi en Cython, mais avec quelques limitations. Pour plus d'informations sur les limitations, vous pouvez visiter cette page.
Le fichier Python ordinaire a une extension .py, mais le fichier Cython a plutôt l'extension .pyx. Le même code Python peut être écrit à l'intérieur des fichiers .pyx, mais ceux-ci vous permettent également d'utiliser du code Cython. Notez que placer simplement le code Python dans un fichier .pyx peut accélérer le processus par rapport à l'exécution du code Python directement, mais pas autant que lorsque les types de variables sont également déclarés. Ainsi, l'objet de ce tutoriel n'est pas seulement d'écrire le code Python dans le fichier .pyx, mais aussi de faire des modifications qui le rendront plus rapide. Ce faisant, nous ajoutons un peu de difficulté à la programmation, mais nous gagnons beaucoup de temps. Si vous avez de l'expérience en programmation C, cela vous sera encore plus facile.
Cythonisation de code Python simple
Pour transformer votre Python en Cython, vous devez d'abord créer un fichier avec l'extension .pyx plutôt qu'avec l'extension .py. Dans ce fichier, vous pouvez commencer par écrire du code Python standard (notez qu'il y a certaines limitations dans le code Python accepté par Cython, comme précisé dans la documentation de Cython).
Avant d'aller de l'avant, assurez-vous que Cython est installé. Vous pouvez le faire avec la commande suivante.
pip install cython
Pour générer le fichier .pyd/.so, nous devons d'abord construire le fichier Cython. Le fichier .pyd/.so représente le module qui sera importé plus tard. Pour construire le fichier Cython, un fichier setup.py sera utilisé. Créez ce fichier et placez le code ci-dessous à l'intérieur. Nous utiliserons la fonction distutils.core.setup() pour appeler la fonction Cython.Build.cythonize(), qui va cythoniser le fichier .pyx. Cette fonction accepte le chemin du fichier que vous souhaitez cythoniser. Ici, je suppose que le fichier setup.py est placé au même emplacement que le fichier test_cython.pyx.
import distutils.core
import Cython.Build
distutils.core.setup(
ext_modules = Cython.Build.cythonize("test_cython.pyx"))
Pour construire le fichier Cython, exécutez la commande ci-dessous dans l'invite de commandes. Le répertoire actuel de l'invite de commandes doit être le même que celui du fichier setup.py.
python setup.py build_ext --inplace
Après que cette commande soit terminée, deux fichiers seront placés à côté du fichier .pyx. Le premier a l'extension .c et l'autre fichier aura l'extension .pyd (ou similaire, en fonction du système d'exploitation utilisé). Pour utiliser le fichier généré, il suffit d'importer le module test_cython et le message « Hello Cython » apparaîtra directement, comme vous le voyez ci-dessous.
Nous avons maintenant réussi à cythoniser le code Python. La section suivante traite de la cythonisation d'un fichier .pyx dans lequel une boucle est créée.
Cythoniser une boucle “for ”
Maintenant, optimisons notre tâche mentionnée précédemment : une boucle for qui itère à travers 1 million de nombres et les additionne. Commençons par examiner l'efficacité des simples itérations de la boucle. Le module time est importé pour estimer combien de temps il faut pour exécuter.
import time
t1 = time.time()
for k in range(1000000):
pass
t2 = time.time()
t = t2-t1
print("%.20f" % t)
Dans un fichier .pyx, le temps moyen pour 3 exécutions est de 0,0281 secondes. Le code s'exécute sur une machine avec un processeur Core i7-6500U à 2,5 GHz et 16 Go de RAM DDR3.
Comparez cela avec le temps qu'il faut pour s'exécuter dans un fichier Python normal, dont la moyenne est de 0,0411 secondes. Cela signifie que Cython est 1,46 fois plus rapide que Python pour les itérations seules, même si nous n'avons pas besoin de modifier la boucle for pour l'exécuter à la vitesse C.
Ajoutons maintenant la tâche de somme. Nous allons utiliser la fonction range() pour cela.
import time
t1 = time.time()
total = 0
for k in range(1000000):
total = total + k
print "Total =", total
t2 = time.time()
t = t2-t1
print("%.100f" % t)
Notez que les deux scripts renvoient la même valeur, qui est 499999500000. En Python, cela prend en moyenne 0,1183 secondes à s'exécuter (entre trois essais). En Cython, c'est 1,35 fois plus rapide, avec une moyenne de 0,0875 secondes.
Voyons un autre exemple dans lequel la boucle itère à travers 1 milliard de nombres en commençant à 0.
import time
t1 = time.time()
total = 0
for k in range(1000000000):
total = total + k
print "Total =", total
t2 = time.time()
t = t2-t1
print("%.20f" % t)
Le script Cython s'est terminé en presque 85 secondes (1,4 minutes) tandis que le script Python s'est terminé en presque 115 secondes (1,9 minutes). Dans les deux cas, c'est tout simplement trop de temps. Quel est l'avantage d'utiliser Cython s'il dure plus d'une minute pour une tâche aussi triviale ? Notez que c'est notre faute, pas celle de Cython.
Comme discuté précédemment, écrire le code Python dans le script Cython .pyx est une amélioration, mais cela ne réduit pas énormément le temps d'exécution. Nous devons apporter des modifications au code Python dans le script Cython. La première chose à faire est de définir explicitement les types de données des variables utilisées.
Affectation des types de données C aux variables
Selon le code précédent, il y a 5 variables utilisées : total, k, t1, t2 et t. Toutes ces variables ont leurs types de données déduits implicitement par le code, ce qui prend donc plus de temps. Pour gagner du temps en déduisant leurs types de données, attribuons-leur leurs types de données du langage C à la place.
Le type de la variable total est unsigned long long int. C'est un entier car la somme de tous les nombres est un entier, et il est non signé car la somme sera positive. Mais pourquoi est-ce long long? Parce que la somme de tous les nombres est très grande, long long est ajouté pour augmenter la taille de la variable au maximum possible.
Le type défini pour la variable k est int, et le type float est affecté aux trois variables restantes t1, t2 et t.
import time
cdef unsigned long long int total
cdef int k
cdef float t1, t2, t
t1 = time.time()
for k in range(1000000000):
total = total + k
print "Total =", total
t2 = time.time()
t = t2-t1
print("%.100f" % t)
Notez que la précision définie dans la dernière instruction d'impression est fixée à 100, et tous ces nombres sont des zéros (voir la figure suivante). C'est ce à quoi nous pouvons nous attendre en utilisant Cython. Alors que Python prend plus de 1,9 minute, Cython ne prend littéralement aucun temps. Je ne peux même pas dire que la vitesse est 1000 ou 100000 fois plus rapide que Python ; j'ai essayé différentes précisions pour le temps imprimé, et aucun nombre n'apparaît toujours.
Notez que vous pouvez également créer une variable entière pour stocker la valeur passée à la fonction range(). Cela améliorera encore plus les performances. Le nouveau code est listé ci-dessous, où la valeur est stockée dans la variable entière maxval.
import time
cdef unsigned long long int maxval
cdef unsigned long long int total
cdef int k
cdef float t1, t2, t
maxval=1000000000
t1=time.time()
for k in range(maxval):
total = total + k
print "Total =", total
t2=time.time()
t = t2-t1
print("%.100f" % t)
Maintenant que nous avons vu comment améliorer les performances des scripts Python en utilisant Cython, appliquons cela au Raspberry Pi (RPi).
Accéder à Raspberry Pi depuis un PC
Si c'est la première fois que vous utilisez votre Raspberry Pi, alors votre PC et le RPi doivent être connectés à un réseau. Vous pouvez le faire en les connectant tous les deux à un switch dans lequel le DHCP (Dynamic Host Configuration Protocol) est actif pour leur attribuer des adresses IP automatiquement. Après la création réussie du réseau, vous pouvez accéder au RPi en fonction de l'adresse IPv4 qui lui est assignée. Comment savoir quelle est l'adresse IPv4 assignée à votre RPi ? Ne vous inquiétez pas, vous pouvez simplement utiliser un outil de scanner IP. Dans ce tutoriel, j'utiliserai une application gratuite appelée Advanced IP Scanner.
L'interface de l'application est comme indiqué ci-dessous. Elle accepte une plage d'adresses IPv4 à rechercher et renvoie les informations sur les dispositifs actifs.
Vous devez entrer la plage des adresses IPv4 de votre réseau local. Si vous ne connaissez pas la plage, il suffit de taper la commande ipconfig dans Windows (ou ifconfig dans Linux) pour connaître l'adresse IPv4 de votre PC (comme indiqué dans la figure ci-dessous). Dans mon cas, l'adresse IPv4 assignée à l'adaptateur Wi-Fi de mon PC est 192.168.43.177 et le masque de sous-réseau est 255.255.255.0. Cela signifie que la plage d'adresses IPv4 dans le réseau va de 192.168.43.1 à 192.168.43.255. D'après la figure, l'adresse IPv4 192.168.43.1 est assignée à la passerelle. Notez que la dernière adresse IPv4 dans la plage, 192.168.43.255, est réservée pour les messages de diffusion. Ainsi, la plage à rechercher devrait commencer à 192.168.43.2 et se terminer à 192.168.43.254.
Selon le résultat du scan montré dans la figure suivante, l'adresse IPv4 assignée au RPi est 192.168.43.63. Cette adresse IPv4 peut être utilisée pour créer une session de shell sécurisé (SSH).
Pour établir la session SSH, j'utiliserai un logiciel gratuit appelé MobaXterm. L'interface de l'application est la suivante.
Pour créer une session SSH, il vous suffit de cliquer sur le bouton Session dans le coin supérieur gauche. Une nouvelle fenêtre apparaît comme indiqué ci-dessous.
Depuis cette fenêtre, cliquez sur le bouton SSH dans le coin supérieur gauche pour ouvrir la fenêtre affichée ci-dessous. Entrez simplement l'adresse IPv4 du RPi et le nom d'utilisateur (qui est par défaut pi), puis cliquez sur OK pour commencer la session.
Après avoir cliqué sur le bouton OK, une nouvelle fenêtre apparaît demandant le mot de passe. Le mot de passe par défaut est raspberrypi. Après s'être connecté, la fenêtre suivante apparaît. Le panneau à gauche aide à naviguer facilement dans les répertoires du RPi. Il y a aussi une ligne de commande pour entrer des commandes.
Utiliser Cython avec Raspberry Pi
Créez un nouveau fichier et définissez son extension sur .pyx pour écrire le code du dernier exemple. Il y a des options dans la barre du panneau de gauche pour créer de nouveaux fichiers et répertoires. Vous pouvez utiliser l'icône de nouveau fichier pour rendre les choses encore plus simples, comme montré dans la figure suivante. J'ai créé un fichier nommé test_cython.pyx dans le répertoire racine du RPi.
Il suffit de double-cliquer sur le fichier pour l'ouvrir, de coller le code et de l'enregistrer. Après cela, nous pouvons créer le fichier setup.py, qui est exactement le même que celui dont nous avons discuté précédemment. Ensuite, nous devons exécuter la commande suivante pour compiler le script Cython.
python3 setup.py build_ext --inplace
Après que cette commande se soit exécutée avec succès, vous pouvez trouver les fichiers de sortie listés dans le panneau de gauche selon la figure suivante. Notez que l'extension du module à importer est désormais .so, car nous n'utilisons plus Windows.
Maintenant, activons Python et importons le module, comme indiqué ci-dessous. Les mêmes résultats obtenus sur le PC sont également obtenus ici ; le temps consommé est essentiellement nul.
Conclusion
Ce tutoriel a discuté de l'utilisation de Cython pour réduire le temps de calcul de l'exécution des scripts Python. Nous avons examiné l'exemple d'utilisation d'une boucle for pour additionner tous les éléments d'une liste Python contenant 1 milliard de nombres, et avons comparé son temps d'exécution avec et sans déclaration des types de variables. Alors que cela prend presque deux minutes à s'exécuter en Python pur, cela prend pratiquement aucun temps à s'exécuter avec des variables statiques déclarées en utilisant Cython.
Dans le prochain tutoriel, nous remplacerons cette liste Python par un tableau NumPy et verrons comment nous pouvons optimiser le traitement des tableaux NumPy en utilisant Cython. Ensuite, nous examinerons comment cythoniser des scripts Python plus avancés, tels que les algorithmes génétiques. Ce sont d'excellents moyens d'améliorer facilement l'efficacité de vos projets d'apprentissage automatique.















