... implementare una regressione logistica con PyTorch

Regressione logistica con Pytorch

In questo articolo voglio impementare un esempio di classificazione per mezzo di una regressione logistica, usando il pacchetto Pytorch per Python.

La spiegazione di cosa è una regressione logistica la lascio alla pagina di Wikipedia – Regressione Logistica, qui solo un estratto:

In statistica e in econometria, il modello logit, noto anche come modello logistico o regressione logistica, è un modello di regressione nonlineare utilizzato quando la variabile dipendente è di tipo dicotomico. L’obiettivo del modello è di stabilire la probabilità con cui un’osservazione può generare uno o l’altro valore della variabile dipendente; può inoltre essere utilizzato per classificare le osservazioni, in base alla caratteristiche di queste, in due categorie.

In parole povere:

Dati una serie di variabili indipendenti (o di ingresso) voglio predire una variabile di uscita, che può essere 1 o 0 (dicotomica). La variabile di uscita è anche chiamata label.

Il dataset che uso in questo esempio è preso dal corso di Edx – Introduction to Artificial Intelligence (AI)

Il dataset è costituito dalle informazioni mediche di 15000 paziente. Ogni riga corrisponde ad un paziente. Per ogni paziente ci sono una serie di dati medici, quali età, numero di gravidanze, glucosio, pressione, ed altro.
La label, quindi il dato che intendo predire con il mio modello, è costituito dall’essere il paziente diabetico o meno.

In [1]:
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import LabelBinarizer

Load dataset

Carico i dati dal foglio Excel

In [2]:
exc1 = pd.read_csv('diabetes.csv')
exc2 = pd.read_csv('doctors.csv', encoding = "ISO-8859-1")

Vediamo cosa contengono i due dataframe appena creati:

In [3]:
exc1.head()
Out[3]:
PatientID Pregnancies PlasmaGlucose DiastolicBloodPressure TricepsThickness SerumInsulin BMI DiabetesPedigree Age Diabetic
0 1354778 0 171 80 34 23 43.509726 1.213191 21 0
1 1147438 8 92 93 47 36 21.240576 0.158365 23 0
2 1640031 7 115 47 52 35 41.511523 0.079019 23 0
3 1883350 9 103 78 25 304 29.582192 1.282870 43 1
4 1424119 1 85 59 27 35 42.604536 0.549542 22 0
In [4]:
exc2.head()
Out[4]:
PatientID Physician
0 1000038 Jeanne Vestergaard
1 1000069 Sheldon Comeaux
2 1000118 Brain Dulaney
3 1000183 Alaine Poisson
4 1000326 Erik Collado

Preprocessing dei dati

I due dataframe exc1 e exc2 contengono tutti e due un PatientID, per cui devo concatenare i due dataframe orizzontalmente in base al PatientID.

Piccola parentesi su come fare il merge dei due dataframe

In [5]:
df1 = pd.DataFrame([['1','antonio'],['2','giuseppe']], columns=['numID','nome'])
In [6]:
df2 = pd.DataFrame([['1','verro'],['2','rossi'],['3','Bianchi']], columns=['numID','cognome'])
In [7]:
pd.merge(df1, df2, on='numID', how='outer')
Out[7]:
numID nome cognome
0 1 antonio verro
1 2 giuseppe rossi
2 3 NaN Bianchi

Quindi la funzione merge() ha bisogno di due argomenti per fare il merge correttamente:

  • on=’chiave per il merge’
  • how=’outer’, viene usato un OR di tutte le chiavi, anche se uno dei due dataframe non ha il recordcon la chiave scelta
In [8]:
lista_pazienti = pd.merge(exc1, exc2, on='PatientID', how='outer')
lista_pazienti.head()
Out[8]:
PatientID Pregnancies PlasmaGlucose DiastolicBloodPressure TricepsThickness SerumInsulin BMI DiabetesPedigree Age Diabetic Physician
0 1354778 0 171 80 34 23 43.509726 1.213191 21 0 Dan Drayton
1 1147438 8 92 93 47 36 21.240576 0.158365 23 0 Anthony Frizzell
2 1640031 7 115 47 52 35 41.511523 0.079019 23 0 Gordon Fredrickson
3 1883350 9 103 78 25 304 29.582192 1.282870 43 1 Chad Corbitt
4 1424119 1 85 59 27 35 42.604536 0.549542 22 0 Zachary Fellows

Vediamo un riassunto delle proprietà statistiche del dataframe.

In [9]:
lista_pazienti.describe()
Out[9]:
PatientID Pregnancies PlasmaGlucose DiastolicBloodPressure TricepsThickness SerumInsulin BMI DiabetesPedigree Age Diabetic
count 1.500000e+04 15000.000000 15000.000000 15000.000000 15000.000000 15000.000000 15000.000000 15000.000000 15000.000000 15000.000000
mean 1.502922e+06 3.224533 107.856867 71.220667 28.814000 137.852133 31.509646 0.398968 30.137733 0.333333
std 2.892534e+05 3.391020 31.981975 16.758716 14.555716 133.068252 9.759000 0.377944 12.089703 0.471420
min 1.000038e+06 0.000000 44.000000 24.000000 7.000000 14.000000 18.200512 0.078044 21.000000 0.000000
25% 1.252866e+06 0.000000 84.000000 58.000000 15.000000 39.000000 21.259887 0.137743 22.000000 0.000000
50% 1.505508e+06 2.000000 104.000000 72.000000 31.000000 83.000000 31.767940 0.200297 24.000000 0.000000
75% 1.755205e+06 6.000000 129.000000 85.000000 41.000000 195.000000 39.259692 0.616285 35.000000 1.000000
max 1.999997e+06 14.000000 192.000000 117.000000 93.000000 799.000000 56.034628 2.301594 77.000000 1.000000

Salta subito all’occhio che la funzione ha fatto la statistica anche di PatientID, che è un codice numerico rappresentante il paziente pertanto deve essere considerato come una stringa piuttosto che un numero.
Cambio il tipo dei dati con la funzione astype()

In [10]:
lista_pazienti['PatientID'] = lista_pazienti['PatientID'].astype(str)

Vediamo quali sono le colonne aventi dati numerici, usiamo la funzione select_dtypes(). Verifichiamo che PatientID non è tra questi.

In [11]:
lista_pazienti.select_dtypes(include='number').head()
Out[11]:
Pregnancies PlasmaGlucose DiastolicBloodPressure TricepsThickness SerumInsulin BMI DiabetesPedigree Age Diabetic
0 0 171 80 34 23 43.509726 1.213191 21 0
1 8 92 93 47 36 21.240576 0.158365 23 0
2 7 115 47 52 35 41.511523 0.079019 23 0
3 9 103 78 25 304 29.582192 1.282870 43 1
4 1 85 59 27 35 42.604536 0.549542 22 0

Ci sono elementi non definiti (nan) nelle righe? Possiamo usare la funzione count.

In [12]:
lista_pazienti.count()
Out[12]:
PatientID                 15000
Pregnancies               15000
PlasmaGlucose             15000
DiastolicBloodPressure    15000
TricepsThickness          15000
SerumInsulin              15000
BMI                       15000
DiabetesPedigree          15000
Age                       15000
Diabetic                  15000
Physician                 15000
dtype: int64

Il dataframe ha 15000 righe ed ogni campo(o colonna) ha 15000 elementi. Quindi apparentemente non ci sono nan. Dico apparentemente perché potrebbero essere stati definiti con una codifica particolare, per esempio usando un numero molto negativo (-999) per l’indice di massa corporea (BMI), quando questo può essee un valore solo positivo.
Per riconoscere questi casi particolari dovremmo esplorare visualmente il dataframe e in caso scrivere un codice ad hoc.

Vediamo la distribuzione dei valori per ogni colonna. Per questo userò la funzione distplot() del pacchetto seaborn.

In [13]:
sns.distplot(lista_pazienti.Pregnancies)
plt.show()
In [14]:
sns.distplot(lista_pazienti.PlasmaGlucose)
plt.show()
In [15]:
sns.distplot(lista_pazienti.DiastolicBloodPressure)
plt.show()
In [16]:
sns.distplot(lista_pazienti.TricepsThickness)
plt.show()
In [17]:
sns.distplot(lista_pazienti.SerumInsulin)
plt.show()
In [18]:
sns.distplot(lista_pazienti.BMI)
plt.show()
In [19]:
sns.distplot(lista_pazienti.DiabetesPedigree)
plt.show()
In [20]:
sns.distplot(lista_pazienti.Age)
plt.show()

Poiché il campo dell’età (Age) è piuttosto sbilanciato, il tutorial di Edx suggeriva di usare log(Age) al suo posto.
Aggiungiamo una colonna con log(Age):

In [21]:
lista_pazienti['logAge'] = np.log10(lista_pazienti['Age'].values)
In [22]:
sns.distplot(lista_pazienti.logAge)
plt.show()

Adesso voglio normalizzare i dati perché la convergenza è più facile quando i dati sono normalizzati.

La normalizzazione avviene per colonna. Per le colonne che hanno una distribuzione simile a quella gaussiana (o normale), posso normalizzare usando lo Z-score, ovvero si calcola la media ($\mu$) e la varianza ($\sigma^2$) dei dati in una colonna e poi si normalizza ogni singolo elemento della colonna secondo:
\begin{equation*}
\hat{x_i} = \frac{(x_i – \mu)}{\sigma^2} \quad \forall i
\end{equation*}

Per le colonne che non hanno una distribuzione simil-gaussiana posso usare una normalizzazione max-min. Ovvero calcolo il max dei dai su una colonna ($M$) e il minimo ($m$) degli stessi. Dopodichè applico la normalizzazione:
\begin{equation*}
\hat{x_i} = \frac{(x_i – m)}{(M-m)} \quad \forall{i}
\end{equation*}

Per fortuna non dobbiamo farlo manualmente, ma possiamo usare le funzioni di scikit-learn StandardScaler() e MinMaxScaler().

In [23]:
from sklearn.preprocessing import StandardScaler, MinMaxScaler

Prima di lavorare sul mio dataset faccio un esempio di uso di MinMaxScaler():

In [24]:
Mmsc = MinMaxScaler()
preg_norm = Mmsc.fit_transform(lista_pazienti.Pregnancies.values.astype(np.float64).reshape(-1,1))
print(preg_norm.max())
print(preg_norm.min())
1.0
0.0

In questo caso ho usato la funzione fit_transform()

Definisco su quali colonne usare l’algoritmo MinMax e su quali il Zscore:

In [25]:
columns_minmax = ['Pregnancies','SerumInsulin','BMI','DiabetesPedigree','logAge']
columns_zscore = ['PlasmaGlucose','DiastolicBloodPressure','TricepsThickness']
In [26]:
lista_pazienti_norm = pd.DataFrame(index=range(15000))
for column in columns_minmax:
    lista_pazienti_norm[column + '_norm'] = Mmsc.fit_transform(lista_pazienti[column].values.astype(np.float64).reshape(-1, 1))
for column in columns_zscore:
    lista_pazienti_norm[column + '_norm'] = Mmsc.fit_transform(lista_pazienti[column].values.astype(np.float64).reshape(-1, 1))

lista_pazienti_norm[lista_pazienti.select_dtypes(exclude='number').columns] = lista_pazienti.select_dtypes(exclude='number')   #colonne non mumeriche

lista_pazienti_norm['Diabetic'] = lista_pazienti['Diabetic']   #È la label del modello
In [27]:
lista_pazienti_norm.head()
Out[27]:
Pregnancies_norm SerumInsulin_norm BMI_norm DiabetesPedigree_norm logAge_norm PlasmaGlucose_norm DiastolicBloodPressure_norm TricepsThickness_norm PatientID Physician Diabetic
0 0.000000 0.011465 0.668952 0.510511 0.000000 0.858108 0.602151 0.313953 1354778 Dan Drayton 0
1 0.571429 0.028025 0.080352 0.036123 0.070017 0.324324 0.741935 0.465116 1147438 Anthony Frizzell 0
2 0.500000 0.026752 0.616137 0.000438 0.070017 0.479730 0.247312 0.523256 1640031 Gordon Fredrickson 0
3 0.642857 0.369427 0.300831 0.541848 0.551595 0.398649 0.580645 0.209302 1883350 Chad Corbitt 1
4 0.071429 0.026752 0.645027 0.212047 0.035804 0.277027 0.376344 0.232558 1424119 Zachary Fellows 0

Un’ultima considerazione. Il valore di PatientID probabilmente non influisce sul risultato. Cioè non dovrebbe esserci alcuna correlazione tr il PatientID e la label (Diabetic).

Invece potrebbe esserci correlazione tra il medico (Physician) e il fatto che il paziente sia diabetico. Un cattivo medico aumenta la probabilità che il paziente si ammali. Per cui decido di usare l’informazione sul nome del medico.

Come primo passo voglio vedere quanti sono i medici nella lista. Utilizzo la funzione unique() per vedere quali sono i valori unici della colonna Physician. E shape per contarli.

In [28]:
lista_pazienti_norm.Physician.unique().shape
Out[28]:
(109,)

In tutto ho 109 medici. 109 medici su 15000 dati… può essere che ci sia una correlazione.

La regressione logistica è un algoritmo puramente matematico, che fa uso di numeri per determinare una uscita. Se voglio usare la colonna Physician, devo trasformare i valori in una informazione numerica che possa essere usata dall’algoritmo.

Per fare questo uso il one-hot-encoding. Scikit-learn ha una funzione apposta per realizzare l’encoding, si chiama LabelBinarizer()

In [29]:
lb = LabelBinarizer()
Phis_ohe = lb.fit_transform(lista_pazienti_norm.Physician)
Phis_ohe.shape
Out[29]:
(15000, 109)

L’output di LabelBinarizer() è un array bidimensionale. Per comodità voglio mettere tutti i dati in un dataframe, per cui devo dare un nome a ciscuna delle colonne di Phis_ohe.

In [30]:
columns_name = []
for i in range(109):
    columns_name += ['phis_ohe' + str(i)]

Creo il dataframe con i dati Phis_ohe e i nomi delle colonne columns_name

In [31]:
lista_dottori = pd.DataFrame(Phis_ohe, columns=columns_name)

Concateno la lista dei pazienti e quella dei dottori, poichè l’ordine delle righe non è cambiato, posso semplicemente accostarle una con l’altra (axis=1):

In [32]:
lista_pazienti_e_dottori = pd.concat([lista_pazienti_norm, lista_dottori], axis=1)

Dal dataframe devo rimuovere alcune colonne

In [33]:
#rimuovo la colonna Physician che è stata rimpiazzata dalla stessa informazione codificata
#rimuovo la colonna PatientID perché non è informativa
#rimuovo la colonna Diabetic perché costituisce il dato da predire
lista_pazienti_e_dottori = lista_pazienti_e_dottori.drop(columns=['Physician','PatientID','Diabetic'])
In [34]:
lista_pazienti_e_dottori.head()
Out[34]:
Pregnancies_norm SerumInsulin_norm BMI_norm DiabetesPedigree_norm logAge_norm PlasmaGlucose_norm DiastolicBloodPressure_norm TricepsThickness_norm phis_ohe0 phis_ohe1 phis_ohe99 phis_ohe100 phis_ohe101 phis_ohe102 phis_ohe103 phis_ohe104 phis_ohe105 phis_ohe106 phis_ohe107 phis_ohe108
0 0.000000 0.011465 0.668952 0.510511 0.000000 0.858108 0.602151 0.313953 0 0 0 0 0 0 0 0 0 0 0 0
1 0.571429 0.028025 0.080352 0.036123 0.070017 0.324324 0.741935 0.465116 0 0 0 0 0 0 0 0 0 0 0 0
2 0.500000 0.026752 0.616137 0.000438 0.070017 0.479730 0.247312 0.523256 0 0 0 0 0 0 0 0 0 0 0 0
3 0.642857 0.369427 0.300831 0.541848 0.551595 0.398649 0.580645 0.209302 0 0 0 0 0 0 0 0 0 0 0 0
4 0.071429 0.026752 0.645027 0.212047 0.035804 0.277027 0.376344 0.232558 0 0 0 0 0 0 0 0 0 0 1 0

5 rows × 117 columns

In [35]:
lista_pazienti_e_dottori.shape
Out[35]:
(15000, 117)

In realtà, rimane aperta una questione. Come trattare i pazienti che hanno più di una riga? Probabilmente si tratta di pazienti che hanno registrato dati in momenti diversi, in due viste diverse. Al momento decido di non riservare a questi pazienti un particolare trattamento, cioè li tratto come se fossero due pazienti diversi con dati diversi. In effetti dando un’occhiata veloce, lo stesso paziente può avere dati molto diversi tra loro.
Questi sono i pazienti duplicati:

In [36]:
lista_pazienti.loc[lista_pazienti.PatientID.duplicated(),:].head()
Out[36]:
PatientID Pregnancies PlasmaGlucose DiastolicBloodPressure TricepsThickness SerumInsulin BMI DiabetesPedigree Age Diabetic Physician logAge
72 1912150 8 133 59 12 477 42.477097 0.858930 21 1 Noreen Branch 1.322219
205 1854671 1 53 78 44 29 18.413402 0.127919 23 0 Demi Vadeboncoeur 1.361728
377 1192134 0 92 50 31 39 38.658578 0.664133 23 0 Carolos Lamy 1.361728
417 1366655 5 106 67 44 77 29.835662 0.177607 22 1 Eleanor Bryan 1.342423
458 1455760 7 121 53 35 312 19.824888 0.114730 21 0 Rocco Yarborough 1.322219

Partizionare il dataset

Divido i dati di ingresso in un set per il training e un set per il test.
Per farlo utilizzo la funzione train_test_split() di scikit-learn

In [37]:
from sklearn.model_selection import train_test_split

input_data = lista_pazienti_e_dottori.values.astype(np.float32) #le operazioni con i tensori richiedono il tipo float
label_data = lista_pazienti.Diabetic.values.astype(np.float32)

input_data_train, input_data_test, label_train, label_test = train_test_split(input_data, label_data, test_size=0.3)

Converto i numpy array in tensori:

In [38]:
input_data_train_tens = torch.from_numpy(input_data_train)
input_data_test_tens = torch.from_numpy(input_data_test)
label_train_tens = torch.from_numpy(label_train).view(-1,1)
label_test_tens = torch.from_numpy(label_test).view(-1,1)

Si noti che avrei anche potuto convertire in float in questo punto, con:

label_train_tens = torch.from_numpy(label_train).type(dtype=torch.float).view(-1,1)
label_test_tens = torch.from_numpy(label_test).type(dtype=torch.float).view(-1,1)

Definizione del modello

Definiamo un modello costituito dalla sequenza di un layer lineare e dalla funzione Sigmoid().
Questo realizza il semplice modello della regressione logistica, definito come segue:
\begin{equation}
h(\bar x) = \sigma (\bar w \cdot \bar x+b)
\end{equation}
dove:

  • $\bar x$ è l’ingresso pluridimensionale Nx1
  • $\bar{w}$ è il vettore 1xN contenente il peso di ciascun ingresso
  • $b$ è uno scalare
  • $\sigma$ è la funzione Sigmoid(), che è una funzione da $\mathbb{R}$ in $\Bbb{R}$ definita come segue:

\begin{equation}
\sigma(z) = \frac{1}{1 + e^{-z}}
\end{equation}

  • $h(\bar x)$ è l’uscita del modello, ed esprime la probabilità che che l’uscita sia 1

Per implementare questo modello utilizziamo un modello Sequential(), composto da un layer lineare e un layer che implementa la funzione sigmoid.
Il modello avrà 117 ingressi (tante sono le variabili di ingresso di lista_pazienti_e_dottori) e 1 uscita:

In [39]:
model_logreg = nn.Sequential(
    nn.Linear(117,1), 
    nn.Sigmoid()
)
In [40]:
list(model_logreg.parameters())
Out[40]:
[Parameter containing:
 tensor([[ 0.0609, -0.0227,  0.0209, -0.0228,  0.0567,  0.0363,  0.0910,  0.0672,
          -0.0488, -0.0206, -0.0035, -0.0726, -0.0577, -0.0013,  0.0887,  0.0720,
          -0.0279, -0.0205, -0.0741, -0.0718,  0.0855, -0.0499, -0.0109,  0.0178,
           0.0264,  0.0753,  0.0537, -0.0328, -0.0460, -0.0634, -0.0641,  0.0761,
          -0.0501, -0.0784, -0.0082,  0.0559,  0.0466,  0.0005, -0.0414, -0.0019,
           0.0325,  0.0708, -0.0266, -0.0812,  0.0119, -0.0372, -0.0422,  0.0649,
           0.0028, -0.0367, -0.0545, -0.0341,  0.0650, -0.0345, -0.0905, -0.0804,
          -0.0542, -0.0861,  0.0616,  0.0859,  0.0780, -0.0573,  0.0135, -0.0007,
           0.0327,  0.0412,  0.0179, -0.0397, -0.0749,  0.0221, -0.0516,  0.0275,
           0.0825, -0.0694,  0.0656, -0.0064, -0.0496, -0.0532, -0.0735,  0.0903,
          -0.0594,  0.0762, -0.0465, -0.0795,  0.0869,  0.0790, -0.0758, -0.0512,
          -0.0063,  0.0429, -0.0818, -0.0323, -0.0544,  0.0783,  0.0325, -0.0595,
          -0.0231, -0.0921, -0.0039,  0.0530, -0.0526, -0.0644, -0.0356,  0.0633,
          -0.0283,  0.0347, -0.0355, -0.0137,  0.0469, -0.0690,  0.0168,  0.0632,
          -0.0129,  0.0579, -0.0813,  0.0172,  0.0497]], requires_grad=True),
 Parameter containing:
 tensor([-0.0875], requires_grad=True)]

Verifichiamo che funzioni, generando una uscita scalare:

In [41]:
output_data_train = model_logreg( input_data_train_tens )
output_data_train.shape
Out[41]:
torch.Size([10500, 1])

Definiamo come funzione errore la funzione Binary Cross Entropy BCELoss(), questa è cosi definita:

\begin{equation}
error(x) = \sum_{k=1}^n \left[ -y \cdot log(h(x)) – (1-y) \cdot log(1-h(x)) \right]
\end{equation}

dove:
$y$ è la label, cioè il valore vero
$h(x)$ è l’uscita del modello di regressione, compreso tra 0 e 1, esprime la probabilità che l’uscita sia 1.

In [42]:
error = nn.BCELoss()

Definisco un ottimizzatore, precisamente lo stochastic gradient descent SGD, con argomenti i parametri del modello di regressione logistico e il learning rate:

In [43]:
learning_rate = 1
optimizer = torch.optim.SGD(model_logreg.parameters(), lr = learning_rate)

In loop eseguiamo i seguenti step:

  • calcolo dell’uscita con i parametri correnti
  • calcolo dell’errore
  • calcolo del gradiente
  • aggiornamento dei parametri
  • azzeramneto del gradiente
In [44]:
for i in range(1000):
    output_data_train = model_logreg( input_data_train_tens ) #calcolo l'uscita
    loss = error(output_data_train, label_train_tens)         #calcolo l'errore
    loss.backward()        #calcolo del gradiente
    optimizer.step()       #aggiornamento dei parametri
    optimizer.zero_grad()  #azzeramento del gradiente
    if np.mod(i,100)==0:
        print(loss)
tensor(0.6882, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.4838, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.4561, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.4458, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.4408, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.4380, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.4364, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.4354, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.4347, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.4343, grad_fn=<BinaryCrossEntropyBackward>)
In [45]:
output_data_test = model_logreg( torch.from_numpy(input_data_test) )

loss = error(output_data_test, label_test_tens)
loss
Out[45]:
tensor(0.4416, grad_fn=<BinaryCrossEntropyBackward>)

Una metrica per definire la bontà del modello è la curva ROC.
Un’occhiata alla pagina di Wikipedia vi chiarirà tutto sulla curva ROC.
Facciamo il grafico della curva ROC, usando la funzione roc_curve() di scikit-learn. Il codice sotto è un adattamento dell’esempio sulla pagina di scikit-learn Receiver Operating Characteristic (ROC).

In [46]:
from sklearn.metrics import roc_curve
In [47]:
fpr_train, tpr_train, _ = roc_curve(label_train_tens,output_data_train.detach())
fpr_test, tpr_test, _   = roc_curve(label_test_tens ,output_data_test.detach())
In [48]:
fgr1 = plt.figure()
lw = 2
plt.plot(fpr_test, tpr_test, color='darkorange', label="test", lw=lw)
plt.plot(fpr_train, tpr_train, color='darkgreen', label="train", lw=lw)
plt.plot([0, 1], [0, 1], color='navy', lw=lw, linestyle='--')
plt.xlim([0.0, 1])
plt.ylim([0.0, 1.05])
plt.xticks(np.arange(0, 1, 0.1))
plt.yticks(np.arange(0, 1, 0.1))
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver operating characteristic (ROC curve)')
plt.legend(loc="lower right")
plt.grid(which='both')
plt.show()

Una metrica più semplice è la confusion matrix. Che può essere spiegata con il grafico sotto.
Come si vede ho due assi, valore vero e valore predetto.
Ogni esempio del dataset può essere inserito in una delle quattro classi:

  • true positive (tp): il valore vero è 1 e il valore predetto è 1
  • false positive (fp): il valore vero è 0 ma il valore predetto è 1
  • true negative (tn): il valore vero è 0 e il valore predetto è 0
  • false negative (fn): il valore vero è 1 ma il valore predetto è 0

Come si intuisce due delle classi sono correttamente classificate (true positive e true negative), mentre le altre due sono classificate erroneamente.

title

Scikit-learn ci mette a disposizione la funzione confusion_matrix() per fare questo calcolo:

In [49]:
from sklearn.metrics import confusion_matrix
In [50]:
output_data_train = model_logreg( input_data_train_tens )
output_data_train = (output_data_train.detach()>0.5).numpy()
In [51]:
tn, fp, fn, tp = confusion_matrix(label_train, output_data_train ).ravel()
tn, fp, fn, tp
Out[51]:
(6152, 811, 1372, 2165)

Con questi valori posso costruire altre metriche:

$
Accuratezza: \quad A = \frac{\text{nr. di esempi correttamente classificati}}{\text{nr. totale di esempi}} = \frac{tp+tn}{tp+tn+fn+fp}
$

$
Precisione: \quad P = \frac{\text{nr. di true positive}}{\text{nr. di predizioni positive}} = \frac{tp}{tp+fp}
$

$
Recall: \quad R = \frac{\text{nr. di true positive}}{\text{nr. veri positivi}} = \frac{tp}{tp+fn}
$

$
F_1 score: \quad F_1 = 2 \frac{P\cdot R}{P+R}
$

In [52]:
A = (tp+tn)/(tp+tn+fn+fp)
P = tp/(tp+fp)
R = tp/(tp+fn)
F1 = 2*(P*R)/(P+R)
print('L\'accuratezza è %0.1f%%' %(A*100))
print('La precisione è %0.1f%%' %(P*100))
print('Il recall è %0.1f%%' %(R*100))
print('Il punteggio F1 è %0.1f%%' %(F1*100))
L'accuratezza è 79.2%
La precisione è 72.7%
Il recall è 61.2%
Il punteggio F1 è 66.5%

Un punteggio F1 non molto al di sopra del 50% (probabilità di indovinare il lato di una moneta bilanciata) non è un gran punteggio. Vediamo se riesco a tirarne fuori qualcosa di più.

Ricordiamo come funziona la regressione logistica. Per semplificare consideriamo il caso di due ingressi $x1$ e $x2$.
Gli ingressi sono combinati linearnemente con i pesi $w1$ e $w2$, un fattore bias $b$ viene sommato: $z=w_1 \cdot x_1 + w_2 \cdot x_2 + b$
La somma viene applicata alla funzione sigmoid $\sigma(z)$, la cui uscita è compresa tra 0 e 1.
Sul risultato della funzione sigmoid viene fatta la decisione:
\begin{equation}
\sigma(z) > 0.5 \implies y=1
\end{equation}
\begin{equation}
\sigma(z) \le 0.5 \implies y=0
\end{equation}

In [53]:
x = np.arange(-10,10,0.1)
x_tens = torch.from_numpy(x)
y = torch.sigmoid( x_tens )

plt.plot( x,y.numpy() )
plt.xticks(np.arange(-10, 10, 2.5))
plt.yticks(np.arange(0, 1, 0.1))
plt.grid(which='both')
plt.xlabel('z')
plt.ylabel('sigma(z)')
plt.title('funzione Sigmoid')
plt.show()

Come si vede dal grafico della funzione Sigmoid sopra, $\sigma(z)$ è maggiore di 0.5 se z > 0. Ovvero se:

\begin{equation}
w_1 \cdot x_1 + w_2 \cdot x_2 + b > 0
\end{equation}

La disequazione sopra definisce una regione dello spazio bidimensionale delimitata dalla retta:
$x_2 = -\frac{w_1}{w_2} x_1 – \frac{b}{w_2}$

Questa divisione dello spazio può andare bene in alcuni casi casi, per esempio nella situazione sotto la retta riesce a classificare abbastanza bene tra punti verdi e punti rossi.

Invece nel caso rappresentato sotto, una retta non darebbe buoni risultati nella classificazione tra punti rossi e verdi. Serve invece qualcosa come una circonferenza. Ovvero qualcosa del tipo:
\begin{equation}
(x_1-x_{1,0})^2 + (x_2-x_{2,0})^2 = R^2
\end{equation}

Quindi occorre che nel modello di regressione compaiano anche i termini al quadrato degli ingressi $x_1$ e $x_2$.
Questa tecnica si chiama features mapping, e consiste nel definire (mappare) come nuove features di ingresso le potenze degli ingressi originali e tutti i possibili prodotti. Per esempio, nel caso sotto potrei considerare come features di ingresso i seguenti:

\begin{equation}
input =
\begin{bmatrix}
x_1\cr
x_2\cr
x_1\cdot x_2\cr
x_1^2\cr
x_2^2
\end{bmatrix}
\end{equation}

Una volta introdotti i termini quadratici tra gli ingressi, l’ottimizzatore riuscirà (speriamo) a trovare la giusta combinazione di questi per ottenere il contorno di decisione di forma circolare come nella figura sottostante.

La cosa che potrebbe non essere immediatamente chiara adesso è: cosa vuol dire aggiungere i termini non lineari agli ingressi?
Basta guardare la figura sotto per rendersene conto. Devo aggiungere al np.array() originario le colonne con i termini non lineari.

title

Ovviamente una volta appresa la tecnica del features mapping possiamo sbizzarrirci ad aggiungere potenze di qualsiasi ordine. In questo modo il contorno che definisce la soglia di decisione prenderà le forme più disparate (attenzione all’overfitting!). Facciamo ancora un esempio con le potenze al cubo.
In questo caso gli ingressi saranno:
\begin{equation}
input =
\begin{bmatrix}
x_1\cr
x_2\cr
x_1\cdot x_2\cr
x_1^2\cr
x_2^2\cr
x_1^2\cdot x_2\cr
x_1\cdot x_2^2\cr
x_1^3\cr
x_2^3
\end{bmatrix}
\end{equation}

Per generare i termini non lineari di un numpy array x (ogni colonna una feature diversa) uso il codice sotto. La programmazione di questo mi ha dato abbastanza mal di testa, usatelo con cautela.

In [56]:
def generateNonLinearFeatures(x,max_exp):
#given a marix with features in column, generate a new matrix with all the polynomial combination up to max_exp

    import itertools

    terms = []
    num_feat = x.shape[1]
    
    #il prossimo comando genera una lista delle possibili combinazioni delle variabili fino al max_exp
    #Esempio: num_Feat=2 max_exp=2 genera:
    #[(0, 0),  (0, 1), (0, 2), (1, 1), (1, 2), (2, 2)] corrispondenti a:
    # x^0 x^0  x^0 x^1 x^0 x^2 x^1 x^1 x^1 x^2 x^2 x^2
    terms = list(itertools.combinations_with_replacement(list(range(num_feat+1)), max_exp))
    
    if 'pol' in locals(): #rimuove la variabile pol se esiste già
        del pol

    for i in range(len(terms)): 
        polcol = np.ones(x.shape[0]).reshape(x.shape[0],1)
        sum=0
        for j in range(max_exp):
#             print(i,j) #for debug only
            sum=sum+int(terms[i][j])
            if terms[i][j]!=0:
                polcol = np.multiply(polcol,x[:,int(terms[i][j])-1].reshape(x.shape[0],1))
        if (sum!=0): #exclude case all zeros (0,0), don't add the column
            if 'pol' in locals():
                pol = np.column_stack([pol, polcol])
            else:
                pol = polcol

    return pol

Per generare le features non lineari decido di rinunciare alla codifica dei dottori perché sono 109 colonne che renderebbero il calcolo molto pesante. Potrei concatenare la lista alla fine senza termini non lineari. Per il momento la lascio fuori.

Riparto dal dataframe dei pazienti, normalizzato:

In [57]:
lista_pazienti_norm2 = lista_pazienti_norm.drop(columns=['Diabetic','Physician','PatientID'])
# lista_pazienti_norm2.values

Genero i termini non lineari fino al secondo ordine, si noti che la funzione generateNonLinearFeatures necessita come argomento un array (non un dataframe) per cui devo usare l’attributo .values

In [58]:
lista_pazient_norm_nl = generateNonLinearFeatures(lista_pazienti_norm2.values,2)

Ridefinisco la label e verifico che la lunghezza dei vettori sia corretta

In [59]:
label = lista_pazienti_norm.Diabetic.values
lista_pazient_norm_nl.shape, label.shape
Out[59]:
((15000, 44), (15000,))

Divido il dataset in test e train:

In [60]:
lista_pazienti_norm_nl_train, lista_pazienti_norm_nl_test, label_train, label_test = train_test_split(lista_pazient_norm_nl, label, test_size=0.3)

lista_pazienti_norm_nl_train.shape, lista_pazienti_norm_nl_test.shape, label_train.shape, label_test.shape
Out[60]:
((10500, 44), (4500, 44), (10500,), (4500,))

Converto gli array in tensori con elementi float:

In [61]:
input_data_train_tens = torch.from_numpy(lista_pazienti_norm_nl_train).type(dtype=torch.float)
input_data_test_tens = torch.from_numpy(lista_pazienti_norm_nl_test).type(dtype=torch.float)
label_train_tens = torch.from_numpy(label_train).type(dtype=torch.float).view(-1,1)
label_test_tens = torch.from_numpy(label_test).type(dtype=torch.float).view(-1,1)

Sono un po’ paranoico con le verifiche:

In [62]:
input_data_train_tens.shape, label_train_tens.shape
Out[62]:
(torch.Size([10500, 44]), torch.Size([10500, 1]))

Ridefinisco il modello sequenziale con il giusto numero di ingressi:

In [63]:
model_logreg = nn.Sequential(
    nn.Linear(input_data_train_tens.shape[1],1), 
    nn.Sigmoid()
)

definisco l’ottimizzatore

In [64]:
learning_rate = 0.1
optimizer = torch.optim.Adam(model_logreg.parameters(), lr = learning_rate)

e il loop di training del modello

In [65]:
for i in range(1000):
    output_data_train = model_logreg( input_data_train_tens ) #calcolo l'uscita
    loss = error(output_data_train, label_train_tens)         #calcolo l'errore
    loss.backward()        #calcolo del gradiente
    optimizer.step()       #aggiornamento dei parametri
    optimizer.zero_grad()  #azzeramento del gradiente
    if np.mod(i,100)==0:
        print(loss)
tensor(0.7127, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.4006, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.3835, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.3781, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.3759, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.3747, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.3739, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.3733, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.3727, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.3722, grad_fn=<BinaryCrossEntropyBackward>)

calcolo l’uscita per il dataset si test, per il dataset di train ce l’ho già dal loop precedente:

In [66]:
output_data_test = model_logreg( input_data_test_tens )

Calcolo e grafico le curve ROC

In [67]:
fpr_train, tpr_train, _ = roc_curve(label_train_tens,output_data_train.detach())
fpr_test, tpr_test, _   = roc_curve(label_test_tens ,output_data_test.detach())
In [68]:
fgr1 = plt.figure()
lw = 2
plt.plot(fpr_test, tpr_test, color='darkorange', label="test", lw=lw)
plt.plot(fpr_train, tpr_train, color='darkgreen', label="train", lw=lw)
plt.plot([0, 1], [0, 1], color='navy', lw=lw, linestyle='--')
plt.xlim([0.0, 1])
plt.ylim([0.0, 1.05])
plt.xticks(np.arange(0, 1, 0.1))
plt.yticks(np.arange(0, 1, 0.1))
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver operating characteristic (ROC curve)')
plt.legend(loc="lower right")
plt.grid(which='both')
plt.show()

Applico la funzione di decisione della classe:

In [69]:
# output_data_train = model_logreg( input_data_train_tens )
# output_data_train_bin = (output_data_train.detach()>0.5).numpy()
output_data_test_bin = (output_data_test.detach()>0.5).numpy()

calcolo la matrice di confusione:

In [70]:
tn, fp, fn, tp = confusion_matrix(label_test_tens, output_data_test_bin ).ravel()

e le quantità ad essa connesse:

In [71]:
A = (tp+tn)/(tp+tn+fn+fp)
P = tp/(tp+fp)
R = tp/(tp+fn)
F1 = 2*(P*R)/(P+R)
print('L\'accuratezza è %0.1f%%' %(A*100))
print('La precisione è %0.1f%%' %(P*100))
print('Il recall è %0.1f%%' %(R*100))
print('Il punteggio F1 è %0.1f%%' %(F1*100))
L'accuratezza è 82.6%
La precisione è 77.2%
Il recall è 68.9%
Il punteggio F1 è 72.8%

Quindi aggiungendo i termini quadratici il punteggio F1 è salito dal 66% al 73%.

Proviamo ad aggiungere i termini al cubo. Ovviamente una cosa intelligente sarebbe quella di raccogliere tutti gli step in una funzione. Cosa che non farò, affidandomi al più veloce CUT and PASTE di Jupyter.

In [72]:
lista_pazient_norm_nl = generateNonLinearFeatures(lista_pazienti_norm2.values,3)
label = lista_pazienti_norm.Diabetic.values
lista_pazienti_norm_nl_train, lista_pazienti_norm_nl_test, label_train, label_test = train_test_split(lista_pazient_norm_nl, label, test_size=0.3)

input_data_train_tens = torch.from_numpy(lista_pazienti_norm_nl_train).type(dtype=torch.float)
input_data_test_tens = torch.from_numpy(lista_pazienti_norm_nl_test).type(dtype=torch.float)
label_train_tens = torch.from_numpy(label_train).type(dtype=torch.float).view(-1,1)
label_test_tens = torch.from_numpy(label_test).type(dtype=torch.float).view(-1,1)

model_logreg = nn.Sequential(
    nn.Linear(input_data_train_tens.shape[1],1), 
    nn.Sigmoid()
)

learning_rate = 0.1
optimizer = torch.optim.Adam(model_logreg.parameters(), lr = learning_rate)

for i in range(1000):
    output_data_train = model_logreg( input_data_train_tens ) #calcolo l'uscita
    loss = error(output_data_train, label_train_tens)         #calcolo l'errore
    loss.backward()        #calcolo del gradiente
    optimizer.step()       #aggiornamento ei parametri
    optimizer.zero_grad()  #azzeramento del gradiente
    if np.mod(i,100)==0:
        print(loss)

output_data_test = model_logreg( input_data_test_tens )

fpr_train, tpr_train, _ = roc_curve(label_train_tens,output_data_train.detach())
fpr_test, tpr_test, _   = roc_curve(label_test_tens ,output_data_test.detach())

fgr1 = plt.figure()
lw = 2
plt.plot(fpr_test, tpr_test, color='darkorange', label="test", lw=lw)
plt.plot(fpr_train, tpr_train, color='darkgreen', label="train", lw=lw)
plt.plot([0, 1], [0, 1], color='navy', lw=lw, linestyle='--')
plt.xlim([0.0, 1])
plt.ylim([0.0, 1.05])
plt.xticks(np.arange(0, 1, 0.1))
plt.yticks(np.arange(0, 1, 0.1))
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver operating characteristic (ROC curve)')
plt.legend(loc="lower right")
plt.grid(which='both')
plt.show()

# output_data_train = model_logreg( input_data_train_tens )
# output_data_train_bin = (output_data_train.detach()>0.5).numpy()
output_data_test_bin = (output_data_test.detach()>0.5).numpy()

tn, fp, fn, tp = confusion_matrix(label_test_tens, output_data_test_bin ).ravel()

A = (tp+tn)/(tp+tn+fn+fp)
P = tp/(tp+fp)
R = tp/(tp+fn)
F1 = 2*(P*R)/(P+R)
print('L\'accuratezza è %0.1f%%' %(A*100))
print('La precisione è %0.1f%%' %(P*100))
print('Il recall è %0.1f%%' %(R*100))
print('Il punteggio F1 è %0.1f%%' %(F1*100))
tensor(0.7082, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.3818, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.3548, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.3331, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.3163, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.3033, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2930, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2846, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2776, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2716, grad_fn=<BinaryCrossEntropyBackward>)
L'accuratezza è 88.8%
La precisione è 84.8%
Il recall è 80.6%
Il punteggio F1 è 82.6%

Sembra che il gioco funzioni piuttosto bene, sono passato da un F1 di 73% ad uno di 83%. Vediamo fino a dove posso spingermi.

In [73]:
lista_pazient_norm_nl = generateNonLinearFeatures(lista_pazienti_norm2.values,4)
label = lista_pazienti_norm.Diabetic.values
lista_pazienti_norm_nl_train, lista_pazienti_norm_nl_test, label_train, label_test = train_test_split(lista_pazient_norm_nl, label, test_size=0.3)

input_data_train_tens = torch.from_numpy(lista_pazienti_norm_nl_train).type(dtype=torch.float)
input_data_test_tens = torch.from_numpy(lista_pazienti_norm_nl_test).type(dtype=torch.float)
label_train_tens = torch.from_numpy(label_train).type(dtype=torch.float).view(-1,1)
label_test_tens = torch.from_numpy(label_test).type(dtype=torch.float).view(-1,1)

model_logreg = nn.Sequential(
    nn.Linear(input_data_train_tens.shape[1],1), 
    nn.Sigmoid()
)

learning_rate = 0.1
optimizer = torch.optim.Adam(model_logreg.parameters(), lr = learning_rate)

for i in range(1000):
    output_data_train = model_logreg( input_data_train_tens ) #calcolo l'uscita
    loss = error(output_data_train, label_train_tens)         #calcolo l'errore
    loss.backward()        #calcolo del gradiente
    optimizer.step()       #aggiornamento ei parametri
    optimizer.zero_grad()  #azzeramento del gradiente
    if np.mod(i,100)==0:
        print(loss)

output_data_test = model_logreg( input_data_test_tens )

fpr_train, tpr_train, _ = roc_curve(label_train_tens,output_data_train.detach())
fpr_test, tpr_test, _   = roc_curve(label_test_tens ,output_data_test.detach())

fgr1 = plt.figure()
lw = 2
plt.plot(fpr_test, tpr_test, color='darkorange', label="test", lw=lw)
plt.plot(fpr_train, tpr_train, color='darkgreen', label="train", lw=lw)
plt.plot([0, 1], [0, 1], color='navy', lw=lw, linestyle='--')
plt.xlim([0.0, 1])
plt.ylim([0.0, 1.05])
plt.xticks(np.arange(0, 1, 0.1))
plt.yticks(np.arange(0, 1, 0.1))
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver operating characteristic (ROC curve)')
plt.legend(loc="lower right")
plt.grid(which='both')
plt.show()

# output_data_train = model_logreg( input_data_train_tens )
# output_data_train_bin = (output_data_train.detach()>0.5).numpy()
output_data_test_bin = (output_data_test.detach()>0.5).numpy()

tn, fp, fn, tp = confusion_matrix(label_test_tens, output_data_test_bin ).ravel()

A = (tp+tn)/(tp+tn+fn+fp)
P = tp/(tp+fp)
R = tp/(tp+fn)
F1 = 2*(P*R)/(P+R)
print('L\'accuratezza è %0.1f%%' %(A*100))
print('La precisione è %0.1f%%' %(P*100))
print('Il recall è %0.1f%%' %(R*100))
print('Il punteggio F1 è %0.1f%%' %(F1*100))
tensor(0.6963, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.3385, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2931, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2704, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2565, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2472, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2404, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2353, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2313, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2286, grad_fn=<BinaryCrossEntropyBackward>)
L'accuratezza è 89.9%
La precisione è 86.0%
Il recall è 82.9%
Il punteggio F1 è 84.4%

Da 83% a 84.5%, sono quasi al limite delle possibilità. Proviamo un altro step.

In [74]:
lista_pazient_norm_nl = generateNonLinearFeatures(lista_pazienti_norm2.values,5)
label = lista_pazienti_norm.Diabetic.values
lista_pazienti_norm_nl_train, lista_pazienti_norm_nl_test, label_train, label_test = train_test_split(lista_pazient_norm_nl, label, test_size=0.3)

input_data_train_tens = torch.from_numpy(lista_pazienti_norm_nl_train).type(dtype=torch.float)
input_data_test_tens = torch.from_numpy(lista_pazienti_norm_nl_test).type(dtype=torch.float)
label_train_tens = torch.from_numpy(label_train).type(dtype=torch.float).view(-1,1)
label_test_tens = torch.from_numpy(label_test).type(dtype=torch.float).view(-1,1)

model_logreg = nn.Sequential(
    nn.Linear(input_data_train_tens.shape[1],1), 
    nn.Sigmoid()
)

learning_rate = 0.1
optimizer = torch.optim.Adam(model_logreg.parameters(), lr = learning_rate)

for i in range(1000):
    output_data_train = model_logreg( input_data_train_tens ) #calcolo l'uscita
    loss = error(output_data_train, label_train_tens)         #calcolo l'errore
    loss.backward()        #calcolo del gradiente
    optimizer.step()       #aggiornamento ei parametri
    optimizer.zero_grad()  #azzeramento del gradiente
    if np.mod(i,100)==0:
        print(loss)

output_data_test = model_logreg( input_data_test_tens )

fpr_train, tpr_train, _ = roc_curve(label_train_tens,output_data_train.detach())
fpr_test, tpr_test, _   = roc_curve(label_test_tens ,output_data_test.detach())

fgr1 = plt.figure()
lw = 2
plt.plot(fpr_test, tpr_test, color='darkorange', label="test", lw=lw)
plt.plot(fpr_train, tpr_train, color='darkgreen', label="train", lw=lw)
plt.plot([0, 1], [0, 1], color='navy', lw=lw, linestyle='--')
plt.xlim([0.0, 1])
plt.ylim([0.0, 1.05])
plt.xticks(np.arange(0, 1, 0.1))
plt.yticks(np.arange(0, 1, 0.1))
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver operating characteristic (ROC curve)')
plt.legend(loc="lower right")
plt.grid(which='both')
plt.show()

# output_data_train = model_logreg( input_data_train_tens )
# output_data_train_bin = (output_data_train.detach()>0.5).numpy()
output_data_test_bin = (output_data_test.detach()>0.5).numpy()

tn, fp, fn, tp = confusion_matrix(label_test_tens, output_data_test_bin ).ravel()

A = (tp+tn)/(tp+tn+fn+fp)
P = tp/(tp+fp)
R = tp/(tp+fn)
F1 = 2*(P*R)/(P+R)
print('L\'accuratezza è %0.1f%%' %(A*100))
print('La precisione è %0.1f%%' %(P*100))
print('Il recall è %0.1f%%' %(R*100))
print('Il punteggio F1 è %0.1f%%' %(F1*100))
tensor(0.6859, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.3103, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2702, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2514, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2401, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2325, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2271, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2229, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2196, grad_fn=<BinaryCrossEntropyBackward>)
tensor(0.2169, grad_fn=<BinaryCrossEntropyBackward>)
L'accuratezza è 89.8%
La precisione è 86.2%
Il recall è 83.0%
Il punteggio F1 è 84.5%

Il valore di F1 è rimasto intorno al 84%, quindi nessun miglioramento nel passare dalla quarta potenza alla quinta. Inoltre c’è una certa distanza tra la curva riguardante i dati di train e quella di test. Questo indica che c’è un certo overfitting.

Leave a Reply

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>