From 62571b79eb08398a031873c3704da4e9cfd2c301 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 5 Dec 2023 20:13:14 +0100 Subject: [PATCH 01/59] [R] Enable multi-output objectives (#9839) --- R-package/R/utils.R | 27 +++++++------- R-package/R/xgb.DMatrix.R | 7 ++-- R-package/src/init.c | 2 +- R-package/src/xgboost_R.cc | 18 ++++------ R-package/tests/testthat/test_basic.R | 51 +++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 27 deletions(-) diff --git a/R-package/R/utils.R b/R-package/R/utils.R index 2fc2321ca..6f1a1b4ec 100644 --- a/R-package/R/utils.R +++ b/R-package/R/utils.R @@ -160,23 +160,24 @@ xgb.iter.update <- function(booster_handle, dtrain, iter, obj) { ) gpair <- obj(pred, dtrain) n_samples <- dim(dtrain)[1] + grad <- gpair$grad + hess <- gpair$hess - msg <- paste( - "Since 2.1.0, the shape of the gradient and hessian is required to be ", - "(n_samples, n_targets) or (n_samples, n_classes).", - sep = "" - ) - if (is.matrix(gpair$grad) && dim(gpair$grad)[1] != n_samples) { - warning(msg) - } - if (is.numeric(gpair$grad) && length(gpair$grad) != n_samples) { - warning(msg) + if ((is.matrix(grad) && dim(grad)[1] != n_samples) || + (is.vector(grad) && length(grad) != n_samples) || + (is.vector(grad) != is.vector(hess))) { + warning(paste( + "Since 2.1.0, the shape of the gradient and hessian is required to be ", + "(n_samples, n_targets) or (n_samples, n_classes). Will reshape assuming ", + "column-major order.", + sep = "" + )) + grad <- matrix(grad, nrow = n_samples) + hess <- matrix(hess, nrow = n_samples) } - gpair$grad <- matrix(gpair$grad, nrow = n_samples) - gpair$hess <- matrix(gpair$hess, nrow = n_samples) .Call( - XGBoosterBoostOneIter_R, booster_handle, dtrain, iter, gpair$grad, gpair$hess + XGBoosterTrainOneIter_R, booster_handle, dtrain, iter, grad, hess ) } return(TRUE) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 8e19e87b0..3a1b6afc8 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -243,6 +243,9 @@ getinfo.xgb.DMatrix <- function(object, name, ...) { ret <- .Call(XGDMatrixGetStrFeatureInfo_R, object, name) } else if (name != "nrow") { ret <- .Call(XGDMatrixGetInfo_R, object, name) + if (length(ret) > nrow(object)) { + ret <- matrix(ret, nrow = nrow(object), byrow = TRUE) + } } else { ret <- nrow(object) } @@ -286,9 +289,9 @@ setinfo <- function(object, ...) UseMethod("setinfo") #' @export setinfo.xgb.DMatrix <- function(object, name, info, ...) { if (name == "label") { - if (length(info) != nrow(object)) + if (NROW(info) != nrow(object)) stop("The length of labels must equal to the number of rows in the input data") - .Call(XGDMatrixSetInfo_R, object, name, as.numeric(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } if (name == "label_lower_bound") { diff --git a/R-package/src/init.c b/R-package/src/init.c index 5c8e179d6..afac524e3 100644 --- a/R-package/src/init.c +++ b/R-package/src/init.c @@ -52,7 +52,7 @@ extern SEXP XGBGetGlobalConfig_R(void); extern SEXP XGBoosterFeatureScore_R(SEXP, SEXP); static const R_CallMethodDef CallEntries[] = { - {"XGBoosterBoostOneIter_R", (DL_FUNC) &XGBoosterTrainOneIter_R, 5}, + {"XGBoosterTrainOneIter_R", (DL_FUNC) &XGBoosterTrainOneIter_R, 5}, {"XGBoosterCreate_R", (DL_FUNC) &XGBoosterCreate_R, 1}, {"XGBoosterCreateInEmptyObj_R", (DL_FUNC) &XGBoosterCreateInEmptyObj_R, 2}, {"XGBoosterDumpModel_R", (DL_FUNC) &XGBoosterDumpModel_R, 4}, diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 8da00aa58..1ab3e7641 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -342,9 +342,11 @@ XGB_DLL SEXP XGDMatrixSaveBinary_R(SEXP handle, SEXP fname, SEXP silent) { XGB_DLL SEXP XGDMatrixSetInfo_R(SEXP handle, SEXP field, SEXP array) { R_API_BEGIN(); SEXP field_ = PROTECT(Rf_asChar(field)); + SEXP arr_dim = Rf_getAttrib(array, R_DimSymbol); int res_code; { - const std::string array_str = MakeArrayInterfaceFromRVector(array); + const std::string array_str = Rf_isNull(arr_dim)? + MakeArrayInterfaceFromRVector(array) : MakeArrayInterfaceFromRMat(array); res_code = XGDMatrixSetInfoFromInterface( R_ExternalPtrAddr(handle), CHAR(field_), array_str.c_str()); } @@ -513,20 +515,14 @@ XGB_DLL SEXP XGBoosterTrainOneIter_R(SEXP handle, SEXP dtrain, SEXP iter, SEXP g R_API_BEGIN(); CHECK_EQ(Rf_xlength(grad), Rf_xlength(hess)) << "gradient and hess must have same length."; SEXP gdim = getAttrib(grad, R_DimSymbol); - auto n_samples = static_cast(INTEGER(gdim)[0]); - auto n_targets = static_cast(INTEGER(gdim)[1]); - SEXP hdim = getAttrib(hess, R_DimSymbol); - CHECK_EQ(INTEGER(hdim)[0], n_samples) << "mismatched size between gradient and hessian"; - CHECK_EQ(INTEGER(hdim)[1], n_targets) << "mismatched size between gradient and hessian"; - double const *d_grad = REAL(grad); - double const *d_hess = REAL(hess); int res_code; { - auto ctx = xgboost::detail::BoosterCtx(R_ExternalPtrAddr(handle)); - auto [s_grad, s_hess] = xgboost::detail::MakeGradientInterface( - ctx, d_grad, d_hess, xgboost::linalg::kF, n_samples, n_targets); + const std::string s_grad = Rf_isNull(gdim)? + MakeArrayInterfaceFromRVector(grad) : MakeArrayInterfaceFromRMat(grad); + const std::string s_hess = Rf_isNull(hdim)? + MakeArrayInterfaceFromRVector(hess) : MakeArrayInterfaceFromRMat(hess); res_code = XGBoosterTrainOneIter(R_ExternalPtrAddr(handle), R_ExternalPtrAddr(dtrain), asInteger(iter), s_grad.c_str(), s_hess.c_str()); } diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index b7e819738..97c1353dc 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -565,3 +565,54 @@ test_that("'predict' accepts CSR data", { expect_equal(p_csc, p_csr) expect_equal(p_csc, p_spv) }) + +test_that("Can use multi-output labels with built-in objectives", { + data("mtcars") + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + y_mirrored <- cbind(y, -y) + dm <- xgb.DMatrix(x, label = y_mirrored, nthread = n_threads) + model <- xgb.train( + params = list( + tree_method = "hist", + multi_strategy = "multi_output_tree", + objective = "reg:squarederror", + nthread = n_threads + ), + data = dm, + nrounds = 5 + ) + pred <- predict(model, x, reshape = TRUE) + expect_equal(pred[, 1], -pred[, 2]) + expect_true(cor(y, pred[, 1]) > 0.9) + expect_true(cor(y, pred[, 2]) < -0.9) +}) + +test_that("Can use multi-output labels with custom objectives", { + data("mtcars") + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + y_mirrored <- cbind(y, -y) + dm <- xgb.DMatrix(x, label = y_mirrored, nthread = n_threads) + model <- xgb.train( + params = list( + tree_method = "hist", + multi_strategy = "multi_output_tree", + base_score = 0, + objective = function(pred, dtrain) { + y <- getinfo(dtrain, "label") + grad <- pred - y + hess <- rep(1, nrow(grad) * ncol(grad)) + hess <- matrix(hess, nrow = nrow(grad)) + return(list(grad = grad, hess = hess)) + }, + nthread = n_threads + ), + data = dm, + nrounds = 5 + ) + pred <- predict(model, x, reshape = TRUE) + expect_equal(pred[, 1], -pred[, 2]) + expect_true(cor(y, pred[, 1]) > 0.9) + expect_true(cor(y, pred[, 2]) < -0.9) +}) From 0716c64ef75d62384437f587eab8fa3925763e38 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 6 Dec 2023 10:43:51 +0100 Subject: [PATCH 02/59] [R] Error out on multidimensional arrays (#9852) --- R-package/src/xgboost_R.cc | 3 +++ R-package/tests/testthat/test_dmatrix.R | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 1ab3e7641..214ca6cb4 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -50,6 +50,9 @@ SEXP SafeMkChar(const char *c_str, SEXP continuation_token) { [[nodiscard]] std::string MakeArrayInterfaceFromRMat(SEXP R_mat) { SEXP mat_dims = Rf_getAttrib(R_mat, R_DimSymbol); + if (Rf_xlength(mat_dims) > 2) { + LOG(FATAL) << "Passed input array with more than two dimensions, which is not supported."; + } const int *ptr_mat_dims = INTEGER(mat_dims); // Lambda for type dispatch. diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index a0cf90088..4db7aad08 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -297,3 +297,11 @@ test_that("xgb.DMatrix: Inf as missing", { file.remove("inf.dmatrix") file.remove("nan.dmatrix") }) + +test_that("xgb.DMatrix: error on three-dimensional array", { + set.seed(123) + x <- matrix(rnorm(500), nrow = 50) + y <- rnorm(400) + dim(y) <- c(50, 4, 2) + expect_error(xgb.DMatrix(data = x, label = y)) +}) From 1de3f4135cb56cebd1edb3f8f65e1acbd22ce829 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 6 Dec 2023 13:32:20 +0100 Subject: [PATCH 03/59] [R] Enable vector-valued parameters (#9849) --- R-package/R/utils.R | 8 ++++++++ R-package/R/xgb.Booster.R | 8 +++++++- R-package/tests/testthat/test_basic.R | 27 +++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/R-package/R/utils.R b/R-package/R/utils.R index 6f1a1b4ec..bf08c481d 100644 --- a/R-package/R/utils.R +++ b/R-package/R/utils.R @@ -93,6 +93,14 @@ check.booster.params <- function(params, ...) { interaction_constraints <- sapply(params[['interaction_constraints']], function(x) paste0('[', paste(x, collapse = ','), ']')) params[['interaction_constraints']] <- paste0('[', paste(interaction_constraints, collapse = ','), ']') } + + # for evaluation metrics, should generate multiple entries per metric + if (NROW(params[['eval_metric']]) > 1) { + eval_metrics <- as.list(params[["eval_metric"]]) + names(eval_metrics) <- rep("eval_metric", length(eval_metrics)) + params_without_ev_metrics <- within(params, rm("eval_metric")) + params <- c(params_without_ev_metrics, eval_metrics) + } return(params) } diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index 37cfc199e..a5c9e5088 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -697,7 +697,13 @@ xgb.config <- function(object) { stop("parameter names cannot be empty strings") } names(p) <- gsub(".", "_", names(p), fixed = TRUE) - p <- lapply(p, function(x) as.character(x)[1]) + p <- lapply(p, function(x) { + if (is.vector(x) && length(x) == 1) { + return(as.character(x)[1]) + } else { + return(jsonlite::toJSON(x, auto_unbox = TRUE)) + } + }) handle <- xgb.get.handle(object) for (i in seq_along(p)) { .Call(XGBoosterSetParam_R, handle, names(p[i]), p[[i]]) diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index 97c1353dc..c96871e11 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -566,6 +566,33 @@ test_that("'predict' accepts CSR data", { expect_equal(p_csc, p_spv) }) +test_that("Quantile regression accepts multiple quantiles", { + data(mtcars) + y <- mtcars[, 1] + x <- as.matrix(mtcars[, -1]) + dm <- xgb.DMatrix(data = x, label = y) + model <- xgb.train( + data = dm, + params = list( + objective = "reg:quantileerror", + tree_method = "exact", + quantile_alpha = c(0.05, 0.5, 0.95), + nthread = n_threads + ), + nrounds = 15 + ) + pred <- predict(model, x, reshape = TRUE) + + expect_equal(dim(pred)[1], nrow(x)) + expect_equal(dim(pred)[2], 3) + expect_true(all(pred[, 1] <= pred[, 3])) + + cors <- cor(y, pred) + expect_true(cors[2] > cors[1]) + expect_true(cors[2] > cors[3]) + expect_true(cors[2] > 0.85) +}) + test_that("Can use multi-output labels with built-in objectives", { data("mtcars") y <- mtcars$mpg From 4bc1f3a388b9d5e4cb8260eb20aa56ab3f371bb8 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 7 Dec 2023 00:12:45 +0800 Subject: [PATCH 04/59] [R] Bump requirement to 4.3.0. (#9847) --- .github/workflows/r_tests.yml | 6 +++--- R-package/CMakeLists.txt | 9 +++++++++ R-package/DESCRIPTION | 2 +- tests/ci_build/test_r_package.py | 2 ++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/r_tests.yml b/.github/workflows/r_tests.yml index c353fd0d2..917245ec6 100644 --- a/.github/workflows/r_tests.yml +++ b/.github/workflows/r_tests.yml @@ -25,7 +25,7 @@ jobs: with: submodules: 'true' - - uses: r-lib/actions/setup-r@11a22a908006c25fe054c4ef0ac0436b1de3edbe # v2.6.4 + - uses: r-lib/actions/setup-r@e40ad904310fc92e96951c1b0d64f3de6cbe9e14 # v2.6.5 with: r-version: ${{ matrix.config.r }} @@ -54,7 +54,7 @@ jobs: matrix: config: - {os: windows-latest, r: 'release', compiler: 'mingw', build: 'autotools'} - - {os: windows-latest, r: '4.2.0', compiler: 'msvc', build: 'cmake'} + - {os: windows-latest, r: '4.3.0', compiler: 'msvc', build: 'cmake'} env: R_REMOTES_NO_ERRORS_FROM_WARNINGS: true RSPM: ${{ matrix.config.rspm }} @@ -64,7 +64,7 @@ jobs: with: submodules: 'true' - - uses: r-lib/actions/setup-r@11a22a908006c25fe054c4ef0ac0436b1de3edbe # v2.6.4 + - uses: r-lib/actions/setup-r@e40ad904310fc92e96951c1b0d64f3de6cbe9e14 # v2.6.5 with: r-version: ${{ matrix.config.r }} diff --git a/R-package/CMakeLists.txt b/R-package/CMakeLists.txt index a19e56f4e..d3a69abc2 100644 --- a/R-package/CMakeLists.txt +++ b/R-package/CMakeLists.txt @@ -14,6 +14,15 @@ if(ENABLE_ALL_WARNINGS) target_compile_options(xgboost-r PRIVATE -Wall -Wextra) endif() +if(MSVC) + # https://github.com/microsoft/LightGBM/pull/6061 + # MSVC doesn't work with anonymous types in structs. (R complex) + # + # syntax error: missing ';' before identifier 'private_data_c' + # + target_compile_definitions(xgboost-r PRIVATE -DR_LEGACY_RCOMPLEX) +endif() + target_compile_definitions( xgboost-r PUBLIC -DXGBOOST_STRICT_R_MODE=1 diff --git a/R-package/DESCRIPTION b/R-package/DESCRIPTION index d301b0a5c..92b53a660 100644 --- a/R-package/DESCRIPTION +++ b/R-package/DESCRIPTION @@ -58,7 +58,7 @@ Suggests: float, titanic Depends: - R (>= 3.3.0) + R (>= 4.3.0) Imports: Matrix (>= 1.1-0), methods, diff --git a/tests/ci_build/test_r_package.py b/tests/ci_build/test_r_package.py index 853bf0502..cc1225b03 100644 --- a/tests/ci_build/test_r_package.py +++ b/tests/ci_build/test_r_package.py @@ -261,6 +261,8 @@ def test_with_cmake(args: argparse.Namespace) -> None: "-DCMAKE_CONFIGURATION_TYPES=Release", "-A", "x64", + "-G", + "Visual Studio 17 2022", ] ) subprocess.check_call( From 9e9d41b95c0074c49c98e06fe1ee9716fbc123e7 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 6 Dec 2023 20:11:15 +0100 Subject: [PATCH 05/59] [R] Add note about serialization of DMatrix objects (#9853) --- R-package/R/xgb.DMatrix.R | 6 ++++++ R-package/man/xgb.DMatrix.Rd | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 3a1b6afc8..0d9a89912 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -16,6 +16,12 @@ #' @param nthread Number of threads used for creating DMatrix. #' @param ... the \code{info} data could be passed directly as parameters, without creating an \code{info} list. #' +#' @details +#' Note that DMatrix objects are not serializable through R functions such as \code{saveRDS} or \code{save}. +#' If a DMatrix gets serialized and then de-serialized (for example, when saving data in an R session or caching +#' chunks in an Rmd file), the resulting object will not be usable anymore and will need to be reconstructed +#' from the original source of data. +#' #' @examples #' data(agaricus.train, package='xgboost') #' ## Keep the number of threads to 1 for examples diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index 38a65c638..96f050c72 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -36,6 +36,12 @@ Construct xgb.DMatrix object from either a dense matrix, a sparse matrix, or a l Supported input file formats are either a LIBSVM text file or a binary file that was created previously by \code{\link{xgb.DMatrix.save}}). } +\details{ +Note that DMatrix objects are not serializable through R functions such as \code{saveRDS} or \code{save}. +If a DMatrix gets serialized and then de-serialized (for example, when saving data in an R session or caching +chunks in an Rmd file), the resulting object will not be usable anymore and will need to be reconstructed +from the original source of data. +} \examples{ data(agaricus.train, package='xgboost') ## Keep the number of threads to 1 for examples From 2c0fc9730600980a0571329e35772dac8e173ed2 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 6 Dec 2023 22:17:15 +0100 Subject: [PATCH 06/59] Remove note about multi-quantile being python-only (#9854) --- demo/guide-python/quantile_regression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/guide-python/quantile_regression.py b/demo/guide-python/quantile_regression.py index 4f69a8c80..5d186714c 100644 --- a/demo/guide-python/quantile_regression.py +++ b/demo/guide-python/quantile_regression.py @@ -9,7 +9,7 @@ https://scikit-learn.org/stable/auto_examples/ensemble/plot_gradient_boosting_qu .. note:: - The feature is only supported using the Python package. In addition, quantile + The feature is only supported using the Python, R, and C packages. In addition, quantile crossing can happen due to limitation in the algorithm. """ From 39c637ee199303d1c84ef25e55d2651d83c217e5 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Fri, 8 Dec 2023 03:42:14 +0800 Subject: [PATCH 07/59] Use array interface in Python prediction return. (#9855) --- include/xgboost/linalg.h | 4 +- python-package/xgboost/compat.py | 10 ++++ python-package/xgboost/core.py | 81 ++++++++++++++------------------ src/common/device_helpers.cuh | 2 + 4 files changed, 49 insertions(+), 48 deletions(-) diff --git a/include/xgboost/linalg.h b/include/xgboost/linalg.h index 8806818fb..581b2f080 100644 --- a/include/xgboost/linalg.h +++ b/include/xgboost/linalg.h @@ -683,7 +683,7 @@ using MatrixView = TensorView; * * `stream` is optionally included when data is on CUDA device. */ -template +template Json ArrayInterface(TensorView const &t) { Json array_interface{Object{}}; array_interface["data"] = std::vector(2); @@ -691,7 +691,7 @@ Json ArrayInterface(TensorView const &t) { array_interface["data"][1] = Boolean{true}; if (t.Device().IsCUDA()) { // Change this once we have different CUDA stream. - array_interface["stream"] = Null{}; + array_interface["stream"] = Integer{2}; } std::vector shape(t.Shape().size()); std::vector stride(t.Stride().size()); diff --git a/python-package/xgboost/compat.py b/python-package/xgboost/compat.py index c40dea5fd..7c11495f7 100644 --- a/python-package/xgboost/compat.py +++ b/python-package/xgboost/compat.py @@ -100,6 +100,16 @@ def is_cupy_available() -> bool: return False +def import_cupy() -> types.ModuleType: + """Import cupy.""" + if not is_cupy_available(): + raise ImportError("`cupy` is required for handling CUDA buffer.") + + import cupy # pylint: disable=import-error + + return cupy + + try: import scipy.sparse as scipy_sparse from scipy.sparse import csr_matrix as scipy_csr diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index bfc94aa04..ffc7db8dd 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -3,7 +3,6 @@ """Core XGBoost Library.""" import copy import ctypes -import importlib.util import json import os import re @@ -45,7 +44,6 @@ from ._typing import ( CStrPptr, CStrPtr, CTypeT, - CupyT, DataType, FeatureInfo, FeatureNames, @@ -55,7 +53,7 @@ from ._typing import ( TransformedData, c_bst_ulong, ) -from .compat import PANDAS_INSTALLED, DataFrame, py_str +from .compat import PANDAS_INSTALLED, DataFrame, import_cupy, py_str from .libpath import find_lib_path @@ -380,34 +378,6 @@ def ctypes2numpy(cptr: CNumericPtr, length: int, dtype: Type[np.number]) -> np.n return res -def ctypes2cupy(cptr: CNumericPtr, length: int, dtype: Type[np.number]) -> CupyT: - """Convert a ctypes pointer array to a cupy array.""" - # pylint: disable=import-error - import cupy - from cupy.cuda.memory import MemoryPointer, UnownedMemory - - CUPY_TO_CTYPES_MAPPING: Dict[Type[np.number], Type[CNumeric]] = { - cupy.float32: ctypes.c_float, - cupy.uint32: ctypes.c_uint, - } - if dtype not in CUPY_TO_CTYPES_MAPPING: - raise RuntimeError(f"Supported types: {CUPY_TO_CTYPES_MAPPING.keys()}") - addr = ctypes.cast(cptr, ctypes.c_void_p).value - # pylint: disable=c-extension-no-member,no-member - device = cupy.cuda.runtime.pointerGetAttributes(addr).device - # The owner field is just used to keep the memory alive with ref count. As - # unowned's life time is scoped within this function we don't need that. - unownd = UnownedMemory( - addr, length * ctypes.sizeof(CUPY_TO_CTYPES_MAPPING[dtype]), owner=None - ) - memptr = MemoryPointer(unownd, 0) - # pylint: disable=unexpected-keyword-arg - mem = cupy.ndarray((length,), dtype=dtype, memptr=memptr) - assert mem.device.id == device - arr = cupy.array(mem, copy=True) - return arr - - def ctypes2buffer(cptr: CStrPtr, length: int) -> bytearray: """Convert ctypes pointer to buffer type.""" if not isinstance(cptr, ctypes.POINTER(ctypes.c_char)): @@ -466,14 +436,8 @@ def from_array_interface(interface: dict) -> NumpyOrCupy: if "stream" in interface: # CUDA stream is presented, this is a __cuda_array_interface__. - spec = importlib.util.find_spec("cupy") - if spec is None: - raise ImportError("`cupy` is required for handling CUDA buffer.") - - import cupy as cp # pylint: disable=import-error - arr.__cuda_array_interface__ = interface - out = cp.array(arr, copy=True) + out = import_cupy().array(arr, copy=True) else: arr.__array_interface__ = interface out = np.array(arr, copy=True) @@ -481,17 +445,42 @@ def from_array_interface(interface: dict) -> NumpyOrCupy: return out +def make_array_interface( + ptr: CNumericPtr, shape: Tuple[int, ...], dtype: Type[np.number], is_cuda: bool +) -> Dict[str, Union[int, tuple, None]]: + """Make an __(cuda)_array_interface__ from a pointer.""" + # Use an empty array to handle typestr and descr + if is_cuda: + empty = import_cupy().empty(shape=(0,), dtype=dtype) + array = empty.__cuda_array_interface__ # pylint: disable=no-member + else: + empty = np.empty(shape=(0,), dtype=dtype) + array = empty.__array_interface__ # pylint: disable=no-member + + addr = ctypes.cast(ptr, ctypes.c_void_p).value + length = int(np.prod(shape)) + # Handle empty dataset. + assert addr is not None or length == 0 + + if addr is None: + return array + + array["data"] = (addr, True) + if is_cuda: + array["stream"] = 2 + array["shape"] = shape + array["strides"] = None + return array + + def _prediction_output( shape: CNumericPtr, dims: c_bst_ulong, predts: CFloatPtr, is_cuda: bool ) -> NumpyOrCupy: - arr_shape = ctypes2numpy(shape, dims.value, np.uint64) - length = int(np.prod(arr_shape)) - if is_cuda: - arr_predict = ctypes2cupy(predts, length, np.float32) - else: - arr_predict = ctypes2numpy(predts, length, np.float32) - arr_predict = arr_predict.reshape(arr_shape) - return arr_predict + arr_shape = tuple(ctypes2numpy(shape, dims.value, np.uint64).flatten()) + array = from_array_interface( + make_array_interface(predts, arr_shape, np.float32, is_cuda) + ) + return array class DataIter(ABC): # pylint: disable=too-many-instance-attributes diff --git a/src/common/device_helpers.cuh b/src/common/device_helpers.cuh index ffe61800e..46f76c415 100644 --- a/src/common/device_helpers.cuh +++ b/src/common/device_helpers.cuh @@ -1097,6 +1097,8 @@ inline void CUDAEvent::Record(CUDAStreamView stream) { // NOLINT dh::safe_cuda(cudaEventRecord(event_, cudaStream_t{stream})); } +// Changing this has effect on prediction return, where we need to pass the pointer to +// third-party libraries like cuPy inline CUDAStreamView DefaultStream() { #ifdef CUDA_API_PER_THREAD_DEFAULT_STREAM return CUDAStreamView{cudaStreamPerThread}; From 42de9206fca7e4674ae837020e758d87e12d63a4 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Fri, 8 Dec 2023 05:50:41 +0800 Subject: [PATCH 08/59] Support multi-target, fit intercept for hinge. (#9850) --- src/common/linalg_op.cuh | 54 ++++++++----- src/common/linalg_op.h | 30 ++++--- src/objective/hinge.cu | 126 ++++++++++++++++-------------- src/objective/quantile_obj.cu | 33 ++++---- src/objective/regression_obj.cu | 43 +++++----- tests/cpp/common/test_linalg.cu | 12 ++- tests/cpp/data/test_metainfo.h | 23 +++--- tests/cpp/objective/test_hinge.cc | 55 +++++++++---- 8 files changed, 221 insertions(+), 155 deletions(-) diff --git a/src/common/linalg_op.cuh b/src/common/linalg_op.cuh index 361f926e9..9ef36598d 100644 --- a/src/common/linalg_op.cuh +++ b/src/common/linalg_op.cuh @@ -1,31 +1,48 @@ -/*! - * Copyright 2021-2022 by XGBoost Contributors +/** + * Copyright 2021-2023, XGBoost Contributors */ #ifndef XGBOOST_COMMON_LINALG_OP_CUH_ #define XGBOOST_COMMON_LINALG_OP_CUH_ -#include "device_helpers.cuh" +#include // for int32_t +#include // for size_t +#include // for apply + +#include "device_helpers.cuh" // for LaunchN #include "linalg_op.h" -#include "xgboost/context.h" -#include "xgboost/linalg.h" +#include "xgboost/context.h" // for Context +#include "xgboost/linalg.h" // for TensorView namespace xgboost { namespace linalg { -template -void ElementWiseKernelDevice(linalg::TensorView t, Fn&& fn, cudaStream_t s = nullptr) { - dh::safe_cuda(cudaSetDevice(t.Device().ordinal)); - static_assert(std::is_void>::value, - "For function with return, use transform instead."); - if (t.Contiguous()) { - auto ptr = t.Values().data(); - dh::LaunchN(t.Size(), s, [=] __device__(size_t i) mutable { fn(i, ptr[i]); }); - } else { - dh::LaunchN(t.Size(), s, [=] __device__(size_t i) mutable { - T& v = detail::Apply(t, linalg::UnravelIndex(i, t.Shape())); - fn(i, v); +namespace cuda_impl { +// Use template specialization to dispatch, Windows + CUDA 11.8 doesn't support extended +// lambda inside constexpr if +template +struct ElementWiseImpl { + template + void operator()(linalg::TensorView t, Fn&& fn, cudaStream_t s) { + static_assert(D > 1); + dh::LaunchN(t.Size(), s, [=] __device__(std::size_t i) mutable { + std::apply(fn, linalg::UnravelIndex(i, t.Shape())); }); } +}; + +template +struct ElementWiseImpl { + template + void operator()(linalg::TensorView t, Fn&& fn, cudaStream_t s) { + dh::LaunchN(t.Size(), s, [=] __device__(std::size_t i) { fn(i); }); + } +}; + +template +void ElementWiseKernel(linalg::TensorView t, Fn&& fn, cudaStream_t s = nullptr) { + dh::safe_cuda(cudaSetDevice(t.Device().ordinal)); + cuda_impl::ElementWiseImpl{}(t, fn, s); } +} // namespace cuda_impl template void ElementWiseTransformDevice(linalg::TensorView t, Fn&& fn, cudaStream_t s = nullptr) { @@ -42,7 +59,8 @@ void ElementWiseTransformDevice(linalg::TensorView t, Fn&& fn, cudaStream_ template void ElementWiseKernel(Context const* ctx, linalg::TensorView t, Fn&& fn) { - ctx->IsCUDA() ? ElementWiseKernelDevice(t, fn) : ElementWiseKernelHost(t, ctx->Threads(), fn); + ctx->IsCUDA() ? cuda_impl::ElementWiseKernel(t, fn) + : ElementWiseKernelHost(t, ctx->Threads(), fn); } } // namespace linalg } // namespace xgboost diff --git a/src/common/linalg_op.h b/src/common/linalg_op.h index d89e5a736..52141164f 100644 --- a/src/common/linalg_op.h +++ b/src/common/linalg_op.h @@ -1,5 +1,5 @@ -/*! - * Copyright 2021-2022 by XGBoost Contributors +/** + * Copyright 2021-2023, XGBoost Contributors */ #ifndef XGBOOST_COMMON_LINALG_OP_H_ #define XGBOOST_COMMON_LINALG_OP_H_ @@ -27,17 +27,23 @@ void ElementWiseTransformHost(linalg::TensorView t, int32_t n_threads, Fn& } } -template -void ElementWiseKernelHost(linalg::TensorView t, int32_t n_threads, Fn&& fn) { - static_assert(std::is_void>::value, - "For function with return, use transform instead."); - if (t.Contiguous()) { - auto ptr = t.Values().data(); - common::ParallelFor(t.Size(), n_threads, [&](size_t i) { fn(i, ptr[i]); }); +template +void ElementWiseKernelHost(linalg::TensorView t, std::int32_t n_threads, Fn &&fn) { + if constexpr (D == 1) { + common::ParallelFor(t.Size(), n_threads, [&](std::size_t i) { fn(i); }); + } else if (D == 2 && t.CContiguous() && t.Shape(0) > t.Shape(1) * 64) { + // Heuristic. Tall, c-contiguous matrix, + auto n_rows = t.Shape(0); + auto n_columns = t.Shape(1); + common::ParallelFor(n_rows, n_threads, [&](std::size_t i) { + for (std::size_t j = 0; j < n_columns; ++j) { + fn(i, j); + } + }); } else { - common::ParallelFor(t.Size(), n_threads, [&](size_t i) { - auto& v = detail::Apply(t, linalg::UnravelIndex(i, t.Shape())); - fn(i, v); + common::ParallelFor(t.Size(), n_threads, [&](std::size_t i) { + auto idx = linalg::UnravelIndex(i, t.Shape()); + std::apply(fn, idx); }); } } diff --git a/src/objective/hinge.cu b/src/objective/hinge.cu index dd9a19b13..fb51f325b 100644 --- a/src/objective/hinge.cu +++ b/src/objective/hinge.cu @@ -4,71 +4,85 @@ * \brief Provides an implementation of the hinge loss function * \author Henry Gouk */ -#include "xgboost/objective.h" -#include "xgboost/json.h" -#include "xgboost/span.h" -#include "xgboost/host_device_vector.h" +#include // for max +#include // for size_t +#include // for int32_t -#include "../common/math.h" -#include "../common/transform.h" -#include "../common/common.h" +#include "../common/common.h" // for Range +#if defined(XGBOOST_USE_CUDA) +#include "../common/linalg_op.cuh" +#endif +#include "../common/linalg_op.h" +#include "../common/optional_weight.h" // for OptionalWeights +#include "../common/transform.h" // for Transform +#include "init_estimation.h" // for FitIntercept +#include "xgboost/data.h" // for MetaInfo +#include "xgboost/host_device_vector.h" // HostDeviceVector +#include "xgboost/json.h" // for Json +#include "xgboost/linalg.h" // for UnravelIndex +#include "xgboost/span.h" // for Span namespace xgboost::obj { - #if defined(XGBOOST_USE_CUDA) DMLC_REGISTRY_FILE_TAG(hinge_obj_gpu); #endif // defined(XGBOOST_USE_CUDA) -class HingeObj : public ObjFunction { +class HingeObj : public FitIntercept { public: HingeObj() = default; - void Configure(Args const&) override {} + void Configure(Args const &) override {} ObjInfo Task() const override { return ObjInfo::kRegression; } - void GetGradient(const HostDeviceVector &preds, const MetaInfo &info, - std::int32_t /*iter*/, linalg::Matrix *out_gpair) override { - CHECK_NE(info.labels.Size(), 0U) << "label set cannot be empty"; - CHECK_EQ(preds.Size(), info.labels.Size()) - << "labels are not correctly provided" - << "preds.size=" << preds.Size() - << ", label.size=" << info.labels.Size(); - - const size_t ndata = preds.Size(); - const bool is_null_weight = info.weights_.Size() == 0; - if (!is_null_weight) { - CHECK_EQ(info.weights_.Size(), ndata) - << "Number of weights should be equal to number of data points."; - } - CHECK_EQ(info.labels.Shape(1), 1) << "Multi-target for `binary:hinge` is not yet supported."; - out_gpair->Reshape(ndata, 1); - common::Transform<>::Init( - [=] XGBOOST_DEVICE(size_t _idx, - common::Span _out_gpair, - common::Span _preds, - common::Span _labels, - common::Span _weights) { - bst_float p = _preds[_idx]; - bst_float w = is_null_weight ? 1.0f : _weights[_idx]; - bst_float y = _labels[_idx] * 2.0 - 1.0; - bst_float g, h; - if (p * y < 1.0) { - g = -y * w; - h = w; - } else { - g = 0.0; - h = std::numeric_limits::min(); - } - _out_gpair[_idx] = GradientPair(g, h); - }, - common::Range{0, static_cast(ndata)}, this->ctx_->Threads(), - ctx_->Device()).Eval( - out_gpair->Data(), &preds, info.labels.Data(), &info.weights_); + [[nodiscard]] bst_target_t Targets(MetaInfo const &info) const override { + // Multi-target regression. + return std::max(static_cast(1), info.labels.Shape(1)); } - void PredTransform(HostDeviceVector *io_preds) const override { + void GetGradient(HostDeviceVector const &preds, MetaInfo const &info, + std::int32_t /*iter*/, linalg::Matrix *out_gpair) override { + CheckInitInputs(info); + CHECK_EQ(info.labels.Size(), preds.Size()) << "Invalid shape of labels."; + if (!info.weights_.Empty()) { + CHECK_EQ(info.weights_.Size(), info.num_row_) + << "Number of weights should be equal to number of data points."; + } + + bst_target_t n_targets = this->Targets(info); + out_gpair->Reshape(info.num_row_, n_targets); + auto gpair = out_gpair->View(ctx_->Device()); + + preds.SetDevice(ctx_->Device()); + auto predt = linalg::MakeTensorView(ctx_, &preds, info.num_row_, n_targets); + + auto labels = info.labels.View(ctx_->Device()); + + info.weights_.SetDevice(ctx_->Device()); + common::OptionalWeights weight{ctx_->IsCUDA() ? info.weights_.ConstDeviceSpan() + : info.weights_.ConstHostSpan()}; + + linalg::ElementWiseKernel(this->ctx_, labels, + [=] XGBOOST_DEVICE(std::size_t i, std::size_t j) mutable { + auto w = weight[i]; + + auto p = predt(i, j); + auto y = labels(i, j) * 2.0 - 1.0; + + float g, h; + if (p * y < 1.0) { + g = -y * w; + h = w; + } else { + g = 0.0; + h = std::numeric_limits::min(); + } + gpair(i, j) = GradientPair{g, h}; + }); + } + + void PredTransform(HostDeviceVector *io_preds) const override { common::Transform<>::Init( - [] XGBOOST_DEVICE(size_t _idx, common::Span _preds) { + [] XGBOOST_DEVICE(std::size_t _idx, common::Span _preds) { _preds[_idx] = _preds[_idx] > 0.0 ? 1.0 : 0.0; }, common::Range{0, static_cast(io_preds->Size()), 1}, this->ctx_->Threads(), @@ -76,12 +90,10 @@ class HingeObj : public ObjFunction { .Eval(io_preds); } - [[nodiscard]] const char* DefaultEvalMetric() const override { - return "error"; - } + [[nodiscard]] const char *DefaultEvalMetric() const override { return "error"; } - void SaveConfig(Json* p_out) const override { - auto& out = *p_out; + void SaveConfig(Json *p_out) const override { + auto &out = *p_out; out["name"] = String("binary:hinge"); } void LoadConfig(Json const &) override {} @@ -89,7 +101,7 @@ class HingeObj : public ObjFunction { // register the objective functions XGBOOST_REGISTER_OBJECTIVE(HingeObj, "binary:hinge") -.describe("Hinge loss. Expects labels to be in [0,1f]") -.set_body([]() { return new HingeObj(); }); + .describe("Hinge loss. Expects labels to be in [0,1f]") + .set_body([]() { return new HingeObj(); }); } // namespace xgboost::obj diff --git a/src/objective/quantile_obj.cu b/src/objective/quantile_obj.cu index 57f432c7f..15ec72f95 100644 --- a/src/objective/quantile_obj.cu +++ b/src/objective/quantile_obj.cu @@ -75,28 +75,25 @@ class QuantileRegression : public ObjFunction { : info.weights_.ConstHostSpan()}; preds.SetDevice(ctx_->Device()); - auto predt = linalg::MakeVec(&preds); - auto n_samples = info.num_row_; + auto predt = linalg::MakeTensorView(ctx_, &preds, info.num_row_, n_targets); alpha_.SetDevice(ctx_->Device()); auto alpha = ctx_->IsCUDA() ? alpha_.ConstDeviceSpan() : alpha_.ConstHostSpan(); - linalg::ElementWiseKernel( - ctx_, gpair, [=] XGBOOST_DEVICE(std::size_t i, GradientPair const&) mutable { - auto [sample_id, quantile_id, target_id] = - linalg::UnravelIndex(i, n_samples, alpha.size(), n_targets / alpha.size()); - assert(target_id == 0); - - auto d = predt(i) - labels(sample_id, target_id); - auto h = weight[sample_id]; - if (d >= 0) { - auto g = (1.0f - alpha[quantile_id]) * weight[sample_id]; - gpair(sample_id, quantile_id) = GradientPair{g, h}; - } else { - auto g = (-alpha[quantile_id] * weight[sample_id]); - gpair(sample_id, quantile_id) = GradientPair{g, h}; - } - }); + linalg::ElementWiseKernel(ctx_, gpair, + [=] XGBOOST_DEVICE(std::size_t i, std::size_t j) mutable { + // j is the quantile index + // 0 is the target index + auto d = predt(i, j) - labels(i, 0); + auto h = weight[i]; + if (d >= 0) { + auto g = (1.0f - alpha[j]) * weight[i]; + gpair(i, j) = GradientPair{g, h}; + } else { + auto g = (-alpha[j] * weight[i]); + gpair(i, j) = GradientPair{g, h}; + } + }); } void InitEstimation(MetaInfo const& info, linalg::Vector* base_score) const override { diff --git a/src/objective/regression_obj.cu b/src/objective/regression_obj.cu index f74d01acc..5627600fc 100644 --- a/src/objective/regression_obj.cu +++ b/src/objective/regression_obj.cu @@ -255,24 +255,24 @@ class PseudoHuberRegression : public FitIntercept { auto gpair = out_gpair->View(ctx_->Device()); preds.SetDevice(ctx_->Device()); - auto predt = linalg::MakeVec(&preds); + auto predt = linalg::MakeTensorView(ctx_, &preds, info.num_row_, this->Targets(info)); info.weights_.SetDevice(ctx_->Device()); common::OptionalWeights weight{ctx_->IsCUDA() ? info.weights_.ConstDeviceSpan() : info.weights_.ConstHostSpan()}; - linalg::ElementWiseKernel(ctx_, labels, [=] XGBOOST_DEVICE(size_t i, float const y) mutable { - auto sample_id = std::get<0>(linalg::UnravelIndex(i, labels.Shape())); - const float z = predt(i) - y; - const float scale_sqrt = std::sqrt(1 + common::Sqr(z) / common::Sqr(slope)); - float grad = z / scale_sqrt; + linalg::ElementWiseKernel( + ctx_, labels, [=] XGBOOST_DEVICE(std::size_t i, std::size_t j) mutable { + float z = predt(i, j) - labels(i, j); + float scale_sqrt = std::sqrt(1 + common::Sqr(z) / common::Sqr(slope)); + float grad = z / scale_sqrt; - auto scale = common::Sqr(slope) + common::Sqr(z); - float hess = common::Sqr(slope) / (scale * scale_sqrt); + auto scale = common::Sqr(slope) + common::Sqr(z); + float hess = common::Sqr(slope) / (scale * scale_sqrt); - auto w = weight[sample_id]; - gpair(i) = {grad * w, hess * w}; - }); + auto w = weight[i]; + gpair(i) = {grad * w, hess * w}; + }); } [[nodiscard]] const char* DefaultEvalMetric() const override { return "mphe"; } @@ -635,20 +635,21 @@ class MeanAbsoluteError : public ObjFunction { auto gpair = out_gpair->View(ctx_->Device()); preds.SetDevice(ctx_->Device()); - auto predt = linalg::MakeVec(&preds); + auto predt = linalg::MakeTensorView(ctx_, &preds, info.num_row_, this->Targets(info)); info.weights_.SetDevice(ctx_->Device()); common::OptionalWeights weight{ctx_->IsCUDA() ? info.weights_.ConstDeviceSpan() : info.weights_.ConstHostSpan()}; - linalg::ElementWiseKernel(ctx_, labels, [=] XGBOOST_DEVICE(std::size_t i, float y) mutable { - auto sign = [](auto x) { - return (x > static_cast(0)) - (x < static_cast(0)); - }; - auto [sample_id, target_id] = linalg::UnravelIndex(i, labels.Shape()); - auto grad = sign(predt(i) - y) * weight[sample_id]; - auto hess = weight[sample_id]; - gpair(sample_id, target_id) = GradientPair{grad, hess}; - }); + linalg::ElementWiseKernel( + ctx_, labels, [=] XGBOOST_DEVICE(std::size_t i, std::size_t j) mutable { + auto sign = [](auto x) { + return (x > static_cast(0)) - (x < static_cast(0)); + }; + auto y = labels(i, j); + auto hess = weight[i]; + auto grad = sign(predt(i, j) - y) * hess; + gpair(i, j) = GradientPair{grad, hess}; + }); } void InitEstimation(MetaInfo const& info, linalg::Tensor* base_margin) const override { diff --git a/tests/cpp/common/test_linalg.cu b/tests/cpp/common/test_linalg.cu index 4823b1191..5f8bab4a3 100644 --- a/tests/cpp/common/test_linalg.cu +++ b/tests/cpp/common/test_linalg.cu @@ -23,7 +23,7 @@ void TestElementWiseKernel() { ElementWiseTransformDevice(t, [] __device__(size_t i, float) { return i; }); // CPU view t = l.View(DeviceOrd::CPU()).Slice(linalg::All(), 1, linalg::All()); - size_t k = 0; + std::size_t k = 0; for (size_t i = 0; i < l.Shape(0); ++i) { for (size_t j = 0; j < l.Shape(2); ++j) { ASSERT_EQ(k++, t(i, j)); @@ -31,7 +31,15 @@ void TestElementWiseKernel() { } t = l.View(device).Slice(linalg::All(), 1, linalg::All()); - ElementWiseKernelDevice(t, [] XGBOOST_DEVICE(size_t i, float v) { SPAN_CHECK(v == i); }); + cuda_impl::ElementWiseKernel( + t, [=] XGBOOST_DEVICE(std::size_t i, std::size_t j) mutable { t(i, j) = i + j; }); + + t = l.Slice(linalg::All(), 1, linalg::All()); + for (size_t i = 0; i < l.Shape(0); ++i) { + for (size_t j = 0; j < l.Shape(2); ++j) { + ASSERT_EQ(i + j, t(i, j)); + } + } } { diff --git a/tests/cpp/data/test_metainfo.h b/tests/cpp/data/test_metainfo.h index fba882e0e..92cd6cb91 100644 --- a/tests/cpp/data/test_metainfo.h +++ b/tests/cpp/data/test_metainfo.h @@ -31,12 +31,10 @@ inline void TestMetaInfoStridedData(DeviceOrd device) { auto const& h_result = info.labels.View(DeviceOrd::CPU()); ASSERT_EQ(h_result.Shape().size(), 2); auto in_labels = labels.View(DeviceOrd::CPU()); - linalg::ElementWiseKernelHost(h_result, omp_get_max_threads(), [&](size_t i, float& v_0) { - auto tup = linalg::UnravelIndex(i, h_result.Shape()); - auto i0 = std::get<0>(tup); - auto i1 = std::get<1>(tup); + linalg::ElementWiseKernelHost(h_result, omp_get_max_threads(), [&](size_t i, std::size_t j) { // Sliced at second dimension. - auto v_1 = in_labels(i0, 0, i1); + auto v_0 = h_result(i, j); + auto v_1 = in_labels(i, 0, j); CHECK_EQ(v_0, v_1); }); } @@ -65,14 +63,13 @@ inline void TestMetaInfoStridedData(DeviceOrd device) { auto const& h_result = info.base_margin_.View(DeviceOrd::CPU()); ASSERT_EQ(h_result.Shape().size(), 2); auto in_margin = base_margin.View(DeviceOrd::CPU()); - linalg::ElementWiseKernelHost(h_result, omp_get_max_threads(), [&](size_t i, float v_0) { - auto tup = linalg::UnravelIndex(i, h_result.Shape()); - auto i0 = std::get<0>(tup); - auto i1 = std::get<1>(tup); - // Sliced at second dimension. - auto v_1 = in_margin(i0, 0, i1); - CHECK_EQ(v_0, v_1); - }); + linalg::ElementWiseKernelHost(h_result, omp_get_max_threads(), + [&](std::size_t i, std::size_t j) { + // Sliced at second dimension. + auto v_0 = h_result(i, j); + auto v_1 = in_margin(i, 0, j); + CHECK_EQ(v_0, v_1); + }); } } } // namespace xgboost diff --git a/tests/cpp/objective/test_hinge.cc b/tests/cpp/objective/test_hinge.cc index 17d2609d4..70e8b5626 100644 --- a/tests/cpp/objective/test_hinge.cc +++ b/tests/cpp/objective/test_hinge.cc @@ -1,28 +1,55 @@ -// Copyright by Contributors +/** + * Copyright 2018-2023, XGBoost Contributors + */ #include #include #include #include "../helpers.h" +#include "../../../src/common/linalg_op.h" namespace xgboost { TEST(Objective, DeclareUnifiedTest(HingeObj)) { Context ctx = MakeCUDACtx(GPUIDX); std::unique_ptr obj{ObjFunction::Create("binary:hinge", &ctx)}; float eps = std::numeric_limits::min(); - CheckObjFunction(obj, - {-1.0f, -0.5f, 0.5f, 1.0f, -1.0f, -0.5f, 0.5f, 1.0f}, - { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f}, - { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, - { 0.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 0.0f}, - { eps, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, eps }); - CheckObjFunction(obj, - {-1.0f, -0.5f, 0.5f, 1.0f, -1.0f, -0.5f, 0.5f, 1.0f}, - { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f}, - {}, // Empty weight. - { 0.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 0.0f}, - { eps, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, eps }); + std::vector predt{-1.0f, -0.5f, 0.5f, 1.0f, -1.0f, -0.5f, 0.5f, 1.0f}; + std::vector label{ 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f}; + std::vector grad{0.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 0.0f}; + std::vector hess{eps, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, eps}; - ASSERT_NO_THROW(obj->DefaultEvalMetric()); + CheckObjFunction(obj, predt, label, {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, grad, hess); + CheckObjFunction(obj, predt, label, {/* Empty weight. */}, grad, hess); + + ASSERT_EQ(obj->DefaultEvalMetric(), StringView{"error"}); + + MetaInfo info; + info.num_row_ = label.size(); + info.labels.Reshape(info.num_row_, 3); + ASSERT_EQ(obj->Targets(info), 3); + auto h_labels = info.labels.HostView(); + for (std::size_t j = 0; j < obj->Targets(info); ++j) { + for (std::size_t i = 0; i < info.num_row_; ++i) { + h_labels(i, j) = label[i]; + } + } + linalg::Tensor t_predt{}; + t_predt.Reshape(info.labels.Shape()); + for (std::size_t j = 0; j < obj->Targets(info); ++j) { + for (std::size_t i = 0; i < info.num_row_; ++i) { + t_predt(i, j) = predt[i]; + } + } + linalg::Matrix out_gpair; + obj->GetGradient(*t_predt.Data(), info, 0, &out_gpair); + + for (std::size_t j = 0; j < obj->Targets(info); ++j) { + auto gh = out_gpair.Slice(linalg::All(), j); + ASSERT_EQ(gh.Size(), info.num_row_); + for (std::size_t i = 0; i < gh.Size(); ++i) { + ASSERT_EQ(gh(i).GetGrad(), grad[i]); + ASSERT_EQ(gh(i).GetHess(), hess[i]); + } + } } } // namespace xgboost From 1094d6015df7be6f5829e53a4c257f3481b43ee5 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Fri, 8 Dec 2023 17:23:16 +0800 Subject: [PATCH 09/59] [py] Use the first found native library. (#9860) --- python-package/xgboost/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index ffc7db8dd..bf72d95cc 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -211,6 +211,7 @@ def _load_lib() -> ctypes.CDLL: lib = ctypes.cdll.LoadLibrary(lib_path) setattr(lib, "path", os.path.normpath(lib_path)) lib_success = True + break except OSError as e: os_error_list.append(str(e)) continue From 562352101d5dd1630eded1522b9b4ed9b07e8692 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Sat, 9 Dec 2023 19:45:28 +0100 Subject: [PATCH 10/59] [R] Move all DMatrix fields to function arguments (#9862) --- R-package/R/xgb.DMatrix.R | 148 +++++++++++++----- R-package/man/getinfo.Rd | 18 ++- R-package/man/setinfo.Rd | 14 +- R-package/man/xgb.DMatrix.Rd | 35 ++++- R-package/src/init.c | 6 +- R-package/src/xgboost_R.cc | 21 ++- R-package/src/xgboost_R.h | 12 +- R-package/tests/testthat/test_basic.R | 31 +++- R-package/tests/testthat/test_dmatrix.R | 17 ++ .../testthat/test_interaction_constraints.R | 2 +- 10 files changed, 236 insertions(+), 68 deletions(-) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 0d9a89912..9be990670 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -8,13 +8,24 @@ #' a \code{dgRMatrix} object, #' a \code{dsparseVector} object (only when making predictions from a fitted model, will be #' interpreted as a row vector), or a character string representing a filename. -#' @param info a named list of additional information to store in the \code{xgb.DMatrix} object. -#' See \code{\link{setinfo}} for the specific allowed kinds of +#' @param label Label of the training data. +#' @param weight Weight for each instance. +#' +#' Note that, for ranking task, weights are per-group. In ranking task, one weight +#' is assigned to each group (not each data point). This is because we +#' only care about the relative ordering of data points within each group, +#' so it doesn't make sense to assign weights to individual data points. +#' @param base_margin Base margin used for boosting from existing model. #' @param missing a float value to represents missing values in data (used only when input is a dense matrix). #' It is useful when a 0 or some other extreme value represents missing values in data. #' @param silent whether to suppress printing an informational message after loading from a file. +#' @param feature_names Set names for features. #' @param nthread Number of threads used for creating DMatrix. -#' @param ... the \code{info} data could be passed directly as parameters, without creating an \code{info} list. +#' @param group Group size for all ranking group. +#' @param qid Query ID for data samples, used for ranking. +#' @param label_lower_bound Lower bound for survival training. +#' @param label_upper_bound Upper bound for survival training. +#' @param feature_weights Set feature weights for column sampling. #' #' @details #' Note that DMatrix objects are not serializable through R functions such as \code{saveRDS} or \code{save}. @@ -34,8 +45,24 @@ #' dtrain <- xgb.DMatrix('xgb.DMatrix.data') #' if (file.exists('xgb.DMatrix.data')) file.remove('xgb.DMatrix.data') #' @export -xgb.DMatrix <- function(data, info = list(), missing = NA, silent = FALSE, nthread = NULL, ...) { - cnames <- NULL +xgb.DMatrix <- function( + data, + label = NULL, + weight = NULL, + base_margin = NULL, + missing = NA, + silent = FALSE, + feature_names = colnames(data), + nthread = NULL, + group = NULL, + qid = NULL, + label_lower_bound = NULL, + label_upper_bound = NULL, + feature_weights = NULL +) { + if (!is.null(group) && !is.null(qid)) { + stop("Either one of 'group' or 'qid' should be NULL") + } if (typeof(data) == "character") { if (length(data) > 1) stop("'data' has class 'character' and length ", length(data), @@ -44,7 +71,6 @@ xgb.DMatrix <- function(data, info = list(), missing = NA, silent = FALSE, nthre handle <- .Call(XGDMatrixCreateFromFile_R, data, as.integer(silent)) } else if (is.matrix(data)) { handle <- .Call(XGDMatrixCreateFromMat_R, data, missing, as.integer(NVL(nthread, -1))) - cnames <- colnames(data) } else if (inherits(data, "dgCMatrix")) { handle <- .Call( XGDMatrixCreateFromCSC_R, @@ -55,7 +81,6 @@ xgb.DMatrix <- function(data, info = list(), missing = NA, silent = FALSE, nthre missing, as.integer(NVL(nthread, -1)) ) - cnames <- colnames(data) } else if (inherits(data, "dgRMatrix")) { handle <- .Call( XGDMatrixCreateFromCSR_R, @@ -66,7 +91,6 @@ xgb.DMatrix <- function(data, info = list(), missing = NA, silent = FALSE, nthre missing, as.integer(NVL(nthread, -1)) ) - cnames <- colnames(data) } else if (inherits(data, "dsparseVector")) { indptr <- c(0L, as.integer(length(data@i))) ind <- as.integer(data@i) - 1L @@ -82,17 +106,38 @@ xgb.DMatrix <- function(data, info = list(), missing = NA, silent = FALSE, nthre } else { stop("xgb.DMatrix does not support construction from ", typeof(data)) } + dmat <- handle attributes(dmat) <- list(class = "xgb.DMatrix") - if (!is.null(cnames)) { - setinfo(dmat, "feature_name", cnames) + + if (!is.null(label)) { + setinfo(dmat, "label", label) + } + if (!is.null(weight)) { + setinfo(dmat, "weight", weight) + } + if (!is.null(base_margin)) { + setinfo(dmat, "base_margin", base_margin) + } + if (!is.null(feature_names)) { + setinfo(dmat, "feature_name", feature_names) + } + if (!is.null(group)) { + setinfo(dmat, "group", group) + } + if (!is.null(qid)) { + setinfo(dmat, "qid", qid) + } + if (!is.null(label_lower_bound)) { + setinfo(dmat, "label_lower_bound", label_lower_bound) + } + if (!is.null(label_upper_bound)) { + setinfo(dmat, "label_upper_bound", label_upper_bound) + } + if (!is.null(feature_weights)) { + setinfo(dmat, "feature_weights", feature_weights) } - info <- append(info, list(...)) - for (i in seq_along(info)) { - p <- info[i] - setinfo(dmat, names(p), p[[1]]) - } return(dmat) } @@ -211,14 +256,20 @@ dimnames.xgb.DMatrix <- function(x) { #' The \code{name} field can be one of the following: #' #' \itemize{ -#' \item \code{label}: label XGBoost learn from ; -#' \item \code{weight}: to do a weight rescale ; -#' \item \code{base_margin}: base margin is the base prediction XGBoost will boost from ; -#' \item \code{nrow}: number of rows of the \code{xgb.DMatrix}. -#' +#' \item \code{label} +#' \item \code{weight} +#' \item \code{base_margin} +#' \item \code{label_lower_bound} +#' \item \code{label_upper_bound} +#' \item \code{group} +#' \item \code{feature_type} +#' \item \code{feature_name} +#' \item \code{nrow} #' } +#' See the documentation for \link{xgb.DMatrix} for more information about these fields. #' -#' \code{group} can be setup by \code{setinfo} but can't be retrieved by \code{getinfo}. +#' Note that, while 'qid' cannot be retrieved, it's possible to get the equivalent 'group' +#' for a DMatrix that had 'qid' assigned. #' #' @examples #' data(agaricus.train, package='xgboost') @@ -236,24 +287,37 @@ getinfo <- function(object, ...) UseMethod("getinfo") #' @rdname getinfo #' @export getinfo.xgb.DMatrix <- function(object, name, ...) { + allowed_int_fields <- 'group' + allowed_float_fields <- c( + 'label', 'weight', 'base_margin', + 'label_lower_bound', 'label_upper_bound' + ) + allowed_str_fields <- c("feature_type", "feature_name") + allowed_fields <- c(allowed_float_fields, allowed_int_fields, allowed_str_fields, 'nrow') + if (typeof(name) != "character" || length(name) != 1 || - !name %in% c('label', 'weight', 'base_margin', 'nrow', - 'label_lower_bound', 'label_upper_bound', "feature_type", "feature_name")) { - stop( - "getinfo: name must be one of the following\n", - " 'label', 'weight', 'base_margin', 'nrow', 'label_lower_bound', 'label_upper_bound', 'feature_type', 'feature_name'" - ) + !name %in% allowed_fields) { + stop("getinfo: name must be one of the following\n", + paste(paste0("'", allowed_fields, "'"), collapse = ", ")) } - if (name == "feature_name" || name == "feature_type") { + if (name == "nrow") { + ret <- nrow(object) + } else if (name %in% allowed_str_fields) { ret <- .Call(XGDMatrixGetStrFeatureInfo_R, object, name) - } else if (name != "nrow") { - ret <- .Call(XGDMatrixGetInfo_R, object, name) + } else if (name %in% allowed_float_fields) { + ret <- .Call(XGDMatrixGetFloatInfo_R, object, name) + if (length(ret) > nrow(object)) { + ret <- matrix(ret, nrow = nrow(object), byrow = TRUE) + } + } else if (name %in% allowed_int_fields) { + if (name == "group") { + name <- "group_ptr" + } + ret <- .Call(XGDMatrixGetUIntInfo_R, object, name) if (length(ret) > nrow(object)) { ret <- matrix(ret, nrow = nrow(object), byrow = TRUE) } - } else { - ret <- nrow(object) } if (length(ret) == 0) return(NULL) return(ret) @@ -270,13 +334,15 @@ getinfo.xgb.DMatrix <- function(object, name, ...) { #' @param ... other parameters #' #' @details -#' The \code{name} field can be one of the following: +#' See the documentation for \link{xgb.DMatrix} for possible fields that can be set +#' (which correspond to arguments in that function). #' -#' \itemize{ -#' \item \code{label}: label XGBoost learn from ; -#' \item \code{weight}: to do a weight rescale ; -#' \item \code{base_margin}: base margin is the base prediction XGBoost will boost from ; -#' \item \code{group}: number of rows in each group (to use with \code{rank:pairwise} objective). +#' Note that the following fields are allowed in the construction of an \code{xgb.DMatrix} +#' but \bold{aren't} allowed here:\itemize{ +#' \item data +#' \item missing +#' \item silent +#' \item nthread #' } #' #' @examples @@ -328,6 +394,12 @@ setinfo.xgb.DMatrix <- function(object, name, info, ...) { .Call(XGDMatrixSetInfo_R, object, name, as.integer(info)) return(TRUE) } + if (name == "qid") { + if (NROW(info) != nrow(object)) + stop("The length of qid assignments must equal to the number of rows in the input data") + .Call(XGDMatrixSetInfo_R, object, name, as.integer(info)) + return(TRUE) + } if (name == "feature_weights") { if (length(info) != ncol(object)) { stop("The number of feature weights must equal to the number of columns in the input data") diff --git a/R-package/man/getinfo.Rd b/R-package/man/getinfo.Rd index 63222f341..9503c2154 100644 --- a/R-package/man/getinfo.Rd +++ b/R-package/man/getinfo.Rd @@ -23,14 +23,20 @@ Get information of an xgb.DMatrix object The \code{name} field can be one of the following: \itemize{ - \item \code{label}: label XGBoost learn from ; - \item \code{weight}: to do a weight rescale ; - \item \code{base_margin}: base margin is the base prediction XGBoost will boost from ; - \item \code{nrow}: number of rows of the \code{xgb.DMatrix}. - + \item \code{label} + \item \code{weight} + \item \code{base_margin} + \item \code{label_lower_bound} + \item \code{label_upper_bound} + \item \code{group} + \item \code{feature_type} + \item \code{feature_name} + \item \code{nrow} } +See the documentation for \link{xgb.DMatrix} for more information about these fields. -\code{group} can be setup by \code{setinfo} but can't be retrieved by \code{getinfo}. +Note that, while 'qid' cannot be retrieved, it's possible to get the equivalent 'group' +for a DMatrix that had 'qid' assigned. } \examples{ data(agaricus.train, package='xgboost') diff --git a/R-package/man/setinfo.Rd b/R-package/man/setinfo.Rd index c29fcfa7e..a8bc56b02 100644 --- a/R-package/man/setinfo.Rd +++ b/R-package/man/setinfo.Rd @@ -22,13 +22,15 @@ setinfo(object, ...) Set information of an xgb.DMatrix object } \details{ -The \code{name} field can be one of the following: +See the documentation for \link{xgb.DMatrix} for possible fields that can be set +(which correspond to arguments in that function). -\itemize{ - \item \code{label}: label XGBoost learn from ; - \item \code{weight}: to do a weight rescale ; - \item \code{base_margin}: base margin is the base prediction XGBoost will boost from ; - \item \code{group}: number of rows in each group (to use with \code{rank:pairwise} objective). +Note that the following fields are allowed in the construction of an \code{xgb.DMatrix} +but \bold{aren't} allowed here:\itemize{ +\item data +\item missing +\item silent +\item nthread } } \examples{ diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index 96f050c72..13dc3d9f5 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -6,11 +6,18 @@ \usage{ xgb.DMatrix( data, - info = list(), + label = NULL, + weight = NULL, + base_margin = NULL, missing = NA, silent = FALSE, + feature_names = colnames(data), nthread = NULL, - ... + group = NULL, + qid = NULL, + label_lower_bound = NULL, + label_upper_bound = NULL, + feature_weights = NULL ) } \arguments{ @@ -19,17 +26,35 @@ a \code{dgRMatrix} object, a \code{dsparseVector} object (only when making predictions from a fitted model, will be interpreted as a row vector), or a character string representing a filename.} -\item{info}{a named list of additional information to store in the \code{xgb.DMatrix} object. -See \code{\link{setinfo}} for the specific allowed kinds of} +\item{label}{Label of the training data.} + +\item{weight}{Weight for each instance. + +Note that, for ranking task, weights are per-group. In ranking task, one weight +is assigned to each group (not each data point). This is because we +only care about the relative ordering of data points within each group, +so it doesn't make sense to assign weights to individual data points.} + +\item{base_margin}{Base margin used for boosting from existing model.} \item{missing}{a float value to represents missing values in data (used only when input is a dense matrix). It is useful when a 0 or some other extreme value represents missing values in data.} \item{silent}{whether to suppress printing an informational message after loading from a file.} +\item{feature_names}{Set names for features.} + \item{nthread}{Number of threads used for creating DMatrix.} -\item{...}{the \code{info} data could be passed directly as parameters, without creating an \code{info} list.} +\item{group}{Group size for all ranking group.} + +\item{qid}{Query ID for data samples, used for ranking.} + +\item{label_lower_bound}{Lower bound for survival training.} + +\item{label_upper_bound}{Upper bound for survival training.} + +\item{feature_weights}{Set feature weights for column sampling.} } \description{ Construct xgb.DMatrix object from either a dense matrix, a sparse matrix, or a local file. diff --git a/R-package/src/init.c b/R-package/src/init.c index afac524e3..c35e0ecf5 100644 --- a/R-package/src/init.c +++ b/R-package/src/init.c @@ -39,7 +39,8 @@ extern SEXP XGDMatrixCreateFromCSC_R(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP XGDMatrixCreateFromCSR_R(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP XGDMatrixCreateFromFile_R(SEXP, SEXP); extern SEXP XGDMatrixCreateFromMat_R(SEXP, SEXP, SEXP); -extern SEXP XGDMatrixGetInfo_R(SEXP, SEXP); +extern SEXP XGDMatrixGetFloatInfo_R(SEXP, SEXP); +extern SEXP XGDMatrixGetUIntInfo_R(SEXP, SEXP); extern SEXP XGDMatrixGetStrFeatureInfo_R(SEXP, SEXP); extern SEXP XGDMatrixNumCol_R(SEXP); extern SEXP XGDMatrixNumRow_R(SEXP); @@ -76,7 +77,8 @@ static const R_CallMethodDef CallEntries[] = { {"XGDMatrixCreateFromCSR_R", (DL_FUNC) &XGDMatrixCreateFromCSR_R, 6}, {"XGDMatrixCreateFromFile_R", (DL_FUNC) &XGDMatrixCreateFromFile_R, 2}, {"XGDMatrixCreateFromMat_R", (DL_FUNC) &XGDMatrixCreateFromMat_R, 3}, - {"XGDMatrixGetInfo_R", (DL_FUNC) &XGDMatrixGetInfo_R, 2}, + {"XGDMatrixGetFloatInfo_R", (DL_FUNC) &XGDMatrixGetFloatInfo_R, 2}, + {"XGDMatrixGetUIntInfo_R", (DL_FUNC) &XGDMatrixGetUIntInfo_R, 2}, {"XGDMatrixGetStrFeatureInfo_R", (DL_FUNC) &XGDMatrixGetStrFeatureInfo_R, 2}, {"XGDMatrixNumCol_R", (DL_FUNC) &XGDMatrixNumCol_R, 1}, {"XGDMatrixNumRow_R", (DL_FUNC) &XGDMatrixNumRow_R, 1}, diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 214ca6cb4..9bebc5851 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -412,17 +413,27 @@ XGB_DLL SEXP XGDMatrixGetStrFeatureInfo_R(SEXP handle, SEXP field) { return ret; } -XGB_DLL SEXP XGDMatrixGetInfo_R(SEXP handle, SEXP field) { +XGB_DLL SEXP XGDMatrixGetFloatInfo_R(SEXP handle, SEXP field) { SEXP ret; R_API_BEGIN(); bst_ulong olen; const float *res; CHECK_CALL(XGDMatrixGetFloatInfo(R_ExternalPtrAddr(handle), CHAR(asChar(field)), &olen, &res)); ret = PROTECT(allocVector(REALSXP, olen)); - double *ret_ = REAL(ret); - for (size_t i = 0; i < olen; ++i) { - ret_[i] = res[i]; - } + std::copy(res, res + olen, REAL(ret)); + R_API_END(); + UNPROTECT(1); + return ret; +} + +XGB_DLL SEXP XGDMatrixGetUIntInfo_R(SEXP handle, SEXP field) { + SEXP ret; + R_API_BEGIN(); + bst_ulong olen; + const unsigned *res; + CHECK_CALL(XGDMatrixGetUIntInfo(R_ExternalPtrAddr(handle), CHAR(asChar(field)), &olen, &res)); + ret = PROTECT(allocVector(INTSXP, olen)); + std::copy(res, res + olen, INTEGER(ret)); R_API_END(); UNPROTECT(1); return ret; diff --git a/R-package/src/xgboost_R.h b/R-package/src/xgboost_R.h index 7f0833b15..4ec80b5ff 100644 --- a/R-package/src/xgboost_R.h +++ b/R-package/src/xgboost_R.h @@ -106,12 +106,20 @@ XGB_DLL SEXP XGDMatrixSaveBinary_R(SEXP handle, SEXP fname, SEXP silent); XGB_DLL SEXP XGDMatrixSetInfo_R(SEXP handle, SEXP field, SEXP array); /*! - * \brief get info vector from matrix + * \brief get info vector (float type) from matrix * \param handle a instance of data matrix * \param field field name * \return info vector */ -XGB_DLL SEXP XGDMatrixGetInfo_R(SEXP handle, SEXP field); +XGB_DLL SEXP XGDMatrixGetFloatInfo_R(SEXP handle, SEXP field); + +/*! + * \brief get info vector (uint type) from matrix + * \param handle a instance of data matrix + * \param field field name + * \return info vector + */ +XGB_DLL SEXP XGDMatrixGetUIntInfo_R(SEXP handle, SEXP field); /*! * \brief return number of rows diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index c96871e11..8ecf86e87 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -56,7 +56,7 @@ test_that("parameter validation works", { y <- d[, "x1"] + d[, "x2"]^2 + ifelse(d[, "x3"] > .5, d[, "x3"]^2, 2^d[, "x3"]) + rnorm(10) - dtrain <- xgb.DMatrix(data = d, info = list(label = y), nthread = n_threads) + dtrain <- xgb.DMatrix(data = d, label = y, nthread = n_threads) correct <- function() { params <- list( @@ -124,7 +124,7 @@ test_that("dart prediction works", { expect_false(all(matrix(pred_by_xgboost_0, byrow = TRUE) == matrix(pred_by_xgboost_2, byrow = TRUE))) set.seed(1994) - dtrain <- xgb.DMatrix(data = d, info = list(label = y), nthread = n_threads) + dtrain <- xgb.DMatrix(data = d, label = y, nthread = n_threads) booster_by_train <- xgb.train( params = list( booster = "dart", @@ -186,7 +186,7 @@ test_that("train and predict softprob", { x3 = rnorm(100) ) y <- sample.int(10, 100, replace = TRUE) - 1 - dtrain <- xgb.DMatrix(data = d, info = list(label = y), nthread = n_threads) + dtrain <- xgb.DMatrix(data = d, label = y, nthread = n_threads) booster <- xgb.train( params = list(tree_method = "hist", nthread = n_threads), data = dtrain, nrounds = 4, num_class = 10, @@ -643,3 +643,28 @@ test_that("Can use multi-output labels with custom objectives", { expect_true(cor(y, pred[, 1]) > 0.9) expect_true(cor(y, pred[, 2]) < -0.9) }) + +test_that("Can use ranking objectives with either 'qid' or 'group'", { + set.seed(123) + x <- matrix(rnorm(100 * 10), nrow = 100) + y <- sample(2, size = 100, replace = TRUE) - 1 + qid <- c(rep(1, 20), rep(2, 20), rep(3, 60)) + gr <- c(20, 20, 60) + + dmat_qid <- xgb.DMatrix(x, label = y, qid = qid) + dmat_gr <- xgb.DMatrix(x, label = y, group = gr) + + params <- list(tree_method = "hist", + lambdarank_num_pair_per_sample = 8, + objective = "rank:ndcg", + lambdarank_pair_method = "topk", + nthread = n_threads) + set.seed(123) + model_qid <- xgb.train(params, dmat_qid, nrounds = 5) + set.seed(123) + model_gr <- xgb.train(params, dmat_gr, nrounds = 5) + + pred_qid <- predict(model_qid, x) + pred_gr <- predict(model_gr, x) + expect_equal(pred_qid, pred_gr) +}) diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index 4db7aad08..f1eaa9d80 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -305,3 +305,20 @@ test_that("xgb.DMatrix: error on three-dimensional array", { dim(y) <- c(50, 4, 2) expect_error(xgb.DMatrix(data = x, label = y)) }) + +test_that("xgb.DMatrix: can get group for both 'qid' and 'group' constructors", { + set.seed(123) + x <- matrix(rnorm(1000), nrow = 100) + group <- c(20, 20, 60) + qid <- c(rep(1, 20), rep(2, 20), rep(3, 60)) + + gr_mat <- xgb.DMatrix(x, group = group) + qid_mat <- xgb.DMatrix(x, qid = qid) + + info_gr <- getinfo(gr_mat, "group") + info_qid <- getinfo(qid_mat, "group") + expect_equal(info_gr, info_qid) + + expected_gr <- c(0, 20, 40, 100) + expect_equal(info_gr, expected_gr) +}) diff --git a/R-package/tests/testthat/test_interaction_constraints.R b/R-package/tests/testthat/test_interaction_constraints.R index 1ac804501..ee4c453b3 100644 --- a/R-package/tests/testthat/test_interaction_constraints.R +++ b/R-package/tests/testthat/test_interaction_constraints.R @@ -47,7 +47,7 @@ test_that("interaction constraints scientific representation", { d <- matrix(rexp(rows, rate = .1), nrow = rows, ncol = cols) y <- rnorm(rows) - dtrain <- xgb.DMatrix(data = d, info = list(label = y), nthread = n_threads) + dtrain <- xgb.DMatrix(data = d, label = y, nthread = n_threads) inc <- list(c(seq.int(from = 0, to = cols, by = 1))) with_inc <- xgb.train( From b3700bbb3fc057f4e6948a493b92c3e6ec28cc51 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 12 Dec 2023 07:34:01 +0800 Subject: [PATCH 11/59] Flexible find protobuf. (#9867) --- plugin/federated/CMakeLists.txt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugin/federated/CMakeLists.txt b/plugin/federated/CMakeLists.txt index c4d5ea378..0865e756e 100644 --- a/plugin/federated/CMakeLists.txt +++ b/plugin/federated/CMakeLists.txt @@ -1,7 +1,16 @@ # gRPC needs to be installed first. See README.md. set(protobuf_MODULE_COMPATIBLE TRUE) set(protobuf_BUILD_SHARED_LIBS TRUE) -find_package(Protobuf CONFIG REQUIRED) + +find_package(Protobuf CONFIG) +if(NOT Protobuf_FOUND) + find_package(Protobuf) +endif() +if(NOT Protobuf_FOUND) + # let CMake emit error + find_package(Protobuf CONFIG REQUIRED) +endif() + find_package(gRPC CONFIG REQUIRED) message(STATUS "Found gRPC: ${gRPC_CONFIG}") From faf0f2df10ea2583063942b273b7cbaea5644696 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 12 Dec 2023 09:56:31 +0800 Subject: [PATCH 12/59] Support dataframe data format in native XGBoost. (#9828) - Implement a columnar adapter. - Refactor Python pandas handling code to avoid converting into a single numpy array. - Add support in R for transforming columns. - Support R data.frame and factor type. --- R-package/R/xgb.DMatrix.R | 58 ++++- R-package/man/xgb.DMatrix.Rd | 9 +- R-package/src/init.c | 2 + R-package/src/xgboost_R.cc | 64 +++++ R-package/src/xgboost_R.h | 10 + R-package/tests/testthat/test_dmatrix.R | 27 ++ demo/guide-python/cat_in_the_dat.py | 4 + include/xgboost/c_api.h | 45 ++++ python-package/xgboost/core.py | 23 +- python-package/xgboost/data.py | 327 +++++++++++++++--------- src/c_api/c_api.cc | 76 +++++- src/common/quantile.cc | 2 + src/data/adapter.h | 107 ++++++-- src/data/data.cc | 54 ++-- src/data/gradient_index.cc | 2 +- src/data/proxy_dmatrix.cc | 15 ++ src/data/proxy_dmatrix.h | 13 + src/data/simple_dmatrix.cc | 4 +- src/predictor/cpu_predictor.cc | 3 + tests/python/test_with_modin.py | 27 +- tests/python/test_with_pandas.py | 67 ++++- 21 files changed, 718 insertions(+), 221 deletions(-) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 9be990670..602164afe 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -19,7 +19,8 @@ #' @param missing a float value to represents missing values in data (used only when input is a dense matrix). #' It is useful when a 0 or some other extreme value represents missing values in data. #' @param silent whether to suppress printing an informational message after loading from a file. -#' @param feature_names Set names for features. +#' @param feature_names Set names for features. Overrides column names in data +#' frame and matrix. #' @param nthread Number of threads used for creating DMatrix. #' @param group Group size for all ranking group. #' @param qid Query ID for data samples, used for ranking. @@ -32,6 +33,8 @@ #' If a DMatrix gets serialized and then de-serialized (for example, when saving data in an R session or caching #' chunks in an Rmd file), the resulting object will not be usable anymore and will need to be reconstructed #' from the original source of data. +#' @param enable_categorical Experimental support of specializing for +#' categorical features. JSON/UBJSON serialization format is required. #' #' @examples #' data(agaricus.train, package='xgboost') @@ -58,19 +61,26 @@ xgb.DMatrix <- function( qid = NULL, label_lower_bound = NULL, label_upper_bound = NULL, - feature_weights = NULL + feature_weights = NULL, + enable_categorical = FALSE ) { if (!is.null(group) && !is.null(qid)) { stop("Either one of 'group' or 'qid' should be NULL") } + ctypes <- NULL if (typeof(data) == "character") { - if (length(data) > 1) - stop("'data' has class 'character' and length ", length(data), - ".\n 'data' accepts either a numeric matrix or a single filename.") + if (length(data) > 1) { + stop( + "'data' has class 'character' and length ", length(data), + ".\n 'data' accepts either a numeric matrix or a single filename." + ) + } data <- path.expand(data) handle <- .Call(XGDMatrixCreateFromFile_R, data, as.integer(silent)) } else if (is.matrix(data)) { - handle <- .Call(XGDMatrixCreateFromMat_R, data, missing, as.integer(NVL(nthread, -1))) + handle <- .Call( + XGDMatrixCreateFromMat_R, data, missing, as.integer(NVL(nthread, -1)) + ) } else if (inherits(data, "dgCMatrix")) { handle <- .Call( XGDMatrixCreateFromCSC_R, @@ -103,6 +113,39 @@ xgb.DMatrix <- function( missing, as.integer(NVL(nthread, -1)) ) + } else if (is.data.frame(data)) { + ctypes <- sapply(data, function(x) { + if (is.factor(x)) { + if (!enable_categorical) { + stop( + "When factor type is used, the parameter `enable_categorical`", + " must be set to TRUE." + ) + } + "c" + } else if (is.integer(x)) { + "int" + } else if (is.logical(x)) { + "i" + } else { + if (!is.numeric(x)) { + stop("Invalid type in dataframe.") + } + "float" + } + }) + ## as.data.frame somehow converts integer/logical into real. + data <- as.data.frame(sapply(data, function(x) { + if (is.factor(x)) { + ## XGBoost uses 0-based indexing. + as.numeric(x) - 1 + } else { + x + } + })) + handle <- .Call( + XGDMatrixCreateFromDF_R, data, missing, as.integer(NVL(nthread, -1)) + ) } else { stop("xgb.DMatrix does not support construction from ", typeof(data)) } @@ -137,6 +180,9 @@ xgb.DMatrix <- function( if (!is.null(feature_weights)) { setinfo(dmat, "feature_weights", feature_weights) } + if (!is.null(ctypes)) { + setinfo(dmat, "feature_type", ctypes) + } return(dmat) } diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index 13dc3d9f5..619f5d730 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -17,7 +17,8 @@ xgb.DMatrix( qid = NULL, label_lower_bound = NULL, label_upper_bound = NULL, - feature_weights = NULL + feature_weights = NULL, + enable_categorical = FALSE ) } \arguments{ @@ -42,7 +43,8 @@ It is useful when a 0 or some other extreme value represents missing values in d \item{silent}{whether to suppress printing an informational message after loading from a file.} -\item{feature_names}{Set names for features.} +\item{feature_names}{Set names for features. Overrides column names in data +frame and matrix.} \item{nthread}{Number of threads used for creating DMatrix.} @@ -55,6 +57,9 @@ It is useful when a 0 or some other extreme value represents missing values in d \item{label_upper_bound}{Upper bound for survival training.} \item{feature_weights}{Set feature weights for column sampling.} + +\item{enable_categorical}{Experimental support of specializing for +categorical features. JSON/UBJSON serialization format is required.} } \description{ Construct xgb.DMatrix object from either a dense matrix, a sparse matrix, or a local file. diff --git a/R-package/src/init.c b/R-package/src/init.c index c35e0ecf5..f957229af 100644 --- a/R-package/src/init.c +++ b/R-package/src/init.c @@ -41,6 +41,7 @@ extern SEXP XGDMatrixCreateFromFile_R(SEXP, SEXP); extern SEXP XGDMatrixCreateFromMat_R(SEXP, SEXP, SEXP); extern SEXP XGDMatrixGetFloatInfo_R(SEXP, SEXP); extern SEXP XGDMatrixGetUIntInfo_R(SEXP, SEXP); +extern SEXP XGDMatrixCreateFromDF_R(SEXP, SEXP, SEXP); extern SEXP XGDMatrixGetStrFeatureInfo_R(SEXP, SEXP); extern SEXP XGDMatrixNumCol_R(SEXP); extern SEXP XGDMatrixNumRow_R(SEXP); @@ -79,6 +80,7 @@ static const R_CallMethodDef CallEntries[] = { {"XGDMatrixCreateFromMat_R", (DL_FUNC) &XGDMatrixCreateFromMat_R, 3}, {"XGDMatrixGetFloatInfo_R", (DL_FUNC) &XGDMatrixGetFloatInfo_R, 2}, {"XGDMatrixGetUIntInfo_R", (DL_FUNC) &XGDMatrixGetUIntInfo_R, 2}, + {"XGDMatrixCreateFromDF_R", (DL_FUNC) &XGDMatrixCreateFromDF_R, 3}, {"XGDMatrixGetStrFeatureInfo_R", (DL_FUNC) &XGDMatrixGetStrFeatureInfo_R, 2}, {"XGDMatrixNumCol_R", (DL_FUNC) &XGDMatrixNumCol_R, 1}, {"XGDMatrixNumRow_R", (DL_FUNC) &XGDMatrixNumRow_R, 1}, diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 9bebc5851..c27dc5e86 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -223,6 +223,69 @@ XGB_DLL SEXP XGDMatrixCreateFromMat_R(SEXP mat, SEXP missing, SEXP n_threads) { return ret; } +XGB_DLL SEXP XGDMatrixCreateFromDF_R(SEXP df, SEXP missing, SEXP n_threads) { + SEXP ret = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + R_API_BEGIN(); + + DMatrixHandle handle; + + auto make_vec = [&](auto const *ptr, std::int32_t len) { + auto v = xgboost::linalg::MakeVec(ptr, len); + return xgboost::linalg::ArrayInterface(v); + }; + + std::int32_t rc{0}; + { + using xgboost::Json; + auto n_features = Rf_xlength(df); + std::vector array(n_features); + CHECK_GT(n_features, 0); + auto len = Rf_xlength(VECTOR_ELT(df, 0)); + // The `data.frame` in R actually converts all data into numeric. The other type + // handlers here are not used. At the moment they are kept as a reference for when we + // can avoid making data copies during transformation. + for (decltype(n_features) i = 0; i < n_features; ++i) { + switch (TYPEOF(VECTOR_ELT(df, i))) { + case INTSXP: { + auto const *ptr = INTEGER(VECTOR_ELT(df, i)); + array[i] = make_vec(ptr, len); + break; + } + case REALSXP: { + auto const *ptr = REAL(VECTOR_ELT(df, i)); + array[i] = make_vec(ptr, len); + break; + } + case LGLSXP: { + auto const *ptr = LOGICAL(VECTOR_ELT(df, i)); + array[i] = make_vec(ptr, len); + break; + } + default: { + LOG(FATAL) << "data.frame has unsupported type."; + } + } + } + + Json jinterface{std::move(array)}; + auto sinterface = Json::Dump(jinterface); + Json jconfig{xgboost::Object{}}; + jconfig["missing"] = asReal(missing); + jconfig["nthread"] = asInteger(n_threads); + auto sconfig = Json::Dump(jconfig); + + rc = XGDMatrixCreateFromColumnar(sinterface.c_str(), sconfig.c_str(), &handle); + } + + CHECK_CALL(rc); + R_SetExternalPtrAddr(ret, handle); + R_RegisterCFinalizerEx(ret, _DMatrixFinalizer, TRUE); + R_API_END(); + Rf_unprotect(1); + + return ret; +} + namespace { void CreateFromSparse(SEXP indptr, SEXP indices, SEXP data, std::string *indptr_str, std::string *indices_str, std::string *data_str) { @@ -298,6 +361,7 @@ XGB_DLL SEXP XGDMatrixCreateFromCSR_R(SEXP indptr, SEXP indices, SEXP data, SEXP res_code = XGDMatrixCreateFromCSR(sindptr.c_str(), sindices.c_str(), sdata.c_str(), ncol, config.c_str(), &handle); } + CHECK_CALL(res_code); R_SetExternalPtrAddr(ret, handle); R_RegisterCFinalizerEx(ret, _DMatrixFinalizer, TRUE); R_API_END(); diff --git a/R-package/src/xgboost_R.h b/R-package/src/xgboost_R.h index 4ec80b5ff..2e874e3a6 100644 --- a/R-package/src/xgboost_R.h +++ b/R-package/src/xgboost_R.h @@ -53,6 +53,16 @@ XGB_DLL SEXP XGDMatrixCreateFromFile_R(SEXP fname, SEXP silent); XGB_DLL SEXP XGDMatrixCreateFromMat_R(SEXP mat, SEXP missing, SEXP n_threads); + +/** + * @brief Create matrix content from a data frame. + * @param data R data.frame object + * @param missing which value to represent missing value + * @param n_threads Number of threads used to construct DMatrix from dense matrix. + * @return created dmatrix + */ +XGB_DLL SEXP XGDMatrixCreateFromDF_R(SEXP df, SEXP missing, SEXP n_threads); + /*! * \brief create a matrix content from CSC format * \param indptr pointer to column headers diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index f1eaa9d80..87a73d84b 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -322,3 +322,30 @@ test_that("xgb.DMatrix: can get group for both 'qid' and 'group' constructors", expected_gr <- c(0, 20, 40, 100) expect_equal(info_gr, expected_gr) }) + +test_that("xgb.DMatrix: data.frame", { + df <- data.frame( + a = (1:4) / 10, + num = c(1, NA, 3, 4), + as.int = as.integer(c(1, 2, 3, 4)), + lo = c(TRUE, FALSE, NA, TRUE), + str.fac = c("a", "b", "d", "c"), + as.fac = as.factor(c(3, 5, 8, 11)), + stringsAsFactors = TRUE + ) + + m <- xgb.DMatrix(df, enable_categorical = TRUE) + expect_equal(colnames(m), colnames(df)) + expect_equal( + getinfo(m, "feature_type"), c("float", "float", "int", "i", "c", "c") + ) + expect_error(xgb.DMatrix(df)) + + df <- data.frame( + missing = c("a", "b", "d", NA), + valid = c("a", "b", "d", "c"), + stringsAsFactors = TRUE + ) + m <- xgb.DMatrix(df, enable_categorical = TRUE) + expect_equal(getinfo(m, "feature_type"), c("c", "c")) +}) diff --git a/demo/guide-python/cat_in_the_dat.py b/demo/guide-python/cat_in_the_dat.py index a7b94cdca..2b2d20682 100644 --- a/demo/guide-python/cat_in_the_dat.py +++ b/demo/guide-python/cat_in_the_dat.py @@ -78,6 +78,10 @@ def categorical_model(X: pd.DataFrame, y: pd.Series, output_dir: str) -> None: X_train, X_test, y_train, y_test = train_test_split( X, y, random_state=1994, test_size=0.2 ) + # Be aware that the encoding for X_train and X_test are the same here. In practice, + # we should try to use an encoder like (sklearn OrdinalEncoder) to obtain the + # categorical values. + # Specify `enable_categorical` to True. clf = xgb.XGBClassifier( **params, diff --git a/include/xgboost/c_api.h b/include/xgboost/c_api.h index 59d4d0881..a56ead278 100644 --- a/include/xgboost/c_api.h +++ b/include/xgboost/c_api.h @@ -159,6 +159,16 @@ XGB_DLL int XGDMatrixCreateFromURI(char const *config, DMatrixHandle *out); XGB_DLL int XGDMatrixCreateFromCSREx(const size_t *indptr, const unsigned *indices, const float *data, size_t nindptr, size_t nelem, size_t num_col, DMatrixHandle *out); +/** + * @brief Create a DMatrix from columnar data. (table) + * + * @param data See @ref XGBoosterPredictFromColumnar for details. + * @param config See @ref XGDMatrixCreateFromDense for details. + * @param out The created dmatrix. + * + * @return 0 when success, -1 when failure happens + */ +XGB_DLL int XGDMatrixCreateFromColumnar(char const *data, char const *config, DMatrixHandle *out); /** * @example c-api-demo.c @@ -514,6 +524,16 @@ XGB_DLL int XGProxyDMatrixSetDataCudaArrayInterface(DMatrixHandle handle, const char *c_interface_str); +/** + * @brief Set columnar (table) data on a DMatrix proxy. + * + * @param handle A DMatrix proxy created by @ref XGProxyDMatrixCreate + * @param c_interface_str See @ref XGBoosterPredictFromColumnar for details. + * + * @return 0 when success, -1 when failure happens + */ +XGB_DLL int XGProxyDMatrixSetDataCudaColumnar(DMatrixHandle handle, char const *c_interface_str); + /*! * \brief Set data on a DMatrix proxy. * @@ -1113,6 +1133,31 @@ XGB_DLL int XGBoosterPredictFromDense(BoosterHandle handle, char const *values, * @example inference.c */ +/** + * @brief Inplace prediction from CPU columnar data. (Table) + * + * @note If the booster is configured to run on a CUDA device, XGBoost falls back to run + * prediction with DMatrix with a performance warning. + * + * @param handle Booster handle. + * @param values An JSON array of __array_interface__ for each column. + * @param config See @ref XGBoosterPredictFromDMatrix for more info. + * Additional fields for inplace prediction are: + * - "missing": float + * @param m An optional (NULL if not available) proxy DMatrix instance + * storing meta info. + * + * @param out_shape See @ref XGBoosterPredictFromDMatrix for more info. + * @param out_dim See @ref XGBoosterPredictFromDMatrix for more info. + * @param out_result See @ref XGBoosterPredictFromDMatrix for more info. + * + * @return 0 when success, -1 when failure happens + */ +XGB_DLL int XGBoosterPredictFromColumnar(BoosterHandle handle, char const *array_interface, + char const *c_json_config, DMatrixHandle m, + bst_ulong const **out_shape, bst_ulong *out_dim, + const float **out_result); + /** * \brief Inplace prediction from CPU CSR matrix. * diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index bf72d95cc..3c864a1c8 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -822,8 +822,7 @@ class DMatrix: # pylint: disable=too-many-instance-attributes,too-many-public-m .. note:: This parameter is experimental - Experimental support of specializing for categorical features. Do not set - to True unless you are interested in development. Also, JSON/UBJSON + Experimental support of specializing for categorical features. JSON/UBJSON serialization format is required. """ @@ -1431,6 +1430,12 @@ class _ProxyDMatrix(DMatrix): _LIB.XGProxyDMatrixSetDataDense(self.handle, _array_interface(data)) ) + def _set_data_from_pandas(self, data: DataType) -> None: + """Set data from a pandas DataFrame. The input is a PandasTransformed instance.""" + _check_call( + _LIB.XGProxyDMatrixSetDataColumnar(self.handle, data.array_interface()) + ) + def _set_data_from_csr(self, csr: scipy.sparse.csr_matrix) -> None: """Set data from scipy csr""" from .data import _array_interface @@ -2440,6 +2445,7 @@ class Booster: assert proxy is None or isinstance(proxy, _ProxyDMatrix) from .data import ( + PandasTransformed, _array_interface, _arrow_transform, _is_arrow, @@ -2494,6 +2500,19 @@ class Booster: ) ) return _prediction_output(shape, dims, preds, False) + if isinstance(data, PandasTransformed): + _check_call( + _LIB.XGBoosterPredictFromColumnar( + self.handle, + data.array_interface(), + args, + p_handle, + ctypes.byref(shape), + ctypes.byref(dims), + ctypes.byref(preds), + ) + ) + return _prediction_output(shape, dims, preds, False) if isinstance(data, scipy.sparse.csr_matrix): from .data import transform_scipy_sparse diff --git a/python-package/xgboost/data.py b/python-package/xgboost/data.py index 49287d817..74b2966fe 100644 --- a/python-package/xgboost/data.py +++ b/python-package/xgboost/data.py @@ -65,13 +65,18 @@ def _is_scipy_csr(data: DataType) -> bool: return isinstance(data, scipy.sparse.csr_matrix) -def _array_interface(data: np.ndarray) -> bytes: +def _array_interface_dict(data: np.ndarray) -> dict: assert ( data.dtype.hasobject is False ), "Input data contains `object` dtype. Expecting numeric data." interface = data.__array_interface__ if "mask" in interface: interface["mask"] = interface["mask"].__array_interface__ + return interface + + +def _array_interface(data: np.ndarray) -> bytes: + interface = _array_interface_dict(data) interface_str = bytes(json.dumps(interface), "utf-8") return interface_str @@ -265,24 +270,24 @@ pandas_nullable_mapper = { "Int16": "int", "Int32": "int", "Int64": "int", - "UInt8": "i", - "UInt16": "i", - "UInt32": "i", - "UInt64": "i", + "UInt8": "int", + "UInt16": "int", + "UInt32": "int", + "UInt64": "int", "Float32": "float", "Float64": "float", "boolean": "i", } pandas_pyarrow_mapper = { - "int8[pyarrow]": "i", - "int16[pyarrow]": "i", - "int32[pyarrow]": "i", - "int64[pyarrow]": "i", - "uint8[pyarrow]": "i", - "uint16[pyarrow]": "i", - "uint32[pyarrow]": "i", - "uint64[pyarrow]": "i", + "int8[pyarrow]": "int", + "int16[pyarrow]": "int", + "int32[pyarrow]": "int", + "int64[pyarrow]": "int", + "uint8[pyarrow]": "int", + "uint16[pyarrow]": "int", + "uint32[pyarrow]": "int", + "uint64[pyarrow]": "int", "float[pyarrow]": "float", "float32[pyarrow]": "float", "double[pyarrow]": "float", @@ -295,7 +300,7 @@ _pandas_dtype_mapper.update(pandas_pyarrow_mapper) _ENABLE_CAT_ERR = ( - "When categorical type is supplied, The experimental DMatrix parameter" + "When categorical type is supplied, the experimental DMatrix parameter" "`enable_categorical` must be set to `True`." ) @@ -407,89 +412,122 @@ def is_pd_sparse_dtype(dtype: PandasDType) -> bool: return is_sparse(dtype) -def pandas_cat_null(data: DataFrame) -> DataFrame: - """Handle categorical dtype and nullable extension types from pandas.""" - import pandas as pd - - # handle category codes and nullable. - cat_columns = [] - nul_columns = [] - # avoid an unnecessary conversion if possible - for col, dtype in zip(data.columns, data.dtypes): - if is_pd_cat_dtype(dtype): - cat_columns.append(col) - elif is_pa_ext_categorical_dtype(dtype): - raise ValueError( - "pyarrow dictionary type is not supported. Use pandas category instead." - ) - elif is_nullable_dtype(dtype): - nul_columns.append(col) - - if cat_columns or nul_columns: - # Avoid transformation due to: PerformanceWarning: DataFrame is highly - # fragmented - transformed = data.copy(deep=False) - else: - transformed = data - - def cat_codes(ser: pd.Series) -> pd.Series: - if is_pd_cat_dtype(ser.dtype): - return ser.cat.codes - assert is_pa_ext_categorical_dtype(ser.dtype) - # Not yet supported, the index is not ordered for some reason. Alternately: - # `combine_chunks().to_pandas().cat.codes`. The result is the same. - return ser.array.__arrow_array__().combine_chunks().dictionary_encode().indices - - if cat_columns: - # DF doesn't have the cat attribute, as a result, we use apply here - transformed[cat_columns] = ( - transformed[cat_columns] - .apply(cat_codes) - .astype(np.float32) - .replace(-1.0, np.NaN) - ) - if nul_columns: - transformed[nul_columns] = transformed[nul_columns].astype(np.float32) - - # TODO(jiamingy): Investigate the possibility of using dataframe protocol or arrow - # IPC format for pandas so that we can apply the data transformation inside XGBoost - # for better memory efficiency. - - return transformed - - -def pandas_ext_num_types(data: DataFrame) -> DataFrame: - """Experimental suppport for handling pyarrow extension numeric types.""" +def pandas_pa_type(ser: Any) -> np.ndarray: + """Handle pandas pyarrow extention.""" import pandas as pd import pyarrow as pa + # No copy, callstack: + # pandas.core.internals.managers.SingleBlockManager.array_values() + # pandas.core.internals.blocks.EABackedBlock.values + d_array: pd.arrays.ArrowExtensionArray = ser.array + # no copy in __arrow_array__ + # ArrowExtensionArray._data is a chunked array + aa: pa.ChunkedArray = d_array.__arrow_array__() + # combine_chunks takes the most significant amount of time + chunk: pa.Array = aa.combine_chunks() + # When there's null value, we have to use copy + zero_copy = chunk.null_count == 0 + # Alternately, we can use chunk.buffers(), which returns a list of buffers and + # we need to concatenate them ourselves. + # FIXME(jiamingy): Is there a better way to access the arrow buffer along with + # its mask? + # Buffers from chunk.buffers() have the address attribute, but don't expose the + # mask. + arr: np.ndarray = chunk.to_numpy(zero_copy_only=zero_copy, writable=False) + arr, _ = _ensure_np_dtype(arr, arr.dtype) + return arr + + +def pandas_transform_data(data: DataFrame) -> List[np.ndarray]: + """Handle categorical dtype and extension types from pandas.""" + import pandas as pd + from pandas import Float32Dtype, Float64Dtype + + result: List[np.ndarray] = [] + + def cat_codes(ser: pd.Series) -> np.ndarray: + if is_pd_cat_dtype(ser.dtype): + return _ensure_np_dtype( + ser.cat.codes.astype(np.float32) + .replace(-1.0, np.NaN) + .to_numpy(na_value=np.nan), + np.float32, + )[0] + # Not yet supported, the index is not ordered for some reason. Alternately: + # `combine_chunks().to_pandas().cat.codes`. The result is the same. + assert is_pa_ext_categorical_dtype(ser.dtype) + return ( + ser.array.__arrow_array__() + .combine_chunks() + .dictionary_encode() + .indices.astype(np.float32) + .replace(-1.0, np.NaN) + ) + + def nu_type(ser: pd.Series) -> np.ndarray: + # Avoid conversion when possible + if isinstance(dtype, Float32Dtype): + res_dtype: NumpyDType = np.float32 + elif isinstance(dtype, Float64Dtype): + res_dtype = np.float64 + else: + res_dtype = np.float32 + return _ensure_np_dtype( + ser.to_numpy(dtype=res_dtype, na_value=np.nan), res_dtype + )[0] + + def oth_type(ser: pd.Series) -> np.ndarray: + # The dtypes module is added in 1.25. + npdtypes = np.lib.NumpyVersion(np.__version__) > np.lib.NumpyVersion("1.25.0") + npdtypes = npdtypes and isinstance( + ser.dtype, + ( + # pylint: disable=no-member + np.dtypes.Float32DType, # type: ignore + # pylint: disable=no-member + np.dtypes.Float64DType, # type: ignore + ), + ) + + if npdtypes or dtype in {np.float32, np.float64}: + array = ser.to_numpy() + else: + # Specifying the dtype can significantly slow down the conversion (about + # 15% slow down for dense inplace-predict) + array = ser.to_numpy(dtype=np.float32, na_value=np.nan) + return _ensure_np_dtype(array, array.dtype)[0] + for col, dtype in zip(data.columns, data.dtypes): - if not is_pa_ext_dtype(dtype): - continue - # No copy, callstack: - # pandas.core.internals.managers.SingleBlockManager.array_values() - # pandas.core.internals.blocks.EABackedBlock.values - d_array: pd.arrays.ArrowExtensionArray = data[col].array - # no copy in __arrow_array__ - # ArrowExtensionArray._data is a chunked array - aa: pa.ChunkedArray = d_array.__arrow_array__() - chunk: pa.Array = aa.combine_chunks() - # Alternately, we can use chunk.buffers(), which returns a list of buffers and - # we need to concatenate them ourselves. - arr = chunk.__array__() - data[col] = arr - return data + if is_pa_ext_categorical_dtype(dtype): + raise ValueError( + "pyarrow dictionary type is not supported. Use pandas category instead." + ) + if is_pd_cat_dtype(dtype): + result.append(cat_codes(data[col])) + elif is_pa_ext_dtype(dtype): + result.append(pandas_pa_type(data[col])) + elif is_nullable_dtype(dtype): + result.append(nu_type(data[col])) + elif is_pd_sparse_dtype(dtype): + arr = cast(pd.arrays.SparseArray, data[col].values) + arr = arr.to_dense() + if _is_np_array_like(arr): + arr, _ = _ensure_np_dtype(arr, arr.dtype) + result.append(arr) + else: + result.append(oth_type(data[col])) + + # FIXME(jiamingy): Investigate the possibility of using dataframe protocol or arrow + # IPC format for pandas so that we can apply the data transformation inside XGBoost + # for better memory efficiency. + return result -def _transform_pandas_df( - data: DataFrame, - enable_categorical: bool, - feature_names: Optional[FeatureNames] = None, - feature_types: Optional[FeatureTypes] = None, - meta: Optional[str] = None, - meta_type: Optional[NumpyDType] = None, -) -> Tuple[np.ndarray, Optional[FeatureNames], Optional[FeatureTypes]]: - pyarrow_extension = False +def pandas_check_dtypes(data: DataFrame, enable_categorical: bool) -> None: + """Validate the input types, returns True if the dataframe is backed by arrow.""" + sparse_extension = False + for dtype in data.dtypes: if not ( (dtype.name in _pandas_dtype_mapper) @@ -498,27 +536,65 @@ def _transform_pandas_df( or is_pa_ext_dtype(dtype) ): _invalid_dataframe_dtype(data) - if is_pa_ext_dtype(dtype): - pyarrow_extension = True + + if is_pd_sparse_dtype(dtype): + sparse_extension = True + + if sparse_extension: + warnings.warn("Sparse arrays from pandas are converted into dense.") + + +class PandasTransformed: + """A storage class for transformed pandas DataFrame.""" + + def __init__(self, columns: List[np.ndarray]) -> None: + self.columns = columns + + def array_interface(self) -> bytes: + """Return a byte string for JSON encoded array interface.""" + aitfs = list(map(_array_interface_dict, self.columns)) + sarrays = bytes(json.dumps(aitfs), "utf-8") + return sarrays + + @property + def shape(self) -> Tuple[int, int]: + """Return shape of the transformed DataFrame.""" + return self.columns[0].shape[0], len(self.columns) + + +def _transform_pandas_df( + data: DataFrame, + enable_categorical: bool, + feature_names: Optional[FeatureNames] = None, + feature_types: Optional[FeatureTypes] = None, + meta: Optional[str] = None, +) -> Tuple[PandasTransformed, Optional[FeatureNames], Optional[FeatureTypes]]: + pandas_check_dtypes(data, enable_categorical) + if meta and len(data.columns) > 1 and meta not in _matrix_meta: + raise ValueError(f"DataFrame for {meta} cannot have multiple columns") feature_names, feature_types = pandas_feature_info( data, meta, feature_names, feature_types, enable_categorical ) - transformed = pandas_cat_null(data) - if pyarrow_extension: - if transformed is data: - transformed = data.copy(deep=False) - transformed = pandas_ext_num_types(transformed) + arrays = pandas_transform_data(data) + return PandasTransformed(arrays), feature_names, feature_types - if meta and len(data.columns) > 1 and meta not in _matrix_meta: - raise ValueError(f"DataFrame for {meta} cannot have multiple columns") - dtype = meta_type if meta_type else np.float32 - arr: np.ndarray = transformed.values - if meta_type: - arr = arr.astype(dtype) - return arr, feature_names, feature_types +def _meta_from_pandas_df( + data: DataType, + name: str, + dtype: Optional[NumpyDType], + handle: ctypes.c_void_p, +) -> None: + data, _, _ = _transform_pandas_df(data, False, meta=name) + if len(data.columns) == 1: + array = data.columns[0] + else: + array = np.stack(data.columns).T + + array, dtype = _ensure_np_dtype(array, dtype) + _meta_from_numpy(array, name, dtype, handle) def _from_pandas_df( @@ -530,12 +606,21 @@ def _from_pandas_df( feature_types: Optional[FeatureTypes], data_split_mode: DataSplitMode = DataSplitMode.ROW, ) -> DispatchedDataBackendReturnType: - data, feature_names, feature_types = _transform_pandas_df( + df, feature_names, feature_types = _transform_pandas_df( data, enable_categorical, feature_names, feature_types ) - return _from_numpy_array( - data, missing, nthread, feature_names, feature_types, data_split_mode + + handle = ctypes.c_void_p() + _check_call( + _LIB.XGDMatrixCreateFromColumnar( + df.array_interface(), + make_jcargs( + nthread=nthread, missing=missing, data_split_mode=data_split_mode + ), + ctypes.byref(handle), + ) ) + return handle, feature_names, feature_types def _is_pandas_series(data: DataType) -> bool: @@ -550,7 +635,12 @@ def _meta_from_pandas_series( data: DataType, name: str, dtype: Optional[NumpyDType], handle: ctypes.c_void_p ) -> None: """Help transform pandas series for meta data like labels""" - data = data.values.astype("float") + if is_pd_sparse_dtype(data.dtype): + data = data.values.to_dense().astype(np.float32) + elif is_pa_ext_dtype(data.dtype): + data = pandas_pa_type(data) + else: + data = data.to_numpy(np.float32, na_value=np.nan) if is_pd_sparse_dtype(getattr(data, "dtype", data)): data = data.to_dense() # type: ignore @@ -732,6 +822,8 @@ def _arrow_transform(data: DataType) -> Any: return pd.ArrowDtype(pa.bool_()) return None + # For common cases, this is zero-copy, can check with: + # pa.total_allocated_bytes() df = data.to_pandas(types_mapper=type_mapper) return df @@ -859,11 +951,10 @@ def _from_cudf_df( ) interfaces_str = _cudf_array_interfaces(data, cat_codes) handle = ctypes.c_void_p() - config = bytes(json.dumps({"missing": missing, "nthread": nthread}), "utf-8") _check_call( _LIB.XGDMatrixCreateFromCudaColumnar( interfaces_str, - config, + make_jcargs(nthread=nthread, missing=missing), ctypes.byref(handle), ) ) @@ -1221,8 +1312,7 @@ def dispatch_meta_backend( if _is_arrow(data): data = _arrow_transform(data) if _is_pandas_df(data): - data, _, _ = _transform_pandas_df(data, False, meta=name, meta_type=dtype) - _meta_from_numpy(data, name, dtype, handle) + _meta_from_pandas_df(data, name, dtype=dtype, handle=handle) return if _is_pandas_series(data): _meta_from_pandas_series(data, name, dtype, handle) @@ -1244,8 +1334,7 @@ def dispatch_meta_backend( _meta_from_dt(data, name, dtype, handle) return if _is_modin_df(data): - data, _, _ = _transform_pandas_df(data, False, meta=name, meta_type=dtype) - _meta_from_numpy(data, name, dtype, handle) + _meta_from_pandas_df(data, name, dtype=dtype, handle=handle) return if _is_modin_series(data): data = data.values.astype("float") @@ -1317,11 +1406,10 @@ def _proxy_transform( if _is_arrow(data): data = _arrow_transform(data) if _is_pandas_df(data): - arr, feature_names, feature_types = _transform_pandas_df( + df, feature_names, feature_types = _transform_pandas_df( data, enable_categorical, feature_names, feature_types ) - arr, _ = _ensure_np_dtype(arr, arr.dtype) - return arr, None, feature_names, feature_types + return df, None, feature_names, feature_types raise TypeError("Value type is not supported for data iterator:" + str(type(data))) @@ -1356,6 +1444,9 @@ def dispatch_proxy_set_data( if not allow_host: raise err + if isinstance(data, PandasTransformed): + proxy._set_data_from_pandas(data) # pylint: disable=W0212 + return if _is_np_array_like(data): _check_data_shape(data) proxy._set_data_from_array(data) # pylint: disable=W0212 diff --git a/src/c_api/c_api.cc b/src/c_api/c_api.cc index 22f03640e..9aedcef2e 100644 --- a/src/c_api/c_api.cc +++ b/src/c_api/c_api.cc @@ -361,49 +361,57 @@ XGB_DLL int XGProxyDMatrixCreate(DMatrixHandle *out) { API_END(); } -XGB_DLL int -XGProxyDMatrixSetDataCudaArrayInterface(DMatrixHandle handle, - char const *c_interface_str) { +XGB_DLL int XGProxyDMatrixSetDataCudaArrayInterface(DMatrixHandle handle, + char const *c_interface_str) { API_BEGIN(); CHECK_HANDLE(); xgboost_CHECK_C_ARG_PTR(c_interface_str); auto p_m = static_cast *>(handle); CHECK(p_m); - auto m = static_cast(p_m->get()); + auto m = static_cast(p_m->get()); CHECK(m) << "Current DMatrix type does not support set data."; m->SetCUDAArray(c_interface_str); API_END(); } -XGB_DLL int XGProxyDMatrixSetDataCudaColumnar(DMatrixHandle handle, - char const *c_interface_str) { +XGB_DLL int XGProxyDMatrixSetDataCudaColumnar(DMatrixHandle handle, char const *c_interface_str) { API_BEGIN(); CHECK_HANDLE(); xgboost_CHECK_C_ARG_PTR(c_interface_str); auto p_m = static_cast *>(handle); CHECK(p_m); - auto m = static_cast(p_m->get()); + auto m = static_cast(p_m->get()); CHECK(m) << "Current DMatrix type does not support set data."; m->SetCUDAArray(c_interface_str); API_END(); } -XGB_DLL int XGProxyDMatrixSetDataDense(DMatrixHandle handle, - char const *c_interface_str) { +XGB_DLL int XGProxyDMatrixSetDataColumnar(DMatrixHandle handle, char const *c_interface_str) { API_BEGIN(); CHECK_HANDLE(); xgboost_CHECK_C_ARG_PTR(c_interface_str); auto p_m = static_cast *>(handle); CHECK(p_m); - auto m = static_cast(p_m->get()); + auto m = static_cast(p_m->get()); + CHECK(m) << "Current DMatrix type does not support set data."; + m->SetColumnarData(c_interface_str); + API_END(); +} + +XGB_DLL int XGProxyDMatrixSetDataDense(DMatrixHandle handle, char const *c_interface_str) { + API_BEGIN(); + CHECK_HANDLE(); + xgboost_CHECK_C_ARG_PTR(c_interface_str); + auto p_m = static_cast *>(handle); + CHECK(p_m); + auto m = static_cast(p_m->get()); CHECK(m) << "Current DMatrix type does not support set data."; m->SetArrayData(c_interface_str); API_END(); } -XGB_DLL int XGProxyDMatrixSetDataCSR(DMatrixHandle handle, char const *indptr, - char const *indices, char const *data, - xgboost::bst_ulong ncol) { +XGB_DLL int XGProxyDMatrixSetDataCSR(DMatrixHandle handle, char const *indptr, char const *indices, + char const *data, xgboost::bst_ulong ncol) { API_BEGIN(); CHECK_HANDLE(); xgboost_CHECK_C_ARG_PTR(indptr); @@ -411,7 +419,7 @@ XGB_DLL int XGProxyDMatrixSetDataCSR(DMatrixHandle handle, char const *indptr, xgboost_CHECK_C_ARG_PTR(data); auto p_m = static_cast *>(handle); CHECK(p_m); - auto m = static_cast(p_m->get()); + auto m = static_cast(p_m->get()); CHECK(m) << "Current DMatrix type does not support set data."; m->SetCSRData(indptr, indices, data, ncol, true); API_END(); @@ -429,6 +437,25 @@ XGB_DLL int XGDMatrixCreateFromCSREx(const size_t *indptr, const unsigned *indic API_END(); } +XGB_DLL int XGDMatrixCreateFromColumnar(char const *data, char const *c_json_config, + DMatrixHandle *out) { + API_BEGIN(); + xgboost_CHECK_C_ARG_PTR(c_json_config); + xgboost_CHECK_C_ARG_PTR(data); + + auto config = Json::Load(c_json_config); + float missing = GetMissing(config); + auto n_threads = OptionalArg(config, "nthread", 0); + auto data_split_mode = + static_cast(OptionalArg(config, "data_split_mode", 0)); + + data::ColumnarAdapter adapter{data}; + *out = new std::shared_ptr( + DMatrix::Create(&adapter, missing, n_threads, "", data_split_mode)); + + API_END(); +} + XGB_DLL int XGDMatrixCreateFromCSR(char const *indptr, char const *indices, char const *data, xgboost::bst_ulong ncol, char const *c_json_config, DMatrixHandle *out) { @@ -1196,6 +1223,27 @@ XGB_DLL int XGBoosterPredictFromDense(BoosterHandle handle, char const *array_in API_END(); } +XGB_DLL int XGBoosterPredictFromColumnar(BoosterHandle handle, char const *array_interface, + char const *c_json_config, DMatrixHandle m, + xgboost::bst_ulong const **out_shape, + xgboost::bst_ulong *out_dim, const float **out_result) { + API_BEGIN(); + CHECK_HANDLE(); + std::shared_ptr p_m{nullptr}; + if (!m) { + p_m.reset(new data::DMatrixProxy); + } else { + p_m = *static_cast *>(m); + } + auto proxy = dynamic_cast(p_m.get()); + CHECK(proxy) << "Invalid input type for inplace predict."; + xgboost_CHECK_C_ARG_PTR(array_interface); + proxy->SetColumnarData(array_interface); + auto *learner = static_cast(handle); + InplacePredictImpl(p_m, c_json_config, learner, out_shape, out_dim, out_result); + API_END(); +} + XGB_DLL int XGBoosterPredictFromCSR(BoosterHandle handle, char const *indptr, char const *indices, char const *data, xgboost::bst_ulong cols, char const *c_json_config, DMatrixHandle m, diff --git a/src/common/quantile.cc b/src/common/quantile.cc index 0490add26..c74db99e4 100644 --- a/src/common/quantile.cc +++ b/src/common/quantile.cc @@ -97,6 +97,7 @@ void HostSketchContainer::PushAdapterBatch(Batch const &batch, size_t base_rowid // the nnz from info is not reliable as sketching might be the first place to go through // the data. auto is_dense = info.num_nonzero_ == info.num_col_ * info.num_row_; + CHECK(!this->columns_size_.empty()); this->PushRowPageImpl(batch, base_rowid, weights, info.num_nonzero_, info.num_col_, is_dense, is_valid); } @@ -110,6 +111,7 @@ INSTANTIATE(CSRArrayAdapterBatch) INSTANTIATE(CSCAdapterBatch) INSTANTIATE(DataTableAdapterBatch) INSTANTIATE(SparsePageAdapterBatch) +INSTANTIATE(ColumnarAdapterBatch) namespace { /** diff --git a/src/data/adapter.h b/src/data/adapter.h index 9e7058aba..e9a4ad9fc 100644 --- a/src/data/adapter.h +++ b/src/data/adapter.h @@ -25,9 +25,7 @@ #include "xgboost/span.h" #include "xgboost/string_view.h" -namespace xgboost { -namespace data { - +namespace xgboost::data { /** External data formats should implement an adapter as below. The * adapter provides a uniform access to data outside xgboost, allowing * construction of DMatrix objects from a range of sources without duplicating @@ -279,9 +277,9 @@ class ArrayAdapterBatch : public detail::NoMetaInfo { return Line{array_interface_, idx}; } - size_t NumRows() const { return array_interface_.Shape(0); } - size_t NumCols() const { return array_interface_.Shape(1); } - size_t Size() const { return this->NumRows(); } + [[nodiscard]] std::size_t NumRows() const { return array_interface_.Shape(0); } + [[nodiscard]] std::size_t NumCols() const { return array_interface_.Shape(1); } + [[nodiscard]] std::size_t Size() const { return this->NumRows(); } explicit ArrayAdapterBatch(ArrayInterface<2> array_interface) : array_interface_{std::move(array_interface)} {} @@ -326,11 +324,11 @@ class CSRArrayAdapterBatch : public detail::NoMetaInfo { : indices_{std::move(indices)}, values_{std::move(values)}, ridx_{ridx}, offset_{offset} {} - COOTuple GetElement(std::size_t idx) const { + [[nodiscard]] COOTuple GetElement(std::size_t idx) const { return {ridx_, TypedIndex{indices_}(offset_ + idx), values_(offset_ + idx)}; } - size_t Size() const { + [[nodiscard]] std::size_t Size() const { return values_.Shape(0); } }; @@ -539,9 +537,11 @@ class CSCArrayAdapter : public detail::SingleBatchDataIter batch_{CSCArrayAdapterBatch{indptr_, indices_, values_}} {} // JVM package sends 0 as unknown - size_t NumRows() const { return num_rows_ == 0 ? kAdapterUnknownSize : num_rows_; } - size_t NumColumns() const { return indptr_.n - 1; } - const CSCArrayAdapterBatch& Value() const override { return batch_; } + [[nodiscard]] std::size_t NumRows() const { + return num_rows_ == 0 ? kAdapterUnknownSize : num_rows_; + } + [[nodiscard]] std::size_t NumColumns() const { return indptr_.n - 1; } + [[nodiscard]] const CSCArrayAdapterBatch& Value() const override { return batch_; } }; class DataTableAdapterBatch : public detail::NoMetaInfo { @@ -634,15 +634,15 @@ class DataTableAdapterBatch : public detail::NoMetaInfo { public: Line(std::size_t ridx, void const* const* const data, std::vector const& ft) : row_idx_{ridx}, data_{data}, feature_types_{ft} {} - std::size_t Size() const { return feature_types_.size(); } - COOTuple GetElement(std::size_t idx) const { + [[nodiscard]] std::size_t Size() const { return feature_types_.size(); } + [[nodiscard]] COOTuple GetElement(std::size_t idx) const { return COOTuple{row_idx_, idx, DTGetValue(data_[idx], feature_types_[idx], row_idx_)}; } }; public: - size_t Size() const { return num_rows_; } - const Line GetLine(std::size_t ridx) const { return {ridx, data_, feature_types_}; } + [[nodiscard]] size_t Size() const { return num_rows_; } + [[nodiscard]] const Line GetLine(std::size_t ridx) const { return {ridx, data_, feature_types_}; } static constexpr bool kIsRowMajor = true; private: @@ -659,9 +659,9 @@ class DataTableAdapter : public detail::SingleBatchDataIter> columns_; + + class Line { + common::Span> const& columns_; + std::size_t ridx_; + + public: + explicit Line(common::Span> const& columns, std::size_t ridx) + : columns_{columns}, ridx_{ridx} {} + [[nodiscard]] std::size_t Size() const { return columns_.empty() ? 0 : columns_.size(); } + + [[nodiscard]] COOTuple GetElement(std::size_t idx) const { + return {ridx_, idx, columns_[idx](ridx_)}; + } + }; + + public: + ColumnarAdapterBatch() = default; + explicit ColumnarAdapterBatch(common::Span> columns) + : columns_{columns} {} + [[nodiscard]] Line GetLine(std::size_t ridx) const { return Line{columns_, ridx}; } + [[nodiscard]] std::size_t Size() const { + return columns_.empty() ? 0 : columns_.front().Shape(0); + } + [[nodiscard]] std::size_t NumCols() const { return columns_.empty() ? 0 : columns_.size(); } + [[nodiscard]] std::size_t NumRows() const { return this->Size(); } + + static constexpr bool kIsRowMajor = true; +}; + +class ColumnarAdapter : public detail::SingleBatchDataIter { + std::vector> columns_; + ColumnarAdapterBatch batch_; + + public: + explicit ColumnarAdapter(StringView columns) { + auto jarray = Json::Load(columns); + CHECK(IsA(jarray)); + auto const& array = get(jarray); + for (auto col : array) { + columns_.emplace_back(get(col)); + } + bool consistent = + columns_.empty() || + std::all_of(columns_.cbegin(), columns_.cend(), [&](ArrayInterface<1, false> const& array) { + return array.Shape(0) == columns_[0].Shape(0); + }); + CHECK(consistent) << "Size of columns should be the same."; + batch_ = ColumnarAdapterBatch{columns_}; + } + + [[nodiscard]] ColumnarAdapterBatch const& Value() const override { return batch_; } + + [[nodiscard]] std::size_t NumRows() const { + if (!columns_.empty()) { + return columns_.front().shape[0]; + } + return 0; + } + [[nodiscard]] std::size_t NumColumns() const { + if (!columns_.empty()) { + return columns_.size(); + } + return 0; + } +}; + class FileAdapterBatch { public: class Line { @@ -851,6 +919,5 @@ class SparsePageAdapterBatch { Line GetLine(size_t ridx) const { return Line{page_[ridx].data(), page_[ridx].size(), ridx}; } size_t Size() const { return page_.Size(); } }; -}; // namespace data -} // namespace xgboost +} // namespace xgboost::data #endif // XGBOOST_DATA_ADAPTER_H_ diff --git a/src/data/data.cc b/src/data/data.cc index 50f64a406..96560a11f 100644 --- a/src/data/data.cc +++ b/src/data/data.cc @@ -947,38 +947,24 @@ DMatrix* DMatrix::Create(AdapterT* adapter, float missing, int nthread, const st return new data::SimpleDMatrix(adapter, missing, nthread, data_split_mode); } -template DMatrix* DMatrix::Create(data::DenseAdapter* adapter, float missing, - std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); -template DMatrix* DMatrix::Create(data::ArrayAdapter* adapter, float missing, - std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); -template DMatrix* DMatrix::Create(data::CSRAdapter* adapter, float missing, - std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); -template DMatrix* DMatrix::Create(data::CSCAdapter* adapter, float missing, - std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); -template DMatrix* DMatrix::Create(data::DataTableAdapter* adapter, - float missing, std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); -template DMatrix* DMatrix::Create(data::FileAdapter* adapter, float missing, - std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); -template DMatrix* DMatrix::Create(data::CSRArrayAdapter* adapter, - float missing, std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); -template DMatrix* DMatrix::Create(data::CSCArrayAdapter* adapter, - float missing, std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); +// Instantiate the factory function for various adapters +#define INSTANTIATION_CREATE(_AdapterT) \ + template DMatrix* DMatrix::Create( \ + data::_AdapterT * adapter, float missing, std::int32_t nthread, \ + const std::string& cache_prefix, DataSplitMode data_split_mode); + +INSTANTIATION_CREATE(DenseAdapter) +INSTANTIATION_CREATE(ArrayAdapter) +INSTANTIATION_CREATE(CSRAdapter) +INSTANTIATION_CREATE(CSCAdapter) +INSTANTIATION_CREATE(DataTableAdapter) +INSTANTIATION_CREATE(FileAdapter) +INSTANTIATION_CREATE(CSRArrayAdapter) +INSTANTIATION_CREATE(CSCArrayAdapter) +INSTANTIATION_CREATE(ColumnarAdapter) + +#undef INSTANTIATION_CREATE + template DMatrix* DMatrix::Create( data::IteratorAdapter* adapter, float missing, int nthread, const std::string& cache_prefix, DataSplitMode data_split_mode); @@ -1156,7 +1142,6 @@ uint64_t SparsePage::Push(const AdapterBatchT& batch, float missing, int nthread builder.InitStorage(); // Second pass over batch, placing elements in correct position - auto is_valid = data::IsValidFunctor{missing}; #pragma omp parallel num_threads(nthread) { @@ -1253,9 +1238,10 @@ template uint64_t SparsePage::Push(const data::CSCAdapterBatch& batch, float mis template uint64_t SparsePage::Push(const data::DataTableAdapterBatch& batch, float missing, int nthread); template uint64_t SparsePage::Push(const data::FileAdapterBatch& batch, float missing, int nthread); +template uint64_t SparsePage::Push(const data::ColumnarAdapterBatch& batch, float missing, + std::int32_t nthread); namespace data { - // List of files that will be force linked in static links. DMLC_REGISTRY_LINK_TAG(sparse_page_raw_format); DMLC_REGISTRY_LINK_TAG(gradient_index_format); diff --git a/src/data/gradient_index.cc b/src/data/gradient_index.cc index a2b3f3e54..1d3faf945 100644 --- a/src/data/gradient_index.cc +++ b/src/data/gradient_index.cc @@ -120,7 +120,7 @@ void GHistIndexMatrix::PushAdapterBatchColumns(Context const *ctx, Batch const & INSTANTIATION_PUSH(data::CSRArrayAdapterBatch) INSTANTIATION_PUSH(data::ArrayAdapterBatch) INSTANTIATION_PUSH(data::SparsePageAdapterBatch) - +INSTANTIATION_PUSH(data::ColumnarAdapterBatch) #undef INSTANTIATION_PUSH void GHistIndexMatrix::ResizeIndex(const size_t n_index, const bool isDense) { diff --git a/src/data/proxy_dmatrix.cc b/src/data/proxy_dmatrix.cc index c6e840539..a28448e3b 100644 --- a/src/data/proxy_dmatrix.cc +++ b/src/data/proxy_dmatrix.cc @@ -5,7 +5,22 @@ #include "proxy_dmatrix.h" +#include // for shared_ptr + +#include "xgboost/context.h" // for Context +#include "xgboost/data.h" // for DMatrix +#include "xgboost/logging.h" +#include "xgboost/string_view.h" // for StringView + namespace xgboost::data { +void DMatrixProxy::SetColumnarData(StringView interface_str) { + std::shared_ptr adapter{new ColumnarAdapter{interface_str}}; + this->batch_ = adapter; + this->Info().num_col_ = adapter->NumColumns(); + this->Info().num_row_ = adapter->NumRows(); + this->ctx_.Init(Args{{"device", "cpu"}}); +} + void DMatrixProxy::SetArrayData(StringView interface_str) { std::shared_ptr adapter{new ArrayAdapter{interface_str}}; this->batch_ = adapter; diff --git a/src/data/proxy_dmatrix.h b/src/data/proxy_dmatrix.h index 3bcdfbff3..544c4c81c 100644 --- a/src/data/proxy_dmatrix.h +++ b/src/data/proxy_dmatrix.h @@ -62,6 +62,8 @@ class DMatrixProxy : public DMatrix { #endif // defined(XGBOOST_USE_CUDA) } + void SetColumnarData(StringView interface_str); + void SetArrayData(StringView interface_str); void SetCSRData(char const* c_indptr, char const* c_indices, char const* c_values, bst_feature_t n_features, bool on_host); @@ -151,6 +153,17 @@ decltype(auto) HostAdapterDispatch(DMatrixProxy const* proxy, Fn fn, bool* type_ if (type_error) { *type_error = false; } + } else if (proxy->Adapter().type() == typeid(std::shared_ptr)) { + if constexpr (get_value) { + auto value = std::any_cast>(proxy->Adapter())->Value(); + return fn(value); + } else { + auto value = std::any_cast>(proxy->Adapter()); + return fn(value); + } + if (type_error) { + *type_error = false; + } } else { if (type_error) { *type_error = true; diff --git a/src/data/simple_dmatrix.cc b/src/data/simple_dmatrix.cc index 2bf81892f..99bf67ba0 100644 --- a/src/data/simple_dmatrix.cc +++ b/src/data/simple_dmatrix.cc @@ -1,5 +1,5 @@ /** - * Copyright 2014~2023 by XGBoost Contributors + * Copyright 2014~2023, XGBoost Contributors * \file simple_dmatrix.cc * \brief the input data structure for gradient boosting * \author Tianqi Chen @@ -356,6 +356,8 @@ template SimpleDMatrix::SimpleDMatrix(DataTableAdapter* adapter, float missing, DataSplitMode data_split_mode); template SimpleDMatrix::SimpleDMatrix(FileAdapter* adapter, float missing, int nthread, DataSplitMode data_split_mode); +template SimpleDMatrix::SimpleDMatrix(ColumnarAdapter* adapter, float missing, int nthread, + DataSplitMode data_split_mode); template SimpleDMatrix::SimpleDMatrix( IteratorAdapter* adapter, float missing, int nthread, DataSplitMode data_split_mode); diff --git a/src/predictor/cpu_predictor.cc b/src/predictor/cpu_predictor.cc index 20305850a..d97b527f0 100644 --- a/src/predictor/cpu_predictor.cc +++ b/src/predictor/cpu_predictor.cc @@ -761,6 +761,9 @@ class CPUPredictor : public Predictor { } else if (x.type() == typeid(std::shared_ptr)) { this->DispatchedInplacePredict(x, p_m, model, missing, out_preds, tree_begin, tree_end); + } else if (x.type() == typeid(std::shared_ptr)) { + this->DispatchedInplacePredict( + x, p_m, model, missing, out_preds, tree_begin, tree_end); } else { return false; } diff --git a/tests/python/test_with_modin.py b/tests/python/test_with_modin.py index 3f1f9cf97..75dae0654 100644 --- a/tests/python/test_with_modin.py +++ b/tests/python/test_with_modin.py @@ -16,7 +16,7 @@ pytestmark = pytest.mark.skipif(**tm.no_modin()) class TestModin: @pytest.mark.xfail - def test_modin(self): + def test_modin(self) -> None: df = md.DataFrame([[1, 2., True], [2, 3., False]], columns=['a', 'b', 'c']) dm = xgb.DMatrix(df, label=md.Series([1, 2])) @@ -67,8 +67,8 @@ class TestModin: enable_categorical=False) exp = np.array([[1., 1., 0., 0.], [2., 0., 1., 0.], - [3., 0., 0., 1.]]) - np.testing.assert_array_equal(result, exp) + [3., 0., 0., 1.]]).T + np.testing.assert_array_equal(result.columns, exp) dm = xgb.DMatrix(dummies) assert dm.feature_names == ['B', 'A_X', 'A_Y', 'A_Z'] assert dm.feature_types == ['int', 'int', 'int', 'int'] @@ -108,20 +108,23 @@ class TestModin: def test_modin_label(self): # label must be a single column - df = md.DataFrame({'A': ['X', 'Y', 'Z'], 'B': [1, 2, 3]}) + df = md.DataFrame({"A": ["X", "Y", "Z"], "B": [1, 2, 3]}) with pytest.raises(ValueError): - xgb.data._transform_pandas_df(df, False, None, None, 'label', 'float') + xgb.data._transform_pandas_df(df, False, None, None, "label") # label must be supported dtype - df = md.DataFrame({'A': np.array(['a', 'b', 'c'], dtype=object)}) + df = md.DataFrame({"A": np.array(["a", "b", "c"], dtype=object)}) with pytest.raises(ValueError): - xgb.data._transform_pandas_df(df, False, None, None, 'label', 'float') + xgb.data._transform_pandas_df(df, False, None, None, "label") - df = md.DataFrame({'A': np.array([1, 2, 3], dtype=int)}) - result, _, _ = xgb.data._transform_pandas_df(df, False, None, None, - 'label', 'float') - np.testing.assert_array_equal(result, np.array([[1.], [2.], [3.]], - dtype=float)) + df = md.DataFrame({"A": np.array([1, 2, 3], dtype=int)}) + result, _, _ = xgb.data._transform_pandas_df( + df, False, None, None, "label" + ) + np.testing.assert_array_equal( + np.stack(result.columns, axis=1), + np.array([[1.0], [2.0], [3.0]], dtype=float), + ) dm = xgb.DMatrix(np.random.randn(3, 2), label=df) assert dm.num_row() == 3 assert dm.num_col() == 2 diff --git a/tests/python/test_with_pandas.py b/tests/python/test_with_pandas.py index 6a9ed4a84..c7b99a991 100644 --- a/tests/python/test_with_pandas.py +++ b/tests/python/test_with_pandas.py @@ -105,8 +105,8 @@ class TestPandas: result, _, _ = xgb.data._transform_pandas_df(dummies, enable_categorical=False) exp = np.array( [[1.0, 1.0, 0.0, 0.0], [2.0, 0.0, 1.0, 0.0], [3.0, 0.0, 0.0, 1.0]] - ) - np.testing.assert_array_equal(result, exp) + ).T + np.testing.assert_array_equal(result.columns, exp) dm = xgb.DMatrix(dummies, data_split_mode=data_split_mode) assert dm.num_row() == 3 if data_split_mode == DataSplitMode.ROW: @@ -202,6 +202,20 @@ class TestPandas: else: assert dm.num_col() == 1 * world_size + @pytest.mark.skipif(**tm.no_sklearn()) + def test_multi_target(self) -> None: + from sklearn.datasets import make_regression + + X, y = make_regression(n_samples=1024, n_features=4, n_targets=3) + ydf = pd.DataFrame({i: y[:, i] for i in range(y.shape[1])}) + + Xy = xgb.DMatrix(X, ydf) + assert Xy.num_row() == y.shape[0] + assert Xy.get_label().size == y.shape[0] * y.shape[1] + Xy = xgb.QuantileDMatrix(X, ydf) + assert Xy.num_row() == y.shape[0] + assert Xy.get_label().size == y.shape[0] * y.shape[1] + def test_slice(self): rng = np.random.RandomState(1994) rows = 100 @@ -233,13 +247,14 @@ class TestPandas: X, enable_categorical=True ) - assert transformed[:, 0].min() == 0 + assert transformed.columns[0].min() == 0 # test missing value X = pd.DataFrame({"f0": ["a", "b", np.NaN]}) X["f0"] = X["f0"].astype("category") arr, _, _ = xgb.data._transform_pandas_df(X, enable_categorical=True) - assert not np.any(arr == -1.0) + for c in arr.columns: + assert not np.any(c == -1.0) X = X["f0"] y = y[: X.shape[0]] @@ -273,24 +288,25 @@ class TestPandas: predt_dense = booster.predict(xgb.DMatrix(X.sparse.to_dense())) np.testing.assert_allclose(predt_sparse, predt_dense) - def test_pandas_label(self, data_split_mode=DataSplitMode.ROW): + def test_pandas_label( + self, data_split_mode: DataSplitMode = DataSplitMode.ROW + ) -> None: world_size = xgb.collective.get_world_size() # label must be a single column df = pd.DataFrame({"A": ["X", "Y", "Z"], "B": [1, 2, 3]}) with pytest.raises(ValueError): - xgb.data._transform_pandas_df(df, False, None, None, "label", "float") + xgb.data._transform_pandas_df(df, False, None, None, "label") # label must be supported dtype df = pd.DataFrame({"A": np.array(["a", "b", "c"], dtype=object)}) with pytest.raises(ValueError): - xgb.data._transform_pandas_df(df, False, None, None, "label", "float") + xgb.data._transform_pandas_df(df, False, None, None, "label") df = pd.DataFrame({"A": np.array([1, 2, 3], dtype=int)}) - result, _, _ = xgb.data._transform_pandas_df( - df, False, None, None, "label", "float" - ) + result, _, _ = xgb.data._transform_pandas_df(df, False, None, None, "label") np.testing.assert_array_equal( - result, np.array([[1.0], [2.0], [3.0]], dtype=float) + np.stack(result.columns, axis=1), + np.array([[1.0], [2.0], [3.0]], dtype=float), ) dm = xgb.DMatrix( np.random.randn(3, 2), label=df, data_split_mode=data_split_mode @@ -507,6 +523,35 @@ class TestPandas: np.testing.assert_allclose(m_orig.get_label(), m_etype.get_label()) np.testing.assert_allclose(m_etype.get_label(), y.values) + @pytest.mark.parametrize("DMatrixT", [xgb.DMatrix, xgb.QuantileDMatrix]) + def test_mixed_type(self, DMatrixT: Type[xgb.DMatrix]) -> None: + f0 = np.arange(0, 4) + f1 = pd.Series(f0, dtype="int64[pyarrow]") + f2l = list(f0) + f2l[0] = pd.NA + f2 = pd.Series(f2l, dtype=pd.Int64Dtype()) + + df = pd.DataFrame({"f0": f0}) + df["f2"] = f2 + + m = DMatrixT(df) + assert m.num_col() == df.shape[1] + + df["f1"] = f1 + m = DMatrixT(df) + assert m.num_col() == df.shape[1] + assert m.num_row() == df.shape[0] + assert m.num_nonmissing() == df.size - 1 + assert m.feature_names == list(map(str, df.columns)) + assert m.feature_types == ["int"] * df.shape[1] + + y = f0 + m.set_info(label=y) + booster = xgb.train({}, m) + p0 = booster.inplace_predict(df) + p1 = booster.predict(m) + np.testing.assert_allclose(p0, p1) + @pytest.mark.skipif(tm.is_windows(), reason="Rabit does not run on windows") def test_pandas_column_split(self): tm.run_with_rabit( From ddab49a8be6c37c4d72395a610d7ac72fd4bca15 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 12 Dec 2023 05:13:47 +0100 Subject: [PATCH 13/59] [doc][R] Update arguments for ellipsis in predict (#9868) --- R-package/R/xgb.Booster.R | 2 +- R-package/man/predict.xgb.Booster.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index a5c9e5088..aa5e65e40 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -194,7 +194,7 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { #' @param strict_shape Default is \code{FALSE}. When it's set to \code{TRUE}, output #' type and shape of prediction are invariant to model type. #' -#' @param ... Parameters passed to \code{predict.xgb.Booster} +#' @param ... Not used. #' #' @details #' diff --git a/R-package/man/predict.xgb.Booster.Rd b/R-package/man/predict.xgb.Booster.Rd index ee3b370c4..c1e58f63b 100644 --- a/R-package/man/predict.xgb.Booster.Rd +++ b/R-package/man/predict.xgb.Booster.Rd @@ -66,7 +66,7 @@ to \code{c(1, 1)} XGBoost will use all trees.} \item{strict_shape}{Default is \code{FALSE}. When it's set to \code{TRUE}, output type and shape of prediction are invariant to model type.} -\item{...}{Parameters passed to \code{predict.xgb.Booster}} +\item{...}{Not used.} } \value{ The return type is different depending whether \code{strict_shape} is set to \code{TRUE}. By default, From 43897b829680d241491abe1ecd46b2ba9d338967 Mon Sep 17 00:00:00 2001 From: Dmitry Razdoburdin Date: Tue, 12 Dec 2023 07:41:50 +0100 Subject: [PATCH 14/59] Sycl implementation for objective functions (#9846) --------- Co-authored-by: Dmitry Razdoburdin <> --- include/xgboost/objective.h | 6 + plugin/CMakeLists.txt | 2 + plugin/sycl/objective/multiclass_obj.cc | 210 +++++++++ plugin/sycl/objective/regression_obj.cc | 197 +++++++++ src/objective/multiclass_obj.cu | 11 +- src/objective/multiclass_param.h | 25 ++ src/objective/objective.cc | 22 +- src/objective/regression_obj.cu | 11 +- src/objective/regression_param.h | 25 ++ tests/cpp/objective/test_multiclass_obj.cc | 19 +- tests/cpp/objective/test_multiclass_obj.h | 19 + .../cpp/objective/test_multiclass_obj_cpu.cc | 25 ++ .../cpp/objective/test_multiclass_obj_gpu.cu | 2 +- tests/cpp/objective/test_regression_obj.cc | 414 +----------------- tests/cpp/objective/test_regression_obj.h | 23 + .../cpp/objective/test_regression_obj_cpu.cc | 412 +++++++++++++++++ .../cpp/objective/test_regression_obj_gpu.cu | 2 +- tests/cpp/plugin/test_sycl_multiclass_obj.cc | 28 ++ tests/cpp/plugin/test_sycl_regression_obj.cc | 99 +++++ 19 files changed, 1129 insertions(+), 423 deletions(-) create mode 100644 plugin/sycl/objective/multiclass_obj.cc create mode 100644 plugin/sycl/objective/regression_obj.cc create mode 100644 src/objective/multiclass_param.h create mode 100644 src/objective/regression_param.h create mode 100644 tests/cpp/objective/test_multiclass_obj.h create mode 100644 tests/cpp/objective/test_multiclass_obj_cpu.cc create mode 100644 tests/cpp/objective/test_regression_obj.h create mode 100644 tests/cpp/objective/test_regression_obj_cpu.cc create mode 100644 tests/cpp/plugin/test_sycl_multiclass_obj.cc create mode 100644 tests/cpp/plugin/test_sycl_regression_obj.cc diff --git a/include/xgboost/objective.h b/include/xgboost/objective.h index d2623ee01..b88c30552 100644 --- a/include/xgboost/objective.h +++ b/include/xgboost/objective.h @@ -129,6 +129,12 @@ class ObjFunction : public Configurable { * \param name Name of the objective. */ static ObjFunction* Create(const std::string& name, Context const* ctx); + + /*! + * \brief Return sycl specific implementation name if possible. + * \param name Name of the objective. + */ + static std::string GetSyclImplementationName(const std::string& name); }; /*! diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt index 0fecb4fb2..e575f1a41 100644 --- a/plugin/CMakeLists.txt +++ b/plugin/CMakeLists.txt @@ -1,6 +1,8 @@ if(PLUGIN_SYCL) set(CMAKE_CXX_COMPILER "icpx") add_library(plugin_sycl OBJECT + ${xgboost_SOURCE_DIR}/plugin/sycl/objective/regression_obj.cc + ${xgboost_SOURCE_DIR}/plugin/sycl/objective/multiclass_obj.cc ${xgboost_SOURCE_DIR}/plugin/sycl/device_manager.cc ${xgboost_SOURCE_DIR}/plugin/sycl/predictor/predictor.cc) target_include_directories(plugin_sycl diff --git a/plugin/sycl/objective/multiclass_obj.cc b/plugin/sycl/objective/multiclass_obj.cc new file mode 100644 index 000000000..3104dd35e --- /dev/null +++ b/plugin/sycl/objective/multiclass_obj.cc @@ -0,0 +1,210 @@ +/*! + * Copyright 2015-2023 by Contributors + * \file multiclass_obj.cc + * \brief Definition of multi-class classification objectives. + */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" +#pragma GCC diagnostic ignored "-W#pragma-messages" +#include +#pragma GCC diagnostic pop + +#include +#include +#include +#include + +#include "xgboost/parameter.h" +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" +#include "xgboost/data.h" +#include "../../src/common/math.h" +#pragma GCC diagnostic pop +#include "xgboost/logging.h" +#include "xgboost/objective.h" +#include "xgboost/json.h" +#include "xgboost/span.h" + +#include "../../../src/objective/multiclass_param.h" + +#include "../device_manager.h" +#include + +namespace xgboost { +namespace sycl { +namespace obj { + +DMLC_REGISTRY_FILE_TAG(multiclass_obj_sycl); + +class SoftmaxMultiClassObj : public ObjFunction { + public: + explicit SoftmaxMultiClassObj(bool output_prob) + : output_prob_(output_prob) {} + + void Configure(Args const& args) override { + param_.UpdateAllowUnknown(args); + qu_ = device_manager.GetQueue(ctx_->Device()); + } + + void GetGradient(const HostDeviceVector& preds, + const MetaInfo& info, + int iter, + linalg::Matrix* out_gpair) override { + if (preds.Size() == 0) return; + if (info.labels.Size() == 0) return; + + CHECK(preds.Size() == (static_cast(param_.num_class) * info.labels.Size())) + << "SoftmaxMultiClassObj: label size and pred size does not match.\n" + << "label.Size() * num_class: " + << info.labels.Size() * static_cast(param_.num_class) << "\n" + << "num_class: " << param_.num_class << "\n" + << "preds.Size(): " << preds.Size(); + + const int nclass = param_.num_class; + const auto ndata = static_cast(preds.Size() / nclass); + + out_gpair->Reshape(info.num_row_, static_cast(nclass)); + + const bool is_null_weight = info.weights_.Size() == 0; + if (!is_null_weight) { + CHECK_EQ(info.weights_.Size(), ndata) + << "Number of weights should be equal to number of data points."; + } + + ::sycl::buffer preds_buf(preds.HostPointer(), preds.Size()); + ::sycl::buffer labels_buf(info.labels.Data()->HostPointer(), info.labels.Size()); + ::sycl::buffer out_gpair_buf(out_gpair->Data()->HostPointer(), + out_gpair->Size()); + ::sycl::buffer weights_buf(is_null_weight ? NULL : info.weights_.HostPointer(), + is_null_weight ? 1 : info.weights_.Size()); + + int flag = 1; + { + ::sycl::buffer flag_buf(&flag, 1); + qu_.submit([&](::sycl::handler& cgh) { + auto preds_acc = preds_buf.get_access<::sycl::access::mode::read>(cgh); + auto labels_acc = labels_buf.get_access<::sycl::access::mode::read>(cgh); + auto weights_acc = weights_buf.get_access<::sycl::access::mode::read>(cgh); + auto out_gpair_acc = out_gpair_buf.get_access<::sycl::access::mode::write>(cgh); + auto flag_buf_acc = flag_buf.get_access<::sycl::access::mode::write>(cgh); + cgh.parallel_for<>(::sycl::range<1>(ndata), [=](::sycl::id<1> pid) { + int idx = pid[0]; + + bst_float const * point = &preds_acc[idx * nclass]; + + // Part of Softmax function + bst_float wmax = std::numeric_limits::min(); + for (int k = 0; k < nclass; k++) { wmax = ::sycl::max(point[k], wmax); } + float wsum = 0.0f; + for (int k = 0; k < nclass; k++) { wsum += ::sycl::exp(point[k] - wmax); } + auto label = labels_acc[idx]; + if (label < 0 || label >= nclass) { + flag_buf_acc[0] = 0; + label = 0; + } + bst_float wt = is_null_weight ? 1.0f : weights_acc[idx]; + for (int k = 0; k < nclass; ++k) { + bst_float p = expf(point[k] - wmax) / static_cast(wsum); + const float eps = 1e-16f; + const bst_float h = ::sycl::max(2.0f * p * (1.0f - p) * wt, eps); + p = label == k ? p - 1.0f : p; + out_gpair_acc[idx * nclass + k] = GradientPair(p * wt, h); + } + }); + }).wait(); + } + // flag_buf is destroyed, content is copyed to the "flag" + + if (flag == 0) { + LOG(FATAL) << "SYCL::SoftmaxMultiClassObj: label must be in [0, num_class)."; + } + } + void PredTransform(HostDeviceVector* io_preds) const override { + this->Transform(io_preds, output_prob_); + } + void EvalTransform(HostDeviceVector* io_preds) override { + this->Transform(io_preds, true); + } + const char* DefaultEvalMetric() const override { + return "mlogloss"; + } + + inline void Transform(HostDeviceVector *io_preds, bool prob) const { + if (io_preds->Size() == 0) return; + const int nclass = param_.num_class; + const auto ndata = static_cast(io_preds->Size() / nclass); + max_preds_.Resize(ndata); + + { + ::sycl::buffer io_preds_buf(io_preds->HostPointer(), io_preds->Size()); + + if (prob) { + qu_.submit([&](::sycl::handler& cgh) { + auto io_preds_acc = io_preds_buf.get_access<::sycl::access::mode::read_write>(cgh); + cgh.parallel_for<>(::sycl::range<1>(ndata), [=](::sycl::id<1> pid) { + int idx = pid[0]; + auto it = io_preds_acc.begin() + idx * nclass; + common::Softmax(it, it + nclass); + }); + }).wait(); + } else { + ::sycl::buffer max_preds_buf(max_preds_.HostPointer(), max_preds_.Size()); + + qu_.submit([&](::sycl::handler& cgh) { + auto io_preds_acc = io_preds_buf.get_access<::sycl::access::mode::read>(cgh); + auto max_preds_acc = max_preds_buf.get_access<::sycl::access::mode::read_write>(cgh); + cgh.parallel_for<>(::sycl::range<1>(ndata), [=](::sycl::id<1> pid) { + int idx = pid[0]; + auto it = io_preds_acc.begin() + idx * nclass; + max_preds_acc[idx] = common::FindMaxIndex(it, it + nclass) - it; + }); + }).wait(); + } + } + + if (!prob) { + io_preds->Resize(max_preds_.Size()); + io_preds->Copy(max_preds_); + } + } + + struct ObjInfo Task() const override {return {ObjInfo::kClassification}; } + + void SaveConfig(Json* p_out) const override { + auto& out = *p_out; + if (this->output_prob_) { + out["name"] = String("multi:softprob"); + } else { + out["name"] = String("multi:softmax"); + } + out["softmax_multiclass_param"] = ToJson(param_); + } + + void LoadConfig(Json const& in) override { + FromJson(in["softmax_multiclass_param"], ¶m_); + } + + private: + // output probability + bool output_prob_; + // parameter + xgboost::obj::SoftmaxMultiClassParam param_; + // Cache for max_preds + mutable HostDeviceVector max_preds_; + + sycl::DeviceManager device_manager; + + mutable ::sycl::queue qu_; +}; + +XGBOOST_REGISTER_OBJECTIVE(SoftmaxMultiClass, "multi:softmax_sycl") +.describe("Softmax for multi-class classification, output class index.") +.set_body([]() { return new SoftmaxMultiClassObj(false); }); + +XGBOOST_REGISTER_OBJECTIVE(SoftprobMultiClass, "multi:softprob_sycl") +.describe("Softmax for multi-class classification, output probability distribution.") +.set_body([]() { return new SoftmaxMultiClassObj(true); }); + +} // namespace obj +} // namespace sycl +} // namespace xgboost diff --git a/plugin/sycl/objective/regression_obj.cc b/plugin/sycl/objective/regression_obj.cc new file mode 100644 index 000000000..985498717 --- /dev/null +++ b/plugin/sycl/objective/regression_obj.cc @@ -0,0 +1,197 @@ +/*! + * Copyright 2015-2023 by Contributors + * \file regression_obj.cc + * \brief Definition of regression objectives. + */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" +#pragma GCC diagnostic ignored "-W#pragma-messages" +#include +#include +#pragma GCC diagnostic pop +#include + +#include +#include +#include + +#include "xgboost/host_device_vector.h" +#include "xgboost/json.h" +#include "xgboost/parameter.h" +#include "xgboost/span.h" + +#include "../../src/common/transform.h" +#include "../../src/common/common.h" +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" +#include "../../../src/objective/regression_loss.h" +#pragma GCC diagnostic pop +#include "../../../src/objective/regression_param.h" + +#include "../device_manager.h" + +#include + +namespace xgboost { +namespace sycl { +namespace obj { + +DMLC_REGISTRY_FILE_TAG(regression_obj_sycl); + +template +class RegLossObj : public ObjFunction { + protected: + HostDeviceVector label_correct_; + + public: + RegLossObj() = default; + + void Configure(const std::vector >& args) override { + param_.UpdateAllowUnknown(args); + qu_ = device_manager.GetQueue(ctx_->Device()); + } + + void GetGradient(const HostDeviceVector& preds, + const MetaInfo &info, + int iter, + linalg::Matrix* out_gpair) override { + if (info.labels.Size() == 0) return; + CHECK_EQ(preds.Size(), info.labels.Size()) + << " " << "labels are not correctly provided" + << "preds.size=" << preds.Size() << ", label.size=" << info.labels.Size() << ", " + << "Loss: " << Loss::Name(); + + size_t const ndata = preds.Size(); + auto const n_targets = this->Targets(info); + out_gpair->Reshape(info.num_row_, n_targets); + + // TODO(razdoburdin): add label_correct check + label_correct_.Resize(1); + label_correct_.Fill(1); + + bool is_null_weight = info.weights_.Size() == 0; + + ::sycl::buffer preds_buf(preds.HostPointer(), preds.Size()); + ::sycl::buffer labels_buf(info.labels.Data()->HostPointer(), info.labels.Size()); + ::sycl::buffer out_gpair_buf(out_gpair->Data()->HostPointer(), + out_gpair->Size()); + ::sycl::buffer weights_buf(is_null_weight ? NULL : info.weights_.HostPointer(), + is_null_weight ? 1 : info.weights_.Size()); + + auto scale_pos_weight = param_.scale_pos_weight; + if (!is_null_weight) { + CHECK_EQ(info.weights_.Size(), info.labels.Shape(0)) + << "Number of weights should be equal to number of data points."; + } + + int flag = 1; + { + ::sycl::buffer flag_buf(&flag, 1); + qu_.submit([&](::sycl::handler& cgh) { + auto preds_acc = preds_buf.get_access<::sycl::access::mode::read>(cgh); + auto labels_acc = labels_buf.get_access<::sycl::access::mode::read>(cgh); + auto weights_acc = weights_buf.get_access<::sycl::access::mode::read>(cgh); + auto out_gpair_acc = out_gpair_buf.get_access<::sycl::access::mode::write>(cgh); + auto flag_buf_acc = flag_buf.get_access<::sycl::access::mode::write>(cgh); + cgh.parallel_for<>(::sycl::range<1>(ndata), [=](::sycl::id<1> pid) { + int idx = pid[0]; + bst_float p = Loss::PredTransform(preds_acc[idx]); + bst_float w = is_null_weight ? 1.0f : weights_acc[idx/n_targets]; + bst_float label = labels_acc[idx]; + if (label == 1.0f) { + w *= scale_pos_weight; + } + if (!Loss::CheckLabel(label)) { + // If there is an incorrect label, the host code will know. + flag_buf_acc[0] = 0; + } + out_gpair_acc[idx] = GradientPair(Loss::FirstOrderGradient(p, label) * w, + Loss::SecondOrderGradient(p, label) * w); + }); + }).wait(); + } + // flag_buf is destroyed, content is copyed to the "flag" + + if (flag == 0) { + LOG(FATAL) << Loss::LabelErrorMsg(); + } + } + + public: + const char* DefaultEvalMetric() const override { + return Loss::DefaultEvalMetric(); + } + + void PredTransform(HostDeviceVector *io_preds) const override { + size_t const ndata = io_preds->Size(); + if (ndata == 0) return; + ::sycl::buffer io_preds_buf(io_preds->HostPointer(), io_preds->Size()); + + qu_.submit([&](::sycl::handler& cgh) { + auto io_preds_acc = io_preds_buf.get_access<::sycl::access::mode::read_write>(cgh); + cgh.parallel_for<>(::sycl::range<1>(ndata), [=](::sycl::id<1> pid) { + int idx = pid[0]; + io_preds_acc[idx] = Loss::PredTransform(io_preds_acc[idx]); + }); + }).wait(); + } + + float ProbToMargin(float base_score) const override { + return Loss::ProbToMargin(base_score); + } + + struct ObjInfo Task() const override { + return Loss::Info(); + }; + + uint32_t Targets(MetaInfo const& info) const override { + // Multi-target regression. + return std::max(static_cast(1), info.labels.Shape(1)); + } + + void SaveConfig(Json* p_out) const override { + auto& out = *p_out; + out["name"] = String(Loss::Name()); + out["reg_loss_param"] = ToJson(param_); + } + + void LoadConfig(Json const& in) override { + FromJson(in["reg_loss_param"], ¶m_); + } + + protected: + xgboost::obj::RegLossParam param_; + sycl::DeviceManager device_manager; + + mutable ::sycl::queue qu_; +}; + +XGBOOST_REGISTER_OBJECTIVE(SquaredLossRegression, + std::string(xgboost::obj::LinearSquareLoss::Name()) + "_sycl") +.describe("Regression with squared error with SYCL backend.") +.set_body([]() { return new RegLossObj(); }); + +XGBOOST_REGISTER_OBJECTIVE(SquareLogError, + std::string(xgboost::obj::SquaredLogError::Name()) + "_sycl") +.describe("Regression with root mean squared logarithmic error with SYCL backend.") +.set_body([]() { return new RegLossObj(); }); + +XGBOOST_REGISTER_OBJECTIVE(LogisticRegression, + std::string(xgboost::obj::LogisticRegression::Name()) + "_sycl") +.describe("Logistic regression for probability regression task with SYCL backend.") +.set_body([]() { return new RegLossObj(); }); + +XGBOOST_REGISTER_OBJECTIVE(LogisticClassification, + std::string(xgboost::obj::LogisticClassification::Name()) + "_sycl") +.describe("Logistic regression for binary classification task with SYCL backend.") +.set_body([]() { return new RegLossObj(); }); + +XGBOOST_REGISTER_OBJECTIVE(LogisticRaw, + std::string(xgboost::obj::LogisticRaw::Name()) + "_sycl") +.describe("Logistic regression for classification, output score " + "before logistic transformation with SYCL backend.") +.set_body([]() { return new RegLossObj(); }); + +} // namespace obj +} // namespace sycl +} // namespace xgboost diff --git a/src/objective/multiclass_obj.cu b/src/objective/multiclass_obj.cu index 38880f911..1a3df3884 100644 --- a/src/objective/multiclass_obj.cu +++ b/src/objective/multiclass_obj.cu @@ -21,6 +21,8 @@ #include "../common/math.h" #include "../common/transform.h" +#include "multiclass_param.h" + namespace xgboost { namespace obj { @@ -28,15 +30,6 @@ namespace obj { DMLC_REGISTRY_FILE_TAG(multiclass_obj_gpu); #endif // defined(XGBOOST_USE_CUDA) -struct SoftmaxMultiClassParam : public XGBoostParameter { - int num_class; - // declare parameters - DMLC_DECLARE_PARAMETER(SoftmaxMultiClassParam) { - DMLC_DECLARE_FIELD(num_class).set_lower_bound(1) - .describe("Number of output class in the multi-class classification."); - } -}; - class SoftmaxMultiClassObj : public ObjFunction { public: explicit SoftmaxMultiClassObj(bool output_prob) diff --git a/src/objective/multiclass_param.h b/src/objective/multiclass_param.h new file mode 100644 index 000000000..d1dea15fd --- /dev/null +++ b/src/objective/multiclass_param.h @@ -0,0 +1,25 @@ +/*! + * Copyright 2015-2023 by Contributors + * \file multiclass_param.h + * \brief Definition of multi-class classification parameters. + */ +#ifndef XGBOOST_OBJECTIVE_MULTICLASS_PARAM_H_ +#define XGBOOST_OBJECTIVE_MULTICLASS_PARAM_H_ + +#include "xgboost/parameter.h" + +namespace xgboost { +namespace obj { + +struct SoftmaxMultiClassParam : public XGBoostParameter { + int num_class; + // declare parameters + DMLC_DECLARE_PARAMETER(SoftmaxMultiClassParam) { + DMLC_DECLARE_FIELD(num_class).set_lower_bound(1) + .describe("Number of output class in the multi-class classification."); + } +}; + +} // namespace obj +} // namespace xgboost +#endif // XGBOOST_OBJECTIVE_MULTICLASS_PARAM_H_ diff --git a/src/objective/objective.cc b/src/objective/objective.cc index 85cd9803d..1ccf53264 100644 --- a/src/objective/objective.cc +++ b/src/objective/objective.cc @@ -18,7 +18,11 @@ DMLC_REGISTRY_ENABLE(::xgboost::ObjFunctionReg); namespace xgboost { // implement factory functions ObjFunction* ObjFunction::Create(const std::string& name, Context const* ctx) { - auto *e = ::dmlc::Registry< ::xgboost::ObjFunctionReg>::Get()->Find(name); + std::string obj_name = name; + if (ctx->IsSycl()) { + obj_name = GetSyclImplementationName(obj_name); + } + auto *e = ::dmlc::Registry< ::xgboost::ObjFunctionReg>::Get()->Find(obj_name); if (e == nullptr) { std::stringstream ss; for (const auto& entry : ::dmlc::Registry< ::xgboost::ObjFunctionReg>::List()) { @@ -32,6 +36,22 @@ ObjFunction* ObjFunction::Create(const std::string& name, Context const* ctx) { return pobj; } +/* If the objective function has sycl-specific implementation, + * returns the specific implementation name. + * Otherwise return the orginal name without modifications. + */ +std::string ObjFunction::GetSyclImplementationName(const std::string& name) { + const std::string sycl_postfix = "_sycl"; + auto *e = ::dmlc::Registry< ::xgboost::ObjFunctionReg>::Get()->Find(name + sycl_postfix); + if (e != nullptr) { + // Function has specific sycl implementation + return name + sycl_postfix; + } else { + // Function hasn't specific sycl implementation + return name; + } +} + void ObjFunction::InitEstimation(MetaInfo const&, linalg::Tensor* base_score) const { CHECK(base_score); base_score->Reshape(1); diff --git a/src/objective/regression_obj.cu b/src/objective/regression_obj.cu index 5627600fc..df30b354b 100644 --- a/src/objective/regression_obj.cu +++ b/src/objective/regression_obj.cu @@ -35,6 +35,8 @@ #include "xgboost/span.h" #include "xgboost/tree_model.h" // RegTree +#include "regression_param.h" + #if defined(XGBOOST_USE_CUDA) #include "../common/cuda_context.cuh" // for CUDAContext #include "../common/device_helpers.cuh" @@ -53,14 +55,7 @@ void CheckRegInputs(MetaInfo const& info, HostDeviceVector const& pre DMLC_REGISTRY_FILE_TAG(regression_obj_gpu); #endif // defined(XGBOOST_USE_CUDA) -struct RegLossParam : public XGBoostParameter { - float scale_pos_weight; - // declare parameters - DMLC_DECLARE_PARAMETER(RegLossParam) { - DMLC_DECLARE_FIELD(scale_pos_weight).set_default(1.0f).set_lower_bound(0.0f) - .describe("Scale the weight of positive examples by this factor"); - } -}; + template class RegLossObj : public FitIntercept { diff --git a/src/objective/regression_param.h b/src/objective/regression_param.h new file mode 100644 index 000000000..8f5cd7112 --- /dev/null +++ b/src/objective/regression_param.h @@ -0,0 +1,25 @@ +/*! + * Copyright 2015-2023 by Contributors + * \file multiclass_param.h + * \brief Definition of single-value regression and classification parameters. + */ +#ifndef XGBOOST_OBJECTIVE_REGRESSION_PARAM_H_ +#define XGBOOST_OBJECTIVE_REGRESSION_PARAM_H_ + +#include "xgboost/parameter.h" + +namespace xgboost { +namespace obj { + +struct RegLossParam : public XGBoostParameter { + float scale_pos_weight; + // declare parameters + DMLC_DECLARE_PARAMETER(RegLossParam) { + DMLC_DECLARE_FIELD(scale_pos_weight).set_default(1.0f).set_lower_bound(0.0f) + .describe("Scale the weight of positive examples by this factor"); + } +}; + +} // namespace obj +} // namespace xgboost +#endif // XGBOOST_OBJECTIVE_REGRESSION_PARAM_H_ diff --git a/tests/cpp/objective/test_multiclass_obj.cc b/tests/cpp/objective/test_multiclass_obj.cc index d028ef9cf..734e097b8 100644 --- a/tests/cpp/objective/test_multiclass_obj.cc +++ b/tests/cpp/objective/test_multiclass_obj.cc @@ -1,18 +1,18 @@ /*! - * Copyright 2018-2019 XGBoost contributors + * Copyright 2018-2023 XGBoost contributors */ #include #include #include "../../src/common/common.h" #include "../helpers.h" +#include "test_multiclass_obj.h" namespace xgboost { -TEST(Objective, DeclareUnifiedTest(SoftmaxMultiClassObjGPair)) { - Context ctx = MakeCUDACtx(GPUIDX); +void TestSoftmaxMultiClassObjGPair(const Context* ctx) { std::vector> args {{"num_class", "3"}}; std::unique_ptr obj { - ObjFunction::Create("multi:softmax", &ctx) + ObjFunction::Create("multi:softmax", ctx) }; obj->Configure(args); @@ -35,12 +35,11 @@ TEST(Objective, DeclareUnifiedTest(SoftmaxMultiClassObjGPair)) { ASSERT_NO_THROW(obj->DefaultEvalMetric()); } -TEST(Objective, DeclareUnifiedTest(SoftmaxMultiClassBasic)) { - auto ctx = MakeCUDACtx(GPUIDX); +void TestSoftmaxMultiClassBasic(const Context* ctx) { std::vector> args{ std::pair("num_class", "3")}; - std::unique_ptr obj{ObjFunction::Create("multi:softmax", &ctx)}; + std::unique_ptr obj{ObjFunction::Create("multi:softmax", ctx)}; obj->Configure(args); CheckConfigReload(obj, "multi:softmax"); @@ -56,13 +55,12 @@ TEST(Objective, DeclareUnifiedTest(SoftmaxMultiClassBasic)) { } } -TEST(Objective, DeclareUnifiedTest(SoftprobMultiClassBasic)) { - Context ctx = MakeCUDACtx(GPUIDX); +void TestSoftprobMultiClassBasic(const Context* ctx) { std::vector> args { std::pair("num_class", "3")}; std::unique_ptr obj { - ObjFunction::Create("multi:softprob", &ctx) + ObjFunction::Create("multi:softprob", ctx) }; obj->Configure(args); CheckConfigReload(obj, "multi:softprob"); @@ -77,4 +75,5 @@ TEST(Objective, DeclareUnifiedTest(SoftprobMultiClassBasic)) { EXPECT_NEAR(preds[i], out_preds[i], 0.01f); } } + } // namespace xgboost diff --git a/tests/cpp/objective/test_multiclass_obj.h b/tests/cpp/objective/test_multiclass_obj.h new file mode 100644 index 000000000..bf6f9258c --- /dev/null +++ b/tests/cpp/objective/test_multiclass_obj.h @@ -0,0 +1,19 @@ +/** + * Copyright 2020-2023 by XGBoost Contributors + */ +#ifndef XGBOOST_TEST_MULTICLASS_OBJ_H_ +#define XGBOOST_TEST_MULTICLASS_OBJ_H_ + +#include // for Context + +namespace xgboost { + +void TestSoftmaxMultiClassObjGPair(const Context* ctx); + +void TestSoftmaxMultiClassBasic(const Context* ctx); + +void TestSoftprobMultiClassBasic(const Context* ctx); + +} // namespace xgboost + +#endif // XGBOOST_TEST_MULTICLASS_OBJ_H_ diff --git a/tests/cpp/objective/test_multiclass_obj_cpu.cc b/tests/cpp/objective/test_multiclass_obj_cpu.cc new file mode 100644 index 000000000..d3cb8aa1f --- /dev/null +++ b/tests/cpp/objective/test_multiclass_obj_cpu.cc @@ -0,0 +1,25 @@ +/*! + * Copyright 2018-2023 XGBoost contributors + */ +#include +#include + +#include "../helpers.h" +#include "test_multiclass_obj.h" + +namespace xgboost { +TEST(Objective, DeclareUnifiedTest(SoftmaxMultiClassObjGPair)) { + Context ctx = MakeCUDACtx(GPUIDX); + TestSoftmaxMultiClassObjGPair(&ctx); +} + +TEST(Objective, DeclareUnifiedTest(SoftmaxMultiClassBasic)) { + auto ctx = MakeCUDACtx(GPUIDX); + TestSoftmaxMultiClassBasic(&ctx); +} + +TEST(Objective, DeclareUnifiedTest(SoftprobMultiClassBasic)) { + Context ctx = MakeCUDACtx(GPUIDX); + TestSoftprobMultiClassBasic(&ctx); +} +} // namespace xgboost diff --git a/tests/cpp/objective/test_multiclass_obj_gpu.cu b/tests/cpp/objective/test_multiclass_obj_gpu.cu index 7567d3242..f80f07ce8 100644 --- a/tests/cpp/objective/test_multiclass_obj_gpu.cu +++ b/tests/cpp/objective/test_multiclass_obj_gpu.cu @@ -1 +1 @@ -#include "test_multiclass_obj.cc" +#include "test_multiclass_obj_cpu.cc" diff --git a/tests/cpp/objective/test_regression_obj.cc b/tests/cpp/objective/test_regression_obj.cc index 35e8287b6..4bd693936 100644 --- a/tests/cpp/objective/test_regression_obj.cc +++ b/tests/cpp/objective/test_regression_obj.cc @@ -14,13 +14,15 @@ #include "xgboost/data.h" #include "xgboost/linalg.h" +#include "test_regression_obj.h" + namespace xgboost { -TEST(Objective, DeclareUnifiedTest(LinearRegressionGPair)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; +void TestLinearRegressionGPair(const Context* ctx) { + std::string obj_name = "reg:squarederror"; - std::unique_ptr obj{ObjFunction::Create("reg:squarederror", &ctx)}; + std::vector> args; + std::unique_ptr obj{ObjFunction::Create(obj_name, ctx)}; obj->Configure(args); CheckObjFunction(obj, @@ -38,13 +40,13 @@ TEST(Objective, DeclareUnifiedTest(LinearRegressionGPair)) { ASSERT_NO_THROW(obj->DefaultEvalMetric()); } -TEST(Objective, DeclareUnifiedTest(SquaredLog)) { - Context ctx = MakeCUDACtx(GPUIDX); +void TestSquaredLog(const Context* ctx) { + std::string obj_name = "reg:squaredlogerror"; std::vector> args; - std::unique_ptr obj{ObjFunction::Create("reg:squaredlogerror", &ctx)}; + std::unique_ptr obj{ObjFunction::Create(obj_name, ctx)}; obj->Configure(args); - CheckConfigReload(obj, "reg:squaredlogerror"); + CheckConfigReload(obj, obj_name); CheckObjFunction(obj, {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred @@ -61,42 +63,13 @@ TEST(Objective, DeclareUnifiedTest(SquaredLog)) { ASSERT_EQ(obj->DefaultEvalMetric(), std::string{"rmsle"}); } -TEST(Objective, DeclareUnifiedTest(PseudoHuber)) { - Context ctx = MakeCUDACtx(GPUIDX); - Args args; - - std::unique_ptr obj{ObjFunction::Create("reg:pseudohubererror", &ctx)}; - obj->Configure(args); - CheckConfigReload(obj, "reg:pseudohubererror"); - - CheckObjFunction(obj, {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred - {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // labels - {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // weights - {-0.668965f, -0.624695f, -0.514496f, -0.196116f, 0.514496f}, // out_grad - {0.410660f, 0.476140f, 0.630510f, 0.9428660f, 0.630510f}); // out_hess - CheckObjFunction(obj, {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred - {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // labels - {}, // empty weights - {-0.668965f, -0.624695f, -0.514496f, -0.196116f, 0.514496f}, // out_grad - {0.410660f, 0.476140f, 0.630510f, 0.9428660f, 0.630510f}); // out_hess - ASSERT_EQ(obj->DefaultEvalMetric(), std::string{"mphe"}); - - obj->Configure({{"huber_slope", "0.1"}}); - CheckConfigReload(obj, "reg:pseudohubererror"); - CheckObjFunction(obj, {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred - {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // labels - {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // weights - {-0.099388f, -0.099228f, -0.098639f, -0.089443f, 0.098639f}, // out_grad - {0.0013467f, 0.001908f, 0.004443f, 0.089443f, 0.004443f}); // out_hess -} - -TEST(Objective, DeclareUnifiedTest(LogisticRegressionGPair)) { - Context ctx = MakeCUDACtx(GPUIDX); +void TestLogisticRegressionGPair(const Context* ctx) { + std::string obj_name = "reg:logistic"; std::vector> args; - std::unique_ptr obj{ObjFunction::Create("reg:logistic", &ctx)}; + std::unique_ptr obj{ObjFunction::Create(obj_name, ctx)}; obj->Configure(args); - CheckConfigReload(obj, "reg:logistic"); + CheckConfigReload(obj, obj_name); CheckObjFunction(obj, { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, // preds @@ -106,13 +79,13 @@ TEST(Objective, DeclareUnifiedTest(LogisticRegressionGPair)) { {0.25f, 0.24f, 0.20f, 0.19f, 0.25f, 0.24f, 0.20f, 0.19f}); // out_hess } -TEST(Objective, DeclareUnifiedTest(LogisticRegressionBasic)) { - Context ctx = MakeCUDACtx(GPUIDX); +void TestLogisticRegressionBasic(const Context* ctx) { + std::string obj_name = "reg:logistic"; std::vector> args; - std::unique_ptr obj{ObjFunction::Create("reg:logistic", &ctx)}; + std::unique_ptr obj{ObjFunction::Create(obj_name, ctx)}; obj->Configure(args); - CheckConfigReload(obj, "reg:logistic"); + CheckConfigReload(obj, obj_name); // test label validation EXPECT_ANY_THROW(CheckObjFunction(obj, {0}, {10}, {1}, {0}, {0})) @@ -135,12 +108,10 @@ TEST(Objective, DeclareUnifiedTest(LogisticRegressionBasic)) { } } -TEST(Objective, DeclareUnifiedTest(LogisticRawGPair)) { - Context ctx = MakeCUDACtx(GPUIDX); +void TestsLogisticRawGPair(const Context* ctx) { + std::string obj_name = "binary:logitraw"; std::vector> args; - std::unique_ptr obj { - ObjFunction::Create("binary:logitraw", &ctx) - }; + std::unique_ptr obj {ObjFunction::Create(obj_name, ctx)}; obj->Configure(args); CheckObjFunction(obj, @@ -151,347 +122,4 @@ TEST(Objective, DeclareUnifiedTest(LogisticRawGPair)) { {0.25f, 0.24f, 0.20f, 0.19f, 0.25f, 0.24f, 0.20f, 0.19f}); } -TEST(Objective, DeclareUnifiedTest(PoissonRegressionGPair)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; - std::unique_ptr obj { - ObjFunction::Create("count:poisson", &ctx) - }; - - args.emplace_back("max_delta_step", "0.1f"); - obj->Configure(args); - - CheckObjFunction(obj, - { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - { 0, 0, 0, 0, 1, 1, 1, 1}, - { 1, 1, 1, 1, 1, 1, 1, 1}, - { 1, 1.10f, 2.45f, 2.71f, 0, 0.10f, 1.45f, 1.71f}, - {1.10f, 1.22f, 2.71f, 3.00f, 1.10f, 1.22f, 2.71f, 3.00f}); - CheckObjFunction(obj, - { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - { 0, 0, 0, 0, 1, 1, 1, 1}, - {}, // Empty weight - { 1, 1.10f, 2.45f, 2.71f, 0, 0.10f, 1.45f, 1.71f}, - {1.10f, 1.22f, 2.71f, 3.00f, 1.10f, 1.22f, 2.71f, 3.00f}); -} - -TEST(Objective, DeclareUnifiedTest(PoissonRegressionBasic)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; - std::unique_ptr obj { - ObjFunction::Create("count:poisson", &ctx) - }; - - obj->Configure(args); - CheckConfigReload(obj, "count:poisson"); - - // test label validation - EXPECT_ANY_THROW(CheckObjFunction(obj, {0}, {-1}, {1}, {0}, {0})) - << "Expected error when label < 0 for PoissonRegression"; - - // test ProbToMargin - EXPECT_NEAR(obj->ProbToMargin(0.1f), -2.30f, 0.01f); - EXPECT_NEAR(obj->ProbToMargin(0.5f), -0.69f, 0.01f); - EXPECT_NEAR(obj->ProbToMargin(0.9f), -0.10f, 0.01f); - - // test PredTransform - HostDeviceVector io_preds = {0, 0.1f, 0.5f, 0.9f, 1}; - std::vector out_preds = {1, 1.10f, 1.64f, 2.45f, 2.71f}; - obj->PredTransform(&io_preds); - auto& preds = io_preds.HostVector(); - for (int i = 0; i < static_cast(io_preds.Size()); ++i) { - EXPECT_NEAR(preds[i], out_preds[i], 0.01f); - } -} - -TEST(Objective, DeclareUnifiedTest(GammaRegressionGPair)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; - std::unique_ptr obj { - ObjFunction::Create("reg:gamma", &ctx) - }; - - obj->Configure(args); - CheckObjFunction(obj, - {0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - {2, 2, 2, 2, 1, 1, 1, 1}, - {1, 1, 1, 1, 1, 1, 1, 1}, - {-1, -0.809, 0.187, 0.264, 0, 0.09f, 0.59f, 0.63f}, - {2, 1.809, 0.813, 0.735, 1, 0.90f, 0.40f, 0.36f}); - CheckObjFunction(obj, - {0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - {2, 2, 2, 2, 1, 1, 1, 1}, - {}, // Empty weight - {-1, -0.809, 0.187, 0.264, 0, 0.09f, 0.59f, 0.63f}, - {2, 1.809, 0.813, 0.735, 1, 0.90f, 0.40f, 0.36f}); -} - -TEST(Objective, DeclareUnifiedTest(GammaRegressionBasic)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; - std::unique_ptr obj{ObjFunction::Create("reg:gamma", &ctx)}; - - obj->Configure(args); - CheckConfigReload(obj, "reg:gamma"); - - // test label validation - EXPECT_ANY_THROW(CheckObjFunction(obj, {0}, {0}, {1}, {0}, {0})) - << "Expected error when label = 0 for GammaRegression"; - EXPECT_ANY_THROW(CheckObjFunction(obj, {-1}, {-1}, {1}, {-1}, {-3})) - << "Expected error when label < 0 for GammaRegression"; - - // test ProbToMargin - EXPECT_NEAR(obj->ProbToMargin(0.1f), -2.30f, 0.01f); - EXPECT_NEAR(obj->ProbToMargin(0.5f), -0.69f, 0.01f); - EXPECT_NEAR(obj->ProbToMargin(0.9f), -0.10f, 0.01f); - - // test PredTransform - HostDeviceVector io_preds = {0, 0.1f, 0.5f, 0.9f, 1}; - std::vector out_preds = {1, 1.10f, 1.64f, 2.45f, 2.71f}; - obj->PredTransform(&io_preds); - auto& preds = io_preds.HostVector(); - for (int i = 0; i < static_cast(io_preds.Size()); ++i) { - EXPECT_NEAR(preds[i], out_preds[i], 0.01f); - } -} - -TEST(Objective, DeclareUnifiedTest(TweedieRegressionGPair)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; - std::unique_ptr obj{ObjFunction::Create("reg:tweedie", &ctx)}; - - args.emplace_back("tweedie_variance_power", "1.1f"); - obj->Configure(args); - - CheckObjFunction(obj, - { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - { 0, 0, 0, 0, 1, 1, 1, 1}, - { 1, 1, 1, 1, 1, 1, 1, 1}, - { 1, 1.09f, 2.24f, 2.45f, 0, 0.10f, 1.33f, 1.55f}, - {0.89f, 0.98f, 2.02f, 2.21f, 1, 1.08f, 2.11f, 2.30f}); - CheckObjFunction(obj, - { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - { 0, 0, 0, 0, 1, 1, 1, 1}, - {}, // Empty weight. - { 1, 1.09f, 2.24f, 2.45f, 0, 0.10f, 1.33f, 1.55f}, - {0.89f, 0.98f, 2.02f, 2.21f, 1, 1.08f, 2.11f, 2.30f}); - ASSERT_EQ(obj->DefaultEvalMetric(), std::string{"tweedie-nloglik@1.1"}); -} - -#if defined(__CUDACC__) -TEST(Objective, CPU_vs_CUDA) { - Context ctx = MakeCUDACtx(GPUIDX); - - std::unique_ptr obj{ObjFunction::Create("reg:squarederror", &ctx)}; - linalg::Matrix cpu_out_preds; - linalg::Matrix cuda_out_preds; - - constexpr size_t kRows = 400; - constexpr size_t kCols = 100; - auto pdmat = RandomDataGenerator(kRows, kCols, 0).Seed(0).GenerateDMatrix(); - HostDeviceVector preds; - preds.Resize(kRows); - auto& h_preds = preds.HostVector(); - for (size_t i = 0; i < h_preds.size(); ++i) { - h_preds[i] = static_cast(i); - } - auto& info = pdmat->Info(); - - info.labels.Reshape(kRows); - auto& h_labels = info.labels.Data()->HostVector(); - for (size_t i = 0; i < h_labels.size(); ++i) { - h_labels[i] = 1 / static_cast(i+1); - } - - { - // CPU - ctx = ctx.MakeCPU(); - obj->GetGradient(preds, info, 0, &cpu_out_preds); - } - { - // CUDA - ctx = ctx.MakeCUDA(0); - obj->GetGradient(preds, info, 0, &cuda_out_preds); - } - - auto h_cpu_out = cpu_out_preds.HostView(); - auto h_cuda_out = cuda_out_preds.HostView(); - - float sgrad = 0; - float shess = 0; - for (size_t i = 0; i < kRows; ++i) { - sgrad += std::pow(h_cpu_out(i).GetGrad() - h_cuda_out(i).GetGrad(), 2); - shess += std::pow(h_cpu_out(i).GetHess() - h_cuda_out(i).GetHess(), 2); - } - ASSERT_NEAR(sgrad, 0.0f, kRtEps); - ASSERT_NEAR(shess, 0.0f, kRtEps); -} -#endif - -TEST(Objective, DeclareUnifiedTest(TweedieRegressionBasic)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; - std::unique_ptr obj{ObjFunction::Create("reg:tweedie", &ctx)}; - - obj->Configure(args); - CheckConfigReload(obj, "reg:tweedie"); - - // test label validation - EXPECT_ANY_THROW(CheckObjFunction(obj, {0}, {-1}, {1}, {0}, {0})) - << "Expected error when label < 0 for TweedieRegression"; - - // test ProbToMargin - EXPECT_NEAR(obj->ProbToMargin(0.1f), -2.30f, 0.01f); - EXPECT_NEAR(obj->ProbToMargin(0.5f), -0.69f, 0.01f); - EXPECT_NEAR(obj->ProbToMargin(0.9f), -0.10f, 0.01f); - - // test PredTransform - HostDeviceVector io_preds = {0, 0.1f, 0.5f, 0.9f, 1}; - std::vector out_preds = {1, 1.10f, 1.64f, 2.45f, 2.71f}; - obj->PredTransform(&io_preds); - auto& preds = io_preds.HostVector(); - for (int i = 0; i < static_cast(io_preds.Size()); ++i) { - EXPECT_NEAR(preds[i], out_preds[i], 0.01f); - } -} - -// CoxRegression not implemented in GPU code, no need for testing. -#if !defined(__CUDACC__) -TEST(Objective, CoxRegressionGPair) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; - std::unique_ptr obj{ObjFunction::Create("survival:cox", &ctx)}; - - obj->Configure(args); - CheckObjFunction(obj, - { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - { 0, -2, -2, 2, 3, 5, -10, 100}, - { 1, 1, 1, 1, 1, 1, 1, 1}, - { 0, 0, 0, -0.799f, -0.788f, -0.590f, 0.910f, 1.006f}, - { 0, 0, 0, 0.160f, 0.186f, 0.348f, 0.610f, 0.639f}); -} -#endif - -TEST(Objective, DeclareUnifiedTest(AbsoluteError)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::unique_ptr obj{ObjFunction::Create("reg:absoluteerror", &ctx)}; - obj->Configure({}); - CheckConfigReload(obj, "reg:absoluteerror"); - - MetaInfo info; - std::vector labels{0.f, 3.f, 2.f, 5.f, 4.f, 7.f}; - info.labels.Reshape(6, 1); - info.labels.Data()->HostVector() = labels; - info.num_row_ = labels.size(); - HostDeviceVector predt{1.f, 2.f, 3.f, 4.f, 5.f, 6.f}; - info.weights_.HostVector() = {1.f, 1.f, 1.f, 1.f, 1.f, 1.f}; - - CheckObjFunction(obj, predt.HostVector(), labels, info.weights_.HostVector(), - {1.f, -1.f, 1.f, -1.f, 1.f, -1.f}, info.weights_.HostVector()); - - RegTree tree; - tree.ExpandNode(0, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); - - HostDeviceVector position(labels.size(), 0); - auto& h_position = position.HostVector(); - for (size_t i = 0; i < labels.size(); ++i) { - if (i < labels.size() / 2) { - h_position[i] = 1; // left - } else { - h_position[i] = 2; // right - } - } - - auto& h_predt = predt.HostVector(); - for (size_t i = 0; i < h_predt.size(); ++i) { - h_predt[i] = labels[i] + i; - } - - tree::TrainParam param; - param.Init(Args{}); - auto lr = param.learning_rate; - - obj->UpdateTreeLeaf(position, info, param.learning_rate, predt, 0, &tree); - ASSERT_EQ(tree[1].LeafValue(), -1.0f * lr); - ASSERT_EQ(tree[2].LeafValue(), -4.0f * lr); -} - -TEST(Objective, DeclareUnifiedTest(AbsoluteErrorLeaf)) { - Context ctx = MakeCUDACtx(GPUIDX); - bst_target_t constexpr kTargets = 3, kRows = 16; - std::unique_ptr obj{ObjFunction::Create("reg:absoluteerror", &ctx)}; - obj->Configure({}); - - MetaInfo info; - info.num_row_ = kRows; - info.labels.Reshape(16, kTargets); - HostDeviceVector predt(info.labels.Size()); - - for (bst_target_t t{0}; t < kTargets; ++t) { - auto h_labels = info.labels.HostView().Slice(linalg::All(), t); - std::iota(linalg::begin(h_labels), linalg::end(h_labels), 0); - - auto h_predt = - linalg::MakeTensorView(&ctx, predt.HostSpan(), kRows, kTargets).Slice(linalg::All(), t); - for (size_t i = 0; i < h_predt.Size(); ++i) { - h_predt(i) = h_labels(i) + i; - } - - HostDeviceVector position(h_labels.Size(), 0); - auto& h_position = position.HostVector(); - for (int32_t i = 0; i < 3; ++i) { - h_position[i] = ~i; // negation for sampled nodes. - } - for (size_t i = 3; i < 8; ++i) { - h_position[i] = 3; - } - // empty leaf for node 4 - for (size_t i = 8; i < 13; ++i) { - h_position[i] = 5; - } - for (size_t i = 13; i < h_labels.Size(); ++i) { - h_position[i] = 6; - } - - RegTree tree; - tree.ExpandNode(0, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); - tree.ExpandNode(1, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); - tree.ExpandNode(2, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); - ASSERT_EQ(tree.GetNumLeaves(), 4); - - auto empty_leaf = tree[4].LeafValue(); - - tree::TrainParam param; - param.Init(Args{}); - auto lr = param.learning_rate; - - obj->UpdateTreeLeaf(position, info, lr, predt, t, &tree); - ASSERT_EQ(tree[3].LeafValue(), -5.0f * lr); - ASSERT_EQ(tree[4].LeafValue(), empty_leaf * lr); - ASSERT_EQ(tree[5].LeafValue(), -10.0f * lr); - ASSERT_EQ(tree[6].LeafValue(), -14.0f * lr); - } -} - -TEST(Adaptive, DeclareUnifiedTest(MissingLeaf)) { - std::vector missing{1, 3}; - - std::vector h_nidx = {2, 4, 5}; - std::vector h_nptr = {0, 4, 8, 16}; - - obj::detail::FillMissingLeaf(missing, &h_nidx, &h_nptr); - - ASSERT_EQ(h_nidx[0], missing[0]); - ASSERT_EQ(h_nidx[2], missing[1]); - ASSERT_EQ(h_nidx[1], 2); - ASSERT_EQ(h_nidx[3], 4); - ASSERT_EQ(h_nidx[4], 5); - - ASSERT_EQ(h_nptr[0], 0); - ASSERT_EQ(h_nptr[1], 0); // empty - ASSERT_EQ(h_nptr[2], 4); - ASSERT_EQ(h_nptr[3], 4); // empty - ASSERT_EQ(h_nptr[4], 8); - ASSERT_EQ(h_nptr[5], 16); -} } // namespace xgboost diff --git a/tests/cpp/objective/test_regression_obj.h b/tests/cpp/objective/test_regression_obj.h new file mode 100644 index 000000000..41f7c370e --- /dev/null +++ b/tests/cpp/objective/test_regression_obj.h @@ -0,0 +1,23 @@ +/** + * Copyright 2020-2023 by XGBoost Contributors + */ +#ifndef XGBOOST_TEST_REGRESSION_OBJ_H_ +#define XGBOOST_TEST_REGRESSION_OBJ_H_ + +#include // for Context + +namespace xgboost { + +void TestLinearRegressionGPair(const Context* ctx); + +void TestSquaredLog(const Context* ctx); + +void TestLogisticRegressionGPair(const Context* ctx); + +void TestLogisticRegressionBasic(const Context* ctx); + +void TestsLogisticRawGPair(const Context* ctx); + +} // namespace xgboost + +#endif // XGBOOST_TEST_REGRESSION_OBJ_H_ diff --git a/tests/cpp/objective/test_regression_obj_cpu.cc b/tests/cpp/objective/test_regression_obj_cpu.cc new file mode 100644 index 000000000..3613d0d90 --- /dev/null +++ b/tests/cpp/objective/test_regression_obj_cpu.cc @@ -0,0 +1,412 @@ +/*! + * Copyright 2018-2023 XGBoost contributors + */ +#include +#include +#include + +#include "../../../src/objective/adaptive.h" +#include "../../../src/tree/param.h" // for TrainParam +#include "../helpers.h" + +#include "test_regression_obj.h" + +namespace xgboost { +TEST(Objective, DeclareUnifiedTest(LinearRegressionGPair)) { + Context ctx = MakeCUDACtx(GPUIDX); + TestLinearRegressionGPair(&ctx); +} + +TEST(Objective, DeclareUnifiedTest(SquaredLog)) { + Context ctx = MakeCUDACtx(GPUIDX); + TestSquaredLog(&ctx); +} + +TEST(Objective, DeclareUnifiedTest(PseudoHuber)) { + Context ctx = MakeCUDACtx(GPUIDX); + Args args; + + std::unique_ptr obj{ObjFunction::Create("reg:pseudohubererror", &ctx)}; + obj->Configure(args); + CheckConfigReload(obj, "reg:pseudohubererror"); + + CheckObjFunction(obj, {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred + {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // labels + {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // weights + {-0.668965f, -0.624695f, -0.514496f, -0.196116f, 0.514496f}, // out_grad + {0.410660f, 0.476140f, 0.630510f, 0.9428660f, 0.630510f}); // out_hess + CheckObjFunction(obj, {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred + {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // labels + {}, // empty weights + {-0.668965f, -0.624695f, -0.514496f, -0.196116f, 0.514496f}, // out_grad + {0.410660f, 0.476140f, 0.630510f, 0.9428660f, 0.630510f}); // out_hess + ASSERT_EQ(obj->DefaultEvalMetric(), std::string{"mphe"}); + + obj->Configure({{"huber_slope", "0.1"}}); + CheckConfigReload(obj, "reg:pseudohubererror"); + CheckObjFunction(obj, {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred + {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // labels + {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // weights + {-0.099388f, -0.099228f, -0.098639f, -0.089443f, 0.098639f}, // out_grad + {0.0013467f, 0.001908f, 0.004443f, 0.089443f, 0.004443f}); // out_hess +} + +TEST(Objective, DeclareUnifiedTest(LogisticRegressionGPair)) { + Context ctx = MakeCUDACtx(GPUIDX); + TestLogisticRegressionGPair(&ctx); +} + +TEST(Objective, DeclareUnifiedTest(LogisticRegressionBasic)) { + Context ctx = MakeCUDACtx(GPUIDX); + TestLogisticRegressionBasic(&ctx); +} + +TEST(Objective, DeclareUnifiedTest(LogisticRawGPair)) { + Context ctx = MakeCUDACtx(GPUIDX); + TestsLogisticRawGPair(&ctx); +} + +TEST(Objective, DeclareUnifiedTest(PoissonRegressionGPair)) { + Context ctx = MakeCUDACtx(GPUIDX); + std::vector> args; + std::unique_ptr obj { + ObjFunction::Create("count:poisson", &ctx) + }; + + args.emplace_back("max_delta_step", "0.1f"); + obj->Configure(args); + + CheckObjFunction(obj, + { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, + { 0, 0, 0, 0, 1, 1, 1, 1}, + { 1, 1, 1, 1, 1, 1, 1, 1}, + { 1, 1.10f, 2.45f, 2.71f, 0, 0.10f, 1.45f, 1.71f}, + {1.10f, 1.22f, 2.71f, 3.00f, 1.10f, 1.22f, 2.71f, 3.00f}); + CheckObjFunction(obj, + { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, + { 0, 0, 0, 0, 1, 1, 1, 1}, + {}, // Empty weight + { 1, 1.10f, 2.45f, 2.71f, 0, 0.10f, 1.45f, 1.71f}, + {1.10f, 1.22f, 2.71f, 3.00f, 1.10f, 1.22f, 2.71f, 3.00f}); +} + +TEST(Objective, DeclareUnifiedTest(PoissonRegressionBasic)) { + Context ctx = MakeCUDACtx(GPUIDX); + std::vector> args; + std::unique_ptr obj { + ObjFunction::Create("count:poisson", &ctx) + }; + + obj->Configure(args); + CheckConfigReload(obj, "count:poisson"); + + // test label validation + EXPECT_ANY_THROW(CheckObjFunction(obj, {0}, {-1}, {1}, {0}, {0})) + << "Expected error when label < 0 for PoissonRegression"; + + // test ProbToMargin + EXPECT_NEAR(obj->ProbToMargin(0.1f), -2.30f, 0.01f); + EXPECT_NEAR(obj->ProbToMargin(0.5f), -0.69f, 0.01f); + EXPECT_NEAR(obj->ProbToMargin(0.9f), -0.10f, 0.01f); + + // test PredTransform + HostDeviceVector io_preds = {0, 0.1f, 0.5f, 0.9f, 1}; + std::vector out_preds = {1, 1.10f, 1.64f, 2.45f, 2.71f}; + obj->PredTransform(&io_preds); + auto& preds = io_preds.HostVector(); + for (int i = 0; i < static_cast(io_preds.Size()); ++i) { + EXPECT_NEAR(preds[i], out_preds[i], 0.01f); + } +} + +TEST(Objective, DeclareUnifiedTest(GammaRegressionGPair)) { + Context ctx = MakeCUDACtx(GPUIDX); + std::vector> args; + std::unique_ptr obj { + ObjFunction::Create("reg:gamma", &ctx) + }; + + obj->Configure(args); + CheckObjFunction(obj, + {0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, + {2, 2, 2, 2, 1, 1, 1, 1}, + {1, 1, 1, 1, 1, 1, 1, 1}, + {-1, -0.809, 0.187, 0.264, 0, 0.09f, 0.59f, 0.63f}, + {2, 1.809, 0.813, 0.735, 1, 0.90f, 0.40f, 0.36f}); + CheckObjFunction(obj, + {0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, + {2, 2, 2, 2, 1, 1, 1, 1}, + {}, // Empty weight + {-1, -0.809, 0.187, 0.264, 0, 0.09f, 0.59f, 0.63f}, + {2, 1.809, 0.813, 0.735, 1, 0.90f, 0.40f, 0.36f}); +} + +TEST(Objective, DeclareUnifiedTest(GammaRegressionBasic)) { + Context ctx = MakeCUDACtx(GPUIDX); + std::vector> args; + std::unique_ptr obj{ObjFunction::Create("reg:gamma", &ctx)}; + + obj->Configure(args); + CheckConfigReload(obj, "reg:gamma"); + + // test label validation + EXPECT_ANY_THROW(CheckObjFunction(obj, {0}, {0}, {1}, {0}, {0})) + << "Expected error when label = 0 for GammaRegression"; + EXPECT_ANY_THROW(CheckObjFunction(obj, {-1}, {-1}, {1}, {-1}, {-3})) + << "Expected error when label < 0 for GammaRegression"; + + // test ProbToMargin + EXPECT_NEAR(obj->ProbToMargin(0.1f), -2.30f, 0.01f); + EXPECT_NEAR(obj->ProbToMargin(0.5f), -0.69f, 0.01f); + EXPECT_NEAR(obj->ProbToMargin(0.9f), -0.10f, 0.01f); + + // test PredTransform + HostDeviceVector io_preds = {0, 0.1f, 0.5f, 0.9f, 1}; + std::vector out_preds = {1, 1.10f, 1.64f, 2.45f, 2.71f}; + obj->PredTransform(&io_preds); + auto& preds = io_preds.HostVector(); + for (int i = 0; i < static_cast(io_preds.Size()); ++i) { + EXPECT_NEAR(preds[i], out_preds[i], 0.01f); + } +} + +TEST(Objective, DeclareUnifiedTest(TweedieRegressionGPair)) { + Context ctx = MakeCUDACtx(GPUIDX); + std::vector> args; + std::unique_ptr obj{ObjFunction::Create("reg:tweedie", &ctx)}; + + args.emplace_back("tweedie_variance_power", "1.1f"); + obj->Configure(args); + + CheckObjFunction(obj, + { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, + { 0, 0, 0, 0, 1, 1, 1, 1}, + { 1, 1, 1, 1, 1, 1, 1, 1}, + { 1, 1.09f, 2.24f, 2.45f, 0, 0.10f, 1.33f, 1.55f}, + {0.89f, 0.98f, 2.02f, 2.21f, 1, 1.08f, 2.11f, 2.30f}); + CheckObjFunction(obj, + { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, + { 0, 0, 0, 0, 1, 1, 1, 1}, + {}, // Empty weight. + { 1, 1.09f, 2.24f, 2.45f, 0, 0.10f, 1.33f, 1.55f}, + {0.89f, 0.98f, 2.02f, 2.21f, 1, 1.08f, 2.11f, 2.30f}); + ASSERT_EQ(obj->DefaultEvalMetric(), std::string{"tweedie-nloglik@1.1"}); +} + +#if defined(__CUDACC__) +TEST(Objective, CPU_vs_CUDA) { + Context ctx = MakeCUDACtx(GPUIDX); + + std::unique_ptr obj{ObjFunction::Create("reg:squarederror", &ctx)}; + linalg::Matrix cpu_out_preds; + linalg::Matrix cuda_out_preds; + + constexpr size_t kRows = 400; + constexpr size_t kCols = 100; + auto pdmat = RandomDataGenerator(kRows, kCols, 0).Seed(0).GenerateDMatrix(); + HostDeviceVector preds; + preds.Resize(kRows); + auto& h_preds = preds.HostVector(); + for (size_t i = 0; i < h_preds.size(); ++i) { + h_preds[i] = static_cast(i); + } + auto& info = pdmat->Info(); + + info.labels.Reshape(kRows); + auto& h_labels = info.labels.Data()->HostVector(); + for (size_t i = 0; i < h_labels.size(); ++i) { + h_labels[i] = 1 / static_cast(i+1); + } + + { + // CPU + ctx = ctx.MakeCPU(); + obj->GetGradient(preds, info, 0, &cpu_out_preds); + } + { + // CUDA + ctx = ctx.MakeCUDA(0); + obj->GetGradient(preds, info, 0, &cuda_out_preds); + } + + auto h_cpu_out = cpu_out_preds.HostView(); + auto h_cuda_out = cuda_out_preds.HostView(); + + float sgrad = 0; + float shess = 0; + for (size_t i = 0; i < kRows; ++i) { + sgrad += std::pow(h_cpu_out(i).GetGrad() - h_cuda_out(i).GetGrad(), 2); + shess += std::pow(h_cpu_out(i).GetHess() - h_cuda_out(i).GetHess(), 2); + } + ASSERT_NEAR(sgrad, 0.0f, kRtEps); + ASSERT_NEAR(shess, 0.0f, kRtEps); +} +#endif + +TEST(Objective, DeclareUnifiedTest(TweedieRegressionBasic)) { + Context ctx = MakeCUDACtx(GPUIDX); + std::vector> args; + std::unique_ptr obj{ObjFunction::Create("reg:tweedie", &ctx)}; + + obj->Configure(args); + CheckConfigReload(obj, "reg:tweedie"); + + // test label validation + EXPECT_ANY_THROW(CheckObjFunction(obj, {0}, {-1}, {1}, {0}, {0})) + << "Expected error when label < 0 for TweedieRegression"; + + // test ProbToMargin + EXPECT_NEAR(obj->ProbToMargin(0.1f), -2.30f, 0.01f); + EXPECT_NEAR(obj->ProbToMargin(0.5f), -0.69f, 0.01f); + EXPECT_NEAR(obj->ProbToMargin(0.9f), -0.10f, 0.01f); + + // test PredTransform + HostDeviceVector io_preds = {0, 0.1f, 0.5f, 0.9f, 1}; + std::vector out_preds = {1, 1.10f, 1.64f, 2.45f, 2.71f}; + obj->PredTransform(&io_preds); + auto& preds = io_preds.HostVector(); + for (int i = 0; i < static_cast(io_preds.Size()); ++i) { + EXPECT_NEAR(preds[i], out_preds[i], 0.01f); + } +} + +// CoxRegression not implemented in GPU code, no need for testing. +#if !defined(__CUDACC__) +TEST(Objective, CoxRegressionGPair) { + Context ctx = MakeCUDACtx(GPUIDX); + std::vector> args; + std::unique_ptr obj{ObjFunction::Create("survival:cox", &ctx)}; + + obj->Configure(args); + CheckObjFunction(obj, + { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, + { 0, -2, -2, 2, 3, 5, -10, 100}, + { 1, 1, 1, 1, 1, 1, 1, 1}, + { 0, 0, 0, -0.799f, -0.788f, -0.590f, 0.910f, 1.006f}, + { 0, 0, 0, 0.160f, 0.186f, 0.348f, 0.610f, 0.639f}); +} +#endif + +TEST(Objective, DeclareUnifiedTest(AbsoluteError)) { + Context ctx = MakeCUDACtx(GPUIDX); + std::unique_ptr obj{ObjFunction::Create("reg:absoluteerror", &ctx)}; + obj->Configure({}); + CheckConfigReload(obj, "reg:absoluteerror"); + + MetaInfo info; + std::vector labels{0.f, 3.f, 2.f, 5.f, 4.f, 7.f}; + info.labels.Reshape(6, 1); + info.labels.Data()->HostVector() = labels; + info.num_row_ = labels.size(); + HostDeviceVector predt{1.f, 2.f, 3.f, 4.f, 5.f, 6.f}; + info.weights_.HostVector() = {1.f, 1.f, 1.f, 1.f, 1.f, 1.f}; + + CheckObjFunction(obj, predt.HostVector(), labels, info.weights_.HostVector(), + {1.f, -1.f, 1.f, -1.f, 1.f, -1.f}, info.weights_.HostVector()); + + RegTree tree; + tree.ExpandNode(0, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); + + HostDeviceVector position(labels.size(), 0); + auto& h_position = position.HostVector(); + for (size_t i = 0; i < labels.size(); ++i) { + if (i < labels.size() / 2) { + h_position[i] = 1; // left + } else { + h_position[i] = 2; // right + } + } + + auto& h_predt = predt.HostVector(); + for (size_t i = 0; i < h_predt.size(); ++i) { + h_predt[i] = labels[i] + i; + } + + tree::TrainParam param; + param.Init(Args{}); + auto lr = param.learning_rate; + + obj->UpdateTreeLeaf(position, info, param.learning_rate, predt, 0, &tree); + ASSERT_EQ(tree[1].LeafValue(), -1.0f * lr); + ASSERT_EQ(tree[2].LeafValue(), -4.0f * lr); +} + +TEST(Objective, DeclareUnifiedTest(AbsoluteErrorLeaf)) { + Context ctx = MakeCUDACtx(GPUIDX); + bst_target_t constexpr kTargets = 3, kRows = 16; + std::unique_ptr obj{ObjFunction::Create("reg:absoluteerror", &ctx)}; + obj->Configure({}); + + MetaInfo info; + info.num_row_ = kRows; + info.labels.Reshape(16, kTargets); + HostDeviceVector predt(info.labels.Size()); + + for (bst_target_t t{0}; t < kTargets; ++t) { + auto h_labels = info.labels.HostView().Slice(linalg::All(), t); + std::iota(linalg::begin(h_labels), linalg::end(h_labels), 0); + + auto h_predt = + linalg::MakeTensorView(&ctx, predt.HostSpan(), kRows, kTargets).Slice(linalg::All(), t); + for (size_t i = 0; i < h_predt.Size(); ++i) { + h_predt(i) = h_labels(i) + i; + } + + HostDeviceVector position(h_labels.Size(), 0); + auto& h_position = position.HostVector(); + for (int32_t i = 0; i < 3; ++i) { + h_position[i] = ~i; // negation for sampled nodes. + } + for (size_t i = 3; i < 8; ++i) { + h_position[i] = 3; + } + // empty leaf for node 4 + for (size_t i = 8; i < 13; ++i) { + h_position[i] = 5; + } + for (size_t i = 13; i < h_labels.Size(); ++i) { + h_position[i] = 6; + } + + RegTree tree; + tree.ExpandNode(0, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); + tree.ExpandNode(1, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); + tree.ExpandNode(2, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); + ASSERT_EQ(tree.GetNumLeaves(), 4); + + auto empty_leaf = tree[4].LeafValue(); + + tree::TrainParam param; + param.Init(Args{}); + auto lr = param.learning_rate; + + obj->UpdateTreeLeaf(position, info, lr, predt, t, &tree); + ASSERT_EQ(tree[3].LeafValue(), -5.0f * lr); + ASSERT_EQ(tree[4].LeafValue(), empty_leaf * lr); + ASSERT_EQ(tree[5].LeafValue(), -10.0f * lr); + ASSERT_EQ(tree[6].LeafValue(), -14.0f * lr); + } +} + +TEST(Adaptive, DeclareUnifiedTest(MissingLeaf)) { + std::vector missing{1, 3}; + + std::vector h_nidx = {2, 4, 5}; + std::vector h_nptr = {0, 4, 8, 16}; + + obj::detail::FillMissingLeaf(missing, &h_nidx, &h_nptr); + + ASSERT_EQ(h_nidx[0], missing[0]); + ASSERT_EQ(h_nidx[2], missing[1]); + ASSERT_EQ(h_nidx[1], 2); + ASSERT_EQ(h_nidx[3], 4); + ASSERT_EQ(h_nidx[4], 5); + + ASSERT_EQ(h_nptr[0], 0); + ASSERT_EQ(h_nptr[1], 0); // empty + ASSERT_EQ(h_nptr[2], 4); + ASSERT_EQ(h_nptr[3], 4); // empty + ASSERT_EQ(h_nptr[4], 8); + ASSERT_EQ(h_nptr[5], 16); +} +} // namespace xgboost diff --git a/tests/cpp/objective/test_regression_obj_gpu.cu b/tests/cpp/objective/test_regression_obj_gpu.cu index 38f29b8a8..746468f71 100644 --- a/tests/cpp/objective/test_regression_obj_gpu.cu +++ b/tests/cpp/objective/test_regression_obj_gpu.cu @@ -3,4 +3,4 @@ */ // Dummy file to keep the CUDA tests. -#include "test_regression_obj.cc" +#include "test_regression_obj_cpu.cc" diff --git a/tests/cpp/plugin/test_sycl_multiclass_obj.cc b/tests/cpp/plugin/test_sycl_multiclass_obj.cc new file mode 100644 index 000000000..d809ecad3 --- /dev/null +++ b/tests/cpp/plugin/test_sycl_multiclass_obj.cc @@ -0,0 +1,28 @@ +/*! + * Copyright 2018-2023 XGBoost contributors + */ +#include +#include + +#include "../objective/test_multiclass_obj.h" + +namespace xgboost { + +TEST(SyclObjective, SoftmaxMultiClassObjGPair) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestSoftmaxMultiClassObjGPair(&ctx); +} + +TEST(SyclObjective, SoftmaxMultiClassBasic) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestSoftmaxMultiClassObjGPair(&ctx); +} + +TEST(SyclObjective, SoftprobMultiClassBasic) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestSoftprobMultiClassBasic(&ctx); +} +} // namespace xgboost diff --git a/tests/cpp/plugin/test_sycl_regression_obj.cc b/tests/cpp/plugin/test_sycl_regression_obj.cc new file mode 100644 index 000000000..66b4ea508 --- /dev/null +++ b/tests/cpp/plugin/test_sycl_regression_obj.cc @@ -0,0 +1,99 @@ +/*! + * Copyright 2017-2019 XGBoost contributors + */ +#include +#include +#include + +#include "../helpers.h" +#include "../objective/test_regression_obj.h" + +namespace xgboost { + +TEST(SyclObjective, LinearRegressionGPair) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestLinearRegressionGPair(&ctx); +} + +TEST(SyclObjective, SquaredLog) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestSquaredLog(&ctx); +} + +TEST(SyclObjective, LogisticRegressionGPair) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestLogisticRegressionGPair(&ctx); +} + +TEST(SyclObjective, LogisticRegressionBasic) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + + TestLogisticRegressionBasic(&ctx); +} + +TEST(SyclObjective, LogisticRawGPair) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestsLogisticRawGPair(&ctx); +} + +TEST(SyclObjective, CPUvsSycl) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + ObjFunction * obj_sycl = + ObjFunction::Create("reg:squarederror_sycl", &ctx); + + ctx = ctx.MakeCPU(); + ObjFunction * obj_cpu = + ObjFunction::Create("reg:squarederror", &ctx); + + linalg::Matrix cpu_out_preds; + linalg::Matrix sycl_out_preds; + + constexpr size_t kRows = 400; + constexpr size_t kCols = 100; + auto pdmat = RandomDataGenerator(kRows, kCols, 0).Seed(0).GenerateDMatrix(); + HostDeviceVector preds; + preds.Resize(kRows); + auto& h_preds = preds.HostVector(); + for (size_t i = 0; i < h_preds.size(); ++i) { + h_preds[i] = static_cast(i); + } + auto& info = pdmat->Info(); + + info.labels.Reshape(kRows, 1); + auto& h_labels = info.labels.Data()->HostVector(); + for (size_t i = 0; i < h_labels.size(); ++i) { + h_labels[i] = 1 / static_cast(i+1); + } + + { + // CPU + obj_cpu->GetGradient(preds, info, 0, &cpu_out_preds); + } + { + // sycl + obj_sycl->GetGradient(preds, info, 0, &sycl_out_preds); + } + + auto h_cpu_out = cpu_out_preds.HostView(); + auto h_sycl_out = sycl_out_preds.HostView(); + + float sgrad = 0; + float shess = 0; + for (size_t i = 0; i < kRows; ++i) { + sgrad += std::pow(h_cpu_out(i).GetGrad() - h_sycl_out(i).GetGrad(), 2); + shess += std::pow(h_cpu_out(i).GetHess() - h_sycl_out(i).GetHess(), 2); + } + ASSERT_NEAR(sgrad, 0.0f, kRtEps); + ASSERT_NEAR(shess, 0.0f, kRtEps); + + delete obj_cpu; + delete obj_sycl; +} + +} // namespace xgboost From d530d37707743954e06f9e825e2f9084c771cb07 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 22:58:03 +0900 Subject: [PATCH 15/59] [CI] Update RAPIDS to latest stable (#9857) --- tests/buildkite/conftest.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/buildkite/conftest.sh b/tests/buildkite/conftest.sh index 881f98672..3df79b58d 100755 --- a/tests/buildkite/conftest.sh +++ b/tests/buildkite/conftest.sh @@ -24,7 +24,7 @@ set -x CUDA_VERSION=11.8.0 NCCL_VERSION=2.16.5-1 -RAPIDS_VERSION=23.10 +RAPIDS_VERSION=23.12 SPARK_VERSION=3.4.0 JDK_VERSION=8 R_VERSION=4.3.2 From 42173d7bc34e5ea791d0afaee1e08f82bcfc8b3b Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 13 Dec 2023 01:39:41 +0100 Subject: [PATCH 16/59] [doc] Clarify the effect of `enable_categorical` (#9877) --- R-package/R/xgb.DMatrix.R | 14 ++++++++++++-- R-package/man/xgb.DMatrix.Rd | 14 ++++++++++++-- python-package/xgboost/core.py | 14 ++++++++++++-- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 602164afe..fead30413 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -27,14 +27,24 @@ #' @param label_lower_bound Lower bound for survival training. #' @param label_upper_bound Upper bound for survival training. #' @param feature_weights Set feature weights for column sampling. +#' @param enable_categorical Experimental support of specializing for categorical features. +#' +#' If passing 'TRUE' and 'data' is a data frame, +#' columns of categorical types will automatically +#' be set to be of categorical type (feature_type='c') in the resulting DMatrix. +#' +#' If passing 'FALSE' and 'data' is a data frame with categorical columns, +#' it will result in an error being thrown. +#' +#' If 'data' is not a data frame, this argument is ignored. +#' +#' JSON/UBJSON serialization format is required for this. #' #' @details #' Note that DMatrix objects are not serializable through R functions such as \code{saveRDS} or \code{save}. #' If a DMatrix gets serialized and then de-serialized (for example, when saving data in an R session or caching #' chunks in an Rmd file), the resulting object will not be usable anymore and will need to be reconstructed #' from the original source of data. -#' @param enable_categorical Experimental support of specializing for -#' categorical features. JSON/UBJSON serialization format is required. #' #' @examples #' data(agaricus.train, package='xgboost') diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index 619f5d730..95cc8d3cd 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -58,8 +58,18 @@ frame and matrix.} \item{feature_weights}{Set feature weights for column sampling.} -\item{enable_categorical}{Experimental support of specializing for -categorical features. JSON/UBJSON serialization format is required.} +\item{enable_categorical}{Experimental support of specializing for categorical features. + + If passing 'TRUE' and 'data' is a data frame, + columns of categorical types will automatically + be set to be of categorical type (feature_type='c') in the resulting DMatrix. + + If passing 'FALSE' and 'data' is a data frame with categorical columns, + it will result in an error being thrown. + + If 'data' is not a data frame, this argument is ignored. + + JSON/UBJSON serialization format is required for this.} } \description{ Construct xgb.DMatrix object from either a dense matrix, a sparse matrix, or a local file. diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index 3c864a1c8..1bddadbbe 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -822,8 +822,18 @@ class DMatrix: # pylint: disable=too-many-instance-attributes,too-many-public-m .. note:: This parameter is experimental - Experimental support of specializing for categorical features. JSON/UBJSON - serialization format is required. + Experimental support of specializing for categorical features. + + If passing 'True' and 'data' is a data frame (from supported libraries + such as Pandas or Modin), columns of categorical types will automatically + be set to be of categorical type (feature_type='c') in the resulting DMatrix. + + If passing 'False' and 'data' is a data frame with categorical columns, + it will result in an error being thrown. + + If 'data' is not a data frame, this argument is ignored. + + JSON/UBJSON serialization format is required for this. """ if group is not None and qid is not None: From 936b22fdf30b628dc307add837b29b932d4122be Mon Sep 17 00:00:00 2001 From: Philip Hyunsu Cho Date: Wed, 13 Dec 2023 15:25:51 -0800 Subject: [PATCH 17/59] [CI] Upload libxgboost4j.dylib (M1) to S3 bucket (#9886) * [CI] Upload libxgboost4j.dylib (M1) to S3 bucket * Fix typo --- tests/buildkite/test-macos-m1-clang11.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/buildkite/test-macos-m1-clang11.sh b/tests/buildkite/test-macos-m1-clang11.sh index 97b4fef93..401701b42 100755 --- a/tests/buildkite/test-macos-m1-clang11.sh +++ b/tests/buildkite/test-macos-m1-clang11.sh @@ -24,6 +24,20 @@ popd rm -rf build set +x +echo "--- Upload Python wheel" +set -x +pushd lib +mv -v libxgboost4j.dylib libxgboost4j_m1_${BUILDKITE_COMMIT}.dylib +buildkite-agent artifact upload libxgboost4j_m1_${BUILDKITE_COMMIT}.dylib +if [[ ($is_pull_request == 0) && ($is_release_branch == 1) ]] +then + aws s3 cp libxgboost4j_m1_${BUILDKITE_COMMIT}.dylib \ + s3://xgboost-nightly-builds/${BRANCH_NAME}/libxgboost4j/ \ + --acl public-read --no-progress +fi +popd +set +x + # Ensure that XGBoost can be built with Clang 11 echo "--- Build and Test XGBoost with MacOS M1, Clang 11" set -x From cd473c9da39e56cf3a59a488511256a0f076d734 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Thu, 14 Dec 2023 02:16:53 +0100 Subject: [PATCH 18/59] [R] enable multi-dimensional base_margin (#9885) --- R-package/R/xgb.DMatrix.R | 6 +++--- R-package/man/xgb.DMatrix.Rd | 4 +++- R-package/tests/testthat/test_dmatrix.R | 26 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index fead30413..4b2bb0d2a 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -16,6 +16,8 @@ #' only care about the relative ordering of data points within each group, #' so it doesn't make sense to assign weights to individual data points. #' @param base_margin Base margin used for boosting from existing model. +#' +#' In the case of multi-output models, one can also pass multi-dimensional base_margin. #' @param missing a float value to represents missing values in data (used only when input is a dense matrix). #' It is useful when a 0 or some other extreme value represents missing values in data. #' @param silent whether to suppress printing an informational message after loading from a file. @@ -439,9 +441,7 @@ setinfo.xgb.DMatrix <- function(object, name, info, ...) { return(TRUE) } if (name == "base_margin") { - # if (length(info)!=nrow(object)) - # stop("The length of base margin must equal to the number of rows in the input data") - .Call(XGDMatrixSetInfo_R, object, name, as.numeric(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } if (name == "group") { diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index 95cc8d3cd..a1ef39f0b 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -36,7 +36,9 @@ is assigned to each group (not each data point). This is because we only care about the relative ordering of data points within each group, so it doesn't make sense to assign weights to individual data points.} -\item{base_margin}{Base margin used for boosting from existing model.} +\item{base_margin}{Base margin used for boosting from existing model. + + In the case of multi-output models, one can also pass multi-dimensional base_margin.} \item{missing}{a float value to represents missing values in data (used only when input is a dense matrix). It is useful when a 0 or some other extreme value represents missing values in data.} diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index 87a73d84b..55a699687 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -349,3 +349,29 @@ test_that("xgb.DMatrix: data.frame", { m <- xgb.DMatrix(df, enable_categorical = TRUE) expect_equal(getinfo(m, "feature_type"), c("c", "c")) }) + +test_that("xgb.DMatrix: can take multi-dimensional 'base_margin'", { + set.seed(123) + x <- matrix(rnorm(100 * 10), nrow = 100) + y <- matrix(rnorm(100 * 2), nrow = 100) + b <- matrix(rnorm(100 * 2), nrow = 100) + model <- xgb.train( + data = xgb.DMatrix(data = x, label = y, nthread = n_threads), + params = list( + objective = "reg:squarederror", + tree_method = "hist", + multi_strategy = "multi_output_tree", + base_score = 0, + nthread = n_threads + ), + nround = 1 + ) + pred_only_x <- predict(model, x, nthread = n_threads, reshape = TRUE) + pred_w_base <- predict( + model, + xgb.DMatrix(data = x, base_margin = b, nthread = n_threads), + nthread = n_threads, + reshape = TRUE + ) + expect_equal(pred_only_x, pred_w_base - b, tolerance = 1e-5) +}) From 1aa8c8d9be17d985c77a403e0b3c9b794f363843 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 14 Dec 2023 18:28:37 +0800 Subject: [PATCH 19/59] Support more scipy types. (#9881) --- python-package/xgboost/data.py | 73 ++++++++++++++++++++------- tests/python/test_dmatrix.py | 33 ------------- tests/python/test_with_scipy.py | 87 +++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 50 deletions(-) create mode 100644 tests/python/test_with_scipy.py diff --git a/python-package/xgboost/data.py b/python-package/xgboost/data.py index 74b2966fe..05337e788 100644 --- a/python-package/xgboost/data.py +++ b/python-package/xgboost/data.py @@ -57,12 +57,23 @@ def _check_data_shape(data: DataType) -> None: raise ValueError("Please reshape the input data into 2-dimensional matrix.") -def _is_scipy_csr(data: DataType) -> bool: +def is_scipy_csr(data: DataType) -> bool: + """Predicate for scipy CSR input.""" + is_array = False + is_matrix = False try: - import scipy.sparse + from scipy.sparse import csr_array + + is_array = isinstance(data, csr_array) except ImportError: - return False - return isinstance(data, scipy.sparse.csr_matrix) + pass + try: + from scipy.sparse import csr_matrix + + is_matrix = isinstance(data, csr_matrix) + except ImportError: + pass + return is_array or is_matrix def _array_interface_dict(data: np.ndarray) -> dict: @@ -135,12 +146,23 @@ def _from_scipy_csr( return handle, feature_names, feature_types -def _is_scipy_csc(data: DataType) -> bool: +def is_scipy_csc(data: DataType) -> bool: + """Predicate for scipy CSC input.""" + is_array = False + is_matrix = False try: - import scipy.sparse + from scipy.sparse import csc_array + + is_array = isinstance(data, csc_array) except ImportError: - return False - return isinstance(data, scipy.sparse.csc_matrix) + pass + try: + from scipy.sparse import csc_matrix + + is_matrix = isinstance(data, csc_matrix) + except ImportError: + pass + return is_array or is_matrix def _from_scipy_csc( @@ -171,12 +193,23 @@ def _from_scipy_csc( return handle, feature_names, feature_types -def _is_scipy_coo(data: DataType) -> bool: +def is_scipy_coo(data: DataType) -> bool: + """Predicate for scipy COO input.""" + is_array = False + is_matrix = False try: - import scipy.sparse + from scipy.sparse import coo_array + + is_array = isinstance(data, coo_array) except ImportError: - return False - return isinstance(data, scipy.sparse.coo_matrix) + pass + try: + from scipy.sparse import coo_matrix + + is_matrix = isinstance(data, coo_matrix) + except ImportError: + pass + return is_array or is_matrix def _is_np_array_like(data: DataType) -> bool: @@ -1138,15 +1171,15 @@ def dispatch_data_backend( """Dispatch data for DMatrix.""" if not _is_cudf_ser(data) and not _is_pandas_series(data): _check_data_shape(data) - if _is_scipy_csr(data): + if is_scipy_csr(data): return _from_scipy_csr( data, missing, threads, feature_names, feature_types, data_split_mode ) - if _is_scipy_csc(data): + if is_scipy_csc(data): return _from_scipy_csc( data, missing, threads, feature_names, feature_types, data_split_mode ) - if _is_scipy_coo(data): + if is_scipy_coo(data): return _from_scipy_csr( data.tocsr(), missing, @@ -1396,9 +1429,15 @@ def _proxy_transform( if _is_np_array_like(data): data, _ = _ensure_np_dtype(data, data.dtype) return data, None, feature_names, feature_types - if _is_scipy_csr(data): + if is_scipy_csr(data): data = transform_scipy_sparse(data, True) return data, None, feature_names, feature_types + if is_scipy_csc(data): + data = transform_scipy_sparse(data.tocsr(), True) + return data, None, feature_names, feature_types + if is_scipy_coo(data): + data = transform_scipy_sparse(data.tocsr(), True) + return data, None, feature_names, feature_types if _is_pandas_series(data): import pandas as pd @@ -1451,7 +1490,7 @@ def dispatch_proxy_set_data( _check_data_shape(data) proxy._set_data_from_array(data) # pylint: disable=W0212 return - if _is_scipy_csr(data): + if is_scipy_csr(data): proxy._set_data_from_csr(data) # pylint: disable=W0212 return raise err diff --git a/tests/python/test_dmatrix.py b/tests/python/test_dmatrix.py index 05a9af3b0..2e8a1a2a6 100644 --- a/tests/python/test_dmatrix.py +++ b/tests/python/test_dmatrix.py @@ -112,39 +112,6 @@ class TestDMatrix: with pytest.raises(ValueError): xgb.DMatrix(data) - def test_csr(self): - indptr = np.array([0, 2, 3, 6]) - indices = np.array([0, 2, 2, 0, 1, 2]) - data = np.array([1, 2, 3, 4, 5, 6]) - X = scipy.sparse.csr_matrix((data, indices, indptr), shape=(3, 3)) - dtrain = xgb.DMatrix(X) - assert dtrain.num_row() == 3 - assert dtrain.num_col() == 3 - - def test_csc(self): - row = np.array([0, 2, 2, 0, 1, 2]) - col = np.array([0, 0, 1, 2, 2, 2]) - data = np.array([1, 2, 3, 4, 5, 6]) - X = scipy.sparse.csc_matrix((data, (row, col)), shape=(3, 3)) - dtrain = xgb.DMatrix(X) - assert dtrain.num_row() == 3 - assert dtrain.num_col() == 3 - - indptr = np.array([0, 3, 5]) - data = np.array([0, 1, 2, 3, 4]) - row_idx = np.array([0, 1, 2, 0, 2]) - X = scipy.sparse.csc_matrix((data, row_idx, indptr), shape=(3, 2)) - assert tm.predictor_equal(xgb.DMatrix(X.tocsr()), xgb.DMatrix(X)) - - def test_coo(self): - row = np.array([0, 2, 2, 0, 1, 2]) - col = np.array([0, 0, 1, 2, 2, 2]) - data = np.array([1, 2, 3, 4, 5, 6]) - X = scipy.sparse.coo_matrix((data, (row, col)), shape=(3, 3)) - dtrain = xgb.DMatrix(X) - assert dtrain.num_row() == 3 - assert dtrain.num_col() == 3 - def test_np_view(self): # Sliced Float32 array y = np.array([12, 34, 56], np.float32)[::2] diff --git a/tests/python/test_with_scipy.py b/tests/python/test_with_scipy.py new file mode 100644 index 000000000..ab54d2a43 --- /dev/null +++ b/tests/python/test_with_scipy.py @@ -0,0 +1,87 @@ +import itertools +import warnings +from typing import Type + +import numpy as np +import pytest +import scipy.sparse + +import xgboost as xgb +from xgboost import testing as tm + + +@pytest.mark.filterwarnings("error") +@pytest.mark.parametrize( + "DMatrixT,CSR", + [ + (m, n) + for m, n in itertools.product( + (xgb.DMatrix, xgb.QuantileDMatrix), + (scipy.sparse.csr_matrix, scipy.sparse.csr_array), + ) + ], +) +def test_csr(DMatrixT: Type[xgb.DMatrix], CSR: Type) -> None: + with warnings.catch_warnings(): + indptr = np.array([0, 2, 3, 6]) + indices = np.array([0, 2, 2, 0, 1, 2]) + data = np.array([1, 2, 3, 4, 5, 6]) + X = CSR((data, indices, indptr), shape=(3, 3)) + dtrain = DMatrixT(X) + assert dtrain.num_row() == 3 + assert dtrain.num_col() == 3 + assert dtrain.num_nonmissing() == data.size + + +@pytest.mark.filterwarnings("error") +@pytest.mark.parametrize( + "DMatrixT,CSC", + [ + (m, n) + for m, n in itertools.product( + (xgb.DMatrix, xgb.QuantileDMatrix), + (scipy.sparse.csc_matrix, scipy.sparse.csc_array), + ) + ], +) +def test_csc(DMatrixT: Type[xgb.DMatrix], CSC: Type) -> None: + with warnings.catch_warnings(): + row = np.array([0, 2, 2, 0, 1, 2]) + col = np.array([0, 0, 1, 2, 2, 2]) + data = np.array([1, 2, 3, 4, 5, 6]) + X = CSC((data, (row, col)), shape=(3, 3)) + dtrain = DMatrixT(X) + assert dtrain.num_row() == 3 + assert dtrain.num_col() == 3 + assert dtrain.num_nonmissing() == data.size + + indptr = np.array([0, 3, 5]) + data = np.array([0, 1, 2, 3, 4]) + row_idx = np.array([0, 1, 2, 0, 2]) + X = CSC((data, row_idx, indptr), shape=(3, 2)) + assert tm.predictor_equal(DMatrixT(X.tocsr()), DMatrixT(X)) + + +@pytest.mark.filterwarnings("error") +@pytest.mark.parametrize( + "DMatrixT,COO", + [ + (m, n) + for m, n in itertools.product( + (xgb.DMatrix, xgb.QuantileDMatrix), + (scipy.sparse.coo_matrix, scipy.sparse.coo_array), + ) + ], +) +def test_coo(DMatrixT: Type[xgb.DMatrix], COO: Type) -> None: + with warnings.catch_warnings(): + row = np.array([0, 2, 2, 0, 1, 2]) + col = np.array([0, 0, 1, 2, 2, 2]) + data = np.array([1, 2, 3, 4, 5, 6]) + X = COO((data, (row, col)), shape=(3, 3)) + dtrain = DMatrixT(X) + assert dtrain.num_row() == 3 + assert dtrain.num_col() == 3 + assert dtrain.num_nonmissing() == data.size + + assert tm.predictor_equal(DMatrixT(X.tocsr()), DMatrixT(X)) From 125bc812f81ec908ed365d63cc8db97ff7699975 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 14 Dec 2023 23:29:19 +0800 Subject: [PATCH 20/59] [doc] Reference `enable_categorical` doc in sklearn. (#9884) --- python-package/xgboost/core.py | 7 ++++--- python-package/xgboost/sklearn.py | 8 +------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index 1bddadbbe..2e72dbbd2 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -824,9 +824,10 @@ class DMatrix: # pylint: disable=too-many-instance-attributes,too-many-public-m Experimental support of specializing for categorical features. - If passing 'True' and 'data' is a data frame (from supported libraries - such as Pandas or Modin), columns of categorical types will automatically - be set to be of categorical type (feature_type='c') in the resulting DMatrix. + If passing 'True' and 'data' is a data frame (from supported libraries such + as Pandas, Modin or cuDF), columns of categorical types will automatically + be set to be of categorical type (feature_type='c') in the resulting + DMatrix. If passing 'False' and 'data' is a data frame with categorical columns, it will result in an error being thrown. diff --git a/python-package/xgboost/sklearn.py b/python-package/xgboost/sklearn.py index 748b26cf6..ded37881f 100644 --- a/python-package/xgboost/sklearn.py +++ b/python-package/xgboost/sklearn.py @@ -276,13 +276,7 @@ __model_doc = f""" enable_categorical : bool - .. versionadded:: 1.5.0 - - .. note:: This parameter is experimental - - Experimental support for categorical data. When enabled, cudf/pandas.DataFrame - should be used to specify categorical data type. Also, JSON/UBJSON - serialization format is required. + See the same parameter of :py:class:`DMatrix` for details. feature_types : Optional[FeatureTypes] From 1c6e031c7558163e807e515c783b3dbcbbbe80d5 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Fri, 15 Dec 2023 01:30:43 +0800 Subject: [PATCH 21/59] [R] Fix clang warning. (#9874) --- R-package/src/xgboost_R.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index c27dc5e86..c675e873d 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -154,12 +154,12 @@ SEXP SafeMkChar(const char *c_str, SEXP continuation_token) { } \ PutRNGstate(); -/*! - * \brief macro to check the call. +/** + * @brief Macro for checking XGBoost return code. */ -#define CHECK_CALL(x) \ - if ((x) != 0) { \ - error(XGBGetLastError()); \ +#define CHECK_CALL(__rc) \ + if ((__rc) != 0) { \ + Rf_error("%s", XGBGetLastError()); \ } using dmlc::BeginPtr; From 2a6ab2547d1af8126e82114c4b1ed475059efffc Mon Sep 17 00:00:00 2001 From: Dmitry Razdoburdin Date: Fri, 15 Dec 2023 04:04:39 +0100 Subject: [PATCH 22/59] SYCL inference optimization (#9876) --------- Co-authored-by: Dmitry Razdoburdin <> --- plugin/sycl/data.h | 22 ++- plugin/sycl/predictor/predictor.cc | 240 +++++++++++++++-------------- 2 files changed, 136 insertions(+), 126 deletions(-) diff --git a/plugin/sycl/data.h b/plugin/sycl/data.h index 179c7cd1f..489fde989 100644 --- a/plugin/sycl/data.h +++ b/plugin/sycl/data.h @@ -66,13 +66,13 @@ class USMVector { public: USMVector() : size_(0), capacity_(0), data_(nullptr) {} - USMVector(::sycl::queue& qu, size_t size) : size_(size), capacity_(size) { + USMVector(::sycl::queue* qu, size_t size) : size_(size), capacity_(size) { data_ = allocate_memory_(qu, size_); } - USMVector(::sycl::queue& qu, size_t size, T v) : size_(size), capacity_(size) { + USMVector(::sycl::queue* qu, size_t size, T v) : size_(size), capacity_(size) { data_ = allocate_memory_(qu, size_); - qu.fill(data_.get(), v, size_).wait(); + qu->fill(data_.get(), v, size_).wait(); } USMVector(::sycl::queue* qu, const std::vector &vec) { @@ -147,25 +147,22 @@ class USMVector { } } - ::sycl::event ResizeAsync(::sycl::queue* qu, size_t size_new, T v) { + void Resize(::sycl::queue* qu, size_t size_new, T v, ::sycl::event* event) { if (size_new <= size_) { size_ = size_new; - return ::sycl::event(); } else if (size_new <= capacity_) { auto event = qu->fill(data_.get() + size_, v, size_new - size_); size_ = size_new; - return event; } else { size_t size_old = size_; auto data_old = data_; size_ = size_new; capacity_ = size_new; data_ = allocate_memory_(qu, size_); - ::sycl::event event; if (size_old > 0) { - event = qu->memcpy(data_.get(), data_old.get(), sizeof(T) * size_old); + *event = qu->memcpy(data_.get(), data_old.get(), sizeof(T) * size_old, *event); } - return qu->fill(data_.get() + size_old, v, size_new - size_old, event); + *event = qu->fill(data_.get() + size_old, v, size_new - size_old, *event); } } @@ -210,7 +207,7 @@ struct DeviceMatrix { DMatrix* p_mat; // Pointer to the original matrix on the host ::sycl::queue qu_; USMVector row_ptr; - USMVector data; + USMVector data; size_t total_offset; DeviceMatrix(::sycl::queue qu, DMatrix* dmat) : p_mat(dmat), qu_(qu) { @@ -238,8 +235,9 @@ struct DeviceMatrix { for (size_t i = 0; i < batch_size; i++) row_ptr[i + batch.base_rowid] += batch.base_rowid; } - std::copy(data_vec.data(), data_vec.data() + offset_vec[batch_size], - data.Data() + data_offset); + qu.memcpy(data.Data() + data_offset, + data_vec.data(), + offset_vec[batch_size] * sizeof(Entry)).wait(); data_offset += offset_vec[batch_size]; } } diff --git a/plugin/sycl/predictor/predictor.cc b/plugin/sycl/predictor/predictor.cc index 3ceb99f1e..dd56dd3bd 100755 --- a/plugin/sycl/predictor/predictor.cc +++ b/plugin/sycl/predictor/predictor.cc @@ -20,6 +20,7 @@ #include "xgboost/tree_model.h" #include "xgboost/predictor.h" #include "xgboost/tree_updater.h" +#include "../../../src/common/timer.h" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wtautological-constant-compare" @@ -36,36 +37,37 @@ namespace predictor { DMLC_REGISTRY_FILE_TAG(predictor_sycl); -/* Wrapper for descriptor of a tree node */ -struct DeviceNode { - DeviceNode() - : fidx(-1), left_child_idx(-1), right_child_idx(-1) {} - - union NodeValue { - float leaf_weight; - float fvalue; - }; +union NodeValue { + float leaf_weight; + float fvalue; +}; +class Node { int fidx; int left_child_idx; int right_child_idx; NodeValue val; - explicit DeviceNode(const RegTree::Node& n) { - this->left_child_idx = n.LeftChild(); - this->right_child_idx = n.RightChild(); - this->fidx = n.SplitIndex(); + public: + explicit Node(const RegTree::Node& n) { + left_child_idx = n.LeftChild(); + right_child_idx = n.RightChild(); + fidx = n.SplitIndex(); if (n.DefaultLeft()) { fidx |= (1U << 31); } if (n.IsLeaf()) { - this->val.leaf_weight = n.LeafValue(); + val.leaf_weight = n.LeafValue(); } else { - this->val.fvalue = n.SplitCond(); + val.fvalue = n.SplitCond(); } } + int LeftChildIdx() const {return left_child_idx; } + + int RightChildIdx() const {return right_child_idx; } + bool IsLeaf() const { return left_child_idx == -1; } int GetFidx() const { return fidx & ((1U << 31) - 1U); } @@ -74,9 +76,9 @@ struct DeviceNode { int MissingIdx() const { if (MissingLeft()) { - return this->left_child_idx; + return left_child_idx; } else { - return this->right_child_idx; + return right_child_idx; } } @@ -85,105 +87,79 @@ struct DeviceNode { float GetWeight() const { return val.leaf_weight; } }; -/* SYCL implementation of a device model, - * storing tree structure in USM buffers to provide access from device kernels - */ class DeviceModel { public: - ::sycl::queue qu_; - USMVector nodes_; - USMVector tree_segments_; - USMVector tree_group_; - size_t tree_beg_; - size_t tree_end_; - int num_group_; + USMVector nodes; + USMVector first_node_position; + USMVector tree_group; + size_t tree_beg; + size_t tree_end; + int num_group; - DeviceModel() {} - - ~DeviceModel() {} - - void Init(::sycl::queue qu, const gbm::GBTreeModel& model, size_t tree_begin, size_t tree_end) { - qu_ = qu; - - tree_segments_.Resize(&qu_, (tree_end - tree_begin) + 1); - int sum = 0; - tree_segments_[0] = sum; + void Init(::sycl::queue* qu, const gbm::GBTreeModel& model, size_t tree_begin, size_t tree_end) { + int n_nodes = 0; + first_node_position.Resize(qu, (tree_end - tree_begin) + 1); + first_node_position[0] = n_nodes; for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { if (model.trees[tree_idx]->HasCategoricalSplit()) { LOG(FATAL) << "Categorical features are not yet supported by sycl"; } - sum += model.trees[tree_idx]->GetNodes().size(); - tree_segments_[tree_idx - tree_begin + 1] = sum; + n_nodes += model.trees[tree_idx]->GetNodes().size(); + first_node_position[tree_idx - tree_begin + 1] = n_nodes; } - nodes_.Resize(&qu_, sum); + nodes.Resize(qu, n_nodes); for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { auto& src_nodes = model.trees[tree_idx]->GetNodes(); - for (size_t node_idx = 0; node_idx < src_nodes.size(); node_idx++) - nodes_[node_idx + tree_segments_[tree_idx - tree_begin]] = - static_cast(src_nodes[node_idx]); + size_t n_nodes_shift = first_node_position[tree_idx - tree_begin]; + for (size_t node_idx = 0; node_idx < src_nodes.size(); node_idx++) { + nodes[node_idx + n_nodes_shift] = static_cast(src_nodes[node_idx]); + } } - tree_group_.Resize(&qu_, model.tree_info.size()); + tree_group.Resize(qu, model.tree_info.size()); for (size_t tree_idx = 0; tree_idx < model.tree_info.size(); tree_idx++) - tree_group_[tree_idx] = model.tree_info[tree_idx]; + tree_group[tree_idx] = model.tree_info[tree_idx]; - tree_beg_ = tree_begin; - tree_end_ = tree_end; - num_group_ = model.learner_model_param->num_output_group; + tree_beg = tree_begin; + tree_end = tree_end; + num_group = model.learner_model_param->num_output_group; } }; -float GetFvalue(int ridx, int fidx, Entry* data, size_t* row_ptr, bool* is_missing) { - // Binary search - auto begin_ptr = data + row_ptr[ridx]; - auto end_ptr = data + row_ptr[ridx + 1]; - Entry* previous_middle = nullptr; - while (end_ptr != begin_ptr) { - auto middle = begin_ptr + (end_ptr - begin_ptr) / 2; - if (middle == previous_middle) { - break; +float GetLeafWeight(const Node* nodes, const float* fval_buff, const uint8_t* miss_buff) { + const Node* node = nodes; + while (!node->IsLeaf()) { + if (miss_buff[node->GetFidx()] == 1) { + node = nodes + node->MissingIdx(); } else { - previous_middle = middle; - } - - if (middle->index == fidx) { - *is_missing = false; - return middle->fvalue; - } else if (middle->index < fidx) { - begin_ptr = middle; - } else { - end_ptr = middle; - } - } - *is_missing = true; - return 0.0; -} - -float GetLeafWeight(int ridx, const DeviceNode* tree, Entry* data, size_t* row_ptr) { - DeviceNode n = tree[0]; - int node_id = 0; - bool is_missing; - while (!n.IsLeaf()) { - float fvalue = GetFvalue(ridx, n.GetFidx(), data, row_ptr, &is_missing); - // Missing value - if (is_missing) { - n = tree[n.MissingIdx()]; - } else { - if (fvalue < n.GetFvalue()) { - node_id = n.left_child_idx; - n = tree[n.left_child_idx]; + const float fvalue = fval_buff[node->GetFidx()]; + if (fvalue < node->GetFvalue()) { + node = nodes + node->LeftChildIdx(); } else { - node_id = n.right_child_idx; - n = tree[n.right_child_idx]; + node = nodes + node->RightChildIdx(); } } } - return n.GetWeight(); + return node->GetWeight(); } -void DevicePredictInternal(::sycl::queue qu, - sycl::DeviceMatrix* dmat, +float GetLeafWeight(const Node* nodes, const float* fval_buff) { + const Node* node = nodes; + while (!node->IsLeaf()) { + const float fvalue = fval_buff[node->GetFidx()]; + if (fvalue < node->GetFvalue()) { + node = nodes + node->LeftChildIdx(); + } else { + node = nodes + node->RightChildIdx(); + } + } + return node->GetWeight(); +} + +template +void DevicePredictInternal(::sycl::queue* qu, + const sycl::DeviceMatrix& dmat, HostDeviceVector* out_preds, const gbm::GBTreeModel& model, size_t tree_begin, @@ -194,43 +170,75 @@ void DevicePredictInternal(::sycl::queue qu, DeviceModel device_model; device_model.Init(qu, model, tree_begin, tree_end); - auto& out_preds_vec = out_preds->HostVector(); - - DeviceNode* nodes = device_model.nodes_.Data(); - ::sycl::buffer out_preds_buf(out_preds_vec.data(), out_preds_vec.size()); - size_t* tree_segments = device_model.tree_segments_.Data(); - int* tree_group = device_model.tree_group_.Data(); - size_t* row_ptr = dmat->row_ptr.Data(); - Entry* data = dmat->data.Data(); - int num_features = dmat->p_mat->Info().num_col_; - int num_rows = dmat->row_ptr.Size() - 1; + const Node* nodes = device_model.nodes.DataConst(); + const size_t* first_node_position = device_model.first_node_position.DataConst(); + const int* tree_group = device_model.tree_group.DataConst(); + const size_t* row_ptr = dmat.row_ptr.DataConst(); + const Entry* data = dmat.data.DataConst(); + int num_features = dmat.p_mat->Info().num_col_; + int num_rows = dmat.row_ptr.Size() - 1; int num_group = model.learner_model_param->num_output_group; - qu.submit([&](::sycl::handler& cgh) { + USMVector fval_buff(qu, num_features * num_rows); + USMVector miss_buff; + auto* fval_buff_ptr = fval_buff.Data(); + + std::vector<::sycl::event> events(1); + if constexpr (any_missing) { + miss_buff.Resize(qu, num_features * num_rows, 1, &events[0]); + } + auto* miss_buff_ptr = miss_buff.Data(); + + auto& out_preds_vec = out_preds->HostVector(); + ::sycl::buffer out_preds_buf(out_preds_vec.data(), out_preds_vec.size()); + events[0] = qu->submit([&](::sycl::handler& cgh) { + cgh.depends_on(events[0]); auto out_predictions = out_preds_buf.template get_access<::sycl::access::mode::read_write>(cgh); cgh.parallel_for<>(::sycl::range<1>(num_rows), [=](::sycl::id<1> pid) { - int global_idx = pid[0]; - if (global_idx >= num_rows) return; + int row_idx = pid[0]; + auto* fval_buff_row_ptr = fval_buff_ptr + num_features * row_idx; + auto* miss_buff_row_ptr = miss_buff_ptr + num_features * row_idx; + + const Entry* first_entry = data + row_ptr[row_idx]; + const Entry* last_entry = data + row_ptr[row_idx + 1]; + for (const Entry* entry = first_entry; entry < last_entry; entry += 1) { + fval_buff_row_ptr[entry->index] = entry->fvalue; + if constexpr (any_missing) { + miss_buff_row_ptr[entry->index] = 0; + } + } + if (num_group == 1) { float sum = 0.0; for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { - const DeviceNode* tree = nodes + tree_segments[tree_idx - tree_begin]; - sum += GetLeafWeight(global_idx, tree, data, row_ptr); + const Node* first_node = nodes + first_node_position[tree_idx - tree_begin]; + if constexpr (any_missing) { + sum += GetLeafWeight(first_node, fval_buff_row_ptr, miss_buff_row_ptr); + } else { + sum += GetLeafWeight(first_node, fval_buff_row_ptr); + } } - out_predictions[global_idx] += sum; + out_predictions[row_idx] += sum; } else { for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { - const DeviceNode* tree = nodes + tree_segments[tree_idx - tree_begin]; - int out_prediction_idx = global_idx * num_group + tree_group[tree_idx]; - out_predictions[out_prediction_idx] += GetLeafWeight(global_idx, tree, data, row_ptr); + const Node* first_node = nodes + first_node_position[tree_idx - tree_begin]; + int out_prediction_idx = row_idx * num_group + tree_group[tree_idx]; + if constexpr (any_missing) { + out_predictions[out_prediction_idx] += + GetLeafWeight(first_node, fval_buff_row_ptr, miss_buff_row_ptr); + } else { + out_predictions[out_prediction_idx] += + GetLeafWeight(first_node, fval_buff_row_ptr); + } } } }); - }).wait(); + }); + qu->wait(); } class Predictor : public xgboost::Predictor { - protected: + public: void InitOutPredictions(const MetaInfo& info, HostDeviceVector* out_preds, const gbm::GBTreeModel& model) const override { @@ -263,7 +271,6 @@ class Predictor : public xgboost::Predictor { } } - public: explicit Predictor(Context const* context) : xgboost::Predictor::Predictor{context}, cpu_predictor(xgboost::Predictor::Create("cpu_predictor", context)) {} @@ -281,7 +288,12 @@ class Predictor : public xgboost::Predictor { } if (tree_begin < tree_end) { - DevicePredictInternal(qu, &device_matrix, out_preds, model, tree_begin, tree_end); + const bool any_missing = !(dmat->IsDense()); + if (any_missing) { + DevicePredictInternal(&qu, device_matrix, out_preds, model, tree_begin, tree_end); + } else { + DevicePredictInternal(&qu, device_matrix, out_preds, model, tree_begin, tree_end); + } } } From db7f952ed62940208668facef02e513b4ff3da9d Mon Sep 17 00:00:00 2001 From: david-cortes Date: Sat, 16 Dec 2023 05:19:22 +0100 Subject: [PATCH 23/59] update docs for parameters (#9900) --- doc/parameter.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/parameter.rst b/doc/parameter.rst index d2471dfd9..0ce60916e 100644 --- a/doc/parameter.rst +++ b/doc/parameter.rst @@ -78,7 +78,7 @@ Parameters for Tree Booster * ``gamma`` [default=0, alias: ``min_split_loss``] - - Minimum loss reduction required to make a further partition on a leaf node of the tree. The larger ``gamma`` is, the more conservative the algorithm will be. + - Minimum loss reduction required to make a further partition on a leaf node of the tree. The larger ``gamma`` is, the more conservative the algorithm will be. Note that a tree where no splits were made might still contain a single terminal node with a non-zero score. - range: [0,∞] * ``max_depth`` [default=6] @@ -388,6 +388,7 @@ Specify the learning task and the corresponding learning objective. The objectiv - The initial prediction score of all instances, global bias - The parameter is automatically estimated for selected objectives before training. To disable the estimation, specify a real number argument. + - If ``base_margin`` is supplied, ``base_score`` will not be added. - For sufficient number of iterations, changing this value will not have too much effect. * ``eval_metric`` [default according to objective] From 0edd600f3d10059a13d1db41b9c5704a7e0a660c Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Sun, 17 Dec 2023 13:34:34 +0800 Subject: [PATCH 24/59] [doc] Brief introduction to `base_score`. (#9882) --- doc/parameter.rst | 2 + doc/tutorials/custom_metric_obj.rst | 3 +- doc/tutorials/index.rst | 1 + doc/tutorials/intercept.rst | 104 ++++++++++++++++++++++++++++ python-package/xgboost/core.py | 2 +- python-package/xgboost/sklearn.py | 8 +-- 6 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 doc/tutorials/intercept.rst diff --git a/doc/parameter.rst b/doc/parameter.rst index 0ce60916e..a7d8203b0 100644 --- a/doc/parameter.rst +++ b/doc/parameter.rst @@ -391,6 +391,8 @@ Specify the learning task and the corresponding learning objective. The objectiv - If ``base_margin`` is supplied, ``base_score`` will not be added. - For sufficient number of iterations, changing this value will not have too much effect. + See :doc:`/tutorials/intercept` for more info. + * ``eval_metric`` [default according to objective] - Evaluation metrics for validation data, a default metric will be assigned according to objective (rmse for regression, and logloss for classification, `mean average precision` for ``rank:map``, etc.) diff --git a/doc/tutorials/custom_metric_obj.rst b/doc/tutorials/custom_metric_obj.rst index f5c08bf59..76ee1b3de 100644 --- a/doc/tutorials/custom_metric_obj.rst +++ b/doc/tutorials/custom_metric_obj.rst @@ -271,7 +271,8 @@ available in XGBoost: We use ``multi:softmax`` to illustrate the differences of transformed prediction. With ``softprob`` the output prediction array has shape ``(n_samples, n_classes)`` while for ``softmax`` it's ``(n_samples, )``. A demo for multi-class objective function is also -available at :ref:`sphx_glr_python_examples_custom_softmax.py`. +available at :ref:`sphx_glr_python_examples_custom_softmax.py`. Also, see +:doc:`/tutorials/intercept` for some more explanation. ********************** diff --git a/doc/tutorials/index.rst b/doc/tutorials/index.rst index 5d090ce65..c82abf43f 100644 --- a/doc/tutorials/index.rst +++ b/doc/tutorials/index.rst @@ -30,4 +30,5 @@ See `Awesome XGBoost `_ for mo input_format param_tuning custom_metric_obj + intercept privacy_preserving \ No newline at end of file diff --git a/doc/tutorials/intercept.rst b/doc/tutorials/intercept.rst new file mode 100644 index 000000000..8452918e1 --- /dev/null +++ b/doc/tutorials/intercept.rst @@ -0,0 +1,104 @@ +######### +Intercept +######### + +.. versionadded:: 2.0.0 + +Since 2.0.0, XGBoost supports estimating the model intercept (named ``base_score``) +automatically based on targets upon training. The behavior can be controlled by setting +``base_score`` to a constant value. The following snippet disables the automatic +estimation: + +.. code-block:: python + + import xgboost as xgb + + reg = xgb.XGBRegressor() + reg.set_params(base_score=0.5) + +In addition, here 0.5 represents the value after applying the inverse link function. See +the end of the document for a description. + +Other than the ``base_score``, users can also provide global bias via the data field +``base_margin``, which is a vector or a matrix depending on the task. With multi-output +and multi-class, the ``base_margin`` is a matrix with size ``(n_samples, n_targets)`` or +``(n_samples, n_classes)``. + +.. code-block:: python + + import xgboost as xgb + from sklearn.datasets import make_regression + + X, y = make_regression() + + reg = xgb.XGBRegressor() + reg.fit(X, y) + # Request for raw prediction + m = reg.predict(X, output_margin=True) + + reg_1 = xgb.XGBRegressor() + # Feed the prediction into the next model + reg.fit(X, y, base_margin=m) + reg.predict(X, base_margin=m) + + +It specifies the bias for each sample and can be used for stacking an XGBoost model on top +of other models, see :ref:`sphx_glr_python_examples_boost_from_prediction.py` for a worked +example. When ``base_margin`` is specified, it automatically overrides the ``base_score`` +parameter. If you are stacking XGBoost models, then the usage should be relatively +straightforward, with the previous model providing raw prediction and a new model using +the prediction as bias. For more customized inputs, users need to take extra care of the +link function. Let :math:`F` be the model and :math:`g` be the link function, since +``base_score`` is overridden when sample-specific ``base_margin`` is available, we will +omit it here: + +.. math:: + + g(E[y_i]) = F(x_i) + + +When base margin :math:`b` is provided, it's added to the raw model output :math:`F`: + +.. math:: + + g(E[y_i]) = F(x_i) + b_i + +and the output of the final model is: + + +.. math:: + + g^{-1}(F(x_i) + b_i) + +Using the gamma deviance objective ``reg:gamma`` as an example, which has a log link +function, hence: + +.. math:: + + \ln{(E[y_i])} = F(x_i) + b_i \\ + E[y_i] = \exp{(F(x_i) + b_i)} + +As a result, if you are feeding outputs from models like GLM with a corresponding +objective function, make sure the outputs are not yet transformed by the inverse link. + +In the case of ``base_score`` (intercept), it can be accessed through +:py:meth:`~xgboost.Booster.save_config` after estimation. Unlike the ``base_margin``, the +returned value represents a value after applying inverse link. With logistic regression +and the logit link function as an example, given the ``base_score`` as 0.5, +:math:`g(intercept) = logit(0.5) = 0` is added to the raw model output: + +.. math:: + + E[y_i] = g^{-1}{(F(x_i) + g(intercept))} + +and 0.5 is the same as :math:`base_score = g^{-1}(0) = 0.5`. This is more intuitive if you +remove the model and consider only the intercept, which is estimated before the model is +fitted: + +.. math:: + + E[y] = g^{-1}{g(intercept))} \\ + E[y] = intercept + +For some objectives like MAE, there are close solutions, while for others it's estimated +with one step Newton method. \ No newline at end of file diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index 2e72dbbd2..097fb0935 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -785,7 +785,7 @@ class DMatrix: # pylint: disable=too-many-instance-attributes,too-many-public-m so it doesn't make sense to assign weights to individual data points. base_margin : - Base margin used for boosting from existing model. + Global bias for each instance. See :doc:`/tutorials/intercept` for details. missing : Value in the input data which needs to be present as a missing value. If None, defaults to np.nan. diff --git a/python-package/xgboost/sklearn.py b/python-package/xgboost/sklearn.py index ded37881f..ea8d8d041 100644 --- a/python-package/xgboost/sklearn.py +++ b/python-package/xgboost/sklearn.py @@ -1006,7 +1006,7 @@ class XGBModel(XGBModelBase): sample_weight : instance weights base_margin : - global bias for each instance. + Global bias for each instance. See :doc:`/tutorials/intercept` for details. eval_set : A list of (X, y) tuple pairs to use as validation sets, for which metrics will be computed. @@ -1146,7 +1146,7 @@ class XGBModel(XGBModelBase): When this is True, validate that the Booster's and data's feature_names are identical. Otherwise, it is assumed that the feature_names are the same. base_margin : - Margin added to prediction. + Global bias for each instance. See :doc:`/tutorials/intercept` for details. iteration_range : Specifies which layer of trees are used in prediction. For example, if a random forest is trained with 100 rounds. Specifying ``iteration_range=(10, @@ -1599,7 +1599,7 @@ class XGBClassifier(XGBModel, XGBClassifierBase): When this is True, validate that the Booster's and data's feature_names are identical. Otherwise, it is assumed that the feature_names are the same. base_margin : - Margin added to prediction. + Global bias for each instance. See :doc:`/tutorials/intercept` for details. iteration_range : Specifies which layer of trees are used in prediction. For example, if a random forest is trained with 100 rounds. Specifying `iteration_range=(10, @@ -1942,7 +1942,7 @@ class XGBRanker(XGBModel, XGBRankerMixIn): weights to individual data points. base_margin : - Global bias for each instance. + Global bias for each instance. See :doc:`/tutorials/intercept` for details. eval_set : A list of (X, y) tuple pairs to use as validation sets, for which metrics will be computed. From ff3d82c006083c01f7e1d9796564d32844ea952c Mon Sep 17 00:00:00 2001 From: david-cortes Date: Mon, 18 Dec 2023 13:31:01 +0100 Subject: [PATCH 25/59] [R] Refactor field logic for dmatrix (#9901) --- R-package/NAMESPACE | 1 + R-package/R/xgb.DMatrix.R | 70 ++++++++++++++++++++++------ R-package/R/xgb.cv.R | 5 +- R-package/man/setinfo.Rd | 2 +- R-package/man/xgb.DMatrix.hasinfo.Rd | 32 +++++++++++++ 5 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 R-package/man/xgb.DMatrix.hasinfo.Rd diff --git a/R-package/NAMESPACE b/R-package/NAMESPACE index bbb5ee225..40ede23a5 100644 --- a/R-package/NAMESPACE +++ b/R-package/NAMESPACE @@ -28,6 +28,7 @@ export(setinfo) export(slice) export(xgb.Booster.complete) export(xgb.DMatrix) +export(xgb.DMatrix.hasinfo) export(xgb.DMatrix.save) export(xgb.attr) export(xgb.attributes) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 4b2bb0d2a..11d1105e6 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -163,7 +163,10 @@ xgb.DMatrix <- function( } dmat <- handle - attributes(dmat) <- list(class = "xgb.DMatrix") + attributes(dmat) <- list( + class = "xgb.DMatrix", + fields = new.env() + ) if (!is.null(label)) { setinfo(dmat, "label", label) @@ -199,6 +202,35 @@ xgb.DMatrix <- function( return(dmat) } +#' @title Check whether DMatrix object has a field +#' @description Checks whether an xgb.DMatrix object has a given field assigned to +#' it, such as weights, labels, etc. +#' @param object The DMatrix object to check for the given \code{info} field. +#' @param info The field to check for presence or absence in \code{object}. +#' @seealso \link{xgb.DMatrix}, \link{getinfo.xgb.DMatrix}, \link{setinfo.xgb.DMatrix} +#' @examples +#' library(xgboost) +#' x <- matrix(1:10, nrow = 5) +#' dm <- xgb.DMatrix(x, nthread = 1) +#' +#' # 'dm' so far doesn't have any fields set +#' xgb.DMatrix.hasinfo(dm, "label") +#' +#' # Fields can be added after construction +#' setinfo(dm, "label", 1:5) +#' xgb.DMatrix.hasinfo(dm, "label") +#' @export +xgb.DMatrix.hasinfo <- function(object, info) { + if (!inherits(object, "xgb.DMatrix")) { + stop("Object is not an 'xgb.DMatrix'.") + } + if (.Call(XGCheckNullPtr_R, object)) { + warning("xgb.DMatrix object is invalid. Must be constructed again.") + return(FALSE) + } + return(NVL(attr(object, "fields")[[info]], FALSE)) +} + # get dmatrix from data, label # internal helper method @@ -389,7 +421,7 @@ getinfo.xgb.DMatrix <- function(object, name, ...) { #' @param object Object of class "xgb.DMatrix" #' @param name the name of the field to get #' @param info the specific field of information to set -#' @param ... other parameters +#' @param ... Not used. #' #' @details #' See the documentation for \link{xgb.DMatrix} for possible fields that can be set @@ -418,6 +450,12 @@ setinfo <- function(object, ...) UseMethod("setinfo") #' @rdname setinfo #' @export setinfo.xgb.DMatrix <- function(object, name, info, ...) { + .internal.setinfo.xgb.DMatrix(object, name, info, ...) + attr(object, "fields")[[name]] <- TRUE + return(TRUE) +} + +.internal.setinfo.xgb.DMatrix <- function(object, name, info, ...) { if (name == "label") { if (NROW(info) != nrow(object)) stop("The length of labels must equal to the number of rows in the input data") @@ -425,19 +463,19 @@ setinfo.xgb.DMatrix <- function(object, name, info, ...) { return(TRUE) } if (name == "label_lower_bound") { - if (length(info) != nrow(object)) + if (NROW(info) != nrow(object)) stop("The length of lower-bound labels must equal to the number of rows in the input data") - .Call(XGDMatrixSetInfo_R, object, name, as.numeric(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } if (name == "label_upper_bound") { - if (length(info) != nrow(object)) + if (NROW(info) != nrow(object)) stop("The length of upper-bound labels must equal to the number of rows in the input data") - .Call(XGDMatrixSetInfo_R, object, name, as.numeric(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } if (name == "weight") { - .Call(XGDMatrixSetInfo_R, object, name, as.numeric(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } if (name == "base_margin") { @@ -447,20 +485,20 @@ setinfo.xgb.DMatrix <- function(object, name, info, ...) { if (name == "group") { if (sum(info) != nrow(object)) stop("The sum of groups must equal to the number of rows in the input data") - .Call(XGDMatrixSetInfo_R, object, name, as.integer(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } if (name == "qid") { if (NROW(info) != nrow(object)) stop("The length of qid assignments must equal to the number of rows in the input data") - .Call(XGDMatrixSetInfo_R, object, name, as.integer(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } if (name == "feature_weights") { - if (length(info) != ncol(object)) { + if (NROW(info) != ncol(object)) { stop("The number of feature weights must equal to the number of columns in the input data") } - .Call(XGDMatrixSetInfo_R, object, name, as.numeric(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } @@ -568,11 +606,15 @@ slice.xgb.DMatrix <- function(object, idxset, ...) { #' @method print xgb.DMatrix #' @export print.xgb.DMatrix <- function(x, verbose = FALSE, ...) { + if (.Call(XGCheckNullPtr_R, x)) { + cat("INVALID xgb.DMatrix object. Must be constructed anew.\n") + return(invisible(x)) + } cat('xgb.DMatrix dim:', nrow(x), 'x', ncol(x), ' info: ') infos <- character(0) - if (length(getinfo(x, 'label')) > 0) infos <- 'label' - if (length(getinfo(x, 'weight')) > 0) infos <- c(infos, 'weight') - if (length(getinfo(x, 'base_margin')) > 0) infos <- c(infos, 'base_margin') + if (xgb.DMatrix.hasinfo(x, 'label')) infos <- 'label' + if (xgb.DMatrix.hasinfo(x, 'weight')) infos <- c(infos, 'weight') + if (xgb.DMatrix.hasinfo(x, 'base_margin')) infos <- c(infos, 'base_margin') if (length(infos) == 0) infos <- 'NA' cat(infos) cnames <- colnames(x) diff --git a/R-package/R/xgb.cv.R b/R-package/R/xgb.cv.R index 9e1ffeddc..1c17d86f0 100644 --- a/R-package/R/xgb.cv.R +++ b/R-package/R/xgb.cv.R @@ -126,6 +126,9 @@ xgb.cv <- function(params = list(), data, nrounds, nfold, label = NULL, missing early_stopping_rounds = NULL, maximize = NULL, callbacks = list(), ...) { check.deprecation(...) + if (inherits(data, "xgb.DMatrix") && .Call(XGCheckNullPtr_R, data)) { + stop("'data' is an invalid 'xgb.DMatrix' object. Must be constructed again.") + } params <- check.booster.params(params, ...) # TODO: should we deprecate the redundant 'metrics' parameter? @@ -136,7 +139,7 @@ xgb.cv <- function(params = list(), data, nrounds, nfold, label = NULL, missing check.custom.eval() # Check the labels - if ((inherits(data, 'xgb.DMatrix') && is.null(getinfo(data, 'label'))) || + if ((inherits(data, 'xgb.DMatrix') && !xgb.DMatrix.hasinfo(data, 'label')) || (!inherits(data, 'xgb.DMatrix') && is.null(label))) { stop("Labels must be provided for CV either through xgb.DMatrix, or through 'label=' when 'data' is matrix") } else if (inherits(data, 'xgb.DMatrix')) { diff --git a/R-package/man/setinfo.Rd b/R-package/man/setinfo.Rd index a8bc56b02..299e72675 100644 --- a/R-package/man/setinfo.Rd +++ b/R-package/man/setinfo.Rd @@ -12,7 +12,7 @@ setinfo(object, ...) \arguments{ \item{object}{Object of class "xgb.DMatrix"} -\item{...}{other parameters} +\item{...}{Not used.} \item{name}{the name of the field to get} diff --git a/R-package/man/xgb.DMatrix.hasinfo.Rd b/R-package/man/xgb.DMatrix.hasinfo.Rd new file mode 100644 index 000000000..308d9b42e --- /dev/null +++ b/R-package/man/xgb.DMatrix.hasinfo.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.DMatrix.R +\name{xgb.DMatrix.hasinfo} +\alias{xgb.DMatrix.hasinfo} +\title{Check whether DMatrix object has a field} +\usage{ +xgb.DMatrix.hasinfo(object, info) +} +\arguments{ +\item{object}{The DMatrix object to check for the given \code{info} field.} + +\item{info}{The field to check for presence or absence in \code{object}.} +} +\description{ +Checks whether an xgb.DMatrix object has a given field assigned to +it, such as weights, labels, etc. +} +\examples{ +library(xgboost) +x <- matrix(1:10, nrow = 5) +dm <- xgb.DMatrix(x, nthread = 1) + +# 'dm' so far doesn't have any fields set +xgb.DMatrix.hasinfo(dm, "label") + +# Fields can be added after construction +setinfo(dm, "label", 1:5) +xgb.DMatrix.hasinfo(dm, "label") +} +\seealso{ +\link{xgb.DMatrix}, \link{getinfo.xgb.DMatrix}, \link{setinfo.xgb.DMatrix} +} From ae32936ba242e3fe083f79e8592b337220c1cb73 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 19 Dec 2023 03:45:03 +0100 Subject: [PATCH 26/59] [R] Catch C++ exceptions (#9903) --- R-package/src/xgboost_R.cc | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index c675e873d..79b9e6037 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -138,21 +138,44 @@ SEXP SafeMkChar(const char *c_str, SEXP continuation_token) { } } // namespace +struct RRNGStateController { + RRNGStateController() { + GetRNGstate(); + } + + ~RRNGStateController() { + PutRNGstate(); + } +}; + /*! * \brief macro to annotate begin of api */ #define R_API_BEGIN() \ - GetRNGstate(); \ - try { + try { \ + RRNGStateController rng_controller{}; + +/* Note: an R error triggers a long jump, hence all C++ objects that +allocated memory through non-R allocators, including the exception +object, need to be destructed before triggering the R error. +In order to preserve the error message, it gets copied to a temporary +buffer, and the R error section is reached through a 'goto' statement +that bypasses usual function control flow. */ +char cpp_ex_msg[256]; /*! * \brief macro to annotate end of api */ #define R_API_END() \ } catch(dmlc::Error& e) { \ - PutRNGstate(); \ - error(e.what()); \ + Rf_error("%s", e.what()); \ + } catch(std::exception &e) { \ + std::strncpy(cpp_ex_msg, e.what(), 256); \ + goto throw_cpp_ex_as_R_err; \ } \ - PutRNGstate(); + if (false) { \ + throw_cpp_ex_as_R_err: \ + Rf_error("%s", cpp_ex_msg); \ + } /** * @brief Macro for checking XGBoost return code. From 9d122293bcbeb519415f97e6c318688f718e4519 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Wed, 20 Dec 2023 09:17:00 +0800 Subject: [PATCH 27/59] [doc] Fix typo. [skip ci] (#9904) --- doc/tutorials/intercept.rst | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/tutorials/intercept.rst b/doc/tutorials/intercept.rst index 8452918e1..3f6775611 100644 --- a/doc/tutorials/intercept.rst +++ b/doc/tutorials/intercept.rst @@ -38,8 +38,8 @@ and multi-class, the ``base_margin`` is a matrix with size ``(n_samples, n_targe reg_1 = xgb.XGBRegressor() # Feed the prediction into the next model - reg.fit(X, y, base_margin=m) - reg.predict(X, base_margin=m) + reg_1.fit(X, y, base_margin=m) + reg_1.predict(X, base_margin=m) It specifies the bias for each sample and can be used for stacking an XGBoost model on top @@ -79,7 +79,8 @@ function, hence: E[y_i] = \exp{(F(x_i) + b_i)} As a result, if you are feeding outputs from models like GLM with a corresponding -objective function, make sure the outputs are not yet transformed by the inverse link. +objective function, make sure the outputs are not yet transformed by the inverse link +(activation). In the case of ``base_score`` (intercept), it can be accessed through :py:meth:`~xgboost.Booster.save_config` after estimation. Unlike the ``base_margin``, the @@ -91,13 +92,13 @@ and the logit link function as an example, given the ``base_score`` as 0.5, E[y_i] = g^{-1}{(F(x_i) + g(intercept))} -and 0.5 is the same as :math:`base_score = g^{-1}(0) = 0.5`. This is more intuitive if you -remove the model and consider only the intercept, which is estimated before the model is -fitted: +and 0.5 is the same as :math:`base\_score = g^{-1}(0) = 0.5`. This is more intuitive if +you remove the model and consider only the intercept, which is estimated before the model +is fitted: .. math:: - E[y] = g^{-1}{g(intercept))} \\ + E[y] = g^{-1}{(g(intercept))} \\ E[y] = intercept For some objectives like MAE, there are close solutions, while for others it's estimated From 252e018275195b5e6a7fc49f069dccb2ec2d8c46 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 20 Dec 2023 03:52:00 +0100 Subject: [PATCH 28/59] correct name of function in header (#9905) --- include/xgboost/c_api.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xgboost/c_api.h b/include/xgboost/c_api.h index a56ead278..795c78946 100644 --- a/include/xgboost/c_api.h +++ b/include/xgboost/c_api.h @@ -532,7 +532,7 @@ XGProxyDMatrixSetDataCudaArrayInterface(DMatrixHandle handle, * * @return 0 when success, -1 when failure happens */ -XGB_DLL int XGProxyDMatrixSetDataCudaColumnar(DMatrixHandle handle, char const *c_interface_str); +XGB_DLL int XGProxyDMatrixSetDataColumnar(DMatrixHandle handle, char const *c_interface_str); /*! * \brief Set data on a DMatrix proxy. From b807f3e30c8caaeda010afd87ddd2797b36b029f Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Thu, 21 Dec 2023 03:01:30 +0100 Subject: [PATCH 29/59] [R] improve docstrings for "xgb.Booster.R" (#9906) --- R-package/DESCRIPTION | 3 +- R-package/R/xgb.Booster.R | 362 ++++++++++++++----------- R-package/R/xgb.create.features.R | 2 +- R-package/R/xgb.plot.shap.R | 4 +- R-package/R/xgboost.R | 8 +- R-package/man/agaricus.test.Rd | 8 +- R-package/man/agaricus.train.Rd | 8 +- R-package/man/cb.save.model.Rd | 2 +- R-package/man/getinfo.Rd | 18 +- R-package/man/predict.xgb.Booster.Rd | 184 +++++++------ R-package/man/print.xgb.Booster.Rd | 27 +- R-package/man/xgb.Booster.complete.Rd | 25 +- R-package/man/xgb.DMatrix.Rd | 18 +- R-package/man/xgb.attr.Rd | 48 ++-- R-package/man/xgb.config.Rd | 19 +- R-package/man/xgb.create.features.Rd | 4 +- R-package/man/xgb.cv.Rd | 86 +++--- R-package/man/xgb.importance.Rd | 20 +- R-package/man/xgb.model.dt.tree.Rd | 24 +- R-package/man/xgb.parameters.Rd | 17 +- R-package/man/xgb.plot.deepness.Rd | 6 +- R-package/man/xgb.plot.shap.Rd | 6 +- R-package/man/xgb.plot.shap.summary.Rd | 6 +- R-package/man/xgb.plot.tree.Rd | 16 +- R-package/man/xgb.save.raw.Rd | 6 +- R-package/man/xgb.shap.data.Rd | 6 +- R-package/man/xgb.train.Rd | 259 +++++++++--------- R-package/man/xgb.unserialize.Rd | 2 +- 28 files changed, 661 insertions(+), 533 deletions(-) diff --git a/R-package/DESCRIPTION b/R-package/DESCRIPTION index 92b53a660..7c01d50c6 100644 --- a/R-package/DESCRIPTION +++ b/R-package/DESCRIPTION @@ -63,7 +63,8 @@ Imports: Matrix (>= 1.1-0), methods, data.table (>= 1.9.6), - jsonlite (>= 1.0), + jsonlite (>= 1.0) +Roxygen: list(markdown = TRUE) RoxygenNote: 7.2.3 Encoding: UTF-8 SystemRequirements: GNU make, C++17 diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index aa5e65e40..4e980641a 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -79,36 +79,45 @@ xgb.get.handle <- function(object) { handle } -#' Restore missing parts of an incomplete xgb.Booster object. +#' Restore missing parts of an incomplete xgb.Booster object #' -#' It attempts to complete an \code{xgb.Booster} object by restoring either its missing -#' raw model memory dump (when it has no \code{raw} data but its \code{xgb.Booster.handle} is valid) -#' or its missing internal handle (when its \code{xgb.Booster.handle} is not valid +#' It attempts to complete an `xgb.Booster` object by restoring either its missing +#' raw model memory dump (when it has no `raw` data but its `xgb.Booster.handle` is valid) +#' or its missing internal handle (when its `xgb.Booster.handle` is not valid #' but it has a raw Booster memory dump). #' -#' @param object object of class \code{xgb.Booster} -#' @param saveraw a flag indicating whether to append \code{raw} Booster memory dump data +#' @param object Object of class `xgb.Booster`. +#' @param saveraw A flag indicating whether to append `raw` Booster memory dump data #' when it doesn't already exist. #' #' @details #' #' While this method is primarily for internal use, it might be useful in some practical situations. #' -#' E.g., when an \code{xgb.Booster} model is saved as an R object and then is loaded as an R object, +#' E.g., when an `xgb.Booster` model is saved as an R object and then is loaded as an R object, #' its handle (pointer) to an internal xgboost model would be invalid. The majority of xgboost methods #' should still work for such a model object since those methods would be using -#' \code{xgb.Booster.complete} internally. However, one might find it to be more efficient to call the -#' \code{xgb.Booster.complete} function explicitly once after loading a model as an R-object. +#' `xgb.Booster.complete()` internally. However, one might find it to be more efficient to call the +#' `xgb.Booster.complete()` function explicitly once after loading a model as an R-object. #' That would prevent further repeated implicit reconstruction of an internal booster model. #' #' @return -#' An object of \code{xgb.Booster} class. +#' An object of `xgb.Booster` class. #' #' @examples #' -#' data(agaricus.train, package='xgboost') -#' bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +#' data(agaricus.train, package = "xgboost") +#' +#' bst <- xgboost( +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' max_depth = 2, +#' eta = 1, +#' nthread = 2, +#' nrounds = 2, +#' objective = "binary:logistic" +#' ) +#' #' saveRDS(bst, "xgb.model.rds") #' #' # Warning: The resulting RDS file is only compatible with the current XGBoost version. @@ -161,112 +170,100 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { return(object) } -#' Predict method for eXtreme Gradient Boosting model +#' Predict method for XGBoost model #' #' Predicted values based on either xgboost model or model handle object. #' -#' @param object Object of class \code{xgb.Booster} or \code{xgb.Booster.handle} -#' @param newdata takes \code{matrix}, \code{dgCMatrix}, \code{dgRMatrix}, \code{dsparseVector}, -#' local data file or \code{xgb.DMatrix}. -#' -#' For single-row predictions on sparse data, it's recommended to use CSR format. If passing -#' a sparse vector, it will take it as a row vector. -#' @param missing Missing is only used when input is dense matrix. Pick a float value that represents -#' missing values in data (e.g., sometimes 0 or some other extreme value is used). -#' @param outputmargin whether the prediction should be returned in the for of original untransformed -#' sum of predictions from boosting iterations' results. E.g., setting \code{outputmargin=TRUE} for -#' logistic regression would result in predictions for log-odds instead of probabilities. -#' @param ntreelimit Deprecated, use \code{iterationrange} instead. -#' @param predleaf whether predict leaf index. -#' @param predcontrib whether to return feature contributions to individual predictions (see Details). -#' @param approxcontrib whether to use a fast approximation for feature contributions (see Details). -#' @param predinteraction whether to return contributions of feature interactions to individual predictions (see Details). -#' @param reshape whether to reshape the vector of predictions to a matrix form when there are several -#' prediction outputs per case. This option has no effect when either of predleaf, predcontrib, -#' or predinteraction flags is TRUE. -#' @param training whether is the prediction result used for training. For dart booster, +#' @param object Object of class `xgb.Booster` or `xgb.Booster.handle`. +#' @param newdata Takes `matrix`, `dgCMatrix`, `dgRMatrix`, `dsparseVector`, +#' local data file, or `xgb.DMatrix`. +#' For single-row predictions on sparse data, it is recommended to use the CSR format. +#' If passing a sparse vector, it will take it as a row vector. +#' @param missing Only used when input is a dense matrix. Pick a float value that represents +#' missing values in data (e.g., 0 or some other extreme value). +#' @param outputmargin Whether the prediction should be returned in the form of original untransformed +#' sum of predictions from boosting iterations' results. E.g., setting `outputmargin=TRUE` for +#' logistic regression would return log-odds instead of probabilities. +#' @param ntreelimit Deprecated, use `iterationrange` instead. +#' @param predleaf Whether to predict pre-tree leaf indices. +#' @param predcontrib Whether to return feature contributions to individual predictions (see Details). +#' @param approxcontrib Whether to use a fast approximation for feature contributions (see Details). +#' @param predinteraction Whether to return contributions of feature interactions to individual predictions (see Details). +#' @param reshape Whether to reshape the vector of predictions to matrix form when there are several +#' prediction outputs per case. No effect if `predleaf`, `predcontrib`, +#' or `predinteraction` is `TRUE`. +#' @param training Whether the predictions are used for training. For dart booster, #' training predicting will perform dropout. -#' @param iterationrange Specifies which layer of trees are used in prediction. For -#' example, if a random forest is trained with 100 rounds. Specifying -#' `iterationrange=(1, 21)`, then only the forests built during [1, 21) (half open set) -#' rounds are used in this prediction. It's 1-based index just like R vector. When set -#' to \code{c(1, 1)} XGBoost will use all trees. -#' @param strict_shape Default is \code{FALSE}. When it's set to \code{TRUE}, output -#' type and shape of prediction are invariant to model type. -#' +#' @param iterationrange Specifies which trees are used in prediction. For +#' example, take a random forest with 100 rounds. +#' With `iterationrange=c(1, 21)`, only the trees built during `[1, 21)` (half open set) +#' rounds are used in this prediction. The index is 1-based just like an R vector. When set +#' to `c(1, 1)`, XGBoost will use all trees. +#' @param strict_shape Default is `FALSE`. When set to `TRUE`, the output +#' type and shape of predictions are invariant to the model type. #' @param ... Not used. #' #' @details #' -#' Note that \code{iterationrange} would currently do nothing for predictions from gblinear, -#' since gblinear doesn't keep its boosting history. +#' Note that `iterationrange` would currently do nothing for predictions from "gblinear", +#' since "gblinear" doesn't keep its boosting history. #' -#' One possible practical applications of the \code{predleaf} option is to use the model +#' One possible practical applications of the `predleaf` option is to use the model #' as a generator of new features which capture non-linearity and interactions, -#' e.g., as implemented in \code{\link{xgb.create.features}}. +#' e.g., as implemented in [xgb.create.features()]. #' -#' Setting \code{predcontrib = TRUE} allows to calculate contributions of each feature to +#' Setting `predcontrib = TRUE` allows to calculate contributions of each feature to #' individual predictions. For "gblinear" booster, feature contributions are simply linear terms #' (feature_beta * feature_value). For "gbtree" booster, feature contributions are SHAP #' values (Lundberg 2017) that sum to the difference between the expected output #' of the model and the current prediction (where the hessian weights are used to compute the expectations). -#' Setting \code{approxcontrib = TRUE} approximates these values following the idea explained +#' Setting `approxcontrib = TRUE` approximates these values following the idea explained #' in \url{http://blog.datadive.net/interpreting-random-forests/}. #' -#' With \code{predinteraction = TRUE}, SHAP values of contributions of interaction of each pair of features +#' With `predinteraction = TRUE`, SHAP values of contributions of interaction of each pair of features #' are computed. Note that this operation might be rather expensive in terms of compute and memory. #' Since it quadratically depends on the number of features, it is recommended to perform selection #' of the most important features first. See below about the format of the returned results. #' -#' The \code{predict()} method uses as many threads as defined in \code{xgb.Booster} object (all by default). -#' If you want to change their number, then assign a new number to \code{nthread} using \code{\link{xgb.parameters<-}}. -#' Note also that converting a matrix to \code{\link{xgb.DMatrix}} uses multiple threads too. +#' The `predict()` method uses as many threads as defined in `xgb.Booster` object (all by default). +#' If you want to change their number, assign a new number to `nthread` using [xgb.parameters<-()]. +#' Note that converting a matrix to [xgb.DMatrix()] uses multiple threads too. #' #' @return -#' The return type is different depending whether \code{strict_shape} is set to \code{TRUE}. By default, -#' for regression or binary classification, it returns a vector of length \code{nrows(newdata)}. -#' For multiclass classification, either a \code{num_class * nrows(newdata)} vector or -#' a \code{(nrows(newdata), num_class)} dimension matrix is returned, depending on -#' the \code{reshape} value. -#' -#' When \code{predleaf = TRUE}, the output is a matrix object with the -#' number of columns corresponding to the number of trees. -#' -#' When \code{predcontrib = TRUE} and it is not a multiclass setting, the output is a matrix object with -#' \code{num_features + 1} columns. The last "+ 1" column in a matrix corresponds to bias. -#' For a multiclass case, a list of \code{num_class} elements is returned, where each element is -#' such a matrix. The contribution values are on the scale of untransformed margin -#' (e.g., for binary classification would mean that the contributions are log-odds deviations from bias). -#' -#' When \code{predinteraction = TRUE} and it is not a multiclass setting, the output is a 3d array with -#' dimensions \code{c(nrow, num_features + 1, num_features + 1)}. The off-diagonal (in the last two dimensions) -#' elements represent different features interaction contributions. The array is symmetric WRT the last -#' two dimensions. The "+ 1" columns corresponds to bias. Summing this array along the last dimension should -#' produce practically the same result as predict with \code{predcontrib = TRUE}. -#' For a multiclass case, a list of \code{num_class} elements is returned, where each element is -#' such an array. -#' -#' When \code{strict_shape} is set to \code{TRUE}, the output is always an array. For -#' normal prediction, the output is a 2-dimension array \code{(num_class, nrow(newdata))}. -#' -#' For \code{predcontrib = TRUE}, output is \code{(ncol(newdata) + 1, num_class, nrow(newdata))} -#' For \code{predinteraction = TRUE}, output is \code{(ncol(newdata) + 1, ncol(newdata) + 1, num_class, nrow(newdata))} -#' For \code{predleaf = TRUE}, output is \code{(n_trees_in_forest, num_class, n_iterations, nrow(newdata))} -#' -#' @seealso -#' \code{\link{xgb.train}}. +#' The return type depends on `strict_shape`. If `FALSE` (default): +#' - For regression or binary classification: A vector of length `nrows(newdata)`. +#' - For multiclass classification: A vector of length `num_class * nrows(newdata)` or +#' a `(nrows(newdata), num_class)` matrix, depending on the `reshape` value. +#' - When `predleaf = TRUE`: A matrix with one column per tree. +#' - When `predcontrib = TRUE`: When not multiclass, a matrix with +#' ` num_features + 1` columns. The last "+ 1" column corresponds to the baseline value. +#' In the multiclass case, a list of `num_class` such matrices. +#' The contribution values are on the scale of untransformed margin +#' (e.g., for binary classification, the values are log-odds deviations from the baseline). +#' - When `predinteraction = TRUE`: When not multiclass, the output is a 3d array of +#' dimension `c(nrow, num_features + 1, num_features + 1)`. The off-diagonal (in the last two dimensions) +#' elements represent different feature interaction contributions. The array is symmetric WRT the last +#' two dimensions. The "+ 1" columns corresponds to the baselines. Summing this array along the last dimension should +#' produce practically the same result as `predcontrib = TRUE`. +#' In the multiclass case, a list of `num_class` such arrays. #' +#' When `strict_shape = TRUE`, the output is always an array: +#' - For normal predictions, the output has dimension `(num_class, nrow(newdata))`. +#' - For `predcontrib = TRUE`, the dimension is `(ncol(newdata) + 1, num_class, nrow(newdata))`. +#' - For `predinteraction = TRUE`, the dimension is `(ncol(newdata) + 1, ncol(newdata) + 1, num_class, nrow(newdata))`. +#' - For `predleaf = TRUE`, the dimension is `(n_trees_in_forest, num_class, n_iterations, nrow(newdata))`. +#' @seealso [xgb.train()] #' @references -#' -#' Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", NIPS Proceedings 2017, \url{https://arxiv.org/abs/1705.07874} -#' -#' Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", \url{https://arxiv.org/abs/1706.06060} +#' 1. Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", +#' NIPS Proceedings 2017, \url{https://arxiv.org/abs/1705.07874} +#' 2. Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", +#' \url{https://arxiv.org/abs/1706.06060} #' #' @examples #' ## binary classification: #' -#' data(agaricus.train, package='xgboost') -#' data(agaricus.test, package='xgboost') +#' data(agaricus.train, package = "xgboost") +#' data(agaricus.test, package = "xgboost") #' #' ## Keep the number of threads to 2 for examples #' nthread <- 2 @@ -275,8 +272,16 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { #' train <- agaricus.train #' test <- agaricus.test #' -#' bst <- xgboost(data = train$data, label = train$label, max_depth = 2, -#' eta = 0.5, nthread = nthread, nrounds = 5, objective = "binary:logistic") +#' bst <- xgboost( +#' data = train$data, +#' label = train$label, +#' max_depth = 2, +#' eta = 0.5, +#' nthread = nthread, +#' nrounds = 5, +#' objective = "binary:logistic" +#' ) +#' #' # use all trees by default #' pred <- predict(bst, test$data) #' # use only the 1st tree @@ -308,32 +313,53 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { #' #' lb <- as.numeric(iris$Species) - 1 #' num_class <- 3 +#' #' set.seed(11) -#' bst <- xgboost(data = as.matrix(iris[, -5]), label = lb, -#' max_depth = 4, eta = 0.5, nthread = 2, nrounds = 10, subsample = 0.5, -#' objective = "multi:softprob", num_class = num_class) +#' +#' bst <- xgboost( +#' data = as.matrix(iris[, -5]), +#' label = lb, +#' max_depth = 4, +#' eta = 0.5, +#' nthread = 2, +#' nrounds = 10, +#' subsample = 0.5, +#' objective = "multi:softprob", +#' num_class = num_class +#' ) +#' #' # predict for softmax returns num_class probability numbers per case: #' pred <- predict(bst, as.matrix(iris[, -5])) #' str(pred) #' # reshape it to a num_class-columns matrix -#' pred <- matrix(pred, ncol=num_class, byrow=TRUE) +#' pred <- matrix(pred, ncol = num_class, byrow = TRUE) #' # convert the probabilities to softmax labels #' pred_labels <- max.col(pred) - 1 #' # the following should result in the same error as seen in the last iteration -#' sum(pred_labels != lb)/length(lb) +#' sum(pred_labels != lb) / length(lb) #' -#' # compare that to the predictions from softmax: +#' # compare with predictions from softmax: #' set.seed(11) -#' bst <- xgboost(data = as.matrix(iris[, -5]), label = lb, -#' max_depth = 4, eta = 0.5, nthread = 2, nrounds = 10, subsample = 0.5, -#' objective = "multi:softmax", num_class = num_class) +#' +#' bst <- xgboost( +#' data = as.matrix(iris[, -5]), +#' label = lb, +#' max_depth = 4, +#' eta = 0.5, +#' nthread = 2, +#' nrounds = 10, +#' subsample = 0.5, +#' objective = "multi:softmax", +#' num_class = num_class +#' ) +#' #' pred <- predict(bst, as.matrix(iris[, -5])) #' str(pred) #' all.equal(pred, pred_labels) #' # prediction from using only 5 iterations should result #' # in the same error as seen in iteration 5: -#' pred5 <- predict(bst, as.matrix(iris[, -5]), iterationrange=c(1, 6)) -#' sum(pred5 != lb)/length(lb) +#' pred5 <- predict(bst, as.matrix(iris[, -5]), iterationrange = c(1, 6)) +#' sum(pred5 != lb) / length(lb) #' #' @rdname predict.xgb.Booster #' @export @@ -497,63 +523,69 @@ predict.xgb.Booster.handle <- function(object, ...) { } -#' Accessors for serializable attributes of a model. +#' Accessors for serializable attributes of a model #' #' These methods allow to manipulate the key-value attribute strings of an xgboost model. #' -#' @param object Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}. -#' @param name a non-empty character string specifying which attribute is to be accessed. -#' @param value a value of an attribute for \code{xgb.attr<-}; for \code{xgb.attributes<-} -#' it's a list (or an object coercible to a list) with the names of attributes to set +#' @param object Object of class `xgb.Booster` or `xgb.Booster.handle`. +#' @param name A non-empty character string specifying which attribute is to be accessed. +#' @param value For `xgb.attr<-`, a value of an attribute; for `xgb.attributes<-`, +#' it is a list (or an object coercible to a list) with the names of attributes to set #' and the elements corresponding to attribute values. #' Non-character values are converted to character. -#' When attribute value is not a scalar, only the first index is used. -#' Use \code{NULL} to remove an attribute. +#' When an attribute value is not a scalar, only the first index is used. +#' Use `NULL` to remove an attribute. #' #' @details -#' The primary purpose of xgboost model attributes is to store some meta-data about the model. +#' The primary purpose of xgboost model attributes is to store some meta data about the model. #' Note that they are a separate concept from the object attributes in R. #' Specifically, they refer to key-value strings that can be attached to an xgboost model, #' stored together with the model's binary representation, and accessed later #' (from R or any other interface). -#' In contrast, any R-attribute assigned to an R-object of \code{xgb.Booster} class -#' would not be saved by \code{xgb.save} because an xgboost model is an external memory object +#' In contrast, any R attribute assigned to an R object of `xgb.Booster` class +#' would not be saved by [xgb.save()] because an xgboost model is an external memory object #' and its serialization is handled externally. #' Also, setting an attribute that has the same name as one of xgboost's parameters wouldn't #' change the value of that parameter for a model. -#' Use \code{\link{xgb.parameters<-}} to set or change model parameters. +#' Use [xgb.parameters<-()] to set or change model parameters. #' -#' The attribute setters would usually work more efficiently for \code{xgb.Booster.handle} -#' than for \code{xgb.Booster}, since only just a handle (pointer) would need to be copied. +#' The attribute setters would usually work more efficiently for `xgb.Booster.handle` +#' than for `xgb.Booster`, since only just a handle (pointer) would need to be copied. #' That would only matter if attributes need to be set many times. -#' Note, however, that when feeding a handle of an \code{xgb.Booster} object to the attribute setters, -#' the raw model cache of an \code{xgb.Booster} object would not be automatically updated, -#' and it would be user's responsibility to call \code{xgb.serialize} to update it. +#' Note, however, that when feeding a handle of an `xgb.Booster` object to the attribute setters, +#' the raw model cache of an `xgb.Booster` object would not be automatically updated, +#' and it would be the user's responsibility to call [xgb.serialize()] to update it. #' -#' The \code{xgb.attributes<-} setter either updates the existing or adds one or several attributes, +#' The `xgb.attributes<-` setter either updates the existing or adds one or several attributes, #' but it doesn't delete the other existing attributes. #' #' @return -#' \code{xgb.attr} returns either a string value of an attribute -#' or \code{NULL} if an attribute wasn't stored in a model. -#' -#' \code{xgb.attributes} returns a list of all attribute stored in a model -#' or \code{NULL} if a model has no stored attributes. +#' - `xgb.attr()` returns either a string value of an attribute +#' or `NULL` if an attribute wasn't stored in a model. +#' - `xgb.attributes()` returns a list of all attributes stored in a model +#' or `NULL` if a model has no stored attributes. #' #' @examples -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") #' train <- agaricus.train #' -#' bst <- xgboost(data = train$data, label = train$label, max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +#' bst <- xgboost( +#' data = train$data, +#' label = train$label, +#' max_depth = 2, +#' eta = 1, +#' nthread = 2, +#' nrounds = 2, +#' objective = "binary:logistic" +#' ) #' #' xgb.attr(bst, "my_attribute") <- "my attribute value" #' print(xgb.attr(bst, "my_attribute")) #' xgb.attributes(bst) <- list(a = 123, b = "abc") #' -#' xgb.save(bst, 'xgb.model') -#' bst1 <- xgb.load('xgb.model') -#' if (file.exists('xgb.model')) file.remove('xgb.model') +#' xgb.save(bst, "xgb.model") +#' bst1 <- xgb.load("xgb.model") +#' if (file.exists("xgb.model")) file.remove("xgb.model") #' print(xgb.attr(bst1, "my_attribute")) #' print(xgb.attributes(bst1)) #' @@ -632,22 +664,29 @@ xgb.attributes <- function(object) { object } -#' Accessors for model parameters as JSON string. +#' Accessors for model parameters as JSON string #' -#' @param object Object of class \code{xgb.Booster} +#' @param object Object of class `xgb.Booster`. #' @param value A JSON string. #' #' @examples -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") +#' #' ## Keep the number of threads to 1 for examples #' nthread <- 1 #' data.table::setDTthreads(nthread) #' train <- agaricus.train #' #' bst <- xgboost( -#' data = train$data, label = train$label, max_depth = 2, -#' eta = 1, nthread = nthread, nrounds = 2, objective = "binary:logistic" +#' data = train$data, +#' label = train$label, +#' max_depth = 2, +#' eta = 1, +#' nthread = nthread, +#' nrounds = 2, +#' objective = "binary:logistic" #' ) +#' #' config <- xgb.config(bst) #' #' @rdname xgb.config @@ -667,24 +706,31 @@ xgb.config <- function(object) { object } -#' Accessors for model parameters. +#' Accessors for model parameters #' #' Only the setter for xgboost parameters is currently implemented. #' -#' @param object Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}. -#' @param value a list (or an object coercible to a list) with the names of parameters to set +#' @param object Object of class `xgb.Booster` or `xgb.Booster.handle`. +#' @param value A list (or an object coercible to a list) with the names of parameters to set #' and the elements corresponding to parameter values. #' #' @details -#' Note that the setter would usually work more efficiently for \code{xgb.Booster.handle} -#' than for \code{xgb.Booster}, since only just a handle would need to be copied. +#' Note that the setter would usually work more efficiently for `xgb.Booster.handle` +#' than for `xgb.Booster`, since only just a handle would need to be copied. #' #' @examples -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") #' train <- agaricus.train #' -#' bst <- xgboost(data = train$data, label = train$label, max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +#' bst <- xgboost( +#' data = train$data, +#' label = train$label, +#' max_depth = 2, +#' eta = 1, +#' nthread = 2, +#' nrounds = 2, +#' objective = "binary:logistic" +#' ) #' #' xgb.parameters(bst) <- list(eta = 0.1) #' @@ -724,23 +770,31 @@ xgb.ntree <- function(bst) { #' Print xgb.Booster #' -#' Print information about xgb.Booster. +#' Print information about `xgb.Booster`. #' -#' @param x an xgb.Booster object -#' @param verbose whether to print detailed data (e.g., attribute values) -#' @param ... not currently used +#' @param x An `xgb.Booster` object. +#' @param verbose Whether to print detailed data (e.g., attribute values). +#' @param ... Not currently used. #' #' @examples -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") #' train <- agaricus.train -#' bst <- xgboost(data = train$data, label = train$label, max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") -#' attr(bst, 'myattr') <- 'memo' +#' +#' bst <- xgboost( +#' data = train$data, +#' label = train$label, +#' max_depth = 2, +#' eta = 1, +#' nthread = 2, +#' nrounds = 2, +#' objective = "binary:logistic" +#' ) +#' +#' attr(bst, "myattr") <- "memo" #' #' print(bst) -#' print(bst, verbose=TRUE) +#' print(bst, verbose = TRUE) #' -#' @method print xgb.Booster #' @export print.xgb.Booster <- function(x, verbose = FALSE, ...) { cat('##### xgb.Booster\n') diff --git a/R-package/R/xgb.create.features.R b/R-package/R/xgb.create.features.R index 98b03ea8a..baef3bb03 100644 --- a/R-package/R/xgb.create.features.R +++ b/R-package/R/xgb.create.features.R @@ -51,7 +51,7 @@ #' dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) #' dtest <- with(agaricus.test, xgb.DMatrix(data, label = label, nthread = 2)) #' -#' param <- list(max_depth=2, eta=1, silent=1, objective='binary:logistic') +#' param <- list(max_depth=2, eta=1, objective='binary:logistic') #' nrounds = 4 #' #' bst = xgb.train(params = param, data = dtrain, nrounds = nrounds, nthread = 2) diff --git a/R-package/R/xgb.plot.shap.R b/R-package/R/xgb.plot.shap.R index d9afd5546..d61bd23d4 100644 --- a/R-package/R/xgb.plot.shap.R +++ b/R-package/R/xgb.plot.shap.R @@ -7,7 +7,7 @@ #' \code{data}. When it is NULL, it is computed internally using \code{model} and \code{data}. #' @param features a vector of either column indices or of feature names to plot. When it is NULL, #' feature importance is calculated, and \code{top_n} high ranked features are taken. -#' @param top_n when \code{features} is NULL, top_n [1, 100] most important features in a model are taken. +#' @param top_n when \code{features} is NULL, top_n `[1, 100]` most important features in a model are taken. #' @param model an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} #' or \code{features} is missing. #' @param trees passed to \code{\link{xgb.importance}} when \code{features = NULL}. @@ -197,7 +197,7 @@ xgb.plot.shap <- function(data, shap_contrib = NULL, features = NULL, top_n = 1, #' hence allows us to see which features have a negative / positive contribution #' on the model prediction, and whether the contribution is different for larger #' or smaller values of the feature. We effectively try to replicate the -#' \code{summary_plot} function from https://github.com/shap/shap. +#' \code{summary_plot} function from . #' #' @inheritParams xgb.plot.shap #' diff --git a/R-package/R/xgboost.R b/R-package/R/xgboost.R index e60ea2de8..f61c535e2 100644 --- a/R-package/R/xgboost.R +++ b/R-package/R/xgboost.R @@ -40,10 +40,10 @@ xgboost <- function(data = NULL, label = NULL, missing = NA, weight = NULL, #' } #' #' @references -#' https://archive.ics.uci.edu/ml/datasets/Mushroom +#' #' #' Bache, K. & Lichman, M. (2013). UCI Machine Learning Repository -#' [http://archive.ics.uci.edu/ml]. Irvine, CA: University of California, +#' . Irvine, CA: University of California, #' School of Information and Computer Science. #' #' @docType data @@ -67,10 +67,10 @@ NULL #' } #' #' @references -#' https://archive.ics.uci.edu/ml/datasets/Mushroom +#' #' #' Bache, K. & Lichman, M. (2013). UCI Machine Learning Repository -#' [http://archive.ics.uci.edu/ml]. Irvine, CA: University of California, +#' . Irvine, CA: University of California, #' School of Information and Computer Science. #' #' @docType data diff --git a/R-package/man/agaricus.test.Rd b/R-package/man/agaricus.test.Rd index e3694ae0d..f90a5a0d2 100644 --- a/R-package/man/agaricus.test.Rd +++ b/R-package/man/agaricus.test.Rd @@ -19,15 +19,15 @@ UCI Machine Learning Repository. This data set includes the following fields: \itemize{ - \item \code{label} the label for each record - \item \code{data} a sparse Matrix of \code{dgCMatrix} class, with 126 columns. +\item \code{label} the label for each record +\item \code{data} a sparse Matrix of \code{dgCMatrix} class, with 126 columns. } } \references{ -https://archive.ics.uci.edu/ml/datasets/Mushroom +\url{https://archive.ics.uci.edu/ml/datasets/Mushroom} Bache, K. & Lichman, M. (2013). UCI Machine Learning Repository -[http://archive.ics.uci.edu/ml]. Irvine, CA: University of California, +\url{http://archive.ics.uci.edu/ml}. Irvine, CA: University of California, School of Information and Computer Science. } \keyword{datasets} diff --git a/R-package/man/agaricus.train.Rd b/R-package/man/agaricus.train.Rd index 92692c965..dd05410ba 100644 --- a/R-package/man/agaricus.train.Rd +++ b/R-package/man/agaricus.train.Rd @@ -19,15 +19,15 @@ UCI Machine Learning Repository. This data set includes the following fields: \itemize{ - \item \code{label} the label for each record - \item \code{data} a sparse Matrix of \code{dgCMatrix} class, with 126 columns. +\item \code{label} the label for each record +\item \code{data} a sparse Matrix of \code{dgCMatrix} class, with 126 columns. } } \references{ -https://archive.ics.uci.edu/ml/datasets/Mushroom +\url{https://archive.ics.uci.edu/ml/datasets/Mushroom} Bache, K. & Lichman, M. (2013). UCI Machine Learning Repository -[http://archive.ics.uci.edu/ml]. Irvine, CA: University of California, +\url{http://archive.ics.uci.edu/ml}. Irvine, CA: University of California, School of Information and Computer Science. } \keyword{datasets} diff --git a/R-package/man/cb.save.model.Rd b/R-package/man/cb.save.model.Rd index fd564b3e8..584fd69b7 100644 --- a/R-package/man/cb.save.model.Rd +++ b/R-package/man/cb.save.model.Rd @@ -13,7 +13,7 @@ cb.save.model(save_period = 0, save_name = "xgboost.model") \item{save_name}{the name or path for the saved model file. It can contain a \code{\link[base]{sprintf}} formatting specifier to include the integer iteration number in the file name. -E.g., with \code{save_name} = 'xgboost_%04d.model', +E.g., with \code{save_name} = 'xgboost_\%04d.model', the file saved at iteration 50 would be named "xgboost_0050.model".} } \description{ diff --git a/R-package/man/getinfo.Rd b/R-package/man/getinfo.Rd index 9503c2154..71f855d8a 100644 --- a/R-package/man/getinfo.Rd +++ b/R-package/man/getinfo.Rd @@ -23,15 +23,15 @@ Get information of an xgb.DMatrix object The \code{name} field can be one of the following: \itemize{ - \item \code{label} - \item \code{weight} - \item \code{base_margin} - \item \code{label_lower_bound} - \item \code{label_upper_bound} - \item \code{group} - \item \code{feature_type} - \item \code{feature_name} - \item \code{nrow} +\item \code{label} +\item \code{weight} +\item \code{base_margin} +\item \code{label_lower_bound} +\item \code{label_upper_bound} +\item \code{group} +\item \code{feature_type} +\item \code{feature_name} +\item \code{nrow} } See the documentation for \link{xgb.DMatrix} for more information about these fields. diff --git a/R-package/man/predict.xgb.Booster.Rd b/R-package/man/predict.xgb.Booster.Rd index c1e58f63b..135177dda 100644 --- a/R-package/man/predict.xgb.Booster.Rd +++ b/R-package/man/predict.xgb.Booster.Rd @@ -3,7 +3,7 @@ \name{predict.xgb.Booster} \alias{predict.xgb.Booster} \alias{predict.xgb.Booster.handle} -\title{Predict method for eXtreme Gradient Boosting model} +\title{Predict method for XGBoost model} \usage{ \method{predict}{xgb.Booster}( object, @@ -25,90 +25,86 @@ \method{predict}{xgb.Booster.handle}(object, ...) } \arguments{ -\item{object}{Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}} +\item{object}{Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}.} -\item{newdata}{takes \code{matrix}, \code{dgCMatrix}, \code{dgRMatrix}, \code{dsparseVector}, - local data file or \code{xgb.DMatrix}. +\item{newdata}{Takes \code{matrix}, \code{dgCMatrix}, \code{dgRMatrix}, \code{dsparseVector}, +local data file, or \code{xgb.DMatrix}. +For single-row predictions on sparse data, it is recommended to use the CSR format. +If passing a sparse vector, it will take it as a row vector.} - For single-row predictions on sparse data, it's recommended to use CSR format. If passing - a sparse vector, it will take it as a row vector.} +\item{missing}{Only used when input is a dense matrix. Pick a float value that represents +missing values in data (e.g., 0 or some other extreme value).} -\item{missing}{Missing is only used when input is dense matrix. Pick a float value that represents -missing values in data (e.g., sometimes 0 or some other extreme value is used).} - -\item{outputmargin}{whether the prediction should be returned in the for of original untransformed +\item{outputmargin}{Whether the prediction should be returned in the form of original untransformed sum of predictions from boosting iterations' results. E.g., setting \code{outputmargin=TRUE} for -logistic regression would result in predictions for log-odds instead of probabilities.} +logistic regression would return log-odds instead of probabilities.} \item{ntreelimit}{Deprecated, use \code{iterationrange} instead.} -\item{predleaf}{whether predict leaf index.} +\item{predleaf}{Whether to predict pre-tree leaf indices.} -\item{predcontrib}{whether to return feature contributions to individual predictions (see Details).} +\item{predcontrib}{Whether to return feature contributions to individual predictions (see Details).} -\item{approxcontrib}{whether to use a fast approximation for feature contributions (see Details).} +\item{approxcontrib}{Whether to use a fast approximation for feature contributions (see Details).} -\item{predinteraction}{whether to return contributions of feature interactions to individual predictions (see Details).} +\item{predinteraction}{Whether to return contributions of feature interactions to individual predictions (see Details).} -\item{reshape}{whether to reshape the vector of predictions to a matrix form when there are several -prediction outputs per case. This option has no effect when either of predleaf, predcontrib, -or predinteraction flags is TRUE.} +\item{reshape}{Whether to reshape the vector of predictions to matrix form when there are several +prediction outputs per case. No effect if \code{predleaf}, \code{predcontrib}, +or \code{predinteraction} is \code{TRUE}.} -\item{training}{whether is the prediction result used for training. For dart booster, +\item{training}{Whether the predictions are used for training. For dart booster, training predicting will perform dropout.} -\item{iterationrange}{Specifies which layer of trees are used in prediction. For -example, if a random forest is trained with 100 rounds. Specifying -`iterationrange=(1, 21)`, then only the forests built during [1, 21) (half open set) -rounds are used in this prediction. It's 1-based index just like R vector. When set -to \code{c(1, 1)} XGBoost will use all trees.} +\item{iterationrange}{Specifies which trees are used in prediction. For +example, take a random forest with 100 rounds. +With \code{iterationrange=c(1, 21)}, only the trees built during \verb{[1, 21)} (half open set) +rounds are used in this prediction. The index is 1-based just like an R vector. When set +to \code{c(1, 1)}, XGBoost will use all trees.} -\item{strict_shape}{Default is \code{FALSE}. When it's set to \code{TRUE}, output -type and shape of prediction are invariant to model type.} +\item{strict_shape}{Default is \code{FALSE}. When set to \code{TRUE}, the output +type and shape of predictions are invariant to the model type.} \item{...}{Not used.} } \value{ -The return type is different depending whether \code{strict_shape} is set to \code{TRUE}. By default, -for regression or binary classification, it returns a vector of length \code{nrows(newdata)}. -For multiclass classification, either a \code{num_class * nrows(newdata)} vector or -a \code{(nrows(newdata), num_class)} dimension matrix is returned, depending on -the \code{reshape} value. +The return type depends on \code{strict_shape}. If \code{FALSE} (default): +\itemize{ +\item For regression or binary classification: A vector of length \code{nrows(newdata)}. +\item For multiclass classification: A vector of length \code{num_class * nrows(newdata)} or +a \verb{(nrows(newdata), num_class)} matrix, depending on the \code{reshape} value. +\item When \code{predleaf = TRUE}: A matrix with one column per tree. +\item When \code{predcontrib = TRUE}: When not multiclass, a matrix with +\code{ num_features + 1} columns. The last "+ 1" column corresponds to the baseline value. +In the multiclass case, a list of \code{num_class} such matrices. +The contribution values are on the scale of untransformed margin +(e.g., for binary classification, the values are log-odds deviations from the baseline). +\item When \code{predinteraction = TRUE}: When not multiclass, the output is a 3d array of +dimension \code{c(nrow, num_features + 1, num_features + 1)}. The off-diagonal (in the last two dimensions) +elements represent different feature interaction contributions. The array is symmetric WRT the last +two dimensions. The "+ 1" columns corresponds to the baselines. Summing this array along the last dimension should +produce practically the same result as \code{predcontrib = TRUE}. +In the multiclass case, a list of \code{num_class} such arrays. +} -When \code{predleaf = TRUE}, the output is a matrix object with the -number of columns corresponding to the number of trees. - -When \code{predcontrib = TRUE} and it is not a multiclass setting, the output is a matrix object with -\code{num_features + 1} columns. The last "+ 1" column in a matrix corresponds to bias. -For a multiclass case, a list of \code{num_class} elements is returned, where each element is -such a matrix. The contribution values are on the scale of untransformed margin -(e.g., for binary classification would mean that the contributions are log-odds deviations from bias). - -When \code{predinteraction = TRUE} and it is not a multiclass setting, the output is a 3d array with -dimensions \code{c(nrow, num_features + 1, num_features + 1)}. The off-diagonal (in the last two dimensions) -elements represent different features interaction contributions. The array is symmetric WRT the last -two dimensions. The "+ 1" columns corresponds to bias. Summing this array along the last dimension should -produce practically the same result as predict with \code{predcontrib = TRUE}. -For a multiclass case, a list of \code{num_class} elements is returned, where each element is -such an array. - -When \code{strict_shape} is set to \code{TRUE}, the output is always an array. For -normal prediction, the output is a 2-dimension array \code{(num_class, nrow(newdata))}. - -For \code{predcontrib = TRUE}, output is \code{(ncol(newdata) + 1, num_class, nrow(newdata))} -For \code{predinteraction = TRUE}, output is \code{(ncol(newdata) + 1, ncol(newdata) + 1, num_class, nrow(newdata))} -For \code{predleaf = TRUE}, output is \code{(n_trees_in_forest, num_class, n_iterations, nrow(newdata))} +When \code{strict_shape = TRUE}, the output is always an array: +\itemize{ +\item For normal predictions, the output has dimension \verb{(num_class, nrow(newdata))}. +\item For \code{predcontrib = TRUE}, the dimension is \verb{(ncol(newdata) + 1, num_class, nrow(newdata))}. +\item For \code{predinteraction = TRUE}, the dimension is \verb{(ncol(newdata) + 1, ncol(newdata) + 1, num_class, nrow(newdata))}. +\item For \code{predleaf = TRUE}, the dimension is \verb{(n_trees_in_forest, num_class, n_iterations, nrow(newdata))}. +} } \description{ Predicted values based on either xgboost model or model handle object. } \details{ -Note that \code{iterationrange} would currently do nothing for predictions from gblinear, -since gblinear doesn't keep its boosting history. +Note that \code{iterationrange} would currently do nothing for predictions from "gblinear", +since "gblinear" doesn't keep its boosting history. One possible practical applications of the \code{predleaf} option is to use the model as a generator of new features which capture non-linearity and interactions, -e.g., as implemented in \code{\link{xgb.create.features}}. +e.g., as implemented in \code{\link[=xgb.create.features]{xgb.create.features()}}. Setting \code{predcontrib = TRUE} allows to calculate contributions of each feature to individual predictions. For "gblinear" booster, feature contributions are simply linear terms @@ -124,14 +120,14 @@ Since it quadratically depends on the number of features, it is recommended to p of the most important features first. See below about the format of the returned results. The \code{predict()} method uses as many threads as defined in \code{xgb.Booster} object (all by default). -If you want to change their number, then assign a new number to \code{nthread} using \code{\link{xgb.parameters<-}}. -Note also that converting a matrix to \code{\link{xgb.DMatrix}} uses multiple threads too. +If you want to change their number, assign a new number to \code{nthread} using \code{\link[=xgb.parameters<-]{xgb.parameters<-()}}. +Note that converting a matrix to \code{\link[=xgb.DMatrix]{xgb.DMatrix()}} uses multiple threads too. } \examples{ ## binary classification: -data(agaricus.train, package='xgboost') -data(agaricus.test, package='xgboost') +data(agaricus.train, package = "xgboost") +data(agaricus.test, package = "xgboost") ## Keep the number of threads to 2 for examples nthread <- 2 @@ -140,8 +136,16 @@ data.table::setDTthreads(nthread) train <- agaricus.train test <- agaricus.test -bst <- xgboost(data = train$data, label = train$label, max_depth = 2, - eta = 0.5, nthread = nthread, nrounds = 5, objective = "binary:logistic") +bst <- xgboost( + data = train$data, + label = train$label, + max_depth = 2, + eta = 0.5, + nthread = nthread, + nrounds = 5, + objective = "binary:logistic" +) + # use all trees by default pred <- predict(bst, test$data) # use only the 1st tree @@ -173,39 +177,63 @@ par(mar = old_mar) lb <- as.numeric(iris$Species) - 1 num_class <- 3 + set.seed(11) -bst <- xgboost(data = as.matrix(iris[, -5]), label = lb, - max_depth = 4, eta = 0.5, nthread = 2, nrounds = 10, subsample = 0.5, - objective = "multi:softprob", num_class = num_class) + +bst <- xgboost( + data = as.matrix(iris[, -5]), + label = lb, + max_depth = 4, + eta = 0.5, + nthread = 2, + nrounds = 10, + subsample = 0.5, + objective = "multi:softprob", + num_class = num_class +) + # predict for softmax returns num_class probability numbers per case: pred <- predict(bst, as.matrix(iris[, -5])) str(pred) # reshape it to a num_class-columns matrix -pred <- matrix(pred, ncol=num_class, byrow=TRUE) +pred <- matrix(pred, ncol = num_class, byrow = TRUE) # convert the probabilities to softmax labels pred_labels <- max.col(pred) - 1 # the following should result in the same error as seen in the last iteration -sum(pred_labels != lb)/length(lb) +sum(pred_labels != lb) / length(lb) -# compare that to the predictions from softmax: +# compare with predictions from softmax: set.seed(11) -bst <- xgboost(data = as.matrix(iris[, -5]), label = lb, - max_depth = 4, eta = 0.5, nthread = 2, nrounds = 10, subsample = 0.5, - objective = "multi:softmax", num_class = num_class) + +bst <- xgboost( + data = as.matrix(iris[, -5]), + label = lb, + max_depth = 4, + eta = 0.5, + nthread = 2, + nrounds = 10, + subsample = 0.5, + objective = "multi:softmax", + num_class = num_class +) + pred <- predict(bst, as.matrix(iris[, -5])) str(pred) all.equal(pred, pred_labels) # prediction from using only 5 iterations should result # in the same error as seen in iteration 5: -pred5 <- predict(bst, as.matrix(iris[, -5]), iterationrange=c(1, 6)) -sum(pred5 != lb)/length(lb) +pred5 <- predict(bst, as.matrix(iris[, -5]), iterationrange = c(1, 6)) +sum(pred5 != lb) / length(lb) } \references{ -Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", NIPS Proceedings 2017, \url{https://arxiv.org/abs/1705.07874} - -Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", \url{https://arxiv.org/abs/1706.06060} +\enumerate{ +\item Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", +NIPS Proceedings 2017, \url{https://arxiv.org/abs/1705.07874} +\item Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", +\url{https://arxiv.org/abs/1706.06060} +} } \seealso{ -\code{\link{xgb.train}}. +\code{\link[=xgb.train]{xgb.train()}} } diff --git a/R-package/man/print.xgb.Booster.Rd b/R-package/man/print.xgb.Booster.Rd index d684882f5..4d09bb5ec 100644 --- a/R-package/man/print.xgb.Booster.Rd +++ b/R-package/man/print.xgb.Booster.Rd @@ -7,23 +7,32 @@ \method{print}{xgb.Booster}(x, verbose = FALSE, ...) } \arguments{ -\item{x}{an xgb.Booster object} +\item{x}{An \code{xgb.Booster} object.} -\item{verbose}{whether to print detailed data (e.g., attribute values)} +\item{verbose}{Whether to print detailed data (e.g., attribute values).} -\item{...}{not currently used} +\item{...}{Not currently used.} } \description{ -Print information about xgb.Booster. +Print information about \code{xgb.Booster}. } \examples{ -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") train <- agaricus.train -bst <- xgboost(data = train$data, label = train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") -attr(bst, 'myattr') <- 'memo' + +bst <- xgboost( + data = train$data, + label = train$label, + max_depth = 2, + eta = 1, + nthread = 2, + nrounds = 2, + objective = "binary:logistic" +) + +attr(bst, "myattr") <- "memo" print(bst) -print(bst, verbose=TRUE) +print(bst, verbose = TRUE) } diff --git a/R-package/man/xgb.Booster.complete.Rd b/R-package/man/xgb.Booster.complete.Rd index 214694565..0adb1a69f 100644 --- a/R-package/man/xgb.Booster.complete.Rd +++ b/R-package/man/xgb.Booster.complete.Rd @@ -2,14 +2,14 @@ % Please edit documentation in R/xgb.Booster.R \name{xgb.Booster.complete} \alias{xgb.Booster.complete} -\title{Restore missing parts of an incomplete xgb.Booster object.} +\title{Restore missing parts of an incomplete xgb.Booster object} \usage{ xgb.Booster.complete(object, saveraw = TRUE) } \arguments{ -\item{object}{object of class \code{xgb.Booster}} +\item{object}{Object of class \code{xgb.Booster}.} -\item{saveraw}{a flag indicating whether to append \code{raw} Booster memory dump data +\item{saveraw}{A flag indicating whether to append \code{raw} Booster memory dump data when it doesn't already exist.} } \value{ @@ -27,15 +27,24 @@ While this method is primarily for internal use, it might be useful in some prac E.g., when an \code{xgb.Booster} model is saved as an R object and then is loaded as an R object, its handle (pointer) to an internal xgboost model would be invalid. The majority of xgboost methods should still work for such a model object since those methods would be using -\code{xgb.Booster.complete} internally. However, one might find it to be more efficient to call the -\code{xgb.Booster.complete} function explicitly once after loading a model as an R-object. +\code{xgb.Booster.complete()} internally. However, one might find it to be more efficient to call the +\code{xgb.Booster.complete()} function explicitly once after loading a model as an R-object. That would prevent further repeated implicit reconstruction of an internal booster model. } \examples{ -data(agaricus.train, package='xgboost') -bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +data(agaricus.train, package = "xgboost") + +bst <- xgboost( + data = agaricus.train$data, + label = agaricus.train$label, + max_depth = 2, + eta = 1, + nthread = 2, + nrounds = 2, + objective = "binary:logistic" +) + saveRDS(bst, "xgb.model.rds") # Warning: The resulting RDS file is only compatible with the current XGBoost version. diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index a1ef39f0b..01436ba14 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -38,7 +38,8 @@ so it doesn't make sense to assign weights to individual data points.} \item{base_margin}{Base margin used for boosting from existing model. - In the case of multi-output models, one can also pass multi-dimensional base_margin.} +\if{html}{\out{
}}\preformatted{ In the case of multi-output models, one can also pass multi-dimensional base_margin. +}\if{html}{\out{
}}} \item{missing}{a float value to represents missing values in data (used only when input is a dense matrix). It is useful when a 0 or some other extreme value represents missing values in data.} @@ -62,16 +63,17 @@ frame and matrix.} \item{enable_categorical}{Experimental support of specializing for categorical features. - If passing 'TRUE' and 'data' is a data frame, - columns of categorical types will automatically - be set to be of categorical type (feature_type='c') in the resulting DMatrix. +\if{html}{\out{
}}\preformatted{ If passing 'TRUE' and 'data' is a data frame, + columns of categorical types will automatically + be set to be of categorical type (feature_type='c') in the resulting DMatrix. - If passing 'FALSE' and 'data' is a data frame with categorical columns, - it will result in an error being thrown. + If passing 'FALSE' and 'data' is a data frame with categorical columns, + it will result in an error being thrown. - If 'data' is not a data frame, this argument is ignored. + If 'data' is not a data frame, this argument is ignored. - JSON/UBJSON serialization format is required for this.} + JSON/UBJSON serialization format is required for this. +}\if{html}{\out{
}}} } \description{ Construct xgb.DMatrix object from either a dense matrix, a sparse matrix, or a local file. diff --git a/R-package/man/xgb.attr.Rd b/R-package/man/xgb.attr.Rd index 03779e420..9203e6281 100644 --- a/R-package/man/xgb.attr.Rd +++ b/R-package/man/xgb.attr.Rd @@ -5,7 +5,7 @@ \alias{xgb.attr<-} \alias{xgb.attributes} \alias{xgb.attributes<-} -\title{Accessors for serializable attributes of a model.} +\title{Accessors for serializable attributes of a model} \usage{ xgb.attr(object, name) @@ -18,62 +18,70 @@ xgb.attributes(object) <- value \arguments{ \item{object}{Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}.} -\item{name}{a non-empty character string specifying which attribute is to be accessed.} +\item{name}{A non-empty character string specifying which attribute is to be accessed.} -\item{value}{a value of an attribute for \code{xgb.attr<-}; for \code{xgb.attributes<-} -it's a list (or an object coercible to a list) with the names of attributes to set +\item{value}{For \verb{xgb.attr<-}, a value of an attribute; for \verb{xgb.attributes<-}, +it is a list (or an object coercible to a list) with the names of attributes to set and the elements corresponding to attribute values. Non-character values are converted to character. -When attribute value is not a scalar, only the first index is used. +When an attribute value is not a scalar, only the first index is used. Use \code{NULL} to remove an attribute.} } \value{ -\code{xgb.attr} returns either a string value of an attribute +\itemize{ +\item \code{xgb.attr()} returns either a string value of an attribute or \code{NULL} if an attribute wasn't stored in a model. - -\code{xgb.attributes} returns a list of all attribute stored in a model +\item \code{xgb.attributes()} returns a list of all attributes stored in a model or \code{NULL} if a model has no stored attributes. } +} \description{ These methods allow to manipulate the key-value attribute strings of an xgboost model. } \details{ -The primary purpose of xgboost model attributes is to store some meta-data about the model. +The primary purpose of xgboost model attributes is to store some meta data about the model. Note that they are a separate concept from the object attributes in R. Specifically, they refer to key-value strings that can be attached to an xgboost model, stored together with the model's binary representation, and accessed later (from R or any other interface). -In contrast, any R-attribute assigned to an R-object of \code{xgb.Booster} class -would not be saved by \code{xgb.save} because an xgboost model is an external memory object +In contrast, any R attribute assigned to an R object of \code{xgb.Booster} class +would not be saved by \code{\link[=xgb.save]{xgb.save()}} because an xgboost model is an external memory object and its serialization is handled externally. Also, setting an attribute that has the same name as one of xgboost's parameters wouldn't change the value of that parameter for a model. -Use \code{\link{xgb.parameters<-}} to set or change model parameters. +Use \code{\link[=xgb.parameters<-]{xgb.parameters<-()}} to set or change model parameters. The attribute setters would usually work more efficiently for \code{xgb.Booster.handle} than for \code{xgb.Booster}, since only just a handle (pointer) would need to be copied. That would only matter if attributes need to be set many times. Note, however, that when feeding a handle of an \code{xgb.Booster} object to the attribute setters, the raw model cache of an \code{xgb.Booster} object would not be automatically updated, -and it would be user's responsibility to call \code{xgb.serialize} to update it. +and it would be the user's responsibility to call \code{\link[=xgb.serialize]{xgb.serialize()}} to update it. -The \code{xgb.attributes<-} setter either updates the existing or adds one or several attributes, +The \verb{xgb.attributes<-} setter either updates the existing or adds one or several attributes, but it doesn't delete the other existing attributes. } \examples{ -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") train <- agaricus.train -bst <- xgboost(data = train$data, label = train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +bst <- xgboost( + data = train$data, + label = train$label, + max_depth = 2, + eta = 1, + nthread = 2, + nrounds = 2, + objective = "binary:logistic" +) xgb.attr(bst, "my_attribute") <- "my attribute value" print(xgb.attr(bst, "my_attribute")) xgb.attributes(bst) <- list(a = 123, b = "abc") -xgb.save(bst, 'xgb.model') -bst1 <- xgb.load('xgb.model') -if (file.exists('xgb.model')) file.remove('xgb.model') +xgb.save(bst, "xgb.model") +bst1 <- xgb.load("xgb.model") +if (file.exists("xgb.model")) file.remove("xgb.model") print(xgb.attr(bst1, "my_attribute")) print(xgb.attributes(bst1)) diff --git a/R-package/man/xgb.config.Rd b/R-package/man/xgb.config.Rd index 35545cc77..83040b877 100644 --- a/R-package/man/xgb.config.Rd +++ b/R-package/man/xgb.config.Rd @@ -3,31 +3,38 @@ \name{xgb.config} \alias{xgb.config} \alias{xgb.config<-} -\title{Accessors for model parameters as JSON string.} +\title{Accessors for model parameters as JSON string} \usage{ xgb.config(object) xgb.config(object) <- value } \arguments{ -\item{object}{Object of class \code{xgb.Booster}} +\item{object}{Object of class \code{xgb.Booster}.} \item{value}{A JSON string.} } \description{ -Accessors for model parameters as JSON string. +Accessors for model parameters as JSON string } \examples{ -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") + ## Keep the number of threads to 1 for examples nthread <- 1 data.table::setDTthreads(nthread) train <- agaricus.train bst <- xgboost( - data = train$data, label = train$label, max_depth = 2, - eta = 1, nthread = nthread, nrounds = 2, objective = "binary:logistic" + data = train$data, + label = train$label, + max_depth = 2, + eta = 1, + nthread = nthread, + nrounds = 2, + objective = "binary:logistic" ) + config <- xgb.config(bst) } diff --git a/R-package/man/xgb.create.features.Rd b/R-package/man/xgb.create.features.Rd index 64d4af158..68b561997 100644 --- a/R-package/man/xgb.create.features.Rd +++ b/R-package/man/xgb.create.features.Rd @@ -48,7 +48,7 @@ be the binary vector \code{[0, 1, 0, 1, 0]}, where the first 3 entries correspond to the leaves of the first subtree and last 2 to those of the second subtree. -[...] +\link{...} We can understand boosted decision tree based transformation as a supervised feature encoding that @@ -62,7 +62,7 @@ data(agaricus.test, package='xgboost') dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) dtest <- with(agaricus.test, xgb.DMatrix(data, label = label, nthread = 2)) -param <- list(max_depth=2, eta=1, silent=1, objective='binary:logistic') +param <- list(max_depth=2, eta=1, objective='binary:logistic') nrounds = 4 bst = xgb.train(params = param, data = dtrain, nrounds = nrounds, nthread = 2) diff --git a/R-package/man/xgb.cv.Rd b/R-package/man/xgb.cv.Rd index 9c69eb97f..2d8508c4d 100644 --- a/R-package/man/xgb.cv.Rd +++ b/R-package/man/xgb.cv.Rd @@ -29,22 +29,22 @@ xgb.cv( } \arguments{ \item{params}{the list of parameters. The complete list of parameters is - available in the \href{http://xgboost.readthedocs.io/en/latest/parameter.html}{online documentation}. Below - is a shorter summary: +available in the \href{http://xgboost.readthedocs.io/en/latest/parameter.html}{online documentation}. Below +is a shorter summary: \itemize{ - \item \code{objective} objective function, common ones are - \itemize{ - \item \code{reg:squarederror} Regression with squared loss. - \item \code{binary:logistic} logistic regression for classification. - \item See \code{\link[=xgb.train]{xgb.train}()} for complete list of objectives. - } - \item \code{eta} step size of each boosting step - \item \code{max_depth} maximum depth of the tree - \item \code{nthread} number of thread used in training, if not set, all threads are used +\item \code{objective} objective function, common ones are +\itemize{ +\item \code{reg:squarederror} Regression with squared loss. +\item \code{binary:logistic} logistic regression for classification. +\item See \code{\link[=xgb.train]{xgb.train}()} for complete list of objectives. +} +\item \code{eta} step size of each boosting step +\item \code{max_depth} maximum depth of the tree +\item \code{nthread} number of thread used in training, if not set, all threads are used } - See \code{\link{xgb.train}} for further details. - See also demo/ for walkthrough example in R.} +See \code{\link{xgb.train}} for further details. +See also demo/ for walkthrough example in R.} \item{data}{takes an \code{xgb.DMatrix}, \code{matrix}, or \code{dgCMatrix} as the input.} @@ -64,17 +64,17 @@ from each CV model. This parameter engages the \code{\link{cb.cv.predict}} callb \item{showsd}{\code{boolean}, whether to show standard deviation of cross validation} \item{metrics, }{list of evaluation metrics to be used in cross validation, - when it is not specified, the evaluation metric is chosen according to objective function. - Possible options are: +when it is not specified, the evaluation metric is chosen according to objective function. +Possible options are: \itemize{ - \item \code{error} binary classification error rate - \item \code{rmse} Rooted mean square error - \item \code{logloss} negative log-likelihood function - \item \code{mae} Mean absolute error - \item \code{mape} Mean absolute percentage error - \item \code{auc} Area under curve - \item \code{aucpr} Area under PR curve - \item \code{merror} Exact matching error, used to evaluate multi-class classification +\item \code{error} binary classification error rate +\item \code{rmse} Rooted mean square error +\item \code{logloss} negative log-likelihood function +\item \code{mae} Mean absolute error +\item \code{mape} Mean absolute percentage error +\item \code{auc} Area under curve +\item \code{aucpr} Area under PR curve +\item \code{merror} Exact matching error, used to evaluate multi-class classification }} \item{obj}{customized objective function. Returns gradient and second order @@ -120,26 +120,26 @@ to customize the training process.} \value{ An object of class \code{xgb.cv.synchronous} with the following elements: \itemize{ - \item \code{call} a function call. - \item \code{params} parameters that were passed to the xgboost library. Note that it does not - capture parameters changed by the \code{\link{cb.reset.parameters}} callback. - \item \code{callbacks} callback functions that were either automatically assigned or - explicitly passed. - \item \code{evaluation_log} evaluation history stored as a \code{data.table} with the - first column corresponding to iteration number and the rest corresponding to the - CV-based evaluation means and standard deviations for the training and test CV-sets. - It is created by the \code{\link{cb.evaluation.log}} callback. - \item \code{niter} number of boosting iterations. - \item \code{nfeatures} number of features in training data. - \item \code{folds} the list of CV folds' indices - either those passed through the \code{folds} - parameter or randomly generated. - \item \code{best_iteration} iteration number with the best evaluation metric value - (only available with early stopping). - \item \code{best_ntreelimit} and the \code{ntreelimit} Deprecated attributes, use \code{best_iteration} instead. - \item \code{pred} CV prediction values available when \code{prediction} is set. - It is either vector or matrix (see \code{\link{cb.cv.predict}}). - \item \code{models} a list of the CV folds' models. It is only available with the explicit - setting of the \code{cb.cv.predict(save_models = TRUE)} callback. +\item \code{call} a function call. +\item \code{params} parameters that were passed to the xgboost library. Note that it does not +capture parameters changed by the \code{\link{cb.reset.parameters}} callback. +\item \code{callbacks} callback functions that were either automatically assigned or +explicitly passed. +\item \code{evaluation_log} evaluation history stored as a \code{data.table} with the +first column corresponding to iteration number and the rest corresponding to the +CV-based evaluation means and standard deviations for the training and test CV-sets. +It is created by the \code{\link{cb.evaluation.log}} callback. +\item \code{niter} number of boosting iterations. +\item \code{nfeatures} number of features in training data. +\item \code{folds} the list of CV folds' indices - either those passed through the \code{folds} +parameter or randomly generated. +\item \code{best_iteration} iteration number with the best evaluation metric value +(only available with early stopping). +\item \code{best_ntreelimit} and the \code{ntreelimit} Deprecated attributes, use \code{best_iteration} instead. +\item \code{pred} CV prediction values available when \code{prediction} is set. +It is either vector or matrix (see \code{\link{cb.cv.predict}}). +\item \code{models} a list of the CV folds' models. It is only available with the explicit +setting of the \code{cb.cv.predict(save_models = TRUE)} callback. } } \description{ diff --git a/R-package/man/xgb.importance.Rd b/R-package/man/xgb.importance.Rd index d9367b211..12daca365 100644 --- a/R-package/man/xgb.importance.Rd +++ b/R-package/man/xgb.importance.Rd @@ -35,20 +35,20 @@ is zero-based (e.g., use \code{trees = 0:4} for first 5 trees).} \value{ For a tree model, a \code{data.table} with the following columns: \itemize{ - \item \code{Features} names of the features used in the model; - \item \code{Gain} represents fractional contribution of each feature to the model based on - the total gain of this feature's splits. Higher percentage means a more important - predictive feature. - \item \code{Cover} metric of the number of observation related to this feature; - \item \code{Frequency} percentage representing the relative number of times - a feature have been used in trees. +\item \code{Features} names of the features used in the model; +\item \code{Gain} represents fractional contribution of each feature to the model based on +the total gain of this feature's splits. Higher percentage means a more important +predictive feature. +\item \code{Cover} metric of the number of observation related to this feature; +\item \code{Frequency} percentage representing the relative number of times +a feature have been used in trees. } A linear model's importance \code{data.table} has the following columns: \itemize{ - \item \code{Features} names of the features used in the model; - \item \code{Weight} the linear coefficient of this feature; - \item \code{Class} (only for multiclass models) class label. +\item \code{Features} names of the features used in the model; +\item \code{Weight} the linear coefficient of this feature; +\item \code{Class} (only for multiclass models) class label. } If \code{feature_names} is not provided and \code{model} doesn't have \code{feature_names}, diff --git a/R-package/man/xgb.model.dt.tree.Rd b/R-package/man/xgb.model.dt.tree.Rd index 5a17f9d90..131830bde 100644 --- a/R-package/man/xgb.model.dt.tree.Rd +++ b/R-package/man/xgb.model.dt.tree.Rd @@ -41,18 +41,18 @@ A \code{data.table} with detailed information about model trees' nodes. The columns of the \code{data.table} are: \itemize{ - \item \code{Tree}: integer ID of a tree in a model (zero-based index) - \item \code{Node}: integer ID of a node in a tree (zero-based index) - \item \code{ID}: character identifier of a node in a model (only when \code{use_int_id=FALSE}) - \item \code{Feature}: for a branch node, it's a feature id or name (when available); - for a leaf note, it simply labels it as \code{'Leaf'} - \item \code{Split}: location of the split for a branch node (split condition is always "less than") - \item \code{Yes}: ID of the next node when the split condition is met - \item \code{No}: ID of the next node when the split condition is not met - \item \code{Missing}: ID of the next node when branch value is missing - \item \code{Quality}: either the split gain (change in loss) or the leaf value - \item \code{Cover}: metric related to the number of observation either seen by a split - or collected by a leaf during training. +\item \code{Tree}: integer ID of a tree in a model (zero-based index) +\item \code{Node}: integer ID of a node in a tree (zero-based index) +\item \code{ID}: character identifier of a node in a model (only when \code{use_int_id=FALSE}) +\item \code{Feature}: for a branch node, it's a feature id or name (when available); +for a leaf note, it simply labels it as \code{'Leaf'} +\item \code{Split}: location of the split for a branch node (split condition is always "less than") +\item \code{Yes}: ID of the next node when the split condition is met +\item \code{No}: ID of the next node when the split condition is not met +\item \code{Missing}: ID of the next node when branch value is missing +\item \code{Quality}: either the split gain (change in loss) or the leaf value +\item \code{Cover}: metric related to the number of observation either seen by a split +or collected by a leaf during training. } When \code{use_int_id=FALSE}, columns "Yes", "No", and "Missing" point to model-wide node identifiers diff --git a/R-package/man/xgb.parameters.Rd b/R-package/man/xgb.parameters.Rd index ab2695650..5305afa51 100644 --- a/R-package/man/xgb.parameters.Rd +++ b/R-package/man/xgb.parameters.Rd @@ -2,14 +2,14 @@ % Please edit documentation in R/xgb.Booster.R \name{xgb.parameters<-} \alias{xgb.parameters<-} -\title{Accessors for model parameters.} +\title{Accessors for model parameters} \usage{ xgb.parameters(object) <- value } \arguments{ \item{object}{Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}.} -\item{value}{a list (or an object coercible to a list) with the names of parameters to set +\item{value}{A list (or an object coercible to a list) with the names of parameters to set and the elements corresponding to parameter values.} } \description{ @@ -20,11 +20,18 @@ Note that the setter would usually work more efficiently for \code{xgb.Booster.h than for \code{xgb.Booster}, since only just a handle would need to be copied. } \examples{ -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") train <- agaricus.train -bst <- xgboost(data = train$data, label = train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +bst <- xgboost( + data = train$data, + label = train$label, + max_depth = 2, + eta = 1, + nthread = 2, + nrounds = 2, + objective = "binary:logistic" +) xgb.parameters(bst) <- list(eta = 0.1) diff --git a/R-package/man/xgb.plot.deepness.Rd b/R-package/man/xgb.plot.deepness.Rd index 9e23ac130..12c5c68e2 100644 --- a/R-package/man/xgb.plot.deepness.Rd +++ b/R-package/man/xgb.plot.deepness.Rd @@ -44,9 +44,9 @@ Visualizes distributions related to depth of tree leafs. When \code{which="2x1"}, two distributions with respect to the leaf depth are plotted on top of each other: \itemize{ - \item the distribution of the number of leafs in a tree model at a certain depth; - \item the distribution of average weighted number of observations ("cover") - ending up in leafs at certain depth. +\item the distribution of the number of leafs in a tree model at a certain depth; +\item the distribution of average weighted number of observations ("cover") +ending up in leafs at certain depth. } Those could be helpful in determining sensible ranges of the \code{max_depth} and \code{min_child_weight} parameters. diff --git a/R-package/man/xgb.plot.shap.Rd b/R-package/man/xgb.plot.shap.Rd index 6f2d0dfa6..75f8d2d0f 100644 --- a/R-package/man/xgb.plot.shap.Rd +++ b/R-package/man/xgb.plot.shap.Rd @@ -41,7 +41,7 @@ xgb.plot.shap( \item{features}{a vector of either column indices or of feature names to plot. When it is NULL, feature importance is calculated, and \code{top_n} high ranked features are taken.} -\item{top_n}{when \code{features} is NULL, top_n [1, 100] most important features in a model are taken.} +\item{top_n}{when \code{features} is NULL, top_n \verb{[1, 100]} most important features in a model are taken.} \item{model}{an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} or \code{features} is missing.} @@ -94,8 +94,8 @@ more than 5 distinct values.} \value{ In addition to producing plots (when \code{plot=TRUE}), it silently returns a list of two matrices: \itemize{ - \item \code{data} the values of selected features; - \item \code{shap_contrib} the contributions of selected features. +\item \code{data} the values of selected features; +\item \code{shap_contrib} the contributions of selected features. } } \description{ diff --git a/R-package/man/xgb.plot.shap.summary.Rd b/R-package/man/xgb.plot.shap.summary.Rd index 3ff8af21c..910119e6f 100644 --- a/R-package/man/xgb.plot.shap.summary.Rd +++ b/R-package/man/xgb.plot.shap.summary.Rd @@ -38,7 +38,7 @@ xgb.plot.shap.summary( \item{features}{a vector of either column indices or of feature names to plot. When it is NULL, feature importance is calculated, and \code{top_n} high ranked features are taken.} -\item{top_n}{when \code{features} is NULL, top_n [1, 100] most important features in a model are taken.} +\item{top_n}{when \code{features} is NULL, top_n \verb{[1, 100]} most important features in a model are taken.} \item{model}{an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} or \code{features} is missing.} @@ -67,12 +67,12 @@ Each point (observation) is coloured based on its feature value. The plot hence allows us to see which features have a negative / positive contribution on the model prediction, and whether the contribution is different for larger or smaller values of the feature. We effectively try to replicate the -\code{summary_plot} function from https://github.com/shap/shap. +\code{summary_plot} function from \url{https://github.com/shap/shap}. } \examples{ # See \code{\link{xgb.plot.shap}}. } \seealso{ \code{\link{xgb.plot.shap}}, \code{\link{xgb.ggplot.shap.summary}}, - \url{https://github.com/shap/shap} +\url{https://github.com/shap/shap} } diff --git a/R-package/man/xgb.plot.tree.Rd b/R-package/man/xgb.plot.tree.Rd index d419eb76a..224e393ce 100644 --- a/R-package/man/xgb.plot.tree.Rd +++ b/R-package/man/xgb.plot.tree.Rd @@ -52,14 +52,14 @@ Read a tree model text dump and plot the model. The content of each node is organised that way: \itemize{ - \item Feature name. - \item \code{Cover}: The sum of second order gradient of training data classified to the leaf. - If it is square loss, this simply corresponds to the number of instances seen by a split - or collected by a leaf during training. - The deeper in the tree a node is, the lower this metric will be. - \item \code{Gain} (for split nodes): the information gain metric of a split - (corresponds to the importance of the node in the model). - \item \code{Value} (for leafs): the margin value that the leaf may contribute to prediction. +\item Feature name. +\item \code{Cover}: The sum of second order gradient of training data classified to the leaf. +If it is square loss, this simply corresponds to the number of instances seen by a split +or collected by a leaf during training. +The deeper in the tree a node is, the lower this metric will be. +\item \code{Gain} (for split nodes): the information gain metric of a split +(corresponds to the importance of the node in the model). +\item \code{Value} (for leafs): the margin value that the leaf may contribute to prediction. } The tree root nodes also indicate the Tree index (0-based). diff --git a/R-package/man/xgb.save.raw.Rd b/R-package/man/xgb.save.raw.Rd index c7c93a734..083551933 100644 --- a/R-package/man/xgb.save.raw.Rd +++ b/R-package/man/xgb.save.raw.Rd @@ -12,9 +12,9 @@ xgb.save.raw(model, raw_format = "deprecated") \item{raw_format}{The format for encoding the booster. Available options are \itemize{ - \item \code{json}: Encode the booster into JSON text document. - \item \code{ubj}: Encode the booster into Universal Binary JSON. - \item \code{deprecated}: Encode the booster into old customized binary format. +\item \code{json}: Encode the booster into JSON text document. +\item \code{ubj}: Encode the booster into Universal Binary JSON. +\item \code{deprecated}: Encode the booster into old customized binary format. } Right now the default is \code{deprecated} but will be changed to \code{ubj} in upcoming release.} diff --git a/R-package/man/xgb.shap.data.Rd b/R-package/man/xgb.shap.data.Rd index 2f0e4adea..6c4336cde 100644 --- a/R-package/man/xgb.shap.data.Rd +++ b/R-package/man/xgb.shap.data.Rd @@ -27,7 +27,7 @@ xgb.shap.data( \item{features}{a vector of either column indices or of feature names to plot. When it is NULL, feature importance is calculated, and \code{top_n} high ranked features are taken.} -\item{top_n}{when \code{features} is NULL, top_n [1, 100] most important features in a model are taken.} +\item{top_n}{when \code{features} is NULL, top_n \verb{[1, 100]} most important features in a model are taken.} \item{model}{an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} or \code{features} is missing.} @@ -45,8 +45,8 @@ it is set so that up to 100K data points are used.} } \value{ A list containing: 'data', a matrix containing sample observations - and their feature values; 'shap_contrib', a matrix containing the SHAP contribution - values for these observations. +and their feature values; 'shap_contrib', a matrix containing the SHAP contribution +values for these observations. } \description{ Prepare data for SHAP plots. To be used in xgb.plot.shap, xgb.plot.shap.summary, etc. diff --git a/R-package/man/xgb.train.Rd b/R-package/man/xgb.train.Rd index 105009cf8..0ef2e2216 100644 --- a/R-package/man/xgb.train.Rd +++ b/R-package/man/xgb.train.Rd @@ -43,111 +43,114 @@ xgboost( } \arguments{ \item{params}{the list of parameters. The complete list of parameters is - available in the \href{http://xgboost.readthedocs.io/en/latest/parameter.html}{online documentation}. Below - is a shorter summary: - -1. General Parameters - -\itemize{ - \item \code{booster} which booster to use, can be \code{gbtree} or \code{gblinear}. Default: \code{gbtree}. +available in the \href{http://xgboost.readthedocs.io/en/latest/parameter.html}{online documentation}. Below +is a shorter summary: +\enumerate{ +\item General Parameters } -2. Booster Parameters +\itemize{ +\item \code{booster} which booster to use, can be \code{gbtree} or \code{gblinear}. Default: \code{gbtree}. +} +\enumerate{ +\item Booster Parameters +} 2.1. Parameters for Tree Booster \itemize{ - \item{ \code{eta} control the learning rate: scale the contribution of each tree by a factor of \code{0 < eta < 1} - when it is added to the current approximation. - Used to prevent overfitting by making the boosting process more conservative. - Lower value for \code{eta} implies larger value for \code{nrounds}: low \code{eta} value means model - more robust to overfitting but slower to compute. Default: 0.3} - \item{ \code{gamma} minimum loss reduction required to make a further partition on a leaf node of the tree. - the larger, the more conservative the algorithm will be.} - \item \code{max_depth} maximum depth of a tree. Default: 6 - \item{\code{min_child_weight} minimum sum of instance weight (hessian) needed in a child. - If the tree partition step results in a leaf node with the sum of instance weight less than min_child_weight, - then the building process will give up further partitioning. - In linear regression mode, this simply corresponds to minimum number of instances needed to be in each node. - The larger, the more conservative the algorithm will be. Default: 1} - \item{ \code{subsample} subsample ratio of the training instance. - Setting it to 0.5 means that xgboost randomly collected half of the data instances to grow trees - and this will prevent overfitting. It makes computation shorter (because less data to analyse). - It is advised to use this parameter with \code{eta} and increase \code{nrounds}. Default: 1} - \item \code{colsample_bytree} subsample ratio of columns when constructing each tree. Default: 1 - \item \code{lambda} L2 regularization term on weights. Default: 1 - \item \code{alpha} L1 regularization term on weights. (there is no L1 reg on bias because it is not important). Default: 0 - \item{ \code{num_parallel_tree} Experimental parameter. number of trees to grow per round. - Useful to test Random Forest through XGBoost - (set \code{colsample_bytree < 1}, \code{subsample < 1} and \code{round = 1}) accordingly. - Default: 1} - \item{ \code{monotone_constraints} A numerical vector consists of \code{1}, \code{0} and \code{-1} with its length - equals to the number of features in the training data. - \code{1} is increasing, \code{-1} is decreasing and \code{0} is no constraint.} - \item{ \code{interaction_constraints} A list of vectors specifying feature indices of permitted interactions. - Each item of the list represents one permitted interaction where specified features are allowed to interact with each other. - Feature index values should start from \code{0} (\code{0} references the first column). - Leave argument unspecified for no interaction constraints.} +\item{ \code{eta} control the learning rate: scale the contribution of each tree by a factor of \code{0 < eta < 1} +when it is added to the current approximation. +Used to prevent overfitting by making the boosting process more conservative. +Lower value for \code{eta} implies larger value for \code{nrounds}: low \code{eta} value means model +more robust to overfitting but slower to compute. Default: 0.3} +\item{ \code{gamma} minimum loss reduction required to make a further partition on a leaf node of the tree. +the larger, the more conservative the algorithm will be.} +\item \code{max_depth} maximum depth of a tree. Default: 6 +\item{\code{min_child_weight} minimum sum of instance weight (hessian) needed in a child. +If the tree partition step results in a leaf node with the sum of instance weight less than min_child_weight, +then the building process will give up further partitioning. +In linear regression mode, this simply corresponds to minimum number of instances needed to be in each node. +The larger, the more conservative the algorithm will be. Default: 1} +\item{ \code{subsample} subsample ratio of the training instance. +Setting it to 0.5 means that xgboost randomly collected half of the data instances to grow trees +and this will prevent overfitting. It makes computation shorter (because less data to analyse). +It is advised to use this parameter with \code{eta} and increase \code{nrounds}. Default: 1} +\item \code{colsample_bytree} subsample ratio of columns when constructing each tree. Default: 1 +\item \code{lambda} L2 regularization term on weights. Default: 1 +\item \code{alpha} L1 regularization term on weights. (there is no L1 reg on bias because it is not important). Default: 0 +\item{ \code{num_parallel_tree} Experimental parameter. number of trees to grow per round. +Useful to test Random Forest through XGBoost +(set \code{colsample_bytree < 1}, \code{subsample < 1} and \code{round = 1}) accordingly. +Default: 1} +\item{ \code{monotone_constraints} A numerical vector consists of \code{1}, \code{0} and \code{-1} with its length +equals to the number of features in the training data. +\code{1} is increasing, \code{-1} is decreasing and \code{0} is no constraint.} +\item{ \code{interaction_constraints} A list of vectors specifying feature indices of permitted interactions. +Each item of the list represents one permitted interaction where specified features are allowed to interact with each other. +Feature index values should start from \code{0} (\code{0} references the first column). +Leave argument unspecified for no interaction constraints.} } 2.2. Parameters for Linear Booster \itemize{ - \item \code{lambda} L2 regularization term on weights. Default: 0 - \item \code{lambda_bias} L2 regularization term on bias. Default: 0 - \item \code{alpha} L1 regularization term on weights. (there is no L1 reg on bias because it is not important). Default: 0 +\item \code{lambda} L2 regularization term on weights. Default: 0 +\item \code{lambda_bias} L2 regularization term on bias. Default: 0 +\item \code{alpha} L1 regularization term on weights. (there is no L1 reg on bias because it is not important). Default: 0 +} +\enumerate{ +\item Task Parameters } - -3. Task Parameters \itemize{ \item{ \code{objective} specify the learning task and the corresponding learning objective, users can pass a self-defined function to it. - The default objective options are below: - \itemize{ - \item \code{reg:squarederror} Regression with squared loss (Default). - \item{ \code{reg:squaredlogerror}: regression with squared log loss \eqn{1/2 * (log(pred + 1) - log(label + 1))^2}. - All inputs are required to be greater than -1. - Also, see metric rmsle for possible issue with this objective.} - \item \code{reg:logistic} logistic regression. - \item \code{reg:pseudohubererror}: regression with Pseudo Huber loss, a twice differentiable alternative to absolute loss. - \item \code{binary:logistic} logistic regression for binary classification. Output probability. - \item \code{binary:logitraw} logistic regression for binary classification, output score before logistic transformation. - \item \code{binary:hinge}: hinge loss for binary classification. This makes predictions of 0 or 1, rather than producing probabilities. - \item{ \code{count:poisson}: Poisson regression for count data, output mean of Poisson distribution. - \code{max_delta_step} is set to 0.7 by default in poisson regression (used to safeguard optimization).} - \item{ \code{survival:cox}: Cox regression for right censored survival time data (negative values are considered right censored). - Note that predictions are returned on the hazard ratio scale (i.e., as HR = exp(marginal_prediction) in the proportional - hazard function \code{h(t) = h0(t) * HR)}.} - \item{ \code{survival:aft}: Accelerated failure time model for censored survival time data. See - \href{https://xgboost.readthedocs.io/en/latest/tutorials/aft_survival_analysis.html}{Survival Analysis with Accelerated Failure Time} - for details.} - \item \code{aft_loss_distribution}: Probability Density Function used by \code{survival:aft} and \code{aft-nloglik} metric. - \item{ \code{multi:softmax} set xgboost to do multiclass classification using the softmax objective. - Class is represented by a number and should be from 0 to \code{num_class - 1}.} - \item{ \code{multi:softprob} same as softmax, but prediction outputs a vector of ndata * nclass elements, which can be - further reshaped to ndata, nclass matrix. The result contains predicted probabilities of each data point belonging - to each class.} - \item \code{rank:pairwise} set xgboost to do ranking task by minimizing the pairwise loss. - \item{ \code{rank:ndcg}: Use LambdaMART to perform list-wise ranking where - \href{https://en.wikipedia.org/wiki/Discounted_cumulative_gain}{Normalized Discounted Cumulative Gain (NDCG)} is maximized.} - \item{ \code{rank:map}: Use LambdaMART to perform list-wise ranking where - \href{https://en.wikipedia.org/wiki/Evaluation_measures_(information_retrieval)#Mean_average_precision}{Mean Average Precision (MAP)} - is maximized.} - \item{ \code{reg:gamma}: gamma regression with log-link. - Output is a mean of gamma distribution. - It might be useful, e.g., for modeling insurance claims severity, or for any outcome that might be - \href{https://en.wikipedia.org/wiki/Gamma_distribution#Applications}{gamma-distributed}.} - \item{ \code{reg:tweedie}: Tweedie regression with log-link. - It might be useful, e.g., for modeling total loss in insurance, or for any outcome that might be - \href{https://en.wikipedia.org/wiki/Tweedie_distribution#Applications}{Tweedie-distributed}.} - } - } - \item \code{base_score} the initial prediction score of all instances, global bias. Default: 0.5 - \item{ \code{eval_metric} evaluation metrics for validation data. - Users can pass a self-defined function to it. - Default: metric will be assigned according to objective - (rmse for regression, and error for classification, mean average precision for ranking). - List is provided in detail section.} +The default objective options are below: +\itemize{ +\item \code{reg:squarederror} Regression with squared loss (Default). +\item{ \code{reg:squaredlogerror}: regression with squared log loss \eqn{1/2 * (log(pred + 1) - log(label + 1))^2}. +All inputs are required to be greater than -1. +Also, see metric rmsle for possible issue with this objective.} +\item \code{reg:logistic} logistic regression. +\item \code{reg:pseudohubererror}: regression with Pseudo Huber loss, a twice differentiable alternative to absolute loss. +\item \code{binary:logistic} logistic regression for binary classification. Output probability. +\item \code{binary:logitraw} logistic regression for binary classification, output score before logistic transformation. +\item \code{binary:hinge}: hinge loss for binary classification. This makes predictions of 0 or 1, rather than producing probabilities. +\item{ \code{count:poisson}: Poisson regression for count data, output mean of Poisson distribution. +\code{max_delta_step} is set to 0.7 by default in poisson regression (used to safeguard optimization).} +\item{ \code{survival:cox}: Cox regression for right censored survival time data (negative values are considered right censored). +Note that predictions are returned on the hazard ratio scale (i.e., as HR = exp(marginal_prediction) in the proportional +hazard function \code{h(t) = h0(t) * HR)}.} +\item{ \code{survival:aft}: Accelerated failure time model for censored survival time data. See +\href{https://xgboost.readthedocs.io/en/latest/tutorials/aft_survival_analysis.html}{Survival Analysis with Accelerated Failure Time} +for details.} +\item \code{aft_loss_distribution}: Probability Density Function used by \code{survival:aft} and \code{aft-nloglik} metric. +\item{ \code{multi:softmax} set xgboost to do multiclass classification using the softmax objective. +Class is represented by a number and should be from 0 to \code{num_class - 1}.} +\item{ \code{multi:softprob} same as softmax, but prediction outputs a vector of ndata * nclass elements, which can be +further reshaped to ndata, nclass matrix. The result contains predicted probabilities of each data point belonging +to each class.} +\item \code{rank:pairwise} set xgboost to do ranking task by minimizing the pairwise loss. +\item{ \code{rank:ndcg}: Use LambdaMART to perform list-wise ranking where +\href{https://en.wikipedia.org/wiki/Discounted_cumulative_gain}{Normalized Discounted Cumulative Gain (NDCG)} is maximized.} +\item{ \code{rank:map}: Use LambdaMART to perform list-wise ranking where +\href{https://en.wikipedia.org/wiki/Evaluation_measures_(information_retrieval)#Mean_average_precision}{Mean Average Precision (MAP)} +is maximized.} +\item{ \code{reg:gamma}: gamma regression with log-link. +Output is a mean of gamma distribution. +It might be useful, e.g., for modeling insurance claims severity, or for any outcome that might be +\href{https://en.wikipedia.org/wiki/Gamma_distribution#Applications}{gamma-distributed}.} +\item{ \code{reg:tweedie}: Tweedie regression with log-link. +It might be useful, e.g., for modeling total loss in insurance, or for any outcome that might be +\href{https://en.wikipedia.org/wiki/Tweedie_distribution#Applications}{Tweedie-distributed}.} +} +} +\item \code{base_score} the initial prediction score of all instances, global bias. Default: 0.5 +\item{ \code{eval_metric} evaluation metrics for validation data. +Users can pass a self-defined function to it. +Default: metric will be assigned according to objective +(rmse for regression, and error for classification, mean average precision for ranking). +List is provided in detail section.} }} \item{data}{training dataset. \code{xgb.train} accepts only an \code{xgb.DMatrix} as the input. @@ -218,24 +221,24 @@ This parameter is only used when input is a dense matrix.} \value{ An object of class \code{xgb.Booster} with the following elements: \itemize{ - \item \code{handle} a handle (pointer) to the xgboost model in memory. - \item \code{raw} a cached memory dump of the xgboost model saved as R's \code{raw} type. - \item \code{niter} number of boosting iterations. - \item \code{evaluation_log} evaluation history stored as a \code{data.table} with the - first column corresponding to iteration number and the rest corresponding to evaluation - metrics' values. It is created by the \code{\link{cb.evaluation.log}} callback. - \item \code{call} a function call. - \item \code{params} parameters that were passed to the xgboost library. Note that it does not - capture parameters changed by the \code{\link{cb.reset.parameters}} callback. - \item \code{callbacks} callback functions that were either automatically assigned or - explicitly passed. - \item \code{best_iteration} iteration number with the best evaluation metric value - (only available with early stopping). - \item \code{best_score} the best evaluation metric value during early stopping. - (only available with early stopping). - \item \code{feature_names} names of the training dataset features - (only when column names were defined in training data). - \item \code{nfeatures} number of features in training data. +\item \code{handle} a handle (pointer) to the xgboost model in memory. +\item \code{raw} a cached memory dump of the xgboost model saved as R's \code{raw} type. +\item \code{niter} number of boosting iterations. +\item \code{evaluation_log} evaluation history stored as a \code{data.table} with the +first column corresponding to iteration number and the rest corresponding to evaluation +metrics' values. It is created by the \code{\link{cb.evaluation.log}} callback. +\item \code{call} a function call. +\item \code{params} parameters that were passed to the xgboost library. Note that it does not +capture parameters changed by the \code{\link{cb.reset.parameters}} callback. +\item \code{callbacks} callback functions that were either automatically assigned or +explicitly passed. +\item \code{best_iteration} iteration number with the best evaluation metric value +(only available with early stopping). +\item \code{best_score} the best evaluation metric value during early stopping. +(only available with early stopping). +\item \code{feature_names} names of the training dataset features +(only when column names were defined in training data). +\item \code{nfeatures} number of features in training data. } } \description{ @@ -258,29 +261,29 @@ when the \code{eval_metric} parameter is not provided. User may set one or several \code{eval_metric} parameters. Note that when using a customized metric, only this single metric can be used. The following is the list of built-in metrics for which XGBoost provides optimized implementation: - \itemize{ - \item \code{rmse} root mean square error. \url{https://en.wikipedia.org/wiki/Root_mean_square_error} - \item \code{logloss} negative log-likelihood. \url{https://en.wikipedia.org/wiki/Log-likelihood} - \item \code{mlogloss} multiclass logloss. \url{https://scikit-learn.org/stable/modules/generated/sklearn.metrics.log_loss.html} - \item \code{error} Binary classification error rate. It is calculated as \code{(# wrong cases) / (# all cases)}. - By default, it uses the 0.5 threshold for predicted values to define negative and positive instances. - Different threshold (e.g., 0.) could be specified as "error@0." - \item \code{merror} Multiclass classification error rate. It is calculated as \code{(# wrong cases) / (# all cases)}. - \item \code{mae} Mean absolute error - \item \code{mape} Mean absolute percentage error - \item{ \code{auc} Area under the curve. - \url{https://en.wikipedia.org/wiki/Receiver_operating_characteristic#'Area_under_curve} for ranking evaluation.} - \item \code{aucpr} Area under the PR curve. \url{https://en.wikipedia.org/wiki/Precision_and_recall} for ranking evaluation. - \item \code{ndcg} Normalized Discounted Cumulative Gain (for ranking task). \url{https://en.wikipedia.org/wiki/NDCG} - } +\itemize{ +\item \code{rmse} root mean square error. \url{https://en.wikipedia.org/wiki/Root_mean_square_error} +\item \code{logloss} negative log-likelihood. \url{https://en.wikipedia.org/wiki/Log-likelihood} +\item \code{mlogloss} multiclass logloss. \url{https://scikit-learn.org/stable/modules/generated/sklearn.metrics.log_loss.html} +\item \code{error} Binary classification error rate. It is calculated as \code{(# wrong cases) / (# all cases)}. +By default, it uses the 0.5 threshold for predicted values to define negative and positive instances. +Different threshold (e.g., 0.) could be specified as "error@0." +\item \code{merror} Multiclass classification error rate. It is calculated as \code{(# wrong cases) / (# all cases)}. +\item \code{mae} Mean absolute error +\item \code{mape} Mean absolute percentage error +\item{ \code{auc} Area under the curve. +\url{https://en.wikipedia.org/wiki/Receiver_operating_characteristic#'Area_under_curve} for ranking evaluation.} +\item \code{aucpr} Area under the PR curve. \url{https://en.wikipedia.org/wiki/Precision_and_recall} for ranking evaluation. +\item \code{ndcg} Normalized Discounted Cumulative Gain (for ranking task). \url{https://en.wikipedia.org/wiki/NDCG} +} The following callbacks are automatically created when certain parameters are set: \itemize{ - \item \code{cb.print.evaluation} is turned on when \code{verbose > 0}; - and the \code{print_every_n} parameter is passed to it. - \item \code{cb.evaluation.log} is on when \code{watchlist} is present. - \item \code{cb.early.stop}: when \code{early_stopping_rounds} is set. - \item \code{cb.save.model}: when \code{save_period > 0} is set. +\item \code{cb.print.evaluation} is turned on when \code{verbose > 0}; +and the \code{print_every_n} parameter is passed to it. +\item \code{cb.evaluation.log} is on when \code{watchlist} is present. +\item \code{cb.early.stop}: when \code{early_stopping_rounds} is set. +\item \code{cb.save.model}: when \code{save_period > 0} is set. } } \examples{ diff --git a/R-package/man/xgb.unserialize.Rd b/R-package/man/xgb.unserialize.Rd index d191d77d4..f83ee635d 100644 --- a/R-package/man/xgb.unserialize.Rd +++ b/R-package/man/xgb.unserialize.Rd @@ -11,7 +11,7 @@ xgb.unserialize(buffer, handle = NULL) \item{handle}{An \code{xgb.Booster.handle} object which will be overwritten with the new deserialized object. Must be a null handle (e.g. when loading the model through -`readRDS`). If not provided, a new handle will be created.} +\code{readRDS}). If not provided, a new handle will be created.} } \value{ An \code{xgb.Booster.handle} object. From 6a5f6ba694c980a884ac82407b1098a80cb0c6a0 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Sun, 24 Dec 2023 00:09:05 +0800 Subject: [PATCH 30/59] [CI] Add timeout for distributed GPU tests. (#9917) --- .../test_gpu_with_dask/test_gpu_demos.py | 10 ++++++---- .../test_gpu_with_dask/test_gpu_with_dask.py | 4 +++- .../test_distributed/test_gpu_with_spark/test_data.py | 5 ++++- .../test_gpu_with_spark/test_gpu_spark.py | 5 ++++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/tests/test_distributed/test_gpu_with_dask/test_gpu_demos.py b/tests/test_distributed/test_gpu_with_dask/test_gpu_demos.py index 92539aaa1..c0ae8b849 100644 --- a/tests/test_distributed/test_gpu_with_dask/test_gpu_demos.py +++ b/tests/test_distributed/test_gpu_with_dask/test_gpu_demos.py @@ -5,9 +5,13 @@ import pytest from xgboost import testing as tm +pytestmark = [ + pytest.mark.skipif(**tm.no_dask()), + pytest.mark.skipif(**tm.no_dask_cuda()), + tm.timeout(60), +] + -@pytest.mark.skipif(**tm.no_dask()) -@pytest.mark.skipif(**tm.no_dask_cuda()) @pytest.mark.skipif(**tm.no_cupy()) @pytest.mark.mgpu def test_dask_training(): @@ -16,8 +20,6 @@ def test_dask_training(): subprocess.check_call(cmd) -@pytest.mark.skipif(**tm.no_dask_cuda()) -@pytest.mark.skipif(**tm.no_dask()) @pytest.mark.mgpu def test_dask_sklearn_demo(): script = os.path.join(tm.demo_dir(__file__), "dask", "sklearn_gpu_training.py") diff --git a/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py b/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py index f25ac9fb0..a15e1903d 100644 --- a/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py +++ b/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py @@ -1,4 +1,4 @@ -"""Copyright 2019-2022 XGBoost contributors""" +"""Copyright 2019-2023, XGBoost contributors""" import asyncio import json from collections import OrderedDict @@ -18,6 +18,7 @@ from xgboost.testing.params import hist_parameter_strategy pytestmark = [ pytest.mark.skipif(**tm.no_dask()), pytest.mark.skipif(**tm.no_dask_cuda()), + tm.timeout(60), ] from ..test_with_dask.test_with_dask import generate_array @@ -629,6 +630,7 @@ def test_nccl_load(local_cuda_client: Client, tree_method: str) -> None: def run(wid: int) -> None: # FIXME(jiamingy): https://github.com/dmlc/xgboost/issues/9147 from xgboost.core import _LIB, _register_log_callback + _register_log_callback(_LIB) with CommunicatorContext(**args): diff --git a/tests/test_distributed/test_gpu_with_spark/test_data.py b/tests/test_distributed/test_gpu_with_spark/test_data.py index 72d9c190f..c2e068a87 100644 --- a/tests/test_distributed/test_gpu_with_spark/test_data.py +++ b/tests/test_distributed/test_gpu_with_spark/test_data.py @@ -2,7 +2,10 @@ import pytest from xgboost import testing as tm -pytestmark = pytest.mark.skipif(**tm.no_spark()) +pytestmark = [ + pytest.mark.skipif(**tm.no_spark()), + tm.timeout(120), +] from ..test_with_spark.test_data import run_dmatrix_ctor diff --git a/tests/test_distributed/test_gpu_with_spark/test_gpu_spark.py b/tests/test_distributed/test_gpu_with_spark/test_gpu_spark.py index 3bf94c954..f389d6d26 100644 --- a/tests/test_distributed/test_gpu_with_spark/test_gpu_spark.py +++ b/tests/test_distributed/test_gpu_with_spark/test_gpu_spark.py @@ -8,7 +8,10 @@ import sklearn from xgboost import testing as tm -pytestmark = pytest.mark.skipif(**tm.no_spark()) +pytestmark = [ + pytest.mark.skipif(**tm.no_spark()), + tm.timeout(240), +] from pyspark.ml.linalg import Vectors from pyspark.ml.tuning import CrossValidator, ParamGridBuilder From 52620fdb341c91cc7edcd882a40b09305947dc7b Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Tue, 26 Dec 2023 10:30:13 +0100 Subject: [PATCH 31/59] [R] Improve more docstrings (#9919) --- R-package/R/xgb.ggplot.R | 31 ++-- R-package/R/xgb.importance.R | 142 +++++++++------ R-package/R/xgb.model.dt.tree.R | 89 +++++----- R-package/R/xgb.plot.deepness.R | 83 +++++---- R-package/R/xgb.plot.importance.R | 81 +++++---- R-package/R/xgb.plot.multi.trees.R | 51 +++--- R-package/R/xgb.plot.shap.R | 236 ++++++++++++++++--------- R-package/R/xgb.plot.tree.R | 89 +++++----- R-package/man/xgb.importance.Rd | 134 +++++++++----- R-package/man/xgb.model.dt.tree.Rd | 83 +++++---- R-package/man/xgb.plot.deepness.Rd | 78 ++++---- R-package/man/xgb.plot.importance.Rd | 78 ++++---- R-package/man/xgb.plot.multi.trees.Rd | 58 +++--- R-package/man/xgb.plot.shap.Rd | 193 +++++++++++++------- R-package/man/xgb.plot.shap.summary.Rd | 55 +++--- R-package/man/xgb.plot.tree.Rd | 87 +++++---- R-package/man/xgb.shap.data.Rd | 55 ------ 17 files changed, 931 insertions(+), 692 deletions(-) delete mode 100644 R-package/man/xgb.shap.data.Rd diff --git a/R-package/R/xgb.ggplot.R b/R-package/R/xgb.ggplot.R index e79644543..1fe30ba2b 100644 --- a/R-package/R/xgb.ggplot.R +++ b/R-package/R/xgb.ggplot.R @@ -127,22 +127,20 @@ xgb.ggplot.shap.summary <- function(data, shap_contrib = NULL, features = NULL, p } -#' Combine and melt feature values and SHAP contributions for sample -#' observations. +#' Combine feature values and SHAP values #' -#' Conforms to data format required for ggplot functions. +#' Internal function used to combine and melt feature values and SHAP contributions +#' as required for ggplot functions related to SHAP. #' -#' Internal utility function. +#' @param data_list The result of `xgb.shap.data()`. +#' @param normalize Whether to standardize feature values to mean 0 and +#' standard deviation 1. This is useful for comparing multiple features on the same +#' plot. Default is \code{FALSE}. #' -#' @param data_list List containing 'data' and 'shap_contrib' returned by -#' \code{xgb.shap.data()}. -#' @param normalize Whether to standardize feature values to have mean 0 and -#' standard deviation 1 (useful for comparing multiple features on the same -#' plot). Default \code{FALSE}. -#' -#' @return A data.table containing the observation ID, the feature name, the +#' @return A `data.table` containing the observation ID, the feature name, the #' feature value (normalized if specified), and the SHAP contribution value. #' @noRd +#' @keywords internal prepare.ggplot.shap.data <- function(data_list, normalize = FALSE) { data <- data_list[["data"]] shap_contrib <- data_list[["shap_contrib"]] @@ -163,15 +161,16 @@ prepare.ggplot.shap.data <- function(data_list, normalize = FALSE) { p_data } -#' Scale feature value to have mean 0, standard deviation 1 +#' Scale feature values #' -#' This is used to compare multiple features on the same plot. -#' Internal utility function +#' Internal function that scales feature values to mean 0 and standard deviation 1. +#' Useful to compare multiple features on the same plot. #' -#' @param x Numeric vector +#' @param x Numeric vector. #' -#' @return Numeric vector with mean 0 and sd 1. +#' @return Numeric vector with mean 0 and standard deviation 1. #' @noRd +#' @keywords internal normalize <- function(x) { loc <- mean(x, na.rm = TRUE) scale <- stats::sd(x, na.rm = TRUE) diff --git a/R-package/R/xgb.importance.R b/R-package/R/xgb.importance.R index 844e36cdf..c94e1babb 100644 --- a/R-package/R/xgb.importance.R +++ b/R-package/R/xgb.importance.R @@ -1,83 +1,115 @@ -#' Importance of features in a model. +#' Feature importance #' -#' Creates a \code{data.table} of feature importances in a model. +#' Creates a `data.table` of feature importances. #' -#' @param feature_names character vector of feature names. If the model already -#' contains feature names, those would be used when \code{feature_names=NULL} (default value). -#' Non-null \code{feature_names} could be provided to override those in the model. -#' @param model object of class \code{xgb.Booster}. -#' @param trees (only for the gbtree booster) an integer vector of tree indices that should be included -#' into the importance calculation. If set to \code{NULL}, all trees of the model are parsed. -#' It could be useful, e.g., in multiclass classification to get feature importances -#' for each class separately. IMPORTANT: the tree index in xgboost models -#' is zero-based (e.g., use \code{trees = 0:4} for first 5 trees). -#' @param data deprecated. -#' @param label deprecated. -#' @param target deprecated. +#' @param feature_names Character vector used to overwrite the feature names +#' of the model. The default is `NULL` (use original feature names). +#' @param model Object of class `xgb.Booster`. +#' @param trees An integer vector of tree indices that should be included +#' into the importance calculation (only for the "gbtree" booster). +#' The default (`NULL`) parses all trees. +#' It could be useful, e.g., in multiclass classification to get feature importances +#' for each class separately. *Important*: the tree index in XGBoost models +#' is zero-based (e.g., use `trees = 0:4` for the first five trees). +#' @param data Deprecated. +#' @param label Deprecated. +#' @param target Deprecated. #' #' @details #' #' This function works for both linear and tree models. #' #' For linear models, the importance is the absolute magnitude of linear coefficients. -#' For that reason, in order to obtain a meaningful ranking by importance for a linear model, -#' the features need to be on the same scale (which you also would want to do when using either -#' L1 or L2 regularization). +#' To obtain a meaningful ranking by importance for linear models, the features need to +#' be on the same scale (which is also recommended when using L1 or L2 regularization). #' -#' @return +#' @return A `data.table` with the following columns: #' -#' For a tree model, a \code{data.table} with the following columns: -#' \itemize{ -#' \item \code{Features} names of the features used in the model; -#' \item \code{Gain} represents fractional contribution of each feature to the model based on -#' the total gain of this feature's splits. Higher percentage means a more important -#' predictive feature. -#' \item \code{Cover} metric of the number of observation related to this feature; -#' \item \code{Frequency} percentage representing the relative number of times -#' a feature have been used in trees. -#' } +#' For a tree model: +#' - `Features`: Names of the features used in the model. +#' - `Gain`: Fractional contribution of each feature to the model based on +#' the total gain of this feature's splits. Higher percentage means higher importance. +#' - `Cover`: Metric of the number of observation related to this feature. +#' - `Frequency`: Percentage of times a feature has been used in trees. #' -#' A linear model's importance \code{data.table} has the following columns: -#' \itemize{ -#' \item \code{Features} names of the features used in the model; -#' \item \code{Weight} the linear coefficient of this feature; -#' \item \code{Class} (only for multiclass models) class label. -#' } +#' For a linear model: +#' - `Features`: Names of the features used in the model. +#' - `Weight`: Linear coefficient of this feature. +#' - `Class`: Class label (only for multiclass models). #' -#' If \code{feature_names} is not provided and \code{model} doesn't have \code{feature_names}, -#' index of the features will be used instead. Because the index is extracted from the model dump +#' If `feature_names` is not provided and `model` doesn't have `feature_names`, +#' the index of the features will be used instead. Because the index is extracted from the model dump #' (based on C++ code), it starts at 0 (as in C/C++ or Python) instead of 1 (usual in R). #' #' @examples #' -#' # binomial classification using gbtree: -#' data(agaricus.train, package='xgboost') -#' bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +#' # binomial classification using "gbtree": +#' data(agaricus.train, package = "xgboost") +#' +#' bst <- xgboost( +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' max_depth = 2, +#' eta = 1, +#' nthread = 2, +#' nrounds = 2, +#' objective = "binary:logistic" +#' ) +#' #' xgb.importance(model = bst) #' -#' # binomial classification using gblinear: -#' bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, booster = "gblinear", -#' eta = 0.3, nthread = 1, nrounds = 20, objective = "binary:logistic") +#' # binomial classification using "gblinear": +#' bst <- xgboost( +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' booster = "gblinear", +#' eta = 0.3, +#' nthread = 1, +#' nrounds = 20,objective = "binary:logistic" +#' ) +#' #' xgb.importance(model = bst) #' -#' # multiclass classification using gbtree: +#' # multiclass classification using "gbtree": #' nclass <- 3 #' nrounds <- 10 -#' mbst <- xgboost(data = as.matrix(iris[, -5]), label = as.numeric(iris$Species) - 1, -#' max_depth = 3, eta = 0.2, nthread = 2, nrounds = nrounds, -#' objective = "multi:softprob", num_class = nclass) +#' mbst <- xgboost( +#' data = as.matrix(iris[, -5]), +#' label = as.numeric(iris$Species) - 1, +#' max_depth = 3, +#' eta = 0.2, +#' nthread = 2, +#' nrounds = nrounds, +#' objective = "multi:softprob", +#' num_class = nclass +#' ) +#' #' # all classes clumped together: #' xgb.importance(model = mbst) -#' # inspect importances separately for each class: -#' xgb.importance(model = mbst, trees = seq(from=0, by=nclass, length.out=nrounds)) -#' xgb.importance(model = mbst, trees = seq(from=1, by=nclass, length.out=nrounds)) -#' xgb.importance(model = mbst, trees = seq(from=2, by=nclass, length.out=nrounds)) #' -#' # multiclass classification using gblinear: -#' mbst <- xgboost(data = scale(as.matrix(iris[, -5])), label = as.numeric(iris$Species) - 1, -#' booster = "gblinear", eta = 0.2, nthread = 1, nrounds = 15, -#' objective = "multi:softprob", num_class = nclass) +#' # inspect importances separately for each class: +#' xgb.importance( +#' model = mbst, trees = seq(from = 0, by = nclass, length.out = nrounds) +#' ) +#' xgb.importance( +#' model = mbst, trees = seq(from = 1, by = nclass, length.out = nrounds) +#' ) +#' xgb.importance( +#' model = mbst, trees = seq(from = 2, by = nclass, length.out = nrounds) +#' ) +#' +#' # multiclass classification using "gblinear": +#' mbst <- xgboost( +#' data = scale(as.matrix(iris[, -5])), +#' label = as.numeric(iris$Species) - 1, +#' booster = "gblinear", +#' eta = 0.2, +#' nthread = 1, +#' nrounds = 15, +#' objective = "multi:softprob", +#' num_class = nclass +#' ) +#' #' xgb.importance(model = mbst) #' #' @export diff --git a/R-package/R/xgb.model.dt.tree.R b/R-package/R/xgb.model.dt.tree.R index d69169b89..8e74ea4b4 100644 --- a/R-package/R/xgb.model.dt.tree.R +++ b/R-package/R/xgb.model.dt.tree.R @@ -1,57 +1,58 @@ -#' Parse a boosted tree model text dump +#' Parse model text dump #' -#' Parse a boosted tree model text dump into a \code{data.table} structure. +#' Parse a boosted tree model text dump into a `data.table` structure. #' -#' @param feature_names character vector of feature names. If the model already -#' contains feature names, those would be used when \code{feature_names=NULL} (default value). -#' Non-null \code{feature_names} could be provided to override those in the model. -#' @param model object of class \code{xgb.Booster} -#' @param text \code{character} vector previously generated by the \code{xgb.dump} -#' function (where parameter \code{with_stats = TRUE} should have been set). -#' \code{text} takes precedence over \code{model}. -#' @param trees an integer vector of tree indices that should be parsed. -#' If set to \code{NULL}, all trees of the model are parsed. -#' It could be useful, e.g., in multiclass classification to get only -#' the trees of one certain class. IMPORTANT: the tree index in xgboost models -#' is zero-based (e.g., use \code{trees = 0:4} for first 5 trees). -#' @param use_int_id a logical flag indicating whether nodes in columns "Yes", "No", "Missing" should be -#' represented as integers (when FALSE) or as "Tree-Node" character strings (when FALSE). -#' @param ... currently not used. +#' @param feature_names Character vector used to overwrite the feature names +#' of the model. The default (`NULL`) uses the original feature names. +#' @param model Object of class `xgb.Booster`. +#' @param text Character vector previously generated by the function [xgb.dump()] +#' (called with parameter `with_stats = TRUE`). `text` takes precedence over `model`. +#' @param trees An integer vector of tree indices that should be used. +#' The default (`NULL`) uses all trees. +#' Useful, e.g., in multiclass classification to get only +#' the trees of one class. *Important*: the tree index in XGBoost models +#' is zero-based (e.g., use `trees = 0:4` for the first five trees). +#' @param use_int_id A logical flag indicating whether nodes in columns "Yes", "No", and +#' "Missing" should be represented as integers (when `TRUE`) or as "Tree-Node" +#' character strings (when `FALSE`, default). +#' @param ... Currently not used. #' #' @return -#' A \code{data.table} with detailed information about model trees' nodes. +#' A `data.table` with detailed information about tree nodes. It has the following columns: +#' - `Tree`: integer ID of a tree in a model (zero-based index). +#' - `Node`: integer ID of a node in a tree (zero-based index). +#' - `ID`: character identifier of a node in a model (only when `use_int_id = FALSE`). +#' - `Feature`: for a branch node, a feature ID or name (when available); +#' for a leaf node, it simply labels it as `"Leaf"`. +#' - `Split`: location of the split for a branch node (split condition is always "less than"). +#' - `Yes`: ID of the next node when the split condition is met. +#' - `No`: ID of the next node when the split condition is not met. +#' - `Missing`: ID of the next node when the branch value is missing. +#' - `Quality`: either the split gain (change in loss) or the leaf value. +#' - `Cover`: metric related to the number of observations either seen by a split +#' or collected by a leaf during training. #' -#' The columns of the \code{data.table} are: -#' -#' \itemize{ -#' \item \code{Tree}: integer ID of a tree in a model (zero-based index) -#' \item \code{Node}: integer ID of a node in a tree (zero-based index) -#' \item \code{ID}: character identifier of a node in a model (only when \code{use_int_id=FALSE}) -#' \item \code{Feature}: for a branch node, it's a feature id or name (when available); -#' for a leaf note, it simply labels it as \code{'Leaf'} -#' \item \code{Split}: location of the split for a branch node (split condition is always "less than") -#' \item \code{Yes}: ID of the next node when the split condition is met -#' \item \code{No}: ID of the next node when the split condition is not met -#' \item \code{Missing}: ID of the next node when branch value is missing -#' \item \code{Quality}: either the split gain (change in loss) or the leaf value -#' \item \code{Cover}: metric related to the number of observation either seen by a split -#' or collected by a leaf during training. -#' } -#' -#' When \code{use_int_id=FALSE}, columns "Yes", "No", and "Missing" point to model-wide node identifiers -#' in the "ID" column. When \code{use_int_id=TRUE}, those columns point to node identifiers from +#' When `use_int_id = FALSE`, columns "Yes", "No", and "Missing" point to model-wide node identifiers +#' in the "ID" column. When `use_int_id = TRUE`, those columns point to node identifiers from #' the corresponding trees in the "Node" column. #' #' @examples #' # Basic use: #' -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") #' ## Keep the number of threads to 1 for examples #' nthread <- 1 #' data.table::setDTthreads(nthread) #' -#' bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, -#' eta = 1, nthread = nthread, nrounds = 2,objective = "binary:logistic") +#' bst <- xgboost( +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' max_depth = 2, +#' eta = 1, +#' nthread = nthread, +#' nrounds = 2, +#' objective = "binary:logistic" +#' ) #' #' (dt <- xgb.model.dt.tree(colnames(agaricus.train$data), bst)) #' @@ -60,8 +61,12 @@ #' (dt <- xgb.model.dt.tree(model = bst)) #' #' # How to match feature names of splits that are following a current 'Yes' branch: -#' -#' merge(dt, dt[, .(ID, Y.Feature=Feature)], by.x='Yes', by.y='ID', all.x=TRUE)[order(Tree,Node)] +#' merge( +#' dt, +#' dt[, .(ID, Y.Feature = Feature)], by.x = "Yes", by.y = "ID", all.x = TRUE +#' )[ +#' order(Tree, Node) +#' ] #' #' @export xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, diff --git a/R-package/R/xgb.plot.deepness.R b/R-package/R/xgb.plot.deepness.R index f0fe0f134..092b07d38 100644 --- a/R-package/R/xgb.plot.deepness.R +++ b/R-package/R/xgb.plot.deepness.R @@ -1,65 +1,74 @@ -#' Plot model trees deepness +#' Plot model tree depth #' -#' Visualizes distributions related to depth of tree leafs. -#' \code{xgb.plot.deepness} uses base R graphics, while \code{xgb.ggplot.deepness} uses the ggplot backend. +#' Visualizes distributions related to the depth of tree leaves. +#' - `xgb.plot.deepness()` uses base R graphics, while +#' - `xgb.ggplot.deepness()` uses "ggplot2". #' -#' @param model either an \code{xgb.Booster} model generated by the \code{xgb.train} function -#' or a data.table result of the \code{xgb.model.dt.tree} function. -#' @param plot (base R barplot) whether a barplot should be produced. -#' If FALSE, only a data.table is returned. -#' @param which which distribution to plot (see details). -#' @param ... other parameters passed to \code{barplot} or \code{plot}. +#' @param model Either an `xgb.Booster` model, or the "data.table" returned by [xgb.model.dt.tree()]. +#' @param which Which distribution to plot (see details). +#' @param plot Should the plot be shown? Default is `TRUE`. +#' @param ... Other parameters passed to [graphics::barplot()] or [graphics::plot()]. #' #' @details #' -#' When \code{which="2x1"}, two distributions with respect to the leaf depth +#' When `which = "2x1"`, two distributions with respect to the leaf depth #' are plotted on top of each other: -#' \itemize{ -#' \item the distribution of the number of leafs in a tree model at a certain depth; -#' \item the distribution of average weighted number of observations ("cover") -#' ending up in leafs at certain depth. -#' } -#' Those could be helpful in determining sensible ranges of the \code{max_depth} -#' and \code{min_child_weight} parameters. +#' 1. The distribution of the number of leaves in a tree model at a certain depth. +#' 2. The distribution of the average weighted number of observations ("cover") +#' ending up in leaves at a certain depth. #' -#' When \code{which="max.depth"} or \code{which="med.depth"}, plots of either maximum or median depth -#' per tree with respect to tree number are created. And \code{which="med.weight"} allows to see how +#' Those could be helpful in determining sensible ranges of the `max_depth` +#' and `min_child_weight` parameters. +#' +#' When `which = "max.depth"` or `which = "med.depth"`, plots of either maximum or +#' median depth per tree with respect to the tree number are created. +#' +#' Finally, `which = "med.weight"` allows to see how #' a tree's median absolute leaf weight changes through the iterations. #' -#' This function was inspired by the blog post -#' \url{https://github.com/aysent/random-forest-leaf-visualization}. +#' These functions have been inspired by the blog post +#' . #' #' @return +#' The return value of the two functions is as follows: +#' - `xgb.plot.deepness()`: A "data.table" (invisibly). +#' Each row corresponds to a terminal leaf in the model. It contains its information +#' about depth, cover, and weight (used in calculating predictions). +#' If `plot = TRUE`, also a plot is shown. +#' - `xgb.ggplot.deepness()`: When `which = "2x1"`, a list of two "ggplot" objects, +#' and a single "ggplot" object otherwise. #' -#' Other than producing plots (when \code{plot=TRUE}), the \code{xgb.plot.deepness} function -#' silently returns a processed data.table where each row corresponds to a terminal leaf in a tree model, -#' and contains information about leaf's depth, cover, and weight (which is used in calculating predictions). -#' -#' The \code{xgb.ggplot.deepness} silently returns either a list of two ggplot graphs when \code{which="2x1"} -#' or a single ggplot graph for the other \code{which} options. -#' -#' @seealso -#' -#' \code{\link{xgb.train}}, \code{\link{xgb.model.dt.tree}}. +#' @seealso [xgb.train()] and [xgb.model.dt.tree()]. #' #' @examples #' -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") #' ## Keep the number of threads to 2 for examples #' nthread <- 2 #' data.table::setDTthreads(nthread) #' #' ## Change max_depth to a higher number to get a more significant result -#' bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 6, -#' eta = 0.1, nthread = nthread, nrounds = 50, objective = "binary:logistic", -#' subsample = 0.5, min_child_weight = 2) +#' bst <- xgboost( +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' max_depth = 6, +#' nthread = nthread, +#' nrounds = 50, +#' objective = "binary:logistic", +#' subsample = 0.5, +#' min_child_weight = 2 +#' ) #' #' xgb.plot.deepness(bst) #' xgb.ggplot.deepness(bst) #' -#' xgb.plot.deepness(bst, which='max.depth', pch=16, col=rgb(0,0,1,0.3), cex=2) +#' xgb.plot.deepness( +#' bst, which = "max.depth", pch = 16, col = rgb(0, 0, 1, 0.3), cex = 2 +#' ) #' -#' xgb.plot.deepness(bst, which='med.weight', pch=16, col=rgb(0,0,1,0.3), cex=2) +#' xgb.plot.deepness( +#' bst, which = "med.weight", pch = 16, col = rgb(0, 0, 1, 0.3), cex = 2 +#' ) #' #' @rdname xgb.plot.deepness #' @export diff --git a/R-package/R/xgb.plot.importance.R b/R-package/R/xgb.plot.importance.R index 07220375d..1848a3a86 100644 --- a/R-package/R/xgb.plot.importance.R +++ b/R-package/R/xgb.plot.importance.R @@ -1,64 +1,75 @@ -#' Plot feature importance as a bar graph +#' Plot feature importance #' #' Represents previously calculated feature importance as a bar graph. -#' \code{xgb.plot.importance} uses base R graphics, while \code{xgb.ggplot.importance} uses the ggplot backend. +#' - `xgb.plot.importance()` uses base R graphics, while +#' - `xgb.ggplot.importance()` uses "ggplot". #' -#' @param importance_matrix a \code{data.table} returned by \code{\link{xgb.importance}}. -#' @param top_n maximal number of top features to include into the plot. -#' @param measure the name of importance measure to plot. -#' When \code{NULL}, 'Gain' would be used for trees and 'Weight' would be used for gblinear. -#' @param rel_to_first whether importance values should be represented as relative to the highest ranked feature. -#' See Details. -#' @param left_margin (base R barplot) allows to adjust the left margin size to fit feature names. -#' When it is NULL, the existing \code{par('mar')} is used. -#' @param cex (base R barplot) passed as \code{cex.names} parameter to \code{barplot}. -#' @param plot (base R barplot) whether a barplot should be produced. -#' If FALSE, only a data.table is returned. -#' @param n_clusters (ggplot only) a \code{numeric} vector containing the min and the max range +#' @param importance_matrix A `data.table` as returned by [xgb.importance()]. +#' @param top_n Maximal number of top features to include into the plot. +#' @param measure The name of importance measure to plot. +#' When `NULL`, 'Gain' would be used for trees and 'Weight' would be used for gblinear. +#' @param rel_to_first Whether importance values should be represented as relative to +#' the highest ranked feature, see Details. +#' @param left_margin Adjust the left margin size to fit feature names. +#' When `NULL`, the existing `par("mar")` is used. +#' @param cex Passed as `cex.names` parameter to [graphics::barplot()]. +#' @param plot Should the barplot be shown? Default is `TRUE`. +#' @param n_clusters A numeric vector containing the min and the max range #' of the possible number of clusters of bars. -#' @param ... other parameters passed to \code{barplot} (except horiz, border, cex.names, names.arg, and las). +#' @param ... Other parameters passed to [graphics::barplot()] +#' (except `horiz`, `border`, `cex.names`, `names.arg`, and `las`). +#' Only used in `xgb.plot.importance()`. #' #' @details #' The graph represents each feature as a horizontal bar of length proportional to the importance of a feature. -#' Features are shown ranked in a decreasing importance order. -#' It works for importances from both \code{gblinear} and \code{gbtree} models. +#' Features are sorted by decreasing importance. +#' It works for both "gblinear" and "gbtree" models. #' -#' When \code{rel_to_first = FALSE}, the values would be plotted as they were in \code{importance_matrix}. -#' For gbtree model, that would mean being normalized to the total of 1 +#' When `rel_to_first = FALSE`, the values would be plotted as in `importance_matrix`. +#' For a "gbtree" model, that would mean being normalized to the total of 1 #' ("what is feature's importance contribution relative to the whole model?"). -#' For linear models, \code{rel_to_first = FALSE} would show actual values of the coefficients. -#' Setting \code{rel_to_first = TRUE} allows to see the picture from the perspective of +#' For linear models, `rel_to_first = FALSE` would show actual values of the coefficients. +#' Setting `rel_to_first = TRUE` allows to see the picture from the perspective of #' "what is feature's importance contribution relative to the most important feature?" #' -#' The ggplot-backend method also performs 1-D clustering of the importance values, -#' with bar colors corresponding to different clusters that have somewhat similar importance values. +#' The "ggplot" backend performs 1-D clustering of the importance values, +#' with bar colors corresponding to different clusters having similar importance values. #' #' @return -#' The \code{xgb.plot.importance} function creates a \code{barplot} (when \code{plot=TRUE}) -#' and silently returns a processed data.table with \code{n_top} features sorted by importance. +#' The return value depends on the function: +#' - `xgb.plot.importance()`: Invisibly, a "data.table" with `n_top` features sorted +#' by importance. If `plot = TRUE`, the values are also plotted as barplot. +#' - `xgb.ggplot.importance()`: A customizable "ggplot" object. +#' E.g., to change the title, set `+ ggtitle("A GRAPH NAME")`. #' -#' The \code{xgb.ggplot.importance} function returns a ggplot graph which could be customized afterwards. -#' E.g., to change the title of the graph, add \code{+ ggtitle("A GRAPH NAME")} to the result. -#' -#' @seealso -#' \code{\link[graphics]{barplot}}. +#' @seealso [graphics::barplot()] #' #' @examples #' data(agaricus.train) +#' #' ## Keep the number of threads to 2 for examples #' nthread <- 2 #' data.table::setDTthreads(nthread) #' #' bst <- xgboost( -#' data = agaricus.train$data, label = agaricus.train$label, max_depth = 3, -#' eta = 1, nthread = nthread, nrounds = 2, objective = "binary:logistic" +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' max_depth = 3, +#' eta = 1, +#' nthread = nthread, +#' nrounds = 2, +#' objective = "binary:logistic" #' ) #' #' importance_matrix <- xgb.importance(colnames(agaricus.train$data), model = bst) +#' xgb.plot.importance( +#' importance_matrix, rel_to_first = TRUE, xlab = "Relative importance" +#' ) #' -#' xgb.plot.importance(importance_matrix, rel_to_first = TRUE, xlab = "Relative importance") -#' -#' (gg <- xgb.ggplot.importance(importance_matrix, measure = "Frequency", rel_to_first = TRUE)) +#' gg <- xgb.ggplot.importance( +#' importance_matrix, measure = "Frequency", rel_to_first = TRUE +#' ) +#' gg #' gg + ggplot2::ylab("Frequency") #' #' @rdname xgb.plot.importance diff --git a/R-package/R/xgb.plot.multi.trees.R b/R-package/R/xgb.plot.multi.trees.R index f4d797a61..6402cb767 100644 --- a/R-package/R/xgb.plot.multi.trees.R +++ b/R-package/R/xgb.plot.multi.trees.R @@ -1,14 +1,10 @@ -#' Project all trees on one tree and plot it +#' Project all trees on one tree #' #' Visualization of the ensemble of trees as a single collective unit. #' -#' @param model produced by the \code{xgb.train} function. -#' @param feature_names names of each feature as a \code{character} vector. -#' @param features_keep number of features to keep in each position of the multi trees. -#' @param plot_width width in pixels of the graph to produce -#' @param plot_height height in pixels of the graph to produce -#' @param render a logical flag for whether the graph should be rendered (see Value). -#' @param ... currently not used +#' @inheritParams xgb.plot.tree +#' @param features_keep Number of features to keep in each position of the multi trees, +#' by default 5. #' #' @details #' @@ -24,33 +20,31 @@ #' Moreover, the trees tend to reuse the same features. #' #' The function projects each tree onto one, and keeps for each position the -#' \code{features_keep} first features (based on the Gain per feature measure). +#' `features_keep` first features (based on the Gain per feature measure). #' #' This function is inspired by this blog post: -#' \url{https://wellecks.wordpress.com/2015/02/21/peering-into-the-black-box-visualizing-lambdamart/} +#' #' -#' @return -#' -#' When \code{render = TRUE}: -#' returns a rendered graph object which is an \code{htmlwidget} of class \code{grViz}. -#' Similar to ggplot objects, it needs to be printed to see it when not running from command line. -#' -#' When \code{render = FALSE}: -#' silently returns a graph object which is of DiagrammeR's class \code{dgr_graph}. -#' This could be useful if one wants to modify some of the graph attributes -#' before rendering the graph with \code{\link[DiagrammeR]{render_graph}}. +#' @inherit xgb.plot.tree return #' #' @examples #' -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") +#' #' ## Keep the number of threads to 2 for examples #' nthread <- 2 #' data.table::setDTthreads(nthread) #' #' bst <- xgboost( -#' data = agaricus.train$data, label = agaricus.train$label, max_depth = 15, -#' eta = 1, nthread = nthread, nrounds = 30, objective = "binary:logistic", -#' min_child_weight = 50, verbose = 0 +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' max_depth = 15, +#' eta = 1, +#' nthread = nthread, +#' nrounds = 30, +#' objective = "binary:logistic", +#' min_child_weight = 50, +#' verbose = 0 #' ) #' #' p <- xgb.plot.multi.trees(model = bst, features_keep = 3) @@ -58,10 +52,13 @@ #' #' \dontrun{ #' # Below is an example of how to save this plot to a file. -#' # Note that for `export_graph` to work, the DiagrammeRsvg and rsvg packages must also be installed. +#' # Note that for export_graph() to work, the {DiagrammeRsvg} and {rsvg} packages +#' # must also be installed. +#' #' library(DiagrammeR) -#' gr <- xgb.plot.multi.trees(model=bst, features_keep = 3, render=FALSE) -#' export_graph(gr, 'tree.pdf', width=1500, height=600) +#' +#' gr <- xgb.plot.multi.trees(model = bst, features_keep = 3, render = FALSE) +#' export_graph(gr, "tree.pdf", width = 1500, height = 600) #' } #' #' @export diff --git a/R-package/R/xgb.plot.shap.R b/R-package/R/xgb.plot.shap.R index d61bd23d4..35cf664ec 100644 --- a/R-package/R/xgb.plot.shap.R +++ b/R-package/R/xgb.plot.shap.R @@ -1,110 +1,165 @@ -#' SHAP contribution dependency plots +#' SHAP dependence plots #' -#' Visualizing the SHAP feature contribution to prediction dependencies on feature value. +#' Visualizes SHAP values against feature values to gain an impression of feature effects. #' -#' @param data data as a \code{matrix} or \code{dgCMatrix}. -#' @param shap_contrib a matrix of SHAP contributions that was computed earlier for the above -#' \code{data}. When it is NULL, it is computed internally using \code{model} and \code{data}. -#' @param features a vector of either column indices or of feature names to plot. When it is NULL, -#' feature importance is calculated, and \code{top_n} high ranked features are taken. -#' @param top_n when \code{features} is NULL, top_n `[1, 100]` most important features in a model are taken. -#' @param model an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} -#' or \code{features} is missing. -#' @param trees passed to \code{\link{xgb.importance}} when \code{features = NULL}. -#' @param target_class is only relevant for multiclass models. When it is set to a 0-based class index, -#' only SHAP contributions for that specific class are used. -#' If it is not set, SHAP importances are averaged over all classes. -#' @param approxcontrib passed to \code{\link{predict.xgb.Booster}} when \code{shap_contrib = NULL}. -#' @param subsample a random fraction of data points to use for plotting. When it is NULL, -#' it is set so that up to 100K data points are used. -#' @param n_col a number of columns in a grid of plots. -#' @param col color of the scatterplot markers. -#' @param pch scatterplot marker. -#' @param discrete_n_uniq a maximal number of unique values in a feature to consider it as discrete. -#' @param discrete_jitter an \code{amount} parameter of jitter added to discrete features' positions. -#' @param ylab a y-axis label in 1D plots. -#' @param plot_NA whether the contributions of cases with missing values should also be plotted. -#' @param col_NA a color of marker for missing value contributions. -#' @param pch_NA a marker type for NA values. -#' @param pos_NA a relative position of the x-location where NA values are shown: -#' \code{min(x) + (max(x) - min(x)) * pos_NA}. -#' @param plot_loess whether to plot loess-smoothed curves. The smoothing is only done for features with -#' more than 5 distinct values. -#' @param col_loess a color to use for the loess curves. -#' @param span_loess the \code{span} parameter in \code{\link[stats]{loess}}'s call. -#' @param which whether to do univariate or bivariate plotting. NOTE: only 1D is implemented so far. -#' @param plot whether a plot should be drawn. If FALSE, only a list of matrices is returned. -#' @param ... other parameters passed to \code{plot}. +#' @param data The data to explain as a `matrix` or `dgCMatrix`. +#' @param shap_contrib Matrix of SHAP contributions of `data`. +#' The default (`NULL`) computes it from `model` and `data`. +#' @param features Vector of column indices or feature names to plot. +#' When `NULL` (default), the `top_n` most important features are selected +#' by [xgb.importance()]. +#' @param top_n How many of the most important features (<= 100) should be selected? +#' By default 1 for SHAP dependence and 10 for SHAP summary). +#' Only used when `features = NULL`. +#' @param model An `xgb.Booster` model. Only required when `shap_contrib = NULL` or +#' `features = NULL`. +#' @param trees Passed to [xgb.importance()] when `features = NULL`. +#' @param target_class Only relevant for multiclass models. The default (`NULL`) +#' averages the SHAP values over all classes. Pass a (0-based) class index +#' to show only SHAP values of that class. +#' @param approxcontrib Passed to `predict()` when `shap_contrib = NULL`. +#' @param subsample Fraction of data points randomly picked for plotting. +#' The default (`NULL`) will use up to 100k data points. +#' @param n_col Number of columns in a grid of plots. +#' @param col Color of the scatterplot markers. +#' @param pch Scatterplot marker. +#' @param discrete_n_uniq Maximal number of unique feature values to consider the +#' feature as discrete. +#' @param discrete_jitter Jitter amount added to the values of discrete features. +#' @param ylab The y-axis label in 1D plots. +#' @param plot_NA Should contributions of cases with missing values be plotted? +#' Default is `TRUE`. +#' @param col_NA Color of marker for missing value contributions. +#' @param pch_NA Marker type for `NA` values. +#' @param pos_NA Relative position of the x-location where `NA` values are shown: +#' `min(x) + (max(x) - min(x)) * pos_NA`. +#' @param plot_loess Should loess-smoothed curves be plotted? (Default is `TRUE`). +#' The smoothing is only done for features with more than 5 distinct values. +#' @param col_loess Color of loess curves. +#' @param span_loess The `span` parameter of [stats::loess()]. +#' @param which Whether to do univariate or bivariate plotting. Currently, only "1d" is implemented. +#' @param plot Should the plot be drawn? (Default is `TRUE`). +#' If `FALSE`, only a list of matrices is returned. +#' @param ... Other parameters passed to [graphics::plot()]. #' #' @details #' #' These scatterplots represent how SHAP feature contributions depend of feature values. -#' The similarity to partial dependency plots is that they also give an idea for how feature values -#' affect predictions. However, in partial dependency plots, we usually see marginal dependencies -#' of model prediction on feature value, while SHAP contribution dependency plots display the estimated -#' contributions of a feature to model prediction for each individual case. +#' The similarity to partial dependence plots is that they also give an idea for how feature values +#' affect predictions. However, in partial dependence plots, we see marginal dependencies +#' of model prediction on feature value, while SHAP dependence plots display the estimated +#' contributions of a feature to the prediction for each individual case. #' -#' When \code{plot_loess = TRUE} is set, feature values are rounded to 3 significant digits and -#' weighted LOESS is computed and plotted, where weights are the numbers of data points +#' When `plot_loess = TRUE`, feature values are rounded to three significant digits and +#' weighted LOESS is computed and plotted, where the weights are the numbers of data points #' at each rounded value. #' -#' Note: SHAP contributions are shown on the scale of model margin. E.g., for a logistic binomial objective, -#' the margin is prediction before a sigmoidal transform into probability-like values. +#' Note: SHAP contributions are on the scale of the model margin. +#' E.g., for a logistic binomial objective, the margin is on log-odds scale. #' Also, since SHAP stands for "SHapley Additive exPlanation" (model prediction = sum of SHAP #' contributions for all features + bias), depending on the objective used, transforming SHAP #' contributions for a feature from the marginal to the prediction space is not necessarily #' a meaningful thing to do. #' #' @return -#' -#' In addition to producing plots (when \code{plot=TRUE}), it silently returns a list of two matrices: -#' \itemize{ -#' \item \code{data} the values of selected features; -#' \item \code{shap_contrib} the contributions of selected features. -#' } +#' In addition to producing plots (when `plot = TRUE`), it silently returns a list of two matrices: +#' - `data`: Feature value matrix. +#' - `shap_contrib`: Corresponding SHAP value matrix. #' #' @references -#' -#' Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", NIPS Proceedings 2017, \url{https://arxiv.org/abs/1705.07874} -#' -#' Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", \url{https://arxiv.org/abs/1706.06060} +#' 1. Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", +#' NIPS Proceedings 2017, +#' 2. Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", +#' #' #' @examples #' -#' data(agaricus.train, package='xgboost') -#' data(agaricus.test, package='xgboost') +#' data(agaricus.train, package = "xgboost") +#' data(agaricus.test, package = "xgboost") #' #' ## Keep the number of threads to 1 for examples #' nthread <- 1 #' data.table::setDTthreads(nthread) #' nrounds <- 20 #' -#' bst <- xgboost(agaricus.train$data, agaricus.train$label, nrounds = nrounds, -#' eta = 0.1, max_depth = 3, subsample = .5, -#' method = "hist", objective = "binary:logistic", nthread = nthread, verbose = 0) +#' bst <- xgboost( +#' agaricus.train$data, +#' agaricus.train$label, +#' nrounds = nrounds, +#' eta = 0.1, +#' max_depth = 3, +#' subsample = 0.5, +#' objective = "binary:logistic", +#' nthread = nthread, +#' verbose = 0 +#' ) #' #' xgb.plot.shap(agaricus.test$data, model = bst, features = "odor=none") +#' #' contr <- predict(bst, agaricus.test$data, predcontrib = TRUE) #' xgb.plot.shap(agaricus.test$data, contr, model = bst, top_n = 12, n_col = 3) -#' xgb.ggplot.shap.summary(agaricus.test$data, contr, model = bst, top_n = 12) # Summary plot #' -#' # multiclass example - plots for each class separately: +#' # Summary plot +#' xgb.ggplot.shap.summary(agaricus.test$data, contr, model = bst, top_n = 12) +#' +#' # Multiclass example - plots for each class separately: #' nclass <- 3 #' x <- as.matrix(iris[, -5]) #' set.seed(123) #' is.na(x[sample(nrow(x) * 4, 30)]) <- TRUE # introduce some missing values -#' mbst <- xgboost(data = x, label = as.numeric(iris$Species) - 1, nrounds = nrounds, -#' max_depth = 2, eta = 0.3, subsample = .5, nthread = nthread, -#' objective = "multi:softprob", num_class = nclass, verbose = 0) -#' trees0 <- seq(from=0, by=nclass, length.out=nrounds) +#' +#' mbst <- xgboost( +#' data = x, +#' label = as.numeric(iris$Species) - 1, +#' nrounds = nrounds, +#' max_depth = 2, +#' eta = 0.3, +#' subsample = 0.5, +#' nthread = nthread, +#' objective = "multi:softprob", +#' num_class = nclass, +#' verbose = 0 +#' ) +#' trees0 <- seq(from = 0, by = nclass, length.out = nrounds) #' col <- rgb(0, 0, 1, 0.5) -#' xgb.plot.shap(x, model = mbst, trees = trees0, target_class = 0, top_n = 4, -#' n_col = 2, col = col, pch = 16, pch_NA = 17) -#' xgb.plot.shap(x, model = mbst, trees = trees0 + 1, target_class = 1, top_n = 4, -#' n_col = 2, col = col, pch = 16, pch_NA = 17) -#' xgb.plot.shap(x, model = mbst, trees = trees0 + 2, target_class = 2, top_n = 4, -#' n_col = 2, col = col, pch = 16, pch_NA = 17) -#' xgb.ggplot.shap.summary(x, model = mbst, target_class = 0, top_n = 4) # Summary plot +#' xgb.plot.shap( +#' x, +#' model = mbst, +#' trees = trees0, +#' target_class = 0, +#' top_n = 4, +#' n_col = 2, +#' col = col, +#' pch = 16, +#' pch_NA = 17 +#' ) +#' +#' xgb.plot.shap( +#' x, +#' model = mbst, +#' trees = trees0 + 1, +#' target_class = 1, +#' top_n = 4, +#' n_col = 2, +#' col = col, +#' pch = 16, +#' pch_NA = 17 +#' ) +#' +#' xgb.plot.shap( +#' x, +#' model = mbst, +#' trees = trees0 + 2, +#' target_class = 2, +#' top_n = 4, +#' n_col = 2, +#' col = col, +#' pch = 16, +#' pch_NA = 17 +#' ) +#' +#' # Summary plot +#' xgb.ggplot.shap.summary(x, model = mbst, target_class = 0, top_n = 4) #' #' @rdname xgb.plot.shap #' @export @@ -187,41 +242,48 @@ xgb.plot.shap <- function(data, shap_contrib = NULL, features = NULL, top_n = 1, invisible(list(data = data, shap_contrib = shap_contrib)) } -#' SHAP contribution dependency summary plot +#' SHAP summary plot #' -#' Compare SHAP contributions of different features. +#' Visualizes SHAP contributions of different features. #' -#' A point plot (each point representing one sample from \code{data}) is +#' A point plot (each point representing one observation from `data`) is #' produced for each feature, with the points plotted on the SHAP value axis. -#' Each point (observation) is coloured based on its feature value. The plot -#' hence allows us to see which features have a negative / positive contribution +#' Each point (observation) is coloured based on its feature value. +#' +#' The plot allows to see which features have a negative / positive contribution #' on the model prediction, and whether the contribution is different for larger -#' or smaller values of the feature. We effectively try to replicate the -#' \code{summary_plot} function from . +#' or smaller values of the feature. Inspired by the summary plot of +#' . #' #' @inheritParams xgb.plot.shap #' -#' @return A \code{ggplot2} object. +#' @return A `ggplot2` object. #' @export #' -#' @examples # See \code{\link{xgb.plot.shap}}. -#' @seealso \code{\link{xgb.plot.shap}}, \code{\link{xgb.ggplot.shap.summary}}, -#' \url{https://github.com/shap/shap} +#' @examples +#' # See examples in xgb.plot.shap() +#' +#' @seealso [xgb.plot.shap()], [xgb.ggplot.shap.summary()], +#' and the Python library . xgb.plot.shap.summary <- function(data, shap_contrib = NULL, features = NULL, top_n = 10, model = NULL, trees = NULL, target_class = NULL, approxcontrib = FALSE, subsample = NULL) { # Only ggplot implementation is available. xgb.ggplot.shap.summary(data, shap_contrib, features, top_n, model, trees, target_class, approxcontrib, subsample) } -#' Prepare data for SHAP plots. To be used in xgb.plot.shap, xgb.plot.shap.summary, etc. -#' Internal utility function. +#' Prepare data for SHAP plots +#' +#' Internal function used in [xgb.plot.shap()], [xgb.plot.shap.summary()], etc. #' #' @inheritParams xgb.plot.shap +#' @param max_observations Maximum number of observations to consider. #' @keywords internal +#' @noRd #' -#' @return A list containing: 'data', a matrix containing sample observations -#' and their feature values; 'shap_contrib', a matrix containing the SHAP contribution -#' values for these observations. +#' @return +#' A list containing: +#' - `data`: The matrix of feature values. +#' - `shap_contrib`: The matrix with corresponding SHAP values. xgb.shap.data <- function(data, shap_contrib = NULL, features = NULL, top_n = 1, model = NULL, trees = NULL, target_class = NULL, approxcontrib = FALSE, subsample = NULL, max_observations = 100000) { diff --git a/R-package/R/xgb.plot.tree.R b/R-package/R/xgb.plot.tree.R index 956c13cf7..29d00e111 100644 --- a/R-package/R/xgb.plot.tree.R +++ b/R-package/R/xgb.plot.tree.R @@ -1,69 +1,78 @@ -#' Plot a boosted tree model +#' Plot boosted trees #' #' Read a tree model text dump and plot the model. #' -#' @param feature_names names of each feature as a \code{character} vector. -#' @param model produced by the \code{xgb.train} function. -#' @param trees an integer vector of tree indices that should be visualized. -#' If set to \code{NULL}, all trees of the model are included. -#' IMPORTANT: the tree index in xgboost model is zero-based -#' (e.g., use \code{trees = 0:2} for the first 3 trees in a model). -#' @param plot_width the width of the diagram in pixels. -#' @param plot_height the height of the diagram in pixels. -#' @param render a logical flag for whether the graph should be rendered (see Value). +#' @param feature_names Character vector used to overwrite the feature names +#' of the model. The default (`NULL`) uses the original feature names. +#' @param model Object of class `xgb.Booster`. +#' @param trees An integer vector of tree indices that should be used. +#' The default (`NULL`) uses all trees. +#' Useful, e.g., in multiclass classification to get only +#' the trees of one class. *Important*: the tree index in XGBoost models +#' is zero-based (e.g., use `trees = 0:2` for the first three trees). +#' @param plot_width,plot_height Width and height of the graph in pixels. +#' The values are passed to [DiagrammeR::render_graph()]. +#' @param render Should the graph be rendered or not? The default is `TRUE`. #' @param show_node_id a logical flag for whether to show node id's in the graph. #' @param ... currently not used. #' #' @details #' -#' The content of each node is organised that way: -#' -#' \itemize{ -#' \item Feature name. -#' \item \code{Cover}: The sum of second order gradient of training data classified to the leaf. -#' If it is square loss, this simply corresponds to the number of instances seen by a split -#' or collected by a leaf during training. -#' The deeper in the tree a node is, the lower this metric will be. -#' \item \code{Gain} (for split nodes): the information gain metric of a split +#' The content of each node is visualized like this: +#' - *Feature name*. +#' - *Cover:* The sum of second order gradients of training data. +#' For the squared loss, this simply corresponds to the number of instances in the node. +#' The deeper in the tree, the lower the value. +#' - *Gain* (for split nodes): Information gain metric of a split #' (corresponds to the importance of the node in the model). -#' \item \code{Value} (for leafs): the margin value that the leaf may contribute to prediction. -#' } -#' The tree root nodes also indicate the Tree index (0-based). +#' - *Value* (for leaves): Margin value that the leaf may contribute to the prediction. +#' +#' The tree root nodes also indicate the tree index (0-based). #' #' The "Yes" branches are marked by the "< split_value" label. -#' The branches that also used for missing values are marked as bold +#' The branches also used for missing values are marked as bold #' (as in "carrying extra capacity"). #' -#' This function uses \href{https://www.graphviz.org/}{GraphViz} as a backend of DiagrammeR. +#' This function uses [GraphViz](https://www.graphviz.org/) as DiagrammeR backend. #' #' @return -#' -#' When \code{render = TRUE}: -#' returns a rendered graph object which is an \code{htmlwidget} of class \code{grViz}. -#' Similar to ggplot objects, it needs to be printed to see it when not running from command line. -#' -#' When \code{render = FALSE}: -#' silently returns a graph object which is of DiagrammeR's class \code{dgr_graph}. -#' This could be useful if one wants to modify some of the graph attributes -#' before rendering the graph with \code{\link[DiagrammeR]{render_graph}}. +#' The value depends on the `render` parameter: +#' - If `render = TRUE` (default): Rendered graph object which is an htmlwidget of +#' class `grViz`. Similar to "ggplot" objects, it needs to be printed when not +#' running from the command line. +#' - If `render = FALSE`: Graph object which is of DiagrammeR's class `dgr_graph`. +#' This could be useful if one wants to modify some of the graph attributes +#' before rendering the graph with [DiagrammeR::render_graph()]. #' #' @examples -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") +#' +#' bst <- xgboost( +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' max_depth = 3, +#' eta = 1, +#' nthread = 2, +#' nrounds = 2, +#' objective = "binary:logistic" +#' ) #' -#' bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 3, -#' eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") #' # plot all the trees #' xgb.plot.tree(model = bst) +#' #' # plot only the first tree and display the node ID: #' xgb.plot.tree(model = bst, trees = 0, show_node_id = TRUE) #' #' \dontrun{ #' # Below is an example of how to save this plot to a file. -#' # Note that for `export_graph` to work, the DiagrammeRsvg and rsvg packages must also be installed. +#' # Note that for export_graph() to work, the {DiagrammeRsvg} +#' # and {rsvg} packages must also be installed. +#' #' library(DiagrammeR) -#' gr <- xgb.plot.tree(model=bst, trees=0:1, render=FALSE) -#' export_graph(gr, 'tree.pdf', width=1500, height=1900) -#' export_graph(gr, 'tree.png', width=1500, height=1900) +#' +#' gr <- xgb.plot.tree(model = bst, trees = 0:1, render = FALSE) +#' export_graph(gr, "tree.pdf", width = 1500, height = 1900) +#' export_graph(gr, "tree.png", width = 1500, height = 1900) #' } #' #' @export diff --git a/R-package/man/xgb.importance.Rd b/R-package/man/xgb.importance.Rd index 12daca365..fca1b70c4 100644 --- a/R-package/man/xgb.importance.Rd +++ b/R-package/man/xgb.importance.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/xgb.importance.R \name{xgb.importance} \alias{xgb.importance} -\title{Importance of features in a model.} +\title{Feature importance} \usage{ xgb.importance( feature_names = NULL, @@ -14,88 +14,126 @@ xgb.importance( ) } \arguments{ -\item{feature_names}{character vector of feature names. If the model already -contains feature names, those would be used when \code{feature_names=NULL} (default value). -Non-null \code{feature_names} could be provided to override those in the model.} +\item{feature_names}{Character vector used to overwrite the feature names +of the model. The default is \code{NULL} (use original feature names).} -\item{model}{object of class \code{xgb.Booster}.} +\item{model}{Object of class \code{xgb.Booster}.} -\item{trees}{(only for the gbtree booster) an integer vector of tree indices that should be included -into the importance calculation. If set to \code{NULL}, all trees of the model are parsed. +\item{trees}{An integer vector of tree indices that should be included +into the importance calculation (only for the "gbtree" booster). +The default (\code{NULL}) parses all trees. It could be useful, e.g., in multiclass classification to get feature importances -for each class separately. IMPORTANT: the tree index in xgboost models -is zero-based (e.g., use \code{trees = 0:4} for first 5 trees).} +for each class separately. \emph{Important}: the tree index in XGBoost models +is zero-based (e.g., use \code{trees = 0:4} for the first five trees).} -\item{data}{deprecated.} +\item{data}{Deprecated.} -\item{label}{deprecated.} +\item{label}{Deprecated.} -\item{target}{deprecated.} +\item{target}{Deprecated.} } \value{ -For a tree model, a \code{data.table} with the following columns: +A \code{data.table} with the following columns: + +For a tree model: \itemize{ -\item \code{Features} names of the features used in the model; -\item \code{Gain} represents fractional contribution of each feature to the model based on -the total gain of this feature's splits. Higher percentage means a more important -predictive feature. -\item \code{Cover} metric of the number of observation related to this feature; -\item \code{Frequency} percentage representing the relative number of times -a feature have been used in trees. +\item \code{Features}: Names of the features used in the model. +\item \code{Gain}: Fractional contribution of each feature to the model based on +the total gain of this feature's splits. Higher percentage means higher importance. +\item \code{Cover}: Metric of the number of observation related to this feature. +\item \code{Frequency}: Percentage of times a feature has been used in trees. } -A linear model's importance \code{data.table} has the following columns: +For a linear model: \itemize{ -\item \code{Features} names of the features used in the model; -\item \code{Weight} the linear coefficient of this feature; -\item \code{Class} (only for multiclass models) class label. +\item \code{Features}: Names of the features used in the model. +\item \code{Weight}: Linear coefficient of this feature. +\item \code{Class}: Class label (only for multiclass models). } If \code{feature_names} is not provided and \code{model} doesn't have \code{feature_names}, -index of the features will be used instead. Because the index is extracted from the model dump +the index of the features will be used instead. Because the index is extracted from the model dump (based on C++ code), it starts at 0 (as in C/C++ or Python) instead of 1 (usual in R). } \description{ -Creates a \code{data.table} of feature importances in a model. +Creates a \code{data.table} of feature importances. } \details{ This function works for both linear and tree models. For linear models, the importance is the absolute magnitude of linear coefficients. -For that reason, in order to obtain a meaningful ranking by importance for a linear model, -the features need to be on the same scale (which you also would want to do when using either -L1 or L2 regularization). +To obtain a meaningful ranking by importance for linear models, the features need to +be on the same scale (which is also recommended when using L1 or L2 regularization). } \examples{ -# binomial classification using gbtree: -data(agaricus.train, package='xgboost') -bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +# binomial classification using "gbtree": +data(agaricus.train, package = "xgboost") + +bst <- xgboost( + data = agaricus.train$data, + label = agaricus.train$label, + max_depth = 2, + eta = 1, + nthread = 2, + nrounds = 2, + objective = "binary:logistic" +) + xgb.importance(model = bst) -# binomial classification using gblinear: -bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, booster = "gblinear", - eta = 0.3, nthread = 1, nrounds = 20, objective = "binary:logistic") +# binomial classification using "gblinear": +bst <- xgboost( + data = agaricus.train$data, + label = agaricus.train$label, + booster = "gblinear", + eta = 0.3, + nthread = 1, + nrounds = 20,objective = "binary:logistic" +) + xgb.importance(model = bst) -# multiclass classification using gbtree: +# multiclass classification using "gbtree": nclass <- 3 nrounds <- 10 -mbst <- xgboost(data = as.matrix(iris[, -5]), label = as.numeric(iris$Species) - 1, - max_depth = 3, eta = 0.2, nthread = 2, nrounds = nrounds, - objective = "multi:softprob", num_class = nclass) +mbst <- xgboost( + data = as.matrix(iris[, -5]), + label = as.numeric(iris$Species) - 1, + max_depth = 3, + eta = 0.2, + nthread = 2, + nrounds = nrounds, + objective = "multi:softprob", + num_class = nclass +) + # all classes clumped together: xgb.importance(model = mbst) -# inspect importances separately for each class: -xgb.importance(model = mbst, trees = seq(from=0, by=nclass, length.out=nrounds)) -xgb.importance(model = mbst, trees = seq(from=1, by=nclass, length.out=nrounds)) -xgb.importance(model = mbst, trees = seq(from=2, by=nclass, length.out=nrounds)) -# multiclass classification using gblinear: -mbst <- xgboost(data = scale(as.matrix(iris[, -5])), label = as.numeric(iris$Species) - 1, - booster = "gblinear", eta = 0.2, nthread = 1, nrounds = 15, - objective = "multi:softprob", num_class = nclass) +# inspect importances separately for each class: +xgb.importance( + model = mbst, trees = seq(from = 0, by = nclass, length.out = nrounds) +) +xgb.importance( + model = mbst, trees = seq(from = 1, by = nclass, length.out = nrounds) +) +xgb.importance( + model = mbst, trees = seq(from = 2, by = nclass, length.out = nrounds) +) + +# multiclass classification using "gblinear": +mbst <- xgboost( + data = scale(as.matrix(iris[, -5])), + label = as.numeric(iris$Species) - 1, + booster = "gblinear", + eta = 0.2, + nthread = 1, + nrounds = 15, + objective = "multi:softprob", + num_class = nclass +) + xgb.importance(model = mbst) } diff --git a/R-package/man/xgb.model.dt.tree.Rd b/R-package/man/xgb.model.dt.tree.Rd index 131830bde..477c40775 100644 --- a/R-package/man/xgb.model.dt.tree.Rd +++ b/R-package/man/xgb.model.dt.tree.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/xgb.model.dt.tree.R \name{xgb.model.dt.tree} \alias{xgb.model.dt.tree} -\title{Parse a boosted tree model text dump} +\title{Parse model text dump} \usage{ xgb.model.dt.tree( feature_names = NULL, @@ -14,49 +14,45 @@ xgb.model.dt.tree( ) } \arguments{ -\item{feature_names}{character vector of feature names. If the model already -contains feature names, those would be used when \code{feature_names=NULL} (default value). -Non-null \code{feature_names} could be provided to override those in the model.} +\item{feature_names}{Character vector used to overwrite the feature names +of the model. The default (\code{NULL}) uses the original feature names.} -\item{model}{object of class \code{xgb.Booster}} +\item{model}{Object of class \code{xgb.Booster}.} -\item{text}{\code{character} vector previously generated by the \code{xgb.dump} -function (where parameter \code{with_stats = TRUE} should have been set). -\code{text} takes precedence over \code{model}.} +\item{text}{Character vector previously generated by the function \code{\link[=xgb.dump]{xgb.dump()}} +(called with parameter \code{with_stats = TRUE}). \code{text} takes precedence over \code{model}.} -\item{trees}{an integer vector of tree indices that should be parsed. -If set to \code{NULL}, all trees of the model are parsed. -It could be useful, e.g., in multiclass classification to get only -the trees of one certain class. IMPORTANT: the tree index in xgboost models -is zero-based (e.g., use \code{trees = 0:4} for first 5 trees).} +\item{trees}{An integer vector of tree indices that should be used. +The default (\code{NULL}) uses all trees. +Useful, e.g., in multiclass classification to get only +the trees of one class. \emph{Important}: the tree index in XGBoost models +is zero-based (e.g., use \code{trees = 0:4} for the first five trees).} -\item{use_int_id}{a logical flag indicating whether nodes in columns "Yes", "No", "Missing" should be -represented as integers (when FALSE) or as "Tree-Node" character strings (when FALSE).} +\item{use_int_id}{A logical flag indicating whether nodes in columns "Yes", "No", and +"Missing" should be represented as integers (when \code{TRUE}) or as "Tree-Node" +character strings (when \code{FALSE}, default).} -\item{...}{currently not used.} +\item{...}{Currently not used.} } \value{ -A \code{data.table} with detailed information about model trees' nodes. - -The columns of the \code{data.table} are: - +A \code{data.table} with detailed information about tree nodes. It has the following columns: \itemize{ -\item \code{Tree}: integer ID of a tree in a model (zero-based index) -\item \code{Node}: integer ID of a node in a tree (zero-based index) -\item \code{ID}: character identifier of a node in a model (only when \code{use_int_id=FALSE}) -\item \code{Feature}: for a branch node, it's a feature id or name (when available); -for a leaf note, it simply labels it as \code{'Leaf'} -\item \code{Split}: location of the split for a branch node (split condition is always "less than") -\item \code{Yes}: ID of the next node when the split condition is met -\item \code{No}: ID of the next node when the split condition is not met -\item \code{Missing}: ID of the next node when branch value is missing -\item \code{Quality}: either the split gain (change in loss) or the leaf value -\item \code{Cover}: metric related to the number of observation either seen by a split +\item \code{Tree}: integer ID of a tree in a model (zero-based index). +\item \code{Node}: integer ID of a node in a tree (zero-based index). +\item \code{ID}: character identifier of a node in a model (only when \code{use_int_id = FALSE}). +\item \code{Feature}: for a branch node, a feature ID or name (when available); +for a leaf node, it simply labels it as \code{"Leaf"}. +\item \code{Split}: location of the split for a branch node (split condition is always "less than"). +\item \code{Yes}: ID of the next node when the split condition is met. +\item \code{No}: ID of the next node when the split condition is not met. +\item \code{Missing}: ID of the next node when the branch value is missing. +\item \code{Quality}: either the split gain (change in loss) or the leaf value. +\item \code{Cover}: metric related to the number of observations either seen by a split or collected by a leaf during training. } -When \code{use_int_id=FALSE}, columns "Yes", "No", and "Missing" point to model-wide node identifiers -in the "ID" column. When \code{use_int_id=TRUE}, those columns point to node identifiers from +When \code{use_int_id = FALSE}, columns "Yes", "No", and "Missing" point to model-wide node identifiers +in the "ID" column. When \code{use_int_id = TRUE}, those columns point to node identifiers from the corresponding trees in the "Node" column. } \description{ @@ -65,13 +61,20 @@ Parse a boosted tree model text dump into a \code{data.table} structure. \examples{ # Basic use: -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") ## Keep the number of threads to 1 for examples nthread <- 1 data.table::setDTthreads(nthread) -bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, - eta = 1, nthread = nthread, nrounds = 2,objective = "binary:logistic") +bst <- xgboost( + data = agaricus.train$data, + label = agaricus.train$label, + max_depth = 2, + eta = 1, + nthread = nthread, + nrounds = 2, + objective = "binary:logistic" +) (dt <- xgb.model.dt.tree(colnames(agaricus.train$data), bst)) @@ -80,7 +83,11 @@ bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_dep (dt <- xgb.model.dt.tree(model = bst)) # How to match feature names of splits that are following a current 'Yes' branch: - -merge(dt, dt[, .(ID, Y.Feature=Feature)], by.x='Yes', by.y='ID', all.x=TRUE)[order(Tree,Node)] +merge( + dt, + dt[, .(ID, Y.Feature = Feature)], by.x = "Yes", by.y = "ID", all.x = TRUE +)[ + order(Tree, Node) +] } diff --git a/R-package/man/xgb.plot.deepness.Rd b/R-package/man/xgb.plot.deepness.Rd index 12c5c68e2..43c0dac77 100644 --- a/R-package/man/xgb.plot.deepness.Rd +++ b/R-package/man/xgb.plot.deepness.Rd @@ -3,7 +3,7 @@ \name{xgb.ggplot.deepness} \alias{xgb.ggplot.deepness} \alias{xgb.plot.deepness} -\title{Plot model trees deepness} +\title{Plot model tree depth} \usage{ xgb.ggplot.deepness( model = NULL, @@ -18,66 +18,84 @@ xgb.plot.deepness( ) } \arguments{ -\item{model}{either an \code{xgb.Booster} model generated by the \code{xgb.train} function -or a data.table result of the \code{xgb.model.dt.tree} function.} +\item{model}{Either an \code{xgb.Booster} model, or the "data.table" returned by \code{\link[=xgb.model.dt.tree]{xgb.model.dt.tree()}}.} -\item{which}{which distribution to plot (see details).} +\item{which}{Which distribution to plot (see details).} -\item{plot}{(base R barplot) whether a barplot should be produced. -If FALSE, only a data.table is returned.} +\item{plot}{Should the plot be shown? Default is \code{TRUE}.} -\item{...}{other parameters passed to \code{barplot} or \code{plot}.} +\item{...}{Other parameters passed to \code{\link[graphics:barplot]{graphics::barplot()}} or \code{\link[graphics:plot.default]{graphics::plot()}}.} } \value{ -Other than producing plots (when \code{plot=TRUE}), the \code{xgb.plot.deepness} function -silently returns a processed data.table where each row corresponds to a terminal leaf in a tree model, -and contains information about leaf's depth, cover, and weight (which is used in calculating predictions). - -The \code{xgb.ggplot.deepness} silently returns either a list of two ggplot graphs when \code{which="2x1"} -or a single ggplot graph for the other \code{which} options. +The return value of the two functions is as follows: +\itemize{ +\item \code{xgb.plot.deepness()}: A "data.table" (invisibly). +Each row corresponds to a terminal leaf in the model. It contains its information +about depth, cover, and weight (used in calculating predictions). +If \code{plot = TRUE}, also a plot is shown. +\item \code{xgb.ggplot.deepness()}: When \code{which = "2x1"}, a list of two "ggplot" objects, +and a single "ggplot" object otherwise. +} } \description{ -Visualizes distributions related to depth of tree leafs. -\code{xgb.plot.deepness} uses base R graphics, while \code{xgb.ggplot.deepness} uses the ggplot backend. +Visualizes distributions related to the depth of tree leaves. +\itemize{ +\item \code{xgb.plot.deepness()} uses base R graphics, while +\item \code{xgb.ggplot.deepness()} uses "ggplot2". +} } \details{ -When \code{which="2x1"}, two distributions with respect to the leaf depth +When \code{which = "2x1"}, two distributions with respect to the leaf depth are plotted on top of each other: -\itemize{ -\item the distribution of the number of leafs in a tree model at a certain depth; -\item the distribution of average weighted number of observations ("cover") -ending up in leafs at certain depth. +\enumerate{ +\item The distribution of the number of leaves in a tree model at a certain depth. +\item The distribution of the average weighted number of observations ("cover") +ending up in leaves at a certain depth. } + Those could be helpful in determining sensible ranges of the \code{max_depth} and \code{min_child_weight} parameters. -When \code{which="max.depth"} or \code{which="med.depth"}, plots of either maximum or median depth -per tree with respect to tree number are created. And \code{which="med.weight"} allows to see how +When \code{which = "max.depth"} or \code{which = "med.depth"}, plots of either maximum or +median depth per tree with respect to the tree number are created. + +Finally, \code{which = "med.weight"} allows to see how a tree's median absolute leaf weight changes through the iterations. -This function was inspired by the blog post +These functions have been inspired by the blog post \url{https://github.com/aysent/random-forest-leaf-visualization}. } \examples{ -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") ## Keep the number of threads to 2 for examples nthread <- 2 data.table::setDTthreads(nthread) ## Change max_depth to a higher number to get a more significant result -bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 6, - eta = 0.1, nthread = nthread, nrounds = 50, objective = "binary:logistic", - subsample = 0.5, min_child_weight = 2) +bst <- xgboost( + data = agaricus.train$data, + label = agaricus.train$label, + max_depth = 6, + nthread = nthread, + nrounds = 50, + objective = "binary:logistic", + subsample = 0.5, + min_child_weight = 2 +) xgb.plot.deepness(bst) xgb.ggplot.deepness(bst) -xgb.plot.deepness(bst, which='max.depth', pch=16, col=rgb(0,0,1,0.3), cex=2) +xgb.plot.deepness( + bst, which = "max.depth", pch = 16, col = rgb(0, 0, 1, 0.3), cex = 2 +) -xgb.plot.deepness(bst, which='med.weight', pch=16, col=rgb(0,0,1,0.3), cex=2) +xgb.plot.deepness( + bst, which = "med.weight", pch = 16, col = rgb(0, 0, 1, 0.3), cex = 2 +) } \seealso{ -\code{\link{xgb.train}}, \code{\link{xgb.model.dt.tree}}. +\code{\link[=xgb.train]{xgb.train()}} and \code{\link[=xgb.model.dt.tree]{xgb.model.dt.tree()}}. } diff --git a/R-package/man/xgb.plot.importance.Rd b/R-package/man/xgb.plot.importance.Rd index 4dba62afe..e9c5930c2 100644 --- a/R-package/man/xgb.plot.importance.Rd +++ b/R-package/man/xgb.plot.importance.Rd @@ -3,7 +3,7 @@ \name{xgb.ggplot.importance} \alias{xgb.ggplot.importance} \alias{xgb.plot.importance} -\title{Plot feature importance as a bar graph} +\title{Plot feature importance} \usage{ xgb.ggplot.importance( importance_matrix = NULL, @@ -26,74 +26,90 @@ xgb.plot.importance( ) } \arguments{ -\item{importance_matrix}{a \code{data.table} returned by \code{\link{xgb.importance}}.} +\item{importance_matrix}{A \code{data.table} as returned by \code{\link[=xgb.importance]{xgb.importance()}}.} -\item{top_n}{maximal number of top features to include into the plot.} +\item{top_n}{Maximal number of top features to include into the plot.} -\item{measure}{the name of importance measure to plot. +\item{measure}{The name of importance measure to plot. When \code{NULL}, 'Gain' would be used for trees and 'Weight' would be used for gblinear.} -\item{rel_to_first}{whether importance values should be represented as relative to the highest ranked feature. -See Details.} +\item{rel_to_first}{Whether importance values should be represented as relative to +the highest ranked feature, see Details.} -\item{n_clusters}{(ggplot only) a \code{numeric} vector containing the min and the max range +\item{n_clusters}{A numeric vector containing the min and the max range of the possible number of clusters of bars.} -\item{...}{other parameters passed to \code{barplot} (except horiz, border, cex.names, names.arg, and las).} +\item{...}{Other parameters passed to \code{\link[graphics:barplot]{graphics::barplot()}} +(except \code{horiz}, \code{border}, \code{cex.names}, \code{names.arg}, and \code{las}). +Only used in \code{xgb.plot.importance()}.} -\item{left_margin}{(base R barplot) allows to adjust the left margin size to fit feature names. -When it is NULL, the existing \code{par('mar')} is used.} +\item{left_margin}{Adjust the left margin size to fit feature names. +When \code{NULL}, the existing \code{par("mar")} is used.} -\item{cex}{(base R barplot) passed as \code{cex.names} parameter to \code{barplot}.} +\item{cex}{Passed as \code{cex.names} parameter to \code{\link[graphics:barplot]{graphics::barplot()}}.} -\item{plot}{(base R barplot) whether a barplot should be produced. -If FALSE, only a data.table is returned.} +\item{plot}{Should the barplot be shown? Default is \code{TRUE}.} } \value{ -The \code{xgb.plot.importance} function creates a \code{barplot} (when \code{plot=TRUE}) -and silently returns a processed data.table with \code{n_top} features sorted by importance. - -The \code{xgb.ggplot.importance} function returns a ggplot graph which could be customized afterwards. -E.g., to change the title of the graph, add \code{+ ggtitle("A GRAPH NAME")} to the result. +The return value depends on the function: +\itemize{ +\item \code{xgb.plot.importance()}: Invisibly, a "data.table" with \code{n_top} features sorted +by importance. If \code{plot = TRUE}, the values are also plotted as barplot. +\item \code{xgb.ggplot.importance()}: A customizable "ggplot" object. +E.g., to change the title, set \code{+ ggtitle("A GRAPH NAME")}. +} } \description{ Represents previously calculated feature importance as a bar graph. -\code{xgb.plot.importance} uses base R graphics, while \code{xgb.ggplot.importance} uses the ggplot backend. +\itemize{ +\item \code{xgb.plot.importance()} uses base R graphics, while +\item \code{xgb.ggplot.importance()} uses "ggplot". +} } \details{ The graph represents each feature as a horizontal bar of length proportional to the importance of a feature. -Features are shown ranked in a decreasing importance order. -It works for importances from both \code{gblinear} and \code{gbtree} models. +Features are sorted by decreasing importance. +It works for both "gblinear" and "gbtree" models. -When \code{rel_to_first = FALSE}, the values would be plotted as they were in \code{importance_matrix}. -For gbtree model, that would mean being normalized to the total of 1 +When \code{rel_to_first = FALSE}, the values would be plotted as in \code{importance_matrix}. +For a "gbtree" model, that would mean being normalized to the total of 1 ("what is feature's importance contribution relative to the whole model?"). For linear models, \code{rel_to_first = FALSE} would show actual values of the coefficients. Setting \code{rel_to_first = TRUE} allows to see the picture from the perspective of "what is feature's importance contribution relative to the most important feature?" -The ggplot-backend method also performs 1-D clustering of the importance values, -with bar colors corresponding to different clusters that have somewhat similar importance values. +The "ggplot" backend performs 1-D clustering of the importance values, +with bar colors corresponding to different clusters having similar importance values. } \examples{ data(agaricus.train) + ## Keep the number of threads to 2 for examples nthread <- 2 data.table::setDTthreads(nthread) bst <- xgboost( - data = agaricus.train$data, label = agaricus.train$label, max_depth = 3, - eta = 1, nthread = nthread, nrounds = 2, objective = "binary:logistic" + data = agaricus.train$data, + label = agaricus.train$label, + max_depth = 3, + eta = 1, + nthread = nthread, + nrounds = 2, + objective = "binary:logistic" ) importance_matrix <- xgb.importance(colnames(agaricus.train$data), model = bst) +xgb.plot.importance( + importance_matrix, rel_to_first = TRUE, xlab = "Relative importance" +) -xgb.plot.importance(importance_matrix, rel_to_first = TRUE, xlab = "Relative importance") - -(gg <- xgb.ggplot.importance(importance_matrix, measure = "Frequency", rel_to_first = TRUE)) +gg <- xgb.ggplot.importance( + importance_matrix, measure = "Frequency", rel_to_first = TRUE +) +gg gg + ggplot2::ylab("Frequency") } \seealso{ -\code{\link[graphics]{barplot}}. +\code{\link[graphics:barplot]{graphics::barplot()}} } diff --git a/R-package/man/xgb.plot.multi.trees.Rd b/R-package/man/xgb.plot.multi.trees.Rd index 4fa526b90..d98a3482c 100644 --- a/R-package/man/xgb.plot.multi.trees.Rd +++ b/R-package/man/xgb.plot.multi.trees.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/xgb.plot.multi.trees.R \name{xgb.plot.multi.trees} \alias{xgb.plot.multi.trees} -\title{Project all trees on one tree and plot it} +\title{Project all trees on one tree} \usage{ xgb.plot.multi.trees( model, @@ -15,29 +15,31 @@ xgb.plot.multi.trees( ) } \arguments{ -\item{model}{produced by the \code{xgb.train} function.} +\item{model}{Object of class \code{xgb.Booster}.} -\item{feature_names}{names of each feature as a \code{character} vector.} +\item{feature_names}{Character vector used to overwrite the feature names +of the model. The default (\code{NULL}) uses the original feature names.} -\item{features_keep}{number of features to keep in each position of the multi trees.} +\item{features_keep}{Number of features to keep in each position of the multi trees, +by default 5.} -\item{plot_width}{width in pixels of the graph to produce} +\item{plot_width, plot_height}{Width and height of the graph in pixels. +The values are passed to \code{\link[DiagrammeR:render_graph]{DiagrammeR::render_graph()}}.} -\item{plot_height}{height in pixels of the graph to produce} +\item{render}{Should the graph be rendered or not? The default is \code{TRUE}.} -\item{render}{a logical flag for whether the graph should be rendered (see Value).} - -\item{...}{currently not used} +\item{...}{currently not used.} } \value{ -When \code{render = TRUE}: -returns a rendered graph object which is an \code{htmlwidget} of class \code{grViz}. -Similar to ggplot objects, it needs to be printed to see it when not running from command line. - -When \code{render = FALSE}: -silently returns a graph object which is of DiagrammeR's class \code{dgr_graph}. +The value depends on the \code{render} parameter: +\itemize{ +\item If \code{render = TRUE} (default): Rendered graph object which is an htmlwidget of +class \code{grViz}. Similar to "ggplot" objects, it needs to be printed when not +running from the command line. +\item If \code{render = FALSE}: Graph object which is of DiagrammeR's class \code{dgr_graph}. This could be useful if one wants to modify some of the graph attributes -before rendering the graph with \code{\link[DiagrammeR]{render_graph}}. +before rendering the graph with \code{\link[DiagrammeR:render_graph]{DiagrammeR::render_graph()}}. +} } \description{ Visualization of the ensemble of trees as a single collective unit. @@ -62,15 +64,22 @@ This function is inspired by this blog post: } \examples{ -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") + ## Keep the number of threads to 2 for examples nthread <- 2 data.table::setDTthreads(nthread) bst <- xgboost( - data = agaricus.train$data, label = agaricus.train$label, max_depth = 15, - eta = 1, nthread = nthread, nrounds = 30, objective = "binary:logistic", - min_child_weight = 50, verbose = 0 + data = agaricus.train$data, + label = agaricus.train$label, + max_depth = 15, + eta = 1, + nthread = nthread, + nrounds = 30, + objective = "binary:logistic", + min_child_weight = 50, + verbose = 0 ) p <- xgb.plot.multi.trees(model = bst, features_keep = 3) @@ -78,10 +87,13 @@ print(p) \dontrun{ # Below is an example of how to save this plot to a file. -# Note that for `export_graph` to work, the DiagrammeRsvg and rsvg packages must also be installed. +# Note that for export_graph() to work, the {DiagrammeRsvg} and {rsvg} packages +# must also be installed. + library(DiagrammeR) -gr <- xgb.plot.multi.trees(model=bst, features_keep = 3, render=FALSE) -export_graph(gr, 'tree.pdf', width=1500, height=600) + +gr <- xgb.plot.multi.trees(model = bst, features_keep = 3, render = FALSE) +export_graph(gr, "tree.pdf", width = 1500, height = 600) } } diff --git a/R-package/man/xgb.plot.shap.Rd b/R-package/man/xgb.plot.shap.Rd index 75f8d2d0f..b460fa1fb 100644 --- a/R-package/man/xgb.plot.shap.Rd +++ b/R-package/man/xgb.plot.shap.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/xgb.plot.shap.R \name{xgb.plot.shap} \alias{xgb.plot.shap} -\title{SHAP contribution dependency plots} +\title{SHAP dependence plots} \usage{ xgb.plot.shap( data, @@ -33,87 +33,93 @@ xgb.plot.shap( ) } \arguments{ -\item{data}{data as a \code{matrix} or \code{dgCMatrix}.} +\item{data}{The data to explain as a \code{matrix} or \code{dgCMatrix}.} -\item{shap_contrib}{a matrix of SHAP contributions that was computed earlier for the above -\code{data}. When it is NULL, it is computed internally using \code{model} and \code{data}.} +\item{shap_contrib}{Matrix of SHAP contributions of \code{data}. +The default (\code{NULL}) computes it from \code{model} and \code{data}.} -\item{features}{a vector of either column indices or of feature names to plot. When it is NULL, -feature importance is calculated, and \code{top_n} high ranked features are taken.} +\item{features}{Vector of column indices or feature names to plot. +When \code{NULL} (default), the \code{top_n} most important features are selected +by \code{\link[=xgb.importance]{xgb.importance()}}.} -\item{top_n}{when \code{features} is NULL, top_n \verb{[1, 100]} most important features in a model are taken.} +\item{top_n}{How many of the most important features (<= 100) should be selected? +By default 1 for SHAP dependence and 10 for SHAP summary). +Only used when \code{features = NULL}.} -\item{model}{an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} -or \code{features} is missing.} +\item{model}{An \code{xgb.Booster} model. Only required when \code{shap_contrib = NULL} or +\code{features = NULL}.} -\item{trees}{passed to \code{\link{xgb.importance}} when \code{features = NULL}.} +\item{trees}{Passed to \code{\link[=xgb.importance]{xgb.importance()}} when \code{features = NULL}.} -\item{target_class}{is only relevant for multiclass models. When it is set to a 0-based class index, -only SHAP contributions for that specific class are used. -If it is not set, SHAP importances are averaged over all classes.} +\item{target_class}{Only relevant for multiclass models. The default (\code{NULL}) +averages the SHAP values over all classes. Pass a (0-based) class index +to show only SHAP values of that class.} -\item{approxcontrib}{passed to \code{\link{predict.xgb.Booster}} when \code{shap_contrib = NULL}.} +\item{approxcontrib}{Passed to \code{predict()} when \code{shap_contrib = NULL}.} -\item{subsample}{a random fraction of data points to use for plotting. When it is NULL, -it is set so that up to 100K data points are used.} +\item{subsample}{Fraction of data points randomly picked for plotting. +The default (\code{NULL}) will use up to 100k data points.} -\item{n_col}{a number of columns in a grid of plots.} +\item{n_col}{Number of columns in a grid of plots.} -\item{col}{color of the scatterplot markers.} +\item{col}{Color of the scatterplot markers.} -\item{pch}{scatterplot marker.} +\item{pch}{Scatterplot marker.} -\item{discrete_n_uniq}{a maximal number of unique values in a feature to consider it as discrete.} +\item{discrete_n_uniq}{Maximal number of unique feature values to consider the +feature as discrete.} -\item{discrete_jitter}{an \code{amount} parameter of jitter added to discrete features' positions.} +\item{discrete_jitter}{Jitter amount added to the values of discrete features.} -\item{ylab}{a y-axis label in 1D plots.} +\item{ylab}{The y-axis label in 1D plots.} -\item{plot_NA}{whether the contributions of cases with missing values should also be plotted.} +\item{plot_NA}{Should contributions of cases with missing values be plotted? +Default is \code{TRUE}.} -\item{col_NA}{a color of marker for missing value contributions.} +\item{col_NA}{Color of marker for missing value contributions.} -\item{pch_NA}{a marker type for NA values.} +\item{pch_NA}{Marker type for \code{NA} values.} -\item{pos_NA}{a relative position of the x-location where NA values are shown: +\item{pos_NA}{Relative position of the x-location where \code{NA} values are shown: \code{min(x) + (max(x) - min(x)) * pos_NA}.} -\item{plot_loess}{whether to plot loess-smoothed curves. The smoothing is only done for features with -more than 5 distinct values.} +\item{plot_loess}{Should loess-smoothed curves be plotted? (Default is \code{TRUE}). +The smoothing is only done for features with more than 5 distinct values.} -\item{col_loess}{a color to use for the loess curves.} +\item{col_loess}{Color of loess curves.} -\item{span_loess}{the \code{span} parameter in \code{\link[stats]{loess}}'s call.} +\item{span_loess}{The \code{span} parameter of \code{\link[stats:loess]{stats::loess()}}.} -\item{which}{whether to do univariate or bivariate plotting. NOTE: only 1D is implemented so far.} +\item{which}{Whether to do univariate or bivariate plotting. Currently, only "1d" is implemented.} -\item{plot}{whether a plot should be drawn. If FALSE, only a list of matrices is returned.} +\item{plot}{Should the plot be drawn? (Default is \code{TRUE}). +If \code{FALSE}, only a list of matrices is returned.} -\item{...}{other parameters passed to \code{plot}.} +\item{...}{Other parameters passed to \code{\link[graphics:plot.default]{graphics::plot()}}.} } \value{ -In addition to producing plots (when \code{plot=TRUE}), it silently returns a list of two matrices: +In addition to producing plots (when \code{plot = TRUE}), it silently returns a list of two matrices: \itemize{ -\item \code{data} the values of selected features; -\item \code{shap_contrib} the contributions of selected features. +\item \code{data}: Feature value matrix. +\item \code{shap_contrib}: Corresponding SHAP value matrix. } } \description{ -Visualizing the SHAP feature contribution to prediction dependencies on feature value. +Visualizes SHAP values against feature values to gain an impression of feature effects. } \details{ These scatterplots represent how SHAP feature contributions depend of feature values. -The similarity to partial dependency plots is that they also give an idea for how feature values -affect predictions. However, in partial dependency plots, we usually see marginal dependencies -of model prediction on feature value, while SHAP contribution dependency plots display the estimated -contributions of a feature to model prediction for each individual case. +The similarity to partial dependence plots is that they also give an idea for how feature values +affect predictions. However, in partial dependence plots, we see marginal dependencies +of model prediction on feature value, while SHAP dependence plots display the estimated +contributions of a feature to the prediction for each individual case. -When \code{plot_loess = TRUE} is set, feature values are rounded to 3 significant digits and -weighted LOESS is computed and plotted, where weights are the numbers of data points +When \code{plot_loess = TRUE}, feature values are rounded to three significant digits and +weighted LOESS is computed and plotted, where the weights are the numbers of data points at each rounded value. -Note: SHAP contributions are shown on the scale of model margin. E.g., for a logistic binomial objective, -the margin is prediction before a sigmoidal transform into probability-like values. +Note: SHAP contributions are on the scale of the model margin. +E.g., for a logistic binomial objective, the margin is on log-odds scale. Also, since SHAP stands for "SHapley Additive exPlanation" (model prediction = sum of SHAP contributions for all features + bias), depending on the objective used, transforming SHAP contributions for a feature from the marginal to the prediction space is not necessarily @@ -121,44 +127,99 @@ a meaningful thing to do. } \examples{ -data(agaricus.train, package='xgboost') -data(agaricus.test, package='xgboost') +data(agaricus.train, package = "xgboost") +data(agaricus.test, package = "xgboost") ## Keep the number of threads to 1 for examples nthread <- 1 data.table::setDTthreads(nthread) nrounds <- 20 -bst <- xgboost(agaricus.train$data, agaricus.train$label, nrounds = nrounds, - eta = 0.1, max_depth = 3, subsample = .5, - method = "hist", objective = "binary:logistic", nthread = nthread, verbose = 0) +bst <- xgboost( + agaricus.train$data, + agaricus.train$label, + nrounds = nrounds, + eta = 0.1, + max_depth = 3, + subsample = 0.5, + objective = "binary:logistic", + nthread = nthread, + verbose = 0 +) xgb.plot.shap(agaricus.test$data, model = bst, features = "odor=none") + contr <- predict(bst, agaricus.test$data, predcontrib = TRUE) xgb.plot.shap(agaricus.test$data, contr, model = bst, top_n = 12, n_col = 3) -xgb.ggplot.shap.summary(agaricus.test$data, contr, model = bst, top_n = 12) # Summary plot -# multiclass example - plots for each class separately: +# Summary plot +xgb.ggplot.shap.summary(agaricus.test$data, contr, model = bst, top_n = 12) + +# Multiclass example - plots for each class separately: nclass <- 3 x <- as.matrix(iris[, -5]) set.seed(123) is.na(x[sample(nrow(x) * 4, 30)]) <- TRUE # introduce some missing values -mbst <- xgboost(data = x, label = as.numeric(iris$Species) - 1, nrounds = nrounds, - max_depth = 2, eta = 0.3, subsample = .5, nthread = nthread, - objective = "multi:softprob", num_class = nclass, verbose = 0) -trees0 <- seq(from=0, by=nclass, length.out=nrounds) + +mbst <- xgboost( + data = x, + label = as.numeric(iris$Species) - 1, + nrounds = nrounds, + max_depth = 2, + eta = 0.3, + subsample = 0.5, + nthread = nthread, + objective = "multi:softprob", + num_class = nclass, + verbose = 0 +) +trees0 <- seq(from = 0, by = nclass, length.out = nrounds) col <- rgb(0, 0, 1, 0.5) -xgb.plot.shap(x, model = mbst, trees = trees0, target_class = 0, top_n = 4, - n_col = 2, col = col, pch = 16, pch_NA = 17) -xgb.plot.shap(x, model = mbst, trees = trees0 + 1, target_class = 1, top_n = 4, - n_col = 2, col = col, pch = 16, pch_NA = 17) -xgb.plot.shap(x, model = mbst, trees = trees0 + 2, target_class = 2, top_n = 4, - n_col = 2, col = col, pch = 16, pch_NA = 17) -xgb.ggplot.shap.summary(x, model = mbst, target_class = 0, top_n = 4) # Summary plot +xgb.plot.shap( + x, + model = mbst, + trees = trees0, + target_class = 0, + top_n = 4, + n_col = 2, + col = col, + pch = 16, + pch_NA = 17 +) + +xgb.plot.shap( + x, + model = mbst, + trees = trees0 + 1, + target_class = 1, + top_n = 4, + n_col = 2, + col = col, + pch = 16, + pch_NA = 17 +) + +xgb.plot.shap( + x, + model = mbst, + trees = trees0 + 2, + target_class = 2, + top_n = 4, + n_col = 2, + col = col, + pch = 16, + pch_NA = 17 +) + +# Summary plot +xgb.ggplot.shap.summary(x, model = mbst, target_class = 0, top_n = 4) } \references{ -Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", NIPS Proceedings 2017, \url{https://arxiv.org/abs/1705.07874} - -Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", \url{https://arxiv.org/abs/1706.06060} +\enumerate{ +\item Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", +NIPS Proceedings 2017, \url{https://arxiv.org/abs/1705.07874} +\item Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", +\url{https://arxiv.org/abs/1706.06060} +} } diff --git a/R-package/man/xgb.plot.shap.summary.Rd b/R-package/man/xgb.plot.shap.summary.Rd index 910119e6f..b0ad20dd7 100644 --- a/R-package/man/xgb.plot.shap.summary.Rd +++ b/R-package/man/xgb.plot.shap.summary.Rd @@ -3,7 +3,7 @@ \name{xgb.ggplot.shap.summary} \alias{xgb.ggplot.shap.summary} \alias{xgb.plot.shap.summary} -\title{SHAP contribution dependency summary plot} +\title{SHAP summary plot} \usage{ xgb.ggplot.shap.summary( data, @@ -30,49 +30,54 @@ xgb.plot.shap.summary( ) } \arguments{ -\item{data}{data as a \code{matrix} or \code{dgCMatrix}.} +\item{data}{The data to explain as a \code{matrix} or \code{dgCMatrix}.} -\item{shap_contrib}{a matrix of SHAP contributions that was computed earlier for the above -\code{data}. When it is NULL, it is computed internally using \code{model} and \code{data}.} +\item{shap_contrib}{Matrix of SHAP contributions of \code{data}. +The default (\code{NULL}) computes it from \code{model} and \code{data}.} -\item{features}{a vector of either column indices or of feature names to plot. When it is NULL, -feature importance is calculated, and \code{top_n} high ranked features are taken.} +\item{features}{Vector of column indices or feature names to plot. +When \code{NULL} (default), the \code{top_n} most important features are selected +by \code{\link[=xgb.importance]{xgb.importance()}}.} -\item{top_n}{when \code{features} is NULL, top_n \verb{[1, 100]} most important features in a model are taken.} +\item{top_n}{How many of the most important features (<= 100) should be selected? +By default 1 for SHAP dependence and 10 for SHAP summary). +Only used when \code{features = NULL}.} -\item{model}{an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} -or \code{features} is missing.} +\item{model}{An \code{xgb.Booster} model. Only required when \code{shap_contrib = NULL} or +\code{features = NULL}.} -\item{trees}{passed to \code{\link{xgb.importance}} when \code{features = NULL}.} +\item{trees}{Passed to \code{\link[=xgb.importance]{xgb.importance()}} when \code{features = NULL}.} -\item{target_class}{is only relevant for multiclass models. When it is set to a 0-based class index, -only SHAP contributions for that specific class are used. -If it is not set, SHAP importances are averaged over all classes.} +\item{target_class}{Only relevant for multiclass models. The default (\code{NULL}) +averages the SHAP values over all classes. Pass a (0-based) class index +to show only SHAP values of that class.} -\item{approxcontrib}{passed to \code{\link{predict.xgb.Booster}} when \code{shap_contrib = NULL}.} +\item{approxcontrib}{Passed to \code{predict()} when \code{shap_contrib = NULL}.} -\item{subsample}{a random fraction of data points to use for plotting. When it is NULL, -it is set so that up to 100K data points are used.} +\item{subsample}{Fraction of data points randomly picked for plotting. +The default (\code{NULL}) will use up to 100k data points.} } \value{ A \code{ggplot2} object. } \description{ -Compare SHAP contributions of different features. +Visualizes SHAP contributions of different features. } \details{ -A point plot (each point representing one sample from \code{data}) is +A point plot (each point representing one observation from \code{data}) is produced for each feature, with the points plotted on the SHAP value axis. -Each point (observation) is coloured based on its feature value. The plot -hence allows us to see which features have a negative / positive contribution +Each point (observation) is coloured based on its feature value. + +The plot allows to see which features have a negative / positive contribution on the model prediction, and whether the contribution is different for larger -or smaller values of the feature. We effectively try to replicate the -\code{summary_plot} function from \url{https://github.com/shap/shap}. +or smaller values of the feature. Inspired by the summary plot of +\url{https://github.com/shap/shap}. } \examples{ -# See \code{\link{xgb.plot.shap}}. +# See examples in xgb.plot.shap() + } \seealso{ -\code{\link{xgb.plot.shap}}, \code{\link{xgb.ggplot.shap.summary}}, -\url{https://github.com/shap/shap} +\code{\link[=xgb.plot.shap]{xgb.plot.shap()}}, \code{\link[=xgb.ggplot.shap.summary]{xgb.ggplot.shap.summary()}}, +and the Python library \url{https://github.com/shap/shap}. } diff --git a/R-package/man/xgb.plot.tree.Rd b/R-package/man/xgb.plot.tree.Rd index 224e393ce..7571487eb 100644 --- a/R-package/man/xgb.plot.tree.Rd +++ b/R-package/man/xgb.plot.tree.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/xgb.plot.tree.R \name{xgb.plot.tree} \alias{xgb.plot.tree} -\title{Plot a boosted tree model} +\title{Plot boosted trees} \usage{ xgb.plot.tree( feature_names = NULL, @@ -16,76 +16,89 @@ xgb.plot.tree( ) } \arguments{ -\item{feature_names}{names of each feature as a \code{character} vector.} +\item{feature_names}{Character vector used to overwrite the feature names +of the model. The default (\code{NULL}) uses the original feature names.} -\item{model}{produced by the \code{xgb.train} function.} +\item{model}{Object of class \code{xgb.Booster}.} -\item{trees}{an integer vector of tree indices that should be visualized. -If set to \code{NULL}, all trees of the model are included. -IMPORTANT: the tree index in xgboost model is zero-based -(e.g., use \code{trees = 0:2} for the first 3 trees in a model).} +\item{trees}{An integer vector of tree indices that should be used. +The default (\code{NULL}) uses all trees. +Useful, e.g., in multiclass classification to get only +the trees of one class. \emph{Important}: the tree index in XGBoost models +is zero-based (e.g., use \code{trees = 0:2} for the first three trees).} -\item{plot_width}{the width of the diagram in pixels.} +\item{plot_width, plot_height}{Width and height of the graph in pixels. +The values are passed to \code{\link[DiagrammeR:render_graph]{DiagrammeR::render_graph()}}.} -\item{plot_height}{the height of the diagram in pixels.} - -\item{render}{a logical flag for whether the graph should be rendered (see Value).} +\item{render}{Should the graph be rendered or not? The default is \code{TRUE}.} \item{show_node_id}{a logical flag for whether to show node id's in the graph.} \item{...}{currently not used.} } \value{ -When \code{render = TRUE}: -returns a rendered graph object which is an \code{htmlwidget} of class \code{grViz}. -Similar to ggplot objects, it needs to be printed to see it when not running from command line. - -When \code{render = FALSE}: -silently returns a graph object which is of DiagrammeR's class \code{dgr_graph}. +The value depends on the \code{render} parameter: +\itemize{ +\item If \code{render = TRUE} (default): Rendered graph object which is an htmlwidget of +class \code{grViz}. Similar to "ggplot" objects, it needs to be printed when not +running from the command line. +\item If \code{render = FALSE}: Graph object which is of DiagrammeR's class \code{dgr_graph}. This could be useful if one wants to modify some of the graph attributes -before rendering the graph with \code{\link[DiagrammeR]{render_graph}}. +before rendering the graph with \code{\link[DiagrammeR:render_graph]{DiagrammeR::render_graph()}}. +} } \description{ Read a tree model text dump and plot the model. } \details{ -The content of each node is organised that way: - +The content of each node is visualized like this: \itemize{ -\item Feature name. -\item \code{Cover}: The sum of second order gradient of training data classified to the leaf. -If it is square loss, this simply corresponds to the number of instances seen by a split -or collected by a leaf during training. -The deeper in the tree a node is, the lower this metric will be. -\item \code{Gain} (for split nodes): the information gain metric of a split +\item \emph{Feature name}. +\item \emph{Cover:} The sum of second order gradients of training data. +For the squared loss, this simply corresponds to the number of instances in the node. +The deeper in the tree, the lower the value. +\item \emph{Gain} (for split nodes): Information gain metric of a split (corresponds to the importance of the node in the model). -\item \code{Value} (for leafs): the margin value that the leaf may contribute to prediction. +\item \emph{Value} (for leaves): Margin value that the leaf may contribute to the prediction. } -The tree root nodes also indicate the Tree index (0-based). + +The tree root nodes also indicate the tree index (0-based). The "Yes" branches are marked by the "< split_value" label. -The branches that also used for missing values are marked as bold +The branches also used for missing values are marked as bold (as in "carrying extra capacity"). -This function uses \href{https://www.graphviz.org/}{GraphViz} as a backend of DiagrammeR. +This function uses \href{https://www.graphviz.org/}{GraphViz} as DiagrammeR backend. } \examples{ -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") + +bst <- xgboost( + data = agaricus.train$data, + label = agaricus.train$label, + max_depth = 3, + eta = 1, + nthread = 2, + nrounds = 2, + objective = "binary:logistic" +) -bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 3, - eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") # plot all the trees xgb.plot.tree(model = bst) + # plot only the first tree and display the node ID: xgb.plot.tree(model = bst, trees = 0, show_node_id = TRUE) \dontrun{ # Below is an example of how to save this plot to a file. -# Note that for `export_graph` to work, the DiagrammeRsvg and rsvg packages must also be installed. +# Note that for export_graph() to work, the {DiagrammeRsvg} +# and {rsvg} packages must also be installed. + library(DiagrammeR) -gr <- xgb.plot.tree(model=bst, trees=0:1, render=FALSE) -export_graph(gr, 'tree.pdf', width=1500, height=1900) -export_graph(gr, 'tree.png', width=1500, height=1900) + +gr <- xgb.plot.tree(model = bst, trees = 0:1, render = FALSE) +export_graph(gr, "tree.pdf", width = 1500, height = 1900) +export_graph(gr, "tree.png", width = 1500, height = 1900) } } diff --git a/R-package/man/xgb.shap.data.Rd b/R-package/man/xgb.shap.data.Rd deleted file mode 100644 index 6c4336cde..000000000 --- a/R-package/man/xgb.shap.data.Rd +++ /dev/null @@ -1,55 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xgb.plot.shap.R -\name{xgb.shap.data} -\alias{xgb.shap.data} -\title{Prepare data for SHAP plots. To be used in xgb.plot.shap, xgb.plot.shap.summary, etc. -Internal utility function.} -\usage{ -xgb.shap.data( - data, - shap_contrib = NULL, - features = NULL, - top_n = 1, - model = NULL, - trees = NULL, - target_class = NULL, - approxcontrib = FALSE, - subsample = NULL, - max_observations = 1e+05 -) -} -\arguments{ -\item{data}{data as a \code{matrix} or \code{dgCMatrix}.} - -\item{shap_contrib}{a matrix of SHAP contributions that was computed earlier for the above -\code{data}. When it is NULL, it is computed internally using \code{model} and \code{data}.} - -\item{features}{a vector of either column indices or of feature names to plot. When it is NULL, -feature importance is calculated, and \code{top_n} high ranked features are taken.} - -\item{top_n}{when \code{features} is NULL, top_n \verb{[1, 100]} most important features in a model are taken.} - -\item{model}{an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} -or \code{features} is missing.} - -\item{trees}{passed to \code{\link{xgb.importance}} when \code{features = NULL}.} - -\item{target_class}{is only relevant for multiclass models. When it is set to a 0-based class index, -only SHAP contributions for that specific class are used. -If it is not set, SHAP importances are averaged over all classes.} - -\item{approxcontrib}{passed to \code{\link{predict.xgb.Booster}} when \code{shap_contrib = NULL}.} - -\item{subsample}{a random fraction of data points to use for plotting. When it is NULL, -it is set so that up to 100K data points are used.} -} -\value{ -A list containing: 'data', a matrix containing sample observations -and their feature values; 'shap_contrib', a matrix containing the SHAP contribution -values for these observations. -} -\description{ -Prepare data for SHAP plots. To be used in xgb.plot.shap, xgb.plot.shap.summary, etc. -Internal utility function. -} -\keyword{internal} From a197899161fa70e681101de4232745fdfe737804 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 26 Dec 2023 13:29:55 +0100 Subject: [PATCH 32/59] [R] avoid leaking exception objects (#9916) --- R-package/src/xgboost_R.cc | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 79b9e6037..fb05c33b4 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -161,15 +161,13 @@ object, need to be destructed before triggering the R error. In order to preserve the error message, it gets copied to a temporary buffer, and the R error section is reached through a 'goto' statement that bypasses usual function control flow. */ -char cpp_ex_msg[256]; +char cpp_ex_msg[512]; /*! * \brief macro to annotate end of api */ #define R_API_END() \ - } catch(dmlc::Error& e) { \ - Rf_error("%s", e.what()); \ } catch(std::exception &e) { \ - std::strncpy(cpp_ex_msg, e.what(), 256); \ + std::strncpy(cpp_ex_msg, e.what(), 512); \ goto throw_cpp_ex_as_R_err; \ } \ if (false) { \ From a7226c02223246be78a59c3a4e8c32d1c68c1ff9 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 28 Dec 2023 22:45:13 +0800 Subject: [PATCH 33/59] Fix feature names with special characters. (#9923) --- src/common/common.h | 12 +++ src/tree/tree_model.cc | 118 +++++++++++++++--------------- tests/cpp/tree/test_tree_model.cc | 2 +- tests/python/test_basic_models.py | 24 ++++-- 4 files changed, 88 insertions(+), 68 deletions(-) diff --git a/src/common/common.h b/src/common/common.h index 4b20ce7c2..950dee521 100644 --- a/src/common/common.h +++ b/src/common/common.h @@ -66,8 +66,20 @@ inline std::vector Split(const std::string& s, char delim) { return ret; } +/** + * @brief Add escapes for a UTF-8 string. + */ void EscapeU8(std::string const &string, std::string *p_buffer); +/** + * @brief Add escapes for a UTF-8 string with newly created buffer as return. + */ +inline std::string EscapeU8(std::string const &str) { + std::string buffer; + EscapeU8(str, &buffer); + return buffer; +} + template XGBOOST_DEVICE T Max(T a, T b) { return a < b ? b : a; diff --git a/src/tree/tree_model.cc b/src/tree/tree_model.cc index d37be14b8..f18b51926 100644 --- a/src/tree/tree_model.cc +++ b/src/tree/tree_model.cc @@ -1,5 +1,5 @@ /** - * Copyright 2015-2023 by Contributors + * Copyright 2015-2023, XGBoost Contributors * \file tree_model.cc * \brief model structure for tree */ @@ -15,9 +15,9 @@ #include #include "../common/categorical.h" -#include "../common/common.h" +#include "../common/common.h" // for EscapeU8 #include "../predictor/predict_fn.h" -#include "io_utils.h" // GetElem +#include "io_utils.h" // for GetElem #include "param.h" #include "xgboost/base.h" #include "xgboost/data.h" @@ -207,8 +207,9 @@ TreeGenerator* TreeGenerator::Create(std::string const& attrs, FeatureMap const& __make_ ## TreeGenReg ## _ ## UniqueId ## __ = \ ::dmlc::Registry< ::xgboost::TreeGenReg>::Get()->__REGISTER__(Name) -std::vector GetSplitCategories(RegTree const &tree, int32_t nidx) { - auto const &csr = tree.GetCategoriesMatrix(); +namespace { +std::vector GetSplitCategories(RegTree const& tree, int32_t nidx) { + auto const& csr = tree.GetCategoriesMatrix(); auto seg = csr.node_ptr[nidx]; auto split = common::KCatBitField{csr.categories.subspan(seg.beg, seg.size)}; @@ -221,7 +222,7 @@ std::vector GetSplitCategories(RegTree const &tree, int32_t nidx) { return cats; } -std::string PrintCatsAsSet(std::vector const &cats) { +std::string PrintCatsAsSet(std::vector const& cats) { std::stringstream ss; ss << "{"; for (size_t i = 0; i < cats.size(); ++i) { @@ -234,6 +235,15 @@ std::string PrintCatsAsSet(std::vector const &cats) { return ss.str(); } +std::string GetFeatureName(FeatureMap const& fmap, bst_feature_t split_index) { + CHECK_LE(fmap.Size(), std::numeric_limits::max()); + auto fname = split_index < static_cast(fmap.Size()) + ? fmap.Name(split_index) + : ('f' + std::to_string(split_index)); + return common::EscapeU8(fname); +} +} // anonymous namespace + class TextGenerator : public TreeGenerator { using SuperT = TreeGenerator; @@ -263,7 +273,7 @@ class TextGenerator : public TreeGenerator { std::string result = SuperT::Match( kIndicatorTemplate, {{"{nid}", std::to_string(nid)}, - {"{fname}", fmap_.Name(split_index)}, + {"{fname}", GetFeatureName(fmap_, split_index)}, {"{yes}", std::to_string(nyes)}, {"{no}", std::to_string(tree[nid].DefaultChild())}}); return result; @@ -277,8 +287,7 @@ class TextGenerator : public TreeGenerator { template_str, {{"{tabs}", SuperT::Tabs(depth)}, {"{nid}", std::to_string(nid)}, - {"{fname}", split_index < fmap_.Size() ? fmap_.Name(split_index) : - std::to_string(split_index)}, + {"{fname}", GetFeatureName(fmap_, split_index)}, {"{cond}", cond}, {"{left}", std::to_string(tree[nid].LeftChild())}, {"{right}", std::to_string(tree[nid].RightChild())}, @@ -308,7 +317,7 @@ class TextGenerator : public TreeGenerator { std::string PlainNode(RegTree const& tree, int32_t nid, uint32_t depth) const override { auto cond = tree[nid].SplitCond(); static std::string const kNodeTemplate = - "{tabs}{nid}:[f{fname}<{cond}] yes={left},no={right},missing={missing}"; + "{tabs}{nid}:[{fname}<{cond}] yes={left},no={right},missing={missing}"; return SplitNodeImpl(tree, nid, kNodeTemplate, SuperT::ToStr(cond), depth); } @@ -376,7 +385,7 @@ class JsonGenerator : public TreeGenerator { return result; } - std::string LeafNode(RegTree const& tree, int32_t nid, uint32_t) const override { + std::string LeafNode(RegTree const& tree, bst_node_t nid, uint32_t) const override { static std::string const kLeafTemplate = R"L({ "nodeid": {nid}, "leaf": {leaf} {stat}})L"; static std::string const kStatTemplate = @@ -392,26 +401,22 @@ class JsonGenerator : public TreeGenerator { return result; } - std::string Indicator(RegTree const& tree, int32_t nid, uint32_t depth) const override { + std::string Indicator(RegTree const& tree, bst_node_t nid, uint32_t depth) const override { int32_t nyes = tree[nid].DefaultLeft() ? tree[nid].RightChild() : tree[nid].LeftChild(); static std::string const kIndicatorTemplate = R"ID( "nodeid": {nid}, "depth": {depth}, "split": "{fname}", "yes": {yes}, "no": {no})ID"; auto split_index = tree[nid].SplitIndex(); - auto fname = fmap_.Name(split_index); - std::string qfname; // quoted - common::EscapeU8(fname, &qfname); - auto result = SuperT::Match( - kIndicatorTemplate, - {{"{nid}", std::to_string(nid)}, - {"{depth}", std::to_string(depth)}, - {"{fname}", qfname}, - {"{yes}", std::to_string(nyes)}, - {"{no}", std::to_string(tree[nid].DefaultChild())}}); + auto result = + SuperT::Match(kIndicatorTemplate, {{"{nid}", std::to_string(nid)}, + {"{depth}", std::to_string(depth)}, + {"{fname}", GetFeatureName(fmap_, split_index)}, + {"{yes}", std::to_string(nyes)}, + {"{no}", std::to_string(tree[nid].DefaultChild())}}); return result; } - std::string Categorical(RegTree const& tree, int32_t nid, uint32_t depth) const override { + std::string Categorical(RegTree const& tree, bst_node_t nid, uint32_t depth) const override { auto cats = GetSplitCategories(tree, nid); static std::string const kCategoryTemplate = R"I( "nodeid": {nid}, "depth": {depth}, "split": "{fname}", )I" @@ -429,22 +434,17 @@ class JsonGenerator : public TreeGenerator { return results; } - std::string SplitNodeImpl(RegTree const &tree, int32_t nid, - std::string const &template_str, std::string cond, - uint32_t depth) const { + std::string SplitNodeImpl(RegTree const& tree, bst_node_t nid, std::string const& template_str, + std::string cond, uint32_t depth) const { auto split_index = tree[nid].SplitIndex(); - auto fname = split_index < fmap_.Size() ? fmap_.Name(split_index) : std::to_string(split_index); - std::string qfname; // quoted - common::EscapeU8(fname, &qfname); - std::string const result = SuperT::Match( - template_str, - {{"{nid}", std::to_string(nid)}, - {"{depth}", std::to_string(depth)}, - {"{fname}", qfname}, - {"{cond}", cond}, - {"{left}", std::to_string(tree[nid].LeftChild())}, - {"{right}", std::to_string(tree[nid].RightChild())}, - {"{missing}", std::to_string(tree[nid].DefaultChild())}}); + std::string const result = + SuperT::Match(template_str, {{"{nid}", std::to_string(nid)}, + {"{depth}", std::to_string(depth)}, + {"{fname}", GetFeatureName(fmap_, split_index)}, + {"{cond}", cond}, + {"{left}", std::to_string(tree[nid].LeftChild())}, + {"{right}", std::to_string(tree[nid].RightChild())}, + {"{missing}", std::to_string(tree[nid].DefaultChild())}}); return result; } @@ -605,9 +605,8 @@ class GraphvizGenerator : public TreeGenerator { auto const& extra = kwargs["graph_attrs"]; static std::string const kGraphTemplate = " graph [ {key}=\"{value}\" ]\n"; for (auto const& kv : extra) { - param_.graph_attrs += SuperT::Match(kGraphTemplate, - {{"{key}", kv.first}, - {"{value}", kv.second}}); + param_.graph_attrs += + SuperT::Match(kGraphTemplate, {{"{key}", kv.first}, {"{value}", kv.second}}); } kwargs.erase("graph_attrs"); @@ -646,20 +645,18 @@ class GraphvizGenerator : public TreeGenerator { // Only indicator is different, so we combine all different node types into this // function. std::string PlainNode(RegTree const& tree, int32_t nid, uint32_t) const override { - auto split = tree[nid].SplitIndex(); + auto split_index = tree[nid].SplitIndex(); auto cond = tree[nid].SplitCond(); - static std::string const kNodeTemplate = - " {nid} [ label=\"{fname}{<}{cond}\" {params}]\n"; + static std::string const kNodeTemplate = " {nid} [ label=\"{fname}{<}{cond}\" {params}]\n"; - // Indicator only has fname. - bool has_less = (split >= fmap_.Size()) || fmap_.TypeOf(split) != FeatureMap::kIndicator; - std::string result = SuperT::Match(kNodeTemplate, { - {"{nid}", std::to_string(nid)}, - {"{fname}", split < fmap_.Size() ? fmap_.Name(split) : - 'f' + std::to_string(split)}, - {"{<}", has_less ? "<" : ""}, - {"{cond}", has_less ? SuperT::ToStr(cond) : ""}, - {"{params}", param_.condition_node_params}}); + bool has_less = + (split_index >= fmap_.Size()) || fmap_.TypeOf(split_index) != FeatureMap::kIndicator; + std::string result = + SuperT::Match(kNodeTemplate, {{"{nid}", std::to_string(nid)}, + {"{fname}", GetFeatureName(fmap_, split_index)}, + {"{<}", has_less ? "<" : ""}, + {"{cond}", has_less ? SuperT::ToStr(cond) : ""}, + {"{params}", param_.condition_node_params}}); result += BuildEdge(tree, nid, tree[nid].LeftChild(), true); result += BuildEdge(tree, nid, tree[nid].RightChild(), false); @@ -672,14 +669,13 @@ class GraphvizGenerator : public TreeGenerator { " {nid} [ label=\"{fname}:{cond}\" {params}]\n"; auto cats = GetSplitCategories(tree, nid); auto cats_str = PrintCatsAsSet(cats); - auto split = tree[nid].SplitIndex(); - std::string result = SuperT::Match( - kLabelTemplate, - {{"{nid}", std::to_string(nid)}, - {"{fname}", split < fmap_.Size() ? fmap_.Name(split) - : 'f' + std::to_string(split)}, - {"{cond}", cats_str}, - {"{params}", param_.condition_node_params}}); + auto split_index = tree[nid].SplitIndex(); + + std::string result = + SuperT::Match(kLabelTemplate, {{"{nid}", std::to_string(nid)}, + {"{fname}", GetFeatureName(fmap_, split_index)}, + {"{cond}", cats_str}, + {"{params}", param_.condition_node_params}}); result += BuildEdge(tree, nid, tree[nid].LeftChild(), true); result += BuildEdge(tree, nid, tree[nid].RightChild(), false); diff --git a/tests/cpp/tree/test_tree_model.cc b/tests/cpp/tree/test_tree_model.cc index 44708ebd1..2dc1893dd 100644 --- a/tests/cpp/tree/test_tree_model.cc +++ b/tests/cpp/tree/test_tree_model.cc @@ -404,7 +404,7 @@ TEST(Tree, DumpText) { } ASSERT_EQ(n_conditions, 3ul); - ASSERT_NE(str.find("[f0<0]"), std::string::npos); + ASSERT_NE(str.find("[f0<0]"), std::string::npos) << str; ASSERT_NE(str.find("[f1<1]"), std::string::npos); ASSERT_NE(str.find("[f2<2]"), std::string::npos); diff --git a/tests/python/test_basic_models.py b/tests/python/test_basic_models.py index f0c80124d..45bef1f25 100644 --- a/tests/python/test_basic_models.py +++ b/tests/python/test_basic_models.py @@ -28,10 +28,11 @@ def json_model(model_path: str, parameters: dict) -> dict: if model_path.endswith("ubj"): import ubjson + with open(model_path, "rb") as ubjfd: model = ubjson.load(ubjfd) else: - with open(model_path, 'r') as fd: + with open(model_path, "r") as fd: model = json.load(fd) return model @@ -439,25 +440,34 @@ class TestModels: 'objective': 'multi:softmax'} validate_model(parameters) - def test_special_model_dump_characters(self): + def test_special_model_dump_characters(self) -> None: params = {"objective": "reg:squarederror", "max_depth": 3} - feature_names = ['"feature 0"', "\tfeature\n1", "feature 2"] + feature_names = ['"feature 0"', "\tfeature\n1", """feature "2"."""] X, y, w = tm.make_regression(n_samples=128, n_features=3, use_cupy=False) Xy = xgb.DMatrix(X, label=y, feature_names=feature_names) booster = xgb.train(params, Xy, num_boost_round=3) + json_dump = booster.get_dump(dump_format="json") assert len(json_dump) == 3 - def validate(obj: dict) -> None: + def validate_json(obj: dict) -> None: for k, v in obj.items(): if k == "split": assert v in feature_names elif isinstance(v, dict): - validate(v) + validate_json(v) for j_tree in json_dump: loaded = json.loads(j_tree) - validate(loaded) + validate_json(loaded) + + dot_dump = booster.get_dump(dump_format="dot") + for d in dot_dump: + assert d.find(r"feature \"2\"") != -1 + + text_dump = booster.get_dump(dump_format="text") + for d in text_dump: + assert d.find(r"feature \"2\"") != -1 def test_categorical_model_io(self): X, y = tm.make_categorical(256, 16, 71, False) @@ -485,6 +495,7 @@ class TestModels: @pytest.mark.skipif(**tm.no_sklearn()) def test_attributes(self): from sklearn.datasets import load_iris + X, y = load_iris(return_X_y=True) cls = xgb.XGBClassifier(n_estimators=2) cls.fit(X, y, early_stopping_rounds=1, eval_set=[(X, y)]) @@ -674,6 +685,7 @@ class TestModels: @pytest.mark.skipif(**tm.no_pandas()) def test_feature_info(self): import pandas as pd + rows = 100 cols = 10 X = rng.randn(rows, cols) From ef8bdaa047ebf6504245a08d7046c49528cc1a13 Mon Sep 17 00:00:00 2001 From: Philip Hyunsu Cho Date: Fri, 29 Dec 2023 11:15:38 -0800 Subject: [PATCH 34/59] [CI] Update machine images (#9932) --- cmake/modules/FindLibR.cmake | 5 +++- doc/contrib/ci.rst | 7 ++--- tests/buildkite/build-containers.sh | 3 +- tests/buildkite/build-cpu-arm64.sh | 2 +- tests/buildkite/build-cpu.sh | 2 +- tests/buildkite/build-cuda-with-rmm.sh | 6 ++-- tests/buildkite/build-cuda.sh | 6 ++-- tests/buildkite/build-gpu-rpkg.sh | 2 +- tests/buildkite/build-jvm-doc.sh | 2 +- tests/buildkite/build-jvm-packages-gpu.sh | 2 +- tests/buildkite/build-jvm-packages.sh | 4 +-- tests/buildkite/deploy-jvm-packages.sh | 2 +- .../aws-stack-creator/create_stack.py | 2 +- .../aws-stack-creator/metadata.py | 28 +++++++++---------- .../linux-amd64-gpu-bootstrap.yml | 12 ++++---- .../windows-gpu-bootstrap.yml | 25 ++++++----------- tests/buildkite/run-clang-tidy.sh | 2 +- tests/buildkite/test-cpp-gpu.sh | 4 +-- tests/buildkite/test-cpp-mgpu.sh | 2 +- .../test-integration-jvm-packages.sh | 2 +- tests/buildkite/test-python-cpu-arm64.sh | 2 +- tests/buildkite/test-python-cpu.sh | 2 +- tests/buildkite/test-python-gpu.sh | 2 +- tests/ci_build/build_r_pkg_with_cuda_win64.sh | 6 ++-- tests/ci_build/ci_build.sh | 28 ++++++++++++------- tests/cpp/common/test_device_helpers.cu | 2 +- 26 files changed, 82 insertions(+), 80 deletions(-) diff --git a/cmake/modules/FindLibR.cmake b/cmake/modules/FindLibR.cmake index 1eb384238..c406ae0d9 100644 --- a/cmake/modules/FindLibR.cmake +++ b/cmake/modules/FindLibR.cmake @@ -65,7 +65,10 @@ function(create_rlib_for_msvc) execute_process(COMMAND ${DLLTOOL_EXE} "--input-def" "${CMAKE_CURRENT_BINARY_DIR}/R.def" - "--output-lib" "${CMAKE_CURRENT_BINARY_DIR}/R.lib") + "--output-lib" "${CMAKE_CURRENT_BINARY_DIR}/R.lib" + "--temp-prefix" "Rlibtemp" + COMMAND_ECHO STDOUT + COMMAND_ERROR_IS_FATAL ANY) endfunction() diff --git a/doc/contrib/ci.rst b/doc/contrib/ci.rst index 2db6f80bc..af9e65562 100644 --- a/doc/contrib/ci.rst +++ b/doc/contrib/ci.rst @@ -59,7 +59,7 @@ For your convenience, we provide the wrapper script ``tests/ci_build/ci_build.sh .. code-block:: bash - tests/ci_build/ci_build.sh --build-arg \ + tests/ci_build/ci_build.sh --use-gpus --build-arg \ ... where: @@ -68,8 +68,7 @@ where: container definition (Dockerfile) located at ``tests/ci_build/Dockerfile.``. For example, setting the container type to ``gpu`` will cause the script to load the Dockerfile ``tests/ci_build/Dockerfile.gpu``. -* ```` must be either ``docker`` or ``nvidia-docker``. Choose ``nvidia-docker`` - as long as you need to run any GPU code. +* Specify ``--use-gpus`` to run any GPU code. This flag will grant the container access to all NVIDIA GPUs in the base machine. Omit the flag if the access to GPUs is not necessary. * ```` is a build argument to be passed to Docker. Must be of form ``VAR=VALUE``. Example: ``--build-arg CUDA_VERSION_ARG=11.0``. You can pass multiple ``--build-arg``. * ```` is the command to run inside the Docker container. This can be more than one argument. @@ -83,7 +82,7 @@ arguments to Docker. For example: # Allocate extra space in /dev/shm to enable NCCL export CI_DOCKER_EXTRA_PARAMS_INIT='--shm-size=4g' # Run multi-GPU test suite - tests/ci_build/ci_build.sh gpu nvidia-docker --build-arg CUDA_VERSION_ARG=11.0 \ + tests/ci_build/ci_build.sh gpu --use-gpus --build-arg CUDA_VERSION_ARG=11.0 \ tests/ci_build/test_python.sh mgpu To pass multiple extra arguments: diff --git a/tests/buildkite/build-containers.sh b/tests/buildkite/build-containers.sh index f46e6ccd0..9aec33d1f 100755 --- a/tests/buildkite/build-containers.sh +++ b/tests/buildkite/build-containers.sh @@ -22,6 +22,7 @@ case "${container}" in gpu) BUILD_ARGS="$BUILD_ARGS --build-arg CUDA_VERSION_ARG=$CUDA_VERSION" + BUILD_ARGS="$BUILD_ARGS --build-arg NCCL_VERSION_ARG=$NCCL_VERSION" BUILD_ARGS="$BUILD_ARGS --build-arg RAPIDS_VERSION_ARG=$RAPIDS_VERSION" ;; @@ -43,4 +44,4 @@ case "${container}" in esac # Run a no-op command. This will simply build the container and push it to the private registry -tests/ci_build/ci_build.sh ${container} docker ${BUILD_ARGS} bash +tests/ci_build/ci_build.sh ${container} ${BUILD_ARGS} bash diff --git a/tests/buildkite/build-cpu-arm64.sh b/tests/buildkite/build-cpu-arm64.sh index fd00a7971..3bbc95472 100755 --- a/tests/buildkite/build-cpu-arm64.sh +++ b/tests/buildkite/build-cpu-arm64.sh @@ -8,7 +8,7 @@ echo "--- Build CPU code targeting ARM64" source tests/buildkite/conftest.sh -command_wrapper="tests/ci_build/ci_build.sh aarch64 docker" +command_wrapper="tests/ci_build/ci_build.sh aarch64" echo "--- Build libxgboost from the source" $command_wrapper tests/ci_build/build_via_cmake.sh --conda-env=aarch64_test \ diff --git a/tests/buildkite/build-cpu.sh b/tests/buildkite/build-cpu.sh index 73e88d8aa..11679d644 100755 --- a/tests/buildkite/build-cpu.sh +++ b/tests/buildkite/build-cpu.sh @@ -6,7 +6,7 @@ echo "--- Build CPU code" source tests/buildkite/conftest.sh -command_wrapper="tests/ci_build/ci_build.sh cpu docker" +command_wrapper="tests/ci_build/ci_build.sh cpu" $command_wrapper rm -fv dmlc-core/include/dmlc/build_config_default.h # This step is not necessary, but here we include it, to ensure that diff --git a/tests/buildkite/build-cuda-with-rmm.sh b/tests/buildkite/build-cuda-with-rmm.sh index 615608249..559bad8a7 100755 --- a/tests/buildkite/build-cuda-with-rmm.sh +++ b/tests/buildkite/build-cuda-with-rmm.sh @@ -15,7 +15,7 @@ else arch_flag="" fi -command_wrapper="tests/ci_build/ci_build.sh gpu_build_centos7 docker --build-arg "` +command_wrapper="tests/ci_build/ci_build.sh gpu_build_centos7 --build-arg "` `"CUDA_VERSION_ARG=$CUDA_VERSION --build-arg "` `"NCCL_VERSION_ARG=$NCCL_VERSION --build-arg "` `"RAPIDS_VERSION_ARG=$RAPIDS_VERSION" @@ -40,13 +40,13 @@ $command_wrapper python tests/ci_build/rename_whl.py python-package/dist/*.whl \ ${BUILDKITE_COMMIT} ${WHEEL_TAG} echo "--- Audit binary wheel to ensure it's compliant with manylinux2014 standard" -tests/ci_build/ci_build.sh auditwheel_x86_64 docker auditwheel repair \ +tests/ci_build/ci_build.sh auditwheel_x86_64 auditwheel repair \ --plat ${WHEEL_TAG} python-package/dist/*.whl $command_wrapper python tests/ci_build/rename_whl.py wheelhouse/*.whl \ ${BUILDKITE_COMMIT} ${WHEEL_TAG} mv -v wheelhouse/*.whl python-package/dist/ # Make sure that libgomp.so is vendored in the wheel -tests/ci_build/ci_build.sh auditwheel_x86_64 docker bash -c \ +tests/ci_build/ci_build.sh auditwheel_x86_64 bash -c \ "unzip -l python-package/dist/*.whl | grep libgomp || exit -1" echo "--- Upload Python wheel" diff --git a/tests/buildkite/build-cuda.sh b/tests/buildkite/build-cuda.sh index 7bd3492a2..5abc5ca5a 100755 --- a/tests/buildkite/build-cuda.sh +++ b/tests/buildkite/build-cuda.sh @@ -15,7 +15,7 @@ else arch_flag="" fi -command_wrapper="tests/ci_build/ci_build.sh gpu_build_centos7 docker --build-arg "` +command_wrapper="tests/ci_build/ci_build.sh gpu_build_centos7 --build-arg "` `"CUDA_VERSION_ARG=$CUDA_VERSION --build-arg "` `"NCCL_VERSION_ARG=$NCCL_VERSION --build-arg "` `"RAPIDS_VERSION_ARG=$RAPIDS_VERSION" @@ -39,13 +39,13 @@ $command_wrapper python tests/ci_build/rename_whl.py python-package/dist/*.whl \ ${BUILDKITE_COMMIT} ${WHEEL_TAG} echo "--- Audit binary wheel to ensure it's compliant with manylinux2014 standard" -tests/ci_build/ci_build.sh auditwheel_x86_64 docker auditwheel repair \ +tests/ci_build/ci_build.sh auditwheel_x86_64 auditwheel repair \ --plat ${WHEEL_TAG} python-package/dist/*.whl $command_wrapper python tests/ci_build/rename_whl.py wheelhouse/*.whl \ ${BUILDKITE_COMMIT} ${WHEEL_TAG} mv -v wheelhouse/*.whl python-package/dist/ # Make sure that libgomp.so is vendored in the wheel -tests/ci_build/ci_build.sh auditwheel_x86_64 docker bash -c \ +tests/ci_build/ci_build.sh auditwheel_x86_64 bash -c \ "unzip -l python-package/dist/*.whl | grep libgomp || exit -1" echo "--- Upload Python wheel" diff --git a/tests/buildkite/build-gpu-rpkg.sh b/tests/buildkite/build-gpu-rpkg.sh index 585dc79ae..78a534615 100755 --- a/tests/buildkite/build-gpu-rpkg.sh +++ b/tests/buildkite/build-gpu-rpkg.sh @@ -6,7 +6,7 @@ source tests/buildkite/conftest.sh echo "--- Build XGBoost R package with CUDA" -tests/ci_build/ci_build.sh gpu_build_r_centos7 docker \ +tests/ci_build/ci_build.sh gpu_build_r_centos7 \ --build-arg CUDA_VERSION_ARG=${CUDA_VERSION} \ --build-arg R_VERSION_ARG=${R_VERSION} \ tests/ci_build/build_r_pkg_with_cuda.sh \ diff --git a/tests/buildkite/build-jvm-doc.sh b/tests/buildkite/build-jvm-doc.sh index a2d658e48..d168eb8cc 100755 --- a/tests/buildkite/build-jvm-doc.sh +++ b/tests/buildkite/build-jvm-doc.sh @@ -5,7 +5,7 @@ set -euo pipefail source tests/buildkite/conftest.sh echo "--- Build JVM packages doc" -tests/ci_build/ci_build.sh jvm docker tests/ci_build/build_jvm_doc.sh ${BRANCH_NAME} +tests/ci_build/ci_build.sh jvm tests/ci_build/build_jvm_doc.sh ${BRANCH_NAME} if [[ ($is_pull_request == 0) && ($is_release_branch == 1) ]] then echo "--- Upload JVM packages doc" diff --git a/tests/buildkite/build-jvm-packages-gpu.sh b/tests/buildkite/build-jvm-packages-gpu.sh index 6a9a29cb3..64be7cc0a 100755 --- a/tests/buildkite/build-jvm-packages-gpu.sh +++ b/tests/buildkite/build-jvm-packages-gpu.sh @@ -13,7 +13,7 @@ else arch_flag="" fi -tests/ci_build/ci_build.sh jvm_gpu_build nvidia-docker \ +tests/ci_build/ci_build.sh jvm_gpu_build --use-gpus \ --build-arg CUDA_VERSION_ARG=${CUDA_VERSION} \ --build-arg NCCL_VERSION_ARG=${NCCL_VERSION} \ tests/ci_build/build_jvm_packages.sh \ diff --git a/tests/buildkite/build-jvm-packages.sh b/tests/buildkite/build-jvm-packages.sh index 33cfffe71..12393c561 100755 --- a/tests/buildkite/build-jvm-packages.sh +++ b/tests/buildkite/build-jvm-packages.sh @@ -5,13 +5,13 @@ set -euo pipefail source tests/buildkite/conftest.sh echo "--- Build XGBoost JVM packages scala 2.12" -tests/ci_build/ci_build.sh jvm docker tests/ci_build/build_jvm_packages.sh \ +tests/ci_build/ci_build.sh jvm tests/ci_build/build_jvm_packages.sh \ ${SPARK_VERSION} echo "--- Build XGBoost JVM packages scala 2.13" -tests/ci_build/ci_build.sh jvm docker tests/ci_build/build_jvm_packages.sh \ +tests/ci_build/ci_build.sh jvm tests/ci_build/build_jvm_packages.sh \ ${SPARK_VERSION} "" "" "true" echo "--- Stash XGBoost4J JARs" diff --git a/tests/buildkite/deploy-jvm-packages.sh b/tests/buildkite/deploy-jvm-packages.sh index a3410b294..812a6c5ca 100755 --- a/tests/buildkite/deploy-jvm-packages.sh +++ b/tests/buildkite/deploy-jvm-packages.sh @@ -7,7 +7,7 @@ source tests/buildkite/conftest.sh if [[ ($is_pull_request == 0) && ($is_release_branch == 1) ]] then echo "--- Deploy JVM packages to xgboost-maven-repo S3 repo" - tests/ci_build/ci_build.sh jvm_gpu_build docker \ + tests/ci_build/ci_build.sh jvm_gpu_build \ --build-arg CUDA_VERSION_ARG=${CUDA_VERSION} \ --build-arg NCCL_VERSION_ARG=${NCCL_VERSION} \ tests/ci_build/deploy_jvm_packages.sh ${SPARK_VERSION} diff --git a/tests/buildkite/infrastructure/aws-stack-creator/create_stack.py b/tests/buildkite/infrastructure/aws-stack-creator/create_stack.py index 4277eed53..8f8db348a 100644 --- a/tests/buildkite/infrastructure/aws-stack-creator/create_stack.py +++ b/tests/buildkite/infrastructure/aws-stack-creator/create_stack.py @@ -63,7 +63,7 @@ def format_params(args, *, stack_id, agent_iam_policy): params["BuildkiteAgentToken"] = args.agent_token params["VpcId"] = default_vpc.id params["Subnets"] = ",".join(subnets) - params["ManagedPolicyARN"] = agent_iam_policy + params["ManagedPolicyARNs"] = agent_iam_policy params.update(COMMON_STACK_PARAMS) return [{"ParameterKey": k, "ParameterValue": v} for k, v in params.items()] diff --git a/tests/buildkite/infrastructure/aws-stack-creator/metadata.py b/tests/buildkite/infrastructure/aws-stack-creator/metadata.py index 36d8595a6..3b56a2d8c 100644 --- a/tests/buildkite/infrastructure/aws-stack-creator/metadata.py +++ b/tests/buildkite/infrastructure/aws-stack-creator/metadata.py @@ -1,34 +1,34 @@ AMI_ID = { # Managed by XGBoost team "linux-amd64-gpu": { - "us-west-2": "ami-094271bed4788ddb5", + "us-west-2": "ami-08c3bc1dd5ec8bc5c", }, "linux-amd64-mgpu": { - "us-west-2": "ami-094271bed4788ddb5", + "us-west-2": "ami-08c3bc1dd5ec8bc5c", }, "windows-gpu": { - "us-west-2": "ami-0839681594a1d7627", + "us-west-2": "ami-03c7f2156f93b22a7", }, "windows-cpu": { - "us-west-2": "ami-0839681594a1d7627", + "us-west-2": "ami-03c7f2156f93b22a7", }, # Managed by BuildKite # from https://s3.amazonaws.com/buildkite-aws-stack/latest/aws-stack.yml "linux-amd64-cpu": { - "us-west-2": "ami-00f2127550cf03658", + "us-west-2": "ami-015e64acb52b3e595", }, "pipeline-loader": { - "us-west-2": "ami-00f2127550cf03658", + "us-west-2": "ami-015e64acb52b3e595", }, "linux-arm64-cpu": { - "us-west-2": "ami-0c5789068f4a2d1b5", + "us-west-2": "ami-0884e9c23a2fa98d0", }, } STACK_PARAMS = { "linux-amd64-gpu": { "InstanceOperatingSystem": "linux", - "InstanceType": "g4dn.xlarge", + "InstanceTypes": "g4dn.xlarge", "AgentsPerInstance": "1", "MinSize": "0", "MaxSize": "8", @@ -38,7 +38,7 @@ STACK_PARAMS = { }, "linux-amd64-mgpu": { "InstanceOperatingSystem": "linux", - "InstanceType": "g4dn.12xlarge", + "InstanceTypes": "g4dn.12xlarge", "AgentsPerInstance": "1", "MinSize": "0", "MaxSize": "1", @@ -48,7 +48,7 @@ STACK_PARAMS = { }, "windows-gpu": { "InstanceOperatingSystem": "windows", - "InstanceType": "g4dn.2xlarge", + "InstanceTypes": "g4dn.2xlarge", "AgentsPerInstance": "1", "MinSize": "0", "MaxSize": "2", @@ -58,7 +58,7 @@ STACK_PARAMS = { }, "windows-cpu": { "InstanceOperatingSystem": "windows", - "InstanceType": "c5a.2xlarge", + "InstanceTypes": "c5a.2xlarge", "AgentsPerInstance": "1", "MinSize": "0", "MaxSize": "2", @@ -68,7 +68,7 @@ STACK_PARAMS = { }, "linux-amd64-cpu": { "InstanceOperatingSystem": "linux", - "InstanceType": "c5a.4xlarge", + "InstanceTypes": "c5a.4xlarge", "AgentsPerInstance": "1", "MinSize": "0", "MaxSize": "16", @@ -78,7 +78,7 @@ STACK_PARAMS = { }, "pipeline-loader": { "InstanceOperatingSystem": "linux", - "InstanceType": "t3a.micro", + "InstanceTypes": "t3a.micro", "AgentsPerInstance": "1", "MinSize": "2", "MaxSize": "2", @@ -88,7 +88,7 @@ STACK_PARAMS = { }, "linux-arm64-cpu": { "InstanceOperatingSystem": "linux", - "InstanceType": "c6g.4xlarge", + "InstanceTypes": "c6g.4xlarge", "AgentsPerInstance": "1", "MinSize": "0", "MaxSize": "8", diff --git a/tests/buildkite/infrastructure/worker-image-pipeline/linux-amd64-gpu-bootstrap.yml b/tests/buildkite/infrastructure/worker-image-pipeline/linux-amd64-gpu-bootstrap.yml index a5c82a7fa..88403911c 100644 --- a/tests/buildkite/infrastructure/worker-image-pipeline/linux-amd64-gpu-bootstrap.yml +++ b/tests/buildkite/infrastructure/worker-image-pipeline/linux-amd64-gpu-bootstrap.yml @@ -12,15 +12,13 @@ phases: - | yum groupinstall -y "Development tools" yum install -y kernel-devel-$(uname -r) + dnf install -y kernel-modules-extra aws s3 cp --recursive s3://ec2-linux-nvidia-drivers/latest/ . chmod +x NVIDIA-Linux-x86_64*.run - CC=/usr/bin/gcc10-cc ./NVIDIA-Linux-x86_64*.run --silent + ./NVIDIA-Linux-x86_64*.run --silent - amazon-linux-extras install docker - systemctl --now enable docker - distribution=$(. /etc/os-release;echo $ID$VERSION_ID) \ - && curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.repo \ - | sudo tee /etc/yum.repos.d/nvidia-container-toolkit.repo + curl -s -L https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo | tee /etc/yum.repos.d/nvidia-container-toolkit.repo + yum install -y nvidia-container-toolkit yum clean expire-cache - yum install -y nvidia-docker2 + nvidia-ctk runtime configure --runtime=docker systemctl restart docker diff --git a/tests/buildkite/infrastructure/worker-image-pipeline/windows-gpu-bootstrap.yml b/tests/buildkite/infrastructure/worker-image-pipeline/windows-gpu-bootstrap.yml index 03fb105a7..e4d212fda 100644 --- a/tests/buildkite/infrastructure/worker-image-pipeline/windows-gpu-bootstrap.yml +++ b/tests/buildkite/infrastructure/worker-image-pipeline/windows-gpu-bootstrap.yml @@ -15,9 +15,9 @@ phases: choco --version choco feature enable -n=allowGlobalConfirmation - # CMake 3.25 - Write-Host '>>> Installing CMake 3.25...' - choco install cmake --version 3.25.2 --installargs "ADD_CMAKE_TO_PATH=System" + # CMake 3.27 + Write-Host '>>> Installing CMake 3.27...' + choco install cmake --version 3.27.9 --installargs "ADD_CMAKE_TO_PATH=System" if ($LASTEXITCODE -ne 0) { throw "Last command failed" } # Notepad++ @@ -25,15 +25,14 @@ phases: choco install notepadplusplus if ($LASTEXITCODE -ne 0) { throw "Last command failed" } - # Miniconda - Write-Host '>>> Installing Miniconda...' - choco install miniconda3 /RegisterPython:1 /D:C:\tools\miniconda3 - C:\tools\miniconda3\Scripts\conda.exe init --user --system + # Mambaforge + Write-Host '>>> Installing Mambaforge...' + choco install mambaforge /RegisterPython:1 /D:C:\tools\mambaforge + C:\tools\mambaforge\Scripts\conda.exe init --user --system if ($LASTEXITCODE -ne 0) { throw "Last command failed" } . "C:\Windows\System32\WindowsPowerShell\v1.0\profile.ps1" if ($LASTEXITCODE -ne 0) { throw "Last command failed" } conda config --set auto_activate_base false - conda config --prepend channels conda-forge # Install Java 11 Write-Host '>>> Installing Java 11...' @@ -59,15 +58,9 @@ phases: choco install cuda --version=11.8.0.52206 if ($LASTEXITCODE -ne 0) { throw "Last command failed" } - # Install Python packages - Write-Host '>>> Installing Python packages...' - conda activate - conda install -y mamba - if ($LASTEXITCODE -ne 0) { throw "Last command failed" } - # Install R Write-Host '>>> Installing R...' - choco install r.project --version=3.6.3 + choco install r.project --version=4.3.2 if ($LASTEXITCODE -ne 0) { throw "Last command failed" } - choco install rtools --version=3.5.0.4 + choco install rtools --version=4.3.5550 if ($LASTEXITCODE -ne 0) { throw "Last command failed" } diff --git a/tests/buildkite/run-clang-tidy.sh b/tests/buildkite/run-clang-tidy.sh index 1a664f568..95ff010c2 100755 --- a/tests/buildkite/run-clang-tidy.sh +++ b/tests/buildkite/run-clang-tidy.sh @@ -6,6 +6,6 @@ echo "--- Run clang-tidy" source tests/buildkite/conftest.sh -tests/ci_build/ci_build.sh clang_tidy docker \ +tests/ci_build/ci_build.sh clang_tidy \ --build-arg CUDA_VERSION_ARG=${CUDA_VERSION} \ python3 tests/ci_build/tidy.py --cuda-archs 75 diff --git a/tests/buildkite/test-cpp-gpu.sh b/tests/buildkite/test-cpp-gpu.sh index 36f54cd3d..d7197db2e 100755 --- a/tests/buildkite/test-cpp-gpu.sh +++ b/tests/buildkite/test-cpp-gpu.sh @@ -7,7 +7,7 @@ source tests/buildkite/conftest.sh echo "--- Run Google Tests with CUDA, using a GPU" buildkite-agent artifact download "build/testxgboost" . --step build-cuda chmod +x build/testxgboost -tests/ci_build/ci_build.sh gpu nvidia-docker \ +tests/ci_build/ci_build.sh gpu --use-gpus \ --build-arg CUDA_VERSION_ARG=$CUDA_VERSION \ --build-arg RAPIDS_VERSION_ARG=$RAPIDS_VERSION \ --build-arg NCCL_VERSION_ARG=$NCCL_VERSION \ @@ -17,7 +17,7 @@ echo "--- Run Google Tests with CUDA, using a GPU, RMM enabled" rm -rfv build/ buildkite-agent artifact download "build/testxgboost" . --step build-cuda-with-rmm chmod +x build/testxgboost -tests/ci_build/ci_build.sh gpu nvidia-docker \ +tests/ci_build/ci_build.sh gpu --use-gpus \ --build-arg CUDA_VERSION_ARG=$CUDA_VERSION \ --build-arg RAPIDS_VERSION_ARG=$RAPIDS_VERSION \ --build-arg NCCL_VERSION_ARG=$NCCL_VERSION \ diff --git a/tests/buildkite/test-cpp-mgpu.sh b/tests/buildkite/test-cpp-mgpu.sh index 2aac47407..65614b191 100755 --- a/tests/buildkite/test-cpp-mgpu.sh +++ b/tests/buildkite/test-cpp-mgpu.sh @@ -10,7 +10,7 @@ export CI_DOCKER_EXTRA_PARAMS_INIT='--shm-size=4g' echo "--- Run Google Tests with CUDA, using multiple GPUs" buildkite-agent artifact download "build/testxgboost" . --step build-cuda chmod +x build/testxgboost -tests/ci_build/ci_build.sh gpu nvidia-docker \ +tests/ci_build/ci_build.sh gpu --use-gpus \ --build-arg CUDA_VERSION_ARG=$CUDA_VERSION \ --build-arg RAPIDS_VERSION_ARG=$RAPIDS_VERSION \ --build-arg NCCL_VERSION_ARG=$NCCL_VERSION \ diff --git a/tests/buildkite/test-integration-jvm-packages.sh b/tests/buildkite/test-integration-jvm-packages.sh index 9f477720a..51f74afe9 100755 --- a/tests/buildkite/test-integration-jvm-packages.sh +++ b/tests/buildkite/test-integration-jvm-packages.sh @@ -9,5 +9,5 @@ buildkite-agent artifact download "jvm-packages/xgboost4j/target/*.jar" . --step buildkite-agent artifact download "jvm-packages/xgboost4j-spark/target/*.jar" . --step build-jvm-packages buildkite-agent artifact download "jvm-packages/xgboost4j-example/target/*.jar" . --step build-jvm-packages export CI_DOCKER_EXTRA_PARAMS_INIT='-e RUN_INTEGRATION_TEST=1' -tests/ci_build/ci_build.sh jvm_cross docker --build-arg JDK_VERSION=${JDK_VERSION} \ +tests/ci_build/ci_build.sh jvm_cross --build-arg JDK_VERSION=${JDK_VERSION} \ --build-arg SPARK_VERSION=${SPARK_VERSION} tests/ci_build/test_jvm_cross.sh diff --git a/tests/buildkite/test-python-cpu-arm64.sh b/tests/buildkite/test-python-cpu-arm64.sh index ed1ad101b..68a428034 100755 --- a/tests/buildkite/test-python-cpu-arm64.sh +++ b/tests/buildkite/test-python-cpu-arm64.sh @@ -8,4 +8,4 @@ echo "--- Test Python CPU ARM64" buildkite-agent artifact download "python-package/dist/*.whl" . --step build-cpu-arm64 buildkite-agent artifact download "xgboost" . --step build-cpu-arm64 chmod +x ./xgboost -tests/ci_build/ci_build.sh aarch64 docker tests/ci_build/test_python.sh cpu-arm64 +tests/ci_build/ci_build.sh aarch64 tests/ci_build/test_python.sh cpu-arm64 diff --git a/tests/buildkite/test-python-cpu.sh b/tests/buildkite/test-python-cpu.sh index 938e1184e..6c53dc282 100755 --- a/tests/buildkite/test-python-cpu.sh +++ b/tests/buildkite/test-python-cpu.sh @@ -13,4 +13,4 @@ chmod +x ./xgboost export BUILDKITE_ANALYTICS_TOKEN=$(get_aws_secret buildkite/test_analytics/cpu) set_buildkite_env_vars_in_container -tests/ci_build/ci_build.sh cpu docker tests/ci_build/test_python.sh cpu +tests/ci_build/ci_build.sh cpu tests/ci_build/test_python.sh cpu diff --git a/tests/buildkite/test-python-gpu.sh b/tests/buildkite/test-python-gpu.sh index c2376c021..bb61a980d 100755 --- a/tests/buildkite/test-python-gpu.sh +++ b/tests/buildkite/test-python-gpu.sh @@ -22,7 +22,7 @@ chmod +x build/testxgboost # Allocate extra space in /dev/shm to enable NCCL export CI_DOCKER_EXTRA_PARAMS_INIT='--shm-size=4g' -command_wrapper="tests/ci_build/ci_build.sh gpu nvidia-docker --build-arg "` +command_wrapper="tests/ci_build/ci_build.sh gpu --use-gpus --build-arg "` `"CUDA_VERSION_ARG=$CUDA_VERSION --build-arg "` `"RAPIDS_VERSION_ARG=$RAPIDS_VERSION --build-arg "` `"NCCL_VERSION_ARG=$NCCL_VERSION" diff --git a/tests/ci_build/build_r_pkg_with_cuda_win64.sh b/tests/ci_build/build_r_pkg_with_cuda_win64.sh index d44a418d1..580358883 100644 --- a/tests/ci_build/build_r_pkg_with_cuda_win64.sh +++ b/tests/ci_build/build_r_pkg_with_cuda_win64.sh @@ -18,7 +18,7 @@ mv xgboost/ xgboost_rpack/ mkdir build cd build -cmake .. -G"Visual Studio 17 2022" -A x64 -DUSE_CUDA=ON -DR_LIB=ON -DLIBR_HOME="c:\\Program Files\\R\\R-3.6.3" +cmake .. -G"Visual Studio 17 2022" -A x64 -DUSE_CUDA=ON -DR_LIB=ON -DLIBR_HOME="c:\\Program Files\\R\\R-4.3.2" -DCMAKE_PREFIX_PATH="C:\\rtools43\\x86_64-w64-mingw32.static.posix\\bin" cmake --build . --config Release --parallel cd .. @@ -32,5 +32,5 @@ cp -v lib/xgboost.dll xgboost_rpack/src/ echo 'all:' > xgboost_rpack/src/Makefile echo 'all:' > xgboost_rpack/src/Makefile.win mv xgboost_rpack/ xgboost/ -/c/Rtools/bin/tar -cvf xgboost_r_gpu_win64_${commit_hash}.tar xgboost/ -/c/Rtools/bin/gzip -9c xgboost_r_gpu_win64_${commit_hash}.tar > xgboost_r_gpu_win64_${commit_hash}.tar.gz +/c/Rtools43/usr/bin/tar -cvf xgboost_r_gpu_win64_${commit_hash}.tar xgboost/ +/c/Rtools43/usr/bin/gzip -9c xgboost_r_gpu_win64_${commit_hash}.tar > xgboost_r_gpu_win64_${commit_hash}.tar.gz diff --git a/tests/ci_build/ci_build.sh b/tests/ci_build/ci_build.sh index ef0c69183..a2f2d6063 100755 --- a/tests/ci_build/ci_build.sh +++ b/tests/ci_build/ci_build.sh @@ -2,14 +2,14 @@ # # Execute command within a docker container # -# Usage: ci_build.sh +# Usage: ci_build.sh [--use-gpus] # [--dockerfile ] [-it] # [--build-arg ] # # CONTAINER_TYPE: Type of the docker container used the run the build: e.g., # (cpu | gpu) # -# DOCKER_BINARY: Command to invoke docker, e.g. (docker | nvidia-docker). +# --use-gpus: Whether to grant the container access to NVIDIA GPUs. # # DOCKERFILE_PATH: (Optional) Path to the Dockerfile used for docker build. If # this optional value is not supplied (via the --dockerfile @@ -29,9 +29,12 @@ shift 1 DOCKERFILE_PATH="${SCRIPT_DIR}/Dockerfile.${CONTAINER_TYPE}" DOCKER_CONTEXT_PATH="${SCRIPT_DIR}" -# Get docker binary command (should be either docker or nvidia-docker) -DOCKER_BINARY="$1" -shift 1 +GPU_FLAG='' +if [[ "$1" == "--use-gpus" ]]; then + echo "Using NVIDIA GPUs" + GPU_FLAG='--gpus all' + shift 1 +fi if [[ "$1" == "--dockerfile" ]]; then DOCKERFILE_PATH="$2" @@ -144,21 +147,21 @@ then DOCKER_CACHE_REPO="${DOCKER_CACHE_ECR_ID}.dkr.ecr.${DOCKER_CACHE_ECR_REGION}.amazonaws.com" echo "Using AWS ECR; repo URL = ${DOCKER_CACHE_REPO}" # Login for Docker registry - echo "\$(aws ecr get-login --no-include-email --region ${DOCKER_CACHE_ECR_REGION} --registry-ids ${DOCKER_CACHE_ECR_ID})" - $(aws ecr get-login --no-include-email --region ${DOCKER_CACHE_ECR_REGION} --registry-ids ${DOCKER_CACHE_ECR_ID}) + echo "aws ecr get-login-password --region ${DOCKER_CACHE_ECR_REGION} | docker login --username AWS --password-stdin ${DOCKER_CACHE_REPO}" + aws ecr get-login-password --region ${DOCKER_CACHE_ECR_REGION} | docker login --username AWS --password-stdin ${DOCKER_CACHE_REPO} # Pull pre-build container from Docker build cache, # if one exists for the particular branch or pull request DOCKER_TAG="${BRANCH_NAME//\//-}" # Slashes are not allow in Docker tag echo "docker pull --quiet ${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:${DOCKER_TAG}" if time docker pull --quiet "${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:${DOCKER_TAG}" then - CACHE_FROM_CMD="--cache-from ${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:${DOCKER_TAG}" + CACHE_FROM_CMD="--cache-from ${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:${DOCKER_TAG} --build-arg BUILDKIT_INLINE_CACHE=1" else # If the build cache is empty of the particular branch or pull request, # use the build cache associated with the master branch echo "docker pull --quiet ${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:master" docker pull --quiet "${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:master" || true - CACHE_FROM_CMD="--cache-from ${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:master" + CACHE_FROM_CMD="--cache-from ${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:master --build-arg BUILDKIT_INLINE_CACHE=1" fi else CACHE_FROM_CMD='' @@ -166,11 +169,15 @@ fi echo "docker build \ ${CI_DOCKER_BUILD_ARG} \ + --progress=plain \ + --ulimit nofile=1024000:1024000 \ -t ${DOCKER_IMG_NAME} \ -f ${DOCKERFILE_PATH} ${DOCKER_CONTEXT_PATH} \ ${CACHE_FROM_CMD}" docker build \ ${CI_DOCKER_BUILD_ARG} \ + --progress=plain \ + --ulimit nofile=1024000:1024000 \ -t "${DOCKER_IMG_NAME}" \ -f "${DOCKERFILE_PATH}" "${DOCKER_CONTEXT_PATH}" \ ${CACHE_FROM_CMD} @@ -231,7 +238,8 @@ echo "Running '${COMMAND[*]}' inside ${DOCKER_IMG_NAME}..." # and share the PID namespace (--pid=host) so the process inside does not have # pid 1 and SIGKILL is propagated to the process inside (jenkins can kill it). set -x -${DOCKER_BINARY} run --rm --pid=host \ +docker run --rm --pid=host \ + ${GPU_FLAG} \ -v "${WORKSPACE}":/workspace \ -w /workspace \ ${USER_IDS} \ diff --git a/tests/cpp/common/test_device_helpers.cu b/tests/cpp/common/test_device_helpers.cu index 7ae8faf03..1d10a48ad 100644 --- a/tests/cpp/common/test_device_helpers.cu +++ b/tests/cpp/common/test_device_helpers.cu @@ -165,7 +165,7 @@ TEST(SegmentedUnique, Regression) { } } -TEST(Allocator, OOM) { +TEST(Allocator, DISABLED_OOM) { auto size = dh::AvailableMemory(0) * 4; ASSERT_THROW({dh::caching_device_vector vec(size);}, dmlc::Error); ASSERT_THROW({dh::device_vector vec(size);}, dmlc::Error); From e40c4260edddc0f96d640ca0e762d4e65fd57a8a Mon Sep 17 00:00:00 2001 From: david-cortes Date: Sat, 30 Dec 2023 06:28:27 +0100 Subject: [PATCH 35/59] [R] Enable 'dot' dump format (#9930) --- R-package/R/xgb.dump.R | 14 ++++++++-- R-package/R/xgb.plot.tree.R | 49 +++++++++++++++++++++++++++++++--- R-package/man/xgb.dump.Rd | 11 ++++++-- R-package/man/xgb.plot.tree.Rd | 35 ++++++++++++++++++++++-- 4 files changed, 100 insertions(+), 9 deletions(-) diff --git a/R-package/R/xgb.dump.R b/R-package/R/xgb.dump.R index a2de26c26..4421836d1 100644 --- a/R-package/R/xgb.dump.R +++ b/R-package/R/xgb.dump.R @@ -13,7 +13,10 @@ #' When this option is on, the model dump contains two additional values: #' gain is the approximate loss function gain we get in each split; #' cover is the sum of second order gradient in each node. -#' @param dump_format either 'text' or 'json' format could be specified. +#' @param dump_format either 'text', 'json', or 'dot' (graphviz) format could be specified. +#' +#' Format 'dot' for a single tree can be passed directly to packages that consume this format +#' for graph visualization, such as function [DiagrammeR::grViz()] #' @param ... currently not used #' #' @return @@ -37,9 +40,13 @@ #' # print in JSON format: #' cat(xgb.dump(bst, with_stats = TRUE, dump_format='json')) #' +#' # plot first tree leveraging the 'dot' format +#' if (requireNamespace('DiagrammeR', quietly = TRUE)) { +#' DiagrammeR::grViz(xgb.dump(bst, dump_format = "dot")[[1L]]) +#' } #' @export xgb.dump <- function(model, fname = NULL, fmap = "", with_stats = FALSE, - dump_format = c("text", "json"), ...) { + dump_format = c("text", "json", "dot"), ...) { check.deprecation(...) dump_format <- match.arg(dump_format) if (!inherits(model, "xgb.Booster")) @@ -52,6 +59,9 @@ xgb.dump <- function(model, fname = NULL, fmap = "", with_stats = FALSE, model <- xgb.Booster.complete(model) model_dump <- .Call(XGBoosterDumpModel_R, model$handle, NVL(fmap, "")[1], as.integer(with_stats), as.character(dump_format)) + if (dump_format == "dot") { + return(sapply(model_dump, function(x) gsub("^booster\\[\\d+\\]\\n", "\\1", x))) + } if (is.null(fname)) model_dump <- gsub('\t', '', model_dump, fixed = TRUE) diff --git a/R-package/R/xgb.plot.tree.R b/R-package/R/xgb.plot.tree.R index 29d00e111..8b12d8a68 100644 --- a/R-package/R/xgb.plot.tree.R +++ b/R-package/R/xgb.plot.tree.R @@ -14,11 +14,33 @@ #' The values are passed to [DiagrammeR::render_graph()]. #' @param render Should the graph be rendered or not? The default is `TRUE`. #' @param show_node_id a logical flag for whether to show node id's in the graph. +#' @param style Style to use for the plot. Options are:\itemize{ +#' \item `"xgboost"`: will use the plot style defined in the core XGBoost library, +#' which is shared between different interfaces through the 'dot' format. This +#' style was not available before version 2.1.0 in R. It always plots the trees +#' vertically (from top to bottom). +#' \item `"R"`: will use the style defined from XGBoost's R interface, which predates +#' the introducition of the standardized style from the core library. It might plot +#' the trees horizontally (from left to right). +#' } +#' +#' Note that `style="xgboost"` is only supported when all of the following conditions are met:\itemize{ +#' \item Only a single tree is being plotted. +#' \item Node IDs are not added to the graph. +#' \item The graph is being returned as `htmlwidget` (`render=TRUE`). +#' } #' @param ... currently not used. #' #' @details #' -#' The content of each node is visualized like this: +#' When using `style="xgboost"`, the content of each node is visualized as follows: +#' - For non-terminal nodes, it will display the split condition (number or name if +#' available, and the condition that would decide to which node to go next). +#' - Those nodes will be connected to their children by arrows that indicate whether the +#' branch corresponds to the condition being met or not being met. +#' - Terminal (leaf) nodes contain the margin to add when ending there. +#' +#' When using `style="R"`, the content of each node is visualized like this: #' - *Feature name*. #' - *Cover:* The sum of second order gradients of training data. #' For the squared loss, this simply corresponds to the number of instances in the node. @@ -57,8 +79,13 @@ #' objective = "binary:logistic" #' ) #' +#' # plot the first tree, using the style from xgboost's core library +#' # (this plot should look identical to the ones generated from other +#' # interfaces like the python package for xgboost) +#' xgb.plot.tree(model = bst, trees = 1, style = "xgboost") +#' #' # plot all the trees -#' xgb.plot.tree(model = bst) +#' xgb.plot.tree(model = bst, trees = NULL) #' #' # plot only the first tree and display the node ID: #' xgb.plot.tree(model = bst, trees = 0, show_node_id = TRUE) @@ -77,7 +104,7 @@ #' #' @export xgb.plot.tree <- function(feature_names = NULL, model = NULL, trees = NULL, plot_width = NULL, plot_height = NULL, - render = TRUE, show_node_id = FALSE, ...) { + render = TRUE, show_node_id = FALSE, style = c("R", "xgboost"), ...) { check.deprecation(...) if (!inherits(model, "xgb.Booster")) { stop("model: Has to be an object of class xgb.Booster") @@ -87,6 +114,22 @@ xgb.plot.tree <- function(feature_names = NULL, model = NULL, trees = NULL, plot stop("DiagrammeR package is required for xgb.plot.tree", call. = FALSE) } + style <- as.character(head(style, 1L)) + stopifnot(style %in% c("R", "xgboost")) + if (style == "xgboost") { + if (NROW(trees) != 1L || !render || show_node_id) { + stop("style='xgboost' is only supported for single, rendered tree, without node IDs.") + } + if (!is.null(feature_names)) { + stop( + "style='xgboost' cannot override 'feature_names'. Will automatically take them from the model." + ) + } + + txt <- xgb.dump(model, dump_format = "dot") + return(DiagrammeR::grViz(txt[[trees + 1]], width = plot_width, height = plot_height)) + } + dt <- xgb.model.dt.tree(feature_names = feature_names, model = model, trees = trees) dt[, label := paste0(Feature, "\nCover: ", Cover, ifelse(Feature == "Leaf", "\nValue: ", "\nGain: "), Quality)] diff --git a/R-package/man/xgb.dump.Rd b/R-package/man/xgb.dump.Rd index 791e74d96..2cdb6b16a 100644 --- a/R-package/man/xgb.dump.Rd +++ b/R-package/man/xgb.dump.Rd @@ -9,7 +9,7 @@ xgb.dump( fname = NULL, fmap = "", with_stats = FALSE, - dump_format = c("text", "json"), + dump_format = c("text", "json", "dot"), ... ) } @@ -29,7 +29,10 @@ When this option is on, the model dump contains two additional values: gain is the approximate loss function gain we get in each split; cover is the sum of second order gradient in each node.} -\item{dump_format}{either 'text' or 'json' format could be specified.} +\item{dump_format}{either 'text', 'json', or 'dot' (graphviz) format could be specified. + +Format 'dot' for a single tree can be passed directly to packages that consume this format +for graph visualization, such as function \code{\link[DiagrammeR:grViz]{DiagrammeR::grViz()}}} \item{...}{currently not used} } @@ -57,4 +60,8 @@ print(xgb.dump(bst, with_stats = TRUE)) # print in JSON format: cat(xgb.dump(bst, with_stats = TRUE, dump_format='json')) +# plot first tree leveraging the 'dot' format +if (requireNamespace('DiagrammeR', quietly = TRUE)) { + DiagrammeR::grViz(xgb.dump(bst, dump_format = "dot")[[1L]]) +} } diff --git a/R-package/man/xgb.plot.tree.Rd b/R-package/man/xgb.plot.tree.Rd index 7571487eb..a09bb7183 100644 --- a/R-package/man/xgb.plot.tree.Rd +++ b/R-package/man/xgb.plot.tree.Rd @@ -12,6 +12,7 @@ xgb.plot.tree( plot_height = NULL, render = TRUE, show_node_id = FALSE, + style = c("R", "xgboost"), ... ) } @@ -34,6 +35,22 @@ The values are passed to \code{\link[DiagrammeR:render_graph]{DiagrammeR::render \item{show_node_id}{a logical flag for whether to show node id's in the graph.} +\item{style}{Style to use for the plot. Options are:\itemize{ +\item \code{"xgboost"}: will use the plot style defined in the core XGBoost library, +which is shared between different interfaces through the 'dot' format. This +style was not available before version 2.1.0 in R. It always plots the trees +vertically (from top to bottom). +\item \code{"R"}: will use the style defined from XGBoost's R interface, which predates +the introducition of the standardized style from the core library. It might plot +the trees horizontally (from left to right). +} + +Note that \code{style="xgboost"} is only supported when all of the following conditions are met:\itemize{ +\item Only a single tree is being plotted. +\item Node IDs are not added to the graph. +\item The graph is being returned as \code{htmlwidget} (\code{render=TRUE}). +}} + \item{...}{currently not used.} } \value{ @@ -51,7 +68,16 @@ before rendering the graph with \code{\link[DiagrammeR:render_graph]{DiagrammeR: Read a tree model text dump and plot the model. } \details{ -The content of each node is visualized like this: +When using \code{style="xgboost"}, the content of each node is visualized as follows: +\itemize{ +\item For non-terminal nodes, it will display the split condition (number or name if +available, and the condition that would decide to which node to go next). +\item Those nodes will be connected to their children by arrows that indicate whether the +branch corresponds to the condition being met or not being met. +\item Terminal (leaf) nodes contain the margin to add when ending there. +} + +When using \code{style="R"}, the content of each node is visualized like this: \itemize{ \item \emph{Feature name}. \item \emph{Cover:} The sum of second order gradients of training data. @@ -83,8 +109,13 @@ bst <- xgboost( objective = "binary:logistic" ) +# plot the first tree, using the style from xgboost's core library +# (this plot should look identical to the ones generated from other +# interfaces like the python package for xgboost) +xgb.plot.tree(model = bst, trees = 1, style = "xgboost") + # plot all the trees -xgb.plot.tree(model = bst) +xgb.plot.tree(model = bst, trees = NULL) # plot only the first tree and display the node ID: xgb.plot.tree(model = bst, trees = 0, show_node_id = TRUE) From 8b9c98b65b511339c473cdc3043c390aa545fc2c Mon Sep 17 00:00:00 2001 From: david-cortes Date: Sun, 31 Dec 2023 03:45:04 +0100 Subject: [PATCH 36/59] [R] Clearer function signatures for S3 methods (#9937) --- R-package/R/xgb.DMatrix.R | 19 ++++++++----------- R-package/man/getinfo.Rd | 6 ++---- R-package/man/setinfo.Rd | 6 ++---- R-package/man/slice.xgb.DMatrix.Rd | 6 ++---- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 11d1105e6..6acd1e6b2 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -340,7 +340,6 @@ dimnames.xgb.DMatrix <- function(x) { #' Get information of an xgb.DMatrix object #' @param object Object of class \code{xgb.DMatrix} #' @param name the name of the information field to get (see details) -#' @param ... other parameters #' #' @details #' The \code{name} field can be one of the following: @@ -372,11 +371,11 @@ dimnames.xgb.DMatrix <- function(x) { #' stopifnot(all(labels2 == 1-labels)) #' @rdname getinfo #' @export -getinfo <- function(object, ...) UseMethod("getinfo") +getinfo <- function(object, name) UseMethod("getinfo") #' @rdname getinfo #' @export -getinfo.xgb.DMatrix <- function(object, name, ...) { +getinfo.xgb.DMatrix <- function(object, name) { allowed_int_fields <- 'group' allowed_float_fields <- c( 'label', 'weight', 'base_margin', @@ -421,7 +420,6 @@ getinfo.xgb.DMatrix <- function(object, name, ...) { #' @param object Object of class "xgb.DMatrix" #' @param name the name of the field to get #' @param info the specific field of information to set -#' @param ... Not used. #' #' @details #' See the documentation for \link{xgb.DMatrix} for possible fields that can be set @@ -445,17 +443,17 @@ getinfo.xgb.DMatrix <- function(object, name, ...) { #' stopifnot(all.equal(labels2, 1-labels)) #' @rdname setinfo #' @export -setinfo <- function(object, ...) UseMethod("setinfo") +setinfo <- function(object, name, info) UseMethod("setinfo") #' @rdname setinfo #' @export -setinfo.xgb.DMatrix <- function(object, name, info, ...) { - .internal.setinfo.xgb.DMatrix(object, name, info, ...) +setinfo.xgb.DMatrix <- function(object, name, info) { + .internal.setinfo.xgb.DMatrix(object, name, info) attr(object, "fields")[[name]] <- TRUE return(TRUE) } -.internal.setinfo.xgb.DMatrix <- function(object, name, info, ...) { +.internal.setinfo.xgb.DMatrix <- function(object, name, info) { if (name == "label") { if (NROW(info) != nrow(object)) stop("The length of labels must equal to the number of rows in the input data") @@ -538,7 +536,6 @@ setinfo.xgb.DMatrix <- function(object, name, info, ...) { #' @param object Object of class "xgb.DMatrix" #' @param idxset a integer vector of indices of rows needed #' @param colset currently not used (columns subsetting is not available) -#' @param ... other parameters (currently not used) #' #' @examples #' data(agaricus.train, package='xgboost') @@ -552,11 +549,11 @@ setinfo.xgb.DMatrix <- function(object, name, info, ...) { #' #' @rdname slice.xgb.DMatrix #' @export -slice <- function(object, ...) UseMethod("slice") +slice <- function(object, idxset) UseMethod("slice") #' @rdname slice.xgb.DMatrix #' @export -slice.xgb.DMatrix <- function(object, idxset, ...) { +slice.xgb.DMatrix <- function(object, idxset) { if (!inherits(object, "xgb.DMatrix")) { stop("object must be xgb.DMatrix") } diff --git a/R-package/man/getinfo.Rd b/R-package/man/getinfo.Rd index 71f855d8a..cb552886b 100644 --- a/R-package/man/getinfo.Rd +++ b/R-package/man/getinfo.Rd @@ -5,15 +5,13 @@ \alias{getinfo.xgb.DMatrix} \title{Get information of an xgb.DMatrix object} \usage{ -getinfo(object, ...) +getinfo(object, name) -\method{getinfo}{xgb.DMatrix}(object, name, ...) +\method{getinfo}{xgb.DMatrix}(object, name) } \arguments{ \item{object}{Object of class \code{xgb.DMatrix}} -\item{...}{other parameters} - \item{name}{the name of the information field to get (see details)} } \description{ diff --git a/R-package/man/setinfo.Rd b/R-package/man/setinfo.Rd index 299e72675..549fc9b20 100644 --- a/R-package/man/setinfo.Rd +++ b/R-package/man/setinfo.Rd @@ -5,15 +5,13 @@ \alias{setinfo.xgb.DMatrix} \title{Set information of an xgb.DMatrix object} \usage{ -setinfo(object, ...) +setinfo(object, name, info) -\method{setinfo}{xgb.DMatrix}(object, name, info, ...) +\method{setinfo}{xgb.DMatrix}(object, name, info) } \arguments{ \item{object}{Object of class "xgb.DMatrix"} -\item{...}{Not used.} - \item{name}{the name of the field to get} \item{info}{the specific field of information to set} diff --git a/R-package/man/slice.xgb.DMatrix.Rd b/R-package/man/slice.xgb.DMatrix.Rd index cb65083e2..a2dfb699b 100644 --- a/R-package/man/slice.xgb.DMatrix.Rd +++ b/R-package/man/slice.xgb.DMatrix.Rd @@ -7,17 +7,15 @@ \title{Get a new DMatrix containing the specified rows of original xgb.DMatrix object} \usage{ -slice(object, ...) +slice(object, idxset) -\method{slice}{xgb.DMatrix}(object, idxset, ...) +\method{slice}{xgb.DMatrix}(object, idxset) \method{[}{xgb.DMatrix}(object, idxset, colset = NULL) } \arguments{ \item{object}{Object of class "xgb.DMatrix"} -\item{...}{other parameters (currently not used)} - \item{idxset}{a integer vector of indices of rows needed} \item{colset}{currently not used (columns subsetting is not available)} From 73713de6016163252958463147c9c6cd509e79b1 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Sun, 31 Dec 2023 06:01:00 +0100 Subject: [PATCH 37/59] [R] rename Quality -> Gain (#9938) --- R-package/R/xgb.model.dt.tree.R | 10 +++++----- R-package/R/xgb.plot.deepness.R | 4 ++-- R-package/R/xgb.plot.multi.trees.R | 4 ++-- R-package/R/xgb.plot.tree.R | 4 ++-- R-package/man/xgb.model.dt.tree.Rd | 2 +- R-package/tests/testthat/test_helpers.R | 4 ++-- R-package/tests/testthat/test_update.R | 16 ++++++++-------- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/R-package/R/xgb.model.dt.tree.R b/R-package/R/xgb.model.dt.tree.R index 8e74ea4b4..9a32d82a0 100644 --- a/R-package/R/xgb.model.dt.tree.R +++ b/R-package/R/xgb.model.dt.tree.R @@ -28,7 +28,7 @@ #' - `Yes`: ID of the next node when the split condition is met. #' - `No`: ID of the next node when the split condition is not met. #' - `Missing`: ID of the next node when the branch value is missing. -#' - `Quality`: either the split gain (change in loss) or the leaf value. +#' - `Gain`: either the split gain (change in loss) or the leaf value. #' - `Cover`: metric related to the number of observations either seen by a split #' or collected by a leaf during training. #' @@ -122,7 +122,7 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, # parse branch lines branch_rx <- paste0("f(\\d+)<(", anynumber_regex, ")\\] yes=(\\d+),no=(\\d+),missing=(\\d+),", "gain=(", anynumber_regex, "),cover=(", anynumber_regex, ")") - branch_cols <- c("Feature", "Split", "Yes", "No", "Missing", "Quality", "Cover") + branch_cols <- c("Feature", "Split", "Yes", "No", "Missing", "Gain", "Cover") td[ isLeaf == FALSE, (branch_cols) := { @@ -132,7 +132,7 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, xtr[, 3:5] <- add.tree.id(xtr[, 3:5], Tree) if (length(xtr) == 0) { as.data.table( - list(Feature = "NA", Split = "NA", Yes = "NA", No = "NA", Missing = "NA", Quality = "NA", Cover = "NA") + list(Feature = "NA", Split = "NA", Yes = "NA", No = "NA", Missing = "NA", Gain = "NA", Cover = "NA") ) } else { as.data.table(xtr) @@ -152,7 +152,7 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, # parse leaf lines leaf_rx <- paste0("leaf=(", anynumber_regex, "),cover=(", anynumber_regex, ")") - leaf_cols <- c("Feature", "Quality", "Cover") + leaf_cols <- c("Feature", "Gain", "Cover") td[ isLeaf == TRUE, (leaf_cols) := { @@ -167,7 +167,7 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, ] # convert some columns to numeric - numeric_cols <- c("Split", "Quality", "Cover") + numeric_cols <- c("Split", "Gain", "Cover") td[, (numeric_cols) := lapply(.SD, as.numeric), .SDcols = numeric_cols] if (use_int_id) { int_cols <- c("Yes", "No", "Missing") diff --git a/R-package/R/xgb.plot.deepness.R b/R-package/R/xgb.plot.deepness.R index 092b07d38..8e1972374 100644 --- a/R-package/R/xgb.plot.deepness.R +++ b/R-package/R/xgb.plot.deepness.R @@ -92,7 +92,7 @@ xgb.plot.deepness <- function(model = NULL, which = c("2x1", "max.depth", "med.d stop("Model tree columns are not as expected!\n", " Note that this function works only for tree models.") - dt_depths <- merge(get.leaf.depth(dt_tree), dt_tree[, .(ID, Cover, Weight = Quality)], by = "ID") + dt_depths <- merge(get.leaf.depth(dt_tree), dt_tree[, .(ID, Cover, Weight = Gain)], by = "ID") setkeyv(dt_depths, c("Tree", "ID")) # count by depth levels, and also calculate average cover at a depth dt_summaries <- dt_depths[, .(.N, Cover = mean(Cover)), Depth] @@ -157,6 +157,6 @@ get.leaf.depth <- function(dt_tree) { # They are mainly column names inferred by Data.table... globalVariables( c( - ".N", "N", "Depth", "Quality", "Cover", "Tree", "ID", "Yes", "No", "Feature", "Leaf", "Weight" + ".N", "N", "Depth", "Gain", "Cover", "Tree", "ID", "Yes", "No", "Feature", "Leaf", "Weight" ) ) diff --git a/R-package/R/xgb.plot.multi.trees.R b/R-package/R/xgb.plot.multi.trees.R index 6402cb767..88616cfb7 100644 --- a/R-package/R/xgb.plot.multi.trees.R +++ b/R-package/R/xgb.plot.multi.trees.R @@ -95,13 +95,13 @@ xgb.plot.multi.trees <- function(model, feature_names = NULL, features_keep = 5, data.table::set(tree.matrix, j = nm, value = sub("^\\d+-", "", tree.matrix[[nm]])) nodes.dt <- tree.matrix[ - , .(Quality = sum(Quality)) + , .(Gain = sum(Gain)) , by = .(abs.node.position, Feature) ][, .(Text = paste0( paste0( Feature[seq_len(min(length(Feature), features_keep))], " (", - format(Quality[seq_len(min(length(Quality), features_keep))], digits = 5), + format(Gain[seq_len(min(length(Gain), features_keep))], digits = 5), ")" ), collapse = "\n" diff --git a/R-package/R/xgb.plot.tree.R b/R-package/R/xgb.plot.tree.R index 8b12d8a68..c75a42e84 100644 --- a/R-package/R/xgb.plot.tree.R +++ b/R-package/R/xgb.plot.tree.R @@ -132,7 +132,7 @@ xgb.plot.tree <- function(feature_names = NULL, model = NULL, trees = NULL, plot dt <- xgb.model.dt.tree(feature_names = feature_names, model = model, trees = trees) - dt[, label := paste0(Feature, "\nCover: ", Cover, ifelse(Feature == "Leaf", "\nValue: ", "\nGain: "), Quality)] + dt[, label := paste0(Feature, "\nCover: ", Cover, ifelse(Feature == "Leaf", "\nValue: ", "\nGain: "), Gain)] if (show_node_id) dt[, label := paste0(ID, ": ", label)] dt[Node == 0, label := paste0("Tree ", Tree, "\n", label)] @@ -199,4 +199,4 @@ xgb.plot.tree <- function(feature_names = NULL, model = NULL, trees = NULL, plot # Avoid error messages during CRAN check. # The reason is that these variables are never declared # They are mainly column names inferred by Data.table... -globalVariables(c("Feature", "ID", "Cover", "Quality", "Split", "Yes", "No", "Missing", ".", "shape", "filledcolor", "label")) +globalVariables(c("Feature", "ID", "Cover", "Gain", "Split", "Yes", "No", "Missing", ".", "shape", "filledcolor", "label")) diff --git a/R-package/man/xgb.model.dt.tree.Rd b/R-package/man/xgb.model.dt.tree.Rd index 477c40775..330998ab8 100644 --- a/R-package/man/xgb.model.dt.tree.Rd +++ b/R-package/man/xgb.model.dt.tree.Rd @@ -46,7 +46,7 @@ for a leaf node, it simply labels it as \code{"Leaf"}. \item \code{Yes}: ID of the next node when the split condition is met. \item \code{No}: ID of the next node when the split condition is not met. \item \code{Missing}: ID of the next node when the branch value is missing. -\item \code{Quality}: either the split gain (change in loss) or the leaf value. +\item \code{Gain}: either the split gain (change in loss) or the leaf value. \item \code{Cover}: metric related to the number of observations either seen by a split or collected by a leaf during training. } diff --git a/R-package/tests/testthat/test_helpers.R b/R-package/tests/testthat/test_helpers.R index de6a099fc..7fae052b4 100644 --- a/R-package/tests/testthat/test_helpers.R +++ b/R-package/tests/testthat/test_helpers.R @@ -275,7 +275,7 @@ test_that("xgb.Booster serializing as R object works", { test_that("xgb.model.dt.tree works with and without feature names", { .skip_if_vcd_not_available() - names.dt.trees <- c("Tree", "Node", "ID", "Feature", "Split", "Yes", "No", "Missing", "Quality", "Cover") + names.dt.trees <- c("Tree", "Node", "ID", "Feature", "Split", "Yes", "No", "Missing", "Gain", "Cover") dt.tree <- xgb.model.dt.tree(feature_names = feature.names, model = bst.Tree) expect_equal(names.dt.trees, names(dt.tree)) if (!flag_32bit) @@ -341,7 +341,7 @@ test_that("xgb.importance works with and without feature names", { trees = trees )[ Feature != "Leaf", .( - Gain = sum(Quality), + Gain = sum(Gain), Cover = sum(Cover), Frequency = .N ), diff --git a/R-package/tests/testthat/test_update.R b/R-package/tests/testthat/test_update.R index cf8b6f007..f37bb0d21 100644 --- a/R-package/tests/testthat/test_update.R +++ b/R-package/tests/testthat/test_update.R @@ -53,9 +53,9 @@ test_that("updating the model works", { # should be the same evaluation but different gains and larger cover expect_equal(bst2$evaluation_log, bst2r$evaluation_log) if (!win32_flag) { - expect_equal(tr2[Feature == 'Leaf']$Quality, tr2r[Feature == 'Leaf']$Quality) + expect_equal(tr2[Feature == 'Leaf']$Gain, tr2r[Feature == 'Leaf']$Gain) } - expect_gt(sum(abs(tr2[Feature != 'Leaf']$Quality - tr2r[Feature != 'Leaf']$Quality)), 100) + expect_gt(sum(abs(tr2[Feature != 'Leaf']$Gain - tr2r[Feature != 'Leaf']$Gain)), 100) expect_gt(sum(tr2r$Cover) / sum(tr2$Cover), 1.5) # process type 'update' for no-subsampling model, refreshing the tree stats AND leaves from training data: @@ -72,8 +72,8 @@ test_that("updating the model works", { tr2u <- xgb.model.dt.tree(model = bst2u) # should be the same evaluation but different gains and larger cover expect_equal(bst2$evaluation_log, bst2u$evaluation_log) - expect_equal(tr2[Feature == 'Leaf']$Quality, tr2u[Feature == 'Leaf']$Quality) - expect_gt(sum(abs(tr2[Feature != 'Leaf']$Quality - tr2u[Feature != 'Leaf']$Quality)), 100) + expect_equal(tr2[Feature == 'Leaf']$Gain, tr2u[Feature == 'Leaf']$Gain) + expect_gt(sum(abs(tr2[Feature != 'Leaf']$Gain - tr2u[Feature != 'Leaf']$Gain)), 100) expect_gt(sum(tr2u$Cover) / sum(tr2$Cover), 1.5) # the results should be the same as for the model with an extra 'refresh' updater expect_equal(bst2r$evaluation_log, bst2u$evaluation_log) @@ -87,8 +87,8 @@ test_that("updating the model works", { tr1ut <- xgb.model.dt.tree(model = bst1ut) # should be the same evaluations but different gains and smaller cover (test data is smaller) expect_equal(bst1$evaluation_log, bst1ut$evaluation_log) - expect_equal(tr1[Feature == 'Leaf']$Quality, tr1ut[Feature == 'Leaf']$Quality) - expect_gt(sum(abs(tr1[Feature != 'Leaf']$Quality - tr1ut[Feature != 'Leaf']$Quality)), 100) + expect_equal(tr1[Feature == 'Leaf']$Gain, tr1ut[Feature == 'Leaf']$Gain) + expect_gt(sum(abs(tr1[Feature != 'Leaf']$Gain - tr1ut[Feature != 'Leaf']$Gain)), 100) expect_lt(sum(tr1ut$Cover) / sum(tr1$Cover), 0.5) }) @@ -111,7 +111,7 @@ test_that("updating works for multiclass & multitree", { # should be the same evaluation but different gains and larger cover expect_equal(bst0$evaluation_log, bst0u$evaluation_log) - expect_equal(tr0[Feature == 'Leaf']$Quality, tr0u[Feature == 'Leaf']$Quality) - expect_gt(sum(abs(tr0[Feature != 'Leaf']$Quality - tr0u[Feature != 'Leaf']$Quality)), 100) + expect_equal(tr0[Feature == 'Leaf']$Gain, tr0u[Feature == 'Leaf']$Gain) + expect_gt(sum(abs(tr0[Feature != 'Leaf']$Gain - tr0u[Feature != 'Leaf']$Gain)), 100) expect_gt(sum(tr0u$Cover) / sum(tr0$Cover), 1.5) }) From 32cbab1cc00e5640fd79fd8557c098128d7efbec Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 2 Jan 2024 08:20:51 +0100 Subject: [PATCH 38/59] [R] put 'verbose' in correct argument (#9942) --- R-package/R/xgb.train.R | 16 ++++++++-------- R-package/man/xgb.train.Rd | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/R-package/R/xgb.train.R b/R-package/R/xgb.train.R index d93a0643d..e20c1af3e 100644 --- a/R-package/R/xgb.train.R +++ b/R-package/R/xgb.train.R @@ -251,9 +251,9 @@ #' watchlist <- list(train = dtrain, eval = dtest) #' #' ## A simple xgb.train example: -#' param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread, +#' param <- list(max_depth = 2, eta = 1, nthread = nthread, #' objective = "binary:logistic", eval_metric = "auc") -#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist) +#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0) #' #' ## An xgb.train example where custom objective and evaluation metric are #' ## used: @@ -272,13 +272,13 @@ #' #' # These functions could be used by passing them either: #' # as 'objective' and 'eval_metric' parameters in the params list: -#' param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread, +#' param <- list(max_depth = 2, eta = 1, nthread = nthread, #' objective = logregobj, eval_metric = evalerror) -#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist) +#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0) #' #' # or through the ... arguments: -#' param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread) -#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, +#' param <- list(max_depth = 2, eta = 1, nthread = nthread) +#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, #' objective = logregobj, eval_metric = evalerror) #' #' # or as dedicated 'obj' and 'feval' parameters of xgb.train: @@ -287,10 +287,10 @@ #' #' #' ## An xgb.train example of using variable learning rates at each iteration: -#' param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread, +#' param <- list(max_depth = 2, eta = 1, nthread = nthread, #' objective = "binary:logistic", eval_metric = "auc") #' my_etas <- list(eta = c(0.5, 0.1)) -#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, +#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, #' callbacks = list(cb.reset.parameters(my_etas))) #' #' ## Early stopping: diff --git a/R-package/man/xgb.train.Rd b/R-package/man/xgb.train.Rd index 0ef2e2216..b2eaff27c 100644 --- a/R-package/man/xgb.train.Rd +++ b/R-package/man/xgb.train.Rd @@ -303,9 +303,9 @@ dtest <- with( watchlist <- list(train = dtrain, eval = dtest) ## A simple xgb.train example: -param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread, +param <- list(max_depth = 2, eta = 1, nthread = nthread, objective = "binary:logistic", eval_metric = "auc") -bst <- xgb.train(param, dtrain, nrounds = 2, watchlist) +bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0) ## An xgb.train example where custom objective and evaluation metric are ## used: @@ -324,13 +324,13 @@ evalerror <- function(preds, dtrain) { # These functions could be used by passing them either: # as 'objective' and 'eval_metric' parameters in the params list: -param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread, +param <- list(max_depth = 2, eta = 1, nthread = nthread, objective = logregobj, eval_metric = evalerror) -bst <- xgb.train(param, dtrain, nrounds = 2, watchlist) +bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0) # or through the ... arguments: -param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread) -bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, +param <- list(max_depth = 2, eta = 1, nthread = nthread) +bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, objective = logregobj, eval_metric = evalerror) # or as dedicated 'obj' and 'feval' parameters of xgb.train: @@ -339,10 +339,10 @@ bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, ## An xgb.train example of using variable learning rates at each iteration: -param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread, +param <- list(max_depth = 2, eta = 1, nthread = nthread, objective = "binary:logistic", eval_metric = "auc") my_etas <- list(eta = c(0.5, 0.1)) -bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, +bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, callbacks = list(cb.reset.parameters(my_etas))) ## Early stopping: From 9e33a102021aa2fa2283d5a1e6447f24c3ce9633 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 2 Jan 2024 14:20:01 +0100 Subject: [PATCH 39/59] [R] Replace `xgboost()` with `xgb.train()` in most tests and examples (#9941) --- R-package/R/utils.R | 5 +- R-package/R/xgb.Booster.R | 15 ++-- R-package/R/xgb.load.R | 6 +- R-package/R/xgb.save.R | 6 +- R-package/R/xgb.save.raw.R | 4 +- R-package/R/xgb.serialize.R | 4 +- R-package/demo/create_sparse_matrix.R | 4 +- R-package/demo/interaction_constraints.R | 18 ++-- R-package/demo/poisson_regression.R | 4 +- .../a-compatibility-note-for-saveRDS-save.Rd | 5 +- R-package/man/predict.xgb.Booster.Rd | 15 ++-- R-package/man/xgb.load.Rd | 6 +- R-package/man/xgb.save.Rd | 6 +- R-package/man/xgb.save.raw.Rd | 4 +- R-package/man/xgb.serialize.Rd | 4 +- R-package/tests/testthat/test_basic.R | 84 ++++++++++--------- R-package/tests/testthat/test_callbacks.R | 8 +- R-package/tests/testthat/test_gc_safety.R | 4 +- R-package/tests/testthat/test_helpers.R | 49 ++++++----- .../testthat/test_interaction_constraints.R | 6 +- R-package/tests/testthat/test_interactions.R | 10 +-- R-package/tests/testthat/test_io.R | 4 +- R-package/tests/testthat/test_monotone.R | 6 +- .../tests/testthat/test_parameter_exposure.R | 14 ++-- .../tests/testthat/test_poisson_regression.R | 4 +- R-package/tests/testthat/test_unicode.R | 6 +- R-package/vignettes/xgboostfromJSON.Rmd | 5 +- 27 files changed, 156 insertions(+), 150 deletions(-) diff --git a/R-package/R/utils.R b/R-package/R/utils.R index bf08c481d..1798e4ad1 100644 --- a/R-package/R/utils.R +++ b/R-package/R/utils.R @@ -383,8 +383,9 @@ NULL #' #' @examples #' data(agaricus.train, package='xgboost') -#' bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +#' bst <- xgb.train(data = xgb.DMatrix(agaricus.train$data, label = agaricus.train$label), +#' max_depth = 2, eta = 1, nthread = 2, nrounds = 2, +#' objective = "binary:logistic") #' #' # Save as a stand-alone file; load it with xgb.load() #' xgb.save(bst, 'xgb.model') diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index 4e980641a..371f3d129 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -272,9 +272,8 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { #' train <- agaricus.train #' test <- agaricus.test #' -#' bst <- xgboost( -#' data = train$data, -#' label = train$label, +#' bst <- xgb.train( +#' data = xgb.DMatrix(train$data, label = train$label), #' max_depth = 2, #' eta = 0.5, #' nthread = nthread, @@ -316,9 +315,8 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { #' #' set.seed(11) #' -#' bst <- xgboost( -#' data = as.matrix(iris[, -5]), -#' label = lb, +#' bst <- xgb.train( +#' data = xgb.DMatrix(as.matrix(iris[, -5]), label = lb), #' max_depth = 4, #' eta = 0.5, #' nthread = 2, @@ -341,9 +339,8 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { #' # compare with predictions from softmax: #' set.seed(11) #' -#' bst <- xgboost( -#' data = as.matrix(iris[, -5]), -#' label = lb, +#' bst <- xgb.train( +#' data = xgb.DMatrix(as.matrix(iris[, -5]), label = lb), #' max_depth = 4, #' eta = 0.5, #' nthread = 2, diff --git a/R-package/R/xgb.load.R b/R-package/R/xgb.load.R index cbdbdacc3..e8f9e0023 100644 --- a/R-package/R/xgb.load.R +++ b/R-package/R/xgb.load.R @@ -29,8 +29,10 @@ #' #' train <- agaricus.train #' test <- agaricus.test -#' bst <- xgboost( -#' data = train$data, label = train$label, max_depth = 2, eta = 1, +#' bst <- xgb.train( +#' data = xgb.DMatrix(train$data, label = train$label), +#' max_depth = 2, +#' eta = 1, #' nthread = nthread, #' nrounds = 2, #' objective = "binary:logistic" diff --git a/R-package/R/xgb.save.R b/R-package/R/xgb.save.R index ab55bc4a9..32b7d9618 100644 --- a/R-package/R/xgb.save.R +++ b/R-package/R/xgb.save.R @@ -32,8 +32,10 @@ #' #' train <- agaricus.train #' test <- agaricus.test -#' bst <- xgboost( -#' data = train$data, label = train$label, max_depth = 2, eta = 1, +#' bst <- xgb.train( +#' data = xgb.DMatrix(train$data, label = train$label), +#' max_depth = 2, +#' eta = 1, #' nthread = nthread, #' nrounds = 2, #' objective = "binary:logistic" diff --git a/R-package/R/xgb.save.raw.R b/R-package/R/xgb.save.raw.R index cad0fb0e0..63c06e071 100644 --- a/R-package/R/xgb.save.raw.R +++ b/R-package/R/xgb.save.raw.R @@ -23,8 +23,8 @@ #' #' train <- agaricus.train #' test <- agaricus.test -#' bst <- xgboost(data = train$data, label = train$label, max_depth = 2, -#' eta = 1, nthread = nthread, nrounds = 2,objective = "binary:logistic") +#' bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, +#' eta = 1, nthread = nthread, nrounds = 2,objective = "binary:logistic") #' #' raw <- xgb.save.raw(bst) #' bst <- xgb.load.raw(raw) diff --git a/R-package/R/xgb.serialize.R b/R-package/R/xgb.serialize.R index 00bbb4293..c20d2b51c 100644 --- a/R-package/R/xgb.serialize.R +++ b/R-package/R/xgb.serialize.R @@ -9,8 +9,8 @@ #' data(agaricus.test, package='xgboost') #' train <- agaricus.train #' test <- agaricus.test -#' bst <- xgboost(data = train$data, label = train$label, max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") +#' bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, +#' eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") #' raw <- xgb.serialize(bst) #' bst <- xgb.unserialize(raw) #' diff --git a/R-package/demo/create_sparse_matrix.R b/R-package/demo/create_sparse_matrix.R index f8afb14ba..08a40608c 100644 --- a/R-package/demo/create_sparse_matrix.R +++ b/R-package/demo/create_sparse_matrix.R @@ -81,8 +81,8 @@ output_vector <- df[, Y := 0][Improved == "Marked", Y := 1][, Y] # Following is the same process as other demo cat("Learning...\n") -bst <- xgboost(data = sparse_matrix, label = output_vector, max_depth = 9, - eta = 1, nthread = 2, nrounds = 10, objective = "binary:logistic") +bst <- xgb.train(data = xgb.DMatrix(sparse_matrix, label = output_vector), max_depth = 9, + eta = 1, nthread = 2, nrounds = 10, objective = "binary:logistic") importance <- xgb.importance(feature_names = colnames(sparse_matrix), model = bst) print(importance) diff --git a/R-package/demo/interaction_constraints.R b/R-package/demo/interaction_constraints.R index 9e694e3eb..72287513e 100644 --- a/R-package/demo/interaction_constraints.R +++ b/R-package/demo/interaction_constraints.R @@ -74,26 +74,26 @@ cols2ids <- function(object, col_names) { interaction_list_fid <- cols2ids(interaction_list, colnames(train)) # Fit model with interaction constraints -bst <- xgboost(data = train, label = y, max_depth = 4, - eta = 0.1, nthread = 2, nrounds = 1000, - interaction_constraints = interaction_list_fid) +bst <- xgb.train(data = xgb.DMatrix(train, label = y), max_depth = 4, + eta = 0.1, nthread = 2, nrounds = 1000, + interaction_constraints = interaction_list_fid) bst_tree <- xgb.model.dt.tree(colnames(train), bst) bst_interactions <- treeInteractions(bst_tree, 4) # interactions constrained to combinations of V1*V2 and V3*V4*V5 # Fit model without interaction constraints -bst2 <- xgboost(data = train, label = y, max_depth = 4, - eta = 0.1, nthread = 2, nrounds = 1000) +bst2 <- xgb.train(data = xgb.DMatrix(train, label = y), max_depth = 4, + eta = 0.1, nthread = 2, nrounds = 1000) bst2_tree <- xgb.model.dt.tree(colnames(train), bst2) bst2_interactions <- treeInteractions(bst2_tree, 4) # much more interactions # Fit model with both interaction and monotonicity constraints -bst3 <- xgboost(data = train, label = y, max_depth = 4, - eta = 0.1, nthread = 2, nrounds = 1000, - interaction_constraints = interaction_list_fid, - monotone_constraints = c(-1, 0, 0, 0, 0, 0, 0, 0, 0, 0)) +bst3 <- xgb.train(data = xgb.DMatrix(train, label = y), max_depth = 4, + eta = 0.1, nthread = 2, nrounds = 1000, + interaction_constraints = interaction_list_fid, + monotone_constraints = c(-1, 0, 0, 0, 0, 0, 0, 0, 0, 0)) bst3_tree <- xgb.model.dt.tree(colnames(train), bst3) bst3_interactions <- treeInteractions(bst3_tree, 4) diff --git a/R-package/demo/poisson_regression.R b/R-package/demo/poisson_regression.R index 121ac17f2..685314b30 100644 --- a/R-package/demo/poisson_regression.R +++ b/R-package/demo/poisson_regression.R @@ -1,6 +1,6 @@ data(mtcars) head(mtcars) -bst <- xgboost(data = as.matrix(mtcars[, -11]), label = mtcars[, 11], - objective = 'count:poisson', nrounds = 5) +bst <- xgb.train(data = xgb.DMatrix(as.matrix(mtcars[, -11]), label = mtcars[, 11]), + objective = 'count:poisson', nrounds = 5) pred <- predict(bst, as.matrix(mtcars[, -11])) sqrt(mean((pred - mtcars[, 11]) ^ 2)) diff --git a/R-package/man/a-compatibility-note-for-saveRDS-save.Rd b/R-package/man/a-compatibility-note-for-saveRDS-save.Rd index 85b52243c..023cff9fd 100644 --- a/R-package/man/a-compatibility-note-for-saveRDS-save.Rd +++ b/R-package/man/a-compatibility-note-for-saveRDS-save.Rd @@ -33,8 +33,9 @@ For more details and explanation about model persistence and archival, consult t } \examples{ data(agaricus.train, package='xgboost') -bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +bst <- xgb.train(data = xgb.DMatrix(agaricus.train$data, label = agaricus.train$label), + max_depth = 2, eta = 1, nthread = 2, nrounds = 2, + objective = "binary:logistic") # Save as a stand-alone file; load it with xgb.load() xgb.save(bst, 'xgb.model') diff --git a/R-package/man/predict.xgb.Booster.Rd b/R-package/man/predict.xgb.Booster.Rd index 135177dda..f47cab321 100644 --- a/R-package/man/predict.xgb.Booster.Rd +++ b/R-package/man/predict.xgb.Booster.Rd @@ -136,9 +136,8 @@ data.table::setDTthreads(nthread) train <- agaricus.train test <- agaricus.test -bst <- xgboost( - data = train$data, - label = train$label, +bst <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, eta = 0.5, nthread = nthread, @@ -180,9 +179,8 @@ num_class <- 3 set.seed(11) -bst <- xgboost( - data = as.matrix(iris[, -5]), - label = lb, +bst <- xgb.train( + data = xgb.DMatrix(as.matrix(iris[, -5]), label = lb), max_depth = 4, eta = 0.5, nthread = 2, @@ -205,9 +203,8 @@ sum(pred_labels != lb) / length(lb) # compare with predictions from softmax: set.seed(11) -bst <- xgboost( - data = as.matrix(iris[, -5]), - label = lb, +bst <- xgb.train( + data = xgb.DMatrix(as.matrix(iris[, -5]), label = lb), max_depth = 4, eta = 0.5, nthread = 2, diff --git a/R-package/man/xgb.load.Rd b/R-package/man/xgb.load.Rd index 1a406cc21..63f551d7a 100644 --- a/R-package/man/xgb.load.Rd +++ b/R-package/man/xgb.load.Rd @@ -34,8 +34,10 @@ data.table::setDTthreads(nthread) train <- agaricus.train test <- agaricus.test -bst <- xgboost( - data = train$data, label = train$label, max_depth = 2, eta = 1, +bst <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), + max_depth = 2, + eta = 1, nthread = nthread, nrounds = 2, objective = "binary:logistic" diff --git a/R-package/man/xgb.save.Rd b/R-package/man/xgb.save.Rd index a7e160a12..22c6c8fa3 100644 --- a/R-package/man/xgb.save.Rd +++ b/R-package/man/xgb.save.Rd @@ -38,8 +38,10 @@ data.table::setDTthreads(nthread) train <- agaricus.train test <- agaricus.test -bst <- xgboost( - data = train$data, label = train$label, max_depth = 2, eta = 1, +bst <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), + max_depth = 2, + eta = 1, nthread = nthread, nrounds = 2, objective = "binary:logistic" diff --git a/R-package/man/xgb.save.raw.Rd b/R-package/man/xgb.save.raw.Rd index 083551933..498272148 100644 --- a/R-package/man/xgb.save.raw.Rd +++ b/R-package/man/xgb.save.raw.Rd @@ -32,8 +32,8 @@ data.table::setDTthreads(nthread) train <- agaricus.train test <- agaricus.test -bst <- xgboost(data = train$data, label = train$label, max_depth = 2, - eta = 1, nthread = nthread, nrounds = 2,objective = "binary:logistic") +bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, + eta = 1, nthread = nthread, nrounds = 2,objective = "binary:logistic") raw <- xgb.save.raw(bst) bst <- xgb.load.raw(raw) diff --git a/R-package/man/xgb.serialize.Rd b/R-package/man/xgb.serialize.Rd index 952441d98..5bf4205f8 100644 --- a/R-package/man/xgb.serialize.Rd +++ b/R-package/man/xgb.serialize.Rd @@ -21,8 +21,8 @@ data(agaricus.train, package='xgboost') data(agaricus.test, package='xgboost') train <- agaricus.train test <- agaricus.test -bst <- xgboost(data = train$data, label = train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") +bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, + eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") raw <- xgb.serialize(bst) bst <- xgb.unserialize(raw) diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index 8ecf86e87..d4b3a6be3 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -16,10 +16,11 @@ n_threads <- 1 test_that("train and predict binary classification", { nrounds <- 2 expect_output( - bst <- xgboost( - data = train$data, label = train$label, max_depth = 2, + bst <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, eta = 1, nthread = n_threads, nrounds = nrounds, - objective = "binary:logistic", eval_metric = "error" + objective = "binary:logistic", eval_metric = "error", + watchlist = list(train = xgb.DMatrix(train$data, label = train$label)) ), "train-error" ) @@ -104,9 +105,8 @@ test_that("dart prediction works", { rnorm(100) set.seed(1994) - booster_by_xgboost <- xgboost( - data = d, - label = y, + booster_by_xgboost <- xgb.train( + data = xgb.DMatrix(d, label = y), max_depth = 2, booster = "dart", rate_drop = 0.5, @@ -151,10 +151,11 @@ test_that("train and predict softprob", { lb <- as.numeric(iris$Species) - 1 set.seed(11) expect_output( - bst <- xgboost( - data = as.matrix(iris[, -5]), label = lb, + bst <- xgb.train( + data = xgb.DMatrix(as.matrix(iris[, -5]), label = lb), max_depth = 3, eta = 0.5, nthread = n_threads, nrounds = 5, - objective = "multi:softprob", num_class = 3, eval_metric = "merror" + objective = "multi:softprob", num_class = 3, eval_metric = "merror", + watchlist = list(train = xgb.DMatrix(as.matrix(iris[, -5]), label = lb)) ), "train-merror" ) @@ -201,10 +202,11 @@ test_that("train and predict softmax", { lb <- as.numeric(iris$Species) - 1 set.seed(11) expect_output( - bst <- xgboost( - data = as.matrix(iris[, -5]), label = lb, + bst <- xgb.train( + data = xgb.DMatrix(as.matrix(iris[, -5]), label = lb), max_depth = 3, eta = 0.5, nthread = n_threads, nrounds = 5, - objective = "multi:softmax", num_class = 3, eval_metric = "merror" + objective = "multi:softmax", num_class = 3, eval_metric = "merror", + watchlist = list(train = xgb.DMatrix(as.matrix(iris[, -5]), label = lb)) ), "train-merror" ) @@ -222,11 +224,12 @@ test_that("train and predict RF", { set.seed(11) lb <- train$label # single iteration - bst <- xgboost( - data = train$data, label = lb, max_depth = 5, + bst <- xgb.train( + data = xgb.DMatrix(train$data, label = lb), max_depth = 5, nthread = n_threads, nrounds = 1, objective = "binary:logistic", eval_metric = "error", - num_parallel_tree = 20, subsample = 0.6, colsample_bytree = 0.1 + num_parallel_tree = 20, subsample = 0.6, colsample_bytree = 0.1, + watchlist = list(train = xgb.DMatrix(train$data, label = lb)) ) expect_equal(bst$niter, 1) expect_equal(xgb.ntree(bst), 20) @@ -248,12 +251,13 @@ test_that("train and predict RF with softprob", { lb <- as.numeric(iris$Species) - 1 nrounds <- 15 set.seed(11) - bst <- xgboost( - data = as.matrix(iris[, -5]), label = lb, + bst <- xgb.train( + data = xgb.DMatrix(as.matrix(iris[, -5]), label = lb), max_depth = 3, eta = 0.9, nthread = n_threads, nrounds = nrounds, objective = "multi:softprob", eval_metric = "merror", num_class = 3, verbose = 0, - num_parallel_tree = 4, subsample = 0.5, colsample_bytree = 0.5 + num_parallel_tree = 4, subsample = 0.5, colsample_bytree = 0.5, + watchlist = list(train = xgb.DMatrix(as.matrix(iris[, -5]), label = lb)) ) expect_equal(bst$niter, 15) expect_equal(xgb.ntree(bst), 15 * 3 * 4) @@ -271,10 +275,11 @@ test_that("train and predict RF with softprob", { test_that("use of multiple eval metrics works", { expect_output( - bst <- xgboost( - data = train$data, label = train$label, max_depth = 2, + bst <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, eta = 1, nthread = n_threads, nrounds = 2, objective = "binary:logistic", - eval_metric = "error", eval_metric = "auc", eval_metric = "logloss" + eval_metric = "error", eval_metric = "auc", eval_metric = "logloss", + watchlist = list(train = xgb.DMatrix(train$data, label = train$label)) ), "train-error.*train-auc.*train-logloss" ) @@ -282,10 +287,11 @@ test_that("use of multiple eval metrics works", { expect_equal(dim(bst$evaluation_log), c(2, 4)) expect_equal(colnames(bst$evaluation_log), c("iter", "train_error", "train_auc", "train_logloss")) expect_output( - bst2 <- xgboost( - data = train$data, label = train$label, max_depth = 2, + bst2 <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, eta = 1, nthread = n_threads, nrounds = 2, objective = "binary:logistic", - eval_metric = list("error", "auc", "logloss") + eval_metric = list("error", "auc", "logloss"), + watchlist = list(train = xgb.DMatrix(train$data, label = train$label)) ), "train-error.*train-auc.*train-logloss" ) @@ -361,7 +367,7 @@ test_that("xgb.cv works", { expect_is(cv, "xgb.cv.synchronous") expect_false(is.null(cv$evaluation_log)) expect_lt(cv$evaluation_log[, min(test_error_mean)], 0.03) - expect_lt(cv$evaluation_log[, min(test_error_std)], 0.008) + expect_lt(cv$evaluation_log[, min(test_error_std)], 0.0085) expect_equal(cv$niter, 2) expect_false(is.null(cv$folds) && is.list(cv$folds)) expect_length(cv$folds, 5) @@ -391,8 +397,8 @@ test_that("xgb.cv works with stratified folds", { test_that("train and predict with non-strict classes", { # standard dense matrix input train_dense <- as.matrix(train$data) - bst <- xgboost( - data = train_dense, label = train$label, max_depth = 2, + bst <- xgb.train( + data = xgb.DMatrix(train_dense, label = train$label), max_depth = 2, eta = 1, nthread = n_threads, nrounds = 2, objective = "binary:logistic", verbose = 0 ) @@ -402,8 +408,8 @@ test_that("train and predict with non-strict classes", { class(train_dense) <- "shmatrix" expect_true(is.matrix(train_dense)) expect_error( - bst <- xgboost( - data = train_dense, label = train$label, max_depth = 2, + bst <- xgb.train( + data = xgb.DMatrix(train_dense, label = train$label), max_depth = 2, eta = 1, nthread = n_threads, nrounds = 2, objective = "binary:logistic", verbose = 0 ), @@ -416,8 +422,8 @@ test_that("train and predict with non-strict classes", { class(train_dense) <- c("pphmatrix", "shmatrix") expect_true(is.matrix(train_dense)) expect_error( - bst <- xgboost( - data = train_dense, label = train$label, max_depth = 2, + bst <- xgb.train( + data = xgb.DMatrix(train_dense, label = train$label), max_depth = 2, eta = 1, nthread = n_threads, nrounds = 2, objective = "binary:logistic", verbose = 0 ), @@ -480,8 +486,8 @@ test_that("colsample_bytree works", { }) test_that("Configuration works", { - bst <- xgboost( - data = train$data, label = train$label, max_depth = 2, + bst <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, eta = 1, nthread = n_threads, nrounds = 2, objective = "binary:logistic", eval_metric = "error", eval_metric = "auc", eval_metric = "logloss" ) @@ -521,8 +527,8 @@ test_that("strict_shape works", { y <- as.numeric(iris$Species) - 1 X <- as.matrix(iris[, -5]) - bst <- xgboost( - data = X, label = y, + bst <- xgb.train( + data = xgb.DMatrix(X, label = y), max_depth = 2, nrounds = n_rounds, nthread = n_threads, objective = "multi:softprob", num_class = 3, eval_metric = "merror" ) @@ -536,8 +542,8 @@ test_that("strict_shape works", { X <- agaricus.train$data y <- agaricus.train$label - bst <- xgboost( - data = X, label = y, max_depth = 2, nthread = n_threads, + bst <- xgb.train( + data = xgb.DMatrix(X, label = y), max_depth = 2, nthread = n_threads, nrounds = n_rounds, objective = "binary:logistic", eval_metric = "error", eval_metric = "auc", eval_metric = "logloss" ) @@ -555,8 +561,8 @@ test_that("'predict' accepts CSR data", { x_csc <- as(X[1L, , drop = FALSE], "CsparseMatrix") x_csr <- as(x_csc, "RsparseMatrix") x_spv <- as(x_csc, "sparseVector") - bst <- xgboost( - data = X, label = y, objective = "binary:logistic", + bst <- xgb.train( + data = xgb.DMatrix(X, label = y), objective = "binary:logistic", nrounds = 5L, verbose = FALSE, nthread = n_threads, ) p_csc <- predict(bst, x_csc) diff --git a/R-package/tests/testthat/test_callbacks.R b/R-package/tests/testthat/test_callbacks.R index b5d3c5310..63a4c3f25 100644 --- a/R-package/tests/testthat/test_callbacks.R +++ b/R-package/tests/testthat/test_callbacks.R @@ -265,14 +265,14 @@ test_that("early stopping works with titanic", { dtx <- model.matrix(~ 0 + ., data = titanic[, c("Pclass", "Sex")]) dty <- titanic$Survived - xgboost::xgboost( - data = dtx, - label = dty, + xgboost::xgb.train( + data = xgb.DMatrix(dtx, label = dty), objective = "binary:logistic", eval_metric = "auc", nrounds = 100, early_stopping_rounds = 3, - nthread = n_threads + nthread = n_threads, + watchlist = list(train = xgb.DMatrix(dtx, label = dty)) ) expect_true(TRUE) # should not crash diff --git a/R-package/tests/testthat/test_gc_safety.R b/R-package/tests/testthat/test_gc_safety.R index f77af1eab..44d8f81a4 100644 --- a/R-package/tests/testthat/test_gc_safety.R +++ b/R-package/tests/testthat/test_gc_safety.R @@ -6,8 +6,8 @@ test_that("train and prediction when gctorture is on", { train <- agaricus.train test <- agaricus.test gctorture(TRUE) - bst <- xgboost(data = train$data, label = train$label, max.depth = 2, - eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") + bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max.depth = 2, + eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") pred <- predict(bst, test$data) gctorture(FALSE) expect_length(pred, length(test$label)) diff --git a/R-package/tests/testthat/test_helpers.R b/R-package/tests/testthat/test_helpers.R index 7fae052b4..fd1fffbac 100644 --- a/R-package/tests/testthat/test_helpers.R +++ b/R-package/tests/testthat/test_helpers.R @@ -25,15 +25,15 @@ if (isTRUE(VCD_AVAILABLE)) { label <- df[, ifelse(Improved == "Marked", 1, 0)] # binary - bst.Tree <- xgboost(data = sparse_matrix, label = label, max_depth = 9, - eta = 1, nthread = 2, nrounds = nrounds, verbose = 0, - objective = "binary:logistic", booster = "gbtree", - base_score = 0.5) + bst.Tree <- xgb.train(data = xgb.DMatrix(sparse_matrix, label = label), max_depth = 9, + eta = 1, nthread = 2, nrounds = nrounds, verbose = 0, + objective = "binary:logistic", booster = "gbtree", + base_score = 0.5) - bst.GLM <- xgboost(data = sparse_matrix, label = label, - eta = 1, nthread = 1, nrounds = nrounds, verbose = 0, - objective = "binary:logistic", booster = "gblinear", - base_score = 0.5) + bst.GLM <- xgb.train(data = xgb.DMatrix(sparse_matrix, label = label), + eta = 1, nthread = 1, nrounds = nrounds, verbose = 0, + objective = "binary:logistic", booster = "gblinear", + base_score = 0.5) feature.names <- colnames(sparse_matrix) } @@ -41,13 +41,13 @@ if (isTRUE(VCD_AVAILABLE)) { # multiclass mlabel <- as.numeric(iris$Species) - 1 nclass <- 3 -mbst.Tree <- xgboost(data = as.matrix(iris[, -5]), label = mlabel, verbose = 0, - max_depth = 3, eta = 0.5, nthread = 2, nrounds = nrounds, - objective = "multi:softprob", num_class = nclass, base_score = 0) +mbst.Tree <- xgb.train(data = xgb.DMatrix(as.matrix(iris[, -5]), label = mlabel), verbose = 0, + max_depth = 3, eta = 0.5, nthread = 2, nrounds = nrounds, + objective = "multi:softprob", num_class = nclass, base_score = 0) -mbst.GLM <- xgboost(data = as.matrix(iris[, -5]), label = mlabel, verbose = 0, - booster = "gblinear", eta = 0.1, nthread = 1, nrounds = nrounds, - objective = "multi:softprob", num_class = nclass, base_score = 0) +mbst.GLM <- xgb.train(data = xgb.DMatrix(as.matrix(iris[, -5]), label = mlabel), verbose = 0, + booster = "gblinear", eta = 0.1, nthread = 1, nrounds = nrounds, + objective = "multi:softprob", num_class = nclass, base_score = 0) test_that("xgb.dump works", { @@ -71,8 +71,9 @@ test_that("xgb.dump works for gblinear", { expect_length(xgb.dump(bst.GLM), 14) # also make sure that it works properly for a sparse model where some coefficients # are 0 from setting large L1 regularization: - bst.GLM.sp <- xgboost(data = sparse_matrix, label = label, eta = 1, nthread = 2, nrounds = 1, - alpha = 2, objective = "binary:logistic", booster = "gblinear") + bst.GLM.sp <- xgb.train(data = xgb.DMatrix(sparse_matrix, label = label), eta = 1, + nthread = 2, nrounds = 1, + alpha = 2, objective = "binary:logistic", booster = "gblinear") d.sp <- xgb.dump(bst.GLM.sp) expect_length(d.sp, 14) expect_gt(sum(d.sp == "0"), 0) @@ -168,7 +169,7 @@ test_that("SHAPs sum to predictions, with or without DART", { nrounds <- 30 for (booster in list("gbtree", "dart")) { - fit <- xgboost( + fit <- xgb.train( params = c( list( nthread = 2, @@ -177,8 +178,7 @@ test_that("SHAPs sum to predictions, with or without DART", { eval_metric = "rmse"), if (booster == "dart") list(rate_drop = .01, one_drop = TRUE)), - data = d, - label = y, + data = xgb.DMatrix(d, label = y), nrounds = nrounds) pr <- function(...) { @@ -360,9 +360,8 @@ test_that("xgb.importance works with and without feature names", { expect_equal(importance_from_dump(), importance, tolerance = 1e-6) ## decision stump - m <- xgboost::xgboost( - data = as.matrix(data.frame(x = c(0, 1))), - label = c(1, 2), + m <- xgboost::xgb.train( + data = xgb.DMatrix(as.matrix(data.frame(x = c(0, 1))), label = c(1, 2)), nrounds = 1, base_score = 0.5, nthread = 2 @@ -393,9 +392,9 @@ test_that("xgb.importance works with GLM model", { test_that("xgb.model.dt.tree and xgb.importance work with a single split model", { .skip_if_vcd_not_available() - bst1 <- xgboost(data = sparse_matrix, label = label, max_depth = 1, - eta = 1, nthread = 2, nrounds = 1, verbose = 0, - objective = "binary:logistic") + bst1 <- xgb.train(data = xgb.DMatrix(sparse_matrix, label = label), max_depth = 1, + eta = 1, nthread = 2, nrounds = 1, verbose = 0, + objective = "binary:logistic") expect_error(dt <- xgb.model.dt.tree(model = bst1), regexp = NA) # no error expect_equal(nrow(dt), 3) expect_error(imp <- xgb.importance(model = bst1), regexp = NA) # no error diff --git a/R-package/tests/testthat/test_interaction_constraints.R b/R-package/tests/testthat/test_interaction_constraints.R index ee4c453b3..cfffb029c 100644 --- a/R-package/tests/testthat/test_interaction_constraints.R +++ b/R-package/tests/testthat/test_interaction_constraints.R @@ -13,9 +13,9 @@ train <- matrix(c(x1, x2, x3), ncol = 3) test_that("interaction constraints for regression", { # Fit a model that only allows interaction between x1 and x2 - bst <- xgboost(data = train, label = y, max_depth = 3, - eta = 0.1, nthread = 2, nrounds = 100, verbose = 0, - interaction_constraints = list(c(0, 1))) + bst <- xgb.train(data = xgb.DMatrix(train, label = y), max_depth = 3, + eta = 0.1, nthread = 2, nrounds = 100, verbose = 0, + interaction_constraints = list(c(0, 1))) # Set all observations to have the same x3 values then increment # by the same amount diff --git a/R-package/tests/testthat/test_interactions.R b/R-package/tests/testthat/test_interactions.R index 398531e0e..645efc12a 100644 --- a/R-package/tests/testthat/test_interactions.R +++ b/R-package/tests/testthat/test_interactions.R @@ -98,15 +98,14 @@ test_that("SHAP contribution values are not NAN", { ivs <- c("x1", "x2") - fit <- xgboost( + fit <- xgb.train( verbose = 0, params = list( objective = "reg:squarederror", eval_metric = "rmse", nthread = n_threads ), - data = as.matrix(subset(d, fold == 2)[, ivs]), - label = subset(d, fold == 2)$y, + data = xgb.DMatrix(as.matrix(subset(d, fold == 2)[, ivs]), label = subset(d, fold == 2)$y), nrounds = 3 ) @@ -169,9 +168,8 @@ test_that("multiclass feature interactions work", { test_that("SHAP single sample works", { train <- agaricus.train test <- agaricus.test - booster <- xgboost( - data = train$data, - label = train$label, + booster <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, nrounds = 4, objective = "binary:logistic", diff --git a/R-package/tests/testthat/test_io.R b/R-package/tests/testthat/test_io.R index 8cf5a9ae9..3c64ddc72 100644 --- a/R-package/tests/testthat/test_io.R +++ b/R-package/tests/testthat/test_io.R @@ -7,8 +7,8 @@ test <- agaricus.test test_that("load/save raw works", { nrounds <- 8 - booster <- xgboost( - data = train$data, label = train$label, + booster <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), nrounds = nrounds, objective = "binary:logistic", nthread = 2 ) diff --git a/R-package/tests/testthat/test_monotone.R b/R-package/tests/testthat/test_monotone.R index cb5827698..671c02bd0 100644 --- a/R-package/tests/testthat/test_monotone.R +++ b/R-package/tests/testthat/test_monotone.R @@ -7,9 +7,9 @@ train <- matrix(x, ncol = 1) test_that("monotone constraints for regression", { - bst <- xgboost(data = train, label = y, max_depth = 2, - eta = 0.1, nthread = 2, nrounds = 100, verbose = 0, - monotone_constraints = -1) + bst <- xgb.train(data = xgb.DMatrix(train, label = y), max_depth = 2, + eta = 0.1, nthread = 2, nrounds = 100, verbose = 0, + monotone_constraints = -1) pred <- predict(bst, train) diff --git a/R-package/tests/testthat/test_parameter_exposure.R b/R-package/tests/testthat/test_parameter_exposure.R index ea71ca7b7..5b12fde01 100644 --- a/R-package/tests/testthat/test_parameter_exposure.R +++ b/R-package/tests/testthat/test_parameter_exposure.R @@ -10,13 +10,13 @@ dtest <- xgb.DMatrix( agaricus.test$data, label = agaricus.test$label, nthread = 2 ) -bst <- xgboost(data = dtrain, - max_depth = 2, - eta = 1, - nrounds = 10, - nthread = 1, - verbose = 0, - objective = "binary:logistic") +bst <- xgb.train(data = dtrain, + max_depth = 2, + eta = 1, + nrounds = 10, + nthread = 1, + verbose = 0, + objective = "binary:logistic") test_that("call is exposed to R", { expect_false(is.null(bst$call)) diff --git a/R-package/tests/testthat/test_poisson_regression.R b/R-package/tests/testthat/test_poisson_regression.R index 55918b57a..e251a13ad 100644 --- a/R-package/tests/testthat/test_poisson_regression.R +++ b/R-package/tests/testthat/test_poisson_regression.R @@ -4,8 +4,8 @@ set.seed(1994) test_that("Poisson regression works", { data(mtcars) - bst <- xgboost( - data = as.matrix(mtcars[, -11]), label = mtcars[, 11], + bst <- xgb.train( + data = xgb.DMatrix(as.matrix(mtcars[, -11]), label = mtcars[, 11]), objective = 'count:poisson', nrounds = 10, verbose = 0, nthread = 2 ) expect_equal(class(bst), "xgb.Booster") diff --git a/R-package/tests/testthat/test_unicode.R b/R-package/tests/testthat/test_unicode.R index c8a225716..718d58109 100644 --- a/R-package/tests/testthat/test_unicode.R +++ b/R-package/tests/testthat/test_unicode.R @@ -8,9 +8,9 @@ set.seed(1994) test_that("Can save and load models with Unicode paths", { nrounds <- 2 - bst <- xgboost(data = train$data, label = train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = nrounds, objective = "binary:logistic", - eval_metric = "error") + bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, + eta = 1, nthread = 2, nrounds = nrounds, objective = "binary:logistic", + eval_metric = "error") tmpdir <- tempdir() lapply(c("모델.json", "がうる・ぐら.json", "类继承.ubj"), function(x) { path <- file.path(tmpdir, x) diff --git a/R-package/vignettes/xgboostfromJSON.Rmd b/R-package/vignettes/xgboostfromJSON.Rmd index e7ccdf3a9..f5bc3ad9b 100644 --- a/R-package/vignettes/xgboostfromJSON.Rmd +++ b/R-package/vignettes/xgboostfromJSON.Rmd @@ -52,9 +52,8 @@ labels <- c(1, 1, 1, data <- data.frame(dates = dates, labels = labels) -bst <- xgboost( - data = as.matrix(data$dates), - label = labels, +bst <- xgb.train( + data = xgb.DMatrix(as.matrix(data$dates), label = labels), nthread = 2, nrounds = 1, objective = "binary:logistic", From 49247458f9ede5e4073f5a38b4d6deafc20238c8 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 3 Jan 2024 08:26:55 +0100 Subject: [PATCH 40/59] [R] Minor improvements for evaluation printing (#9940) --- R-package/R/callbacks.R | 3 ++- R-package/R/xgb.cv.R | 5 +++-- R-package/tests/testthat/test_callbacks.R | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/R-package/R/callbacks.R b/R-package/R/callbacks.R index 54f821a79..f8f3b5a30 100644 --- a/R-package/R/callbacks.R +++ b/R-package/R/callbacks.R @@ -770,7 +770,8 @@ xgb.gblinear.history <- function(model, class_index = NULL) { if (!is.null(eval_err)) { if (length(eval_res) != length(eval_err)) stop('eval_res & eval_err lengths mismatch') - res <- paste0(sprintf("%s:%f+%f", enames, eval_res, eval_err), collapse = '\t') + # Note: UTF-8 code for plus/minus sign is U+00B1 + res <- paste0(sprintf("%s:%f\U00B1%f", enames, eval_res, eval_err), collapse = '\t') } else { res <- paste0(sprintf("%s:%f", enames, eval_res), collapse = '\t') } diff --git a/R-package/R/xgb.cv.R b/R-package/R/xgb.cv.R index 1c17d86f0..b0d8c4ebe 100644 --- a/R-package/R/xgb.cv.R +++ b/R-package/R/xgb.cv.R @@ -244,8 +244,9 @@ xgb.cv <- function(params = list(), data, nrounds, nfold, label = NULL, missing ) }) msg <- simplify2array(msg) - bst_evaluation <- rowMeans(msg) - bst_evaluation_err <- sqrt(rowMeans(msg^2) - bst_evaluation^2) # nolint + # Note: these variables might look unused here, but they are used in the callbacks + bst_evaluation <- rowMeans(msg) # nolint + bst_evaluation_err <- apply(msg, 1, sd) # nolint for (f in cb$post_iter) f() diff --git a/R-package/tests/testthat/test_callbacks.R b/R-package/tests/testthat/test_callbacks.R index 63a4c3f25..de5150380 100644 --- a/R-package/tests/testthat/test_callbacks.R +++ b/R-package/tests/testthat/test_callbacks.R @@ -57,7 +57,7 @@ test_that("cb.print.evaluation works as expected", { expect_output(f5(), "\\[7\\]\ttrain-auc:0.900000\ttest-auc:0.800000") bst_evaluation_err <- c('train-auc' = 0.1, 'test-auc' = 0.2) - expect_output(f1(), "\\[7\\]\ttrain-auc:0.900000\\+0.100000\ttest-auc:0.800000\\+0.200000") + expect_output(f1(), "\\[7\\]\ttrain-auc:0.900000±0.100000\ttest-auc:0.800000±0.200000") }) test_that("cb.evaluation.log works as expected", { From 3c004a4145c667df84cf7785a672defbde30c2b6 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 3 Jan 2024 10:29:21 +0100 Subject: [PATCH 41/59] [R] Add missing DMatrix functions (#9929) * `XGDMatrixGetQuantileCut` * `XGDMatrixNumNonMissing` * `XGDMatrixGetDataAsCSR` --------- Co-authored-by: Jiaming Yuan --- R-package/NAMESPACE | 5 + R-package/R/xgb.DMatrix.R | 105 ++++++++++++++ R-package/R/xgboost.R | 3 +- R-package/man/xgb.get.DMatrix.data.Rd | 19 +++ .../man/xgb.get.DMatrix.num.non.missing.Rd | 17 +++ R-package/man/xgb.get.DMatrix.qcut.Rd | 58 ++++++++ R-package/src/Makevars.in | 1 + R-package/src/Makevars.win | 1 + R-package/src/init.c | 6 + R-package/src/xgboost_R.cc | 130 +++++++++++++++++- R-package/src/xgboost_R.h | 25 ++++ R-package/tests/testthat/test_dmatrix.R | 59 ++++++++ src/data/array_interface.cc | 13 ++ src/data/array_interface.h | 5 - 14 files changed, 438 insertions(+), 9 deletions(-) create mode 100644 R-package/man/xgb.get.DMatrix.data.Rd create mode 100644 R-package/man/xgb.get.DMatrix.num.non.missing.Rd create mode 100644 R-package/man/xgb.get.DMatrix.qcut.Rd create mode 100644 src/data/array_interface.cc diff --git a/R-package/NAMESPACE b/R-package/NAMESPACE index 40ede23a5..e6f7a82b8 100644 --- a/R-package/NAMESPACE +++ b/R-package/NAMESPACE @@ -37,6 +37,9 @@ export(xgb.create.features) export(xgb.cv) export(xgb.dump) export(xgb.gblinear.history) +export(xgb.get.DMatrix.data) +export(xgb.get.DMatrix.num.non.missing) +export(xgb.get.DMatrix.qcut) export(xgb.get.config) export(xgb.ggplot.deepness) export(xgb.ggplot.importance) @@ -60,6 +63,7 @@ export(xgb.unserialize) export(xgboost) import(methods) importClassesFrom(Matrix,dgCMatrix) +importClassesFrom(Matrix,dgRMatrix) importClassesFrom(Matrix,dgeMatrix) importFrom(Matrix,colSums) importFrom(Matrix,sparse.model.matrix) @@ -83,6 +87,7 @@ importFrom(graphics,points) importFrom(graphics,title) importFrom(jsonlite,fromJSON) importFrom(jsonlite,toJSON) +importFrom(methods,new) importFrom(stats,median) importFrom(stats,predict) importFrom(utils,head) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 6acd1e6b2..cc16e18da 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -526,6 +526,111 @@ setinfo.xgb.DMatrix <- function(object, name, info) { stop("setinfo: unknown info name ", name) } +#' @title Get Quantile Cuts from DMatrix +#' @description Get the quantile cuts (a.k.a. borders) from an `xgb.DMatrix` +#' that has been quantized for the histogram method (`tree_method="hist"`). +#' +#' These cuts are used in order to assign observations to bins - i.e. these are ordered +#' boundaries which are used to determine assignment condition `border_low < x < border_high`. +#' As such, the first and last bin will be outside of the range of the data, so as to include +#' all of the observations there. +#' +#' If a given column has 'n' bins, then there will be 'n+1' cuts / borders for that column, +#' which will be output in sorted order from lowest to highest. +#' +#' Different columns can have different numbers of bins according to their range. +#' @param dmat An `xgb.DMatrix` object, as returned by \link{xgb.DMatrix}. +#' @param output Output format for the quantile cuts. Possible options are:\itemize{ +#' \item `"list"` will return the output as a list with one entry per column, where +#' each column will have a numeric vector with the cuts. The list will be named if +#' `dmat` has column names assigned to it. +#' \item `"arrays"` will return a list with entries `indptr` (base-0 indexing) and +#' `data`. Here, the cuts for column 'i' are obtained by slicing 'data' from entries +#' `indptr[i]+1` to `indptr[i+1]`. +#' } +#' @return The quantile cuts, in the format specified by parameter `output`. +#' @examples +#' library(xgboost) +#' data(mtcars) +#' y <- mtcars$mpg +#' x <- as.matrix(mtcars[, -1]) +#' dm <- xgb.DMatrix(x, label = y, nthread = 1) +#' +#' # DMatrix is not quantized right away, but will be once a hist model is generated +#' model <- xgb.train( +#' data = dm, +#' params = list( +#' tree_method = "hist", +#' max_bin = 8, +#' nthread = 1 +#' ), +#' nrounds = 3 +#' ) +#' +#' # Now can get the quantile cuts +#' xgb.get.DMatrix.qcut(dm) +#' @export +xgb.get.DMatrix.qcut <- function(dmat, output = c("list", "arrays")) { # nolint + stopifnot(inherits(dmat, "xgb.DMatrix")) + output <- head(output, 1L) + stopifnot(output %in% c("list", "arrays")) + res <- .Call(XGDMatrixGetQuantileCut_R, dmat) + if (output == "arrays") { + return(res) + } else { + feature_names <- getinfo(dmat, "feature_name") + ncols <- length(res$indptr) - 1 + out <- lapply( + seq(1, ncols), + function(col) { + st <- res$indptr[col] + end <- res$indptr[col + 1] + if (end <= st) { + return(numeric()) + } + return(res$data[seq(1 + st, end)]) + } + ) + if (NROW(feature_names)) { + names(out) <- feature_names + } + return(out) + } +} + +#' @title Get Number of Non-Missing Entries in DMatrix +#' @param dmat An `xgb.DMatrix` object, as returned by \link{xgb.DMatrix}. +#' @return The number of non-missing entries in the DMatrix +#' @export +xgb.get.DMatrix.num.non.missing <- function(dmat) { # nolint + stopifnot(inherits(dmat, "xgb.DMatrix")) + return(.Call(XGDMatrixNumNonMissing_R, dmat)) +} + +#' @title Get DMatrix Data +#' @param dmat An `xgb.DMatrix` object, as returned by \link{xgb.DMatrix}. +#' @return The data held in the DMatrix, as a sparse CSR matrix (class `dgRMatrix` +#' from package `Matrix`). If it had feature names, these will be added as column names +#' in the output. +#' @export +xgb.get.DMatrix.data <- function(dmat) { + stopifnot(inherits(dmat, "xgb.DMatrix")) + res <- .Call(XGDMatrixGetDataAsCSR_R, dmat) + out <- methods::new("dgRMatrix") + nrows <- as.integer(length(res$indptr) - 1) + out@p <- res$indptr + out@j <- res$indices + out@x <- res$data + out@Dim <- as.integer(c(nrows, res$ncols)) + + feature_names <- getinfo(dmat, "feature_name") + dim_names <- list(NULL, NULL) + if (NROW(feature_names)) { + dim_names[[2L]] <- feature_names + } + out@Dimnames <- dim_names + return(out) +} #' Get a new DMatrix containing the specified rows of #' original xgb.DMatrix object diff --git a/R-package/R/xgboost.R b/R-package/R/xgboost.R index f61c535e2..af6253a72 100644 --- a/R-package/R/xgboost.R +++ b/R-package/R/xgboost.R @@ -82,7 +82,7 @@ NULL NULL # Various imports -#' @importClassesFrom Matrix dgCMatrix dgeMatrix +#' @importClassesFrom Matrix dgCMatrix dgeMatrix dgRMatrix #' @importFrom Matrix colSums #' @importFrom Matrix sparse.model.matrix #' @importFrom Matrix sparseVector @@ -98,6 +98,7 @@ NULL #' @importFrom data.table setnames #' @importFrom jsonlite fromJSON #' @importFrom jsonlite toJSON +#' @importFrom methods new #' @importFrom utils object.size str tail #' @importFrom stats predict #' @importFrom stats median diff --git a/R-package/man/xgb.get.DMatrix.data.Rd b/R-package/man/xgb.get.DMatrix.data.Rd new file mode 100644 index 000000000..36783f583 --- /dev/null +++ b/R-package/man/xgb.get.DMatrix.data.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.DMatrix.R +\name{xgb.get.DMatrix.data} +\alias{xgb.get.DMatrix.data} +\title{Get DMatrix Data} +\usage{ +xgb.get.DMatrix.data(dmat) +} +\arguments{ +\item{dmat}{An \code{xgb.DMatrix} object, as returned by \link{xgb.DMatrix}.} +} +\value{ +The data held in the DMatrix, as a sparse CSR matrix (class \code{dgRMatrix} +from package \code{Matrix}). If it had feature names, these will be added as column names +in the output. +} +\description{ +Get DMatrix Data +} diff --git a/R-package/man/xgb.get.DMatrix.num.non.missing.Rd b/R-package/man/xgb.get.DMatrix.num.non.missing.Rd new file mode 100644 index 000000000..4eb2697f8 --- /dev/null +++ b/R-package/man/xgb.get.DMatrix.num.non.missing.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.DMatrix.R +\name{xgb.get.DMatrix.num.non.missing} +\alias{xgb.get.DMatrix.num.non.missing} +\title{Get Number of Non-Missing Entries in DMatrix} +\usage{ +xgb.get.DMatrix.num.non.missing(dmat) +} +\arguments{ +\item{dmat}{An \code{xgb.DMatrix} object, as returned by \link{xgb.DMatrix}.} +} +\value{ +The number of non-missing entries in the DMatrix +} +\description{ +Get Number of Non-Missing Entries in DMatrix +} diff --git a/R-package/man/xgb.get.DMatrix.qcut.Rd b/R-package/man/xgb.get.DMatrix.qcut.Rd new file mode 100644 index 000000000..8f7c3da75 --- /dev/null +++ b/R-package/man/xgb.get.DMatrix.qcut.Rd @@ -0,0 +1,58 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.DMatrix.R +\name{xgb.get.DMatrix.qcut} +\alias{xgb.get.DMatrix.qcut} +\title{Get Quantile Cuts from DMatrix} +\usage{ +xgb.get.DMatrix.qcut(dmat, output = c("list", "arrays")) +} +\arguments{ +\item{dmat}{An \code{xgb.DMatrix} object, as returned by \link{xgb.DMatrix}.} + +\item{output}{Output format for the quantile cuts. Possible options are:\itemize{ +\item \code{"list"} will return the output as a list with one entry per column, where +each column will have a numeric vector with the cuts. The list will be named if +\code{dmat} has column names assigned to it. +\item \code{"arrays"} will return a list with entries \code{indptr} (base-0 indexing) and +\code{data}. Here, the cuts for column 'i' are obtained by slicing 'data' from entries +\code{indptr[i]+1} to \code{indptr[i+1]}. +}} +} +\value{ +The quantile cuts, in the format specified by parameter \code{output}. +} +\description{ +Get the quantile cuts (a.k.a. borders) from an \code{xgb.DMatrix} +that has been quantized for the histogram method (\code{tree_method="hist"}). + +These cuts are used in order to assign observations to bins - i.e. these are ordered +boundaries which are used to determine assignment condition \verb{border_low < x < border_high}. +As such, the first and last bin will be outside of the range of the data, so as to include +all of the observations there. + +If a given column has 'n' bins, then there will be 'n+1' cuts / borders for that column, +which will be output in sorted order from lowest to highest. + +Different columns can have different numbers of bins according to their range. +} +\examples{ +library(xgboost) +data(mtcars) +y <- mtcars$mpg +x <- as.matrix(mtcars[, -1]) +dm <- xgb.DMatrix(x, label = y, nthread = 1) + +# DMatrix is not quantized right away, but will be once a hist model is generated +model <- xgb.train( + data = dm, + params = list( + tree_method = "hist", + max_bin = 8, + nthread = 1 + ), + nrounds = 3 +) + +# Now can get the quantile cuts +xgb.get.DMatrix.qcut(dm) +} diff --git a/R-package/src/Makevars.in b/R-package/src/Makevars.in index 8af5dbbf6..dd13983f5 100644 --- a/R-package/src/Makevars.in +++ b/R-package/src/Makevars.in @@ -63,6 +63,7 @@ OBJECTS= \ $(PKGROOT)/src/gbm/gblinear.o \ $(PKGROOT)/src/gbm/gblinear_model.o \ $(PKGROOT)/src/data/adapter.o \ + $(PKGROOT)/src/data/array_interface.o \ $(PKGROOT)/src/data/simple_dmatrix.o \ $(PKGROOT)/src/data/data.o \ $(PKGROOT)/src/data/sparse_page_raw_format.o \ diff --git a/R-package/src/Makevars.win b/R-package/src/Makevars.win index 60f754fef..46a862711 100644 --- a/R-package/src/Makevars.win +++ b/R-package/src/Makevars.win @@ -63,6 +63,7 @@ OBJECTS= \ $(PKGROOT)/src/gbm/gblinear.o \ $(PKGROOT)/src/gbm/gblinear_model.o \ $(PKGROOT)/src/data/adapter.o \ + $(PKGROOT)/src/data/array_interface.o \ $(PKGROOT)/src/data/simple_dmatrix.o \ $(PKGROOT)/src/data/data.o \ $(PKGROOT)/src/data/sparse_page_raw_format.o \ diff --git a/R-package/src/init.c b/R-package/src/init.c index f957229af..5eee8ebe6 100644 --- a/R-package/src/init.c +++ b/R-package/src/init.c @@ -45,6 +45,9 @@ extern SEXP XGDMatrixCreateFromDF_R(SEXP, SEXP, SEXP); extern SEXP XGDMatrixGetStrFeatureInfo_R(SEXP, SEXP); extern SEXP XGDMatrixNumCol_R(SEXP); extern SEXP XGDMatrixNumRow_R(SEXP); +extern SEXP XGDMatrixGetQuantileCut_R(SEXP); +extern SEXP XGDMatrixNumNonMissing_R(SEXP); +extern SEXP XGDMatrixGetDataAsCSR_R(SEXP); extern SEXP XGDMatrixSaveBinary_R(SEXP, SEXP, SEXP); extern SEXP XGDMatrixSetInfo_R(SEXP, SEXP, SEXP); extern SEXP XGDMatrixSetStrFeatureInfo_R(SEXP, SEXP, SEXP); @@ -84,6 +87,9 @@ static const R_CallMethodDef CallEntries[] = { {"XGDMatrixGetStrFeatureInfo_R", (DL_FUNC) &XGDMatrixGetStrFeatureInfo_R, 2}, {"XGDMatrixNumCol_R", (DL_FUNC) &XGDMatrixNumCol_R, 1}, {"XGDMatrixNumRow_R", (DL_FUNC) &XGDMatrixNumRow_R, 1}, + {"XGDMatrixGetQuantileCut_R", (DL_FUNC) &XGDMatrixGetQuantileCut_R, 1}, + {"XGDMatrixNumNonMissing_R", (DL_FUNC) &XGDMatrixNumNonMissing_R, 1}, + {"XGDMatrixGetDataAsCSR_R", (DL_FUNC) &XGDMatrixGetDataAsCSR_R, 1}, {"XGDMatrixSaveBinary_R", (DL_FUNC) &XGDMatrixSaveBinary_R, 3}, {"XGDMatrixSetInfo_R", (DL_FUNC) &XGDMatrixSetInfo_R, 3}, {"XGDMatrixSetStrFeatureInfo_R", (DL_FUNC) &XGDMatrixSetStrFeatureInfo_R, 3}, diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index fb05c33b4..60a3fe68b 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -1,5 +1,5 @@ /** - * Copyright 2014-2023 by XGBoost Contributors + * Copyright 2014-2024, XGBoost Contributors */ #include #include @@ -9,9 +9,11 @@ #include #include +#include #include #include #include +#include #include #include #include @@ -20,14 +22,14 @@ #include "../../src/c_api/c_api_error.h" #include "../../src/c_api/c_api_utils.h" // MakeSparseFromPtr #include "../../src/common/threading_utils.h" +#include "../../src/data/array_interface.h" // for ArrayInterface #include "./xgboost_R.h" // Must follow other includes. namespace { - struct ErrorWithUnwind : public std::exception {}; -void ThrowExceptionFromRError(void *unused, Rboolean jump) { +void ThrowExceptionFromRError(void *, Rboolean jump) { if (jump) { throw ErrorWithUnwind(); } @@ -49,6 +51,30 @@ SEXP SafeMkChar(const char *c_str, SEXP continuation_token) { continuation_token); } +SEXP WrappedAllocReal(void *void_ptr) { + size_t *size = static_cast(void_ptr); + return Rf_allocVector(REALSXP, *size); +} + +SEXP SafeAllocReal(size_t size, SEXP continuation_token) { + return R_UnwindProtect( + WrappedAllocReal, static_cast(&size), + ThrowExceptionFromRError, nullptr, + continuation_token); +} + +SEXP WrappedAllocInteger(void *void_ptr) { + size_t *size = static_cast(void_ptr); + return Rf_allocVector(INTSXP, *size); +} + +SEXP SafeAllocInteger(size_t size, SEXP continuation_token) { + return R_UnwindProtect( + WrappedAllocInteger, static_cast(&size), + ThrowExceptionFromRError, nullptr, + continuation_token); +} + [[nodiscard]] std::string MakeArrayInterfaceFromRMat(SEXP R_mat) { SEXP mat_dims = Rf_getAttrib(R_mat, R_DimSymbol); if (Rf_xlength(mat_dims) > 2) { @@ -136,6 +162,37 @@ SEXP SafeMkChar(const char *c_str, SEXP continuation_token) { jconfig["nthread"] = Rf_asInteger(n_threads); return Json::Dump(jconfig); } + +// Allocate a R vector and copy an array interface encoded object to it. +[[nodiscard]] SEXP CopyArrayToR(const char *array_str, SEXP ctoken) { + xgboost::ArrayInterface<1> array{xgboost::StringView{array_str}}; + // R supports only int and double. + bool is_int = + xgboost::DispatchDType(array.type, [](auto t) { return std::is_integral_v; }); + bool is_float = xgboost::DispatchDType( + array.type, [](auto v) { return std::is_floating_point_v; }); + CHECK(is_int || is_float) << "Internal error: Invalid DType."; + CHECK(array.is_contiguous) << "Internal error: Return by XGBoost should be contiguous"; + + // Allocate memory in R + SEXP out = + Rf_protect(is_int ? SafeAllocInteger(array.n, ctoken) : SafeAllocReal(array.n, ctoken)); + + xgboost::DispatchDType(array.type, [&](auto t) { + using T = decltype(t); + auto in_ptr = static_cast(array.data); + if (is_int) { + auto out_ptr = INTEGER(out); + std::copy_n(in_ptr, array.n, out_ptr); + } else { + auto out_ptr = REAL(out); + std::copy_n(in_ptr, array.n, out_ptr); + } + }); + + Rf_unprotect(1); + return out; +} } // namespace struct RRNGStateController { @@ -540,6 +597,73 @@ XGB_DLL SEXP XGDMatrixNumCol_R(SEXP handle) { return ScalarInteger(static_cast(ncol)); } +XGB_DLL SEXP XGDMatrixGetQuantileCut_R(SEXP handle) { + const char *out_names[] = {"indptr", "data", ""}; + SEXP continuation_token = Rf_protect(R_MakeUnwindCont()); + SEXP out = Rf_protect(Rf_mkNamed(VECSXP, out_names)); + R_API_BEGIN(); + const char *out_indptr; + const char *out_data; + CHECK_CALL(XGDMatrixGetQuantileCut(R_ExternalPtrAddr(handle), "{}", &out_indptr, &out_data)); + try { + SET_VECTOR_ELT(out, 0, CopyArrayToR(out_indptr, continuation_token)); + SET_VECTOR_ELT(out, 1, CopyArrayToR(out_data, continuation_token)); + } catch (ErrorWithUnwind &e) { + R_ContinueUnwind(continuation_token); + } + R_API_END(); + Rf_unprotect(2); + return out; +} + +XGB_DLL SEXP XGDMatrixNumNonMissing_R(SEXP handle) { + SEXP out = Rf_protect(Rf_allocVector(REALSXP, 1)); + R_API_BEGIN(); + bst_ulong out_; + CHECK_CALL(XGDMatrixNumNonMissing(R_ExternalPtrAddr(handle), &out_)); + REAL(out)[0] = static_cast(out_); + R_API_END(); + Rf_unprotect(1); + return out; +} + +XGB_DLL SEXP XGDMatrixGetDataAsCSR_R(SEXP handle) { + const char *out_names[] = {"indptr", "indices", "data", "ncols", ""}; + SEXP out = Rf_protect(Rf_mkNamed(VECSXP, out_names)); + R_API_BEGIN(); + + bst_ulong nrows, ncols, nnz; + CHECK_CALL(XGDMatrixNumRow(R_ExternalPtrAddr(handle), &nrows)); + CHECK_CALL(XGDMatrixNumCol(R_ExternalPtrAddr(handle), &ncols)); + CHECK_CALL(XGDMatrixNumNonMissing(R_ExternalPtrAddr(handle), &nnz)); + if (std::max(nrows, ncols) > std::numeric_limits::max()) { + Rf_error("%s", "Error: resulting DMatrix data does not fit into R 'dgRMatrix'."); + } + + SET_VECTOR_ELT(out, 0, Rf_allocVector(INTSXP, nrows + 1)); + SET_VECTOR_ELT(out, 1, Rf_allocVector(INTSXP, nnz)); + SET_VECTOR_ELT(out, 2, Rf_allocVector(REALSXP, nnz)); + SET_VECTOR_ELT(out, 3, Rf_ScalarInteger(ncols)); + + std::unique_ptr indptr(new bst_ulong[nrows + 1]); + std::unique_ptr indices(new unsigned[nnz]); + std::unique_ptr data(new float[nnz]); + + CHECK_CALL(XGDMatrixGetDataAsCSR(R_ExternalPtrAddr(handle), + "{}", + indptr.get(), + indices.get(), + data.get())); + + std::copy(indptr.get(), indptr.get() + nrows + 1, INTEGER(VECTOR_ELT(out, 0))); + std::copy(indices.get(), indices.get() + nnz, INTEGER(VECTOR_ELT(out, 1))); + std::copy(data.get(), data.get() + nnz, REAL(VECTOR_ELT(out, 2))); + + R_API_END(); + Rf_unprotect(1); + return out; +} + // functions related to booster void _BoosterFinalizer(SEXP ext) { if (R_ExternalPtrAddr(ext) == NULL) return; diff --git a/R-package/src/xgboost_R.h b/R-package/src/xgboost_R.h index 2e874e3a6..4e3458957 100644 --- a/R-package/src/xgboost_R.h +++ b/R-package/src/xgboost_R.h @@ -143,6 +143,31 @@ XGB_DLL SEXP XGDMatrixNumRow_R(SEXP handle); */ XGB_DLL SEXP XGDMatrixNumCol_R(SEXP handle); +/*! + * \brief return the quantile cuts used for the histogram method + * \param handle an instance of data matrix + * \return A list with entries 'indptr' and 'data' + */ +XGB_DLL SEXP XGDMatrixGetQuantileCut_R(SEXP handle); + +/*! + * \brief get the number of non-missing entries in a dmatrix + * \param handle an instance of data matrix + * \return the number of non-missing entries + */ +XGB_DLL SEXP XGDMatrixNumNonMissing_R(SEXP handle); + +/*! + * \brief get the data in a dmatrix in CSR format + * \param handle an instance of data matrix + * \return R list with the following entries in this order: + * - 'indptr + * - 'indices + * - 'data' + * - 'ncol' + */ +XGB_DLL SEXP XGDMatrixGetDataAsCSR_R(SEXP handle); + /*! * \brief create xgboost learner * \param dmats a list of dmatrix handles that will be cached diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index 55a699687..81ac884d0 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -375,3 +375,62 @@ test_that("xgb.DMatrix: can take multi-dimensional 'base_margin'", { ) expect_equal(pred_only_x, pred_w_base - b, tolerance = 1e-5) }) + +test_that("xgb.DMatrix: number of non-missing matches data", { + x <- matrix(1:10, nrow = 5) + dm1 <- xgb.DMatrix(x) + expect_equal(xgb.get.DMatrix.num.non.missing(dm1), 10) + + x[2, 2] <- NA + x[4, 1] <- NA + dm2 <- xgb.DMatrix(x) + expect_equal(xgb.get.DMatrix.num.non.missing(dm2), 8) +}) + +test_that("xgb.DMatrix: retrieving data as CSR", { + data(mtcars) + dm <- xgb.DMatrix(as.matrix(mtcars)) + csr <- xgb.get.DMatrix.data(dm) + expect_equal(dim(csr), dim(mtcars)) + expect_equal(colnames(csr), colnames(mtcars)) + expect_equal(unname(as.matrix(csr)), unname(as.matrix(mtcars)), tolerance = 1e-6) +}) + +test_that("xgb.DMatrix: quantile cuts look correct", { + data(mtcars) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + dm <- xgb.DMatrix(x, label = y) + model <- xgb.train( + data = dm, + params = list( + tree_method = "hist", + max_bin = 8, + nthread = 1 + ), + nrounds = 3 + ) + qcut_list <- xgb.get.DMatrix.qcut(dm, "list") + qcut_arrays <- xgb.get.DMatrix.qcut(dm, "arrays") + + expect_equal(length(qcut_arrays), 2) + expect_equal(names(qcut_arrays), c("indptr", "data")) + expect_equal(length(qcut_arrays$indptr), ncol(x) + 1) + expect_true(min(diff(qcut_arrays$indptr)) > 0) + + col_min <- apply(x, 2, min) + col_max <- apply(x, 2, max) + + expect_equal(length(qcut_list), ncol(x)) + expect_equal(names(qcut_list), colnames(x)) + lapply( + seq(1, ncol(x)), + function(col) { + cuts <- qcut_list[[col]] + expect_true(min(diff(cuts)) > 0) + expect_true(col_min[col] > cuts[1]) + expect_true(col_max[col] < cuts[length(cuts)]) + expect_true(length(cuts) <= 9) + } + ) +}) diff --git a/src/data/array_interface.cc b/src/data/array_interface.cc new file mode 100644 index 000000000..06b9ed00c --- /dev/null +++ b/src/data/array_interface.cc @@ -0,0 +1,13 @@ +/** + * Copyright 2019-2024, XGBoost Contributors + */ +#include "array_interface.h" + +#include "../common/common.h" // for AssertGPUSupport + +namespace xgboost { +#if !defined(XGBOOST_USE_CUDA) +void ArrayInterfaceHandler::SyncCudaStream(int64_t) { common::AssertGPUSupport(); } +bool ArrayInterfaceHandler::IsCudaPtr(void const *) { return false; } +#endif // !defined(XGBOOST_USE_CUDA) +} // namespace xgboost diff --git a/src/data/array_interface.h b/src/data/array_interface.h index 0170e6a84..6f2438f37 100644 --- a/src/data/array_interface.h +++ b/src/data/array_interface.h @@ -375,11 +375,6 @@ struct ToDType { static constexpr ArrayInterfaceHandler::Type kType = ArrayInterfaceHandler::kI8; }; -#if !defined(XGBOOST_USE_CUDA) -inline void ArrayInterfaceHandler::SyncCudaStream(int64_t) { common::AssertGPUSupport(); } -inline bool ArrayInterfaceHandler::IsCudaPtr(void const *) { return false; } -#endif // !defined(XGBOOST_USE_CUDA) - /** * \brief A type erased view over __array_interface__ protocol defined by numpy * From 9f73127a238c19aeb95954a41519de56d267186e Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 4 Jan 2024 13:15:18 +0800 Subject: [PATCH 42/59] Cleanup Python GPU tests. (#9934) * Cleanup Python GPU tests. - Remove the use of `gpu_hist` and `gpu_id` in cudf/cupy tests. - Move base margin test into the testing directory. --- demo/guide-python/individual_trees.py | 2 +- python-package/xgboost/testing/data.py | 60 ++++++- tests/ci_build/lint_python.py | 9 +- .../test_device_quantile_dmatrix.py | 5 +- tests/python-gpu/test_from_cudf.py | 163 +++++++++++------- tests/python-gpu/test_from_cupy.py | 96 +++++------ tests/python-gpu/test_gpu_basic_models.py | 18 +- tests/python-gpu/test_gpu_demos.py | 12 +- tests/python-gpu/test_gpu_linear.py | 55 +++--- .../test_gpu_training_continuation.py | 17 +- .../python-gpu/test_monotonic_constraints.py | 21 +-- tests/python/test_dmatrix.py | 54 +----- tests/python/test_with_modin.py | 4 +- tests/python/test_with_pandas.py | 6 +- 14 files changed, 282 insertions(+), 240 deletions(-) diff --git a/demo/guide-python/individual_trees.py b/demo/guide-python/individual_trees.py index d940e8521..93a9aad2b 100644 --- a/demo/guide-python/individual_trees.py +++ b/demo/guide-python/individual_trees.py @@ -58,7 +58,7 @@ def individual_tree() -> None: def model_slices() -> None: - """Inference with each individual using model slices.""" + """Inference with each individual tree using model slices.""" X_train, y_train = load_svmlight_file(train) X_test, y_test = load_svmlight_file(test) Xy_train = xgb.QuantileDMatrix(X_train, y_train) diff --git a/python-package/xgboost/testing/data.py b/python-package/xgboost/testing/data.py index b5f07d981..c0d53f914 100644 --- a/python-package/xgboost/testing/data.py +++ b/python-package/xgboost/testing/data.py @@ -3,7 +3,17 @@ import os import zipfile from dataclasses import dataclass -from typing import Any, Generator, List, NamedTuple, Optional, Tuple, Union +from typing import ( + Any, + Callable, + Generator, + List, + NamedTuple, + Optional, + Tuple, + Type, + Union, +) from urllib import request import numpy as np @@ -603,3 +613,51 @@ def sort_ltr_samples( data = X, clicks, y, qid return data + + +def run_base_margin_info( + DType: Callable, DMatrixT: Type[xgboost.DMatrix], device: str +) -> None: + """Run tests for base margin.""" + rng = np.random.default_rng() + X = DType(rng.normal(0, 1.0, size=100).astype(np.float32).reshape(50, 2)) + if hasattr(X, "iloc"): + y = X.iloc[:, 0] + else: + y = X[:, 0] + base_margin = X + # no error at set + Xy = DMatrixT(X, y, base_margin=base_margin) + # Error at train, caused by check in predictor. + with pytest.raises(ValueError, match=r".*base_margin.*"): + xgboost.train({"tree_method": "hist", "device": device}, Xy) + + if not hasattr(X, "iloc"): + # column major matrix + got = DType(Xy.get_base_margin().reshape(50, 2)) + assert (got == base_margin).all() + + assert base_margin.T.flags.c_contiguous is False + assert base_margin.T.flags.f_contiguous is True + Xy.set_info(base_margin=base_margin.T) + got = DType(Xy.get_base_margin().reshape(2, 50)) + assert (got == base_margin.T).all() + + # Row vs col vec. + base_margin = y + Xy.set_base_margin(base_margin) + bm_col = Xy.get_base_margin() + Xy.set_base_margin(base_margin.reshape(1, base_margin.size)) + bm_row = Xy.get_base_margin() + assert (bm_row == bm_col).all() + + # type + base_margin = base_margin.astype(np.float64) + Xy.set_base_margin(base_margin) + bm_f64 = Xy.get_base_margin() + assert (bm_f64 == bm_col).all() + + # too many dimensions + base_margin = X.reshape(2, 5, 2, 5) + with pytest.raises(ValueError, match=r".*base_margin.*"): + Xy.set_base_margin(base_margin) diff --git a/tests/ci_build/lint_python.py b/tests/ci_build/lint_python.py index fdd643da0..c9170b100 100644 --- a/tests/ci_build/lint_python.py +++ b/tests/ci_build/lint_python.py @@ -27,13 +27,8 @@ class LintersPaths: "tests/python/test_tree_regularization.py", "tests/python/test_shap.py", "tests/python/test_with_pandas.py", - "tests/python-gpu/test_gpu_data_iterator.py", - "tests/python-gpu/test_gpu_prediction.py", - "tests/python-gpu/load_pickle.py", - "tests/python-gpu/test_gpu_pickling.py", - "tests/python-gpu/test_gpu_eval_metrics.py", - "tests/python-gpu/test_gpu_with_sklearn.py", - "tests/python-sycl/test_sycl_prediction.py", + "tests/python-gpu/", + "tests/python-sycl/", "tests/test_distributed/test_with_spark/", "tests/test_distributed/test_gpu_with_spark/", # demo diff --git a/tests/python-gpu/test_device_quantile_dmatrix.py b/tests/python-gpu/test_device_quantile_dmatrix.py index 4cfc61321..6b8ceac4a 100644 --- a/tests/python-gpu/test_device_quantile_dmatrix.py +++ b/tests/python-gpu/test_device_quantile_dmatrix.py @@ -203,9 +203,7 @@ class TestQuantileDMatrix: np.testing.assert_equal(h_ret.indptr, d_ret.indptr) np.testing.assert_equal(h_ret.indices, d_ret.indices) - booster = xgb.train( - {"tree_method": "hist", "device": "cuda:0"}, dtrain=d_m - ) + booster = xgb.train({"tree_method": "hist", "device": "cuda:0"}, dtrain=d_m) np.testing.assert_allclose( booster.predict(d_m), @@ -215,6 +213,7 @@ class TestQuantileDMatrix: def test_ltr(self) -> None: import cupy as cp + X, y, qid, w = tm.make_ltr(100, 3, 3, 5) # make sure GPU is used to run sketching. cpX = cp.array(X) diff --git a/tests/python-gpu/test_from_cudf.py b/tests/python-gpu/test_from_cudf.py index 610c717a9..8707af0c8 100644 --- a/tests/python-gpu/test_from_cudf.py +++ b/tests/python-gpu/test_from_cudf.py @@ -1,19 +1,17 @@ import json -import sys import numpy as np import pytest import xgboost as xgb from xgboost import testing as tm +from xgboost.testing.data import run_base_margin_info -sys.path.append("tests/python") -from test_dmatrix import set_base_margin_info +cudf = pytest.importorskip("cudf") def dmatrix_from_cudf(input_type, DMatrixT, missing=np.NAN): - '''Test constructing DMatrix from cudf''' - import cudf + """Test constructing DMatrix from cudf""" import pandas as pd kRows = 80 @@ -25,9 +23,7 @@ def dmatrix_from_cudf(input_type, DMatrixT, missing=np.NAN): na[5, 0] = missing na[3, 1] = missing - pa = pd.DataFrame({'0': na[:, 0], - '1': na[:, 1], - '2': na[:, 2].astype(np.int32)}) + pa = pd.DataFrame({"0": na[:, 0], "1": na[:, 1], "2": na[:, 2].astype(np.int32)}) np_label = np.random.randn(kRows).astype(input_type) pa_label = pd.DataFrame(np_label) @@ -41,8 +37,7 @@ def dmatrix_from_cudf(input_type, DMatrixT, missing=np.NAN): def _test_from_cudf(DMatrixT): - '''Test constructing DMatrix from cudf''' - import cudf + """Test constructing DMatrix from cudf""" dmatrix_from_cudf(np.float32, DMatrixT, np.NAN) dmatrix_from_cudf(np.float64, DMatrixT, np.NAN) @@ -50,37 +45,38 @@ def _test_from_cudf(DMatrixT): dmatrix_from_cudf(np.int32, DMatrixT, -2) dmatrix_from_cudf(np.int64, DMatrixT, -3) - cd = cudf.DataFrame({'x': [1, 2, 3], 'y': [0.1, 0.2, 0.3]}) + cd = cudf.DataFrame({"x": [1, 2, 3], "y": [0.1, 0.2, 0.3]}) dtrain = DMatrixT(cd) - assert dtrain.feature_names == ['x', 'y'] - assert dtrain.feature_types == ['int', 'float'] + assert dtrain.feature_names == ["x", "y"] + assert dtrain.feature_types == ["int", "float"] - series = cudf.DataFrame({'x': [1, 2, 3]}).iloc[:, 0] + series = cudf.DataFrame({"x": [1, 2, 3]}).iloc[:, 0] assert isinstance(series, cudf.Series) dtrain = DMatrixT(series) - assert dtrain.feature_names == ['x'] - assert dtrain.feature_types == ['int'] + assert dtrain.feature_names == ["x"] + assert dtrain.feature_types == ["int"] with pytest.raises(ValueError, match=r".*multi.*"): dtrain = DMatrixT(cd, label=cd) - xgb.train({"tree_method": "gpu_hist", "objective": "multi:softprob"}, dtrain) + xgb.train( + {"tree_method": "hist", "device": "cuda", "objective": "multi:softprob"}, + dtrain, + ) # Test when number of elements is less than 8 - X = cudf.DataFrame({'x': cudf.Series([0, 1, 2, np.NAN, 4], - dtype=np.int32)}) + X = cudf.DataFrame({"x": cudf.Series([0, 1, 2, np.NAN, 4], dtype=np.int32)}) dtrain = DMatrixT(X) assert dtrain.num_col() == 1 assert dtrain.num_row() == 5 # Boolean is not supported. - X_boolean = cudf.DataFrame({'x': cudf.Series([True, False])}) + X_boolean = cudf.DataFrame({"x": cudf.Series([True, False])}) with pytest.raises(Exception): dtrain = DMatrixT(X_boolean) - y_boolean = cudf.DataFrame({ - 'x': cudf.Series([True, False, True, True, True])}) + y_boolean = cudf.DataFrame({"x": cudf.Series([True, False, True, True, True])}) with pytest.raises(Exception): dtrain = DMatrixT(X_boolean, label=y_boolean) @@ -88,6 +84,7 @@ def _test_from_cudf(DMatrixT): def _test_cudf_training(DMatrixT): import pandas as pd from cudf import DataFrame as df + np.random.seed(1) X = pd.DataFrame(np.random.randn(50, 10)) y = pd.DataFrame(np.random.randn(50)) @@ -97,21 +94,33 @@ def _test_cudf_training(DMatrixT): cudf_base_margin = df.from_pandas(pd.DataFrame(base_margin)) evals_result_cudf = {} - dtrain_cudf = DMatrixT(df.from_pandas(X), df.from_pandas(y), weight=cudf_weights, - base_margin=cudf_base_margin) - params = {'gpu_id': 0, 'tree_method': 'gpu_hist'} - xgb.train(params, dtrain_cudf, evals=[(dtrain_cudf, "train")], - evals_result=evals_result_cudf) + dtrain_cudf = DMatrixT( + df.from_pandas(X), + df.from_pandas(y), + weight=cudf_weights, + base_margin=cudf_base_margin, + ) + params = {"device": "cuda", "tree_method": "hist"} + xgb.train( + params, + dtrain_cudf, + evals=[(dtrain_cudf, "train")], + evals_result=evals_result_cudf, + ) evals_result_np = {} dtrain_np = xgb.DMatrix(X, y, weight=weights, base_margin=base_margin) - xgb.train(params, dtrain_np, evals=[(dtrain_np, "train")], - evals_result=evals_result_np) - assert np.array_equal(evals_result_cudf["train"]["rmse"], evals_result_np["train"]["rmse"]) + xgb.train( + params, dtrain_np, evals=[(dtrain_np, "train")], evals_result=evals_result_np + ) + assert np.array_equal( + evals_result_cudf["train"]["rmse"], evals_result_np["train"]["rmse"] + ) def _test_cudf_metainfo(DMatrixT): import pandas as pd from cudf import DataFrame as df + n = 100 X = np.random.random((n, 2)) dmat_cudf = DMatrixT(df.from_pandas(pd.DataFrame(X))) @@ -120,39 +129,53 @@ def _test_cudf_metainfo(DMatrixT): uints = np.array([4, 2, 8]).astype("uint32") cudf_floats = df.from_pandas(pd.DataFrame(floats)) cudf_uints = df.from_pandas(pd.DataFrame(uints)) - dmat.set_float_info('weight', floats) - dmat.set_float_info('label', floats) - dmat.set_float_info('base_margin', floats) - dmat.set_uint_info('group', uints) + dmat.set_float_info("weight", floats) + dmat.set_float_info("label", floats) + dmat.set_float_info("base_margin", floats) + dmat.set_uint_info("group", uints) dmat_cudf.set_info(weight=cudf_floats) dmat_cudf.set_info(label=cudf_floats) dmat_cudf.set_info(base_margin=cudf_floats) dmat_cudf.set_info(group=cudf_uints) # Test setting info with cudf DataFrame - assert np.array_equal(dmat.get_float_info('weight'), dmat_cudf.get_float_info('weight')) - assert np.array_equal(dmat.get_float_info('label'), dmat_cudf.get_float_info('label')) - assert np.array_equal(dmat.get_float_info('base_margin'), - dmat_cudf.get_float_info('base_margin')) - assert np.array_equal(dmat.get_uint_info('group_ptr'), dmat_cudf.get_uint_info('group_ptr')) + assert np.array_equal( + dmat.get_float_info("weight"), dmat_cudf.get_float_info("weight") + ) + assert np.array_equal( + dmat.get_float_info("label"), dmat_cudf.get_float_info("label") + ) + assert np.array_equal( + dmat.get_float_info("base_margin"), dmat_cudf.get_float_info("base_margin") + ) + assert np.array_equal( + dmat.get_uint_info("group_ptr"), dmat_cudf.get_uint_info("group_ptr") + ) # Test setting info with cudf Series dmat_cudf.set_info(weight=cudf_floats[cudf_floats.columns[0]]) dmat_cudf.set_info(label=cudf_floats[cudf_floats.columns[0]]) dmat_cudf.set_info(base_margin=cudf_floats[cudf_floats.columns[0]]) dmat_cudf.set_info(group=cudf_uints[cudf_uints.columns[0]]) - assert np.array_equal(dmat.get_float_info('weight'), dmat_cudf.get_float_info('weight')) - assert np.array_equal(dmat.get_float_info('label'), dmat_cudf.get_float_info('label')) - assert np.array_equal(dmat.get_float_info('base_margin'), - dmat_cudf.get_float_info('base_margin')) - assert np.array_equal(dmat.get_uint_info('group_ptr'), dmat_cudf.get_uint_info('group_ptr')) + assert np.array_equal( + dmat.get_float_info("weight"), dmat_cudf.get_float_info("weight") + ) + assert np.array_equal( + dmat.get_float_info("label"), dmat_cudf.get_float_info("label") + ) + assert np.array_equal( + dmat.get_float_info("base_margin"), dmat_cudf.get_float_info("base_margin") + ) + assert np.array_equal( + dmat.get_uint_info("group_ptr"), dmat_cudf.get_uint_info("group_ptr") + ) - set_base_margin_info(df, DMatrixT, "gpu_hist") + run_base_margin_info(df, DMatrixT, "cuda") class TestFromColumnar: - '''Tests for constructing DMatrix from data structure conforming Apache -Arrow specification.''' + """Tests for constructing DMatrix from data structure conforming Apache + Arrow specification.""" @pytest.mark.skipif(**tm.no_cudf()) def test_simple_dmatrix_from_cudf(self): @@ -180,7 +203,6 @@ Arrow specification.''' @pytest.mark.skipif(**tm.no_cudf()) def test_cudf_categorical(self) -> None: - import cudf n_features = 30 _X, _y = tm.make_categorical(100, n_features, 17, False) X = cudf.from_pandas(_X) @@ -251,6 +273,7 @@ def test_cudf_training_with_sklearn(): import pandas as pd from cudf import DataFrame as df from cudf import Series as ss + np.random.seed(1) X = pd.DataFrame(np.random.randn(50, 10)) y = pd.DataFrame((np.random.randn(50) > 0).astype(np.int8)) @@ -264,29 +287,34 @@ def test_cudf_training_with_sklearn(): y_cudf_series = ss(data=y.iloc[:, 0]) for y_obj in [y_cudf, y_cudf_series]: - clf = xgb.XGBClassifier(gpu_id=0, tree_method='gpu_hist') - clf.fit(X_cudf, y_obj, sample_weight=cudf_weights, base_margin=cudf_base_margin, - eval_set=[(X_cudf, y_obj)]) + clf = xgb.XGBClassifier(tree_method="hist", device="cuda:0") + clf.fit( + X_cudf, + y_obj, + sample_weight=cudf_weights, + base_margin=cudf_base_margin, + eval_set=[(X_cudf, y_obj)], + ) pred = clf.predict(X_cudf) assert np.array_equal(np.unique(pred), np.array([0, 1])) class IterForDMatrixTest(xgb.core.DataIter): - '''A data iterator for XGBoost DMatrix. + """A data iterator for XGBoost DMatrix. `reset` and `next` are required for any data iterator, other functions here are utilites for demonstration's purpose. - ''' - ROWS_PER_BATCH = 100 # data is splited by rows + """ + + ROWS_PER_BATCH = 100 # data is splited by rows BATCHES = 16 def __init__(self, categorical): - '''Generate some random data for demostration. + """Generate some random data for demostration. Actual data can be anything that is currently supported by XGBoost. - ''' - import cudf + """ self.rows = self.ROWS_PER_BATCH if categorical: @@ -300,34 +328,37 @@ class IterForDMatrixTest(xgb.core.DataIter): rng = np.random.RandomState(1994) self._data = [ cudf.DataFrame( - {'a': rng.randn(self.ROWS_PER_BATCH), - 'b': rng.randn(self.ROWS_PER_BATCH)})] * self.BATCHES + { + "a": rng.randn(self.ROWS_PER_BATCH), + "b": rng.randn(self.ROWS_PER_BATCH), + } + ) + ] * self.BATCHES self._labels = [rng.randn(self.rows)] * self.BATCHES - self.it = 0 # set iterator to 0 + self.it = 0 # set iterator to 0 super().__init__(cache_prefix=None) def as_array(self): - import cudf return cudf.concat(self._data) def as_array_labels(self): return np.concatenate(self._labels) def data(self): - '''Utility function for obtaining current batch of data.''' + """Utility function for obtaining current batch of data.""" return self._data[self.it] def labels(self): - '''Utility function for obtaining current batch of label.''' + """Utility function for obtaining current batch of label.""" return self._labels[self.it] def reset(self): - '''Reset the iterator''' + """Reset the iterator""" self.it = 0 def next(self, input_data): - '''Yield next batch of data''' + """Yield next batch of data""" if self.it == len(self._data): # Return 0 when there's no more batch. return 0 @@ -341,7 +372,7 @@ class IterForDMatrixTest(xgb.core.DataIter): def test_from_cudf_iter(enable_categorical): rounds = 100 it = IterForDMatrixTest(enable_categorical) - params = {"tree_method": "gpu_hist"} + params = {"tree_method": "hist", "device": "cuda"} # Use iterator m_it = xgb.QuantileDMatrix(it, enable_categorical=enable_categorical) diff --git a/tests/python-gpu/test_from_cupy.py b/tests/python-gpu/test_from_cupy.py index b811ba090..79814a1bb 100644 --- a/tests/python-gpu/test_from_cupy.py +++ b/tests/python-gpu/test_from_cupy.py @@ -1,31 +1,25 @@ import json -import sys import numpy as np import pytest import xgboost as xgb - -sys.path.append("tests/python") -from test_dmatrix import set_base_margin_info - from xgboost import testing as tm +from xgboost.testing.data import run_base_margin_info -cupy = pytest.importorskip("cupy") +cp = pytest.importorskip("cupy") def test_array_interface() -> None: - arr = cupy.array([[1, 2, 3, 4], [1, 2, 3, 4]]) + arr = cp.array([[1, 2, 3, 4], [1, 2, 3, 4]]) i_arr = arr.__cuda_array_interface__ i_arr = json.loads(json.dumps(i_arr)) ret = xgb.core.from_array_interface(i_arr) - np.testing.assert_equal(cupy.asnumpy(arr), cupy.asnumpy(ret)) + np.testing.assert_equal(cp.asnumpy(arr), cp.asnumpy(ret)) def dmatrix_from_cupy(input_type, DMatrixT, missing=np.NAN): - '''Test constructing DMatrix from cupy''' - import cupy as cp - + """Test constructing DMatrix from cupy""" kRows = 80 kCols = 3 @@ -51,9 +45,7 @@ def dmatrix_from_cupy(input_type, DMatrixT, missing=np.NAN): def _test_from_cupy(DMatrixT): - '''Test constructing DMatrix from cupy''' - import cupy as cp - + """Test constructing DMatrix from cupy""" dmatrix_from_cupy(np.float16, DMatrixT, np.NAN) dmatrix_from_cupy(np.float32, DMatrixT, np.NAN) dmatrix_from_cupy(np.float64, DMatrixT, np.NAN) @@ -73,7 +65,6 @@ def _test_from_cupy(DMatrixT): def _test_cupy_training(DMatrixT): - import cupy as cp np.random.seed(1) cp.random.seed(1) X = cp.random.randn(50, 10, dtype="float32") @@ -85,19 +76,23 @@ def _test_cupy_training(DMatrixT): evals_result_cupy = {} dtrain_cp = DMatrixT(X, y, weight=cupy_weights, base_margin=cupy_base_margin) - params = {'gpu_id': 0, 'nthread': 1, 'tree_method': 'gpu_hist'} - xgb.train(params, dtrain_cp, evals=[(dtrain_cp, "train")], - evals_result=evals_result_cupy) + params = {"tree_method": "hist", "device": "cuda:0"} + xgb.train( + params, dtrain_cp, evals=[(dtrain_cp, "train")], evals_result=evals_result_cupy + ) evals_result_np = {} - dtrain_np = xgb.DMatrix(cp.asnumpy(X), cp.asnumpy(y), weight=weights, - base_margin=base_margin) - xgb.train(params, dtrain_np, evals=[(dtrain_np, "train")], - evals_result=evals_result_np) - assert np.array_equal(evals_result_cupy["train"]["rmse"], evals_result_np["train"]["rmse"]) + dtrain_np = xgb.DMatrix( + cp.asnumpy(X), cp.asnumpy(y), weight=weights, base_margin=base_margin + ) + xgb.train( + params, dtrain_np, evals=[(dtrain_np, "train")], evals_result=evals_result_np + ) + assert np.array_equal( + evals_result_cupy["train"]["rmse"], evals_result_np["train"]["rmse"] + ) def _test_cupy_metainfo(DMatrixT): - import cupy as cp n = 100 X = np.random.random((n, 2)) dmat_cupy = DMatrixT(cp.array(X)) @@ -106,33 +101,35 @@ def _test_cupy_metainfo(DMatrixT): uints = np.array([4, 2, 8]).astype("uint32") cupy_floats = cp.array(floats) cupy_uints = cp.array(uints) - dmat.set_float_info('weight', floats) - dmat.set_float_info('label', floats) - dmat.set_float_info('base_margin', floats) - dmat.set_uint_info('group', uints) + dmat.set_float_info("weight", floats) + dmat.set_float_info("label", floats) + dmat.set_float_info("base_margin", floats) + dmat.set_uint_info("group", uints) dmat_cupy.set_info(weight=cupy_floats) dmat_cupy.set_info(label=cupy_floats) dmat_cupy.set_info(base_margin=cupy_floats) dmat_cupy.set_info(group=cupy_uints) # Test setting info with cupy - assert np.array_equal(dmat.get_float_info('weight'), - dmat_cupy.get_float_info('weight')) - assert np.array_equal(dmat.get_float_info('label'), - dmat_cupy.get_float_info('label')) - assert np.array_equal(dmat.get_float_info('base_margin'), - dmat_cupy.get_float_info('base_margin')) - assert np.array_equal(dmat.get_uint_info('group_ptr'), - dmat_cupy.get_uint_info('group_ptr')) + assert np.array_equal( + dmat.get_float_info("weight"), dmat_cupy.get_float_info("weight") + ) + assert np.array_equal( + dmat.get_float_info("label"), dmat_cupy.get_float_info("label") + ) + assert np.array_equal( + dmat.get_float_info("base_margin"), dmat_cupy.get_float_info("base_margin") + ) + assert np.array_equal( + dmat.get_uint_info("group_ptr"), dmat_cupy.get_uint_info("group_ptr") + ) - set_base_margin_info(cp.asarray, DMatrixT, "gpu_hist") + run_base_margin_info(cp.asarray, DMatrixT, "cuda") @pytest.mark.skipif(**tm.no_cupy()) @pytest.mark.skipif(**tm.no_sklearn()) def test_cupy_training_with_sklearn(): - import cupy as cp - np.random.seed(1) cp.random.seed(1) X = cp.random.randn(50, 10, dtype="float32") @@ -142,7 +139,7 @@ def test_cupy_training_with_sklearn(): base_margin = np.random.random(50) cupy_base_margin = cp.array(base_margin) - clf = xgb.XGBClassifier(gpu_id=0, tree_method="gpu_hist") + clf = xgb.XGBClassifier(tree_method="hist", device="cuda:0") clf.fit( X, y, @@ -155,8 +152,8 @@ def test_cupy_training_with_sklearn(): class TestFromCupy: - '''Tests for constructing DMatrix from data structure conforming Apache -Arrow specification.''' + """Tests for constructing DMatrix from data structure conforming Apache + Arrow specification.""" @pytest.mark.skipif(**tm.no_cupy()) def test_simple_dmat_from_cupy(self): @@ -184,19 +181,17 @@ Arrow specification.''' @pytest.mark.skipif(**tm.no_cupy()) def test_dlpack_simple_dmat(self): - import cupy as cp n = 100 X = cp.random.random((n, 2)) xgb.DMatrix(X.toDlpack()) @pytest.mark.skipif(**tm.no_cupy()) def test_cupy_categorical(self): - import cupy as cp n_features = 10 X, y = tm.make_categorical(10, n_features, n_categories=4, onehot=False) X = cp.asarray(X.values.astype(cp.float32)) y = cp.array(y) - feature_types = ['c'] * n_features + feature_types = ["c"] * n_features assert isinstance(X, cp.ndarray) Xy = xgb.DMatrix(X, y, feature_types=feature_types) @@ -204,7 +199,6 @@ Arrow specification.''' @pytest.mark.skipif(**tm.no_cupy()) def test_dlpack_device_dmat(self): - import cupy as cp n = 100 X = cp.random.random((n, 2)) m = xgb.QuantileDMatrix(X.toDlpack()) @@ -213,7 +207,6 @@ Arrow specification.''' @pytest.mark.skipif(**tm.no_cupy()) def test_qid(self): - import cupy as cp rng = cp.random.RandomState(1994) rows = 100 cols = 10 @@ -223,19 +216,16 @@ Arrow specification.''' Xy = xgb.DMatrix(X, y) Xy.set_info(qid=qid) - group_ptr = Xy.get_uint_info('group_ptr') + group_ptr = Xy.get_uint_info("group_ptr") assert group_ptr[0] == 0 assert group_ptr[-1] == rows @pytest.mark.skipif(**tm.no_cupy()) @pytest.mark.mgpu def test_specified_device(self): - import cupy as cp cp.cuda.runtime.setDevice(0) dtrain = dmatrix_from_cupy(np.float32, xgb.QuantileDMatrix, np.nan) - with pytest.raises( - xgb.core.XGBoostError, match="Invalid device ordinal" - ): + with pytest.raises(xgb.core.XGBoostError, match="Invalid device ordinal"): xgb.train( - {'tree_method': 'gpu_hist', 'gpu_id': 1}, dtrain, num_boost_round=10 + {"tree_method": "hist", "device": "cuda:1"}, dtrain, num_boost_round=10 ) diff --git a/tests/python-gpu/test_gpu_basic_models.py b/tests/python-gpu/test_gpu_basic_models.py index e97ca210e..8b74c7971 100644 --- a/tests/python-gpu/test_gpu_basic_models.py +++ b/tests/python-gpu/test_gpu_basic_models.py @@ -21,21 +21,21 @@ class TestGPUBasicModels: cpu_test_bm = test_bm.TestModels() def run_cls(self, X, y): - cls = xgb.XGBClassifier(tree_method='gpu_hist') + cls = xgb.XGBClassifier(tree_method="hist", device="cuda") cls.fit(X, y) - cls.get_booster().save_model('test_deterministic_gpu_hist-0.json') + cls.get_booster().save_model("test_deterministic_gpu_hist-0.json") - cls = xgb.XGBClassifier(tree_method='gpu_hist') + cls = xgb.XGBClassifier(tree_method="hist", device="cuda") cls.fit(X, y) - cls.get_booster().save_model('test_deterministic_gpu_hist-1.json') + cls.get_booster().save_model("test_deterministic_gpu_hist-1.json") - with open('test_deterministic_gpu_hist-0.json', 'r') as fd: + with open("test_deterministic_gpu_hist-0.json", "r") as fd: model_0 = fd.read() - with open('test_deterministic_gpu_hist-1.json', 'r') as fd: + with open("test_deterministic_gpu_hist-1.json", "r") as fd: model_1 = fd.read() - os.remove('test_deterministic_gpu_hist-0.json') - os.remove('test_deterministic_gpu_hist-1.json') + os.remove("test_deterministic_gpu_hist-0.json") + os.remove("test_deterministic_gpu_hist-1.json") return hash(model_0), hash(model_1) @@ -43,7 +43,7 @@ class TestGPUBasicModels: self.cpu_test_bm.run_custom_objective("gpu_hist") def test_eta_decay(self): - self.cpu_test_cb.run_eta_decay('gpu_hist') + self.cpu_test_cb.run_eta_decay("gpu_hist") @pytest.mark.parametrize( "objective", ["binary:logistic", "reg:absoluteerror", "reg:quantileerror"] diff --git a/tests/python-gpu/test_gpu_demos.py b/tests/python-gpu/test_gpu_demos.py index 2c4260fd1..d3b6089a3 100644 --- a/tests/python-gpu/test_gpu_demos.py +++ b/tests/python-gpu/test_gpu_demos.py @@ -12,18 +12,18 @@ import test_demos as td # noqa @pytest.mark.skipif(**tm.no_cupy()) def test_data_iterator(): - script = os.path.join(td.PYTHON_DEMO_DIR, 'quantile_data_iterator.py') - cmd = ['python', script] + script = os.path.join(td.PYTHON_DEMO_DIR, "quantile_data_iterator.py") + cmd = ["python", script] subprocess.check_call(cmd) def test_update_process_demo(): - script = os.path.join(td.PYTHON_DEMO_DIR, 'update_process.py') - cmd = ['python', script] + script = os.path.join(td.PYTHON_DEMO_DIR, "update_process.py") + cmd = ["python", script] subprocess.check_call(cmd) def test_categorical_demo(): - script = os.path.join(td.PYTHON_DEMO_DIR, 'categorical.py') - cmd = ['python', script] + script = os.path.join(td.PYTHON_DEMO_DIR, "categorical.py") + cmd = ["python", script] subprocess.check_call(cmd) diff --git a/tests/python-gpu/test_gpu_linear.py b/tests/python-gpu/test_gpu_linear.py index 04f9be256..174277c7e 100644 --- a/tests/python-gpu/test_gpu_linear.py +++ b/tests/python-gpu/test_gpu_linear.py @@ -6,22 +6,29 @@ from xgboost import testing as tm pytestmark = tm.timeout(10) -parameter_strategy = strategies.fixed_dictionaries({ - 'booster': strategies.just('gblinear'), - 'eta': strategies.floats(0.01, 0.25), - 'tolerance': strategies.floats(1e-5, 1e-2), - 'nthread': strategies.integers(1, 4), - 'feature_selector': strategies.sampled_from(['cyclic', 'shuffle', - 'greedy', 'thrifty']), - 'top_k': strategies.integers(1, 10), -}) +parameter_strategy = strategies.fixed_dictionaries( + { + "booster": strategies.just("gblinear"), + "eta": strategies.floats(0.01, 0.25), + "tolerance": strategies.floats(1e-5, 1e-2), + "nthread": strategies.integers(1, 4), + "feature_selector": strategies.sampled_from( + ["cyclic", "shuffle", "greedy", "thrifty"] + ), + "top_k": strategies.integers(1, 10), + } +) def train_result(param, dmat, num_rounds): result = {} booster = xgb.train( - param, dmat, num_rounds, [(dmat, 'train')], verbose_eval=False, - evals_result=result + param, + dmat, + num_rounds, + [(dmat, "train")], + verbose_eval=False, + evals_result=result, ) assert booster.num_boosted_rounds() == num_rounds return result @@ -32,9 +39,11 @@ class TestGPULinear: @settings(deadline=None, max_examples=20, print_blob=True) def test_gpu_coordinate(self, param, num_rounds, dataset): assume(len(dataset.y) > 0) - param['updater'] = 'gpu_coord_descent' + param["updater"] = "gpu_coord_descent" param = dataset.set_params(param) - result = train_result(param, dataset.get_dmat(), num_rounds)['train'][dataset.metric] + result = train_result(param, dataset.get_dmat(), num_rounds)["train"][ + dataset.metric + ] note(result) assert tm.non_increasing(result) @@ -46,16 +55,18 @@ class TestGPULinear: strategies.integers(10, 50), tm.make_dataset_strategy(), strategies.floats(1e-5, 0.8), - strategies.floats(1e-5, 0.8) + strategies.floats(1e-5, 0.8), ) @settings(deadline=None, max_examples=20, print_blob=True) def test_gpu_coordinate_regularised(self, param, num_rounds, dataset, alpha, lambd): assume(len(dataset.y) > 0) - param['updater'] = 'gpu_coord_descent' - param['alpha'] = alpha - param['lambda'] = lambd + param["updater"] = "gpu_coord_descent" + param["alpha"] = alpha + param["lambda"] = lambd param = dataset.set_params(param) - result = train_result(param, dataset.get_dmat(), num_rounds)['train'][dataset.metric] + result = train_result(param, dataset.get_dmat(), num_rounds)["train"][ + dataset.metric + ] note(result) assert tm.non_increasing([result[0], result[-1]]) @@ -64,8 +75,12 @@ class TestGPULinear: # Training linear model is quite expensive, so we don't include it in # test_from_cupy.py import cupy - params = {'booster': 'gblinear', 'updater': 'gpu_coord_descent', - 'n_estimators': 100} + + params = { + "booster": "gblinear", + "updater": "gpu_coord_descent", + "n_estimators": 100, + } X, y = tm.get_california_housing() cpu_model = xgb.XGBRegressor(**params) cpu_model.fit(X, y) diff --git a/tests/python-gpu/test_gpu_training_continuation.py b/tests/python-gpu/test_gpu_training_continuation.py index 6a908af27..a67d2f26b 100644 --- a/tests/python-gpu/test_gpu_training_continuation.py +++ b/tests/python-gpu/test_gpu_training_continuation.py @@ -14,14 +14,18 @@ class TestGPUTrainingContinuation: X = np.random.randn(kRows, kCols) y = np.random.randn(kRows) dtrain = xgb.DMatrix(X, y) - params = {'tree_method': 'gpu_hist', 'max_depth': '2', - 'gamma': '0.1', 'alpha': '0.01'} + params = { + "tree_method": "gpu_hist", + "max_depth": "2", + "gamma": "0.1", + "alpha": "0.01", + } bst_0 = xgb.train(params, dtrain, num_boost_round=64) - dump_0 = bst_0.get_dump(dump_format='json') + dump_0 = bst_0.get_dump(dump_format="json") bst_1 = xgb.train(params, dtrain, num_boost_round=32) bst_1 = xgb.train(params, dtrain, num_boost_round=32, xgb_model=bst_1) - dump_1 = bst_1.get_dump(dump_format='json') + dump_1 = bst_1.get_dump(dump_format="json") def recursive_compare(obj_0, obj_1): if isinstance(obj_0, float): @@ -37,9 +41,8 @@ class TestGPUTrainingContinuation: values_1 = list(obj_1.values()) for i in range(len(obj_0.items())): assert keys_0[i] == keys_1[i] - if list(obj_0.keys())[i] != 'missing': - recursive_compare(values_0[i], - values_1[i]) + if list(obj_0.keys())[i] != "missing": + recursive_compare(values_0[i], values_1[i]) else: for i in range(len(obj_0)): recursive_compare(obj_0[i], obj_1[i]) diff --git a/tests/python-gpu/test_monotonic_constraints.py b/tests/python-gpu/test_monotonic_constraints.py index 3bf4f0deb..4586b617a 100644 --- a/tests/python-gpu/test_monotonic_constraints.py +++ b/tests/python-gpu/test_monotonic_constraints.py @@ -22,12 +22,13 @@ def non_increasing(L): def assert_constraint(constraint, tree_method): from sklearn.datasets import make_regression + n = 1000 X, y = make_regression(n, random_state=rng, n_features=1, n_informative=1) dtrain = xgb.DMatrix(X, y) param = {} - param['tree_method'] = tree_method - param['monotone_constraints'] = "(" + str(constraint) + ")" + param["tree_method"] = tree_method + param["monotone_constraints"] = "(" + str(constraint) + ")" bst = xgb.train(param, dtrain) dpredict = xgb.DMatrix(X[X[:, 0].argsort()]) pred = bst.predict(dpredict) @@ -40,15 +41,15 @@ def assert_constraint(constraint, tree_method): @pytest.mark.skipif(**tm.no_sklearn()) def test_gpu_hist_basic(): - assert_constraint(1, 'gpu_hist') - assert_constraint(-1, 'gpu_hist') + assert_constraint(1, "gpu_hist") + assert_constraint(-1, "gpu_hist") def test_gpu_hist_depthwise(): params = { - 'tree_method': 'gpu_hist', - 'grow_policy': 'depthwise', - 'monotone_constraints': '(1, -1)' + "tree_method": "gpu_hist", + "grow_policy": "depthwise", + "monotone_constraints": "(1, -1)", } model = xgb.train(params, tmc.training_dset) tmc.is_correctly_constrained(model) @@ -56,9 +57,9 @@ def test_gpu_hist_depthwise(): def test_gpu_hist_lossguide(): params = { - 'tree_method': 'gpu_hist', - 'grow_policy': 'lossguide', - 'monotone_constraints': '(1, -1)' + "tree_method": "gpu_hist", + "grow_policy": "lossguide", + "monotone_constraints": "(1, -1)", } model = xgb.train(params, tmc.training_dset) tmc.is_correctly_constrained(model) diff --git a/tests/python/test_dmatrix.py b/tests/python/test_dmatrix.py index 2e8a1a2a6..c718378c5 100644 --- a/tests/python/test_dmatrix.py +++ b/tests/python/test_dmatrix.py @@ -1,6 +1,5 @@ import csv import os -import sys import tempfile import numpy as np @@ -12,59 +11,12 @@ from scipy.sparse import csr_matrix, rand import xgboost as xgb from xgboost import testing as tm from xgboost.core import DataSplitMode -from xgboost.testing.data import np_dtypes - -rng = np.random.RandomState(1) +from xgboost.testing.data import np_dtypes, run_base_margin_info dpath = "demo/data/" rng = np.random.RandomState(1994) -def set_base_margin_info(DType, DMatrixT, tm: str): - rng = np.random.default_rng() - X = DType(rng.normal(0, 1.0, size=100).astype(np.float32).reshape(50, 2)) - if hasattr(X, "iloc"): - y = X.iloc[:, 0] - else: - y = X[:, 0] - base_margin = X - # no error at set - Xy = DMatrixT(X, y, base_margin=base_margin) - # Error at train, caused by check in predictor. - with pytest.raises(ValueError, match=r".*base_margin.*"): - xgb.train({"tree_method": tm}, Xy) - - if not hasattr(X, "iloc"): - # column major matrix - got = DType(Xy.get_base_margin().reshape(50, 2)) - assert (got == base_margin).all() - - assert base_margin.T.flags.c_contiguous is False - assert base_margin.T.flags.f_contiguous is True - Xy.set_info(base_margin=base_margin.T) - got = DType(Xy.get_base_margin().reshape(2, 50)) - assert (got == base_margin.T).all() - - # Row vs col vec. - base_margin = y - Xy.set_base_margin(base_margin) - bm_col = Xy.get_base_margin() - Xy.set_base_margin(base_margin.reshape(1, base_margin.size)) - bm_row = Xy.get_base_margin() - assert (bm_row == bm_col).all() - - # type - base_margin = base_margin.astype(np.float64) - Xy.set_base_margin(base_margin) - bm_f64 = Xy.get_base_margin() - assert (bm_f64 == bm_col).all() - - # too many dimensions - base_margin = X.reshape(2, 5, 2, 5) - with pytest.raises(ValueError, match=r".*base_margin.*"): - Xy.set_base_margin(base_margin) - - class TestDMatrix: def test_warn_missing(self): from xgboost import data @@ -417,8 +369,8 @@ class TestDMatrix: ) np.testing.assert_equal(np.array(Xy.feature_types), np.array(feature_types)) - def test_base_margin(self): - set_base_margin_info(np.asarray, xgb.DMatrix, "hist") + def test_base_margin(self) -> None: + run_base_margin_info(np.asarray, xgb.DMatrix, "cpu") @given( strategies.integers(0, 1000), diff --git a/tests/python/test_with_modin.py b/tests/python/test_with_modin.py index 75dae0654..ce0dbd609 100644 --- a/tests/python/test_with_modin.py +++ b/tests/python/test_with_modin.py @@ -1,9 +1,9 @@ import numpy as np import pytest -from test_dmatrix import set_base_margin_info import xgboost as xgb from xgboost import testing as tm +from xgboost.testing.data import run_base_margin_info try: import modin.pandas as md @@ -145,4 +145,4 @@ class TestModin: np.testing.assert_array_equal(data.get_weight(), w) def test_base_margin(self): - set_base_margin_info(md.DataFrame, xgb.DMatrix, "hist") + run_base_margin_info(md.DataFrame, xgb.DMatrix, "cpu") diff --git a/tests/python/test_with_pandas.py b/tests/python/test_with_pandas.py index c7b99a991..4dd0c640d 100644 --- a/tests/python/test_with_pandas.py +++ b/tests/python/test_with_pandas.py @@ -1,14 +1,12 @@ -import sys from typing import Type import numpy as np import pytest -from test_dmatrix import set_base_margin_info import xgboost as xgb from xgboost import testing as tm from xgboost.core import DataSplitMode -from xgboost.testing.data import pd_arrow_dtypes, pd_dtypes +from xgboost.testing.data import pd_arrow_dtypes, pd_dtypes, run_base_margin_info try: import pandas as pd @@ -336,7 +334,7 @@ class TestPandas: np.testing.assert_array_equal(data.get_weight(), w) def test_base_margin(self): - set_base_margin_info(pd.DataFrame, xgb.DMatrix, "hist") + run_base_margin_info(pd.DataFrame, xgb.DMatrix, "cpu") def test_cv_as_pandas(self): dm, _ = tm.load_agaricus(__file__) From 26a5436a652790524b508268efa79fc2a7127e02 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 4 Jan 2024 14:52:19 +0800 Subject: [PATCH 43/59] [doc] Describe feature info behavior. [skip ci] (#9866) --- doc/contrib/consistency.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/contrib/consistency.rst b/doc/contrib/consistency.rst index a268820eb..b74234602 100644 --- a/doc/contrib/consistency.rst +++ b/doc/contrib/consistency.rst @@ -59,4 +59,13 @@ XGBoost has a default logger builtin that can be a wrapper over binding-specific Minimum Amount of Data Manipulation *********************************** -XGBoost is mostly a machine learning library providing boosting algorithm implementation. Some other implementations might perform some sort of data manipulation implicitly like deciding the coding of the data, and transforming the data according to some heuristic before training. We prefer to keep these operations based on necessities instead of convenience to keep the scope of the project well-defined. Whenever possible, we should leave these features to 3-party libraries and consider how a user can compose their pipeline. For instance, XGBoost itself should not perform ordinal encoding for categorical data, users will pick an encoder that fits their use cases (like out-of-core implementation, distributed implementation, known mapping, etc). If some transformations are decided to be part of the algorithm, we can have it inside the core instead of the language binding. Examples would be target-encoding or sketching the response variables. If we were to support them, we could have it inside the core implementation as part of the ML algorithm. This aligns with the same principles of default parameters, various bindings should provide similar (if not the same) results given the same set of parameters and data. \ No newline at end of file +XGBoost is mostly a machine learning library providing boosting algorithm implementation. Some other implementations might perform some sort of data manipulation implicitly like deciding the coding of the data, and transforming the data according to some heuristic before training. We prefer to keep these operations based on necessities instead of convenience to keep the scope of the project well-defined. Whenever possible, we should leave these features to 3-party libraries and consider how a user can compose their pipeline. For instance, XGBoost itself should not perform ordinal encoding for categorical data, users will pick an encoder that fits their use cases (like out-of-core implementation, distributed implementation, known mapping, etc). If some transformations are decided to be part of the algorithm, we can have it inside the core instead of the language binding. Examples would be target-encoding or sketching the response variables. If we were to support them, we could have it inside the core implementation as part of the ML algorithm. This aligns with the same principles of default parameters, various bindings should provide similar (if not the same) results given the same set of parameters and data. + +************ +Feature Info +************ + +XGBoost accepts data structures that contain meta info about predictors, including the names and types of features. Example inputs are :py:class:`pandas.DataFrame`, R `data.frame`. We have the following heuristics: +- When the input data structure contains such information, we set the `feature_names` and `feature_types` for `DMatrix` accordingly. +- When a user provides this information as explicit parameters, the user-provided version should override the one provided by the data structure. +- When both sources are missing, the `DMatrix` class contain empty info. \ No newline at end of file From 5f7b5a69213bbf82238832a277f1a11046b0d57f Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 4 Jan 2024 14:52:48 +0800 Subject: [PATCH 44/59] Add tests for pickling with custom obj and metric. (#9943) --- doc/tutorials/custom_metric_obj.rst | 1 - doc/tutorials/saving_model.rst | 2 ++ python-package/xgboost/sklearn.py | 27 ++++++++++++-------- python-package/xgboost/testing/__init__.py | 7 ++++++ tests/python/test_pickling.py | 27 ++++++++++++++++++++ tests/python/test_with_sklearn.py | 29 ++++++++++------------ 6 files changed, 65 insertions(+), 28 deletions(-) diff --git a/doc/tutorials/custom_metric_obj.rst b/doc/tutorials/custom_metric_obj.rst index 76ee1b3de..118a099c1 100644 --- a/doc/tutorials/custom_metric_obj.rst +++ b/doc/tutorials/custom_metric_obj.rst @@ -279,7 +279,6 @@ available at :ref:`sphx_glr_python_examples_custom_softmax.py`. Also, see Scikit-Learn Interface ********************** - The scikit-learn interface of XGBoost has some utilities to improve the integration with standard scikit-learn functions. For instance, after XGBoost 1.6.0 users can use the cost function (not scoring functions) from scikit-learn out of the box: diff --git a/doc/tutorials/saving_model.rst b/doc/tutorials/saving_model.rst index 54c217249..34b5430df 100644 --- a/doc/tutorials/saving_model.rst +++ b/doc/tutorials/saving_model.rst @@ -101,6 +101,8 @@ snapshot generated by an earlier version of XGBoost may result in errors or unde **If a model is persisted with** ``pickle.dump`` (Python) or ``saveRDS`` (R), **then the model may not be accessible in later versions of XGBoost.** +.. _custom-obj-metric: + *************************** Custom objective and metric *************************** diff --git a/python-package/xgboost/sklearn.py b/python-package/xgboost/sklearn.py index ea8d8d041..3383ae0b7 100644 --- a/python-package/xgboost/sklearn.py +++ b/python-package/xgboost/sklearn.py @@ -192,11 +192,16 @@ __model_doc = f""" Boosting learning rate (xgb's "eta") verbosity : Optional[int] The degree of verbosity. Valid values are 0 (silent) - 3 (debug). + objective : {SklObjective} - Specify the learning task and the corresponding learning objective or - a custom objective function to be used (see note below). + + Specify the learning task and the corresponding learning objective or a custom + objective function to be used. For custom objective, see + :doc:`/tutorials/custom_metric_obj` and :ref:`custom-obj-metric` for more + information. + booster: Optional[str] - Specify which booster to use: gbtree, gblinear or dart. + Specify which booster to use: `gbtree`, `gblinear` or `dart`. tree_method: Optional[str] Specify which tree method to use. Default to auto. If this parameter is set to default, XGBoost will choose the most conservative option available. It's @@ -328,21 +333,21 @@ __model_doc = f""" Metric used for monitoring the training result and early stopping. It can be a string or list of strings as names of predefined metric in XGBoost (See - doc/parameter.rst), one of the metrics in :py:mod:`sklearn.metrics`, or any other - user defined metric that looks like `sklearn.metrics`. + doc/parameter.rst), one of the metrics in :py:mod:`sklearn.metrics`, or any + other user defined metric that looks like `sklearn.metrics`. If custom objective is also provided, then custom metric should implement the corresponding reverse link function. Unlike the `scoring` parameter commonly used in scikit-learn, when a callable - object is provided, it's assumed to be a cost function and by default XGBoost will - minimize the result during early stopping. + object is provided, it's assumed to be a cost function and by default XGBoost + will minimize the result during early stopping. - For advanced usage on Early stopping like directly choosing to maximize instead of - minimize, see :py:obj:`xgboost.callback.EarlyStopping`. + For advanced usage on Early stopping like directly choosing to maximize instead + of minimize, see :py:obj:`xgboost.callback.EarlyStopping`. - See :doc:`Custom Objective and Evaluation Metric ` - for more. + See :doc:`/tutorials/custom_metric_obj` and :ref:`custom-obj-metric` for more + information. .. note:: diff --git a/python-package/xgboost/testing/__init__.py b/python-package/xgboost/testing/__init__.py index 6b8daf561..373ad1c58 100644 --- a/python-package/xgboost/testing/__init__.py +++ b/python-package/xgboost/testing/__init__.py @@ -815,6 +815,13 @@ def softprob_obj( return objective +def ls_obj(y_true: np.ndarray, y_pred: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Least squared error.""" + grad = y_pred - y_true + hess = np.ones(len(y_true)) + return grad, hess + + class DirectoryExcursion: """Change directory. Change back and optionally cleaning up the directory when exit. diff --git a/tests/python/test_pickling.py b/tests/python/test_pickling.py index 2f4d77bf0..083a2a7fd 100644 --- a/tests/python/test_pickling.py +++ b/tests/python/test_pickling.py @@ -1,10 +1,13 @@ import json import os import pickle +import tempfile import numpy as np +import pytest import xgboost as xgb +from xgboost import testing as tm kRows = 100 kCols = 10 @@ -61,3 +64,27 @@ class TestPickling: params = {"nthread": 8, "tree_method": "exact", "subsample": 0.5} config = self.run_model_pickling(params) check(config) + + @pytest.mark.skipif(**tm.no_sklearn()) + def test_with_sklearn_obj_metric(self) -> None: + from sklearn.metrics import mean_squared_error + + X, y = tm.datasets.make_regression() + reg = xgb.XGBRegressor(objective=tm.ls_obj, eval_metric=mean_squared_error) + reg.fit(X, y) + + pkl = pickle.dumps(reg) + reg_1 = pickle.loads(pkl) + assert callable(reg_1.objective) + assert callable(reg_1.eval_metric) + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "model.json") + reg.save_model(path) + + reg_2 = xgb.XGBRegressor() + reg_2.load_model(path) + + assert not callable(reg_2.objective) + assert not callable(reg_2.eval_metric) + assert reg_2.eval_metric is None diff --git a/tests/python/test_with_sklearn.py b/tests/python/test_with_sklearn.py index 1e49ed053..ee0085d51 100644 --- a/tests/python/test_with_sklearn.py +++ b/tests/python/test_with_sklearn.py @@ -504,15 +504,10 @@ def test_regression_with_custom_objective(): from sklearn.metrics import mean_squared_error from sklearn.model_selection import KFold - def objective_ls(y_true, y_pred): - grad = (y_pred - y_true) - hess = np.ones(len(y_true)) - return grad, hess - X, y = fetch_california_housing(return_X_y=True) kf = KFold(n_splits=2, shuffle=True, random_state=rng) for train_index, test_index in kf.split(X, y): - xgb_model = xgb.XGBRegressor(objective=objective_ls).fit( + xgb_model = xgb.XGBRegressor(objective=tm.ls_obj).fit( X[train_index], y[train_index] ) preds = xgb_model.predict(X[test_index]) @@ -530,27 +525,29 @@ def test_regression_with_custom_objective(): np.testing.assert_raises(XGBCustomObjectiveException, xgb_model.fit, X, y) +def logregobj(y_true, y_pred): + y_pred = 1.0 / (1.0 + np.exp(-y_pred)) + grad = y_pred - y_true + hess = y_pred * (1.0 - y_pred) + return grad, hess + + def test_classification_with_custom_objective(): from sklearn.datasets import load_digits from sklearn.model_selection import KFold - def logregobj(y_true, y_pred): - y_pred = 1.0 / (1.0 + np.exp(-y_pred)) - grad = y_pred - y_true - hess = y_pred * (1.0 - y_pred) - return grad, hess - digits = load_digits(n_class=2) - y = digits['target'] - X = digits['data'] + y = digits["target"] + X = digits["data"] kf = KFold(n_splits=2, shuffle=True, random_state=rng) for train_index, test_index in kf.split(X, y): xgb_model = xgb.XGBClassifier(objective=logregobj) xgb_model.fit(X[train_index], y[train_index]) preds = xgb_model.predict(X[test_index]) labels = y[test_index] - err = sum(1 for i in range(len(preds)) - if int(preds[i] > 0.5) != labels[i]) / float(len(preds)) + err = sum( + 1 for i in range(len(preds)) if int(preds[i] > 0.5) != labels[i] + ) / float(len(preds)) assert err < 0.1 # Test that the custom objective function is actually used From 621348abb367cb012879786a9c6c60949dc40bd9 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 4 Jan 2024 16:41:13 +0800 Subject: [PATCH 45/59] Fix multi-output with alternating strategies. (#9933) --------- Co-authored-by: Philip Hyunsu Cho --- python-package/xgboost/testing/updater.py | 11 +++ src/tree/updater_quantile_hist.cc | 8 +- tests/ci_build/lint_python.py | 2 + tests/python/test_basic_models.py | 6 +- tests/python/test_multi_target.py | 105 ++++++++++++++++++++++ tests/python/test_updaters.py | 64 ------------- 6 files changed, 123 insertions(+), 73 deletions(-) create mode 100644 tests/python/test_multi_target.py diff --git a/python-package/xgboost/testing/updater.py b/python-package/xgboost/testing/updater.py index af5acf428..00c982bd0 100644 --- a/python-package/xgboost/testing/updater.py +++ b/python-package/xgboost/testing/updater.py @@ -394,3 +394,14 @@ def train_result( assert booster.feature_types == dmat.feature_types return result + + +class ResetStrategy(xgb.callback.TrainingCallback): + """Callback for testing multi-output.""" + + def after_iteration(self, model: xgb.Booster, epoch: int, evals_log: dict) -> bool: + if epoch % 2 == 0: + model.set_param({"multi_strategy": "multi_output_tree"}) + else: + model.set_param({"multi_strategy": "one_output_per_tree"}) + return False diff --git a/src/tree/updater_quantile_hist.cc b/src/tree/updater_quantile_hist.cc index 2bb5b0b49..7731f505e 100644 --- a/src/tree/updater_quantile_hist.cc +++ b/src/tree/updater_quantile_hist.cc @@ -545,12 +545,12 @@ class QuantileHistMaker : public TreeUpdater { } bool UpdatePredictionCache(const DMatrix *data, linalg::MatrixView out_preds) override { - if (p_impl_) { - return p_impl_->UpdatePredictionCache(data, out_preds); - } else if (p_mtimpl_) { + if (out_preds.Shape(1) > 1) { + CHECK(p_mtimpl_); return p_mtimpl_->UpdatePredictionCache(data, out_preds); } else { - return false; + CHECK(p_impl_); + return p_impl_->UpdatePredictionCache(data, out_preds); } } diff --git a/tests/ci_build/lint_python.py b/tests/ci_build/lint_python.py index c9170b100..ed33a96e5 100644 --- a/tests/ci_build/lint_python.py +++ b/tests/ci_build/lint_python.py @@ -22,6 +22,7 @@ class LintersPaths: "tests/python/test_dmatrix.py", "tests/python/test_dt.py", "tests/python/test_demos.py", + "tests/python/test_multi_target.py", "tests/python/test_predict.py", "tests/python/test_quantile_dmatrix.py", "tests/python/test_tree_regularization.py", @@ -79,6 +80,7 @@ class LintersPaths: "tests/python/test_dt.py", "tests/python/test_demos.py", "tests/python/test_data_iterator.py", + "tests/python/test_multi_target.py", "tests/python-gpu/test_gpu_data_iterator.py", "tests/python-gpu/load_pickle.py", "tests/test_distributed/test_with_spark/test_data.py", diff --git a/tests/python/test_basic_models.py b/tests/python/test_basic_models.py index 45bef1f25..8f83e1fcc 100644 --- a/tests/python/test_basic_models.py +++ b/tests/python/test_basic_models.py @@ -8,6 +8,7 @@ import pytest import xgboost as xgb from xgboost import testing as tm +from xgboost.testing.updater import ResetStrategy dpath = tm.data_dir(__file__) @@ -653,11 +654,6 @@ class TestModels: num_parallel_tree = 4 num_boost_round = 16 - class ResetStrategy(xgb.callback.TrainingCallback): - def after_iteration(self, model, epoch: int, evals_log) -> bool: - model.set_param({"multi_strategy": "multi_output_tree"}) - return False - booster = xgb.train( { "num_parallel_tree": num_parallel_tree, diff --git a/tests/python/test_multi_target.py b/tests/python/test_multi_target.py new file mode 100644 index 000000000..446d44062 --- /dev/null +++ b/tests/python/test_multi_target.py @@ -0,0 +1,105 @@ +from typing import Any, Dict + +from hypothesis import given, note, settings, strategies + +import xgboost as xgb +from xgboost import testing as tm +from xgboost.testing.params import ( + exact_parameter_strategy, + hist_cache_strategy, + hist_multi_parameter_strategy, + hist_parameter_strategy, +) +from xgboost.testing.updater import ResetStrategy, train_result + + +class TestTreeMethodMulti: + @given( + exact_parameter_strategy, strategies.integers(1, 20), tm.multi_dataset_strategy + ) + @settings(deadline=None, print_blob=True) + def test_exact(self, param: dict, num_rounds: int, dataset: tm.TestDataset) -> None: + if dataset.name.endswith("-l1"): + return + param["tree_method"] = "exact" + param = dataset.set_params(param) + result = train_result(param, dataset.get_dmat(), num_rounds) + assert tm.non_increasing(result["train"][dataset.metric]) + + @given( + exact_parameter_strategy, + hist_parameter_strategy, + hist_cache_strategy, + strategies.integers(1, 20), + tm.multi_dataset_strategy, + ) + @settings(deadline=None, print_blob=True) + def test_approx( + self, + param: Dict[str, Any], + hist_param: Dict[str, Any], + cache_param: Dict[str, Any], + num_rounds: int, + dataset: tm.TestDataset, + ) -> None: + param["tree_method"] = "approx" + param = dataset.set_params(param) + param.update(hist_param) + param.update(cache_param) + result = train_result(param, dataset.get_dmat(), num_rounds) + note(str(result)) + assert tm.non_increasing(result["train"][dataset.metric]) + + @given( + exact_parameter_strategy, + hist_multi_parameter_strategy, + hist_cache_strategy, + strategies.integers(1, 20), + tm.multi_dataset_strategy, + ) + @settings(deadline=None, print_blob=True) + def test_hist( + self, + param: Dict[str, Any], + hist_param: Dict[str, Any], + cache_param: Dict[str, Any], + num_rounds: int, + dataset: tm.TestDataset, + ) -> None: + if dataset.name.endswith("-l1"): + return + param["tree_method"] = "hist" + param = dataset.set_params(param) + param.update(hist_param) + param.update(cache_param) + result = train_result(param, dataset.get_dmat(), num_rounds) + note(str(result)) + assert tm.non_increasing(result["train"][dataset.metric]) + + +def test_multiclass() -> None: + X, y = tm.datasets.make_classification( + 128, n_features=12, n_informative=10, n_classes=4 + ) + clf = xgb.XGBClassifier( + multi_strategy="multi_output_tree", callbacks=[ResetStrategy()], n_estimators=10 + ) + clf.fit(X, y, eval_set=[(X, y)]) + assert clf.objective == "multi:softprob" + assert tm.non_increasing(clf.evals_result()["validation_0"]["mlogloss"]) + + proba = clf.predict_proba(X) + assert proba.shape == (y.shape[0], 4) + + +def test_multilabel() -> None: + X, y = tm.datasets.make_multilabel_classification(128) + clf = xgb.XGBClassifier( + multi_strategy="multi_output_tree", callbacks=[ResetStrategy()], n_estimators=10 + ) + clf.fit(X, y, eval_set=[(X, y)]) + assert clf.objective == "binary:logistic" + assert tm.non_increasing(clf.evals_result()["validation_0"]["logloss"]) + + proba = clf.predict_proba(X) + assert proba.shape == y.shape diff --git a/tests/python/test_updaters.py b/tests/python/test_updaters.py index c4c0de032..9a0cdecd2 100644 --- a/tests/python/test_updaters.py +++ b/tests/python/test_updaters.py @@ -12,7 +12,6 @@ from xgboost.testing.params import ( cat_parameter_strategy, exact_parameter_strategy, hist_cache_strategy, - hist_multi_parameter_strategy, hist_parameter_strategy, ) from xgboost.testing.updater import ( @@ -25,69 +24,6 @@ from xgboost.testing.updater import ( ) -class TestTreeMethodMulti: - @given( - exact_parameter_strategy, strategies.integers(1, 20), tm.multi_dataset_strategy - ) - @settings(deadline=None, print_blob=True) - def test_exact(self, param: dict, num_rounds: int, dataset: tm.TestDataset) -> None: - if dataset.name.endswith("-l1"): - return - param["tree_method"] = "exact" - param = dataset.set_params(param) - result = train_result(param, dataset.get_dmat(), num_rounds) - assert tm.non_increasing(result["train"][dataset.metric]) - - @given( - exact_parameter_strategy, - hist_parameter_strategy, - hist_cache_strategy, - strategies.integers(1, 20), - tm.multi_dataset_strategy, - ) - @settings(deadline=None, print_blob=True) - def test_approx( - self, param: Dict[str, Any], - hist_param: Dict[str, Any], - cache_param: Dict[str, Any], - num_rounds: int, - dataset: tm.TestDataset, - ) -> None: - param["tree_method"] = "approx" - param = dataset.set_params(param) - param.update(hist_param) - param.update(cache_param) - result = train_result(param, dataset.get_dmat(), num_rounds) - note(str(result)) - assert tm.non_increasing(result["train"][dataset.metric]) - - @given( - exact_parameter_strategy, - hist_multi_parameter_strategy, - hist_cache_strategy, - strategies.integers(1, 20), - tm.multi_dataset_strategy, - ) - @settings(deadline=None, print_blob=True) - def test_hist( - self, - param: Dict[str, Any], - hist_param: Dict[str, Any], - cache_param: Dict[str, Any], - num_rounds: int, - dataset: tm.TestDataset, - ) -> None: - if dataset.name.endswith("-l1"): - return - param["tree_method"] = "hist" - param = dataset.set_params(param) - param.update(hist_param) - param.update(cache_param) - result = train_result(param, dataset.get_dmat(), num_rounds) - note(str(result)) - assert tm.non_increasing(result["train"][dataset.metric]) - - class TestTreeMethod: USE_ONEHOT = np.iinfo(np.int32).max USE_PART = 1 From db396ee34046f29443cdc801903990341ea89fa8 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Thu, 4 Jan 2024 09:51:22 +0100 Subject: [PATCH 46/59] [R] make sure output fits into int32 (#9949) --- R-package/src/xgboost_R.cc | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 60a3fe68b..d7d4c49e1 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -167,21 +167,38 @@ SEXP SafeAllocInteger(size_t size, SEXP continuation_token) { [[nodiscard]] SEXP CopyArrayToR(const char *array_str, SEXP ctoken) { xgboost::ArrayInterface<1> array{xgboost::StringView{array_str}}; // R supports only int and double. - bool is_int = + bool is_int_type = xgboost::DispatchDType(array.type, [](auto t) { return std::is_integral_v; }); bool is_float = xgboost::DispatchDType( array.type, [](auto v) { return std::is_floating_point_v; }); - CHECK(is_int || is_float) << "Internal error: Invalid DType."; + CHECK(is_int_type || is_float) << "Internal error: Invalid DType."; CHECK(array.is_contiguous) << "Internal error: Return by XGBoost should be contiguous"; + // Note: the only case in which this will receive an integer type is + // for the 'indptr' part of the quantile cut outputs, which comes + // in sorted order, so the last element contains the maximum value. + bool fits_into_C_int = xgboost::DispatchDType(array.type, [&](auto t) { + using T = decltype(t); + if (!std::is_integral_v) { + return false; + } + auto ptr = static_cast(array.data); + T last_elt = ptr[array.n - 1]; + if (last_elt < 0) { + last_elt = -last_elt; // no std::abs overload for all possible types + } + return last_elt <= std::numeric_limits::max(); + }); + bool use_int = is_int_type && fits_into_C_int; + // Allocate memory in R SEXP out = - Rf_protect(is_int ? SafeAllocInteger(array.n, ctoken) : SafeAllocReal(array.n, ctoken)); + Rf_protect(use_int ? SafeAllocInteger(array.n, ctoken) : SafeAllocReal(array.n, ctoken)); xgboost::DispatchDType(array.type, [&](auto t) { using T = decltype(t); auto in_ptr = static_cast(array.data); - if (is_int) { + if (use_int) { auto out_ptr = INTEGER(out); std::copy_n(in_ptr, array.n, out_ptr); } else { From c03a4d50886ed2891bb289f5bcbf41d5ce67b61d Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 4 Jan 2024 16:51:33 +0800 Subject: [PATCH 47/59] Check support status for categorical features. (#9946) --- include/xgboost/data.h | 17 +++++++---- src/common/error_msg.h | 6 +++- src/data/data.cc | 45 +++++++++++++++++++++++------- src/data/iterative_dmatrix.h | 2 +- src/gbm/gblinear.cc | 26 ++++++++--------- src/tree/updater_colmaker.cc | 22 +++++++++------ tests/python/test_data_iterator.py | 38 +++++++++++++++++++++++++ 7 files changed, 116 insertions(+), 40 deletions(-) diff --git a/include/xgboost/data.h b/include/xgboost/data.h index 69176994b..08d3d119a 100644 --- a/include/xgboost/data.h +++ b/include/xgboost/data.h @@ -1,5 +1,5 @@ /** - * Copyright 2015-2023 by XGBoost Contributors + * Copyright 2015-2024, XGBoost Contributors * \file data.h * \brief The input data structure of xgboost. * \author Tianqi Chen @@ -158,15 +158,15 @@ class MetaInfo { void SetFeatureInfo(const char *key, const char **info, const bst_ulong size); void GetFeatureInfo(const char *field, std::vector* out_str_vecs) const; - /* - * \brief Extend with other MetaInfo. + /** + * @brief Extend with other MetaInfo. * - * \param that The other MetaInfo object. + * @param that The other MetaInfo object. * - * \param accumulate_rows Whether rows need to be accumulated in this function. If + * @param accumulate_rows Whether rows need to be accumulated in this function. If * client code knows number of rows in advance, set this * parameter to false. - * \param check_column Whether the extend method should check the consistency of + * @param check_column Whether the extend method should check the consistency of * columns. */ void Extend(MetaInfo const& that, bool accumulate_rows, bool check_column); @@ -203,6 +203,10 @@ class MetaInfo { * learning where labels are only available on worker 0. */ bool ShouldHaveLabels() const; + /** + * @brief Flag for whether the DMatrix has categorical features. + */ + bool HasCategorical() const { return has_categorical_; } private: void SetInfoFromHost(Context const& ctx, StringView key, Json arr); @@ -210,6 +214,7 @@ class MetaInfo { /*! \brief argsort of labels */ mutable std::vector label_order_cache_; + bool has_categorical_{false}; }; /*! \brief Element from a sparse vector */ diff --git a/src/common/error_msg.h b/src/common/error_msg.h index 995fe11d5..7264c3532 100644 --- a/src/common/error_msg.h +++ b/src/common/error_msg.h @@ -1,5 +1,5 @@ /** - * Copyright 2023 by XGBoost contributors + * Copyright 2023-2024, XGBoost contributors * * \brief Common error message for various checks. */ @@ -99,5 +99,9 @@ constexpr StringView InvalidCUDAOrdinal() { void MismatchedDevices(Context const* booster, Context const* data); inline auto NoFederated() { return "XGBoost is not compiled with federated learning support."; } + +inline auto NoCategorical(std::string name) { + return name + " doesn't support categorical features."; +} } // namespace xgboost::error #endif // XGBOOST_COMMON_ERROR_MSG_H_ diff --git a/src/data/data.cc b/src/data/data.cc index 96560a11f..24b41640c 100644 --- a/src/data/data.cc +++ b/src/data/data.cc @@ -1,5 +1,5 @@ /** - * Copyright 2015-2023 by XGBoost Contributors + * Copyright 2015-2024, XGBoost Contributors * \file data.cc */ #include "xgboost/data.h" @@ -260,9 +260,14 @@ void MetaInfo::SaveBinary(dmlc::Stream *fo) const { CHECK_EQ(field_cnt, kNumField) << "Wrong number of fields"; } -void LoadFeatureType(std::vectorconst& type_names, std::vector* types) { +/** + * @brief Load feature type info from names, returns whether there's categorical features. + */ +[[nodiscard]] bool LoadFeatureType(std::vector const& type_names, + std::vector* types) { types->clear(); - for (auto const &elem : type_names) { + bool has_cat{false}; + for (auto const& elem : type_names) { if (elem == "int") { types->emplace_back(FeatureType::kNumerical); } else if (elem == "float") { @@ -273,10 +278,12 @@ void LoadFeatureType(std::vectorconst& type_names, std::vectoremplace_back(FeatureType::kNumerical); } else if (elem == "c") { types->emplace_back(FeatureType::kCategorical); + has_cat = true; } else { LOG(FATAL) << "All feature_types must be one of {int, float, i, q, c}."; } } + return has_cat; } const std::vector& MetaInfo::LabelAbsSort(Context const* ctx) const { @@ -340,7 +347,8 @@ void MetaInfo::LoadBinary(dmlc::Stream *fi) { LoadVectorField(fi, u8"feature_names", DataType::kStr, &feature_names); LoadVectorField(fi, u8"feature_types", DataType::kStr, &feature_type_names); LoadVectorField(fi, u8"feature_weights", DataType::kFloat32, &feature_weights); - LoadFeatureType(feature_type_names, &feature_types.HostVector()); + + this->has_categorical_ = LoadFeatureType(feature_type_names, &feature_types.HostVector()); } template @@ -639,6 +647,7 @@ void MetaInfo::SetFeatureInfo(const char* key, const char **info, const bst_ulon CHECK_EQ(size, this->num_col_) << "Length of " << key << " must be equal to number of columns."; CHECK(info); } + if (!std::strcmp(key, "feature_type")) { feature_type_names.clear(); for (size_t i = 0; i < size; ++i) { @@ -651,7 +660,7 @@ void MetaInfo::SetFeatureInfo(const char* key, const char **info, const bst_ulon << "Length of " << key << " must be equal to number of columns."; } auto& h_feature_types = feature_types.HostVector(); - LoadFeatureType(feature_type_names, &h_feature_types); + this->has_categorical_ = LoadFeatureType(feature_type_names, &h_feature_types); } else if (!std::strcmp(key, "feature_name")) { if (IsColumnSplit()) { std::vector local_feature_names{}; @@ -674,9 +683,8 @@ void MetaInfo::SetFeatureInfo(const char* key, const char **info, const bst_ulon } } -void MetaInfo::GetFeatureInfo(const char *field, - std::vector *out_str_vecs) const { - auto &str_vecs = *out_str_vecs; +void MetaInfo::GetFeatureInfo(const char* field, std::vector* out_str_vecs) const { + auto& str_vecs = *out_str_vecs; if (!std::strcmp(field, "feature_type")) { str_vecs.resize(feature_type_names.size()); std::copy(feature_type_names.cbegin(), feature_type_names.cend(), str_vecs.begin()); @@ -689,6 +697,9 @@ void MetaInfo::GetFeatureInfo(const char *field, } void MetaInfo::Extend(MetaInfo const& that, bool accumulate_rows, bool check_column) { + /** + * shape + */ if (accumulate_rows) { this->num_row_ += that.num_row_; } @@ -702,6 +713,9 @@ void MetaInfo::Extend(MetaInfo const& that, bool accumulate_rows, bool check_col } this->num_col_ = that.num_col_; + /** + * info with n_samples + */ linalg::Stack(&this->labels, that.labels); this->weights_.SetDevice(that.weights_.Device()); @@ -715,6 +729,9 @@ void MetaInfo::Extend(MetaInfo const& that, bool accumulate_rows, bool check_col linalg::Stack(&this->base_margin_, that.base_margin_); + /** + * group + */ if (this->group_ptr_.size() == 0) { this->group_ptr_ = that.group_ptr_; } else { @@ -727,17 +744,25 @@ void MetaInfo::Extend(MetaInfo const& that, bool accumulate_rows, bool check_col group_ptr.end()); } + /** + * info with n_features + */ if (!that.feature_names.empty()) { this->feature_names = that.feature_names; } + if (!that.feature_type_names.empty()) { this->feature_type_names = that.feature_type_names; - auto &h_feature_types = feature_types.HostVector(); - LoadFeatureType(this->feature_type_names, &h_feature_types); + auto& h_feature_types = feature_types.HostVector(); + this->has_categorical_ = LoadFeatureType(this->feature_type_names, &h_feature_types); } else if (!that.feature_types.Empty()) { + // FIXME(jiamingy): https://github.com/dmlc/xgboost/pull/9171/files#r1440188612 this->feature_types.Resize(that.feature_types.Size()); this->feature_types.Copy(that.feature_types); + auto const& ft = this->feature_types.ConstHostVector(); + this->has_categorical_ = std::any_of(ft.cbegin(), ft.cend(), common::IsCatOp{}); } + if (!that.feature_weights.Empty()) { this->feature_weights.Resize(that.feature_weights.Size()); this->feature_weights.SetDevice(that.feature_weights.Device()); diff --git a/src/data/iterative_dmatrix.h b/src/data/iterative_dmatrix.h index bcaa5b63c..760a181d1 100644 --- a/src/data/iterative_dmatrix.h +++ b/src/data/iterative_dmatrix.h @@ -93,7 +93,7 @@ class IterativeDMatrix : public DMatrix { return nullptr; } BatchSet GetRowBatches() override { - LOG(FATAL) << "Not implemented."; + LOG(FATAL) << "Not implemented for `QuantileDMatrix`."; return BatchSet(BatchIterator(nullptr)); } BatchSet GetColumnBatches(Context const *) override { diff --git a/src/gbm/gblinear.cc b/src/gbm/gblinear.cc index 4b05d55f3..71905debc 100644 --- a/src/gbm/gblinear.cc +++ b/src/gbm/gblinear.cc @@ -1,5 +1,5 @@ /** - * Copyright 2014-2023, XGBoost Contributors + * Copyright 2014-2024, XGBoost Contributors * \file gblinear.cc * \brief Implementation of Linear booster, with L1/L2 regularization: Elastic Net * the update rule is parallel coordinate descent (shotgun) @@ -8,25 +8,24 @@ #include #include -#include -#include -#include #include #include +#include +#include +#include +#include "../common/common.h" +#include "../common/error_msg.h" // NoCategorical, DeprecatedFunc +#include "../common/threading_utils.h" +#include "../common/timer.h" +#include "gblinear_model.h" #include "xgboost/gbm.h" #include "xgboost/json.h" -#include "xgboost/predictor.h" -#include "xgboost/linear_updater.h" -#include "xgboost/logging.h" #include "xgboost/learner.h" #include "xgboost/linalg.h" - -#include "gblinear_model.h" -#include "../common/timer.h" -#include "../common/common.h" -#include "../common/threading_utils.h" -#include "../common/error_msg.h" +#include "xgboost/linear_updater.h" +#include "xgboost/logging.h" +#include "xgboost/predictor.h" namespace xgboost::gbm { DMLC_REGISTRY_FILE_TAG(gblinear); @@ -145,6 +144,7 @@ class GBLinear : public GradientBooster { ObjFunction const*) override { monitor_.Start("DoBoost"); + CHECK(!p_fmat->Info().HasCategorical()) << error::NoCategorical("`gblinear`"); model_.LazyInitModel(); this->LazySumWeights(p_fmat); diff --git a/src/tree/updater_colmaker.cc b/src/tree/updater_colmaker.cc index e366811f7..ef166fae5 100644 --- a/src/tree/updater_colmaker.cc +++ b/src/tree/updater_colmaker.cc @@ -1,21 +1,22 @@ /** - * Copyright 2014-2023 by XGBoost Contributors + * Copyright 2014-2024, XGBoost Contributors * \file updater_colmaker.cc * \brief use columnwise update to construct a tree * \author Tianqi Chen */ -#include -#include #include +#include +#include +#include "../common/error_msg.h" // for NoCategorical +#include "../common/random.h" +#include "constraints.h" +#include "param.h" +#include "split_evaluator.h" +#include "xgboost/json.h" +#include "xgboost/logging.h" #include "xgboost/parameter.h" #include "xgboost/tree_updater.h" -#include "xgboost/logging.h" -#include "xgboost/json.h" -#include "param.h" -#include "constraints.h" -#include "../common/random.h" -#include "split_evaluator.h" namespace xgboost::tree { @@ -102,6 +103,9 @@ class ColMaker: public TreeUpdater { LOG(FATAL) << "Updater `grow_colmaker` or `exact` tree method doesn't " "support external memory training."; } + if (dmat->Info().HasCategorical()) { + LOG(FATAL) << error::NoCategorical("Updater `grow_colmaker` or `exact` tree method"); + } this->LazyGetColumnDensity(dmat); // rescale learning rate according to size of trees interaction_constraints_.Configure(*param, dmat->Info().num_row_); diff --git a/tests/python/test_data_iterator.py b/tests/python/test_data_iterator.py index e6bdfd2e7..174f5606c 100644 --- a/tests/python/test_data_iterator.py +++ b/tests/python/test_data_iterator.py @@ -1,3 +1,5 @@ +import os +import tempfile import weakref from typing import Any, Callable, Dict, List @@ -195,3 +197,39 @@ def test_data_cache() -> None: assert called == 1 xgb.data._proxy_transform = transform + + +def test_cat_check() -> None: + n_batches = 3 + n_features = 2 + n_samples_per_batch = 16 + + batches = [] + + for i in range(n_batches): + X, y = tm.make_categorical( + n_samples=n_samples_per_batch, + n_features=n_features, + n_categories=3, + onehot=False, + ) + batches.append((X, y)) + + X, y = list(zip(*batches)) + it = tm.IteratorForTest(X, y, None, cache=None) + Xy: xgb.DMatrix = xgb.QuantileDMatrix(it, enable_categorical=True) + + with pytest.raises(ValueError, match="categorical features"): + xgb.train({"tree_method": "exact"}, Xy) + + Xy = xgb.DMatrix(X[0], y[0], enable_categorical=True) + with pytest.raises(ValueError, match="categorical features"): + xgb.train({"tree_method": "exact"}, Xy) + + with tempfile.TemporaryDirectory() as tmpdir: + cache_path = os.path.join(tmpdir, "cache") + + it = tm.IteratorForTest(X, y, None, cache=cache_path) + Xy = xgb.DMatrix(it, enable_categorical=True) + with pytest.raises(ValueError, match="categorical features"): + xgb.train({"booster": "gblinear"}, Xy) From 38dd91f49137305e5822f459ac338a1497f2e400 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Fri, 5 Jan 2024 17:53:36 +0800 Subject: [PATCH 48/59] Save model in ubj as the default. (#9947) --- .../spark/ml/util/XGBoostReadWrite.scala | 3 - .../scala/spark/XGBoostClassifierSuite.scala | 17 +- .../scala/spark/XGBoostRegressorSuite.scala | 29 +- .../java/ml/dmlc/xgboost4j/java/Booster.java | 5 +- .../ml/dmlc/xgboost4j/scala/Booster.scala | 3 +- python-package/xgboost/core.py | 2 +- python-package/xgboost/testing/__init__.py | 5 +- src/c_api/c_api.cc | 16 +- tests/ci_build/lint_python.py | 2 + tests/python/test_basic.py | 182 ++++---- tests/python/test_basic_models.py | 194 +-------- tests/python/test_callback.py | 32 +- tests/python/test_config.py | 10 +- tests/python/test_dmatrix.py | 39 +- tests/python/test_early_stopping.py | 6 +- tests/python/test_eval_metrics.py | 24 +- tests/python/test_linear.py | 10 +- tests/python/test_model_io.py | 406 ++++++++++++++++++ tests/python/test_pickling.py | 27 -- tests/python/test_shap.py | 2 +- tests/python/test_updaters.py | 4 +- tests/python/test_with_pandas.py | 7 - tests/python/test_with_sklearn.py | 123 ------ 23 files changed, 598 insertions(+), 550 deletions(-) create mode 100644 tests/python/test_model_io.py diff --git a/jvm-packages/xgboost4j-spark/src/main/scala/org/apache/spark/ml/util/XGBoostReadWrite.scala b/jvm-packages/xgboost4j-spark/src/main/scala/org/apache/spark/ml/util/XGBoostReadWrite.scala index 672241be1..ff732b78c 100644 --- a/jvm-packages/xgboost4j-spark/src/main/scala/org/apache/spark/ml/util/XGBoostReadWrite.scala +++ b/jvm-packages/xgboost4j-spark/src/main/scala/org/apache/spark/ml/util/XGBoostReadWrite.scala @@ -30,9 +30,6 @@ import org.apache.spark.ml.param.Params import org.apache.spark.ml.util.DefaultParamsReader.Metadata abstract class XGBoostWriter extends MLWriter { - - /** Currently it's using the "deprecated" format as - * default, which will be changed into `ubj` in future releases. */ def getModelFormat(): String = { optionMap.getOrElse("format", JBooster.DEFAULT_FORMAT) } diff --git a/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostClassifierSuite.scala b/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostClassifierSuite.scala index 9b53c7642..48e7dae52 100644 --- a/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostClassifierSuite.scala +++ b/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostClassifierSuite.scala @@ -1,5 +1,5 @@ /* - Copyright (c) 2014-2022 by Contributors + Copyright (c) 2014-2024 by Contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -432,6 +432,7 @@ class XGBoostClassifierSuite extends AnyFunSuite with PerTest with TmpFolderPerS val xgb = new XGBoostClassifier(paramMap) val model = xgb.fit(trainingDF) + // test json val modelPath = new File(tempDir.toFile, "xgbc").getPath model.write.option("format", "json").save(modelPath) val nativeJsonModelPath = new File(tempDir.toFile, "nativeModel.json").getPath @@ -439,21 +440,21 @@ class XGBoostClassifierSuite extends AnyFunSuite with PerTest with TmpFolderPerS assert(compareTwoFiles(new File(modelPath, "data/XGBoostClassificationModel").getPath, nativeJsonModelPath)) - // test default "deprecated" + // test ubj val modelUbjPath = new File(tempDir.toFile, "xgbcUbj").getPath model.write.save(modelUbjPath) - val nativeDeprecatedModelPath = new File(tempDir.toFile, "nativeModel").getPath - model.nativeBooster.saveModel(nativeDeprecatedModelPath) + val nativeUbjModelPath = new File(tempDir.toFile, "nativeModel.ubj").getPath + model.nativeBooster.saveModel(nativeUbjModelPath) assert(compareTwoFiles(new File(modelUbjPath, "data/XGBoostClassificationModel").getPath, - nativeDeprecatedModelPath)) + nativeUbjModelPath)) // json file should be indifferent with ubj file val modelJsonPath = new File(tempDir.toFile, "xgbcJson").getPath model.write.option("format", "json").save(modelJsonPath) - val nativeUbjModelPath = new File(tempDir.toFile, "nativeModel1.ubj").getPath - model.nativeBooster.saveModel(nativeUbjModelPath) + val nativeUbjModelPath1 = new File(tempDir.toFile, "nativeModel1.ubj").getPath + model.nativeBooster.saveModel(nativeUbjModelPath1) assert(!compareTwoFiles(new File(modelJsonPath, "data/XGBoostClassificationModel").getPath, - nativeUbjModelPath)) + nativeUbjModelPath1)) } test("native json model file should store feature_name and feature_type") { diff --git a/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostRegressorSuite.scala b/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostRegressorSuite.scala index 1bdea7a82..0698541c7 100644 --- a/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostRegressorSuite.scala +++ b/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostRegressorSuite.scala @@ -1,5 +1,5 @@ /* - Copyright (c) 2014-2022 by Contributors + Copyright (c) 2014-2024 by Contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -333,21 +333,24 @@ class XGBoostRegressorSuite extends AnyFunSuite with PerTest with TmpFolderPerSu assert(compareTwoFiles(new File(modelPath, "data/XGBoostRegressionModel").getPath, nativeJsonModelPath)) - // test default "deprecated" + // test default "ubj" val modelUbjPath = new File(tempDir.toFile, "xgbcUbj").getPath model.write.save(modelUbjPath) - val nativeDeprecatedModelPath = new File(tempDir.toFile, "nativeModel").getPath - model.nativeBooster.saveModel(nativeDeprecatedModelPath) - assert(compareTwoFiles(new File(modelUbjPath, "data/XGBoostRegressionModel").getPath, - nativeDeprecatedModelPath)) - // json file should be indifferent with ubj file - val modelJsonPath = new File(tempDir.toFile, "xgbcJson").getPath - model.write.option("format", "json").save(modelJsonPath) - val nativeUbjModelPath = new File(tempDir.toFile, "nativeModel1.ubj").getPath + val nativeUbjModelPath = new File(tempDir.toFile, "nativeModel.ubj").getPath model.nativeBooster.saveModel(nativeUbjModelPath) - assert(!compareTwoFiles(new File(modelJsonPath, "data/XGBoostRegressionModel").getPath, - nativeUbjModelPath)) - } + assert(compareTwoFiles(new File(modelUbjPath, "data/XGBoostRegressionModel").getPath, + nativeUbjModelPath)) + + // test the deprecated format + val modelDeprecatedPath = new File(tempDir.toFile, "modelDeprecated").getPath + model.write.option("format", "deprecated").save(modelDeprecatedPath) + + val nativeDeprecatedModelPath = new File(tempDir.toFile, "nativeModel.deprecated").getPath + model.nativeBooster.saveModel(nativeDeprecatedModelPath) + + assert(compareTwoFiles(new File(modelDeprecatedPath, "data/XGBoostRegressionModel").getPath, + nativeDeprecatedModelPath)) + } } diff --git a/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/Booster.java b/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/Booster.java index 51959ce0c..22ed6dc82 100644 --- a/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/Booster.java +++ b/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/Booster.java @@ -34,7 +34,7 @@ import org.apache.commons.logging.LogFactory; * Booster for xgboost, this is a model API that support interactive build of a XGBoost Model */ public class Booster implements Serializable, KryoSerializable { - public static final String DEFAULT_FORMAT = "deprecated"; + public static final String DEFAULT_FORMAT = "ubj"; private static final Log logger = LogFactory.getLog(Booster.class); // handle to the booster. private long handle = 0; @@ -788,8 +788,7 @@ public class Booster implements Serializable, KryoSerializable { } /** - * Save model into raw byte array. Currently it's using the deprecated format as - * default, which will be changed into `ubj` in future releases. + * Save model into raw byte array in the UBJSON ("ubj") format. * * @return the saved byte array * @throws XGBoostError native error diff --git a/jvm-packages/xgboost4j/src/main/scala/ml/dmlc/xgboost4j/scala/Booster.scala b/jvm-packages/xgboost4j/src/main/scala/ml/dmlc/xgboost4j/scala/Booster.scala index c288bfab1..57c3b9a5d 100644 --- a/jvm-packages/xgboost4j/src/main/scala/ml/dmlc/xgboost4j/scala/Booster.scala +++ b/jvm-packages/xgboost4j/src/main/scala/ml/dmlc/xgboost4j/scala/Booster.scala @@ -337,8 +337,7 @@ class Booster private[xgboost4j](private[xgboost4j] var booster: JBooster) } /** - * Save model into a raw byte array. Currently it's using the deprecated format as - * default, which will be changed into `ubj` in future releases. + * Save model into a raw byte array in the UBJSON ("ubj") format. */ @throws(classOf[XGBoostError]) def toByteArray: Array[Byte] = { diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index 097fb0935..d554a8d3a 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -2613,7 +2613,7 @@ class Booster: else: raise TypeError("fname must be a string or os PathLike") - def save_raw(self, raw_format: str = "deprecated") -> bytearray: + def save_raw(self, raw_format: str = "ubj") -> bytearray: """Save the model to a in memory buffer representation instead of file. Parameters diff --git a/python-package/xgboost/testing/__init__.py b/python-package/xgboost/testing/__init__.py index 373ad1c58..46bbf8800 100644 --- a/python-package/xgboost/testing/__init__.py +++ b/python-package/xgboost/testing/__init__.py @@ -630,7 +630,7 @@ sparse_datasets_strategy = strategies.sampled_from( def make_datasets_with_margin( unweighted_strategy: strategies.SearchStrategy, -) -> Callable: +) -> Callable[[], strategies.SearchStrategy[TestDataset]]: """Factory function for creating strategies that generates datasets with weight and base margin. @@ -668,8 +668,7 @@ def make_datasets_with_margin( # A strategy for drawing from a set of example datasets. May add random weights to the # dataset -@memory.cache -def make_dataset_strategy() -> Callable: +def make_dataset_strategy() -> strategies.SearchStrategy[TestDataset]: _unweighted_datasets_strategy = strategies.sampled_from( [ TestDataset( diff --git a/src/c_api/c_api.cc b/src/c_api/c_api.cc index 9aedcef2e..d4cc217d1 100644 --- a/src/c_api/c_api.cc +++ b/src/c_api/c_api.cc @@ -1313,10 +1313,8 @@ XGB_DLL int XGBoosterLoadModel(BoosterHandle handle, const char* fname) { namespace { void WarnOldModel() { - if (XGBOOST_VER_MAJOR >= 2) { - LOG(WARNING) << "Saving into deprecated binary model format, please consider using `json` or " - "`ubj`. Model format will default to JSON in XGBoost 2.2 if not specified."; - } + LOG(WARNING) << "Saving into deprecated binary model format, please consider using `json` or " + "`ubj`. Model format is default to UBJSON in XGBoost 2.1 if not specified."; } } // anonymous namespace @@ -1339,14 +1337,14 @@ XGB_DLL int XGBoosterSaveModel(BoosterHandle handle, const char *fname) { save_json(std::ios::out); } else if (common::FileExtension(fname) == "ubj") { save_json(std::ios::binary); - } else if (XGBOOST_VER_MAJOR == 2 && XGBOOST_VER_MINOR >= 2) { - LOG(WARNING) << "Saving model to JSON as default. You can use file extension `json`, `ubj` or " - "`deprecated` to choose between formats."; - save_json(std::ios::out); - } else { + } else if (common::FileExtension(fname) == "deprecated") { WarnOldModel(); auto *bst = static_cast(handle); bst->SaveModel(fo.get()); + } else { + LOG(WARNING) << "Saving model in the UBJSON format as default. You can use file extension:" + " `json`, `ubj` or `deprecated` to choose between formats."; + save_json(std::ios::binary); } API_END(); } diff --git a/tests/ci_build/lint_python.py b/tests/ci_build/lint_python.py index ed33a96e5..1e414f3b5 100644 --- a/tests/ci_build/lint_python.py +++ b/tests/ci_build/lint_python.py @@ -27,6 +27,7 @@ class LintersPaths: "tests/python/test_quantile_dmatrix.py", "tests/python/test_tree_regularization.py", "tests/python/test_shap.py", + "tests/python/test_model_io.py", "tests/python/test_with_pandas.py", "tests/python-gpu/", "tests/python-sycl/", @@ -83,6 +84,7 @@ class LintersPaths: "tests/python/test_multi_target.py", "tests/python-gpu/test_gpu_data_iterator.py", "tests/python-gpu/load_pickle.py", + "tests/python/test_model_io.py", "tests/test_distributed/test_with_spark/test_data.py", "tests/test_distributed/test_gpu_with_spark/test_data.py", "tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py", diff --git a/tests/python/test_basic.py b/tests/python/test_basic.py index b99351c7f..cdc571a91 100644 --- a/tests/python/test_basic.py +++ b/tests/python/test_basic.py @@ -10,46 +10,48 @@ import pytest import xgboost as xgb from xgboost import testing as tm -dpath = 'demo/data/' +dpath = "demo/data/" rng = np.random.RandomState(1994) class TestBasic: def test_compat(self): from xgboost.compat import lazy_isinstance + a = np.array([1, 2, 3]) - assert lazy_isinstance(a, 'numpy', 'ndarray') - assert not lazy_isinstance(a, 'numpy', 'dataframe') + assert lazy_isinstance(a, "numpy", "ndarray") + assert not lazy_isinstance(a, "numpy", "dataframe") def test_basic(self): dtrain, dtest = tm.load_agaricus(__file__) - param = {'max_depth': 2, 'eta': 1, - 'objective': 'binary:logistic'} + param = {"max_depth": 2, "eta": 1, "objective": "binary:logistic"} # specify validations set to watch performance - watchlist = [(dtrain, 'train')] + watchlist = [(dtrain, "train")] num_round = 2 - bst = xgb.train(param, dtrain, num_round, watchlist, verbose_eval=True) + bst = xgb.train(param, dtrain, num_round, evals=watchlist, verbose_eval=True) preds = bst.predict(dtrain) labels = dtrain.get_label() - err = sum(1 for i in range(len(preds)) - if int(preds[i] > 0.5) != labels[i]) / float(len(preds)) + err = sum( + 1 for i in range(len(preds)) if int(preds[i] > 0.5) != labels[i] + ) / float(len(preds)) # error must be smaller than 10% assert err < 0.1 preds = bst.predict(dtest) labels = dtest.get_label() - err = sum(1 for i in range(len(preds)) - if int(preds[i] > 0.5) != labels[i]) / float(len(preds)) + err = sum( + 1 for i in range(len(preds)) if int(preds[i] > 0.5) != labels[i] + ) / float(len(preds)) # error must be smaller than 10% assert err < 0.1 with tempfile.TemporaryDirectory() as tmpdir: - dtest_path = os.path.join(tmpdir, 'dtest.dmatrix') + dtest_path = os.path.join(tmpdir, "dtest.dmatrix") # save dmatrix into binary buffer dtest.save_binary(dtest_path) # save model - model_path = os.path.join(tmpdir, 'model.booster') + model_path = os.path.join(tmpdir, "model.ubj") bst.save_model(model_path) # load model and data in bst2 = xgb.Booster(model_file=model_path) @@ -59,17 +61,21 @@ class TestBasic: assert np.sum(np.abs(preds2 - preds)) == 0 def test_metric_config(self): - # Make sure that the metric configuration happens in booster so the - # string `['error', 'auc']` doesn't get passed down to core. + # Make sure that the metric configuration happens in booster so the string + # `['error', 'auc']` doesn't get passed down to core. dtrain, dtest = tm.load_agaricus(__file__) - param = {'max_depth': 2, 'eta': 1, 'verbosity': 0, - 'objective': 'binary:logistic', 'eval_metric': ['error', 'auc']} - watchlist = [(dtest, 'eval'), (dtrain, 'train')] + param = { + "max_depth": 2, + "eta": 1, + "objective": "binary:logistic", + "eval_metric": ["error", "auc"], + } + watchlist = [(dtest, "eval"), (dtrain, "train")] num_round = 2 - booster = xgb.train(param, dtrain, num_round, watchlist) + booster = xgb.train(param, dtrain, num_round, evals=watchlist) predt_0 = booster.predict(dtrain) with tempfile.TemporaryDirectory() as tmpdir: - path = os.path.join(tmpdir, 'model.json') + path = os.path.join(tmpdir, "model.json") booster.save_model(path) booster = xgb.Booster(params=param, model_file=path) @@ -78,22 +84,23 @@ class TestBasic: def test_multiclass(self): dtrain, dtest = tm.load_agaricus(__file__) - param = {'max_depth': 2, 'eta': 1, 'verbosity': 0, 'num_class': 2} + param = {"max_depth": 2, "eta": 1, "num_class": 2} # specify validations set to watch performance - watchlist = [(dtest, 'eval'), (dtrain, 'train')] + watchlist = [(dtest, "eval"), (dtrain, "train")] num_round = 2 - bst = xgb.train(param, dtrain, num_round, watchlist) + bst = xgb.train(param, dtrain, num_round, evals=watchlist) # this is prediction preds = bst.predict(dtest) labels = dtest.get_label() - err = sum(1 for i in range(len(preds)) - if preds[i] != labels[i]) / float(len(preds)) + err = sum(1 for i in range(len(preds)) if preds[i] != labels[i]) / float( + len(preds) + ) # error must be smaller than 10% assert err < 0.1 with tempfile.TemporaryDirectory() as tmpdir: - dtest_path = os.path.join(tmpdir, 'dtest.buffer') - model_path = os.path.join(tmpdir, 'xgb.model') + dtest_path = os.path.join(tmpdir, "dtest.buffer") + model_path = os.path.join(tmpdir, "model.ubj") # save dmatrix into binary buffer dtest.save_binary(dtest_path) # save model @@ -108,33 +115,39 @@ class TestBasic: def test_dump(self): data = np.random.randn(100, 2) target = np.array([0, 1] * 50) - features = ['Feature1', 'Feature2'] + features = ["Feature1", "Feature2"] dm = xgb.DMatrix(data, label=target, feature_names=features) - params = {'objective': 'binary:logistic', - 'eval_metric': 'logloss', - 'eta': 0.3, - 'max_depth': 1} + params = { + "objective": "binary:logistic", + "eval_metric": "logloss", + "eta": 0.3, + "max_depth": 1, + } bst = xgb.train(params, dm, num_boost_round=1) # number of feature importances should == number of features dump1 = bst.get_dump() - assert len(dump1) == 1, 'Expected only 1 tree to be dumped.' - len(dump1[0].splitlines()) == 3, 'Expected 1 root and 2 leaves - 3 lines in dump.' + assert len(dump1) == 1, "Expected only 1 tree to be dumped." + len( + dump1[0].splitlines() + ) == 3, "Expected 1 root and 2 leaves - 3 lines in dump." dump2 = bst.get_dump(with_stats=True) - assert dump2[0].count('\n') == 3, 'Expected 1 root and 2 leaves - 3 lines in dump.' - msg = 'Expected more info when with_stats=True is given.' - assert dump2[0].find('\n') > dump1[0].find('\n'), msg + assert ( + dump2[0].count("\n") == 3 + ), "Expected 1 root and 2 leaves - 3 lines in dump." + msg = "Expected more info when with_stats=True is given." + assert dump2[0].find("\n") > dump1[0].find("\n"), msg dump3 = bst.get_dump(dump_format="json") dump3j = json.loads(dump3[0]) - assert dump3j['nodeid'] == 0, 'Expected the root node on top.' + assert dump3j["nodeid"] == 0, "Expected the root node on top." dump4 = bst.get_dump(dump_format="json", with_stats=True) dump4j = json.loads(dump4[0]) - assert 'gain' in dump4j, "Expected 'gain' to be dumped in JSON." + assert "gain" in dump4j, "Expected 'gain' to be dumped in JSON." with pytest.raises(ValueError): bst.get_dump(fmap="foo") @@ -163,12 +176,14 @@ class TestBasic: def test_load_file_invalid(self): with pytest.raises(xgb.core.XGBoostError): - xgb.Booster(model_file='incorrect_path') + xgb.Booster(model_file="incorrect_path") with pytest.raises(xgb.core.XGBoostError): - xgb.Booster(model_file=u'不正なパス') + xgb.Booster(model_file="不正なパス") - @pytest.mark.parametrize("path", ["모델.ubj", "がうる・ぐら.json"], ids=["path-0", "path-1"]) + @pytest.mark.parametrize( + "path", ["모델.ubj", "がうる・ぐら.json"], ids=["path-0", "path-1"] + ) def test_unicode_path(self, tmpdir, path): model_path = pathlib.Path(tmpdir) / path dtrain, _ = tm.load_agaricus(__file__) @@ -180,12 +195,11 @@ class TestBasic: assert bst.get_dump(dump_format="text") == bst2.get_dump(dump_format="text") def test_dmatrix_numpy_init_omp(self): - rows = [1000, 11326, 15000] cols = 50 for row in rows: X = np.random.randn(row, cols) - y = np.random.randn(row).astype('f') + y = np.random.randn(row).astype("f") dm = xgb.DMatrix(X, y, nthread=0) np.testing.assert_array_equal(dm.get_label(), y) assert dm.num_row() == row @@ -198,8 +212,7 @@ class TestBasic: def test_cv(self): dm, _ = tm.load_agaricus(__file__) - params = {'max_depth': 2, 'eta': 1, 'verbosity': 0, - 'objective': 'binary:logistic'} + params = {"max_depth": 2, "eta": 1, "objective": "binary:logistic"} # return np.ndarray cv = xgb.cv(params, dm, num_boost_round=10, nfold=10, as_pandas=False) @@ -208,19 +221,18 @@ class TestBasic: def test_cv_no_shuffle(self): dm, _ = tm.load_agaricus(__file__) - params = {'max_depth': 2, 'eta': 1, 'verbosity': 0, - 'objective': 'binary:logistic'} + params = {"max_depth": 2, "eta": 1, "objective": "binary:logistic"} # return np.ndarray - cv = xgb.cv(params, dm, num_boost_round=10, shuffle=False, nfold=10, - as_pandas=False) + cv = xgb.cv( + params, dm, num_boost_round=10, shuffle=False, nfold=10, as_pandas=False + ) assert isinstance(cv, dict) assert len(cv) == (4) def test_cv_explicit_fold_indices(self): dm, _ = tm.load_agaricus(__file__) - params = {'max_depth': 2, 'eta': 1, 'verbosity': 0, 'objective': - 'binary:logistic'} + params = {"max_depth": 2, "eta": 1, "objective": "binary:logistic"} folds = [ # Train Test ([1, 3], [5, 8]), @@ -228,15 +240,13 @@ class TestBasic: ] # return np.ndarray - cv = xgb.cv(params, dm, num_boost_round=10, folds=folds, - as_pandas=False) + cv = xgb.cv(params, dm, num_boost_round=10, folds=folds, as_pandas=False) assert isinstance(cv, dict) assert len(cv) == (4) @pytest.mark.skipif(**tm.skip_s390x()) def test_cv_explicit_fold_indices_labels(self): - params = {'max_depth': 2, 'eta': 1, 'verbosity': 0, 'objective': - 'reg:squarederror'} + params = {"max_depth": 2, "eta": 1, "objective": "reg:squarederror"} N = 100 F = 3 dm = xgb.DMatrix(data=np.random.randn(N, F), label=np.arange(N)) @@ -252,9 +262,10 @@ class TestBasic: super().__init__() def after_iteration( - self, model, + self, + model, epoch: int, - evals_log: xgb.callback.TrainingCallback.EvalsLog + evals_log: xgb.callback.TrainingCallback.EvalsLog, ): print([fold.dtest.get_label() for fold in model.cvfolds]) @@ -263,12 +274,18 @@ class TestBasic: # Run cross validation and capture standard out to test callback result with tm.captured_output() as (out, err): xgb.cv( - params, dm, num_boost_round=1, folds=folds, callbacks=[cb], - as_pandas=False + params, + dm, + num_boost_round=1, + folds=folds, + callbacks=[cb], + as_pandas=False, ) output = out.getvalue().strip() - solution = ('[array([5., 8.], dtype=float32), array([23., 43., 11.],' + - ' dtype=float32)]') + solution = ( + "[array([5., 8.], dtype=float32), array([23., 43., 11.]," + + " dtype=float32)]" + ) assert output == solution @@ -285,7 +302,7 @@ class TestBasicPathLike: """Saving to a binary file using pathlib from a DMatrix.""" data = np.random.randn(100, 2) target = np.array([0, 1] * 50) - features = ['Feature1', 'Feature2'] + features = ["Feature1", "Feature2"] dm = xgb.DMatrix(data, label=target, feature_names=features) @@ -299,42 +316,3 @@ class TestBasicPathLike: """An invalid model_file path should raise XGBoostError.""" with pytest.raises(xgb.core.XGBoostError): xgb.Booster(model_file=Path("invalidpath")) - - def test_Booster_save_and_load(self): - """Saving and loading model files from paths.""" - save_path = Path("saveload.model") - - data = np.random.randn(100, 2) - target = np.array([0, 1] * 50) - features = ['Feature1', 'Feature2'] - - dm = xgb.DMatrix(data, label=target, feature_names=features) - params = {'objective': 'binary:logistic', - 'eval_metric': 'logloss', - 'eta': 0.3, - 'max_depth': 1} - - bst = xgb.train(params, dm, num_boost_round=1) - - # save, assert exists - bst.save_model(save_path) - assert save_path.exists() - - def dump_assertions(dump): - """Assertions for the expected dump from Booster""" - assert len(dump) == 1, 'Exepcted only 1 tree to be dumped.' - assert len(dump[0].splitlines()) == 3, 'Expected 1 root and 2 leaves - 3 lines.' - - # load the model again using Path - bst2 = xgb.Booster(model_file=save_path) - dump2 = bst2.get_dump() - dump_assertions(dump2) - - # load again using load_model - bst3 = xgb.Booster() - bst3.load_model(save_path) - dump3 = bst3.get_dump() - dump_assertions(dump3) - - # remove file - Path.unlink(save_path) diff --git a/tests/python/test_basic_models.py b/tests/python/test_basic_models.py index 8f83e1fcc..ca35c4e91 100644 --- a/tests/python/test_basic_models.py +++ b/tests/python/test_basic_models.py @@ -15,33 +15,9 @@ dpath = tm.data_dir(__file__) rng = np.random.RandomState(1994) -def json_model(model_path: str, parameters: dict) -> dict: - datasets = pytest.importorskip("sklearn.datasets") - - X, y = datasets.make_classification(64, n_features=8, n_classes=3, n_informative=6) - if parameters.get("objective", None) == "multi:softmax": - parameters["num_class"] = 3 - - dm1 = xgb.DMatrix(X, y) - - bst = xgb.train(parameters, dm1) - bst.save_model(model_path) - - if model_path.endswith("ubj"): - import ubjson - - with open(model_path, "rb") as ubjfd: - model = ubjson.load(ubjfd) - else: - with open(model_path, "r") as fd: - model = json.load(fd) - - return model - - class TestModels: def test_glm(self): - param = {'verbosity': 0, 'objective': 'binary:logistic', + param = {'objective': 'binary:logistic', 'booster': 'gblinear', 'alpha': 0.0001, 'lambda': 1, 'nthread': 1} dtrain, dtest = tm.load_agaricus(__file__) @@ -73,7 +49,7 @@ class TestModels: with tempfile.TemporaryDirectory() as tmpdir: dtest_path = os.path.join(tmpdir, 'dtest.dmatrix') - model_path = os.path.join(tmpdir, 'xgboost.model.dart') + model_path = os.path.join(tmpdir, "xgboost.model.dart.ubj") # save dmatrix into binary buffer dtest.save_binary(dtest_path) model_path = model_path @@ -101,7 +77,6 @@ class TestModels: # check whether sample_type and normalize_type work num_round = 50 - param['verbosity'] = 0 param['learning_rate'] = 0.1 param['rate_drop'] = 0.1 preds_list = [] @@ -214,8 +189,7 @@ class TestModels: assert set(evals_result['eval'].keys()) == {'auc', 'error', 'logloss'} def test_fpreproc(self): - param = {'max_depth': 2, 'eta': 1, 'verbosity': 0, - 'objective': 'binary:logistic'} + param = {'max_depth': 2, 'eta': 1, 'objective': 'binary:logistic'} num_round = 2 def fpreproc(dtrain, dtest, param): @@ -229,8 +203,7 @@ class TestModels: metrics={'auc'}, seed=0, fpreproc=fpreproc) def test_show_stdv(self): - param = {'max_depth': 2, 'eta': 1, 'verbosity': 0, - 'objective': 'binary:logistic'} + param = {'max_depth': 2, 'eta': 1, 'objective': 'binary:logistic'} num_round = 2 dtrain, _ = tm.load_agaricus(__file__) xgb.cv(param, dtrain, num_round, nfold=5, @@ -273,142 +246,6 @@ class TestModels: bst = xgb.train([], dm2) bst.predict(dm2) # success - def test_model_binary_io(self): - model_path = 'test_model_binary_io.bin' - parameters = {'tree_method': 'hist', 'booster': 'gbtree', - 'scale_pos_weight': '0.5'} - X = np.random.random((10, 3)) - y = np.random.random((10,)) - dtrain = xgb.DMatrix(X, y) - bst = xgb.train(parameters, dtrain, num_boost_round=2) - bst.save_model(model_path) - bst = xgb.Booster(model_file=model_path) - os.remove(model_path) - config = json.loads(bst.save_config()) - assert float(config['learner']['objective'][ - 'reg_loss_param']['scale_pos_weight']) == 0.5 - - buf = bst.save_raw() - from_raw = xgb.Booster() - from_raw.load_model(buf) - - buf_from_raw = from_raw.save_raw() - assert buf == buf_from_raw - - def run_model_json_io(self, parameters: dict, ext: str) -> None: - if ext == "ubj" and tm.no_ubjson()["condition"]: - pytest.skip(tm.no_ubjson()["reason"]) - - loc = locale.getpreferredencoding(False) - model_path = 'test_model_json_io.' + ext - j_model = json_model(model_path, parameters) - assert isinstance(j_model['learner'], dict) - - bst = xgb.Booster(model_file=model_path) - - bst.save_model(fname=model_path) - if ext == "ubj": - import ubjson - with open(model_path, "rb") as ubjfd: - j_model = ubjson.load(ubjfd) - else: - with open(model_path, 'r') as fd: - j_model = json.load(fd) - - assert isinstance(j_model['learner'], dict) - - os.remove(model_path) - assert locale.getpreferredencoding(False) == loc - - json_raw = bst.save_raw(raw_format="json") - from_jraw = xgb.Booster() - from_jraw.load_model(json_raw) - - ubj_raw = bst.save_raw(raw_format="ubj") - from_ubjraw = xgb.Booster() - from_ubjraw.load_model(ubj_raw) - - if parameters.get("multi_strategy", None) != "multi_output_tree": - # old binary model is not supported. - old_from_json = from_jraw.save_raw(raw_format="deprecated") - old_from_ubj = from_ubjraw.save_raw(raw_format="deprecated") - - assert old_from_json == old_from_ubj - - raw_json = bst.save_raw(raw_format="json") - pretty = json.dumps(json.loads(raw_json), indent=2) + "\n\n" - bst.load_model(bytearray(pretty, encoding="ascii")) - - if parameters.get("multi_strategy", None) != "multi_output_tree": - # old binary model is not supported. - old_from_json = from_jraw.save_raw(raw_format="deprecated") - old_from_ubj = from_ubjraw.save_raw(raw_format="deprecated") - - assert old_from_json == old_from_ubj - - rng = np.random.default_rng() - X = rng.random(size=from_jraw.num_features() * 10).reshape( - (10, from_jraw.num_features()) - ) - predt_from_jraw = from_jraw.predict(xgb.DMatrix(X)) - predt_from_bst = bst.predict(xgb.DMatrix(X)) - np.testing.assert_allclose(predt_from_jraw, predt_from_bst) - - @pytest.mark.parametrize("ext", ["json", "ubj"]) - def test_model_json_io(self, ext: str) -> None: - parameters = {"booster": "gbtree", "tree_method": "hist"} - self.run_model_json_io(parameters, ext) - parameters = { - "booster": "gbtree", - "tree_method": "hist", - "multi_strategy": "multi_output_tree", - "objective": "multi:softmax", - } - self.run_model_json_io(parameters, ext) - parameters = {"booster": "gblinear"} - self.run_model_json_io(parameters, ext) - parameters = {"booster": "dart", "tree_method": "hist"} - self.run_model_json_io(parameters, ext) - - @pytest.mark.skipif(**tm.no_json_schema()) - def test_json_io_schema(self): - import jsonschema - model_path = 'test_json_schema.json' - path = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - doc = os.path.join(path, 'doc', 'model.schema') - with open(doc, 'r') as fd: - schema = json.load(fd) - parameters = {'tree_method': 'hist', 'booster': 'gbtree'} - jsonschema.validate(instance=json_model(model_path, parameters), - schema=schema) - os.remove(model_path) - - parameters = {'tree_method': 'hist', 'booster': 'dart'} - jsonschema.validate(instance=json_model(model_path, parameters), - schema=schema) - os.remove(model_path) - - try: - dtrain, _ = tm.load_agaricus(__file__) - xgb.train({'objective': 'foo'}, dtrain, num_boost_round=1) - except ValueError as e: - e_str = str(e) - beg = e_str.find('Objective candidate') - end = e_str.find('Stack trace') - e_str = e_str[beg: end] - e_str = e_str.strip() - splited = e_str.splitlines() - objectives = [s.split(': ')[1] for s in splited] - j_objectives = schema['properties']['learner']['properties'][ - 'objective']['oneOf'] - objectives_from_schema = set() - for j_obj in j_objectives: - objectives_from_schema.add( - j_obj['properties']['name']['const']) - objectives = set(objectives) - assert objectives == objectives_from_schema - @pytest.mark.skipif(**tm.no_json_schema()) def test_json_dump_schema(self): import jsonschema @@ -470,29 +307,6 @@ class TestModels: for d in text_dump: assert d.find(r"feature \"2\"") != -1 - def test_categorical_model_io(self): - X, y = tm.make_categorical(256, 16, 71, False) - Xy = xgb.DMatrix(X, y, enable_categorical=True) - booster = xgb.train({"tree_method": "approx"}, Xy, num_boost_round=16) - predt_0 = booster.predict(Xy) - - with tempfile.TemporaryDirectory() as tempdir: - path = os.path.join(tempdir, "model.binary") - with pytest.raises(ValueError, match=r".*JSON/UBJSON.*"): - booster.save_model(path) - - path = os.path.join(tempdir, "model.json") - booster.save_model(path) - booster = xgb.Booster(model_file=path) - predt_1 = booster.predict(Xy) - np.testing.assert_allclose(predt_0, predt_1) - - path = os.path.join(tempdir, "model.ubj") - booster.save_model(path) - booster = xgb.Booster(model_file=path) - predt_1 = booster.predict(Xy) - np.testing.assert_allclose(predt_0, predt_1) - @pytest.mark.skipif(**tm.no_sklearn()) def test_attributes(self): from sklearn.datasets import load_iris diff --git a/tests/python/test_callback.py b/tests/python/test_callback.py index 262c09c99..4893ad074 100644 --- a/tests/python/test_callback.py +++ b/tests/python/test_callback.py @@ -278,14 +278,18 @@ class TestCallbacks: dtrain, dtest = tm.load_agaricus(__file__) - watchlist = [(dtest, 'eval'), (dtrain, 'train')] + watchlist = [(dtest, "eval"), (dtrain, "train")] num_round = 4 # learning_rates as a list # init eta with 0 to check whether learning_rates work - param = {'max_depth': 2, 'eta': 0, 'verbosity': 0, - 'objective': 'binary:logistic', 'eval_metric': 'error', - 'tree_method': tree_method} + param = { + "max_depth": 2, + "eta": 0, + "objective": "binary:logistic", + "eval_metric": "error", + "tree_method": tree_method, + } evals_result = {} bst = xgb.train( param, @@ -295,15 +299,19 @@ class TestCallbacks: callbacks=[scheduler([0.8, 0.7, 0.6, 0.5])], evals_result=evals_result, ) - eval_errors_0 = list(map(float, evals_result['eval']['error'])) + eval_errors_0 = list(map(float, evals_result["eval"]["error"])) assert isinstance(bst, xgb.core.Booster) # validation error should decrease, if eta > 0 assert eval_errors_0[0] > eval_errors_0[-1] # init learning_rate with 0 to check whether learning_rates work - param = {'max_depth': 2, 'learning_rate': 0, 'verbosity': 0, - 'objective': 'binary:logistic', 'eval_metric': 'error', - 'tree_method': tree_method} + param = { + "max_depth": 2, + "learning_rate": 0, + "objective": "binary:logistic", + "eval_metric": "error", + "tree_method": tree_method, + } evals_result = {} bst = xgb.train( @@ -314,15 +322,17 @@ class TestCallbacks: callbacks=[scheduler([0.8, 0.7, 0.6, 0.5])], evals_result=evals_result, ) - eval_errors_1 = list(map(float, evals_result['eval']['error'])) + eval_errors_1 = list(map(float, evals_result["eval"]["error"])) assert isinstance(bst, xgb.core.Booster) # validation error should decrease, if learning_rate > 0 assert eval_errors_1[0] > eval_errors_1[-1] # check if learning_rates override default value of eta/learning_rate param = { - 'max_depth': 2, 'verbosity': 0, 'objective': 'binary:logistic', - 'eval_metric': 'error', 'tree_method': tree_method + "max_depth": 2, + "objective": "binary:logistic", + "eval_metric": "error", + "tree_method": tree_method, } evals_result = {} bst = xgb.train( diff --git a/tests/python/test_config.py b/tests/python/test_config.py index 01b5c2d99..3f741c25d 100644 --- a/tests/python/test_config.py +++ b/tests/python/test_config.py @@ -12,6 +12,7 @@ def test_global_config_verbosity(verbosity_level): return xgb.get_config()["verbosity"] old_verbosity = get_current_verbosity() + assert old_verbosity == 1 with xgb.config_context(verbosity=verbosity_level): new_verbosity = get_current_verbosity() assert new_verbosity == verbosity_level @@ -30,7 +31,10 @@ def test_global_config_use_rmm(use_rmm): assert old_use_rmm_flag == get_current_use_rmm_flag() -def test_nested_config(): +def test_nested_config() -> None: + verbosity = xgb.get_config()["verbosity"] + assert verbosity == 1 + with xgb.config_context(verbosity=3): assert xgb.get_config()["verbosity"] == 3 with xgb.config_context(verbosity=2): @@ -45,13 +49,15 @@ def test_nested_config(): with xgb.config_context(verbosity=None): assert xgb.get_config()["verbosity"] == 3 # None has no effect - verbosity = xgb.get_config()["verbosity"] xgb.set_config(verbosity=2) assert xgb.get_config()["verbosity"] == 2 with xgb.config_context(verbosity=3): assert xgb.get_config()["verbosity"] == 3 xgb.set_config(verbosity=verbosity) # reset + verbosity = xgb.get_config()["verbosity"] + assert verbosity == 1 + def test_thread_safty(): n_threads = multiprocessing.cpu_count() diff --git a/tests/python/test_dmatrix.py b/tests/python/test_dmatrix.py index c718378c5..9d123ddb9 100644 --- a/tests/python/test_dmatrix.py +++ b/tests/python/test_dmatrix.py @@ -1,6 +1,7 @@ import csv import os import tempfile +import warnings import numpy as np import pytest @@ -24,20 +25,18 @@ class TestDMatrix: with pytest.warns(UserWarning): data._warn_unused_missing("uri", 4) - with pytest.warns(None) as record: + with warnings.catch_warnings(): + warnings.simplefilter("error") data._warn_unused_missing("uri", None) data._warn_unused_missing("uri", np.nan) - assert len(record) == 0 - - with pytest.warns(None) as record: + with warnings.catch_warnings(): + warnings.simplefilter("error") x = rng.randn(10, 10) y = rng.randn(10) xgb.DMatrix(x, y, missing=4) - assert len(record) == 0 - def test_dmatrix_numpy_init(self): data = np.random.randn(5, 5) dm = xgb.DMatrix(data) @@ -264,7 +263,7 @@ class TestDMatrix: dtrain = xgb.DMatrix(x, label=rng.binomial(1, 0.3, nrow)) assert (dtrain.num_row(), dtrain.num_col()) == (nrow, ncol) watchlist = [(dtrain, "train")] - param = {"max_depth": 3, "objective": "binary:logistic", "verbosity": 0} + param = {"max_depth": 3, "objective": "binary:logistic"} bst = xgb.train(param, dtrain, 5, watchlist) bst.predict(dtrain) @@ -302,7 +301,7 @@ class TestDMatrix: dtrain = xgb.DMatrix(x, label=rng.binomial(1, 0.3, nrow)) assert (dtrain.num_row(), dtrain.num_col()) == (nrow, ncol) watchlist = [(dtrain, "train")] - param = {"max_depth": 3, "objective": "binary:logistic", "verbosity": 0} + param = {"max_depth": 3, "objective": "binary:logistic"} bst = xgb.train(param, dtrain, 5, watchlist) bst.predict(dtrain) @@ -475,17 +474,19 @@ class TestDMatrixColumnSplit: def test_uri(self): def verify_uri(): rank = xgb.collective.get_rank() - data = np.random.rand(5, 5) - filename = f"test_data_{rank}.csv" - with open(filename, mode="w", newline="") as file: - writer = csv.writer(file) - for row in data: - writer.writerow(row) - dtrain = xgb.DMatrix( - f"{filename}?format=csv", data_split_mode=DataSplitMode.COL - ) - assert dtrain.num_row() == 5 - assert dtrain.num_col() == 5 * xgb.collective.get_world_size() + with tempfile.TemporaryDirectory() as tmpdir: + filename = os.path.join(tmpdir, f"test_data_{rank}.csv") + + data = np.random.rand(5, 5) + with open(filename, mode="w", newline="") as file: + writer = csv.writer(file) + for row in data: + writer.writerow(row) + dtrain = xgb.DMatrix( + f"{filename}?format=csv", data_split_mode=DataSplitMode.COL + ) + assert dtrain.num_row() == 5 + assert dtrain.num_col() == 5 * xgb.collective.get_world_size() tm.run_with_rabit(world_size=3, test_fn=verify_uri) diff --git a/tests/python/test_early_stopping.py b/tests/python/test_early_stopping.py index 47f58cbd6..7695c6861 100644 --- a/tests/python/test_early_stopping.py +++ b/tests/python/test_early_stopping.py @@ -67,8 +67,10 @@ class TestEarlyStopping: X = digits['data'] y = digits['target'] dm = xgb.DMatrix(X, label=y) - params = {'max_depth': 2, 'eta': 1, 'verbosity': 0, - 'objective': 'binary:logistic', 'eval_metric': 'error'} + params = { + 'max_depth': 2, 'eta': 1, 'objective': 'binary:logistic', + 'eval_metric': 'error' + } cv = xgb.cv(params, dm, num_boost_round=10, nfold=10, early_stopping_rounds=10) diff --git a/tests/python/test_eval_metrics.py b/tests/python/test_eval_metrics.py index 147c87a27..92726014b 100644 --- a/tests/python/test_eval_metrics.py +++ b/tests/python/test_eval_metrics.py @@ -9,29 +9,13 @@ rng = np.random.RandomState(1337) class TestEvalMetrics: - xgb_params_01 = { - 'verbosity': 0, - 'nthread': 1, - 'eval_metric': 'error' - } + xgb_params_01 = {'nthread': 1, 'eval_metric': 'error'} - xgb_params_02 = { - 'verbosity': 0, - 'nthread': 1, - 'eval_metric': ['error'] - } + xgb_params_02 = {'nthread': 1, 'eval_metric': ['error']} - xgb_params_03 = { - 'verbosity': 0, - 'nthread': 1, - 'eval_metric': ['rmse', 'error'] - } + xgb_params_03 = {'nthread': 1, 'eval_metric': ['rmse', 'error']} - xgb_params_04 = { - 'verbosity': 0, - 'nthread': 1, - 'eval_metric': ['error', 'rmse'] - } + xgb_params_04 = {'nthread': 1, 'eval_metric': ['error', 'rmse']} def evalerror_01(self, preds, dtrain): labels = dtrain.get_label() diff --git a/tests/python/test_linear.py b/tests/python/test_linear.py index 0a198a036..5d281d415 100644 --- a/tests/python/test_linear.py +++ b/tests/python/test_linear.py @@ -22,8 +22,14 @@ coord_strategy = strategies.fixed_dictionaries({ def train_result(param, dmat, num_rounds): result = {} - xgb.train(param, dmat, num_rounds, [(dmat, 'train')], verbose_eval=False, - evals_result=result) + xgb.train( + param, + dmat, + num_rounds, + evals=[(dmat, "train")], + verbose_eval=False, + evals_result=result, + ) return result diff --git a/tests/python/test_model_io.py b/tests/python/test_model_io.py new file mode 100644 index 000000000..dc843a7f4 --- /dev/null +++ b/tests/python/test_model_io.py @@ -0,0 +1,406 @@ +import json +import locale +import os +import pickle +import tempfile +from pathlib import Path +from typing import List + +import numpy as np +import pytest + +import xgboost as xgb +from xgboost import testing as tm + + +def json_model(model_path: str, parameters: dict) -> dict: + datasets = pytest.importorskip("sklearn.datasets") + + X, y = datasets.make_classification(64, n_features=8, n_classes=3, n_informative=6) + if parameters.get("objective", None) == "multi:softmax": + parameters["num_class"] = 3 + + dm1 = xgb.DMatrix(X, y) + + bst = xgb.train(parameters, dm1) + bst.save_model(model_path) + + if model_path.endswith("ubj"): + import ubjson + + with open(model_path, "rb") as ubjfd: + model = ubjson.load(ubjfd) + else: + with open(model_path, "r") as fd: + model = json.load(fd) + + return model + + +class TestBoosterIO: + def run_model_json_io(self, parameters: dict, ext: str) -> None: + config = xgb.config.get_config() + assert config["verbosity"] == 1 + + if ext == "ubj" and tm.no_ubjson()["condition"]: + pytest.skip(tm.no_ubjson()["reason"]) + + loc = locale.getpreferredencoding(False) + model_path = "test_model_json_io." + ext + j_model = json_model(model_path, parameters) + assert isinstance(j_model["learner"], dict) + + bst = xgb.Booster(model_file=model_path) + + bst.save_model(fname=model_path) + if ext == "ubj": + import ubjson + + with open(model_path, "rb") as ubjfd: + j_model = ubjson.load(ubjfd) + else: + with open(model_path, "r") as fd: + j_model = json.load(fd) + + assert isinstance(j_model["learner"], dict) + + os.remove(model_path) + assert locale.getpreferredencoding(False) == loc + + json_raw = bst.save_raw(raw_format="json") + from_jraw = xgb.Booster() + from_jraw.load_model(json_raw) + + ubj_raw = bst.save_raw(raw_format="ubj") + from_ubjraw = xgb.Booster() + from_ubjraw.load_model(ubj_raw) + + if parameters.get("multi_strategy", None) != "multi_output_tree": + # Old binary model is not supported for vector leaf. + with pytest.warns(Warning, match="Model format is default to UBJSON"): + old_from_json = from_jraw.save_raw(raw_format="deprecated") + old_from_ubj = from_ubjraw.save_raw(raw_format="deprecated") + + assert old_from_json == old_from_ubj + + raw_json = bst.save_raw(raw_format="json") + pretty = json.dumps(json.loads(raw_json), indent=2) + "\n\n" + bst.load_model(bytearray(pretty, encoding="ascii")) + + if parameters.get("multi_strategy", None) != "multi_output_tree": + # old binary model is not supported. + with pytest.warns(Warning, match="Model format is default to UBJSON"): + old_from_json = from_jraw.save_raw(raw_format="deprecated") + old_from_ubj = from_ubjraw.save_raw(raw_format="deprecated") + + assert old_from_json == old_from_ubj + + rng = np.random.default_rng() + X = rng.random(size=from_jraw.num_features() * 10).reshape( + (10, from_jraw.num_features()) + ) + predt_from_jraw = from_jraw.predict(xgb.DMatrix(X)) + predt_from_bst = bst.predict(xgb.DMatrix(X)) + np.testing.assert_allclose(predt_from_jraw, predt_from_bst) + + @pytest.mark.parametrize("ext", ["json", "ubj"]) + def test_model_json_io(self, ext: str) -> None: + parameters = {"booster": "gbtree", "tree_method": "hist"} + self.run_model_json_io(parameters, ext) + parameters = { + "booster": "gbtree", + "tree_method": "hist", + "multi_strategy": "multi_output_tree", + "objective": "multi:softmax", + } + self.run_model_json_io(parameters, ext) + parameters = {"booster": "gblinear"} + self.run_model_json_io(parameters, ext) + parameters = {"booster": "dart", "tree_method": "hist"} + self.run_model_json_io(parameters, ext) + + def test_categorical_model_io(self) -> None: + X, y = tm.make_categorical(256, 16, 71, False) + Xy = xgb.DMatrix(X, y, enable_categorical=True) + booster = xgb.train({"tree_method": "approx"}, Xy, num_boost_round=16) + predt_0 = booster.predict(Xy) + + with tempfile.TemporaryDirectory() as tempdir: + path = os.path.join(tempdir, "model.deprecated") + with pytest.raises(ValueError, match=r".*JSON/UBJSON.*"): + with pytest.warns(Warning, match="Model format is default to UBJSON"): + booster.save_model(path) + + path = os.path.join(tempdir, "model.json") + booster.save_model(path) + booster = xgb.Booster(model_file=path) + predt_1 = booster.predict(Xy) + np.testing.assert_allclose(predt_0, predt_1) + + path = os.path.join(tempdir, "model.ubj") + booster.save_model(path) + booster = xgb.Booster(model_file=path) + predt_1 = booster.predict(Xy) + np.testing.assert_allclose(predt_0, predt_1) + + @pytest.mark.skipif(**tm.no_json_schema()) + def test_json_io_schema(self) -> None: + import jsonschema + + model_path = "test_json_schema.json" + path = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + doc = os.path.join(path, "doc", "model.schema") + with open(doc, "r") as fd: + schema = json.load(fd) + parameters = {"tree_method": "hist", "booster": "gbtree"} + jsonschema.validate(instance=json_model(model_path, parameters), schema=schema) + os.remove(model_path) + + parameters = {"tree_method": "hist", "booster": "dart"} + jsonschema.validate(instance=json_model(model_path, parameters), schema=schema) + os.remove(model_path) + + try: + dtrain, _ = tm.load_agaricus(__file__) + xgb.train({"objective": "foo"}, dtrain, num_boost_round=1) + except ValueError as e: + e_str = str(e) + beg = e_str.find("Objective candidate") + end = e_str.find("Stack trace") + e_str = e_str[beg:end] + e_str = e_str.strip() + splited = e_str.splitlines() + objectives = [s.split(": ")[1] for s in splited] + j_objectives = schema["properties"]["learner"]["properties"]["objective"][ + "oneOf" + ] + objectives_from_schema = set() + for j_obj in j_objectives: + objectives_from_schema.add(j_obj["properties"]["name"]["const"]) + assert set(objectives) == objectives_from_schema + + def test_model_binary_io(self) -> None: + model_path = "test_model_binary_io.deprecated" + parameters = { + "tree_method": "hist", + "booster": "gbtree", + "scale_pos_weight": "0.5", + } + X = np.random.random((10, 3)) + y = np.random.random((10,)) + dtrain = xgb.DMatrix(X, y) + bst = xgb.train(parameters, dtrain, num_boost_round=2) + with pytest.warns(Warning, match="Model format is default to UBJSON"): + bst.save_model(model_path) + bst = xgb.Booster(model_file=model_path) + os.remove(model_path) + config = json.loads(bst.save_config()) + assert ( + float(config["learner"]["objective"]["reg_loss_param"]["scale_pos_weight"]) + == 0.5 + ) + + buf = bst.save_raw() + from_raw = xgb.Booster() + from_raw.load_model(buf) + + buf_from_raw = from_raw.save_raw() + assert buf == buf_from_raw + + def test_with_pathlib(self) -> None: + """Saving and loading model files from paths.""" + save_path = Path("model.ubj") + + rng = np.random.default_rng(1994) + + data = rng.normal(size=(100, 2)) + target = np.array([0, 1] * 50) + features = ["Feature1", "Feature2"] + + dm = xgb.DMatrix(data, label=target, feature_names=features) + params = { + "objective": "binary:logistic", + "eval_metric": "logloss", + "eta": 0.3, + "max_depth": 1, + } + + bst = xgb.train(params, dm, num_boost_round=1) + + # save, assert exists + bst.save_model(save_path) + assert save_path.exists() + + def dump_assertions(dump: List[str]) -> None: + """Assertions for the expected dump from Booster""" + assert len(dump) == 1, "Exepcted only 1 tree to be dumped." + assert ( + len(dump[0].splitlines()) == 3 + ), "Expected 1 root and 2 leaves - 3 lines." + + # load the model again using Path + bst2 = xgb.Booster(model_file=save_path) + dump2 = bst2.get_dump() + dump_assertions(dump2) + + # load again using load_model + bst3 = xgb.Booster() + bst3.load_model(save_path) + dump3 = bst3.get_dump() + dump_assertions(dump3) + + # remove file + Path.unlink(save_path) + + +def save_load_model(model_path: str) -> None: + from sklearn.datasets import load_digits + from sklearn.model_selection import KFold + + rng = np.random.RandomState(1994) + + digits = load_digits(n_class=2) + y = digits["target"] + X = digits["data"] + kf = KFold(n_splits=2, shuffle=True, random_state=rng) + for train_index, test_index in kf.split(X, y): + xgb_model = xgb.XGBClassifier().fit(X[train_index], y[train_index]) + xgb_model.save_model(model_path) + + xgb_model = xgb.XGBClassifier() + xgb_model.load_model(model_path) + + assert isinstance(xgb_model.classes_, np.ndarray) + np.testing.assert_equal(xgb_model.classes_, np.array([0, 1])) + assert isinstance(xgb_model._Booster, xgb.Booster) + + preds = xgb_model.predict(X[test_index]) + labels = y[test_index] + err = sum( + 1 for i in range(len(preds)) if int(preds[i] > 0.5) != labels[i] + ) / float(len(preds)) + assert err < 0.1 + assert xgb_model.get_booster().attr("scikit_learn") is None + + # test native booster + preds = xgb_model.predict(X[test_index], output_margin=True) + booster = xgb.Booster(model_file=model_path) + predt_1 = booster.predict(xgb.DMatrix(X[test_index]), output_margin=True) + assert np.allclose(preds, predt_1) + + with pytest.raises(TypeError): + xgb_model = xgb.XGBModel() + xgb_model.load_model(model_path) + + clf = xgb.XGBClassifier(booster="gblinear", early_stopping_rounds=1) + clf.fit(X, y, eval_set=[(X, y)]) + best_iteration = clf.best_iteration + best_score = clf.best_score + predt_0 = clf.predict(X) + clf.save_model(model_path) + clf.load_model(model_path) + assert clf.booster == "gblinear" + predt_1 = clf.predict(X) + np.testing.assert_allclose(predt_0, predt_1) + assert clf.best_iteration == best_iteration + assert clf.best_score == best_score + + clfpkl = pickle.dumps(clf) + clf = pickle.loads(clfpkl) + predt_2 = clf.predict(X) + np.testing.assert_allclose(predt_0, predt_2) + assert clf.best_iteration == best_iteration + assert clf.best_score == best_score + + +@pytest.mark.skipif(**tm.no_sklearn()) +def test_sklearn_model() -> None: + from sklearn.datasets import load_digits + from sklearn.model_selection import train_test_split + + with tempfile.TemporaryDirectory() as tempdir: + model_path = os.path.join(tempdir, "digits.deprecated") + with pytest.warns(Warning, match="Model format is default to UBJSON"): + save_load_model(model_path) + + with tempfile.TemporaryDirectory() as tempdir: + model_path = os.path.join(tempdir, "digits.model.json") + save_load_model(model_path) + + with tempfile.TemporaryDirectory() as tempdir: + model_path = os.path.join(tempdir, "digits.model.ubj") + digits = load_digits(n_class=2) + y = digits["target"] + X = digits["data"] + booster = xgb.train( + {"tree_method": "hist", "objective": "binary:logistic"}, + dtrain=xgb.DMatrix(X, y), + num_boost_round=4, + ) + predt_0 = booster.predict(xgb.DMatrix(X)) + booster.save_model(model_path) + cls = xgb.XGBClassifier() + cls.load_model(model_path) + + proba = cls.predict_proba(X) + assert proba.shape[0] == X.shape[0] + assert proba.shape[1] == 2 # binary + + predt_1 = cls.predict_proba(X)[:, 1] + assert np.allclose(predt_0, predt_1) + + cls = xgb.XGBModel() + cls.load_model(model_path) + predt_1 = cls.predict(X) + assert np.allclose(predt_0, predt_1) + + # mclass + X, y = load_digits(n_class=10, return_X_y=True) + # small test_size to force early stop + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.01, random_state=1 + ) + clf = xgb.XGBClassifier( + n_estimators=64, tree_method="hist", early_stopping_rounds=2 + ) + clf.fit(X_train, y_train, eval_set=[(X_test, y_test)]) + score = clf.best_score + clf.save_model(model_path) + + clf = xgb.XGBClassifier() + clf.load_model(model_path) + assert clf.classes_.size == 10 + assert clf.objective == "multi:softprob" + + np.testing.assert_equal(clf.classes_, np.arange(10)) + assert clf.n_classes_ == 10 + + assert clf.best_iteration == 27 + assert clf.best_score == score + + +@pytest.mark.skipif(**tm.no_sklearn()) +def test_with_sklearn_obj_metric() -> None: + from sklearn.metrics import mean_squared_error + + X, y = tm.datasets.make_regression() + reg = xgb.XGBRegressor(objective=tm.ls_obj, eval_metric=mean_squared_error) + reg.fit(X, y) + + pkl = pickle.dumps(reg) + reg_1 = pickle.loads(pkl) + assert callable(reg_1.objective) + assert callable(reg_1.eval_metric) + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "model.json") + reg.save_model(path) + + reg_2 = xgb.XGBRegressor() + reg_2.load_model(path) + + assert not callable(reg_2.objective) + assert not callable(reg_2.eval_metric) + assert reg_2.eval_metric is None diff --git a/tests/python/test_pickling.py b/tests/python/test_pickling.py index 083a2a7fd..2f4d77bf0 100644 --- a/tests/python/test_pickling.py +++ b/tests/python/test_pickling.py @@ -1,13 +1,10 @@ import json import os import pickle -import tempfile import numpy as np -import pytest import xgboost as xgb -from xgboost import testing as tm kRows = 100 kCols = 10 @@ -64,27 +61,3 @@ class TestPickling: params = {"nthread": 8, "tree_method": "exact", "subsample": 0.5} config = self.run_model_pickling(params) check(config) - - @pytest.mark.skipif(**tm.no_sklearn()) - def test_with_sklearn_obj_metric(self) -> None: - from sklearn.metrics import mean_squared_error - - X, y = tm.datasets.make_regression() - reg = xgb.XGBRegressor(objective=tm.ls_obj, eval_metric=mean_squared_error) - reg.fit(X, y) - - pkl = pickle.dumps(reg) - reg_1 = pickle.loads(pkl) - assert callable(reg_1.objective) - assert callable(reg_1.eval_metric) - - with tempfile.TemporaryDirectory() as tmpdir: - path = os.path.join(tmpdir, "model.json") - reg.save_model(path) - - reg_2 = xgb.XGBRegressor() - reg_2.load_model(path) - - assert not callable(reg_2.objective) - assert not callable(reg_2.eval_metric) - assert reg_2.eval_metric is None diff --git a/tests/python/test_shap.py b/tests/python/test_shap.py index bbbdcedc0..88149c054 100644 --- a/tests/python/test_shap.py +++ b/tests/python/test_shap.py @@ -49,7 +49,7 @@ class TestSHAP: def fn(max_depth: int, num_rounds: int) -> None: # train - params = {"max_depth": max_depth, "eta": 1, "verbosity": 0} + params = {"max_depth": max_depth, "eta": 1} bst = xgb.train(params, dtrain, num_boost_round=num_rounds) # predict diff --git a/tests/python/test_updaters.py b/tests/python/test_updaters.py index 9a0cdecd2..e7641348d 100644 --- a/tests/python/test_updaters.py +++ b/tests/python/test_updaters.py @@ -117,7 +117,6 @@ class TestTreeMethod: ag_param = {'max_depth': 2, 'tree_method': 'hist', 'eta': 1, - 'verbosity': 0, 'objective': 'binary:logistic', 'eval_metric': 'auc'} hist_res = {} @@ -340,7 +339,8 @@ class TestTreeMethod: assert get_score(config_0) == get_score(config_1) - raw_booster = booster_1.save_raw(raw_format="deprecated") + with pytest.warns(Warning, match="Model format is default to UBJSON"): + raw_booster = booster_1.save_raw(raw_format="deprecated") booster_2 = xgb.Booster(model_file=raw_booster) config_2 = json.loads(booster_2.save_config()) assert get_score(config_1) == get_score(config_2) diff --git a/tests/python/test_with_pandas.py b/tests/python/test_with_pandas.py index 4dd0c640d..e53e7adcc 100644 --- a/tests/python/test_with_pandas.py +++ b/tests/python/test_with_pandas.py @@ -341,7 +341,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", "eval_metric": "error", } @@ -372,7 +371,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", "eval_metric": "auc", } @@ -383,7 +381,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", "eval_metric": ["auc"], } @@ -394,7 +391,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", "eval_metric": ["auc"], } @@ -413,7 +409,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", } cv = xgb.cv( @@ -424,7 +419,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", } cv = xgb.cv( @@ -435,7 +429,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", "eval_metric": ["auc"], } diff --git a/tests/python/test_with_sklearn.py b/tests/python/test_with_sklearn.py index ee0085d51..47f1778d6 100644 --- a/tests/python/test_with_sklearn.py +++ b/tests/python/test_with_sklearn.py @@ -678,7 +678,6 @@ def test_split_value_histograms(): params = { "max_depth": 6, "eta": 0.01, - "verbosity": 0, "objective": "binary:logistic", "base_score": 0.5, } @@ -897,128 +896,6 @@ def test_validation_weights(): run_validation_weights(xgb.XGBClassifier) -def save_load_model(model_path): - from sklearn.datasets import load_digits - from sklearn.model_selection import KFold - - digits = load_digits(n_class=2) - y = digits['target'] - X = digits['data'] - kf = KFold(n_splits=2, shuffle=True, random_state=rng) - for train_index, test_index in kf.split(X, y): - xgb_model = xgb.XGBClassifier().fit(X[train_index], y[train_index]) - xgb_model.save_model(model_path) - - xgb_model = xgb.XGBClassifier() - xgb_model.load_model(model_path) - - assert isinstance(xgb_model.classes_, np.ndarray) - np.testing.assert_equal(xgb_model.classes_, np.array([0, 1])) - assert isinstance(xgb_model._Booster, xgb.Booster) - - preds = xgb_model.predict(X[test_index]) - labels = y[test_index] - err = sum(1 for i in range(len(preds)) - if int(preds[i] > 0.5) != labels[i]) / float(len(preds)) - assert err < 0.1 - assert xgb_model.get_booster().attr('scikit_learn') is None - - # test native booster - preds = xgb_model.predict(X[test_index], output_margin=True) - booster = xgb.Booster(model_file=model_path) - predt_1 = booster.predict(xgb.DMatrix(X[test_index]), - output_margin=True) - assert np.allclose(preds, predt_1) - - with pytest.raises(TypeError): - xgb_model = xgb.XGBModel() - xgb_model.load_model(model_path) - - clf = xgb.XGBClassifier(booster="gblinear", early_stopping_rounds=1) - clf.fit(X, y, eval_set=[(X, y)]) - best_iteration = clf.best_iteration - best_score = clf.best_score - predt_0 = clf.predict(X) - clf.save_model(model_path) - clf.load_model(model_path) - assert clf.booster == "gblinear" - predt_1 = clf.predict(X) - np.testing.assert_allclose(predt_0, predt_1) - assert clf.best_iteration == best_iteration - assert clf.best_score == best_score - - clfpkl = pickle.dumps(clf) - clf = pickle.loads(clfpkl) - predt_2 = clf.predict(X) - np.testing.assert_allclose(predt_0, predt_2) - assert clf.best_iteration == best_iteration - assert clf.best_score == best_score - - -def test_save_load_model(): - with tempfile.TemporaryDirectory() as tempdir: - model_path = os.path.join(tempdir, "digits.model") - save_load_model(model_path) - - with tempfile.TemporaryDirectory() as tempdir: - model_path = os.path.join(tempdir, "digits.model.json") - save_load_model(model_path) - - from sklearn.datasets import load_digits - from sklearn.model_selection import train_test_split - - with tempfile.TemporaryDirectory() as tempdir: - model_path = os.path.join(tempdir, "digits.model.ubj") - digits = load_digits(n_class=2) - y = digits["target"] - X = digits["data"] - booster = xgb.train( - {"tree_method": "hist", "objective": "binary:logistic"}, - dtrain=xgb.DMatrix(X, y), - num_boost_round=4, - ) - predt_0 = booster.predict(xgb.DMatrix(X)) - booster.save_model(model_path) - cls = xgb.XGBClassifier() - cls.load_model(model_path) - - proba = cls.predict_proba(X) - assert proba.shape[0] == X.shape[0] - assert proba.shape[1] == 2 # binary - - predt_1 = cls.predict_proba(X)[:, 1] - assert np.allclose(predt_0, predt_1) - - cls = xgb.XGBModel() - cls.load_model(model_path) - predt_1 = cls.predict(X) - assert np.allclose(predt_0, predt_1) - - # mclass - X, y = load_digits(n_class=10, return_X_y=True) - # small test_size to force early stop - X_train, X_test, y_train, y_test = train_test_split( - X, y, test_size=0.01, random_state=1 - ) - clf = xgb.XGBClassifier( - n_estimators=64, tree_method="hist", early_stopping_rounds=2 - ) - clf.fit(X_train, y_train, eval_set=[(X_test, y_test)]) - score = clf.best_score - clf.save_model(model_path) - - clf = xgb.XGBClassifier() - clf.load_model(model_path) - assert clf.classes_.size == 10 - assert clf.objective == "multi:softprob" - - np.testing.assert_equal(clf.classes_, np.arange(10)) - assert clf.n_classes_ == 10 - - assert clf.best_iteration == 27 - assert clf.best_score == score - - def test_RFECV(): from sklearn.datasets import load_breast_cancer, load_diabetes, load_iris from sklearn.feature_selection import RFECV From 3976455af9a2a7b72338ff805742d15b32d5fd79 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Mon, 8 Jan 2024 13:26:12 +0800 Subject: [PATCH 49/59] [jvm-packages] Use UBJ for checkpoints. (#9954) --- .../scala/spark/ExternalCheckpointManagerSuite.scala | 12 ++++++------ .../xgboost4j/java/ExternalCheckpointManager.java | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/ExternalCheckpointManagerSuite.scala b/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/ExternalCheckpointManagerSuite.scala index e6835158d..729bd9c77 100755 --- a/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/ExternalCheckpointManagerSuite.scala +++ b/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/ExternalCheckpointManagerSuite.scala @@ -50,13 +50,13 @@ class ExternalCheckpointManagerSuite extends AnyFunSuite with TmpFolderPerSuite manager.updateCheckpoint(model2._booster.booster) var files = FileSystem.get(sc.hadoopConfiguration).listStatus(new Path(tmpPath)) assert(files.length == 1) - assert(files.head.getPath.getName == "1.model") + assert(files.head.getPath.getName == "1.ubj") assert(manager.loadCheckpointAsScalaBooster().getNumBoostedRound == 2) manager.updateCheckpoint(model4._booster) files = FileSystem.get(sc.hadoopConfiguration).listStatus(new Path(tmpPath)) assert(files.length == 1) - assert(files.head.getPath.getName == "3.model") + assert(files.head.getPath.getName == "3.ubj") assert(manager.loadCheckpointAsScalaBooster().getNumBoostedRound == 4) } @@ -66,10 +66,10 @@ class ExternalCheckpointManagerSuite extends AnyFunSuite with TmpFolderPerSuite val manager = new ExternalCheckpointManager(tmpPath, FileSystem.get(sc.hadoopConfiguration)) manager.updateCheckpoint(model4._booster) manager.cleanUpHigherVersions(3) - assert(new File(s"$tmpPath/3.model").exists()) + assert(new File(s"$tmpPath/3.ubj").exists()) manager.cleanUpHigherVersions(2) - assert(!new File(s"$tmpPath/3.model").exists()) + assert(!new File(s"$tmpPath/3.ubj").exists()) } test("test checkpoint rounds") { @@ -105,8 +105,8 @@ class ExternalCheckpointManagerSuite extends AnyFunSuite with TmpFolderPerSuite // Check only one model is kept after training val files = FileSystem.get(sc.hadoopConfiguration).listStatus(new Path(tmpPath)) assert(files.length == 1) - assert(files.head.getPath.getName == "4.model") - val tmpModel = SXGBoost.loadModel(s"$tmpPath/4.model") + assert(files.head.getPath.getName == "4.ubj") + val tmpModel = SXGBoost.loadModel(s"$tmpPath/4.ubj") // Train next model based on prev model val nextModel = new XGBoostClassifier(paramMap ++ Seq("num_round" -> 8)).fit(training) assert(error(tmpModel) >= error(prevModel._booster)) diff --git a/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/ExternalCheckpointManager.java b/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/ExternalCheckpointManager.java index 3d794756d..d5b8e8b9c 100644 --- a/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/ExternalCheckpointManager.java +++ b/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/ExternalCheckpointManager.java @@ -29,7 +29,7 @@ import org.apache.hadoop.fs.Path; public class ExternalCheckpointManager { private Log logger = LogFactory.getLog("ExternalCheckpointManager"); - private String modelSuffix = ".model"; + private String modelSuffix = ".ubj"; private Path checkpointPath; // directory for checkpoints private FileSystem fs; From 3ff3a5f1ed35503feeaec4ae3f159746159623e6 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Mon, 8 Jan 2024 17:30:49 +0800 Subject: [PATCH 50/59] [jvm-packages] support jdk 17 for test (#9959) --- jvm-packages/pom.xml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/jvm-packages/pom.xml b/jvm-packages/pom.xml index de778a995..1acff2240 100644 --- a/jvm-packages/pom.xml +++ b/jvm-packages/pom.xml @@ -48,6 +48,22 @@ cuda11 3.2.17 2.11.0 + + + + -XX:+IgnoreUnrecognizedVMOptions + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.lang.invoke=ALL-UNNAMED + --add-opens=java.base/java.io=ALL-UNNAMED + --add-opens=java.base/java.net=ALL-UNNAMED + --add-opens=java.base/java.nio=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent=ALL-UNNAMED + --add-opens=java.base/sun.nio.ch=ALL-UNNAMED + --add-opens=java.base/sun.nio.cs=ALL-UNNAMED + --add-opens=java.base/sun.security.action=ALL-UNNAMED + --add-opens=java.base/sun.util.calendar=ALL-UNNAMED + @@ -337,6 +353,9 @@ org.scalatest scalatest-maven-plugin 2.2.0 + + -ea -Xmx4g -Xss4m ${extraJavaTestArgs} + test From 9a30bdd3137a23669b9df9851cf7bd6230cf846d Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Mon, 8 Jan 2024 19:26:24 +0800 Subject: [PATCH 51/59] Test loading models with invalid file extensions. (#9955) --- doc/tutorials/saving_model.rst | 30 ++++++++++------ tests/python/test_model_io.py | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/doc/tutorials/saving_model.rst b/doc/tutorials/saving_model.rst index 34b5430df..326834cd4 100644 --- a/doc/tutorials/saving_model.rst +++ b/doc/tutorials/saving_model.rst @@ -2,14 +2,20 @@ Introduction to Model IO ######################## +Since 2.1.0, the default model format for XGBoost is the UBJSON format, the option is +enabled for serializing models to file, serializing models to buffer, and for memory +snapshot (pickle and alike). + In XGBoost 1.0.0, we introduced support of using `JSON `_ for saving/loading XGBoost models and related hyper-parameters for training, aiming to replace the old binary internal format with an open format that can be easily reused. Later in XGBoost 1.6.0, additional support for `Universal Binary JSON `__ is added as an optimization for more -efficient model IO. They have the same document structure with different representations, -and we will refer them collectively as the JSON format. This tutorial aims to share some -basic insights into the JSON serialisation method used in XGBoost. Without explicitly +efficient model IO, which is set to default in 2.1. + +JSON and UBJSON have the same document structure with different representations, and we +will refer them collectively as the JSON format. This tutorial aims to share some basic +insights into the JSON serialisation method used in XGBoost. Without explicitly mentioned, the following sections assume you are using the one of the 2 outputs formats, which can be enabled by providing the file name with ``.json`` (or ``.ubj`` for binary JSON) as file extension when saving/loading model: ``booster.save_model('model.json')``. @@ -25,12 +31,13 @@ If you come from Deep Learning community, then it should be clear to you that there are differences between the neural network structures composed of weights with fixed tensor operations, and the optimizers (like RMSprop) used to train them. -So when one calls ``booster.save_model`` (``xgb.save`` in R), XGBoost saves the trees, some model -parameters like number of input columns in trained trees, and the objective function, which combined -to represent the concept of "model" in XGBoost. As for why are we saving the objective as -part of model, that's because objective controls transformation of global bias (called -``base_score`` in XGBoost). Users can share this model with others for prediction, -evaluation or continue the training with a different set of hyper-parameters etc. +So when one calls ``booster.save_model`` (``xgb.save`` in R), XGBoost saves the trees, +some model parameters like number of input columns in trained trees, and the objective +function, which combined to represent the concept of "model" in XGBoost. As for why are +we saving the objective as part of model, that's because objective controls transformation +of global bias (called ``base_score`` in XGBoost) and task-specific information. Users +can share this model with others for prediction, evaluation or continue the training with +a different set of hyper-parameters etc. However, this is not the end of story. There are cases where we need to save something more than just the model itself. For example, in distributed training, XGBoost performs @@ -81,7 +88,10 @@ a filename with ``.json`` or ``.ubj`` as file extension, the latter is the exten JSON files that were produced by an external source may lead to undefined behaviors and crashes. -While for memory snapshot, UBJSON is the default starting with xgboost 1.6. +While for memory snapshot, UBJSON is the default starting with xgboost 1.6. When loading +the model back, XGBoost recognizes the file extensions ``.json`` and ``.ubj``, and can +dispatch accordingly. If the extension is not specified, XGBoost tries to guess the right +one. *************************************************************** A note on backward compatibility of models and memory snapshots diff --git a/tests/python/test_model_io.py b/tests/python/test_model_io.py index dc843a7f4..884bba08f 100644 --- a/tests/python/test_model_io.py +++ b/tests/python/test_model_io.py @@ -254,6 +254,68 @@ class TestBoosterIO: # remove file Path.unlink(save_path) + def test_invalid_postfix(self) -> None: + """Test mis-specified model format, no special hanlding is expected, the + JSON/UBJ parser can emit parsing errors. + + """ + X, y, w = tm.make_regression(64, 16, False) + booster = xgb.train({}, xgb.QuantileDMatrix(X, y, weight=w), num_boost_round=3) + + def rename(src: str, dst: str) -> None: + if os.path.exists(dst): + # Windows cannot overwrite an existing file. + os.remove(dst) + os.rename(src, dst) + + with tempfile.TemporaryDirectory() as tmpdir: + path_dep = os.path.join(tmpdir, "model.deprecated") + # save into deprecated format + with pytest.warns(UserWarning, match="UBJSON"): + booster.save_model(path_dep) + + path_ubj = os.path.join(tmpdir, "model.ubj") + rename(path_dep, path_ubj) + + with pytest.raises(ValueError, match="{"): + xgb.Booster(model_file=path_ubj) + + path_json = os.path.join(tmpdir, "model.json") + rename(path_ubj, path_json) + + with pytest.raises(ValueError, match="{"): + xgb.Booster(model_file=path_json) + + # save into ubj format + booster.save_model(path_ubj) + rename(path_ubj, path_dep) + # deprecated is not a recognized format internally, XGBoost can guess the + # right format + xgb.Booster(model_file=path_dep) + rename(path_dep, path_json) + with pytest.raises(ValueError, match="Expecting"): + xgb.Booster(model_file=path_json) + + # save into JSON format + booster.save_model(path_json) + rename(path_json, path_dep) + # deprecated is not a recognized format internally, XGBoost can guess the + # right format + xgb.Booster(model_file=path_dep) + rename(path_dep, path_ubj) + with pytest.raises(ValueError, match="Expecting"): + xgb.Booster(model_file=path_ubj) + + # save model without file extension + path_no = os.path.join(tmpdir, "model") + with pytest.warns(UserWarning, match="UBJSON"): + booster.save_model(path_no) + + booster_1 = xgb.Booster(model_file=path_no) + r0 = booster.save_raw(raw_format="json") + r1 = booster_1.save_raw(raw_format="json") + assert r0 == r1 + def save_load_model(model_path: str) -> None: from sklearn.datasets import load_digits From fa5e2f6c45175ea1ab197852012af2a6a6f11da3 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 9 Jan 2024 00:54:23 +0800 Subject: [PATCH 52/59] Synthesize the AMES housing dataset for tests. (#9963) --- python-package/xgboost/testing/data.py | 210 +++++++++++++++++++++---- 1 file changed, 178 insertions(+), 32 deletions(-) diff --git a/python-package/xgboost/testing/data.py b/python-package/xgboost/testing/data.py index c0d53f914..0c4f29008 100644 --- a/python-package/xgboost/testing/data.py +++ b/python-package/xgboost/testing/data.py @@ -4,10 +4,11 @@ import os import zipfile from dataclasses import dataclass from typing import ( + TYPE_CHECKING, Any, Callable, + Dict, Generator, - List, NamedTuple, Optional, Tuple, @@ -25,6 +26,11 @@ from scipy import sparse import xgboost from xgboost.data import pandas_pyarrow_mapper +if TYPE_CHECKING: + from ..compat import DataFrame as DataFrameT +else: + DataFrameT = Any + joblib = pytest.importorskip("joblib") memory = joblib.Memory("./cachedir", verbose=0) @@ -256,46 +262,186 @@ def get_sparse() -> Tuple[np.ndarray, np.ndarray]: return X, y +# pylint: disable=too-many-statements @memory.cache -def get_ames_housing() -> Tuple[np.ndarray, np.ndarray]: - """ +def get_ames_housing() -> Tuple[DataFrameT, np.ndarray]: + """Get a synthetic version of the amse housing dataset. + + The real one can be obtained via: + + .. code-block:: + + from sklearn import datasets + + datasets.fetch_openml(data_id=42165, as_frame=True, return_X_y=True) + Number of samples: 1460 Number of features: 20 Number of categorical features: 10 Number of numerical features: 10 """ - datasets = pytest.importorskip("sklearn.datasets") - X, y = datasets.fetch_openml(data_id=42165, as_frame=True, return_X_y=True) + pytest.importorskip("pandas") + import pandas as pd - categorical_columns_subset: List[str] = [ - "BldgType", # 5 cats, no nan - "GarageFinish", # 3 cats, nan - "LotConfig", # 5 cats, no nan - "Functional", # 7 cats, no nan - "MasVnrType", # 4 cats, nan - "HouseStyle", # 8 cats, no nan - "FireplaceQu", # 5 cats, nan - "ExterCond", # 5 cats, no nan - "ExterQual", # 4 cats, no nan - "PoolQC", # 3 cats, nan - ] + rng = np.random.default_rng(1994) + n_samples = 1460 + df = pd.DataFrame() - numerical_columns_subset: List[str] = [ - "3SsnPorch", - "Fireplaces", - "BsmtHalfBath", - "HalfBath", - "GarageCars", - "TotRmsAbvGrd", - "BsmtFinSF1", - "BsmtFinSF2", - "GrLivArea", - "ScreenPorch", - ] + def synth_cat( + name_proba: Dict[Union[str, float], float], density: float + ) -> pd.Series: + n_nulls = int(n_samples * (1 - density)) + has_nan = np.abs(1.0 - density) > 1e-6 and n_nulls > 0 + if has_nan: + sparsity = 1.0 - density + name_proba[np.nan] = sparsity - X = X[categorical_columns_subset + numerical_columns_subset] - X[categorical_columns_subset] = X[categorical_columns_subset].astype("category") - return X, y + keys = list(name_proba.keys()) + p = list(name_proba.values()) + p[-1] += 1.0 - np.sum(p) # Fix floating point error + x = rng.choice(keys, size=n_samples, p=p) + + series = pd.Series( + x, + dtype=pd.CategoricalDtype( + # not NA + filter(lambda x: isinstance(x, str), keys) + ), + ) + return series + + df["BldgType"] = synth_cat( + { + "1Fam": 0.835616, + "2fmCon": 0.078082, + "Duplex": 0.035616, + "Twnhs": 0.029452, + "TwnhsE": 0.021233, + }, + 1.0, + ) + df["GarageFinish"] = synth_cat( + {"Unf": 0.414384, "RFn": 0.289041, "Fin": 0.241096}, 0.94452 + ) + df["LotConfig"] = synth_cat( + { + "Corner": 0.180137, + "CulDSac": 0.064384, + "FR2": 0.032192, + "FR3": 0.002740, + }, + 1.0, + ) + df["Functional"] = synth_cat( + { + "Typ": 0.931506, + "Min2": 0.023287, + "Min1": 0.021232, + "Mod": 0.010273, + "Maj1": 0.009589, + "Maj2": 0.003424, + "Sev": 0.000684, + }, + 1.0, + ) + df["MasVnrType"] = synth_cat( + { + "None": 0.591780, + "BrkFace": 0.304794, + "Stone": 0.087671, + "BrkCmn": 0.010273, + }, + 0.99452, + ) + df["HouseStyle"] = synth_cat( + { + "1Story": 0.497260, + "2Story": 0.304794, + "1.5Fin": 0.105479, + "SLvl": 0.044520, + "SFoyer": 0.025342, + "1.5Unf": 0.009589, + "2.5Unf": 0.007534, + "2.5Fin": 0.005479, + }, + 1.0, + ) + df["FireplaceQu"] = synth_cat( + { + "Gd": 0.260273, + "TA": 0.214383, + "Fa": 0.022602, + "Ex": 0.016438, + "Po": 0.013698, + }, + 0.527397, + ) + df["ExterCond"] = synth_cat( + { + "TA": 0.878082, + "Gd": 0.1, + "Fa": 0.019178, + "Ex": 0.002054, + "Po": 0.000684, + }, + 1.0, + ) + df["ExterQual"] = synth_cat( + { + "TA": 0.620547, + "Gd": 0.334246, + "Ex": 0.035616, + "Fa": 0.009589, + }, + 1.0, + ) + df["PoolQC"] = synth_cat( + { + "Gd": 0.002054, + "Ex": 0.001369, + "Fa": 0.001369, + }, + 0.004794, + ) + + # We focus on the cateogircal values here, for numerical features, simple normal + # distribution is used, which doesn't match the original data. + def synth_num(loc: float, std: float, density: float) -> pd.Series: + x = rng.normal(loc=loc, scale=std, size=n_samples) + n_nulls = int(n_samples * (1 - density)) + if np.abs(1.0 - density) > 1e-6 and n_nulls > 0: + null_idx = rng.choice(n_samples, size=n_nulls, replace=False) + x[null_idx] = np.nan + return pd.Series(x, dtype=np.float64) + + df["3SsnPorch"] = synth_num(3.4095890410958902, 29.31733055678188, 1.0) + df["Fireplaces"] = synth_num(0.613013698630137, 0.6446663863122295, 1.0) + df["BsmtHalfBath"] = synth_num(0.057534246575342465, 0.23875264627921178, 1.0) + df["HalfBath"] = synth_num(0.38287671232876713, 0.5028853810928914, 1.0) + df["GarageCars"] = synth_num(1.7671232876712328, 0.7473150101111095, 1.0) + df["TotRmsAbvGrd"] = synth_num(6.517808219178082, 1.6253932905840505, 1.0) + df["BsmtFinSF1"] = synth_num(443.6397260273973, 456.0980908409277, 1.0) + df["BsmtFinSF2"] = synth_num(46.54931506849315, 161.31927280654173, 1.0) + df["GrLivArea"] = synth_num(1515.463698630137, 525.4803834232025, 1.0) + df["ScreenPorch"] = synth_num(15.060958904109588, 55.757415281874174, 1.0) + + columns = list(df.columns) + rng.shuffle(columns) + df = df[columns] + + # linear interaction for testing purposes. + y = np.zeros(shape=(n_samples,)) + for c in df.columns: + if isinstance(df[c].dtype, pd.CategoricalDtype): + y += df[c].cat.codes.astype(np.float64) + else: + y += df[c].values + + # Shift and scale to match the original y. + y *= 79442.50288288662 / y.std() + y += 180921.19589041095 - y.mean() + + return df, y @memory.cache From b3eb5d09452fccc26f95684a1786d91a5f1aeb8d Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 9 Jan 2024 03:22:15 +0800 Subject: [PATCH 53/59] Use UBJ in Python checkpoint. (#9958) --- demo/guide-python/callbacks.py | 33 ++++++++++++------ doc/python/python_api.rst | 4 --- python-package/xgboost/callback.py | 34 ++++++++++++++++--- python-package/xgboost/core.py | 29 ++++++++-------- tests/ci_build/lint_python.py | 3 ++ tests/python/test_callback.py | 17 ++++++---- .../test_with_dask/test_with_dask.py | 30 ++++++++++++---- 7 files changed, 104 insertions(+), 46 deletions(-) diff --git a/demo/guide-python/callbacks.py b/demo/guide-python/callbacks.py index 9c12f70de..0676c732e 100644 --- a/demo/guide-python/callbacks.py +++ b/demo/guide-python/callbacks.py @@ -7,6 +7,7 @@ Demo for using and defining callback functions import argparse import os import tempfile +from typing import Dict import numpy as np from matplotlib import pyplot as plt @@ -17,24 +18,26 @@ import xgboost as xgb class Plotting(xgb.callback.TrainingCallback): - """Plot evaluation result during training. Only for demonstration purpose as it's quite - slow to draw. + """Plot evaluation result during training. Only for demonstration purpose as it's + quite slow to draw using matplotlib. """ - def __init__(self, rounds): + def __init__(self, rounds: int) -> None: self.fig = plt.figure() self.ax = self.fig.add_subplot(111) self.rounds = rounds - self.lines = {} + self.lines: Dict[str, plt.Line2D] = {} self.fig.show() self.x = np.linspace(0, self.rounds, self.rounds) plt.ion() - def _get_key(self, data, metric): + def _get_key(self, data: str, metric: str) -> str: return f"{data}-{metric}" - def after_iteration(self, model, epoch, evals_log): + def after_iteration( + self, model: xgb.Booster, epoch: int, evals_log: Dict[str, dict] + ) -> bool: """Update the plot.""" if not self.lines: for data, metric in evals_log.items(): @@ -55,7 +58,7 @@ class Plotting(xgb.callback.TrainingCallback): return False -def custom_callback(): +def custom_callback() -> None: """Demo for defining a custom callback function that plots evaluation result during training.""" X, y = load_breast_cancer(return_X_y=True) @@ -82,19 +85,27 @@ def custom_callback(): ) -def check_point_callback(): - # only for demo, set a larger value (like 100) in practice as checkpointing is quite +def check_point_callback() -> None: + """Demo for using the checkpoint callback. Custom logic for handling output is + usually required and users are encouraged to define their own callback for + checkpointing operations. The builtin one can be used as a starting point. + + """ + # Only for demo, set a larger value (like 100) in practice as checkpointing is quite # slow. rounds = 2 - def check(as_pickle): + def check(as_pickle: bool) -> None: for i in range(0, 10, rounds): if i == 0: continue if as_pickle: path = os.path.join(tmpdir, "model_" + str(i) + ".pkl") else: - path = os.path.join(tmpdir, "model_" + str(i) + ".json") + path = os.path.join( + tmpdir, + f"model_{i}.{xgb.callback.TrainingCheckPoint.default_format}", + ) assert os.path.exists(path) X, y = load_breast_cancer(return_X_y=True) diff --git a/doc/python/python_api.rst b/doc/python/python_api.rst index 38b22a994..4ba520fe4 100644 --- a/doc/python/python_api.rst +++ b/doc/python/python_api.rst @@ -88,22 +88,18 @@ Callback API .. autoclass:: xgboost.callback.EvaluationMonitor :members: - :inherited-members: :show-inheritance: .. autoclass:: xgboost.callback.EarlyStopping :members: - :inherited-members: :show-inheritance: .. autoclass:: xgboost.callback.LearningRateScheduler :members: - :inherited-members: :show-inheritance: .. autoclass:: xgboost.callback.TrainingCheckPoint :members: - :inherited-members: :show-inheritance: .. _dask_api: diff --git a/python-package/xgboost/callback.py b/python-package/xgboost/callback.py index 29d880539..b8d5a751e 100644 --- a/python-package/xgboost/callback.py +++ b/python-package/xgboost/callback.py @@ -62,11 +62,31 @@ class TrainingCallback(ABC): return model def before_iteration(self, model: _Model, epoch: int, evals_log: EvalsLog) -> bool: - """Run before each iteration. Return True when training should stop.""" + """Run before each iteration. Returns True when training should stop. See + :py:meth:`after_iteration` for details. + + """ return False def after_iteration(self, model: _Model, epoch: int, evals_log: EvalsLog) -> bool: - """Run after each iteration. Return True when training should stop.""" + """Run after each iteration. Returns `True` when training should stop. + + Parameters + ---------- + + model : + Eeither a :py:class:`~xgboost.Booster` object or a CVPack if the cv function + in xgboost is being used. + epoch : + The current training iteration. + evals_log : + A dictionary containing the evaluation history: + + .. code-block:: python + + {"data_name": {"metric_name": [0.5, ...]}} + + """ return False @@ -547,14 +567,16 @@ class TrainingCheckPoint(TrainingCallback): .. versionadded:: 1.3.0 + Since XGBoost 2.1.0, the default format is changed to UBJSON. + Parameters ---------- directory : Output model directory. name : - pattern of output model file. Models will be saved as name_0.json, name_1.json, - name_2.json .... + pattern of output model file. Models will be saved as name_0.ubj, name_1.ubj, + name_2.ubj .... as_pickle : When set to True, all training parameters will be saved in pickle format, instead of saving only the model. @@ -564,6 +586,8 @@ class TrainingCheckPoint(TrainingCallback): """ + default_format = "ubj" + def __init__( self, directory: Union[str, os.PathLike], @@ -592,7 +616,7 @@ class TrainingCheckPoint(TrainingCallback): self._name + "_" + (str(epoch + self._start)) - + (".pkl" if self._as_pickle else ".json"), + + (".pkl" if self._as_pickle else f".{self.default_format}"), ) self._epoch = 0 # reset counter if collective.get_rank() == 0: diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index d554a8d3a..4d093293d 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -2591,9 +2591,8 @@ class Booster: The model is saved in an XGBoost internal format which is universal among the various XGBoost interfaces. Auxiliary attributes of the Python Booster object - (such as feature_names) will not be saved when using binary format. To save - those attributes, use JSON/UBJ instead. See :doc:`Model IO - ` for more info. + (such as feature_names) are only saved when using JSON or UBJSON (default) + format. See :doc:`Model IO ` for more info. .. code-block:: python @@ -2616,12 +2615,15 @@ class Booster: def save_raw(self, raw_format: str = "ubj") -> bytearray: """Save the model to a in memory buffer representation instead of file. + The model is saved in an XGBoost internal format which is universal among the + various XGBoost interfaces. Auxiliary attributes of the Python Booster object + (such as feature_names) are only saved when using JSON or UBJSON (default) + format. See :doc:`Model IO ` for more info. + Parameters ---------- raw_format : - Format of output buffer. Can be `json`, `ubj` or `deprecated`. Right now - the default is `deprecated` but it will be changed to `ubj` (univeral binary - json) in the future. + Format of output buffer. Can be `json`, `ubj` or `deprecated`. Returns ------- @@ -2640,11 +2642,10 @@ class Booster: def load_model(self, fname: ModelIn) -> None: """Load the model from a file or a bytearray. - The model is loaded from XGBoost format which is universal among the various - XGBoost interfaces. Auxiliary attributes of the Python Booster object (such as - feature_names) will not be loaded when using binary format. To save those - attributes, use JSON/UBJ instead. See :doc:`Model IO ` - for more info. + The model is saved in an XGBoost internal format which is universal among the + various XGBoost interfaces. Auxiliary attributes of the Python Booster object + (such as feature_names) are only saved when using JSON or UBJSON (default) + format. See :doc:`Model IO ` for more info. .. code-block:: python @@ -2769,9 +2770,9 @@ class Booster: with_stats: bool = False, dump_format: str = "text", ) -> List[str]: - """Returns the model dump as a list of strings. Unlike :py:meth:`save_model`, the output - format is primarily used for visualization or interpretation, hence it's more - human readable but cannot be loaded back to XGBoost. + """Returns the model dump as a list of strings. Unlike :py:meth:`save_model`, + the output format is primarily used for visualization or interpretation, hence + it's more human readable but cannot be loaded back to XGBoost. Parameters ---------- diff --git a/tests/ci_build/lint_python.py b/tests/ci_build/lint_python.py index 1e414f3b5..87d76607f 100644 --- a/tests/ci_build/lint_python.py +++ b/tests/ci_build/lint_python.py @@ -31,6 +31,8 @@ class LintersPaths: "tests/python/test_with_pandas.py", "tests/python-gpu/", "tests/python-sycl/", + "tests/test_distributed/test_with_dask/", + "tests/test_distributed/test_gpu_with_dask/", "tests/test_distributed/test_with_spark/", "tests/test_distributed/test_gpu_with_spark/", # demo @@ -91,6 +93,7 @@ class LintersPaths: # demo "demo/json-model/json_parser.py", "demo/guide-python/external_memory.py", + "demo/guide-python/callbacks.py", "demo/guide-python/cat_in_the_dat.py", "demo/guide-python/categorical.py", "demo/guide-python/cat_pipeline.py", diff --git a/tests/python/test_callback.py b/tests/python/test_callback.py index 4893ad074..3a7501e48 100644 --- a/tests/python/test_callback.py +++ b/tests/python/test_callback.py @@ -244,7 +244,7 @@ class TestCallbacks: assert booster.num_boosted_rounds() == booster.best_iteration + 1 with tempfile.TemporaryDirectory() as tmpdir: - path = os.path.join(tmpdir, 'model.json') + path = os.path.join(tmpdir, "model.json") cls.save_model(path) cls = xgb.XGBClassifier() cls.load_model(path) @@ -378,7 +378,7 @@ class TestCallbacks: scheduler = xgb.callback.LearningRateScheduler dtrain, dtest = tm.load_agaricus(__file__) - watchlist = [(dtest, 'eval'), (dtrain, 'train')] + watchlist = [(dtest, "eval"), (dtrain, "train")] param = { "max_depth": 2, @@ -429,7 +429,7 @@ class TestCallbacks: assert tree_3th_0["split_conditions"] != tree_3th_1["split_conditions"] @pytest.mark.parametrize("tree_method", ["hist", "approx", "approx"]) - def test_eta_decay(self, tree_method): + def test_eta_decay(self, tree_method: str) -> None: self.run_eta_decay(tree_method) @pytest.mark.parametrize( @@ -446,7 +446,7 @@ class TestCallbacks: def test_eta_decay_leaf_output(self, tree_method: str, objective: str) -> None: self.run_eta_decay_leaf_output(tree_method, objective) - def test_check_point(self): + def test_check_point(self) -> None: from sklearn.datasets import load_breast_cancer X, y = load_breast_cancer(return_X_y=True) @@ -463,7 +463,12 @@ class TestCallbacks: callbacks=[check_point], ) for i in range(1, 10): - assert os.path.exists(os.path.join(tmpdir, "model_" + str(i) + ".json")) + assert os.path.exists( + os.path.join( + tmpdir, + f"model_{i}.{xgb.callback.TrainingCheckPoint.default_format}", + ) + ) check_point = xgb.callback.TrainingCheckPoint( directory=tmpdir, interval=1, as_pickle=True, name="model" @@ -478,7 +483,7 @@ class TestCallbacks: for i in range(1, 10): assert os.path.exists(os.path.join(tmpdir, "model_" + str(i) + ".pkl")) - def test_callback_list(self): + def test_callback_list(self) -> None: X, y = tm.data.get_california_housing() m = xgb.DMatrix(X, y) callbacks = [xgb.callback.EarlyStopping(rounds=10)] diff --git a/tests/test_distributed/test_with_dask/test_with_dask.py b/tests/test_distributed/test_with_dask/test_with_dask.py index d380f0dee..79df025fe 100644 --- a/tests/test_distributed/test_with_dask/test_with_dask.py +++ b/tests/test_distributed/test_with_dask/test_with_dask.py @@ -1590,7 +1590,7 @@ class TestWithDask: @given( params=hist_parameter_strategy, cache_param=hist_cache_strategy, - dataset=tm.make_dataset_strategy() + dataset=tm.make_dataset_strategy(), ) @settings( deadline=None, max_examples=10, suppress_health_check=suppress, print_blob=True @@ -2250,16 +2250,27 @@ class TestDaskCallbacks: ], ) for i in range(1, 10): - assert os.path.exists(os.path.join(tmpdir, "model_" + str(i) + ".json")) + assert os.path.exists( + os.path.join( + tmpdir, + f"model_{i}.{xgb.callback.TrainingCheckPoint.default_format}", + ) + ) -@gen_cluster(client=True, clean_kwargs={"processes": False, "threads": False}, allow_unclosed=True) +@gen_cluster( + client=True, + clean_kwargs={"processes": False, "threads": False}, + allow_unclosed=True, +) async def test_worker_left(c, s, a, b): async with Worker(s.address): dx = da.random.random((1000, 10)).rechunk(chunks=(10, None)) dy = da.random.random((1000,)).rechunk(chunks=(10,)) d_train = await xgb.dask.DaskDMatrix( - c, dx, dy, + c, + dx, + dy, ) await async_poll_for(lambda: len(s.workers) == 2, timeout=5) with pytest.raises(RuntimeError, match="Missing"): @@ -2271,12 +2282,19 @@ async def test_worker_left(c, s, a, b): ) -@gen_cluster(client=True, Worker=Nanny, clean_kwargs={"processes": False, "threads": False}, allow_unclosed=True) +@gen_cluster( + client=True, + Worker=Nanny, + clean_kwargs={"processes": False, "threads": False}, + allow_unclosed=True, +) async def test_worker_restarted(c, s, a, b): dx = da.random.random((1000, 10)).rechunk(chunks=(10, None)) dy = da.random.random((1000,)).rechunk(chunks=(10,)) d_train = await xgb.dask.DaskDMatrix( - c, dx, dy, + c, + dx, + dy, ) await c.restart_workers([a.worker_address]) with pytest.raises(RuntimeError, match="Missing"): From 7ff6d44efaf5dfe09320f2a67a1c8ae99370c62b Mon Sep 17 00:00:00 2001 From: david-cortes Date: Mon, 8 Jan 2024 20:43:21 +0100 Subject: [PATCH 54/59] [R] Use R's error stream for printing warnings (#9965) --- R-package/src/xgboost_custom.cc | 6 +++++- R-package/tests/testthat/test_basic.R | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/R-package/src/xgboost_custom.cc b/R-package/src/xgboost_custom.cc index f196297ec..6aaa3696a 100644 --- a/R-package/src/xgboost_custom.cc +++ b/R-package/src/xgboost_custom.cc @@ -17,7 +17,11 @@ namespace xgboost { ConsoleLogger::~ConsoleLogger() { if (cur_verbosity_ == LogVerbosity::kIgnore || cur_verbosity_ <= GlobalVerbosity()) { - dmlc::CustomLogMessage::Log(log_stream_.str()); + if (cur_verbosity_ == LogVerbosity::kWarning) { + REprintf("%s\n", log_stream_.str().c_str()); + } else { + dmlc::CustomLogMessage::Log(log_stream_.str()); + } } } TrackerLogger::~TrackerLogger() { diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index d4b3a6be3..1f6cebbca 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -83,7 +83,8 @@ test_that("parameter validation works", { bar = "foo" ) output <- capture.output( - xgb.train(params = params, data = dtrain, nrounds = nrounds) + xgb.train(params = params, data = dtrain, nrounds = nrounds), + type = "message" ) print(output) } From bed0349954d6acc77de88905e188b744019b2d33 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Mon, 8 Jan 2024 20:43:48 +0100 Subject: [PATCH 55/59] [R] Don't write files to user's directory (#9966) --- R-package/R/utils.R | 17 ++++----- R-package/R/xgb.Booster.R | 12 +++---- R-package/R/xgb.DMatrix.R | 6 ++-- R-package/R/xgb.DMatrix.save.R | 6 ++-- R-package/R/xgb.load.R | 6 ++-- R-package/R/xgb.save.R | 6 ++-- R-package/inst/make-r-def.R | 2 +- .../a-compatibility-note-for-saveRDS-save.Rd | 17 ++++----- R-package/man/xgb.Booster.complete.Rd | 6 ++-- R-package/man/xgb.DMatrix.Rd | 6 ++-- R-package/man/xgb.DMatrix.save.Rd | 6 ++-- R-package/man/xgb.attr.Rd | 6 ++-- R-package/man/xgb.load.Rd | 6 ++-- R-package/man/xgb.save.Rd | 6 ++-- R-package/tests/testthat/test_basic.R | 8 ++--- R-package/tests/testthat/test_callbacks.R | 24 ++++++------- R-package/tests/testthat/test_dmatrix.R | 36 ++++++++++--------- R-package/tests/testthat/test_helpers.R | 18 +++++----- R-package/vignettes/xgboostPresentation.Rmd | 14 ++++---- 19 files changed, 108 insertions(+), 100 deletions(-) diff --git a/R-package/R/utils.R b/R-package/R/utils.R index 1798e4ad1..c011ab8ed 100644 --- a/R-package/R/utils.R +++ b/R-package/R/utils.R @@ -388,13 +388,14 @@ NULL #' objective = "binary:logistic") #' #' # Save as a stand-alone file; load it with xgb.load() -#' xgb.save(bst, 'xgb.model') -#' bst2 <- xgb.load('xgb.model') +#' fname <- file.path(tempdir(), "xgb_model.ubj") +#' xgb.save(bst, fname) +#' bst2 <- xgb.load(fname) #' #' # Save as a stand-alone file (JSON); load it with xgb.load() -#' xgb.save(bst, 'xgb.model.json') -#' bst2 <- xgb.load('xgb.model.json') -#' if (file.exists('xgb.model.json')) file.remove('xgb.model.json') +#' fname <- file.path(tempdir(), "xgb_model.json") +#' xgb.save(bst, fname) +#' bst2 <- xgb.load(fname) #' #' # Save as a raw byte vector; load it with xgb.load.raw() #' xgb_bytes <- xgb.save.raw(bst) @@ -405,12 +406,12 @@ NULL #' # Persist the R object. Here, saveRDS() is okay, since it doesn't persist #' # xgb.Booster directly. What's being persisted is the future-proof byte representation #' # as given by xgb.save.raw(). -#' saveRDS(obj, 'my_object.rds') +#' fname <- file.path(tempdir(), "my_object.Rds") +#' saveRDS(obj, fname) #' # Read back the R object -#' obj2 <- readRDS('my_object.rds') +#' obj2 <- readRDS(fname) #' # Re-construct xgb.Booster object from the bytes #' bst2 <- xgb.load.raw(obj2$xgb_model_bytes) -#' if (file.exists('my_object.rds')) file.remove('my_object.rds') #' #' @name a-compatibility-note-for-saveRDS-save NULL diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index 371f3d129..9fdfc9dd6 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -118,12 +118,12 @@ xgb.get.handle <- function(object) { #' objective = "binary:logistic" #' ) #' -#' saveRDS(bst, "xgb.model.rds") +#' fname <- file.path(tempdir(), "xgb_model.Rds") +#' saveRDS(bst, fname) #' #' # Warning: The resulting RDS file is only compatible with the current XGBoost version. #' # Refer to the section titled "a-compatibility-note-for-saveRDS-save". -#' bst1 <- readRDS("xgb.model.rds") -#' if (file.exists("xgb.model.rds")) file.remove("xgb.model.rds") +#' bst1 <- readRDS(fname) #' # the handle is invalid: #' print(bst1$handle) #' @@ -580,9 +580,9 @@ predict.xgb.Booster.handle <- function(object, ...) { #' print(xgb.attr(bst, "my_attribute")) #' xgb.attributes(bst) <- list(a = 123, b = "abc") #' -#' xgb.save(bst, "xgb.model") -#' bst1 <- xgb.load("xgb.model") -#' if (file.exists("xgb.model")) file.remove("xgb.model") +#' fname <- file.path(tempdir(), "xgb.ubj") +#' xgb.save(bst, fname) +#' bst1 <- xgb.load(fname) #' print(xgb.attr(bst1, "my_attribute")) #' print(xgb.attributes(bst1)) #' diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index cc16e18da..f9a810c85 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -56,9 +56,9 @@ #' dtrain <- with( #' agaricus.train, xgb.DMatrix(data, label = label, nthread = nthread) #' ) -#' xgb.DMatrix.save(dtrain, 'xgb.DMatrix.data') -#' dtrain <- xgb.DMatrix('xgb.DMatrix.data') -#' if (file.exists('xgb.DMatrix.data')) file.remove('xgb.DMatrix.data') +#' fname <- file.path(tempdir(), "xgb.DMatrix.data") +#' xgb.DMatrix.save(dtrain, fname) +#' dtrain <- xgb.DMatrix(fname) #' @export xgb.DMatrix <- function( data, diff --git a/R-package/R/xgb.DMatrix.save.R b/R-package/R/xgb.DMatrix.save.R index 91edb32d0..ef4599d0e 100644 --- a/R-package/R/xgb.DMatrix.save.R +++ b/R-package/R/xgb.DMatrix.save.R @@ -8,9 +8,9 @@ #' @examples #' data(agaricus.train, package='xgboost') #' dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) -#' xgb.DMatrix.save(dtrain, 'xgb.DMatrix.data') -#' dtrain <- xgb.DMatrix('xgb.DMatrix.data') -#' if (file.exists('xgb.DMatrix.data')) file.remove('xgb.DMatrix.data') +#' fname <- file.path(tempdir(), "xgb.DMatrix.data") +#' xgb.DMatrix.save(dtrain, fname) +#' dtrain <- xgb.DMatrix(fname) #' @export xgb.DMatrix.save <- function(dmatrix, fname) { if (typeof(fname) != "character") diff --git a/R-package/R/xgb.load.R b/R-package/R/xgb.load.R index e8f9e0023..27844ed8c 100644 --- a/R-package/R/xgb.load.R +++ b/R-package/R/xgb.load.R @@ -38,9 +38,9 @@ #' objective = "binary:logistic" #' ) #' -#' xgb.save(bst, 'xgb.model') -#' bst <- xgb.load('xgb.model') -#' if (file.exists('xgb.model')) file.remove('xgb.model') +#' fname <- file.path(tempdir(), "xgb.ubj") +#' xgb.save(bst, fname) +#' bst <- xgb.load(fname) #' @export xgb.load <- function(modelfile) { if (is.null(modelfile)) diff --git a/R-package/R/xgb.save.R b/R-package/R/xgb.save.R index 32b7d9618..474153bda 100644 --- a/R-package/R/xgb.save.R +++ b/R-package/R/xgb.save.R @@ -40,9 +40,9 @@ #' nrounds = 2, #' objective = "binary:logistic" #' ) -#' xgb.save(bst, 'xgb.model') -#' bst <- xgb.load('xgb.model') -#' if (file.exists('xgb.model')) file.remove('xgb.model') +#' fname <- file.path(tempdir(), "xgb.ubj") +#' xgb.save(bst, fname) +#' bst <- xgb.load(fname) #' @export xgb.save <- function(model, fname) { if (typeof(fname) != "character") diff --git a/R-package/inst/make-r-def.R b/R-package/inst/make-r-def.R index f40e54c56..c187b10a0 100644 --- a/R-package/inst/make-r-def.R +++ b/R-package/inst/make-r-def.R @@ -55,7 +55,7 @@ message(sprintf("Creating '%s' from '%s'", OUT_DEF_FILE, IN_DLL_FILE)) } # use objdump to dump all the symbols -OBJDUMP_FILE <- "objdump-out.txt" +OBJDUMP_FILE <- file.path(tempdir(), "objdump-out.txt") .pipe_shell_command_to_stdout( command = "objdump" , args = c("-p", IN_DLL_FILE) diff --git a/R-package/man/a-compatibility-note-for-saveRDS-save.Rd b/R-package/man/a-compatibility-note-for-saveRDS-save.Rd index 023cff9fd..a8f46547e 100644 --- a/R-package/man/a-compatibility-note-for-saveRDS-save.Rd +++ b/R-package/man/a-compatibility-note-for-saveRDS-save.Rd @@ -38,13 +38,14 @@ bst <- xgb.train(data = xgb.DMatrix(agaricus.train$data, label = agaricus.train$ objective = "binary:logistic") # Save as a stand-alone file; load it with xgb.load() -xgb.save(bst, 'xgb.model') -bst2 <- xgb.load('xgb.model') +fname <- file.path(tempdir(), "xgb_model.ubj") +xgb.save(bst, fname) +bst2 <- xgb.load(fname) # Save as a stand-alone file (JSON); load it with xgb.load() -xgb.save(bst, 'xgb.model.json') -bst2 <- xgb.load('xgb.model.json') -if (file.exists('xgb.model.json')) file.remove('xgb.model.json') +fname <- file.path(tempdir(), "xgb_model.json") +xgb.save(bst, fname) +bst2 <- xgb.load(fname) # Save as a raw byte vector; load it with xgb.load.raw() xgb_bytes <- xgb.save.raw(bst) @@ -55,11 +56,11 @@ obj <- list(xgb_model_bytes = xgb.save.raw(bst), description = "My first XGBoost # Persist the R object. Here, saveRDS() is okay, since it doesn't persist # xgb.Booster directly. What's being persisted is the future-proof byte representation # as given by xgb.save.raw(). -saveRDS(obj, 'my_object.rds') +fname <- file.path(tempdir(), "my_object.Rds") +saveRDS(obj, fname) # Read back the R object -obj2 <- readRDS('my_object.rds') +obj2 <- readRDS(fname) # Re-construct xgb.Booster object from the bytes bst2 <- xgb.load.raw(obj2$xgb_model_bytes) -if (file.exists('my_object.rds')) file.remove('my_object.rds') } diff --git a/R-package/man/xgb.Booster.complete.Rd b/R-package/man/xgb.Booster.complete.Rd index 0adb1a69f..102224a8f 100644 --- a/R-package/man/xgb.Booster.complete.Rd +++ b/R-package/man/xgb.Booster.complete.Rd @@ -45,12 +45,12 @@ bst <- xgboost( objective = "binary:logistic" ) -saveRDS(bst, "xgb.model.rds") +fname <- file.path(tempdir(), "xgb_model.Rds") +saveRDS(bst, fname) # Warning: The resulting RDS file is only compatible with the current XGBoost version. # Refer to the section titled "a-compatibility-note-for-saveRDS-save". -bst1 <- readRDS("xgb.model.rds") -if (file.exists("xgb.model.rds")) file.remove("xgb.model.rds") +bst1 <- readRDS(fname) # the handle is invalid: print(bst1$handle) diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index 01436ba14..eb667377f 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -94,7 +94,7 @@ data.table::setDTthreads(nthread) dtrain <- with( agaricus.train, xgb.DMatrix(data, label = label, nthread = nthread) ) -xgb.DMatrix.save(dtrain, 'xgb.DMatrix.data') -dtrain <- xgb.DMatrix('xgb.DMatrix.data') -if (file.exists('xgb.DMatrix.data')) file.remove('xgb.DMatrix.data') +fname <- file.path(tempdir(), "xgb.DMatrix.data") +xgb.DMatrix.save(dtrain, fname) +dtrain <- xgb.DMatrix(fname) } diff --git a/R-package/man/xgb.DMatrix.save.Rd b/R-package/man/xgb.DMatrix.save.Rd index 401516059..d5c0563b3 100644 --- a/R-package/man/xgb.DMatrix.save.Rd +++ b/R-package/man/xgb.DMatrix.save.Rd @@ -17,7 +17,7 @@ Save xgb.DMatrix object to binary file \examples{ data(agaricus.train, package='xgboost') dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) -xgb.DMatrix.save(dtrain, 'xgb.DMatrix.data') -dtrain <- xgb.DMatrix('xgb.DMatrix.data') -if (file.exists('xgb.DMatrix.data')) file.remove('xgb.DMatrix.data') +fname <- file.path(tempdir(), "xgb.DMatrix.data") +xgb.DMatrix.save(dtrain, fname) +dtrain <- xgb.DMatrix(fname) } diff --git a/R-package/man/xgb.attr.Rd b/R-package/man/xgb.attr.Rd index 9203e6281..2aab62812 100644 --- a/R-package/man/xgb.attr.Rd +++ b/R-package/man/xgb.attr.Rd @@ -79,9 +79,9 @@ xgb.attr(bst, "my_attribute") <- "my attribute value" print(xgb.attr(bst, "my_attribute")) xgb.attributes(bst) <- list(a = 123, b = "abc") -xgb.save(bst, "xgb.model") -bst1 <- xgb.load("xgb.model") -if (file.exists("xgb.model")) file.remove("xgb.model") +fname <- file.path(tempdir(), "xgb.ubj") +xgb.save(bst, fname) +bst1 <- xgb.load(fname) print(xgb.attr(bst1, "my_attribute")) print(xgb.attributes(bst1)) diff --git a/R-package/man/xgb.load.Rd b/R-package/man/xgb.load.Rd index 63f551d7a..e6a2d6cdd 100644 --- a/R-package/man/xgb.load.Rd +++ b/R-package/man/xgb.load.Rd @@ -43,9 +43,9 @@ bst <- xgb.train( objective = "binary:logistic" ) -xgb.save(bst, 'xgb.model') -bst <- xgb.load('xgb.model') -if (file.exists('xgb.model')) file.remove('xgb.model') +fname <- file.path(tempdir(), "xgb.ubj") +xgb.save(bst, fname) +bst <- xgb.load(fname) } \seealso{ \code{\link{xgb.save}}, \code{\link{xgb.Booster.complete}}. diff --git a/R-package/man/xgb.save.Rd b/R-package/man/xgb.save.Rd index 22c6c8fa3..ee4b799c5 100644 --- a/R-package/man/xgb.save.Rd +++ b/R-package/man/xgb.save.Rd @@ -46,9 +46,9 @@ bst <- xgb.train( nrounds = 2, objective = "binary:logistic" ) -xgb.save(bst, 'xgb.model') -bst <- xgb.load('xgb.model') -if (file.exists('xgb.model')) file.remove('xgb.model') +fname <- file.path(tempdir(), "xgb.ubj") +xgb.save(bst, fname) +bst <- xgb.load(fname) } \seealso{ \code{\link{xgb.load}}, \code{\link{xgb.Booster.complete}}. diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index 1f6cebbca..44c530c90 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -330,17 +330,17 @@ test_that("training continuation works", { } expect_equal(dim(bst2$evaluation_log), c(2, 2)) # test continuing from a model in file - xgb.save(bst1, "xgboost.json") - bst2 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, xgb_model = "xgboost.json") + fname <- file.path(tempdir(), "xgboost.json") + xgb.save(bst1, fname) + bst2 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, xgb_model = fname) if (!windows_flag && !solaris_flag) { expect_equal(bst$raw, bst2$raw) } expect_equal(dim(bst2$evaluation_log), c(2, 2)) - file.remove("xgboost.json") }) test_that("model serialization works", { - out_path <- "model_serialization" + out_path <- file.path(tempdir(), "model_serialization") dtrain <- xgb.DMatrix(train$data, label = train$label, nthread = n_threads) watchlist <- list(train = dtrain) param <- list(objective = "binary:logistic", nthread = n_threads) diff --git a/R-package/tests/testthat/test_callbacks.R b/R-package/tests/testthat/test_callbacks.R index de5150380..bee98f688 100644 --- a/R-package/tests/testthat/test_callbacks.R +++ b/R-package/tests/testthat/test_callbacks.R @@ -174,16 +174,17 @@ test_that("cb.reset.parameters works as expected", { test_that("cb.save.model works as expected", { files <- c('xgboost_01.json', 'xgboost_02.json', 'xgboost.json') + files <- unname(sapply(files, function(f) file.path(tempdir(), f))) for (f in files) if (file.exists(f)) file.remove(f) bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, eta = 1, verbose = 0, - save_period = 1, save_name = "xgboost_%02d.json") - expect_true(file.exists('xgboost_01.json')) - expect_true(file.exists('xgboost_02.json')) - b1 <- xgb.load('xgboost_01.json') + save_period = 1, save_name = file.path(tempdir(), "xgboost_%02d.json")) + expect_true(file.exists(files[1])) + expect_true(file.exists(files[2])) + b1 <- xgb.load(files[1]) xgb.parameters(b1) <- list(nthread = 2) expect_equal(xgb.ntree(b1), 1) - b2 <- xgb.load('xgboost_02.json') + b2 <- xgb.load(files[2]) xgb.parameters(b2) <- list(nthread = 2) expect_equal(xgb.ntree(b2), 2) @@ -193,9 +194,9 @@ test_that("cb.save.model works as expected", { # save_period = 0 saves the last iteration's model bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, eta = 1, verbose = 0, - save_period = 0, save_name = 'xgboost.json') - expect_true(file.exists('xgboost.json')) - b2 <- xgb.load('xgboost.json') + save_period = 0, save_name = file.path(tempdir(), 'xgboost.json')) + expect_true(file.exists(files[3])) + b2 <- xgb.load(files[3]) xgb.config(b2) <- xgb.config(bst) expect_equal(bst$raw, b2$raw) @@ -225,14 +226,13 @@ test_that("early stopping xgb.train works", { ) expect_equal(bst$evaluation_log, bst0$evaluation_log) - xgb.save(bst, "model.bin") - loaded <- xgb.load("model.bin") + fname <- file.path(tempdir(), "model.bin") + xgb.save(bst, fname) + loaded <- xgb.load(fname) expect_false(is.null(loaded$best_iteration)) expect_equal(loaded$best_iteration, bst$best_ntreelimit) expect_equal(loaded$best_ntreelimit, bst$best_ntreelimit) - - file.remove("model.bin") }) test_that("early stopping using a specific metric works", { diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index 81ac884d0..568aaa3bd 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -67,20 +67,22 @@ test_that("xgb.DMatrix: NA", { x[1, "x1"] <- NA m <- xgb.DMatrix(x, nthread = n_threads) - xgb.DMatrix.save(m, "int.dmatrix") + fname_int <- file.path(tempdir(), "int.dmatrix") + xgb.DMatrix.save(m, fname_int) x <- matrix(as.numeric(x), nrow = n_samples, ncol = 2) colnames(x) <- c("x1", "x2") m <- xgb.DMatrix(x, nthread = n_threads) - xgb.DMatrix.save(m, "float.dmatrix") + fname_float <- file.path(tempdir(), "float.dmatrix") + xgb.DMatrix.save(m, fname_float) - iconn <- file("int.dmatrix", "rb") - fconn <- file("float.dmatrix", "rb") + iconn <- file(fname_int, "rb") + fconn <- file(fname_float, "rb") - expect_equal(file.size("int.dmatrix"), file.size("float.dmatrix")) + expect_equal(file.size(fname_int), file.size(fname_float)) - bytes <- file.size("int.dmatrix") + bytes <- file.size(fname_int) idmatrix <- readBin(iconn, "raw", n = bytes) fdmatrix <- readBin(fconn, "raw", n = bytes) @@ -90,8 +92,8 @@ test_that("xgb.DMatrix: NA", { close(iconn) close(fconn) - file.remove("int.dmatrix") - file.remove("float.dmatrix") + file.remove(fname_int) + file.remove(fname_float) }) test_that("xgb.DMatrix: saving, loading", { @@ -274,17 +276,19 @@ test_that("xgb.DMatrix: Inf as missing", { x_nan[2, 1] <- NA_real_ m_inf <- xgb.DMatrix(x_inf, nthread = n_threads, missing = Inf) - xgb.DMatrix.save(m_inf, "inf.dmatrix") + fname_inf <- file.path(tempdir(), "inf.dmatrix") + xgb.DMatrix.save(m_inf, fname_inf) m_nan <- xgb.DMatrix(x_nan, nthread = n_threads, missing = NA_real_) - xgb.DMatrix.save(m_nan, "nan.dmatrix") + fname_nan <- file.path(tempdir(), "nan.dmatrix") + xgb.DMatrix.save(m_nan, fname_nan) - infconn <- file("inf.dmatrix", "rb") - nanconn <- file("nan.dmatrix", "rb") + infconn <- file(fname_inf, "rb") + nanconn <- file(fname_nan, "rb") - expect_equal(file.size("inf.dmatrix"), file.size("nan.dmatrix")) + expect_equal(file.size(fname_inf), file.size(fname_nan)) - bytes <- file.size("inf.dmatrix") + bytes <- file.size(fname_inf) infdmatrix <- readBin(infconn, "raw", n = bytes) nandmatrix <- readBin(nanconn, "raw", n = bytes) @@ -294,8 +298,8 @@ test_that("xgb.DMatrix: Inf as missing", { close(infconn) close(nanconn) - file.remove("inf.dmatrix") - file.remove("nan.dmatrix") + file.remove(fname_inf) + file.remove(fname_nan) }) test_that("xgb.DMatrix: error on three-dimensional array", { diff --git a/R-package/tests/testthat/test_helpers.R b/R-package/tests/testthat/test_helpers.R index fd1fffbac..62a8c44bc 100644 --- a/R-package/tests/testthat/test_helpers.R +++ b/R-package/tests/testthat/test_helpers.R @@ -217,9 +217,9 @@ test_that("xgb-attribute functionality", { xgb.attributes(bst.Tree) <- list.val expect_equal(xgb.attributes(bst.Tree), list.ch) # serializing: - xgb.save(bst.Tree, 'xgb.model') - bst <- xgb.load('xgb.model') - if (file.exists('xgb.model')) file.remove('xgb.model') + fname <- file.path(tempdir(), "xgb.model") + xgb.save(bst.Tree, fname) + bst <- xgb.load(fname) expect_equal(xgb.attr(bst, "my_attr"), val) expect_equal(xgb.attributes(bst), list.ch) # deletion: @@ -256,15 +256,15 @@ if (grepl('Windows', Sys.info()[['sysname']], fixed = TRUE) || test_that("xgb.Booster serializing as R object works", { .skip_if_vcd_not_available() - saveRDS(bst.Tree, 'xgb.model.rds') - bst <- readRDS('xgb.model.rds') + fname_rds <- file.path(tempdir(), "xgb.model.rds") + saveRDS(bst.Tree, fname_rds) + bst <- readRDS(fname_rds) dtrain <- xgb.DMatrix(sparse_matrix, label = label, nthread = 2) expect_equal(predict(bst.Tree, dtrain), predict(bst, dtrain), tolerance = float_tolerance) expect_equal(xgb.dump(bst.Tree), xgb.dump(bst)) - xgb.save(bst, 'xgb.model') - if (file.exists('xgb.model')) file.remove('xgb.model') - bst <- readRDS('xgb.model.rds') - if (file.exists('xgb.model.rds')) file.remove('xgb.model.rds') + fname_bin <- file.path(tempdir(), "xgb.model") + xgb.save(bst, fname_bin) + bst <- readRDS(fname_rds) nil_ptr <- new("externalptr") class(nil_ptr) <- "xgb.Booster.handle" expect_true(identical(bst$handle, nil_ptr)) diff --git a/R-package/vignettes/xgboostPresentation.Rmd b/R-package/vignettes/xgboostPresentation.Rmd index 1b015fab8..90393060f 100644 --- a/R-package/vignettes/xgboostPresentation.Rmd +++ b/R-package/vignettes/xgboostPresentation.Rmd @@ -400,9 +400,10 @@ In simple cases, it will happen because there is nothing better than a linear al Like saving models, `xgb.DMatrix` object (which groups both dataset and outcome) can also be saved using `xgb.DMatrix.save` function. ```{r DMatrixSave, message=F, warning=F} -xgb.DMatrix.save(dtrain, "dtrain.buffer") +fname <- file.path(tempdir(), "dtrain.buffer") +xgb.DMatrix.save(dtrain, fname) # to load it in, simply call xgb.DMatrix -dtrain2 <- xgb.DMatrix("dtrain.buffer") +dtrain2 <- xgb.DMatrix(fname) bst <- xgb.train( data = dtrain2 , max_depth = 2 @@ -415,7 +416,7 @@ bst <- xgb.train( ``` ```{r DMatrixDel, include=FALSE} -file.remove("dtrain.buffer") +file.remove(fname) ``` #### Information extraction @@ -466,7 +467,8 @@ Hopefully for you, **XGBoost** implements such functions. ```{r saveModel, message=F, warning=F} # save model to binary local file -xgb.save(bst, "xgboost.model") +fname <- file.path(tempdir(), "xgb_model.ubj") +xgb.save(bst, fname) ``` > `xgb.save` function should return `r TRUE` if everything goes well and crashes otherwise. @@ -475,7 +477,7 @@ An interesting test to see how identical our saved model is to the original one ```{r loadModel, message=F, warning=F} # load binary model to R -bst2 <- xgb.load("xgboost.model") +bst2 <- xgb.load(fname) xgb.parameters(bst2) <- list(nthread = 2) pred2 <- predict(bst2, test$data) @@ -485,7 +487,7 @@ print(paste("sum(abs(pred2-pred))=", sum(abs(pred2 - pred)))) ```{r clean, include=FALSE} # delete the created model -file.remove("./xgboost.model") +file.remove(fname) ``` > result is `0`? We are good! From 2f57bbde3c5feeb04c26fb2bb1391c63568662cf Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 9 Jan 2024 09:54:39 +0800 Subject: [PATCH 56/59] Additional tests for attributes and model booosted rounds. (#9962) --- src/learner.cc | 8 ++- tests/python/test_basic_models.py | 82 ++++++++++++++++++++----------- tests/python/test_model_io.py | 30 +++++++++++ 3 files changed, 90 insertions(+), 30 deletions(-) diff --git a/src/learner.cc b/src/learner.cc index 6b0fd7e4b..db72f7164 100644 --- a/src/learner.cc +++ b/src/learner.cc @@ -535,8 +535,7 @@ class LearnerConfiguration : public Learner { tparam_.booster = get(gradient_booster["name"]); if (!gbm_) { - gbm_.reset(GradientBooster::Create(tparam_.booster, - &ctx_, &learner_model_param_)); + gbm_.reset(GradientBooster::Create(tparam_.booster, &ctx_, &learner_model_param_)); } gbm_->LoadConfig(gradient_booster); @@ -1095,6 +1094,11 @@ class LearnerIO : public LearnerConfiguration { std::vector > extra_attr; mparam.contain_extra_attrs = 1; + if (!this->feature_names_.empty() || !this->feature_types_.empty()) { + LOG(WARNING) << "feature names and feature types are being disregarded, use JSON/UBJSON " + "format instead."; + } + { // Similar to JSON model IO, we save the objective. Json j_obj { Object() }; diff --git a/tests/python/test_basic_models.py b/tests/python/test_basic_models.py index ca35c4e91..828c24862 100644 --- a/tests/python/test_basic_models.py +++ b/tests/python/test_basic_models.py @@ -1,5 +1,4 @@ import json -import locale import os import tempfile @@ -110,20 +109,39 @@ class TestModels: predt_2 = bst.predict(dtrain) assert np.all(np.abs(predt_2 - predt_1) < 1e-6) - def test_boost_from_existing_model(self): + def test_boost_from_existing_model(self) -> None: X, _ = tm.load_agaricus(__file__) - booster = xgb.train({'tree_method': 'hist'}, X, num_boost_round=4) + booster = xgb.train({"tree_method": "hist"}, X, num_boost_round=4) assert booster.num_boosted_rounds() == 4 - booster = xgb.train({'tree_method': 'hist'}, X, num_boost_round=4, - xgb_model=booster) + booster.set_param({"tree_method": "approx"}) + assert booster.num_boosted_rounds() == 4 + booster = xgb.train( + {"tree_method": "hist"}, X, num_boost_round=4, xgb_model=booster + ) assert booster.num_boosted_rounds() == 8 - booster = xgb.train({'updater': 'prune', 'process_type': 'update'}, X, - num_boost_round=4, xgb_model=booster) + with pytest.warns(UserWarning, match="`updater`"): + booster = xgb.train( + {"updater": "prune", "process_type": "update"}, + X, + num_boost_round=4, + xgb_model=booster, + ) # Trees are moved for update, the rounds is reduced. This test is # written for being compatible with current code (1.0.0). If the # behaviour is considered sub-optimal, feel free to change. assert booster.num_boosted_rounds() == 4 + booster = xgb.train({"booster": "gblinear"}, X, num_boost_round=4) + assert booster.num_boosted_rounds() == 4 + booster.set_param({"updater": "coord_descent"}) + assert booster.num_boosted_rounds() == 4 + booster.set_param({"updater": "shotgun"}) + assert booster.num_boosted_rounds() == 4 + booster = xgb.train( + {"booster": "gblinear"}, X, num_boost_round=4, xgb_model=booster + ) + assert booster.num_boosted_rounds() == 8 + def run_custom_objective(self, tree_method=None): param = { 'max_depth': 2, @@ -307,25 +325,6 @@ class TestModels: for d in text_dump: assert d.find(r"feature \"2\"") != -1 - @pytest.mark.skipif(**tm.no_sklearn()) - def test_attributes(self): - from sklearn.datasets import load_iris - - X, y = load_iris(return_X_y=True) - cls = xgb.XGBClassifier(n_estimators=2) - cls.fit(X, y, early_stopping_rounds=1, eval_set=[(X, y)]) - assert cls.get_booster().best_iteration == cls.n_estimators - 1 - assert cls.best_iteration == cls.get_booster().best_iteration - - with tempfile.TemporaryDirectory() as tmpdir: - path = os.path.join(tmpdir, "cls.json") - cls.save_model(path) - - cls = xgb.XGBClassifier(n_estimators=2) - cls.load_model(path) - assert cls.get_booster().best_iteration == cls.n_estimators - 1 - assert cls.best_iteration == cls.get_booster().best_iteration - def run_slice( self, booster: xgb.Booster, @@ -493,18 +492,23 @@ class TestModels: np.testing.assert_allclose(predt0, predt1, atol=1e-5) @pytest.mark.skipif(**tm.no_pandas()) - def test_feature_info(self): + @pytest.mark.parametrize("ext", ["json", "ubj"]) + def test_feature_info(self, ext: str) -> None: import pandas as pd + # make data rows = 100 cols = 10 X = rng.randn(rows, cols) y = rng.randn(rows) + + # Test with pandas, which has feature info. feature_names = ["test_feature_" + str(i) for i in range(cols)] X_pd = pd.DataFrame(X, columns=feature_names) X_pd[f"test_feature_{3}"] = X_pd.iloc[:, 3].astype(np.int32) Xy = xgb.DMatrix(X_pd, y) + assert Xy.feature_types is not None assert Xy.feature_types[3] == "int" booster = xgb.train({}, dtrain=Xy, num_boost_round=1) @@ -513,10 +517,32 @@ class TestModels: assert booster.feature_types == Xy.feature_types with tempfile.TemporaryDirectory() as tmpdir: - path = tmpdir + "model.json" + path = tmpdir + f"model.{ext}" booster.save_model(path) booster = xgb.Booster() booster.load_model(path) assert booster.feature_names == Xy.feature_names assert booster.feature_types == Xy.feature_types + + # Test with numpy, no feature info is set + Xy = xgb.DMatrix(X, y) + assert Xy.feature_names is None + assert Xy.feature_types is None + + booster = xgb.train({}, dtrain=Xy, num_boost_round=1) + assert booster.feature_names is None + assert booster.feature_types is None + + # test explicitly set + fns = [str(i) for i in range(cols)] + booster.feature_names = fns + + assert booster.feature_names == fns + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, f"model.{ext}") + booster.save_model(path) + + booster = xgb.Booster(model_file=path) + assert booster.feature_names == fns diff --git a/tests/python/test_model_io.py b/tests/python/test_model_io.py index 884bba08f..df0fff22e 100644 --- a/tests/python/test_model_io.py +++ b/tests/python/test_model_io.py @@ -466,3 +466,33 @@ def test_with_sklearn_obj_metric() -> None: assert not callable(reg_2.objective) assert not callable(reg_2.eval_metric) assert reg_2.eval_metric is None + + +@pytest.mark.skipif(**tm.no_sklearn()) +def test_attributes() -> None: + from sklearn.datasets import load_iris + + X, y = load_iris(return_X_y=True) + clf = xgb.XGBClassifier(n_estimators=2, early_stopping_rounds=1) + clf.fit(X, y, eval_set=[(X, y)]) + best_iteration = clf.get_booster().best_iteration + assert best_iteration is not None + assert clf.n_estimators is not None + assert best_iteration == clf.n_estimators - 1 + + best_iteration = clf.best_iteration + assert best_iteration == clf.get_booster().best_iteration + + clf.get_booster().set_attr(foo="bar") + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "clf.json") + clf.save_model(path) + + clf = xgb.XGBClassifier(n_estimators=2) + clf.load_model(path) + assert clf.n_estimators is not None + assert clf.get_booster().best_iteration == clf.n_estimators - 1 + assert clf.best_iteration == clf.get_booster().best_iteration + + assert clf.get_booster().attributes()["foo"] == "bar" From 01c471155627f9d81c230d0a16a7655c9fdd16c4 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 9 Jan 2024 19:59:01 +0800 Subject: [PATCH 57/59] Check `__cuda_array_interface__` instead of cupy class. (#9971) * Now XGBoost can directly consume CUDA data from torch. --- doc/python/python_intro.rst | 2 ++ python-package/xgboost/compat.py | 4 ++-- python-package/xgboost/core.py | 19 +++++++++++-------- python-package/xgboost/dask/__init__.py | 4 ++-- python-package/xgboost/data.py | 25 +++++++++++-------------- python-package/xgboost/sklearn.py | 6 +++--- 6 files changed, 31 insertions(+), 29 deletions(-) diff --git a/doc/python/python_intro.rst b/doc/python/python_intro.rst index cc0e461e0..0d26a5253 100644 --- a/doc/python/python_intro.rst +++ b/doc/python/python_intro.rst @@ -162,6 +162,8 @@ Support Matrix +-------------------------+-----------+-------------------+-----------+-----------+--------------------+-------------+ | cupy.ndarray | T | T | T | T | T | T | +-------------------------+-----------+-------------------+-----------+-----------+--------------------+-------------+ +| torch.Tensor | T | T | T | T | T | T | ++-------------------------+-----------+-------------------+-----------+-----------+--------------------+-------------+ | dlpack | CPA | CPA | | CPA | FF | FF | +-------------------------+-----------+-------------------+-----------+-----------+--------------------+-------------+ | datatable.Frame | T | FF | | NPA | FF | | diff --git a/python-package/xgboost/compat.py b/python-package/xgboost/compat.py index 7c11495f7..729750f1f 100644 --- a/python-package/xgboost/compat.py +++ b/python-package/xgboost/compat.py @@ -138,9 +138,9 @@ def concat(value: Sequence[_T]) -> _T: # pylint: disable=too-many-return-statem from cudf import concat as CUDF_concat # pylint: disable=import-error return CUDF_concat(value, axis=0) - from .data import _is_cupy_array + from .data import _is_cupy_alike - if _is_cupy_array(value[0]): + if _is_cupy_alike(value[0]): import cupy # pylint: disable=import-error # pylint: disable=c-extension-no-member,no-member diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index 4d093293d..36be766b1 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -357,10 +357,13 @@ def _numpy2ctypes_type(dtype: Type[np.number]) -> Type[CNumeric]: return _NUMPY_TO_CTYPES_MAPPING[dtype] +def _array_hasobject(data: DataType) -> bool: + return hasattr(data.dtype, "hasobject") and data.dtype.hasobject + + def _cuda_array_interface(data: DataType) -> bytes: - assert ( - data.dtype.hasobject is False - ), "Input data contains `object` dtype. Expecting numeric data." + if _array_hasobject(data): + raise ValueError("Input data contains `object` dtype. Expecting numeric data.") interface = data.__cuda_array_interface__ if "mask" in interface: interface["mask"] = interface["mask"].__cuda_array_interface__ @@ -2102,7 +2105,7 @@ class Booster: _array_interface, _cuda_array_interface, _ensure_np_dtype, - _is_cupy_array, + _is_cupy_alike, ) self._assign_dmatrix_features(dtrain) @@ -2116,7 +2119,7 @@ class Booster: "Expecting `np.ndarray` or `cupy.ndarray` for gradient and hessian." f" Got: {type(array)}" ) - if not isinstance(array, np.ndarray) and not _is_cupy_array(array): + if not isinstance(array, np.ndarray) and not _is_cupy_alike(array): raise TypeError(msg) n_samples = dtrain.num_row() @@ -2131,7 +2134,7 @@ class Booster: if isinstance(array, np.ndarray): array, _ = _ensure_np_dtype(array, array.dtype) interface = _array_interface(array) - elif _is_cupy_array(array): + elif _is_cupy_alike(array): interface = _cuda_array_interface(array) else: raise TypeError(msg) @@ -2461,7 +2464,7 @@ class Booster: _arrow_transform, _is_arrow, _is_cudf_df, - _is_cupy_array, + _is_cupy_alike, _is_list, _is_np_array_like, _is_pandas_df, @@ -2543,7 +2546,7 @@ class Booster: ) ) return _prediction_output(shape, dims, preds, False) - if _is_cupy_array(data): + if _is_cupy_alike(data): from .data import _transform_cupy_array data = _transform_cupy_array(data) diff --git a/python-package/xgboost/dask/__init__.py b/python-package/xgboost/dask/__init__.py index 046a2c982..6b4ae5b07 100644 --- a/python-package/xgboost/dask/__init__.py +++ b/python-package/xgboost/dask/__init__.py @@ -75,7 +75,7 @@ from xgboost.core import ( _deprecate_positional_args, _expect, ) -from xgboost.data import _is_cudf_ser, _is_cupy_array +from xgboost.data import _is_cudf_ser, _is_cupy_alike from xgboost.sklearn import ( XGBClassifier, XGBClassifierBase, @@ -1909,7 +1909,7 @@ class DaskXGBClassifier(DaskScikitLearnBase, XGBClassifierBase): self.classes_ = await self.client.compute(y.drop_duplicates()) if _is_cudf_ser(self.classes_): self.classes_ = self.classes_.to_cupy() - if _is_cupy_array(self.classes_): + if _is_cupy_alike(self.classes_): self.classes_ = self.classes_.get() self.classes_ = numpy.array(self.classes_) self.n_classes_ = len(self.classes_) diff --git a/python-package/xgboost/data.py b/python-package/xgboost/data.py index 05337e788..49a0f43b7 100644 --- a/python-package/xgboost/data.py +++ b/python-package/xgboost/data.py @@ -26,6 +26,7 @@ from .core import ( DataIter, DataSplitMode, DMatrix, + _array_hasobject, _check_call, _cuda_array_interface, _ProxyDMatrix, @@ -77,9 +78,8 @@ def is_scipy_csr(data: DataType) -> bool: def _array_interface_dict(data: np.ndarray) -> dict: - assert ( - data.dtype.hasobject is False - ), "Input data contains `object` dtype. Expecting numeric data." + if _array_hasobject(data): + raise ValueError("Input data contains `object` dtype. Expecting numeric data.") interface = data.__array_interface__ if "mask" in interface: interface["mask"] = interface["mask"].__array_interface__ @@ -219,7 +219,7 @@ def _is_np_array_like(data: DataType) -> bool: def _ensure_np_dtype( data: DataType, dtype: Optional[NumpyDType] ) -> Tuple[np.ndarray, Optional[NumpyDType]]: - if data.dtype.hasobject or data.dtype in [np.float16, np.bool_]: + if _array_hasobject(data) or data.dtype in [np.float16, np.bool_]: dtype = np.float32 data = data.astype(dtype, copy=False) if not data.flags.aligned: @@ -998,11 +998,8 @@ def _is_cudf_ser(data: DataType) -> bool: return lazy_isinstance(data, "cudf.core.series", "Series") -def _is_cupy_array(data: DataType) -> bool: - return any( - lazy_isinstance(data, n, "ndarray") - for n in ("cupy.core.core", "cupy", "cupy._core.core") - ) +def _is_cupy_alike(data: DataType) -> bool: + return hasattr(data, "__cuda_array_interface__") def _transform_cupy_array(data: DataType) -> CupyT: @@ -1010,7 +1007,7 @@ def _transform_cupy_array(data: DataType) -> CupyT: if not hasattr(data, "__cuda_array_interface__") and hasattr(data, "__array__"): data = cupy.array(data, copy=False) - if data.dtype.hasobject or data.dtype in [cupy.bool_]: + if _array_hasobject(data) or data.dtype in [cupy.bool_]: data = data.astype(cupy.float32, copy=False) return data @@ -1222,7 +1219,7 @@ def dispatch_data_backend( return _from_cudf_df( data, missing, threads, feature_names, feature_types, enable_categorical ) - if _is_cupy_array(data): + if _is_cupy_alike(data): return _from_cupy_array(data, missing, threads, feature_names, feature_types) if _is_cupy_csr(data): raise TypeError("cupyx CSR is not supported yet.") @@ -1354,7 +1351,7 @@ def dispatch_meta_backend( data = _transform_dlpack(data) _meta_from_cupy_array(data, name, handle) return - if _is_cupy_array(data): + if _is_cupy_alike(data): _meta_from_cupy_array(data, name, handle) return if _is_cudf_ser(data): @@ -1419,7 +1416,7 @@ def _proxy_transform( return _transform_cudf_df( data, feature_names, feature_types, enable_categorical ) - if _is_cupy_array(data): + if _is_cupy_alike(data): data = _transform_cupy_array(data) return data, None, feature_names, feature_types if _is_dlpack(data): @@ -1470,7 +1467,7 @@ def dispatch_proxy_set_data( # pylint: disable=W0212 proxy._set_data_from_cuda_columnar(data, cast(List, cat_codes)) return - if _is_cupy_array(data): + if _is_cupy_alike(data): proxy._set_data_from_cuda_interface(data) # pylint: disable=W0212 return if _is_dlpack(data): diff --git a/python-package/xgboost/sklearn.py b/python-package/xgboost/sklearn.py index 3383ae0b7..8c3a96784 100644 --- a/python-package/xgboost/sklearn.py +++ b/python-package/xgboost/sklearn.py @@ -39,7 +39,7 @@ from .core import ( _deprecate_positional_args, _parse_eval_str, ) -from .data import _is_cudf_df, _is_cudf_ser, _is_cupy_array, _is_pandas_df +from .data import _is_cudf_df, _is_cudf_ser, _is_cupy_alike, _is_pandas_df from .training import train @@ -1177,7 +1177,7 @@ class XGBModel(XGBModelBase): base_margin=base_margin, validate_features=validate_features, ) - if _is_cupy_array(predts): + if _is_cupy_alike(predts): import cupy # pylint: disable=import-error predts = cupy.asnumpy(predts) # ensure numpy array is used. @@ -1458,7 +1458,7 @@ class XGBClassifier(XGBModel, XGBClassifierBase): classes = cp.unique(y.values) self.n_classes_ = len(classes) expected_classes = cp.array(self.classes_) - elif _is_cupy_array(y): + elif _is_cupy_alike(y): import cupy as cp # pylint: disable=E0401 classes = cp.unique(y) From d3a8d284abd0ac060493809e76d4896160147f3f Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 10 Jan 2024 22:08:42 +0100 Subject: [PATCH 58/59] [R] On-demand serialization + standardization of attributes (#9924) --------- Co-authored-by: Jiaming Yuan --- R-package/NAMESPACE | 13 +- R-package/R/callbacks.R | 81 ++- R-package/R/utils.R | 75 +- R-package/R/xgb.Booster.R | 683 +++++++++++------- R-package/R/xgb.DMatrix.R | 30 +- R-package/R/xgb.cv.R | 14 +- R-package/R/xgb.dump.R | 10 +- R-package/R/xgb.importance.R | 20 +- R-package/R/xgb.load.R | 15 +- R-package/R/xgb.load.raw.R | 19 +- R-package/R/xgb.model.dt.tree.R | 55 +- R-package/R/xgb.plot.shap.R | 8 +- R-package/R/xgb.save.R | 27 +- R-package/R/xgb.save.raw.R | 4 +- R-package/R/xgb.serialize.R | 21 - R-package/R/xgb.train.R | 114 +-- R-package/R/xgb.unserialize.R | 41 -- R-package/R/xgboost.R | 2 + R-package/demo/00Index | 1 - R-package/demo/README.md | 1 - R-package/demo/caret_wrapper.R | 44 -- R-package/demo/predict_leaf_indices.R | 2 +- R-package/demo/runall.R | 1 - .../a-compatibility-note-for-saveRDS-save.Rd | 53 +- R-package/man/cb.save.model.Rd | 17 +- R-package/man/coef.xgb.Booster.Rd | 50 ++ R-package/man/getinfo.Rd | 57 +- R-package/man/predict.xgb.Booster.Rd | 5 +- R-package/man/print.xgb.Booster.Rd | 10 +- R-package/man/setinfo.Rd | 42 -- R-package/man/variable.names.xgb.Booster.Rd | 22 + R-package/man/xgb.Booster.complete.Rd | 61 -- R-package/man/xgb.attr.Rd | 15 +- R-package/man/xgb.config.Rd | 14 +- R-package/man/xgb.copy.Booster.Rd | 53 ++ R-package/man/xgb.gblinear.history.Rd | 11 +- R-package/man/xgb.get.num.boosted.rounds.Rd | 22 + R-package/man/xgb.is.same.Booster.Rd | 59 ++ R-package/man/xgb.load.Rd | 2 +- R-package/man/xgb.load.raw.Rd | 4 +- R-package/man/xgb.model.dt.tree.Rd | 9 +- R-package/man/xgb.parameters.Rd | 16 +- R-package/man/xgb.save.Rd | 24 +- R-package/man/xgb.save.raw.Rd | 6 +- R-package/man/xgb.serialize.Rd | 29 - R-package/man/xgb.train.Rd | 44 +- R-package/man/xgb.unserialize.Rd | 21 - R-package/src/init.c | 18 +- R-package/src/xgboost_R.cc | 255 ++++++- R-package/src/xgboost_R.h | 53 +- R-package/tests/helper_scripts/install_deps.R | 1 - R-package/tests/testthat/test_basic.R | 216 ++++-- R-package/tests/testthat/test_callbacks.R | 64 +- .../tests/testthat/test_custom_objective.R | 8 +- R-package/tests/testthat/test_glm.R | 16 +- R-package/tests/testthat/test_helpers.R | 44 +- R-package/tests/testthat/test_io.R | 47 +- .../tests/testthat/test_model_compatibility.R | 10 +- .../tests/testthat/test_parameter_exposure.R | 6 +- R-package/tests/testthat/test_ranking.R | 8 +- R-package/tests/testthat/test_update.R | 36 +- R-package/vignettes/xgboost.Rnw | 223 ------ R-package/vignettes/xgboostPresentation.Rmd | 119 +-- R-package/vignettes/xgboostfromJSON.Rmd | 3 +- 64 files changed, 1773 insertions(+), 1281 deletions(-) delete mode 100644 R-package/R/xgb.serialize.R delete mode 100644 R-package/R/xgb.unserialize.R delete mode 100644 R-package/demo/caret_wrapper.R create mode 100644 R-package/man/coef.xgb.Booster.Rd delete mode 100644 R-package/man/setinfo.Rd create mode 100644 R-package/man/variable.names.xgb.Booster.Rd delete mode 100644 R-package/man/xgb.Booster.complete.Rd create mode 100644 R-package/man/xgb.copy.Booster.Rd create mode 100644 R-package/man/xgb.get.num.boosted.rounds.Rd create mode 100644 R-package/man/xgb.is.same.Booster.Rd delete mode 100644 R-package/man/xgb.serialize.Rd delete mode 100644 R-package/man/xgb.unserialize.Rd delete mode 100644 R-package/vignettes/xgboost.Rnw diff --git a/R-package/NAMESPACE b/R-package/NAMESPACE index e6f7a82b8..a29c9b1e0 100644 --- a/R-package/NAMESPACE +++ b/R-package/NAMESPACE @@ -2,16 +2,19 @@ S3method("[",xgb.DMatrix) S3method("dimnames<-",xgb.DMatrix) +S3method(coef,xgb.Booster) S3method(dim,xgb.DMatrix) S3method(dimnames,xgb.DMatrix) +S3method(getinfo,xgb.Booster) S3method(getinfo,xgb.DMatrix) S3method(predict,xgb.Booster) -S3method(predict,xgb.Booster.handle) S3method(print,xgb.Booster) S3method(print,xgb.DMatrix) S3method(print,xgb.cv.synchronous) +S3method(setinfo,xgb.Booster) S3method(setinfo,xgb.DMatrix) S3method(slice,xgb.DMatrix) +S3method(variable.names,xgb.Booster) export("xgb.attr<-") export("xgb.attributes<-") export("xgb.config<-") @@ -26,13 +29,13 @@ export(cb.save.model) export(getinfo) export(setinfo) export(slice) -export(xgb.Booster.complete) export(xgb.DMatrix) export(xgb.DMatrix.hasinfo) export(xgb.DMatrix.save) export(xgb.attr) export(xgb.attributes) export(xgb.config) +export(xgb.copy.Booster) export(xgb.create.features) export(xgb.cv) export(xgb.dump) @@ -41,10 +44,12 @@ export(xgb.get.DMatrix.data) export(xgb.get.DMatrix.num.non.missing) export(xgb.get.DMatrix.qcut) export(xgb.get.config) +export(xgb.get.num.boosted.rounds) export(xgb.ggplot.deepness) export(xgb.ggplot.importance) export(xgb.ggplot.shap.summary) export(xgb.importance) +export(xgb.is.same.Booster) export(xgb.load) export(xgb.load.raw) export(xgb.model.dt.tree) @@ -56,10 +61,8 @@ export(xgb.plot.shap.summary) export(xgb.plot.tree) export(xgb.save) export(xgb.save.raw) -export(xgb.serialize) export(xgb.set.config) export(xgb.train) -export(xgb.unserialize) export(xgboost) import(methods) importClassesFrom(Matrix,dgCMatrix) @@ -88,8 +91,10 @@ importFrom(graphics,title) importFrom(jsonlite,fromJSON) importFrom(jsonlite,toJSON) importFrom(methods,new) +importFrom(stats,coef) importFrom(stats,median) importFrom(stats,predict) +importFrom(stats,variable.names) importFrom(utils,head) importFrom(utils,object.size) importFrom(utils,str) diff --git a/R-package/R/callbacks.R b/R-package/R/callbacks.R index f8f3b5a30..b3d6bdb1a 100644 --- a/R-package/R/callbacks.R +++ b/R-package/R/callbacks.R @@ -228,7 +228,7 @@ cb.reset.parameters <- function(new_params) { }) if (!is.null(env$bst)) { - xgb.parameters(env$bst$handle) <- pars + xgb.parameters(env$bst) <- pars } else { for (fd in env$bst_folds) xgb.parameters(fd$bst) <- pars @@ -333,13 +333,13 @@ cb.early.stop <- function(stopping_rounds, maximize = FALSE, if (!is.null(env$bst)) { if (!inherits(env$bst, 'xgb.Booster')) stop("'bst' in the parent frame must be an 'xgb.Booster'") - if (!is.null(best_score <- xgb.attr(env$bst$handle, 'best_score'))) { + if (!is.null(best_score <- xgb.attr(env$bst, 'best_score'))) { best_score <<- as.numeric(best_score) - best_iteration <<- as.numeric(xgb.attr(env$bst$handle, 'best_iteration')) + 1 - best_msg <<- as.numeric(xgb.attr(env$bst$handle, 'best_msg')) + best_iteration <<- as.numeric(xgb.attr(env$bst, 'best_iteration')) + 1 + best_msg <<- as.numeric(xgb.attr(env$bst, 'best_msg')) } else { - xgb.attributes(env$bst$handle) <- list(best_iteration = best_iteration - 1, - best_score = best_score) + xgb.attributes(env$bst) <- list(best_iteration = best_iteration - 1, + best_score = best_score) } } else if (is.null(env$bst_folds) || is.null(env$basket)) { stop("Parent frame has neither 'bst' nor ('bst_folds' and 'basket')") @@ -348,7 +348,7 @@ cb.early.stop <- function(stopping_rounds, maximize = FALSE, finalizer <- function(env) { if (!is.null(env$bst)) { - attr_best_score <- as.numeric(xgb.attr(env$bst$handle, 'best_score')) + attr_best_score <- as.numeric(xgb.attr(env$bst, 'best_score')) if (best_score != attr_best_score) { # If the difference is too big, throw an error if (abs(best_score - attr_best_score) >= 1e-14) { @@ -358,9 +358,9 @@ cb.early.stop <- function(stopping_rounds, maximize = FALSE, # If the difference is due to floating-point truncation, update best_score best_score <- attr_best_score } - env$bst$best_iteration <- best_iteration - env$bst$best_ntreelimit <- best_ntreelimit - env$bst$best_score <- best_score + xgb.attr(env$bst, "best_iteration") <- best_iteration + xgb.attr(env$bst, "best_ntreelimit") <- best_ntreelimit + xgb.attr(env$bst, "best_score") <- best_score } else { env$basket$best_iteration <- best_iteration env$basket$best_ntreelimit <- best_ntreelimit @@ -412,11 +412,15 @@ cb.early.stop <- function(stopping_rounds, maximize = FALSE, #' @param save_period save the model to disk after every #' \code{save_period} iterations; 0 means save the model at the end. #' @param save_name the name or path for the saved model file. +#' +#' Note that the format of the model being saved is determined by the file +#' extension specified here (see \link{xgb.save} for details about how it works). +#' #' It can contain a \code{\link[base]{sprintf}} formatting specifier #' to include the integer iteration number in the file name. -#' E.g., with \code{save_name} = 'xgboost_%04d.model', -#' the file saved at iteration 50 would be named "xgboost_0050.model". -#' +#' E.g., with \code{save_name} = 'xgboost_%04d.ubj', +#' the file saved at iteration 50 would be named "xgboost_0050.ubj". +#' @seealso \link{xgb.save} #' @details #' This callback function allows to save an xgb-model file, either periodically after each \code{save_period}'s or at the end. #' @@ -430,7 +434,7 @@ cb.early.stop <- function(stopping_rounds, maximize = FALSE, #' \code{\link{callbacks}} #' #' @export -cb.save.model <- function(save_period = 0, save_name = "xgboost.model") { +cb.save.model <- function(save_period = 0, save_name = "xgboost.ubj") { if (save_period < 0) stop("'save_period' cannot be negative") @@ -440,8 +444,13 @@ cb.save.model <- function(save_period = 0, save_name = "xgboost.model") { stop("'save_model' callback requires the 'bst' booster object in its calling frame") if ((save_period > 0 && (env$iteration - env$begin_iteration) %% save_period == 0) || - (save_period == 0 && env$iteration == env$end_iteration)) - xgb.save(env$bst, sprintf(save_name, env$iteration)) + (save_period == 0 && env$iteration == env$end_iteration)) { + # Note: this throws a warning if the name doesn't have anything to format through 'sprintf' + suppressWarnings({ + save_name <- sprintf(save_name, env$iteration) + }) + xgb.save(env$bst, save_name) + } } attr(callback, 'call') <- match.call() attr(callback, 'name') <- 'cb.save.model' @@ -512,8 +521,7 @@ cb.cv.predict <- function(save_models = FALSE) { env$basket$pred <- pred if (save_models) { env$basket$models <- lapply(env$bst_folds, function(fd) { - xgb.attr(fd$bst, 'niter') <- env$end_iteration - 1 - xgb.Booster.complete(xgb.handleToBooster(handle = fd$bst, raw = NULL), saveraw = TRUE) + return(fd$bst) }) } } @@ -665,7 +673,7 @@ cb.gblinear.history <- function(sparse = FALSE) { } else { # xgb.cv: cf <- vector("list", length(env$bst_folds)) for (i in seq_along(env$bst_folds)) { - dmp <- xgb.dump(xgb.handleToBooster(handle = env$bst_folds[[i]]$bst, raw = NULL)) + dmp <- xgb.dump(env$bst_folds[[i]]$bst) cf[[i]] <- as.numeric(grep('(booster|bias|weigh)', dmp, invert = TRUE, value = TRUE)) if (sparse) cf[[i]] <- as(cf[[i]], "sparseVector") } @@ -685,14 +693,19 @@ cb.gblinear.history <- function(sparse = FALSE) { callback } -#' Extract gblinear coefficients history. -#' -#' A helper function to extract the matrix of linear coefficients' history +#' @title Extract gblinear coefficients history. +#' @description A helper function to extract the matrix of linear coefficients' history #' from a gblinear model created while using the \code{cb.gblinear.history()} #' callback. +#' @details Note that this is an R-specific function that relies on R attributes that +#' are not saved when using xgboost's own serialization functions like \link{xgb.load} +#' or \link{xgb.load.raw}. #' +#' In order for a serialized model to be accepted by tgis function, one must use R +#' serializers such as \link{saveRDS}. #' @param model either an \code{xgb.Booster} or a result of \code{xgb.cv()}, trained -#' using the \code{cb.gblinear.history()} callback. +#' using the \code{cb.gblinear.history()} callback, but \bold{not} a booster +#' loaded from \link{xgb.load} or \link{xgb.load.raw}. #' @param class_index zero-based class index to extract the coefficients for only that #' specific class in a multinomial multiclass model. When it is NULL, all the #' coefficients are returned. Has no effect in non-multiclass models. @@ -713,20 +726,18 @@ xgb.gblinear.history <- function(model, class_index = NULL) { stop("model must be an object of either xgb.Booster or xgb.cv.synchronous class") is_cv <- inherits(model, "xgb.cv.synchronous") - if (is.null(model[["callbacks"]]) || is.null(model$callbacks[["cb.gblinear.history"]])) + if (is_cv) { + callbacks <- model$callbacks + } else { + callbacks <- attributes(model)$callbacks + } + + if (is.null(callbacks) || is.null(callbacks$cb.gblinear.history)) stop("model must be trained while using the cb.gblinear.history() callback") if (!is_cv) { - # extract num_class & num_feat from the internal model - dmp <- xgb.dump(model) - if (length(dmp) < 2 || dmp[2] != "bias:") - stop("It does not appear to be a gblinear model") - dmp <- dmp[-c(1, 2)] - n <- which(dmp == 'weight:') - if (length(n) != 1) - stop("It does not appear to be a gblinear model") - num_class <- n - 1 - num_feat <- (length(dmp) - 4) / num_class + num_class <- xgb.num_class(model) + num_feat <- xgb.num_feature(model) } else { # in case of CV, the object is expected to have this info if (model$params$booster != "gblinear") @@ -742,7 +753,7 @@ xgb.gblinear.history <- function(model, class_index = NULL) { (class_index[1] < 0 || class_index[1] >= num_class)) stop("class_index has to be within [0,", num_class - 1, "]") - coef_path <- environment(model$callbacks$cb.gblinear.history)[["coefs"]] + coef_path <- environment(callbacks$cb.gblinear.history)[["coefs"]] if (!is.null(class_index) && num_class > 1) { coef_path <- if (is.list(coef_path)) { lapply(coef_path, diff --git a/R-package/R/utils.R b/R-package/R/utils.R index c011ab8ed..945d86132 100644 --- a/R-package/R/utils.R +++ b/R-package/R/utils.R @@ -148,19 +148,17 @@ check.custom.eval <- function(env = parent.frame()) { # Update a booster handle for an iteration with dtrain data -xgb.iter.update <- function(booster_handle, dtrain, iter, obj) { - if (!identical(class(booster_handle), "xgb.Booster.handle")) { - stop("booster_handle must be of xgb.Booster.handle class") - } +xgb.iter.update <- function(bst, dtrain, iter, obj) { if (!inherits(dtrain, "xgb.DMatrix")) { stop("dtrain must be of xgb.DMatrix class") } + handle <- xgb.get.handle(bst) if (is.null(obj)) { - .Call(XGBoosterUpdateOneIter_R, booster_handle, as.integer(iter), dtrain) + .Call(XGBoosterUpdateOneIter_R, handle, as.integer(iter), dtrain) } else { pred <- predict( - booster_handle, + bst, dtrain, outputmargin = TRUE, training = TRUE, @@ -185,7 +183,7 @@ xgb.iter.update <- function(booster_handle, dtrain, iter, obj) { } .Call( - XGBoosterTrainOneIter_R, booster_handle, dtrain, iter, grad, hess + XGBoosterTrainOneIter_R, handle, dtrain, iter, grad, hess ) } return(TRUE) @@ -195,23 +193,22 @@ xgb.iter.update <- function(booster_handle, dtrain, iter, obj) { # Evaluate one iteration. # Returns a named vector of evaluation metrics # with the names in a 'datasetname-metricname' format. -xgb.iter.eval <- function(booster_handle, watchlist, iter, feval) { - if (!identical(class(booster_handle), "xgb.Booster.handle")) - stop("class of booster_handle must be xgb.Booster.handle") +xgb.iter.eval <- function(bst, watchlist, iter, feval) { + handle <- xgb.get.handle(bst) if (length(watchlist) == 0) return(NULL) evnames <- names(watchlist) if (is.null(feval)) { - msg <- .Call(XGBoosterEvalOneIter_R, booster_handle, as.integer(iter), watchlist, as.list(evnames)) + msg <- .Call(XGBoosterEvalOneIter_R, handle, as.integer(iter), watchlist, as.list(evnames)) mat <- matrix(strsplit(msg, '\\s+|:')[[1]][-1], nrow = 2) res <- structure(as.numeric(mat[2, ]), names = mat[1, ]) } else { res <- sapply(seq_along(watchlist), function(j) { w <- watchlist[[j]] ## predict using all trees - preds <- predict(booster_handle, w, outputmargin = TRUE, iterationrange = c(1, 1)) + preds <- predict(bst, w, outputmargin = TRUE, iterationrange = c(1, 1)) eval_res <- feval(preds, w) out <- eval_res$value names(out) <- paste0(evnames[j], "-", eval_res$metric) @@ -352,16 +349,45 @@ xgb.createFolds <- function(y, k) { #' @name xgboost-deprecated NULL -#' Do not use \code{\link[base]{saveRDS}} or \code{\link[base]{save}} for long-term archival of -#' models. Instead, use \code{\link{xgb.save}} or \code{\link{xgb.save.raw}}. +#' @title Model Serialization and Compatibility +#' @description #' -#' It is a common practice to use the built-in \code{\link[base]{saveRDS}} function (or -#' \code{\link[base]{save}}) to persist R objects to the disk. While it is possible to persist -#' \code{xgb.Booster} objects using \code{\link[base]{saveRDS}}, it is not advisable to do so if -#' the model is to be accessed in the future. If you train a model with the current version of -#' XGBoost and persist it with \code{\link[base]{saveRDS}}, the model is not guaranteed to be -#' accessible in later releases of XGBoost. To ensure that your model can be accessed in future -#' releases of XGBoost, use \code{\link{xgb.save}} or \code{\link{xgb.save.raw}} instead. +#' When it comes to serializing XGBoost models, it's possible to use R serializers such as +#' \link{save} or \link{saveRDS} to serialize an XGBoost R model, but XGBoost also provides +#' its own serializers with better compatibility guarantees, which allow loading +#' said models in other language bindings of XGBoost. +#' +#' Note that an `xgb.Booster` object, outside of its core components, might also keep:\itemize{ +#' \item Additional model configuration (accessible through \link{xgb.config}), +#' which includes model fitting parameters like `max_depth` and runtime parameters like `nthread`. +#' These are not necessarily useful for prediction/importance/plotting. +#' \item Additional R-specific attributes - e.g. results of callbacks, such as evaluation logs, +#' which are kept as a `data.table` object, accessible through `attributes(model)$evaluation_log` +#' if present. +#' } +#' +#' The first one (configurations) does not have the same compatibility guarantees as +#' the model itself, including attributes that are set and accessed through \link{xgb.attributes} - that is, such configuration +#' might be lost after loading the booster in a different XGBoost version, regardless of the +#' serializer that was used. These are saved when using \link{saveRDS}, but will be discarded +#' if loaded into an incompatible XGBoost version. They are not saved when using XGBoost's +#' serializers from its public interface including \link{xgb.save} and \link{xgb.save.raw}. +#' +#' The second ones (R attributes) are not part of the standard XGBoost model structure, and thus are +#' not saved when using XGBoost's own serializers. These attributes are only used for informational +#' purposes, such as keeping track of evaluation metrics as the model was fit, or saving the R +#' call that produced the model, but are otherwise not used for prediction / importance / plotting / etc. +#' These R attributes are only preserved when using R's serializers. +#' +#' Note that XGBoost models in R starting from version `2.1.0` and onwards, and XGBoost models +#' before version `2.1.0`; have a very different R object structure and are incompatible with +#' each other. Hence, models that were saved with R serializers live `saveRDS` or `save` before +#' version `2.1.0` will not work with latter `xgboost` versions and vice versa. Be aware that +#' the structure of R model objects could in theory change again in the future, so XGBoost's serializers +#' should be preferred for long-term storage. +#' +#' Furthermore, note that using the package `qs` for serialization will require version 0.26 or +#' higher of said package, and will have the same compatibility restrictions as R serializers. #' #' @details #' Use \code{\link{xgb.save}} to save the XGBoost model as a stand-alone file. You may opt into @@ -374,9 +400,10 @@ NULL #' The \code{\link{xgb.save.raw}} function is useful if you'd like to persist the XGBoost model #' as part of another R object. #' -#' Note: Do not use \code{\link{xgb.serialize}} to store models long-term. It persists not only the -#' model but also internal configurations and parameters, and its format is not stable across -#' multiple XGBoost versions. Use \code{\link{xgb.serialize}} only for checkpointing. +#' Use \link{saveRDS} if you require the R-specific attributes that a booster might have, such +#' as evaluation logs, but note that future compatibility of such objects is outside XGBoost's +#' control as it relies on R's serialization format (see e.g. the details section in +#' \link{serialize} and \link{save} from base R). #' #' For more details and explanation about model persistence and archival, consult the page #' \url{https://xgboost.readthedocs.io/en/latest/tutorials/saving_model.html}. diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index 9fdfc9dd6..cee7e9fc5 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -1,180 +1,85 @@ -# Construct an internal xgboost Booster and return a handle to it. +# Construct an internal xgboost Booster and get its current number of rounds. # internal utility function -xgb.Booster.handle <- function(params, cachelist, modelfile, handle) { +# Note: the number of rounds in the C booster gets reset to zero when changing +# key booster parameters like 'process_type=update', but in some cases, when +# replacing previous iterations, it needs to make a check that the new number +# of iterations doesn't exceed the previous ones, hence it keeps track of the +# current number of iterations before resetting the parameters in order to +# perform the check later on. +xgb.Booster <- function(params, cachelist, modelfile) { if (typeof(cachelist) != "list" || !all(vapply(cachelist, inherits, logical(1), what = 'xgb.DMatrix'))) { stop("cachelist must be a list of xgb.DMatrix objects") } ## Load existing model, dispatch for on disk model file and in memory buffer if (!is.null(modelfile)) { - if (typeof(modelfile) == "character") { + if (is.character(modelfile)) { ## A filename - handle <- .Call(XGBoosterCreate_R, cachelist) + bst <- .Call(XGBoosterCreate_R, cachelist) modelfile <- path.expand(modelfile) - .Call(XGBoosterLoadModel_R, handle, enc2utf8(modelfile[1])) - class(handle) <- "xgb.Booster.handle" + .Call(XGBoosterLoadModel_R, xgb.get.handle(bst), enc2utf8(modelfile[1])) + niter <- xgb.get.num.boosted.rounds(bst) if (length(params) > 0) { - xgb.parameters(handle) <- params + xgb.parameters(bst) <- params } - return(handle) - } else if (typeof(modelfile) == "raw") { + return(list(bst = bst, niter = niter)) + } else if (is.raw(modelfile)) { ## A memory buffer - bst <- xgb.unserialize(modelfile, handle) + bst <- xgb.load.raw(modelfile) + niter <- xgb.get.num.boosted.rounds(bst) xgb.parameters(bst) <- params - return(bst) + return(list(bst = bst, niter = niter)) } else if (inherits(modelfile, "xgb.Booster")) { ## A booster object - bst <- xgb.Booster.complete(modelfile, saveraw = TRUE) - bst <- xgb.unserialize(bst$raw) + bst <- .Call(XGDuplicate_R, modelfile) + niter <- xgb.get.num.boosted.rounds(bst) xgb.parameters(bst) <- params - return(bst) + return(list(bst = bst, niter = niter)) } else { stop("modelfile must be either character filename, or raw booster dump, or xgb.Booster object") } } ## Create new model - handle <- .Call(XGBoosterCreate_R, cachelist) - class(handle) <- "xgb.Booster.handle" + bst <- .Call(XGBoosterCreate_R, cachelist) if (length(params) > 0) { - xgb.parameters(handle) <- params + xgb.parameters(bst) <- params } - return(handle) + return(list(bst = bst, niter = 0L)) } -# Convert xgb.Booster.handle to xgb.Booster -# internal utility function -xgb.handleToBooster <- function(handle, raw) { - bst <- list(handle = handle, raw = raw) - class(bst) <- "xgb.Booster" - return(bst) -} - -# Check whether xgb.Booster.handle is null +# Check whether xgb.Booster handle is null # internal utility function is.null.handle <- function(handle) { if (is.null(handle)) return(TRUE) - if (!identical(class(handle), "xgb.Booster.handle")) - stop("argument type must be xgb.Booster.handle") + if (!inherits(handle, "externalptr")) + stop("argument type must be 'externalptr'") - if (.Call(XGCheckNullPtr_R, handle)) - return(TRUE) - - return(FALSE) + return(.Call(XGCheckNullPtr_R, handle)) } -# Return a verified to be valid handle out of either xgb.Booster.handle or -# xgb.Booster internal utility function +# Return a verified to be valid handle out of xgb.Booster +# internal utility function xgb.get.handle <- function(object) { if (inherits(object, "xgb.Booster")) { - handle <- object$handle - } else if (inherits(object, "xgb.Booster.handle")) { - handle <- object + handle <- object$ptr + if (is.null(handle) || !inherits(handle, "externalptr")) { + stop("'xgb.Booster' object is corrupted or is from an incompatible xgboost version.") + } } else { - stop("argument must be of either xgb.Booster or xgb.Booster.handle class") + stop("argument must be an 'xgb.Booster' object.") } if (is.null.handle(handle)) { - stop("invalid xgb.Booster.handle") + stop("invalid 'xgb.Booster' (blank 'externalptr').") } - handle -} - -#' Restore missing parts of an incomplete xgb.Booster object -#' -#' It attempts to complete an `xgb.Booster` object by restoring either its missing -#' raw model memory dump (when it has no `raw` data but its `xgb.Booster.handle` is valid) -#' or its missing internal handle (when its `xgb.Booster.handle` is not valid -#' but it has a raw Booster memory dump). -#' -#' @param object Object of class `xgb.Booster`. -#' @param saveraw A flag indicating whether to append `raw` Booster memory dump data -#' when it doesn't already exist. -#' -#' @details -#' -#' While this method is primarily for internal use, it might be useful in some practical situations. -#' -#' E.g., when an `xgb.Booster` model is saved as an R object and then is loaded as an R object, -#' its handle (pointer) to an internal xgboost model would be invalid. The majority of xgboost methods -#' should still work for such a model object since those methods would be using -#' `xgb.Booster.complete()` internally. However, one might find it to be more efficient to call the -#' `xgb.Booster.complete()` function explicitly once after loading a model as an R-object. -#' That would prevent further repeated implicit reconstruction of an internal booster model. -#' -#' @return -#' An object of `xgb.Booster` class. -#' -#' @examples -#' -#' data(agaricus.train, package = "xgboost") -#' -#' bst <- xgboost( -#' data = agaricus.train$data, -#' label = agaricus.train$label, -#' max_depth = 2, -#' eta = 1, -#' nthread = 2, -#' nrounds = 2, -#' objective = "binary:logistic" -#' ) -#' -#' fname <- file.path(tempdir(), "xgb_model.Rds") -#' saveRDS(bst, fname) -#' -#' # Warning: The resulting RDS file is only compatible with the current XGBoost version. -#' # Refer to the section titled "a-compatibility-note-for-saveRDS-save". -#' bst1 <- readRDS(fname) -#' # the handle is invalid: -#' print(bst1$handle) -#' -#' bst1 <- xgb.Booster.complete(bst1) -#' # now the handle points to a valid internal booster model: -#' print(bst1$handle) -#' -#' @export -xgb.Booster.complete <- function(object, saveraw = TRUE) { - if (!inherits(object, "xgb.Booster")) - stop("argument type must be xgb.Booster") - - if (is.null.handle(object$handle)) { - object$handle <- xgb.Booster.handle( - params = list(), - cachelist = list(), - modelfile = object$raw, - handle = object$handle - ) - } else { - if (is.null(object$raw) && saveraw) { - object$raw <- xgb.serialize(object$handle) - } - } - - attrs <- xgb.attributes(object) - if (!is.null(attrs$best_ntreelimit)) { - object$best_ntreelimit <- as.integer(attrs$best_ntreelimit) - } - if (!is.null(attrs$best_iteration)) { - ## Convert from 0 based back to 1 based. - object$best_iteration <- as.integer(attrs$best_iteration) + 1 - } - if (!is.null(attrs$best_score)) { - object$best_score <- as.numeric(attrs$best_score) - } - if (!is.null(attrs$best_msg)) { - object$best_msg <- attrs$best_msg - } - if (!is.null(attrs$niter)) { - object$niter <- as.integer(attrs$niter) - } - - return(object) + return(handle) } #' Predict method for XGBoost model #' #' Predicted values based on either xgboost model or model handle object. #' -#' @param object Object of class `xgb.Booster` or `xgb.Booster.handle`. +#' @param object Object of class `xgb.Booster`. #' @param newdata Takes `matrix`, `dgCMatrix`, `dgRMatrix`, `dsparseVector`, #' local data file, or `xgb.DMatrix`. #' For single-row predictions on sparse data, it is recommended to use the CSR format. @@ -358,27 +263,19 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { #' pred5 <- predict(bst, as.matrix(iris[, -5]), iterationrange = c(1, 6)) #' sum(pred5 != lb) / length(lb) #' -#' @rdname predict.xgb.Booster #' @export predict.xgb.Booster <- function(object, newdata, missing = NA, outputmargin = FALSE, ntreelimit = NULL, predleaf = FALSE, predcontrib = FALSE, approxcontrib = FALSE, predinteraction = FALSE, reshape = FALSE, training = FALSE, iterationrange = NULL, strict_shape = FALSE, ...) { - object <- xgb.Booster.complete(object, saveraw = FALSE) - if (!inherits(newdata, "xgb.DMatrix")) { - config <- jsonlite::fromJSON(xgb.config(object)) - nthread <- strtoi(config$learner$generic_param$nthread) + nthread <- xgb.nthread(object) newdata <- xgb.DMatrix( newdata, missing = missing, nthread = NVL(nthread, -1) ) } - if (!is.null(object[["feature_names"]]) && - !is.null(colnames(newdata)) && - !identical(object[["feature_names"]], colnames(newdata))) - stop("Feature names stored in `object` and `newdata` are different!") - if (NVL(object$params[['booster']], '') == 'gblinear' || is.null(ntreelimit)) + if (NVL(xgb.booster_type(object), '') == 'gblinear' || is.null(ntreelimit)) ntreelimit <- 0 if (ntreelimit != 0 && is.null(iterationrange)) { @@ -391,11 +288,12 @@ predict.xgb.Booster <- function(object, newdata, missing = NA, outputmargin = FA ## both are specified, let libgxgboost throw an error } else { ## no limit is supplied, use best - if (is.null(object$best_iteration)) { + best_iteration <- xgb.best_iteration(object) + if (is.null(best_iteration)) { iterationrange <- c(0, 0) } else { ## We don't need to + 1 as R is 1-based index. - iterationrange <- c(0, as.integer(object$best_iteration)) + iterationrange <- c(0, as.integer(best_iteration)) } } ## Handle the 0 length values. @@ -438,7 +336,10 @@ predict.xgb.Booster <- function(object, newdata, missing = NA, outputmargin = FA } predts <- .Call( - XGBoosterPredictFromDMatrix_R, object$handle, newdata, jsonlite::toJSON(args, auto_unbox = TRUE) + XGBoosterPredictFromDMatrix_R, + xgb.get.handle(object), + newdata, + jsonlite::toJSON(args, auto_unbox = TRUE) ) names(predts) <- c("shape", "results") shape <- predts$shape @@ -509,22 +410,12 @@ predict.xgb.Booster <- function(object, newdata, missing = NA, outputmargin = FA return(arr) } -#' @rdname predict.xgb.Booster -#' @export -predict.xgb.Booster.handle <- function(object, ...) { - bst <- xgb.handleToBooster(handle = object, raw = NULL) - - ret <- predict(bst, ...) - return(ret) -} - - -#' Accessors for serializable attributes of a model +#' @title Accessors for serializable attributes of a model #' -#' These methods allow to manipulate the key-value attribute strings of an xgboost model. +#' @description These methods allow to manipulate the key-value attribute strings of an xgboost model. #' -#' @param object Object of class `xgb.Booster` or `xgb.Booster.handle`. +#' @param object Object of class `xgb.Booster`. \bold{Will be modified in-place} when assigning to it. #' @param name A non-empty character string specifying which attribute is to be accessed. #' @param value For `xgb.attr<-`, a value of an attribute; for `xgb.attributes<-`, #' it is a list (or an object coercible to a list) with the names of attributes to set @@ -546,16 +437,15 @@ predict.xgb.Booster.handle <- function(object, ...) { #' change the value of that parameter for a model. #' Use [xgb.parameters<-()] to set or change model parameters. #' -#' The attribute setters would usually work more efficiently for `xgb.Booster.handle` -#' than for `xgb.Booster`, since only just a handle (pointer) would need to be copied. -#' That would only matter if attributes need to be set many times. -#' Note, however, that when feeding a handle of an `xgb.Booster` object to the attribute setters, -#' the raw model cache of an `xgb.Booster` object would not be automatically updated, -#' and it would be the user's responsibility to call [xgb.serialize()] to update it. -#' #' The `xgb.attributes<-` setter either updates the existing or adds one or several attributes, #' but it doesn't delete the other existing attributes. #' +#' Important: since this modifies the booster's C object, semantics for assignment here +#' will differ from R's, as any object reference to the same booster will be modified +#' too, while assignment of R attributes through `attributes(model)$ <- ` +#' will follow the usual copy-on-write R semantics (see \link{xgb.copy.Booster} for an +#' example of these behaviors). +#' #' @return #' - `xgb.attr()` returns either a string value of an attribute #' or `NULL` if an attribute wasn't stored in a model. @@ -597,14 +487,25 @@ predict.xgb.Booster.handle <- function(object, ...) { xgb.attr <- function(object, name) { if (is.null(name) || nchar(as.character(name[1])) == 0) stop("invalid attribute name") handle <- xgb.get.handle(object) - .Call(XGBoosterGetAttr_R, handle, as.character(name[1])) + out <- .Call(XGBoosterGetAttr_R, handle, as.character(name[1])) + if (!NROW(out) || !nchar(out)) { + return(NULL) + } + if (!is.null(out)) { + if (name %in% c("best_iteration", "best_ntreelimit", "best_score")) { + out <- as.numeric(out) + } + } + return(out) } #' @rdname xgb.attr #' @export `xgb.attr<-` <- function(object, name, value) { - if (is.null(name) || nchar(as.character(name[1])) == 0) stop("invalid attribute name") + name <- as.character(name[1]) + if (!NROW(name) || !nchar(name)) stop("invalid attribute name") handle <- xgb.get.handle(object) + if (!is.null(value)) { # Coerce the elements to be scalar strings. # Q: should we warn user about non-scalar elements? @@ -614,11 +515,8 @@ xgb.attr <- function(object, name) { value <- as.character(value[1]) } } - .Call(XGBoosterSetAttr_R, handle, as.character(name[1]), value) - if (is(object, 'xgb.Booster') && !is.null(object$raw)) { - object$raw <- xgb.serialize(object$handle) - } - object + .Call(XGBoosterSetAttr_R, handle, name, value) + return(object) } #' @rdname xgb.attr @@ -626,12 +524,10 @@ xgb.attr <- function(object, name) { xgb.attributes <- function(object) { handle <- xgb.get.handle(object) attr_names <- .Call(XGBoosterGetAttrNames_R, handle) - if (is.null(attr_names)) return(NULL) - res <- lapply(attr_names, function(x) { - .Call(XGBoosterGetAttr_R, handle, x) - }) - names(res) <- attr_names - res + if (!NROW(attr_names)) return(list()) + out <- lapply(attr_names, function(name) xgb.attr(object, name)) + names(out) <- attr_names + return(out) } #' @rdname xgb.attr @@ -641,31 +537,21 @@ xgb.attributes <- function(object) { if (is.null(names(a)) || any(nchar(names(a)) == 0)) { stop("attribute names cannot be empty strings") } - # Coerce the elements to be scalar strings. - # Q: should we warn a user about non-scalar elements? - a <- lapply(a, function(x) { - if (is.null(x)) return(NULL) - if (is.numeric(x[1])) { - format(x[1], digits = 17) - } else { - as.character(x[1]) - } - }) - handle <- xgb.get.handle(object) for (i in seq_along(a)) { - .Call(XGBoosterSetAttr_R, handle, names(a[i]), a[[i]]) + xgb.attr(object, names(a[i])) <- a[[i]] } - if (is(object, 'xgb.Booster') && !is.null(object$raw)) { - object$raw <- xgb.serialize(object$handle) - } - object + return(object) } -#' Accessors for model parameters as JSON string -#' -#' @param object Object of class `xgb.Booster`. -#' @param value A JSON string. +#' @title Accessors for model parameters as JSON string +#' @details Note that assignment is performed in-place on the booster C object, which unlike assignment +#' of R attributes, doesn't follow typical copy-on-write semantics for assignment - i.e. all references +#' to the same booster will also get updated. #' +#' See \link{xgb.copy.Booster} for an example of this behavior. +#' @param object Object of class `xgb.Booster`. \bold{Will be modified in-place} when assigning to it. +#' @param value An R list. +#' @return `xgb.config` will return the parameters as an R list. #' @examples #' data(agaricus.train, package = "xgboost") #' @@ -690,31 +576,36 @@ xgb.attributes <- function(object) { #' @export xgb.config <- function(object) { handle <- xgb.get.handle(object) - .Call(XGBoosterSaveJsonConfig_R, handle) + return(jsonlite::fromJSON(.Call(XGBoosterSaveJsonConfig_R, handle))) } #' @rdname xgb.config #' @export `xgb.config<-` <- function(object, value) { handle <- xgb.get.handle(object) - .Call(XGBoosterLoadJsonConfig_R, handle, value) - object$raw <- NULL # force renew the raw buffer - object <- xgb.Booster.complete(object) - object + .Call( + XGBoosterLoadJsonConfig_R, + handle, + jsonlite::toJSON(value, auto_unbox = TRUE, null = "null") + ) + return(object) } -#' Accessors for model parameters +#' @title Accessors for model parameters +#' @description Only the setter for xgboost parameters is currently implemented. +#' @details Just like \link{xgb.attr}, this function will make in-place modifications +#' on the booster object which do not follow typical R assignment semantics - that is, +#' all references to the same booster will also be updated, unlike assingment of R +#' attributes which follow copy-on-write semantics. #' -#' Only the setter for xgboost parameters is currently implemented. +#' See \link{xgb.copy.Booster} for an example of this behavior. #' -#' @param object Object of class `xgb.Booster` or `xgb.Booster.handle`. +#' Be aware that setting parameters of a fitted booster related to training continuation / updates +#' will reset its number of rounds indicator to zero. +#' @param object Object of class `xgb.Booster`. \bold{Will be modified in-place}. #' @param value A list (or an object coercible to a list) with the names of parameters to set #' and the elements corresponding to parameter values. -#' -#' @details -#' Note that the setter would usually work more efficiently for `xgb.Booster.handle` -#' than for `xgb.Booster`, since only just a handle would need to be copied. -#' +#' @return The same booster `object`, which gets modified in-place. #' @examples #' data(agaricus.train, package = "xgboost") #' train <- agaricus.train @@ -751,28 +642,301 @@ xgb.config <- function(object) { for (i in seq_along(p)) { .Call(XGBoosterSetParam_R, handle, names(p[i]), p[[i]]) } - if (is(object, 'xgb.Booster') && !is.null(object$raw)) { - object$raw <- xgb.serialize(object$handle) + return(object) +} + +#' @rdname getinfo +#' @export +getinfo.xgb.Booster <- function(object, name) { + name <- as.character(head(name, 1L)) + allowed_fields <- c("feature_name", "feature_type") + if (!(name %in% allowed_fields)) { + stop("getinfo: name must be one of the following: ", paste(allowed_fields, collapse = ", ")) } - object + handle <- xgb.get.handle(object) + out <- .Call( + XGBoosterGetStrFeatureInfo_R, + handle, + name + ) + if (!NROW(out)) { + return(NULL) + } + return(out) +} + +#' @rdname getinfo +#' @export +setinfo.xgb.Booster <- function(object, name, info) { + name <- as.character(head(name, 1L)) + allowed_fields <- c("feature_name", "feature_type") + if (!(name %in% allowed_fields)) { + stop("setinfo: unknown info name ", name) + } + info <- as.character(info) + handle <- xgb.get.handle(object) + .Call( + XGBoosterSetStrFeatureInfo_R, + handle, + name, + info + ) + return(TRUE) +} + +#' @title Get number of boosting in a fitted booster +#' @param model A fitted `xgb.Booster` model. +#' @return The number of rounds saved in the model, as an integer. +#' @details Note that setting booster parameters related to training +#' continuation / updates through \link{xgb.parameters<-} will reset the +#' number of rounds to zero. +#' @export +xgb.get.num.boosted.rounds <- function(model) { + return(.Call(XGBoosterBoostedRounds_R, xgb.get.handle(model))) +} + +#' @title Get Features Names from Booster +#' @description Returns the feature / variable / column names from a fitted +#' booster object, which are set automatically during the call to \link{xgb.train} +#' from the DMatrix names, or which can be set manually through \link{setinfo}. +#' +#' If the object doesn't have feature names, will return `NULL`. +#' +#' It is equivalent to calling `getinfo(object, "feature_name")`. +#' @param object An `xgb.Booster` object. +#' @param ... Not used. +#' @export +variable.names.xgb.Booster <- function(object, ...) { + return(getinfo(object, "feature_name")) } -# Extract the number of trees in a model. -# TODO: either add a getter to C-interface, or simply set an 'ntree' attribute after each iteration. -# internal utility function xgb.ntree <- function(bst) { - length(grep('^booster', xgb.dump(bst))) + config <- xgb.config(bst) + out <- strtoi(config$learner$gradient_booster$gbtree_model_param$num_trees) + return(out) } +xgb.nthread <- function(bst) { + config <- xgb.config(bst) + out <- strtoi(config$learner$generic_param$nthread) + return(out) +} -#' Print xgb.Booster +xgb.booster_type <- function(bst) { + config <- xgb.config(bst) + out <- config$learner$learner_train_param$booster + return(out) +} + +xgb.num_class <- function(bst) { + config <- xgb.config(bst) + out <- strtoi(config$learner$learner_model_param$num_class) + return(out) +} + +xgb.feature_names <- function(bst) { + return(getinfo(bst, "feature_name")) +} + +xgb.feature_types <- function(bst) { + return(getinfo(bst, "feature_type")) +} + +xgb.num_feature <- function(bst) { + handle <- xgb.get.handle(bst) + return(.Call(XGBoosterGetNumFeature_R, handle)) +} + +xgb.best_iteration <- function(bst) { + out <- xgb.attr(bst, "best_iteration") + if (!NROW(out) || !nchar(out)) { + out <- NULL + } + return(out) +} + +#' @title Extract coefficients from linear booster +#' @description Extracts the coefficients from a 'gblinear' booster object, +#' as produced by \code{xgb.train} when using parameter `booster="gblinear"`. #' -#' Print information about `xgb.Booster`. +#' Note: this function will error out if passing a booster model +#' which is not of "gblinear" type. +#' @param object A fitted booster of 'gblinear' type. +#' @param ... Not used. +#' @return The extracted coefficients:\itemize{ +#' \item If there's only one coefficient per column in the data, will be returned as a +#' vector, potentially containing the feature names if available, with the intercept +#' as first column. +#' \item If there's more than one coefficient per column in the data (e.g. when using +#' `objective="multi:softmax"`), will be returned as a matrix with dimensions equal +#' to `[num_features, num_cols]`, with the intercepts as first row. Note that the column +#' (classes in multi-class classification) dimension will not be named. +#' } #' +#' The intercept returned here will include the 'base_score' parameter (unlike the 'bias' +#' or the last coefficient in the model dump, which doesn't have 'base_score' added to it), +#' hence one should get the same values from calling `predict(..., outputmargin = TRUE)` and +#' from performing a matrix multiplication with `model.matrix(~., ...)`. +#' +#' Be aware that the coefficients are obtained by first converting them to strings and +#' back, so there will always be some very small lose of precision compared to the actual +#' coefficients as used by \link{predict.xgb.Booster}. +#' @examples +#' library(xgboost) +#' data(mtcars) +#' y <- mtcars[, 1] +#' x <- as.matrix(mtcars[, -1]) +#' dm <- xgb.DMatrix(data = x, label = y, nthread = 1) +#' params <- list(booster = "gblinear", nthread = 1) +#' model <- xgb.train(data = dm, params = params, nrounds = 2) +#' coef(model) +#' @export +coef.xgb.Booster <- function(object, ...) { + booster_type <- xgb.booster_type(object) + if (booster_type != "gblinear") { + stop("Coefficients are not defined for Booster type ", booster_type) + } + model_json <- jsonlite::fromJSON(rawToChar(xgb.save.raw(object, raw_format = "json"))) + base_score <- model_json$learner$learner_model_param$base_score + num_feature <- as.numeric(model_json$learner$learner_model_param$num_feature) + + weights <- model_json$learner$gradient_booster$model$weights + n_cols <- length(weights) / (num_feature + 1) + if (n_cols != floor(n_cols) || n_cols < 1) { + stop("Internal error: could not determine shape of coefficients.") + } + sep <- num_feature * n_cols + coefs <- weights[seq(1, sep)] + intercepts <- weights[seq(sep + 1, length(weights))] + intercepts <- intercepts + as.numeric(base_score) + + feature_names <- xgb.feature_names(object) + if (!NROW(feature_names)) { + # This mimics the default naming in R which names columns as "V1..N" + # when names are needed but not available + feature_names <- paste0("V", seq(1L, num_feature)) + } + feature_names <- c("(Intercept)", feature_names) + if (n_cols == 1L) { + out <- c(intercepts, coefs) + names(out) <- feature_names + } else { + coefs <- matrix(coefs, nrow = num_feature, byrow = TRUE) + dim(intercepts) <- c(1L, n_cols) + out <- rbind(intercepts, coefs) + row.names(out) <- feature_names + # TODO: if a class names attributes is added, + # should use those names here. + } + return(out) +} + +#' @title Deep-copies a Booster Object +#' @description Creates a deep copy of an 'xgb.Booster' object, such that the +#' C object pointer contained will be a different object, and hence functions +#' like \link{xgb.attr} will not affect the object from which it was copied. +#' @param model An 'xgb.Booster' object. +#' @return A deep copy of `model` - it will be identical in every way, but C-level +#' functions called on that copy will not affect the `model` variable. +#' @examples +#' library(xgboost) +#' data(mtcars) +#' y <- mtcars$mpg +#' x <- mtcars[, -1] +#' dm <- xgb.DMatrix(x, label = y, nthread = 1) +#' model <- xgb.train( +#' data = dm, +#' params = list(nthread = 1), +#' nround = 3 +#' ) +#' +#' # Set an arbitrary attribute kept at the C level +#' xgb.attr(model, "my_attr") <- 100 +#' print(xgb.attr(model, "my_attr")) +#' +#' # Just assigning to a new variable will not create +#' # a deep copy - C object pointer is shared, and in-place +#' # modifications will affect both objects +#' model_shallow_copy <- model +#' xgb.attr(model_shallow_copy, "my_attr") <- 333 +#' # 'model' was also affected by this change: +#' print(xgb.attr(model, "my_attr")) +#' +#' model_deep_copy <- xgb.copy.Booster(model) +#' xgb.attr(model_deep_copy, "my_attr") <- 444 +#' # 'model' was NOT affected by this change +#' # (keeps previous value that was assigned before) +#' print(xgb.attr(model, "my_attr")) +#' +#' # Verify that the new object was actually modified +#' print(xgb.attr(model_deep_copy, "my_attr")) +#' @export +xgb.copy.Booster <- function(model) { + if (!inherits(model, "xgb.Booster")) { + stop("'model' must be an 'xgb.Booster' object.") + } + return(.Call(XGDuplicate_R, model)) +} + +#' @title Check if two boosters share the same C object +#' @description Checks whether two booster objects refer to the same underlying C object. +#' @details As booster objects (as returned by e.g. \link{xgb.train}) contain an R 'externalptr' +#' object, they don't follow typical copy-on-write semantics of other R objects - that is, if +#' one assigns a booster to a different variable and modifies that new variable through in-place +#' methods like \link{xgb.attr<-}, the modification will be applied to both the old and the new +#' variable, unlike typical R assignments which would only modify the latter. +#' +#' This function allows checking whether two booster objects share the same 'externalptr', +#' regardless of the R attributes that they might have. +#' +#' In order to duplicate a booster in such a way that the copy wouldn't share the same +#' 'externalptr', one can use function \link{xgb.copy.Booster}. +#' @param obj1 Booster model to compare with `obj2`. +#' @param obj2 Booster model to compare with `obj1`. +#' @return Either `TRUE` or `FALSE` according to whether the two boosters share +#' the underlying C object. +#' @seealso \link{xgb.copy.Booster} +#' @examples +#' library(xgboost) +#' data(mtcars) +#' y <- mtcars$mpg +#' x <- as.matrix(mtcars[, -1]) +#' model <- xgb.train( +#' params = list(nthread = 1), +#' data = xgb.DMatrix(x, label = y, nthread = 1), +#' nround = 3 +#' ) +#' +#' model_shallow_copy <- model +#' xgb.is.same.Booster(model, model_shallow_copy) # same C object +#' +#' model_deep_copy <- xgb.copy.Booster(model) +#' xgb.is.same.Booster(model, model_deep_copy) # different C objects +#' +#' # In-place assignments modify all references, +#' # but not full/deep copies of the booster +#' xgb.attr(model_shallow_copy, "my_attr") <- 111 +#' xgb.attr(model, "my_attr") # gets modified +#' xgb.attr(model_deep_copy, "my_attr") # doesn't get modified +#' @export +xgb.is.same.Booster <- function(obj1, obj2) { + if (!inherits(obj1, "xgb.Booster") || !inherits(obj2, "xgb.Booster")) { + stop("'xgb.is.same.Booster' is only applicable to 'xgb.Booster' objects.") + } + return( + .Call( + XGPointerEqComparison_R, + xgb.get.handle(obj1), + xgb.get.handle(obj2) + ) + ) +} + +#' @title Print xgb.Booster +#' @description Print information about `xgb.Booster`. #' @param x An `xgb.Booster` object. -#' @param verbose Whether to print detailed data (e.g., attribute values). -#' @param ... Not currently used. -#' +#' @param ... Not used. +#' @return The same `x` object, returned invisibly #' @examples #' data(agaricus.train, package = "xgboost") #' train <- agaricus.train @@ -790,79 +954,40 @@ xgb.ntree <- function(bst) { #' attr(bst, "myattr") <- "memo" #' #' print(bst) -#' print(bst, verbose = TRUE) #' #' @export -print.xgb.Booster <- function(x, verbose = FALSE, ...) { +print.xgb.Booster <- function(x, ...) { + # this lets it error out when the object comes from an earlier R xgboost version + handle <- xgb.get.handle(x) cat('##### xgb.Booster\n') - valid_handle <- !is.null.handle(x$handle) - if (!valid_handle) - cat("Handle is invalid! Suggest using xgb.Booster.complete\n") - - cat('raw: ') - if (!is.null(x$raw)) { - cat(format(object.size(x$raw), units = "auto"), '\n') - } else { - cat('NULL\n') - } - if (!is.null(x$call)) { + R_attrs <- attributes(x) + if (!is.null(R_attrs$call)) { cat('call:\n ') - print(x$call) + print(R_attrs$call) } - if (!is.null(x$params)) { - cat('params (as set within xgb.train):\n') - cat(' ', - paste(names(x$params), - paste0('"', unlist(x$params), '"'), - sep = ' = ', collapse = ', '), '\n', sep = '') - } - # TODO: need an interface to access all the xgboosts parameters + cat('# of features:', xgb.num_feature(x), '\n') + cat('# of rounds: ', xgb.get.num.boosted.rounds(x), '\n') - attrs <- character(0) - if (valid_handle) - attrs <- xgb.attributes(x) - if (length(attrs) > 0) { + attr_names <- .Call(XGBoosterGetAttrNames_R, handle) + if (NROW(attr_names)) { cat('xgb.attributes:\n') - if (verbose) { - cat(paste(paste0(' ', names(attrs)), - paste0('"', unlist(attrs), '"'), - sep = ' = ', collapse = '\n'), '\n', sep = '') - } else { - cat(' ', paste(names(attrs), collapse = ', '), '\n', sep = '') - } + cat(" ", paste(attr_names, collapse = ", "), "\n") } - if (!is.null(x$callbacks) && length(x$callbacks) > 0) { + if (!is.null(R_attrs$callbacks) && length(R_attrs$callbacks) > 0) { cat('callbacks:\n') - lapply(callback.calls(x$callbacks), function(x) { + lapply(callback.calls(R_attrs$callbacks), function(x) { cat(' ') print(x) }) } - if (!is.null(x$feature_names)) - cat('# of features:', length(x$feature_names), '\n') - - cat('niter: ', x$niter, '\n', sep = '') - # TODO: uncomment when faster xgb.ntree is implemented - #cat('ntree: ', xgb.ntree(x), '\n', sep='') - - for (n in setdiff(names(x), c('handle', 'raw', 'call', 'params', 'callbacks', - 'evaluation_log', 'niter', 'feature_names'))) { - if (is.atomic(x[[n]])) { - cat(n, ':', x[[n]], '\n', sep = ' ') - } else { - cat(n, ':\n\t', sep = ' ') - print(x[[n]]) - } - } - - if (!is.null(x$evaluation_log)) { + if (!is.null(R_attrs$evaluation_log)) { cat('evaluation_log:\n') - print(x$evaluation_log, row.names = FALSE, topn = 2) + print(R_attrs$evaluation_log, row.names = FALSE, topn = 2) } - invisible(x) + return(invisible(x)) } diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index f9a810c85..7c4c30bd3 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -335,14 +335,13 @@ dimnames.xgb.DMatrix <- function(x) { } -#' Get information of an xgb.DMatrix object -#' -#' Get information of an xgb.DMatrix object -#' @param object Object of class \code{xgb.DMatrix} +#' @title Get or set information of xgb.DMatrix and xgb.Booster objects +#' @param object Object of class \code{xgb.DMatrix} of `xgb.Booster`. #' @param name the name of the information field to get (see details) -#' +#' @return For `getinfo`, will return the requested field. For `setinfo`, will always return value `TRUE` +#' if it succeeds. #' @details -#' The \code{name} field can be one of the following: +#' The \code{name} field can be one of the following for `xgb.DMatrix`: #' #' \itemize{ #' \item \code{label} @@ -357,9 +356,17 @@ dimnames.xgb.DMatrix <- function(x) { #' } #' See the documentation for \link{xgb.DMatrix} for more information about these fields. #' +#' For `xgb.Booster`, can be one of the following: +#' \itemize{ +#' \item \code{feature_type} +#' \item \code{feature_name} +#' } +#' #' Note that, while 'qid' cannot be retrieved, it's possible to get the equivalent 'group' #' for a DMatrix that had 'qid' assigned. #' +#' \bold{Important}: when calling `setinfo`, the objects are modified in-place. See +#' \link{xgb.copy.Booster} for an idea of this in-place assignment works. #' @examples #' data(agaricus.train, package='xgboost') #' dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) @@ -412,13 +419,7 @@ getinfo.xgb.DMatrix <- function(object, name) { return(ret) } - -#' Set information of an xgb.DMatrix object -#' -#' Set information of an xgb.DMatrix object -#' -#' @param object Object of class "xgb.DMatrix" -#' @param name the name of the field to get +#' @rdname getinfo #' @param info the specific field of information to set #' #' @details @@ -441,11 +442,10 @@ getinfo.xgb.DMatrix <- function(object, name) { #' setinfo(dtrain, 'label', 1-labels) #' labels2 <- getinfo(dtrain, 'label') #' stopifnot(all.equal(labels2, 1-labels)) -#' @rdname setinfo #' @export setinfo <- function(object, name, info) UseMethod("setinfo") -#' @rdname setinfo +#' @rdname getinfo #' @export setinfo.xgb.DMatrix <- function(object, name, info) { .internal.setinfo.xgb.DMatrix(object, name, info) diff --git a/R-package/R/xgb.cv.R b/R-package/R/xgb.cv.R index b0d8c4ebe..a960957ca 100644 --- a/R-package/R/xgb.cv.R +++ b/R-package/R/xgb.cv.R @@ -204,13 +204,13 @@ xgb.cv <- function(params = list(), data, nrounds, nfold, label = NULL, missing dtrain <- slice(dall, unlist(folds[-k])) else dtrain <- slice(dall, train_folds[[k]]) - handle <- xgb.Booster.handle( + bst <- xgb.Booster( params = params, cachelist = list(dtrain, dtest), - modelfile = NULL, - handle = NULL + modelfile = NULL ) - list(dtrain = dtrain, bst = handle, watchlist = list(train = dtrain, test = dtest), index = folds[[k]]) + bst <- bst$bst + list(dtrain = dtrain, bst = bst, watchlist = list(train = dtrain, test = dtest), index = folds[[k]]) }) rm(dall) # a "basket" to collect some results from callbacks @@ -231,13 +231,13 @@ xgb.cv <- function(params = list(), data, nrounds, nfold, label = NULL, missing msg <- lapply(bst_folds, function(fd) { xgb.iter.update( - booster_handle = fd$bst, + bst = fd$bst, dtrain = fd$dtrain, iter = iteration - 1, obj = obj ) xgb.iter.eval( - booster_handle = fd$bst, + bst = fd$bst, watchlist = fd$watchlist, iter = iteration - 1, feval = feval @@ -267,7 +267,7 @@ xgb.cv <- function(params = list(), data, nrounds, nfold, label = NULL, missing ret <- c(ret, basket) class(ret) <- 'xgb.cv.synchronous' - invisible(ret) + return(invisible(ret)) } diff --git a/R-package/R/xgb.dump.R b/R-package/R/xgb.dump.R index 4421836d1..3a3d2c7dc 100644 --- a/R-package/R/xgb.dump.R +++ b/R-package/R/xgb.dump.R @@ -56,9 +56,13 @@ xgb.dump <- function(model, fname = NULL, fmap = "", with_stats = FALSE, if (!(is.null(fmap) || is.character(fmap))) stop("fmap: argument must be a character string (when provided)") - model <- xgb.Booster.complete(model) - model_dump <- .Call(XGBoosterDumpModel_R, model$handle, NVL(fmap, "")[1], as.integer(with_stats), - as.character(dump_format)) + model_dump <- .Call( + XGBoosterDumpModel_R, + xgb.get.handle(model), + NVL(fmap, "")[1], + as.integer(with_stats), + as.character(dump_format) + ) if (dump_format == "dot") { return(sapply(model_dump, function(x) gsub("^booster\\[\\d+\\]\\n", "\\1", x))) } diff --git a/R-package/R/xgb.importance.R b/R-package/R/xgb.importance.R index c94e1babb..44f2eb9b3 100644 --- a/R-package/R/xgb.importance.R +++ b/R-package/R/xgb.importance.R @@ -119,21 +119,21 @@ xgb.importance <- function(feature_names = NULL, model = NULL, trees = NULL, if (!(is.null(data) && is.null(label) && is.null(target))) warning("xgb.importance: parameters 'data', 'label' and 'target' are deprecated") - if (!inherits(model, "xgb.Booster")) - stop("model: must be an object of class xgb.Booster") - - if (is.null(feature_names) && !is.null(model$feature_names)) - feature_names <- model$feature_names + if (is.null(feature_names)) { + model_feature_names <- xgb.feature_names(model) + if (NROW(model_feature_names)) { + feature_names <- model_feature_names + } + } if (!(is.null(feature_names) || is.character(feature_names))) stop("feature_names: Has to be a character vector") - model <- xgb.Booster.complete(model) - config <- jsonlite::fromJSON(xgb.config(model)) - if (config$learner$gradient_booster$name == "gblinear") { + handle <- xgb.get.handle(model) + if (xgb.booster_type(model) == "gblinear") { args <- list(importance_type = "weight", feature_names = feature_names) results <- .Call( - XGBoosterFeatureScore_R, model$handle, jsonlite::toJSON(args, auto_unbox = TRUE, null = "null") + XGBoosterFeatureScore_R, handle, jsonlite::toJSON(args, auto_unbox = TRUE, null = "null") ) names(results) <- c("features", "shape", "weight") if (length(results$shape) == 2) { @@ -154,7 +154,7 @@ xgb.importance <- function(feature_names = NULL, model = NULL, trees = NULL, for (importance_type in c("weight", "total_gain", "total_cover")) { args <- list(importance_type = importance_type, feature_names = feature_names, tree_idx = trees) results <- .Call( - XGBoosterFeatureScore_R, model$handle, jsonlite::toJSON(args, auto_unbox = TRUE, null = "null") + XGBoosterFeatureScore_R, handle, jsonlite::toJSON(args, auto_unbox = TRUE, null = "null") ) names(results) <- c("features", "shape", importance_type) concatenated[ diff --git a/R-package/R/xgb.load.R b/R-package/R/xgb.load.R index 27844ed8c..7d1eab7e9 100644 --- a/R-package/R/xgb.load.R +++ b/R-package/R/xgb.load.R @@ -17,7 +17,7 @@ #' An object of \code{xgb.Booster} class. #' #' @seealso -#' \code{\link{xgb.save}}, \code{\link{xgb.Booster.complete}}. +#' \code{\link{xgb.save}} #' #' @examples #' data(agaricus.train, package='xgboost') @@ -46,25 +46,20 @@ xgb.load <- function(modelfile) { if (is.null(modelfile)) stop("xgb.load: modelfile cannot be NULL") - handle <- xgb.Booster.handle( + bst <- xgb.Booster( params = list(), cachelist = list(), - modelfile = modelfile, - handle = NULL + modelfile = modelfile ) + bst <- bst$bst # re-use modelfile if it is raw so we do not need to serialize if (typeof(modelfile) == "raw") { warning( paste( "The support for loading raw booster with `xgb.load` will be ", - "discontinued in upcoming release. Use `xgb.load.raw` or", - " `xgb.unserialize` instead. " + "discontinued in upcoming release. Use `xgb.load.raw` instead. " ) ) - bst <- xgb.handleToBooster(handle = handle, raw = modelfile) - } else { - bst <- xgb.handleToBooster(handle = handle, raw = NULL) } - bst <- xgb.Booster.complete(bst, saveraw = TRUE) return(bst) } diff --git a/R-package/R/xgb.load.raw.R b/R-package/R/xgb.load.raw.R index b159e9de1..73ac50dc6 100644 --- a/R-package/R/xgb.load.raw.R +++ b/R-package/R/xgb.load.raw.R @@ -3,21 +3,10 @@ #' User can generate raw memory buffer by calling xgb.save.raw #' #' @param buffer the buffer returned by xgb.save.raw -#' @param as_booster Return the loaded model as xgb.Booster instead of xgb.Booster.handle. -#' #' @export -xgb.load.raw <- function(buffer, as_booster = FALSE) { +xgb.load.raw <- function(buffer) { cachelist <- list() - handle <- .Call(XGBoosterCreate_R, cachelist) - .Call(XGBoosterLoadModelFromRaw_R, handle, buffer) - class(handle) <- "xgb.Booster.handle" - - if (as_booster) { - booster <- list(handle = handle, raw = NULL) - class(booster) <- "xgb.Booster" - booster <- xgb.Booster.complete(booster, saveraw = TRUE) - return(booster) - } else { - return(handle) - } + bst <- .Call(XGBoosterCreate_R, cachelist) + .Call(XGBoosterLoadModelFromRaw_R, xgb.get.handle(bst), buffer) + return(bst) } diff --git a/R-package/R/xgb.model.dt.tree.R b/R-package/R/xgb.model.dt.tree.R index 9a32d82a0..df0e672a9 100644 --- a/R-package/R/xgb.model.dt.tree.R +++ b/R-package/R/xgb.model.dt.tree.R @@ -2,8 +2,10 @@ #' #' Parse a boosted tree model text dump into a `data.table` structure. #' -#' @param feature_names Character vector used to overwrite the feature names -#' of the model. The default (`NULL`) uses the original feature names. +#' @param feature_names Character vector of feature names. If the model already +#' contains feature names, those will be used when \code{feature_names=NULL} (default value). +#' +#' Note that, if the model already contains feature names, it's \bold{not} possible to override them here. #' @param model Object of class `xgb.Booster`. #' @param text Character vector previously generated by the function [xgb.dump()] #' (called with parameter `with_stats = TRUE`). `text` takes precedence over `model`. @@ -54,8 +56,6 @@ #' objective = "binary:logistic" #' ) #' -#' (dt <- xgb.model.dt.tree(colnames(agaricus.train$data), bst)) -#' #' # This bst model already has feature_names stored with it, so those would be used when #' # feature_names is not set: #' (dt <- xgb.model.dt.tree(model = bst)) @@ -79,8 +79,15 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, " (or NULL if 'model' was provided).") } - if (is.null(feature_names) && !is.null(model) && !is.null(model$feature_names)) - feature_names <- model$feature_names + model_feature_names <- NULL + if (inherits(model, "xgb.Booster")) { + model_feature_names <- xgb.feature_names(model) + if (NROW(model_feature_names) && !is.null(feature_names)) { + stop("'model' contains feature names. Cannot override them.") + } + } + if (is.null(feature_names) && !is.null(model) && !is.null(model_feature_names)) + feature_names <- model_feature_names if (!(is.null(feature_names) || is.character(feature_names))) { stop("feature_names: must be a character vector") @@ -90,8 +97,10 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, stop("trees: must be a vector of integers.") } + from_text <- TRUE if (is.null(text)) { text <- xgb.dump(model = model, with_stats = TRUE) + from_text <- FALSE } if (length(text) < 2 || !any(grepl('leaf=(\\d+)', text))) { @@ -120,8 +129,28 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, td[, isLeaf := grepl("leaf", t, fixed = TRUE)] # parse branch lines - branch_rx <- paste0("f(\\d+)<(", anynumber_regex, ")\\] yes=(\\d+),no=(\\d+),missing=(\\d+),", - "gain=(", anynumber_regex, "),cover=(", anynumber_regex, ")") + branch_rx_nonames <- paste0("f(\\d+)<(", anynumber_regex, ")\\] yes=(\\d+),no=(\\d+),missing=(\\d+),", + "gain=(", anynumber_regex, "),cover=(", anynumber_regex, ")") + branch_rx_w_names <- paste0("\\d+:\\[(.+)<(", anynumber_regex, ")\\] yes=(\\d+),no=(\\d+),missing=(\\d+),", + "gain=(", anynumber_regex, "),cover=(", anynumber_regex, ")") + text_has_feature_names <- FALSE + if (NROW(model_feature_names)) { + branch_rx <- branch_rx_w_names + text_has_feature_names <- TRUE + } else { + # Note: when passing a text dump, it might or might not have feature names, + # but that aspect is unknown from just the text attributes + branch_rx <- branch_rx_nonames + if (from_text) { + if (sum(grepl(branch_rx_w_names, text)) > sum(grepl(branch_rx_nonames, text))) { + branch_rx <- branch_rx_w_names + text_has_feature_names <- TRUE + } + } + } + if (text_has_feature_names && is.null(model) && !is.null(feature_names)) { + stop("'text' contains feature names. Cannot override them.") + } branch_cols <- c("Feature", "Split", "Yes", "No", "Missing", "Gain", "Cover") td[ isLeaf == FALSE, @@ -144,10 +173,12 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, is_stump <- function() { return(length(td$Feature) == 1 && is.na(td$Feature)) } - if (!is.null(feature_names) && !is_stump()) { - if (length(feature_names) <= max(as.numeric(td$Feature), na.rm = TRUE)) - stop("feature_names has less elements than there are features used in the model") - td[isLeaf == FALSE, Feature := feature_names[as.numeric(Feature) + 1]] + if (!text_has_feature_names) { + if (!is.null(feature_names) && !is_stump()) { + if (length(feature_names) <= max(as.numeric(td$Feature), na.rm = TRUE)) + stop("feature_names has less elements than there are features used in the model") + td[isLeaf == FALSE, Feature := feature_names[as.numeric(Feature) + 1]] + } } # parse leaf lines diff --git a/R-package/R/xgb.plot.shap.R b/R-package/R/xgb.plot.shap.R index 35cf664ec..788a09539 100644 --- a/R-package/R/xgb.plot.shap.R +++ b/R-package/R/xgb.plot.shap.R @@ -303,7 +303,11 @@ xgb.shap.data <- function(data, shap_contrib = NULL, features = NULL, top_n = 1, if (is.character(features) && is.null(colnames(data))) stop("either provide `data` with column names or provide `features` as column indices") - if (is.null(model$feature_names) && model$nfeatures != ncol(data)) + model_feature_names <- NULL + if (is.null(features) && !is.null(model)) { + model_feature_names <- xgb.feature_names(model) + } + if (is.null(model_feature_names) && xgb.num_feature(model) != ncol(data)) stop("if model has no feature_names, columns in `data` must match features in model") if (!is.null(subsample)) { @@ -332,7 +336,7 @@ xgb.shap.data <- function(data, shap_contrib = NULL, features = NULL, top_n = 1, } if (is.null(features)) { - if (!is.null(model$feature_names)) { + if (!is.null(model_feature_names)) { imp <- xgb.importance(model = model, trees = trees) } else { imp <- xgb.importance(model = model, trees = trees, feature_names = colnames(data)) diff --git a/R-package/R/xgb.save.R b/R-package/R/xgb.save.R index 474153bda..e1a61d196 100644 --- a/R-package/R/xgb.save.R +++ b/R-package/R/xgb.save.R @@ -1,12 +1,24 @@ #' Save xgboost model to binary file #' -#' Save xgboost model to a file in binary format. +#' Save xgboost model to a file in binary or JSON format. #' -#' @param model model object of \code{xgb.Booster} class. -#' @param fname name of the file to write. +#' @param model Model object of \code{xgb.Booster} class. +#' @param fname Name of the file to write. +#' +#' Note that the extension of this file name determined the serialization format to use:\itemize{ +#' \item Extension ".ubj" will use the universal binary JSON format (recommended). +#' This format uses binary types for e.g. floating point numbers, thereby preventing any loss +#' of precision when converting to a human-readable JSON text or similar. +#' \item Extension ".json" will use plain JSON, which is a human-readable format. +#' \item Extension ".deprecated" will use a \bold{deprecated} binary format. This format will +#' not be able to save attributes introduced after v1 of XGBoost, such as the "best_iteration" +#' attribute that boosters might keep, nor feature names or user-specifiec attributes. +#' \item If the format is not specified by passing one of the file extensions above, will +#' default to UBJ. +#' } #' #' @details -#' This methods allows to save a model in an xgboost-internal binary format which is universal +#' This methods allows to save a model in an xgboost-internal binary or text format which is universal #' among the various xgboost interfaces. In R, the saved model file could be read-in later #' using either the \code{\link{xgb.load}} function or the \code{xgb_model} parameter #' of \code{\link{xgb.train}}. @@ -14,13 +26,13 @@ #' Note: a model can also be saved as an R-object (e.g., by using \code{\link[base]{readRDS}} #' or \code{\link[base]{save}}). However, it would then only be compatible with R, and #' corresponding R-methods would need to be used to load it. Moreover, persisting the model with -#' \code{\link[base]{readRDS}} or \code{\link[base]{save}}) will cause compatibility problems in +#' \code{\link[base]{readRDS}} or \code{\link[base]{save}}) might cause compatibility problems in #' future versions of XGBoost. Consult \code{\link{a-compatibility-note-for-saveRDS-save}} to learn #' how to persist models in a future-proof way, i.e. to make the model accessible in future #' releases of XGBoost. #' #' @seealso -#' \code{\link{xgb.load}}, \code{\link{xgb.Booster.complete}}. +#' \code{\link{xgb.load}} #' #' @examples #' data(agaricus.train, package='xgboost') @@ -51,8 +63,7 @@ xgb.save <- function(model, fname) { stop("model must be xgb.Booster.", if (inherits(model, "xgb.DMatrix")) " Use xgb.DMatrix.save to save an xgb.DMatrix object." else "") } - model <- xgb.Booster.complete(model, saveraw = FALSE) fname <- path.expand(fname) - .Call(XGBoosterSaveModel_R, model$handle, enc2utf8(fname[1])) + .Call(XGBoosterSaveModel_R, xgb.get.handle(model), enc2utf8(fname[1])) return(TRUE) } diff --git a/R-package/R/xgb.save.raw.R b/R-package/R/xgb.save.raw.R index 63c06e071..c124a752b 100644 --- a/R-package/R/xgb.save.raw.R +++ b/R-package/R/xgb.save.raw.R @@ -11,8 +11,6 @@ #' \item \code{deprecated}: Encode the booster into old customized binary format. #' } #' -#' Right now the default is \code{deprecated} but will be changed to \code{ubj} in upcoming release. -#' #' @examples #' data(agaricus.train, package='xgboost') #' data(agaricus.test, package='xgboost') @@ -30,7 +28,7 @@ #' bst <- xgb.load.raw(raw) #' #' @export -xgb.save.raw <- function(model, raw_format = "deprecated") { +xgb.save.raw <- function(model, raw_format = "ubj") { handle <- xgb.get.handle(model) args <- list(format = raw_format) .Call(XGBoosterSaveModelToRaw_R, handle, jsonlite::toJSON(args, auto_unbox = TRUE)) diff --git a/R-package/R/xgb.serialize.R b/R-package/R/xgb.serialize.R deleted file mode 100644 index c20d2b51c..000000000 --- a/R-package/R/xgb.serialize.R +++ /dev/null @@ -1,21 +0,0 @@ -#' Serialize the booster instance into R's raw vector. The serialization method differs -#' from \code{\link{xgb.save.raw}} as the latter one saves only the model but not -#' parameters. This serialization format is not stable across different xgboost versions. -#' -#' @param booster the booster instance -#' -#' @examples -#' data(agaricus.train, package='xgboost') -#' data(agaricus.test, package='xgboost') -#' train <- agaricus.train -#' test <- agaricus.test -#' bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") -#' raw <- xgb.serialize(bst) -#' bst <- xgb.unserialize(raw) -#' -#' @export -xgb.serialize <- function(booster) { - handle <- xgb.get.handle(booster) - .Call(XGBoosterSerializeToBuffer_R, handle) -} diff --git a/R-package/R/xgb.train.R b/R-package/R/xgb.train.R index e20c1af3e..a313ed32f 100644 --- a/R-package/R/xgb.train.R +++ b/R-package/R/xgb.train.R @@ -152,6 +152,10 @@ #' See \code{\link{callbacks}}. Some of the callbacks are automatically created depending on the #' parameters' values. User can provide either existing or their own callback methods in order #' to customize the training process. +#' +#' Note that some callbacks might try to set an evaluation log - be aware that these evaluation logs +#' are kept as R attributes, and thus do not get saved when using non-R serializaters like +#' \link{xgb.save} (but are kept when using R serializers like \link{saveRDS}). #' @param ... other parameters to pass to \code{params}. #' @param label vector of response values. Should not be provided when data is #' a local data file name or an \code{xgb.DMatrix}. @@ -160,6 +164,9 @@ #' This parameter is only used when input is a dense matrix. #' @param weight a vector indicating the weight for each row of the input. #' +#' @return +#' An object of class \code{xgb.Booster}. +#' #' @details #' These are the training functions for \code{xgboost}. #' @@ -201,28 +208,20 @@ #' \item \code{cb.save.model}: when \code{save_period > 0} is set. #' } #' -#' @return -#' An object of class \code{xgb.Booster} with the following elements: -#' \itemize{ -#' \item \code{handle} a handle (pointer) to the xgboost model in memory. -#' \item \code{raw} a cached memory dump of the xgboost model saved as R's \code{raw} type. -#' \item \code{niter} number of boosting iterations. -#' \item \code{evaluation_log} evaluation history stored as a \code{data.table} with the -#' first column corresponding to iteration number and the rest corresponding to evaluation -#' metrics' values. It is created by the \code{\link{cb.evaluation.log}} callback. -#' \item \code{call} a function call. -#' \item \code{params} parameters that were passed to the xgboost library. Note that it does not -#' capture parameters changed by the \code{\link{cb.reset.parameters}} callback. -#' \item \code{callbacks} callback functions that were either automatically assigned or -#' explicitly passed. -#' \item \code{best_iteration} iteration number with the best evaluation metric value -#' (only available with early stopping). -#' \item \code{best_score} the best evaluation metric value during early stopping. -#' (only available with early stopping). -#' \item \code{feature_names} names of the training dataset features -#' (only when column names were defined in training data). -#' \item \code{nfeatures} number of features in training data. -#' } +#' Note that objects of type `xgb.Booster` as returned by this function behave a bit differently +#' from typical R objects (it's an 'altrep' list class), and it makes a separation between +#' internal booster attributes (restricted to jsonifyable data), accessed through \link{xgb.attr} +#' and shared between interfaces through serialization functions like \link{xgb.save}; and +#' R-specific attributes, accessed through \link{attributes} and \link{attr}, which are otherwise +#' only used in the R interface, only kept when using R's serializers like \link{saveRDS}, and +#' not anyhow used by functions like \link{predict.xgb.Booster}. +#' +#' Be aware that one such R attribute that is automatically added is `params` - this attribute +#' is assigned from the `params` argument to this function, and is only meant to serve as a +#' reference for what went into the booster, but is not used in other methods that take a booster +#' object - so for example, changing the booster's configuration requires calling `xgb.config<-` +#' or 'xgb.parameters<-', while simply modifying `attributes(model)$params$<...>` will have no +#' effect elsewhere. #' #' @seealso #' \code{\link{callbacks}}, @@ -371,27 +370,31 @@ xgb.train <- function(params = list(), data, nrounds, watchlist = list(), # The tree updating process would need slightly different handling is_update <- NVL(params[['process_type']], '.') == 'update' + past_evaluation_log <- NULL + if (inherits(xgb_model, "xgb.Booster")) { + past_evaluation_log <- attributes(xgb_model)$evaluation_log + } + # Construct a booster (either a new one or load from xgb_model) - handle <- xgb.Booster.handle( + bst <- xgb.Booster( params = params, cachelist = append(watchlist, dtrain), - modelfile = xgb_model, - handle = NULL + modelfile = xgb_model + ) + niter_init <- bst$niter + bst <- bst$bst + .Call( + XGBoosterCopyInfoFromDMatrix_R, + xgb.get.handle(bst), + dtrain ) - bst <- xgb.handleToBooster(handle = handle, raw = NULL) # extract parameters that can affect the relationship b/w #trees and #iterations - num_class <- max(as.numeric(NVL(params[['num_class']], 1)), 1) - num_parallel_tree <- max(as.numeric(NVL(params[['num_parallel_tree']], 1)), 1) + # Note: it might look like these aren't used, but they need to be defined in this + # environment for the callbacks for work correctly. + num_class <- max(as.numeric(NVL(params[['num_class']], 1)), 1) # nolint + num_parallel_tree <- max(as.numeric(NVL(params[['num_parallel_tree']], 1)), 1) # nolint - # When the 'xgb_model' was set, find out how many boosting iterations it has - niter_init <- 0 - if (!is.null(xgb_model)) { - niter_init <- as.numeric(xgb.attr(bst, 'niter')) + 1 - if (length(niter_init) == 0) { - niter_init <- xgb.ntree(bst) %/% (num_parallel_tree * num_class) - } - } if (is_update && nrounds > niter_init) stop("nrounds cannot be larger than ", niter_init, " (nrounds of xgb_model)") @@ -405,7 +408,7 @@ xgb.train <- function(params = list(), data, nrounds, watchlist = list(), for (f in cb$pre_iter) f() xgb.iter.update( - booster_handle = bst$handle, + bst = bst, dtrain = dtrain, iter = iteration - 1, obj = obj @@ -413,46 +416,43 @@ xgb.train <- function(params = list(), data, nrounds, watchlist = list(), if (length(watchlist) > 0) { bst_evaluation <- xgb.iter.eval( # nolint: object_usage_linter - booster_handle = bst$handle, + bst = bst, watchlist = watchlist, iter = iteration - 1, feval = feval ) } - xgb.attr(bst$handle, 'niter') <- iteration - 1 - for (f in cb$post_iter) f() if (stop_condition) break } for (f in cb$finalize) f(finalize = TRUE) - bst <- xgb.Booster.complete(bst, saveraw = TRUE) - - # store the total number of boosting iterations - bst$niter <- end_iteration - # store the evaluation results - if (length(evaluation_log) > 0 && - nrow(evaluation_log) > 0) { + keep_evaluation_log <- FALSE + if (length(evaluation_log) > 0 && nrow(evaluation_log) > 0) { + keep_evaluation_log <- TRUE # include the previous compatible history when available if (inherits(xgb_model, 'xgb.Booster') && !is_update && - !is.null(xgb_model$evaluation_log) && + !is.null(past_evaluation_log) && isTRUE(all.equal(colnames(evaluation_log), - colnames(xgb_model$evaluation_log)))) { - evaluation_log <- rbindlist(list(xgb_model$evaluation_log, evaluation_log)) + colnames(past_evaluation_log)))) { + evaluation_log <- rbindlist(list(past_evaluation_log, evaluation_log)) } - bst$evaluation_log <- evaluation_log } - bst$call <- match.call() - bst$params <- params - bst$callbacks <- callbacks - if (!is.null(colnames(dtrain))) - bst$feature_names <- colnames(dtrain) - bst$nfeatures <- ncol(dtrain) + extra_attrs <- list( + call = match.call(), + params = params, + callbacks = callbacks + ) + if (keep_evaluation_log) { + extra_attrs$evaluation_log <- evaluation_log + } + curr_attrs <- attributes(bst) + attributes(bst) <- c(curr_attrs, extra_attrs) return(bst) } diff --git a/R-package/R/xgb.unserialize.R b/R-package/R/xgb.unserialize.R deleted file mode 100644 index 291d3e7da..000000000 --- a/R-package/R/xgb.unserialize.R +++ /dev/null @@ -1,41 +0,0 @@ -#' Load the instance back from \code{\link{xgb.serialize}} -#' -#' @param buffer the buffer containing booster instance saved by \code{\link{xgb.serialize}} -#' @param handle An \code{xgb.Booster.handle} object which will be overwritten with -#' the new deserialized object. Must be a null handle (e.g. when loading the model through -#' `readRDS`). If not provided, a new handle will be created. -#' @return An \code{xgb.Booster.handle} object. -#' -#' @export -xgb.unserialize <- function(buffer, handle = NULL) { - cachelist <- list() - if (is.null(handle)) { - handle <- .Call(XGBoosterCreate_R, cachelist) - } else { - if (!is.null.handle(handle)) - stop("'handle' is not null/empty. Cannot overwrite existing handle.") - .Call(XGBoosterCreateInEmptyObj_R, cachelist, handle) - } - tryCatch( - .Call(XGBoosterUnserializeFromBuffer_R, handle, buffer), - error = function(e) { - error_msg <- conditionMessage(e) - m <- regexec("(src[\\\\/]learner.cc:[0-9]+): Check failed: (header == serialisation_header_)", - error_msg, perl = TRUE) - groups <- regmatches(error_msg, m)[[1]] - if (length(groups) == 3) { - warning(paste("The model had been generated by XGBoost version 1.0.0 or earlier and was ", - "loaded from a RDS file. We strongly ADVISE AGAINST using saveRDS() ", - "function, to ensure that your model can be read in current and upcoming ", - "XGBoost releases. Please use xgb.save() instead to preserve models for the ", - "long term. For more details and explanation, see ", - "https://xgboost.readthedocs.io/en/latest/tutorials/saving_model.html", - sep = "")) - .Call(XGBoosterLoadModelFromRaw_R, handle, buffer) - } else { - stop(e) - } - }) - class(handle) <- "xgb.Booster.handle" - return(handle) -} diff --git a/R-package/R/xgboost.R b/R-package/R/xgboost.R index af6253a72..170aa5ffd 100644 --- a/R-package/R/xgboost.R +++ b/R-package/R/xgboost.R @@ -100,8 +100,10 @@ NULL #' @importFrom jsonlite toJSON #' @importFrom methods new #' @importFrom utils object.size str tail +#' @importFrom stats coef #' @importFrom stats predict #' @importFrom stats median +#' @importFrom stats variable.names #' @importFrom utils head #' @importFrom graphics barplot #' @importFrom graphics lines diff --git a/R-package/demo/00Index b/R-package/demo/00Index index 13ffdc6b6..fa09fa900 100644 --- a/R-package/demo/00Index +++ b/R-package/demo/00Index @@ -1,5 +1,4 @@ basic_walkthrough Basic feature walkthrough -caret_wrapper Use xgboost to train in caret library custom_objective Customize loss function, and evaluation metric boost_from_prediction Boosting from existing prediction predict_first_ntree Predicting using first n trees diff --git a/R-package/demo/README.md b/R-package/demo/README.md index 0a07a7426..99a492230 100644 --- a/R-package/demo/README.md +++ b/R-package/demo/README.md @@ -1,7 +1,6 @@ XGBoost R Feature Walkthrough ==== * [Basic walkthrough of wrappers](basic_walkthrough.R) -* [Train a xgboost model from caret library](caret_wrapper.R) * [Customize loss function, and evaluation metric](custom_objective.R) * [Boosting from existing prediction](boost_from_prediction.R) * [Predicting using first n trees](predict_first_ntree.R) diff --git a/R-package/demo/caret_wrapper.R b/R-package/demo/caret_wrapper.R deleted file mode 100644 index 0e63f27ce..000000000 --- a/R-package/demo/caret_wrapper.R +++ /dev/null @@ -1,44 +0,0 @@ -# install development version of caret library that contains xgboost models -require(caret) -require(xgboost) -require(data.table) -require(vcd) -require(e1071) - -# Load Arthritis dataset in memory. -data(Arthritis) -# Create a copy of the dataset with data.table package -# (data.table is 100% compliant with R dataframe but its syntax is a lot more consistent -# and its performance are really good). -df <- data.table(Arthritis, keep.rownames = FALSE) - -# Let's add some new categorical features to see if it helps. -# Of course these feature are highly correlated to the Age feature. -# Usually it's not a good thing in ML, but Tree algorithms (including boosted trees) are able to select the best features, -# even in case of highly correlated features. -# For the first feature we create groups of age by rounding the real age. -# Note that we transform it to factor (categorical data) so the algorithm treat them as independant values. -df[, AgeDiscret := as.factor(round(Age / 10, 0))] - -# Here is an even stronger simplification of the real age with an arbitrary split at 30 years old. -# I choose this value based on nothing. -# We will see later if simplifying the information based on arbitrary values is a good strategy -# (I am sure you already have an idea of how well it will work!). -df[, AgeCat := as.factor(ifelse(Age > 30, "Old", "Young"))] - -# We remove ID as there is nothing to learn from this feature (it will just add some noise as the dataset is small). -df[, ID := NULL] - -#-------------Basic Training using XGBoost in caret Library----------------- -# Set up control parameters for caret::train -# Here we use 10-fold cross-validation, repeating twice, and using random search for tuning hyper-parameters. -fitControl <- trainControl(method = "repeatedcv", number = 10, repeats = 2, search = "random") -# train a xgbTree model using caret::train -model <- train(factor(Improved) ~ ., data = df, method = "xgbTree", trControl = fitControl) - -# Instead of tree for our boosters, you can also fit a linear regression or logistic regression model -# using xgbLinear -# model <- train(factor(Improved)~., data = df, method = "xgbLinear", trControl = fitControl) - -# See model results -print(model) diff --git a/R-package/demo/predict_leaf_indices.R b/R-package/demo/predict_leaf_indices.R index 5ef9372ac..21b6fa71d 100644 --- a/R-package/demo/predict_leaf_indices.R +++ b/R-package/demo/predict_leaf_indices.R @@ -27,7 +27,7 @@ head(pred_with_leaf) create.new.tree.features <- function(model, original.features) { pred_with_leaf <- predict(model, original.features, predleaf = TRUE) cols <- list() - for (i in 1:model$niter) { + for (i in 1:xgb.get.num.boosted.rounds(model)) { # max is not the real max but it s not important for the purpose of adding features leaf.id <- sort(unique(pred_with_leaf[, i])) cols[[i]] <- factor(x = pred_with_leaf[, i], level = leaf.id) diff --git a/R-package/demo/runall.R b/R-package/demo/runall.R index 7a35e247b..ab1822a5b 100644 --- a/R-package/demo/runall.R +++ b/R-package/demo/runall.R @@ -9,6 +9,5 @@ demo(create_sparse_matrix, package = 'xgboost') demo(predict_leaf_indices, package = 'xgboost') demo(early_stopping, package = 'xgboost') demo(poisson_regression, package = 'xgboost') -demo(caret_wrapper, package = 'xgboost') demo(tweedie_regression, package = 'xgboost') #demo(gpu_accelerated, package = 'xgboost') # can only run when built with GPU support diff --git a/R-package/man/a-compatibility-note-for-saveRDS-save.Rd b/R-package/man/a-compatibility-note-for-saveRDS-save.Rd index a8f46547e..860f4f0c1 100644 --- a/R-package/man/a-compatibility-note-for-saveRDS-save.Rd +++ b/R-package/man/a-compatibility-note-for-saveRDS-save.Rd @@ -2,16 +2,44 @@ % Please edit documentation in R/utils.R \name{a-compatibility-note-for-saveRDS-save} \alias{a-compatibility-note-for-saveRDS-save} -\title{Do not use \code{\link[base]{saveRDS}} or \code{\link[base]{save}} for long-term archival of -models. Instead, use \code{\link{xgb.save}} or \code{\link{xgb.save.raw}}.} +\title{Model Serialization and Compatibility} \description{ -It is a common practice to use the built-in \code{\link[base]{saveRDS}} function (or -\code{\link[base]{save}}) to persist R objects to the disk. While it is possible to persist -\code{xgb.Booster} objects using \code{\link[base]{saveRDS}}, it is not advisable to do so if -the model is to be accessed in the future. If you train a model with the current version of -XGBoost and persist it with \code{\link[base]{saveRDS}}, the model is not guaranteed to be -accessible in later releases of XGBoost. To ensure that your model can be accessed in future -releases of XGBoost, use \code{\link{xgb.save}} or \code{\link{xgb.save.raw}} instead. +When it comes to serializing XGBoost models, it's possible to use R serializers such as +\link{save} or \link{saveRDS} to serialize an XGBoost R model, but XGBoost also provides +its own serializers with better compatibility guarantees, which allow loading +said models in other language bindings of XGBoost. + +Note that an \code{xgb.Booster} object, outside of its core components, might also keep:\itemize{ +\item Additional model configuration (accessible through \link{xgb.config}), +which includes model fitting parameters like \code{max_depth} and runtime parameters like \code{nthread}. +These are not necessarily useful for prediction/importance/plotting. +\item Additional R-specific attributes - e.g. results of callbacks, such as evaluation logs, +which are kept as a \code{data.table} object, accessible through \code{attributes(model)$evaluation_log} +if present. +} + +The first one (configurations) does not have the same compatibility guarantees as +the model itself, including attributes that are set and accessed through \link{xgb.attributes} - that is, such configuration +might be lost after loading the booster in a different XGBoost version, regardless of the +serializer that was used. These are saved when using \link{saveRDS}, but will be discarded +if loaded into an incompatible XGBoost version. They are not saved when using XGBoost's +serializers from its public interface including \link{xgb.save} and \link{xgb.save.raw}. + +The second ones (R attributes) are not part of the standard XGBoost model structure, and thus are +not saved when using XGBoost's own serializers. These attributes are only used for informational +purposes, such as keeping track of evaluation metrics as the model was fit, or saving the R +call that produced the model, but are otherwise not used for prediction / importance / plotting / etc. +These R attributes are only preserved when using R's serializers. + +Note that XGBoost models in R starting from version \verb{2.1.0} and onwards, and XGBoost models +before version \verb{2.1.0}; have a very different R object structure and are incompatible with +each other. Hence, models that were saved with R serializers live \code{saveRDS} or \code{save} before +version \verb{2.1.0} will not work with latter \code{xgboost} versions and vice versa. Be aware that +the structure of R model objects could in theory change again in the future, so XGBoost's serializers +should be preferred for long-term storage. + +Furthermore, note that using the package \code{qs} for serialization will require version 0.26 or +higher of said package, and will have the same compatibility restrictions as R serializers. } \details{ Use \code{\link{xgb.save}} to save the XGBoost model as a stand-alone file. You may opt into @@ -24,9 +52,10 @@ re-construct the corresponding model. To read the model back, use \code{\link{xg The \code{\link{xgb.save.raw}} function is useful if you'd like to persist the XGBoost model as part of another R object. -Note: Do not use \code{\link{xgb.serialize}} to store models long-term. It persists not only the -model but also internal configurations and parameters, and its format is not stable across -multiple XGBoost versions. Use \code{\link{xgb.serialize}} only for checkpointing. +Use \link{saveRDS} if you require the R-specific attributes that a booster might have, such +as evaluation logs, but note that future compatibility of such objects is outside XGBoost's +control as it relies on R's serialization format (see e.g. the details section in +\link{serialize} and \link{save} from base R). For more details and explanation about model persistence and archival, consult the page \url{https://xgboost.readthedocs.io/en/latest/tutorials/saving_model.html}. diff --git a/R-package/man/cb.save.model.Rd b/R-package/man/cb.save.model.Rd index 584fd69b7..7701ad990 100644 --- a/R-package/man/cb.save.model.Rd +++ b/R-package/man/cb.save.model.Rd @@ -4,17 +4,22 @@ \alias{cb.save.model} \title{Callback closure for saving a model file.} \usage{ -cb.save.model(save_period = 0, save_name = "xgboost.model") +cb.save.model(save_period = 0, save_name = "xgboost.ubj") } \arguments{ \item{save_period}{save the model to disk after every \code{save_period} iterations; 0 means save the model at the end.} \item{save_name}{the name or path for the saved model file. -It can contain a \code{\link[base]{sprintf}} formatting specifier -to include the integer iteration number in the file name. -E.g., with \code{save_name} = 'xgboost_\%04d.model', -the file saved at iteration 50 would be named "xgboost_0050.model".} + +\if{html}{\out{
}}\preformatted{ Note that the format of the model being saved is determined by the file + extension specified here (see \link{xgb.save} for details about how it works). + + It can contain a \code{\link[base]{sprintf}} formatting specifier + to include the integer iteration number in the file name. + E.g., with \code{save_name} = 'xgboost_\%04d.ubj', + the file saved at iteration 50 would be named "xgboost_0050.ubj". +}\if{html}{\out{
}}} } \description{ Callback closure for saving a model file. @@ -29,5 +34,7 @@ Callback function expects the following values to be set in its calling frame: \code{end_iteration}. } \seealso{ +\link{xgb.save} + \code{\link{callbacks}} } diff --git a/R-package/man/coef.xgb.Booster.Rd b/R-package/man/coef.xgb.Booster.Rd new file mode 100644 index 000000000..7318077bb --- /dev/null +++ b/R-package/man/coef.xgb.Booster.Rd @@ -0,0 +1,50 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.Booster.R +\name{coef.xgb.Booster} +\alias{coef.xgb.Booster} +\title{Extract coefficients from linear booster} +\usage{ +\method{coef}{xgb.Booster}(object, ...) +} +\arguments{ +\item{object}{A fitted booster of 'gblinear' type.} + +\item{...}{Not used.} +} +\value{ +The extracted coefficients:\itemize{ +\item If there's only one coefficient per column in the data, will be returned as a +vector, potentially containing the feature names if available, with the intercept +as first column. +\item If there's more than one coefficient per column in the data (e.g. when using +\code{objective="multi:softmax"}), will be returned as a matrix with dimensions equal +to \verb{[num_features, num_cols]}, with the intercepts as first row. Note that the column +(classes in multi-class classification) dimension will not be named. +} + +The intercept returned here will include the 'base_score' parameter (unlike the 'bias' +or the last coefficient in the model dump, which doesn't have 'base_score' added to it), +hence one should get the same values from calling \code{predict(..., outputmargin = TRUE)} and +from performing a matrix multiplication with \code{model.matrix(~., ...)}. + +Be aware that the coefficients are obtained by first converting them to strings and +back, so there will always be some very small lose of precision compared to the actual +coefficients as used by \link{predict.xgb.Booster}. +} +\description{ +Extracts the coefficients from a 'gblinear' booster object, +as produced by \code{xgb.train} when using parameter \code{booster="gblinear"}. + +Note: this function will error out if passing a booster model +which is not of "gblinear" type. +} +\examples{ +library(xgboost) +data(mtcars) +y <- mtcars[, 1] +x <- as.matrix(mtcars[, -1]) +dm <- xgb.DMatrix(data = x, label = y, nthread = 1) +params <- list(booster = "gblinear", nthread = 1) +model <- xgb.train(data = dm, params = params, nrounds = 2) +coef(model) +} diff --git a/R-package/man/getinfo.Rd b/R-package/man/getinfo.Rd index cb552886b..7cc0d6ecb 100644 --- a/R-package/man/getinfo.Rd +++ b/R-package/man/getinfo.Rd @@ -1,24 +1,42 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xgb.DMatrix.R -\name{getinfo} +% Please edit documentation in R/xgb.Booster.R, R/xgb.DMatrix.R +\name{getinfo.xgb.Booster} +\alias{getinfo.xgb.Booster} +\alias{setinfo.xgb.Booster} \alias{getinfo} \alias{getinfo.xgb.DMatrix} -\title{Get information of an xgb.DMatrix object} +\alias{setinfo} +\alias{setinfo.xgb.DMatrix} +\title{Get or set information of xgb.DMatrix and xgb.Booster objects} \usage{ +\method{getinfo}{xgb.Booster}(object, name) + +\method{setinfo}{xgb.Booster}(object, name, info) + getinfo(object, name) \method{getinfo}{xgb.DMatrix}(object, name) + +setinfo(object, name, info) + +\method{setinfo}{xgb.DMatrix}(object, name, info) } \arguments{ -\item{object}{Object of class \code{xgb.DMatrix}} +\item{object}{Object of class \code{xgb.DMatrix} of \code{xgb.Booster}.} \item{name}{the name of the information field to get (see details)} + +\item{info}{the specific field of information to set} +} +\value{ +For \code{getinfo}, will return the requested field. For \code{setinfo}, will always return value \code{TRUE} +if it succeeds. } \description{ -Get information of an xgb.DMatrix object +Get or set information of xgb.DMatrix and xgb.Booster objects } \details{ -The \code{name} field can be one of the following: +The \code{name} field can be one of the following for \code{xgb.DMatrix}: \itemize{ \item \code{label} @@ -33,8 +51,28 @@ The \code{name} field can be one of the following: } See the documentation for \link{xgb.DMatrix} for more information about these fields. +For \code{xgb.Booster}, can be one of the following: +\itemize{ +\item \code{feature_type} +\item \code{feature_name} +} + Note that, while 'qid' cannot be retrieved, it's possible to get the equivalent 'group' for a DMatrix that had 'qid' assigned. + +\bold{Important}: when calling \code{setinfo}, the objects are modified in-place. See +\link{xgb.copy.Booster} for an idea of this in-place assignment works. + +See the documentation for \link{xgb.DMatrix} for possible fields that can be set +(which correspond to arguments in that function). + +Note that the following fields are allowed in the construction of an \code{xgb.DMatrix} +but \bold{aren't} allowed here:\itemize{ +\item data +\item missing +\item silent +\item nthread +} } \examples{ data(agaricus.train, package='xgboost') @@ -45,4 +83,11 @@ setinfo(dtrain, 'label', 1-labels) labels2 <- getinfo(dtrain, 'label') stopifnot(all(labels2 == 1-labels)) +data(agaricus.train, package='xgboost') +dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) + +labels <- getinfo(dtrain, 'label') +setinfo(dtrain, 'label', 1-labels) +labels2 <- getinfo(dtrain, 'label') +stopifnot(all.equal(labels2, 1-labels)) } diff --git a/R-package/man/predict.xgb.Booster.Rd b/R-package/man/predict.xgb.Booster.Rd index f47cab321..66194c64f 100644 --- a/R-package/man/predict.xgb.Booster.Rd +++ b/R-package/man/predict.xgb.Booster.Rd @@ -2,7 +2,6 @@ % Please edit documentation in R/xgb.Booster.R \name{predict.xgb.Booster} \alias{predict.xgb.Booster} -\alias{predict.xgb.Booster.handle} \title{Predict method for XGBoost model} \usage{ \method{predict}{xgb.Booster}( @@ -21,11 +20,9 @@ strict_shape = FALSE, ... ) - -\method{predict}{xgb.Booster.handle}(object, ...) } \arguments{ -\item{object}{Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}.} +\item{object}{Object of class \code{xgb.Booster}.} \item{newdata}{Takes \code{matrix}, \code{dgCMatrix}, \code{dgRMatrix}, \code{dsparseVector}, local data file, or \code{xgb.DMatrix}. diff --git a/R-package/man/print.xgb.Booster.Rd b/R-package/man/print.xgb.Booster.Rd index 4d09bb5ec..9a783efaf 100644 --- a/R-package/man/print.xgb.Booster.Rd +++ b/R-package/man/print.xgb.Booster.Rd @@ -4,14 +4,15 @@ \alias{print.xgb.Booster} \title{Print xgb.Booster} \usage{ -\method{print}{xgb.Booster}(x, verbose = FALSE, ...) +\method{print}{xgb.Booster}(x, ...) } \arguments{ \item{x}{An \code{xgb.Booster} object.} -\item{verbose}{Whether to print detailed data (e.g., attribute values).} - -\item{...}{Not currently used.} +\item{...}{Not used.} +} +\value{ +The same \code{x} object, returned invisibly } \description{ Print information about \code{xgb.Booster}. @@ -33,6 +34,5 @@ bst <- xgboost( attr(bst, "myattr") <- "memo" print(bst) -print(bst, verbose = TRUE) } diff --git a/R-package/man/setinfo.Rd b/R-package/man/setinfo.Rd deleted file mode 100644 index 549fc9b20..000000000 --- a/R-package/man/setinfo.Rd +++ /dev/null @@ -1,42 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xgb.DMatrix.R -\name{setinfo} -\alias{setinfo} -\alias{setinfo.xgb.DMatrix} -\title{Set information of an xgb.DMatrix object} -\usage{ -setinfo(object, name, info) - -\method{setinfo}{xgb.DMatrix}(object, name, info) -} -\arguments{ -\item{object}{Object of class "xgb.DMatrix"} - -\item{name}{the name of the field to get} - -\item{info}{the specific field of information to set} -} -\description{ -Set information of an xgb.DMatrix object -} -\details{ -See the documentation for \link{xgb.DMatrix} for possible fields that can be set -(which correspond to arguments in that function). - -Note that the following fields are allowed in the construction of an \code{xgb.DMatrix} -but \bold{aren't} allowed here:\itemize{ -\item data -\item missing -\item silent -\item nthread -} -} -\examples{ -data(agaricus.train, package='xgboost') -dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) - -labels <- getinfo(dtrain, 'label') -setinfo(dtrain, 'label', 1-labels) -labels2 <- getinfo(dtrain, 'label') -stopifnot(all.equal(labels2, 1-labels)) -} diff --git a/R-package/man/variable.names.xgb.Booster.Rd b/R-package/man/variable.names.xgb.Booster.Rd new file mode 100644 index 000000000..aec09751d --- /dev/null +++ b/R-package/man/variable.names.xgb.Booster.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.Booster.R +\name{variable.names.xgb.Booster} +\alias{variable.names.xgb.Booster} +\title{Get Features Names from Booster} +\usage{ +\method{variable.names}{xgb.Booster}(object, ...) +} +\arguments{ +\item{object}{An \code{xgb.Booster} object.} + +\item{...}{Not used.} +} +\description{ +Returns the feature / variable / column names from a fitted +booster object, which are set automatically during the call to \link{xgb.train} +from the DMatrix names, or which can be set manually through \link{setinfo}. + +If the object doesn't have feature names, will return \code{NULL}. + +It is equivalent to calling \code{getinfo(object, "feature_name")}. +} diff --git a/R-package/man/xgb.Booster.complete.Rd b/R-package/man/xgb.Booster.complete.Rd deleted file mode 100644 index 102224a8f..000000000 --- a/R-package/man/xgb.Booster.complete.Rd +++ /dev/null @@ -1,61 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xgb.Booster.R -\name{xgb.Booster.complete} -\alias{xgb.Booster.complete} -\title{Restore missing parts of an incomplete xgb.Booster object} -\usage{ -xgb.Booster.complete(object, saveraw = TRUE) -} -\arguments{ -\item{object}{Object of class \code{xgb.Booster}.} - -\item{saveraw}{A flag indicating whether to append \code{raw} Booster memory dump data -when it doesn't already exist.} -} -\value{ -An object of \code{xgb.Booster} class. -} -\description{ -It attempts to complete an \code{xgb.Booster} object by restoring either its missing -raw model memory dump (when it has no \code{raw} data but its \code{xgb.Booster.handle} is valid) -or its missing internal handle (when its \code{xgb.Booster.handle} is not valid -but it has a raw Booster memory dump). -} -\details{ -While this method is primarily for internal use, it might be useful in some practical situations. - -E.g., when an \code{xgb.Booster} model is saved as an R object and then is loaded as an R object, -its handle (pointer) to an internal xgboost model would be invalid. The majority of xgboost methods -should still work for such a model object since those methods would be using -\code{xgb.Booster.complete()} internally. However, one might find it to be more efficient to call the -\code{xgb.Booster.complete()} function explicitly once after loading a model as an R-object. -That would prevent further repeated implicit reconstruction of an internal booster model. -} -\examples{ - -data(agaricus.train, package = "xgboost") - -bst <- xgboost( - data = agaricus.train$data, - label = agaricus.train$label, - max_depth = 2, - eta = 1, - nthread = 2, - nrounds = 2, - objective = "binary:logistic" -) - -fname <- file.path(tempdir(), "xgb_model.Rds") -saveRDS(bst, fname) - -# Warning: The resulting RDS file is only compatible with the current XGBoost version. -# Refer to the section titled "a-compatibility-note-for-saveRDS-save". -bst1 <- readRDS(fname) -# the handle is invalid: -print(bst1$handle) - -bst1 <- xgb.Booster.complete(bst1) -# now the handle points to a valid internal booster model: -print(bst1$handle) - -} diff --git a/R-package/man/xgb.attr.Rd b/R-package/man/xgb.attr.Rd index 2aab62812..8038a2048 100644 --- a/R-package/man/xgb.attr.Rd +++ b/R-package/man/xgb.attr.Rd @@ -16,7 +16,7 @@ xgb.attributes(object) xgb.attributes(object) <- value } \arguments{ -\item{object}{Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}.} +\item{object}{Object of class \code{xgb.Booster}. \bold{Will be modified in-place} when assigning to it.} \item{name}{A non-empty character string specifying which attribute is to be accessed.} @@ -51,15 +51,14 @@ Also, setting an attribute that has the same name as one of xgboost's parameters change the value of that parameter for a model. Use \code{\link[=xgb.parameters<-]{xgb.parameters<-()}} to set or change model parameters. -The attribute setters would usually work more efficiently for \code{xgb.Booster.handle} -than for \code{xgb.Booster}, since only just a handle (pointer) would need to be copied. -That would only matter if attributes need to be set many times. -Note, however, that when feeding a handle of an \code{xgb.Booster} object to the attribute setters, -the raw model cache of an \code{xgb.Booster} object would not be automatically updated, -and it would be the user's responsibility to call \code{\link[=xgb.serialize]{xgb.serialize()}} to update it. - The \verb{xgb.attributes<-} setter either updates the existing or adds one or several attributes, but it doesn't delete the other existing attributes. + +Important: since this modifies the booster's C object, semantics for assignment here +will differ from R's, as any object reference to the same booster will be modified +too, while assignment of R attributes through \verb{attributes(model)$ <- } +will follow the usual copy-on-write R semantics (see \link{xgb.copy.Booster} for an +example of these behaviors). } \examples{ data(agaricus.train, package = "xgboost") diff --git a/R-package/man/xgb.config.Rd b/R-package/man/xgb.config.Rd index 83040b877..1ab810644 100644 --- a/R-package/man/xgb.config.Rd +++ b/R-package/man/xgb.config.Rd @@ -10,13 +10,23 @@ xgb.config(object) xgb.config(object) <- value } \arguments{ -\item{object}{Object of class \code{xgb.Booster}.} +\item{object}{Object of class \code{xgb.Booster}. \bold{Will be modified in-place} when assigning to it.} -\item{value}{A JSON string.} +\item{value}{An R list.} +} +\value{ +\code{xgb.config} will return the parameters as an R list. } \description{ Accessors for model parameters as JSON string } +\details{ +Note that assignment is performed in-place on the booster C object, which unlike assignment +of R attributes, doesn't follow typical copy-on-write semantics for assignment - i.e. all references +to the same booster will also get updated. + +See \link{xgb.copy.Booster} for an example of this behavior. +} \examples{ data(agaricus.train, package = "xgboost") diff --git a/R-package/man/xgb.copy.Booster.Rd b/R-package/man/xgb.copy.Booster.Rd new file mode 100644 index 000000000..8426d039e --- /dev/null +++ b/R-package/man/xgb.copy.Booster.Rd @@ -0,0 +1,53 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.Booster.R +\name{xgb.copy.Booster} +\alias{xgb.copy.Booster} +\title{Deep-copies a Booster Object} +\usage{ +xgb.copy.Booster(model) +} +\arguments{ +\item{model}{An 'xgb.Booster' object.} +} +\value{ +A deep copy of \code{model} - it will be identical in every way, but C-level +functions called on that copy will not affect the \code{model} variable. +} +\description{ +Creates a deep copy of an 'xgb.Booster' object, such that the +C object pointer contained will be a different object, and hence functions +like \link{xgb.attr} will not affect the object from which it was copied. +} +\examples{ +library(xgboost) +data(mtcars) +y <- mtcars$mpg +x <- mtcars[, -1] +dm <- xgb.DMatrix(x, label = y, nthread = 1) +model <- xgb.train( + data = dm, + params = list(nthread = 1), + nround = 3 +) + +# Set an arbitrary attribute kept at the C level +xgb.attr(model, "my_attr") <- 100 +print(xgb.attr(model, "my_attr")) + +# Just assigning to a new variable will not create +# a deep copy - C object pointer is shared, and in-place +# modifications will affect both objects +model_shallow_copy <- model +xgb.attr(model_shallow_copy, "my_attr") <- 333 +# 'model' was also affected by this change: +print(xgb.attr(model, "my_attr")) + +model_deep_copy <- xgb.copy.Booster(model) +xgb.attr(model_deep_copy, "my_attr") <- 444 +# 'model' was NOT affected by this change +# (keeps previous value that was assigned before) +print(xgb.attr(model, "my_attr")) + +# Verify that the new object was actually modified +print(xgb.attr(model_deep_copy, "my_attr")) +} diff --git a/R-package/man/xgb.gblinear.history.Rd b/R-package/man/xgb.gblinear.history.Rd index bc8d46747..103be16f1 100644 --- a/R-package/man/xgb.gblinear.history.Rd +++ b/R-package/man/xgb.gblinear.history.Rd @@ -8,7 +8,8 @@ xgb.gblinear.history(model, class_index = NULL) } \arguments{ \item{model}{either an \code{xgb.Booster} or a result of \code{xgb.cv()}, trained -using the \code{cb.gblinear.history()} callback.} +using the \code{cb.gblinear.history()} callback, but \bold{not} a booster +loaded from \link{xgb.load} or \link{xgb.load.raw}.} \item{class_index}{zero-based class index to extract the coefficients for only that specific class in a multinomial multiclass model. When it is NULL, all the @@ -27,3 +28,11 @@ A helper function to extract the matrix of linear coefficients' history from a gblinear model created while using the \code{cb.gblinear.history()} callback. } +\details{ +Note that this is an R-specific function that relies on R attributes that +are not saved when using xgboost's own serialization functions like \link{xgb.load} +or \link{xgb.load.raw}. + +In order for a serialized model to be accepted by tgis function, one must use R +serializers such as \link{saveRDS}. +} diff --git a/R-package/man/xgb.get.num.boosted.rounds.Rd b/R-package/man/xgb.get.num.boosted.rounds.Rd new file mode 100644 index 000000000..74c94d95b --- /dev/null +++ b/R-package/man/xgb.get.num.boosted.rounds.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.Booster.R +\name{xgb.get.num.boosted.rounds} +\alias{xgb.get.num.boosted.rounds} +\title{Get number of boosting in a fitted booster} +\usage{ +xgb.get.num.boosted.rounds(model) +} +\arguments{ +\item{model}{A fitted \code{xgb.Booster} model.} +} +\value{ +The number of rounds saved in the model, as an integer. +} +\description{ +Get number of boosting in a fitted booster +} +\details{ +Note that setting booster parameters related to training +continuation / updates through \link{xgb.parameters<-} will reset the +number of rounds to zero. +} diff --git a/R-package/man/xgb.is.same.Booster.Rd b/R-package/man/xgb.is.same.Booster.Rd new file mode 100644 index 000000000..d2a2f4d17 --- /dev/null +++ b/R-package/man/xgb.is.same.Booster.Rd @@ -0,0 +1,59 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.Booster.R +\name{xgb.is.same.Booster} +\alias{xgb.is.same.Booster} +\title{Check if two boosters share the same C object} +\usage{ +xgb.is.same.Booster(obj1, obj2) +} +\arguments{ +\item{obj1}{Booster model to compare with \code{obj2}.} + +\item{obj2}{Booster model to compare with \code{obj1}.} +} +\value{ +Either \code{TRUE} or \code{FALSE} according to whether the two boosters share +the underlying C object. +} +\description{ +Checks whether two booster objects refer to the same underlying C object. +} +\details{ +As booster objects (as returned by e.g. \link{xgb.train}) contain an R 'externalptr' +object, they don't follow typical copy-on-write semantics of other R objects - that is, if +one assigns a booster to a different variable and modifies that new variable through in-place +methods like \link{xgb.attr<-}, the modification will be applied to both the old and the new +variable, unlike typical R assignments which would only modify the latter. + +This function allows checking whether two booster objects share the same 'externalptr', +regardless of the R attributes that they might have. + +In order to duplicate a booster in such a way that the copy wouldn't share the same +'externalptr', one can use function \link{xgb.copy.Booster}. +} +\examples{ +library(xgboost) +data(mtcars) +y <- mtcars$mpg +x <- as.matrix(mtcars[, -1]) +model <- xgb.train( + params = list(nthread = 1), + data = xgb.DMatrix(x, label = y, nthread = 1), + nround = 3 +) + +model_shallow_copy <- model +xgb.is.same.Booster(model, model_shallow_copy) # same C object + +model_deep_copy <- xgb.copy.Booster(model) +xgb.is.same.Booster(model, model_deep_copy) # different C objects + +# In-place assignments modify all references, +# but not full/deep copies of the booster +xgb.attr(model_shallow_copy, "my_attr") <- 111 +xgb.attr(model, "my_attr") # gets modified +xgb.attr(model_deep_copy, "my_attr") # doesn't get modified +} +\seealso{ +\link{xgb.copy.Booster} +} diff --git a/R-package/man/xgb.load.Rd b/R-package/man/xgb.load.Rd index e6a2d6cdd..1a6873171 100644 --- a/R-package/man/xgb.load.Rd +++ b/R-package/man/xgb.load.Rd @@ -48,5 +48,5 @@ xgb.save(bst, fname) bst <- xgb.load(fname) } \seealso{ -\code{\link{xgb.save}}, \code{\link{xgb.Booster.complete}}. +\code{\link{xgb.save}} } diff --git a/R-package/man/xgb.load.raw.Rd b/R-package/man/xgb.load.raw.Rd index 0af890e69..f0248cd9e 100644 --- a/R-package/man/xgb.load.raw.Rd +++ b/R-package/man/xgb.load.raw.Rd @@ -4,12 +4,10 @@ \alias{xgb.load.raw} \title{Load serialised xgboost model from R's raw vector} \usage{ -xgb.load.raw(buffer, as_booster = FALSE) +xgb.load.raw(buffer) } \arguments{ \item{buffer}{the buffer returned by xgb.save.raw} - -\item{as_booster}{Return the loaded model as xgb.Booster instead of xgb.Booster.handle.} } \description{ User can generate raw memory buffer by calling xgb.save.raw diff --git a/R-package/man/xgb.model.dt.tree.Rd b/R-package/man/xgb.model.dt.tree.Rd index 330998ab8..e63bd4b10 100644 --- a/R-package/man/xgb.model.dt.tree.Rd +++ b/R-package/man/xgb.model.dt.tree.Rd @@ -14,8 +14,11 @@ xgb.model.dt.tree( ) } \arguments{ -\item{feature_names}{Character vector used to overwrite the feature names -of the model. The default (\code{NULL}) uses the original feature names.} +\item{feature_names}{Character vector of feature names. If the model already +contains feature names, those will be used when \code{feature_names=NULL} (default value). + +\if{html}{\out{
}}\preformatted{ Note that, if the model already contains feature names, it's \\bold\{not\} possible to override them here. +}\if{html}{\out{
}}} \item{model}{Object of class \code{xgb.Booster}.} @@ -76,8 +79,6 @@ bst <- xgboost( objective = "binary:logistic" ) -(dt <- xgb.model.dt.tree(colnames(agaricus.train$data), bst)) - # This bst model already has feature_names stored with it, so those would be used when # feature_names is not set: (dt <- xgb.model.dt.tree(model = bst)) diff --git a/R-package/man/xgb.parameters.Rd b/R-package/man/xgb.parameters.Rd index 5305afa51..8d5044cab 100644 --- a/R-package/man/xgb.parameters.Rd +++ b/R-package/man/xgb.parameters.Rd @@ -7,17 +7,27 @@ xgb.parameters(object) <- value } \arguments{ -\item{object}{Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}.} +\item{object}{Object of class \code{xgb.Booster}. \bold{Will be modified in-place}.} \item{value}{A list (or an object coercible to a list) with the names of parameters to set and the elements corresponding to parameter values.} } +\value{ +The same booster \code{object}, which gets modified in-place. +} \description{ Only the setter for xgboost parameters is currently implemented. } \details{ -Note that the setter would usually work more efficiently for \code{xgb.Booster.handle} -than for \code{xgb.Booster}, since only just a handle would need to be copied. +Just like \link{xgb.attr}, this function will make in-place modifications +on the booster object which do not follow typical R assignment semantics - that is, +all references to the same booster will also be updated, unlike assingment of R +attributes which follow copy-on-write semantics. + +See \link{xgb.copy.Booster} for an example of this behavior. + +Be aware that setting parameters of a fitted booster related to training continuation / updates +will reset its number of rounds indicator to zero. } \examples{ data(agaricus.train, package = "xgboost") diff --git a/R-package/man/xgb.save.Rd b/R-package/man/xgb.save.Rd index ee4b799c5..0db80a120 100644 --- a/R-package/man/xgb.save.Rd +++ b/R-package/man/xgb.save.Rd @@ -7,15 +7,27 @@ xgb.save(model, fname) } \arguments{ -\item{model}{model object of \code{xgb.Booster} class.} +\item{model}{Model object of \code{xgb.Booster} class.} -\item{fname}{name of the file to write.} +\item{fname}{Name of the file to write. + +Note that the extension of this file name determined the serialization format to use:\itemize{ +\item Extension ".ubj" will use the universal binary JSON format (recommended). +This format uses binary types for e.g. floating point numbers, thereby preventing any loss +of precision when converting to a human-readable JSON text or similar. +\item Extension ".json" will use plain JSON, which is a human-readable format. +\item Extension ".deprecated" will use a \bold{deprecated} binary format. This format will +not be able to save attributes introduced after v1 of XGBoost, such as the "best_iteration" +attribute that boosters might keep, nor feature names or user-specifiec attributes. +\item If the format is not specified by passing one of the file extensions above, will +default to UBJ. +}} } \description{ -Save xgboost model to a file in binary format. +Save xgboost model to a file in binary or JSON format. } \details{ -This methods allows to save a model in an xgboost-internal binary format which is universal +This methods allows to save a model in an xgboost-internal binary or text format which is universal among the various xgboost interfaces. In R, the saved model file could be read-in later using either the \code{\link{xgb.load}} function or the \code{xgb_model} parameter of \code{\link{xgb.train}}. @@ -23,7 +35,7 @@ of \code{\link{xgb.train}}. Note: a model can also be saved as an R-object (e.g., by using \code{\link[base]{readRDS}} or \code{\link[base]{save}}). However, it would then only be compatible with R, and corresponding R-methods would need to be used to load it. Moreover, persisting the model with -\code{\link[base]{readRDS}} or \code{\link[base]{save}}) will cause compatibility problems in +\code{\link[base]{readRDS}} or \code{\link[base]{save}}) might cause compatibility problems in future versions of XGBoost. Consult \code{\link{a-compatibility-note-for-saveRDS-save}} to learn how to persist models in a future-proof way, i.e. to make the model accessible in future releases of XGBoost. @@ -51,5 +63,5 @@ xgb.save(bst, fname) bst <- xgb.load(fname) } \seealso{ -\code{\link{xgb.load}}, \code{\link{xgb.Booster.complete}}. +\code{\link{xgb.load}} } diff --git a/R-package/man/xgb.save.raw.Rd b/R-package/man/xgb.save.raw.Rd index 498272148..15400bb14 100644 --- a/R-package/man/xgb.save.raw.Rd +++ b/R-package/man/xgb.save.raw.Rd @@ -5,7 +5,7 @@ \title{Save xgboost model to R's raw vector, user can call xgb.load.raw to load the model back from raw vector} \usage{ -xgb.save.raw(model, raw_format = "deprecated") +xgb.save.raw(model, raw_format = "ubj") } \arguments{ \item{model}{the model object.} @@ -15,9 +15,7 @@ xgb.save.raw(model, raw_format = "deprecated") \item \code{json}: Encode the booster into JSON text document. \item \code{ubj}: Encode the booster into Universal Binary JSON. \item \code{deprecated}: Encode the booster into old customized binary format. -} - -Right now the default is \code{deprecated} but will be changed to \code{ubj} in upcoming release.} +}} } \description{ Save xgboost model from xgboost or xgb.train diff --git a/R-package/man/xgb.serialize.Rd b/R-package/man/xgb.serialize.Rd deleted file mode 100644 index 5bf4205f8..000000000 --- a/R-package/man/xgb.serialize.Rd +++ /dev/null @@ -1,29 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xgb.serialize.R -\name{xgb.serialize} -\alias{xgb.serialize} -\title{Serialize the booster instance into R's raw vector. The serialization method differs -from \code{\link{xgb.save.raw}} as the latter one saves only the model but not -parameters. This serialization format is not stable across different xgboost versions.} -\usage{ -xgb.serialize(booster) -} -\arguments{ -\item{booster}{the booster instance} -} -\description{ -Serialize the booster instance into R's raw vector. The serialization method differs -from \code{\link{xgb.save.raw}} as the latter one saves only the model but not -parameters. This serialization format is not stable across different xgboost versions. -} -\examples{ -data(agaricus.train, package='xgboost') -data(agaricus.test, package='xgboost') -train <- agaricus.train -test <- agaricus.test -bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, - eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") -raw <- xgb.serialize(bst) -bst <- xgb.unserialize(raw) - -} diff --git a/R-package/man/xgb.train.Rd b/R-package/man/xgb.train.Rd index b2eaff27c..0421b9c4a 100644 --- a/R-package/man/xgb.train.Rd +++ b/R-package/man/xgb.train.Rd @@ -205,7 +205,12 @@ file with a previously saved model.} \item{callbacks}{a list of callback functions to perform various task during boosting. See \code{\link{callbacks}}. Some of the callbacks are automatically created depending on the parameters' values. User can provide either existing or their own callback methods in order -to customize the training process.} +to customize the training process. + +\if{html}{\out{
}}\preformatted{ Note that some callbacks might try to set an evaluation log - be aware that these evaluation logs + are kept as R attributes, and thus do not get saved when using non-R serializaters like + \link{xgb.save} (but are kept when using R serializers like \link{saveRDS}). +}\if{html}{\out{
}}} \item{...}{other parameters to pass to \code{params}.} @@ -219,27 +224,7 @@ This parameter is only used when input is a dense matrix.} \item{weight}{a vector indicating the weight for each row of the input.} } \value{ -An object of class \code{xgb.Booster} with the following elements: -\itemize{ -\item \code{handle} a handle (pointer) to the xgboost model in memory. -\item \code{raw} a cached memory dump of the xgboost model saved as R's \code{raw} type. -\item \code{niter} number of boosting iterations. -\item \code{evaluation_log} evaluation history stored as a \code{data.table} with the -first column corresponding to iteration number and the rest corresponding to evaluation -metrics' values. It is created by the \code{\link{cb.evaluation.log}} callback. -\item \code{call} a function call. -\item \code{params} parameters that were passed to the xgboost library. Note that it does not -capture parameters changed by the \code{\link{cb.reset.parameters}} callback. -\item \code{callbacks} callback functions that were either automatically assigned or -explicitly passed. -\item \code{best_iteration} iteration number with the best evaluation metric value -(only available with early stopping). -\item \code{best_score} the best evaluation metric value during early stopping. -(only available with early stopping). -\item \code{feature_names} names of the training dataset features -(only when column names were defined in training data). -\item \code{nfeatures} number of features in training data. -} +An object of class \code{xgb.Booster}. } \description{ \code{xgb.train} is an advanced interface for training an xgboost model. @@ -285,6 +270,21 @@ and the \code{print_every_n} parameter is passed to it. \item \code{cb.early.stop}: when \code{early_stopping_rounds} is set. \item \code{cb.save.model}: when \code{save_period > 0} is set. } + +Note that objects of type \code{xgb.Booster} as returned by this function behave a bit differently +from typical R objects (it's an 'altrep' list class), and it makes a separation between +internal booster attributes (restricted to jsonifyable data), accessed through \link{xgb.attr} +and shared between interfaces through serialization functions like \link{xgb.save}; and +R-specific attributes, accessed through \link{attributes} and \link{attr}, which are otherwise +only used in the R interface, only kept when using R's serializers like \link{saveRDS}, and +not anyhow used by functions like \link{predict.xgb.Booster}. + +Be aware that one such R attribute that is automatically added is \code{params} - this attribute +is assigned from the \code{params} argument to this function, and is only meant to serve as a +reference for what went into the booster, but is not used in other methods that take a booster +object - so for example, changing the booster's configuration requires calling \verb{xgb.config<-} +or 'xgb.parameters<-', while simply modifying \verb{attributes(model)$params$<...>} will have no +effect elsewhere. } \examples{ data(agaricus.train, package='xgboost') diff --git a/R-package/man/xgb.unserialize.Rd b/R-package/man/xgb.unserialize.Rd deleted file mode 100644 index f83ee635d..000000000 --- a/R-package/man/xgb.unserialize.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xgb.unserialize.R -\name{xgb.unserialize} -\alias{xgb.unserialize} -\title{Load the instance back from \code{\link{xgb.serialize}}} -\usage{ -xgb.unserialize(buffer, handle = NULL) -} -\arguments{ -\item{buffer}{the buffer containing booster instance saved by \code{\link{xgb.serialize}}} - -\item{handle}{An \code{xgb.Booster.handle} object which will be overwritten with -the new deserialized object. Must be a null handle (e.g. when loading the model through -\code{readRDS}). If not provided, a new handle will be created.} -} -\value{ -An \code{xgb.Booster.handle} object. -} -\description{ -Load the instance back from \code{\link{xgb.serialize}} -} diff --git a/R-package/src/init.c b/R-package/src/init.c index 5eee8ebe6..81c28c401 100644 --- a/R-package/src/init.c +++ b/R-package/src/init.c @@ -15,9 +15,16 @@ Check these declarations against the C/Fortran source code. */ /* .Call calls */ +extern void XGBInitializeAltrepClass_R(DllInfo *info); +extern SEXP XGDuplicate_R(SEXP); +extern SEXP XGPointerEqComparison_R(SEXP, SEXP); extern SEXP XGBoosterTrainOneIter_R(SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP XGBoosterCreate_R(SEXP); -extern SEXP XGBoosterCreateInEmptyObj_R(SEXP, SEXP); +extern SEXP XGBoosterCopyInfoFromDMatrix_R(SEXP, SEXP); +extern SEXP XGBoosterSetStrFeatureInfo_R(SEXP, SEXP, SEXP); +extern SEXP XGBoosterGetStrFeatureInfo_R(SEXP, SEXP); +extern SEXP XGBoosterBoostedRounds_R(SEXP); +extern SEXP XGBoosterGetNumFeature_R(SEXP); extern SEXP XGBoosterDumpModel_R(SEXP, SEXP, SEXP, SEXP); extern SEXP XGBoosterEvalOneIter_R(SEXP, SEXP, SEXP, SEXP); extern SEXP XGBoosterGetAttrNames_R(SEXP); @@ -57,9 +64,15 @@ extern SEXP XGBGetGlobalConfig_R(void); extern SEXP XGBoosterFeatureScore_R(SEXP, SEXP); static const R_CallMethodDef CallEntries[] = { + {"XGDuplicate_R", (DL_FUNC) &XGDuplicate_R, 1}, + {"XGPointerEqComparison_R", (DL_FUNC) &XGPointerEqComparison_R, 2}, {"XGBoosterTrainOneIter_R", (DL_FUNC) &XGBoosterTrainOneIter_R, 5}, {"XGBoosterCreate_R", (DL_FUNC) &XGBoosterCreate_R, 1}, - {"XGBoosterCreateInEmptyObj_R", (DL_FUNC) &XGBoosterCreateInEmptyObj_R, 2}, + {"XGBoosterCopyInfoFromDMatrix_R", (DL_FUNC) &XGBoosterCopyInfoFromDMatrix_R, 2}, + {"XGBoosterSetStrFeatureInfo_R",(DL_FUNC) &XGBoosterSetStrFeatureInfo_R,3}, // NOLINT + {"XGBoosterGetStrFeatureInfo_R",(DL_FUNC) &XGBoosterGetStrFeatureInfo_R,2}, // NOLINT + {"XGBoosterBoostedRounds_R", (DL_FUNC) &XGBoosterBoostedRounds_R, 1}, + {"XGBoosterGetNumFeature_R", (DL_FUNC) &XGBoosterGetNumFeature_R, 1}, {"XGBoosterDumpModel_R", (DL_FUNC) &XGBoosterDumpModel_R, 4}, {"XGBoosterEvalOneIter_R", (DL_FUNC) &XGBoosterEvalOneIter_R, 4}, {"XGBoosterGetAttrNames_R", (DL_FUNC) &XGBoosterGetAttrNames_R, 1}, @@ -106,4 +119,5 @@ __declspec(dllexport) void attribute_visible R_init_xgboost(DllInfo *dll) { R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); R_useDynamicSymbols(dll, FALSE); + XGBInitializeAltrepClass_R(dll); } diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index d7d4c49e1..63f36ad6a 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -260,16 +260,18 @@ char cpp_ex_msg[512]; using dmlc::BeginPtr; XGB_DLL SEXP XGCheckNullPtr_R(SEXP handle) { - return ScalarLogical(R_ExternalPtrAddr(handle) == NULL); + return Rf_ScalarLogical(R_ExternalPtrAddr(handle) == nullptr); } -XGB_DLL void _DMatrixFinalizer(SEXP ext) { +namespace { +void _DMatrixFinalizer(SEXP ext) { R_API_BEGIN(); if (R_ExternalPtrAddr(ext) == NULL) return; CHECK_CALL(XGDMatrixFree(R_ExternalPtrAddr(ext))); R_ClearExternalPtr(ext); R_API_END(); } +} /* namespace */ XGB_DLL SEXP XGBSetGlobalConfig_R(SEXP json_str) { R_API_BEGIN(); @@ -527,8 +529,14 @@ XGB_DLL SEXP XGDMatrixSetStrFeatureInfo_R(SEXP handle, SEXP field, SEXP array) { } SEXP str_info_holder = PROTECT(Rf_allocVector(VECSXP, len)); - for (size_t i = 0; i < len; ++i) { - SET_VECTOR_ELT(str_info_holder, i, Rf_asChar(VECTOR_ELT(array, i))); + if (TYPEOF(array) == STRSXP) { + for (size_t i = 0; i < len; ++i) { + SET_VECTOR_ELT(str_info_holder, i, STRING_ELT(array, i)); + } + } else { + for (size_t i = 0; i < len; ++i) { + SET_VECTOR_ELT(str_info_holder, i, Rf_asChar(VECTOR_ELT(array, i))); + } } SEXP field_ = PROTECT(Rf_asChar(field)); @@ -614,6 +622,14 @@ XGB_DLL SEXP XGDMatrixNumCol_R(SEXP handle) { return ScalarInteger(static_cast(ncol)); } +XGB_DLL SEXP XGDuplicate_R(SEXP obj) { + return Rf_duplicate(obj); +} + +XGB_DLL SEXP XGPointerEqComparison_R(SEXP obj1, SEXP obj2) { + return Rf_ScalarLogical(R_ExternalPtrAddr(obj1) == R_ExternalPtrAddr(obj2)); +} + XGB_DLL SEXP XGDMatrixGetQuantileCut_R(SEXP handle) { const char *out_names[] = {"indptr", "data", ""}; SEXP continuation_token = Rf_protect(R_MakeUnwindCont()); @@ -682,14 +698,134 @@ XGB_DLL SEXP XGDMatrixGetDataAsCSR_R(SEXP handle) { } // functions related to booster -void _BoosterFinalizer(SEXP ext) { - if (R_ExternalPtrAddr(ext) == NULL) return; - CHECK_CALL(XGBoosterFree(R_ExternalPtrAddr(ext))); - R_ClearExternalPtr(ext); +namespace { +void _BoosterFinalizer(SEXP R_ptr) { + if (R_ExternalPtrAddr(R_ptr) == NULL) return; + CHECK_CALL(XGBoosterFree(R_ExternalPtrAddr(R_ptr))); + R_ClearExternalPtr(R_ptr); +} + +/* Booster is represented as an altrep list with one element which +corresponds to an 'externalptr' holding the C object, forbidding +modification by not implementing setters, and adding custom serialization. */ +R_altrep_class_t XGBAltrepPointerClass; + +R_xlen_t XGBAltrepPointerLength_R(SEXP R_altrepped_obj) { + return 1; +} + +SEXP XGBAltrepPointerGetElt_R(SEXP R_altrepped_obj, R_xlen_t idx) { + return R_altrep_data1(R_altrepped_obj); +} + +SEXP XGBMakeEmptyAltrep() { + SEXP class_name = Rf_protect(Rf_mkString("xgb.Booster")); + SEXP elt_names = Rf_protect(Rf_mkString("ptr")); + SEXP R_ptr = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + SEXP R_altrepped_obj = Rf_protect(R_new_altrep(XGBAltrepPointerClass, R_ptr, R_NilValue)); + Rf_setAttrib(R_altrepped_obj, R_NamesSymbol, elt_names); + Rf_setAttrib(R_altrepped_obj, R_ClassSymbol, class_name); + Rf_unprotect(4); + return R_altrepped_obj; +} + +/* Note: the idea for separating this function from the one above is to be +able to trigger all R allocations first before doing non-R allocations. */ +void XGBAltrepSetPointer(SEXP R_altrepped_obj, BoosterHandle handle) { + SEXP R_ptr = R_altrep_data1(R_altrepped_obj); + R_SetExternalPtrAddr(R_ptr, handle); + R_RegisterCFinalizerEx(R_ptr, _BoosterFinalizer, TRUE); +} + +SEXP XGBAltrepSerializer_R(SEXP R_altrepped_obj) { + R_API_BEGIN(); + BoosterHandle handle = R_ExternalPtrAddr(R_altrep_data1(R_altrepped_obj)); + char const *serialized_bytes; + bst_ulong serialized_length; + CHECK_CALL(XGBoosterSerializeToBuffer( + handle, &serialized_length, &serialized_bytes)); + SEXP R_state = Rf_protect(Rf_allocVector(RAWSXP, serialized_length)); + if (serialized_length != 0) { + std::memcpy(RAW(R_state), serialized_bytes, serialized_length); + } + Rf_unprotect(1); + return R_state; + R_API_END(); + return R_NilValue; /* <- should not be reached */ +} + +SEXP XGBAltrepDeserializer_R(SEXP unused, SEXP R_state) { + SEXP R_altrepped_obj = Rf_protect(XGBMakeEmptyAltrep()); + R_API_BEGIN(); + BoosterHandle handle = nullptr; + CHECK_CALL(XGBoosterCreate(nullptr, 0, &handle)); + int res_code = XGBoosterUnserializeFromBuffer(handle, + RAW(R_state), + Rf_xlength(R_state)); + if (res_code != 0) { + XGBoosterFree(handle); + } + CHECK_CALL(res_code); + XGBAltrepSetPointer(R_altrepped_obj, handle); + R_API_END(); + Rf_unprotect(1); + return R_altrepped_obj; +} + +// https://purrple.cat/blog/2018/10/14/altrep-and-cpp/ +Rboolean XGBAltrepInspector_R( + SEXP x, int pre, int deep, int pvec, + void (*inspect_subtree)(SEXP, int, int, int)) { + Rprintf("Altrepped external pointer [address:%p]\n", + R_ExternalPtrAddr(R_altrep_data1(x))); + return TRUE; +} + +SEXP XGBAltrepDuplicate_R(SEXP R_altrepped_obj, Rboolean deep) { + R_API_BEGIN(); + if (!deep) { + SEXP out = Rf_protect(XGBMakeEmptyAltrep()); + R_set_altrep_data1(out, R_altrep_data1(R_altrepped_obj)); + Rf_unprotect(1); + return out; + } else { + SEXP out = Rf_protect(XGBMakeEmptyAltrep()); + char const *serialized_bytes; + bst_ulong serialized_length; + CHECK_CALL(XGBoosterSerializeToBuffer( + R_ExternalPtrAddr(R_altrep_data1(R_altrepped_obj)), + &serialized_length, &serialized_bytes)); + BoosterHandle new_handle = nullptr; + CHECK_CALL(XGBoosterCreate(nullptr, 0, &new_handle)); + int res_code = XGBoosterUnserializeFromBuffer(new_handle, + serialized_bytes, + serialized_length); + if (res_code != 0) { + XGBoosterFree(new_handle); + } + CHECK_CALL(res_code); + XGBAltrepSetPointer(out, new_handle); + Rf_unprotect(1); + return out; + } + R_API_END(); + return R_NilValue; /* <- should not be reached */ +} + +} /* namespace */ + +XGB_DLL void XGBInitializeAltrepClass_R(DllInfo *dll) { + XGBAltrepPointerClass = R_make_altlist_class("XGBAltrepPointerClass", "xgboost", dll); + R_set_altrep_Length_method(XGBAltrepPointerClass, XGBAltrepPointerLength_R); + R_set_altlist_Elt_method(XGBAltrepPointerClass, XGBAltrepPointerGetElt_R); + R_set_altrep_Inspect_method(XGBAltrepPointerClass, XGBAltrepInspector_R); + R_set_altrep_Serialized_state_method(XGBAltrepPointerClass, XGBAltrepSerializer_R); + R_set_altrep_Unserialize_method(XGBAltrepPointerClass, XGBAltrepDeserializer_R); + R_set_altrep_Duplicate_method(XGBAltrepPointerClass, XGBAltrepDuplicate_R); } XGB_DLL SEXP XGBoosterCreate_R(SEXP dmats) { - SEXP ret = PROTECT(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + SEXP out = Rf_protect(XGBMakeEmptyAltrep()); R_API_BEGIN(); R_xlen_t len = Rf_xlength(dmats); BoosterHandle handle; @@ -703,33 +839,104 @@ XGB_DLL SEXP XGBoosterCreate_R(SEXP dmats) { res_code = XGBoosterCreate(BeginPtr(dvec), dvec.size(), &handle); } CHECK_CALL(res_code); - R_SetExternalPtrAddr(ret, handle); - R_RegisterCFinalizerEx(ret, _BoosterFinalizer, TRUE); + XGBAltrepSetPointer(out, handle); R_API_END(); - UNPROTECT(1); - return ret; + Rf_unprotect(1); + return out; } -XGB_DLL SEXP XGBoosterCreateInEmptyObj_R(SEXP dmats, SEXP R_handle) { +XGB_DLL SEXP XGBoosterCopyInfoFromDMatrix_R(SEXP booster, SEXP dmat) { R_API_BEGIN(); - R_xlen_t len = Rf_xlength(dmats); - BoosterHandle handle; + char const **feature_names; + bst_ulong len_feature_names = 0; + CHECK_CALL(XGDMatrixGetStrFeatureInfo(R_ExternalPtrAddr(dmat), + "feature_name", + &len_feature_names, + &feature_names)); + if (len_feature_names) { + CHECK_CALL(XGBoosterSetStrFeatureInfo(R_ExternalPtrAddr(booster), + "feature_name", + feature_names, + len_feature_names)); + } + + char const **feature_types; + bst_ulong len_feature_types = 0; + CHECK_CALL(XGDMatrixGetStrFeatureInfo(R_ExternalPtrAddr(dmat), + "feature_type", + &len_feature_types, + &feature_types)); + if (len_feature_types) { + CHECK_CALL(XGBoosterSetStrFeatureInfo(R_ExternalPtrAddr(booster), + "feature_type", + feature_types, + len_feature_types)); + } + R_API_END(); + return R_NilValue; +} + +XGB_DLL SEXP XGBoosterSetStrFeatureInfo_R(SEXP handle, SEXP field, SEXP features) { + R_API_BEGIN(); + SEXP field_char = Rf_protect(Rf_asChar(field)); + bst_ulong len_features = Rf_xlength(features); int res_code; { - std::vector dvec(len); - for (R_xlen_t i = 0; i < len; ++i) { - dvec[i] = R_ExternalPtrAddr(VECTOR_ELT(dmats, i)); + std::vector str_arr(len_features); + for (bst_ulong idx = 0; idx < len_features; idx++) { + str_arr[idx] = CHAR(STRING_ELT(features, idx)); } - res_code = XGBoosterCreate(BeginPtr(dvec), dvec.size(), &handle); + res_code = XGBoosterSetStrFeatureInfo(R_ExternalPtrAddr(handle), + CHAR(field_char), + str_arr.data(), + len_features); } CHECK_CALL(res_code); - R_SetExternalPtrAddr(R_handle, handle); - R_RegisterCFinalizerEx(R_handle, _BoosterFinalizer, TRUE); + Rf_unprotect(1); R_API_END(); return R_NilValue; } +XGB_DLL SEXP XGBoosterGetStrFeatureInfo_R(SEXP handle, SEXP field) { + R_API_BEGIN(); + bst_ulong len; + const char **out_features; + SEXP field_char = Rf_protect(Rf_asChar(field)); + CHECK_CALL(XGBoosterGetStrFeatureInfo(R_ExternalPtrAddr(handle), + CHAR(field_char), &len, &out_features)); + SEXP out = Rf_protect(Rf_allocVector(STRSXP, len)); + for (bst_ulong idx = 0; idx < len; idx++) { + SET_STRING_ELT(out, idx, Rf_mkChar(out_features[idx])); + } + Rf_unprotect(2); + return out; + R_API_END(); + return R_NilValue; /* <- should not be reached */ +} + +XGB_DLL SEXP XGBoosterBoostedRounds_R(SEXP handle) { + SEXP out = Rf_protect(Rf_allocVector(INTSXP, 1)); + R_API_BEGIN(); + CHECK_CALL(XGBoosterBoostedRounds(R_ExternalPtrAddr(handle), INTEGER(out))); + R_API_END(); + Rf_unprotect(1); + return out; +} + +/* Note: R's integer class is 32-bit-and-signed only, while xgboost +supports more, so it returns it as a floating point instead */ +XGB_DLL SEXP XGBoosterGetNumFeature_R(SEXP handle) { + SEXP out = Rf_protect(Rf_allocVector(REALSXP, 1)); + R_API_BEGIN(); + bst_ulong res; + CHECK_CALL(XGBoosterGetNumFeature(R_ExternalPtrAddr(handle), &res)); + REAL(out)[0] = static_cast(res); + R_API_END(); + Rf_unprotect(1); + return out; +} + XGB_DLL SEXP XGBoosterSetParam_R(SEXP handle, SEXP name, SEXP val) { R_API_BEGIN(); SEXP name_ = PROTECT(Rf_asChar(name)); @@ -745,8 +952,8 @@ XGB_DLL SEXP XGBoosterSetParam_R(SEXP handle, SEXP name, SEXP val) { XGB_DLL SEXP XGBoosterUpdateOneIter_R(SEXP handle, SEXP iter, SEXP dtrain) { R_API_BEGIN(); CHECK_CALL(XGBoosterUpdateOneIter(R_ExternalPtrAddr(handle), - asInteger(iter), - R_ExternalPtrAddr(dtrain))); + Rf_asInteger(iter), + R_ExternalPtrAddr(dtrain))); R_API_END(); return R_NilValue; } diff --git a/R-package/src/xgboost_R.h b/R-package/src/xgboost_R.h index 4e3458957..79d441792 100644 --- a/R-package/src/xgboost_R.h +++ b/R-package/src/xgboost_R.h @@ -8,7 +8,9 @@ #define XGBOOST_R_H_ // NOLINT(*) +#include #include +#include #include #include @@ -143,6 +145,25 @@ XGB_DLL SEXP XGDMatrixNumRow_R(SEXP handle); */ XGB_DLL SEXP XGDMatrixNumCol_R(SEXP handle); +/*! + * \brief Call R C-level function 'duplicate' + * \param obj Object to duplicate + */ +XGB_DLL SEXP XGDuplicate_R(SEXP obj); + +/*! + * \brief Equality comparison for two pointers + * \param obj1 R 'externalptr' + * \param obj2 R 'externalptr' + */ +XGB_DLL SEXP XGPointerEqComparison_R(SEXP obj1, SEXP obj2); + +/*! + * \brief Register the Altrep class used for the booster + * \param dll DLL info as provided by R_init + */ +XGB_DLL void XGBInitializeAltrepClass_R(DllInfo *dll); + /*! * \brief return the quantile cuts used for the histogram method * \param handle an instance of data matrix @@ -174,13 +195,37 @@ XGB_DLL SEXP XGDMatrixGetDataAsCSR_R(SEXP handle); */ XGB_DLL SEXP XGBoosterCreate_R(SEXP dmats); +/*! + * \brief copy information about features from a DMatrix into a Booster + * \param booster R 'externalptr' pointing to a booster object + * \param dmat R 'externalptr' pointing to a DMatrix object + */ +XGB_DLL SEXP XGBoosterCopyInfoFromDMatrix_R(SEXP booster, SEXP dmat); /*! - * \brief create xgboost learner, saving the pointer into an existing R object - * \param dmats a list of dmatrix handles that will be cached - * \param R_handle a clean R external pointer (not holding any object) + * \brief handle R 'externalptr' holding the booster object + * \param field field name + * \param features features to set for the field */ -XGB_DLL SEXP XGBoosterCreateInEmptyObj_R(SEXP dmats, SEXP R_handle); +XGB_DLL SEXP XGBoosterSetStrFeatureInfo_R(SEXP handle, SEXP field, SEXP features); + +/*! + * \brief handle R 'externalptr' holding the booster object + * \param field field name + */ +XGB_DLL SEXP XGBoosterGetStrFeatureInfo_R(SEXP handle, SEXP field); + +/*! + * \brief Get the number of boosted rounds from a model + * \param handle R 'externalptr' holding the booster object + */ +XGB_DLL SEXP XGBoosterBoostedRounds_R(SEXP handle); + +/*! + * \brief Get the number of features to which the model was fitted + * \param handle R 'externalptr' holding the booster object + */ +XGB_DLL SEXP XGBoosterGetNumFeature_R(SEXP handle); /*! * \brief set parameters diff --git a/R-package/tests/helper_scripts/install_deps.R b/R-package/tests/helper_scripts/install_deps.R index cf9ab0034..3ae44f6b1 100644 --- a/R-package/tests/helper_scripts/install_deps.R +++ b/R-package/tests/helper_scripts/install_deps.R @@ -3,7 +3,6 @@ ## inconsistent is found. pkgs <- c( ## CI - "caret", "pkgbuild", "roxygen2", "XML", diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index 44c530c90..8dd934765 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -25,10 +25,10 @@ test_that("train and predict binary classification", { "train-error" ) expect_equal(class(bst), "xgb.Booster") - expect_equal(bst$niter, nrounds) - expect_false(is.null(bst$evaluation_log)) - expect_equal(nrow(bst$evaluation_log), nrounds) - expect_lt(bst$evaluation_log[, min(train_error)], 0.03) + expect_equal(xgb.get.num.boosted.rounds(bst), nrounds) + expect_false(is.null(attributes(bst)$evaluation_log)) + expect_equal(nrow(attributes(bst)$evaluation_log), nrounds) + expect_lt(attributes(bst)$evaluation_log[, min(train_error)], 0.03) pred <- predict(bst, test$data) expect_length(pred, 1611) @@ -36,7 +36,7 @@ test_that("train and predict binary classification", { pred1 <- predict(bst, train$data, ntreelimit = 1) expect_length(pred1, 6513) err_pred1 <- sum((pred1 > 0.5) != train$label) / length(train$label) - err_log <- bst$evaluation_log[1, train_error] + err_log <- attributes(bst)$evaluation_log[1, train_error] expect_lt(abs(err_pred1 - err_log), 10e-6) pred2 <- predict(bst, train$data, iterationrange = c(1, 2)) @@ -160,9 +160,9 @@ test_that("train and predict softprob", { ), "train-merror" ) - expect_false(is.null(bst$evaluation_log)) - expect_lt(bst$evaluation_log[, min(train_merror)], 0.025) - expect_equal(bst$niter * 3, xgb.ntree(bst)) + expect_false(is.null(attributes(bst)$evaluation_log)) + expect_lt(attributes(bst)$evaluation_log[, min(train_merror)], 0.025) + expect_equal(xgb.get.num.boosted.rounds(bst) * 3, xgb.ntree(bst)) pred <- predict(bst, as.matrix(iris[, -5])) expect_length(pred, nrow(iris) * 3) # row sums add up to total probability of 1: @@ -172,12 +172,12 @@ test_that("train and predict softprob", { expect_equal(as.numeric(t(mpred)), pred) pred_labels <- max.col(mpred) - 1 err <- sum(pred_labels != lb) / length(lb) - expect_equal(bst$evaluation_log[5, train_merror], err, tolerance = 5e-6) + expect_equal(attributes(bst)$evaluation_log[5, train_merror], err, tolerance = 5e-6) # manually calculate error at the 1st iteration: mpred <- predict(bst, as.matrix(iris[, -5]), reshape = TRUE, ntreelimit = 1) pred_labels <- max.col(mpred) - 1 err <- sum(pred_labels != lb) / length(lb) - expect_equal(bst$evaluation_log[1, train_merror], err, tolerance = 5e-6) + expect_equal(attributes(bst)$evaluation_log[1, train_merror], err, tolerance = 5e-6) mpred1 <- predict(bst, as.matrix(iris[, -5]), reshape = TRUE, iterationrange = c(1, 2)) expect_equal(mpred, mpred1) @@ -211,14 +211,14 @@ test_that("train and predict softmax", { ), "train-merror" ) - expect_false(is.null(bst$evaluation_log)) - expect_lt(bst$evaluation_log[, min(train_merror)], 0.025) - expect_equal(bst$niter * 3, xgb.ntree(bst)) + expect_false(is.null(attributes(bst)$evaluation_log)) + expect_lt(attributes(bst)$evaluation_log[, min(train_merror)], 0.025) + expect_equal(xgb.get.num.boosted.rounds(bst) * 3, xgb.ntree(bst)) pred <- predict(bst, as.matrix(iris[, -5])) expect_length(pred, nrow(iris)) err <- sum(pred != lb) / length(lb) - expect_equal(bst$evaluation_log[5, train_merror], err, tolerance = 5e-6) + expect_equal(attributes(bst)$evaluation_log[5, train_merror], err, tolerance = 5e-6) }) test_that("train and predict RF", { @@ -232,12 +232,12 @@ test_that("train and predict RF", { num_parallel_tree = 20, subsample = 0.6, colsample_bytree = 0.1, watchlist = list(train = xgb.DMatrix(train$data, label = lb)) ) - expect_equal(bst$niter, 1) + expect_equal(xgb.get.num.boosted.rounds(bst), 1) expect_equal(xgb.ntree(bst), 20) pred <- predict(bst, train$data) pred_err <- sum((pred > 0.5) != lb) / length(lb) - expect_lt(abs(bst$evaluation_log[1, train_error] - pred_err), 10e-6) + expect_lt(abs(attributes(bst)$evaluation_log[1, train_error] - pred_err), 10e-6) # expect_lt(pred_err, 0.03) pred <- predict(bst, train$data, ntreelimit = 20) @@ -260,18 +260,18 @@ test_that("train and predict RF with softprob", { num_parallel_tree = 4, subsample = 0.5, colsample_bytree = 0.5, watchlist = list(train = xgb.DMatrix(as.matrix(iris[, -5]), label = lb)) ) - expect_equal(bst$niter, 15) + expect_equal(xgb.get.num.boosted.rounds(bst), 15) expect_equal(xgb.ntree(bst), 15 * 3 * 4) # predict for all iterations: pred <- predict(bst, as.matrix(iris[, -5]), reshape = TRUE) expect_equal(dim(pred), c(nrow(iris), 3)) pred_labels <- max.col(pred) - 1 err <- sum(pred_labels != lb) / length(lb) - expect_equal(bst$evaluation_log[nrounds, train_merror], err, tolerance = 5e-6) + expect_equal(attributes(bst)$evaluation_log[nrounds, train_merror], err, tolerance = 5e-6) # predict for 7 iterations and adjust for 4 parallel trees per iteration pred <- predict(bst, as.matrix(iris[, -5]), reshape = TRUE, ntreelimit = 7 * 4) err <- sum((max.col(pred) - 1) != lb) / length(lb) - expect_equal(bst$evaluation_log[7, train_merror], err, tolerance = 5e-6) + expect_equal(attributes(bst)$evaluation_log[7, train_merror], err, tolerance = 5e-6) }) test_that("use of multiple eval metrics works", { @@ -284,9 +284,9 @@ test_that("use of multiple eval metrics works", { ), "train-error.*train-auc.*train-logloss" ) - expect_false(is.null(bst$evaluation_log)) - expect_equal(dim(bst$evaluation_log), c(2, 4)) - expect_equal(colnames(bst$evaluation_log), c("iter", "train_error", "train_auc", "train_logloss")) + expect_false(is.null(attributes(bst)$evaluation_log)) + expect_equal(dim(attributes(bst)$evaluation_log), c(2, 4)) + expect_equal(colnames(attributes(bst)$evaluation_log), c("iter", "train_error", "train_auc", "train_logloss")) expect_output( bst2 <- xgb.train( data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, @@ -296,9 +296,9 @@ test_that("use of multiple eval metrics works", { ), "train-error.*train-auc.*train-logloss" ) - expect_false(is.null(bst2$evaluation_log)) - expect_equal(dim(bst2$evaluation_log), c(2, 4)) - expect_equal(colnames(bst2$evaluation_log), c("iter", "train_error", "train_auc", "train_logloss")) + expect_false(is.null(attributes(bst2)$evaluation_log)) + expect_equal(dim(attributes(bst2)$evaluation_log), c(2, 4)) + expect_equal(colnames(attributes(bst2)$evaluation_log), c("iter", "train_error", "train_auc", "train_logloss")) }) @@ -318,41 +318,25 @@ test_that("training continuation works", { # continue for two more: bst2 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, xgb_model = bst1) if (!windows_flag && !solaris_flag) { - expect_equal(bst$raw, bst2$raw) + expect_equal(xgb.save.raw(bst), xgb.save.raw(bst2)) } - expect_false(is.null(bst2$evaluation_log)) - expect_equal(dim(bst2$evaluation_log), c(4, 2)) - expect_equal(bst2$evaluation_log, bst$evaluation_log) + expect_false(is.null(attributes(bst2)$evaluation_log)) + expect_equal(dim(attributes(bst2)$evaluation_log), c(4, 2)) + expect_equal(attributes(bst2)$evaluation_log, attributes(bst)$evaluation_log) # test continuing from raw model data - bst2 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, xgb_model = bst1$raw) + bst2 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, xgb_model = xgb.save.raw(bst1)) if (!windows_flag && !solaris_flag) { - expect_equal(bst$raw, bst2$raw) + expect_equal(xgb.save.raw(bst), xgb.save.raw(bst2)) } - expect_equal(dim(bst2$evaluation_log), c(2, 2)) + expect_equal(dim(attributes(bst2)$evaluation_log), c(2, 2)) # test continuing from a model in file fname <- file.path(tempdir(), "xgboost.json") xgb.save(bst1, fname) bst2 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, xgb_model = fname) if (!windows_flag && !solaris_flag) { - expect_equal(bst$raw, bst2$raw) + expect_equal(xgb.save.raw(bst), xgb.save.raw(bst2)) } - expect_equal(dim(bst2$evaluation_log), c(2, 2)) -}) - -test_that("model serialization works", { - out_path <- file.path(tempdir(), "model_serialization") - dtrain <- xgb.DMatrix(train$data, label = train$label, nthread = n_threads) - watchlist <- list(train = dtrain) - param <- list(objective = "binary:logistic", nthread = n_threads) - booster <- xgb.train(param, dtrain, nrounds = 4, watchlist) - raw <- xgb.serialize(booster) - saveRDS(raw, out_path) - raw <- readRDS(out_path) - - loaded <- xgb.unserialize(raw) - raw_from_loaded <- xgb.serialize(loaded) - expect_equal(raw, raw_from_loaded) - file.remove(out_path) + expect_equal(dim(attributes(bst2)$evaluation_log), c(2, 2)) }) test_that("xgb.cv works", { @@ -455,8 +439,8 @@ test_that("max_delta_step works", { # model with restricted max_delta_step bst2 <- xgb.train(param, dtrain, nrounds, watchlist, verbose = 1, max_delta_step = 1) # the no-restriction model is expected to have consistently lower loss during the initial iterations - expect_true(all(bst1$evaluation_log$train_logloss < bst2$evaluation_log$train_logloss)) - expect_lt(mean(bst1$evaluation_log$train_logloss) / mean(bst2$evaluation_log$train_logloss), 0.8) + expect_true(all(attributes(bst1)$evaluation_log$train_logloss < attributes(bst2)$evaluation_log$train_logloss)) + expect_lt(mean(attributes(bst1)$evaluation_log$train_logloss) / mean(attributes(bst2)$evaluation_log$train_logloss), 0.8) }) test_that("colsample_bytree works", { @@ -675,3 +659,131 @@ test_that("Can use ranking objectives with either 'qid' or 'group'", { pred_gr <- predict(model_gr, x) expect_equal(pred_qid, pred_gr) }) + +test_that("Coefficients from gblinear have the expected shape and names", { + # Single-column coefficients + data(mtcars) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + mm <- model.matrix(~., data = mtcars[, -1]) + dm <- xgb.DMatrix(x, label = y, nthread = 1) + model <- xgb.train( + data = dm, + params = list( + booster = "gblinear", + nthread = 1 + ), + nrounds = 3 + ) + coefs <- coef(model) + expect_equal(length(coefs), ncol(x) + 1) + expect_equal(names(coefs), c("(Intercept)", colnames(x))) + pred_auto <- predict(model, x) + pred_manual <- as.numeric(mm %*% coefs) + expect_equal(pred_manual, pred_auto, tolerance = 1e-5) + + # Multi-column coefficients + data(iris) + y <- as.numeric(iris$Species) - 1 + x <- as.matrix(iris[, -5]) + dm <- xgb.DMatrix(x, label = y, nthread = 1) + mm <- model.matrix(~., data = iris[, -5]) + model <- xgb.train( + data = dm, + params = list( + booster = "gblinear", + objective = "multi:softprob", + num_class = 3, + nthread = 1 + ), + nrounds = 3 + ) + coefs <- coef(model) + expect_equal(nrow(coefs), ncol(x) + 1) + expect_equal(ncol(coefs), 3) + expect_equal(row.names(coefs), c("(Intercept)", colnames(x))) + pred_auto <- predict(model, x, outputmargin = TRUE, reshape = TRUE) + pred_manual <- unname(mm %*% coefs) + expect_equal(pred_manual, pred_auto, tolerance = 1e-7) +}) + +test_that("Deep copies work as expected", { + data(mtcars) + y <- mtcars$mpg + x <- mtcars[, -1] + dm <- xgb.DMatrix(x, label = y, nthread = 1) + model <- xgb.train( + data = dm, + params = list(nthread = 1), + nrounds = 3 + ) + + xgb.attr(model, "my_attr") <- 100 + model_shallow_copy <- model + xgb.attr(model_shallow_copy, "my_attr") <- 333 + attr_orig <- xgb.attr(model, "my_attr") + attr_shallow <- xgb.attr(model_shallow_copy, "my_attr") + expect_equal(attr_orig, attr_shallow) + + model_deep_copy <- xgb.copy.Booster(model) + xgb.attr(model_deep_copy, "my_attr") <- 444 + attr_orig <- xgb.attr(model, "my_attr") + attr_deep <- xgb.attr(model_deep_copy, "my_attr") + expect_false(attr_orig == attr_deep) +}) + +test_that("Pointer comparison works as expected", { + library(xgboost) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + model <- xgb.train( + params = list(nthread = 1), + data = xgb.DMatrix(x, label = y, nthread = 1), + nrounds = 3 + ) + + model_shallow_copy <- model + expect_true(xgb.is.same.Booster(model, model_shallow_copy)) + + model_deep_copy <- xgb.copy.Booster(model) + expect_false(xgb.is.same.Booster(model, model_deep_copy)) + + xgb.attr(model_shallow_copy, "my_attr") <- 111 + expect_equal(xgb.attr(model, "my_attr"), "111") + expect_null(xgb.attr(model_deep_copy, "my_attr")) +}) + +test_that("DMatrix field are set to booster when training", { + set.seed(123) + y <- rnorm(100) + x <- matrix(rnorm(100 * 3), nrow = 100) + x[, 2] <- abs(as.integer(x[, 2])) + + dm_unnamed <- xgb.DMatrix(x, label = y, nthread = 1) + dm_feature_names <- xgb.DMatrix(x, label = y, feature_names = c("a", "b", "c"), nthread = 1) + dm_feature_types <- xgb.DMatrix(x, label = y) + setinfo(dm_feature_types, "feature_type", c("q", "c", "q")) + dm_both <- xgb.DMatrix(x, label = y, feature_names = c("a", "b", "c"), nthread = 1) + setinfo(dm_both, "feature_type", c("q", "c", "q")) + + params <- list(nthread = 1) + model_unnamed <- xgb.train(data = dm_unnamed, params = params, nrounds = 3) + model_feature_names <- xgb.train(data = dm_feature_names, params = params, nrounds = 3) + model_feature_types <- xgb.train(data = dm_feature_types, params = params, nrounds = 3) + model_both <- xgb.train(data = dm_both, params = params, nrounds = 3) + + expect_null(getinfo(model_unnamed, "feature_name")) + expect_equal(getinfo(model_feature_names, "feature_name"), c("a", "b", "c")) + expect_null(getinfo(model_feature_types, "feature_name")) + expect_equal(getinfo(model_both, "feature_name"), c("a", "b", "c")) + + expect_null(variable.names(model_unnamed)) + expect_equal(variable.names(model_feature_names), c("a", "b", "c")) + expect_null(variable.names(model_feature_types)) + expect_equal(variable.names(model_both), c("a", "b", "c")) + + expect_null(getinfo(model_unnamed, "feature_type")) + expect_null(getinfo(model_feature_names, "feature_type")) + expect_equal(getinfo(model_feature_types, "feature_type"), c("q", "c", "q")) + expect_equal(getinfo(model_both, "feature_type"), c("q", "c", "q")) +}) diff --git a/R-package/tests/testthat/test_callbacks.R b/R-package/tests/testthat/test_callbacks.R index bee98f688..afa270c0b 100644 --- a/R-package/tests/testthat/test_callbacks.R +++ b/R-package/tests/testthat/test_callbacks.R @@ -111,9 +111,9 @@ test_that("can store evaluation_log without printing", { expect_silent( bst <- xgb.train(param, dtrain, nrounds = 10, watchlist, eta = 1, verbose = 0) ) - expect_false(is.null(bst$evaluation_log)) - expect_false(is.null(bst$evaluation_log$train_error)) - expect_lt(bst$evaluation_log[, min(train_error)], 0.2) + expect_false(is.null(attributes(bst)$evaluation_log)) + expect_false(is.null(attributes(bst)$evaluation_log$train_error)) + expect_lt(attributes(bst)$evaluation_log[, min(train_error)], 0.2) }) test_that("cb.reset.parameters works as expected", { @@ -121,34 +121,34 @@ test_that("cb.reset.parameters works as expected", { # fixed eta set.seed(111) bst0 <- xgb.train(param, dtrain, nrounds = 2, watchlist, eta = 0.9, verbose = 0) - expect_false(is.null(bst0$evaluation_log)) - expect_false(is.null(bst0$evaluation_log$train_error)) + expect_false(is.null(attributes(bst0)$evaluation_log)) + expect_false(is.null(attributes(bst0)$evaluation_log$train_error)) # same eta but re-set as a vector parameter in the callback set.seed(111) my_par <- list(eta = c(0.9, 0.9)) bst1 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, callbacks = list(cb.reset.parameters(my_par))) - expect_false(is.null(bst1$evaluation_log$train_error)) - expect_equal(bst0$evaluation_log$train_error, - bst1$evaluation_log$train_error) + expect_false(is.null(attributes(bst1)$evaluation_log$train_error)) + expect_equal(attributes(bst0)$evaluation_log$train_error, + attributes(bst1)$evaluation_log$train_error) # same eta but re-set via a function in the callback set.seed(111) my_par <- list(eta = function(itr, itr_end) 0.9) bst2 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, callbacks = list(cb.reset.parameters(my_par))) - expect_false(is.null(bst2$evaluation_log$train_error)) - expect_equal(bst0$evaluation_log$train_error, - bst2$evaluation_log$train_error) + expect_false(is.null(attributes(bst2)$evaluation_log$train_error)) + expect_equal(attributes(bst0)$evaluation_log$train_error, + attributes(bst2)$evaluation_log$train_error) # different eta re-set as a vector parameter in the callback set.seed(111) my_par <- list(eta = c(0.6, 0.5)) bst3 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, callbacks = list(cb.reset.parameters(my_par))) - expect_false(is.null(bst3$evaluation_log$train_error)) - expect_false(all(bst0$evaluation_log$train_error == bst3$evaluation_log$train_error)) + expect_false(is.null(attributes(bst3)$evaluation_log$train_error)) + expect_false(all(attributes(bst0)$evaluation_log$train_error == attributes(bst3)$evaluation_log$train_error)) # resetting multiple parameters at the same time runs with no error my_par <- list(eta = c(1., 0.5), gamma = c(1, 2), max_depth = c(4, 8)) @@ -166,8 +166,8 @@ test_that("cb.reset.parameters works as expected", { my_par <- list(eta = c(0., 0.)) bstX <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, callbacks = list(cb.reset.parameters(my_par))) - expect_false(is.null(bstX$evaluation_log$train_error)) - er <- unique(bstX$evaluation_log$train_error) + expect_false(is.null(attributes(bstX)$evaluation_log$train_error)) + er <- unique(attributes(bstX)$evaluation_log$train_error) expect_length(er, 1) expect_gt(er, 0.4) }) @@ -183,14 +183,14 @@ test_that("cb.save.model works as expected", { expect_true(file.exists(files[2])) b1 <- xgb.load(files[1]) xgb.parameters(b1) <- list(nthread = 2) - expect_equal(xgb.ntree(b1), 1) + expect_equal(xgb.get.num.boosted.rounds(b1), 1) b2 <- xgb.load(files[2]) xgb.parameters(b2) <- list(nthread = 2) - expect_equal(xgb.ntree(b2), 2) + expect_equal(xgb.get.num.boosted.rounds(b2), 2) xgb.config(b2) <- xgb.config(bst) expect_equal(xgb.config(bst), xgb.config(b2)) - expect_equal(bst$raw, b2$raw) + expect_equal(xgb.save.raw(bst), xgb.save.raw(b2)) # save_period = 0 saves the last iteration's model bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, eta = 1, verbose = 0, @@ -198,7 +198,7 @@ test_that("cb.save.model works as expected", { expect_true(file.exists(files[3])) b2 <- xgb.load(files[3]) xgb.config(b2) <- xgb.config(bst) - expect_equal(bst$raw, b2$raw) + expect_equal(xgb.save.raw(bst), xgb.save.raw(b2)) for (f in files) if (file.exists(f)) file.remove(f) }) @@ -209,14 +209,14 @@ test_that("early stopping xgb.train works", { bst <- xgb.train(param, dtrain, nrounds = 20, watchlist, eta = 0.3, early_stopping_rounds = 3, maximize = FALSE) , "Stopping. Best iteration") - expect_false(is.null(bst$best_iteration)) - expect_lt(bst$best_iteration, 19) - expect_equal(bst$best_iteration, bst$best_ntreelimit) + expect_false(is.null(xgb.attr(bst, "best_iteration"))) + expect_lt(xgb.attr(bst, "best_iteration"), 19) + expect_equal(xgb.attr(bst, "best_iteration"), xgb.attr(bst, "best_ntreelimit")) pred <- predict(bst, dtest) expect_equal(length(pred), 1611) err_pred <- err(ltest, pred) - err_log <- bst$evaluation_log[bst$best_iteration, test_error] + err_log <- attributes(bst)$evaluation_log[xgb.attr(bst, "best_iteration"), test_error] expect_equal(err_log, err_pred, tolerance = 5e-6) set.seed(11) @@ -224,15 +224,15 @@ test_that("early stopping xgb.train works", { bst0 <- xgb.train(param, dtrain, nrounds = 20, watchlist, eta = 0.3, early_stopping_rounds = 3, maximize = FALSE, verbose = 0) ) - expect_equal(bst$evaluation_log, bst0$evaluation_log) + expect_equal(attributes(bst)$evaluation_log, attributes(bst0)$evaluation_log) fname <- file.path(tempdir(), "model.bin") xgb.save(bst, fname) loaded <- xgb.load(fname) - expect_false(is.null(loaded$best_iteration)) - expect_equal(loaded$best_iteration, bst$best_ntreelimit) - expect_equal(loaded$best_ntreelimit, bst$best_ntreelimit) + expect_false(is.null(xgb.attr(loaded, "best_iteration"))) + expect_equal(xgb.attr(loaded, "best_iteration"), xgb.attr(bst, "best_ntreelimit")) + expect_equal(xgb.attr(loaded, "best_ntreelimit"), xgb.attr(bst, "best_ntreelimit")) }) test_that("early stopping using a specific metric works", { @@ -243,14 +243,14 @@ test_that("early stopping using a specific metric works", { callbacks = list(cb.early.stop(stopping_rounds = 3, maximize = FALSE, metric_name = 'test_logloss'))) , "Stopping. Best iteration") - expect_false(is.null(bst$best_iteration)) - expect_lt(bst$best_iteration, 19) - expect_equal(bst$best_iteration, bst$best_ntreelimit) + expect_false(is.null(xgb.attr(bst, "best_iteration"))) + expect_lt(xgb.attr(bst, "best_iteration"), 19) + expect_equal(xgb.attr(bst, "best_iteration"), xgb.attr(bst, "best_ntreelimit")) - pred <- predict(bst, dtest, ntreelimit = bst$best_ntreelimit) + pred <- predict(bst, dtest, ntreelimit = xgb.attr(bst, "best_ntreelimit")) expect_equal(length(pred), 1611) logloss_pred <- sum(-ltest * log(pred) - (1 - ltest) * log(1 - pred)) / length(ltest) - logloss_log <- bst$evaluation_log[bst$best_iteration, test_logloss] + logloss_log <- attributes(bst)$evaluation_log[xgb.attr(bst, "best_iteration"), test_logloss] expect_equal(logloss_log, logloss_pred, tolerance = 1e-5) }) diff --git a/R-package/tests/testthat/test_custom_objective.R b/R-package/tests/testthat/test_custom_objective.R index ff8eb1d6d..c65031246 100644 --- a/R-package/tests/testthat/test_custom_objective.R +++ b/R-package/tests/testthat/test_custom_objective.R @@ -35,9 +35,9 @@ num_round <- 2 test_that("custom objective works", { bst <- xgb.train(param, dtrain, num_round, watchlist) expect_equal(class(bst), "xgb.Booster") - expect_false(is.null(bst$evaluation_log)) - expect_false(is.null(bst$evaluation_log$eval_error)) - expect_lt(bst$evaluation_log[num_round, eval_error], 0.03) + expect_false(is.null(attributes(bst)$evaluation_log)) + expect_false(is.null(attributes(bst)$evaluation_log$eval_error)) + expect_lt(attributes(bst)$evaluation_log[num_round, eval_error], 0.03) }) test_that("custom objective in CV works", { @@ -50,7 +50,7 @@ test_that("custom objective in CV works", { test_that("custom objective with early stop works", { bst <- xgb.train(param, dtrain, 10, watchlist) expect_equal(class(bst), "xgb.Booster") - train_log <- bst$evaluation_log$train_error + train_log <- attributes(bst)$evaluation_log$train_error expect_true(all(diff(train_log) <= 0)) }) diff --git a/R-package/tests/testthat/test_glm.R b/R-package/tests/testthat/test_glm.R index 9e0a3551f..ae698d98f 100644 --- a/R-package/tests/testthat/test_glm.R +++ b/R-package/tests/testthat/test_glm.R @@ -24,28 +24,28 @@ test_that("gblinear works", { bst <- xgb.train(param, dtrain, n, watchlist, verbose = VERB, feature_selector = 'shuffle') ypred <- predict(bst, dtest) expect_equal(length(getinfo(dtest, 'label')), 1611) - expect_lt(bst$evaluation_log$eval_error[n], ERR_UL) + expect_lt(attributes(bst)$evaluation_log$eval_error[n], ERR_UL) bst <- xgb.train(param, dtrain, n, watchlist, verbose = VERB, feature_selector = 'cyclic', callbacks = list(cb.gblinear.history())) - expect_lt(bst$evaluation_log$eval_error[n], ERR_UL) + expect_lt(attributes(bst)$evaluation_log$eval_error[n], ERR_UL) h <- xgb.gblinear.history(bst) expect_equal(dim(h), c(n, ncol(dtrain) + 1)) expect_is(h, "matrix") param$updater <- 'coord_descent' bst <- xgb.train(param, dtrain, n, watchlist, verbose = VERB, feature_selector = 'cyclic') - expect_lt(bst$evaluation_log$eval_error[n], ERR_UL) + expect_lt(attributes(bst)$evaluation_log$eval_error[n], ERR_UL) bst <- xgb.train(param, dtrain, n, watchlist, verbose = VERB, feature_selector = 'shuffle') - expect_lt(bst$evaluation_log$eval_error[n], ERR_UL) + expect_lt(attributes(bst)$evaluation_log$eval_error[n], ERR_UL) bst <- xgb.train(param, dtrain, 2, watchlist, verbose = VERB, feature_selector = 'greedy') - expect_lt(bst$evaluation_log$eval_error[2], ERR_UL) + expect_lt(attributes(bst)$evaluation_log$eval_error[2], ERR_UL) bst <- xgb.train(param, dtrain, n, watchlist, verbose = VERB, feature_selector = 'thrifty', top_k = 50, callbacks = list(cb.gblinear.history(sparse = TRUE))) - expect_lt(bst$evaluation_log$eval_error[n], ERR_UL) + expect_lt(attributes(bst)$evaluation_log$eval_error[n], ERR_UL) h <- xgb.gblinear.history(bst) expect_equal(dim(h), c(n, ncol(dtrain) + 1)) expect_s4_class(h, "dgCMatrix") @@ -72,10 +72,10 @@ test_that("gblinear early stopping works", { booster <- xgb.train( param, dtrain, n, list(eval = dtest, train = dtrain), early_stopping_rounds = es_round ) - expect_equal(booster$best_iteration, 5) + expect_equal(xgb.attr(booster, "best_iteration"), 5) predt_es <- predict(booster, dtrain) - n <- booster$best_iteration + es_round + n <- xgb.attr(booster, "best_iteration") + es_round booster <- xgb.train( param, dtrain, n, list(eval = dtest, train = dtrain), early_stopping_rounds = es_round ) diff --git a/R-package/tests/testthat/test_helpers.R b/R-package/tests/testthat/test_helpers.R index 62a8c44bc..372f2520c 100644 --- a/R-package/tests/testthat/test_helpers.R +++ b/R-package/tests/testthat/test_helpers.R @@ -49,6 +49,9 @@ mbst.GLM <- xgb.train(data = xgb.DMatrix(as.matrix(iris[, -5]), label = mlabel), booster = "gblinear", eta = 0.1, nthread = 1, nrounds = nrounds, objective = "multi:softprob", num_class = nclass, base_score = 0) +# without feature names +bst.Tree.unnamed <- xgb.copy.Booster(bst.Tree) +setinfo(bst.Tree.unnamed, "feature_name", NULL) test_that("xgb.dump works", { .skip_if_vcd_not_available() @@ -204,7 +207,7 @@ test_that("xgb-attribute functionality", { list.ch <- list.val[order(names(list.val))] list.ch <- lapply(list.ch, as.character) # note: iter is 0-index in xgb attributes - list.default <- list(niter = as.character(nrounds - 1)) + list.default <- list() list.ch <- c(list.ch, list.default) # proper input: expect_error(xgb.attr(bst.Tree, NULL)) @@ -212,24 +215,25 @@ test_that("xgb-attribute functionality", { # set & get: expect_null(xgb.attr(bst.Tree, "asdf")) expect_equal(xgb.attributes(bst.Tree), list.default) - xgb.attr(bst.Tree, "my_attr") <- val - expect_equal(xgb.attr(bst.Tree, "my_attr"), val) - xgb.attributes(bst.Tree) <- list.val - expect_equal(xgb.attributes(bst.Tree), list.ch) + bst.Tree.copy <- xgb.copy.Booster(bst.Tree) + xgb.attr(bst.Tree.copy, "my_attr") <- val + expect_equal(xgb.attr(bst.Tree.copy, "my_attr"), val) + xgb.attributes(bst.Tree.copy) <- list.val + expect_equal(xgb.attributes(bst.Tree.copy), list.ch) # serializing: - fname <- file.path(tempdir(), "xgb.model") - xgb.save(bst.Tree, fname) + fname <- file.path(tempdir(), "xgb.ubj") + xgb.save(bst.Tree.copy, fname) bst <- xgb.load(fname) expect_equal(xgb.attr(bst, "my_attr"), val) expect_equal(xgb.attributes(bst), list.ch) # deletion: xgb.attr(bst, "my_attr") <- NULL expect_null(xgb.attr(bst, "my_attr")) - expect_equal(xgb.attributes(bst), list.ch[c("a", "b", "niter")]) + expect_equal(xgb.attributes(bst), list.ch[c("a", "b")]) xgb.attributes(bst) <- list(a = NULL, b = NULL) expect_equal(xgb.attributes(bst), list.default) xgb.attributes(bst) <- list(niter = NULL) - expect_null(xgb.attributes(bst)) + expect_equal(xgb.attributes(bst), list()) }) if (grepl('Windows', Sys.info()[['sysname']], fixed = TRUE) || @@ -262,21 +266,17 @@ test_that("xgb.Booster serializing as R object works", { dtrain <- xgb.DMatrix(sparse_matrix, label = label, nthread = 2) expect_equal(predict(bst.Tree, dtrain), predict(bst, dtrain), tolerance = float_tolerance) expect_equal(xgb.dump(bst.Tree), xgb.dump(bst)) + fname_bin <- file.path(tempdir(), "xgb.model") xgb.save(bst, fname_bin) bst <- readRDS(fname_rds) - nil_ptr <- new("externalptr") - class(nil_ptr) <- "xgb.Booster.handle" - expect_true(identical(bst$handle, nil_ptr)) - bst <- xgb.Booster.complete(bst) - expect_true(!identical(bst$handle, nil_ptr)) expect_equal(predict(bst.Tree, dtrain), predict(bst, dtrain), tolerance = float_tolerance) }) test_that("xgb.model.dt.tree works with and without feature names", { .skip_if_vcd_not_available() names.dt.trees <- c("Tree", "Node", "ID", "Feature", "Split", "Yes", "No", "Missing", "Gain", "Cover") - dt.tree <- xgb.model.dt.tree(feature_names = feature.names, model = bst.Tree) + dt.tree <- xgb.model.dt.tree(model = bst.Tree) expect_equal(names.dt.trees, names(dt.tree)) if (!flag_32bit) expect_equal(dim(dt.tree), c(188, 10)) @@ -286,9 +286,7 @@ test_that("xgb.model.dt.tree works with and without feature names", { expect_equal(dt.tree, dt.tree.0) # when model contains no feature names: - bst.Tree.x <- bst.Tree - bst.Tree.x$feature_names <- NULL - dt.tree.x <- xgb.model.dt.tree(model = bst.Tree.x) + dt.tree.x <- xgb.model.dt.tree(model = bst.Tree.unnamed) expect_output(str(dt.tree.x), 'Feature.*\\"3\\"') expect_equal(dt.tree[, -4, with = FALSE], dt.tree.x[, -4, with = FALSE]) @@ -316,9 +314,7 @@ test_that("xgb.importance works with and without feature names", { expect_equal(importance.Tree, importance.Tree.0, tolerance = float_tolerance) # when model contains no feature names: - bst.Tree.x <- bst.Tree - bst.Tree.x$feature_names <- NULL - importance.Tree.x <- xgb.importance(model = bst.Tree) + importance.Tree.x <- xgb.importance(model = bst.Tree.unnamed) expect_equal(importance.Tree[, -1, with = FALSE], importance.Tree.x[, -1, with = FALSE], tolerance = float_tolerance) @@ -334,7 +330,7 @@ test_that("xgb.importance works with and without feature names", { importance <- xgb.importance(feature_names = feature.names, model = bst.Tree, trees = trees) importance_from_dump <- function() { - model_text_dump <- xgb.dump(model = bst.Tree, with_stats = TRUE, trees = trees) + model_text_dump <- xgb.dump(model = bst.Tree.unnamed, with_stats = TRUE, trees = trees) imp <- xgb.model.dt.tree( feature_names = feature.names, text = model_text_dump, @@ -414,13 +410,13 @@ test_that("xgb.plot.importance de-duplicates features", { test_that("xgb.plot.tree works with and without feature names", { .skip_if_vcd_not_available() - expect_silent(xgb.plot.tree(feature_names = feature.names, model = bst.Tree)) + expect_silent(xgb.plot.tree(feature_names = feature.names, model = bst.Tree.unnamed)) expect_silent(xgb.plot.tree(model = bst.Tree)) }) test_that("xgb.plot.multi.trees works with and without feature names", { .skip_if_vcd_not_available() - xgb.plot.multi.trees(model = bst.Tree, feature_names = feature.names, features_keep = 3) + xgb.plot.multi.trees(model = bst.Tree.unnamed, feature_names = feature.names, features_keep = 3) xgb.plot.multi.trees(model = bst.Tree, features_keep = 3) }) diff --git a/R-package/tests/testthat/test_io.R b/R-package/tests/testthat/test_io.R index 3c64ddc72..36a6d7572 100644 --- a/R-package/tests/testthat/test_io.R +++ b/R-package/tests/testthat/test_io.R @@ -17,8 +17,8 @@ test_that("load/save raw works", { ubj_bytes <- xgb.save.raw(booster, raw_format = "ubj") old_bytes <- xgb.save.raw(booster, raw_format = "deprecated") - from_json <- xgb.load.raw(json_bytes, as_booster = TRUE) - from_ubj <- xgb.load.raw(ubj_bytes, as_booster = TRUE) + from_json <- xgb.load.raw(json_bytes) + from_ubj <- xgb.load.raw(ubj_bytes) json2old <- xgb.save.raw(from_json, raw_format = "deprecated") ubj2old <- xgb.save.raw(from_ubj, raw_format = "deprecated") @@ -26,3 +26,46 @@ test_that("load/save raw works", { expect_equal(json2old, ubj2old) expect_equal(json2old, old_bytes) }) + +test_that("saveRDS preserves C and R attributes", { + data(mtcars) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + dm <- xgb.DMatrix(x, label = y, nthread = 1) + model <- xgb.train( + data = dm, + params = list(nthread = 1, max_depth = 2), + nrounds = 5 + ) + attributes(model)$my_attr <- "qwerty" + xgb.attr(model, "c_attr") <- "asdf" + + fname <- file.path(tempdir(), "xgb_model.Rds") + saveRDS(model, fname) + model_new <- readRDS(fname) + + expect_equal(attributes(model_new)$my_attr, attributes(model)$my_attr) + expect_equal(xgb.attr(model, "c_attr"), xgb.attr(model_new, "c_attr")) +}) + +test_that("R serializers keep C config", { + data(mtcars) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + dm <- xgb.DMatrix(x, label = y, nthread = 1) + model <- xgb.train( + data = dm, + params = list( + tree_method = "approx", + nthread = 1, + max_depth = 2 + ), + nrounds = 3 + ) + model_new <- unserialize(serialize(model, NULL)) + expect_equal( + xgb.config(model)$learner$gradient_booster$gbtree_train_param$tree_method, + xgb.config(model_new)$learner$gradient_booster$gbtree_train_param$tree_method + ) + expect_equal(variable.names(model), variable.names(model_new)) +}) diff --git a/R-package/tests/testthat/test_model_compatibility.R b/R-package/tests/testthat/test_model_compatibility.R index ce1725dc9..613ba066f 100644 --- a/R-package/tests/testthat/test_model_compatibility.R +++ b/R-package/tests/testthat/test_model_compatibility.R @@ -23,11 +23,7 @@ get_num_tree <- function(booster) { } run_booster_check <- function(booster, name) { - # If given a handle, we need to call xgb.Booster.complete() prior to using xgb.config(). - if (inherits(booster, "xgb.Booster") && xgboost:::is.null.handle(booster$handle)) { - booster <- xgb.Booster.complete(booster) - } - config <- jsonlite::fromJSON(xgb.config(booster)) + config <- xgb.config(booster) run_model_param_check(config) if (name == 'cls') { testthat::expect_equal(get_num_tree(booster), @@ -76,6 +72,10 @@ test_that("Models from previous versions of XGBoost can be loaded", { name <- m[3] is_rds <- endsWith(model_file, '.rds') is_json <- endsWith(model_file, '.json') + # TODO: update this test for new RDS format + if (is_rds) { + return(NULL) + } # Expect an R warning when a model is loaded from RDS and it was generated by version < 1.1.x if (is_rds && compareVersion(model_xgb_ver, '1.1.1.1') < 0) { booster <- readRDS(model_file) diff --git a/R-package/tests/testthat/test_parameter_exposure.R b/R-package/tests/testthat/test_parameter_exposure.R index 5b12fde01..ed5c28ca5 100644 --- a/R-package/tests/testthat/test_parameter_exposure.R +++ b/R-package/tests/testthat/test_parameter_exposure.R @@ -19,12 +19,12 @@ bst <- xgb.train(data = dtrain, objective = "binary:logistic") test_that("call is exposed to R", { - expect_false(is.null(bst$call)) - expect_is(bst$call, "call") + expect_false(is.null(attributes(bst)$call)) + expect_is(attributes(bst)$call, "call") }) test_that("params is exposed to R", { - model_params <- bst$params + model_params <- attributes(bst)$params expect_is(model_params, "list") expect_equal(model_params$eta, 1) expect_equal(model_params$max_depth, 2) diff --git a/R-package/tests/testthat/test_ranking.R b/R-package/tests/testthat/test_ranking.R index d4102dfce..277c8f288 100644 --- a/R-package/tests/testthat/test_ranking.R +++ b/R-package/tests/testthat/test_ranking.R @@ -17,8 +17,8 @@ test_that('Test ranking with unweighted data', { eval_metric = 'auc', eval_metric = 'aucpr', nthread = n_threads) bst <- xgb.train(params, dtrain, nrounds = 10, watchlist = list(train = dtrain)) # Check if the metric is monotone increasing - expect_true(all(diff(bst$evaluation_log$train_auc) >= 0)) - expect_true(all(diff(bst$evaluation_log$train_aucpr) >= 0)) + expect_true(all(diff(attributes(bst)$evaluation_log$train_auc) >= 0)) + expect_true(all(diff(attributes(bst)$evaluation_log$train_aucpr) >= 0)) }) test_that('Test ranking with weighted data', { @@ -41,8 +41,8 @@ test_that('Test ranking with weighted data', { ) bst <- xgb.train(params, dtrain, nrounds = 10, watchlist = list(train = dtrain)) # Check if the metric is monotone increasing - expect_true(all(diff(bst$evaluation_log$train_auc) >= 0)) - expect_true(all(diff(bst$evaluation_log$train_aucpr) >= 0)) + expect_true(all(diff(attributes(bst)$evaluation_log$train_auc) >= 0)) + expect_true(all(diff(attributes(bst)$evaluation_log$train_aucpr) >= 0)) for (i in 1:10) { pred <- predict(bst, newdata = dtrain, ntreelimit = i) # is_sorted[i]: is i-th group correctly sorted by the ranking predictor? diff --git a/R-package/tests/testthat/test_update.R b/R-package/tests/testthat/test_update.R index f37bb0d21..3c88178e0 100644 --- a/R-package/tests/testthat/test_update.R +++ b/R-package/tests/testthat/test_update.R @@ -40,7 +40,12 @@ test_that("updating the model works", { bst1r <- xgb.train(p1r, dtrain, nrounds = 10, watchlist, verbose = 0) tr1r <- xgb.model.dt.tree(model = bst1r) # all should be the same when no subsampling - expect_equal(bst1$evaluation_log, bst1r$evaluation_log) + expect_equal(attributes(bst1)$evaluation_log, attributes(bst1r)$evaluation_log) + expect_equal( + jsonlite::fromJSON(rawToChar(xgb.save.raw(bst1, raw_format = "json"))), + jsonlite::fromJSON(rawToChar(xgb.save.raw(bst1r, raw_format = "json"))), + tolerance = 1e-6 + ) if (!win32_flag) { expect_equal(tr1, tr1r, tolerance = 0.00001, check.attributes = FALSE) } @@ -51,7 +56,7 @@ test_that("updating the model works", { bst2r <- xgb.train(p2r, dtrain, nrounds = 10, watchlist, verbose = 0) tr2r <- xgb.model.dt.tree(model = bst2r) # should be the same evaluation but different gains and larger cover - expect_equal(bst2$evaluation_log, bst2r$evaluation_log) + expect_equal(attributes(bst2)$evaluation_log, attributes(bst2r)$evaluation_log) if (!win32_flag) { expect_equal(tr2[Feature == 'Leaf']$Gain, tr2r[Feature == 'Leaf']$Gain) } @@ -59,11 +64,25 @@ test_that("updating the model works", { expect_gt(sum(tr2r$Cover) / sum(tr2$Cover), 1.5) # process type 'update' for no-subsampling model, refreshing the tree stats AND leaves from training data: + set.seed(123) p1u <- modifyList(p1, list(process_type = 'update', updater = 'refresh', refresh_leaf = TRUE)) bst1u <- xgb.train(p1u, dtrain, nrounds = 10, watchlist, verbose = 0, xgb_model = bst1) tr1u <- xgb.model.dt.tree(model = bst1u) # all should be the same when no subsampling - expect_equal(bst1$evaluation_log, bst1u$evaluation_log) + expect_equal(attributes(bst1)$evaluation_log, attributes(bst1u)$evaluation_log) + expect_equal( + jsonlite::fromJSON(rawToChar(xgb.save.raw(bst1, raw_format = "json"))), + jsonlite::fromJSON(rawToChar(xgb.save.raw(bst1u, raw_format = "json"))), + tolerance = 1e-6 + ) + expect_equal(tr1, tr1u, tolerance = 0.00001, check.attributes = FALSE) + + # same thing but with a serialized model + set.seed(123) + bst1u <- xgb.train(p1u, dtrain, nrounds = 10, watchlist, verbose = 0, xgb_model = xgb.save.raw(bst1)) + tr1u <- xgb.model.dt.tree(model = bst1u) + # all should be the same when no subsampling + expect_equal(attributes(bst1)$evaluation_log, attributes(bst1u)$evaluation_log) expect_equal(tr1, tr1u, tolerance = 0.00001, check.attributes = FALSE) # process type 'update' for model with subsampling, refreshing only the tree stats from training data: @@ -71,12 +90,12 @@ test_that("updating the model works", { bst2u <- xgb.train(p2u, dtrain, nrounds = 10, watchlist, verbose = 0, xgb_model = bst2) tr2u <- xgb.model.dt.tree(model = bst2u) # should be the same evaluation but different gains and larger cover - expect_equal(bst2$evaluation_log, bst2u$evaluation_log) + expect_equal(attributes(bst2)$evaluation_log, attributes(bst2u)$evaluation_log) expect_equal(tr2[Feature == 'Leaf']$Gain, tr2u[Feature == 'Leaf']$Gain) expect_gt(sum(abs(tr2[Feature != 'Leaf']$Gain - tr2u[Feature != 'Leaf']$Gain)), 100) expect_gt(sum(tr2u$Cover) / sum(tr2$Cover), 1.5) # the results should be the same as for the model with an extra 'refresh' updater - expect_equal(bst2r$evaluation_log, bst2u$evaluation_log) + expect_equal(attributes(bst2r)$evaluation_log, attributes(bst2u)$evaluation_log) if (!win32_flag) { expect_equal(tr2r, tr2u, tolerance = 0.00001, check.attributes = FALSE) } @@ -86,7 +105,7 @@ test_that("updating the model works", { bst1ut <- xgb.train(p1ut, dtest, nrounds = 10, watchlist, verbose = 0, xgb_model = bst1) tr1ut <- xgb.model.dt.tree(model = bst1ut) # should be the same evaluations but different gains and smaller cover (test data is smaller) - expect_equal(bst1$evaluation_log, bst1ut$evaluation_log) + expect_equal(attributes(bst1)$evaluation_log, attributes(bst1ut)$evaluation_log) expect_equal(tr1[Feature == 'Leaf']$Gain, tr1ut[Feature == 'Leaf']$Gain) expect_gt(sum(abs(tr1[Feature != 'Leaf']$Gain - tr1ut[Feature != 'Leaf']$Gain)), 100) expect_lt(sum(tr1ut$Cover) / sum(tr1$Cover), 0.5) @@ -106,11 +125,12 @@ test_that("updating works for multiclass & multitree", { # run update process for an original model with subsampling p0u <- modifyList(p0, list(process_type = 'update', updater = 'refresh', refresh_leaf = FALSE)) - bst0u <- xgb.train(p0u, dtr, nrounds = bst0$niter, watchlist, xgb_model = bst0, verbose = 0) + bst0u <- xgb.train(p0u, dtr, nrounds = xgb.get.num.boosted.rounds(bst0), + watchlist, xgb_model = bst0, verbose = 0) tr0u <- xgb.model.dt.tree(model = bst0u) # should be the same evaluation but different gains and larger cover - expect_equal(bst0$evaluation_log, bst0u$evaluation_log) + expect_equal(attributes(bst0)$evaluation_log, attributes(bst0u)$evaluation_log) expect_equal(tr0[Feature == 'Leaf']$Gain, tr0u[Feature == 'Leaf']$Gain) expect_gt(sum(abs(tr0[Feature != 'Leaf']$Gain - tr0u[Feature != 'Leaf']$Gain)), 100) expect_gt(sum(tr0u$Cover) / sum(tr0$Cover), 1.5) diff --git a/R-package/vignettes/xgboost.Rnw b/R-package/vignettes/xgboost.Rnw deleted file mode 100644 index 7edf4ace3..000000000 --- a/R-package/vignettes/xgboost.Rnw +++ /dev/null @@ -1,223 +0,0 @@ -\documentclass{article} -\RequirePackage{url} -\usepackage{hyperref} -\RequirePackage{amsmath} -\RequirePackage{natbib} -\RequirePackage[a4paper,lmargin={1.25in},rmargin={1.25in},tmargin={1in},bmargin={1in}]{geometry} - -\makeatletter -% \VignetteIndexEntry{xgboost: eXtreme Gradient Boosting} -%\VignetteKeywords{xgboost, gbm, gradient boosting machines} -%\VignettePackage{xgboost} -% \VignetteEngine{knitr::knitr} -\makeatother - -\begin{document} -%\SweaveOpts{concordance=TRUE} - -<>= -if (require('knitr')) opts_chunk$set(fig.width = 5, fig.height = 5, fig.align = 'center', tidy = FALSE, warning = FALSE, cache = TRUE) -@ - -% -<>= -xgboost.version <- packageDescription("xgboost")$Version - -@ -% - - \begin{center} - \vspace*{6\baselineskip} - \rule{\textwidth}{1.6pt}\vspace*{-\baselineskip}\vspace*{2pt} - \rule{\textwidth}{0.4pt}\\[2\baselineskip] - {\LARGE \textbf{xgboost: eXtreme Gradient Boosting}}\\[1.2\baselineskip] - \rule{\textwidth}{0.4pt}\vspace*{-\baselineskip}\vspace{3.2pt} - \rule{\textwidth}{1.6pt}\\[2\baselineskip] - {\Large Tianqi Chen, Tong He}\\[\baselineskip] - {\large Package Version: \Sexpr{xgboost.version}}\\[\baselineskip] - {\large \today}\par - \vfill - \end{center} - -\thispagestyle{empty} - -\clearpage - -\setcounter{page}{1} - -\section{Introduction} - -This is an introductory document of using the \verb@xgboost@ package in R. - -\verb@xgboost@ is short for eXtreme Gradient Boosting package. It is an efficient - and scalable implementation of gradient boosting framework by \citep{friedman2001greedy} \citep{friedman2000additive}. -The package includes efficient linear model solver and tree learning algorithm. -It supports various objective functions, including regression, classification -and ranking. The package is made to be extendible, so that users are also allowed to define their own objectives easily. It has several features: -\begin{enumerate} - \item{Speed: }{\verb@xgboost@ can automatically do parallel computation on - Windows and Linux, with openmp. It is generally over 10 times faster than - \verb@gbm@.} - \item{Input Type: }{\verb@xgboost@ takes several types of input data:} - \begin{itemize} - \item{Dense Matrix: }{R's dense matrix, i.e. \verb@matrix@} - \item{Sparse Matrix: }{R's sparse matrix \verb@Matrix::dgCMatrix@} - \item{Data File: }{Local data files} - \item{xgb.DMatrix: }{\verb@xgboost@'s own class. Recommended.} - \end{itemize} - \item{Sparsity: }{\verb@xgboost@ accepts sparse input for both tree booster - and linear booster, and is optimized for sparse input.} - \item{Customization: }{\verb@xgboost@ supports customized objective function - and evaluation function} - \item{Performance: }{\verb@xgboost@ has better performance on several different - datasets.} -\end{enumerate} - - -\section{Example with Mushroom data} - -In this section, we will illustrate some common usage of \verb@xgboost@. The -Mushroom data is cited from UCI Machine Learning Repository. \citep{Bache+Lichman:2013} - -<>= -library(xgboost) -data(agaricus.train, package='xgboost') -data(agaricus.test, package='xgboost') -train <- agaricus.train -test <- agaricus.test -bst <- xgboost(data = train$data, label = train$label, max_depth = 2, eta = 1, - nrounds = 2, objective = "binary:logistic", nthread = 2) -xgb.save(bst, 'model.save') -bst = xgb.load('model.save') -xgb.parameters(bst) <- list(nthread = 2) -pred <- predict(bst, test$data) -@ - -\verb@xgboost@ is the main function to train a \verb@Booster@, i.e. a model. -\verb@predict@ does prediction on the model. - -Here we can save the model to a binary local file, and load it when needed. -We can't inspect the trees inside. However we have another function to save the -model in plain text. -<>= -xgb.dump(bst, 'model.dump') -@ - -The output looks like - -\begin{verbatim} -booster[0]: -0:[f28<1.00001] yes=1,no=2,missing=2 - 1:[f108<1.00001] yes=3,no=4,missing=4 - 3:leaf=1.85965 - 4:leaf=-1.94071 - 2:[f55<1.00001] yes=5,no=6,missing=6 - 5:leaf=-1.70044 - 6:leaf=1.71218 -booster[1]: -0:[f59<1.00001] yes=1,no=2,missing=2 - 1:leaf=-6.23624 - 2:[f28<1.00001] yes=3,no=4,missing=4 - 3:leaf=-0.96853 - 4:leaf=0.784718 -\end{verbatim} - -It is important to know \verb@xgboost@'s own data type: \verb@xgb.DMatrix@. -It speeds up \verb@xgboost@, and is needed for advanced features such as -training from initial prediction value, weighted training instance. - -We can use \verb@xgb.DMatrix@ to construct an \verb@xgb.DMatrix@ object: -<>= -dtrain <- xgb.DMatrix(train$data, label = train$label, nthread = 2) -class(dtrain) -head(getinfo(dtrain,'label')) -@ - -We can also save the matrix to a binary file. Then load it simply with -\verb@xgb.DMatrix@ -<>= -xgb.DMatrix.save(dtrain, 'xgb.DMatrix') -dtrain = xgb.DMatrix('xgb.DMatrix') -@ - -\section{Advanced Examples} - -The function \verb@xgboost@ is a simple function with less parameter, in order -to be R-friendly. The core training function is wrapped in \verb@xgb.train@. It is more flexible than \verb@xgboost@, but it requires users to read the document a bit more carefully. - -\verb@xgb.train@ only accept a \verb@xgb.DMatrix@ object as its input, while it supports advanced features as custom objective and evaluation functions. - -<>= -logregobj <- function(preds, dtrain) { - labels <- getinfo(dtrain, "label") - preds <- 1/(1 + exp(-preds)) - grad <- preds - labels - hess <- preds * (1 - preds) - return(list(grad = grad, hess = hess)) -} - -evalerror <- function(preds, dtrain) { - labels <- getinfo(dtrain, "label") - err <- sqrt(mean((preds-labels)^2)) - return(list(metric = "MSE", value = err)) -} - -dtest <- xgb.DMatrix(test$data, label = test$label, nthread = 2) -watchlist <- list(eval = dtest, train = dtrain) -param <- list(max_depth = 2, eta = 1, nthread = 2) - -bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, logregobj, evalerror, maximize = FALSE) -@ - -The gradient and second order gradient is required for the output of customized -objective function. - -We also have \verb@slice@ for row extraction. It is useful in -cross-validation. - -For a walkthrough demo, please see \verb@R-package/demo/@ for further -details. - -\section{The Higgs Boson competition} - -We have made a demo for \href{http://www.kaggle.com/c/higgs-boson}{the Higgs -Boson Machine Learning Challenge}. - -Here are the instructions to make a submission -\begin{enumerate} - \item Download the \href{http://www.kaggle.com/c/higgs-boson/data}{datasets} - and extract them to \verb@data/@. - \item Run scripts under \verb@xgboost/demo/kaggle-higgs/@: - \href{https://github.com/tqchen/xgboost/blob/master/demo/kaggle-higgs/higgs-train.R}{higgs-train.R} - and \href{https://github.com/tqchen/xgboost/blob/master/demo/kaggle-higgs/higgs-pred.R}{higgs-pred.R}. - The computation will take less than a minute on Intel i7. - \item Go to the \href{http://www.kaggle.com/c/higgs-boson/submissions/attach}{submission page} - and submit your result. -\end{enumerate} - -We provide \href{https://github.com/tqchen/xgboost/blob/master/demo/kaggle-higgs/speedtest.R}{a script} -to compare the time cost on the higgs dataset with \verb@gbm@ and \verb@xgboost@. -The training set contains 350000 records and 30 features. - -\verb@xgboost@ can automatically do parallel computation. On a machine with Intel -i7-4700MQ and 24GB memories, we found that \verb@xgboost@ costs about 35 seconds, which is about 20 times faster -than \verb@gbm@. When we limited \verb@xgboost@ to use only one thread, it was -still about two times faster than \verb@gbm@. - -Meanwhile, the result from \verb@xgboost@ reaches -\href{http://www.kaggle.com/c/higgs-boson/details/evaluation}{3.60@AMS} with a -single model. This results stands in the -\href{http://www.kaggle.com/c/higgs-boson/leaderboard}{top 30\%} of the -competition. - -\bibliographystyle{jss} -\nocite{*} % list uncited references -\bibliography{xgboost} - -\end{document} - -<>= -file.remove("xgb.DMatrix") -file.remove("model.dump") -file.remove("model.save") -@ diff --git a/R-package/vignettes/xgboostPresentation.Rmd b/R-package/vignettes/xgboostPresentation.Rmd index 90393060f..efafc624d 100644 --- a/R-package/vignettes/xgboostPresentation.Rmd +++ b/R-package/vignettes/xgboostPresentation.Rmd @@ -107,7 +107,7 @@ train <- agaricus.train test <- agaricus.test ``` -> In the real world, it would be up to you to make this division between `train` and `test` data. The way to do it is out of the purpose of this article, however `caret` package may [help](http://topepo.github.io/caret/data-splitting.html). +> In the real world, it would be up to you to make this division between `train` and `test` data. Each variable is a `list` containing two things, `label` and `data`: @@ -155,11 +155,13 @@ We will train decision tree model using the following parameters: bstSparse <- xgboost( data = train$data , label = train$label - , max_depth = 2 - , eta = 1 - , nthread = 2 + , params = list( + max_depth = 2 + , eta = 1 + , nthread = 2 + , objective = "binary:logistic" + ) , nrounds = 2 - , objective = "binary:logistic" ) ``` @@ -175,11 +177,13 @@ Alternatively, you can put your dataset in a *dense* matrix, i.e. a basic **R** bstDense <- xgboost( data = as.matrix(train$data), label = train$label, - max_depth = 2, - eta = 1, - nthread = 2, - nrounds = 2, - objective = "binary:logistic" + params = list( + max_depth = 2, + eta = 1, + nthread = 2, + objective = "binary:logistic" + ), + nrounds = 2 ) ``` @@ -191,11 +195,13 @@ bstDense <- xgboost( dtrain <- xgb.DMatrix(data = train$data, label = train$label, nthread = 2) bstDMatrix <- xgboost( data = dtrain, - max_depth = 2, - eta = 1, - nthread = 2, - nrounds = 2, - objective = "binary:logistic" + params = list( + max_depth = 2, + eta = 1, + nthread = 2, + objective = "binary:logistic" + ), + nrounds = 2 ) ``` @@ -209,11 +215,13 @@ One of the simplest way to see the training progress is to set the `verbose` opt # verbose = 0, no message bst <- xgboost( data = dtrain - , max_depth = 2 - , eta = 1 - , nthread = 2 + , params = list( + max_depth = 2 + , eta = 1 + , nthread = 2 + , objective = "binary:logistic" + ) , nrounds = 2 - , objective = "binary:logistic" , verbose = 0 ) ``` @@ -222,11 +230,13 @@ bst <- xgboost( # verbose = 1, print evaluation metric bst <- xgboost( data = dtrain - , max_depth = 2 - , eta = 1 - , nthread = 2 + , params = list( + max_depth = 2 + , eta = 1 + , nthread = 2 + , objective = "binary:logistic" + ) , nrounds = 2 - , objective = "binary:logistic" , verbose = 1 ) ``` @@ -235,11 +245,13 @@ bst <- xgboost( # verbose = 2, also print information about tree bst <- xgboost( data = dtrain - , max_depth = 2 - , eta = 1 - , nthread = 2 + , params = list( + max_depth = 2 + , eta = 1 + , nthread = 2 + , objective = "binary:logistic" + ) , nrounds = 2 - , objective = "binary:logistic" , verbose = 2 ) ``` @@ -336,12 +348,14 @@ watchlist <- list(train = dtrain, test = dtest) bst <- xgb.train( data = dtrain - , max_depth = 2 - , eta = 1 - , nthread = 2 + , params = list( + max_depth = 2 + , eta = 1 + , nthread = 2 + , objective = "binary:logistic" + ) , nrounds = 2 , watchlist = watchlist - , objective = "binary:logistic" ) ``` @@ -349,7 +363,7 @@ bst <- xgb.train( Both training and test error related metrics are very similar, and in some way, it makes sense: what we have learned from the training dataset matches the observations from the test dataset. -If with your own dataset you have not such results, you should think about how you divided your dataset in training and test. May be there is something to fix. Again, `caret` package may [help](http://topepo.github.io/caret/data-splitting.html). +If with your own dataset you have not such results, you should think about how you divided your dataset in training and test. May be there is something to fix. For a better understanding of the learning progression, you may want to have some specific metric or even use multiple evaluation metrics. @@ -357,13 +371,15 @@ For a better understanding of the learning progression, you may want to have som bst <- xgb.train( data = dtrain , max_depth = 2 - , eta = 1 - , nthread = 2 + , params = list( + eta = 1 + , nthread = 2 + , objective = "binary:logistic" + , eval_metric = "error" + , eval_metric = "logloss" + ) , nrounds = 2 , watchlist = watchlist - , eval_metric = "error" - , eval_metric = "logloss" - , objective = "binary:logistic" ) ``` @@ -377,14 +393,15 @@ Until now, all the learnings we have performed were based on boosting trees. **X ```{r linearBoosting, message=F, warning=F} bst <- xgb.train( data = dtrain - , booster = "gblinear" - , max_depth = 2 - , nthread = 2 + , params = list( + booster = "gblinear" + , nthread = 2 + , objective = "binary:logistic" + , eval_metric = "error" + , eval_metric = "logloss" + ) , nrounds = 2 , watchlist = watchlist - , eval_metric = "error" - , eval_metric = "logloss" - , objective = "binary:logistic" ) ``` @@ -406,12 +423,14 @@ xgb.DMatrix.save(dtrain, fname) dtrain2 <- xgb.DMatrix(fname) bst <- xgb.train( data = dtrain2 - , max_depth = 2 - , eta = 1 - , nthread = 2 + , params = list( + max_depth = 2 + , eta = 1 + , nthread = 2 + , objective = "binary:logistic" + ) , nrounds = 2 , watchlist = watchlist - , objective = "binary:logistic" ) ``` @@ -492,17 +511,17 @@ file.remove(fname) > result is `0`? We are good! -In some very specific cases, like when you want to pilot **XGBoost** from `caret` package, you will want to save the model as a *R* binary vector. See below how to do it. +In some very specific cases, you will want to save the model as a *R* binary vector. See below how to do it. ```{r saveLoadRBinVectorModel, message=F, warning=F} # save model to R's raw vector -rawVec <- xgb.serialize(bst) +rawVec <- xgb.save.raw(bst) # print class print(class(rawVec)) # load binary model to R -bst3 <- xgb.load(rawVec) +bst3 <- xgb.load.raw(rawVec) xgb.parameters(bst3) <- list(nthread = 2) pred3 <- predict(bst3, test$data) diff --git a/R-package/vignettes/xgboostfromJSON.Rmd b/R-package/vignettes/xgboostfromJSON.Rmd index f5bc3ad9b..e5331b0ff 100644 --- a/R-package/vignettes/xgboostfromJSON.Rmd +++ b/R-package/vignettes/xgboostfromJSON.Rmd @@ -53,11 +53,10 @@ labels <- c(1, 1, 1, data <- data.frame(dates = dates, labels = labels) bst <- xgb.train( - data = xgb.DMatrix(as.matrix(data$dates), label = labels), + data = xgb.DMatrix(as.matrix(data$dates), label = labels, missing = NA), nthread = 2, nrounds = 1, objective = "binary:logistic", - missing = NA, max_depth = 1 ) ``` From 73b3955dd48c85e41a701c8a9869d27a4d5b1101 Mon Sep 17 00:00:00 2001 From: rpopescu Date: Thu, 11 Jan 2024 05:10:21 +0100 Subject: [PATCH 59/59] Fix FieldEntry ctor specialisation syntax error (#9980) Co-authored-by: Radu Popescu --- include/xgboost/parameter.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xgboost/parameter.h b/include/xgboost/parameter.h index fca05cef4..063f1aca7 100644 --- a/include/xgboost/parameter.h +++ b/include/xgboost/parameter.h @@ -53,7 +53,7 @@ namespace parameter { \ template <> \ class FieldEntry : public FieldEntry { \ public: \ - FieldEntry() { \ + FieldEntry() { \ static_assert( \ std::is_same::type>::value, \ "enum class must be backed by int"); \