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..7c01d50c6 100644 --- a/R-package/DESCRIPTION +++ b/R-package/DESCRIPTION @@ -58,12 +58,13 @@ Suggests: float, titanic Depends: - R (>= 3.3.0) + R (>= 4.3.0) 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/NAMESPACE b/R-package/NAMESPACE index bbb5ee225..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,21 +29,27 @@ 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) 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.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) @@ -52,13 +61,12 @@ 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) +importClassesFrom(Matrix,dgRMatrix) importClassesFrom(Matrix,dgeMatrix) importFrom(Matrix,colSums) importFrom(Matrix,sparse.model.matrix) @@ -82,8 +90,11 @@ importFrom(graphics,points) 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 54f821a79..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, @@ -770,7 +781,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/utils.R b/R-package/R/utils.R index 2fc2321ca..945d86132 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) } @@ -140,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, @@ -160,23 +166,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, handle, dtrain, iter, grad, hess ) } return(TRUE) @@ -186,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) @@ -343,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 @@ -365,26 +400,29 @@ 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}. #' #' @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') -#' 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) @@ -395,12 +433,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 37cfc199e..cee7e9fc5 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -1,272 +1,174 @@ -# 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 + return(handle) } -#' 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 -#' 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 -#' 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, -#' 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. -#' -#' @return -#' An object of \code{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") -#' saveRDS(bst, "xgb.model.rds") -#' -#' # 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") -#' # 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) -} - -#' 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`. +#' @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 ... Parameters passed to \code{predict.xgb.Booster} +#' @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 +177,15 @@ 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 <- xgb.train( +#' data = xgb.DMatrix(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,54 +217,65 @@ 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 <- xgb.train( +#' data = xgb.DMatrix(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 <- xgb.train( +#' data = xgb.DMatrix(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 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)) { @@ -368,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. @@ -415,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 @@ -486,74 +410,69 @@ 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 \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`. \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 #' 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. -#' 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. -#' -#' 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. +#' 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). #' -#' \code{xgb.attributes} returns a list of all attribute stored in a model -#' or \code{NULL} if a model has no stored attributes. +#' @return +#' - `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') +#' fname <- file.path(tempdir(), "xgb.ubj") +#' xgb.save(bst, fname) +#' bst1 <- xgb.load(fname) #' print(xgb.attr(bst1, "my_attribute")) #' print(xgb.attributes(bst1)) #' @@ -568,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? @@ -585,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 @@ -597,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 @@ -612,79 +537,88 @@ 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 \code{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') +#' 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 #' @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 \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 +#' 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 \code{xgb.Booster.handle} -#' than for \code{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') +#' 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) #' @@ -697,115 +631,363 @@ 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]]) } - 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. +#' } #' -#' @param x an xgb.Booster object -#' @param verbose whether to print detailed data (e.g., attribute values) -#' @param ... not currently used +#' 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 -#' data(agaricus.train, package='xgboost') +#' 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 ... Not used. +#' @return The same `x` object, returned invisibly +#' @examples +#' 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) #' -#' @method print xgb.Booster #' @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 8e19e87b0..7c4c30bd3 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -8,13 +8,45 @@ #' 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. +#' +#' 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. +#' @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 ... 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. +#' @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. #' #' @examples #' data(agaricus.train, package='xgboost') @@ -24,21 +56,43 @@ #' 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, 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, + 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))) - cnames <- colnames(data) + handle <- .Call( + XGDMatrixCreateFromMat_R, data, missing, as.integer(NVL(nthread, -1)) + ) } else if (inherits(data, "dgCMatrix")) { handle <- .Call( XGDMatrixCreateFromCSC_R, @@ -49,7 +103,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, @@ -60,7 +113,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 @@ -73,23 +125,112 @@ xgb.DMatrix <- function(data, info = list(), missing = NA, silent = FALSE, nthre 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)) } + dmat <- handle - attributes(dmat) <- list(class = "xgb.DMatrix") - if (!is.null(cnames)) { - setinfo(dmat, "feature_name", cnames) + attributes(dmat) <- list( + class = "xgb.DMatrix", + fields = new.env() + ) + + 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) + } + if (!is.null(ctypes)) { + setinfo(dmat, "feature_type", ctypes) } - info <- append(info, list(...)) - for (i in seq_along(info)) { - p <- info[i] - setinfo(dmat, names(p), p[[1]]) - } 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 @@ -194,26 +335,38 @@ 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) -#' @param ... other parameters -#' +#' @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}: 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. #' +#' For `xgb.Booster`, can be one of the following: +#' \itemize{ +#' \item \code{feature_type} +#' \item \code{feature_name} #' } #' -#' \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. #' +#' \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)) @@ -225,49 +378,60 @@ 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', + '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") { - ret <- .Call(XGDMatrixGetStrFeatureInfo_R, object, name) - } else if (name != "nrow") { - ret <- .Call(XGDMatrixGetInfo_R, object, name) - } else { + if (name == "nrow") { ret <- nrow(object) + } else if (name %in% allowed_str_fields) { + ret <- .Call(XGDMatrixGetStrFeatureInfo_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) + } } if (length(ret) == 0) return(NULL) 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 -#' @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 @@ -278,52 +442,61 @@ 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, ...) UseMethod("setinfo") +setinfo <- function(object, name, info) UseMethod("setinfo") -#' @rdname setinfo +#' @rdname getinfo #' @export -setinfo.xgb.DMatrix <- function(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) { 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") { - 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") { - # 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") { 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, 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) } @@ -353,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 @@ -363,7 +641,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') @@ -377,11 +654,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") } @@ -431,11 +708,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.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.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.cv.R b/R-package/R/xgb.cv.R index 9e1ffeddc..a960957ca 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')) { @@ -201,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 @@ -228,21 +231,22 @@ 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 ) }) 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() @@ -263,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 a2de26c26..3a3d2c7dc 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")) @@ -49,9 +56,16 @@ 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))) + } if (is.null(fname)) model_dump <- gsub('\t', '', model_dump, fixed = TRUE) 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..44f2eb9b3 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 @@ -87,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) { @@ -122,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 cbdbdacc3..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') @@ -29,40 +29,37 @@ #' #' 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" #' ) #' -#' 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)) 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 d69169b89..df0e672a9 100644 --- a/R-package/R/xgb.model.dt.tree.R +++ b/R-package/R/xgb.model.dt.tree.R @@ -1,67 +1,72 @@ -#' 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 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`. +#' @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. +#' - `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. #' -#' 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") -#' -#' (dt <- xgb.model.dt.tree(colnames(agaricus.train$data), bst)) +#' bst <- xgboost( +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' max_depth = 2, +#' eta = 1, +#' nthread = nthread, +#' nrounds = 2, +#' objective = "binary:logistic" +#' ) #' #' # 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)) #' #' # 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, @@ -74,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") @@ -85,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))) { @@ -115,9 +129,29 @@ 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_cols <- c("Feature", "Split", "Yes", "No", "Missing", "Quality", "Cover") + 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, (branch_cols) := { @@ -127,7 +161,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) @@ -139,15 +173,17 @@ 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 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) := { @@ -162,7 +198,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 f0fe0f134..8e1972374 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 @@ -83,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] @@ -148,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.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..88616cfb7 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 @@ -98,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.shap.R b/R-package/R/xgb.plot.shap.R index d9afd5546..788a09539 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 https://github.com/shap/shap. +#' 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) { @@ -241,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)) { @@ -270,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.plot.tree.R b/R-package/R/xgb.plot.tree.R index 956c13cf7..c75a42e84 100644 --- a/R-package/R/xgb.plot.tree.R +++ b/R-package/R/xgb.plot.tree.R @@ -1,74 +1,110 @@ -#' 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 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 organised that way: +#' 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. #' -#' \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 +#' 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. +#' 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" +#' ) +#' +#' # 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") #' -#' 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) +#' 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) #' #' \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 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") @@ -78,9 +114,25 @@ 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)] + 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)] @@ -147,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/R/xgb.save.R b/R-package/R/xgb.save.R index ab55bc4a9..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') @@ -32,15 +44,17 @@ #' #' 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" #' ) -#' 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") @@ -49,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 cad0fb0e0..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') @@ -23,14 +21,14 @@ #' #' 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) #' #' @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 00bbb4293..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 <- xgboost(data = 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 d93a0643d..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}}, @@ -251,9 +250,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 +271,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 +286,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: @@ -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 e60ea2de8..170aa5ffd 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 @@ -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,9 +98,12 @@ NULL #' @importFrom data.table setnames #' @importFrom jsonlite fromJSON #' @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/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/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/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 85b52243c..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,26 +52,29 @@ 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}. } \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') -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) @@ -54,11 +85,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/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..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 63222f341..7cc0d6ecb 100644 --- a/R-package/man/getinfo.Rd +++ b/R-package/man/getinfo.Rd @@ -1,36 +1,78 @@ % 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{ -getinfo(object, ...) +\method{getinfo}{xgb.Booster}(object, name) -\method{getinfo}{xgb.DMatrix}(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{...}{other parameters} +\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}: 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. +For \code{xgb.Booster}, can be one of the following: +\itemize{ +\item \code{feature_type} +\item \code{feature_name} } -\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. + +\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') @@ -41,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 ee3b370c4..66194c64f 100644 --- a/R-package/man/predict.xgb.Booster.Rd +++ b/R-package/man/predict.xgb.Booster.Rd @@ -2,8 +2,7 @@ % 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 eXtreme Gradient Boosting model} +\title{Predict method for XGBoost model} \usage{ \method{predict}{xgb.Booster}( object, @@ -21,94 +20,88 @@ 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}. +\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{...}{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, -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 +117,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 +133,15 @@ 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 <- xgb.train( + data = xgb.DMatrix(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 +173,61 @@ 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 <- xgb.train( + data = xgb.DMatrix(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 <- xgb.train( + data = xgb.DMatrix(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..9a783efaf 100644 --- a/R-package/man/print.xgb.Booster.Rd +++ b/R-package/man/print.xgb.Booster.Rd @@ -4,26 +4,35 @@ \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 xgb.Booster object} +\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 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) } diff --git a/R-package/man/setinfo.Rd b/R-package/man/setinfo.Rd deleted file mode 100644 index c29fcfa7e..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, ...) - -\method{setinfo}{xgb.DMatrix}(object, name, info, ...) -} -\arguments{ -\item{object}{Object of class "xgb.DMatrix"} - -\item{...}{other parameters} - -\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{ -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{group}: number of rows in each group (to use with \code{rank:pairwise} objective). -} -} -\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/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)} 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 214694565..000000000 --- a/R-package/man/xgb.Booster.complete.Rd +++ /dev/null @@ -1,52 +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") -saveRDS(bst, "xgb.model.rds") - -# 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") -# 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.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index 38a65c638..eb667377f 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -6,11 +6,19 @@ \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, + enable_categorical = FALSE ) } \arguments{ @@ -19,23 +27,65 @@ 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. + +\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.} \item{silent}{whether to suppress printing an informational message after loading from a file.} +\item{feature_names}{Set names for features. Overrides column names in data +frame and matrix.} + \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.} + +\item{enable_categorical}{Experimental support of specializing for categorical features. + +\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 'data' is not a data frame, this argument is ignored. + + 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. 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 @@ -44,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.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} +} 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 03779e420..8038a2048 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) @@ -16,64 +16,71 @@ 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.} +\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. - -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. + +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') +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') +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.config.Rd b/R-package/man/xgb.config.Rd index 35545cc77..1ab810644 100644 --- a/R-package/man/xgb.config.Rd +++ b/R-package/man/xgb.config.Rd @@ -3,31 +3,48 @@ \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}. \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. +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') +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.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.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.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.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.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/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.importance.Rd b/R-package/man/xgb.importance.Rd index d9367b211..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.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 1a406cc21..1a6873171 100644 --- a/R-package/man/xgb.load.Rd +++ b/R-package/man/xgb.load.Rd @@ -34,17 +34,19 @@ 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" ) -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}}. +\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 5a17f9d90..e63bd4b10 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,48 @@ 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 of feature names. If the model already +contains feature names, those will be used when \code{feature_names=NULL} (default value). -\item{model}{object of class \code{xgb.Booster}} +\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{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{model}{Object of class \code{xgb.Booster}.} -\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{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{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{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{...}{currently not used.} +\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.} } \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 - 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, 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{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. } -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,22 +64,31 @@ 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") - -(dt <- xgb.model.dt.tree(colnames(agaricus.train$data), bst)) +bst <- xgboost( + data = agaricus.train$data, + label = agaricus.train$label, + max_depth = 2, + eta = 1, + nthread = nthread, + nrounds = 2, + objective = "binary:logistic" +) # 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)) # 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.parameters.Rd b/R-package/man/xgb.parameters.Rd index ab2695650..8d5044cab 100644 --- a/R-package/man/xgb.parameters.Rd +++ b/R-package/man/xgb.parameters.Rd @@ -2,29 +2,46 @@ % 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{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 +\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') +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..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 6f2d0dfa6..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 [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 3ff8af21c..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 [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 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 d419eb76a..a09bb7183 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, @@ -12,80 +12,124 @@ xgb.plot.tree( plot_height = NULL, render = TRUE, show_node_id = FALSE, + style = c("R", "xgboost"), ... ) } \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{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{ -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: - +When using \code{style="xgboost"}, the content of each node is visualized as follows: \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 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. } -The tree root nodes also indicate the Tree index (0-based). + +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. +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 \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 "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" +) + +# 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") -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) +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) \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.save.Rd b/R-package/man/xgb.save.Rd index a7e160a12..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. @@ -38,16 +50,18 @@ 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" ) -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}}. +\code{\link{xgb.load}} } diff --git a/R-package/man/xgb.save.raw.Rd b/R-package/man/xgb.save.raw.Rd index c7c93a734..15400bb14 100644 --- a/R-package/man/xgb.save.raw.Rd +++ b/R-package/man/xgb.save.raw.Rd @@ -5,19 +5,17 @@ \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.} \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. -} - -Right now the default is \code{deprecated} but will be changed to \code{ubj} in upcoming release.} +\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. +}} } \description{ Save xgboost model from xgboost or xgb.train @@ -32,8 +30,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 deleted file mode 100644 index 952441d98..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 <- xgboost(data = 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.shap.data.Rd b/R-package/man/xgb.shap.data.Rd deleted file mode 100644 index 2f0e4adea..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 [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} diff --git a/R-package/man/xgb.train.Rd b/R-package/man/xgb.train.Rd index 105009cf8..0421b9c4a 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. @@ -202,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}.} @@ -216,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. @@ -258,30 +246,45 @@ 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. } + +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') @@ -300,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: @@ -321,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: @@ -336,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: diff --git a/R-package/man/xgb.unserialize.Rd b/R-package/man/xgb.unserialize.Rd deleted file mode 100644 index d191d77d4..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 -`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/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 5c8e179d6..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); @@ -39,10 +46,15 @@ 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 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); @@ -52,9 +64,15 @@ extern SEXP XGBGetGlobalConfig_R(void); extern SEXP XGBoosterFeatureScore_R(SEXP, SEXP); static const R_CallMethodDef CallEntries[] = { - {"XGBoosterBoostOneIter_R", (DL_FUNC) &XGBoosterTrainOneIter_R, 5}, + {"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}, @@ -76,10 +94,15 @@ 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}, + {"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}, + {"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}, @@ -96,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 8da00aa58..63f36ad6a 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 @@ -8,9 +8,12 @@ #include #include +#include +#include #include #include #include +#include #include #include #include @@ -19,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(); } @@ -48,8 +51,35 @@ 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) { + 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. @@ -132,45 +162,116 @@ 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_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_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(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 (use_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 { + 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[512]; /*! * \brief macro to annotate end of api */ #define R_API_END() \ - } catch(dmlc::Error& e) { \ - PutRNGstate(); \ - error(e.what()); \ + } catch(std::exception &e) { \ + std::strncpy(cpp_ex_msg, e.what(), 512); \ + goto throw_cpp_ex_as_R_err; \ } \ - PutRNGstate(); + if (false) { \ + throw_cpp_ex_as_R_err: \ + Rf_error("%s", cpp_ex_msg); \ + } -/*! - * \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; 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(); @@ -219,6 +320,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) { @@ -294,6 +458,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(); @@ -342,9 +507,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()); } @@ -362,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)); @@ -407,17 +580,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; @@ -439,15 +622,210 @@ 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()); + 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; - 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; @@ -461,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)); @@ -503,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; } @@ -513,20 +962,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/src/xgboost_R.h b/R-package/src/xgboost_R.h index 7f0833b15..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 @@ -53,6 +55,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 @@ -106,12 +118,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 @@ -125,19 +145,87 @@ 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 + * \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 */ 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/src/xgboost_custom.cc b/R-package/src/xgboost_custom.cc index 92f8a8e1f..4b05361ca 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/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 b7e819738..8dd934765 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -16,18 +16,19 @@ 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" ) 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) @@ -35,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)) @@ -56,7 +57,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( @@ -82,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) } @@ -104,9 +106,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, @@ -124,7 +125,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", @@ -151,16 +152,17 @@ 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" ) - 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: @@ -170,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) @@ -186,7 +188,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, @@ -201,39 +203,41 @@ 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" ) - 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", { 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.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) @@ -248,50 +252,53 @@ 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.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", { 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" ) - 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 <- 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" ) - 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")) }) @@ -311,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 - 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(xgb.save.raw(bst), xgb.save.raw(bst2)) } - expect_equal(dim(bst2$evaluation_log), c(2, 2)) - file.remove("xgboost.json") -}) - -test_that("model serialization works", { - out_path <- "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", { @@ -361,7 +352,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 +382,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 +393,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 +407,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 ), @@ -448,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", { @@ -480,8 +471,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 +512,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 +527,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 +546,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) @@ -565,3 +556,234 @@ test_that("'predict' accepts CSR data", { expect_equal(p_csc, p_csr) 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 + 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) +}) + +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) +}) + +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 b5d3c5310..afa270c0b 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", { @@ -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,38 +166,39 @@ 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) }) 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') + 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, - 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) + expect_equal(xgb.save.raw(bst), xgb.save.raw(b2)) for (f in files) if (file.exists(f)) file.remove(f) }) @@ -208,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) @@ -223,16 +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) - 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") + 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) }) @@ -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_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_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index a0cf90088..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,6 +298,143 @@ 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", { + 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)) +}) + +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) +}) + +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")) +}) + +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) +}) + +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/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_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 de6a099fc..372f2520c 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,14 +41,17 @@ 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) +# 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() @@ -71,8 +74,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 +172,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 +181,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(...) { @@ -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: - 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.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) || @@ -256,27 +260,23 @@ 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') - 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)) + + fname_bin <- file.path(tempdir(), "xgb.model") + xgb.save(bst, fname_bin) + bst <- readRDS(fname_rds) 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", "Quality", "Cover") - dt.tree <- xgb.model.dt.tree(feature_names = feature.names, model = bst.Tree) + names.dt.trees <- c("Tree", "Node", "ID", "Feature", "Split", "Yes", "No", "Missing", "Gain", "Cover") + 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,14 +330,14 @@ 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, trees = trees )[ Feature != "Leaf", .( - Gain = sum(Quality), + Gain = sum(Gain), Cover = sum(Cover), Frequency = .N ), @@ -360,9 +356,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 +388,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 @@ -415,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_interaction_constraints.R b/R-package/tests/testthat/test_interaction_constraints.R index 1ac804501..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 @@ -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( 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..36a6d7572 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 ) @@ -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_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..ed5c28ca5 100644 --- a/R-package/tests/testthat/test_parameter_exposure.R +++ b/R-package/tests/testthat/test_parameter_exposure.R @@ -10,21 +10,21 @@ 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)) - 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_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_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_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/tests/testthat/test_update.R b/R-package/tests/testthat/test_update.R index cf8b6f007..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,19 +56,33 @@ 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']$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: + 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(tr2[Feature == 'Leaf']$Quality, tr2u[Feature == 'Leaf']$Quality) - expect_gt(sum(abs(tr2[Feature != 'Leaf']$Quality - tr2u[Feature != 'Leaf']$Quality)), 100) + 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,9 +105,9 @@ 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(tr1[Feature == 'Leaf']$Quality, tr1ut[Feature == 'Leaf']$Quality) - expect_gt(sum(abs(tr1[Feature != 'Leaf']$Quality - tr1ut[Feature != 'Leaf']$Quality)), 100) + 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,12 +125,13 @@ 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(tr0[Feature == 'Leaf']$Quality, tr0u[Feature == 'Leaf']$Quality) - expect_gt(sum(abs(tr0[Feature != 'Leaf']$Quality - tr0u[Feature != 'Leaf']$Quality)), 100) + 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 1b015fab8..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" ) ``` @@ -400,22 +417,25 @@ 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 - , eta = 1 - , nthread = 2 + , params = list( + max_depth = 2 + , eta = 1 + , nthread = 2 + , objective = "binary:logistic" + ) , nrounds = 2 , watchlist = watchlist - , objective = "binary:logistic" ) ``` ```{r DMatrixDel, include=FALSE} -file.remove("dtrain.buffer") +file.remove(fname) ``` #### Information extraction @@ -466,7 +486,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 +496,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,22 +506,22 @@ 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! -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 e7ccdf3a9..e5331b0ff 100644 --- a/R-package/vignettes/xgboostfromJSON.Rmd +++ b/R-package/vignettes/xgboostfromJSON.Rmd @@ -52,13 +52,11 @@ 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, missing = NA), nthread = 2, nrounds = 1, objective = "binary:logistic", - missing = NA, max_depth = 1 ) ``` 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/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/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/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/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. """ 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/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 diff --git a/doc/parameter.rst b/doc/parameter.rst index d2471dfd9..a7d8203b0 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,8 +388,11 @@ 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. + 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/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/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/doc/tutorials/custom_metric_obj.rst b/doc/tutorials/custom_metric_obj.rst index f5c08bf59..118a099c1 100644 --- a/doc/tutorials/custom_metric_obj.rst +++ b/doc/tutorials/custom_metric_obj.rst @@ -271,14 +271,14 @@ 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. ********************** 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/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..3f6775611 --- /dev/null +++ b/doc/tutorials/intercept.rst @@ -0,0 +1,105 @@ +######### +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_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 +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 +(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 +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/doc/tutorials/saving_model.rst b/doc/tutorials/saving_model.rst index 54c217249..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 @@ -101,6 +111,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/include/xgboost/c_api.h b/include/xgboost/c_api.h index 59d4d0881..795c78946 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 XGProxyDMatrixSetDataColumnar(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/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/include/xgboost/linalg.h b/include/xgboost/linalg.h index fca46d760..41a43ac84 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/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/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"); \ diff --git a/jvm-packages/pom.xml b/jvm-packages/pom.xml index a92fa2179..08bfd841c 100644 --- a/jvm-packages/pom.xml +++ b/jvm-packages/pom.xml @@ -49,6 +49,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 + @@ -338,6 +354,9 @@ org.scalatest scalatest-maven-plugin 2.2.0 + + -ea -Xmx4g -Xss4m ${extraJavaTestArgs} + test 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/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-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/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; 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/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/federated/CMakeLists.txt b/plugin/federated/CMakeLists.txt index 4b9734c4e..82b2a04b8 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}") 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/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/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); + } } } 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/compat.py b/python-package/xgboost/compat.py index c40dea5fd..729750f1f 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 @@ -128,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 bfc94aa04..36be766b1 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 @@ -213,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 @@ -358,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__ @@ -380,34 +382,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 +440,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 +449,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 @@ -795,7 +788,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. @@ -832,9 +825,19 @@ 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 - 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, 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. + + 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: @@ -1441,6 +1444,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 @@ -2096,7 +2105,7 @@ class Booster: _array_interface, _cuda_array_interface, _ensure_np_dtype, - _is_cupy_array, + _is_cupy_alike, ) self._assign_dmatrix_features(dtrain) @@ -2110,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() @@ -2125,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) @@ -2450,11 +2459,12 @@ class Booster: assert proxy is None or isinstance(proxy, _ProxyDMatrix) from .data import ( + PandasTransformed, _array_interface, _arrow_transform, _is_arrow, _is_cudf_df, - _is_cupy_array, + _is_cupy_alike, _is_list, _is_np_array_like, _is_pandas_df, @@ -2504,6 +2514,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 @@ -2523,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) @@ -2571,9 +2594,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 @@ -2593,15 +2615,18 @@ 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. + 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 ------- @@ -2620,11 +2645,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 @@ -2749,9 +2773,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/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 49287d817..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, @@ -57,21 +58,36 @@ 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(data: np.ndarray) -> bytes: - assert ( - data.dtype.hasobject is False - ), "Input data contains `object` dtype. Expecting numeric data." +def _array_interface_dict(data: np.ndarray) -> dict: + 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__ + 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 @@ -130,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( @@ -166,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: @@ -181,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: @@ -265,24 +303,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 +333,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 +445,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 +569,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 +639,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 +668,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 +855,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 +984,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), ) ) @@ -874,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: @@ -886,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 @@ -1047,15 +1168,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, @@ -1098,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.") @@ -1221,8 +1342,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) @@ -1231,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): @@ -1244,8 +1364,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") @@ -1297,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): @@ -1307,9 +1426,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 @@ -1317,11 +1442,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))) @@ -1343,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): @@ -1356,11 +1480,14 @@ 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 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/python-package/xgboost/sklearn.py b/python-package/xgboost/sklearn.py index 748b26cf6..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 @@ -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 @@ -276,13 +281,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] @@ -334,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:: @@ -1012,7 +1011,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. @@ -1152,7 +1151,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, @@ -1178,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. @@ -1459,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) @@ -1605,7 +1604,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, @@ -1948,7 +1947,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. diff --git a/python-package/xgboost/testing/__init__.py b/python-package/xgboost/testing/__init__.py index 6b8daf561..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( @@ -815,6 +814,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/python-package/xgboost/testing/data.py b/python-package/xgboost/testing/data.py index b5f07d981..0c4f29008 100644 --- a/python-package/xgboost/testing/data.py +++ b/python-package/xgboost/testing/data.py @@ -3,7 +3,18 @@ import os import zipfile from dataclasses import dataclass -from typing import Any, Generator, List, NamedTuple, Optional, Tuple, Union +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Generator, + NamedTuple, + Optional, + Tuple, + Type, + Union, +) from urllib import request import numpy as np @@ -15,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) @@ -246,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 @@ -603,3 +759,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/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/c_api/c_api.cc b/src/c_api/c_api.cc index 1ce150ec9..71d7af9db 100644 --- a/src/c_api/c_api.cc +++ b/src/c_api/c_api.cc @@ -364,49 +364,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); @@ -414,7 +422,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(); @@ -432,6 +440,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) { @@ -1199,6 +1226,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, @@ -1268,10 +1316,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 @@ -1294,14 +1340,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/src/common/common.h b/src/common/common.h index 08274b81d..19bb7bc1c 100644 --- a/src/common/common.h +++ b/src/common/common.h @@ -73,8 +73,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/common/device_helpers.cuh b/src/common/device_helpers.cuh index ecba0bd2b..0f5a37210 100644 --- a/src/common/device_helpers.cuh +++ b/src/common/device_helpers.cuh @@ -1099,6 +1099,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}; 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/common/linalg_op.cuh b/src/common/linalg_op.cuh index a05c75ba7..74cee3757 100644 --- a/src/common/linalg_op.cuh +++ b/src/common/linalg_op.cuh @@ -1,32 +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) @@ -44,7 +60,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 325208685..c08a746ae 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/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/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 0a110b29b..7ef2d3871 100644 --- a/src/data/array_interface.h +++ b/src/data/array_interface.h @@ -377,11 +377,6 @@ struct ToDType { static constexpr ArrayInterfaceHandler::Type kType = ArrayInterfaceHandler::kI8; }; -#if !defined(XGBOOST_USE_CUDA) && !defined(XGBOOST_USE_HIP) -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 * diff --git a/src/data/data.cc b/src/data/data.cc index fb087e6a0..0096c9c60 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()); @@ -947,38 +972,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 +1167,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 +1263,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 d865ab14d..097bde62a 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/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/data/proxy_dmatrix.cc b/src/data/proxy_dmatrix.cc index a63d003c0..393b21cc0 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 7f17ecf00..c5e7ab856 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) || defined(XGBOOST_USE_HIP) } + 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/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/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/src/objective/hinge.cu b/src/objective/hinge.cu index ded9a05ed..589b91acc 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) || defined(XGBOOST_USE_HIP) DMLC_REGISTRY_FILE_TAG(hinge_obj_gpu); #endif // defined(XGBOOST_USE_CUDA) || defined(XGBOOST_USE_HIP) -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/multiclass_obj.cu b/src/objective/multiclass_obj.cu index 4813ed5f9..a9266b756 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) || defined(XGBOOST_USE_HIP) -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 7addf957a..b7642e7f5 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/quantile_obj.cu b/src/objective/quantile_obj.cu index b6e3a7d67..444151295 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 0c83db329..20e04ac59 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) || defined(XGBOOST_USE_HIP) #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) || defined(XGBOOST_USE_HIP) -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 { @@ -255,24 +250,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 +630,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/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/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/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/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/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/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/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 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-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 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/ci_build/lint_python.py b/tests/ci_build/lint_python.py index fdd643da0..87d76607f 100644 --- a/tests/ci_build/lint_python.py +++ b/tests/ci_build/lint_python.py @@ -22,18 +22,17 @@ 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", "tests/python/test_shap.py", + "tests/python/test_model_io.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_dask/", + "tests/test_distributed/test_gpu_with_dask/", "tests/test_distributed/test_with_spark/", "tests/test_distributed/test_gpu_with_spark/", # demo @@ -84,14 +83,17 @@ 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/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", # 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/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( diff --git a/tests/cpp/common/test_device_helpers.cu b/tests/cpp/common/test_device_helpers.cu index 49957681b..f62c9d34a 100644 --- a/tests/cpp/common/test_device_helpers.cu +++ b/tests/cpp/common/test_device_helpers.cu @@ -171,7 +171,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); diff --git a/tests/cpp/common/test_linalg.cu b/tests/cpp/common/test_linalg.cu index 87dca05fd..d14260635 100644 --- a/tests/cpp/common/test_linalg.cu +++ b/tests/cpp/common/test_linalg.cu @@ -22,7 +22,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)); @@ -30,7 +30,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 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 8903f9aea..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__) || defined(__HIP_PLATFORM_AMD__) -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__) && !defined(__HIP_PLATFORM_AMD__) -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 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-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_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 f0c80124d..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 @@ -8,38 +7,16 @@ import pytest import xgboost as xgb from xgboost import testing as tm +from xgboost.testing.updater import ResetStrategy 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__) @@ -71,7 +48,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 @@ -99,7 +76,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 = [] @@ -133,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, @@ -212,8 +207,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): @@ -227,8 +221,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, @@ -271,142 +264,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 @@ -439,66 +296,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) - 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) + dot_dump = booster.get_dump(dump_format="dot") + for d in dot_dump: + assert d.find(r"feature \"2\"") != -1 - 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 - 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 + text_dump = booster.get_dump(dump_format="text") + for d in text_dump: + assert d.find(r"feature \"2\"") != -1 def run_slice( self, @@ -642,11 +467,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, @@ -672,17 +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) @@ -691,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_callback.py b/tests/python/test_callback.py index 262c09c99..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) @@ -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( @@ -368,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, @@ -419,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( @@ -436,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) @@ -453,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" @@ -468,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/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_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) diff --git a/tests/python/test_dmatrix.py b/tests/python/test_dmatrix.py index 05a9af3b0..9d123ddb9 100644 --- a/tests/python/test_dmatrix.py +++ b/tests/python/test_dmatrix.py @@ -1,7 +1,7 @@ import csv import os -import sys import tempfile +import warnings import numpy as np import pytest @@ -12,59 +12,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 @@ -72,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) @@ -112,39 +63,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] @@ -345,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) @@ -383,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) @@ -450,8 +368,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), @@ -556,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..df0fff22e --- /dev/null +++ b/tests/python/test_model_io.py @@ -0,0 +1,498 @@ +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 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 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 + + +@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" 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_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 c4c0de032..e7641348d 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 @@ -181,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 = {} @@ -404,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_modin.py b/tests/python/test_with_modin.py index 3f1f9cf97..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 @@ -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 @@ -142,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 6a9ed4a84..e53e7adcc 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 @@ -105,8 +103,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 +200,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 +245,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 +286,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 @@ -320,14 +334,13 @@ 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__) params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", "eval_metric": "error", } @@ -358,7 +371,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", "eval_metric": "auc", } @@ -369,7 +381,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", "eval_metric": ["auc"], } @@ -380,7 +391,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", "eval_metric": ["auc"], } @@ -399,7 +409,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", } cv = xgb.cv( @@ -410,7 +419,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", } cv = xgb.cv( @@ -421,7 +429,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", "eval_metric": ["auc"], } @@ -507,6 +514,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( 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)) diff --git a/tests/python/test_with_sklearn.py b/tests/python/test_with_sklearn.py index 1e49ed053..47f1778d6 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 @@ -681,7 +678,6 @@ def test_split_value_histograms(): params = { "max_depth": 6, "eta": 0.01, - "verbosity": 0, "objective": "binary:logistic", "base_score": 0.5, } @@ -900,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 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 5d9b58ef0..1bc6bbccd 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 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"):