Xgboost: Como o XGBoost gera probabilidades de classe em um problema de classificação multiclasse?

Criado em 8 nov. 2016  ·  15Comentários  ·  Fonte: dmlc/xgboost

Quando tento descartar uma árvore XGBoost treinada, obtenho o seguinte formato:

0:[f37<39811] 
    1:[f52<199.5] 
        3:leaf=-0.021461
        4:[f2<0.00617284] 
            9:leaf=-0.0118755
            10:[f35<-83.548] 
                19:[f13<0.693844] 
                    23:[f37<1831]
                        29:leaf=-0
                        30:leaf=0.123949
                    24:leaf=0.0108628
                20:[f35<-74.6198] 
                    25:leaf=-0.0175897
                    26:leaf=0.051898
    2:leaf=-0.0239901

Embora seja claro qual é a estrutura da árvore, não está claro como interpretar os valores das folhas. Para um problema de classificação _ binário _ e uma função de custo de perda logarítmica, os valores folha podem ser convertidos em probabilidades de classe usando uma função logística: 1 / (1 + exp (valor)). No entanto, para um problema de classificação de _multiclass_ , não há informações sobre a qual classe esses valores pertencem e, sem essas informações, não é claro como calcular as probabilidades de classe.

Alguma ideia? Ou existe alguma outra função para obter essas informações das árvores treinadas?

Comentários muito úteis

Essa é uma observação muito boa, que eu não tinha notado antes. É verdade, o número de árvores no modelo é n_estimator x n_class . No entanto, para descobrir a ordem das árvores, usei o seguinte exemplo de brinquedo:

x = np.concatenate([np.ones([10,1])*np.array([1,0,0]),np.ones([10,1])*np.array([0,1,0]),np.ones([10,1])*np.array([0,0,1])], axis=0)
y = np.array([['a']*10+['c']*10+['b']*10]).reshape([30,1])
model = xgb.XGBClassifier(n_estimators=2, objective='mlogloss').fit(x, y)
model.booster().dump_model('trees.txt')

que basicamente cria um conjunto de dados de treinamento onde [1,0,0] é mapeado para 'a', [0,1,0] é mapeado para 'c' e [0,0,1] é mapeado para 'b'. Eu intencionalmente troquei a ordem de 'b' e 'c' para ver se o xgboost classifica as classes com base em seus rótulos antes de descartar as árvores.

Aqui está o modelo de dumping resultante:

booster[0]:
0:[f0<0.5] yes=1,no=2,missing=1
    1:leaf=-0.0674157
    2:leaf=0.122449
booster[1]:
0:[f2<0.5] yes=1,no=2,missing=1
    1:leaf=-0.0674157
    2:leaf=0.122449
booster[2]:
0:[f1<0.5] yes=1,no=2,missing=1
    1:leaf=-0.0674157
    2:leaf=0.122449
booster[3]:
0:[f0<0.5] yes=1,no=2,missing=1
    1:leaf=-0.0650523
    2:leaf=0.10941
booster[4]:
0:[f2<0.5] yes=1,no=2,missing=1
    1:leaf=-0.0650523
    2:leaf=0.10941
booster[5]:
0:[f1<0.5] yes=1,no=2,missing=1
    1:leaf=-0.0650523
    2:leaf=0.10941

e aqui estão as conclusões:

  1. Como você mencionou, embora n_estimators = 2, o número de árvores é 2x3 = 6.
  2. Olhando para a ordem dos recursos que as árvores usam, parece que a primeira árvore pertence à primeira classe, a segunda árvore à segunda classe e assim por diante, até a última classe. Em seguida, o mesmo padrão é repetido até que todos os estimadores sejam cobertos.
  3. A ordem das classes parece ser consistente com o que numpy.unique() retorna - aqui está em ordem alfabética, e é por isso que a primeira árvore está usando f0 enquanto a segunda está usando f2 .

Seria bom se essas informações fossem adicionadas à documentação do xgboost.

Todos 15 comentários

Eu também tenho a mesma pergunta. A única coisa que notei é que se você tiver 20 classes e definir o número de estimadores para 100, terá 20 * 100 = 2.000 árvores impressas em seu modelo. Agora, meu palpite é que os primeiros 100 estimadores classificam a primeira classe em relação aos outros. Os próximos 100 estimadores classificam a segunda classe versus outros, etc.

Não é possível confirmar, mas talvez possa ser algo assim?

Essa é uma observação muito boa, que eu não tinha notado antes. É verdade, o número de árvores no modelo é n_estimator x n_class . No entanto, para descobrir a ordem das árvores, usei o seguinte exemplo de brinquedo:

x = np.concatenate([np.ones([10,1])*np.array([1,0,0]),np.ones([10,1])*np.array([0,1,0]),np.ones([10,1])*np.array([0,0,1])], axis=0)
y = np.array([['a']*10+['c']*10+['b']*10]).reshape([30,1])
model = xgb.XGBClassifier(n_estimators=2, objective='mlogloss').fit(x, y)
model.booster().dump_model('trees.txt')

que basicamente cria um conjunto de dados de treinamento onde [1,0,0] é mapeado para 'a', [0,1,0] é mapeado para 'c' e [0,0,1] é mapeado para 'b'. Eu intencionalmente troquei a ordem de 'b' e 'c' para ver se o xgboost classifica as classes com base em seus rótulos antes de descartar as árvores.

Aqui está o modelo de dumping resultante:

booster[0]:
0:[f0<0.5] yes=1,no=2,missing=1
    1:leaf=-0.0674157
    2:leaf=0.122449
booster[1]:
0:[f2<0.5] yes=1,no=2,missing=1
    1:leaf=-0.0674157
    2:leaf=0.122449
booster[2]:
0:[f1<0.5] yes=1,no=2,missing=1
    1:leaf=-0.0674157
    2:leaf=0.122449
booster[3]:
0:[f0<0.5] yes=1,no=2,missing=1
    1:leaf=-0.0650523
    2:leaf=0.10941
booster[4]:
0:[f2<0.5] yes=1,no=2,missing=1
    1:leaf=-0.0650523
    2:leaf=0.10941
booster[5]:
0:[f1<0.5] yes=1,no=2,missing=1
    1:leaf=-0.0650523
    2:leaf=0.10941

e aqui estão as conclusões:

  1. Como você mencionou, embora n_estimators = 2, o número de árvores é 2x3 = 6.
  2. Olhando para a ordem dos recursos que as árvores usam, parece que a primeira árvore pertence à primeira classe, a segunda árvore à segunda classe e assim por diante, até a última classe. Em seguida, o mesmo padrão é repetido até que todos os estimadores sejam cobertos.
  3. A ordem das classes parece ser consistente com o que numpy.unique() retorna - aqui está em ordem alfabética, e é por isso que a primeira árvore está usando f0 enquanto a segunda está usando f2 .

Seria bom se essas informações fossem adicionadas à documentação do xgboost.

Estou encerrando este problema agora.

Eu ainda tenho um comentário aqui. Eu concordo com a forma como as árvores são estruturadas no caso de várias classes. Mas como os valores-folha são convertidos em probabilidades?

Parece que para o caso binário você deve aplicar 1 / (1 + exp (valor)), enquanto para o caso multiclasse você deve aplicar 1 / (1 + exp (-valor)).

Isso parece ser verdade, olhando para os exemplos. Obrigado pela dica!

Mas eu não entendo por que eles são definidos de forma diferente para casos binários e multiclasse.

Sim, é muito estranho e confuso para dizer o mínimo.

Cheguei a essa conclusão testando-o no UCI Car Dataset (onde você classifica os carros em inaceitáveis, aceitáveis, bons ou muito bons).

Seria bom ter um recurso extra onde você pode converter as árvores em árvores sklearn ou algo assim (atualmente eu tenho uma base de código onde posso converter essas árvores xgb e árvores sklearn em minha própria árvore de decisão definida). Destas últimas, as árvores sklearn, tenho pelo menos 100% de certeza de que foram convertidas corretamente. Para as árvores xgb, a dúvida permanece.

Concordo que isso seria um ótimo recurso.

@GillesVandewiele @sosata : então nessa logística (digamos para a classe i ) [ 1/(1+exp(-value)) ], value é a soma de todas as pontuações de folha correspondentes às árvores associadas a essa classe particular com a amostra?

Para o exemplo em meu comentário anterior, se eu tentar prever as probabilidades de classe para [1 0 0]:

print(model.predict_proba(np.array([[1,0,0]])))

irá gerar estes resultados:

[[ 0.41852772  0.29073614  0.29073614]]

Não há como 1/(1+exp(-value)) gerar isso. A única maneira é que essas probabilidades sejam geradas por uma função _softmax_ dos valores somados nas classes:

p[i] = exp(val[i])/(exp(val[1])+exp(val[2])+...+exp(val[N]))

onde i é a classe de destino (de N classes), e val [i] é a soma de todos os valores gerados a partir das árvores pertencentes a essa classe.
Em nosso exemplo:

print(np.exp(+0.122449+0.10941)/(np.exp(+0.122449+0.10941)+np.exp(-0.0674157-0.0650523)+np.exp(-0.0674157-0.0650523)))
print(np.exp(-0.0674157-0.0650523)/(np.exp(+0.122449+0.10941)+np.exp(-0.0674157-0.0650523)+np.exp(-0.0674157-0.0650523)))
print(np.exp(-0.0674157-0.0650523)/(np.exp(+0.122449+0.10941)+np.exp(-0.0674157-0.0650523)+np.exp(-0.0674157-0.0650523)))

irá gerar:

0.418527719063
0.290736140469
0.290736140469

que é exatamente o que a função predict_proba () nos deu.

Isso parece estar correto @sosata

No meu caso, eu queria converter cada uma das árvores individualmente (portanto, não usando os valores das folhas de outras árvores). Lá, uma função sigmóide parecia fazer o trabalho.

@sosata Existe

@mlxai Eu também tentei apenas a API Python e não me lembro que consegui obtê-los por aula.

No entanto, na minha opinião, a definição da importância do recurso no XGBoost como o número total de divisões desse recurso é um pouco simplista. Pessoalmente, eu calculo a importância do recurso removendo esse recurso do conjunto de recursos e calculando a queda (ou aumento) resultante na precisão, quando o modelo é treinado e testado sem esse recurso. Eu faço isso para todos os recursos e defino "importância do recurso" como a quantidade de queda (ou aumento negativo) na precisão. Como o treinamento e o teste podem ser feitos várias vezes com diferentes subconjuntos de treinamento / teste para cada remoção de recurso, também se pode estimar os intervalos de confiança da importância do recurso. Além disso, ao calcular a importância separadamente para cada classe, você obtém importâncias de recursos por classe. Isso produziu resultados mais significativos em meus projetos do que contar o número de divisões.

@sosata Seu exemplo é muito intuitivo, mas ainda não sei como você consegue as notas de cada aula, pode me explicar um pouco? Obrigado!

@chrisplyn Você avalia todas as árvores de decisão em seu conjunto pertencentes a uma classe específica e soma as pontuações resultantes.

Como @sosata afirmou corretamente anteriormente: o número de árvores em seu conjunto será igual a n_esimators * n_classes

@sosata, seu exemplo é muito claro. Muito obrigado!

@chrisplyn A instância de previsão [1,0,0] será classificada por reforço [0 ~ 5] e obterá os valores de folha [0,122449, -0,0674157, -0,0674157, 0,10941, -0,0650523, -0,0650523] respectivamente.
Para o reforço [0, 3] pertence à classe 0, o reforço [1, 4] pertence à classe 1 e o reforço [2, 5] pertence à classe 2, então val [0] = (0,122449 + 0,10941), val [1 ] = (-0,0674157 + -0,0650523), val [2] = (-0,0674157 + -0,0650523).
Está mais claro?

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