Estos valores se predicen después de xgboost::xgb.train
:
247367.2 258693.3 149572.2 201675.8 250493.9 292349.2 414828.0 296503.2 260851.9 190413.3
Estos valores se predicen después de xgboost::xgb.save
y xgboost::xgb.load
del modelo anterior:
247508.8 258658.2 149252.1 201692.6 250458.1 292313.4 414787.2 296462.5 260879.0 190430.1
Están cerca, pero no son iguales. Las diferencias entre estas dos predicciones van desde -1317.094
a 1088.859
en un conjunto de 25.000 muestras. Cuando se compara con etiquetas verdaderas, entonces el MAE / RMSE de estas dos predicciones no difieren mucho.
Entonces sospecho que esto tiene que ver con errores de redondeo durante la carga / guardado, ya que MAE / RMSE no difieren tanto. Aún así, encuentro esto extraño ya que el almacenamiento binario del modelo no debería introducir errores de redondeo.
¿Alguien tiene una pista?
PD Cargar y documentar el proceso de formación no me parece importante aquí. Podría proporcionar detalles si es necesario, o hacer una simulación con datos ficticios para probar el punto.
No debería haber ningún error de redondeo tanto para binario como para json. ¿Estás usando dardos?
No, no lo soy:
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)
¿Puede proporcionarnos datos ficticios sobre dónde se produce este fenómeno?
Aquí tienes (Rápido y sucio):
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))
En mi máquina, identical(pred, pred.loaded)
es FALSO (es decir, debería ser VERDADERO). Aquí está la salida de los ú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
Ves que las predicciones a veces difieren ligeramente. ¿Puede volver a ejecutar el código de ejemplo en su máquina y ver si tiene el mismo problema?
Alguna información adicional sobre R y 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
También tenga en cuenta que:
> identical(fit$raw, fit.loaded$raw)
[1] TRUE
Gracias por el guion. Solo una actualización, la ejecuté guardando tanto en json como en un archivo binario con:
xgboost::xgb.save(fit, 'booster.json')
fit.loaded <- xgboost::xgb.load('booster.json')
xgboost::xgb.save(fit.loaded, 'booster-1.json')
Los valores hash (via sha256sum ./booster.json
) de booster.json
y booster-1.json
son exactamente iguales, así que supongo que hay una discrepancia causada por la aritmética de punto flotante.
¿Por qué cerrar el problema sin conocer la causa?
@trivialfis ¿ identical(pred, pred.loaded)
? El OP pregunta por qué las predicciones no coinciden, a pesar de que dos modelos tienen la misma firma binaria.
Intentaré reproducirlo yo mismo.
Oh, lo siento. La causa que encontré es el caché de predicciones. Después de cargar el modelo, los valores de predicción provienen de una predicción verdadera, en lugar de un valor almacenado en caché:
así que supongo que en algún lugar hay una discrepancia causada por la aritmética de punto flotante.
Entonces, ¿la caché de predicción interactúa con la aritmética de punto flotante de una manera destructiva?
@ hcho3 Es un problema que encontré durante la implementación del nuevo método de decapado. Creo que juega un papel importante aquí. Así que primero reduzca la cantidad de árboles a 1000 (que sigue siendo bastante grande y debería ser suficiente para la demostración).
Vuelva a construir DMatrix antes de la predicción para sacar el caché del camino:
dtrain_2 <- xgboost::xgb.DMatrix(data = data.matrix(X), label = Y)
pred <- stats::predict(fit, newdata = dtrain_2)
Pasará la prueba identical
. De lo contrario, falla.
Obtener en más árboles, la prueba idéntica todavía tiene pequeñas diferencias (1e-7 para 2000 árboles). Pero, ¿necesitamos producir un resultado idéntico bit a bit incluso en un entorno de subprocesos múltiples?
Como la suma de coma flotante no es asociativa, podemos hacerla como un elemento pendiente para tener una garantía sólida para el orden de cálculo, si así lo desea.
En realidad, hacer una garantía sólida para el pedido no funcionará (ayudará mucho, pero aún habrá discrepancias). Un punto flotante en el registro de la CPU FPU puede tener una mayor precisión y luego almacenarse en la memoria. (La implementación de hardware puede utilizar una mayor precisión para los valores intermedios, https://en.wikipedia.org/wiki/Extended_precision). Mi punto es que cuando el resultado de 1000 árboles es exactamente reproducible dentro de un flotador de 32 bits, es poco probable que se trate de un error de programación.
Estoy de acuerdo en que la suma de coma flotante no es asociativa. Ejecutaré el script yo mismo y veré si la diferencia es lo suficientemente pequeña como para atribuirla a la aritmética de punto flotante.
En general, suelo usar np.testing.assert_almost_equal
con decimal=5
para probar si dos matrices flotantes son casi iguales entre sí.
Sí. Disculpas por cerrar sin notas detalladas.
@ hcho3 ¿ Alguna actualización?
Todavía no lo he solucionado. Déjame echarle un vistazo esta semana.
@trivialfis Logré reproducir el error. Ejecuté el script proporcionado y obtuve FALSE
por identical(pred, pred.loaded)
. Intenté crear una nueva DMatrix dtrain_2
como sugirió y todavía obtuve FALSE
para la prueba.
Salida del 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
Salida del script modificado, con 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
Entonces, algo más debe estar sucediendo.
También intenté ejecutar una prueba de ida y vuelta:
xgboost::xgb.save(fit, 'booster.raw')
fit.loaded <- xgboost::xgb.load('booster.raw')
xgboost::xgb.save(fit.loaded, 'booster.raw.roundtrip')
y los dos archivos binarios booster.raw
y booster.raw.roundtrip
eran idénticos.
La diferencia máxima entre pred
y pred.loaded
es 0.0008370876.
Un ejemplo más pequeño que se ejecuta más 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))
Producción:
[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
Intenté hacer un viaje de ida y vuelta adicional, y ahora las predicciones no cambian más.
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
Entonces, tal vez la caché de predicción sea un problema.
Volví a ejecutar el script con el almacenamiento en caché de predicciones desactivado:
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_);
(Deshabilitar el almacenamiento en caché de predicciones da como resultado un entrenamiento muy lento).
Producción:
[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
Entonces, la caché de predicción definitivamente NO es la causa de este error.
Las predicciones de hojas también divergen:
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)
Producción:
[1] FALSE
[1] TRUE
Misterio resuelto. Identifiqué la verdadera causa. Cuando el modelo se guarda en el disco, la información sobre la detención anticipada se descarta. En el ejemplo, XGBoost ejecuta 6381 rondas de impulso y encuentra el mejor modelo en 6378 rondas. El objeto modelo en la memoria contiene 6381 árboles, no 6378 árboles, ya que no se elimina ningún árbol. Hay un campo adicional best_iteration
que recuerda qué iteración fue la mejor:
> fit$best_iteration
[1] 6378
Este campo adicional se descarta silenciosamente cuando guardamos el modelo en el disco. Entonces predict()
con el modelo original usa 6378 árboles, mientras que predict()
con el modelo recuperado usa 6381 árboles.
> 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 Me inclino a eliminar árboles físicamente. Si el entrenamiento se detuvo en 6381 rondas y la mejor iteración fue en 6378 rondas, los usuarios esperarán que el modelo final tenga 6378 árboles.
@ hcho3 Creo que es un problema similar en https://github.com/dmlc/xgboost/issues/4052 .
El bset_iteration
debe guardarse en Learner::attributes_
, al que se puede acceder a través de xgboost::xgb.attr
.
@ hcho3 , buen hallazgo!
Tenga en cuenta también la documentación de xgboost:::predict.xgb.Booster()
:
Si entiendo correctamente, ¿la documentación no es del todo correcta? Según la documentación, esperaba que la predicción ya usara todos los árboles. Desafortunadamente, no lo había verificado.
@DavorJ Cuando se activa la parada anticipada, predict()
usará el campo best_iteration
para obtener la predicción.
@trivialfis La situación es peor en el lado de Python, ya que xgb.predict()
no usará la información de la parada temprana en absoluto:
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)))
Producción:
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
Los usuarios tendrán que recordar buscar bst.best_iteration
y pasarlo como argumento ntree_limit
al llamar a predict()
. Esto es propenso a errores y causa una sorpresa desagradable.
Tenemos dos opciones para solucionarlo:
best_iteration
.best_iteration
al serializar el modelo y haga que la función predict()
use.@ hcho3 Tengo una idea a medias sobre esto, que también está relacionada con nuestra opción process_type = update
y el bosque.
Para un breve resumen de los problemas que tenemos con update
, si num_boost_round
usado con update
es menor que el número de árboles ya existentes, se eliminarán aquellos árboles que no se actualicen .
Para una breve introducción de los problemas con el bosque, best_iteration
no se aplica al bosque ya que la función predict
requiere un número específico de árboles en lugar de iteración, por lo que en Python hay algo llamado best_ntree_limit
, lo cual me confunde mucho. Reemplacé explícitamente ntree_limit
en inplace_predict
con iteration_range
para evitar este atributo.
Quiero agregar un método slice
y concat
a booster
, que extraen los árboles en 2 modelos y concatenan árboles de 2 modelos en 1. Si tenemos estos 2 métodos :
base_margin_
ya no es necesario y creo que es más intuitivo para otros usuarios.ntree_limit
en la predicción ya no es necesario, simplemente cortamos el modelo y ejecutamos la predicción en los cortes.update
es autónomo, solo actualice los árboles en porciones de una sola vez, no num_boost_rounds
.También creo que esto está conectado de alguna manera a árboles de múltiples objetivos. Como si pudiéramos admitir árboles de múltiples clases y múltiples objetivos en el futuro, habrá varias formas de organizar los árboles, como usar output_groups
para cada clase, o cada objetivo, emparejándolos con el bosque y la hoja vectorial. ntree_limit
no será suficiente.
También # 5531.
Pero la idea es bastante temprana, así que no tenía la confianza para compartirla, ahora que estamos en este tema, tal vez pueda obtener algunas aportaciones al respecto.
Dada la línea de tiempo 1.1, ¿podemos expandir la documentación para aclarar cómo los usuarios necesitan capturar y usar manualmente esta mejor iteración en la predicción?
¿Y agregarlo a los problemas conocidos en las notas de la versión?
@trivialfis suena interesante, siempre y cuando no estemos complicando más los problemas de configuración al hacer esto.
Eliminar los árboles adicionales del modelo como lo sugiere @ hcho3 es atractivo ya que no tenemos que lidiar con las inconsistencias de tener una longitud de modelo real y una longitud de modelo teórico al mismo tiempo.
Comentario más útil
Misterio resuelto. Identifiqué la verdadera causa. Cuando el modelo se guarda en el disco, la información sobre la detención anticipada se descarta. En el ejemplo, XGBoost ejecuta 6381 rondas de impulso y encuentra el mejor modelo en 6378 rondas. El objeto modelo en la memoria contiene 6381 árboles, no 6378 árboles, ya que no se elimina ningún árbol. Hay un campo adicional
best_iteration
que recuerda qué iteración fue la mejor:Este campo adicional se descarta silenciosamente cuando guardamos el modelo en el disco. Entonces
predict()
con el modelo original usa 6378 árboles, mientras quepredict()
con el modelo recuperado usa 6381 árboles.