Come valutare le prestazioni dei modelli PyTorch
Progettare un modello di deep learning a volte è un'arte. Ci sono molti punti decisionali e non è facile dire quale sia il migliore. Un modo per elaborare un progetto è procedere per tentativi ed errori e valutare il risultato su dati reali. Pertanto, è importante disporre di un metodo scientifico per valutare le prestazioni della rete neurale e dei modelli di deep learning. In effetti, è lo stesso metodo anche per confrontare qualsiasi tipo di modello di machine learning su un utilizzo particolare.
In questo post scoprirai il flusso di lavoro ricevuto per valutare in modo affidabile le prestazioni del modello. Negli esempi utilizzeremo PyTorch per costruire i nostri modelli, ma il metodo può essere applicato anche ad altri modelli. Dopo aver completato questo post, saprai:
- Come valutare un modello PyTorch utilizzando un set di dati di verifica
- Come valutare un modello PyTorch con convalida incrociata k-fold
Avvia il tuo progetto con il mio libro Deep Learning with PyTorch. Fornisce tutorial di autoapprendimento con codice funzionante.
Panoramica
Questo capitolo è diviso in quattro parti; sono:
- Valutazione empirica dei modelli
- Suddivisione dei dati
- Addestramento di un modello PyTorch con convalida
- Convalida incrociata k-Fold
Valutazione empirica dei modelli
Quando si progetta e si configura da zero un modello di deep learning, ci sono molte decisioni da prendere. Ciò include decisioni di progettazione come quanti livelli utilizzare in un modello di deep learning, quanto è grande ciascun livello e che tipo di livelli o funzioni di attivazione utilizzare. Può anche essere la scelta della funzione di perdita, dell'algoritmo di ottimizzazione, del numero di epoche da addestrare e dell'interpretazione dell'output del modello. Fortunatamente, a volte, puoi copiare la struttura delle reti di altre persone. A volte, puoi semplicemente fare la tua scelta usando alcune euristiche. Per capire se hai fatto una buona scelta o meno, il modo migliore è confrontare più alternative valutandole empiricamente con dati reali.
Il deep learning viene spesso utilizzato su problemi con set di dati molto grandi. Si tratta di decine di migliaia o centinaia di migliaia di campioni di dati. Ciò fornisce ampi dati per i test. Ma è necessario disporre di una solida strategia di test per stimare le prestazioni del modello su dati invisibili. Sulla base di ciò, puoi avere una metrica per confrontare tra diverse configurazioni di modello.
Suddivisione dei dati
Se disponi di un set di dati di decine di migliaia di campioni o anche più, non è sempre necessario fornire tutto al tuo modello per l'addestramento. Ciò aumenterà inutilmente la complessità e allungherà i tempi di formazione. Di più non è sempre meglio. Potresti non ottenere il miglior risultato.
Quando disponi di una grande quantità di dati, dovresti prenderne una parte come set di training che viene inserito nel modello per l'addestramento. Un'altra parte viene conservata come set di test da trattenere dall'addestramento ma verificata con un modello addestrato o parzialmente addestrato come valutazione. Questo passaggio viene solitamente chiamato "divisione del treno-test".
Consideriamo il set di dati sul diabete degli indiani Pima. Puoi caricare i dati usando NumPy:
import numpy as np
data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",")
Sono presenti 768 campioni di dati. Non è molto ma basta per dimostrare la scissione. Consideriamo il primo 66% come training set e il restante come test set. Il modo più semplice per farlo è affettare un array:
# find the boundary at 66% of total samples
count = len(data)
n_train = int(count * 0.66)
# split the data at the boundary
train_data = data[:n_train]
test_data = data[n_train:]
La scelta del 66% è arbitraria, ma non si vuole che il set di allenamento sia troppo piccolo. A volte è possibile utilizzare una suddivisione del 70%-30%. Ma se il set di dati è enorme, potresti anche utilizzare una suddivisione del 30%-70% se il 30% dei dati di addestramento è sufficientemente grande.
Se dividi i dati in questo modo, stai suggerendo che i set di dati vengano mescolati in modo che il set di addestramento e il set di test siano ugualmente diversi. Se trovi che il set di dati originale è ordinato e prendi il set di test solo alla fine, potresti scoprire di avere tutti i dati di test appartenenti alla stessa classe o che portano lo stesso valore in una delle funzionalità di input. Non è l'ideale.
Naturalmente, puoi chiamare np.random.shuffle(data)
prima della divisione per evitarlo. Ma molti ingegneri di machine learning di solito usano scikit-learn per questo. Vedi questo esempio:
import numpy as np
from sklearn.model_selection import train_test_split
data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",")
train_data, test_data = train_test_split(data, test_size=0.33)
Ma più comunemente, viene eseguita dopo aver separato la funzione di input e le etichette di output. Nota che questa funzione di scikit-learn può funzionare non solo sugli array NumPy ma anche sui tensori PyTorch:
import numpy as np
import torch
from sklearn.model_selection import train_test_split
data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",")
X = data[:, 0:8]
y = data[:, 8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33)
Addestramento di un modello PyTorch con convalida
Rivisitiamo il codice per creare e addestrare un modello di deep learning su questo set di dati:
import torch
import torch.nn as nn
import torch.optim as optim
import tqdm
...
model = nn.Sequential(
nn.Linear(8, 12),
nn.ReLU(),
nn.Linear(12, 8),
nn.ReLU(),
nn.Linear(8, 1),
nn.Sigmoid()
)
# loss function and optimizer
loss_fn = nn.BCELoss() # binary cross entropy
optimizer = optim.Adam(model.parameters(), lr=0.0001)
n_epochs = 50 # number of epochs to run
batch_size = 10 # size of each batch
batches_per_epoch = len(Xtrain) // batch_size
for epoch in range(n_epochs):
with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0) as bar:
bar.set_description(f"Epoch {epoch}")
for i in bar:
# take a batch
start = i * batch_size
X_batch = X_train[start:start+batch_size]
y_batch = y_train[start:start+batch_size]
# forward pass
y_pred = model(X_batch)
loss = loss_fn(y_pred, y_batch)
# backward pass
optimizer.zero_grad()
loss.backward()
# update weights
optimizer.step()
# print progress
bar.set_postfix(
loss=float(loss)
)
In questo codice, un batch viene estratto dal set di training in ogni iterazione e inviato al modello nel passaggio successivo. Quindi calcoli il gradiente nel passaggio all'indietro e aggiorni i pesi.
Anche se, in questo caso, hai utilizzato l'entropia incrociata binaria come metrica di perdita nel ciclo di addestramento, potresti essere più interessato all'accuratezza della previsione. Calcolare la precisione è facile. Arrotonda l'output (nell'intervallo da 0 a 1) all'intero più vicino in modo da poter ottenere un valore binario di 0 o 1. Quindi conti la percentuale in cui la tua previsione corrisponde all'etichetta; questo ti dà la precisione.
Ma qual è la tua previsione? È y_pred
sopra, che è la previsione del tuo modello attuale su X_batch
. L'aggiunta di precisione al ciclo di allenamento diventa questa:
for epoch in range(n_epochs):
with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0) as bar:
bar.set_description(f"Epoch {epoch}")
for i in bar:
# take a batch
start = i * batch_size
X_batch = X_train[start:start+batch_size]
y_batch = y_train[start:start+batch_size]
# forward pass
y_pred = model(X_batch)
loss = loss_fn(y_pred, y_batch)
# backward pass
optimizer.zero_grad()
loss.backward()
# update weights
optimizer.step()
# print progress, with accuracy
acc = (y_pred.round() == y_batch).float().mean()
bar.set_postfix(
loss=float(loss)
acc=float(acc)
)
Tuttavia, X_batch
e y_batch
vengono utilizzati dall'ottimizzatore e l'ottimizzatore ottimizzerà il tuo modello in modo da poter prevedere y_batch
da < codice>X_batch. E ora stai utilizzando la precisione per verificare se y_pred
corrisponde a y_batch
. È come imbrogliare perché se il tuo modello in qualche modo ricorda la soluzione, può semplicemente segnalarti y_pred
e ottenere una precisione perfetta senza effettivamente dedurre y_pred
da X_batch.
In effetti, un modello di deep learning può essere così contorto che non è possibile sapere se il modello ricorda semplicemente la risposta o la sta deducendo. Pertanto, il modo migliore non è calcolare la precisione da X_batch
o qualsiasi cosa da X_train
ma da qualcos'altro: il tuo set di test. Aggiungiamo una misurazione della precisione dopo ogni epoca utilizzando X_test
:
for epoch in range(n_epochs):
with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0) as bar:
bar.set_description(f"Epoch {epoch}")
for i in bar:
# take a batch
start = i * batch_size
X_batch = X_train[start:start+batch_size]
y_batch = y_train[start:start+batch_size]
# forward pass
y_pred = model(X_batch)
loss = loss_fn(y_pred, y_batch)
# backward pass
optimizer.zero_grad()
loss.backward()
# update weights
optimizer.step()
# print progress
acc = (y_pred.round() == y_batch).float().mean()
bar.set_postfix(
loss=float(loss),
acc=float(acc)
)
# evaluate model at end of epoch
y_pred = model(X_test)
acc = (y_pred.round() == y_test).float().mean()
acc = float(acc)
print(f"End of {epoch}, accuracy {acc}")
In questo caso, acc
nel ciclo for interno è solo una metrica che mostra l'avanzamento. Non c'è molta differenza nella visualizzazione della metrica della perdita, tranne che non è coinvolta nell'algoritmo di discesa del gradiente. E ti aspetti che la precisione migliori man mano che migliora anche la metrica delle perdite.
Nel ciclo for esterno, alla fine di ogni epoca, calcoli la precisione da X_test
. Il flusso di lavoro è simile: fornisci il set di test al modello e chiedi la sua previsione, quindi conti il numero di risultati corrispondenti con le etichette del set di test. Ma questa precisione è quella di cui dovresti preoccuparti. Dovrebbe migliorare man mano che l'allenamento procede, ma se non vedi miglioramenti (cioè aumento di precisione) o addirittura peggiora, devi interrompere l'allenamento perché sembra che inizi a sovraccaricarsi. L'overfitting si verifica quando il modello inizia a ricordare il set di addestramento anziché imparare a dedurre la previsione da esso. Un segno di ciò è che la precisione del set di addestramento continua ad aumentare mentre la precisione del set di test diminuisce.
Quello che segue è il codice completo per implementare tutto quanto sopra, dalla suddivisione dei dati alla convalida utilizzando il set di test:
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import tqdm
from sklearn.model_selection import train_test_split
data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",")
X = data[:, 0:8]
y = data[:, 8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33)
model = nn.Sequential(
nn.Linear(8, 12),
nn.ReLU(),
nn.Linear(12, 8),
nn.ReLU(),
nn.Linear(8, 1),
nn.Sigmoid()
)
# loss function and optimizer
loss_fn = nn.BCELoss() # binary cross entropy
optimizer = optim.Adam(model.parameters(), lr=0.0001)
n_epochs = 50 # number of epochs to run
batch_size = 10 # size of each batch
batches_per_epoch = len(X_train) // batch_size
for epoch in range(n_epochs):
with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0) as bar: #, disable=True) as bar:
bar.set_description(f"Epoch {epoch}")
for i in bar:
# take a batch
start = i * batch_size
X_batch = X_train[start:start+batch_size]
y_batch = y_train[start:start+batch_size]
# forward pass
y_pred = model(X_batch)
loss = loss_fn(y_pred, y_batch)
# backward pass
optimizer.zero_grad()
loss.backward()
# update weights
optimizer.step()
# print progress
acc = (y_pred.round() == y_batch).float().mean()
bar.set_postfix(
loss=float(loss),
acc=float(acc)
)
# evaluate model at end of epoch
y_pred = model(X_test)
acc = (y_pred.round() == y_test).float().mean()
acc = float(acc)
print(f"End of {epoch}, accuracy {acc}")
Il codice sopra stamperà quanto segue:
End of 0, accuracy 0.5787401795387268
End of 1, accuracy 0.6102362275123596
End of 2, accuracy 0.6220472455024719
End of 3, accuracy 0.6220472455024719
End of 4, accuracy 0.6299212574958801
End of 5, accuracy 0.6377952694892883
End of 6, accuracy 0.6496062874794006
End of 7, accuracy 0.6535432934761047
End of 8, accuracy 0.665354311466217
End of 9, accuracy 0.6614173054695129
End of 10, accuracy 0.665354311466217
End of 11, accuracy 0.665354311466217
End of 12, accuracy 0.665354311466217
End of 13, accuracy 0.665354311466217
End of 14, accuracy 0.665354311466217
End of 15, accuracy 0.6732283234596252
End of 16, accuracy 0.6771653294563293
End of 17, accuracy 0.6811023354530334
End of 18, accuracy 0.6850393414497375
End of 19, accuracy 0.6889764070510864
End of 20, accuracy 0.6850393414497375
End of 21, accuracy 0.6889764070510864
End of 22, accuracy 0.6889764070510864
End of 23, accuracy 0.6889764070510864
End of 24, accuracy 0.6889764070510864
End of 25, accuracy 0.6850393414497375
End of 26, accuracy 0.6811023354530334
End of 27, accuracy 0.6771653294563293
End of 28, accuracy 0.6771653294563293
End of 29, accuracy 0.6692913174629211
End of 30, accuracy 0.6732283234596252
End of 31, accuracy 0.6692913174629211
End of 32, accuracy 0.6692913174629211
End of 33, accuracy 0.6732283234596252
End of 34, accuracy 0.6771653294563293
End of 35, accuracy 0.6811023354530334
End of 36, accuracy 0.6811023354530334
End of 37, accuracy 0.6811023354530334
End of 38, accuracy 0.6811023354530334
End of 39, accuracy 0.6811023354530334
End of 40, accuracy 0.6811023354530334
End of 41, accuracy 0.6771653294563293
End of 42, accuracy 0.6771653294563293
End of 43, accuracy 0.6771653294563293
End of 44, accuracy 0.6771653294563293
End of 45, accuracy 0.6771653294563293
End of 46, accuracy 0.6771653294563293
End of 47, accuracy 0.6732283234596252
End of 48, accuracy 0.6732283234596252
End of 49, accuracy 0.6732283234596252
Convalida incrociata k-Fold
Nell'esempio sopra, hai calcolato la precisione dal set di test. Viene utilizzato come punteggio per il modello man mano che avanzi nella formazione. Vuoi fermarti al punto in cui questo punteggio è al massimo. Infatti, semplicemente confrontando il punteggio di questo set di test, sai che il tuo modello funziona meglio dopo l'epoca 21 e in seguito inizia ad adattarsi eccessivamente. È giusto?
Se costruissi due modelli con design diversi, dovresti semplicemente confrontare la precisione di questi modelli sullo stesso set di test e affermare che uno è migliore di un altro?
In realtà, si può sostenere che il set di test non è sufficientemente rappresentativo anche dopo aver mescolato il set di dati prima di estrarre il set di test. Potresti anche sostenere che, per caso, un modello si adatta meglio a questo particolare set di test, ma non sempre meglio. Per argomentare in modo più forte quale modello sia migliore indipendentemente dalla selezione del set di test, puoi provare più set di test e calcolare la precisione media.
Questo è ciò che fa una convalida incrociata k-fold. È un progresso decidere quale design funziona meglio. Funziona ripetendo il processo di addestramento da zero per $k$volte, ciascuno con una diversa composizione dei set di addestramento e test. Per questo motivo, avrai modelli $k$e punteggi di precisione $k$dai rispettivi set di test. Non sei interessato solo alla precisione media ma anche alla deviazione standard. La deviazione standard indica se il punteggio di precisione è coerente o se alcuni set di test sono particolarmente buoni o cattivi in un modello.
Poiché la convalida incrociata k-fold addestra il modello da zero alcune volte, è meglio racchiudere il ciclo di addestramento in una funzione:
def model_train(X_train, y_train, X_test, y_test):
# create new model
model = nn.Sequential(
nn.Linear(8, 12),
nn.ReLU(),
nn.Linear(12, 8),
nn.ReLU(),
nn.Linear(8, 1),
nn.Sigmoid()
)
# loss function and optimizer
loss_fn = nn.BCELoss() # binary cross entropy
optimizer = optim.Adam(model.parameters(), lr=0.0001)
n_epochs = 25 # number of epochs to run
batch_size = 10 # size of each batch
batches_per_epoch = len(X_train) // batch_size
for epoch in range(n_epochs):
with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0, disable=True) as bar:
bar.set_description(f"Epoch {epoch}")
for i in bar:
# take a batch
start = i * batch_size
X_batch = X_train[start:start+batch_size]
y_batch = y_train[start:start+batch_size]
# forward pass
y_pred = model(X_batch)
loss = loss_fn(y_pred, y_batch)
# backward pass
optimizer.zero_grad()
loss.backward()
# update weights
optimizer.step()
# print progress
acc = (y_pred.round() == y_batch).float().mean()
bar.set_postfix(
loss=float(loss),
acc=float(acc)
)
# evaluate accuracy at end of training
y_pred = model(X_test)
acc = (y_pred.round() == y_test).float().mean()
return float(acc)
Il codice sopra deliberatamente non stampa nulla (con disable=True
in tqdm
) per mantenere lo schermo meno ingombrato.
Anche da scikit-learn hai una funzione per la convalida incrociata k-fold. Puoi utilizzarlo per produrre una stima affidabile dell'accuratezza del modello:
from sklearn.model_selection import StratifiedKFold
# define 5-fold cross validation test harness
kfold = StratifiedKFold(n_splits=5, shuffle=True)
cv_scores = []
for train, test in kfold.split(X, y):
# create model, train, and get accuracy
acc = model_train(X[train], y[train], X[test], y[test])
print("Accuracy: %.2f" % acc)
cv_scores.append(acc)
# evaluate the model
print("%.2f%% (+/- %.2f%%)" % (np.mean(cv_scores)*100, np.std(cv_scores)*100))
Eseguendo queste stampe:
Accuracy: 0.64
Accuracy: 0.67
Accuracy: 0.68
Accuracy: 0.63
Accuracy: 0.59
64.05% (+/- 3.30%)
In scikit-learn, sono presenti più funzioni di convalida incrociata k-fold e quella utilizzata qui è k-fold stratificata. Si presuppone che y
siano etichette di classe e tiene conto dei loro valori in modo da fornire una rappresentazione di classe equilibrata nelle suddivisioni.
Il codice precedente utilizzava $k=5$o 5 suddivisioni. Significa dividere il set di dati in cinque parti uguali, sceglierne una come set di test e combinare il resto in un set di training. Esistono cinque modi per farlo, quindi il ciclo for sopra avrà cinque iterazioni. In ogni iterazione, chiami la funzione model_train()
e ottieni in cambio il punteggio di precisione. Quindi lo salvi in un elenco, che verrà utilizzato per calcolare la media e la deviazione standard alla fine.
L'oggetto kfold
ti restituirà gli indici. Pertanto non è necessario eseguire la suddivisione train-test in anticipo ma utilizzare gli indici forniti per estrarre al volo il set di training e il set di test quando si chiama la funzione model_train()
.
Il risultato sopra mostra che il modello è moderatamente buono, con una precisione media del 64%. E questo punteggio è stabile poiché la deviazione standard è al 3%. Ciò significa che nella maggior parte dei casi ci si aspetta che la precisione del modello sia compresa tra il 61% e il 67%. Puoi provare a modificare il modello sopra, ad esempio aggiungendo o rimuovendo un livello, e vedere quanto cambiamento hai nella media e nella deviazione standard. Puoi anche provare ad aumentare il numero di epoche utilizzate nell'allenamento e osservare il risultato.
La media e la deviazione standard dalla convalida incrociata k-fold è ciò che dovresti utilizzare per confrontare la progettazione di un modello.
Mettendo tutto insieme, di seguito è riportato il codice completo per la convalida incrociata k-fold:
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import tqdm
from sklearn.model_selection import StratifiedKFold
data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",")
X = data[:, 0:8]
y = data[:, 8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
def model_train(X_train, y_train, X_test, y_test):
# create new model
model = nn.Sequential(
nn.Linear(8, 12),
nn.ReLU(),
nn.Linear(12, 8),
nn.ReLU(),
nn.Linear(8, 1),
nn.Sigmoid()
)
# loss function and optimizer
loss_fn = nn.BCELoss() # binary cross entropy
optimizer = optim.Adam(model.parameters(), lr=0.0001)
n_epochs = 25 # number of epochs to run
batch_size = 10 # size of each batch
batches_per_epoch = len(X_train) // batch_size
for epoch in range(n_epochs):
with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0, disable=True) as bar:
bar.set_description(f"Epoch {epoch}")
for i in bar:
# take a batch
start = i * batch_size
X_batch = X_train[start:start+batch_size]
y_batch = y_train[start:start+batch_size]
# forward pass
y_pred = model(X_batch)
loss = loss_fn(y_pred, y_batch)
# backward pass
optimizer.zero_grad()
loss.backward()
# update weights
optimizer.step()
# print progress
acc = (y_pred.round() == y_batch).float().mean()
bar.set_postfix(
loss=float(loss),
acc=float(acc)
)
# evaluate accuracy at end of training
y_pred = model(X_test)
acc = (y_pred.round() == y_test).float().mean()
return float(acc)
# define 5-fold cross validation test harness
kfold = StratifiedKFold(n_splits=5, shuffle=True)
cv_scores = []
for train, test in kfold.split(X, y):
# create model, train, and get accuracy
acc = model_train(X[train], y[train], X[test], y[test])
print("Accuracy: %.2f" % acc)
cv_scores.append(acc)
# evaluate the model
print("%.2f%% (+/- %.2f%%)" % (np.mean(cv_scores)*100, np.std(cv_scores)*100))
Riepilogo
In questo post hai scoperto l'importanza di disporre di un metodo affidabile per stimare le prestazioni dei tuoi modelli di deep learning su dati invisibili e hai imparato come farlo. Hai visto:
- Come suddividere i dati in set di training e test utilizzando scikit-learn
- Come eseguire la convalida incrociata k-fold con l'aiuto di scikit-learn
- Come modificare il ciclo di training in un modello PyTorch per incorporare la convalida del set di test e la convalida incrociata