Passos para reproduzir:
@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:
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:
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.
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:
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.
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: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:
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