Apprenez juste ce qu'il faut de numpy pour vous mettre au machine learning (cours 1h)
Les utilisateurs purs et durs de C++ ou de Fortran parmi les physiciens disent souvent que python est trop lent.
Certes, python est un langage interprété et il est lent.
Même les défenseurs de python comme moi le réalisent, mais nous pensons que ce n'est pas vraiment un problème, par exemple parce que :
Et, surtout:
Dans ce didacticiel, vous comprendrez ce qu'est numpy et pourquoi c'est rapide, et apprendrez juste ce qu'il faut savoir pour faire du machine learning.
Ce tutoriel est interactif.
Pour l'exécuter sur Google Colab, cliquez sur ce lien . Vous pourrez exécuter le code vous-même et le modifier.
Prérequis : veuillez d'abord suivre Python Crash Course for Machine Learning , ou assurez-vous de maîtriser les bases de python.
L'objectif principal de numpy est de fournir une structure de données très efficace appelée tableau numpy, et les outils pour manipuler ces tableaux.
Pourquoi numpy est-il si rapide ?
Car, sous le capot, les tableaux sont traités avec du code compilé, optimisé pour le CPU. En particulier, les opérations numpy sont parallèles car elles utilisent SIMD (Single Operation Multiple Data).
Pour avoir une idée de cette rapidité, nous pouvons chronométrer quelques opérations.
Créons une grande liste avec un million d'entiers, puis un tableau numpy à partir de cette liste :
import numpy as np
lst = range(1000000)
arr = np.array(lst)
arr
Calculons maintenant le carré de tous les entiers et voyons combien de temps cela prend.
On commence par la liste :
%timeit squares = [x**2 for x in lst]
Et on fait de même pour le tableau :
%timeit squares = arr**2
Comme vous pouvez le voir, c'est 300 fois plus rapide.
On peut en principe boucler sur le tableau numpy comme ceci :
%timeit squares = [x**2 for x in arr]
Mais alors, on perd complètement les avantages de numpy ! En effet, lorsque nous faisons arr**2
nous utilisons la fonction mise au carré de numpy, qui est intrinsèquement parallèle. Quand on boucle, on traite les éléments un par un avec du python basique. Donc:
Ne jamais boucler sur un tableau numpy ! Vous serez tenté de le faire, mais il ne doit y avoir aucune exception !
Nous avons vu que les tableaux numpy sont traités par du code compilé avec SIMD.
Pour que cela fonctionne, les éléments d'un tableau numpy doivent être :
Au contraire, les listes python peuvent contenir des objets hétérogènes de tout type.
Voici quelques façons de créer des tableaux numpy avec différents types :
from sys import getsizeof as sizeof
# numpy guesses that it should use integers
x = np.array([0, 1, 2])
print(x.dtype)
# or floats:
x = np.array([0., 1., 2.])
print(x.dtype)
# here we specify a python compatible type,
# interpreted by numpy as int64
x = np.array([0., 1., 2.], dtype=int)
print(x.dtype)
# here we specify that we want 8 bits integers
x = np.array([0, 1, 2], dtype=np.int8)
print(x.dtype)
Cela permet d'estimer facilement la taille d'un tableau numpy en mémoire, pour voir si vous allez faire exploser votre ordinateur avant de le faire réellement.
Exercice
Par exemple, considérons un échantillon de 1000 images, chacune avec 200x200 pixels, et 3 canaux de couleur par pixel. L'indice de couleur va de 0 à 255, et peut donc être codé sous forme d'entier de 8 bits.
En supposant que vous stockiez les données de toutes les images dans un seul tableau numpy, quelle serait sa taille en mémoire en Go ?
J'appelle opérations par élément toutes les opérations qui affectent les éléments du tableau, mais préservent la forme du tableau.
Tous les opérateurs habituels sont implémentés dans numpy, pour les tableaux. Par example:
x = np.array(range(5))
x**2
Notez que ces opérateurs, dans numpy, sont par élément :
x+1
Les équivalents des fonctions du package python math
sont disponibles directement à partir du package numpy
, du même nom, par exemple :
np.exp(x)
Enfin, des opérateurs binaires sont disponibles :
x = np.array([0, 1, 2])
y = np.array([1, 2, 3])
x+y
x*y
Jusqu'à présent, nous n'avons vu que des tableaux à une seule dimension. Mais souvent, plus de dimensions sont utilisées.
Les tableaux multidimensionnels peuvent être créés à partir d'une liste de listes, par exemple :
x = np.array([[0, 1], [2, 3], [4,5]])
x
L'attribut shape
nous donne la longueur de chaque dimension :
x.shape
Dans ce cas, nous avons 3 rangées de 2 nombres. La première dimension est la dimension la plus externe et la seconde la dimension la plus interne.
Prenons l'exemple d'une image de 2x2 pixels, avec 3 canaux de couleurs (rouge, bleu, vert) dans chaque pixel :
x = np.array(
[
[ [1,2,3], [4,5,6], ],
[ [7,8,9], [10,11,12]]
]
)
print(x)
print(x.shape)
Pour visualiser plus facilement les tableaux numpy, je pense souvent à la dimension la plus interne séparément. Par exemple, ici, nous avons un tableau de 2x2 pixels, avec un sous-tableau de taille 3 dans chaque pixel.
Et comme dernier exemple, considérons un "vecteur colonne" :
x = np.array([
[0],
[1],
[2],
[3]
])
print(x)
print(x.shape)
Comme vous pouvez le voir, le vecteur colonne a deux dimensions, ce qui peut être contre-intuitif. Il y a un seul nombre (un scalaire) sur la dimension interne.
Veuillez noter que dans numpy, une dimension peut également être appelée un "axe".
Très souvent, les tableaux numpy d'une forme donnée sont construits en initialisant tous les éléments à un nombre fixe ou à un nombre aléatoire. Par example:
np.zeros((2,3))
np.ones(3)
np.ones_like(x)
np.random.rand(2,2)
De nombreux autres outils d'échantillonnage aléatoire sont disponibles.
Voici un tableau 1D :
x = np.arange(10) + 1
x
Les éléments sont accessibles directement, en utilisant leur index dans le tableau (l'index commence à 0) :
x[1]
Et, comme d'habitude dans les séquences python, les indices négatifs commencent par la fin :
x[-2]
Le tableau peut être modifié sur place :
print(id(x))
x[1] = 0
print(id(x))
print(x)
Pour les tableaux multidimensionnels, l'indexation de base est effectuée en spécifiant une liste d'indices séparés par des virgules :
x = np.zeros((2,3))
x[0,1] = 1
x
L'indexation peut être utilisée pour sélectionner des éléments de tableau en fonction d'un masque.
Créons à nouveau notre tableau 1D :
x = np.arange(10) + 1
x
Pour créer un masque, nous évaluons une expression booléenne pour chaque élément du tableau. Par exemple, pour trouver tous les nombres pairs :
x%2 == 0
Que signifie cette expression ?
Puisque x
est un tableau numpy, x%2
est une opération numpy élément par élément qui évalue %2
sur tous les éléments du tableau et renvoie un nouveau tableau avec les résultats :
xmod = x%2
xmod
Ensuite, nous sélectionnons les nombres pairs en demandant que le modulo soit égal à zéro. Encore une fois, l'opérateur ==
est appliqué à un tableau numpy, il s'agit donc d'une opération par élément :
mask = (xmod == 0)
mask
Avec ce masque, nous sélectionnons des nombres pairs et renvoyons un nouveau tableau :
x[mask]
En fait, le nouveau tableau est une vue sur le tableau d'origine. Les données ne sont pas copiées.
Il est possible d'inverser le masque :
x[~mask]
Et bien sûr il est possible de fabriquer des masques à la volée, ce qui est généralement fait :
x[x%2==0]
Quand on fait de la data science, l'indexation booléenne est très souvent utilisée pour sélectionner des données en appliquant des seuils sur les variables choisies.
En python, une tranche (slice) est définie comme un tuple, (départ, fin, pas)
. Il permet de sélectionner une séquence d'éléments dans une séquence (qui, par essence en python, est 1D) :
lst = list(range(1, 10))
print(lst)
lst[1::2]
Nous avons sélectionné des éléments :
stop
n'est pas spécifié. A ce stade, le dernier élément, 9, est inclus ;Exercice:
Jouez avec la définition de tranche dans la cellule ci-dessus. Essayez de:
Dans l'exemple ci-dessus, nous avons décidé de ne pas spécifier stop. C'est possible pour tous les éléments de l'indice de tranche:
print( lst[:5:] )
print( lst[::2] )
print( lst[3::] )
print( lst[::] )
Ces notations peuvent et doivent être simplifiées en :
print( lst[:5] )
print( lst[::2] )
print( lst[3:] )
print( lst[:] )
Exercice:
Pourrait-on simplifier davantage ces expressions en supprimant plus de :
? Que se passerait-il si vous le faisiez ? Testez vos hypothèses dans la cellule ci-dessus.
En numpy, le tranchage est une simple généralisation du tranchage en python, mais cette fois à plusieurs dimensions.
Pour le tester, nous créons une matrice 2D avec 4 lignes et 5 colonnes. Pour cela, nous utilisons la méthode de remodelage qui sera discutée dans la section suivante :
x = np.arange(20).reshape(4,5)
x
Nous pouvons maintenant utiliser la notation de tranche sur n'importe quel dimension. Voici quelques exemples :
x[:, 1]
x[:, :2]
x[:, ::2]
x[:, ::-1]
Dans les exemples précédents, nous avons travaillé sur la dernière dimension, tout en préservant la première dimension. Pour cela, nous devions ajouter un :
comme premier élément d'indexation pour demander une boucle sur la première dimension.
Mais cela peut devenir douloureux lorsqu'il y a plus de 2 dimensions. Par exemple, considérons ce tableau 3D :
y = np.arange(8).reshape((2,2,2))
y
C'est un cube 2x2x2. Si vous souhaitez sélectionner les premiers éléments le long de la dernière dimension, vous pouvez soit faire :
y[:,:,0]
Ou utilisez la notation ellipse :
y[...,0]
Les points de suspension ajoutent autant de :
que de dimensions manquantes dans l'instruction d'indexation. De cette façon, vous n'avez pas besoin de garder une trace du nombre de dimensions, et c'est plus facile à taper.
Mais surtout, votre code devient indépendant du nombre de dimensions du tableau. Par exemple dans ce cas, y[...,0]
sélectionne les premiers éléments le long de la dernière dimension quelle que soit la dimension de y :
y = np.arange(8).reshape(2,4)
print(y)
print(y[...,0])
Le remodelage d'un tableau consiste à réorganiser les données du tableau en un nouveau tableau de forme différente.
Tout d'abord, le remodelage permet de créer facilement un tableau avec une forme donnée, comme nous l'avons fait précédemment :
x = np.arange(20).reshape(4,5)
x
Nous avons créé un tableau 1D plat avec des nombres entiers allant de 0 à 19, et l'avons immédiatement remodelé en un tableau 2D de forme (4, 5)
.
Souvent, il faut aplatir un tableau. Cela peut se faire de plusieurs manières :
y = x.flatten()
y
x.ravel()
x.reshape(-1)
Le résultat est le même, alors quelle est la différence entre aplatir, défiler, et remodeler ?
flatten
renvoie toujours une copie du tableau.
ravel
, lui, tente de renvoyer une vue sur le tableau d'origine et ne copie les données que lorsque cela est nécessaire.
Cela signifie que si vous modifiez la sortie de ravel, vous pouvez modifier le tableau d'origine. En outre, cela signifie que vous devez utiliser ravel sur de très gros tableaux pour économiser de la mémoire, et que ravel sera généralement plus rapide que flatten.
reshape
est plus flexible car vous pouvez remodeler n'importe quoi. Tout comme ravel, il renvoie une vue et n'effectue une copie que lorsque cela est nécessaire. Je suggère d'utiliser flatten ou ravel lorsque vous souhaitez aplatir un tableau, pour rendre votre code plus explicite.
Cela signifie que nous voulons transformer un tableau de forme (N,)
en un tableau de forme (N,1)
. Considérons un tableau de forme (5,)
:
y = np.arange(5)
print(y)
print(y.shape)
Nous le transformons en un tableau de forme en colonnes (20,1) avec l'attribut c_
(notez que ce n'est pas une fonction !)
coly = np.c_[y]
print(coly)
print(coly.shape)
Vous en savez maintenant suffisamment sur numpy pour gérer la plupart des opérations rencontrées dans les projets de machine learning.
Nous allons maintenant travailler sur des exemples réels afin que vous puissiez perfectionner vos compétences en numpy !
Prenons un exemple pratique d'apprentissage automatique supervisé, la classification d'images de chiens et de chats.
Pour entraîner notre modèle d'intelligence artificielle, on lui présente des exemples photos de chiens et de chats.
Chaque exemple consiste en :
Lorsqu'une image de chat lui est présentée, la machine affichera une valeur comprise entre 0 et 1. Nous comparerons cette valeur à la cible et mettrons à jour les paramètres du modèle afin que la prochaine fois, elle puisse se rapprocher de la solution.
L'ensemble de données d'entraînement (l'ensemble d'exemples utilisés pour l'entraînement) est généralement stocké dans des tableaux numpy. Par exemple, on pourrait utiliser deux tableaux numpy :
(10000, 200, 200, 3)
pour les images, en supposant que nous ayons 10000 images couleur de 200x200 pixels.La structure exacte du jeu de données dépendra de l'endroit où vous l'obtenez ou de la façon dont vous le construisez, et vous devrez parfois modifier cette structure pour fournir le jeu de données au modèle.
Comme cas pratique, utilisons l'ensemble de données de chiffres manuscrits MNIST simplifié, inclus dans le package de machine learning scikit-learn. Nous chargeons d'abord le jeu de données :
from sklearn import datasets
digits = datasets.load_digits()
Nous obtenons un objet appelé digits
. Nous pouvons utiliser les fonctions intégrées type
et dir
pour voir ce que c'est :
print( type(digits) )
print( dir(digits) )
Ok, Bunch
est une classe de scikit-learn que nous ne connaissons pas, mais ses attributs ont des noms assez explicites. Imprimons quelques informations supplémentaires :
print( type(digits.images) )
print( type(digits.target) )
Deux tableaux numpy ! Imprimons leur forme :
print( digits.images.shape )
print( digits.target.shape )
Quelques exercices
# your code here
# your code here
La précision est une mesure de la performance des algorithmes de classification. Il est défini la probabilité de bien classer un exemple et calculé comme
$$ a = 1 - \frac{M}{N}, $$où $N$ est le nombre total d'exemples et $M$ le nombre d'exemples mal classés.
Après l'entraînement, la précision est calculée sur un ensemble de données de test distinct de l'ensemble de données d'entraînement. En pratique, l'ensemble de données de test est envoyé sous forme de tableau numpy à la machine, ce qui produit un tableau numpy contenant les résultats de l'évaluation pour chaque exemple de l'ensemble de données de test.
La précision est ensuite calculée à partir de cette sortie et des étiquettes de l'ensemble de données de test.
Supposons que nous travaillons toujours sur l'ensemble de données de chiffres manuscrits MNIST et que l'ensemble de données de test contient 10 exemples, avec les étiquettes suivantes :
true_labels = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
true_labels
Et supposons que la sortie de notre machine pour ces 10 exemples soit :
results = np.array([1, 2, 9, 4, 5, 6, 1, 8, 9, 10])
results
Exercice:
Calculez la précision en pourcentage. Pour cela, notez que vous pouvez additionner toutes les entrées d'un tableau numpy x
en faisant x.sum()
. Aucune boucle autorisée !
# your code here
Les réseaux de neurones n'aiment pas trop les grands nombres.
Donc, généralement, les entrées du réseau de neurones sont redimensionnées pour être de l'ordre de l'unité avant d'être transmises au réseau.
Considérons à nouveau le jeu de données MNIST, qui est déjà chargé dans ce cahier en tant que chiffres
. Voici la première image :
digits.images[0]
Exercice
digits.image
? utilisez np.max
sur ce tableau pour trouver cette valeur.Aucune boucle autorisée !
# your code here
Nous savons déjà que les réseaux de neurones n'aiment pas traiter de grands nombres. Et il n'est pas non plus très bon d'avoir des formes très différentes pour les distributions des variables d'entrée.
Donc généralement, nous normalisons les variables d'entrée. Un choix courant consiste à centrer la moyenne de chaque distribution d'entrée sur 0 et à ramener son écart type à 1.
Construisons un jeu de données avec 1000 exemples et deux variables suivant chacune une distribution de densité de probabilité gaussienne.
On commence par tirer un échantillon aléatoire de 1000 événements pour chaque variable. Notez la grande différence entre la moyenne et le sigma entre les deux variables :
x = np.random.normal(100, 5, 1000)
y = np.random.normal(10, 1, 1000)
Ensuite, nous empilons les deux variables dans un seul tableau numpy :
dataset = np.c_[x, y]
print(dataset.shape)
dataset
Et enfin, nous vérifions la moyenne et l'écart type de chaque variable:
print(np.mean(dataset, axis=0))
print(np.std(dataset, axis=0))
En utilisant axis=0
, nous avons demandé que la moyenne et l'écart type soient calculés le long du premier axe, qui est la dimension la plus externe. Par conséquent, numpy a calculé ces quantités pour les deux colonnes séparément.
Prenez le temps d'y réfléchir et de bien comprendre comment vous vous déplacez dans le tableau 2D lorsque vous suivez la dimension la plus externe.
Exercice
# your code here
# your code here
Par convention, TensorFlow considère que les canaux de couleur d'une image sont ordonnés en rouge, vert, bleu (RVB) le long de la dimension la plus interne. Au contraire, Caffe, une autre bibliothèque d'apprentissage en profondeur, suppose que les couleurs sont dans l'ordre inverse (BVR).
Supposons que vous souhaitiez utiliser un réseau de neurones pré-entraîné avec Caffe. Mais vos images sont en RVB... Évidemment, vous devez convertir vos images de RVB en BGR pour que le réseau puisse utiliser sa connaissance des couleurs.
Dans cet exercice, vous découvrirez comment faire cela sur une seule image.
Tout d'abord, chargeons l'exemple d'image "face"
import scipy.misc
import matplotlib.pyplot as plt
face = scipy.misc.face()
plt.imshow(face)
Exercice
Astuce : vous devrez faire bon usage de la notation des tranches. Essayez également d'utiliser les ellipses
Bien joué ! vous êtes maintenant un utilisateur expérimenté de numpy !
Vos nouvelles compétences s'avéreront utiles, même en dehors du domaine de l'apprentissage automatique.
Juste un avertissement : ne réinventez pas la roue .
Il est fort probable que ce dont vous aurez besoin ait déjà été implémenté. N'hésitez donc pas à jeter un œil aux packages python scientifiques existants avant de commencer à coder. Par example:
Vous pouvez désormais vous rendre sur Matplotlib for Machine Learning pour apprendre à créer vos premiers tracés !
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!
Rejoignez ma mailing list pour plus de posts et du contenu exclusif: