UKOnline

Création de matrice

Les tableaux multidimensionnels de NumPy sont une structure de données très riche. Par exemple, une image en noir et blanc peut être représentée par un tableau à deux dimensions, dont chaque élément représente un pixel de l'image. Une image en couleurs peut, quant à elle, être représentée par un tableau à trois dimensions, superposant trois canaux (rouge, vert et bleu), comme illustré par la figure 1.

En pratique, et notamment en algèbre linéaire, on se retrouve à devoir manipuler des matrices. On pourrait se contenter d'utiliser des tableaux à deux dimensions pour représenter et manipuler des matrices. Néanmoins, la librairie NumPy, mais également tout l'écosystème SciPy, propose du support spécifique pour les matrices, facilitant l'écriture de code qui en manipule et permettant de réaliser toute une série d'opérations qui ne seraient pas pertinentes dans le cas de tableaux multidimensionnels.

Images avec ndarray
Les tableaux multidimensionnels de NumPy peuvent être utilisés pour représenter des images en noir et blanc (avec deux dimensions) ou des images en couleurs (avec trois dimensions).

Objet matrix

Une matrice se représente en NumPy à l'aide d'un objet matrix. Il s'agit en fait d'une sous-classe de ndarray, c'est-à-dire que tout ce que l'on a vu au chapitre précédent s'applique également aux objets matrix. La manière la plus immédiate de créer une matrice consiste à utiliser la fonction matrix, comme dans l'exemple suivant :

Comme on le constate sur le résultat de l'exécution, la fonction matrix a créé un tableau à deux dimensions, dont la forme est $(2, 3)$. La création se fait donc comme avec la fonction array, c'est-à-dire en spécifiant les données de la matrice comme une liste de listes Python :

[[1 2 3]
 [4 5 6]]
(2, 3)

En apparence, aucune différence avec les tableaux multidimensionnels de type ndarray, et c'est normal puisque matrix est une sous-classe de ndarray. On peut vérifier tout cela en affichant le type de l'objet référencé par la variable mat et en vérifiant également si cet objet référencé est une instance de ndarray :

Le résultat obtenu est celui attendu, on a bel et bien créé un objet matrix, ou plus précisément un objet numpy.matrixlib.defmatrix.matrix, qui est également une instance de la classe ndarray :

<class 'numpy.matrixlib.defmatrix.matrix'>
True

Une autre façon de créer une matrice consiste à transformer un tableau multidimensionnel à l'aide de la fonction mat. Voici deux exemples qui utilisent cette fonction :

Dans le premier cas, la fonction mat a transformé un tableau unidimensionnel en un vecteur, c'est-à-dire une matrice-ligne. Dans le deuxième cas, elle transforme simplement un tableau à deux dimensions en une matrice. Le résultat de l'exécution confirme cela :

[[0 1 2 3]]
[[0 1]
 [2 3]]

Voyons deux autres exemples où l'on tente, cette fois-ci, de transformer un tableau de trois dimensions en une matrice :

Le fait qu'un tableau puisse ou pas être transformé en une matrice n'est pas lié au fait qu'il faille exactement deux dimensions au tableau. Dans l'exemple précédent, on a déjà pu voir qu'un tableau à une seule dimension a pu être transformé en une matrice-ligne.

Dans l'exemple que l'on vient de voir, un tableau de dimensions $(4, 1, 3)$ va pouvoir être transformé en une matrice de dimensions $(4, 3)$, sans soucis. Étant donné que l'un des axes ne contient qu'un seul élément, la fonction mat est capable d'obtenir un tableau à deux dimensions en supprimant simplement cet axe. Le résultat obtenu après transformation est donc bien une matrice de forme $(4, 3)$. Par contre, la deuxième transformation que l'on tente échoue en produisant une erreur :

[[1. 1. 1.]
 [1. 1. 1.]
 [1. 1. 1.]
 [1. 1. 1.]]
Traceback (most recent call last):
  File "Desktop/program.py", line 7, in <module>
    print(np.mat(d))
  File "/usr/local/lib/python3.7/site-packages/numpy/matrixlib/defmatrix.py", line 68, in asmatrix
    return matrix(data, dtype=dtype, copy=False)
  File "/usr/local/lib/python3.7/site-packages/numpy/matrixlib/defmatrix.py", line 132, in __new__
    new = data.view(subtype)
  File "/usr/local/lib/python3.7/site-packages/numpy/matrixlib/defmatrix.py", line 177, in __array_finalize__
    raise ValueError("shape too large to be a matrix.")
ValueError: shape too large to be a matrix.

Enfin, il existe une autre façon de créer une matrice, pas disponible pour les tableaux multidimensionnels de manière générale. Il s'agit de décrire les éléments de la matrice par une chaine de caractères où les éléments d'une même ligne de la matrice sont séparés par des virgules, et où les lignes sont séparées par un point-virgule. On peut, par exemple, écrire l'instruction suivante :

L'exécution de cette instruction crée une matrice de trois lignes de deux colonnes, comme on le voit sur le résultat de l'exécution :

[[1 2]
 [3 4]
 [5 6]]

On peut également séparer les éléments d'une même ligne par des espaces, plutôt que par des virgules, pour rendre le code plus lisible. On aurait ainsi pu écrire :

Matrice spécifique

En complément à la fonction matrix, d'autres fonctions sont disponibles pour créer des matrices spécifiques. Certaines de ces fonctions proviennent directement de la librairie NumPy tandis que d'autres viennent de la librairie ScipPy, en particulier du module scipy.linalg.

Matrice identité

On peut construire une matrice identité d'ordre $n$, c'est-à-dire une matrice carrée dont les éléments de la diagonale principale valent $1$ et tous les autres valent $0$, avec la fonction identity :

Comme on le voit sur le résultat de l'exécution, la fonction identity construit, par défaut, une matrice dont les éléments sont de type float :

[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

Pour obtenir des données d'un autre type, on peut toujours utiliser le paramètre optionnel dtype comme pour les ndarray. Pour avoir des nombres entiers, on aurait donc écrit :

On peut aussi créer une matrice identité avec la fonction eye. Cette dernière est néanmoins plus générale que identity, car elle n'est pas limitée aux matrices carrées et car elle permet de décaler la diagonale vers le haut ou vers le bas avec son paramètre optionnel k. Voici trois exemples qui utilisent cette fonction et ses paramètres :

La première instruction construit une matrice identité d'ordre $2$, comme ce que calcule l'expression np.identity(2). La deuxième instruction construit une matrice rectangulaire de deux lignes et quatre colonnes, c'est-à-dire de forme $(2, 4)$, dont la « diagonale principale » (la notion de diagonale principale n'existe que pour les matrices carrées et, pour une rectangulaire, on se réfère à la diagonale principale de la plus petite matrice carrée englobant la matrice rectangulaire considérée, c'est-à-dire celle d'une matrice d'ordre $4$ dans cet exemple, comme illustré par la figure 2) a été décalée d'une position vers le haut. La dernière instruction, quant à elle, construit une matrice identité d'ordre $3$ dont la « diagonale principale » a été décalée de deux positions vers le bas. La figure 2 illustre le résultat de l'exécution que voici :

[[1. 0.]
 [0. 1.]]
[[0. 1. 0. 0.]
 [0. 0. 1. 0.]]
[[0. 0. 0.]
 [0. 0. 0.]
 [1. 0. 0.]]
Matrice identité avec eye
Le paramètre optionnel k de la fonction eye permet de décaler la diagonale principale de la « matrice identité équivalente » vers le haut ou vers le bas.

Matrice triangulaire

On peut également construire une matrice triangulaire avec la fonction tri, c'est-à-dire une matrice remplie de $1$ sur et en-dessous de la diagonale principale et des $0$ de l'autre côté.

Les paramètres sont les mêmes que la fonction eye, c'est-à-dire que l'on peut définir une matrice rectangulaire et indiquer la position de la diagonale délimitant le « triangle de $1$ ». Par défaut, la matrice construite est carrée et la limite du « triangle » est la diagonale principale.

Voici deux exemples de création d'une matrice triangulaire :

Comme on le voit sur le résultat, la première instruction crée une matrice carrée d'ordre $2$ et la seconde une matrice de dimensions $(2, 4)$ :

[[1. 0.]
 [1. 1.]]
[[1. 1. 0. 0.]
 [1. 1. 1. 0.]]

Matrice diagonale

On peut aussi vouloir créer une matrice diagonale, c'est-à-dire une matrice carrée dont tous les éléments sont nuls sauf ceux de la diagonale principale. Pour cela, il suffit d'utiliser la fonction diag en lui passant en paramètre un tableau unidimensionnel qui contient les éléments à placer sur la diagonale. Analysons l'exemple suivant :

L'exécution de ces instructions crée une matrice carrée d'ordre $3$ dont la diagonale principale est le vecteur $(4, 0, -2)$, comme le confirme le résultat de l'exécution :

[[ 4  0  0]
 [ 0  0  0]
 [ 0  0 -2]]

On peut aussi extraire une diagonale d'un tableau à deux dimensions avec la fonction diag. Comme le montre l'exemple suivant, il suffit pour cela de fournir ce tableau en paramètre et d'éventuellement utiliser le paramètre optionnel k pour désigner la diagonale à extraire, la valeur par défaut étant $0$ :

La première diagonale extraite est la diagonale principale de la matrice carrée équivalente, c'est-à-dire celle qui commence à l'élément situé à l'indice $(0, 0)$, à savoir le vecteur $(0, 6, 12)$. La deuxième diagonale extraite est celle située deux positions plus haut. Comme on le voit sur le résultat de l'exécution, elle ne comporte que deux éléments :

[[ 0  1  2  3  4]
 [ 5  6  7  8  9]
 [10 11 12 13 14]]
[ 0  6 12]
[3 9]

Autre matrice

Enfin, on peut créer toute une série de matrices particulières à l'aide de fonctions du module scipy.linalg. On peut créer des matrices circulantes (circulant), des matrices compagnons (companion), des matrices de Hadamard (hadamard), de Hankel (hankel), de Hilbert (hilbert), de Leslie (leslie), de Pascal (pascal), de Toeplitz (toeplitz), ou encore des matrices triangulaires inférieures (tril) ou supérieures (triu). Voici un exemple qui crée une matrice de Pascal :

Ce code imprime la matrice de Pascal d'ordre $4$, comme on peut le voir sur le résultat de l'exécution :

[[ 1  1  1  1]
 [ 1  2  3  4]
 [ 1  3  6 10]
 [ 1  4 10 20]]

Construction avancée

On peut aussi vouloir construire des matrices à partir d'autres, en en combinant plusieurs entre elles. On peut aussi vouloir décrire les éléments d'une matrice à l'aide d'une fonction génératrice. Cette section présente les fonctions proposées par la librairie NumPy pour ce faire.

Tiling

Le tiling consiste à construire une matrice en répétant plusieurs fois un même motif donné, un peu comme on réaliserait une mosaïque avec des carrelages pour couvrir une surface. La librairie NumPy propose une fonction tile, à qui on donne en paramètres une matrice qui est le motif à répéter et une description de la répétition à faire. Voyons d'abord un premier exemple simple :

On construit donc d'abord une matrice carrée d'ordre $2$ stockée dans pattern. On fait ensuite appel à la fonction tile pour répéter $3$ fois le motif. Comme on le voit sur le résultat de l'exécution, la répétition se fait horizontalement, comme ce que fait la fonction hstack :

[[1 2 1 2 1 2]
 [3 4 3 4 3 4]]

La fonction tile permet donc de faire les mêmes empilements que les fonctions hstack et vstack. Néanmoins, on peut aller plus loin avec tile, en spécifiant un tuple d'entiers en second paramètre. L'exemple suivant répète le motif pattern de sorte à avoir un résultat final avec deux lignes, chacune contenant trois fois le motif horizontalement :

Le résultat de l'exécution est une matrice de forme $(4, 9)$ et on voit bien que le motif a été répété six fois en tout :

[[1 2 3 1 2 3 1 2 3]
 [4 5 6 4 5 6 4 5 6]
 [1 2 3 1 2 3 1 2 3]
 [4 5 6 4 5 6 4 5 6]]

Notez que la fonction tile peut en fait être utilisée avec n'importe quel tableau multidimensionnel et n'est pas limitée aux matrices. Le résultat aura toujours un nombre de dimensions qui le maximum entre celui du motif et le nombre d'éléments du tuple décrivant les répétitions à faire.

Matrice par blocs

On peut également définir une matrice par blocs, c'est-à-dire que l'on va construire une matrice à partir d'autres matrices. Pour ce faire, on utilise la fonction bmat comme dans l'exemple suivant :

Les trois premières instructions créent les trois matrices suivantes :

$$A = \left( \begin{array}{cc} 1 & 1 \\ 1 & 1 \end{array} \right) \qquad B = \left( \begin{array}{c} 2 \\ 2 \end{array} \right) \qquad C = \left( \begin{array}{ccc} 3 & 3 & 3 \end{array} \right)$$

La quatrième instruction construit une nouvelle matrice dont les premières lignes sont construites à partir de celles des matrices $A$ et $B$ mises côte-à-côte et dont la dernière ligne est la matrice $C$. La matrice qui est créée est donc la matrice par blocs suivante :

Matrice par blocs

On peut voir sur le résultat de l'exécution que la matrice par blocs créée correspond bien au résultat attendu, à savoir une matrice avec trois lignes et trois colonnes :

[[1 1 2]
 [1 1 2]
 [3 3 3]]

Pour que la construction par blocs fonctionne, il faut évidemment que toutes les matrices combinées soient de dimensions compatibles, c'est-à-dire que le nombre de lignes et de colonnes soient compatibles.

Enfin, tout comme pour la création d'une matrice, on peut définir les blocs à combiner comme une chaine de caractères. L'exemple précédent peut donc également se réécrire comme suit :

Fonction génératrice

On peut vouloir créer une matrice dont la valeur des éléments dépend de la ligne et de la colonne selon une fonction bien précise. La fonction fromfunction de NumPy permet justement de créer une matrice à partir d'une fonction. Voyons, par exemple, comment créer une matrice dont l'élément $(\cdot)_{ij}$ vaut $f(i, j) = i + j$ :

Le premier paramètre de fromfunction est la fonction $f$, que l'on va appeler fonction génératrice, qui définit la valeur de l'élément en ligne $i$ et en colonne $j$ et le second paramètre est la forme désirée. On obtient donc comme résultat une matrice de forme $(4, 4)$ dont la valeur de chaque élément est la somme de l'indice de sa ligne et sa colonne :

[[0 1 2 3]
 [1 2 3 4]
 [2 3 4 5]
 [3 4 5 6]]

Cette fonction fromfunction ne se limite en fait pas aux matrices et peut être utilisée pour créer des tableaux multidimensionnels de n'importe quelle forme. La fonction à passer en premier paramètre doit prendre autant de paramètres qu'il y a de dimensions. Pour créer un tableau à trois dimensions, il faut donc fournir une fonction lambda i, j, k: comme premier paramètre de fromfunction, par exemple.

Matrice creuse

Pour toutes les matrices que l'on a vues jusqu'à présent, on a à chaque fois spécifié les valeurs de leurs différents éléments, de manière explicite ou par construction. Il arrive régulièrement que l'on doive manipuler des matrices creuses, c'est-à-dire des matrices dont la plupart des éléments valent $0$ et dont seuls quelques éléments sont donc différents de zéro.

Afin d'optimiser l'espace mémoire utilisé pour de telles matrices creuses, il existe une série de fonctions qui créent de telles matrices. Ces dernières sont définies dans le module scipy.sparse. Chacune de ces fonctions définit une manière particulière de spécifier les éléments non-nuls et leur position dans la matrice.

Format par coordonnées

Voyons un premier exemple qui crée une matrice creuse de forme $(4, 4)$ qui ne possède que trois éléments non-nuls, en utilisant le format par coordonnées (COO), à l'aide de la fonction coo_matrix :

La matrice que l'on crée ne possède que trois valeurs non-nulles définies par le tableau vals, à savoir $7$, $-1$ et $2$, dont les coordonnées sont décrites par les deux tableaux rows et cols. La première valeur se trouve en $(0, 0)$, la deuxième en $(3, 1)$ et enfin la dernière en $(1, 2)$. L'affichage produit par l'exécution du programme est différent de l'habituel et confirme que l'on a une matrice définie autrement. Seuls les éléments différents de zéro sont mémorisés, avec leur coordonnée :

  (0, 0)	7
  (3, 1)	-1
  (1, 2)	2
(4, 4)

Malgré le format de stockage en mémoire qui est différent (pour gagner de l'espace mémoire), il s'agit toujours d'une matrice (et donc d'un tableau multidimensionnel) qui a été créé par la fonction coo_matrix.

Néanmoins, on ne peut pas utiliser tout ce qui a été vu précédemment avec ces matrices. On ne peut, par exemple, pas accéder aux éléments par leurs indices ni les parcourir à l'aide d'un itérateur. Les matrices creuses possèdent par contre un attribut data pour avoir un tableau avec tous leurs éléments. On peut parcourir la matrice comme suit :

La première instruction affiche un tableau qui contient les mêmes éléments que la matrice creuse mat. Il s'agit en fait d'une vue de la matrice creuse, car toute modification d'un élément est faite sur la matrice originale comme on peut le constater sur le résultat de l'exécution :

[ 7 -1  2]
99
-1
2

Par contre, on peut réaliser des opérations arithmétiques, comme la multiplication par un scalaire ou l'addition matricielle, sur des matrices creuses utilisant le format par coordonnées. Par exemple, on peut écrire :

La première instruction crée une nouvelle matrice creuse comme résultat tandis que la deuxième crée une nouvelle matrice dense, comme on le devine sur le résultat de l'exécution :

  (0, 0)	198
  (3, 1)	-2
  (1, 2)	4
[[100   0   0   0]
 [  0   1   2   0]
 [  0   0   1   0]
 [  0  -1   0   1]]

On peut vérifier si une matrice est une matrice creuse utilisant le format par coordonnées, ou non, en utilisant la fonction isspmatrix_coo, de nouveau définie dans le module scipy.sparse :

La matrice a est donc bien une matrice creuse utilisant le format par coordonnées, et la matrice b non :

True
False

On peut aussi appliquer des fonctions prédéfinies ou d'agrégation sur des matrices creuses pour réaliser des opérations sur ces dernières. On peut, par exemple, trouver le plus grand élément d'une matrice creuse ou calculer le sinus de ses éléments :

La première fonction renvoie un seul élément tandis que la seconde renvoie une nouvelle matrice creuse :

99
  (0, 0)	-0.9992068341863537
  (3, 1)	-0.8414709848078965
  (1, 2)	0.9092974268256817

Enfin, on peut convertir une matrice creuse en une matrice dense à tout moment, à l'aide de la méthode todense à appeler sur la matrice à convertir. Il ne faut évidemment le faire que lorsque c'est effectivement nécessaire puisque, à partir de ce moment, on va utiliser beaucoup plus d'espace mémoire.

Par exemple, pour convertir une matrice creuse mat en une matrice dense, il suffit d'écrire l'instruction suivante :

Le résultat de l'exécution confirme que la variable a contient bel et bien un objet de type matrix :

<class 'numpy.matrixlib.defmatrix.matrix'>

Format compressé

Deux autres formats de stockage possibles sont ceux par lignes ou colonnes creuses compressées (CSR ou CSC). Ils permettent de faire des opérations arithmétiques de manière efficace, entre matrices du même format. De plus, on peut récupérer des sous-matrices, à l'aide de l'opérateur d'accès et de slicing, contrairement au format par coordonnées qui l'interdit. Le slicing de lignes est efficace avec CSR, tandis que le slicing de colonnes est plutôt lent. C'est exactement l'opposé avec CSC qui est plus efficace pour des opérations sur les colonnes que sur les lignes.

Les objets csr_matrix et csc_matrix du module scipy.sparse permettent de construire des matrices suivant les formats CSR et CSC. L'exemple suivant montre la création de deux matrices :

Deux matrices sont créées, avec les mêmes données que celle précédemment créée avec le format par coordonnées, mais en suivant respectivement le format avec lignes creuses compressées (CSR), et colonnes creuses compressées (CSC). Voici le résultat de l'exécution :

  (0, 0)	7
  (1, 2)	2
  (3, 1)	-1
  (0, 0)	7
  (3, 1)	-1
  (1, 2)	2

Deux observations peuvent être faites sur ce résultat. Tout d'abord, on se rend compte qu'au niveau du stockage, il est similaire en apparence à celui réalisé par le format par coordonnées. On peut d'ailleurs facilement effectuer des conversions entre ces formats. En effet, si mat est une matrice de type coo_matrix, on peut écrire :

En regardant de plus près les différences entre les représentations des deux matrices, on réalise que les coordonnées ont été triées par lignes, pour celle créée suivant le format CSR. De manière similaire, la matrice stockée au format CSC retient ses éléments dans l'ordre des indices des colonnes. C'est exactement ce qui se passe et c'est également la raison pour laquelle les opérations sur les lignes sont plus efficaces avec les matrices CSR et inversement pour les CSC.

Terminons en voyant comment se comportent ces matrices lors d'opérations arithmétiques et de slicing :

La variable result contient le résultat de l'addition d'une matrice au format CSR avec une au format CSC. Le format de la matrice résultant de l'addition est au format CSR, c'est-à-dire le type de l'opérande gauche. L'exemple montre également que l'on peut facilement extraire des lignes ou des colonnes, en récupérant la sous-matrices qui commence à la ligne d'indice $1$ et s'étend jusque la dernière ligne, et qui prend toutes les colonnes. Le résultat de l'exécution confirme tout cela :

  (0, 0)	14
  (1, 2)	4
  (3, 1)	-2
<class 'scipy.sparse.csr.csr_matrix'>
  (0, 2)	4
  (2, 1)	-2

Autre format

Le module scipy.sparse définit d'autres formats pour représenter des matrices creuses. En plus des trois formats présentés plus haut, on retrouve du stockage par blocs de lignes creuses (bsr_matrix), par diagonales (dia_matrix), par dictionnaires avec tri par clé (dok_matrix) et enfin par listes chainées de lignes (lil_matrix). Toutes ces classes ont la même classe mère spmatrix.

Les deux formats les plus efficaces pour construire une matrice creuse sont dok_matrix et lil_matrix. Il n'est généralement pas recommandé d'utiliser les fonctions de NumPy pour réaliser des opérations sur les matrices creuses créées avec les fonctions du module scipy.sparse, car elles pourraient ne pas être efficaces ou même, dans de rares cas, ne pas calculer le bon résultat. Mieux vaut passer par les méthodes des matrices creuses, ou les transformer en matrices denses. Par exemple, on obtient le sinus des éléments d'une matrice creuse comme suit :