Esses valores são previstos após xgboost::xgb.train
:
247367.2 258693.3 149572.2 201675.8 250493.9 292349.2 414828.0 296503.2 260851.9 190413.3
Esses valores são previstos após xgboost::xgb.save
e xgboost::xgb.load
do modelo anterior:
247508.8 258658.2 149252.1 201692.6 250458.1 292313.4 414787.2 296462.5 260879.0 190430.1
Eles estão próximos, mas não iguais. As diferenças entre essas duas previsões variam de -1317.094
a 1088.859
em um conjunto de 25 mil amostras. Ao comparar com rótulos verdadeiros, o MAE / RMSE dessas duas previsões não difere muito.
Portanto, suspeito que isso tenha a ver com erros de arredondamento durante o carregamento / salvamento, uma vez que o MAE / RMSE não diferem tanto. Ainda assim, acho isso estranho, já que o armazenamento binário do modelo não deve apresentar erros de arredondamento.
Alguém tem uma pista?
PS Carregar e documentar o processo de treinamento não parece importante para mim aqui. Eu poderia fornecer detalhes, se necessário, ou fazer uma simulação com dados fictícios para provar o ponto.
Não deve haver nenhum erro de arredondamento para binário ou json. Você está usando dardo?
Não, não estou:
params <- list(objective = 'reg:squarederror',
max_depth = 10, eta = 0.02, subsammple = 0.5,
base_score = median(xgboost::getinfo(xgb.train, 'label'))
)
xgboost::xgb.train(
params = params, data = xgb.train,
watchlist = list('train' = xgb.train, 'test' = xgb.test),
nrounds = 10000, verbose = TRUE, print_every_n = 25,
eval_metric = 'mae',
early_stopping_rounds = 3, maximize = FALSE)
Você pode nos fornecer dados fictícios onde esse fenômeno ocorre?
Aqui está (Quick & Dirty):
N <- 100000
set.seed(2020)
X <- data.frame('X1' = rnorm(N), 'X2' = runif(N), 'X3' = rpois(N, lambda = 1))
Y <- with(X, X1 + X2 - X3 + X1*X2^2 - ifelse(X1 > 0, 2, X3))
params <- list(objective = 'reg:squarederror',
max_depth = 5, eta = 0.02, subsammple = 0.5,
base_score = median(Y)
)
dtrain <- xgboost::xgb.DMatrix(data = data.matrix(X), label = Y)
fit <- xgboost::xgb.train(
params = params, data = dtrain,
watchlist = list('train' = dtrain),
nrounds = 10000, verbose = TRUE, print_every_n = 25,
eval_metric = 'mae',
early_stopping_rounds = 3, maximize = FALSE
)
pred <- stats::predict(fit, newdata = dtrain)
xgboost::xgb.save(fit, 'booster.raw')
fit.loaded <- xgboost::xgb.load('booster.raw')
pred.loaded <- stats::predict(fit.loaded, newdata = dtrain)
identical(pred, pred.loaded)
pred[1:10]
pred.loaded[1:10]
sqrt(mean((Y - pred)^2))
sqrt(mean((Y - pred.loaded)^2))
Na minha máquina, identical(pred, pred.loaded)
é FALSE (ou seja, deve ser TRUE). Aqui está a saída dos últimos comandos:
> identical(pred, pred.loaded)
[1] FALSE
> pred[1:10]
[1] -4.7971768 -2.5070562 -0.8889422 -4.9199696 -4.4374819 -0.2739395 -0.9825708 0.4579227 1.3667605 -4.3333349
> pred.loaded[1:10]
[1] -4.7971768 -2.5070562 -0.8889424 -4.9199696 -4.4373770 -0.2739397 -0.9825710 0.4579227 1.3667605 -4.3333349
>
> sqrt(mean((Y - pred)^2))
[1] 0.02890702
> sqrt(mean((Y - pred.loaded)^2))
[1] 0.02890565
Você vê que as previsões às vezes diferem ligeiramente. Você pode executar novamente o código de exemplo em sua máquina e ver se ele tem o mesmo problema?
Algumas informações extras sobre R e xgboost:
> sessionInfo()
R version 3.6.1 (2019-07-05)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows >= 8 x64 (build 9200)
Matrix products: default
locale:
[1] LC_COLLATE=English_United States.1252 LC_CTYPE=English_United States.1252 LC_MONETARY=English_United States.1252 LC_NUMERIC=C
[5] LC_TIME=English_United States.1252
attached base packages:
[1] stats graphics grDevices utils datasets methods base
loaded via a namespace (and not attached):
[1] compiler_3.6.1 magrittr_1.5 Matrix_1.2-17 tools_3.6.1 yaml_2.2.0 xgboost_0.90.0.2 stringi_1.4.3 grid_3.6.1
[9] data.table_1.12.4 lattice_0.20-38
Observe também que:
> identical(fit$raw, fit.loaded$raw)
[1] TRUE
Obrigado pelo script. Apenas uma atualização, executei salvando em json e arquivo binário com:
xgboost::xgb.save(fit, 'booster.json')
fit.loaded <- xgboost::xgb.load('booster.json')
xgboost::xgb.save(fit.loaded, 'booster-1.json')
Os valores de hash (via sha256sum ./booster.json
) de booster.json
e booster-1.json
são exatamente os mesmos, então meu palpite é que em algum lugar há discrepância causada pela aritmética de ponto flutuante.
Por que encerrar o problema sem saber a causa?
@trivialfis Você obteve True para identical(pred, pred.loaded)
? O OP está perguntando por que as previsões não correspondem, embora dois modelos tenham a mesma assinatura binária.
Vou tentar reproduzi-lo sozinho.
Oh, desculpe. A causa que encontrei é o cache de previsão. Depois de carregar o modelo, os valores de predição vêm da predição verdadeira, em vez do valor armazenado em cache:
então, meu palpite é que em algum lugar há discrepância causada pela aritmética de ponto flutuante.
Então, o cache de previsão interage com a aritmética de ponto flutuante de forma destrutiva?
@ hcho3 É um problema que encontrei durante a implementação do novo método de decapagem. Eu acredito que desempenha um papel importante aqui. Portanto, primeiro reduza o número de árvores para 1000 (o que ainda é muito grande e deve ser o suficiente para uma demonstração).
Reconstrua a DMatrix antes da previsão para tirar o cache do caminho:
dtrain_2 <- xgboost::xgb.DMatrix(data = data.matrix(X), label = Y)
pred <- stats::predict(fit, newdata = dtrain_2)
Ele passará no teste identical
. Caso contrário, ele falha.
Entrando em mais árvores, o teste idêntico ainda tem pequenas diferenças (1e-7 para 2.000 árvores). Mas precisamos produzir resultado idêntico bit a bit, mesmo em ambiente multi-threaded?
Como o somatório de ponto flutuante não é associativo, podemos torná-lo um item a fazer para ter uma garantia forte para a ordem de cálculo, se desejado.
Na verdade, fazer uma garantia forte para o pedido não funcionará (ajudará muito, mas ainda assim haverá discrepância). Um ponto flutuante no registro da CPU FPU pode ter maior precisão do que armazenado de volta na memória. (A implementação de hardware pode usar maior precisão para valores intermediários, https://en.wikipedia.org/wiki/Extended_precision). Meu ponto é que quando o resultado para 1000 árvores são exatamente reproduzíveis dentro de um float de 32 bits, é improvável que seja um bug de programação.
Eu concordo que a soma de ponto flutuante não é associativa. Vou executar o script sozinho e ver se a diferença é pequena o suficiente para atribuir à aritmética de ponto flutuante.
Em geral, eu geralmente uso np.testing.assert_almost_equal
com decimal=5
para testar se duas matrizes de float são quase iguais uma à outra.
Sim. Desculpas por fechar sem notas detalhadas.
@ hcho3 Alguma atualização?
Eu não consegui contornar isso ainda. Deixe-me dar uma olhada esta semana.
@trivialfis consegui reproduzir o bug. Eu executei o script fornecido e obtive FALSE
para identical(pred, pred.loaded)
. Tentei criar uma nova DMatrix dtrain_2
como você sugeriu e ainda tenho FALSE
para o teste.
Saída do script de
[1] FALSE # identical(pred, pred.loaded)
[1] -4.7760534 -2.5083885 -0.8860036 -4.9163256 -4.4455137 -0.2548684
[7] -0.9745615 0.4646015 1.3602829 -4.3288369 # pred[1:10]
[1] -4.7760534 -2.5083888 -0.8860038 -4.9163256 -4.4454765 -0.2548686
[7] -0.9745617 0.4646015 1.3602829 -4.3288369 # pred.loaded[1:10]
[1] 0.02456085 # MSE on pred
[1] 0.02455945 # MSE on pred.loaded
Saída do script modificado, com dtrain_2 <- xgboost::xgb.DMatrix(data = data.matrix(X), label = Y)
:
[1] FALSE # identical(pred, pred.loaded)
[1] -4.7760534 -2.5083885 -0.8860036 -4.9163256 -4.4455137 -0.2548684
[7] -0.9745615 0.4646015 1.3602829 -4.3288369 # pred[1:10]
[1] -4.7760534 -2.5083888 -0.8860038 -4.9163256 -4.4454765 -0.2548686
[7] -0.9745617 0.4646015 1.3602829 -4.3288369 # pred.loaded[1:10]
[1] 0.02456085 # MSE on pred
[1] 0.02455945 # MSE on pred.loaded
Portanto, algo mais deve estar acontecendo.
Também tentei fazer um teste de ida e volta:
xgboost::xgb.save(fit, 'booster.raw')
fit.loaded <- xgboost::xgb.load('booster.raw')
xgboost::xgb.save(fit.loaded, 'booster.raw.roundtrip')
e os dois arquivos binários booster.raw
e booster.raw.roundtrip
eram idênticos.
A diferença máxima entre pred
e pred.loaded
é 0,0008370876.
Um exemplo menor que executa mais rápido:
library(xgboost)
N <- 5000
set.seed(2020)
X <- data.frame('X1' = rnorm(N), 'X2' = runif(N), 'X3' = rpois(N, lambda = 1))
Y <- with(X, X1 + X2 - X3 + X1*X2^2 - ifelse(X1 > 0, 2, X3))
params <- list(objective = 'reg:squarederror',
max_depth = 5, eta = 0.02, subsammple = 0.5,
base_score = median(Y)
)
dtrain <- xgboost::xgb.DMatrix(data = data.matrix(X), label = Y)
fit <- xgboost::xgb.train(
params = params, data = dtrain,
watchlist = list('train' = dtrain),
nrounds = 10000, verbose = TRUE, print_every_n = 25,
eval_metric = 'mae',
early_stopping_rounds = 3, maximize = FALSE
)
pred <- stats::predict(fit, newdata = dtrain)
invisible(xgboost::xgb.save(fit, 'booster.raw'))
fit.loaded <- xgboost::xgb.load('booster.raw')
invisible(xgboost::xgb.save(fit.loaded, 'booster.raw.roundtrip'))
pred.loaded <- stats::predict(fit.loaded, newdata = dtrain)
identical(pred, pred.loaded)
pred[1:10]
pred.loaded[1:10]
max(abs(pred - pred.loaded))
sqrt(mean((Y - pred)^2))
sqrt(mean((Y - pred.loaded)^2))
Saída:
[1] FALSE
[1] -2.4875379 -0.9452241 -6.9658904 -2.9985323 -4.2192593 -0.8505422
[7] -0.3928839 -1.6886091 -1.3611379 -3.1278882
[1] -2.4875379 -0.9452239 -6.9658904 -2.9985323 -4.2192593 -0.8505420
[7] -0.3928837 -1.6886090 -1.3611377 -3.1278882
[1] 0.0001592636
[1] 0.01370754
[1] 0.01370706
Apenas tentei fazer uma viagem extra de ida e volta, e agora as previsões não mudam mais.
library(xgboost)
N <- 5000
set.seed(2020)
X <- data.frame('X1' = rnorm(N), 'X2' = runif(N), 'X3' = rpois(N, lambda = 1))
Y <- with(X, X1 + X2 - X3 + X1*X2^2 - ifelse(X1 > 0, 2, X3))
params <- list(objective = 'reg:squarederror',
max_depth = 5, eta = 0.02, subsammple = 0.5,
base_score = median(Y)
)
dtrain <- xgboost::xgb.DMatrix(data = data.matrix(X), label = Y)
fit <- xgboost::xgb.train(
params = params, data = dtrain,
watchlist = list('train' = dtrain),
nrounds = 10000, verbose = TRUE, print_every_n = 25,
eval_metric = 'mae',
early_stopping_rounds = 3, maximize = FALSE
)
pred <- stats::predict(fit, newdata = dtrain)
invisible(xgboost::xgb.save(fit, 'booster.raw'))
fit.loaded <- xgboost::xgb.load('booster.raw')
invisible(xgboost::xgb.save(fit.loaded, 'booster.raw.roundtrip'))
fit.loaded2 <- xgboost::xgb.load('booster.raw.roundtrip')
pred.loaded <- stats::predict(fit.loaded, newdata = dtrain)
pred.loaded2 <- stats::predict(fit.loaded2, newdata = dtrain)
identical(pred, pred.loaded)
identical(pred.loaded, pred.loaded2)
pred[1:10]
pred.loaded[1:10]
pred.loaded2[1:10]
max(abs(pred - pred.loaded))
max(abs(pred.loaded - pred.loaded2))
sqrt(mean((Y - pred)^2))
sqrt(mean((Y - pred.loaded)^2))
sqrt(mean((Y - pred.loaded2)^2))
Resultado:
[1] FALSE
[1] TRUE
[1] -2.4875379 -0.9452241 -6.9658904 -2.9985323 -4.2192593 -0.8505422
[7] -0.3928839 -1.6886091 -1.3611379 -3.1278882
[1] -2.4875379 -0.9452239 -6.9658904 -2.9985323 -4.2192593 -0.8505420
[7] -0.3928837 -1.6886090 -1.3611377 -3.1278882
[1] -2.4875379 -0.9452239 -6.9658904 -2.9985323 -4.2192593 -0.8505420
[7] -0.3928837 -1.6886090 -1.3611377 -3.1278882
[1] 0.0001592636
[1] 0
[1] 0.01370754
[1] 0.01370706
[1] 0.01370706
Então, talvez o cache de previsão seja realmente um problema.
Eu executei novamente o script com o cache de predição desativado:
diff --git a/src/predictor/cpu_predictor.cc b/src/predictor/cpu_predictor.cc
index ebc15128..c40309bc 100644
--- a/src/predictor/cpu_predictor.cc
+++ b/src/predictor/cpu_predictor.cc
@@ -259,7 +259,7 @@ class CPUPredictor : public Predictor {
// delta means {size of forest} * {number of newly accumulated layers}
uint32_t delta = end_version - beg_version;
CHECK_LE(delta, model.trees.size());
- predts->Update(delta);
+ //predts->Update(delta);
CHECK(out_preds->Size() == output_groups * dmat->Info().num_row_ ||
out_preds->Size() == dmat->Info().num_row_);
(Desativar o cache de predição resulta em um treinamento muito lento.)
Saída:
[1] FALSE
[1] TRUE
[1] -2.4908853 -0.9507379 -6.9615889 -2.9935317 -4.2165089 -0.8543566
[7] -0.3940181 -1.6930715 -1.3572118 -3.1403396
[1] -2.4908853 -0.9507380 -6.9615889 -2.9935317 -4.2165089 -0.8543567
[7] -0.3940183 -1.6930716 -1.3572119 -3.1403399
[1] -2.4908853 -0.9507380 -6.9615889 -2.9935317 -4.2165089 -0.8543567
[7] -0.3940183 -1.6930716 -1.3572119 -3.1403399
[1] 0.0001471043
[1] 0
[1] 0.01284297
[1] 0.01284252
[1] 0.01284252
Portanto, o cache de previsão definitivamente NÃO é a causa desse bug.
As previsões da folha também divergem:
invisible(xgboost::xgb.save(fit, 'booster.raw'))
fit.loaded <- xgboost::xgb.load('booster.raw')
invisible(xgboost::xgb.save(fit.loaded, 'booster.raw.roundtrip'))
fit.loaded2 <- xgboost::xgb.load('booster.raw.roundtrip')
x <- predict(fit, newdata = dtrain2, predleaf = TRUE)
x2 <- predict(fit.loaded, newdata = dtrain2, predleaf = TRUE)
x3 <- predict(fit.loaded2, newdata = dtrain2, predleaf = TRUE)
identical(x, x2)
identical(x2, x3)
Saída:
[1] FALSE
[1] TRUE
Mistério resolvido. Eu identifiquei a verdadeira causa. Quando o modelo é salvo no disco, as informações sobre a parada antecipada são descartadas. No exemplo, o XGBoost executa 6381 rodadas de boost e encontra o melhor modelo com 6378 rodadas. O objeto de modelo na memória contém 6381 árvores, não 6378 árvores, uma vez que nenhuma árvore foi removida. Há um campo extra best_iteration
que lembra qual iteração foi a melhor:
> fit$best_iteration
[1] 6378
Este campo extra é descartado silenciosamente quando salvamos o modelo no disco. Portanto, predict()
com o modelo original usa 6378 árvores, enquanto predict()
com o modelo recuperado usa 6381 árvores.
> x <- predict(fit, newdata = dtrain2, predleaf = TRUE)
> x2 <- predict(fit.loaded, newdata = dtrain2, predleaf = TRUE)
> dim(x)
[1] 5000 6378
> dim(x2)
[1] 5000 6381
@trivialfis Estou inclinado a remover árvores fisicamente. Se o treinamento parou em 6381 rodadas e a melhor iteração foi em 6378 rodadas, os usuários esperarão que o modelo final tenha 6378 árvores.
@ hcho3 Acho que é um problema semelhante em https://github.com/dmlc/xgboost/issues/4052 .
O bset_iteration
deve ser salvo em Learner::attributes_
, que pode ser acessado através de xgboost::xgb.attr
.
@ hcho3 , bom achado!
Observe também a documentação de xgboost:::predict.xgb.Booster()
:
Se bem entendi, a documentação não está totalmente correta? Com base na documentação, esperava que a previsão já usasse todas as árvores. Infelizmente eu não tinha verificado isso.
@DavorJ Quando a parada antecipada é ativada, predict()
usará o campo best_iteration
para obter a previsão.
@trivialfis A situação é pior do lado do Python, já que xgb.predict()
não usará nenhuma informação da parada inicial:
import xgboost as xgb
import numpy as np
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
X, y = load_boston(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test, label=y_test)
params = {'objective': 'reg:squarederror'}
bst = xgb.train(params, dtrain, 100, [(dtrain, 'train'), (dtest, 'test')],
early_stopping_rounds=5)
x = bst.predict(dtrain, pred_leaf=True)
x2 = bst.predict(dtrain, pred_leaf=True, ntree_limit=bst.best_iteration)
print(x.shape)
print(x2.shape)
pred = bst.predict(dtrain)
pred2 = bst.predict(dtrain, ntree_limit=bst.best_iteration)
print(np.max(np.abs(pred - pred2)))
Saída:
Will train until test-rmse hasn't improved in 5 rounds.
[1] train-rmse:12.50316 test-rmse:11.92709
...
[25] train-rmse:0.56720 test-rmse:2.56874
[26] train-rmse:0.54151 test-rmse:2.56722
[27] train-rmse:0.51842 test-rmse:2.56124
[28] train-rmse:0.47489 test-rmse:2.56640
[29] train-rmse:0.45489 test-rmse:2.58780
[30] train-rmse:0.43093 test-rmse:2.59385
[31] train-rmse:0.41865 test-rmse:2.59364
[32] train-rmse:0.40823 test-rmse:2.59465
Stopping. Best iteration:
[27] train-rmse:0.51842 test-rmse:2.56124
(404, 33)
(404, 27)
0.81269073
Os usuários terão que se lembrar de buscar bst.best_iteration
e passá-lo como ntree_limit
argumento ao chamar predict()
. Isso está sujeito a erros e é uma surpresa desagradável.
Temos duas opções para corrigir:
best_iteration
.best_iteration
ao serializar o modelo e faça com que a função predict()
as use.@ hcho3 Tenho uma ideia process_type = update
e floresta.
Para uma breve recapitulação dos problemas que temos com update
, se num_boost_round
usado com update
for menor que o número de árvores já existentes, aquelas árvores que não são atualizadas serão removidas .
Para uma breve introdução aos problemas com floresta, best_iteration
não se aplica à floresta, pois predict
função requer um número específico de árvores em vez de iteração, portanto, em Python há algo chamado best_ntree_limit
, o que é muito confuso para mim. Substituí explicitamente ntree_limit
em inplace_predict
por iteration_range
para evitar este atributo.
Quero adicionar um método slice
e um concat
a booster
, que extraem as árvores em 2 modelos e concatenam as árvores de 2 modelos em 1. Se tivermos esses 2 métodos :
base_margin_
não é mais necessário e acredito que seja mais intuitivo para outros usuários.ntree_limit
em previsão não é mais necessário, apenas dividimos o modelo e executamos a previsão nas fatias.update
processo é independente, apenas atualize as árvores em fatias de uma vez, não num_boost_rounds
.Também acredito que isso esteja de alguma forma conectado a árvores de múltiplos alvos. Como se pudéssemos oferecer suporte a árvores multiclasse com múltiplos alvos no futuro, haverá várias maneiras de organizar árvores, como usar output_groups
para cada classe, ou cada alvo, emparelhando com floresta e folha de vetor. ntree_limit
não vai ser suficiente.
Também # 5531.
Mas a ideia é muito cedo, então não tive confiança para compartilhá-la, agora que estamos falando sobre esse assunto, talvez eu possa obter algumas contribuições sobre isso.
Dada a linha do tempo 1.1, podemos expandir a documentação para esclarecer como os usuários precisam capturar e usar manualmente essa melhor iteração na previsão?
E adicioná-lo aos problemas conhecidos nas notas de lançamento?
@trivialfis parece interessante, contanto que não estejamos complicando ainda mais os problemas de configuração ao fazer isso.
Excluir as árvores extras do modelo, conforme sugerido por @ hcho3, é atraente, pois não temos que lidar com quaisquer inconsistências por ter um comprimento de modelo real e um comprimento de modelo teórico ao mesmo tempo.
Comentários muito úteis
Mistério resolvido. Eu identifiquei a verdadeira causa. Quando o modelo é salvo no disco, as informações sobre a parada antecipada são descartadas. No exemplo, o XGBoost executa 6381 rodadas de boost e encontra o melhor modelo com 6378 rodadas. O objeto de modelo na memória contém 6381 árvores, não 6378 árvores, uma vez que nenhuma árvore foi removida. Há um campo extra
best_iteration
que lembra qual iteração foi a melhor:Este campo extra é descartado silenciosamente quando salvamos o modelo no disco. Portanto,
predict()
com o modelo original usa 6378 árvores, enquantopredict()
com o modelo recuperado usa 6381 árvores.