Evalml: Gestapeltes Ensemble spielt schlecht

Erstellt am 6. Apr. 2021  ·  11Kommentare  ·  Quelle: alteryx/evalml

Schritte zum Reproduzieren:

  1. Laden Sie das Glücks-Dataset in evalml
  2. Laufen Sie lange genug, um das Ensemble einzubeziehen
  3. Der Basisregressor wird als höher eingestuft als der gestapelte Regressor.
    Glücksdaten Full Set.csv.zip
bug performance

Alle 11 Kommentare

@dancuarini Ich habe versucht, dies lokal zu reproduzieren, konnte es aber nicht; kann an zusätzlichen Schritten vor dem Ausführen von AutoMLSearch liegen (z. B.: Datenaufteilungsgröße, Löschen von Spalten). Sprechen wir über die Problemkonfiguration!

Folgendes habe ich versucht lokal auszuführen:

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()

Daraus ergeben sich folgende Rankings:

image

Aktueller Fortschritt: mit @dancuarini besprochen, reproduzieren .

@angela97lin warte, bist du sicher, dass du das nicht

Danke für das Teilen des Reproduzierers :)

@dsherry Obwohl es ein wenig verdächtig ist, dass der Stacked-Ensembler nicht an der Spitze steht, bestand das ursprüngliche Problem darin, dass der Stacked-Ensembler so schlecht abgeschnitten hat, dass er über dem Basisregressor eingestuft wurde!

@angela97lin ah ja verstanden! Ich habe dir ein paar Notizen geschickt.

Ich denke, jeder Beweis dafür, dass unsere Ensembles nicht immer ganz oben sind, ist ein Problem.

Habe hier noch ein bisschen nachgeforscht. Ich denke, es gibt einige mögliche Gründe, warum das Ensemble mit diesem Datensatz schlecht abschneidet:

  1. Der Datensatz ist sehr klein, und unsere aktuelle Datenaufteilungsstrategie bedeutet, dass dem Ensemble eine sehr kleine Teilmenge von Daten zur Verfügung gestellt und anhand dieser validiert wird. Wenn wir jetzt einen gestapelten Ensemble trainieren möchten, teilen wir einige Daten (identifiziert mit ensembling_indices ) auf, damit der Ensemble weiter trainieren kann. Dies dient dazu, eine Überanpassung des Ensembles zu verhindern, indem der Metalearner mit denselben Daten trainiert wird, auf denen die Eingabepipelines bereits trainiert wurden. Wir machen dann einen CV-Split und teilen die Daten weiter aus dem ensembling_indices . Für diesen Datensatz mit 128 Zeilen trainieren und validieren wir an 17 bzw. 8 Zeilen. Ich habe #2144 eingereicht, um zu besprechen, ob wir diese zusätzliche CV-Aufteilung durchführen wollen.

  2. Unser Ensemble wird derzeit konstruiert, indem die beste Pipeline jeder gefundenen Modellfamilie genommen und als Eingabepipeline für den gestapelten Ensemble verwendet wird. Wenn jedoch einige der Eingabepipelines eine ziemlich schlechte Leistung erbringen, kann es sein, dass der gestapelte Ensemble nicht so gut arbeitet wie eine leistungsstarke individuelle Pipeline.

Dies ist zum Beispiel die endgültige Rangliste:
image

Wir bemerken, dass das gestapelte Ensemble genau in der Mitte abschneidet – wenn wir vereinfachen und sagen, dass das gestapelte Ensemble die Vorhersagen seiner Eingabepipelines mittelt, ist dies sinnvoll. Um meine Hypothese zu testen, entschied ich mich, nur die Modellfamilien zu verwenden, die besser abschneiden als der gestapelte Ensemble, und nicht alle Modellfamilien, und stellte fest, dass die resultierende Bewertung viel besser abschneidet als jede einzelne Pipeline. Dies lässt mich glauben, dass die leistungsschwachen einzelnen Pipelines dazu führten, dass das Stacked-Ensembler schlechter abgeschnitten hat.

Hier ist der Repro-Code dafür:

Von oben:

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())

Wenn wir nur diese drei Modellfamilien verwenden, erhalten wir einen MAE-Wert von ~0,22, was viel besser ist als bei jeder einzelnen Pipeline.

#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},}),

Daher frage ich mich, ob wir darüber nachdenken müssen, welche Eingabepipelines wir unserem gestapelten Ensemble zuführen sollten.

  1. Der von uns verwendete Metalearner (LinearRegressor) ist nicht der beste. Ich habe dies über den stacking_test Zweig getestet, den ich erstellt habe, in dem ich den Standard-Metalearner auf RidgeCV aktualisiert habe (scikit-learn-Standard, aber wir haben ihn nicht in EvalML), und der Ensemble schneidet viel besser ab:
    image

Nächste Schritte nach Diskussion mit @dsherry :

Probieren Sie Nr. 1 und Nr. 3 (mit Elastic Net) mit anderen Datensätzen aus, führen Sie Leistungstests durch und sehen Sie, ob wir insgesamt eine bessere Leistung erzielen können.

@angela97lin Ihre Punkte zur Aufteilung für winzige Datensätze sind genau richtig. Schließlich müssen wir mit winzigen Datensätzen wirklich anders umgehen als mit größeren, zB indem wir nur xval mit hoher Faltungszahl für den gesamten Datensatz, sogar LOOCV, verwenden und sicherstellen, dass wir die Faltungen für das Ensemble-Metalearner-Training anders konstruieren.

Ich stimme auch zu, dass der Metalearner eine starke Regularisierung verwenden muss. Ich habe Elastic Net in H2O-3 StackedEnsemble verwendet und erinnere mich nur an ein einziges Mal, dass das Ensemble den zweiten Platz in der Bestenliste belegte. Jedes zweite Mal, wenn ich getestet habe, war es das erste. Die Regularisierung sollte niemals zulassen, dass schlechte Modelle die Leistung des Ensembles beeinträchtigen.

Und das fütterte die gesamte Bestenliste von sogar 50 Modellen in den Metallverdiener. :-)

Ich poste nur ein paar zusätzliche Updates dazu:

Lokal getestet mit allen Regressions-Datasets. Ergebnisse finden Sie hier oder nur die Charts hier .

Davon:

  • Einverstanden @rpeck! Wir sollten den Metalearner aktualisieren, um auf jeden Fall eine starke Regularisierung zu verwenden. ElasticNetCV schien bei vielen Datensätzen besser zu funktionieren als unser LinearRegressor. Diese Ausgabe verfolgt dies: https://github.com/alteryx/evalml/issues/1739
  • @dsherry und ich haben unsere Datenaufteilungsstrategie noch einmal diskutiert: Im Moment spalten wir Daten für das Ensemble ab. Dies geschieht jedoch unter der Annahme, dass wir möchten, dass der Metalearner auf diesen Ensemble-Indizes trainiert wird. Wenn wir mit der scikit-learn-Implementierung unsere StackedEnsembler auf diese Ensemble-Indizes-Aufteilung trainieren, trainieren wir am Ende sowohl die Eingabepipelines als auch den Metalearner mit diesem kleinen Datensatz. Dies könnte wahrscheinlich der Grund sein, warum wir nicht gut abschneiden. Während die Parameter für unsere Eingabepipelines von der Abstimmung mit den anderen Daten stammen, sind diese Pipelines nicht angepasst. Langfristig könnte das Rollen unserer eigenen Implementierung es uns ermöglichen, trainierte Pipelines an das Ensemble zu übergeben, in diesem Fall hätten wir das gewünschte Verhalten. Im Moment ist das nicht der Fall.

Nächster Schritt: Testen Sie diese Hypothese manuell mit dem Ensemble. Versuchen Sie, Eingabepipelines mit 80 % der Daten manuell zu trainieren, erstellen Sie kreuzvalidierte Vorhersagen für den Datensatz, der für das Ensemble beiseite gelegt wird, und trainieren Sie Metalearner mit Out-Prognosen.

Die Ergebnisse der Experimente sehen gut aus: https://alteryx.quip.com/4hEyAaTBZDap/Ensembling-Performance-Using-More-Data

Nächste Schritte:

Nach einigem Herumgraben glauben wir, dass das Problem nicht in der Leistung des Ensembles liegt, sondern vielmehr darin, wie wir über die Leistung des Ensembles berichten. Derzeit führen wir einen separaten Ensemble-Split durch, der 20 % der Daten ausmacht, und führen dann einen weiteren Zug-Validierungs-Split durch und melden die Punktzahl des Ensembles als Validierungsdaten. Dies bedeutet, dass der Ensemble-Score in einigen Fällen anhand einer sehr kleinen Anzahl von Zeilen berechnet wird (wie der Glücksdatensatz oben).

Indem wir die Aufteilung der Ensemble-Indizes entfernen und unsere alte Methode zur Berechnung des CV-Trainingsergebnisses für das Ensemble verwenden (alle Daten angeben, trainieren und auf einmal validieren), sehen wir, dass das Ensemble in fast allen Fällen höher eingestuft wird und auftaucht als #1 in vielen weiteren Fällen. Inzwischen ist der Validierungsscore gleich oder etwas besser.

Beachten Sie, dass die Eingabepipelines nicht trainiert werden und das Ensemble nur die Vorhersagen der Eingabepipelines als Eingabe erhält, da wir keine Hyperparameter-Optimierung durchführen, und dass eine Überanpassung kein Problem darstellt. Wir können die Implementierung unseres eigenen Ensembles überdenken und dann die Aufteilungsstrategie aktualisieren, aber im Moment können wir Verbesserungen sehen, indem wir einfach die Datenaufteilungsstrategie und die Implementierung von scikit-learn ändern.

Beachten Sie, dass dies zu einer Verlängerung der Anpassungszeit führt, wenn das Ensemble aktiviert ist: Alle Pipelines sehen mehr Daten (keine reservierten Ensemble-Indizes) und das Ensemble wird mit mehr Daten trainiert. Ich denke, das ist in Ordnung.

Ergebnisse hier tabellarisch: https://alteryx.quip.com/jI2mArnWZfTU/Ensembling-vs-Best-Pipeline-Validation-Scores#MKWACADlCDt

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen