Classement d'images "from scratch"¶

In [ ]:
#version de PyTorch
#https://pytorch.org/
import torch
#version de torchvision
#https://pytorch.org/vision/stable/index.html
import torchvision as tv
#autres imports de librairie
import matplotlib.pyplot as plt
import numpy as np
In [ ]:
#changement de dossier - dossier racine de travail
import os
os.chdir("C:/Users/ricco/Desktop/demo")
In [ ]:
#resizing des images en entrée, elles n'ont pas toutes les mêmes définitions
#(224, 224) parce que c'est la taille pour VGG que nous exploiterons plus loin
#tranformation en tenseur
#normalisation de [0, 1] vers [-1, +1]
my_transform = tv.transforms.Compose(
    [tv.transforms.Resize((224,224)),
     tv.transforms.ToTensor(),
     #valeurs varient entre [0,1]
     #on normalise pour qu'elles varient entre [-1,+1]
     tv.transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))]
)

Images d'apprentissage (TRAIN)¶

In [ ]:
#chargement des images avec transformation à la volée
#on traite le sous-dossier "./train" pour les images d'apprentissage
dataTrain = tv.datasets.ImageFolder(root='./images/train',transform=my_transform)
In [ ]:
#liste des classes
dataTrain.classes
Out[ ]:
['cat', 'dog']
In [ ]:
#distribution des classes dans l'échantillon TRAIN
np.unique(np.array(dataTrain.classes)[np.array(dataTrain.targets)],return_counts=True)
Out[ ]:
(array(['cat', 'dog'], dtype='<U3'), array([100, 100], dtype=int64))

Itérateur pour les images TRAIN¶

In [ ]:
#on va traiter les images individuellement
#pour une meilleure lisibilité de notre tutoriel - batch_size = 1
#shuffle pour que l'ordre soit mélangé au hasard

#transformation en type dataloader
#cf. https://pytorch.org/tutorials/beginner/basics/data_tutorial.html
dataTrain_loader = torch.utils.data.DataLoader(dataTrain,
                                             batch_size=1,
                                             shuffle=True)
In [ ]:
# créer un itérateur pour parcourir les images
dataiter = iter(dataTrain_loader)

#accès à la première image avec next()
image, label = next(dataiter)

#type initial Tensor (de torch)
#oui, car transformation à la volée lors du chargement
type(image)
Out[ ]:
torch.Tensor
In [ ]:
#affichage de l'image maintenant
plt.imshow(np.transpose(image.numpy().squeeze(),(1,2,0)))
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Out[ ]:
<matplotlib.image.AxesImage at 0x21953209050>
No description has been provided for this image
In [ ]:
#valeur du label
print(label)
tensor([1])
In [ ]:
#soit son étiquette
print(np.array(dataTrain.classes)[label])
dog

Classe pour le ConvNet (CNN) + Paramètres¶

In [ ]:
import torch.nn as nn
import torch.nn.functional as F

#classe - CNN pour mes pokémons
class MyNet(nn.Module):

    #outils pour le réseau
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 6, 5)
        self.conv3 = nn.Conv2d(6, 6, 5)
        #voir calcul dimension flatten - http://layer-calc.com/
        self.fc1 = nn.Linear(3456, 128)
        self.fc2 = nn.Linear(128, 84)
        self.fc3 = nn.Linear(84, 2) #2 classes !

    #organisation des opérations
    def forward(self, x):
        #traitement convolutif
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = self.pool(F.relu(self.conv3(x)))
        #vectorisation
        x = torch.flatten(x, 1) # flatten all dimensions except batch
        #perceptron multicouche à partir d'ici
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x
In [ ]:
#instanciation de l'objet
net = MyNet()
In [ ]:
#fonction de perte et algorithme d'optimisation
import torch.optim as optim

#fonction de perte - cf. les implications
#(1) sur la sortie du reséau (fonction de classement)
#(2) sur l'encodage de la cible (codage 0, 1, 2, ...)
#https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html
criterion = nn.CrossEntropyLoss()

#Adam - une variante de la descente de gradient stochastique
#https://pytorch.org/docs/stable/generated/torch.optim.Adam.html
optimizer = optim.Adam(net.parameters())

Entraînement sur les données TRAIN¶

In [ ]:
#nombre d'epochs
n_epochs = 20

#récupérer les valeurs de perte dans un vecteur
losses = np.zeros(n_epochs)

#itérer n_epochs fois sur la base TRAIN entière
for epoch in range(n_epochs):

    #initialisation de la valeur de la perte pour un epoch
    running_loss = 0.0

    #pour chaque individu de train
    for data in iter(dataTrain_loader):
        # accès à un individu : image, étiquette
        input, label = data
        # ràz des gradients
        optimizer.zero_grad()
        # calcul de l'application du réseau (sortie)
        output = net.forward(input) #ou simplement net(input)
        #calcul de la perte
        loss = criterion(output, label)
        #calcul du gradient et rétropropagation
        loss.backward()
        #mise à jour des poids synaptiques
        optimizer.step()
        # additionner la perte pour un epoch
        running_loss += loss.item()
    
    #ajouter la perte mesurée dans le vecteur dédié
    print(running_loss)
    losses[epoch] = running_loss
        
print('Finished Training')
140.11443850398064
139.05864363908768
139.19884571433067
139.00726079940796
139.50956267118454
138.75497615337372
138.59522953629494
129.4292226182297
97.15128077904592
52.7805481830374
14.364189707235667
2.0781679358050837
1.0764091668088795
0.8664476582696068
0.7043595484645522
0.5943724601990041
0.5161827051823522
0.4573330362623551
0.41264909238886816
0.3689894726025216
Finished Training

Images de test (TEST) - Prédiction, évaluation¶

In [ ]:
#chargement des images en test - avec transformation à la volée
dataTest = tv.datasets.ImageFolder(root='./images/test',transform=my_transform)

#liste des classes
print(dataTest.targets)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
In [ ]:
#transformation en "loader" - pas besoin de shuffle ici
dataTest_loader = torch.utils.data.DataLoader(dataTest,
                                             batch_size=1)
In [ ]:
#evaluation - calcul de l'accuracy
#structure pour stocker classe prédite et classe réelle
lst_pred = []
lst_real = []

#itérer sur chaque individu
for data in iter(dataTest_loader):
    #image et etiquette de l'individu
    image,label = data
    #prédiction du réseau
    sortie = net.forward(image)
    #en déduire la classe attribuée
    pred = np.argmax(sortie.detach().numpy())
    #et la classe réelle
    real = label.numpy()[0]
    #stocker
    lst_pred.append(pred)
    lst_real.append(real)

#affichages
print("Prediction : ")
print(lst_pred)

print("Appartenance réelle :")
print(lst_real)
Prediction : 
[0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0]
Appartenance réelle :
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
In [ ]:
#matrice de confusion
import pandas
pandas.crosstab(np.array(lst_real),np.array(lst_pred))
Out[ ]:
col_0 0 1
row_0
0 52 48
1 37 63
In [ ]:
#calcul de l'accuracy
acc = np.mean((np.array(lst_real)==np.array(lst_pred)))
print(f"Accuracy = {acc}")
Accuracy = 0.575

Modèle pré-entraîné VGG19¶

Instanciation du modèle pré-entraîné¶

In [ ]:
#https://pytorch.org/vision/main/models/generated/torchvision.models.vgg19.html
from torchvision.models import vgg, VGG19_Weights

#instanciation
model_vgg = vgg.vgg19(weights=VGG19_Weights.DEFAULT)
model_vgg.eval()

# Step 2: Initialize the inference transforms
preprocess_vgg = VGG19_Weights.DEFAULT.transforms()

Application sur une image¶

In [ ]:
#accès à la première image avec next()
image, label = next(dataiter)
In [ ]:
#affichage image
plt.imshow(np.transpose(image.numpy().squeeze(),(1,2,0)))
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Out[ ]:
<matplotlib.image.AxesImage at 0x21954d26ed0>
No description has been provided for this image

Très important, préparation pour que l'image soit compatible avec VGG19¶

In [ ]:
# transformation en batch
batch_vgg = preprocess_vgg(image)
print(batch_vgg.shape)
torch.Size([1, 3, 224, 224])

Application du modèle pré-entraîné sur une image¶

In [ ]:
#degré d'appartenance aux classes
prediction_vgg = model_vgg(batch_vgg).squeeze(0).softmax(0)
print(prediction_vgg.shape)
torch.Size([1000])

Attention, pas forcément directement compatible avec nos classes¶

In [ ]:
#id de la classe prédite et score d'appartenance
class_id = prediction_vgg.argmax().item()
score = prediction_vgg[class_id].item()
# affichage
category_name = VGG19_Weights.DEFAULT.meta["categories"][class_id]
print(f"{category_name}: {100 * score:.1f}%")
Egyptian cat: 25.2%

Transfer Learning avec VGG19¶

La partie Features¶

In [ ]:
#type de la partie convolution
type(model_vgg.features)
Out[ ]:
torch.nn.modules.container.Sequential

La partie classifier¶

In [ ]:
#type de la partie dense de VGG
type(model_vgg.classifier)
Out[ ]:
torch.nn.modules.container.Sequential
In [ ]:
#nb. couches dans la partie dense
len(model_vgg.classifier)
Out[ ]:
7
In [ ]:
#dimension en entrée de la dernière couche
model_vgg.classifier[-1].in_features
Out[ ]:
4096
In [ ]:
#dimension en sortie -- on a bien les 1000 classes initiales
model_vgg.classifier[-1].out_features
Out[ ]:
1000

Préparation du réseau pour le transfer learning¶

Rendre non-modifiables les poids existants

In [ ]:
#désactiver les mises à jour des poids
for param in model_vgg.parameters():
    param.requires_grad = False

Créer une couche que l'on va substituer à la dernière couche existante, compatible avec (1) l'avant-dernière couche de la structure existante ; (2) le problème à deux classes (chat vs. chien) que l'on veut traiter.

In [ ]:
#nouvelle couche à introduire dans le modèle
my_layer = nn.Sequential(
    nn.Linear(model_vgg.classifier[-1].in_features,2)
)

Remplacer par écrasement de la dernière connexion.

In [ ]:
#remplacer la dernière couche
model_vgg.classifier[-1] = my_layer

Si on applique le réseau sur une image à classer ? 2 valeurs (degré d'appartenance aux deux classes) maintenant.

In [ ]:
#voir ce que ça donne alors
model_vgg(batch_vgg)
Out[ ]:
tensor([[ 0.1314, -0.2861]], grad_fn=<AddmmBackward0>)

Reconfiguration de la perte et de l'algo. d'optimisation

In [ ]:
#fonction de perte - cf. les implications
criterion = nn.CrossEntropyLoss()

#Adam - une variante de la descente de gradient stochastique
#avec les bons paramètres
optimizer = optim.Adam(model_vgg.parameters())

Entraînement du modèle, seuls les poids connectés à la couche de sortie sont corrigés.

In [ ]:
#nombre d'epochs - attention, ça va prendre du temps
n_epochs = 20

#récupérer les valeurs de perte dans un vecteur
losses = np.zeros(n_epochs)

#itérer n_epochs fois sur la base TRAIN entière
for epoch in range(n_epochs):

    #initialisation de la valeur de la perte pour un epoch
    running_loss = 0.0

    #pour chaque individu de train
    for data in iter(dataTrain_loader):
        # accès à un individu : image, étiquette
        input, label = data
        # !!! IMPORTANT -- introduire ici la transformation pour VGG !!!
        input = preprocess_vgg(input)
        # ràz des gradients
        optimizer.zero_grad()
        # calcul de l'application du réseau (sortie)
        output = model_vgg(input)
        #calcul de la perte
        loss = criterion(output, label)
        #calcul du gradient et rétropropagation
        loss.backward()
        #mise à jour des poids synaptiques
        optimizer.step()
        # additionner la perte pour un epoch
        running_loss += loss.item()
    
    #ajouter la perte mesurée dans le vecteur dédié
    print(running_loss)
    losses[epoch] = running_loss
        
print('Finished Training')
39.95400469201503
10.52474943415534
0.9143674446271888
0.3086940237912401
0.23704544217760315
0.19329777072844223
0.16105913557321827
0.13531310515270434
0.11676392941480174
0.100910717834239
0.08716898836058817
0.07723113590639485
0.06715048116763
0.05932121395272816
0.052668168007620864
0.04726188831116218
0.04204303391905739
0.03748159958213648
0.03395382054389273
0.030221468686015385
Finished Training

Confrontation classes prédites et observées

In [ ]:
#evaluation - calcul de l'accuracy
#structure pour stocker classe prédite et classe réelle
lst_pred = []
lst_real = []

#itérer sur chaque individu
for data in iter(dataTest_loader):
    #image et etiquette de l'individu
    image,label = data
    # !!! ATTENTION -- appliquer le preprocessing de vgg19 !!!
    image = preprocess_vgg(image)
    #prédiction du réseau
    sortie = model_vgg(image)
    #en déduire la classe attribuée
    pred = np.argmax(sortie.detach().numpy())
    #et la classe réelle
    real = label.numpy()[0]
    #stocker
    lst_pred.append(pred)
    lst_real.append(real)

#affichages
print("Prediction : ")
print(lst_pred)

print("Appartenance réelle :")
print(lst_real)
Prediction : 
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0]
Appartenance réelle :
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
In [ ]:
#matrice de confusion
import pandas
pandas.crosstab(np.array(lst_real),np.array(lst_pred))
Out[ ]:
col_0 0 1
row_0
0 97 3
1 7 93
In [ ]:
#calcul de l'accuracy
acc = np.mean((np.array(lst_real)==np.array(lst_pred)))
print(f"Accuracy = {acc}")
Accuracy = 0.95