Ces valeurs sont prédites après xgboost::xgb.train
:
247367.2 258693.3 149572.2 201675.8 250493.9 292349.2 414828.0 296503.2 260851.9 190413.3
Ces valeurs sont prédites après xgboost::xgb.save
et xgboost::xgb.load
du modèle précédent :
247508.8 258658.2 149252.1 201692.6 250458.1 292313.4 414787.2 296462.5 260879.0 190430.1
Ils sont proches, mais pas les mêmes. Les différences entre ces deux prédictions vont de -1317.094
à 1088.859
sur un ensemble de 25 000 échantillons. En comparant avec de vraies étiquettes, alors le MAE/RMSE de ces deux prédictions ne diffère pas beaucoup.
Je soupçonne donc que cela a à voir avec des erreurs d'arrondi lors du chargement/de la sauvegarde, car le MAE/RMSE ne diffère pas autant. Pourtant, je trouve cela étrange puisque le stockage binaire du modèle ne devrait pas introduire d'erreurs d'arrondi?
Quelqu'un a une idée ?
PS Le téléchargement et la documentation du processus de formation ne me semblent pas importants ici. Je pourrais fournir des détails si nécessaire, ou faire une simulation avec des données factices pour prouver le point.
Il ne devrait pas y avoir d'erreur d'arrondi pour le binaire ou le json. Utilisez-vous des fléchettes ?
Non, je ne suis pas:
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)
Pouvez-vous nous fournir des données factices où ce phénomène se produit ?
Et voilà (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))
Sur ma machine, identical(pred, pred.loaded)
est FAUX (c'est-à-dire devrait être VRAI). Voici le résultat des dernières commandes :
> 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
Vous voyez que les prédictions diffèrent parfois légèrement. Pouvez-vous réexécuter l'exemple de code sur votre machine et voir s'il a le même problème ?
Quelques informations supplémentaires sur R et 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
Notez également que :
> identical(fit$raw, fit.loaded$raw)
[1] TRUE
Merci pour le scénario. Juste une mise à jour, je l'ai exécuté en enregistrant à la fois un fichier json et un fichier binaire avec :
xgboost::xgb.save(fit, 'booster.json')
fit.loaded <- xgboost::xgb.load('booster.json')
xgboost::xgb.save(fit.loaded, 'booster-1.json')
Les valeurs de hachage (via sha256sum ./booster.json
) de booster.json
et booster-1.json
sont exactement les mêmes, donc je suppose qu'il y a quelque part une divergence causée par l'arithmétique à virgule flottante.
Pourquoi fermer le problème sans en connaître la cause ?
@trivialfis Avez-vous obtenu True pour identical(pred, pred.loaded)
? L'OP demande pourquoi les prédictions ne correspondent pas, même si deux modèles ont la même signature binaire.
Je vais essayer de le reproduire moi-même.
Oh pardon. La cause que j'ai trouvée est le cache de prédiction. Après le chargement du modèle, les valeurs de prédiction proviennent de la prédiction vraie, au lieu de la valeur mise en cache :
donc je suppose qu'il y a quelque part une divergence causée par l'arithmétique à virgule flottante.
Le cache de prédiction interagit donc avec l'arithmétique à virgule flottante de manière destructive ?
@hcho3 C'est un problème que j'ai trouvé lors de la mise en œuvre de la nouvelle méthode de décapage. Je crois qu'il joue un rôle majeur ici. Réduisez donc d'abord le nombre d'arbres à 1000 (ce qui est encore assez énorme et devrait être suffisant pour la démo).
Reconstruisez le DMatrix avant la prédiction pour éliminer le cache :
dtrain_2 <- xgboost::xgb.DMatrix(data = data.matrix(X), label = Y)
pred <- stats::predict(fit, newdata = dtrain_2)
Il passera le test identical
. Sinon, il échoue.
Obtenir dans plus d'arbres le test identique a encore de petites différences (1e-7 pour 2000 arbres). Mais devons-nous produire un résultat identique petit à petit, même dans un environnement multi-thread ?
Comme la sommation en virgule flottante n'est pas associative, nous pouvons en faire une tâche à faire pour avoir une garantie forte pour l'ordre de calcul, si cela est souhaité.
En fait, faire une garantie forte pour la commande ne fonctionnera pas (cela aidera beaucoup, mais il y aura toujours des divergences). Une virgule flottante dans le registre CPU FPU peut avoir une précision plus élevée puis être stockée dans la mémoire. (L'implémentation matérielle peut utiliser une précision plus élevée pour les valeurs intermédiaires, https://en.wikipedia.org/wiki/Extended_precision). Ce que je veux dire, c'est que lorsque le résultat pour 1000 arbres est exactement reproductible dans un float de 32 bits, il est peu probable qu'il s'agisse d'un bogue de programmation.
Je suis d'accord que la sommation à virgule flottante n'est pas associative. Je vais exécuter le script moi-même et voir si la différence est suffisamment petite pour être attribuée à l'arithmétique à virgule flottante.
En général, j'utilise généralement np.testing.assert_almost_equal
avec decimal=5
pour tester si deux tableaux flottants sont presque égaux.
Ouais. Excuses pour la fermeture sans notes détaillées.
@hcho3 Une mise à jour ?
Je ne l'ai pas encore contourné. Laissez-moi jeter un œil cette semaine.
@trivialfis j'ai réussi à reproduire le bug. J'ai exécuté le script fourni et j'ai obtenu FALSE
pour identical(pred, pred.loaded)
. J'ai essayé de créer un nouveau DMatrix dtrain_2
comme vous l'avez suggéré et j'ai toujours reçu FALSE
pour le test.
Sortie du 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
Sortie du script modifié, avec 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
Il doit donc se passer autre chose.
J'ai aussi essayé de faire un test aller-retour :
xgboost::xgb.save(fit, 'booster.raw')
fit.loaded <- xgboost::xgb.load('booster.raw')
xgboost::xgb.save(fit.loaded, 'booster.raw.roundtrip')
et les deux fichiers binaires booster.raw
et booster.raw.roundtrip
étaient identiques.
La différence maximale entre pred
et pred.loaded
est 0,0008370876.
Un petit exemple qui s'exécute plus rapidement :
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))
Sortir:
[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
J'ai juste essayé de faire un aller-retour supplémentaire, et maintenant les prédictions ne changent plus.
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))
Résultat:
[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
Alors peut-être que le cache de prédiction est en effet un problème.
J'ai réexécuté le script avec la mise en cache de prédiction désactivée :
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_);
(La désactivation de la mise en cache des prédictions entraîne un entraînement très lent.)
Sortir:
[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
Le cache de prédiction n'est donc certainement PAS la cause de ce bogue.
Les prédictions des feuilles divergent également :
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)
Sortir:
[1] FALSE
[1] TRUE
Mystère résolu. J'ai identifié la vraie cause. Lorsque le modèle est enregistré sur le disque, les informations sur l'arrêt anticipé sont supprimées. Dans l'exemple, XGBoost exécute 6381 tours de boost et trouve le meilleur modèle à 6378 tours. L'objet modèle en mémoire contient 6381 arbres, et non 6378 arbres, car aucun arbre n'est supprimé. Il y a un champ supplémentaire best_iteration
qui mémorise quelle itération était la meilleure :
> fit$best_iteration
[1] 6378
Ce champ supplémentaire est ignoré en silence lorsque nous sauvegardons le modèle sur le disque. Ainsi, predict()
avec le modèle original utilise 6378 arbres, alors que predict()
avec le modèle récupéré utilise 6381 arbres.
> 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 Je suis enclin à supprimer physiquement les arbres. Si l'entraînement s'arrêtait à 6381 tours et que la meilleure itération était à 6378 tours, les utilisateurs s'attendront à ce que le modèle final ait 6378 arbres.
@hcho3 Je pense que c'est un problème similaire dans https://github.com/dmlc/xgboost/issues/4052 .
Le bset_iteration
doit être enregistré dans Learner::attributes_
, accessible via xgboost::xgb.attr
.
@hcho3 , belle trouvaille !
Notez également la documentation de xgboost:::predict.xgb.Booster()
:
Si je comprends bien, la documentation n'est pas tout à fait correcte ? Sur la base de la documentation, je m'attendais à ce que la prédiction utilisait déjà tous les arbres. Malheureusement, je n'avais pas vérifié cela.
@DavorJ Lorsque l'arrêt anticipé est activé, predict()
utilisera le champ best_iteration
pour obtenir la prédiction.
@trivialfis La situation est pire du côté de Python, car xgb.predict()
n'utilisera pas du tout les informations de l'arrêt précoce :
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)))
Sortir:
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
Les utilisateurs devront se souvenir de récupérer bst.best_iteration
et de le passer comme argument ntree_limit
lors de l'appel de predict()
. Ceci est sujet aux erreurs et crée une mauvaise surprise.
Nous avons deux options pour un correctif :
best_iteration
.best_iteration
lors de la sérialisation du modèle et faites en sorte que la fonction predict()
l'utilise.@hcho3 J'ai une idée à moitié cuite à ce sujet, qui est également liée à notre option process_type = update
et à la forêt.
Pour un bref récapitulatif des problèmes que nous avons avec update
, si num_boost_round
utilisé avec update
est inférieur au nombre d'arbres déjà existants, les arbres qui ne sont pas mis à jour seront supprimés .
Pour une brève introduction aux problèmes de forest, best_iteration
ne s'applique pas à forest car la fonction predict
nécessite un nombre spécifique d'arbres au lieu d'itérations, donc sur Python, il y a quelque chose qui s'appelle best_ntree_limit
, ce qui est très déroutant pour moi. J'ai explicitement remplacé ntree_limit
dans inplace_predict
par iteration_range
pour éviter cet attribut.
Je veux ajouter une méthode slice
et une méthode concat
à booster
, qui extraient les arbres en 2 modèles et concaténent les arbres de 2 modèles en 1. Si nous avons ces 2 méthodes :
base_margin_
n'est plus nécessaire et je pense que c'est plus intuitif pour les autres utilisateurs.ntree_limit
dans la prédiction n'est plus nécessaire, nous découpons simplement le modèle et exécutons la prédiction sur les tranches.update
processus num_boost_rounds
.Je pense également que cela est en quelque sorte lié aux arbres multi-cibles. Comme si nous pouvions à l'avenir prendre en charge les arbres multi-classes et multi-cibles, il y aura plusieurs façons d'organiser les arbres, comme utiliser output_groups
pour chaque classe, ou chaque cible, en associant forêt et feuille vectorielle. ntree_limit
ne sera pas suffisant.
Aussi #5531.
Mais l'idée est assez précoce, donc je n'avais pas la confiance nécessaire pour la partager, maintenant nous sommes sur cette question, je peux peut-être obtenir des informations à ce sujet.
Compte tenu de la chronologie 1.1, pouvons-nous étendre la documentation pour clarifier comment les utilisateurs doivent capturer et utiliser manuellement cette meilleure itération dans la prédiction ?
Et l'ajouter aux problèmes connus dans les notes de version ?
@trivialfis semble intéressant, tant que nous ne compliquons pas davantage les problèmes de configuration en faisant cela.
Supprimer les arbres supplémentaires du modèle comme suggéré par @hcho3 est attrayant car nous n'avons pas à faire face à des incohérences
Commentaire le plus utile
Mystère résolu. J'ai identifié la vraie cause. Lorsque le modèle est enregistré sur le disque, les informations sur l'arrêt anticipé sont supprimées. Dans l'exemple, XGBoost exécute 6381 tours de boost et trouve le meilleur modèle à 6378 tours. L'objet modèle en mémoire contient 6381 arbres, et non 6378 arbres, car aucun arbre n'est supprimé. Il y a un champ supplémentaire
best_iteration
qui mémorise quelle itération était la meilleure :Ce champ supplémentaire est ignoré en silence lorsque nous sauvegardons le modèle sur le disque. Ainsi,
predict()
avec le modèle original utilise 6378 arbres, alors quepredict()
avec le modèle récupéré utilise 6381 arbres.