Matplotlib pour l'apprentissage automatique

Apprenez les bases de matplotlib en 1h. Vous créerez vos premières intrigues avec un exemple d'apprentissage automatique de jouet.

introduction

Python propose un grand nombre de bibliothèques de visualisation.

Les plus récents, comme le bokeh et les holoviews, sont très puissants. Ils nous permettent de créer facilement des tracés interactifs qui peuvent être affichés dans le navigateur. Et l'extension datashader permet d'afficher des données volumineuses sans tuer le navigateur client.

Matplotlib, en revanche, existe depuis un certain temps et peut sembler un peu limité par rapport aux bibliothèques plus récentes. Mais la plupart des gens l'utilisent toujours (ne serait-ce que de temps en temps) et il excelle à produire des tracés de qualité de publication pour les articles.

De toute évidence, vous devez connaître matplotlib si vous souhaitez utiliser du python scientifique, en particulier pour l'apprentissage automatique. Ce tutoriel se concentrera donc sur cet outil.

Pour illustrer les fonctionnalités de base de matplotlib, nous allons travailler sur un problème d'apprentissage automatique de jouets et créer des intrigues réellement utiles dans la vie réelle.

Le problème du jouet sera la classification des exemples avec une seule variable d'entrée x en deux catégories, 0 et 1, en fonction de la valeur de x.

Pour chaque catégorie, les échantillons suivront la distribution normale . Cela signifie qu'ils seront distribués selon une fonction de densité de probabilité gaussienne (pdf).

Ce tutoriel est interactif.

Pour fonctionner sur Google Colab, il vous suffit de suivre ce lien .

Vous pourrez exécuter le code vous-même et le modifier.

La distribution normale / Tracer une fonction

Tout d'abord, nous allons apprendre à tracer une fonction $f(x)$.

Pour $f$, nous choisirons une fonction de densité de probabilité (pdf) que nous utiliserons plus tard pour générer un jeu de données.

Tout d'abord, une brève introduction aux fonctions de densité de probabilité.

Supposons que $x$ est une variable aléatoire continue distribuée entre moins l'infini et plus l'infini.

Si nous tirons au hasard des valeurs de $x$, la distribution de ces valeurs le long de $x$ est décrite par la pdf de $x$. Plus la pdf est élevée à un $x$ donné, plus la probabilité d'obtenir des valeurs autour de $x$ est grande. Plus précisément, la probabilité qu'une valeur se situe entre $x=a$ et $x=a + da$, où $da$ est un intervalle infinitésimal, est $f(a) da$.

La probabilité qu'une valeur tombe entre $-\infty$ et $+\infty$ est de 1, donc une pdf est toujours normalisée à 1 :

$$\int_{-\infty}^{\infty} f(x) dx = 1.$$

En fait, l'intégrale somme simplement les probabilités que la valeur se retrouve dans tous les intervalles infinitésimaux.

Toute fonction peut être une pdf tant qu'elle est normalisée à 1 et toujours positive (une probabilité ne peut pas être négative). Par exemple, la distribution uniforme est parfaitement plate sur toute la gamme $x$.

Ici, nous allons choisir la pdf le plus célèbre, la distribution normale, également connue sous le nom de distribution gaussienne.

La distribution normale est disponible dans scipy.stats. Nous pourrions également utiliser un outil de plus bas niveau, numpy.random.normal .

In [1]:
from scipy.stats import norm

Pour tracer cette fonction, nous devons d'abord définir une plage de valeurs de $x$ pour laquelle nous évaluerons la fonction.

Avec numpy, il est facile de créer un tableau numpy avec des valeurs régulièrement espacées dans une plage donnée :

In [2]:
import numpy as np
np.set_printoptions(suppress=True, threshold=20)
x = np.linspace(-5,5,100)
x
Out[2]:
array([-5.       , -4.8989899, -4.7979798, ...,  4.7979798,  4.8989899,
        5.       ])

Pour que les 100 valeurs soient uniformément positionnées entre -5 et 5, nous obtenons ces nombres décimaux étranges. Pour éviter cela, nous avons besoin d'une valeur à zéro, et nous devons donc générer un nombre impair de valeurs :

In [3]:
import numpy as np
np.set_printoptions(suppress=True, threshold=20)
x = np.linspace(-5,5,101)
x
Out[3]:
array([-5. , -4.9, -4.8, ...,  4.8,  4.9,  5. ])

Ensuite, nous évaluons la fonction pour toutes les valeurs x en une seule fois, encore une fois grâce à numpy :

In [4]:
# we choose mean=0, sigma=1
# for our gaussian pdf
y = norm.pdf(x, 0., 1.)
y
Out[4]:
array([0.00000149, 0.00000244, 0.00000396, ..., 0.00000396, 0.00000244,
       0.00000149])

Enfin, nous pouvons tracer y vs x :

In [5]:
import matplotlib.pyplot as plt
plt.plot(x, y)
Out[5]:
[<matplotlib.lines.Line2D at 0x7f725959ffd0>]

Matplotlib a automatiquement lié les données dans les tableaux y et x (qui doivent avoir la même longueur), a tracé les 101 points et connecté les points avec des lignes droites. Ce n'est pas visible sur le graphique vu le nombre assez important de points.

Notez la forme en cloche de la distribution normale. Il existe une probabilité maximale d'obtenir des valeurs x autour de zéro, avec une certaine incertitude. Cette distribution est typique des mesures expérimentales.

Par exemple, si vous mesurez plusieurs fois la température ambiante, vous pouvez obtenir : 19,7, 19,6, 19,7, 19,5, ... Ces valeurs sont distribuées selon la distribution normale en raison des processus statistiques sous-jacents affectant la mesure. La moyenne de vos mesures est une bonne estimation de la température ambiante réelle, et l'écart vous donne l'incertitude de votre mesure.

Nous avons fait le tracé étape par étape cette fois, mais souvent, nous définissons y (et parfois aussi x) à la volée :

In [6]:
plt.plot(x, norm.pdf(x, 0, 1))
plt.plot(x, norm.pdf(x, 2, 0.5))
Out[6]:
[<matplotlib.lines.Line2D at 0x7f7259092b50>]

Cette fois, nous avons ajouté une seconde distribution normale centrée à 2, avec un spread plus petit.

Cette distribution pourrait représenter la mesure d'une valeur plus élevée, avec une meilleure précision.

A noter que matplotlib est suffisamment astucieux pour changer automatiquement la couleur des différents tracés, et pour adapter la portée de l'axe des y afin que l'ensemble de l'information soit visible.

Si vous êtes physicien des particules, vous connaissez ROOT. Je ne vais pas commenter plus loin.

Cosmétiques : titres, légende, échelle de journal

Les tracés scientifiques doivent être limpides, nous devons donc nommer nos axes et ajouter une légende. On peut aussi donner un titre à l'intrigue. Enfin, il est souvent préférable d'utiliser une échelle logarithmique sur l'axe des y. Faisons tout ça :

In [ ]:
plot = plt.plot(x, norm.pdf(x, 0, 1), label='wide')
plt.plot(x, norm.pdf(x, 2, 0.5), label='narrow')
plt.title('Two Gaussian PDFs')
plt.ylabel('p.d.f.')
plt.xlabel('x')
plt.yscale('log')
# limiting the y axis, or the y axis 
# would go down to 10^(- a lot) and 
# the lines would be compressed at the top 
# of the plot
plt.ylim(0.02, 1)
plt.legend()
Out[ ]:
<matplotlib.legend.Legend at 0x7f117d49d050>

Vous pouvez trouver plus d'exemples dans le [tutoriel pyplot] officiel (https://matplotlib.org/stable/tutorials/introductory/pyplot.html).

Ensemble de données d'apprentissage automatique / Nuage de points et histogrammes

Maintenant que nous savons ce qu'est une distribution normale, nous pouvons l'utiliser pour tirer au hasard un échantillon de valeurs afin de créer un jeu de données de jouets.

Notre objectif est de construire un tableau numpy avec une forme (N, 1) pour l'ensemble des données d'entrée, qui est un tableau en colonnes avec une valeur unique pour chaque exemple, tiré d'un pdf gaussien. Les exemples proviendront de deux catégories notées 0 et 1, correspondant à deux pdf gaussiennes différentes.

Nous avons également besoin d'un tableau de formes (N,) pour les cibles, qui identifient la vraie catégorie de chaque exemple.

Nous utilisons numpy pour tirer 100 valeurs de chaque pdf gaussien :

In [ ]:
sigma = 1.
x0 = np.random.normal(-1.5, sigma, 100)
x1 = np.random.normal(1.5, sigma, 100)
x0.shape
Out[ ]:
(100,)

Pour l'instant, ces tableaux n'ont pas la bonne forme. Nous avons des tableaux de forme 1D (100,) et nous avons besoin de tableaux de forme en colonnes (100,1). De plus, nous devrons mélanger ces tableaux pour obtenir un tableau de données d'entrée complet de forme (200,1).

Avant de faire cela, nous créons les tableaux cibles pour les deux catégories :

In [ ]:
y0 = np.zeros_like(x0)
y1 = np.ones_like(x1)

Les deux échantillons peuvent être visualisés sous forme de nuage de points, avec la catégorie sur l'axe des y :

In [ ]:
plt.plot(x0, y0,'o')
plt.plot(x1, y1,'o')
plt.xlabel('x')
plt.ylabel('category')
Out[ ]:
Text(0, 0.5, 'category')

Et nous pouvons également afficher des histogrammes. Pour chaque catégorie, l'histogramme a 50 bacs, qui comptent le nombre d'exemples avec une valeur tombant dans le bac :

In [ ]:
# alpha is the transparency
plt.hist(x0, bins=50, range=(-5,5), alpha=0.5)
plt.hist(x1, bins=50, range=(-5,5), alpha=0.5)
plt.xlabel('x')
plt.ylabel('counts')
Out[ ]:
Text(0, 0.5, 'counts')

Enfin, nous fusionnons les deux échantillons (à la fois les données d'entrée et les cibles). Pour cela, nous utilisons le pratique np.r_ :

In [ ]:
x = np.r_['0,2,0', x0, x1]
y = np.concatenate([y0, y1])
print(x)
print(y)
[[-1.35496176]
 [-2.55161678]
 [-2.39950925]
 ...
 [ 2.62118653]
 [ 1.44560206]
 [ 3.10210887]]
[0. 0. 0. ... 1. 1. 1.]

Affichage des performances du modèle

Dans ce didacticiel, nous nous concentrons sur le traçage, pas sur l'apprentissage automatique. Nous allons donc simplement entraîner un modèle rapidement, sans essayer de comprendre ce que nous faisons. Nous reviendrons bien sûr sur le machine learning dans un prochain tutoriel.

Pour classer nos exemples, nous utiliserons une "régression logistique", telle qu'implémentée dans scikit-learn.

In [ ]:
from sklearn.linear_model import LogisticRegression
clf = LogisticRegression(solver='lbfgs').fit(x,y)

C'est ça! notre modèle a déjà été formé. Nous pouvons maintenant évaluer le modèle pour une liste de valeurs x. Ici, pour deux valeurs, -3 et 3 :

In [ ]:
clf.predict_proba([[-3], [3]])
Out[ ]:
array([[0.99981307, 0.00018693],
       [0.00028533, 0.99971467]])

Le modèle entraîné donne une probabilité de presque 100 % à la catégorie 0 si x=-3, et à peu près la même probabilité à la catégorie 1 si x=3. ça a l'air de marcher !

Il suffit de jeter un autre coup d'œil aux graphiques ci-dessus pour voir pourquoi.

Maintenant, nous voulons tracer la probabilité prédite pour l'ensemble de la plage x, superposée au nuage de points des exemples. Nous savons déjà comment faire le nuage de points, alors concentrons-nous sur le tracé de la probabilité prédite.

Le modèle peut être vu en fonction de x. Nous devons donc définir un linspace pour x, et calculer les probabilités correspondantes :

In [ ]:
xs = np.linspace(-5, 5, 101)
# xs is a 1D vector, and predict_proba 
# takes a columnar array
# as we have seen. So we turn it to a 
# columnar array: 
cxs = np.c_[xs]
probs = clf.predict_proba(cxs)
probs
Out[ ]:
array([[0.9999993 , 0.0000007 ],
       [0.99999907, 0.00000093],
       [0.99999877, 0.00000123],
       ...,
       [0.00000188, 0.99999812],
       [0.00000142, 0.99999858],
       [0.00000107, 0.99999893]])

Enfin, nous pouvons faire notre intrigue:

In [ ]:
plt.figure(figsize=(10,5), dpi=120)
plt.plot(x0, y0,'o')
plt.plot(x1, y1,'o')
# we select the probability for category 1
plt.plot(xs, probs[:, 1], color='blue')
plt.xlabel('x')
plt.ylabel('probability')
Out[ ]:
Text(0, 0.5, 'probability')

Exercer

Pour chaque catégorie, tracez l'histogramme de la probabilité prédite pour la catégorie 1. Vous devriez obtenir un pic à 0 pour la catégorie 0 et un pic à 1 pour la catégorie 1.

Astuces : utilisez un masque booléen numpy construit avec le tableau y pour sélectionner les entrées d'une catégorie donnée dans x.


Et maintenant?

Vous pourriez être intéressé par d'autres tutoriels de visualisation sur ce blog, basés sur les outils avancés du mode :

Et pour plus d'exemples matplotlib (avec code), vous pouvez consulter la galerie matplotlib .


N'hésitez pas à me donner votre avis dans les commentaires ! Je répondrai à toutes les questions.

Et si vous avez aimé cet article, vous pouvez souscrire à ma newsletter pour être prévenu lorsque j'en sortirai un nouveau. Pas plus d'un mail par semaine, promis!

Retour


Encore plus de data science et de machine learning !

Rejoignez ma mailing list pour plus de posts et du contenu exclusif:

Je ne partagerai jamais vos infos.
Partagez si vous aimez cet article: