Interpretando Random Forest

Decifrando Random Forest

Guida completa sugli algoritmi di Random Forest e come interpretarli

Foto di Sergei A su Unsplash

C’è molta eccitazione sui Large Language Models al giorno d’oggi, ma ciò non significa che gli approcci tradizionali di apprendimento automatico meritino l’estinzione. Dubito che ChatGPT possa essere utile se gli si fornisce un dataset con centinaia di caratteristiche numeriche e si chiede di prevedere un valore obiettivo.

Le reti neurali sono di solito la soluzione migliore per i dati non strutturati (ad esempio, testi, immagini o audio). Ma per i dati tabulari, possiamo ancora beneficiare della buona vecchia Random Forest.

I vantaggi più significativi degli algoritmi di Random Forest sono i seguenti:

  • Hai bisogno solo di fare un po’ di preprocessamento dei dati.
  • È piuttosto difficile fare errori con le Random Forest. Non affronterai problemi di sovradattamento se hai abbastanza alberi nel tuo insieme, poiché aggiungendo più alberi si riduce l’errore.
  • È facile interpretare i risultati.

Ecco perché Random Forest potrebbe essere un buon candidato per il tuo primo modello quando inizi un nuovo compito con dati tabulari.

In questo articolo, vorrei coprire le basi delle Random Forest e illustrare gli approcci per interpretare i risultati del modello.

Impareremo come trovare risposte alle seguenti domande:

  • Quali caratteristiche sono importanti e quali sono ridondanti e possono essere eliminate?
  • Come influisce il valore di ogni caratteristica sulla nostra metrica obiettivo?
  • Quali sono i fattori per ogni previsione?
  • Come stimare la fiducia di ogni previsione?

Preprocessamento

Utilizzeremo il dataset sulla qualità del vino. Mostra la relazione tra la qualità del vino e i test fisico-chimici per le diverse varianti di vino “Vinho Verde” portoghese. Cercheremo di prevedere la qualità del vino in base alle sue caratteristiche.

Con gli alberi decisionali, non è necessario fare molta preprocessamento:

  • Non è necessario creare variabili dummy poiché l’algoritmo può gestirle automaticamente.
  • Non è necessario normalizzare o eliminare i valori anomali perché conta solo l’ordine. Quindi, i modelli basati su alberi decisionali sono resistenti ai valori anomali.

Tuttavia, la realizzazione di scikit-learn degli alberi decisionali non può lavorare con variabili categoriche o valori Null. Quindi, dobbiamo gestirli noi stessi.

Fortunatamente, non ci sono valori mancanti nel nostro dataset.

df.isna().sum().sum()0

E abbiamo solo bisogno di trasformare la variabile type (‘red’ o ‘white’) da string a integer. Possiamo usare la trasformazione Categorical di pandas per farlo.

categories = {}  cat_columns = ['type']for p in cat_columns:    df[p] = pd.Categorical(df[p])        categories[p] = df[p].cat.categoriesdf[cat_columns] = df[cat_columns].apply(lambda x: x.cat.codes)print(categories){'type': Index(['red', 'white'], dtype='object')}

Ora, df['type'] è uguale a 0 per i vini rossi e 1 per i vini bianchi.

L’altra parte cruciale del preprocessamento è dividere il dataset in set di addestramento e di validazione. In questo modo, possiamo utilizzare un set di validazione per valutare la qualità del nostro modello.

import sklearn.model_selectiontrain_df, val_df = sklearn.model_selection.train_test_split(df,     test_size=0.2) train_X, train_y = train_df.drop(['quality'], axis = 1), train_df.qualityval_X, val_y = val_df.drop(['quality'], axis = 1), val_df.qualityprint(train_X.shape, val_X.shape)(5197, 12) (1300, 12)

Abbiamo terminato la fase di preprocessamento e siamo pronti per passare alla parte più interessante: addestrare i modelli.

I fondamenti degli Alberi di Decisione

Prima di entrare nella formazione, dedichiamo del tempo a comprendere come funzionano i Random Forest.

Random Forest è un insieme di Alberi di Decisione. Quindi, dovremmo iniziare con il blocco di costruzione elementare: l’Albero di Decisione.

Nel nostro esempio di previsione della qualità del vino, risolveremo un compito di regressione, quindi cominciamo con esso.

Albero di Decisione: Regressione

Adattiamo un modello di albero di decisione predefinito.

import sklearn.treeimport graphvizmodel = sklearn.tree.DecisionTreeRegressor(max_depth=3)# Ho limitato max_depth principalmente per scopi di visualizzazionemodel.fit(train_X, train_y)

Uno dei vantaggi più significativi degli Alberi di Decisione è che possiamo interpretare facilmente questi modelli: sono solo un insieme di domande. Visualizziamolo.

dot_data = sklearn.tree.export_graphviz(model, out_file=None,                                       feature_names = train_X.columns,                                       filled = True) graph = graphviz.Source(dot_data) # salviamo l'albero in un file pngpng_bytes = graph.pipe(format='png')with open('decision_tree.png','wb') as f:    f.write(png_bytes)
Grafico dell'autore

Come puoi vedere, l’Albero di Decisione è composto da divisioni binarie. Ad ogni nodo, dividiamo il nostro dataset in 2.

Infine, calcoliamo le previsioni per i nodi foglia come media di tutti i punti dati in questo nodo.

Nota: poiché l’Albero di Decisione restituisce una media di tutti i punti dati per un nodo foglia, gli Alberi di Decisione non sono molto buoni nell’extrapolazione. Pertanto, è necessario prestare attenzione alla distribuzione delle caratteristiche durante la formazione e l’inferenza.

Diciamo come identificare la migliore divisione per il nostro dataset. Possiamo iniziare con una variabile e definire la divisione ottimale per essa.

Supponiamo che abbiamo una caratteristica con quattro valori unici: 1, 2, 3 e 4. Quindi, ci sono tre possibili soglie tra di loro.

Grafico dell'autore

Possiamo quindi prendere ciascuna soglia e calcolare i valori previsti per i nostri dati come valore medio per i nodi foglia. Successivamente, possiamo utilizzare questi valori previsti per ottenere l’errore quadratico medio (MSE) per ciascuna soglia. La migliore divisione sarà quella con il MSE più basso. Per impostazione predefinita, il DecisionTreeRegressor di scikit-learn funziona in modo simile e utilizza l’MSE come criterio.

Calcoliamo la migliore divisione per la caratteristica sulphates manualmente per capire meglio come funziona.

def get_binary_split_for_param(param, X, y):    uniq_vals = list(sorted(X[param].unique()))        tmp_data = []        for i in range(1, len(uniq_vals)):        threshold = 0.5 * (uniq_vals[i-1] + uniq_vals[i])                 # split dataset by threshold        split_left = y[X[param] <= threshold]        split_right = y[X[param] > threshold]                # calculate predicted values for each split        pred_left = split_left.mean()        pred_right = split_right.mean()        num_left = split_left.shape[0]        num_right = split_right.shape[0]        mse_left = ((split_left - pred_left) * (split_left - pred_left)).mean()        mse_right = ((split_right - pred_right) * (split_right - pred_right)).mean()        mse = mse_left * num_left / (num_left + num_right) \            + mse_right * num_right / (num_left + num_right)        tmp_data.append(            {                'param': param,                'threshold': threshold,                'mse': mse            }        )                return pd.DataFrame(tmp_data).sort_values('mse')get_binary_split_for_param('sulphates', train_X, train_y).head(5)| param     |   threshold |      mse ||:----------|------------:|---------:|| sulphates |       0.685 | 0.758495 || sulphates |       0.675 | 0.758794 || sulphates |       0.705 | 0.759065 || sulphates |       0.715 | 0.759071 || sulphates |       0.635 | 0.759495 |

Possiamo vedere che per i solfati, la soglia migliore è 0.685 poiché fornisce il MSE più basso.

Ora possiamo utilizzare questa funzione per tutte le caratteristiche che abbiamo per definire la migliore suddivisione complessiva.

def ottieni_suddivisione_binaria(X, y):    tmp_dfs = []    for param in X.columns:        tmp_dfs.append(ottieni_suddivisione_binaria_per_parametro(param, X, y))            return pd.concat(tmp_dfs).sort_values('mse')ottieni_suddivisione_binaria(train_X, train_y).head(5)| parametro   |   soglia |      mse ||:--------|------------:|---------:|| alcol |      10.625 | 0.640368 || alcol |      10.675 | 0.640681 || alcol |      10.85  | 0.641541 || alcol |      10.725 | 0.641576 || alcol |      10.775 | 0.641604 |

Abbiamo ottenuto esattamente lo stesso risultato del nostro albero di decisione iniziale con la prima suddivisione su alcol <= 10.625.

Per costruire l’intero albero di decisione, potremmo calcolare in modo ricorsivo le migliori suddivisioni per ciascuno dei dataset alcol <= 10.625 e alcol > 10.625 e ottenere il livello successivo dell’albero di decisione. Quindi, ripetere.

I criteri di arresto per la ricorsione potrebbero essere la profondità o la dimensione minima del nodo foglia. Ecco un esempio di albero di decisione con almeno 420 elementi nei nodi foglia.

modello = sklearn.tree.DecisionTreeRegressor(min_samples_leaf = 420)
Grafico dell'autore

Calcoliamo l’errore medio assoluto sul set di validazione per capire quanto sia buono il nostro modello. Preferisco l’errore medio assoluto (MAE) rispetto all’errore quadratico medio (MSE) perché è meno influenzato dagli outliers.

import sklearn.metricsprint(sklearn.metrics.mean_absolute_error(model.predict(val_X), val_y))0.5890557338155006

Albero di decisione: Classificazione

Abbiamo esaminato l’esempio di regressione. Nel caso della classificazione, è leggermente diverso. Anche se non entreremo in profondità negli esempi di classificazione in questo articolo, vale comunque la pena discutere le sue basi.

Per la classificazione, invece del valore medio, utilizziamo la classe più comune come previsione per ciascun nodo foglia.

Di solito utilizziamo il coefficiente di Gini per stimare la qualità della suddivisione binaria per la classificazione. Immaginate di prendere un elemento casuale dal campione e poi un altro. Il coefficiente di Gini sarebbe uguale alla probabilità della situazione in cui gli elementi appartengono a diverse classi.

Diciamo che abbiamo solo due classi e la quota di elementi della prima classe è uguale a p. Allora possiamo calcolare il coefficiente di Gini utilizzando la seguente formula:

Se il nostro modello di classificazione è perfetto, il coefficiente di Gini è uguale a 0. Nel caso peggiore (p = 0.5), il coefficiente di Gini è uguale a 0.5.

Per calcolare la metrica per la suddivisione binaria, calcoliamo i coefficienti di Gini per entrambe le parti (sinistra e destra) e li normalizziamo in base al numero di campioni in ogni partizione.

Quindi, possiamo calcolare in modo simile la nostra metrica di ottimizzazione per diverse soglie e utilizzare l’opzione migliore.

Abbiamo addestrato un semplice modello di albero di decisione e discusso come funziona. Ora siamo pronti per passare alle Foreste Casuali.

Foreste Casuali

Le Foreste Casuali si basano sul concetto di Bagging. L’idea è di adattare un gruppo di modelli indipendenti e utilizzare una previsione media da essi. Poiché i modelli sono indipendenti, gli errori non sono correlati. Supponiamo che i nostri modelli non abbiano errori sistematici e la media di molti errori dovrebbe essere vicina a zero.

Come potremmo ottenere molti modelli indipendenti? È piuttosto semplice: possiamo addestrare alberi decisionali su sottoinsiemi casuali di righe e caratteristiche. Sarà una Foresta Casuale.

Addestriamo una Foresta Casuale di base con 100 alberi e una dimensione minima dei nodi foglia pari a 100.

import sklearn.ensemble
import sklearn.metrics

model = sklearn.ensemble.RandomForestRegressor(100, min_samples_leaf=100)
model.fit(train_X, train_y)
print(sklearn.metrics.mean_absolute_error(model.predict(val_X), val_y))
0.5592536196736408

Con una foresta casuale, abbiamo raggiunto una qualità molto migliore rispetto a un solo Albero Decisionale: 0.5592 rispetto a 0.5891.

Overfitting

La domanda significativa è se la Foresta Casuale potrebbe sovradattarsi.

In realtà, no. Poiché stiamo mediando errori non correlati, non possiamo sovradattare il modello aggiungendo più alberi. La qualità migliorerà in modo asintotico con l’aumento del numero di alberi.

Grafico dell'autore

Tuttavia, potresti incorrere in overfitting se hai alberi profondi e non ne hai abbastanza. È facile sovradattare un solo Albero Decisionale.

Errore fuori campione (out-of-bag)

Dato che solo una parte delle righe è utilizzata per ogni albero nella Foresta Casuale, possiamo utilizzarle per stimare l’errore. Per ogni riga, possiamo selezionare solo gli alberi in cui questa riga non è stata utilizzata e utilizzarli per effettuare previsioni. Quindi, possiamo calcolare gli errori basati su queste previsioni. Questo approccio è chiamato “errore fuori campione (out-of-bag)”.

Possiamo vedere che l’errore fuori campione è molto più vicino all’errore sul set di validazione rispetto a quello per l’addestramento, il che significa che è una buona approssimazione.

# dobbiamo specificare oob_score = True per calcolare l'errore fuori campionemodel = sklearn.ensemble.RandomForestRegressor(100, min_samples_leaf=100, oob_score=True)
model.fit(train_X, train_y)
# errore per il set di validazione
print(sklearn.metrics.mean_absolute_error(model.predict(val_X), val_y))
0.5592536196736408
# errore per il set di addestramento
print(sklearn.metrics.mean_absolute_error(model.predict(train_X), train_y))
0.5430398596179975
# errore fuori campione (out-of-bag)
print(sklearn.metrics.mean_absolute_error(model.oob_prediction_, train_y))
0.5571191870008492

Interpretazione del modello

Come ho già accennato in precedenza, il grande vantaggio degli Alberi Decisionali è che è facile interpretarli. Cerchiamo di capire meglio il nostro modello.

Importanza delle caratteristiche

Il calcolo dell’importanza delle caratteristiche è piuttosto semplice. Guardiamo ogni albero decisionale nell’insieme e ogni suddivisione binaria e calcoliamo il suo impatto sulla nostra metrica (squared_error nel nostro caso).

Diamo un’occhiata alla prima suddivisione per alcohol in uno dei nostri alberi decisionali iniziali.

Poi, possiamo fare gli stessi calcoli per tutte le suddivisioni binarie in tutti gli alberi decisionali, sommare tutto, normalizzare e ottenere l’importanza relativa per ogni caratteristica.

Se si utilizza scikit-learn, non è necessario calcolare manualmente l’importanza delle caratteristiche. È sufficiente utilizzare model.feature_importances_.

def plot_feature_importance(model, names, threshold = None):
    feature_importance_df = pd.DataFrame.from_dict({'feature_importance': model.feature_importances_,
                                                    'feature': names})\
            .set_index('feature').sort_values('feature_importance', ascending = False)
    if threshold is not None:
        feature_importance_df = feature_importance_df[feature_importance_df.feature_importance > threshold]
    fig = px.bar(
        feature_importance_df,
        text_auto = '.2f',
        labels = {'value': 'importanza delle caratteristiche'},
        title = "Importanza delle caratteristiche"
    )
    fig.update_layout(showlegend = False)
    fig.show()
plot_feature_importance(model, train_X.columns)

Possiamo vedere che le caratteristiche più importanti in generale sono alcol e acidità volatile.

Grafico dell'autore

Dipendenza Parziale

Capire come ogni caratteristica influisce sulla nostra metrica di destinazione è interessante e spesso utile. Ad esempio, se la qualità aumenta/diminuisce con l’aumento dell’alcol o se esiste una relazione più complessa.

Potremmo semplicemente ottenere i dati dal nostro dataset e tracciare le medie per alcol, ma non sarebbe corretto poiché potrebbero esserci delle correlazioni. Ad esempio, un alcol più elevato nel nostro dataset potrebbe corrispondere anche a livelli di zucchero più alti e una migliore qualità.

Per stimare l’impatto solo dall’alcol, possiamo prendere tutte le righe nel nostro dataset e, utilizzando il modello di machine learning, prevedere la qualità per ogni riga per diversi valori di alcol: 9, 9.1, 9.2, ecc. Quindi, possiamo fare la media dei risultati e ottenere la relazione effettiva tra il livello di alcol e la qualità del vino. Quindi, tutti i dati sono uguali e stiamo solo variando i livelli di alcol.

Questo approccio può essere utilizzato con qualsiasi modello di machine learning, non solo con il Random Forest.

Possiamo utilizzare il modulo sklearn.inspection per tracciare facilmente queste relazioni.

sklearn.inspection.PartialDependenceDisplay.from_estimator(clf, train_X, range(12))

Possiamo ottenere molte informazioni interessanti da questi grafici, ad esempio:

  • la qualità del vino aumenta con l’aumento del diossido di zolfo libero fino a 30, ma diventa stabile dopo questa soglia;
  • con l’alcol, maggiore è il livello, migliore è la qualità.

Possiamo anche esaminare le relazioni tra due variabili. Possono essere piuttosto complesse. Ad esempio, se il livello di alcol è superiore a 11.5, l’acidità volatile non ha effetto. Ma, per livelli di alcol inferiori, l’acidità volatile influisce significativamente sulla qualità.

sklearn.inspection.PartialDependenceDisplay.from_estimator(clf, train_X, [(1, 10)])

Confidenza delle previsioni

Utilizzando Random Forests, possiamo valutare anche quanto è sicura ciascuna previsione. Per fare ciò, potremmo calcolare le previsioni da ogni albero nell’ensemble e osservare la varianza o la deviazione standard.

val_df['predictions_mean'] = np.stack([dt.predict(val_X.values) for dt in model.estimators_]).mean(axis = 0)val_df['predictions_std'] = np.stack([dt.predict(val_X.values) for dt in model.estimators_]).std(axis = 0)ax = val_df.predictions_std.hist(bins = 10)ax.set_title('Distribuzione della deviazione standard delle previsioni')

Possiamo vedere che ci sono previsioni con bassa deviazione standard (cioè inferiore a 0.15) e previsioni con std superiore a 0.3.

Se utilizziamo il modello per scopi aziendali, possiamo trattare casi del genere in modo diverso. Ad esempio, non tenere in considerazione una previsione se std è superiore a X oppure mostrare al cliente gli intervalli (ad esempio, percentiles 25% e percentiles 75%).

Come è stata fatta la previsione?

Possiamo anche utilizzare i pacchetti treeinterpreter e waterfallcharts per capire come è stata fatta ciascuna previsione. Questo può essere utile in alcuni casi aziendali, ad esempio, quando è necessario spiegare ai clienti perché il loro credito è stato respinto.

Guarderemo uno dei vini come esempio. Ha un’alcol relativamente basso e un’alta acidità volatile.

from treeinterpreter import treeinterpreterfrom waterfall_chart import plot as waterfallrow = val_X.iloc[[7]]prediction, bias, contributions = treeinterpreter.predict(model, row.values)waterfall(val_X.columns, contributions[0], threshold=0.03,           rotation_value=45, formatting='{:,.3f}');

Il grafico mostra che questo vino è migliore della media. Il fattore principale che aumenta la qualità è un basso livello di acidità volatile, mentre lo svantaggio principale è un basso livello di alcol.

Grafico di autore

Quindi, ci sono molti strumenti utili che potrebbero aiutarti a capire meglio i tuoi dati e il tuo modello.

Riduzione del numero di alberi

L’altra caratteristica interessante di Random Forest è che possiamo utilizzarla per ridurre il numero di attributi per qualsiasi dato tabulare. Puoi adattare rapidamente una Random Forest e definire un elenco di colonne significative nei tuoi dati.

Più dati non significa sempre una migliore qualità. Inoltre, può influire sulle prestazioni del tuo modello durante l’addestramento e l’inferenza.

Dato che nel nostro dataset iniziale del vino c’erano solo 12 attributi, per questo caso useremo un dataset leggermente più grande – Popolarità delle notizie online.

Esaminare l’importanza delle caratteristiche

Prima di tutto, costruiamo una Random Forest e guardiamo le importanze delle caratteristiche. 34 delle 59 caratteristiche hanno un’importanza inferiore a 0.01.

Proviamo a rimuoverle e guardiamo la precisione.

low_impact_features = feature_importance_df[feature_importance_df.feature_importance <= 0.01].index.valuestrain_X_imp = train_X.drop(low_impact_features, axis = 1)val_X_imp = val_X.drop(low_impact_features, axis = 1)model_imp = sklearn.ensemble.RandomForestRegressor(100, min_samples_leaf=100)model_imp.fit(train_X_sm, train_y)
  • MAE sul set di convalida per tutte le caratteristiche: 2969.73
  • MAE sul set di convalida per 25 caratteristiche importanti: 2975.61

La differenza di qualità non è così grande, ma potremmo rendere il nostro modello più veloce nelle fasi di addestramento e inferenza. Abbiamo già rimosso quasi il 60% delle caratteristiche iniziali – buon lavoro.

Esaminare le caratteristiche ridondanti

Per le caratteristiche rimanenti, vediamo se ce ne sono alcune ridondanti (altamente correlate). Per farlo, useremo uno strumento Fast.AI:

import fastbookfastbook.cluster_columns(train_X_imp)

Possiamo vedere che le seguenti caratteristiche sono vicine tra loro:

  • self_reference_avg_sharess e self_reference_max_shares
  • kw_min_avg e kw_min_max
  • n_non_stop_unique_tokens e n_unique_tokens .

Rimuoviamoli anche.

non_uniq_features = ['self_reference_max_shares', 'kw_min_max',   'n_unique_tokens']train_X_imp_uniq = train_X_imp.drop(non_uniq_features, axis = 1)val_X_imp_uniq = val_X_imp.drop(non_uniq_features, axis = 1)model_imp_uniq = sklearn.ensemble.RandomForestRegressor(100,   min_samples_leaf=100)model_imp_uniq.fit(train_X_imp_uniq, train_y)sklearn.metrics.mean_absolute_error(model_imp_uniq.predict(val_X_imp_uniq),   val_y)2974.853274034488

Qualità migliorata anche solo un po’. Abbiamo quindi ridotto il numero di caratteristiche da 59 a 22 e aumentato l’errore solo dello 0,17%. Ciò dimostra che questo approccio funziona.

Puoi trovare il codice completo su GitHub.

Sommario

In questo articolo abbiamo discusso il funzionamento degli algoritmi degli alberi decisionali e dei random forest. Inoltre, abbiamo imparato come interpretare le random forest:

  • Come utilizzare l’importanza delle caratteristiche per ottenere l’elenco delle caratteristiche più significative e ridurre il numero di parametri nel modello.
  • Come definire l’effetto di ciascun valore caratteristica sulla metrica di destinazione utilizzando la dipendenza parziale.
  • Come stimare l’impatto delle diverse caratteristiche su ogni previsione utilizzando la libreria treeinterpreter.

Grazie mille per aver letto questo articolo. Spero che sia stato utile per te. Se hai domande o commenti, ti preghiamo di lasciarli nella sezione commenti.

Riferimenti

Set di dati

  • Cortez, Paulo, Cerdeira, A., Almeida, F., Matos, T. e Reis, J.. (2009). Wine Quality. UCI Machine Learning Repository. https://doi.org/10.24432/C56S3T
  • Fernandes, Kelwin, Vinagre, Pedro, Cortez, Paulo e Sernadela, Pedro. (2015). Online News Popularity. UCI Machine Learning Repository. https://doi.org/10.24432/C5NS3V

Fonti

Questo articolo è stato ispirato dal Corso di Deep Learning di Fast.AI