Xgboost: [R] Le meilleur index d'itération à partir de l'arrêt précoce est ignoré lorsque le modèle est enregistré sur le disque

Créé le 15 janv. 2020  ·  33Commentaires  ·  Source: dmlc/xgboost

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.

Blocking bug

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 :

> 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

Tous les 33 commentaires

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() :

image

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 :

  1. Supprimez physiquement les arbres qui sont au-delà de best_iteration .
  2. Conservez les informations 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.

Fond

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.

Idée

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 .

Plus loin

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

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

vkuznet picture vkuznet  ·  3Commentaires

lizsz picture lizsz  ·  3Commentaires

nnorton24 picture nnorton24  ·  3Commentaires

nicoJiang picture nicoJiang  ·  4Commentaires

frankzhangrui picture frankzhangrui  ·  3Commentaires