Notes sur la leçon #1 de Fastai – Deep Learning

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_extReload 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 fnames in path with re expression pat

https://docs.fast.ai/vision.data.html#ImageDataBunch.from_name_re

Add normalize transform using stats (defaults to DataBunch.batch_stats)
In the fast.ai library we have imagenet_statscifar_stats and mnist_stats so we can add normalization easily with any of these datasets. Let’s see an example with our dataset of choice: MNIST.

https://docs.fast.ai/vision.data.html#ImageDataBunch.from_name_re

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% !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.