Evalml: One Hot Encoder: Solte um recurso redundante por padrão para recursos com duas categorias

Criado em 5 mar. 2021  ·  14Comentários  ·  Fonte: alteryx/evalml

Nosso codificador único cria um recurso para cada nível do recurso categórico original:

from evalml.pipelines import OneHotEncoder
import pandas as pd
df = pd.DataFrame({"category": ["a", "b"], "number": [4,5 ]})
OneHotEncoder().fit_transform(df).to_dataframe()

image

As colunas category_a e category_b são completamente colineares, o que as torna redundantes. Isso pode ter efeitos adversos no ajuste do estimador. Acho que devemos deixar cair um por padrão.

Para sua informação

enhancement

Comentários muito úteis

Terceira lei do código: não farás == comparações com carros alegóricos

Todos 14 comentários

💯% devemos descartar a coluna de caso negativo.

Se nós mesmos fizermos o OHE primeiro, então o sklearn esperançosamente não os expandirá. Como Freddy disse, você pode pensar nisso como gerar duas colunas que têm colinearidade perfeita.

Há dois problemas que vejo ao expandir um binário em duas colunas em vez de uma:

  1. Como outras formas de colinearidade de recursos, ela atrapalha muitas coisas na interpretabilidade, porque o efeito de uma coluna de origem original é dividido entre as duas colunas OHE. Os novos pacotes SHAP de Freddy resolvem isso, obviamente, mas coisas como gráficos de Importância de Recurso e Dependência Parcial ainda terão o problema.
  2. Modelos de árvore como Random Forest e GBM amostram aleatoriamente seus recursos de entrada. A coluna de origem neste caso será amostrada aleatoriamente duas vezes mais do que realmente deveria ser, para que possa ter um impacto desproporcional no modelo.

@freddyaboulton P: O dataframe acima para as colunas OHE as mostra como floats. Isso é realmente verdade?

@rpeck Sim!

@freddyaboulton O quê ? Isso é estranho. Eu nunca vi nada além de booleanos verdadeiros ou inteiros 0/1. Eu me pergunto como os modelos de árvore realmente lidam com isso. Tem um cheiro ruim para mim.

Terceira lei do código: não farás == comparações com carros alegóricos

(ok, a menos que seja com Math.NaN )

Hmm, eu pensei que estávamos fazendo isso!

Concordo que devemos. Eu pensei que era apenas um sinalizador que tínhamos que definir no impl subjacente.

@dsherry @freddyaboulton Parece que temos suporte para isso por meio de nosso parâmetro drop mas leva em consideração apenas a entrada do usuário e não é usado pelo nosso impl, então esse problema apenas rastreia a configuração do padrão para drop para algo diferente de Nenhum?

https://github.com/alteryx/evalml/blob/91775ffc26c47205adc0fb255832d828ead6e7c9/evalml/pipelines/components/transformers/encoders/onehot_encoder.py#L28

https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html

Poderíamos ir com first ou if_binary , sem saber qual é a decisão certa.

@angela97lin Você está certo que alterar o valor padrão seria suficiente! Acho que first é o caminho a seguir, pois devemos evitar recursos perfeitamente colineares, mesmo quando o número de categorias > 2. O que você acha @rpeck ?

Estava lendo isso um pouco e encontrei este link: https://inmachineswetrust.com/posts/drop-first-columns/

Principais conclusões:

  • A eliminação de colunas só é necessária ao criar um modelo OLS sem regularização (acredito que o regressor linear cai nesta categoria: https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html, https:// scikit-learn.org/stable/modules/linear_model.html#ordinary-least-squares)
  • A eliminação de colunas codificadas one-hot altera os parâmetros e as previsões de um modelo de regressão linear, afetando o modelo retornado. No entanto, é difícil para mim determinar se isso é para melhor ou não.

O primeiro comentário de RE @rpeck : "Como outras formas de colinearidade de recursos, isso atrapalha muitas coisas na interpretabilidade, porque o efeito de uma coluna de origem original é dividido entre as duas colunas OHE. Os novos rollups SHAP de Freddy abordam isso, obviamente, mas coisas como gráficos de Importância de Recurso e Dependência Parcial ainda terão o problema."

Isso faz sentido para casos binários, mas no caso em que temos várias categorias, descartar uma coluna ainda terá esse problema.

Talvez não devêssemos fazer isso por padrão, mas devemos atualizar make_pipeline para criar um OHE com first como parâmetro se o estimador for um regressor linear?

Infelizmente, não tenho uma boa compreensão da matemática subjacente para fazer o julgamento, então adoraria ouvir seus pensamentos, @freddyaboulton @rpeck @dsherry

Pós-discussão com @freddyaboulton @rpeck @dsherry @chukarsten @jeremyliweishih

  • Faremos isso apenas para casos binários.
  • Um "nice-to-have" é usar, no caso binário, é a classe minoritária, mas de outra forma apenas escolher uma das duas categorias deve ser suficiente.

@angela97lin parece um bom comportamento padrão de RE. Outro bom ter: capacidade de substituir esse comportamento padrão por meio dos parâmetros do componente

@dsherry Se estou entendendo corretamente, já que estamos atualizando o valor padrão de drop (um parâmetro), os usuários poderão substituir isso definindo o parâmetro do componente manualmente?

Cavou ao redor para ver o que era necessário para implementar isso. Em particular, eu estava curioso sobre o quão difícil seria sempre remover a classe minoritária no caso binário.

O resultado dessa escavação é:

  • Com o scikit-learn, é muito difícil selecionar qual categoria remover. A partir da documentação, isso parece viável por meio da opção de matriz para o parâmetro drop (https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html). No entanto, depois de experimentá-lo, é necessário que um valor de índice seja especificado para cada coluna. Portanto, o seguinte, que está tentando remover a categoria especificada no índice 0 para a coluna 0 e nenhum outro valor para os erros das colunas 1 e 2:
import pandas as pd
import numpy as np
from sklearn.preprocessing import OneHotEncoder

X = pd.DataFrame({'col_1': ["a", "b", "b", "a", "b"],
                      'col_2': ["a", "b", "a", "c", "b"],
                      'col_3': ["a", "a", "a", "a", "a"]})

indices_to_drop = np.array([0, None, None])

ohe = OneHotEncoder(drop=indices_to_drop)
ohe.fit(X)
ValueError                                Traceback (most recent call last)
<ipython-input-4-a099fa2fc4a7> in <module>
----> 1 ohe.fit(X)

~/Desktop/venv/lib/python3.7/site-packages/sklearn/preprocessing/_encoders.py in fit(self, X, y)
    417         self._fit(X, handle_unknown=self.handle_unknown,
    418                   force_all_finite='allow-nan')
--> 419         self.drop_idx_ = self._compute_drop_idx()
    420         return self
    421 

~/Desktop/venv/lib/python3.7/site-packages/sklearn/preprocessing/_encoders.py in _compute_drop_idx(self)
    394                                 ["Category: {}, Feature: {}".format(c, v)
    395                                     for c, v in missing_drops])))
--> 396                 raise ValueError(msg)
    397             return np.array(drop_indices, dtype=object)
    398 

ValueError: The following categories were supposed to be dropped, but were not found in the training data.
Category: 0, Feature: 0
Category: 1, Feature: None
Category: 2, Feature: None

Acredito que isso também seja metade do que esse problema aponta: https://github.com/scikit-learn/scikit-learn/issues/16511

Uma alternativa que podemos fazer para dar suporte a isso é acompanhar manualmente quais colunas e quais valores queremos eliminar durante o ajuste. Passe os dados para scikit-learn. Em seguida, remova as colunas que armazenamos e especificamos que queremos eliminar. No entanto, isso requer alguma manipulação lógica para determinar o original (recurso, valor) do nome da coluna transformada. (Temos essa lógica em get_feature_names mas isso nos ajuda a conectar os nomes das colunas assumindo que nada deve ser descartado...)

Tudo isso para dizer que talvez apenas usar o padrão scikit-learn if_binary seja suficiente por enquanto, e podemos registrar um problema separado para sempre usar a classe minoritária. Honestamente, também a favor de se afastar da implementação de OHE do scikit-learn, dado o quanto tivemos que trabalhar em torno disso.

Recursos úteis:
Documento OHE: https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html
Código no scikit-learn causando inflexibilidade: https://github.com/scikit-learn/scikit-learn/blob/95119c13af77c76e150b753485c662b7c52a41a2/sklearn/preprocessing/_encoders.py#L338
Problema relacionado: https://github.com/scikit-learn/scikit-learn/issues/16511


Para usar if_binary : scikit-learn requer que handle_unknown seja error . Isso não funciona bem com nossos parâmetros top_n , que descarta tudo, exceto as categorias N principais, porque os dados a serem transformados não saberão o que fazer com as novas categorias. Como Becca observou em https://github.com/alteryx/evalml/pull/830 , teríamos que definir top_n como Nenhum para que esses parâmetros funcionassem.

Com isso em mente, talvez seja melhor apenas rolar nosso próprio imp 🤔

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