Xgboost: [R] Индекс лучшей итерации от ранней остановки отбрасывается при сохранении модели на диск.

Созданный на 15 янв. 2020  ·  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

Они близки, но не совпадают. Различия между этими двумя прогнозами варьируются от -1317.094 до 1088.859 на наборе из 25 тысяч выборок. При сравнении с истинными метками 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)

Можете ли вы предоставить нам фиктивные данные, где происходит это явление?

Вот и все (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))

На моей машине identical(pred, pred.loaded) ЛОЖНО (т.е. должно быть ИСТИННО). Вот результат последних команд:

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

Значения хэша (via sha256sum ./booster.json ) booster.json и booster-1.json абсолютно одинаковы, поэтому я предполагаю, что где-то есть несоответствие, вызванное арифметикой с плавающей запятой.

Зачем закрывать проблему, не зная причины?

@trivialfis Получили ли вы True за identical(pred, pred.loaded) ? OP спрашивает, почему прогнозы не совпадают, хотя две модели имеют одинаковую двоичную подпись.

Попробую воспроизвести сам.

Ой, извини. Причина, которую я обнаружил, - это кеш предсказаний. После загрузки модели значения прогноза берутся из истинного прогноза, а не из кешированного значения:

так что я предполагаю, что где-то есть несоответствие, вызванное арифметикой с плавающей запятой.

Значит, кеш предсказаний деструктивно взаимодействует с арифметикой с плавающей запятой?

@ hcho3 Это проблема, которую я обнаружил при внедрении нового метода травления. Я считаю, что это играет здесь важную роль. Поэтому сначала уменьшите количество деревьев до 1000 (что все еще довольно велико и должно быть достаточно для демонстрации).

Восстановите DMatrix перед предсказанием, чтобы избавиться от кеша:

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

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

Он пройдет тест identical . Иначе не получится.

При добавлении большего количества деревьев идентичный тест все еще имеет небольшие отличия (1e-7 для 2000 деревьев). Но нужно ли нам выводить побитно идентичный результат даже в многопоточной среде?

Поскольку суммирование с плавающей запятой не ассоциативно, мы можем сделать его как элемент дела, чтобы иметь надежную гарантию порядка вычислений, если это необходимо.

На самом деле сделать строгую гарантию заказа не удастся (очень поможет, но все равно будут несоответствия). Плавающая точка в регистре CPU FPU может иметь более высокую точность, чем сохраняться обратно в память. (Аппаратная реализация может использовать более высокую точность для промежуточных значений, https://en.wikipedia.org/wiki/Extended_precision). Я хочу сказать, что когда результат для 1000 деревьев точно воспроизводится в пределах 32-битного числа с плавающей запятой, это вряд ли является ошибкой программирования.

Я согласен с тем, что суммирование с плавающей запятой не ассоциативно. Я сам запущу сценарий и посмотрю, достаточно ли мала разница, чтобы ее можно было отнести к арифметике с плавающей запятой.

В общем, я обычно использую np.testing.assert_almost_equal с decimal=5 чтобы проверить, почти равны ли два массива с плавающей запятой.

Ага. Приносим извинения за закрытие без подробных заметок.

@ hcho3 Есть обновления?

Я еще не дошел до этого. Позвольте мне взглянуть на эту неделю.

@trivialfis Мне удалось воспроизвести ошибку. Я запустил предоставленный сценарий и получил 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.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

Только что попробовал сделать еще один дополнительный обход, и теперь прогнозы больше не меняются.

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 для получения прогноза.

@trivialfis На стороне 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

Пользователи должны не забыть получить bst.best_iteration и передать его как аргумент ntree_limit при вызове predict() . Это чревато ошибками и вызывает неприятный сюрприз.

У нас есть два варианта исправления:

  1. Физически удалить деревья, прошедшие best_iteration .
  2. Сохранять информацию best_iteration при сериализации модели и использовать ее функцией predict() .

@ hcho3 У меня есть наполовину об этом представление, которое также связано с нашей опцией process_type = update и лесом.

Фон

Для краткого обзора проблем, которые у нас есть с update , если num_boost_round используемое с update , меньше, чем количество уже существующих деревьев, те деревья, которые не обновляются, будут удалены. .

Для краткого ознакомления с проблемами с лесом, best_iteration не применяется к лесу, поскольку функция predict требует определенного количества деревьев вместо итераций, поэтому в 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, является привлекательным, поскольку нам не нужно иметь дело с какими-либо несоответствиями из-за наличия фактической длины модели и теоретической длины модели одновременно.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги