Les cours de fastai – Deep Learning for Coders, sont parmi les plus intéressants (les meilleurs à notre avis) disponibles gratuitement sur le Web. L’approche pédagogique qui consiste à apprendre le DL en codant est très efficiente.
Les cours de fastai concernent le Machine Learning et le Deep Learning. Cet article est un ensemble de notes sur la leçon #1 : What’s your pet. Le code, sur (Google Colaboratory Platform) GCP est disponible ici et sur Github ici.
Magic
Le code commence par des Magic commands.
%reload_ext autoreload
%autoreload 2
%matplotlib inline
%reload_ext
Reload an IPython extension by its module name.
autoreload
reloads modules automatically before entering the execution of code typed at the IPython prompt.
C’est rarement utile. Mais on ne sait jamais.
Quant à Reload %autoreload 2, « it Reloads all modules (except those excluded by %aimport
) every time before executing the Python code typed ».
Import fastai
from fastai.vision import *
from fastai.metrics import error_rate
Les librairies fastai sont importées (au dessus de PyTorch).
fastai.vision est la librairie qui s’occupe de la vision (traitement d’images) et fastai.metrics est une bibliothèque de métriques (par exemple RMSE, recall, …)
Données
Le jeu de données sur lequel est appliqué ici un réseau de neurones est The Oxford-IIIT Pet Dataset, un fichier de photos de chiens répartis en 25 catégories et chats répartis en 12 catégories. Chaque catégorie a environ 200 images. Le but de l’application est de deviner à quelle catégorie (parmi les 37) appartient l’animal dont on a la photo.
Obtention des données
fastai utilise les contantes suivantes :
URLs
fastai.datasets.URLs
URLs.PETS
'https://s3.amazonaws.com/fast-ai-imageclas/oxford-iiit-pet'
pour initialiser path (en décompressant le fichier de données)
path = untar_data(URLs.PETS); path
PosixPath('/root/.fastai/data/oxford-iiit-pet')
untar_data(url:str, fname:Union[pathlib.Path, str]=None, dest:Union[pathlib.Path, str]=None, data=True, force_download=False) -> pathlib.Path Download `url` to `fname` if it doesn’t exist, and un-tgz to folder `dest`.
Ce qui donne :
path.ls()
[PosixPath('/home/ubuntu/.fastai/data/oxford-iiit-pet/images'),
PosixPath('/home/ubuntu/.fastai/data/oxford-iiit-pet/annotations')]
On initialise ensuite 2 variables :
note : c’est Python 3 qui permet d’utiliser cette syntaxe novatrice.
path_anno = path/'annotations'
path_img = path/'images'
Consultation des données
Pour voir les noms des 5 premiers fichiers (images) :
fnames = get_image_files(path_img)
fnames[:5]
[PosixPath('/root/.fastai/data/oxford-iiit-pet/images/Maine_Coon_145.jpg'),
PosixPath('/root/.fastai/data/oxford-iiit-pet/images/Abyssinian_81.jpg'),
PosixPath('/root/.fastai/data/oxford-iiit-pet/images/yorkshire_terrier_122.jpg'),
PosixPath('/root/.fastai/data/oxford-iiit-pet/images/scottish_terrier_10.jpg'),
PosixPath('/root/.fastai/data/oxford-iiit-pet/images/samoyed_6.jpg')]
Comme on peut le voir, l’étiquette (la catégorie) est indiquée par le nom du fichier.
expression régulière
np.random.seed(2)
pat = r'/([^/]+)_\d+.jpg$'
Pour calculer la catégorie de l’image dont on connait le nom du fichier, on utilise une expression régulière (pat). Celle-ci dit, prendre ce qui est entre un / et un _ suivi de chiffres et .jpg.
Databunch
fastai utilise la notion de databunch. Le data bunch contient les images (training, validation, test) ainsi que les labels.
data = ImageDataBunch.from_name_re(path_img, fnames, pat, ds_tfms=get_transforms(), size=224, bs=bs).normalize(imagenet_stats)
Le databunch est créé à partir d’images, stockées dans path_img, dont le nom est fnames, dont les catégories sont calculées grâce à l’expression régulière pat. On applique la transformation get_transforms() aux images et on les retaille en carrés de 224px.
Les images sont ensuite normalisées (moyenne, écart-type) pour qu’elles correspondent à Imagenet.
Create from list of
https://docs.fast.ai/vision.data.html#ImageDataBunch.from_name_refnames
inpath
with re expressionpat
Add normalize transform using
https://docs.fast.ai/vision.data.html#ImageDataBunch.from_name_restats
(defaults toDataBunch.batch_stats
)
In the fast.ai library we haveimagenet_stats
,cifar_stats
andmnist_stats
so we can add normalization easily with any of these datasets. Let’s see an example with our dataset of choice: MNIST.
note : il faut que les images aient la même taille (d’où le size=224)
consultation
Affichage de quelques images :
data.show_batch(rows=3, figsize=(7,6))
Classes et nombre de classes :
print(data.classes)
len(data.classes),data.c
['Abyssinian', 'Bengal', 'Birman', 'Bombay', 'British_Shorthair', 'Egyptian_Mau', 'Maine_Coon', 'Persian', 'Ragdoll', 'Russian_Blue', 'Siamese', 'Sphynx', 'american_bulldog', 'american_pit_bull_terrier', 'basset_hound', 'beagle', 'boxer', 'chihuahua', 'english_cocker_spaniel', 'english_setter', 'german_shorthaired', 'great_pyrenees', 'havanese', 'japanese_chin', 'keeshond', 'leonberger', 'miniature_pinscher', 'newfoundland', 'pomeranian', 'pug', 'saint_bernard', 'samoyed', 'scottish_terrier', 'shiba_inu', 'staffordshire_bull_terrier', 'wheaten_terrier', 'yorkshire_terrier']
(37, 37)
Apprentissage
learn = cnn_learner(data, models.resnet34, metrics=error_rate)
fastai propose le concept de learner et plus particulièrement ici celui de cnn_learner qui crée un réseau de neurones à convolutions.
En l’occurence, les données sont ici : data et le modèle, l’architecture : resnet34. fastai propose aussi un resnet50. La metrics est ici une information qu’on affiche, le error_rate.
Lorsqu’on exécute cette instruction, le poids du réseau sont téléchargés. Ces poids ont été calculés sur ImageNet un ensemble de 14 197 122 images réparties en 1000 catégories.
Si on affiche le modèle :
learn.model
on obtient :
Sequential(
(0): Sequential(
(0): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU(inplace)
(3): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(4): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(2): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(5): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(2): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(3): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(6): Sequential(
(0): BasicBlock(
(conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(2): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(3): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(4): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(5): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(7): Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(2): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
)
(1): Sequential(
(0): AdaptiveConcatPool2d(
(ap): AdaptiveAvgPool2d(output_size=1)
(mp): AdaptiveMaxPool2d(output_size=1)
)
(1): Flatten()
(2): BatchNorm1d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(3): Dropout(p=0.25)
(4): Linear(in_features=1024, out_features=512, bias=True)
(5): ReLU(inplace)
(6): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(7): Dropout(p=0.5)
(8): Linear(in_features=512, out_features=37, bias=True)
)
)
fit
Une fois l’architecture décrite, on commence l’apprentissage :
learn.fit_one_cycle(4)
Le paramètre (4) indique le nombre de fois qu’on parcourt l’ensemble du dataset.
Le taux d’erreur, après 4 epochs, est 6% (en 2012, les meilleurs résultats étaient de 43% d’erreur). Le progrès est phénoménal !
Total time: 07:47
epoch train_loss valid_loss error_rate time
0 1.432035 0.331246 0.087280 01:56
1 0.555494 0.232059 0.074425 01:57
2 0.345955 0.202190 0.065629 01:56
3 0.272322 0.210280 0.064953 01:56
On sauvegarde le modèle :
learn.save('stage-1')
Résultats
interp = ClassificationInterpretation.from_learner(learn)
losses,idxs = interp.top_losses()
len(data.valid_ds)==len(losses)==len(idxs)
True
On commence par obtenir une interprétation des résultats (interp). losses nous informe sur les pertes et idxs sur les numéros des images concernées.
On peut visualiser les images les plus mal classées (erreurs importantes) :
interp.plot_top_losses(9, figsize=(15,11))

interp.plot_confusion_matrix(figsize=(12,12), dpi=60)
La matrice de confusion croise les erreurs.

interp.most_confused(min_val=2)
[('Bengal', 'Egyptian_Mau', 7),
('Siamese', 'Birman', 5),
('staffordshire_bull_terrier', 'american_bulldog', 4),
('Birman', 'Persian', 3),
('British_Shorthair', 'Russian_Blue', 3),
('american_pit_bull_terrier', 'american_bulldog', 3),
('american_pit_bull_terrier', 'staffordshire_bull_terrier', 3),
('boxer', 'american_bulldog', 3),
('miniature_pinscher', 'chihuahua', 3),
('Bengal', 'Abyssinian', 2),
('Egyptian_Mau', 'Abyssinian', 2),
('Egyptian_Mau', 'Bengal', 2),
('Maine_Coon', 'Bengal', 2),
('Maine_Coon', 'Ragdoll', 2),
('Ragdoll', 'Birman', 2),
('american_bulldog', 'american_pit_bull_terrier', 2),
('chihuahua', 'american_pit_bull_terrier', 2),
('english_setter', 'english_cocker_spaniel', 2),
('staffordshire_bull_terrier', 'american_pit_bull_terrier', 2),
('yorkshire_terrier', 'havanese', 2)]
Amélioration (fine tuning)
unfreeze
learn.unfreeze()
Lorsqu’on unfreeze notre modèle on impose que l’apprentissage se fasse sur la totalité du modèle (et pas seulement sur les dernières couches du modèle).
Puis on fait tourner le modèle sur un cycle.
learn.fit_one_cycle(1)
L’erreur :
epoch train_loss valid_loss error_rate time
0 0.542906 0.332110 0.106225 01:59
est pire qu’avant ! donc on oublie ce modèle et on repart avec le précédent.
learn.load('stage-1');
et on applique le learning rate finder :
learn.lr_find()
On affiche la courbe
learn.recorder.plot()

On peut ensuite demander un apprentissage qui unfreeze le modèle et l’entraîne avec un learning rate compris dans l’intervalle 1e-6,1e-4 réparti linéairement entre les couches
learn.unfreeze()
learn.fit_one_cycle(2, max_lr=slice(1e-6,1e-4))
Les résultats sont alors :
epoch train_loss valid_loss error_rate time
0 0.222864 0.195487 0.058187 02:01
1 0.209823 0.196844 0.062246 02:04
Encore mieux
On peut faire mieux en utilisant Resnet50. Dans ce cas, l’erreur n’est plus que de 4% !