Le prime reti neurali che sono state pensate, quelle che troviamo descritte nei capitoli iniziali di un qualsiasi libro delle serie "Deep Learning in Python", sono le cosidette reti "Fully Connected".
Una rete Fully Connected è costituita da una serie consecutiva di layer, ciascuno dei quali contiene N neuroni identici.
E' possibile utilizzare una FC anche per elaborare immagini. Ma il risultato che si ottiene è tutt'altro che ottimale.
Infatti, un grande progresso è stato compiuto quando si è iniziato ad utilizzare le CNN: Convolutional Neural Network.
Tuttavia, esaminare l'implementazione di una FC è utile, in quanto semplici, per capire come si costruiscono le Reti Neurali in PyTorch.
In alcuni testi al posto del termine "Fully Connected" si utilizza Multi-layer Perceptron )MLP).
Ah, "last but not the least": per definizione una rete Deep è una rete con molti layer tra l'input ed il layer che fornisce il risultato. Nelle applicazioni pratiche oggi si usano reti con centinaia di layer e centinaia di milioni, se non miliardi di parametri, da apprendere.
In questo capitolo mostrerò:
- come implementare passo passo una rete FC
- come applicarla per costruire un modello di classificazione su dati strutturati (tabellari)
Il modello sarà tutt'altro che ottimale, ma l'esempio è utile per iniziare a comprendere tutti i "mattoncini" per lavorare, in PyTorch, sulle Reti Neurali.
L'efficacia delle FC in modelli per dati tabellari è limitata. Oltretutto, vi sono una serie di complicazioni che comportano spesso un tuning difficile degli iper-parametri. E' per queste ragioni che sono state identificate architetture specializzate, come quella adottata in TabNet, utilizzata sullo stesso dataset in un altro capitolo.
L'esempio di questo capitolo non vuole fornire uno strumento "serio" per trattare dati tabellari, ma un esempio di una rete dall'architettura molto semplice, su cui vogliamo spiegare le basi di PyTorch.
Quindi, se eseguendo i Notebook le prestazioni non risulteranno ... di nostra soddisfazione, nessuna sorpresa.
Una rete FC è una sequenza di layer, su cui è applicata una activation function
Per implementare in PyTorch una rete FC dobbiamo definire una classe, subclassando la classe nn.Module
Nella classe dobbiamo ridefinire i metodi
__init__
- forward
Nel metodo init definiamo la struttura della rete, elencando tutti i layer che hanno parametri. E' fondamentale, come nell'esempio sotto riportato, che anche il dropout sia definito come attributo della classe
Nel metodo forward definiamo la sequenza di operazioni che, nel forward pass, portano dall'input all'output della rete
class FirstFC(nn.Module):
# here we define the structure
def __init__(self):
super(FirstFC, self).__init__()
self.fc1 = nn.Linear(30, 30)
self.fc2 = nn.Linear(30, 30)
self.fc3 = nn.Linear(30, 30)
self.fc4 = nn.Linear(30, 10)
self.fc5 = nn.Linear(10, 1)
# fondamentale memorizzare anche il dropout tra gli attributi
# per disattivarlo in modalità eval
# altrimenti in eval i risultati non sono pienamente riproducibili
self.drop = nn.Dropout(0.1)
# here we implement the forward pass
def forward(self, x):
x = self.fc1(x)
x = F.relu(x)
x = self.drop(x)
x = self.fc2(x)
x = F.relu(x)
x = self.drop(x)
x = F.relu(self.fc3(x))
x = F.relu(self.fc4(x))
# the last layer must give the prob that it is positive... so we use sigmoid
# so we must use nn.BCELoss
x = torch.sigmoid(self.fc5(x))
return x
Il dropout è inserito, sopratutto se si teme che la rete possa andare in overfitting. In sintesi: durante il training un layer di dropout casualmente "spegne" alcuni degli output. L'esperienza ha mostrato che, sopratutto se vi è overfitting, l'introduzione di questa casualità può aiutare la rete ad apprendere e generalizzare meglio.
Il dropout è stato proposto da G. Hinton nel 2012. Un articolo di review, per approfondimenti, è il seguente.
Vi è un dettaglio di implementazione che non va assolutamente dimenticato: noi vogliamo che il dropout sia attivo durante il training del modello, ma assolutamente non durante la validazione. Altrimenti i risultati ottenuti, in fase di validazione, presenterebbero una certa "casualità" e "variabilità".
Per assicurarci che in fase di validazione i layer di dropout siano disattivati devono essere soddisfate queste due condizioni:
- i layer di dropout devono essere definiti come attributi della classe (self.drop nell'esempio precedente)
- come primo passo della validazione si deve invocare model.eval()
PyTorch, se escludiamo Lightning, è un framework che opera un più a basso livello di Keras.
Quindi, storicamente, chi ha iniziato ad usare PyTorch si è abituato a scrivere tutto il codice del training loop.
In genere, si parte da codice di esempio e si adatta alle proprie esigenze.
Nel Notebook first_fc.ipynb vi è tutto il codice per:
- caricare il dataset da un file csv, in un PyTorch Dataset
- costruire la rete
- implementare ed eseguire il training loop
- valutare le prestazioni del modello addestrato su un validation dataset
Il training loop è implementato nella funzione train()
Innanzitutto, vi è un ciclo su tutte le epochs
for epoch in range(epochs):
Per ogni epoch, la prima parte applica l'algoritmo di back-propagation e calcola la training loss.
model.train()
training_loss = 0.0
for batch in train_loader:
optimizer.zero_grad()
inputs, targets = batch
# move to device
inputs = inputs.to(device)
targets = targets.to(device)
# apply the model
outputs = model(inputs)
# we need to add this !
targets = targets.unsqueeze(1)
loss = loss_fn(outputs, targets)
# here we do the back propagation !!
loss.backward()
optimizer.step()
training_loss += loss.data.item() * inputs.size(0)
training_loss /= len(train_loader.dataset)
La seconda parte calcola la validation loss:
model.eval()
val_loss = 0.0
for batch in validation_loader:
inputs, targets = batch
inputs = inputs.to(device)
targets = targets.to(device)
outputs = model(inputs)
targets = targets.unsqueeze(1)
loss = loss_fn(outputs, targets)
val_loss += loss.data.item() * inputs.size(0)
val_loss /= len(validation_loader.dataset)