Skip to main content

🔥 HMC-Torch

A Modular Platform for Hierarchical Multi-Label Classification with R-Matrix Constraints

PyPI Python License Downloads


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 and an Empirical Study of When Hierarchical Constraints Help (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
  • 📊 27 datasets across 5 domains: scientific text, genomics, email, microscopy, medical imaging
  • 🧬 Multiple modalities: production paths for tabular and text; experimental encoders for expression, protein sequences, and images
  • ⚡ GPU-ready: exact sparse reconciliation that scales to 4,000-class Gene Ontology hierarchies
  • 📦 Plugin system: register your own datasets without modifying the package
  • 🔁 Reproducible: one evaluation protocol and scripts that regenerate every number in the paper

📦 Installation

pip install hmc-torch

For optional features:

pip install "hmc-torch[download]"    # Dataset downloaders (kagglehub, HF datasets)
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 --extra download
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 (end-to-end)
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

# Reproduce the paper's recipe: plain BCE + inference-time reconciliation
python -m hmc.main --dataset_name wos --method global --consistency_loss none \
  --dataset_path ./data --output_path ./output

Training-time hierarchy handling (--consistency_loss, for global and globalE2E):

Value Training loss Constraint applied
mc (default) constrained outputs mixed into the loss (C-HMCNN style) training + inference
hinge paper's Eq. 2 added with weight --lambda_hier training + inference
none plain BCE inference only

The constraint is always applied at inference (get_constr_out); the flag only changes what happens during training. The numbers reported in the paper use none, so pass it to reproduce them.

Docker

Build the image from the repository root:

docker build -t hmc-torch .

The GitHub Actions workflow publishes release images to GitHub Container Registry (GHCR) whenever a v* tag is pushed. For example, tag v0.0.10 publishes ghcr.io/sette/hmc-torch:0.0.10 and the latest tag. You can also run the workflow manually from the Actions tab; that publishes an image tagged with the commit SHA.

Download (pull) a published image from GHCR before running it. Use a version tag for a reproducible release, or latest for the most recently published release:

docker pull ghcr.io/sette/hmc-torch:0.0.10
# Or pull the latest release
docker pull ghcr.io/sette/hmc-torch:latest

The image runs python -m hmc.main; without arguments it displays the CLI help. Pass the same options used by the Python CLI to start an experiment. Mount the dataset and output directories so they are available inside the container and results persist after it exits:

docker run --rm \
  -v "$PWD/data:/app/data" \
  -v "$PWD/output:/app/output" \
  ghcr.io/sette/hmc-torch:0.0.10 \
  --dataset_name wos --method global --device cpu \
  --dataset_path /app/data --output_path /app/output

For GPU training, use Docker with NVIDIA Container Toolkit configured and add --gpus all; set --device cuda in the application arguments:

docker run --rm --gpus all \
  -v "$PWD/data:/app/data" \
  -v "$PWD/output:/app/output" \
  ghcr.io/sette/hmc-torch:0.0.10 \
  --dataset_name wos --method global --device cuda \
  --dataset_path /app/data --output_path /app/output

Data

make download-all      # every dataset
make download-arff     # FunCat + Gene Ontology ARFF benchmarks (GoFun family)
make download-arxiv    # ArXiv metadata snapshot (Kaggle)
make download-wos      # Web of Science abstracts
make download-aapd     # AAPD
make download-rcv1     # RCV1-V2
make download-eurlex   # EUR-Lex 57K

The tabular benchmarks land in data/HMC_data_arff/ (the layout hmc.utils.datasets.paths expects, covering the 10 *_FUN, the 9 *_GO and the *_others datasets); the text datasets each get their own data/<name>/.

RCV1-V2 is gated (NIST license), so obtain rcv1.tar.xz and lyrl2004_tokens_train.dat from NIST first and place them in data/rcv1/ (the output directory by default). Then make download-rcv1 clones HBGL when needed, links the source files into its checkout, and runs upstream preprocessing. Pass --raw_dir if the licensed files are stored elsewhere, or --upstream_dir to select another HBGL checkout. The other targets download from public mirrors. For a pipeline smoke test only, python -m hmc.datasets.rcv1.download_rcv1 --sample installs the ~10-document HiAGM files.


🗂️ 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
Email 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 97–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

All numbers come from experiments/run_protocol.py and experiments/run_wos_e2e.py under one protocol: training on the training split, epoch and decision threshold chosen on the validation split, the test split used once, mean over three seeds. experiments/make_paper_artifacts.py regenerates every table and figure of the paper (docs/hmc-paper).

Text (Micro-F1)

Method ArXiv WOS AAPD EUR-Lex 57K
BERT+HiAGM (reimpl. by HGCLR, ACL'22) — 0.8604 — —
BERT+HTCInfoMax (reimpl. by HGCLR, ACL'22) — 0.8630 — —
BERT+HiMatch (Chen+, ACL'21) — 0.8670 — —
HGCLR (Wang+, ACL'22) — 0.8711 — —
HPT (Wang+, EMNLP'22) — 0.8716 — —
HMC-Torch, frozen SPECTER2 + MLP + reconciliation 0.723 0.746 0.823 0.786
HMC-Torch, fine-tuned SPECTER2 + reconciliation — 0.863 ± 0.006 — —

WOS baselines are from Table 1 of HPT (EMNLP'22), which trains on the training split only, as we do. ArXiv and AAPD use our own 64/16/20 splits and EUR-Lex uses the EUROVOC hierarchy, so there are no comparable published numbers for them.

FunCat and Gene Ontology (AUPRC, mean over the 8 datasets with published results)

Method FunCat GO
Surj (Yang & Howe, WWW'22) 0.312 0.467
C-HMCNN (Giunchiglia & Lukasiewicz, NeurIPS'20) 0.264 0.420
HMCN-F (Wehrmann et al., ICML'18) 0.261 0.422
HMC-Torch, MLP (BCE) + reconciliation 0.253 0.411
HMC-Torch, MLP (MCLoss) + reconciliation 0.255 0.411
HMC-Torch, GBDT + reconciliation 0.226 —

What the constraint contributes

Reconciliation at inference raises test AUPRC on 25 of 27 datasets (median +0.002). Adding the constraint to training — a hinge penalty or C-HMCNN's MCLoss — gives no consistent gain over plain BCE followed by reconciliation. The sparse (edge-list) reconciliation is exact and differentiable, so MCLoss trains at batch size 64 on the 4,000-class GO hierarchies.


🧬 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:

  1. Training: $\mathcal{L}_{\text{hier}} = \max(0, p_j - p_i + \gamma)$ for all ancestor pairs
  2. 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

The package has two related data layers: hmc.datasets contains built-in loaders, managers, downloaders, and compatibility adapters; hmc.data defines the shared split, hierarchy, metadata, and registry contracts. The code map and current capability boundaries are documented in docs/project-structure.md.

src/hmc/
├── arguments.py, main.py, train.py  # CLI, dispatch e API Python
├── data/              # Contratos, hierarquia e registro de datasets
├── datasets/          # Managers, adapters e downloaders por dataset
├── features/          # Encoders tabular, text, expression, vision e protein
├── models/            # Modelos globais, locais, hierárquicos e tabulares
├── pipeline/          # Loops de treino por família de modelo
└── utils/             # Métricas, caminhos, cache, manifestos e treino

🤝 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 --extra download
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

Metadata

Release files for hmc-torch 0.0.11

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hmc-torch 0.0.11
File Size Uploaded Provenance Actions
hmc_torch-0.0.11.tar.gz 161.7 kB With provenance Details for hmc_torch-0.0.11.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for hmc-torch 0.0.11
File Interpreter ABI Platform Actions
hmc_torch-0.0.11-py3-none-any.whl Python 3 none any Details for hmc_torch-0.0.11-py3-none-any.whl

Total release size: 378.9 kB

Release files / hmc_torch-0.0.11.tar.gz

Download URL hmc_torch-0.0.11.tar.gz
Size 161.7 kB
Tags Source
SHA-256 checksum
How to use checksums
550743f4b103f5bae5be3650f209a369d172447b55b339d6baae4f9f68306cc6
BLAKE2b-256 checksum
How to use checksums
c9892423470340a620d36a0df554495bc7dbd5006328f1c114b2af1e0b9ae068
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Release files / hmc_torch-0.0.11-py3-none-any.whl

Download URL hmc_torch-0.0.11-py3-none-any.whl
Size 217.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c398073a15a42a90d59127a7b94d54451aab9bf25ae4eac5da05055cca2feada
BLAKE2b-256 checksum
How to use checksums
1aa03b1eb31b52cd31ae940348751cf76184d8c9d33f257dd6570dbeada49281
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Security for hmc-torch 0.0.11

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for hmc_torch-0.0.11.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page