NumPy + TD1 + TD2 ***************** Vous pouvez tester en ligne les exemples grâce aux sites suivant : * https://www.onlinegdb.com/online_Python_compiler * https://www.online-Python.com/ NumPy permet d’exécuter des opérations complexes sur des tableaux en une seule instruction, évitant les boucles explicites et améliorant fortement les performances. Dans la suite, nous supposerons que nous avons importer la librairie NumPy ainsi : .. code-block:: python import numpy as np Les tableaux NumPy ================== Les tableaux NumPy sont des structures de données implémentées en C, stockant des valeurs de même type dans un bloc mémoire ce qui leur permet d’atteindre des performances proches du C. Ils servent de fondement à de nombreuses bibliothèques scientifiques comme PyTorch ou OpenCV. Dans nos applications, nous devons stocker des données (images, sons, textes...). Par exemple, un lot de 50000 images de résolution 36x36 pourra être stocké dans un tableau de dimensions 50000x36x36. Liens utiles : * `La documentation des tableaux NumPy `_ * `NumPy fundamentals `_ .. warning:: Si vous ne précisez pas le type des éléments lors de la création d'un tableau, NumPy en choisit un suivant des règles implicites et il choisira très probablement un type 64 bits. En deep learning, pour éviter toute surconsommation mémoire et toute ambiguïté, il faudra explicitement donner le type voulu. `Fonctions de création `_ : .. list-table:: :widths: 25 50 * - np.zeros(shape=(2,3), dtype=np.int32) - Crée un tableau 2x3 de valeurs entières nulles * - np.ones(shape=(5,5), dtype=np.int32) - Crée un tableau 5x5 d'entiers valant 1 * - np.full(shape=(3,3), fill_value=4) - Crée un tableau 3x3 initialisé avec la valeur 4 * - np.arange(4) - Crée le NumPy array [0,1,2,3] * - np.empty(shape=(3,2), dtype=np.float32) - Crée un tableau de flottants sans initialiser leurs valeurs * - T.copy() - Duplique un NumPy array T `Fonctions de génération aléatoire `_ : .. list-table:: :widths: 25 50 * - np.random.randint(low=3, high=8, size=(2,4)) - Tableau 2x4 avec valeurs aléatoires parmi 3, 4, 5, 6 et 7 * - np.random.rand(3,2) - Tableau 3x2 avec valeurs aléatoires dans [0,1[ avec distribution uniforme * - np.random.normal(loc=0,scale=1,size=(3,2)) - Tableau avec valeurs aléatoires suivant une loi normale(0,1) Conversions .. list-table:: :widths: 25 50 * - A = np.array(L) - Convertit une liste Python en NumPy array * - L = A.tolist() - Convertit un NumPy array en liste Python * - T2 = T1.astype(np.float32) - Crée un nouveau tableau dont les éléments correspondent au type indiqué Dimensions ========== Indices & axes -------------- Les tableaux NumPy peuvent être indexés : * Vecteur : T[i] * Matrice : T[i,j] * Volume : T[i,j,k] L'ordre de lecture des éléments d'un tableau suit le style du langage C : l’indice situé le plus à droite est celui qui varie le plus rapidement. Ainsi, l’élément qui suit T[0,0] est T[0,1], puis T[0,2], et ainsi de suite. .. image:: tbll.png :align: center Il est possible d'indexer une dimension en partant de la fin, ainsi le dernier élément a pour indice -1, le suivant -2 et ainsi de suite : .. image:: array.png :align: center :scale: 70% Chaque dimension correspond à un **axe**. Par exemple, pour un tableau de dimensions (2, 4, 3), on obtient : * T[axe 0, axe 1, axe 2] * axe 0 : 2 indiçages - 0 et 1 * axe 1 : 4 indiçages - 0, 1, 2 et 3 * axe 2 : 3 indiçages - 0, 1 et 2 Sous-tableau ------------ Si le nombre d'indices est inférieur au nombre de dimensions, alors l’indexation permet d’extraire un sous-tableau : .. code-block:: python T = np.array( [[1,2,3],[4,5,6]] ) print(T[0]) >> [1,2,3] print(T[1]) >> [4,5,6] print(T[-2]) >> [1,2,3] .. warning:: Rappelez-vous que l’indexation NumPy s’effectue avec des virgules : T[0,1,2]. Avec le mécanisme des sous-tableaux, il est possible d’obtenir le même résultat en écrivant T[0][1][2], mais cela revient à effectuer trois extractions successives, ce qui dégrade sensiblement les performances. Changement de dimensions ------------------------ Pour changer les dimensions d'un tableau, les nouvelles dimensions doivent contenir le même nombre d'éléments. `Manipulation `_ : .. list-table:: :widths: 25 50 * - T.resize((3,2)) - Modifie les dimensions du tableau T (in-place) * - T2 = T1.reshape((3,2)) - Renvoie un nouveau tableau avec les dimensions demandées, version non in-place * - T.shape - Retourne les dimensions du tableau T * - T.size - Retourne le nombre d'éléments dans le tableau T * - T.ndim - Retourne le nombre de dimensions du tableau T * - T.dtype - Retourne le type des données de T Par conséquent, les éléments d’un tableau réorganisé conservent le même ordre en mémoire que le tableau original. .. image:: tbl.png :align: center .. quiz:: aaazeaa :title: Création des tableaux NumPy :quiz:`{"type":"FB","answer":"ones"}` Quelle fonction crée un tableau initialisé avec des 1 ? :quiz:`{"type":"FB","answer":"zeros"}` Quelle fonction crée un tableau initialisé avec des 0 ? :quiz:`{"type":"FB","answer":"dtype"}` Quel argument permet de sélectionner le type des données ? :quiz:`{"type":"FB","answer":"random"}` Quel package fournit la fonction rand() ? :quiz:`{"type":"FB","answer":"array"}` Quelle fonction crée un tableau NumPy depuis une liste Python ? :quiz:`{"type":"FB","answer":"astype"}` Quelle fonction convertit les éléments d'un tableau vers un autre type ? :quiz:`{"type":"FB","answer":"resize"}` Quelle fonction change les dimensions d'un tableau en mode inplace ? :quiz:`{"type":"FB","answer":"dim"}` Quelle propriété n'appartient pas à un tableau Numpy : dim ou shape ? :quiz:`{"type":"FB","answer":"dtype"}` Quelle propriété retourne le type des données : type, dtype, ntype, xtype ? :quiz:`{"type":"FB","answer":"5"}` Pour un tableau de dimensions (3,4,5), l'axe 2 a quelle taille ? :quiz:`{"type":"FB","answer":"array"}` Quelle est langage fonction qui transforme une liste en tableau ? :quiz:`{"type":"TF","answer":"F"}` Peut-on effectuer un reshape((2,3)) sur un tableau A = np.arange(4) ? :quiz:`{"type":"TF","answer":"T"}` Pour un vecteur de taille 5, l'écriture T[2] et T[-3] est-elle équivalente ? Vue === Pour pouvoir travailler efficacement sur de grands tableaux sans multiplier les copies en mémoire, NumPy a introduit le mécanisme des **vues**. Ainsi, l'appel d'une fonction Numpy peut, **ce n'est pas systèmatique**, retourner un tableau qui ne possède pas ses propres données mais qui les partage avec le tableau d'origine. Par exemple, ce choix est judicieux pour la fonction *reshape*. Cependant, il faut savoir que toute modification du tableau-vue va modifier le tableau original : .. code-block:: python x = np.arange(4) >> array([0, 1, 2, 3]) y = x.reshape((2,2)) >> array([[0, 1],[2, 3]]) y[0,0] = 9 x >> array([9, 1, 2, 3]) y >> array([[9, 1],[2, 3]]) Il faut connaître cette réalité pour éviter certaines surprises. Si vous voulez rompre explicitement le possible lien vers un tableau, il faut utiliser la fonction **copy()**. En deep-learning, les tenseurs des librairies d'IA comme Pytorch ou TensorFlow ont repris la logique des vues car dans un GPU le souci d'optimisation mémoire est encore plus présent. .. quiz:: aaazzerzereaa :title: Vue :quiz:`{"type":"TF","answer":"T"}` La fonction *reshape* peut retourner une vue. :quiz:`{"type":"TF","answer":"F"}` La fonction *reshape* crée toujours une copie. :quiz:`{"type":"TF","answer":"T"}` Les vues permettent d'optimiser l'utilisation de la mémoire. :quiz:`{"type":"TF","answer":"F"}` Le mécanisme des vues n'existe par dans PyTorch et Tensorflow. Le paramètre axis ================= La fonction *mean()* des tableaux Numpy calcule la moyenne des valeurs du tableau. Cependant, grâce à l'argument *axis*, on peut effectuer le calcul dans une direction donnée, ce qui est très intéressant : Pour un tableau *T* de dimension (2,4,3), si on écrit *T.mean(axis=2)*, NumPy va retirer cette dimension du résultat en effectuant la moyenne sur cet axe : * T.means(axis=0) => shape = (4,3) * T.means(axis=1) => shape = (2,3) * T.means(axis=2) => shape = (2,4) Pour un tableau *T* de dimension (2,4,3,6,5) si on donne plusieurs directions, on continue à appliquer la même logique : * T.means(axis=(1,2)) => shape = (2,6,5) * T.means(axis=(0,4)) => shape = (4,3,6) Voici une interprétation graphique : .. image:: tbl2.png :align: center Le paramètre *axis* est utilisé par de nombreuses autres fonctions comme *sum*, *max*, *min*, *std*, *var*... A noter qu'il existe un paramètre supplémentaire **keepdims=True**, qui permet d'obtenir un tableau avec autant de dimensions que le premier. Ainsi avec un tableau *T* de dimension (2,4,3,6,5) on obtient : * T.means(axis=(0,4),keepdims=True) => shape = (1,4,3,6,1) .. quiz:: zerfsdvfc :title: Axis Pour un tableau T = np.array([[1,2,3,4],[5,6,7,8]]), que retourne : T.max(axis=1) ? :quiz:`{"type":"FB","answer":"B"}` réponse A: [5 6 7 8] ou réponse B: [4 8] Pour un tableau T = np.array([[0,1,2],[3,4,5],[6,7,8]]), que faut-il écrire pour obtenir : [2 5 8] ? :quiz:`{"type":"FB","answer":"B"}` réponse A: T.max(axis=0) ou réponse B: T.max(axis=1) Pour T = np.array([ [[11,12],[13,14]], [[21,22],[23,24]], [[31,32],[33,34]] ]), que faut-il écrire pour obtenir [ [13,14], [23,24], [33,34] ] comme réponse ? :quiz:`{"type":"FB","answer":"B"}` réponse A: T.max(axis=0) ou réponse B: T.max(axis=1) ou réponse C: T.max(axis=2) Que faut-il écrire pour obtenir [33, 34] comme réponse avec le même tableau T ? :quiz:`{"type":"FB","answer":"A"}` réponse A: T.max(axis=(0,1)) ou réponse B: T.max(axis=(1,2)) ou réponse C: T.max(axis=(0,2)) Plage d’indices - slicing ========================= Il est possible d'utiliser la syntaxe **start:stop** ou **start:stop:step** pour représenter une plage d’indices. Ce mécanisme, appelé **slicing**, permet d'éviter l'utilisation de boucles et facilite la lecture du code. .. list-table:: :widths: 25 50 * - T[0:5] - Indices de 0 à 4 * - T[0:8:2] - Indices : 0, 2, 4, 6 * - A[:5] - Jusqu'à l'indice 4 compris * - A[5:] - De l'indice 5 jusqu'à la fin * - A[:] - Tous les indices Avec des tableaux 2 dimensions : .. code-block:: python A = np.array( [ [10,11,12,13], [20,21,22,23], [30,31,32,33], [40,41,42,43] ] ) A[1:3,1:3] # sélection des A[i,j] avec 1≤i<3 et 1≤j<3 >> array([[21, 22], [31, 32]]) A[1:3,1:3] = 99 # affectation des indices sélectionnées >> array([ [10, 11, 12, 13], [20, 99, 99, 23], [30, 99, 99, 33], [40, 41, 42, 43]]) Ou encore : .. code-block:: python A = np.array([ [1,2,3,4], [6,7,8,9] ]) A[:,:2] # toutes les lignes, tous les indices < 2 >> array([[1, 2], [6, 7]]) Ou encore : .. code-block:: python A = np.array( [ [10,11,12,13,14,15], [20,21,22,23,24,25], [30,31,32,33,34,35], [40,41,42,43,44,45] ] ) A[:, 0:6:2 ] = 99 # affection des colonnes 0 2 4 ==> array ([ [99, 11, 99, 13, 99, 15], [99, 21, 99, 23, 99, 25], [99, 31, 99, 33, 99, 35], [99, 41, 99, 43, 99, 45]]) Ou encore : .. code-block:: python A = np.array([ [1,2,3,4], [6,7,8,9] ]) A[1] # 2eme ligne A[1,:] # idem >> array([6, 7, 8, 9]) A[0,2:] # 1ere ligne, tous les indices >=2 >> array([3, 4]) A[1,:2] # 2ème ligne, tous les indices < 2 >> array([6, 7]) Indexage avancé =============== Par listes d'index ------------------ Cette technique se déclenche lorsque l'indexation se fait à partir de listes. Elle permet de sélectionner les éléments :math:`(x_i,y_i)` d'un tableau en transmettant les deux listes :math:`(x_i)` et :math:`(y_i)` : .. code-block:: python A = np.array( [ [10,11,12,13,14,15], [20,21,22,23,24,25], [30,31,32,33,34,35], [40,41,42,43,44,45] ] ) A[[0,0,3,3],[0,5,0,5]] # extrait les valeurs aux positions [0,0], [0,5], [3,0] et [3,5] >> array([10, 15, 40, 45]) A[[0,0,3,3],[0,5,0,5]] = 99 # affecte les valeurs aux positions [0,0], [0,5], [3,0] et [3,5] >> [[99 11 12 13 14 99] [20 21 22 23 24 25] [30 31 32 33 34 35] [99 41 42 43 44 99]] On peut coupler la technique d'indexage avancé avec d'autres : .. code-block:: python A = np.array( [ [10,11,12,13,14,15], [20,21,22,23,24,25], [30,31,32,33,34,35], [40,41,42,43,44,45] ] ) A[[1,2],:] # extrait la 2ème et la 3ème ligne du tableau >>array([[20, 21, 22, 23, 24, 25], [30, 31, 32, 33, 34, 35]]) A[:,[1,2]] # extrait la 2ème et la 3ème colonne du tableau array([[11, 12], [21, 22], [31, 32], [41, 42]]) Par booléens ------------ Un tableau *A* peut être indexé à l’aide d'un masque correspondant à un tableau de booléens. Le masque doit être de même dimension que le tableau *A*, il sert ainsi à indiquer quels éléments sélectionner (True) et lesquels ignorer (False). La syntaxe **A[mask]** extrait Les éléments sélectionnés vers un vecteur 1D : .. code-block:: python A = np.arange(12).reshape(3,4) mask = A > 5 vector = A[mask] ----- A ----- [[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11]] ----- mask ----- [[False False False False] [False False True True] [ True True True True]] ----- vector ----- [ 6 7 8 9 10 11] La syntaxe **A[mask] = ...** applique l'affectation uniquement sur les éléments sélectionnés. On peut par exemple écrire : .. code-block:: python A[A>4] = 0 ----- A ----- [[ 0 1 2 3] [ 4 0 0 0] [ 0 0 0 0]] .. quiz:: azeraazeaa :title: Utilisation des tableaux NumPy :quiz:`{"type":"FB","answer":"1,0"}` Pour A = np.array([ [0,1],[2,3],[4,5]]), écrivez sous la forme *x,y* l'indexation de A retournant la valeur 2 . :quiz:`{"type":"FB","answer":"0,2,1"}` Pour A = np.array([[[0,1],[2,3],[4,5]]]), donnez l'indexation de A retournant la valeur 5 (indices positifs). :quiz:`{"type":"FB","answer":"6"}` Pour un tableau 1D, si A[-3] désigne la même cellule que A[3], quelle est la taille de ce tableau ? :quiz:`{"type":"FB","answer":"0 7 2 9"}` Pour le tableau A : [[0,1,2],[4,5,6],[7,8,9]], quels sont les chiffres extraits par : A[[0,2,0,2],[0,0,2,2]]. Donnez les chiffres séparés par des espaces. Broadcasting ============ Introduction ------------ Pourquoi ne pas simplement considérer les tableaux NumPy comme des matrices ? Parce qu’ils permettent des opérations qui seraient interdites dans le monde strictement matriciel comme par exemple : additionner un vecteur ligne à un vecteur colonne ! En NumPy, cette liberté porte un nom : le broadcasting. En voici un exemple : .. image:: vecligne.png :align: center :scale: 80% Que se passe-t-il ? Si vous ajoutez un tableau de taille :math:`(4,1)` avec un tableau de taille :math:`(1,3)`, tout se comporte comme si chaque tableau était dupliqué autant de fois que nécessaire pour obtenir deux tableaux de taille :math:`(4,3)`. Le résultat obtenu est ainsi un tableau de taille :math:`(4,3)`. Voici un exemple : .. code-block:: python import numpy as np A = np.array([[0],[10],[20],[30]]) B = np.array([0,1,2]) A+B >> array([ [ 0, 1, 2], [10, 11, 12], [20, 21, 22], [30, 31, 32] ]) Règle du broadcasting --------------------- Notons :math:`(a_0, \ldots, a_n)` et :math:`(b_0, \ldots, b_n)` les dimensions des deux tableaux en entrée. Pour effecteur une opération de broadcasting entre ces deux tableaux, il faut vérifier que chaque dimension :math:`a_i` soit compatible avec :math:`b_i` : .. panels:: :column: col-lg-10 p-2 Les dimensions :math:`a_i` et :math:`b_i` sont compatibles si : :math:`a_i = b_i` ou :math:`a_i = 1` ou :math:`b_i = 1` La taille :math:`(s_0, \ldots, s_n)` du tableau de sortie est donnée par la formule : .. math:: \boxed{s_i = \max(a_i,b_i)} .. quiz:: borad1 :title: Les dimensions des tableaux sont-elles compatibles ? .. csv-table:: :header: "Taille des tableaux", "Résultat" :delim: ! (3,4) et (3,2) ! :quiz:`{"type":"TF","answer":"F"}` (3,4) et (1,4) ! :quiz:`{"type":"TF","answer":"T"}` (1,5) et (3,1) ! :quiz:`{"type":"TF","answer":"T"}` Aide : * Pour le premier indice, :math:`a_0 = b_0` ce qui convient, mais :math:`a_1 \neq b_1` et :math:`a_1, b_1 > 1` donc les dimensions sont incompatibles. * Pour le premier indice, :math:`\ b_0=1` ce qui convient et pour le deuxième indice : :math:`a_1 = b_1` donc les dimensions sont compatibles et :math:`s = (3,4)`. * Pour le premier indice :math:`a_0 = 1` et :math:`b_1= 1`, les dimensions sont compatibles et :math:`s = (3,5)`. Cas général ----------- Notons :math:`a = (a_0, \ldots, a_n)` et :math:`b = (b_0, \ldots, b_m)` les dimensions des deux tableaux *A* et *B*. Pour effectuer un broadcasting entre *A* et *B* avec :math:`n > m`, il suffit de procéder comme auparavant en considérant que le tableau *B* a pour dimensions : .. image:: ext.png :align: center :scale: 80% Exemple : .. image:: schema.png :align: center :scale: 80% Le tableau entouré en bleu est un tableau de dimensions :math:`(2,2)`, le vecteur colonne sur la gauche a pour dimensions :math:`(3,1,1)`. Après broadcasting, le résultat est un tableau de taille :math:`(3,2,2)`. .. quiz:: borad2 :title: Donnez les dimensions du tableau résultat après une opération de broadcasting .. csv-table:: :header: :math:`a`, :math:`b`, :math:`s` :delim: ! :math:`(3,4)` ! :math:`(1)` ! :math:`(3,4)` :math:`(3,4)` ! :math:`(1,1)` ! :quiz:`{"type":"FB","answer":"(3,4)"}` :math:`(3,1)` ! :math:`(4)` ! :quiz:`{"type":"FB","answer":"(3,4)"}` :math:`(3,1)` ! :math:`(1,4)` ! :quiz:`{"type":"FB","answer":"(3,4)"}` :math:`(1,1,4)` ! :math:`(1,3,1)` ! :quiz:`{"type":"FB","answer":"(1,3,4)"}` :math:`(1,1,4)` ! :math:`(3,2,1)` ! :quiz:`{"type":"FB","answer":"(3,2,4)"}` .. quiz:: borad2 :title: Donnez la dimension des tableaux .. csv-table:: :header: Tableau, Dimension :delim: ! :math:`[ 1, 2 ]` ! :math:`(2)` :math:`[ [1, 2] ]` ! :quiz:`{"type":"FB","answer":"(1,2)"}` :math:`[ [1, 2], [3, 4], [5, 6] ]` ! :quiz:`{"type":"FB","answer":"(3,2)"}` :math:`[ [[1]] ]` ! :quiz:`{"type":"FB","answer":"(1,1,1)"}` :math:`[ [1], [1], [1]]` ! :quiz:`{"type":"FB","answer":"(3,1)"}` :math:`[ [[1]], [[1]], [[1]] ]` ! :quiz:`{"type":"FB","answer":"(3,1,1)"}` Calculs ------- Pour construire le tableau *C* de dimension 2 par broadcasting deux tableaux *A* et *B* de dimensions compatibles, on applique la formule suivante : .. math:: C[i,j] = A[\overline{i},\overline{j}] + B[\overline{i},\overline{j}] La notation :math:`\overline{i}` correspond à l'indice :math:`i` s'il est valide ou à l'indice 0 sinon. Exercice -------- .. quiz:: borad4 :title: Donnez le résultat de l'opération [ [1, 2, 3], [4, 5, 6] ] + [ [0], [1] ] * A : [ [1], [2], [3], [5], [6], [7] ] * B : [ [1, 2, 3], [4, 5, 6], [2, 3, 4], [5, 6, 7] ] * C : [ [1, 2, 3], [5, 6, 7] ] * D : [ [[1, 2, 3], [4, 5, 6]], [[2, 3, 4], [5, 6, 7]] ] :quiz:`{"type":"FB","answer":"C"}` .. warning:: Le système du broadcasting offre l'avantage d'une certaine souplesse. Cependant, son inconvénient est qu'il pourra masquer certaines de vos erreurs en produisant un résultat alors que les entrées sont de tailles non désirées. Pour finir ========== Quelques fonctions utiles que vous rencontrerez sûrement dans des exemples : * *T.ravel()* : variante de reshape qui retourne un tableau 1D contenant tous les éléments de T : aplatissement * *T.flatten()* : comme ravel, mais renvoie toujours une copie * *T.transpose()* : retourne le tableau nD transposé * *T.argmin()* : retourne l’indice de la plus petite valeur dans un tableau, la première rencontrée s'il y en a plusieurs * *T.swapaxes(...)* : échange deux axes d’un tableau NumPy (sans copier les données) * *np.expand_dims(T,...)* : ajoute un axe supplémentaire * *T.squeeze()* : retire les dimensions de taille 1 * concatenate / stack / hstack / vstack : utiles pour fusionner des tableaux TD1 Numpy Array =============== `Le source du Notebook TD1 `_ Vous devez effectuer ce TD et le faire valider à votre responsable de salle. TD2 Slicing and images ====================== `Le source du Notebook TD2 `_ Vous devez effectuer ce TD et le faire valider à votre responsable de salle.