🔥 HMC-Torch
A Modular Platform for Hierarchical Multi-Label Classification with R-Matrix Constraints
HMC-Torch is a modular, extensible platform for Hierarchical Multi-Label Classification (HMC) that integrates the R-matrix constraint — originally proposed by Giunchiglia & Lukasiewicz (2018, NeurIPS) — as a reusable, first-class architectural component.
📄 Paper: HMC-Torch: A Modular Platform for Hierarchical Multi-Label Classification with R-Matrix Constraints (Bruno Sette, UFSCar, 2026)
✨ Key Features
- 🔌 Modular pipeline:
DatasetAdapter → FeatureEncoder → HierarchicalHead → Calibrator → Reconciler - 🧱 R-Matrix as infrastructure: reusable ancestor-closure constraint for training + inference
- 🌳 Explicit hierarchy modeling: Tree (FunCat) and DAG (Gene Ontology) with type-specific reconciliation
- 📊 25+ datasets across 5 domains: scientific text, genomics, email, microscopy, medical imaging
- 🧬 Multi-modal: tabular, text (transformers), protein sequences, images
- ⚡ GPU-accelerated: 14× training speedup
- 📦 Plugin system: register your own datasets without modifying the package
- 🔁 Reproducible: experiment manifests with git SHA, seeds, and dependency versions
📦 Installation
pip install hmc-torch
For optional features:
pip install "hmc-torch[vision]" # Image models (timm)
pip install "hmc-torch[protein]" # Protein models (fair-esm)
pip install "hmc-torch[expression]" # Expression autoencoders
From source
git clone https://github.com/Sette/hmc-torch.git
cd hmc-torch
uv sync --all-groups
export PYTHONPATH=src
🚀 Quick Start
Python API
import hmc
# Train a global classifier with R-matrix constraint
results = hmc.train("wos", method="globalE2E", device="cuda", epochs=5)
# Train a tabular baseline
results = hmc.train("cellcycle_FUN", method="tabular_mlp", device="cuda", epochs=100)
CLI
# Frozen embeddings baseline
python -m hmc.main --dataset_name wos --method global --device cuda \
--dataset_path ./data --output_path ./output
# Fine-tuned transformer (SOTA)
python -m hmc.main --dataset_name arxiv --method globalE2E --device cuda \
--dataset_path ./data --epochs 50 --batch_size 32 --output_path ./output
# Tabular baseline
python -m hmc.main --dataset_name cellcycle_FUN --method tabular_mlp --device cuda \
--dataset_path ./data --output_path ./output
🗂️ Supported Datasets
Built-in datasets (25+)
| Domain | Datasets | Modality | Hierarchy | Classes |
|---|---|---|---|---|
| Scientific Text | ArXiv, WOS | Text (SPECTER2) | Tree | 141–156 |
| Genomics (FunCat) | cellcycle, church, derisi, eisen, expr, gasch1, gasch2, pheno, seq, spo | Tabular | Tree | ~499 |
| Genomics (GO) | cellcycle, derisi, eisen, expr, gasch1, gasch2, pheno, seq, spo | Tabular | DAG | 3,570–4,130 |
| Enron | Tabular | Tree | 56 | |
| Microscopy | Diatoms | Tabular | Tree | 398 |
| Medical Imaging | ImCLEF07a, ImCLEF07d | Tabular | Tree | 46–96 |
| Multi-label Text | AAPD, RCV1, EURLex | Text | Tree | 54–3,993 |
Register your own dataset
The framework derives everything it needs — adjacency matrix, level sizes, edge indices, input/output dimensions — from your labels. Your only job is to supply the raw data and make sure the labels encode the hierarchy in a way the framework can parse.
Minimal example — just data + labels:
from hmc.utils import build_digraph_from_labels
from hmc.data.hierarchy import TreeHierarchy
# 1. Your data
texts = ["paper about deep learning", "paper about transformers", ...]
label_strs = ["cs.AI cs.LG", "cs.CL", ...] # one string per sample (space-separated)
# 2. Build hierarchy from labels (one-liner)
h = TreeHierarchy.from_graph(build_digraph_from_labels(label_strs))
# 3. Encode labels (ancestors auto-activated)
Y_global, Y_local = h.encode_labels(label_strs)
Y_local = Y_local[1:] # drop root level
# 4. Compute features, create splits, and train — see full example below.
The rest of this section walks through each step in detail and shows how to package everything into a reusable manager.
1. Pick a label format
Labels must follow a dot-separated path notation where each segment is
one level of the hierarchy. For example, a paper tagged as cs.AI and
stat.ML belongs to both Artificial Intelligence (under Computer
Science) and Machine Learning (under Statistics):
root
├── cs (level 1 — area)
│ └── cs.AI (level 2 — subcategory)
└── stat (level 1 — area)
└── stat.ML (level 2 — subcategory)
The same convention works for any tree-shaped taxonomy —
A.A1.B1, medicine.cardiology, physics.optics.lasers, etc.
2. Build the hierarchy — just collect your unique labels and pass them to
:func:~hmc.utils.build_digraph_from_labels:
from hmc.utils import build_digraph_from_labels
from hmc.data.hierarchy import TreeHierarchy
# Collect all unique labels from your data
all_labels = ["cs", "cs.AI", "cs.LG", "stat", "stat.ML"]
# One-liner: build the child→parent graph from label strings
g = build_digraph_from_labels(all_labels)
hierarchy = TreeHierarchy.from_graph(g)
# → hierarchy.a, hierarchy.edge_index, hierarchy.to_eval, hierarchy.local_nodes_idx
# are all computed automatically — nothing else to do.
The function infers intermediate nodes, handles duplicates, and supports
custom separators (sep="/") and root names. That's it — you only
need your data and your labels; the framework derives everything else.
3. Encode labels with a single call:
Y_global, Y_local = hierarchy.encode_labels(
["cs.AI", "stat.ML cs.LG"] # one string per sample (space-separated)
)
# Y_global: (n_samples, n_nodes) — ancestors automatically activated
# Y_local: list of (n_samples, n_level_nodes) per depth level
4. Wire everything into your manager (the full __init__ pattern):
class MyManager:
def __init__(self, data_path, ...):
# (a) Load raw data and collect unique labels
texts, label_strings = load_my_data(data_path)
# (b) Build hierarchy
g = build_digraph_from_labels(label_strings)
h = TreeHierarchy.from_graph(g)
# (c) Compute features
X = compute_features(texts)
# (d) Encode labels
Y_global, Y_local_all = h.encode_labels(label_strings)
Y_local = Y_local_all[1:] # drop root level (pipeline convention)
# (e) Create splits
X_train, X_val, X_test, Yg_train, ... = split_data(X, Y_global, Y_local)
# (f) Expose — copy from hierarchy + splits
self.input_dim = X.shape[1]
self.output_dim = h.n_nodes
self.levels_size = {k-1: v for k, v in h.level_sizes.items() if k > 0}
self.max_depth = len(self.levels_size)
self.a = h.adjacency
self.edge_index = h.edge_index # ← now provided by TreeHierarchy
self.nodes_idx = h.node_index
self.local_nodes_idx = h.local_nodes_idx # ← now provided
self.to_eval = h.to_eval # ← now provided
self.hierarchy_map = {}
self._train = Split(X_train, Yg_train, Y_local_train)
self._valid = Split(X_val, Yg_val, Y_local_val)
self._test = Split(X_test, Yg_test, Y_local_test)
def get_datasets(self):
return self._train, self._valid, self._test
The built-in managers follow this exact same pattern — they're a good reference if you want to see a production version:
| Manager | File |
|---|---|
ArXivManager |
src/hmc/datasets/arxiv/manager.py |
WOSManager |
src/hmc/datasets/wos/manager.py |
HMCDatasetManager |
src/hmc/datasets/gofun/manager.py |
5. Register your manager — no need to modify any package source:
from hmc.data import DatasetRegistry
DatasetRegistry.register(
"my_data",
lambda **kw: MyManager(**kw),
defaults={"hidden_dim": 256, "lr": 1e-4, "epochs": 50, "dropout": 0.3},
)
# Use it anywhere
manager = DatasetRegistry.get("my_data", device="cuda", dataset_path="./data")
train, valid, test = manager.get_datasets()
import hmc
hmc.train(dataset_name="my_data", method="globalE2E", device="cuda", epochs=50)
If you ship your manager as a pip-installable package, register it
automatically via pyproject.toml:
[project.entry-points."hmc_torch.datasets"]
my_data = "my_package.manager:create_manager"
The entry-point target must be a callable that accepts **kwargs and
returns a manager instance.
🧠 Methods
| Method | Description |
|---|---|
global |
Frozen embeddings + MLP + R-matrix constraint |
globalE2E |
End-to-end fine-tuned transformer + MLP + R-matrix |
globalSOTA |
E2E + GCN label-graph encoder (HiAGM-style) |
local |
Frozen embeddings, one MLP per hierarchy level |
localE2E |
Fine-tuned transformer + per-level MLPs |
tabular_gbdt |
Gradient Boosting One-vs-Rest baseline |
tabular_mlp |
Residual MLP baseline for tabular data |
📊 Results
Text Benchmarks (Micro-F1)
| Method | ArXiv | WOS |
|---|---|---|
| HiAGM (Zhou+, ACL'20) | 0.5950 | 0.8604 |
| HTC-infoMAX | — | 0.8720 |
| HMC-Torch (frozen) | 0.7295 | 0.7515 |
| HMC-Torch (E2E) | — | 0.8743 |
🏆 New SOTA on ArXiv (+13.5 pts over HiAGM) and WOS (+0.2 pts over HTC-infoMAX)
FunCat Genomic Benchmarks (AUPRC)
| Dataset | HMCN-F | C-HMCNN | HMC-Torch (GBDT) | HMC-Torch (MLP) |
|---|---|---|---|---|
| cellcycle_FUN | 0.235 | 0.248 | 0.253 | 0.255 |
| seq_FUN | 0.291 | 0.299 | 0.306 | 0.307 |
| spo_FUN | 0.228 | 0.228 | 0.229 | 0.229 |
🏆 Beats HMCN-F on seq_FUN (0.307 vs 0.291)
Gene Ontology (Block-Diagonal R-Matrix)
AUPRC improves on all 9 GO datasets (avg +0.0055) with zero hierarchy violations, using $O(N)$ memory instead of $O(N^2)$ (>1,400× reduction).
GPU Speedup
| Configuration | CPU | GPU | Speedup |
|---|---|---|---|
| global (FunCat avg) | 16.7s | 1.2s | 14× |
| tabular_mlp (FunCat avg) | 5.3s | 2.0s | 2.7× |
🧬 Architecture
DatasetAdapter → FeatureEncoder → HierarchicalHead → Calibrator → Reconciler
↓ ↓ ↓ ↓ ↓
DatasetBundle fit/transform GlobalSigmoidHead Platt Scaling Bottom-up
+ Hierarchy (tabular, text, LocalLevelHead Temperature max-prop
+ Splits protein, vision) TreePathHead
R-Matrix Constraint
The ancestor closure matrix $R_{ij} = 1$ iff class $i$ is an ancestor of $j$. Applied at:
- Training: $\mathcal{L}_{\text{hier}} = \max(0, p_j - p_i + \gamma)$ for all ancestor pairs
- Inference: $p_i^{\text{rec}} = \max(p_i, \max_{j: \text{child}(i,j)} p_j^{\text{rec}})$
Block-Diagonal R-Matrix (Sparse)
For large DAGs (4,000+ classes), the dense $R$ matrix requires $>65$ MB. Our sparse approximation uses graph traversal ($O(N+E)$ memory) with zero hierarchy violations.
📁 Project Structure
src/hmc/
├── data/ # Data contracts (DatasetBundle, Split, Hierarchy)
├── datasets/ # Built-in dataset implementations
│ ├── arxiv/ # ArXiv (JSONL + SPECTER2)
│ ├── wos/ # WOS (Web of Science)
│ ├── gofun/ # FunCat + GO (ARFF tabular)
│ ├── aapd/ # Arxiv Academic Paper Dataset
│ ├── rcv1/ # Reuters Corpus Volume 1
│ └── eurlex/ # EUR-Lex documents
├── features/ # Feature encoders (text, tabular, vision, protein)
├── models/ # HMC model components
│ ├── global_classifier/ # Global heads + R-matrix
│ ├── local_classifier/ # Per-level local heads
│ ├── hierarchical/ # Sparse R-matrix, label GCN
│ └── tabular/ # GBDT + MLP baselines
├── pipeline/ # Training pipelines
└── utils/ # Metrics, manifests, caching
🤝 Contributing
Contributions welcome! Areas we'd love help with:
- New dataset adapters
- Additional feature encoders (genomics, graphs)
- New hierarchical heads and reconciliation strategies
- Documentation and tutorials
git clone https://github.com/Sette/hmc-torch.git
cd hmc-torch
uv sync --all-groups
make test
make lint
📚 Citation
@article{sette2026hmctorch,
title = {HMC-Torch: A Modular Platform for Hierarchical Multi-Label
Classification with R-Matrix Constraints},
author = {Bruno Sette},
journal = {arXiv preprint},
year = {2026},
}
📄 License
MIT © Bruno Sette
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hmc_torch-0.0.10.tar.gz.
File metadata
- Download URL: hmc_torch-0.0.10.tar.gz
- Upload date:
- Size: 147.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3ae2de3ec37701364c32b427f4f243622641344111bd98fb5b21750d2795084d
|
|
| MD5 |
165825ede07905c2ff933d8620bf2762
|
|
| BLAKE2b-256 |
1281ebf627935e7864ba9eb480aa3781e25924fa7ae97f74d31b877e01a1b680
|
Provenance
The following attestation bundles were made for hmc_torch-0.0.10.tar.gz:
Publisher:
python-publish.yml on Sette/hmc-torch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hmc_torch-0.0.10.tar.gz -
Subject digest:
3ae2de3ec37701364c32b427f4f243622641344111bd98fb5b21750d2795084d - Sigstore transparency entry: 2401251005
- Sigstore integration time:
-
Permalink:
Sette/hmc-torch@e0561dcc82c9040af6900e23432b037218cc5615 -
Branch / Tag:
refs/tags/0.0.10 - Owner: https://github.com/Sette
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@e0561dcc82c9040af6900e23432b037218cc5615 -
Trigger Event:
release
-
Statement type:
File details
Details for the file hmc_torch-0.0.10-py3-none-any.whl.
File metadata
- Download URL: hmc_torch-0.0.10-py3-none-any.whl
- Upload date:
- Size: 202.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f36415f28ff7cf12ed932d8891820d7ebb0c779d3dfc8b2ffa852fa3d734133b
|
|
| MD5 |
840b534562712609d932a3a8d29dec44
|
|
| BLAKE2b-256 |
5fa198c2d8a45578e9678962c4f10a8fed8830b72ffbd6e0c31f4a32a627ac9f
|
Provenance
The following attestation bundles were made for hmc_torch-0.0.10-py3-none-any.whl:
Publisher:
python-publish.yml on Sette/hmc-torch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hmc_torch-0.0.10-py3-none-any.whl -
Subject digest:
f36415f28ff7cf12ed932d8891820d7ebb0c779d3dfc8b2ffa852fa3d734133b - Sigstore transparency entry: 2401251717
- Sigstore integration time:
-
Permalink:
Sette/hmc-torch@e0561dcc82c9040af6900e23432b037218cc5615 -
Branch / Tag:
refs/tags/0.0.10 - Owner: https://github.com/Sette
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@e0561dcc82c9040af6900e23432b037218cc5615 -
Trigger Event:
release
-
Statement type: