Tenseurs & numérique — numpy, PyTorch, flottants fiche thématique 08 · 24 questions · v1 du 11/09/2026 · couvre les cartes Anki « swe » python::numpy::*, pytorch::*, python::flottants, dl::cross-entropy, numerics, cost, diagnostics, embeddings

Fil rouge : A = np.arange(24).reshape(2,3,4), une matrice M de shape (32, 27) à normaliser par lignes, et une somme de 10⁵ valeurs comprises entre −10⁴ et 10⁴ à comparer à une tolérance de 10⁻⁵.

01

Mémoire et shapes — ruban, vues, reshape

QComment prédire n'importe quel reshape ?

La mémoire d'un ndarray est un ruban linéaire d'éléments ; la shape n'est qu'un découpage. On déroule le ruban 0..n−1 et on le recoupe aux nouvelles dimensions, dans l'ordre C (dernier axe le plus rapide).

arange(24).reshape(2,3,4) : la ligne [0][1] vaut 4,5,6,7.

Au tableau

« Les données sont un ruban et la shape une grille de lecture, donc reshape change la grille sans toucher au ruban, donc on prédit le résultat en recoupant la suite 0..n−1. »

QPourquoi A.reshape(-1, -1) est-il illégal ?

Deux inconnues pour une seule équation x·y = taille : système sous-déterminé. −1 signifie « résous cette dimension » : une division taille / produit des dimensions connues. Une seule inconnue à la fois.

Au tableau

« −1 se calcule en divisant la taille par les dimensions connues, donc il faut que toutes les autres soient connues, donc un seul −1. »

QB = A.transpose(1,0,2) : par quoi commence B.reshape(-1), et pourquoi ce n'est pas le ruban de A ?

transpose ne change que les strides, le ruban reste 0..23. Mais reshape lit dans l'ordre logique de B : B[0] = A[:,0,:] = [0,1,2,3,12,13,14,15]. B est non contigu, donc reshape(-1) doit copier.

Au tableau

« La transposée réordonne la grille de lecture sans bouger la mémoire, donc l'ordre logique diverge du ruban, donc un reshape doit recopier pour produire un ruban contigu. »

QEn PyTorch, quand préférer view à reshape, et inversement ?

view ne copie jamais : il relit le storage avec une nouvelle grille, donc échoue si le tenseur n'est pas contigu — c'est un garde-fou. reshape renvoie une vue si possible, sinon une copie, silencieusement. view quand on veut être sûr de partager la mémoire ; reshape quand on accepte l'incertitude.

Après y = x.reshape(...), écrire dans y modifie x… ou pas, selon la contiguïté. C'est le piège.

Au tableau

« view garantit le partage de mémoire ou échoue, donc il est explicite ; reshape choisit pour vous, donc il cache une copie éventuelle. »

QSlicing, transposée, permute : vue ou copie ? Quel test décisif ?

Une vue : mémoire partagée, souvent non contiguë. Test : compter les éléments — une vue ne peut pas en inventer ; une opération qui produit plus d'éléments que l'entrée a nécessairement copié.

Au tableau

« Ces opérations ne font que changer strides et offset, donc aucune donnée n'est déplacée, donc c'est une vue et toute écriture remonte à l'original. »

02

Agrégation, axis, broadcasting

QUne agrégation avec axis=k : que devient la shape ?

L'axe k disparaît : (4,3).sum(axis=0) → (3,), .sum(axis=1) → (4,). Lecture sûre : « l'axe k est consommé », plutôt que « sommer le long de l'axe », ambigu et source d'inversion.

Au tableau

« L'agrégation contracte un axe, donc cet axe n'existe plus en sortie, donc pour savoir ce qui reste on efface l'axe k de la shape. »

QÀ quoi sert keepdims=True, et que change-t-il à la shape ?

L'axe agrégé est conservé avec taille 1 au lieu d'être supprimé : (4,3).sum(axis=1, keepdims=True) → (4,1). keep the dims, pas keep the values. Sert à ce que le résultat se broadcaste contre l'original.

Au tableau

« Sans keepdims le résultat est 1-D et perd son orientation, donc le broadcasting peut l'aligner sur le mauvais axe, donc on garde l'axe à 1 pour qu'il s'étire au bon endroit. »

QM / M.sum(1) sans keepdim sur M de shape (32, 27) : que se passe-t-il ?

sum(1) rend un 1-D de taille 32, sans orientation. Le broadcasting aligne par la droite : (32,) devient (1,32) face à 27 colonnes → RuntimeError, bug attrapé. Avec (32,26) ou une matrice carrée, ça broadcasterait silencieusement sur le mauvais axe.

Au tableau

« Un vecteur 1-D n'a pas d'orientation, donc le broadcasting le pose à droite, donc il se retrouve face aux colonnes, donc soit ça plante, soit ça calcule faux sans bruit. »

QÉnonce la règle complète du broadcasting numpy.

1. Si les ndim diffèrent, on complète la shape la plus courte à gauche par des 1. 2. On aligne par la droite : deux dimensions sont compatibles si égales ou si l'une vaut 1 ; la dimension 1 s'étire. Sinon erreur.

(5,1,3) + (4,3) : (4,3) devient (1,4,3), résultat (5,4,3). (4,3) + (4,) : erreur, car (4,) s'aligne sur 3.

Au tableau

« On complète à gauche par des 1, donc les shapes ont le même nombre d'axes, donc on compare axe par axe et seul un 1 peut s'étirer. »

Qvalsd (4,1) et ad (1,6) : pourquoi valsd == ad a-t-il la shape (4,6) ?

La shape du résultat est fixée par les shapes des opérandes seules, indépendamment de l'opérateur : (4,1) et (1,6) s'étirent mutuellement en (4,6). C'est le geste qui élimine la boucle sur les valeurs candidates : (vals[:, None] == a[None, :]).sum(1) compte chaque valeur.

Au tableau

« Chaque 1 s'étire contre la dimension de l'autre, donc les deux vecteurs deviennent une grille 4×6, donc l'opérateur s'applique élément par élément sur cette grille. »

03

Masques et comptage

QCompter les occurrences d'une valeur sans boucle ni Counter ?

(a == v).sum(). Formulation : un test produit un masque, la somme d'un masque est un comptage.

Au tableau

« Une comparaison vectorisée rend un tableau de booléens, donc True vaut 1 dans une somme, donc la somme compte les occurrences. »

QSur A et un masque m : différence entre m.sum() et A[m].sum() ?

m.sum() compte les True. A[m].sum() somme les valeurs de A aux positions True.

A = arange(12).reshape(3,4), m = (A > 2) & (A < 8) : m.sum() = 5, A[m].sum() = 3+4+5+6+7 = 25.

Au tableau

« Le masque est un tableau de booléens, donc sa somme est un effectif ; A[m] extrait des valeurs, donc sa somme est un total. »

QPourquoi (A > 2) and (A < 8) lève-t-il une erreur alors que & fonctionne ?

and est un opérateur de court-circuit : il réclame la valeur de vérité d'un opérande entier, ambiguë pour un tableau (truth value of an array … is ambiguous). & est élément par élément. Les parenthèses sont obligatoires : & est prioritaire sur >.

Au tableau

« and doit décider si tout le tableau est vrai, donc il ne peut pas, donc on utilise l'opérateur bit à bit qui travaille élément par élément, entre parenthèses. »

Qnp.bincount ou np.unique(return_counts=True) ?

bincount quand les valeurs sont des entiers ≥ 0, denses et bornés (labels, ids) : O(n) temps, O(max(a)) mémoire — max(a) = 10⁹ alloue 8 Go. unique sinon : trie, O(n log n), toute valeur hashable.

Au tableau

« bincount indexe un tableau par la valeur, donc il est linéaire mais alloue jusqu'au max, donc il faut des entiers petits et denses, sinon unique. »

04

Flottants — erreur relative et accumulation

Qfloat64 : combien de bits de mantisse, quelle erreur relative, quel symptôme ?

53 bits, donc erreur relative d'arrondi 2⁻⁵³ ≈ 1,1·10⁻¹⁶, soit ~16 chiffres significatifs. Symptôme : 1e9 + 1e-7 redonne exactement 1e9 — le petit terme tombe sous le dernier bit du grand.

Au tableau

« La mantisse a 53 bits, donc la résolution relative est 2⁻⁵³, donc tout ajout plus petit que 10⁻¹⁶ fois le nombre est perdu. »

QUne accumulation float64 sur n opérations doit tenir dans une tolérance absolue. Pourquoi les 16 chiffres ne garantissent rien ?

L'erreur d'arrondi est relative — proportionnelle au nombre manipulé — alors que la tolérance est absolue. Grande somme courante + petite tolérance = perte. Et l'erreur s'accumule sur n opérations.

Machine à 4 chiffres : 12,34 + 0,005 ne tient pas ; 100 fois de suite, on a perdu 0,5 sans jamais dépasser l'arrondi unitaire.

Au tableau

« Chaque opération arrondit à ε fois la grandeur courante, donc l'erreur absolue dépend de la somme, donc sur n opérations elle vaut jusqu'à n × ε × max|somme|, à comparer à la tolérance. »

QBudget d'erreur d'une accumulation : la formule et son application au fil rouge.

Erreur ≤ n × ε × max|somme partielle|. Fenêtre glissante, n = 10⁵, valeurs dans ±10⁴, fenêtre k : somme ≤ k·10⁴ ; avec k = 10⁵ : 10⁻¹⁶ × 10⁹ = 10⁻⁷ par opération, × 10⁵ opérations = 10⁻², contre une tolérance de 10⁻⁵ sur la moyenne → la version float ne passe pas. En int, l'erreur est nulle.

Au tableau

« On borne la somme courante, donc l'erreur par opération, donc on multiplie par le nombre d'opérations, donc on compare à la tolérance dans la même unité. »

QComparer un résultat flottant à une valeur exacte ?

Jamais ==. assert abs(x − cible) < eps, ou math.isclose / np.allclose. Toujours la valeur absolue de l'écart : x − cible < eps passe pour x très négatif.

Au tableau

« Un flottant n'a pas de représentation exacte, donc l'égalité stricte échoue au hasard, donc on compare l'écart absolu à une tolérance. »

QPourquoi ne jamais stocker un montant monétaire dans un flottant binaire ? Et que faire ?

La base 2 ne représente finiment que les fractions à dénominateur puissance de 2 : 0,1 est périodique en binaire, donc 0,1 + 0,2 ≠ 0,3. Les entiers restent exacts jusqu'à 2⁵³ : c'est la fraction qui pose problème. Stocker en centimes entiers (ou en décimal).

Au tableau

« Une fraction décimale n'est pas finie en binaire, donc chaque montant porte une erreur, donc les sommes dérivent, donc on stocke des entiers de centimes. »

Piège
Un test qui écrit 899.99 via JSON, relit et compare l'égalité ne distingue pas un stockage exact d'un stockage flottant : la perte a lieu une fois, à la conversion du littéral, identiquement des deux côtés.
05

Deep learning — numérique et coût

QPourquoi F.cross_entropy attend-elle des logits et non des probabilités ?

Elle applique le softmax elle-même pour garder la main sur l'étape numériquement dangereuse (log-sum-exp stable). Lui passer des probabilités les fait re-softmaxer : elles vivent dans [0,1], donc leurs exponentielles dans [1, e], rapport maximal 2,72 entre classes — les prédictions sont écrasées.

Au tableau

« Le softmax puis le log sont instables séparément, donc la fonction les fusionne, donc elle exige l'entrée brute, donc des probabilités en entrée sont softmaxées une seconde fois et aplaties. »

QCross-entropy numériquement stable : comment, et pourquoi le calcul naïf échoue ?

Naïvement on exponentie puis on loggue : e¹⁰⁰ déborde en inf, et une probabilité minuscule tombe à 0 puis log 0 = −inf. Stable : log Σ ezi = m + log Σ ezi−m avec m = max z. Le terme du max vaut e⁰ = 1, donc la somme est ≥ 1 et le log est fini ; l'underflow des autres devient inoffensif.

Au tableau

« Soustraire le max ne change pas le résultat mathématique, donc on exponentie des nombres ≤ 0, donc plus d'overflow, et le terme du max garantit une somme ≥ 1, donc plus de log 0. »

QCoût d'un produit matriciel (n×m)(m×p) ?

Le produit des trois dimensions, n·m·p : les deux qui survivent et celle qui est contractée. L'axe contracté disparaît de la shape de sortie mais pas du coût : il est travaillé puis sommé. Erreur symétrique : compter seulement la sortie n·p.

Au tableau

« Chaque case de sortie coûte une somme de m produits, donc n·p cases fois m, donc l'axe contracté compte autant que les autres. »

QÀ l'initialisation d'un classifieur à V classes, quelle loss attendre, et que signale un écart ?

log V : un modèle qui n'a rien appris met 1/V sur chaque classe, donc −log(1/V). Une loss initiale bien plus haute signale des logits trop grands (init trop confiante) → réduire l'échelle des poids de sortie. exp(loss) se lit comme le nombre de candidats entre lesquels le modèle hésite encore.

Au tableau

« Un modèle vierge devrait être uniforme, donc sa loss vaut log V, donc une loss initiale supérieure trahit une confiance non méritée dans l'initialisation. »

QUn embedding est un produit par une matrice one-hot. Pourquoi ne l'implémente-t-on jamais ainsi ?

La one-hot est nulle partout sauf en une position, donc le produit ne fait que sélectionner une ligne : un lookup coûte d, le produit coûte V·d. Le rapport vaut exactement V, indépendant de d.

Au tableau

« Multiplier par un one-hot revient à lire une ligne, donc on lit la ligne directement, donc on économise un facteur V sans changer le résultat. »

Réactivation, pas découverte. Cartes sources : tags python::numpy (axis, shape, broadcasting, masques, comptage, reshape, strides), pytorch (broadcasting, memory), python::flottants, dl (cross-entropy, numerics, cost, diagnostics, embeddings). Suite : fiche 09 (coding).
À la craie : réponds à voix haute, avec les « donc », avant de retourner la fiche.
Ta réponse orale, comparée à la fiche :

Le tronc et les branches

quelle shape sort, et où l'erreur d'arrondi se cache-t-elle ?ruban / vuesmémoire = ruban, shape = décoview jamais de copie ; reshapslicing / transpose → vuereshape(-1,-1) : deux inconnuaxis / broadcastingaxis=k : l'axe k disparaîtkeepdims : garde la dim à 1aligner par la droite ; 1 s'é(4,1) vs (1,6) → (4,6)masquesmasque = test ; somme = comptm.sum() vs A[m].sum()& pas and : pas de court-circbincount : entiers denses borflottants53 bits : 2⁻⁵³ ≈ 10⁻¹⁶ relatierreur relative × grandeur = budget : n × ε × max|somme|jamais == ; money en entiersDL numériquecross_entropy attend des logilog-sum-exp : soustraire le mmatmul : produit des trois diloss initiale = log V

Trois branches numpy/PyTorch (mémoire et shapes ; agrégation et broadcasting ; masques et comptage), une branche flottants (l'erreur est relative, l'accumulation la rend absolue), et une branche deep learning (là où la numérique et le coût décident : logits, log-sum-exp, coût du matmul, loss initiale, embeddings).