Con i nostri dati a posto, è il momento di addestrare la nostra prima rete neurale. Utilizzeremo un’architettura simile a quella che abbiamo fatto nell’ultimo post del blog della serie, utilizzando una versione lineare della nostra rete neurale con la capacità di gestire modelli lineari:
from torch import nnclass LinearModel(nn.Module):
def __init__(self):
super().__init__()
self.layer_1 = nn.Linear(in_features=12, out_features=5)
self.layer_2 = nn.Linear(in_features=5, out_features=1)
def forward(self, x):
return self.layer_2(self.layer_1(x))
Questa rete neurale utilizza il nn.Linear
modulo da pytorch
per creare una rete neurale con 1 strato profondo (uno strato di input, uno strato profondo e uno strato di output).
Sebbene possiamo creare la nostra classe ereditando da nn.Module
possiamo anche usare (più elegantemente) the nn.Sequential
costruttore per fare lo stesso:
model_0 = nn.Sequential(
nn.Linear(in_features=12, out_features=5),
nn.Linear(in_features=5, out_features=1)
)
Freddo! Quindi la nostra rete neurale contiene un singolo strato interno con 5 neuroni (questo può essere visto dal out_features=5
sul primo strato).
Questo strato interno riceve lo stesso numero di connessioni da ciascun neurone di input. Il 12 pollici in_features
nel primo livello riflette il numero di funzioni e 1 pollice out_features
del secondo livello riflette l’output (un singolo valore compreso tra 0 e 1).
Per addestrare la nostra rete neurale, definiremo una funzione di perdita e un ottimizzatore. Definiremo BCEWithLogitsLoss
(Documentazione PyTorch 2.1) poiché questa funzione di perdita (implementazione della torcia dell’entropia incrociata binaria, appropriata per problemi di classificazione) e la discesa del gradiente stocastico come ottimizzatore (utilizzando torch.optim.SGD
).
# Binary Cross entropy
loss_fn = nn.BCEWithLogitsLoss()# Stochastic Gradient Descent for Optimizer
optimizer = torch.optim.SGD(params=model_0.parameters(),
lr=0.01)
Infine, poiché voglio calcolare anche la precisione per ogni epoca del processo di allenamento, progetteremo una funzione per calcolarlo:
def compute_accuracy(y_true, y_pred):
tp_tn = torch.eq(y_true, y_pred).sum().item()
acc = (tp_tn / len(y_pred)) * 100
return acc
È ora di addestrare il nostro modello! Addestriamo il nostro modello per 1000 epoche e vediamo come una semplice rete lineare è in grado di gestire questi dati:
torch.manual_seed(42)epochs = 1000
train_acc_ev = ()
test_acc_ev = ()
# Build training and evaluation loop
for epoch in range(epochs):
model_0.train()
y_logits = model_0(X_train).squeeze()
loss = loss_fn(y_logits,
y_train)
# Calculating accuracy using predicted logists
acc = compute_accuracy(y_true=y_train,
y_pred=torch.round(torch.sigmoid(y_logits)))
train_acc_ev.append(acc)
# Training steps
optimizer.zero_grad()
loss.backward()
optimizer.step()
model_0.eval()
# Inference mode for prediction on the test data
with torch.inference_mode():
test_logits = model_0(X_test).squeeze()
test_loss = loss_fn(test_logits,
y_test)
test_acc = compute_accuracy(y_true=y_test,
y_pred=torch.round(torch.sigmoid(test_logits)))
test_acc_ev.append(test_acc)
# Print out accuracy and loss every 100 epochs
if epoch % 100 == 0:
print(f"Epoch: {epoch}, Loss: {loss:.5f}, Accuracy: {acc:.2f}% | Test loss: {test_loss:.5f}, Test acc: {test_acc:.2f}%")
Sfortunatamente la rete neurale che abbiamo appena costruito non è abbastanza buona per risolvere questo problema. Vediamo l’evoluzione dell’addestramento e l’accuratezza dei test:
(Sto tracciando l’accuratezza invece della perdita poiché è più facile da interpretare in questo problema)
È interessante notare che la nostra rete neurale non è in grado di migliorare gran parte della precisione del set di test.
Con la conoscenza acquisita dai post precedenti del blog, possiamo provare ad aggiungere più strati e neuroni alla nostra rete neurale. Proviamo a fare entrambe le cose e vediamo il risultato:
deeper_model = nn.Sequential(
nn.Linear(in_features=12, out_features=20),
nn.Linear(in_features=20, out_features=20),
nn.Linear(in_features=20, out_features=1)
)
Sebbene il nostro modello più profondo sia un po’ più complesso con uno strato aggiuntivo e più neuroni, ciò non si traduce in maggiori prestazioni nella rete:
Anche se il nostro modello è più complesso, ciò non apporta maggiore precisione al nostro problema di classificazione.
Per poter ottenere maggiori prestazioni, dobbiamo sbloccare una nuova funzionalità delle reti neurali: le funzioni di attivazione!
Se rendere il nostro modello sempre più grande non ha portato molti miglioramenti, ci deve essere qualcos’altro che possiamo fare con le reti neurali che sarà in grado di migliorarne le prestazioni, giusto?
È qui che possono essere utilizzate le funzioni di attivazione! Nel nostro esempio, torneremo al nostro modello più semplice, ma questa volta con una svolta:
model_non_linear = nn.Sequential(
nn.Linear(in_features=12, out_features=5),
nn.ReLU(),
nn.Linear(in_features=5, out_features=1)
)
Qual è la differenza tra questo modello e il primo? La differenza è che abbiamo aggiunto un nuovo blocco alla nostra rete neurale: nn.ReLU
. IL unità lineare rettificata è una funzione di attivazione che cambierà il calcolo in ciascuno dei pesi della Rete Neurale:
Ogni valore che passa attraverso i nostri pesi nella rete neurale verrà calcolato rispetto a questa funzione. Se il valore della caratteristica moltiplicato per il peso è negativo, il valore viene impostato su 0, altrimenti viene assunto il valore calcolato. Solo questo piccolo cambiamento aggiunge molta potenza a un’architettura di rete neurale: in torch
abbiamo diverse funzioni di attivazione che possiamo usare come nn.ReLU
, nn.Tanh
O nn.ELU
. Per una panoramica di tutte le funzioni di attivazione, controlla qui collegamento.
La nostra architettura di rete neurale contiene una piccola svolta, al momento:
Con questa piccola svolta nella Rete Neurale, ogni valore proveniente dal primo strato (rappresentato da nn.Linear(in_features=12, out_features=5)
) dovrà superare il test “ReLU”.
Vediamo l’impatto dell’adattamento di questa architettura ai nostri dati:
Freddo! Sebbene si osservi un calo delle prestazioni dopo 800 epoche, questo modello non mostra un adattamento eccessivo come i precedenti. Tieni presente che il nostro set di dati è molto piccolo, quindi è possibile che i nostri risultati siano migliori solo per casualità. Tuttavia, aggiungendo funzioni di attivazione al tuo torch
hanno sicuramente un enorme impatto in termini di prestazioni, formazione e generalizzazione, in particolare quando si hanno molti dati su cui allenarsi.
Ora che conosci la potenza delle funzioni di attivazione non lineare, è anche importante sapere:
Fonte: towardsdatascience.com