Skip to content

Latest commit

 

History

History
184 lines (124 loc) · 14.9 KB

File metadata and controls

184 lines (124 loc) · 14.9 KB

Training

Basic Usage

import XGB from '@neabyte/xgb'

// Train a binary classifier with default parameters
const model = new XGB('binary:logistic')
const features = [
  [1.2, 3.4],
  [5.6, 7.8],
  [9.0, 1.1],
  [2.3, 4.5]
]
const labels = [0, 1, 1, 0]
model.fit(features, labels)

The fit method accepts a 2D feature matrix and a 1D label array, runs gradient boosted rounds, records feature importance, and returns the model instance for chaining.

Input Validation

The fit method calls Utils.check before training begins. This validates the shape and consistency of the feature matrix and label array. Every violation throws immediately.

TypeError Cases

Condition Message
matrix is not an array or is empty matrix must be a non-empty 2D array
matrix[0] is not an array matrix must be an array of number arrays
labels is not an array labels must be an array of numbers

RangeError Cases

Condition Message
matrix[0] is an empty array matrix must have at least one feature per row
matrix.length differs from labels.length matrix has {n} rows but labels has {m} entries
Any label is NaN or infinite labels must be finite numbers, got {label}
Any row after the first has a different length than matrix[0] or is not an array matrix[{index}] must have {numFeature} features but has {length}

The width guard runs on matrix[0] right after the array shape checks and before the label checks, so a matrix of rows that are all empty is rejected up front. Without it a call like fit([[], [], []], [0, 1, 0]) was accepted and reported success while producing a model with a numFeature of 0, every tree collapsed to a single zero leaf, constant predictions, a proba of exactly [0.5, 0.5], and an empty importance() array. The earlier empty-matrix guard only rejected a matrix with no rows, never rows with no features.

The row sweep is delegated to Utils.vet, the same helper the inference methods use, called here with the width of matrix[0] as the expected feature count. It checks that each row is an array of that length and stops at the first mismatch.

The label sweep runs before the row width loop and rejects any value that fails Number.isFinite. Without this guard a single NaN or Infinity label spreads through every gradient and hessian on the first round and yields a model whose leaf weights are all NaN, with no error raised at any point during training or prediction.

Multiclass Label Cases

For multi:softprob and multi:softmax, the class count is derived from the labels as max(label) + 1 by strategy.numClass, which fit calls once after Utils.check passes. That single sweep validates every label and throws on the first offending value.

Condition Error Message
Any label is negative or not an integer RangeError multiclass labels must be non-negative integers, got {label}

Without this guard a negative label would be skipped by the running maximum and yield a class count that is silently too small, while a fractional label would reach the class count as a non-integer and fail with an opaque Invalid array length error. Scalar objectives always report a class count of 1 and do not inspect label values here.

Training Loop

Each call to fit runs params.nEstimators boosting rounds. The method initializes a margins array where each row holds a copy of the base margin vector. On every round the method builds one tree per class.

  1. The method computes gradients and hessians for all rows using the current margins
  2. For scalar objectives (binary:logistic, binary:logitraw, reg:squarederror, reg:logistic, count:poisson) it calls strategy.gradient and strategy.hessian per row using the single margin value at index 0
  3. For multiclass objectives (multi:softprob, multi:softmax) it calls strategy.multiGradient once per row, which returns per-class gradient and hessian vectors that are then distributed into separate arrays per class
  4. For each class index, the method grows a tree from the gradients and hessians of that class
  5. After growing, if gamma is greater than 0, the tree undergoes gamma pruning
  6. The method tallies feature importance stats from the tree and then scales all leaf weights by params.learningRate
  7. After all class trees are built, margins are updated by walking each row through each class tree and adding the leaf output to the corresponding margin
  8. When all rounds finish, the method stores the trees. For single-class objectives the trees array is flat. For multiclass objectives each entry is an array of trees, one per class.

The method creates a deterministic PRNG from params.seed at the start of training and uses it throughout all rounds for both row and column subsampling.

Row Subsampling

When params.subsample is less than 1, the method draws a different row subset at the start of each boosting round. It uses Bernoulli sampling where each row is included independently with probability equal to params.subsample. If the sample turns out empty, one random row is picked as a fallback. All class trees within the same round share the same row subset.

When params.subsample is 1, every row is used and no sampling occurs.

Column Subsampling

When params.colsampleByTree is less than 1, the method selects a column subset independently for each class tree within each round. It performs a Fisher-Yates shuffle of all column indices and takes the first Math.max(1, Math.floor(ratio * numFeature)) elements, then sorts them in ascending order. The grow function only considers the selected columns when evaluating splits.

When params.colsampleByTree is 1, all columns are used and no sampling occurs.

Tree Growing

The Utils.grow function recursively builds a decision tree by evaluating every candidate split across the allowed features and thresholds.

Split Finding

At each node the method computes the gradient sum and hessian sum of all rows reaching that node. It then iterates each candidate feature, separates rows with missing values (null, NaN, or infinite) from valid rows, and sorts the valid rows by feature value in ascending order. For each pair of consecutive distinct values it computes a midpoint threshold. A candidate is skipped when that midpoint does not lie strictly between the two values, which happens when the pair is too close for a float midpoint to separate them. Two candidate splits are evaluated at each surviving threshold, one that sends missing values left and one that sends them right.

The gain for a candidate split is computed as the sum of left-child and right-child squared-gradient-over-regularized-hessian terms minus the parent term. Each of those terms comes from Utils.term, the shared helper that computes the regularized squared sum term, called once in Utils.grow for the parent baseline and twice in Utils.pick for the two child terms. A candidate is rejected if either child has a hessian sum below params.minWeight. The method keeps the candidate with the highest gain across all features and thresholds.

Missing Value Handling

During split finding a feature value that is null, NaN, or infinite is treated as missing, so the row is held out of the sorted candidate list and its gradient and hessian are accumulated into the missing bucket instead. Each threshold is then tested twice with those missing rows assigned to the left child and to the right child. The direction that yields higher gain wins and becomes the stored defaultLeft flag. Once the best split is chosen, rows are partitioned with Utils.route, the shared helper that also drives tree descent in Utils.walk, which applies the same rule and sends every null, NaN, or infinite cell down the default direction. Split enumeration and routing therefore agree, so the row partition matches the gain that selected the split.

Leaf Conditions

A node becomes a leaf in three cases:

  • The current depth has reached params.maxDepth
  • The node contains one row or fewer
  • The best candidate gain is at most 1e-6

The leaf weight is computed as the negative gradient sum divided by the regularized hessian sum. When maxStep is greater than 0, the weight is clamped to the range from negative maxStep to positive maxStep.

GrowConfig

Property Type Description
lambda number L2 regularization added to the hessian denominator
gamma number Minimum gain threshold for pruning
minWeight number Minimum hessian sum required in each child
maxStep number Leaf weight clamp bound, 0 means no clamping
maxDepth number Maximum tree depth
numFeature number Total feature count in the matrix
columns number[] | null Column indices to scan, or null for all columns

The maxStep value comes from params.maxStep when it is set. Otherwise it falls back to the strategy default, which varies by objective.

Gamma Pruning

When params.gamma is greater than 0, each tree undergoes bottom-up pruning after it is grown. The Utils.prune function recursively visits every node. When both children of a split node are leaves and the split gain is less than gamma, the node collapses into a leaf using the parentWeight that was recorded during growing. The pruning preserves splits whose gain meets or exceeds the gamma threshold.

When params.gamma is 0, no pruning occurs.

Base Score Initialization

The base score and base margin vector are derived differently depending on the objective and whether params.baseScore is explicitly set.

Base Score

When params.baseScore is provided, that value is used directly. When it is null, the method calls strategy.baseScore(labels) to derive the score from the training labels.

Objective Derived base score
binary:logistic Mean of labels clamped to the range 1e-6 through 1 minus 1e-6
binary:logitraw Mean of labels clamped to the range 1e-6 through 1 minus 1e-6
reg:squarederror Mean of labels
reg:logistic Mean of labels clamped to the range 1e-6 through 1 minus 1e-6
count:poisson Mean of labels with a floor of 1e-6
multi:softprob 0.5
multi:softmax 0.5

An explicit params.baseScore is taken as given and is not passed through the derivation above, so it can legally be 0 or 1 for the logistic objectives. Those endpoints are handled by the clamp inside Utils.logit described under Base Margin.

Base Margin

The base margin is a vector of length equal to the class count. The initialization depends on whether strategy.initVector exists and whether params.baseScore is null.

When strategy.initVector is present and params.baseScore is null, the method calls initVector(labels, numClass). This applies to multi:softprob and multi:softmax, which compute centered log-prior margins by counting label frequencies, taking the log of each frequency, and subtracting the mean log across all classes.

Otherwise, the method fills the vector with strategy.initMargin(baseScore) for every class.

Objective initMargin transform
binary:logistic Logit of the base score
binary:logitraw Logit of the base score
reg:squarederror The base score itself
reg:logistic Logit of the base score
count:poisson Natural log of the base score with a floor of 1e-6
multi:softprob 0
multi:softmax 0

Utils.logit clamps its input to the range 1e-6 through 1 minus 1e-6 before taking the log odds, using the same epsilon as the derived logistic base score. A baseScore of exactly 0 or 1 therefore produces a large but finite margin instead of negative or positive infinity, which would otherwise saturate every prediction in the model.

The count:poisson transform applies the same kind of floor for the same reason. It takes the natural log of the base score raised to a minimum of 1e-6, using the same epsilon as its derived base score. The derived path is already floored, but an explicit params.baseScore bypasses that derivation and reaches the transform unchanged, so a value of 0 would produce an initial margin of negative infinity that collapses every prediction to 0 and a negative value would produce NaN that spreads through the whole model. Both now yield a large but finite negative margin, and valid base scores are unaffected.

MaxStep by Objective

The maxStep value controls leaf weight clamping. When params.maxStep is null, the strategy default is used.

Objective Default maxStep Effect
binary:logistic 0 No clamping
binary:logitraw 0 No clamping
reg:squarederror 0 No clamping
reg:logistic 0 No clamping
count:poisson 0.7 Leaves clamped to the range -0.7 through 0.7
multi:softprob 0 No clamping
multi:softmax 0 No clamping