Evalml: Conjunto empilhado com desempenho insatisfatório

Criado em 6 abr. 2021  ·  11Comentários  ·  Fonte: alteryx/evalml

Passos para reproduzir:

  1. Carregar conjunto de dados Happiness em evalml
  2. Execute o tempo suficiente para incluir o conjunto
  3. O regressor de linha de base mostra uma classificação mais alta do que o regressor empilhado.
    Happiness Data Full Set.csv.zip
bug performance

Todos 11 comentários

@dancuarini tentei reproduzir localmente mas não consegui; pode ser devido a etapas adicionais antes de executar o AutoMLSearch (por exemplo: tamanho da divisão de dados, eliminação de colunas). Vamos falar sobre o problema de configuração!

Aqui está o que tentei executar localmente:

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

Isso resulta nas seguintes classificações:

image

Progresso atual: discutido com @dancuarini sobre não ser possível fazer a reprodução localmente, manteremos contato com @Cmancuso sobre a reprodução e as próximas etapas.

@ angela97lin espere, tem certeza de que não conseguiu reproduzir isso? Aqui, o agrupador empilhado aparece no meio da classificação - eu esperava que estivesse no topo!

Obrigado por compartilhar o reprodutor :)

@dsherry Embora seja um pouco suspeito que o ensembler empilhado não esteja no topo, o problema original era que o ensembler empilhado tinha um desempenho tão ruim que era classificado acima do regressor de linha de base!

@ angela97lin ah sim entendido! Eu te enviei algumas notas.

Acho que qualquer evidência de que nossos conjuntos nem sempre estão perto do topo é um problema.

Mergulhei um pouco mais nisso. Acho que existem alguns motivos potenciais pelos quais o ensembler tem um desempenho ruim com este conjunto de dados:

  1. O conjunto de dados é realmente pequeno e nossa estratégia de divisão de dados atual significa que o ensembler é fornecido e validado em um subconjunto muito pequeno de dados. Agora, se quisermos treinar um ensembler empilhado, dividimos alguns dados (identificados com ensembling_indices ) para o ensembler treinar. Isso evita o ajuste excessivo do ensembler por meio do treinamento do metalearner nos mesmos dados em que os pipelines de entrada já foram treinados. Em seguida, fazemos uma divisão de CV, dividindo ainda mais os dados de ensembling_indices . Para este conjunto de dados de 128 linhas, treinamos e validamos em 17 e 8 linhas, respectivamente. Registrei o número 2144 para discutir se queremos fazer essa divisão de CV adicional.

  2. Nosso ensembler é atualmente construído pegando o melhor pipeline de cada família de modelo encontrada e usando-o como pipelines de entrada para o ensembler empilhado. No entanto, se alguns dos pipelines de entrada tiverem um desempenho muito ruim, o ensembler empilhado pode não funcionar tão bem quanto um pipeline individual de alto desempenho.

Por exemplo, esta é a tabela de classificação final:
image

Notamos que o conjunto empilhado funciona bem no meio - se simplificarmos e dissermos que o conjunto empilhado calcula a média das previsões de seus pipelines de entrada, isso faz sentido. Para testar minha hipótese, decidi usar apenas as famílias de modelo com melhor desempenho do que o ensembler empilhado, em vez de todas as famílias de modelo, e percebi que a pontuação resultante tem um desempenho muito melhor do que qualquer pipeline individual. Isso me leva a acreditar que os dutos individuais de baixo desempenho levaram o ensembler empilhado a um desempenho pior.

Este é o código de reprodução para isso:

De cima:

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

Usando apenas essas três famílias de modelo, obtemos uma pontuação MAE de ~ 0,22, que é muito melhor do que qualquer pipeline individual.

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

Isso me faz pensar se precisamos repensar sobre quais pipelines de entrada devemos alimentar nosso ensembler empilhado.

  1. O metalearner que estamos usando (LinearRegressor) não é o melhor. Eu testei isso por meio do branch stacking_test que criei, onde atualizei o metalearner padrão para RidgeCV (padrão do scikit-learn, mas não temos no EvalML), e o ensembler tem um desempenho muito melhor:
    image

Próximas etapas após discussão com @dsherry :

Experimente # 1 e # 3 (usando Elastic Net) em outros conjuntos de dados, execute testes de desempenho e veja se podemos obter um melhor desempenho geral.

@ angela97lin Seus pontos sobre a divisão, para conjuntos de dados minúsculos, estão acertados. Eventualmente, precisamos lidar com conjuntos de dados minúsculos de forma realmente diferente dos maiores, por exemplo, usando apenas alta contagem de dobras xval em todo o conjunto de dados, até mesmo LOOCV, e certificando-nos de construir as dobras de forma diferente para o treinamento do ensemble metalearner.

Também concordo que o metalearner precisa usar uma regularização forte. Usei o Elastic Net no H2O-3 StackedEnsemble e só me lembro uma vez que o conjunto ficou em segundo lugar na classificação. Todas as outras vezes que testei, foi o primeiro. A regularização nunca deve permitir que modelos ruins prejudiquem o desempenho do conjunto.

E isso alimentava todo o placar de até 50 modelos no metalearner. :-)

Apenas postando algumas atualizações extras sobre isso:

Testado localmente usando todos os conjuntos de dados de regressão. Os resultados podem ser encontrados aqui ou apenas os gráficos aqui .

Disto:

  • @Rpeck concordado! Devemos atualizar o metalearner para usar uma regularização forte com certeza. ElasticNetCV parecia ter um desempenho melhor do que nosso LinearRegressor em muitos conjuntos de dados. Este problema acompanha isso: https://github.com/alteryx/evalml/issues/1739
  • @dsherry e eu rediscutimos nossa estratégia de divisão de dados: Agora, separamos os dados para o conjunto. No entanto, pressupõe-se que queremos que o metalearner seja treinado StackedEnsembler nessa divisão de índices de conjunto, acabamos treinando os pipelines de entrada e o metalearner nesse pequeno conjunto de dados. Provavelmente, é por isso que não estamos apresentando um bom desempenho. Embora os parâmetros para nossos pipelines de entrada sejam do ajuste usando por outros dados, esses pipelines não são ajustados. No longo prazo, lançar nossa própria implementação poderia nos permitir passar pipelines treinados para o ensembler, caso em que teríamos o comportamento que desejamos. Por enquanto, não é esse o caso.

Próxima etapa: Teste essa hipótese com o ensembler manualmente. Tente treinar manualmente os pipelines de entrada em 80% dos dados, crie previsões com validação cruzada nos dados separados para montagem e treine o metalearner com previsões externas.

Os resultados da experimentação parecem bons: https://alteryx.quip.com/4hEyAaTBZDap/Ensembling-Performance-Using-More-Data

Próximos passos:

Depois de pesquisar um pouco, acreditamos que o problema não é o desempenho do conjunto, mas sim como relatamos o desempenho do conjunto. Atualmente, fazemos uma divisão de conjunto separada que é 20% dos dados e, em seguida, fazemos outra divisão de validação de trem e relatamos a pontuação do conjunto como os dados de validação. Isso significa que, em alguns casos, a pontuação do conjunto é calculada usando um número muito pequeno de linhas (como o conjunto de dados de felicidade acima).

Removendo a divisão dos índices de conjunto e usando nosso método antigo de cálculo da pontuação de treinamento cv para o conjunto (fornecer todos os dados, treinar e validar em uma dobra), vemos que o conjunto é classificado mais alto em quase todos os casos e surge como # 1 em muitos outros casos. Enquanto isso, a pontuação de validação é a mesma ou um pouco melhor.

Observe que, como não fazemos nenhum ajuste de hiperparâmetro, os pipelines de entrada não são treinados e o conjunto obtém apenas as previsões dos pipelines de entrada como entrada, o ajuste excessivo não é um problema. Podemos revisitar a implementação de nosso próprio conjunto e atualizar a estratégia de divisão então, mas, por enquanto, podemos ver melhorias apenas alterando a estratégia de divisão de dados e a implementação do scikit-learn.

Observe que isso causará um aumento no tempo de ajuste quando o conjunto estiver habilitado: todos os pipelines veem mais dados (sem índices de conjunto reservados) e o conjunto é treinado em mais dados. Eu acho que isso é bom.

Resultados tabulados aqui: https://alteryx.quip.com/jI2mArnWZfTU/Ensembling-vs-Best-Pipeline-Validation-Scores#MKWACADlCDt

Esta página foi útil?
0 / 5 - 0 avaliações