un peu d'algèbre linéaire, juste un peu...

L'algèbre linéaire n'est pas aussi barbare qu'on le croit. Espaces vectoriels, vecteurs et matrices : Sans Bug. Promis.

L'algèbre linéaire peut sembler effrayante pour les personnes qui n'ont pas de connaissances préalables, mais en réalité, ce n'est pas aussi compliqué qu'il n'y paraît. N'ayez pas peur de ces mots et des concepts qui vont suivre, car nous allons démystifier tout cela ensemble. Je n'ai pas l'ambition de vous transformer en experts en algèbre linéaire, mais plutôt de vous aider à comprendre les idées fondamentales et à acquérir les bases nécessaires pour que vous puissiez poursuivre votre apprentissage dans le domaine de la manipulation des données numériques.

L'algèbre linéaire est une branche des mathématiques qui étudie les structures mathématiques appelées espaces vectoriels. Elle se concentre sur les opérations effectuées sur les vecteurs et les matrices, telles que la somme, le produit scalaire, la multiplication de matrices, la résolution de systèmes linéaires, etc.

L'algèbre linéaire est largement utilisée dans de nombreux domaines, tels que la géométrie, la mécanique quantique, l'optimisation, la statistique, la reconnaissance de formes, etc. Elle joue également un rôle crucial dans le développement des méthodes numériques pour la résolution de problèmes complexes. Raisons pour lesquelles il est important de passer par là pour manipuler et traiter des données, quelles qu'elles soient.

Les espaces vectoriels

Tout commence avec les espaces vectoriels. Comme son nom l'indique, c'est un espace au sein duquel on peut y faire évoluer des vecteurs (on y revient juste après).

C'est un peu comme un terrain de jeu pour les vecteurs, où l'on peut effectuer des opérations pour obtenir de nouveaux vecteurs à partir des anciens. Les propriétés de l'espace sont strictement définies, comme le nombre de dimensions de l'espace par exemple, ce qui va s'avérer très utile par la suite, vous verrez.

voici un espace en 2 dimensions ou 2D (source wikipedia)

Les vecteurs

Maintenant que l'on a évoqué ce que sont les espaces vectoriels, on peut préciser que les vecteurs peuvent être représentés graphiquement sous la forme de flèche ou bien sous la forme d'une séquence de nombre, où chacune de ces valeurs correspond à une dimension de l'espace. Les vecteurs peuvent être additionnés les uns aux autres et peuvent être multipliés par des nombres appelés scalaires.

Le même espace que précédemment avec des vecteurs (source wikipedia)

Il peut être particulièrement utile de représenter les données numériques sous cette forme lorsque l'on souhaite les traiter grâce à tous les outils que nous offre l'algèbre linéaire.

Si vous êtes enseignant et que vous souhaitez représenter et modéliser les performances de chacun de vos élèves, vous pouvez considérer les notes de vos n élèves sur les m derniers examens :

$$eleves = \{eleve_1, ..., eleve_i, ..., eleve_n\}$$ $$eleve_i = (exam_1, ..., exam_m)$$

Si vous êtes Botaniste et que vous souhaitez catégoriser des types de plantes, vous pouvez définir l'une de ces plantes en fonction de ses caractéristiques :

$$specimens = \{ specimen_1, ..., specimen_i, ..., specimen_n\}$$ $$specimen_i = (longueur feuille, largeur feuille, presence fleur)$$

Opérations sur les vecteurs

Nous avions dit que des opérations étaient possibles sur les vecteurs, voici donc quelques outils pour manipuler les vecteurs. Pour enrichir un peu votre vocabulaire informatique, on parle de "primitive" pour "désigner une fonction de base fournie par une couche logicielle, juste au-dessus de l'architecture matérielle d'un ordinateur" (définition Wikipédia). Et c'est ce que l'on va faire ici. Commençons par l'addition et la soustraction. Prenons un vecteur v = (3, 7) et un vecteur w = (10, 8),

Si le vecteur $$ \overrightarrow{u} = \overrightarrow{v} + \overrightarrow{w}$$

alors $$\overrightarrow{u} = (3 + 10, 7 + 8) = (13, 15)$$

Notez que : $$\overrightarrow{u} = \overrightarrow{v} + \overrightarrow{w} = \overrightarrow{w} + \overrightarrow{v} $$

Il en va de même de la soustraction, et je ne vous ferai pas l'affront de refaire un exemple. Si nous souhaitons coder ces opérations en python, voilà ce que cela pourrait donner si nous utilisions les lists en guise de vecteurs :

def add_vectors(v, w):
    return [v_i + w_i for v_i, w_i in zip(v, w)]

def sub_vectors(v, w):
    return [v_i - w_i for v_i, w_i in zip(v, w)]

La dernière opération dont avons parlé est la multiplication d'un vecteur par un scalaire, qui peut être fait de la manière suivante :

def multiply_scalar(C, v):
    return [C * v_i for v_i in v]

Il peut également être intéressant de faire la somme élément par élément d'une liste de vecteurs :

def sum_vector(vectors):
    result = vectors[0]
    for vector in vectors[1:]:
        result = add_vectors(result, vector)
    return result

Ou pour ceux qui ont la "ref" (si vous ne l'avez pas, je vous invite à passer cette fonction :)), on peut rendre cela plus élégant :

def sum_vector(vectors):
    # /!\ il faut définir reduce()
    return reduce(add_vectors, vectors)

On peut aussi vouloir calculer la moyenne élément par élément d'une liste de vecteurs :

def means_vector(vectors):
    return multiply_scalar(1/len(vectors), sum_vector(vectors))

Le produit scalaire de 2 vecteurs correspond à la somme des produits élément par éléments :

def dot_product(v, w):
    return sum(v_i * w_i for v_i, w_i in zip(v, w))

Pour la somme des carrés d'un vecteur :

def sum_of_square(v):
    return dot(v, v)

Cette dernière fonction nous permet de calculer aisément la norme/magnitude/longueur d'un vecteur :

import math

def magnitude(v):
    return math.sqrt(sum_of_square(v))

Finalement, avec ces fonctions, nous pouvons calculer la distance entre 2 vecteurs :

def distance(v, w):
    return magnitude(sub_vectors(v, w))

Dernier point, mais pas des moindres : ces fonctions sont très utiles dans le cas où nous souhaiterions manipuler des vecteurs en Python vanilla (ie: python sans lib.), et par conséquent expliquer l'algèbre linéaire par exemple. Néanmoins, en tant que professionnel, nous utilisons davantage la bibliothèque numpy et ses objets appelés array. L'utilisation de cette bibliothèque est bien plus optimale.

Les matrices, sans bug

Une matrice peut être considérée comme un tableau ou un rectangle rempli de nombres (réels ou complexes), en 2 dimensions donc. On peut voir une matrice comme une liste de liste, mais chacune des listes se doit d'avoir la même dimension. Les matrices peuvent être utilisées pour représenter et manipuler des ensembles de données. Le panel d'opérations sur les matrices est un peu plus varié que leurs composants vecteurs. Il y en a tellement que je ne vais pas les détailler en python vanilla comme je l'ai fait pour les vecteurs, mais voici une liste non-exhaustive des opérations sur les matrices : Nous avons parmi les plus utiles la somme, la multiplication par un réel, le produit de 2 matrices, la transposition, la décomposition, l'inversion, etc.

Les matrices sont souvent utilisées dans les applications mathématiques telles que les systèmes linéaires, la transformation de coordonnées, la simulation, la reconnaissance de formes, etc. Vous l'aurez compris, par reconnaissance de forme, j'entends computer vision. Raisons pour lesquelles je vous parle de tout ça !

Voici donc 2 matrices :

A = [[0, 1, 2],
     [3, 4, 5]]

B = [[0, 1],
     [2, 3],
     [4, 5]]

La matrice A dispose de 2 lignes et de 3 colonnes, tandis que la matrice B a 3 lignes et 2 colonnes. C'est leur "shape", ou forme. Pour accéder à un élément de la matrice, on utilise la notation suivante :

$$M_{i, j} = v$$ $$M_{0, 2} = 2$$

A[i][j] = v
A[0][2] = 2

Autrement dit, la valeur v à la ligne i et la colonne j. Dans notre exemple : la valeur à la ligne 0 et à la colonne 2 est 2.

Oui, notez bien qu'en informatique le premier élément est toujours 0, et non pas 1.

Définissons une fonction permettant d'obtenir la shape de la matrice passée en paramètre :

def shape(M):
    return len(M), len(M[0]) if M else 0

La première valeur concerne le nombre de lignes dans la matrice M et la seconde, le nombre de valeurs dans la première ligne (RAPPEL : toutes les lignes d'une matrice doivent avoir le même nombre de valeurs). Si la matrice est vide, la condition permet d'éviter à la machine de nous retourner le nombre de valeurs d'une ligne qui n'existerait pas, ce qui lèverait donc une erreur.

Nous avons vu qu'une matrice pouvait être vue comme une collection (en programmation, une collection est une structure qui contient des données, ou comme ici d'autres collections) de vecteurs. Il peut être intéressant de ne récupérer qu'un vecteur contenu à une position dans la matrice :

def get_row(M, i):
    return M[i]

def get_col(M, j):
    return [M_i[j] for M_i in M]

La première fonction get_row() permet de récupérer la ligne i de la matrice M. Tandis que la seconde fonction get_col() permet d'obtenir la colonne à la position j.

Une fois de plus, il existe des bibliothèques spécialisées dans la manipulation des matrices et donc plus performantes que les fonctions que nous pourrions implémenter ici.

Les matrices sont un outil indispensable dans le cadre de la data science. Elles permettent de représenter et de manipuler de vastes ensembles de données de manière efficace et structurée. Si l'on prend par exemple un réseau de personnes et que nous souhaitons modéliser les relations (de connaissances par exemple) qu'il existe entre elles, les matrices (d'adjacence) permettent de représenter cela efficacement à partir du moment ou un grand nombre de personnes sont présentes dans notre jeu de donnée.

personnes = ["Alain", "Antonio", "Grace", "Milena", "Pierre", "Karl", "Jade", "Sarah", "Paul", "Eva",
             "Haytham", "Hayasa"]

relations = [("Alain", "Grace"), ("Alain", "Pierre"), ("Alain", "Jade"), ("Alain", "Paul"),
             ("Alain", "Hayasa"), ("Antonio", "Milena"), ("Antonio", "Pierre"), ("Antonio", "Karl"),
             ("Antonio", "Haytham"), ("Grace", "Jade"), ("Grace", "Sarah"), ("Grace", "Eva"),
             ("Milena", "Karl"), ("Milena", "Sarah"), ("Milena", "Haytham"), ("Pierre", "Jade"),
             ("Pierre", "Paul"), ("Karl", "Jade"), ("Karl", "Eva"), ("Karl", "Hayasa"),
             ("Jade", "Paul"), ("Jade", "Haytham"), ("Sarah", "Eva"), ("Sarah", "Haytham"),
             ("Sarah", "Hayasa"), ("Paul", "Haytham"), ("Paul", "Hayasa"), ("Eva", "Haytham"),
             ("Eva", "Hayasa"), ("Haytham", "Hayasa")]

ADJ_MAT = [[1 if (p1, p2) in relations or (p2, p1) in relations else 0 for p2 in personnes]
           for p1 in personnes]

En effet, au lieu de parcourir toutes les relations entre 2 personnes pour vérifier que Pierre et Paul ou Jade et Hayasa se connaissent, il suffirait alors de vérifier simplement de la manière suivante :

def is_friend(M, p1, p2):
    return bool(M[personnes.index(p1)][personnes.index(p2)])


print(is_friend(ADJ_MAT, "Pierre", "Paul"))  # True
print(is_friend(ADJ_MAT, "Jade", "Hayasa"))  # False

Il devient alors possible d'énumérer la liste des connaissances d'une personne donnée :

def is_friend_of(M, p1):
    return [p2 for p2 in personnes if is_friend(M, p1, p2)]


print(is_friend_of(ADJ_MAT, "Haytham")) # ['Antonio', 'Milena', 'Jade', 'Sarah', 'Paul', 'Eva', 'Hayasa']

Les matrices sont également utilisées pour effectuer des transformations sur les données, telles que les normalisations et les centrage-réductions, afin de les préparer pour les algorithmes de machine learning (mais nous y reviendrons dans d'autres articles). En somme, l'utilisation des matrices est un élément clé pour l'analyse des données dans l'apprentissage machine et leur compréhension est fondamentale pour les professionnels travaillant dans ce domaine.