PR: Consolidate NODE as the Single Neural Tabular Path (CheMeleon + NODE) - #71
PR: Consolidate NODE as the Single Neural Tabular Path (CheMeleon + NODE)#71thomasATbayer wants to merge 208 commits into
Conversation
This module implements core utilities for the NODE architecture, including sparse activation functions, base module classes, and an embedding layer for tabular data. It also defines the Oblivious Differentiable Sparsemax Tree (ODST) and DenseODSTBlock for building complex models.
This module provides utility functions for head layers, specifically for flow-based probabilistic predictions. It includes a function to compute mode predictions and uncertainty from a flow distribution, leveraging vectorized operations for efficiency.
update Node
Validate `tree_dropout` before constructing dropout masks or applying inverted-dropout scaling, ensuring the value remains in the valid interval [0, 1). This prevents division by zero and avoids producing inf or NaN values when `tree_dropout=1.0`. Add defensive validation in `DenseODSTBlock` so both direct block usage and estimator-backed usage fail early with a clear `ValueError`.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 24 changed files in this pull request and generated no new comments.
Suppressed comments (2)
scripts/check_docs_python_fences.py:26
- FENCE_RE ends with
\s*$. Because\smatches newlines, the regex can accidentally consume content after the closing fence (including subsequent blocks) and make matching dependent on file layout. It should only allow trailing spaces on the closing fence line.
FENCE_RE = re.compile(
r"(?ms)^[ \t]*```(?P<lang>python|py)(?P<attrs>[^\n]*)\n(?P<code>.*?)\n[ \t]*```\s*$"
)
src/mother/pipeline_utils.py:881
- The
elsebranch clearsfold_estimatorsto “free memory”, butfold_estimatorsis only appended to whenreturn_estimatorsis True (see line 770), so it is already empty here. The branch is redundant and may mislead future readers about actual memory behavior.
else:
fold_estimators.clear() # Clear the list to free memory if not returning
module_logger.info("Returning performance_data only")
Restrict Python fence matching to trailing spaces and tabs on the closing fence line, preventing the regex from consuming subsequent markdown content or code blocks through newline-matching whitespace. Remove the redundant fold_estimators.clear() call from cross-validation result handling, since fold_estimators is only populated when return_estimators=True and is already empty when returning performance data alone.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 24 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/mother/feature_generation/fp_gnn_gen.py:39
- The default CheMeleon checkpoint is downloaded over the network and written to a user cache without any integrity verification (e.g., SHA256). This is a supply-chain risk and can also lead to silent corruption (partial/cached files) being treated as valid. Consider verifying the downloaded file against a known hash (from Zenodo metadata) before moving it into the cache.
with urllib.request.urlopen(_CHEMELEON_ZENODO_URL, timeout=60) as response:
shutil.copyfileobj(response, tmp_file)
tmp_path.replace(_CHEMELEON_CACHE_PATH) # atomic on POSIX; avoids partial files
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 24 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/mother/feature_generation/fp_gnn_gen.py:39
- The CheMeleon checkpoint is downloaded from a remote URL and cached locally, but the download path does not verify integrity (e.g., sha256) before trusting/executing the weights. This is a supply-chain risk if the remote asset changes or is tampered with.
Consider pinning the expected checksum (or fetching it from a trusted manifest) and validating the downloaded file before moving it into the cache path; fail fast with a clear error if verification fails.
with urllib.request.urlopen(_CHEMELEON_ZENODO_URL, timeout=60) as response:
shutil.copyfileobj(response, tmp_file)
tmp_path.replace(_CHEMELEON_CACHE_PATH) # atomic on POSIX; avoids partial files
Keep input_dropout_only_input and tree_dropout_only_head fixed for single-layer Optuna trials because their alternative placements are behaviorally equivalent when no intermediate tree outputs exist. Use input_dropout_only_input=True and tree_dropout_only_head=True for one-layer startup parameters, while continuing to tune both placement flags independently for multi-layer models. Keep input_dropout and tree_dropout rates independently tunable at every depth, and update the corresponding regression tests.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 24 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/mother/feature_generation/fp_gnn_gen.py:147
- CheMeleonFingerprintTransformer.transform() currently materializes
valuesvianp.array(list(X), ...). WhenXis a numpy object array (e.g., shape (n, 1)), iterating yields row sub-arrays, sovaluesbecomes an array of arrays andisinstance(..., Chem.Mol)fails for valid molecules. This contradicts the new unit test that expects single-column object arrays to be flattened into molecules.
values = np.array(list(X), dtype=object).reshape(-1)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/mother/feature_generation/fp_gnn_gen.py:148
- CheMeleonFingerprintTransformer.transform() builds
valuesvianp.array(list(X), ...), which turns a 2D object array likenp.array([[mol]], dtype=object)into an array of row-arrays (notChem.Mol). That makesvalid_maskfalse and returns NaNs, contradicting the intended single-column flattening behavior (and the added unit test).
def transform(self, X: Iterable) -> np.ndarray:
check_is_fitted(self, "is_fitted_")
values = np.array(list(X), dtype=object).reshape(-1)
out = np.full((len(values), self.output_dim), np.nan, dtype=np.float32)
if len(values) == 0:
return out
valid_mask = np.array([isinstance(compound, Chem.Mol) for compound in values], dtype=bool)
src/mother/ml/models/node_head_utils.py:106
nn.Parameteris a subclass oftorch.Tensor, so theisinstance(obj, torch.Tensor)branch will run first and the laterisinstance(obj, nn.Parameter)branch is unreachable. If the intent is to avoid moving Parameters via.to(), the checks should be reordered.
if isinstance(obj, torch.Tensor):
if obj.device != device:
return obj.to(device)
return obj
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/mother/feature_generation/fp_gnn_gen.py:24
get_default_chemeleon_checkpoint()downloads a model checkpoint from a remote URL and caches it locally, but it doesn’t verify integrity (e.g., SHA256) before trusting the file. This is a supply-chain and reliability risk (corrupt/truncated downloads, transparent proxy tampering, etc.).
Consider publishing/embedding an expected hash (or fetching a Zenodo-provided checksum) and verifying it after download; alternatively allow users to opt out of network downloads by default and require an explicit checkpoint_path.
def get_default_chemeleon_checkpoint() -> Path:
"""Return path to chemeleon_mp.pt, downloading from Zenodo on first use."""
if not _CHEMELEON_CACHE_PATH.exists():
_CHEMELEON_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
src/mother/ml/models/node_utils.py:649
- These warnings are emitted from inside the library, but they don’t set
stacklevel, so users will see the warning originating fromnode_utils.pyrather than their own call site. Addingstacklevel=2makes the warning location more actionable for callers.
if input.shape[0] < 256:
warn(
"Data-aware initialization is performed on less than 256 data points. "
"This may reduce threshold initialization quality on some datasets. "
"Prefer at least 256 samples for stable initialization; 512+ can be more robust "
src/mother/ml/models/node_utils.py:785
- This warning also lacks
stacklevel, which makes it point at the library code instead of the caller location that passed an invalidmax_layers_retained. Usingstacklevel=2improves debuggability.
if effective_max_layers_retained is not None and effective_max_layers_retained < 1:
warn(
f"max_layers_retained={effective_max_layers_retained} is smaller than 1; "
"using max_layers_retained=1 to keep dimensions consistent."
)
Consolidate NODE as the Single Neural Tabular Path
Summary
This branch adds two complementary capabilities to Mother:
CheMeleon is a feature generator and is not coupled to NODE. The current CheMeleon example uses Mother's
CatboostRegressorMother; the NODE example is maintained separately inexamples/notebooks/05_advanced/05_NODE.ipynb.NODE architecture
NODE uses differentiable oblivious decision trees (ODSTs). An oblivious tree uses one split feature and threshold at each depth. For a depth-$d$ tree:
Here,$P(\ell \mid x)$ is the differentiable probability that sample $x$ reaches leaf $\ell$ , and $w_\ell$ is that leaf's learned response. Soft routing allows the tree to be trained with gradient descent.
NODE layers use dense connections:
Sparse activations such as
entmax15,sparsemax,entmoid15, andsparsemoidmake feature selection and split gates differentiable while encouraging sparse routing.NODE heads
subsetlinearmlpflowDropout controls
NODE has three independent dropout probabilities:
input_dropouttree_dropoutmlp_dropoutFor dropout probability$p$ , inverted dropout uses a mask $m$ :
so the expected value of the retained signal is preserved.
Two boolean controls determine where input and tree dropout are applied:
input_dropout_only_input=Trueapplies feature dropout only to original input features.Falsealso applies it to dense between-layer inputs.tree_dropout_only_head=Trueapplies tree dropout to the final representation before the head.Falseapplies tree dropout inside every ODST layer.The default behavior keeps feature dropout over dense inputs and applies tree dropout at the head boundary. Setting all dropout rates to zero is valid and produces a deterministic model.
Uncertainty and flow heads
For non-flow heads, MC dropout estimates knowledge uncertainty by repeating prediction with different dropout masks. For$T$ stochastic predictions:
A flow head models a complete conditional distribution$p(y \mid x)$ . Samples from one fixed flow represent data uncertainty. Different dropout passes produce different flow experts and can additionally represent knowledge uncertainty.
Plain-language uncertainty glossary
-0.0000001after floating-point rounding. Tiny negative values close to zero should be treated as zero within tolerance.Let the expert distributions be$p_t(y \mid x)$ and their pooled distribution be:
For the BALD entropy decomposition:
The knowledge term is the mutual-information disagreement signal and is non-negative up to floating-point precision. Differential entropy for a continuous flow can itself be negative; that is valid for a sharply concentrated density.
The combined API supports two knowledge methods:
balduses the additive entropy decomposition above.balsa_emduses a sampled Earth Mover's Distance disagreement score. It is not an entropy term and should not be added todata_uncertaintyto reconstructtotal_uncertainty.Flow types and tuning
NICEflow_transformsNSFflow_transforms,flow_binsRealNVPflow_transformsNAFflow_transforms,flow_signalUNAFflow_transforms,flow_signalBPFflow_degreeGMMflow_componentsParameter meanings:
flow_transforms: number of reversible transformation blocks.flow_bins: number of spline pieces forNSF.flow_degree: polynomial degree forBPF.flow_signal: hidden width forNAFandUNAF.flow_components: number of Gaussian peaks forGMM.Start with
NICEfor speed orNSFfor a strong general-purpose baseline. Increase flow complexity only when validation results support it.Batching for wide one-layer NODE models
A model with
num_layers=1and many trees is wide and shallow:batch_sizebatches rows, not trees. Every row in each minibatch passes through the full tree ensemble:Reducing
batch_sizeis usually the first memory adjustment for a wide model. Reducingnum_treesalso saves memory but changes model capacity.max_layers_retainedaffects dense skip connections only whennum_layers > 1.CheMeleon workflow
CheMeleon converts molecular graphs into fixed-width learned embeddings:
Use the repository's locked Chemprop extra before running the example from a fresh environment:
Notebook documentation
Updated
examples/notebooks/05_advanced/05_NODE.ipynbwith accessible, plain-language explanations and high-contrast callouts covering:max_layers_retained.Dependencies
The NODE optional dependency group contains all NODE-specific dependencies:
skorchis not a base dependency. The environment was verified with:Breaking changes
The neural modeling path is consolidated under NODE. Downstream imports should use:
Standalone flow and MLP wrappers are no longer the supported public path. Use
NODERegressor(head_type="flow")orNODERegressor(head_type="mlp")instead.Validation focus
The branch includes focused coverage for:
References