Fitting a TRAM-DAG¶
This page explains how to train a
CausalFlowDAG and its nodes. It covers the
likelihood, the Adam paths fit and fit_jointly, the classical path
fit_classical, and which recipe to use. The API pages hold the signatures
and defaults.
notebooks/training_strategies.py
runs the Adam recipes end to end and compares their runtime.
notebooks/fit_strategies_benchmark.py
compares fit, fit with n_jobs and fit_jointly.
One module, one sub-model per node¶
A CausalFlowDAG is a single torch.nn.Module with one independent
sub-model per variable. A sub-model has an intercept that produces the
transform parameters \(\theta\) and one shift module per shift term. It also has
the monotone transform \(h\), which holds range buffers but no weights. The nodes
share no parameters. The DAG lives entirely in which parents each node reads:
there is no edge weight matrix and no shared trunk.
The likelihood¶
The flow maps iid standard-logistic latents \(U\) to the observed \(X\) in causal order, and node \(i\) reads its parents as data. The Jacobian is therefore triangular, and the joint log-likelihood decomposes per node, \(\log p(x) = \sum_i \log p(x_i \mid \mathrm{pa}(x_i))\).
- Continuous node. Change of variables through the monotone transform: \(u = h(x;\theta) + s\) and \(\log p(x \mid \mathrm{pa}) = \log f_{\text{logistic}}(u) + \log |h'(x)|\).
- Ordinal node. The ordered-logit head of model.md, the log of the cutpoint-interval probability.
Because parents enter as data, the per-node terms have disjoint parameters,
and maximizing each term maximizes the sum. So every node can fit on its own:
fit minimizes each node's mean negative log-likelihood, plus the node's VC
penalty, one node after the other. fit_jointly minimizes the sum of the
same losses in one loop.
One node alone¶
A Node is a TRAM regression of one variable on its
parents, and it needs no DAG. It fits, scores and saves like the flow, for
one node. notebooks/training_strategies.py
fits one with Adam, and
notebooks/classical_fit_tram_dag.py
fits one classically against R glm. A lone node built with seed= starts from
the weights it has in a flow with that seed and the default init
(code-map.md). A fit with the flow's fit
seed= then gives the weights of that node in the flow.
Path A: stochastic optimization with fit¶
fit is the general-purpose trainer. With fit_jointly, it is one of the
two Adam paths, the only trainers for a model with a CS, CI or VC term
(fit_classical is not). Per node, it is one minibatch Adam loop that keeps
the final weights unless a callback restores others. The
fit docstring lists every option.
- Nodes. The nodes fit in topological order, or in parallel worker
processes with
n_jobs. - Epochs.
epochshas no default. A fixed budget under-spends on one workload and wastes on the next, so every caller states its own. - Per-node arguments.
epochs,learning_rate,optimizerandcallbackstake one value for every node or a dict by node name. A missing name gets the default; forepochsit raises. Acollections.defaultdictgives its own fallback. - Shuffle. Every node shuffles its rows from the fit's
seed=. So a node fits asNode.fitwith that seed, andfit_jointlysees the same row order. - Calibration. The first fit calibrates each node on the training frame.
Every term freezes its data-dependent state there: the intercept its
range_qquantiles and eachinput_transform=its statistics. A loaded checkpoint is never recalibrated. - Validation. With
validation_data=orvalidation_split=,fitrecords each node's validation NLL after every epoch.validation_splittakes the last rows unshuffled, so shuffle the frame first if its row order means anything. - Callbacks. A callback gets the node (under
fit_jointly, the flow) at fit begin, after every epoch and at fit end. ATruereturn after an epoch stops that node. The shipped callback isEarlyStopping, which restores the best-validation weights. A learning-rate scheduler is a few lines of callback around torch'slr_scheduler. - Centered
VCpropensities ride the training frame as a column (varying-coefficients.md).
The marginal start¶
fit(marginal_init=True) puts the optimizer near the data before the first
step. It changes the parameters the optimizer starts from, not the model or
the loss. It sets the \(\boldsymbol{\vartheta}\) of each Bernstein or ordinal
simple intercept so that, at shift \(s = 0\), the node's CDF equals the
empirical marginal \(\hat F\) of its training column. The match is exact for an
ordinal node and a smooth approximation for a continuous node. An intercept
with parents has no single \(\boldsymbol{\vartheta}\) and keeps its seeded
initialization. Without the flag, every simple intercept starts with all its
parameters at zero.
The latent \(U\) is standard logistic, so at \(s = 0\) the model says \(P(X \le x) = \sigma(h(x))\). The start therefore targets \(h = \operatorname{logit} \hat F\).
- An ordinal node targets the cumulative class log-odds and inverts
ordinal_cutpoints(ordinal_marginal_init_theta). - A continuous node sets the Bernstein control points to
\(\operatorname{logit} \hat F\) at equally spaced points of the calibrated
range. Then it inverts the zuko parameterization
(
BernsteinUT.marginal_init_theta). The polynomial smooths \(\hat F\), and a sharp feature such as the dip between two modes smooths most.
The spline and affine transforms have no marginal start, and the flag skips
them. A range_q=0 Bernstein transform puts the range ends at the data
extremes, where the logit is infinite, so fit refuses the flag there.
The zero start is close to a straight line with a steeper \(h\) than the data.
It gives a narrower distribution and a higher NLL at shift zero. The
likelihood of an all-LS model has one optimum, so the start only shortens
the way. A model with a network shift or a learning-rate anneal can reach a
different local optimum. There the start can improve or worsen the causal
estimate. D4 in
paper-replication.md
measures this per variant.
Which recipe¶
All-LS models train to the MLE and keep the final weights. Flexible models
with a CI, CS or VC term overfit observational confounding at the MLE
and need the best-validation weights to recover the causal effect.
| recipe | when |
|---|---|
fit_classical |
all-LS spec: exact, deterministic, seconds |
| plain Adam | an all-LS spec whose fit_classical report is not converged, quick looks |
multi-phase Adam, one fit call per rate |
a tighter MLE without a scheduler |
EarlyStopping() |
any CI, CS or VC model; register it, fit has no default |
EarlyStopping(patience=) |
also stop each node once its best epoch is that old |
EarlyStopping(min_epochs=, smoothing=) |
ignore epochs before min_epochs; judge the best epoch on a moving average of the validation NLL, for small validation sets |
a plateau Callback around torch's ReduceLROnPlateau |
a decaying rate per node |
fit_jointly |
one schedule or stop rule on the summed NLL, as in the reference implementation |
Three details are easy to get wrong.
- A second
fitcall continues training andhistoryaccumulates. That is what makes a multi-phase schedule a loop offitcalls. - A callback instance passed to the flow's
fitserves every node in turn, so its attributes describe the last node only. Use a dict by node name, or adefaultdict, to keep one instance per node. Undern_jobsthe callbacks stay in the workers; readnode.historyinstead. - A post-fit
load_state_dictskips theVCre-centering. Restore weights fromon_fit_endinstead, asEarlyStoppingdoes.
Path B: classical optimization with fit_classical¶
fit_classical is the optimizer for all-LS models, where every
node-conditional is an ordered logit or a transformation model with linear
shifts. The fit_classical
docstring holds the options and the report.
- Full batch, float64, L-BFGS, then Newton. There are no minibatches or schedules, so the same initialization gives bit-identical results. L-BFGS stops short of the optimum on an ill-conditioned NLL. One cause is a parent on a large raw scale next to a rare level. Newton steps on the exact Hessian then finish the fit.
- The model stays float32. Read-outs use the fitted values rounded by
about 1e-7 relative. Compare the report's float64
coefficientswith other software. - Unidentified directions do not move. The
fit_classicaldocstring names them and says how to compare them with other software.
A converged fit_classical leaves the model at the MLE. A fit call from
there stays put, which makes it a warm start. Node.warm_start uses it for
the beta0 of a VC (varying-coefficients.md).
Memory and disk¶
Neither path writes to disk. The parameters, the optimizer state and the
history dicts live in RAM. The only disk access is the explicit save()
and load(). fit(n_jobs=N) forks N copy-on-write copies of the flow and
the training frame.