Xgboost: [Novo recurso] Restrições monotônicas na construção de árvores

Criado em 27 ago. 2016  ·  46Comentários  ·  Fonte: dmlc/xgboost

Recebi alguns pedidos sobre o suporte de restrições monotônicas em certos recursos com relação à saída,

ou seja, quando outros recursos são fixos, força a previsão a ser monotônica, aumentando em relação a determinado recurso especificado. Estou abrindo esta edição para ver o interesse geral sobre esse recurso. Posso adicionar isso se houver interesse suficiente sobre isso,

Eu precisaria da ajuda de voluntários da comunidade para testar o recurso beta e contribuir com documentos e tutoriais sobre o uso desse recurso. Por favor, responda a questão se você estiver interessado

Comentários muito úteis

Atualmente, esse recurso não está na API Sklearn. Você ou alguém pode ajudar a adicioná-lo? Obrigado!

Todos 46 comentários

Uma versão experimental é fornecida em https://github.com/dmlc/xgboost/pull/1516. Para usar isso antes de ser mesclado, clone o repo https://github.com/tqchen/xgboost ,

Ative as seguintes opções (provavelmente possível via python, r API)

monotone_constraints = "(0,1,1,0)"

Existem dois argumentos

  • monotone_constraints é uma lista em comprimento de número de características, 1 indica aumento monotônico, - 1 significa diminuição, 0 significa nenhuma restrição. Se for menor que o número de recursos, 0 será preenchido.

    • Atualmente ele suporta o formato de tupla do Python, você pode passar as coisas como string ao usar r

Coisas para verificar

  • [x] A velocidade dos impulsionadores da árvore original não diminui (eu mudei um pouco a estrutura do código, em teoria a otimização dos modelos irá colocá-los em linha, mas é necessário confirmar)
  • [x] A velocidade e exatidão da regressão monotônica
  • [x] O desempenho ao introduzir esta restrição

Limitações conhecidas

Atualmente suportado apenas algoritmo guloso exato em multi-core. Ainda não disponível na versão distribuída

@tqchen Recebi um pedido no trabalho hoje para construir alguns GBMs com restrições monótonas para testar em comparação com o desempenho de alguns outros modelos. Isso seria com uma perda de desvio de tweedie, então eu teria que ir com uma função de perda personalizada como está hoje.

Em qualquer caso, parece uma boa chance de ajudar e fazer algum trabalho ao mesmo tempo.

Com base no que falamos aqui , GBM (R Package) apenas impõe monotonicidade localmente.
Você poderia esclarecer como o XGBoost impõe restrições monotônicas?
Seria ótimo se o XGBoost pudesse impor restrições globais.

Eu não entendo o que você quer dizer com restrição local ou global, você pode explicar?

Desculpe, colei o link errado, aqui está o certo (Link)
Cada árvore só pode seguir uma restrição monotônica em certo subconjunto do recurso interessado, de modo que muitas árvores se agrupam podem criar uma violação da monotonicidade geral em toda a extensão desse recurso.

OK, no meu entendimento, é aplicado globalmente. Você é bem-vindo para experimentá-lo.

Apenas fiz alguns testes simples de restrição de monotonicidade no contexto de uma regressão univariada. Você pode encontrar o código e uma breve documentação aqui:

https://github.com/XiaoxiaoWang87/xgboost_mono_test/blob/master/xgb_monotonicity_constraint_testing1-univariate.ipynb

Algumas observações iniciais:

  • Para um problema de regressão de variável única, a restrição monotônica = +1 parece funcionar bem
  • Para um problema de regressão de variável única, em meu conjunto de dados a restrição monotônica = -1 não parece produzir uma função monotonicamente decrescente. Em vez disso, dá uma constante. Mas isso também pode ser devido à falta de melhorias ao forçar a restrição. A ser confirmado (de acordo com a sugestão de Tianqi, tente inverter o conjunto de dados e definir a restrição como +1).
  • Adicionar a restrição (corretamente) pode potencialmente prevenir o sobreajuste e trazer algum benefício de desempenho / interpretação.

Acontece que eu introduzo um bug no caso de restrição = -1. Eu pressionei uma correção, por favor, veja se a versão mais recente funciona bem. Verifique também se funciona quando há várias restrições

@tqchen Eu testei sua correção para o bug decrescente, parece que está funcionando agora.

xgboost-no-constraint
xgboost-with-constraint

Vamos confirmar se há redução de velocidade em relação à versão original em algum conjunto de dados padrão, então podemos fundi-lo em

@tqchen eu testei um modelo de duas variáveis, um com restrição crescente e outro decrescente:

params_constrained = params.copy()
params_constrained['updater'] = "grow_monotone_colmaker,prune"
params_constrained['monotone_constraints'] = "(1,-1)"

Os resultados são bons

xgboost-two-vars-increasing
xgboost-two-vars-decreasing

Tentarei encontrar um tempinho para fazer alguns testes de tempo esta tarde.

Fiz uma atualização para # 1516 para permitir a detecção automática de opções montone, agora o usuário só precisa passar monotone_constraints = "(0,1,1,0)" , por favor, verifique se funciona.

Vou mesclar isso se os testes de velocidade estiverem indo bem, e vamos passar para a próxima fase de adição de tutoriais

@madrury @ XiaoxiaoWang87

Adicionados testes para o caso multivariável aqui:

https://github.com/XiaoxiaoWang87/xgboost_mono_test/blob/master/xgb_monotonicity_constraint_testing2-multivariate.ipynb

  • Confirmo agora que ambas as restrições monotônicas = 1 e = -1 funcionam como esperado.
  • Restringir a monotonicidade não leva à degradação óbvia de velocidade *
    * velocidade = média [tempo até a parada precoce / número de iterações de reforço até a parada precoce]

no constraint: 964.9 microseconds per iteration
with constraint: 861.7 microseconds per iteration

(por favor, comente se você tem uma maneira melhor de fazer o teste de velocidade)

  • É preciso ter cuidado ao restringir a direção de uma variável não monotônica. Isso pode levar à degradação do desempenho.
  • Vendo o código travar por causa de Check failed: (wleft) <= (wright) ao brincar com diferentes hiperparâmetros.

Fiz alguns experimentos de tempo em um caderno Jupyter.

Primeiro teste: alguns dados simulados simples. Existem duas características, uma crescente e outra decrescente, mas com uma pequena onda senoidal sobreposta de forma que cada característica não seja verdadeiramente monotônica

X = np.random.random(size=(N, K))
y = (5*X[:, 0] + np.sin(5*2*pi*X[:, 0])
     - 5*X[:, 1] - np.cos(5*2*pi*X[:, 1])
     + np.random.normal(loc=0.0, scale=0.01, size=N))

Aqui estão os resultados de tempo de xgboosts com e sem restrições monótonas. Desativei a parada antecipada e aumentei um determinado número de iterações para cada um.

Primeiro, sem restrições monótonas:

%%timeit -n 100
model_no_constraints = xgb.train(params, dtrain, 
                                 num_boost_round = 2500, 
                                 verbose_eval = False)

100 loops, best of 3: 246 ms per loop

E aqui com restrições de monotonicidade

%%timeit -n 100
model_with_constraints = xgb.train(params_constrained, dtrain, 
                                 num_boost_round = 2500, 
                                 verbose_eval = False)

100 loops, best of 3: 196 ms per loop

Segundo teste: dados de habitação na Califórnia da sklearn. Sem restrições

%%timeit -n 10
model_no_constraints = xgb.train(params, dtrain, 
                                 num_boost_round = 2500, 
                                 verbose_eval = False)

10 loops, best of 3: 5.9 s per loop

Aqui estão as restrições que usei

print(params_constrained['monotone_constraints'])

(1,1,1,0,0,1,0,0)

E o tempo para o modelo restrito

%%timeit -n 10
model_no_constraints = xgb.train(params, dtrain, 
                                 num_boost_round = 2500, 
                                 verbose_eval = False)

10 loops, best of 3: 6.08 s per loop

@ XiaoxiaoWang87 Eu empurrei outro PR para perder o controle sobre wleft e wright, veja se funciona.
@madrury Você também pode comparar com a versão anterior do XGBoost sem o recurso de restrição?

@tqchen Certo. Você pode recomendar um hash de commit para comparação? Devo apenas usar o commit antes de você adicionar as restrições monótonas?

Sim, o anterior servirá

@tqchen Sobre a reconstrução da versão atualizada, estou recebendo alguns erros que não tinha antes. Espero que o motivo salte para você claramente.

Se tento executar o mesmo código de antes, recebo uma exceção. Aqui está o traceback completo:

XGBoostError                              Traceback (most recent call last)
<ipython-input-14-63a9f6e16c9a> in <module>()
      8    model_with_constraints = xgb.train(params, dtrain, 
      9                                        num_boost_round = 1000, evals = evallist,
---> 10                                    early_stopping_rounds = 10)  

/Users/matthewdrury/anaconda/lib/python2.7/site-packages/xgboost-0.6-py2.7.egg/xgboost/training.pyc in train(params, dtrain, num_boost_round, evals, obj, feval, maximize, early_stopping_rounds, evals_result, verbose_eval, learning_rates, xgb_model, callbacks)
    201                            evals=evals,
    202                            obj=obj, feval=feval,
--> 203                            xgb_model=xgb_model, callbacks=callbacks)
    204 
    205 

/Users/matthewdrury/anaconda/lib/python2.7/site-packages/xgboost-0.6-py2.7.egg/xgboost/training.pyc in _train_internal(params, dtrain, num_boost_round, evals, obj, feval, xgb_model, callbacks)
     72         # Skip the first update if it is a recovery step.
     73         if version % 2 == 0:
---> 74             bst.update(dtrain, i, obj)
     75             bst.save_rabit_checkpoint()
     76             version += 1

/Users/matthewdrury/anaconda/lib/python2.7/site-packages/xgboost-0.6-py2.7.egg/xgboost/core.pyc in update(self, dtrain, iteration, fobj)
    804 
    805         if fobj is None:
--> 806             _check_call(_LIB.XGBoosterUpdateOneIter(self.handle, iteration, dtrain.handle))
    807         else:
    808             pred = self.predict(dtrain)

/Users/matthewdrury/anaconda/lib/python2.7/site-packages/xgboost-0.6-py2.7.egg/xgboost/core.pyc in _check_call(ret)
    125     """
    126     if ret != 0:
--> 127         raise XGBoostError(_LIB.XGBGetLastError())
    128 
    129 

XGBoostError: [14:08:41] src/tree/tree_updater.cc:18: Unknown tree updater grow_monotone_colmaker

Se eu mudar tudo para o argumento de palavra-chave que você implementou, também recebo um erro:

TypeError                                 Traceback (most recent call last)
<ipython-input-15-ef7671f72925> in <module>()
      8                                    monotone_constraints="(1)",
      9                                    num_boost_round = 1000, evals = evallist,
---> 10                                    early_stopping_rounds = 10)  

TypeError: train() got an unexpected keyword argument 'monotone_constraints'

remova o argumento do atualizador e mantenha os argumentos das restrições monótonas nos parâmetros, agora que o atualizador da restrição monótona é ativado automaticamente quando as restrições monótonas são apresentadas

@tqchen Meu amigo @amontz me ajudou a descobrir isso imediatamente depois de postar a mensagem. Eu interpretei seu comentário como passando monotone_constraints como um kwarg para .train .

Funciona com esses ajustes. Obrigado.

@madrury você pode confirmar a velocidade?

Também @madrury e @ XiaoxiaoWang87, uma vez que este recurso está perto de ser mesclado, seria ótimo se você pudesse se coordenar para criar um tutorial apresentando esse recurso aos usuários.

Não podemos levar o notebook ipy diretamente para o repositório principal. mas as imagens podem ser enviadas para https://github.com/dmlc/web-data/tree/master/xgboost e marcadas para o repo principal.

Também precisamos alterar a conversão de string da interface de front-end, para que int tupla possa ser convertida no formato de tupla de string que pode ser aceito pelo back-end.

@ hetong007 para mudanças em R e @slundberg para Julia

@tqchen Julia está atualmente anexada à versão 0.4 do XGBoost, então da próxima vez que eu precisar usá-la e tiver tempo reservado, irei atualizar as ligações se ninguém mais tiver feito isso. Nesse ponto, essa alteração também pode ser adicionada.

Aqui está a comparação entre os modelos _sem_ uma restrição monótona de antes da implementação até depois.

Commit 8cac37 : Antes da implementação da restrição monótona. '
Dados simulados : 100 loops, best of 3: 232 ms per loop
Dados da Califórnia : 10 loops, best of 3: 5.89 s per loop

Confirmar b1c224 : Após a implementação da restrição monótona.
Dados simulados : 100 loops, best of 3: 231 ms per loop
Dados da Califórnia : 10 loops, best of 3: 5.61 s per loop

A aceleração para a Califórnia após a implementação parece suspeita para mim, mas tentei duas vezes para cada lado e é consistente.

Eu ficaria feliz em escrever um tutorial. Vou dar uma olhada na documentação existente e montar algo nos próximos dias.

Isso é ótimo, o PR agora está oficialmente mesclado com o mestre. Ansioso para ver o tutorial

Obrigado @madrury. Espero por isso. Deixe-me saber o que posso ajudar. Eu certamente gostaria de ter mais estudos sobre esse assunto.

Vou aprimorá-lo amanhã. Estou apenas curioso sobre o motivo da comunicação com o C ++ por meio de uma string em vez de uma matriz.

Estou testando a partir de R. Gerei dados de duas variáveis ​​aleatoriamente e tento fazer uma previsão.

No entanto, descobri que

  1. xgboost não restringe a previsão.
  2. o parâmetro monotone_constraints torna a previsão ligeiramente diferente.

Por favor, indique se eu cometi algum erro.

O código para reproduzi-lo (testado na versão mais recente do github , não em drat ):

set.seed(1024)
x1 = rnorm(1000, 10)
x2 = rnorm(1000, 10)
y = -1*x1 + rnorm(1000, 0.001) + 3*sin(x2)
train = cbind(x1, x2)

bst = xgboost(data = train, label = y, max_depth = 2,
                   eta = 0.1, nthread = 2, nrounds = 10,
                   monotone_constraints = '(1,-1)')

pred = predict(bst, train)
ind = order(train[,1])
pred.ord = pred[ind]
plot(train[,1], y, main = 'with constraint')
pred.ord = pred[order(train[,1])]
lines(pred.ord)

wc

bst = xgboost(data = train, label = y, max_depth = 2,
                   eta = 0.1, nthread = 2, nrounds = 10)

pred = predict(bst, train)
ind = order(train[,1])
pred.ord = pred[ind]
plot(train[,1], y, main = 'without constraint')
pred.ord = pred[order(train[,1])]
lines(pred.ord)

woc

A restrição foi feita no pedido parcial. Portanto, a restrição só é aplicada se estivermos movendo o eixo montone, mantendo outro eixo fixo

@ hetong007 Para fazer minhas plotagens eu

  • Criei uma matriz contendo a grade de coordenadas x em que eu queria prever essa variável e depois juntei-a no gráfico de linha. Isso usaria seq em R.
  • Defina todas as outras variáveis ​​iguais aos seus valores médios nos dados de treinamento. Isso seria algo como colmeans em R.

Aqui está o código python que usei para os gráficos que incluí acima; ele deve ser facilmente convertido em código R equivalente.

def plot_one_feature_effect(model, X, y, idx=1):

    x_scan = np.linspace(0, 1, 100)    
    X_scan = np.empty((100, X.shape[1]))
    X_scan[:, idx] = x_scan

    left_feature_means = np.tile(X[:, :idx].mean(axis=0), (100, 1))
    right_feature_means = np.tile(X[:, (idx+1):].mean(axis=0), (100, 1))
    X_scan[:, :idx] = left_feature_means
    X_scan[:, (idx+1):] = right_feature_means

    X_plot = xgb.DMatrix(X_scan)
    y_plot = model.predict(X_plot, ntree_limit=bst.best_ntree_limit)

    plt.plot(x_scan, y_plot, color = 'black')
    plt.plot(X[:, idx], y, 'o', alpha = 0.25)

Aqui está como faço os gráficos de dependência parcial (para um modelo arbitrário):

  • Faça a varredura em uma grade de valores para o recurso X.
  • Para cada valor de grade do recurso X:

    • Defina toda a coluna X do recurso (todas as linhas) com este valor. Outros recursos inalterados.

    • Faça previsões para todas as linhas.

    • Faça a média da previsão.

  • Os pares resultantes (valor do recurso X, previsão média) fornecem a dependência parcial do recurso X.

Código:

def plot_partial_dependency(bst, X, y, f_id):

    X_temp = X.copy()

    x_scan = np.linspace(np.percentile(X_temp[:, f_id], 0.1), np.percentile(X_temp[:, f_id], 99.5), 50)
    y_partial = []

    for point in x_scan:

        X_temp[:, f_id] = point

        dpartial = xgb.DMatrix(X_temp[:, feature_ids])
        y_partial.append(np.average(bst.predict(dpartial)))

    y_partial = np.array(y_partial)

    # Plot partial dependence

    fig, ax = plt.subplots()
    fig.set_size_inches(5, 5)
    plt.subplots_adjust(left = 0.17, right = 0.94, bottom = 0.15, top = 0.9)

    ax.plot(x_scan, y_partial, '-', color = 'black', linewidth = 1)
    ax.plot(X[:, f_id], y, 'o', color = 'blue', alpha = 0.02)

    ax.set_xlim(min(x_scan), max(x_scan))
    ax.set_xlabel('Feature X', fontsize = 10)    
    ax.set_ylabel('Partial Dependence', fontsize = 12)

Obrigado pela orientação! Percebi que cometi um erro bobo na trama. Aqui está outro teste em dados univariados, o gráfico parece bom:

set.seed(1024)
x = rnorm(1000, 10)
y = -1*x + rnorm(1000, 0.001) + 3*sin(x)
train = matrix(x, ncol = 1)

bst = xgboost(data = train, label = y, max_depth = 2,
               eta = 0.1, nthread = 2, nrounds = 100,
               monotone_constraints = '(-1)')
pred = predict(bst, train)
ind = order(train[,1])
pred.ord = pred[ind]
plot(train[,1], y, main = 'with constraint', pch=20)
lines(train[ind,1], pred.ord, col=2, lwd = 5)

rplot

bst = xgboost(data = train, label = y, max_depth = 2,
               eta = 0.1, nthread = 2, nrounds = 100)
pred = predict(bst, train)
ind = order(train[,1])
pred.ord = pred[ind]
plot(train[,1], y, main = 'without constraint', pch=20)
lines(train[ind,1], pred.ord, col=2, lwd = 5)

woc

@ hetong007 Portanto, o objetivo na interface R é permitir que o usuário passe o array R além das strings

monotone_constraints=c(1,-1)

Por favor, deixe-nos saber quando você for o PR do tutorial

@ hetong007 Você também é mais do que bem-vindo por fazer uma versão r-blogger

@tqchen Desculpe, pessoal, estive em viagem de trabalho esta semana.

Enviei algumas solicitações de pull para um tutorial de restrição monotônica. Por favor, deixe-me saber o que você pensa, fico feliz com qualquer crítica ou crítica.

Esperançosamente, é apropriado perguntar aqui: isso funcionará agora se atualizarmos usando o git clone --recursive https://github.com/dmlc/xgboost usual?

Eu pergunto quando vi o novo tutorial, mas nada de novo sobre uma mudança no código em si. Obrigado a todos!

sim, o novo recurso é mesclado antes que o tutorial seja mesclado

Olá,

Não tenho certeza se você implementou com sucesso a montonicidade global, pelo que vi em seu código, corresponde mais a uma monotonicidade local.

Aqui está um exemplo simples para quebrar a monotonicidade:

`
df <- data.frame (y = c (2, rep (6.100), 1, rep (11.100)),
x1 = c (rep (1.101), rep (2.101)), x2 = c (1, rep (2.100), 1, rep (2.100)))

biblioteca (xgboost)
set.seed (0)
XGB <- xgboost (data = data.matrix (df [, - 1]), label = df [, 1],
objetivo = " reg: linear ",
bag.fraction = 1, nround = 100, monotone_constraints = c (1,0),
eta = 0,1)

sans_corr <- data.frame (x1 = c (1,2,1,2), x2 = c (1,1,2,2))

sans_corr $ prediction <- predict (XGB, data.matrix (sans_corr))
`

Espero que minha compreensão do seu código e meu exemplo não sejam falsos

Atualmente, esse recurso não está na API Sklearn. Você ou alguém pode ajudar a adicioná-lo? Obrigado!

É possível impor monotonicidade geral em uma variável, sem especificar se ela deve ser crescente ou decrescente?

@davidADSP você pode fazer uma verificação de correlação de Spearman no preditor e alvo desejados para ver se aumentar ou diminuir é apropriado.

Este recurso parece ser inválido quando 'tree_method': 'hist'. @tqchen alguma ajuda? Obrigado a todos.

Como a restrição funciona para objetivos multiclasse como o mlogloss? A restrição de monotonicidade é suportada para perda multiclasse? Se sim, como isso é aplicado. (Como para cada classe existe uma árvore)

Existe algum artigo sobre o algoritmo de monoticidade aplicado no XGBOOST? É global ou local? Local significa específico para certos nós, mas nós em outras partes da árvore podem criar uma violação da monotonicidade geral. Alguém também pode me ajudar a entender a linha L412-417 . Por que "w" é limitado - superior e inferior. Como isso ajuda a manter a monotonicidade. Linha 457 - Por que "mid" é usado?

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