Alle prese con PyTorch - Parte 4: Torchvision e reti convolutive


PyTorch è un framework di deep learning, sviluppato principalmente dal Facebook AI Research (FAIR) group, che negli ultimi mesi ha guadagnato una enorme popolarità fra gli sviluppatori grazie alla sua combinazione di semplicità ed efficienza. Questa serie di tutorial è dedicata ad esplorare la libreria, partendo dai concetti più semplici fino alla definizione di modelli estremamente sofisticati. Nella quarta parte introduciamo una serie di strumenti essenziali per lavorare con le immagini: dataset già pronti, reti convolutive, e modelli pre-allenati.

Questi tutorial sono anche disponibili (parzialmente) in lingua inglese: Fun With PyTorch.

Contenuto di questo tutorial

Ormai arrivati alla quarta parte del tutorial, siamo ben avviati per diventare esperti di PyTorch! In questo post ci occupiamo di una classe di algoritmi che per molti è diventata sinonimo di deep learning: classificazione di immagini e computer vision. Come molti framework di deep learning, PyTorch ha una serie di funzialità per aiutare lo sviluppo di applicazioni di questo tipo, che introduciamo nel resto dell'articolo:

  • Un insieme di dataset già pronti su cui lavorare e testare modelli (es., il classico MNIST).
  • Tutti i moduli necessari a costruire reti neurali convolutive.
  • Funzioni avanzate per processare le immagini.
  • Alcuni modelli di classificazione di immagini già allenati, su cui eventualmente eseguire fine-tuning per nuovi oggetti.

Molte di queste funzionalità sono presenti in un package aggiuntivo di PyTorch, Torchvision. Per installarlo da terminale è sufficiente digitare:

pip install torchvision

Il resto del tutorial assume che il pacchetto sia stato correttamente installato.

Dataset, dataset, dataset!

Cominciamo da uno dei moduli più interessanti di Torchvision: datasets, che contiene tantissimi dataset curati e pronti all'utilizzo per vari problemi di classificazione di immagini.

Tutti i dataset si basano sull'interfaccia di torch.utils.data.Dataset per manipolare dati. Ne avevamo parlato nella seconda parte, è un buon momento per un ripasso!

Per vedere come funziona l'interfaccia scegliamo Fashion MNIST, una estensione del MNIST con immagini di vestiti al posto dei numeri, rilasciata dal team di Zalando l'anno scorso come semplice benchmark per algoritmi di deep learning. Tutte le istruzioni ovviamente si replicano per qualsiasi dataset presente nel modulo.

Cominciamo scaricando il dataset:

from torchvision import datasets
fmnist = datasets.FashionMNIST('fmnist', train=True, download=True)

Due flag sono particolarmente importanti: download permette di scaricare il dataset in automatico al primo uso, mentre train carica solo la porzione dedicata al training. Tutti i dataset presenti in Torchvision hanno uno split predefinito per il test, e la parte di test si può caricare negando il flag:

fmnist_test = datasets.FashionMNIST('fmnist', train=False)

Essendo un oggetto di tipo Dataset, possiamo indicizzarlo o ottenerne le dimensioni:

print(len(fmnist))
# Ritorna: 60000
print(fmnist[0])
# Ritorna: (<PIL.Image.Image image mode=L size=28x28 at 0x7F20B7393048>, tensor(9))

Come per il MNIST, abbiamo 60000 immagini di training, ciascuna di dimensione 28x28 ed in bianco e nero (mentre la parte di test ha altre 10000 immagini independenti rispetto alla parte di training). Si noti come le immagini non sono caricate direttamente come tensori, ma come oggetti di tipo PIL.Image.Image (PIL, acronimo di Python Imaging Library, è la libreria più diffusa per gestire immagini su Python). Questo ci permette di manipolare a piacimento le immagini prima di trasformarle in tensori ed usarle per i nostri modelli, come vediamo tra pochissimo.

Prima di proseguire facciamo due brevissimi commenti:

  1. Una delle classi del modulo datasets, ImageFolder, vi permette di caricare qualsiasi dataset di immagini organizzate in sotto-cartelle sul vostro disco.
  2. È possibile modificare il backend con cui vengono caricate le immagini, come menzionato sul sito. Ad esempio, installare Pillow-SIMD (una versione più efficiente di Pillow), lo imposta automaticamente come default. Una terza alternativa, meno usata ma sviluppata nativamente dal team di PyTorch, è accimage, che però va abilitato esplicitamente (si vedano le istruzioni sul sito).

Elaborazione delle immagini

Una volta caricate le immagini, possiamo processarle in vari modi, ad esempio:

  1. trasformarle in tensori per allenare i modelli;
  2. effettuare ridimensionamenti automatici o cambi di scala di colore;
  3. aumentare il dataset effettuando trasformazioni di vario tipo sulle immagini (es., rotazioni casuali).

Tutto questo è reso possibile dal modulo Transforms, che contiene una serie di operazioni che è possibile applicare in automatico su tutte le immagini del dataset. Per applicarle, è sufficiente passare le trasformazioni da eseguire al modulo di caricamento visto prima. Nel caso più semplice, ad esempio, possiamo trasformare le immagini in tensori di PyTorch con transforms.ToTensor():

from torchvision import transforms
fmnist = datasets.FashionMNIST('fmnist', transform=transforms.ToTensor())

Oltre a cambiare il tipo di dato, ToTensor ne modifica anche l'intervallo di valori, che passa da $[0, 255]$ a $[0, 1]$.

Lavorando con tensori, possiamo ora (ad esempio) usare l'oggetto DataLoader (che avevamo visto a sua volta nella seconda parte del tutorial) per creare mini-batch di immagini ed allenare eventuali classificatori:

# Creiamo un loader per mini-batch di sedici immagini
from torch.utils import data
data_loader = data.DataLoader(fmnist, batch_size=16)

# Prendiamo il primo mini-batch
xb, yb = next(iter(data_loader))

# Lo stampiamo su schermo
from torchvision import utils
import matplotlib.pyplot as plt
out = torchvision.utils.make_grid(xb)
plt.imshow(out.numpy().transpose((1, 2, 0)))

Esempi di immagini da F-MNIST

I dataset di immagini sono generalmente molto grandi: per questo, vale la pena leggere la documentazione del DataLoader per scoprire come gestire queste situazioni. Ad esempio, possiamo settare il flag num_workers per usare più processi in parallelo; oppure, lavorando su GPU, possiamo scegliere pin_memory per migliorare lo scambio di dati tra CPU e GPU.

La libreria contiene decine di trasformazioni, che possiamo combinare fra loro. Ad esempio, possiamo creare rotazioni casuali delle immagini per cercare di irrobustire il classificatore:

# Dobbia trasformazione
tr = transforms.Compose([
    transforms.RandomRotation(degrees=75),
    transforms.ToTensor()
])
fmnist = datasets.FashionMNIST('fmnist', train=True, transform=tr) 

Esempi di immagini da F-MNIST (ruotate)

transforms.Compose permette di concatenare varie trasformazioni; si noti inoltre come l'ordine è importante: RandomRotation, dovendo lavorare sulle immagini, deve essere eseguita prima di ToTensor. Possiamo anche applicare trasformazioni definite manualmente tramite transforms.Lambda; nell'esempio che segue mostriamo il suo uso binarizzando le immagini di partenza.

tr = transforms.Compose([
    transforms.ToTensor(),
    transforms.Lambda(lambda x: (x < 0.5).float())
])
fmnist = torchvision.datasets.FashionMNIST('fmnist', transform=tr)

Esempi di immagini da F-MNIST (binarizzate)

Un'altra trasformazione molto utile è transforms.Normalize, che permtte di rinormalizzare le immagini, ad esempio rendendole a media nulla e deviazione standard unitaria:

# Calcola media e variazione standard
import numpy as np
im_mean = np.mean(fmnist.train_data.numpy())
im_std = np.std(fmnist.train_data.numpy())

# Applica la normalizzazione
tr = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((im_mean / 255.0,), (im_std / 255.0,))
])
fmnist = torchvision.datasets.FashionMNIST('fmnist', train=True, transform=tr)

Vi consigliamo di leggere attentamente la documentazione per scoprire tutte le trasformazioni possibili. Tra l'altro, è importante tenere a mente che le trasformazioni saranno in genere differenti per il training ed il test: ad esempio, trasformazioni usate per ingrandire il dataset, come transforms.RandomRotation, hanno generalmente poco senso se applicate all'insieme di test.

Dai dati ai modelli: reti convolutive

Una volta in possesso dei dati, passiamo ai modelli. Questa è probabilmente la parte più semplice del tutorial: PyTorch mette a disposizione tantissimi blocchi per reti convolutive nel modulo nn, che abbiamo già incontrato nella seconda parte del tutorial. Ad esempio, questa è una rete convolutiva relativamente semplice, con due blocchi convolutivi e due strati per la classificazione, presa da un esempio sul repository ufficiale:

from torch import nn
import torch.nn.functional as F

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = self.fc2(x)
        return F.log_softmax(x, dim=1)

Il modello si instanzia e si allena in maniera equivalente a quanto abbiamo visto in precedenza. Se non siete familiari con le reti convolutive, vi consigliamo di seguire il materiale del corso di Stanford CS231: Convolutional Neural Networks for Visual Recognition.

Modelli pre-allenati e fine-tuning

Concludiamo questa nostra panoramica con un altro dei moduli di Torchvision, models, che contiene numerosi modelli pre-allenati da usare nelle vostre applicazioni. Ad esempio, con una sola istruzione possiamo scaricare una ResNet-18 (se siete curiosi sull'architettura, vi consigliamo la lettura dell'articolo di riferimento):

from torchvision import models
net = models.resnet18(pretrained=True)

La ResNet-18 è pre-allenata su ImageNet, con un'accuratezza di circa il 90% (calcolata sulle prime cinque predizioni) su 1000 classi di oggetti.

Trovate l'accuratezza di ciascun modello nella documentazione ufficiale.

Possiamo testarla scaricando una foto a caso dal catalogo di Ikea:

from PIL import Image
import requests
im = Image.open(requests.get('https://www.ikea.com/us/en/images/products/ekedalen-chair-gray__0516596_PE640434_S4.JPG', stream=True).raw)

Sedia

Se state eseguendo il tutorial su Google Colab, dovete installare la libreria requests. Potreste anche ricevere degli errori riguardanti Pillow; in questo caso, è sufficiente reinstallare la libreria e far ripartire il runtime:

!pip install --no-cache-dir -I pillow

Per utilizzare la ResNet, le immagini devono essere convertite nel formato di ImageNet. Possiamo farlo facilmente utilizzando le trasformazioni viste prima ed applicandole manualmente:

tr = transforms.Compose([
    # Ridimensiona a 224 x 224
    transforms.Resize(224),
    # Transforma in tensore
    transforms.ToTensor(),
    # Normalizza
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
im = tr(im)

Richiediamo una predizione alla rete:

net.eval()
torch.argmax(net(im.unsqueeze(0)))

L'istruzione dovrebbe ritornare 559, l'indice corrispondente a folding chair nella tassonomia di ImageNet.

Ma non è finita! Possiamo usare i modelli pre-allenati per eseguire fine-tuning sui nostri problemi, semplicemente rimuovendo l'ultimo strato della rete ed aggiungendo uno strato di classificazione ad-hoc per nuove classi: in questo modo, possiamo sfruttare tutta la parte pre-allenata della rete per apprendere a riconoscere nuovi oggetti anche con pochissimi esempi.

Fine tuning di CNN

(Copyright immagine: indico.io)

Investigando il codice sorgente del modello, potete vedere come l'ultimo strato corrisponda alla proprietà fc dell'oggetto. Per ottenere una nuova ResNet-18 da far girare sul Fashion MNIST, basta rimuovere lo strato e sostituirlo con un nuovo strato con sole dieci uscite (le classi del Fashion MNIST):

net.fc = torch.nn.Linear(net.fc.in_features, 10)

Di default, allenando un modello in questo modo andremo a modificare tutti i parametri, come possiamo scoprire andando a contate i parametri allenabili (si veda questa discussione per vari modi per contare i parametri di un modello):

print(sum(p.numel() for p in net.parameters() if p.requires_grad))
# Ritorna: 11689512

Il modello ha più di 11 milioni di parametri! Possiamo semplicare di molto il problema bloccando l'aggiornamento di tutti gli strati ad esclusione dell'ultimo:

for param in net.parameters():
    param.requires_grad = False
net.fc = torch.nn.Linear(net.fc.in_features, 10)

Questo nuovo modello ha solo 5130 parametri, ma prestazioni equivalenti. Se siete interessati all'argomento, vi consigliamo la guida al transfer learning che trovate nella documentazione ufficiale della libreria.

Anche per questa volta è tutto! Continuate a seguirci, la prossima volta parleremo di reti ricorrenti ed applicazioni a sequenze di dati.


Se questo articolo ti è piaciuto e vuoi tenerti aggiornato sulle nostre attività, ricordati che l'iscrizione all'Italian Association for Machine Learning è gratuita! Puoi seguirci anche su Facebook e su LinkedIn.

Previous Post Next Post