# SEVAE: Structural Equation–Variational Autoencoder
Interpretable, disentangled latents for tabular data via a theory-driven architecture. SE-VAE mirrors structural-equation modeling (SEM): each **construct** has its own encoder/decoder block, plus an optional **nuisance** latent and **global cross-talk** context.

<sub>The figure should show: Input X → Global Context Encoder → k Construct Encoders → Construct Latents + Nuisance Latent → k Decoders → per-construct Reconstructions→ Reconstructed X.</sub>
---
## Features
- **Per-construct latents** (`K` constructs × `d_per_construct`)
- **Global cross-talk** (`context_dim`) concatenated to each construct encoder
- **Nuisance latent(s)** over the full input (`n_nuisance_blocks × d_nuisance`)
- **Adversarial leakage penalty** (discourages the nuisance latent from reconstructing items alone)
- **KL annealing** with a single knob (`cfg.kl_weight`) you update during training
- **Flexible column indexing**:
- contiguous blocks via `items_per_construct` (default),
- **index lists** with `model.bind_column_groups([...])`,
- **name-based** with `cfg.feature_name_groups` + `model.bind_feature_names(names)`.
---
## Install
```bash
# 1) Install a matching PyTorch build for your platform.
# CPU (generic):
pip install torch
# CUDA example (change CUDA version as needed):
pip install torch --index-url https://download.pytorch.org/whl/cu121
# Apple Silicon (MPS):
pip install torch
# 2) Install SEVAE
pip install sevae
```
## Quickstart
```bash
import torch
from sevae import SEVAE, SEVAEConfig
K, J = 6, 8 # constructs, items per construct
cfg = SEVAEConfig(
n_constructs=K,
items_per_construct=J, # contiguous groups: [F1*][F2*]...[FK*]
d_per_construct=1,
d_nuisance=1,
n_nuisance_blocks=1,
context_dim=1, # small cross-talk
hidden=128,
dropout=0.05,
# structure losses (tune per dataset)
tc_weight=6.4,
ortho_weight=1.0,
leakage_weight=0.5,
# KL is annealed during training by updating this field
kl_weight=0.0
)
model = SEVAE(cfg)
x = torch.randn(64, K * J) # batch of tabular rows
out = model(x) # forward
losses = model.loss(x, out) # dict with loss_total and components
losses["loss_total"].backward()
```
## Flexible column indexing
A) Contiguous (default)
If your columns are already grouped as [F1_Item1..J][F2_Item1..J]...[FK_Item1..J], just set:
```bash
cfg = SEVAEConfig(n_constructs=K, items_per_construct=J, ...)
model = SEVAE(cfg)
```
B) Arbitrary index groups (interleaved columns)
```bash
# Example for 48 columns not stored contiguously:
column_groups = [
[0, 7, 14, 21, 28, 35, 42, 47], # construct 0 item indices
[1, 8, 15, 22, 29, 36, 43, 46], # construct 1
# ...
]
model.bind_column_groups(column_groups) # call once before training
```
C) Name-based groups (with pandas)
```bash
# Suppose df is a pandas DataFrame with columns in any order
feature_name_groups = [
[f"F1_Item{j}" for j in range(1, J+1)],
[f"F2_Item{j}" for j in range(1, J+1)],
# ...
]
cfg = SEVAEConfig(
n_constructs=K,
items_per_construct=J,
feature_name_groups=feature_name_groups,
context_dim=1,
)
model = SEVAE(cfg)
model.bind_feature_names(df.columns.tolist()) # map names → indices once
```
## Training recipe
SEVAE builds its layers lazily on the first forward pass. Create the optimizer after the first tiny forward, and then move the model to the device (or make the model device-aware; see Device tips).
```bash
import torch, torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset
device = torch.device("cpu") # or "cuda", or "mps"
x = ... # (N, K*J) standardized features (e.g., via sklearn StandardScaler)
X_t = torch.tensor(x, dtype=torch.float32)
loader = DataLoader(TensorDataset(X_t), batch_size=512, shuffle=True)
cfg = SEVAEConfig(
n_constructs=K, items_per_construct=J, d_per_construct=1,
d_nuisance=1, n_nuisance_blocks=1, context_dim=1, hidden=32, dropout=0.05,
tc_weight=6.4, ortho_weight=1.0, leakage_weight=0.5,
tc_on_construct_only=True, # TC on constructs (recommended)
adv_include_block_recon=True, # match original objective
recon_reduction="sum", # main recon like the reference script
kl_weight=0.0 # will anneal below
)
model = SEVAE(cfg)
# 1) Build lazily with a tiny CPU forward, then move to device
with torch.no_grad():
_ = model(X_t[:2])
model.to(device)
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
EPOCHS = 100
for epoch in range(1, EPOCHS + 1):
# KL annealing (linear over first 50% of epochs)
model.cfg.kl_weight = min(1.0, epoch / (EPOCHS * 0.5))
model.train()
total = 0.0
for (xb,) in loader:
xb = xb.to(device)
out = model(xb)
loss = model.loss(xb, out)["loss_total"]
opt.zero_grad()
loss.backward()
opt.step()
total += float(loss.item())
if epoch % 10 == 0:
print(f"Epoch {epoch}/{EPOCHS} avg loss {total/len(loader):.4f} (β={model.cfg.kl_weight:.2f})")
```
## Device tips (CPU / CUDA / MPS)
Recommended (robust) pattern
1. Build on CPU with a tiny batch: with torch.no_grad(): _ = model(X_t[:2])
2. Move the model: model.to(device)
3. Create the optimizer after moving: opt = torch.optim.Adam(model.parameters(), lr=...)
4. Move inputs each step: xb = xb.to(device)
Apple MPS
```bash
# Optional: allow CPU fallback for not-yet-supported ops
export PYTORCH_ENABLE_MPS_FALLBACK=1
```
Non-contiguous groups
If you used bind_column_groups or bind_feature_names, the model stores index tensors. After model.to(device), they are automatically used on the same device. If you subclass or modify the model, ensure those indices are on device.
## Citation
If you use this package, please cite:
Zhang, R., Zhao, C., Zhao, X., Nie, L., & Lam, W. F. (2025). Structural Equation-VAE: Disentangled Latent Representations for Tabular Data. arXiv preprint arXiv:2508.06347.
```bash
@article{zhang2025structural,
title={Structural Equation-VAE: Disentangled Latent Representations for Tabular Data},
author={Zhang, Ruiyu and Zhao, Ce and Zhao, Xin and Nie, Lin and Lam, Wai-Fung},
journal={arXiv preprint arXiv:2508.06347},
year={2025}
}
```
Raw data
{
"_id": null,
"home_page": null,
"name": "sevae",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": null,
"keywords": "disentanglement, pytorch, representation-learning, sem, structural-equation-modeling, tabular-data, vae",
"author": null,
"author_email": "Ruiyu Zhang <ruiyuzh@connect.hku.hk>",
"download_url": "https://files.pythonhosted.org/packages/3d/66/b0e3555e759707915df88467019c409191f9a5898aa49cdeb5a07dfd844a/sevae-1.0.2.tar.gz",
"platform": null,
"description": "# SEVAE: Structural Equation\u2013Variational Autoencoder\n\nInterpretable, disentangled latents for tabular data via a theory-driven architecture. SE-VAE mirrors structural-equation modeling (SEM): each **construct** has its own encoder/decoder block, plus an optional **nuisance** latent and **global cross-talk** context.\n\n\n<sub>The figure should show: Input X \u2192 Global Context Encoder \u2192 k Construct Encoders \u2192 Construct Latents + Nuisance Latent \u2192 k Decoders \u2192 per-construct Reconstructions\u2192 Reconstructed X.</sub>\n\n---\n\n## Features\n\n- **Per-construct latents** (`K` constructs \u00d7 `d_per_construct`)\n- **Global cross-talk** (`context_dim`) concatenated to each construct encoder\n- **Nuisance latent(s)** over the full input (`n_nuisance_blocks \u00d7 d_nuisance`)\n- **Adversarial leakage penalty** (discourages the nuisance latent from reconstructing items alone)\n- **KL annealing** with a single knob (`cfg.kl_weight`) you update during training\n- **Flexible column indexing**:\n - contiguous blocks via `items_per_construct` (default),\n - **index lists** with `model.bind_column_groups([...])`,\n - **name-based** with `cfg.feature_name_groups` + `model.bind_feature_names(names)`.\n\n---\n\n## Install\n\n```bash\n# 1) Install a matching PyTorch build for your platform.\n# CPU (generic):\npip install torch\n\n# CUDA example (change CUDA version as needed):\npip install torch --index-url https://download.pytorch.org/whl/cu121\n\n# Apple Silicon (MPS):\npip install torch\n\n# 2) Install SEVAE\npip install sevae\n```\n\n## Quickstart\n\n```bash\nimport torch\nfrom sevae import SEVAE, SEVAEConfig\n\nK, J = 6, 8 # constructs, items per construct\n\ncfg = SEVAEConfig(\n n_constructs=K,\n items_per_construct=J, # contiguous groups: [F1*][F2*]...[FK*]\n d_per_construct=1,\n d_nuisance=1,\n n_nuisance_blocks=1,\n context_dim=1, # small cross-talk\n hidden=128,\n dropout=0.05,\n # structure losses (tune per dataset)\n tc_weight=6.4,\n ortho_weight=1.0,\n leakage_weight=0.5,\n # KL is annealed during training by updating this field\n kl_weight=0.0\n)\n\nmodel = SEVAE(cfg)\n\nx = torch.randn(64, K * J) # batch of tabular rows\nout = model(x) # forward\nlosses = model.loss(x, out) # dict with loss_total and components\nlosses[\"loss_total\"].backward()\n```\n\n## Flexible column indexing\nA) Contiguous (default)\nIf your columns are already grouped as [F1_Item1..J][F2_Item1..J]...[FK_Item1..J], just set:\n```bash\ncfg = SEVAEConfig(n_constructs=K, items_per_construct=J, ...)\nmodel = SEVAE(cfg)\n```\n\nB) Arbitrary index groups (interleaved columns)\n```bash\n# Example for 48 columns not stored contiguously:\ncolumn_groups = [\n [0, 7, 14, 21, 28, 35, 42, 47], # construct 0 item indices\n [1, 8, 15, 22, 29, 36, 43, 46], # construct 1\n # ...\n]\nmodel.bind_column_groups(column_groups) # call once before training\n```\nC) Name-based groups (with pandas)\n```bash\n# Suppose df is a pandas DataFrame with columns in any order\nfeature_name_groups = [\n [f\"F1_Item{j}\" for j in range(1, J+1)],\n [f\"F2_Item{j}\" for j in range(1, J+1)],\n # ...\n]\ncfg = SEVAEConfig(\n n_constructs=K,\n items_per_construct=J,\n feature_name_groups=feature_name_groups,\n context_dim=1,\n)\nmodel = SEVAE(cfg)\nmodel.bind_feature_names(df.columns.tolist()) # map names \u2192 indices once\n```\n\n## Training recipe\nSEVAE builds its layers lazily on the first forward pass. Create the optimizer after the first tiny forward, and then move the model to the device (or make the model device-aware; see Device tips).\n\n```bash\nimport torch, torch.nn.functional as F\nfrom torch.utils.data import DataLoader, TensorDataset\n\ndevice = torch.device(\"cpu\") # or \"cuda\", or \"mps\"\nx = ... # (N, K*J) standardized features (e.g., via sklearn StandardScaler)\nX_t = torch.tensor(x, dtype=torch.float32)\n\nloader = DataLoader(TensorDataset(X_t), batch_size=512, shuffle=True)\n\ncfg = SEVAEConfig(\n n_constructs=K, items_per_construct=J, d_per_construct=1,\n d_nuisance=1, n_nuisance_blocks=1, context_dim=1, hidden=32, dropout=0.05,\n tc_weight=6.4, ortho_weight=1.0, leakage_weight=0.5,\n tc_on_construct_only=True, # TC on constructs (recommended)\n adv_include_block_recon=True, # match original objective\n recon_reduction=\"sum\", # main recon like the reference script\n kl_weight=0.0 # will anneal below\n)\nmodel = SEVAE(cfg)\n\n# 1) Build lazily with a tiny CPU forward, then move to device\nwith torch.no_grad():\n _ = model(X_t[:2])\nmodel.to(device)\nopt = torch.optim.Adam(model.parameters(), lr=1e-3)\n\nEPOCHS = 100\nfor epoch in range(1, EPOCHS + 1):\n # KL annealing (linear over first 50% of epochs)\n model.cfg.kl_weight = min(1.0, epoch / (EPOCHS * 0.5))\n\n model.train()\n total = 0.0\n for (xb,) in loader:\n xb = xb.to(device)\n out = model(xb)\n loss = model.loss(xb, out)[\"loss_total\"]\n opt.zero_grad()\n loss.backward()\n opt.step()\n total += float(loss.item())\n\n if epoch % 10 == 0:\n print(f\"Epoch {epoch}/{EPOCHS} avg loss {total/len(loader):.4f} (\u03b2={model.cfg.kl_weight:.2f})\")\n```\n\n## Device tips (CPU / CUDA / MPS)\n\nRecommended (robust) pattern\n\n1.\tBuild on CPU with a tiny batch: with torch.no_grad(): _ = model(X_t[:2])\n2.\tMove the model: model.to(device)\n3.\tCreate the optimizer after moving: opt = torch.optim.Adam(model.parameters(), lr=...)\n4.\tMove inputs each step: xb = xb.to(device)\n\t\n\nApple MPS\n\n```bash\n# Optional: allow CPU fallback for not-yet-supported ops\nexport PYTORCH_ENABLE_MPS_FALLBACK=1\n```\n\nNon-contiguous groups\n\nIf you used bind_column_groups or bind_feature_names, the model stores index tensors. After model.to(device), they are automatically used on the same device. If you subclass or modify the model, ensure those indices are on device.\n\n## Citation\n\nIf you use this package, please cite:\n\nZhang, R., Zhao, C., Zhao, X., Nie, L., & Lam, W. F. (2025). Structural Equation-VAE: Disentangled Latent Representations for Tabular Data. arXiv preprint arXiv:2508.06347.\n```bash\n@article{zhang2025structural,\n title={Structural Equation-VAE: Disentangled Latent Representations for Tabular Data},\n author={Zhang, Ruiyu and Zhao, Ce and Zhao, Xin and Nie, Lin and Lam, Wai-Fung},\n journal={arXiv preprint arXiv:2508.06347},\n year={2025}\n}\n```\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Structural Equation\u2013VAE (SE-VAE) for interpretable disentangled latents on tabular data (PyTorch)",
"version": "1.0.2",
"project_urls": null,
"split_keywords": [
"disentanglement",
" pytorch",
" representation-learning",
" sem",
" structural-equation-modeling",
" tabular-data",
" vae"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "86fdff4184b8a11359f695f8f0284784a70e0714b3da833d5a7d3b29c7e768b9",
"md5": "42a8e79017deeff308cf8a6d714443da",
"sha256": "2e1e1a50e66564c68b6db911f36a030db7945f60376a9e1f9f23144ba6cf09f5"
},
"downloads": -1,
"filename": "sevae-1.0.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "42a8e79017deeff308cf8a6d714443da",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 9412,
"upload_time": "2025-08-16T10:34:16",
"upload_time_iso_8601": "2025-08-16T10:34:16.583373Z",
"url": "https://files.pythonhosted.org/packages/86/fd/ff4184b8a11359f695f8f0284784a70e0714b3da833d5a7d3b29c7e768b9/sevae-1.0.2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "3d66b0e3555e759707915df88467019c409191f9a5898aa49cdeb5a07dfd844a",
"md5": "ed17de465f0322a5bd26b2d6ef46dff9",
"sha256": "e4be8662a6714f4cceb43ebc7a11410c613706708f4496cfe149c24ffb81470b"
},
"downloads": -1,
"filename": "sevae-1.0.2.tar.gz",
"has_sig": false,
"md5_digest": "ed17de465f0322a5bd26b2d6ef46dff9",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 245911,
"upload_time": "2025-08-16T10:34:21",
"upload_time_iso_8601": "2025-08-16T10:34:21.027688Z",
"url": "https://files.pythonhosted.org/packages/3d/66/b0e3555e759707915df88467019c409191f9a5898aa49cdeb5a07dfd844a/sevae-1.0.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-08-16 10:34:21",
"github": false,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"lcname": "sevae"
}