NumPy est le moteur invisible des calculs scientifiques en Python. Pourtant, 90% des utilisateurs l'utilisent mal. Voici comment gagner 50 fois en vitesse sans changer une ligne de code.
NUMPY, LE MOTEUR QUI FAIT TOURNER L'INTELLIGENCE ARTIFICIELLE
Derrière des bibliothèques comme Pandas, Scikit-Learn ou PyTorch, il y a un héros discret : NumPy. Ce module Python est le cœur battant des calculs numériques. Son secret ? Une implémentation ultra-optimisée en langage C, qui manipule directement des blocs de mémoire sans passer par les lenteurs de Python.
Mais voici le problème : la plupart des gens utilisent NumPy comme du Python normal, avec des boucles for et des calculs naïfs. Résultat ? Des programmes qui consomment des montagnes de mémoire et qui mettent des heures à s'exécuter. La bonne nouvelle, c'est qu'en changeant juste trois petites habitudes, tu peux accélérer tes calculs de 50 fois .
ASTUCE 1 : LA VECTORISATION, OU COMMENT TUER LES BOUCLES FOR
Le pire ennemi de la performance en calcul numérique, ce sont les boucles for. Chaque itération force Python à vérifier les types et à chercher les méthodes, ce qui ralentit tout. Pire encore : beaucoup croient que np.vectorize va les sauver. Mais c'est un piège . Cette fonction ne fait que cacher une boucle Python lente derrière une interface plus propre. Aucun gain de performance.
La solution ? La vectorisation. Au lieu d'écrire des boucles, tu utilises les fonctions natives de NumPy qui tournent directement en C. Par exemple, pour normaliser les colonnes d'une matrice (soustraire la moyenne et diviser par l'écart-type), voici comment faire mal :
import numpy as np
import time
# Création d'une matrice de 50 000 lignes et 1 000 colonnes
matrix = np.random.rand(50000, 1000)
start_time = time.time()
# Normalisation naïve avec des boucles for
res = matrix.copy()
for col in range(matrix.shape[1]):
col_mean = np.mean(matrix[:, col])
col_std = np.std(matrix[:, col])
for row in range(matrix.shape[0]):
res[row, col] = (matrix[row, col] - colmean) / colstd
durationloop = time.time() - starttime
print(f"Calcul avec boucles for terminé en : {duration_loop:.4f} secondes")
Voici comment faire bien, avec la vectorisation :
import numpy as np
import time
# Création d'une matrice de 50 000 lignes et 1 000 colonnes
matrix = np.random.rand(50000, 1000)
start_time = time.time()
# Calcul des moyennes et écarts-types par colonne (en C compilé)
means = np.mean(matrix, axis=0)
stds = np.std(matrix, axis=0)
# Vectorisation : NumPy étire automatiquement les tableaux
res_vectorized = (matrix - means) / stds
durationvectorized = time.time() - starttime
print(f"Calcul vectorisé terminé en : {duration_vectorized:.4f} secondes")
Comment ça marche ? NumPy utilise la diffusion (broadcasting). Quand tu fais (matrix - means), NumPy aligne automatiquement les tableaux de tailles différentes. La matrice a une forme (50000, 1000) et means a une forme (1000,). NumPy étire mentalement le tableau means pour qu'il corresponde à la matrice, sans copier les données. Tout se passe en mémoire, et les calculs sont poussés vers le processeur via des instructions SIMD (Single Instruction, Multiple Data), comme si tu avais 1000 calculs en parallèle.
ASTUCE 2 : LES OPÉRATIONS IN-PLACE, OU COMMENT ÉCONOMISER LA MÉMOIRE
Quand tu écris y = 2 * x + 3, tu penses que NumPy va optimiser le calcul. Mais en réalité, il crée des tableaux temporaires à chaque étape : d'abord 2 * x, puis 2 * x + 3. Pour des tableaux de millions d'éléments, ça signifie des allocations de mémoire inutiles et un gaspillage de temps.
Prenons un exemple concret avec un tableau d'un million d'éléments :
import numpy as np
import time
# Création d'un tableau de 10 millions d'éléments
x = np.random.rand(10000000)
scale = 2.5
offset = 1.2
start_time = time.time()
# Calcul naïf : création de tableaux temporaires
# (scale x) puis (scale x + offset)
y_naive = scale * x + offset
durationnaive = time.time() - starttime
print(f"Calcul naïf terminé en : {duration_naive:.4f} secondes")
Ce code prend 0,0393 seconde. Maintenant, voici comment faire mieux en utilisant les opérations in-place :
import numpy as np
import time
# Création d'un tableau de 10 millions d'éléments
x = np.random.rand(10000000)
scale = 2.5
offset = 1.2
start_time = time.time()
# Pré-allocation du tableau final
yoptimized = np.emptylike(x)
# Calcul direct dans le tableau cible, sans tableaux temporaires
np.multiply(x, scale, out=y_optimized)
np.add(yoptimized, offset, out=yoptimized)
durationoptimized = time.time() - starttime
print(f"Calcul optimisé terminé en : {duration_optimized:.4f} secondes")
print(f"Accélération : {durationnaive / durationoptimized:.2f}x plus rapide .")
L'astuce ? On pré-alloue le tableau y_optimized une seule fois. Ensuite, on utilise les fonctions NumPy comme np.multiply et np.add avec le paramètre out pour écrire directement les résultats dans ce tableau. Résultat : plus de tableaux temporaires, moins de pression sur la mémoire vive, et des calculs plus rapides grâce à une meilleure utilisation du cache CPU.
ASTUCE 3 : LES VUES MÉMOIRE, OU COMMENT ÉVITER LES COPIES INUTILES
Un autre piège classique : les copies de données. Quand tu extrais une sous-matrice avec des indices avancés, NumPy crée une copie physique des données. Pour une matrice de 10 000 x 10 000, ça peut prendre 0,15 seconde, soit 15 000 fois plus lent qu'un simple accès .
Voici comment faire mal :
import numpy as np
import time
# Création d'une matrice de 10 000 x 10 000 éléments
matrix = np.random.rand(10000, 10000)
start_time = time.time()
# Extraction avec indices avancés : FORCE UNE COPIE
rows = np.arange(0, matrix.shape[0], 2)
cols = np.arange(0, matrix.shape[1], 2)
submatrixcopy = matrix[rows[:, None], cols]
durationcopy = time.time() - starttime
print(f"Extraction avec copie terminée en : {duration_copy:.4f} secondes")
Voici comment faire bien, avec les vues mémoire :
import numpy as np
import time
# Création d'une matrice de 10 000 x 10 000 éléments
matrix = np.random.rand(10000, 10000)
start_time = time.time()
# Extraction avec découpage de base : RETOURNE UNE VUE (pas de copie)
submatrixview = matrix[::2, ::2]
durationview = time.time() - starttime
print(f"Extraction avec vue terminée en : {duration_view:.8f} secondes")
La différence ? Quand tu utilises matrix[::2, ::2], NumPy ne copie pas les données. Il crée juste un en-tête de tableau avec de nouvelles métadonnées : une forme différente et de nouveaux strides (le nombre d'octets à sauter pour trouver l'élément suivant). C'est comme si tu avais un plan de métro : au lieu de reconstruire toutes les stations, tu changes juste les numéros des lignes. Résultat : un accès instantané, même pour des matrices géantes.
Attention cependant : une vue partage la même mémoire que l'original. Si tu modifies submatrixview, tu modifies aussi matrix. Pour éviter ça, utilise .copy() si tu veux une vraie copie indépendante.
BILAN : TROIS RÈGLES POUR DES CALCULS ULTRA-RAPIDES
Voici les trois leçons à retenir pour écrire du code NumPy performant :
- Vectorise : remplace les boucles par des opérations sur tableaux entiers. Utilise les fonctions NumPy natives (
np.mean,np.std, etc.) et la diffusion (broadcasting). - Pré-alloue : évite les tableaux temporaires en utilisant
out=ou des opérateurs in-place (*=,+=). - Utilise des vues : privilégie les découpages de base (
arr[::2]) plutôt que les indices avancés (arr[[0,2,4]]) pour éviter les copies.
NumPy n'est pas magique, mais il est incroyablement puissant si tu sais comment l'utiliser. La prochaine fois que ton code met 11 secondes à s'exécuter, rappelle-toi : avec ces trois techniques, tu peux le faire en 0,2 seconde. C'est ça, la différence entre un code qui traîne et un code qui explose.
- KDnuggets
L'indépendance de CLODCO est votre garantie.
Pour que l'actualité de l'IA reste sans filtre et sans concession, votre soutien est indispensable. Votre contribution est le seul moteur de notre liberté éditoriale.
Soutenir CLODCO

