これらの値は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トレーニングプロセスのアップロードと文書化は、ここでは重要ではないようです。 必要に応じて詳細を提供したり、ダミーデータを使用してシミュレーションを行ってポイントを証明したりできます。
バイナリと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.json
とbooster-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_equal
とdecimal=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.raw
とbooster.raw.roundtrip
は同一でした。
pred
とpred.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_iteration
はLearner::attributes_
に保存する必要があります。これには、 xgboost::xgb.attr
からアクセスできます。
@ hcho3 、いい発見!
xgboost:::predict.xgb.Booster()
のドキュメントにも注意してください:
私が正しく理解している場合、ドキュメントは完全に正しくありませんか? ドキュメントに基づいて、予測ではすでにすべてのツリーが使用されていると予想していました。 残念ながら、私はこれを確認していませんでした。
@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つのオプションがあります。
best_iteration
過ぎたツリーを物理的に削除します。best_iteration
情報を保持し、 predict()
関数にそれを使用させます。@ hcho3私はこれについて半ば焼けた考えを持っています。これは、 process_type = update
オプションとフォレストにも関連しています。
update
で発生する問題の簡単な要約として、 num_boost_round
使用されるupdate
が既存のツリーの数より少ない場合、更新されていないツリーは削除されます。
フォレストに関する問題の簡単な紹介として、 predict
関数は反復ではなく特定の数のツリーを必要とするため、 best_iteration
はフォレストに適用されません。したがって、Pythonにはbest_ntree_limit
と呼ばれるものがあります。 、これは私にとって非常に混乱しています。 私は明示的に置き換えるntree_limit
でinplace_predict
とiteration_range
この属性を避けるために。
slice
とconcat
メソッドをbooster
に追加します。これにより、ツリーが2つのモデルに抽出され、2つのモデルのツリーが1に連結されます。これらの2つのメソッドがある場合:
base_margin_
は不要になり、他のユーザーにとってより直感的になると思います。ntree_limit
は不要になりました。モデルをスライスし、スライスに対して予測を実行するだけです。update
プロセスは自己完結型であり、スライス内のツリーを一度に更新するだけで、 num_boost_rounds
はありません。また、これはどういうわけかマルチターゲットツリーに関連していると思います。 将来的にマルチクラスマルチターゲットツリーをサポートできるかのように、ツリーを配置する方法は複数あります。たとえば、クラスごと、またはターゲットごとにoutput_groups
を使用して、フォレストやベクターリーフと組み合わせます。 ntree_limit
では不十分です。
また、#5531。
しかし、アイデアはかなり早いので、私はそれを共有する自信がありませんでした、今私たちはこの問題に取り組んでいます、多分私はこれについていくつかのインプットを得ることができます。
1.1のタイムラインを前提として、ドキュメントを拡張して、ユーザーがこの最良の反復を手動でキャプチャして予測に使用する方法を明確にすることはできますか?
そして、それをリリースノートの既知の問題に追加しますか?
@trivialfisは、これを行うことによって構成の問題をさらに複雑にしない限り、面白
@ hcho3で提案されているように、モデルから余分なツリーを削除することは、実際のモデルの長さと理論上のモデルの長さを同時に持つことによる不整合に対処する必要がないため、魅力的です。
最も参考になるコメント
謎が解けた。 本当の原因を特定しました。 モデルがディスクに保存されると、早期停止に関する情報は破棄されます。 この例では、XGBoostは6381のブーストラウンドを実行し、6378ラウンドで最適なモデルを見つけます。 メモリ内のモデルオブジェクトには、削除されたツリーがないため、6378ツリーではなく6381ツリーが含まれています。 どの反復が最適であったかを記憶する追加のフィールド
best_iteration
があります。モデルをディスクに保存すると、この余分なフィールドはサイレントに破棄されます。 したがって、元のモデルの
predict()
は6378ツリーを使用しますが、復元されたモデルのpredict()
は6381ツリーを使用します。