Evalml: L'ensemble empilé fonctionne mal

Créé le 6 avr. 2021  ·  11Commentaires  ·  Source: alteryx/evalml

Étapes à reproduire :

  1. Charger l'ensemble de données Happiness dans evalml
  2. Exécuter assez longtemps pour inclure l'assemblage
  3. Le régresseur de ligne de base s'affiche comme classé plus haut que le régresseur empilé.
    Ensemble complet de données sur le bonheur.csv.zip
bug performance

Tous les 11 commentaires

@dancuarini J'ai essayé de reproduire cela localement mais je n'ai pas pu; cela peut être dû à des étapes supplémentaires avant d'exécuter AutoMLSearch (par exemple : taille de division des données, suppression des cols). Parlons du problème de configuration !

Voici ce que j'ai essayé d'exécuter localement :

from evalml.automl import AutoMLSearch
import pandas as pd
import woodwork as ww
from evalml.automl.callbacks import raise_error_callback

happiness_data_set = pd.read_csv("Happiness Data Full Set.csv")
y = happiness_data_set['Happiness']
X = happiness_data_set.drop(['Happiness'], axis=1)
# display(X.head())

X = ww.DataTable(X)
X_train, X_holdout, y_train, y_holdout = evalml.preprocessing.split_data(X, y, problem_type='regression', test_size=0.2, random_seed=0)
# print(X.types)

automl = AutoMLSearch(X, y, problem_type="regression", objective="MAE", error_callback=raise_error_callback, max_batches=20, ensembling=True)
automl.search()

Il en résulte les classements suivants :

image

Progrès actuels : discuté avec @dancuarini de l'impossibilité de reproduire localement, restera en contact avec @Cmancuso au sujet de la reproduction et des prochaines étapes.

@angela97lin attendez, êtes-vous sûr de ne pas pouvoir reproduire cela? Ici, l'ensemble empilé apparaît au milieu du classement - je m'attendrais à ce qu'il soit au sommet !

Merci pour le partage du reproducteur :)

@dsherry Bien qu'il soit un peu suspect que l'ensemble empilé ne soit pas au sommet, le problème initial était que l'ensemble empilé fonctionnait si mal qu'il était classé au-dessus du régresseur de base!

@angela97lin ah oui compris ! Je t'ai envoyé quelques notes.

Je pense que toute preuve que nos ensembles ne sont pas toujours proches du sommet est un problème.

A creusé un peu plus. Je pense qu'il y a des raisons potentielles pour lesquelles l'ensembler fonctionne mal avec cet ensemble de données :

  1. L'ensemble de données est vraiment petit, et notre stratégie actuelle de fractionnement des données signifie que l'ensembler est fourni et validé sur un très petit sous-ensemble de données. À l'heure actuelle, si nous voulons former un ensembler empilé, nous divisons certaines données (identifiées par ensembling_indices ) pour que l'ensembler s'entraîne. Cela permet d'éviter de suradapter l'ensemble en formant le metalearner sur les mêmes données que celles sur lesquelles les pipelines d'entrée ont déjà été formés. Nous effectuons ensuite un fractionnement de CV, en divisant davantage les données du ensembling_indices . Pour cet ensemble de données de 128 lignes, nous formons et validons respectivement sur 17 et 8 lignes. J'ai déposé #2144 pour discuter si nous voulons faire cette division de CV supplémentaire.

  2. Notre ensembler est actuellement construit en prenant le meilleur pipeline de chaque famille de modèles trouvée et en l'utilisant comme pipelines d'entrée pour l'ensembler empilé. Cependant, si certains des pipelines d'entrée fonctionnent assez mal, l'ensemble empilé peut ne pas fonctionner aussi bien qu'un pipeline individuel très performant.

Par exemple, voici le tableau de classement final :
image

Nous remarquons que l'ensemble empilé fonctionne parfaitement au milieu - si nous simplifions et disons que l'ensemble empilé fait la moyenne des prédictions de ses pipelines d'entrée, cela a du sens. Pour tester mon hypothèse, j'ai décidé d'utiliser uniquement les familles de modèles qui fonctionnent mieux que l'ensemble empilé, plutôt que toutes les familles de modèles, et j'ai remarqué que le score résultant fonctionne bien mieux que n'importe quel pipeline individuel. Cela m'amène à croire que les pipelines individuels peu performants ont conduit l'ensemble empilé à moins bien performer.

Voici le code repro pour cela :

D'en haut:

import pandas as pd
import woodwork as ww
happiness_data_set = pd.read_csv("Happiness Data Full Set.csv")
y = happiness_data_set['Happiness']
X = happiness_data_set.drop(['Happiness'], axis=1)

X = ww.DataTable(X)
X_train, X_holdout, y_train, y_holdout = evalml.preprocessing.split_data(X, y, problem_type='regression', test_size=0.25, random_seed=0)

automl = AutoMLSearch(X, y, problem_type="regression", objective="MAE", error_callback=raise_error_callback, max_batches=10, ensembling=True)
automl.search()

import woodwork as ww
from evalml.automl.engine import train_and_score_pipeline
from evalml.automl.engine.engine_base import JobLogger

# Get the pipelines fed into the ensemble but only use the ones better than the stacked ensemble
input_pipelines = []
input_info = automl._automl_algorithm._best_pipeline_info
from evalml.model_family import ModelFamily

trimmed = dict()
trimmed.update({ModelFamily.RANDOM_FOREST: input_info[ModelFamily.RANDOM_FOREST]})
trimmed.update({ModelFamily.XGBOOST: input_info[ModelFamily.XGBOOST]})
trimmed.update({ModelFamily.DECISION_TREE: input_info[ModelFamily.EXTRA_TREES]})

for pipeline_dict in trimmed.values():
    pipeline_class = pipeline_dict['pipeline_class']
    pipeline_params = pipeline_dict['parameters']
    input_pipelines.append(pipeline_class(parameters=automl._automl_algorithm._transform_parameters(pipeline_class, pipeline_params),
                                                      random_seed=automl._automl_algorithm.random_seed))
ensemble_pipeline = _make_stacked_ensemble_pipeline(input_pipelines, "regression")
X_train = X.iloc[automl.ensembling_indices]
y_train = ww.DataColumn(y.iloc[automl.ensembling_indices])
train_and_score_pipeline(ensemble_pipeline, automl.automl_config, X_train, y_train, JobLogger())

En utilisant simplement ces trois familles de modèles, nous obtenons un score MAE d'environ 0,22, ce qui est bien meilleur que n'importe quel pipeline individuel.

#output of train_and_score_pipeline(ensemble_pipeline, automl.automl_config, X_train, y_train, JobLogger())
{'scores': {'cv_data': [{'all_objective_scores': OrderedDict([('MAE',
                  0.22281276417465426),
                 ('ExpVariance', 0.9578811127332543),
                 ('MaxError', 0.3858477236606914),
                 ('MedianAE', 0.2790362808260225),
                 ('MSE', 0.0642654425375983),
                 ('R2', 0.9152119239698017),
                 ('Root Mean Squared Error', 0.2535062968401343),
                 ('# Training', 17),
                 ('# Validation', 9)]),
    'mean_cv_score': 0.22281276417465426,
    'binary_classification_threshold': None}],
  'training_time': 9.944366216659546,
  'cv_scores': 0    0.222813
  dtype: float64,
  'cv_score_mean': 0.22281276417465426},
 'pipeline': TemplatedPipeline(parameters={'Stacked Ensemble Regressor':{'input_pipelines': [GeneratedPipeline(parameters={'Imputer':{'categorical_impute_strategy': 'most_frequent', 'numeric_impute_strategy': 'most_frequent', 'categorical_fill_value': None, 'numeric_fill_value': None}, 'One Hot Encoder':{'top_n': 10, 'features_to_encode': None, 'categories': None, 'drop': 'if_binary', 'handle_unknown': 'ignore', 'handle_missing': 'error'}, 'Random Forest Regressor':{'n_estimators': 184, 'max_depth': 25, 'n_jobs': -1},}), GeneratedPipeline(parameters={'Imputer':{'categorical_impute_strategy': 'most_frequent', 'numeric_impute_strategy': 'mean', 'categorical_fill_value': None, 'numeric_fill_value': None}, 'One Hot Encoder':{'top_n': 10, 'features_to_encode': None, 'categories': None, 'drop': 'if_binary', 'handle_unknown': 'ignore', 'handle_missing': 'error'}, 'XGBoost Regressor':{'eta': 0.1, 'max_depth': 6, 'min_child_weight': 1, 'n_estimators': 100},}), GeneratedPipeline(parameters={'Imputer':{'categorical_impute_strategy': 'most_frequent', 'numeric_impute_strategy': 'mean', 'categorical_fill_value': None, 'numeric_fill_value': None}, 'One Hot Encoder':{'top_n': 10, 'features_to_encode': None, 'categories': None, 'drop': 'if_binary', 'handle_unknown': 'ignore', 'handle_missing': 'error'}, 'Extra Trees Regressor':{'n_estimators': 100, 'max_features': 'auto', 'max_depth': 6, 'min_samples_split': 2, 'min_weight_fraction_leaf': 0.0, 'n_jobs': -1},})], 'final_estimator': None, 'cv': None, 'n_jobs': -1},}),

Cela me fait me demander si nous devons repenser aux pipelines d'entrée que nous devrions alimenter notre ensembler empilé.

  1. Le metalearner que nous utilisons (LinearRegressor) n'est pas le meilleur. J'ai testé cela via la branche stacking_test j'ai créée où j'ai mis à jour le metalearner par défaut vers RidgeCV (scikit-learn default, mais nous n'en avons pas dans EvalML), et l'ensembler fonctionne beaucoup mieux :
    image

Prochaines étapes après discussion avec @dsherry :

Essayez #1 et #3 (en utilisant Elastic Net) sur d'autres ensembles de données, exécutez des tests de performances, voyez si nous pouvons obtenir de meilleures performances globales.

@ angela97lin Vos

Je suis également d'accord que le metalearner doit utiliser une régularisation forte. J'ai utilisé Elastic Net dans H2O-3 StackedEnsemble, et je ne me souviens qu'une seule fois que l'ensemble est arrivé en deuxième position dans le classement. Chaque fois que j'ai testé, c'était le premier. La régularisation ne doit jamais permettre à de mauvais modèles de faire baisser les performances de l'ensemble.

Et cela alimentait tout le classement de même 50 modèles dans le metalearner. :-)

Je viens de poster quelques mises à jour supplémentaires à ce sujet :

Testé localement en utilisant tous les ensembles de données de régression. Les résultats peuvent être trouvés ici ou simplement les graphiques ici .

De ceci :

  • D'accord @rpeck ! Nous devrions mettre à jour le metalearner pour utiliser une régularisation forte à coup sûr. ElasticNetCV semble mieux fonctionner que notre LinearRegressor sur de nombreux ensembles de données. Ce problème suit ceci : https://github.com/alteryx/evalml/issues/1739
  • @dsherry et moi avons rediscuté de notre stratégie de séparation des données : pour le moment, nous avons séparé les données de l'ensemble. Cependant, c'est sous l'hypothèse que nous voulons que le metalearner soit formé sur cet ensemble d'indices. Avec l'implémentation scikit-learn, lorsque nous formons notre StackedEnsembler sur cette division d'indices d'ensemble, nous finissons par former à la fois les pipelines d'entrée et le metalearner sur ce petit ensemble de données. Cela pourrait probablement être la raison pour laquelle nous ne fonctionnons pas bien. Bien que les paramètres de nos pipelines d'entrée proviennent du réglage à l'aide des autres données, ces pipelines ne sont pas ajustés. À long terme, le déploiement de notre propre implémentation pourrait nous permettre de transmettre des pipelines formés à l'ensembler, auquel cas nous aurions le comportement que nous souhaitons. Pour l'instant, ce n'est pas le cas.

Étape suivante : testez cette hypothèse manuellement avec l'ensembler. Essayez d'entraîner manuellement les pipelines d'entrée sur 80 % des données, créez des prédictions à validation croisée sur les données mises de côté pour l'assemblage et formez metalearner avec des prédictions supérieures.

Les résultats de l'expérimentation semblent bons : https://alteryx.quip.com/4hEyAaTBZDap/Ensembling-Performance-Using-More-Data

Prochaines étapes:

Après quelques recherches, nous pensons que le problème ne réside pas dans la façon dont l'ensemble joue, mais plutôt dans la façon dont nous rapportons la performance de l'ensemble. Actuellement, nous effectuons une division d'ensemble distincte qui représente 20 % des données, puis nous effectuons une autre division de validation par train et rapportons le score de l'ensemble en tant que données de validation. Cela signifie que dans certains cas, le score d'ensemble est calculé à l'aide d'un très petit nombre de lignes (comme l'ensemble de données sur le bonheur ci-dessus).

En supprimant la division des indices d'ensemble et en utilisant notre ancienne méthode de calcul du score d'entraînement cv pour l'ensemble (donnez-lui toutes les données, entraînez-vous et validez-vous d'un seul coup), nous voyons que l'ensemble est mieux classé dans presque tous les cas, et apparaît comme #1 dans de nombreux autres cas. Pendant ce temps, le score de validation est le même ou légèrement meilleur.

Notez que puisque nous n'effectuons aucun réglage d'hyperparamètres, les pipelines d'entrée ne sont pas entraînés et que l'ensemble n'obtient que les prédictions des pipelines d'entrée en entrée, le surapprentissage n'est pas un problème. Nous pouvons alors revoir la mise en œuvre de notre propre ensemble et mettre à jour la stratégie de division, mais pour l'instant, nous pouvons constater des améliorations en modifiant simplement la stratégie de division des données et la mise en œuvre de scikit-learn.

Notez que cela entraînera une augmentation du temps d'ajustement lorsque l'assemblage est activé : tous les pipelines voient plus de données (pas d'indices d'ensemble réservés) et l'ensemble est entraîné sur plus de données. Je pense que c'est bien.

Résultats tabulés ici : https://alteryx.quip.com/jI2mArnWZfTU/Ensembling-vs-Best-Pipeline-Validation-Scores#MKWACADlCDt

Cette page vous a été utile?
0 / 5 - 0 notes