API reference¶
Estimators¶
- class difftree.DifferentiableTreeRegressor(depth=5, learning_rate=0.01, batch_size=256, n_epochs=200, gamma_init=1.0, gamma_max=5.0, delta_gamma=0.1, sigma_init=1.0, warmup_fraction=0.25, validation_fraction=0.1, n_calibration_steps=300, calibration_lr=0.02, standardize=True, leaf_type='constant', constant_outputs=None, jacobian_weight=1.0, hardening=False, gamma_hard=50.0, n_hard_epochs=50, hard_ridge=1e-06, continuous=False, continuity_weight=None, tied_splits=False, refine_splits=50, random_state=None, verbose=False)[source]¶
Bases:
RegressorMixin,BaseEstimatorSoft oblique decision tree of fixed depth with multivariate Gaussian leaves.
Predictive distribution at each input x is a mixture of multivariate Gaussians, one per leaf, weighted by soft path probabilities mu_l(x). For univariate y this reduces to the heteroscedastic Gaussian model; for multivariate y it is a structured conditional GMM (input-dependent MDN). All outputs share one partition (one set of splits).
- Parameters:
depth (int, default=5)
learning_rate (float, default=0.01)
batch_size (int, default=256)
n_epochs (int, default=200)
gamma_init (float) – Anneal split steepness 1 -> gamma_max during training.
gamma_max (float) – Anneal split steepness 1 -> gamma_max during training.
delta_gamma (float) – Anneal split steepness 1 -> gamma_max during training.
sigma_init (float, default=1.0) – Initial leaf marginal standard deviation (in standardized y units).
warmup_fraction (float, default=0.25)
validation_fraction (float, default=0.1) – Fraction of training data held out for post-hoc gamma+alpha calibration.
n_calibration_steps (calibration hyperparams.)
calibration_lr (calibration hyperparams.)
standardize (bool, default=True)
leaf_type ({"constant", "affine"}, default="constant") –
"constant": leaflpredicts the meanb_l."affine": leaflpredictsA_l x + b_l. The Gaussian noise model is the same in both cases, sopredict(return_std=True)works for either.constant_outputs (sequence of int or None, default=None) – With
leaf_type="affine", output indices whose leaves stay constant (A_lrow fixed at zero), e.g. intensive properties that enter bilinear blending terms. Ignored for constant leaves.jacobian_weight (float, default=1.0) – Weight of the Jacobian-matching (Sobolev) term, used only when
fitreceivesdydx. The term is computed in standardized units.hardening (bool, default=False) – If True,
fitends withharden()on the same train/validation split.gamma_hard (float, default=50.0) – Final split steepness reached while hardening.
n_hard_epochs (int, default=50) – Training epochs used to anneal gamma from
gamma_maxtogamma_hardbefore the partition is frozen.hard_ridge (float, default=1e-6) – Ridge strength of the least-squares leaf refit. Coefficients shrink toward the soft leaf values, which only matters for leaves with too few training points to fit on their own.
continuous (bool or sequence of int, default=False) – Make the hardened model continuous across split boundaries for these outputs (True: every affine output). The hardening refit then requires neighbouring leaves to agree on every facet they share inside the training-data box, so a MILP optimum cannot exploit a jump between leaves. Only for affine outputs, and only appropriate where the true response is continuous.
continuity_weight (float or None, default=None) – Untied trees only. None enforces continuity exactly. A float instead adds a penalty
continuity_weight * ||mismatch||^2on the coefficient mismatch (standardized units) across each shared facet, allowing small jumps.tied_splits (bool, default=False) – Every internal node at the same depth shares one hyperplane (an oblivious oblique tree). The partition is then an arrangement of
depthhyperplanes, and a continuous model on it is the hinge modeltheta_0 . [1, x] + sum_d c_d max(0, w_d . x + b_d). Withcontinuous, hardening fits that form directly and refines the hyperplanes, which keeps continuity cheap. Without tied splits, continuity forces leaves on either side of a deeper split to merge wherever that split meets a parent boundary.refine_splits (int, default=50) – Tied trees with
continuousonly: maximum Powell iterations used to move the hyperplanes so that the continuous fit’s least-squares error is smallest (0 keeps the trained hyperplanes).random_state (int or None)
verbose (bool)
- fit(X, y, dydx=None)[source]¶
Fit the tree.
- Parameters:
X ((n, p) array)
y ((n,) or (n, d_y) array)
dydx ((n, d_y, p) array, optional) – Jacobian of y with respect to x at each training point, in original units. For 1D
ya(n, p)array is also accepted. Requiresleaf_type="affine"; adds a Jacobian-matching term weighted byjacobian_weight.
- harden(X, y, dydx=None, *, continuous=None, bounds=None)[source]¶
Turn the fitted soft tree into a crisp piecewise model.
Steps:
Keep training splits and leaves while gamma grows geometrically from
gamma_maxtogamma_hardovern_hard_epochsepochs, so the splits settle where crisp routing wants them.Freeze the partition: route each training point crisply (
apply()).Refit every leaf by least squares on the points it holds (affine outputs regress on
[1, x], constant outputs take the mean;dydxrows join the least-squares system when given). Withcontinuous, the refit is one joint constrained least-squares problem in which neighbouring leaves agree on their shared facets; withtied_splitsit is instead a hinging-hyperplanes fit, with the hyperplanes refined (refine_splits), which is continuous everywhere. Leaf noise covariances are refit from the residuals.Fit the variance scale
alphain closed form on the held-out validation slice under crisp routing, and set every per-depth gamma togamma_hard.
X, yare split withvalidation_fractionexactly as infit(). Passing the training data used infitis the usual choice. The result of comparing soft and hard predictions on the validation slice before and after hardening is stored inhardening_report_(seesoft_hard_gap()), together withmax_boundary_jump, the largest jump of each output across any shared facet in the box (seedifftree.export.boundary_jumps()).continuousoverrides the constructor setting for this call.bounds = (lo, hi)(original units) sets the box in which leaves must join up continuously; it defaults to the training-data box and should cover the region a MILP will search.Returns self.
- soft_hard_gap(X, y)[source]¶
Compare soft and hard (crisp) predictions on
(X, y).Returns a dict with, per output,
rmse_soft,rmse_hard,rmse_gap = rmse_hard - rmse_soft,r2_soft,r2_hard; the mean negative log-likelihoodsnll_softandnll_hard(original y units);mean_max_leaf_prob, the average probability of each point’s most likely leaf under soft routing (1.0 means routing is already crisp); androuting_agreement, the fraction of points whose most likely soft leaf is the crisp leaf. A large gap means the soft fit relies on blending leaves and is a misleading guide to the crisp (MILP-embeddable) model.
- apply(X)[source]¶
Leaf index (0 .. 2**depth - 1, BFS leaf order) each row routes to under crisp routing: node
igoes right iffw_i . x + b_i > 0.- Return type:
- predict(X, return_std=False, return_components=False, hard=False)[source]¶
Predict marginal mean (and optionally per-output marginal std).
For multivariate y,
stdis the per-output marginal standard deviation (sqrt of diagonal of the predictive covariance). Usepredict_full_covfor the full covariance matrix.With
hard=Trueeach row is routed crisply to one leaf (seeapply()) and the prediction is that leaf’s Gaussian: the std is the leaf noise only and the routing component is zero.
- predict_full_cov(X, hard=False)[source]¶
Predict mean and full predictive covariance per sample.
Returns
(mean, cov)of shapes(n, d_y)and(n, d_y, d_y), in original (un-standardized) y units.hard=Trueuses crisp routing, as inpredict().- Parameters:
hard (bool)
- sample_y(X, n_samples=1, random_state=None)[source]¶
Marginal Monte Carlo draws from p(y|x).
Returns shape
(n_samples, n, d_y)for multivariate or(n_samples, n)for univariate.
- predict_quantile(X, q, n_samples=1000, random_state=None)[source]¶
Approximate marginal quantiles by Monte Carlo. Univariate only.
- predict_conditional(X, observed)[source]¶
Conditional moments p(y_unobs | x, y_obs).
- Parameters:
X ((n, p) array)
observed (dict {output_idx: value}) – Each value is a scalar (broadcast across rows) or an (n,) array.
- Returns:
cond_mean ((n, d_unobs) array)
cond_cov ((n, d_unobs, d_unobs) array)
log_evidence ((n,) array) – log p(y_obs | x), useful for ranking how well x is consistent with the observation.
- sample_y_conditional(X, observed, n_samples=1, random_state=None)[source]¶
Monte Carlo draws from p(y_unobs | x, y_obs).
Returns shape
(n_samples, n, d_unobs)in original y units.
- inverse_design(targets, *, n_starts=16, n_steps=400, learning_rate=0.05, bounds=None, init_x=None, random_state=0)[source]¶
Find x such that p(y_target | x) is maximized.
- Parameters:
targets (dict {output_idx: value}) – Output values to match. Values are scalars in original y units. Subset of outputs is supported (constrained inverse design).
n_starts (number of random restarts.)
n_steps (Adam steps per restart.)
learning_rate (Adam learning rate.)
bounds (tuple of (lo, hi) arrays in original x units, optional.) – Restricts the search to a feature box.
init_x ((n_starts, p) array of starts in original x units, optional.)
random_state (int)
- Returns:
x_best ((p,) ndarray in original x units.)
log_lik_best (float) – log p(y_target | x_best) under the trained likelihood.
x_all ((n_starts, p) ndarray of all restart results.)
log_liks ((n_starts,) ndarray of all final log-likelihoods.)
- class difftree.DifferentiableTreeClassifier(depth=5, learning_rate=0.01, batch_size=256, n_epochs=200, gamma_init=1.0, gamma_max=5.0, delta_gamma=0.1, validation_fraction=0.1, n_calibration_steps=300, calibration_lr=0.05, standardize=True, random_state=None, verbose=False)[source]¶
Bases:
ClassifierMixin,BaseEstimatorSoft oblique decision tree of fixed depth with categorical leaves.
Predictive distribution at each x is a mixture of categoricals (one per leaf), weighted by the soft path probabilities. The total predictive entropy decomposes into an aleatoric (within-leaf) part and an epistemic (mutual-information / between-leaf-routing) part.
Calibration is performed post-hoc on a validation slice by fitting the per-depth
gamma(split steepness) by minimizing validation NLL.See
DifferentiableTreeRegressorfor shared hyperparameter docs.- Parameters:
- apply(X)[source]¶
Leaf index (0 .. 2**depth - 1, BFS leaf order) each row routes to under crisp routing: node
igoes right iffw_i . x + b_i > 0.- Return type:
Export¶
Export a crisp (hardened) regressor for MILP embedding, in original units.
Under crisp routing (predict(hard=True)), internal node i sends x
left iff a_i . x <= t_i, with the split undone from standardized units:
a_i = W_i / x_std, t_i = a_i . x_mean - b_i.
Each leaf is then a polytope {x : G_l x <= h_l}: the half-spaces along its
root-to-leaf path intersected with an input box. Its leaf model is affine,
y = coef_l x + intercept_l (coef_l has zero rows for outputs held
constant per leaf), with Gaussian noise covariance cov_l.
Three entry points:
leaf_polytopes()returns oneLeafPolytopeper leaf, with empty leaves (one LP per leaf) and, optionally, unreached leaves pruned.to_spec()/to_json()emit the solver-neutral v1 JSON spec of jax-ldt (jax_ldt.export.spec), sojax_ldt.export.from_jsonand its discopt adapter can consume a difftree model. Each oblique split becomes one lifted column oftransform_matrix: the matrix is[I_p | a_1 ... a_k], the firstplifted features are the raw inputs (the leaf models use only these), and nodeisplits lifted featurep + jatt_i.predict_spec()evaluates such a spec in NumPy, as a reference.
Two helpers deal with split boundaries, where a piecewise-affine tree can jump. A MILP treats leaf polytopes as closed sets, so its optimum can sit on a boundary and use either neighbouring leaf’s model there:
boundary_jumps()measures the largest jump of each output across every shared facet (useharden(continuous=True)to remove them);repair_solution()moves a MILP solution a distancedeltainto the interior of the leaf the solver chose (seeleaf_from_binaries()), so thatpredict(hard=True)at the returned point uses that leaf.
- class difftree.export.LeafPolytope(leaf, path=<factory>, G=None, h=None, coef=None, intercept=None, cov=None, radius=0.0, n_samples=None)[source]¶
Bases:
objectOne crisp leaf region and its model, in original units.
- Parameters:
- path¶
Internal nodes on the root-to-leaf path and the branch taken (
"left"or"right").
- G, h
Polytope
{x : G x <= h}: one row per path split, then the input box (x <= hiand-x <= -lo).- Type:
ndarray
- coef¶
- Type:
(d_y, p) ndarray
- intercept¶
Leaf mean
coef @ x + intercept.- Type:
(d_y,) ndarray
- cov¶
Leaf noise covariance (calibrated).
- Type:
(d_y, d_y) ndarray
- radius¶
Chebyshev radius of the polytope in standardized x units (radius of the largest ball inside it, capped at 1). Empty polytopes have 0.
- Type:
- n_samples¶
Rows of the
Xpassed toleaf_polytopes()that route here.- Type:
int or None
- difftree.export.leaf_polytopes(model, bounds=None, *, prune_empty=True, X=None, prune_unreached=False, tol=1e-07)[source]¶
Leaf polytopes
{x : G_l x <= h_l}and leaf models in original units.- Parameters:
model (fitted DifferentiableTreeRegressor (ideally hardened).)
bounds ((lo, hi), optional) – Input box in original units (arrays of length p, or scalars). Defaults to the training-data box
(model.x_min_, model.x_max_).prune_empty (bool, default True) – Drop leaves whose polytope has no interior inside the box (one LP per leaf).
X ((n, p) array, optional) – Points to count per leaf (
n_samples), e.g. the training inputs.prune_unreached (bool, default False) – Also drop leaves that no row of
Xreaches. RequiresX.tol (float) – A leaf is empty when its Chebyshev radius (standardized x units) is at most
tol.
- Return type:
list of LeafPolytope, in BFS leaf order.
- difftree.export.to_spec(model, bounds=None, *, prune_empty=True, X=None, prune_unreached=False, tol=1e-07)[source]¶
Serialize the crisp tree to jax-ldt’s solver-neutral v1 spec (a dict).
Arguments are as in
leaf_polytopes(). Pruning collapses every internal node with a pruned side into its other child, so the spec only contains splits that separate kept leaves. Collapsing nodes whose pruned side is empty is exact inside the box; collapsing nodes whose side is only unreached (prune_unreached) reassigns that region to a neighbouring leaf.Layout:
transform_matrixis[I_p | a_1 ... a_k](one lifted column per kept internal node); internal nodes split lifted featurep + jattand go left iffx'_f <= t;leaf_params[node]is[bias; weights]over the lifted features, with weights only on the firstp(raw) columns. Extra difftree information (bounds, BFS leaf ids, leaf covariances) sits under the top-level"difftree"key, which jax-ldt ignores.
- difftree.export.to_json(model, path=None, **kwargs)[source]¶
to_spec()as a JSON string, also written topathif given.
- difftree.export.predict_spec(spec, X)[source]¶
Evaluate a v1 spec (dict or JSON string) at
Xwith NumPy.Returns (n,) for single-target specs, else (n, n_targets).
- difftree.export.boundary_jumps(model, bounds=None, *, tol=1e-07)[source]¶
Largest jump of each output across every shared leaf facet in the box.
For every pair of leaves that share a (p-1)-dimensional facet inside the box, the difference of their affine models is maximized (in absolute value) over that facet with two LPs per output.
- Parameters:
model (fitted DifferentiableTreeRegressor.)
bounds ((lo, hi), optional) – Input box in original units; defaults to the training-data box.
tol (float) – Minimum facet Chebyshev radius (standardized x units) to count.
- Returns:
list of dict, sorted by largest jump first, each with
node(internalnode whose hyperplane holds the facet),
left/right(BFS leafindices),
point(the facet’s Chebyshev center, original units),max_jump((d_y,) array, original y units) andworst_point((d_y, p) array (where on the facet each output’s jump is largest). A)
continuous output has
max_jumpat round-off level on every facet.
- Return type:
- difftree.export.leaf_from_binaries(spec, z)[source]¶
BFS leaf index of the active leaf binary
zof a MILP embedding.zholds one value per leaf ofspec, in the order the spec’s leaves appear (np.where(spec["tree"]["is_leaf"])), which is the order jax-ldt’s discopt adapter uses for its leaf binaries.
- difftree.export.repair_solution(model, x, leaf, *, delta=1e-06, bounds=None)[source]¶
Move
xinto the interior ofleaf’s polytope.Solves one LP: the closest point (max-norm, standardized x units) to
xthat lies at leastdeltainside every face of the leaf’s polytope within the box. Crisp routing of the result isleaf, sopredict(hard=True)there uses the leaf model the MILP optimized, and the surrogate output changes by at mostdeltatimes its slope. Choosedeltaabove the solver’s feasibility tolerance (in standardized units).- Parameters:
model (fitted DifferentiableTreeRegressor.)
x ((p,) array, original units (e.g. the MILP's optimal x).)
leaf (int) – BFS leaf index the solver selected (see
leaf_from_binaries()).delta (float, default 1e-6)
bounds ((lo, hi), optional) – Input box in original units; defaults to the training-data box.
- Return type:
(p,) ndarray in original units.