Licence CC BY-NC-ND Valérie Roy
_images/ensmp-25-alpha.png
import numpy as np

algèbre linéaire

Un des premières utilisations de la librarie numpy sera faite dans le cadre des cours de mathématiques et de la manipulation de matrices.

Aussi allons-nous voir dans ce notebook les quelques fonctions d’algèbre linéaire qui vont vous être utiles, naturellement il vous faudra vous référer au site de Python-scientifique pour chercher des fonctions plus avancées.

Il faut savoir que les fonctions d’algèbre linéaire des libraries np.linalg sont tout particulièrement efficaces et ce pour plusieurs raisons:

  1. déjà ces fontions s’appuient sur les algorithmes efficaces

  2. ensuite ces fonctions sont codées dans des langages de bas niveau très proches de la mémoire donc rapides

  3. enfin les implémentations tirent parti du multithreading (où un programme est découpé en sous-programmes s’exécutant en même temps) qui permet de grandement améliorer les temps de calculs.

Il existe des différences entre les fonctions de numpy et de scipy mais nous n’en dirons pas plus référez vous aux explications des concepteurs de ces librairies comme là https://docs.scipy.org/doc/numpy/reference/routines.linalg.html

import numpy as np

La première chose à savoir c’est que naturellement les fonctions numpy vont s’appliquer sur des tableaux de dimensions supérieures à 2 mais, nous n’allons voir ici, que les vecteurs et matrices donc rester en dimension < à 3.

Nous n’avons vu pour l’instant que des opérations élément par élément. Comme par exemple le np.mult ou encore *, qui est le produit matriciel de Hadamard. Voyons maintenant les fonctions dédiées à l’algèbre linéaire.

application linéaire

On va se mettre d’accord sur matrice, vecteur et produit en numpy.

Attention, comme les matrices et les vecteurs seront des np.ndarray, il faudra naturellement faire attention au type des éléments, lors d’éventuelles modifications. Si on tente de modifier un élément d’une matrice de type entier avec un nombre flottant, celui-ci sera tronqué et transformé en entier: vous perdrez de l’information.

la matrice

Une matrice $A$ de dimension $m$ lignes et $n$ colonnes, qui représente une application linéaire $f$ d’un espace de dimension $n$ vers un espace de dimension $m$, est un np.ndarray de forme $(m, n)$.

Voila un exemple d’une matrice $A$ de dimension $(2 \times 3)$

A = np.array([[2, 3, -7], [6, -4, 5]])
A
array([[ 2,  3, -7],
       [ 6, -4,  5]])

le vecteur

Un vecteur $V$ de taille $n$, qui représente un vecteur d’un espace vectoriel de dimension $n$, est un np.ndarray de forme $(n,)$.

Voila un exemple de vecteur $V$ de dimension $(3,)$

V = np.array([1, -3, 8])
V
array([ 1, -3,  8])

Attention les numpy.ndarray suivant ne sont pas des vecteurs mais des matrices. Il y a bien deux dimensions.

np.array([[100, 200, 300]]).shape
(1, 3)
np.array([[100], [200], [300]]).shape
(3, 1)

Celui là est un vecteur au sens de l’UE11.

np.array([100, 200, 300]).shape
(3,)

le produit d’une matrice et d’un vecteur

Le produit $A \cdot V$ représente $f(V)$.

Quel est en numpy le produit qu’on appelle ici $\times$ ? C’est la fonction np.dot (ou encore la méthode np.ndarray.dot).

Calculons $A \cdot V$

# la fonction
np.dot(A, V)
array([-63,  58])
# la méthode
A.dot(V)
array([-63,  58])

le produit de deux matrices

Maintenant prenons $A$ et $B$, deux matrices représentant respectivement les applications linéaires $f$ et $g$, le produit de matrices $A \cdot B$ est la composition des applications $ƒ ○ g$.

$A$ on l’a déjà, prenons $B$

B = np.array([[-5, 4], [3, 9], [-4, 2]])
B
array([[-5,  4],
       [ 3,  9],
       [-4,  2]])
np.dot(A, B)
array([[ 27,  21],
       [-62,  -2]])
A.dot(B)
array([[ 27,  21],
       [-62,  -2]])

raccourci @ pour le produit matriciel

Il s’avère que numpy possède un fonction np.matmul (qui en dimension 2 est à peu près équivalente à np.dot) et qui s’écrit aussi sous la forme @

np.matmul(A, B)
array([[ 27,  21],
       [-62,  -2]])
A @ B
array([[ 27,  21],
       [-62,  -2]])

Il existe des différences entre np.matmul et np.dot tout d’abord , la multiplication par un scalaire n’est possible qu’avec np.dot mais surtout en dimensions supérieure à 2 leur comportement diffère complètement.

Préférer utiliser np.dot (ou demandez à votre professeur de math).

le produit scalaire

Si on applique le produit np.dot à deux vecteurs on en obtient le produit scalaire.

V1 = np.array([1, -3, 8])
V1
array([ 1, -3,  8])
V2 = np.array([-6, 4, -7])
V2
array([-6,  4, -7])
np.dot(V1, V2)
-74

la norme de vecteur

Pour un vecteur $V1 =[v_1, …, v_n]$, , la norme np.linalg.norm sera la racine carré du produit scalaire par lui-même $\displaystyle \left|{ {V}}\right|{2}={\sqrt {v{1}^{2}+\cdots +v_{n}^{2}}}$

# la fonction idoine
np.linalg.norm(V1)
8.602325267042627
# qu'on peut naturellement paraphraser en
np.sqrt(V1.dot(V1))
8.602325267042627

Pour les autres normes de matrices et vecteurs regardez là https://np.org/doc/stable/reference/generated/np.linalg.norm.html#np.linalg.norm

la transposée d’une matrice

C’est la fonction np.transpose. Elle a une version courte .T pour écrire des codes plus lisibles.

A = np.arange(1, 13).reshape(4, 3)
A
array([[ 1,  2,  3],
       [ 4,  5,  6],
       [ 7,  8,  9],
       [10, 11, 12]])
np.transpose(A)
array([[ 1,  4,  7, 10],
       [ 2,  5,  8, 11],
       [ 3,  6,  9, 12]])
A.T
array([[ 1,  4,  7, 10],
       [ 2,  5,  8, 11],
       [ 3,  6,  9, 12]])

les matrices identité

En numpy la fonction porte le très joli nom de eye (parce que eye et I se prononcent pareil), elle renvoie des matrices de type flottant.

A = np.eye(5)
A
array([[1., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0.],
       [0., 0., 1., 0., 0.],
       [0., 0., 0., 1., 0.],
       [0., 0., 0., 0., 1.]])

le déterminant d’une matrice

Il est donné par la fonction np.linalg.det

A = 2*np.eye(3)
A
array([[2., 0., 0.],
       [0., 2., 0.],
       [0., 0., 2.]])
np.linalg.det(A)
7.999999999999998

Essayez de l’appliquer à une matrice qui n’est pas carrée ? Vous allez recevoir une np.linalg.LinAlgError !

B = np.random.random(size=(3, 4))*10
try:
    np.linalg.det(B)
except np.linalg.LinAlgError as e:
    print(f'Oups pas bon ! ou en mieux dit "{e}"')
Oups pas bon ! ou en mieux dit "Last 2 dimensions of the array must be square"

A vous de jouer. Faites une isométrie (une transformation qui conserve les longueurs) et calculez son déterminant.

# votre code ici (une solution dessous)
M = np.array([[0, -1], [1, 0]])
print(M)
np.linalg.det(M)
[[ 0 -1]
 [ 1  0]]
1.0
# une isométrie
M = np.array([[0, -1], [1, 0]])
print(M)
np.linalg.det(M)
[[ 0 -1]
 [ 1  0]]
1.0

les matrices diagonales

On peut créer une matrice diagonale à partir de la liste des éléments de sa diagonale

np.diag([1., 2, 3])
array([[1., 0., 0.],
       [0., 2., 0.],
       [0., 0., 3.]])

On peut aussi extraire la matrice diagonale d’une matrice.

A = np.random.randint(-100, +100, size=(3, 3))
A
array([[-82,  59,  97],
       [ 55, -95,  56],
       [ 13, -39,  57]])
# returns the diagonal of b
np.diag(A) 
array([-82, -95,  57])
# creates a diagonal matrix
np.diag([1, 2, 3])
array([[1, 0, 0],
       [0, 2, 0],
       [0, 0, 3]])

la trace d’une matrice

C’est la somme des éléments de sa diagonale.

A = np.arange(16).reshape(4, 4)
A
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15]])
np.trace(A)
30
np.sum(np.diag(A))
30

l’inversion d’une matrice

c’est le calcul de $A^{-1}$

# prenons une rotation
R = np.array([0, 1, 0, 0, 0, 1, 1, 0, 0]).reshape(3, 3)
R
array([[0, 1, 0],
       [0, 0, 1],
       [1, 0, 0]])
# pour calculer son inverse
np.linalg.inv(R)
array([[0., 0., 1.],
       [1., 0., 0.],
       [0., 1., 0.]])

pour nous convaincre que ça fonctionne comme attendu :
lorsqu’en maths on écrit $A^{-1}A = I$, en informatique c’est $A^{-1}A \approx I$ (presque égal à cause des approximations)

c’est l’intérêt de np.close que de comparer si deux tableaux sont presque identiques

# on prend maintenant une matrice quelconque
n = 3
A = np.random.random(size=(n, n))
A
array([[0.74888908, 0.21829506, 0.78494135],
       [0.37038305, 0.16662601, 0.90668149],
       [0.21865492, 0.12902651, 0.37001961]])
# son inverse
Ainv = np.linalg.inv(A)

Vérifions que leur produit fait bien l’identité

np.dot(Ainv, A) == np.eye(n)
array([[False, False, False],
       [False, False, False],
       [False, False, False]])

Et bien oui on a des valeurs approchées.

np.isclose(  Ainv @ A,  np.eye(n))
array([[ True,  True,  True],
       [ True,  True,  True],
       [ True,  True,  True]])

les valeurs propres d’une matrice (eigen values)

On va calculer les $v$ tels que:

  • $f(v) = \lambda v$

  • $M \cdot v = \lambda v$

M = np.random.random(size=(3, 3))
M
array([[0.2850117 , 0.34332719, 0.9855683 ],
       [0.89049106, 0.10968501, 0.16301911],
       [0.51723741, 0.49578672, 0.0873873 ]])
ls, vs = np.linalg.eig(M)  # eigen_values, eigen_vectors
ls, vs
(array([ 1.31920128+0.j        , -0.41855864+0.31394671j,
        -0.41855864-0.31394671j]),
 array([[ 0.66280856+0.j        ,  0.43285248-0.3321126j ,
          0.43285248+0.3321126j ],
        [ 0.55563722+0.j        , -0.74731234+0.j        ,
         -0.74731234-0.j        ],
        [ 0.5019483 +0.j        ,  0.05712049+0.37496862j,
          0.05712049-0.37496862j]]))

Vérifions qu’on a bien $M \cdot v = \lambda v$

Attention les vecteurs propres sont dans une matrice (3, 3) et chacun d’eux est une des 3 colonnes de la matrice.

# le premier vecteur propre
v0 = vs[:, 0] # toutes les lignes et la colonne 0
# la première valeur propre
l0 = ls[0]
np.all(np.isclose(np.dot(M, v0),  l0*v0))
True

À vous de jouer, à l’aide d’une boucle for-Python, parcourez les valeurs et les vecteurs propres de $M$ et vérifiez $M \cdot v = \lambda v$.

# votre code ici

Prenons la symétrie x↔︎y et calculons ses valeurs et vecteurs propres.

S = np.array([[0, 1], [1, 0]])
values, vectors = np.linalg.eig(S)

print(values)

# les vecteurs sont normés, naturellement
print(vectors)
[ 1. -1.]
[[ 0.70710678 -0.70710678]
 [ 0.70710678  0.70710678]]

résolution d’un système linéaire

On va trouver les racines du système linéaire $A \cdot x = b$

Prenons une matrice $A$.

A = np.random.random(size=(3, 3))
A
array([[0.83955708, 0.05954924, 0.12420057],
       [0.44674119, 0.96525272, 0.83231312],
       [0.2034883 , 0.85556744, 0.73852452]])

Prenons un vecteur $b$.

b = np.array([1, 2, 3])

Calculons la racine de l’équation $A \cdot x = b$

np.linalg.solve?
x = np.linalg.solve(A, b)
x
array([ -6.04959087, -63.64422492,  79.45971427])
np.all( np.isclose( np.dot(A, x), b) )
True

Si la matrice n’est pas inversible ou si elle n’est pas carrée, une erreur np.linalg.LinAlgError sera levée

les fonctions dont nous avons parlé (rapidement)

fonctions

comportement

np.dot

produit de matrice

np.linalg.norm

normes de matrice

np.transpose

transposition de matrice

np.linalg.det

déterminant

np.linalg.inv

inversion

np.linalg.eig

valeurs propres

np.linalg.solve

résolution de système linéaire

np.eye

matrice identité

np.diag

matrice diagonale

Les exercices intéressants seront faits dans l’UE11.

exercices

construire une matrice diagonale à-la-main (peu d’intérêt)

On vous demande d’écrire une fonction matdiag qui

  1. accepte un paramètre qui est une liste de flottants [$x_1$, $x_2$, …, $x_n$]

  2. retourne une matrice carrée diagonale dont les éléments valent

$$ m_{ii} = x_i $$ $$ m_{ij} = 0 \ pour\ i ≠ j $$

  • Retournez toujours un tableau de type float64

  • Programmez une approche naïve et une approche à base de slicing.

https://nbhosting.inria.fr/auditor/notebook/python-mooc:exos/w7/w7-s05-x5-matdiag

remplir une matrice : m(i, j) = xi * xj

On vous demande d’écrire une fonction qui

  1. accepte un nombre quelconque de paramètres, $x_1$, $x_2$, …, $x_n$, tous des flottants

  2. retourne une matrice carrée symétrique $M$ dont les termes valent

$$ m_{ij} = x_i . x_j $$

Indices. Vous pouvez utiliser l’opérateur @, la méthode array.dot(), le broadcasting, la transposée d’une matrice .T.

https://nbhosting.inria.fr/auditor/notebook/python-mooc:exos/w7/w7-s05-x6-xixj