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.saveおよびxgboost::xgb.loadの後に予測されます。
247508.8 258658.2 149252.1 201692.6 250458.1 292313.4 414787.2 296462.5 260879.0 190430.1

それらは近いですが、同じではありません。 これら2つの予測の違いは、25kサンプルのセットで-1317.094から1088.859です。 真のラベルと比較すると、これら2つの予測のMAE / RMSEに大きな違いはありません。

したがって、MAE / RSMEはそれほど変わらないため、これはロード/保存中の丸め誤差に関係していると思われます。 それでも、モデルを格納するバイナリは丸め誤差を引き起こしてはならないので、これは奇妙だと思いますか?

誰か手がかり?

PSトレーニングプロセスのアップロードと文書化は、ここでは重要ではないようです。 必要に応じて詳細を提供したり、ダミーデータを使用してシミュレーションを行ってポイントを証明したりできます。

Blocking bug

最も参考になるコメント

謎が解けた。 本当の原因を特定しました。 モデルがディスクに保存されると、早期停止に関する情報は破棄されます。 この例では、XGBoostは6381のブーストラウンドを実行し、6378ラウンドで最適なモデルを見つけます。 メモリ内のモデルオブジェクトには、削除されたツリーがないため、6378ツリーではなく6381ツリーが含まれています。 どの反復が最適であったかを記憶する追加のフィールド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は、2つのモデルが同じバイナリ署名を持っているにもかかわらず、予測が一致しない理由を尋ねています。

自分で再現してみます。

あ、ごめんなさい。 私が見つけた原因は予測キャッシュです。 モデルをロードした後、予測値は、キャッシュされた値ではなく、真の予測から取得されます。

ですから、私の推測では、浮動小数点演算によって引き起こされる不一致がどこかにあります。

では、予測キャッシュは浮動小数点演算と破壊的な方法で相互作用しますか?

@ 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)。 しかし、マルチスレッド環境でも少しずつ同じ結果を生成する必要がありますか?

浮動小数点の合計は結合法則ではないため、必要に応じて、計算の順序を強力に保証するtodo項目にすることができます。

実際に注文を強力に保証することはできません(大いに役立ちますが、それでも不一致があります)。 CPU FPUレジスタの浮動小数点は、メモリに格納されるよりも高い精度を持つことができます。 (ハードウェアの実装では、中間値に高い精度を使用できます、https://en.wikipedia.org/wiki/Extended_precision)。 私のポイントは、1000ツリーの結果が32ビットフロート内で正確に再現可能である場合、プログラミングのバグではない可能性が高いということです。

浮動小数点の合計は結合法則ではないことに同意します。 スクリプトを自分で実行し、その差が浮動小数点演算に起因するほど小さいかどうかを確認します。

一般に、私は通常、 np.testing.assert_almost_equaldecimal=5を使用して、2つのfloat配列が互いにほぼ等しいかどうかをテストします。

うん。 詳細なメモなしで終了することをお詫びします。

@ hcho3更新はありますか?

私はまだそれを回避していません。 今週見てみましょう。

@trivialfisバグを再現することができました。 提供されたスクリプトを実行し、 FALSEに対してidentical(pred, pred.loaded) FALSEを取得しました。 あなたが提案したように新しい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')

2つのバイナリファイル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

追加のラウンドトリップを1回実行しようとしましたが、予測はこれ以上変更されません。

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ラウンドで最適なモデルを見つけます。 メモリ内のモデルオブジェクトには、削除されたツリーがないため、6378ツリーではなく6381ツリーが含まれています。 どの反復が最適であったかを記憶する追加のフィールド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ツリーがあることを期待します。

@ hcho3https://github.com/dmlc/xgboost/issues/4052でも同様の問題だと思います

bset_iterationLearner::attributes_に保存する必要があります。これには、 xgboost::xgb.attrからアクセスできます。

@ hcho3 、いい発見!

xgboost:::predict.xgb.Booster()のドキュメントにも注意してください:

image

私が正しく理解している場合、ドキュメントは完全に正しくありませんか? ドキュメントに基づいて、予測ではすでにすべてのツリーが使用されていると予想していました。 残念ながら、私はこれを確認していませんでした。

@DavorJ早期停止がアクティブ化されると、 predict()best_iterationフィールドを使用して予測を取得します。

@trivialfis xgb.predict()は早期停止からの情報をまったく使用しないため、Python側の状況はさらに悪化します。

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引数として渡すことを忘れないでください。 これはエラーが発生しやすく、不快な驚きをもたらします。

修正には2つのオプションがあります。

  1. best_iteration過ぎたツリーを物理的に削除します。
  2. モデルをシリアル化するときにbest_iteration情報を保持し、 predict()関数にそれを使用させます。

@ hcho3私はこれについて半ば焼けた考えを持っています。これは、 process_type = updateオプションとフォレストにも関連しています。

バックグラウンド

updateで発生する問題の簡単な要約として、 num_boost_round使用されるupdateが既存のツリーの数より少ない場合、更新されていないツリーは削除されます。

フォレストに関する問題の簡単な紹介として、 predict関数は反復ではなく特定の数のツリーを必要とするため、 best_iterationはフォレストに適用されません。したがって、Pythonにはbest_ntree_limitと呼ばれるものがあります。 、これは私にとって非常に混乱しています。 私は明示的に置き換えるntree_limitinplace_predictiteration_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 評価