| Title: | Unsupervised Tree Boosting for Learning Probability Distributions |
|---|---|
| Description: | Implements the unsupervised tree boosting method for learning probability distributions introduced by Awaya and Ma (2024). Provides model fitting, density evaluation, simulation, and diagnostic methods. |
| Authors: | Naoki Awaya [aut, cre] |
| Maintainer: | Naoki Awaya <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0 |
| Built: | 2026-10-06 08:20:45 UTC |
| Source: | https://github.com/nawaya040/boostpm-cran |
Implements the unsupervised tree boosting method of Awaya and Ma (2024) for fitting tree-ensemble probability distributions, evaluating fitted densities, and generating samples.
Maintainer: Naoki Awaya [email protected]
Awaya, N. and Ma, L. (2024). Unsupervised Tree Boosting for Learning Probability Distributions. Journal of Machine Learning Research, 25, 1–52.
fit_boostpm(), predict.boostPM_fit(),
simulate.boostPM_fit(), and plot.boostPM_fit().
Fits the unsupervised tree boosting procedure of Awaya and Ma (2024). Marginal trees are fitted first, followed by dependence trees. The fitted ensemble represents a probability distribution on a rectangular support.
fit_boostpm( data, Omega = NULL, max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 8, max_split_depth = 15, min_node_observations = 5, c0 = 0.1, gamma = 0.1, early_stop = NULL, prior_split_prob = 0.9, add_noise = TRUE, progress = c("none", "stage") )fit_boostpm( data, Omega = NULL, max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 8, max_split_depth = 15, min_node_observations = 5, c0 = 0.1, gamma = 0.1, early_stop = NULL, prior_split_prob = 0.9, add_noise = TRUE, progress = c("none", "stage") )
data |
A finite numeric matrix with observations in rows and variables in columns. Missing, infinite, and constant columns are not supported. |
Omega |
Either |
max_marginal_trees |
Non-negative integer giving the maximum number of trees fitted for each marginal distribution. Set to zero to skip marginal fitting. |
max_dependence_trees |
Non-negative integer giving the maximum number of trees fitted to the dependence structure after marginal fitting. Set to zero to skip this stage. |
n_bins |
Integer of at least two. Candidate split fractions are
|
max_split_depth |
Non-negative integer giving the deepest node depth
that remains eligible for splitting. Consequently, terminal leaves may
occur at depth |
min_node_observations |
Positive integer. A node containing fewer than
|
c0 |
Global learning rate, strictly between zero and one. Larger values move each fitted node mass more strongly toward the empirical residual distribution and therefore apply less shrinkage. |
gamma |
Non-negative scale-specific learning-rate exponent. Zero gives
the constant learning rate |
early_stop |
Either |
prior_split_prob |
Numeric value in |
add_noise |
A single logical value. If |
progress |
Character string controlling progress messages. |
An object of class boostPM_fit, represented as a list containing:
Serialized fitted trees used by package methods. This is an internal representation and is not a stable interface for direct editing.
Final residual coordinates as a variables-by-observations numeric matrix. This advanced diagnostic is not intended for direct modification.
A data frame with one row per accepted tree and
columns tree_index, stage, variable, node_count, and max_depth.
stage is "marginal" or "dependence"; variable identifies the
marginal variable and is NA for dependence trees.
Unnormalized accumulated splitting improvement
attributed to each variable. Larger values indicate greater contribution
within the fitted ensemble. The vector is named when data has column
names.
A data frame with one row per held-out
candidate and columns candidate_index, stage, variable,
mean_log_density_improvement, and accepted, or NULL when
early_stop = NULL. The candidate that triggers stopping has
accepted = FALSE.
A variables-by-two matrix containing named lower and
upper original-scale support limits. Row names follow the training
variable names when available.
Elapsed fitting time as a difftime object.
The matched call used to fit the model.
A named list of the validated controls actually used:
max_marginal_trees, max_dependence_trees, n_bins,
max_split_depth, min_node_observations, c0, gamma, early_stop,
prior_split_prob, add_noise, and progress.
The procedure first fits up to max_marginal_trees univariate trees for
each variable. It then fits up to max_dependence_trees multivariate trees
to the remaining dependence structure. These arguments are upper bounds;
adaptive stopping can end a stage earlier.
For a node in the normalized unit cube, the learning rate is
Thus c0 determines the global update size. When gamma = 0, every node
uses c0. When gamma > 0, smaller-volume nodes receive smaller updates,
which imposes stronger shrinkage on local structure.
If early_stop = NULL, all observations fit each candidate tree and no
adaptive stopping is applied. With early_stop = c(threshold, window), each
candidate tree is fitted to a random 90 percent subset of the current
residuals and evaluated by its average log density on the held-out 10
percent. A stage ends when the mean of the most recent window held-out
scores is strictly below threshold. The candidate that triggers stopping
is recorded in heldout_diagnostics with accepted = FALSE but is not added
to the ensemble.
c(0, 50) is the paper-oriented setting; the implementation uses a strict
comparison with zero, whereas the paper describes non-positive improvement.
Data are transformed from Omega to the unit cube before fitting. If
Omega = NULL, the range after optional tie perturbation is expanded by 10
percent on each side. Set add_noise = TRUE when a continuous distribution
is assumed and ties are technical artifacts such as rounding. Set it to
FALSE when perturbing ties would be inappropriate.
The default prior_split_prob = 0.9 follows the archived implementation and
public experiment code. Setting prior_split_prob = 0.5 corresponds to the
constant split prior implied by the 0.5 stopping probability reported in
Appendix C of Awaya and Ma (2024). Larger values favor splitting before the
data-dependent candidate likelihoods are incorporated.
Tree construction is stochastic even when add_noise = FALSE, and adaptive
stopping also draws random fitting subsets. Call set.seed() before every
fit that must be reproducible. Fitting is silent by default; set
progress = "stage" for stage-level messages. Elapsed time is stored in the
result and can be inspected with print() or summary(). Exact equality
across operating systems has not been established.
Awaya, N. and Ma, L. (2024). Unsupervised Tree Boosting for Learning Probability Distributions. Journal of Machine Learning Research, 25, 1–52.
set.seed(42) n <- 400L x1 <- stats::rbeta(n, shape1 = 2, shape2 = 5) x2 <- stats::rbeta(n, shape1 = 2 + 6 * x1, shape2 = 4) x <- cbind(x1 = x1, x2 = x2) support <- cbind(lower = c(0, 0), upper = c(1, 1)) set.seed(123) fit <- fit_boostpm( x, Omega = support, max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 100, max_split_depth = 15, min_node_observations = 10, c0 = 0.1, gamma = 0.5, add_noise = FALSE ) print(fit) grid <- seq(0.01, 0.99, length.out = 50L) evaluation_grid <- as.matrix(expand.grid(x1 = grid, x2 = grid)) fitted_density <- matrix( predict(fit, newdata = evaluation_grid, type = "density"), nrow = length(grid) ) graphics::image(grid, grid, fitted_density, xlab = "x1", ylab = "x2") graphics::contour( grid, grid, fitted_density, add = TRUE, drawlabels = FALSE ) generated <- simulate(fit, nsim = 500, seed = 321) graphics::plot( generated, xlab = "x1", ylab = "x2", main = "Generated observations" )set.seed(42) n <- 400L x1 <- stats::rbeta(n, shape1 = 2, shape2 = 5) x2 <- stats::rbeta(n, shape1 = 2 + 6 * x1, shape2 = 4) x <- cbind(x1 = x1, x2 = x2) support <- cbind(lower = c(0, 0), upper = c(1, 1)) set.seed(123) fit <- fit_boostpm( x, Omega = support, max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 100, max_split_depth = 15, min_node_observations = 10, c0 = 0.1, gamma = 0.5, add_noise = FALSE ) print(fit) grid <- seq(0.01, 0.99, length.out = 50L) evaluation_grid <- as.matrix(expand.grid(x1 = grid, x2 = grid)) fitted_density <- matrix( predict(fit, newdata = evaluation_grid, type = "density"), nrow = length(grid) ) graphics::image(grid, grid, fitted_density, xlab = "x1", ylab = "x2") graphics::contour( grid, grid, fitted_density, add = TRUE, drawlabels = FALSE ) generated <- simulate(fit, nsim = 500, seed = 321) graphics::plot( generated, xlab = "x1", ylab = "x2", main = "Generated observations" )
Displays a bar plot of variable importance, the number of nodes per tree, or maximum tree depths.
## S3 method for class 'boostPM_fit' plot( x, type = c("variable_importance", "tree_node_counts", "tree_depths"), ... )## S3 method for class 'boostPM_fit' plot( x, type = c("variable_importance", "tree_node_counts", "tree_depths"), ... )
x |
A fitted object returned by |
type |
Character string selecting "variable_importance", "tree_node_counts", or "tree_depths". |
... |
Additional arguments passed to |
The bar midpoints returned by graphics::barplot(), invisibly.
set.seed(42) n <- 400L x1 <- stats::rbeta(n, shape1 = 2, shape2 = 5) x <- cbind(x1 = x1, x2 = stats::rbeta(n, 2 + 6 * x1, 4)) set.seed(123) fit <- fit_boostpm( x, Omega = cbind(lower = c(0, 0), upper = c(1, 1)), max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 100, max_split_depth = 15, min_node_observations = 10, c0 = 0.1, gamma = 0.5, add_noise = FALSE ) plot(fit, type = "variable_importance") plot(fit, type = "tree_node_counts") plot(fit, type = "tree_depths")set.seed(42) n <- 400L x1 <- stats::rbeta(n, shape1 = 2, shape2 = 5) x <- cbind(x1 = x1, x2 = stats::rbeta(n, 2 + 6 * x1, 4)) set.seed(123) fit <- fit_boostpm( x, Omega = cbind(lower = c(0, 0), upper = c(1, 1)), max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 100, max_split_depth = 15, min_node_observations = 10, c0 = 0.1, gamma = 0.5, add_noise = FALSE ) plot(fit, type = "variable_importance") plot(fit, type = "tree_node_counts") plot(fit, type = "tree_depths")
Evaluates the probability density represented by a fitted tree ensemble at supplied points.
## S3 method for class 'boostPM_fit' predict(object, newdata, type = c("log_density", "density", "details"), ...)## S3 method for class 'boostPM_fit' predict(object, newdata, type = c("log_density", "density", "details"), ...)
object |
A fitted object returned by |
newdata |
A finite numeric matrix with evaluation points in rows and one column per fitted variable. |
type |
Character string selecting the return value: |
... |
Unused. Additional arguments are an error. |
Points outside the fitted rectangular support receive log density -Inf and
density zero. At a split point, evaluation follows the left-child convention
in the method specification. Because mean_log_density_path averages over
every row of newdata, the entire path is -Inf when any row lies outside
the support.
For type = "log_density" or type = "density", a numeric vector
with one entry per row of newdata, in the original row order. For
type = "details", a list containing:
The same vector returned by type = "log_density".
A numeric vector with one entry per fitted
tree. Entry k is the mean log density over all rows of newdata after
the first k trees have been applied.
set.seed(42) n <- 400L x1 <- stats::rbeta(n, shape1 = 2, shape2 = 5) x <- cbind(x1 = x1, x2 = stats::rbeta(n, 2 + 6 * x1, 4)) set.seed(123) fit <- fit_boostpm( x, Omega = cbind(lower = c(0, 0), upper = c(1, 1)), max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 100, max_split_depth = 15, min_node_observations = 10, c0 = 0.1, gamma = 0.5, add_noise = FALSE ) grid <- seq(0.01, 0.99, length.out = 50L) evaluation_grid <- as.matrix(expand.grid(x1 = grid, x2 = grid)) fitted_density <- matrix( predict(fit, evaluation_grid, type = "density"), nrow = length(grid) ) graphics::image(grid, grid, fitted_density, xlab = "x1", ylab = "x2") graphics::contour( grid, grid, fitted_density, add = TRUE, drawlabels = FALSE )set.seed(42) n <- 400L x1 <- stats::rbeta(n, shape1 = 2, shape2 = 5) x <- cbind(x1 = x1, x2 = stats::rbeta(n, 2 + 6 * x1, 4)) set.seed(123) fit <- fit_boostpm( x, Omega = cbind(lower = c(0, 0), upper = c(1, 1)), max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 100, max_split_depth = 15, min_node_observations = 10, c0 = 0.1, gamma = 0.5, add_noise = FALSE ) grid <- seq(0.01, 0.99, length.out = 50L) evaluation_grid <- as.matrix(expand.grid(x1 = grid, x2 = grid)) fitted_density <- matrix( predict(fit, evaluation_grid, type = "density"), nrow = length(grid) ) graphics::image(grid, grid, fitted_density, xlab = "x1", ylab = "x2") graphics::contour( grid, grid, fitted_density, add = TRUE, drawlabels = FALSE )
Prints a compact description of a boostPM_fit object.
## S3 method for class 'boostPM_fit' print(x, ...)## S3 method for class 'boostPM_fit' print(x, ...)
x |
A fitted object returned by |
... |
Unused. |
The input object, invisibly.
set.seed(42) n <- 400L x1 <- stats::rbeta(n, shape1 = 2, shape2 = 5) x <- cbind(x1 = x1, x2 = stats::rbeta(n, 2 + 6 * x1, 4)) set.seed(123) fit <- fit_boostpm( x, Omega = cbind(lower = c(0, 0), upper = c(1, 1)), max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 100, max_split_depth = 15, min_node_observations = 10, c0 = 0.1, gamma = 0.5, add_noise = FALSE ) print(fit)set.seed(42) n <- 400L x1 <- stats::rbeta(n, shape1 = 2, shape2 = 5) x <- cbind(x1 = x1, x2 = stats::rbeta(n, 2 + 6 * x1, 4)) set.seed(123) fit <- fit_boostpm( x, Omega = cbind(lower = c(0, 0), upper = c(1, 1)), max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 100, max_split_depth = 15, min_node_observations = 10, c0 = 0.1, gamma = 0.5, add_noise = FALSE ) print(fit)
Prints the fitted call, data dimensions, support, structural tree diagnostics, variable importance, and elapsed fitting time.
## S3 method for class 'summary.boostPM_fit' print(x, ...)## S3 method for class 'summary.boostPM_fit' print(x, ...)
x |
An object returned by |
... |
Unused. Additional arguments are an error. |
The input summary object, invisibly.
set.seed(42) n <- 400L x1 <- stats::rbeta(n, shape1 = 2, shape2 = 5) x <- cbind(x1 = x1, x2 = stats::rbeta(n, 2 + 6 * x1, 4)) set.seed(123) fit <- fit_boostpm( x, Omega = cbind(lower = c(0, 0), upper = c(1, 1)), max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 100, max_split_depth = 15, min_node_observations = 10, c0 = 0.1, gamma = 0.5, add_noise = FALSE ) print(summary(fit))set.seed(42) n <- 400L x1 <- stats::rbeta(n, shape1 = 2, shape2 = 5) x <- cbind(x1 = x1, x2 = stats::rbeta(n, 2 + 6 * x1, 4)) set.seed(123) fit <- fit_boostpm( x, Omega = cbind(lower = c(0, 0), upper = c(1, 1)), max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 100, max_split_depth = 15, min_node_observations = 10, c0 = 0.1, gamma = 0.5, add_noise = FALSE ) print(summary(fit))
Draws samples from the probability distribution represented by a fitted tree ensemble.
## S3 method for class 'boostPM_fit' simulate(object, nsim = 1L, seed = NULL, ...)## S3 method for class 'boostPM_fit' simulate(object, nsim = 1L, seed = NULL, ...)
object |
A fitted object returned by |
nsim |
Non-negative integer. Number of observations to simulate. |
seed |
Either |
... |
Unused. Additional arguments are an error. |
With seed = NULL, randomness is controlled by calling set.seed() before
simulate(). Supplying seed sets R's random-number generator and changes
its subsequent state.
A numeric matrix with nsim rows and one column per fitted
variable, in the same order as the training data. Training variable names
are preserved as column names when available. Every draw lies within the
fitted rectangular support.
set.seed(42) n <- 400L x1 <- stats::rbeta(n, shape1 = 2, shape2 = 5) x <- cbind(x1 = x1, x2 = stats::rbeta(n, 2 + 6 * x1, 4)) set.seed(123) fit <- fit_boostpm( x, Omega = cbind(lower = c(0, 0), upper = c(1, 1)), max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 100, max_split_depth = 15, min_node_observations = 10, c0 = 0.1, gamma = 0.5, add_noise = FALSE ) generated <- simulate(fit, nsim = 500, seed = 321) old_par <- graphics::par(mfrow = c(1, 2)) graphics::plot(x, xlab = "x1", ylab = "x2", main = "Training data") graphics::plot( generated, xlab = "x1", ylab = "x2", main = "Generated data" ) graphics::par(old_par)set.seed(42) n <- 400L x1 <- stats::rbeta(n, shape1 = 2, shape2 = 5) x <- cbind(x1 = x1, x2 = stats::rbeta(n, 2 + 6 * x1, 4)) set.seed(123) fit <- fit_boostpm( x, Omega = cbind(lower = c(0, 0), upper = c(1, 1)), max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 100, max_split_depth = 15, min_node_observations = 10, c0 = 0.1, gamma = 0.5, add_noise = FALSE ) generated <- simulate(fit, nsim = 500, seed = 321) old_par <- graphics::par(mfrow = c(1, 2)) graphics::plot(x, xlab = "x1", ylab = "x2", main = "Training data") graphics::plot( generated, xlab = "x1", ylab = "x2", main = "Generated data" ) graphics::par(old_par)
Extracts compact structural diagnostics from a boostPM_fit object.
## S3 method for class 'boostPM_fit' summary(object, ...)## S3 method for class 'boostPM_fit' summary(object, ...)
object |
A fitted object returned by |
... |
Unused. |
An object of class summary.boostPM_fit, represented as a list
containing:
The matched call used to fit the model.
Number of training observations, or NA when it
cannot be recovered from the fitted object.
Number of fitted variables.
Number of accepted trees.
The original-scale lower and upper support limits.
The per-tree diagnostic data frame returned by
fit_boostpm().
Accumulated improvement attributed to each variable.
Elapsed fitting time as a difftime object.
set.seed(42) n <- 400L x1 <- stats::rbeta(n, shape1 = 2, shape2 = 5) x <- cbind(x1 = x1, x2 = stats::rbeta(n, 2 + 6 * x1, 4)) set.seed(123) fit <- fit_boostpm( x, Omega = cbind(lower = c(0, 0), upper = c(1, 1)), max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 100, max_split_depth = 15, min_node_observations = 10, c0 = 0.1, gamma = 0.5, add_noise = FALSE ) summary(fit)set.seed(42) n <- 400L x1 <- stats::rbeta(n, shape1 = 2, shape2 = 5) x <- cbind(x1 = x1, x2 = stats::rbeta(n, 2 + 6 * x1, 4)) set.seed(123) fit <- fit_boostpm( x, Omega = cbind(lower = c(0, 0), upper = c(1, 1)), max_marginal_trees = 100, max_dependence_trees = 1000, n_bins = 100, max_split_depth = 15, min_node_observations = 10, c0 = 0.1, gamma = 0.5, add_noise = FALSE ) summary(fit)