Xgboost: [R] 当模型保存到磁盘时,早期停止的最佳迭代索引被丢弃

创建于 2020-01-15  ·  33评论  ·  资料来源: dmlc/xgboost

这些值是在xgboost::xgb.train之后预测的:
247367.2 258693.3 149572.2 201675.8 250493.9 292349.2 414828.0 296503.2 260851.9 190413.3

这些值是在先前模型的xgboost::xgb.savexgboost::xgb.load之后预测的:
247508.8 258658.2 149252.1 201692.6 250458.1 292313.4 414787.2 296462.5 260879.0 190430.1

它们很接近,但又不一样。 在一组 25k 个样本上,这两个预测之间的差异范围从-1317.0941088.859 。 与真实标签相比,这两个预测的 MAE/RMSE 相差不大。

所以我怀疑这与加载/保存期间的舍入误差有关,因为 MAE/RMSE 没有太大差异。 不过,我觉得这很奇怪,因为存储模型的二进制文件不应该引入舍入错误?

有人有线索吗?

PS 在这里上传和记录培训过程对我来说似乎并不重要。 如有必要,我可以提供详细信息,或者使用虚拟数据进行模拟以证明这一点。

Blocking bug

最有用的评论

谜团已揭开。 我确定了真正的原因。 当模型保存到磁盘时,有关提前停止的信息将被丢弃。 在示例中,XGBoost 运行了 6381 轮提升,并在 6378 轮中找到了最佳模型。 内存中的模型对象包含 6381 棵树,而不是 6378 棵树,因为没有删除任何树。 有一个额外的字段best_iteration可以记住哪个迭代是最好的:

> fit$best_iteration
[1] 6378

当我们将模型保存到磁盘时,这个额外的字段会被悄悄丢弃。 因此,原始模型的predict()使用 6378 棵树,而恢复模型的predict()使用 6381 棵树。

> 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

所有33条评论

二进制或 json 都不应该有任何舍入错误。 你在用飞镖吗?

不我不是:

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)

您能否提供出现这种现象的虚拟数据?

给你(快速和肮脏):

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

在我的机器上, identical(pred, pred.loaded)是 FALSE(即应该是 TRUE)。 这是最后一个命令的输出:

> 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

您会看到预测有时会略有不同。 你能在你的机器上重新运行示例代码,看看它是否有同样的问题吗?

关于 R 和 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 

另请注意:

> identical(fit$raw, fit.loaded$raw)
[1] TRUE

谢谢你的剧本。 只是一个更新,我运行它同时保存到 json 和二进制文件:

xgboost::xgb.save(fit, 'booster.json')
fit.loaded <- xgboost::xgb.load('booster.json')

xgboost::xgb.save(fit.loaded, 'booster-1.json')

booster.jsonbooster-1.json的哈希值(via sha256sum ./booster.json ) 完全相同,所以我的猜测是浮点运算导致了差异。

为什么在不知道原因的情况下关闭问题?

@trivialfis您是否为identical(pred, pred.loaded)获得 True ? OP 询问为什么预测不匹配,即使两个模型具有相同的二进制签名。

我会尝试自己重现它。

哦对不起。 我发现的原因是预测缓存。 加载模型后,预测值来自真实预测,而不是缓存值:

所以我的猜测是浮点运算引起了差异。

那么预测缓存以破坏性的方式与浮点运算交互?

@hcho3这是我在实施新酸洗方法时发现的一个问题。 我相信它在这里起着重要的作用。 所以首先将树的数量减少到 1000(这仍然非常大,应该足以进行演示)。

在预测之前重新构建 DMatrix 以消除缓存:

dtrain_2 <- xgboost::xgb.DMatrix(data = data.matrix(X), label = Y)

pred <- stats::predict(fit, newdata = dtrain_2)

它将通过identical测试。 否则失败。

在更多的树中进行相同的测试仍然有很小的差异(2000 棵树为 1e-7)。 但是,即使在多线程环境中,我们是否需要一点一点地产生相同的结果?

由于浮点求和不是关联的,如果需要,我们可以将其作为一个待办项,以对计算顺序有很强的保证。

实际上对订单做出强有力的保证是行不通的(会有很大帮助,但仍然会有差异)。 CPU FPU 寄存器中的浮点可以具有更高的精度,然后存储回内存。 (硬件实现可以对中间值使用更高的精度,https://en.wikipedia.org/wiki/Extended_precision)。 我的观点是,当 1000 棵树的结果在 32 位浮点数内完全可重现时,不太可能是编程错误。

我同意浮点求和不是关联的。 我将自己运行脚本,看看差异是否小到可以归因于浮点运算。

一般来说,我通常使用np.testing.assert_almost_equaldecimal=5来测试两个浮点数组是否几乎相等。

是的。 没有详细说明就关闭表示歉意。

@hcho3 有更新吗?

我还没有解决它。 这周让我看看。

@trivialfis我设法重现了这个错误。 我运行了提供的脚本并为identical(pred, pred.loaded)获得了FALSE identical(pred, pred.loaded) 。 我尝试按照您的建议创建一个新的 DMatrix dtrain_2 ,但仍然得到FALSE用于测试。

@DavorJ脚本的输出:

[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

修改后的脚本的输出,带有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

所以肯定还有其他事情发生。

我还尝试运行往返测试:

xgboost::xgb.save(fit, 'booster.raw')
fit.loaded <- xgboost::xgb.load('booster.raw')
xgboost::xgb.save(fit.loaded, 'booster.raw.roundtrip')

并且两个二进制文件booster.rawbooster.raw.roundtrip是相同的。

predpred.loaded之间的最大差异为 0.0008370876。

运行速度更快的较小示例:

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

输出:

[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

只是尝试做一次额外的往返,现在预测不再改变。

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

结果:

[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

所以也许预测缓存确实是一个问题。

我在禁用预测缓存的情况下重新运行了脚本:

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_);

(禁用预测缓存会导致训练速度非常慢。)

输出:

[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

所以预测缓存绝对不是这个错误的原因。

叶预测也有分歧:

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)

输出:

[1] FALSE
[1] TRUE

谜团已揭开。 我确定了真正的原因。 当模型保存到磁盘时,有关提前停止的信息将被丢弃。 在示例中,XGBoost 运行了 6381 轮提升,并在 6378 轮中找到了最佳模型。 内存中的模型对象包含 6381 棵树,而不是 6378 棵树,因为没有删除任何树。 有一个额外的字段best_iteration可以记住哪个迭代是最好的:

> fit$best_iteration
[1] 6378

当我们将模型保存到磁盘时,这个额外的字段会被悄悄丢弃。 因此,原始模型的predict()使用 6378 棵树,而恢复模型的predict()使用 6381 棵树。

> 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我倾向于物理移除树木。 如果训练在 6381 轮停止并且最佳迭代在 6378 轮,用户将期望最终模型有 6378 棵树。

@hcho3我认为这是https://github.com/dmlc/xgboost/issues/4052 中的类似问题。

bset_iteration应该保存到Learner::attributes_ ,可以通过xgboost::xgb.attr

@hcho3 ,不错的发现!

另请注意xgboost:::predict.xgb.Booster()的文档:

image

如果我理解正确,文档不完全正确? 根据文档,我期望预测已经使用了所有树。 不幸的是,我没有验证这一点。

@DavorJ当提前停止被激活时, predict()将使用best_iteration字段来获取预测。

@trivalifis Python 方面的情况更糟,因为xgb.predict()根本不会使用早期停止的信息:

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

输出:

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

用户必须记住在调用predict()时获取bst.best_iteration并将其作为ntree_limit参数传递。 这很容易出错,并且会带来令人不快的意外。

我们有两个修复选项:

  1. 物理删除过去best_iteration树。
  2. 在序列化模型时保留best_iteration信息,并让predict()函数使用它。

@hcho3process_type = update选项和森林有关。

背景

对于这些问题很短的回顾,我们有update ,如果num_boost_round与使用update比现有树木的数量较少,那些未被更新的树木将被删除.

对于森林问题的简短介绍, best_iteration不适用于森林,因为predict函数需要特定数量的树而不是迭代,所以在 Python 上有一个叫做best_ntree_limit的东西,这让我很困惑。 我明确地将ntree_limit中的inplace_predict替换iteration_range以避免此属性。

主意

我想添加一个sliceconcat方法booster ,其提取的树木成2个模型和连击树木2款为1。如果我们有这2种方法:

  • base_margin_不再需要,我相信它对其他用户更直观。
  • 不再需要预测中的ntree_limit ,我们只需对模型进行切片并在切片上运行预测。
  • update进程是独立的,只需一次更新切片中的树,没有num_boost_rounds

更远

我也相信这与多目标树有某种联系。 好像我们以后可以支持多类多目标树一样,树的排列方式会有多种,比如每个类使用output_groups ,或者每个目标,搭配森林和向量叶。 ntree_limit是不够的。

还有#5531。

但是这个想法很早,所以我没有信心分享它,现在我们在这个问题上,也许我可以得到一些关于这个的意见。

鉴于 1.1 时间表,我们是否可以扩展文档以阐明用户需要如何手动捕获和使用此最佳迭代进行预测?
并将其添加到发行说明中的​​已知问题中?

@trivialfis听起来很有趣,只要我们这样做不会使配置问题进一步复杂化。

按照@hcho3 的建议从模型中删除额外的树很有吸引力,因为我们不必同时处理实际模型长度和理论模型长度之间的任何不一致。

此页面是否有帮助?
0 / 5 - 0 等级