Xgboost: predição diferente em v1.2.1 e branch master

Criado em 6 nov. 2020  ·  35Comentários  ·  Fonte: dmlc/xgboost

Estou usando a versão 1.2.1 lançada para desenvolvimento devido à sua fácil instalação em python e também escrevo um código c simples para previsão usando o c_api. Se vinculado a v1.2.1 libxgboost.so, a diferença das previsões entre python e c é exatamente zero. No entanto, se vinculado a libxgboost.so do branch master (commit f3a425398 em 5 de novembro de 2020), há uma diferença.

Gostaria de implantar o código c no sistema real usando o branch master, pois gostaria de construir uma lib estática, e agora a diferença das previsões entre a v1.2.1 e o branch master me atrapalha.

Obrigado.

wontfix

Comentários muito úteis

@ShvetsKS

Consideramos essa diferença crítica no futuro? Parece que é uma restrição significativa não permitir a alteração da sequência de operações de ponto flutuante para inferência

Na verdade, nós ( @RAMitchell , @trivialfis e eu) concordamos com você aqui. A obrigatoriedade da reprodutibilidade exata da previsão prejudicará seriamente nossa capacidade de fazer alterações. A aritmética de ponto flutuante é notoriamente não associativa, então a soma de uma lista de números será ligeiramente diferente dependendo da ordem de adição.

Eu fiz um experimento para quantificar o quanto a previsão muda entre o XGBoost 1.2.0 e o último branch master :
Distribution of change in prediction

Eu gerei dados com 1000 sementes aleatórias diferentes e, em seguida, executei a previsão com as 1000 matrizes, usando as versões 1.2.0 e master. A mudança na previsão muda ligeiramente entre as sementes, mas a diferença nunca é maior que 9,2e-7, então provavelmente a mudança na previsão foi causada por aritmética de ponto flutuante e não um erro lógico .

Script para experimento

** test.py **: Gere 1000 matrizes com diferentes sementes aleatórias e executa a previsão para elas.

import numpy as np
import xgboost as xgb
import argparse

def main(args):
    m2 = xgb.Booster({'nthread': '4'})  # init model
    m2.load_model('xgb.model.bin')  # load data
    out = {}
    for seed in range(1000):
        rng = np.random.default_rng(seed=seed)
        rx = rng.standard_normal(size=(100, 127 + 7 + 1))
        rx = rx.astype(np.float32, order='C')
        dtest = xgb.DMatrix(rx, missing=0.0)
        out[str(seed)] = m2.predict(dtest)
    np.savez(args.out_pred, **out)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--out-pred', type=str, required=True)
    args = parser.parse_args()
    main(args)
Comando: `python test.py --out-pred [out.npz]`. Certifique-se de que seu env Python tenha a versão correta do XGBoost. Vamos supor que `xgb120.npz` armazene o resultado para o XGBoost 1.2.0 e` xgblatest.npz` armazene o resultado para o mestre mais recente. ** compare.py **: Faça um gráfico de histograma para a diferença de previsão
import numpy as np
import matplotlib.pyplot as plt

xgb120 = np.load('xgb120.npz')
xgblatest = np.load('xgblatest.npz')

percentile_pts = [50, 90, 99]

colors = ['tab:cyan', 'tab:olive', 'tab:green', 'tab:pink']

percentile = {}
for x in percentile_pts:
    percentile[x] = []
percentile['max'] = []

for seed in range(1000):
    diff = np.abs(xgb120[str(seed)] - xgblatest[str(seed)])
    t = np.percentile(diff, percentile_pts)
    for x, y in zip(percentile_pts, t):
        percentile[x].append(y)
    percentile['max'].append(np.max(diff))

bins = np.linspace(0, np.max(percentile['max']), 100)
idx = 0
for x in percentile_pts:
    plt.hist(percentile[x], label=f'Percentile {x}%', bins=bins, alpha=0.8, color=colors[idx])
    idx += 1
plt.hist(percentile['max'], label='Max', bins=bins, alpha=0.8, color=colors[idx])
plt.legend(loc='best')
plt.title('Distribution in prediction difference between XGBoost 1.2.0\nand master branch, tried over seed=[0..1000]')
plt.xlabel('Absolute difference')
plt.ylabel('Frequency')
plt.savefig('foobar.png', dpi=100)

Todos 35 comentários

Você pode postar um programa de exemplo para que possamos reproduzir o bug?

Além disso, você vê diferentes previsões da versão mestre do pacote Python e da API C?

Há alguma otimização feita no preditor de CPU, pode gerar resultados diferentes por erro de ponto flutuante diferente. Mas sim, você tem um exemplo reproduzível?

Além disso, você vê diferentes previsões da versão mestre do pacote Python e da API C?

Estou comparando a v1.2.1 do pacote python e o branch master da API C.

@ 7starsea Você também pode comparar as saídas do Python e da API C, ambas do branch master? O problema pode ser a maneira como as funções da API C são usadas em seu aplicativo.

@ 7starsea Você também pode comparar as saídas do Python e da API C, ambas do branch master? O problema pode ser a maneira como as funções da API C são usadas em seu aplicativo.

Eu comparei o python e c api, ambos da v1.2.1 e as previsões são exatamente as mesmas.

@ 7starsea Entendi. Se você postar programas Python e C que prevêem do mesmo modelo, poderemos solucionar o problema mais detalhadamente.

@ hcho3 aqui está o código de teste

https://github.com/7starsea/xgboost-testing

@ 7starsea Acabei de

difference: [0. 0. 0. 0.] 0.0 0.0

Usei o commit mais recente do XGBoost (debeae2509d90ec1d3402a3a185fba7a25113ff1).

@ 7starsea Acabei de

difference: [0. 0. 0. 0.] 0.0 0.0

Usei o último commit do XGBoost ( debeae2 ).

Interessante, sua versão do python é v1.2.1?

Eu ainda tenho algumas diferenças entre python-versão 1.2.1 e c api vinculada ao XGBoost ( debeae2 ).

@ 7starsea Não, compilei o XGBoost da fonte mais recente (commit debeae2509d90ec1d3402a3a185fba7a25113ff1), então é mais recente que v1.2.1. Meu pacote XGBoost Python imprime 1.3.0-SNAPSHOT para o campo xgboost.__version__ .

@ hcho3 Estou pensando se o XGBoost deve manter uma previsão consistente entre as diferentes versões (pelo menos versões consecutivas)?
Também esperando o lançamento da v1.3.0.
(parece que preciso treinar o modelo usando o branch master agora)
Obrigado pelo seu tempo.

@ 7starsea Se você carregar um modelo salvo de uma versão anterior, deverá ser capaz de obter a previsão consistente.

Não consegui reproduzir o problema usando seu script. Você pode tentar construir uma imagem Docker ou uma imagem VM e compartilhá-la comigo?

@ 7starsea FYI, também tentei construir o XGBoost 1.2.1 a partir da fonte, da seguinte maneira:

git clone --recursive https://github.com/dmlc/xgboost -b release_1.2.0 xgb_source
cd xgb_source
mkdir build
cd build
cmake ..
make
cd ../python-package
python setup.py install

Os resultados mostram novamente difference: [0. 0. 0. 0.] 0.0 0.0

Para ver a diferença, você precisa de duas versões do XGBoost, v1.2.1 para python e

dtest = xgb.DMatrix(rx, missing=0.0)
y1 = m2.predict(dtest)   # # internally using libxgboost.so  v1.2.1

e um para cpp

m1 = XgbShannonPredictor(fname)
y2 = m1.predict(rx2)   # # internally using libxgboost.so from the master branch (debeae2)

Vou tentar construir uma imagem docker (o que é novo para mim).

Deixe-me ver.

Vou tentar construir uma imagem docker (o que é novo para mim).

Não é necessário.

Na verdade, consegui reproduzir o problema. Acontece que a versão dev do XGBoost produz uma previsão diferente do XGBoost 1.2.0. E o problema é simples de reproduzir; não há necessidade de usar a API C.

Exemplo reproduzível (EDITAR: configurando a semente aleatória):

import numpy as np
import xgboost as xgb

rng = np.random.default_rng(seed=2020)
rx = rng.standard_normal(size=(100, 127 + 7 + 1))
rx = rx.astype(np.float32, order='C')

m2 = xgb.Booster({'nthread': '4'})  # init model
m2.load_model('xgb.model.bin')  # load data

dtest = xgb.DMatrix(rx, missing=0.0)
y1 = m2.predict(dtest)
print(xgb.__version__)
print(y1)

Saída de 1.2.0:

1.2.0
[ 0.00698659 -0.00211251  0.00180039 -0.00016004  0.00526169  0.00801963
  0.00016755  0.00226218  0.00276762  0.00408182  0.00303206  0.00291929
  0.01101092  0.0068329   0.00145864  0.00326979  0.00572816  0.01019934
  0.00074345  0.00784767  0.00173795 -0.00219297  0.0060181   0.00606489
  0.00447372  0.00103396  0.00932363  0.00230178  0.00389203  0.00151157
  0.0034163   0.00821933  0.006686    0.00630778  0.00331488  0.00775066
  0.00443819  0.01030204  0.00924486  0.00645933  0.00777653  0.00231206
  0.00457835  0.00390425  0.00947028  0.00410065  0.00220913  0.00292507
  0.00637993  0.00796807  0.00140873  0.00887537  0.00496858  0.01049942
  0.00908098  0.00332722  0.00799242  0.00228494  0.00463879  0.00213429
  0.00729388  0.01049232  0.00790522  0.01269361 -0.00425893  0.00256333
  0.00859573  0.00472835  0.00077197  0.00191873  0.01546788  0.0014475
  0.00888193  0.00648022  0.00115797  0.00351191  0.00580138  0.00614035
  0.00632426  0.00408354  0.00346044 -0.00034332  0.00599384  0.00302595
  0.00657633  0.01086903  0.00625807  0.00096565  0.00061804  0.00038511
  0.00523874  0.00633043  0.00379965  0.00302553 -0.00123322  0.00153473
  0.00725579  0.00836438  0.01295918  0.00737873]

Observação. executar o script com XGBoost 1.0.0 e 1.1.0 resulta na saída idêntica a 1.2.0.

Saída da versão dev (c5645180a6afb9d3d771165e681985fe3522adf6)

1.3.0-SNAPSHOT
[ 0.00698666 -0.00211278  0.00180034 -0.00016027  0.00526194  0.00801962
  0.00016758  0.00226211  0.00276773  0.00408198  0.00303223  0.00291933
  0.01101091  0.00683288  0.00145871  0.00326988  0.00572827  0.01019943
  0.00074329  0.00784767  0.00173803 -0.00219286  0.00601804  0.00606472
  0.00447388  0.00103391  0.00932358  0.00230171  0.003892    0.00151177
  0.00341637  0.00821943  0.00668607  0.00630774  0.00331502  0.00775074
  0.0044381   0.01030211  0.00924495  0.00645958  0.00777672  0.00231205
  0.00457842  0.00390424  0.00947046  0.00410091  0.0022092   0.00292498
  0.00638005  0.00796804  0.00140869  0.00887531  0.00496863  0.01049942
  0.00908096  0.00332738  0.00799218  0.00228496  0.004639    0.00213413
  0.00729368  0.01049243  0.00790528  0.01269368 -0.00425872  0.00256319
  0.00859569  0.00472848  0.0007721   0.00191874  0.01546813  0.00144742
  0.00888212  0.00648021  0.00115819  0.00351191  0.00580168  0.00614044
  0.00632418  0.0040833   0.00346038 -0.00034315  0.00599405  0.00302578
  0.0065765   0.01086897  0.00625799  0.00096572  0.00061766  0.00038494
  0.00523901  0.00633054  0.00379964  0.00302567 -0.00123339  0.00153471
  0.00725584  0.00836433  0.01295913  0.00737863]

@ hcho3 Deseja dar uma olhada nisso? Posso ajudar na divisão, se necessário.

Espere um segundo, esqueci de definir a semente aleatória no meu repro. Tola eu.

Eu atualizei meu repro com a semente aleatória fixa. O bug ainda persiste. Tentei executar o repro atualizado com o XGBoost 1.0.0 e 1.1.0, e as previsões concordam com a previsão do XGBoost 1.2.0.

Resumidamente:

Prediction from 1.0.0
==  Prediction from 1.1.0
==  Prediction from 1.2.0
!=  Prediction from latest master

@trivialfis Sim, sua ajuda será apreciada.

Marcando isso como bloqueio.

Entendi.

Rastreado até a4ce0eae43f7e0e2f91566ef2360830b86b9fdcf. @ShvetsKS Você gostaria de dar uma olhada?

Rastreado até a4ce0ea . @ShvetsKS Você gostaria de dar uma olhada?

Certo. Você pode ajudar no treinamento do modelo do reprodutor Python:
m2.load_model('xgb.model.bin') # load data
Qual versão do XGBoost é usada para treinamento e quais parâmetros devem ser fornecidos exatamente?

@ShvetsKS Você pode obter o arquivo de modelo xgb.model.bin em https://github.com/7starsea/xgboost-testing. O modelo foi treinado com 1.0.0.

@ShvetsKS Você pode obter o arquivo de modelo xgb.model.bin em https://github.com/7starsea/xgboost-testing. O modelo foi treinado com 1.0.0.

o modelo foi realmente treinado com 1.2.1 e parâmetros

 param = {'max_depth': 8, 'eta': 0.1, 'min_child_weight': 2, 'gamma': 1e-8, 'subsample': 0.6, 'nthread': 4}

Obrigado.

Parece que a pequena diferença se deve à sequência alterada da operação de ponto flutuante.
_ Motivo exato: _
Antes de a4ce0ea, incrementamos todas as respostas das árvores na variável local psum (inicialmente igual a zero) e depois incrementamos o valor apropriado de out_preds .
Em a4ce0ea, incrementamos out_preds valores diretamente por cada resposta da árvore.

A correção está preparada: https://github.com/dmlc/xgboost/pull/6384

@ 7starsea, obrigado por encontrar a diferença, você poderia verificar a correção acima?

@ hcho3 , @trivialfis Consideramos essa diferença crítica no futuro? Parece que é uma restrição significativa não permitir a alteração da sequência de operações de ponto flutuante para inferência. Mas para a fase de treinamento não existe tal requisito, pelo que me lembro.

Consideramos essa diferença crítica no futuro?

Normalmente não. Deixe-me dar uma olhada em suas mudanças. ;-)

@ShvetsKS acabei de verificar e a diferença é exatamente zero agora. Obrigado por corrigir a diferença de previsão.

@ShvetsKS

Consideramos essa diferença crítica no futuro? Parece que é uma restrição significativa não permitir a alteração da sequência de operações de ponto flutuante para inferência

Na verdade, nós ( @RAMitchell , @trivialfis e eu) concordamos com você aqui. A obrigatoriedade da reprodutibilidade exata da previsão prejudicará seriamente nossa capacidade de fazer alterações. A aritmética de ponto flutuante é notoriamente não associativa, então a soma de uma lista de números será ligeiramente diferente dependendo da ordem de adição.

Eu fiz um experimento para quantificar o quanto a previsão muda entre o XGBoost 1.2.0 e o último branch master :
Distribution of change in prediction

Eu gerei dados com 1000 sementes aleatórias diferentes e, em seguida, executei a previsão com as 1000 matrizes, usando as versões 1.2.0 e master. A mudança na previsão muda ligeiramente entre as sementes, mas a diferença nunca é maior que 9,2e-7, então provavelmente a mudança na previsão foi causada por aritmética de ponto flutuante e não um erro lógico .

Script para experimento

** test.py **: Gere 1000 matrizes com diferentes sementes aleatórias e executa a previsão para elas.

import numpy as np
import xgboost as xgb
import argparse

def main(args):
    m2 = xgb.Booster({'nthread': '4'})  # init model
    m2.load_model('xgb.model.bin')  # load data
    out = {}
    for seed in range(1000):
        rng = np.random.default_rng(seed=seed)
        rx = rng.standard_normal(size=(100, 127 + 7 + 1))
        rx = rx.astype(np.float32, order='C')
        dtest = xgb.DMatrix(rx, missing=0.0)
        out[str(seed)] = m2.predict(dtest)
    np.savez(args.out_pred, **out)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--out-pred', type=str, required=True)
    args = parser.parse_args()
    main(args)
Comando: `python test.py --out-pred [out.npz]`. Certifique-se de que seu env Python tenha a versão correta do XGBoost. Vamos supor que `xgb120.npz` armazene o resultado para o XGBoost 1.2.0 e` xgblatest.npz` armazene o resultado para o mestre mais recente. ** compare.py **: Faça um gráfico de histograma para a diferença de previsão
import numpy as np
import matplotlib.pyplot as plt

xgb120 = np.load('xgb120.npz')
xgblatest = np.load('xgblatest.npz')

percentile_pts = [50, 90, 99]

colors = ['tab:cyan', 'tab:olive', 'tab:green', 'tab:pink']

percentile = {}
for x in percentile_pts:
    percentile[x] = []
percentile['max'] = []

for seed in range(1000):
    diff = np.abs(xgb120[str(seed)] - xgblatest[str(seed)])
    t = np.percentile(diff, percentile_pts)
    for x, y in zip(percentile_pts, t):
        percentile[x].append(y)
    percentile['max'].append(np.max(diff))

bins = np.linspace(0, np.max(percentile['max']), 100)
idx = 0
for x in percentile_pts:
    plt.hist(percentile[x], label=f'Percentile {x}%', bins=bins, alpha=0.8, color=colors[idx])
    idx += 1
plt.hist(percentile['max'], label='Max', bins=bins, alpha=0.8, color=colors[idx])
plt.legend(loc='best')
plt.title('Distribution in prediction difference between XGBoost 1.2.0\nand master branch, tried over seed=[0..1000]')
plt.xlabel('Absolute difference')
plt.ylabel('Frequency')
plt.savefig('foobar.png', dpi=100)

Como aqui o problema é que + com float não forma um grupo, podemos testar com soma removida: Prever em uma única árvore. O resultado deve ser exatamente o mesmo.

@trivialfis De fato, quando adicionei ntree_limit=1 argumento a m2.predict() , a diferença desaparece para 0.

Excelente! Então, a próxima coisa é como o documentamos ou se devemos documentá-lo.

Deixe-me dormir sobre isso. Por enquanto, basta dizer que esse problema não é realmente um bug.

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

Questões relacionadas

nicoJiang picture nicoJiang  ·  4Comentários

uasthana15 picture uasthana15  ·  4Comentários

frankzhangrui picture frankzhangrui  ·  3Comentários

wenbo5565 picture wenbo5565  ·  3Comentários

FabHan picture FabHan  ·  4Comentários