contenu de ce notebook (sauter si déjà acquis)¶
- la vectorisation (appliquer une fonction à tout un tableau sans passer par un
for-python
) - les
ufunc
numpy.vectorize
# on importe la librairie numpy
import numpy as np
from matplotlib import pyplot as plt
qu’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 x
153 ms ± 10.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop 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)
670 ms ± 179 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.sin
au lieu demath.sin
; merci à Damien Corral qui a remarqué quenp.sin
appliqué à 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 code
calculer 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):
pass
les 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-python
en prépa - par 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.abs
est-elle uneufunc
?la fonction
abs
de Python est-elle uneufunc
?
# votre code
pour 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 builtinabs
de Python - testez votre fonction sur des scalaires
- créez un
np.ndarray
de scalaires et appliquez-lui la fonction - que se passe-t-il ?
# votre code ici
problè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) # --> BOOM
alors 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
if
est-il vrai quand tous les éléments sont vrais ?np.all(x)
if
est-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
numpy
applique leif
à-chaque-élément - i.e. que la fonction s’exécute de manière vectorisée
la solution:
- demander à
numpy
de vectoriser la fonction avecnp.vectorize
- il 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)
15.5 ms ± 1.46 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)
729 μs ± 91.9 μ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))**2
de 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))**2
code 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
avecout
on 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 result
mais 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))**2
plt.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 result
def 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 result
plt.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
numpy
des 10000 premiers entiers avecnumpy.arange
# votre code
calculez 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
**2
- d. de manière vectorisée avec
np.power
- e. de manière vectorisée avec
np.square
# votre code
- quelles sont les manières de faire les plus rapides ?
# votre code
- utilisez
np.vectorize
pour décorer votre fonction 2.c; que constatez-vous ?
# votre code