contenu de ce notebook (sauter si déjà acquis)¶
la vectorisation (appliquer une fonction à tout un tableau sans passer par un
for-python)les
ufuncnumpy.vectorize
# on importe la librairie numpy
import numpy as np
from matplotlib import pyplot as pltqu’est-ce que la vectorisation ?¶
# pour comparer les choses comparables
import math
n = 1_000_000
x = np.linspace(0, 2*np.pi, n)%%timeit
# la bonne façon
np.sin(x) # np.sin appliquée au tableau x69.2 ms ± 24.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%%timeit
# la mauvaise façon
for e in x: # une boucle for sur un tableau numpy
# c'est toujours une mauvaise idée
math.sin(e)606 ms ± 116 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
dans une première version de ce notebook, pour cette deuxième - et mauvaise - façon de faire on avait artificiellement forcé le trait car:
on avait utilisé
np.sinau lieu demath.sin; merci à Damien Corral qui a remarqué quenp.sinappliqué à un scalaire Python ajoute une inefficacité !et de plus on rangeait les résultats dans une liste, ce qui aggrave encore les écarts
après ces corrections, qui permettent de mieux isoler la perte d’efficacité, on observe toujours un rapport de performance important ! alors qu’on ne garde même pas les résultats du calcul...
dessiner un cercle de rayon r¶
exercice
Dessinez un cercle de rayon r
indices
avec variant de 0 àsi votre cercle apparaît elliptique, c’est que les échelles de vos axes diffèrent
demandez à ce qu’elles soient égales avecplt.axis('equal')
# votre codecalculer une fonction polynomiale¶
exercice
faites une fonction qui retourne le calcul d’un polynome
par exemple
(puissance:**ounp.power)appliquez la directement à un
np.ndarray(sans faire defor) qu’obtenez-vous en retour ?tracez la courbe de la fonction
# votre code ici
def scalar_function(x):
passles ufunc¶
qu’est-ce qu’une ufunc¶
Le mécanisme général qui applique une fonction à un tableau
est connu sous le terme de Universal function - ou encore ufunc
En conclusion, vous devez toujours utiliser les ufunc et plus jamais les for-python
même si ça vous paraît plus difficile
même si vous utilisiez des
for-pythonen prépapar souci de la performance en temps, et de propreté de votre code, vous ne pouvez plus y échapper
Une habitude à prendre:
c’est juste une autre manière de penser le code
vos codes seront compacts et lisibles (élégants)
Souvenez-vous du terme ufunc car c’est utile pour des recherches sur Internet
quelles sont les fonctions vectorisées ?¶
les opérateurs arithmétiques classiques
et leur contre-partie numpy (Ufuncs)
| opérateur | numpy fonction |
|---|---|
+ | np.add |
- | np.substract |
* | np.multiply |
/ | np.divide |
// | np.floor_divide |
% | np.mod |
** | np.power |
les fonctions de comparaison, trigonométriques...
| fonction | numpy fonction |
|---|---|
| comparaison | np.greater, np.less, np.equal, ... |
| valeur absolue | np.absolute or np.abs |
| trigonometrie | np.sin, np.cos, ... |
| exponentielle | np.exp, np.exp2, .. |
| logarithme | np.log, np.log2, np.log10 |
vous allez les utiliser sans même vous en rendre compte !
savoir si une fonction est une ufunc¶
demandez-le lui
np.add
<ufunc 'add'>numpy.add en est !
# essayez !
np.power<ufunc 'power'>exercice
la fonction
numpy.absest-elle uneufunc?la fonction
absde Python est-elle uneufunc?
# votre codepour vectoriser une fonction¶
exercice
le but du jeu ici c’est de voir comment vectoriser une fonction que vous écrivez vous
si vous préférez, vous pouvez choisir d’implémenter une fonction définie par morceaux
genre sur les nombres négatifs et sur les positifs
écrivez une fonction qui calcule la valeur absolue d’un scalaire x
absolute(x)
on s’interdit donc, dans cet exercice, d’utiliser des fonctions denumpy, ni la fonction builtinabsde Pythontestez votre fonction sur des scalaires
créez un
np.ndarrayde scalaires et appliquez-lui la fonctionque se passe-t-il ?
# votre code iciproblème de la fonction absolute¶
supposons que votre code soit:
def absolute (x):
if x >= 0:
return x
return -x
tab = np.array([10, -30, 56.5])
absolute(tab) # --> BOOMalors vous obtenez
----> if x >= 0:
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()car l’expression x >= 0 appliquée à tab rend le tableau array([False, True, False])
mais le if, appliqué au tableau de booléens [False, True, False], ne sait pas quoi faire !
alors il propose des solutions
ifest-il vrai quand tous les éléments sont vrais ?np.all(x)ifest-il vrai quand au moins un élément du tableau est vrai ?np.any(x)
mais vous ne voulez rien de tout cela !¶
vous voulez que
numpyapplique leifà-chaque-élémenti.e. que la fonction s’exécute de manière vectorisée
la solution:
demander à
numpyde vectoriser la fonction avecnp.vectorizeil considérera l’argument comme un tableau
sur lequel le code Python “normal” sera appelé de manière vectorisée
@np.vectorize
def absolute (x):
if x >= 0:
return x
return -x
absolute(tab)
-> array([10. , 30. , 56.5])c’est quoi cette syntaxe ?
le @np.vectorize en première ligne, c’est ce qu’en Python on appelle un décorateur
c’est comme si on avait fait ceci:
def absolute(x):
if x >= 0:
return x
return -x
# et le décorateur produit une fonction (vectorisée)
# à partir de votre fonction "naive"
absolute = np.vectorize(absolute)# le code
@np.vectorize
def absolute (x):
if x >= 0:
return x
return -x# le code
tab = np.array([10, -30, 56.5])
absolute(tab)array([10. , 30. , 56.5])# et d'ailleurs à titre anecdotique:
# elle fonctionne aussi sur une `list` `python`
absolute([-10, -20, 30])array([10, 20, 30])note sur les performances¶
notez bien que cette façon de faire est plus une commodité qu’autre chose, et ne pensez pas que le traitement va être accéléré pour autant
ci-dessous on va reprendre la même idée que absolute avec, juste pour changer, une fonction qui vaut sur les néftifs et sur les positifs
vous allez constater que l’on peut accélérer considérablement les choses par rapport à np.vectorize, au prix d’une empreinte mémoire plus importante
bref, ne pas hésiter surtout à benchmarker !
on n’en parlera plus dans ce cours, mais il existe aussi des outils qui permettent de compiler le code Python,
comme notamment numba, et plein d’autres
par contre ça demande pas mal de travail supplémentaire...
X = np.linspace(-10, 10, 10_000)# la version avec np.vectorize n'est pas spécialement efficace
@np.vectorize
def x2_x3_vec(x):
return x**2 if x < 0 else x**3
%timeit x2_x3_vec(X)14 ms ± 3.25 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
# on peut faire beaucoup mieux avec ce code
# le défaut c'est qu'on calcule 3 tableaux de la même taille
# en plus du tableau résultat
def x2_x3_where(x):
return np.where( x<0, x**2, x**3)
%timeit x2_x3_where(X)623 μs ± 83.5 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
pour les avancés ou les rapides¶
résultats intermédiaires lors de calculs¶
nous appliquons des opérations vectorisées les unes à la suite des autres à des tableaux...
des espaces mémoire intermédiaires sont créés pour recevoir les résultats des calculs
par exemple la fonction trigonométrique
def trigo (x):
return 4*np.exp(np.cos(x))**2de combien de tableaux intermédiaires avons-nous besoin dans ce calcul ?
(un par calcul unitaire)
on développe le code pour montrer les tableaux intermédiaires
def trigo_function_developpee (x):
int_1 = np.cos(x)
int_2 = np.exp(int_1)
int_3 = np.power(int_2, 2) # idem **
return np.multiply(4, int_3) # idem *ici trois tableaux intermédiaires créés inutilement (3 * x.nbytes octets)
le calcul vectoriel crée de nombreux tableaux intermédiaires
qui peuvent coûter très cher en mémoire
une solution aux tableaux intermédiaires¶
def trigo (x):
return 4*np.exp(np.cos(x))**2code montrant les tableaux intermédiaires
def trigo_function_developpee (x):
int_1 = np.cos(x)
int_2 = np.exp(int_1)
int_3 = np.power(int_2, 2)
return np.multiply(4, int_3)la solution ?
utiliser le paramètre optionnel
out=des opérateursnumpy
avecouton spécifie le tableau où ranger le résultat
def trigo_function_developpee_out (x):
result = np.cos(x) # un pour le résultat
np.exp(result, out=result)
np.power(result, 2, out=result)
np.multiply(4, result, out=result)
return resultmais ce code est
beaucoup plus compliqué à écrire que dans sa version compacte, simple et directe
il sera donc plus propice à des erreurs
il est franchement très difficile à lire !
en conclusion ne faites surtout pas cela systématiquement
vous savez que ça existe
vous y penserez le jour où la création de tableaux intermédiaires prendra une place bien trop importante
le code ci-dessous
def trigo_function_compact (x):
return 4*np.exp(np.cos(x))**2plt.plot(trigo_function_compact(np.linspace(0, 2*np.pi, 1000)));
def trigo_function_developpee (x):
int_1 = np.cos(x)
int_2 = np.exp(int_1)
int_3 = np.power(int_2, 2)
result = 4*int_3
return resultdef trigo_function_developpee_out (x):
result = np.cos(x) # il m'en faut bien un pour le résultat !
np.exp(result, out=result)
np.power(result, 2, out=result)
np.multiply(4, result, out=result)
return resultplt.plot(trigo_function_developpee_out(np.linspace(0, 2*np.pi, 1000)));
temps d’exécution de l’élévation d’un tableau au carré - avancé ou rapide¶
exercice
créez un tableau
numpydes 10000 premiers entiers avecnumpy.arange
# votre codecalculez le temps d’exécution de l’élévation au carré des éléments
a. avec un for-python
b. avec une compréhension Python
c. de manière vectorisée avec
**2d. de manière vectorisée avec
np.powere. de manière vectorisée avec
np.square
# votre codequelles sont les manières de faire les plus rapides ?
# votre codeutilisez
np.vectorizepour décorer votre fonction 2.c; que constatez-vous ?
# votre code