Convalida incrociata nidificata e selezione del miglior modello di regressione: è questo il processo SKLearn giusto?


8

Se ho capito bene, il CV nidificato può aiutarmi a valutare quale modello e processo di ottimizzazione dell'iperparametro sono i migliori. Il ciclo interno ( GridSearchCV) trova i migliori iperparametri e il ciclo esterno ( cross_val_score) valuta l'algoritmo di ottimizzazione dell'iperparametro. Scelgo quindi quale combinazione di tuning / modello dal loop esterno che minimizza mse(sto guardando il classificatore di regressione) per il mio test del modello finale.

Ho letto le domande / risposte sulla validazione incrociata nidificata, ma non ho visto un esempio di una pipeline completa che lo utilizza. Quindi, il mio codice qui sotto (per favore ignora gli attuali intervalli di iperparametro - questo è solo per esempio) e il processo di pensiero ha senso?

from sklearn.cross_validation import cross_val_score, train_test_split
from sklearn.grid_search import GridSearchCV
from sklearn.metrics import mean_squared_error
from sklearn.ensemble import RandomForestRegressor
from sklearn.svm import SVR
from sklearn.datasets import make_regression

# create some regression data
X, y = make_regression(n_samples=1000, n_features=10)
params = [{'C':[0.01,0.05,0.1,1]},{'n_estimators':[10,100,1000]}]

# setup models, variables
mean_score = []
models = [SVR(), RandomForestRegressor()]

# split into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.3)

# estimate performance of hyperparameter tuning and model algorithm pipeline
for idx, model in enumerate(models):
    clf = GridSearchCV(model, params[idx], scoring='mean_squared_error')

    # this performs a nested CV in SKLearn
    score = cross_val_score(clf, X_train, y_train, scoring='mean_squared_error')

    # get the mean MSE across each fold
    mean_score.append(np.mean(score))
    print('Model:', model, 'MSE:', mean_score[-1])

# estimate generalization performance of the best model selection technique
best_idx = mean_score.index(max(mean_score)) # because SKLearn flips MSE signs, max works OK here
best_model = models[best_idx]

clf_final = GridSearchCV(best_model, params[best_idx])
clf_final.fit(X_train, y_train)

y_pred = clf_final.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))

print('Final Model': best_model, 'Final model RMSE:', rmse)

Risposte:


8

Il tuo non è un esempio di convalida incrociata nidificata.

La convalida incrociata nidificata è utile per capire se, per esempio, una foresta casuale o un SVM sono più adatti al tuo problema. Il CV nidificato genera solo un punteggio, non genera un modello come nel tuo codice.

Questo sarebbe un esempio di convalida incrociata nidificata:

from sklearn.datasets import load_boston
from sklearn.cross_validation import KFold
from sklearn.metrics import mean_squared_error
from sklearn.grid_search import GridSearchCV
from sklearn.ensemble import RandomForestRegressor
from sklearn.svm import SVR
import numpy as np

params = [{'C': [0.01, 0.05, 0.1, 1]}, {'n_estimators': [10, 100, 1000]}]
models = [SVR(), RandomForestRegressor()]

df = load_boston()
X = df['data']
y = df['target']

cv = [[] for _ in range(len(models))]
for tr, ts in KFold(len(X)):
    for i, (model, param) in enumerate(zip(models, params)):
        best_m = GridSearchCV(model, param)
        best_m.fit(X[tr], y[tr])
        s = mean_squared_error(y[ts], best_m.predict(X[ts]))
        cv[i].append(s)
print(np.mean(cv, 1))

A proposito, un paio di pensieri:

  • Non vedo alcun motivo per cercare in griglia la n_estimatorstua foresta casuale. Ovviamente, più è, più è bello. Cose come max_depthè il tipo di regolarizzazione che si desidera ottimizzare. L'errore per il CV nidificato di RandomForestera molto più alto perché non si ottimizzava per i giusti iperparametri, non necessariamente perché si tratta di un modello peggiore.
  • Potresti anche provare gli alberi con incremento gradiente.

Grazie per quello Il mio obiettivo è fare esattamente quello che hai detto: capire quale algoritmo di classificazione sarebbe più adatto al mio problema. Immagino di essere confuso in termini di documentazione di SKLearn: scikit-learn.org/stable/tutorial/statistical_inference/… (sotto 'validazione incrociata nidificata')
BobbyJohnsonOG

Per testare le prestazioni del modello meglio selezionato, dovrei effettuare una convalida incrociata finale sull'intero set di dati? O dovrei dividere il mio set di dati in treno / test PRIMA del CV nidificato, eseguire CV nidificato sul treno e quindi adattare il modello migliore ai dati del treno e testare su test?
BobbyJohnsonOG,

Ci scusiamo per la raffica di commenti. Quindi il mio modello finale sarebbe:best_idx = np.where(np.mean(cv,1).min())[0]; final_m = GridSearchCV(models[best_idx], params[best_idx]); final_m.fit(X,y)
BobbyJohnsonOG,

Costruendo quello che hai detto, questo era quello che stavo cercando con le funzioni integrate SKLearn (dà lo stesso della tua risposta):for model, param in zip(models, params): clf = GridSearchCV(model, param) my_score = cross_val_score(clf, X, y, scoring='mean_squared_error') my_scores.append(my_score)
BobbyJohnsonOG

7

La convalida incrociata nidificata stima l'errore di generalizzazione di un modello, quindi è un buon modo per scegliere il modello migliore da un elenco di modelli candidati e le relative griglie di parametri associate. Il post originale è vicino a fare un CV nidificato: piuttosto che fare una singola divisione test-treno, si dovrebbe invece usare un secondo splitter di validazione incrociata. Cioè, si "nidifica" uno splitter di convalida incrociata "interno" all'interno di uno splitter di convalida incrociata "esterno".

Lo splitter di convalida incrociata interno viene utilizzato per scegliere gli iperparametri. Lo splitter esterno di convalida incrociata calcola la media dell'errore di test su più split treno-test. La media dell'errore di generalizzazione su più divisioni treno-test fornisce una stima più affidabile dell'accuratezza del modello su dati non visti.

Ho modificato il codice del post originale per aggiornarlo all'ultima versione di sklearn(con sklearn.cross_validationsostituito da sklearn.model_selectione con 'mean_squared_error'sostituito da 'neg_mean_squared_error') e ho usato due KFoldsplitter di convalida incrociata per selezionare il modello migliore. Per ulteriori informazioni sulla convalida incrociata nidificato, vedere la sklearn's esempio a nested convalida incrociata .

from sklearn.model_selection import KFold, cross_val_score, GridSearchCV
from sklearn.datasets import make_regression
from sklearn.ensemble import RandomForestRegressor
from sklearn.svm import SVR
import numpy as np

# `outer_cv` creates 3 folds for estimating generalization error
outer_cv = KFold(3)

# when we train on a certain fold, we use a second cross-validation
# split in order to choose hyperparameters
inner_cv = KFold(3)

# create some regression data
X, y = make_regression(n_samples=1000, n_features=10)

# give shorthand names to models and use those as dictionary keys mapping
# to models and parameter grids for that model
models_and_parameters = {
    'svr': (SVR(),
            {'C': [0.01, 0.05, 0.1, 1]}),
    'rf': (RandomForestRegressor(),
           {'max_depth': [5, 10, 50, 100, 200, 500]})}

# we will collect the average of the scores on the 3 outer folds in this dictionary
# with keys given by the names of the models in `models_and_parameters`
average_scores_across_outer_folds_for_each_model = dict()

# find the model with the best generalization error
for name, (model, params) in models_and_parameters.items():
    # this object is a regressor that also happens to choose
    # its hyperparameters automatically using `inner_cv`
    regressor_that_optimizes_its_hyperparams = GridSearchCV(
        estimator=model, param_grid=params,
        cv=inner_cv, scoring='neg_mean_squared_error')

    # estimate generalization error on the 3-fold splits of the data
    scores_across_outer_folds = cross_val_score(
        regressor_that_optimizes_its_hyperparams,
        X, y, cv=outer_cv, scoring='neg_mean_squared_error')

    # get the mean MSE across each of outer_cv's 3 folds
    average_scores_across_outer_folds_for_each_model[name] = np.mean(scores_across_outer_folds)
    error_summary = 'Model: {name}\nMSE in the 3 outer folds: {scores}.\nAverage error: {avg}'
    print(error_summary.format(
        name=name, scores=scores_across_outer_folds,
        avg=np.mean(scores_across_outer_folds)))
    print()

print('Average score across the outer folds: ',
      average_scores_across_outer_folds_for_each_model)

many_stars = '\n' + '*' * 100 + '\n'
print(many_stars + 'Now we choose the best model and refit on the whole dataset' + many_stars)

best_model_name, best_model_avg_score = max(
    average_scores_across_outer_folds_for_each_model.items(),
    key=(lambda name_averagescore: name_averagescore[1]))

# get the best model and its associated parameter grid
best_model, best_model_params = models_and_parameters[best_model_name]

# now we refit this best model on the whole dataset so that we can start
# making predictions on other data, and now we have a reliable estimate of
# this model's generalization error and we are confident this is the best model
# among the ones we have tried
final_regressor = GridSearchCV(best_model, best_model_params, cv=inner_cv)
final_regressor.fit(X, y)

print('Best model: \n\t{}'.format(best_model), end='\n\n')
print('Estimation of its generalization error (negative mean squared error):\n\t{}'.format(
    best_model_avg_score), end='\n\n')
print('Best parameter choice for this model: \n\t{params}'
      '\n(according to cross-validation `{cv}` on the whole dataset).'.format(
      params=final_regressor.best_params_, cv=inner_cv))

All'ultimo commento, dici di "... rimontare questo modello migliore sull'intero set di formazione" ma lo fai effettivamente sull'intero set di dati ( Xe y). Per quanto ho capito, questa è la cosa giusta da fare, ma il commento deve essere corretto. Cosa ne pensi?
Dror Atariah,

Grazie @DrorAtariah per averlo scoperto. Hai ragione. L'ho riparato.
Charlie Brummitt,

1

Non hai bisogno

# this performs a nested CV in SKLearn
score = cross_val_score(clf, X_train, y_train, scoring='mean_squared_error')

GridSearchCVfa questo per te. Per ottenere l'intuizione del processo di ricerca della griglia, prova a utilizzare GridSearchCV(... , verbose=3)

Per estrarre i punteggi per ogni piega vedere questo esempio nella documentazione di scikit-learn


Pensavo che la ricerca in griglia fosse esclusivamente per l'ottimizzazione degli iperparametri? Come utilizzerei gridsearch insieme a qualcos'altro per capire il miglior algoritmo di classificazione (ovvero SVR vs. RandomForest)?
BobbyJohnsonOG,

Sì. Per ogni combinazione di iperparametri GridSearchCV crea pieghe e calcola i punteggi (errore quadratico medio nel tuo caso) sui dati di sinistra. Pertanto, ogni combinazione di iperparametri ottiene il proprio punteggio medio. L '"ottimizzazione" sta semplicemente scegliendo la combinazione con il miglior punteggio medio. È possibile estrarre quei punteggi medi e confrontarli direttamente per vari modelli.
lanenok,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.