Le reti neurali convoluzionali (CNN) sono un fondamento della moderna visione artificiale, consentendo applicazioni come il riconoscimento delle immagini, il rilevamento facciale e le auto a guida autonoma. Queste reti sono progettate per estrarre automaticamente modelli e caratteristiche dalle immagini, rendendole più potenti rispetto alle tecniche tradizionali di apprendimento automatico per compiti visivi.
In questo tutorial, implementeremo una CNN utilizzando PyTorch, un framework di apprendimento profondo che è sia user-friendly che altamente efficiente per applicazioni di ricerca e produzione.
Prerequisiti: Apprendimento profondo e PyTorch
Prima di entrare nei dettagli delle CNN, è necessario essere familiari con il campo dell’apprendimento profondo e con le librerie Python che utilizzeremo durante la configurazione del nostro ambiente.
L’apprendimento profondo è una sottocategoria dell’apprendimento automatico, in cui la struttura fondamentale del modello è una rete di input, strati nascosti e output. Tale rete può avere uno o più strati nascosti. L’intuizione originale dietro l’apprendimento profondo era quella di creare modelli ispirati a come il cervello umano apprende: attraverso cellule interconnesse chiamate neuroni. Questo è il motivo per cui continuiamo a chiamare i modelli di apprendimento profondo “reti neurali”. Queste strutture modello stratificate richiedono molte più informazioni per apprendere rispetto ad altri modelli di apprendimento supervisionato per derivare modelli dai dati non strutturati. Di solito parliamo di almeno centinaia di migliaia di punti dati.
Mentre ci sono diversi framework e pacchetti disponibili per l’implementazione di algoritmi di deep learning, ci concentreremo su PyTorch, uno dei framework più popolari e ben mantenuti. Oltre ad essere utilizzato dagli ingegneri di deep learning nell’industria, PyTorch è uno strumento preferito tra i ricercatori. Molti articoli di deep learning vengono pubblicati utilizzando PyTorch. È progettato per essere intuitivo e user-friendly, condividendo molte caratteristiche comuni con la libreria Python NumPy.
Se hai bisogno di una panoramica su questi concetti, considera di iscriverti al corso Deep Learning with PyTorch oggi.
Cosa è una Rete Neurale Convoluzionale (CNN)?
Le reti neurali convoluzionali, comunemente chiamate CNN o ConvNet, sono un tipo specifico di rete neurale profonda ben adattata per compiti di visione artificiale. L’invenzione delle CNN risale agli anni ’80. Tuttavia, sono diventate mainstream solo negli anni 2010, a seguito dei progressi informatici che sono derivati dall’implementazione delle unità di elaborazione grafica (GPU). Infatti, la rapida popolarizzazione delle CNN ha aiutato il campo delle reti neurali a tornare in primo piano, portando alla cosiddetta “terza ondata delle reti neurali” che stiamo vivendo ancora oggi.
Le CNN sono specificamente ispirate al cortex visivo biologico. Il cortex ha piccole regioni di cellule sensibili alle specifiche aree del campo visivo. Quest’idea è stata espansa da un affascinante esperimento di Hubel e Wiesel nel 1962.
Le CNN cercano di replicare questa caratteristica creando complesse reti neurali composte da diverse strati specifici per compiti diversi. I CNN sono chiamati “feed-forward” perché le informazioni scorrono direttamente attraverso il modello. Non ci sono connessioni di feedback in cui gli output del modello vengono reinviati a se stessi, a differenza di altri modelli che utilizzano tecniche come il backpropagation.
In particolare, un CNN è tipicamente composto dai seguenti strati:
Strato convoluzionale
Questo è il primo blocco di costruzione di un CNN. Come suggerisce il nome, il compito matematico principale eseguito è chiamato convoluzione, che è l’applicazione di una funzione di finestra scorrevole a una matrice di pixel che rappresenta un’immagine. La funzione scorrevole applicata alla matrice è chiamata kernel o filtro. Nel livello di convoluzione, vengono applicati diversi filtri di dimensioni uguali, e ciascun filtro viene utilizzato per riconoscere un modello specifico dall’immagine, come la curvatura delle cifre, i bordi, la forma completa delle cifre e altro ancora.
Funzione di attivazione
Normalmente, dopo ogni operazione di convoluzione viene applicata una funzione di attivazione ReLU. Questa funzione aiuta la rete a imparare relazioni non lineari tra le caratteristiche nell’immagine, rendendo la rete più robusta nell’identificare diversi modelli. Aiuta anche a mitigare i problemi di gradienti che svaniscono.
Strato di pooling
L’obiettivo del layer di pooling è estrarre le caratteristiche più significative dalla matrice convoluta. Questo avviene applicando alcune operazioni di aggregazione, che riducono la dimensione della mappa delle caratteristiche (matrice convoluta), riducendo così la memoria utilizzata durante l’addestramento della rete. Il pooling è anche rilevante per mitigare l’overfitting.
Layer completamente connessi
Questi layer si trovano nell’ultimo strato della rete neurale convoluzionale, e i loro input corrispondono alla matrice unidimensionale appiattita generata dall’ultimo layer di pooling. Funzioni di attivazione ReLU vengono applicate per introdurre non linearità.
Architettura della rete neurale convoluzionale. Fonte: DataCamp
Puoi leggere una spiegazione più dettagliata della matematica dietro le CNN nel nostro tutorial, Reti Neurali Convoluzionali in Python.
Perché usare le CNN per la classificazione delle immagini?
Le reti neurali convoluzionali sono state una delle innovazioni più influenti nel campo della computer vision. Si sono comportate molto meglio rispetto ai modelli tradizionali di machine learning, come SVM e alberi decisionali, e hanno prodotto risultati all’avanguardia.
Inoltre, gli strati convoluzionali conferiscono alle CNN le loro caratteristiche di invarianza alla traslazione, permettendo loro di identificare ed estrarre modelli e caratteristiche dai dati indipendentemente dalle variazioni di posizione, orientamento, scala o traslazione.
Le CNN hanno dimostrato di avere successo in molti diversi casi di studio e applicazioni della vita reale, come:
- classificazione delle immagini, rilevamento degli oggetti, segmentazione, riconoscimento facciale;
- auto a guida autonoma che sfruttano sistemi di visione basati su CNN;
- classificazione della struttura cristallina utilizzando una rete neurale convoluzionale;
- sistemi di telecamere di sicurezza.
Oltre ai compiti di classificazione delle immagini, le CNN sono versatili e possono essere applicate a una serie di altri domini, come l’elaborazione del linguaggio naturale, l’analisi delle serie temporali e il riconoscimento del parlato.
Implementazione di una CNN con PyTorch.
Ora che sei familiare con la teoria delle CNN, siamo pronti a sporcarci le mani. In questa sezione, costruiremo e alleneremo una semplice CNN con PyTorch. Il nostro obiettivo è costruire un modello per classificare le cifre nelle immagini. Per addestrare e testare il nostro modello, utilizzeremo il famoso dataset MNIST, una raccolta di 70.000 immagini in scala di grigi, 28×28, con cifre scritte a mano.
1. Importazione delle librerie necessarie
Di seguito puoi trovare le librerie che utilizzeremo per questo tutorial. In sostanza, sfrutteremo PyTorch per costruire la nostra CNN e il modulo di visione artificiale di PyTorch torchvision
, per scaricare e caricare il dataset MNIST. Infine, useremo anche torchmetrics
per valutare le prestazioni del nostro modello.
import numpy as np import pandas as pd import matplotlib.pyplot as plt import torch from torch import optim from torch import nn from torch.utils.data import DataLoader from tqdm import tqdm # !pip install torchvision import torchvision import torch.nn.functional as F import torchvision.datasets as datasets import torchvision.transforms as transforms # !pip install torchmetrics import torchmetrics
2. Caricamento e preprocessing del dataset
PyTorch offre anche un ricco ecosistema di strumenti ed estensioni, incluso torchvision, un modulo per la visione artificiale. Torchvision include diversi dataset di immagini che possono essere utilizzati per addestrare e testare reti neurali. Nel nostro tutorial, utilizzeremo il dataset MNIST.
Per prima cosa, scaricheremo e converteremo il dataset MNIST in un tensore, la struttura dati principale in PyTorch, simile agli array NumPy ma con capacità di accelerazione GPU.
Successivamente, utilizzeremo anche DataLoader per gestire il batching e lo shuffle sia per i set di dati di addestramento che di test. Un DataLoader di PyTorch può essere creato da un Dataset per caricare dati, dividerli in batch e apportare trasformazioni ai dati se desiderato. Quindi, restituisce un campione di dati pronto per l’addestramento. Nel codice seguente, carichiamo i dati e li salviamo in DataLoaders con una dimensione di batch di 60 immagini.
batch_size = 60 train_dataset = datasets.MNIST(root="dataset/", download=True, train=True, transform=transforms.ToTensor()) train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True) test_dataset = datasets.MNIST(root="dataset/", download=True, train=False, transform=transforms.ToTensor()) test_loader = DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=True)
Opzionalmente, il set di dati di addestramento potrebbe essere ulteriormente diviso in due partizioni di addestramento e di validazione. La validazione è una tecnica utilizzata nell’apprendimento profondo per valutare le prestazioni del modello durante l’addestramento. Aiuta a individuare il potenziale overfitting e underfitting dei nostri modelli ed è particolarmente utile per ottimizzare gli iperparametri. Tuttavia, per semplicità, non utilizzeremo la validazione per questo tutorial. Se desideri saperne di più sulla validazione, puoi consultare una spiegazione dettagliata nel nostro Corso di Introduzione all’Apprendimento Profondo con PyTorch.
Ora che abbiamo i nostri dati, vediamo come appare un batch casuale di cifre:
def imshow(img): npimg = img.numpy() plt.imshow(np.transpose(npimg, (1, 2, 0))) plt.show() # otteniamo alcune immagini di addestramento casuali dataiter = iter(dataloader_train) images, labels = next(dataiter) labels # mostrare le immagini imshow(torchvision.utils.make_grid(images))
3. Definizione dell’architettura CNN
Per risolvere il problema di classificazione, sfrutteremo la classe nn.Module
, il blocco di costruzione di PyTorch per creare in modo intuitivo architetture di reti neurali sofisticate.
Nel codice seguente, creiamo una classe chiamata CNN
, che eredita le proprietà della classe nn.Module
. La classe CNN
sarà il modello di una CNN con due strati convoluzionali, seguiti da uno strato completamente connesso.
In PyTorch, utilizziamo nn.Conv2d
per definire uno strato convoluzionale. Passiamo il numero di feature map di input e output. Impostiamo anche alcuni parametri per far funzionare lo strato convoluzionale, inclusa la dimensione del kernel o filtro e il padding.
Successivamente, aggiungiamo uno strato di max pooling con nn.MaxPool2d
. In esso, facciamo scorrere una finestra non sovrapposta sull’output dello strato convoluzionale precedente. In ogni posizione, selezioniamo il valore massimo dalla finestra per passarlo in avanti. Questa operazione riduce le dimensioni spaziali delle feature map, riducendo il numero di parametri e la complessità computazionale nella rete. Infine, aggiungiamo uno strato lineare completamente connesso.
La funzione forward()
definisce come i diversi strati sono connessi, aggiungendo diverse funzioni di attivazione ReLU dopo ciascuno strato convoluzionale.
class CNN(nn.Module): def __init__(self, in_channels, num_classes): """ Building blocks of convolutional neural network. Parameters: * in_channels: Number of channels in the input image (for grayscale images, 1) * num_classes: Number of classes to predict. In our problem, 10 (i.e digits from 0 to 9). """ super(CNN, self).__init__() # 1° strato convoluzionale self.conv1 = nn.Conv2d(in_channels=in_channels, out_channels=8, kernel_size=3, padding=1) # Strato di max pooling self.pool = nn.MaxPool2d(kernel_size=2, stride=2) # 2° strato convoluzionale self.conv2 = nn.Conv2d(in_channels=8, out_channels=16, kernel_size=3, padding=1) # Strato completamente connesso self.fc1 = nn.Linear(16 * 7 * 7, num_classes) def forward(self, x): """ Define the forward pass of the neural network. Parameters: x: Input tensor. Returns: torch.Tensor The output tensor after passing through the network. """ x = F.relu(self.conv1(x)) # Applicare la prima convoluzione e l'attivazione ReLU x = self.pool(x) # Applicare max pooling x = F.relu(self.conv2(x)) # Applicare la seconda convoluzione e l'attivazione ReLU x = self.pool(x) # Applicare max pooling x = x.reshape(x.shape[0], -1) # Appiattire il tensore x = self.fc1(x) # Applicare il livello completamente connesso return x x = x.reshape(x.shape[0], -1) # Appiattire il tensore x = self.fc1(x) # Applicare il livello completamente connesso return x
Una volta che abbiamo definito la classe CNN
, possiamo creare il nostro modello e spostarlo sul dispositivo dove verrà addestrato ed eseguito.
Le reti neurali, comprese le CNN, mostrano prestazioni migliori quando vengono eseguite su GPU, ma potrebbe non essere il caso sul tuo computer. Pertanto, eseguiremo il modello su una GPU solo quando disponibile; in caso contrario, utilizzeremo una CPU standard.
device = "cuda" if torch.cuda.is_available() else "cpu" model = CNN(in_channels=1, num_classes=10).to(device) print(model) >>> CNN( (conv1): Conv2d(1, 8, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (conv2): Conv2d(8, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (fc1): Linear(in_features=784, out_features=10, bias=True) )
4. Addestramento del modello CNN
Ora che abbiamo il nostro modello, è tempo di addestrarlo. Per farlo, dobbiamo prima determinare come misureremo le prestazioni del modello. Poiché ci occupiamo di un problema di classificazione multi-classe, utilizzeremo la funzione di perdita cross-entropy, disponibile in PyTorch come nn.CrossEntropyLoss
. Utilizzeremo anche l’ottimizzatore Adam, uno degli algoritmi di ottimizzazione più popolari.
# Definire la funzione di perdita criterion = nn.CrossEntropyLoss() # Definire l'ottimizzatore optimizer = optim.Adam(model.parameters(), lr=0.001)
Itereremo su dieci epoche e lotti di addestramento per addestrare il modello e svolgere la consueta sequenza di passaggi per ogni lotto, come mostrato di seguito.
num_epochs=10 for epoch in range(num_epochs): # Iterare sui lotti di addestramento print(f"Epoch [{epoch + 1}/{num_epochs}]") for batch_index, (data, targets) in enumerate(tqdm(dataloader_train)): data = data.to(device) targets = targets.to(device) scores = model(data) loss = criterion(scores, targets) optimizer.zero_grad() loss.backward() optimizer.step()
Epoch [1/10] 100%|██████████| 1000/1000 [00:13<00:00, 72.94it/s] Epoch [2/10] 100%|██████████| 1000/1000 [00:12<00:00, 77.27it/s] Epoch [3/10] 100%|██████████| 1000/1000 [00:12<00:00, 77.16it/s] Epoch [4/10] 100%|██████████| 1000/1000 [00:12<00:00, 77.00it/s] Epoch [5/10] 100%|██████████| 1000/1000 [00:13<00:00, 75.69it/s] Epoch [6/10] 100%|██████████| 1000/1000 [00:12<00:00, 77.24it/s] Epoch [7/10] 100%|██████████| 1000/1000 [00:12<00:00, 78.23it/s] Epoch [8/10] 100%|██████████| 1000/1000 [00:12<00:00, 78.16it/s] Epoch [9/10] 100%|██████████| 1000/1000 [00:12<00:00, 77.96it/s] Epoch [10/10] 100%|██████████| 1000/1000 [00:12<00:00, 77.93it/s]
5. Valutare il modello
Una volta addestrato il modello, possiamo valutarne le prestazioni sul set di dati di test. Utilizzeremo l’accuratezza, una metrica popolare per i problemi di classificazione. L’accuratezza misura la proporzione di casi classificati correttamente rispetto al numero totale di oggetti nel set di dati. È calcolata dividendo il numero di previsioni corrette per il numero totale di previsioni fatte dal modello.
Prima, impostiamo la metrica di accuratezza da torchmetrics. Successivamente, utilizziamo il metodo .eval del modello per mettere il modello in modalità di valutazione, poiché alcuni strati nei modelli PyTorch si comportano in modo diverso durante le fasi di addestramento rispetto a quelle di test. Aggiungiamo anche un contesto Python con torch.no_grad
, indicando che non eseguiremo il calcolo del gradiente.
Successivamente, iteriamo sugli esempi di test senza calcolare il gradiente. Per ogni batch di test, otteniamo le previsioni del modello, prendiamo la classe più probabile e la passiamo alla funzione di accuratezza insieme alle etichette. Infine, calcoliamo le metriche e stampiamo i risultati. Abbiamo ottenuto un punteggio di accuratezza del 0,98, il che significa che il nostro modello ha classificato correttamente il 98% delle cifre. Non male!
# Impostazione della metrica di accuratezza multiclasse acc = Accuracy(task="multiclass",num_classes=10) # Iterazione sui batch del dataset model.eval() with torch.no_grad(): for images, labels in dataloader_test: # Ottenere le probabilità predette per il batch di dati di test outputs = model(images) _, preds = torch.max(outputs, 1) acc(preds, labels) precision(preds, labels) recall(preds, labels) #Calcolare l'accuratezza totale del test test_accuracy = acc.compute() print(f"Test accuracy: {test_accuracy}") >>> Test accuracy: 0.9857000112533569
Puoi anche utilizzare altre metriche di classificazione popolari, inclusi richiamo e precisione. Ti raccontiamo tutto su queste metriche con esempi pratici nel nostro Corso Intermedio di Deep Learning con PyTorch.
Migliorare le Prestazioni del Modello
Anche se il nostro modello CNN raggiunge una forte performance, ci sono diverse strategie che possiamo utilizzare per migliorare ulteriormente la sua accuratezza, robustezza e generalizzazione ai nuovi dati.
In questa sezione, esploreremo tecniche chiave come l’augmented data, la regolazione degli iperparametri e l’apprendimento per trasferimento per ottimizzare le prestazioni del nostro modello.
Le tecniche di augmented data
Augmented data è una tecnica utilizzata per migliorare l’accuratezza del nostro modello creando casualmente nuovi dati di addestramento. Ad esempio, durante il caricamento, si possono applicare trasformazioni alle immagini di addestramento, come ridimensionamento, ribaltamento orizzontale o verticale, rotazione casuale, e così via. In questo modo, si possono creare immagini augmentate e assegnare loro la stessa etichetta dell’immagine originale, aumentando così la dimensione del set di addestramento.
Aggiungere trasformazioni casuali alle immagini originali ci consente di generare più dati aumentando la dimensione e la diversità del set di addestramento. Rende il modello più robusto alle variazioni e alle distorsioni comunemente presenti nelle immagini del mondo reale e riduce l’overfitting poiché il modello impara a ignorare le trasformazioni casuali.
Tuttavia, è importante fare attenzione all’augmented data, perché a volte può danneggiare il processo di addestramento. Ad esempio, nel nostro problema, se applichiamo il ribaltamento verticale al numero “6”, esso apparirà come il numero “9”. Passarlo al modello etichettato come “6” confonderà il modello e ostacolerà l’addestramento. Questi esempi mostrano che, a volte, specifiche augmentazioni possono influenzare l’etichetta.
Regolazione degli iperparametri
Un’altra strategia per migliorare le prestazioni del nostro modello è cambiare i valori degli iperparametri coinvolti nei diversi strati del modello. Questo ottimizzazione degli iperparametri richiede una profonda comprensione della matematica alla base delle reti neurali e del significato dei diversi iperparametri.
Ad esempio, potresti regolare i tuoi strati CNN cambiando le dimensioni dei filtri o aumentando il padding. Potresti anche impostare un valore diverso per i pesi iniziali dei neuroni.
Dato che non conosciamo in anticipo i valori ottimali degli iperparametri, sarà necessaria una certa dose di tentativi ed errori. Di solito questo viene fatto attraverso una tecnica nota come grid search, che ti permette di valutare sistematicamente un modello su una griglia di valori dei parametri.
Tuttavia, fai attenzione nell’utilizzare questa tecnica, poiché di solito è computazionalmente costosa, specialmente quando si tratta di reti neurali complesse e di grandi set di dati di addestramento.
Allo stesso modo, potresti aumentare la complessità del tuo modello aggiungendo più strati convoluzionali e lineari. Tuttavia, fai attenzione quando aggiungi nuovi strati, poiché il numero di neuroni potrebbe aumentare drasticamente, comportando tempi di addestramento più lunghi e potenziali problemi di sovradattamento.
Puoi approfondire ulteriormente l’ottimizzazione degli iperparametri nel nostro Corso di Introduzione al Deep Learning con PyTorch.
Utilizzando modelli pre-addestrati
Addestrare modelli di deep learning da zero è un processo lungo e noioso, e richiede tipicamente una grande quantità di dati di addestramento. Invece, possiamo spesso utilizzare modelli pre-addestrati, ovvero modelli che sono già stati addestrati su un certo compito.
A volte, possiamo riutilizzare direttamente un modello pre-addestrato se può già risolvere il compito che ci interessa. In altre occasioni, potremmo dover adattare il modello pre-addestrato per adattarlo al nuovo compito. Questo è noto come apprendimento transfer.
Utilizzare modelli pre-addestrati in PyTorch è abbastanza semplice. Torchvision fornisce una collezione di modelli pre-addestrati per vari compiti legati alle immagini. Questi modelli sono pre-addestrati su set di dati di immagini su larga scala e sono facilmente disponibili. Dai un’occhiata al nostro Corsodi Deep Learning per Immagini con PyTorch per imparare tutto ciò che devi sapere su di essi.
Distribuire il Modello CNN
Avere addestrato il tuo modello di classificazione altamente preciso in PyTorch, ora puoi salvare il modello e i suoi pesi pre-addestrati per un uso futuro e condividerlo con il tuo team, assicurandoti che possano caricarlo senza problemi.
Per salvare un modello, possiamo usare torch.save
. Un’estensione di file comune per i modelli torch è pt
o pth
. Per salvare i pesi del modello, passiamo model.state_dict
a torch.save
fornendo il nome del file di output, ad esempio, MulticlassCNN.pth
.
Per caricare un modello salvato, inizializziamo un nuovo modello con la stessa architettura. Successivamente utilizziamo il metodo di caricamento dello stato dict
insieme a torch.load
per caricare i parametri nel nuovo modello.
# Salva il modello torch.save(model.state_dict(), 'MulticlassCNN.pth') # Crea un nuovo modello loaded_model = CNN(in_channels=1, num_classes=10) # Carica il modello salvato loaded_model.load_state_dict(torch.load('MulticlassCNN.pth')) print(loaded_model) CNN( (conv1): Conv2d(1, 8, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (conv2): Conv2d(8, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (fc1): Linear(in_features=784, out_features=10, bias=True) )
Conclusione
Abbiamo fornito una panoramica completa delle CNN, fornendo dettagli su ciascuno strato dell’architettura CNN. Inoltre, abbiamo fornito una guida su come implementare una CNN in PyTorch, coprendo i passaggi principali, dal caricamento dei dati e la progettazione del modello all’addestramento e alla valutazione del modello. Infine, abbiamo anche analizzato diverse strategie per migliorare le prestazioni del nostro modello. Abbiamo applicato tutte queste competenze a uno scenario del mondo reale relativo a un compito di classificazione multiclasse.
C’è molto da imparare sul deep learning, probabilmente uno dei campi più eccitanti e impegnativi nell’ambito dell’IA. Fortunatamente, DataCamp è qui per aiutarti. Scopri i nostri materiali e corsi dedicati e diventa un esperto di reti neurali:
- Introduzione alle reti neurali convoluzionali: una guida completa alle CNN nel deep learning
- Reti Neurali Convoluzionali in Python con Keras
- Corso di Reti Neurali Convoluzionali (CNN) in Python con TensorFlow
- Introduzione alle Funzioni di Attivazione nelle Reti Neurali
- Corso di Deep Learning per il Testo con PyTorch
- Deep Learning in Python
- Come Imparare il Deep Learning nel 2025: Una Guida Completa
- Corso di Deep Learning Intermedio con PyTorch
- Guida alle Certificazioni e Certificati di PyTorch
Source:
https://www.datacamp.com/tutorial/pytorch-cnn-tutorial