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