On ne pose pas de cookies de suivi nous-mêmes. Les vidéos de cours sont hébergées sur YouTube, qui dépose ses propres cookies dès qu'une vidéo se charge — elles restent disponibles en un clic si vous préférez ne pas les accepter maintenant.
Accélérer ses calculs avec la vectorisation · Python — Sup2Tech
Accélérer ses calculs avec la vectorisation
0/6
Vidéo
Étape 1 sur 4, Vidéo
NumPy : la vectorisation
Points clés de cette vidéo
Vectoriser une opération signifie l'appliquer à tout un array d'un coup, sans boucle for explicite
Les « fonctions universelles » (ufuncs) de NumPy — np.sqrt, np.exp, l'opérateur **… — sont implémentées en code compilé, bien plus rapide qu'une boucle Python
Sur de grands volumes de données, la différence de vitesse entre une boucle Python et une opération vectorisée peut être de plusieurs ordres de grandeur
Transcription de la vidéo
Dans cette vidéo, nous allons voir comment appliquer une opération à chaque élément d'un tableau NumPy. Nous savons évidemment faire cela avec ce que l'on connaît en Python comme les boucles for, les compréhensions ou les fonctions génératrices. Mais nous allons voir que pour tirer pleinement parti de la performance des tableaux NumPy, il faut utiliser un nouveau concept que l'on appelle la vectorisation. Ouvrons maintenant un notebook pour commencer à jouer avec cette notion. Commençons par créer un tableau d'entiers. a égale np.arange de 1000 donc un tableau qui contient mille entiers et maintenant, calculons un polynôme sur ce tableau. Regardons cela, je vais le faire de la manière suivante : timeit de, alors je vais le faire avec une compréhension, x au carré plus 2 fois x moins 1 for x in a. Et exécutons cela. Ça, on sait tout à fait le faire, on sait faire des opérations de compréhension, des fonctions génératrices, des boucles for sur les objets itérables et a est itérable. Mais la bonne manière de le faire, c'est d'utiliser les opérations vectorisées. Alors, qu'est-ce que c'est, une opération vectorisée en NumPy ? Les opérations vectorisées exploitent le fait que les tableaux Numpy sont stockés dans des zones contiguës de mémoire et que tous les éléments stockés ont la même dimension. Il est par conséquent extrêmement efficace de parcourir les différents éléments d'un tableau NumPy. Regardons comment est-ce que j'écrirais ça en opération vectorisée. On va voir que c'est extrêmement intuitif et même plus simple qu'une compréhension de liste. Je vais directement effectuer mon opération sur mon tableau NumPy. Je vais écrire timeit de a au carré plus 2 fois a moins 1. J'exécute et mon opération va automatiquement se faire sur chaque élément du tableau, donc l'opération élévation à la puissance, la multiplication, la soustraction et on va combiner le résultat dans un nouveau tableau NumPy et je vois qu'ici le gain de performance est absolument majeur, j'ai quasiment un facteur 100. Alors, tous les opérateurs que l'on utilise classiquement, les opérateurs numériques, le plus, le fois, le divisé, la soustraction, les opérateurs de comparaison, supérieur, inférieur, égal, différent, les opérateurs bitwise, sont tous vectorisés. Et tous ces opérateurs sont systématiquement associés à une fonction NumPy. Alors, en général, on préfère utiliser l'opérateur parce qu'on voit que c'est beaucoup plus intuitif écrire a au carré plus 2 fois a moins 1 plutôt que d'appeler des fonctions, mais par contre, les fonctions permettent de passer des paramètres qui parfois sont utiles ; notamment, dans les fonctions vectorisées, j'ai un paramètre qui s'appelle out. Quel est ce paramètre out ? Il permet de spécifier dans quel objet je vais écrire le résultat. Par défaut, les fonctions vectorisées produisent un nouvel objet. Mais je peux décider d'écrire dans un objet existant ; je vais donc économiser le temps de création de cet objet, et je vais donc avoir un gain important en termes de mémoire et un petit gain en termes de performance. Regardons un exemple. Je vais écrire a égale np.arange de 1 à un million et je vais donner comme dtype np.float64. Et ensuite, je vais faire je vais comparer avec mon timeit donc je vais faire ici une seule opération, moins r 1 moins n 1, donc ça veut dire que je n'exécute qu'une seule fois l'instruction, et je vais faire un np point racine carrée, sqrt de a. Exécutons ça et je vois que ça a pris 5 millisecondes. Maintenant, je vais refaire la même opération mais ici, je vais spécifier l'argument out. Donc ici, je vais écrire out égale a. Je vais réécrire dans le tableau d'origine et je vois qu'ici, j'ai eu un gain extrêmement important de performance. Et en plus, je n'ai pas créé un nouveau tableau en mémoire. Certaines fonctions vectorisées ont une variante à laquelle on peut accéder par at. Regardons un exemple. Donc j'ai un tableau a et je vais écrire a de deux points 5. J'obtiens les premiers éléments de mon tableau a. Maintenant, je peux appeler np.log.at de a virgule 2 virgule 4. Que va faire cet appel de log avec at ? En fait, on va appliquer la fonction log uniquement aux éléments 2 et 4 de a et on va les modifier en place, donc at me permet d'appliquer une fonction à certains éléments d'un tableau NumPy et de les modifier en place. Exécutons cette fonction et regardons maintenant ce que me redonne a de deux points 5 : on voit bien que les éléments 2, donc cet élément, et l'élément 4 ont bien été modifiés en place ; en fait, j'ai appliqué la fonction logarithme à ces deux éléments-là. Nous avons vu également que les fonctions vectorisées s'appliquaient élément par élément. En réalité, toutes les fonctions vectorisées ne s'appliquent pas élément par élément, il y a certaines opérations vectorisées qui font des opérations d'agrégation. Lorsque l'on travaille élément par élément, il n'y a pas de notion d'axe, il n'y a pas de notion de dimension. Par contre, lorsque l'on agrège des éléments, là, il y a bien une notion d'axe, on doit savoir si on agrège, par exemple, suivant les lignes ou suivant les colonnes. Regardons un exemple. Je vais écrire a égale np.arange de 1, 10 et je vais faire un reshape de 3 virgule 3. J'exécute, je regarde mon tableau a et j'ai donc un tableau qui fait trois lignes et trois colonnes. Et maintenant, je vais faire un np.sum de a. Si je fais np.sum de a, qu'est-ce que je vais obtenir ? Je vais faire la somme de tous les éléments de a. Mais à sum, je peux lui passer une notion d'axe, et donc regardons cela ; je peux écrire np.sum de a virgule axis égale 0 et donc là, ça va me faire une somme qui va être le long des lignes, et donc en fait, je vais prendre chaque colonne et je vais additionner les éléments de la colonne. Maintenant, prenons une deuxième variante ; je vais écrire np.sum de a virgule axis égale 1, et maintenant, je vais faire ma somme suivant l'autre direction, je vais prendre chaque ligne et je vais additionner les éléments sur les lignes. Pour finir, j'aimerais revenir sur la notion de NaN, que l'on a notamment avec les floats dans les tableaux NumPy. Regardons un exemple. Je vais de nouveau créer un tableau en spécifiant le dtype qui est égal à np.float64, et je vais faire un reshape de 3 virgule 3. Je regarde ce que j'obtiens, j'ai bien un tableau qui contient des float64, un tableau à trois lignes et trois colonnes. Maintenant, je vais écrire a de 1 virgule 1 égale np.nan. Donc j'ajoute la valeur NaN au milieu de mon tableau, et maintenant, j'ai bien NaN, NaN, c'est possible de l'avoir dans un tableau de floats, puisque ça fait partie des floats. Maintenant, qu'est-ce qu'il se passe si je calcule une moyenne de mon tableau ? Regardons cela. En fait, j'obtiens la valeur NaN. Pourquoi ? Parce que le NaN est contaminant. Comme le NaN est Not a Number, toutes les opérations faites sur des NaN vont produire des NaN. Il se trouve qu'en NumPy, les fonctions que l'on a ont toutes une variante qui ignore les NaN. Et donc ces fonctions démarrent commencent avec le mot nan. Regardons cet exemple. np.nanmean c'est la fonction qui va calculer la moyenne sur les éléments de mon tableau en ignorant les valeurs NaN. De la même manière, je pourrais faire nanmax de a et je calculerais le maximum de tous les éléments de mon tableau en ignorant les valeurs NaN. Dans cette vidéo, nous avons vu la notion de vectorisation. Il s'agit d'un changement important de paradigme par rapport à Python puisque maintenant, pour tirer pleinement parti de la performance des tableaux NumPy, vous devez utiliser ce concept de vectorisation. En pratique, les fonctions vectorisées sont très souvent écrites en C. Heureusement, vous n'avez pas vous-mêmes à écrire des fonctions en C si vous voulez écrire vos propres fonctions vectorisées ; vous pouvez grâce à deux projets phares que sont Numba et Cython écrire vos codes Python natifs et les transformer, de manière extrêmement facile, en fonctions vectorisées. Nous n'en parlerons pas plus dans cette vidéo puisqu'il s'agit d'un sujet avancé. À bientôt !