Files
ludwig-ai--ludwig/examples/hypernetwork/hypernetwork.ipynb
T
wehub-resource-sync 593b94c120
pytest / Unit Tests (push) Waiting to run
pytest / Integration (integration_tests_a) (push) Waiting to run
pytest / Integration (integration_tests_b) (push) Waiting to run
pytest / Integration (integration_tests_c) (push) Waiting to run
pytest / Integration (integration_tests_d) (push) Waiting to run
pytest / Integration (integration_tests_e) (push) Waiting to run
pytest / Integration (integration_tests_f) (push) Waiting to run
pytest / Integration (integration_tests_g) (push) Waiting to run
pytest / Integration (integration_tests_h) (push) Waiting to run
pytest / Integration (integration_tests_i) (push) Waiting to run
pytest / Integration (integration_tests_j) (push) Waiting to run
pytest / Distributed (distributed_a) (push) Waiting to run
pytest / Distributed (distributed_b) (push) Waiting to run
pytest / Distributed (distributed_c) (push) Waiting to run
pytest / Distributed (distributed_d) (push) Waiting to run
pytest / Distributed (distributed_e) (push) Waiting to run
pytest / Distributed (distributed_f) (push) Waiting to run
pytest / Minimal Install (push) Waiting to run
pytest / Event File (push) Waiting to run
pytest (slow) / py-slow (push) Waiting to run
Publish JSON Schema / publish-schema (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:49:20 +08:00

428 lines
16 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"id": "a1b2c3d4-0001-0000-0000-000000000001",
"metadata": {},
"source": [
"# HyperNetworkCombiner: Conditional Feature Processing\n",
"\n",
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/hypernetwork/hypernetwork.ipynb)\n",
"\n",
"> **Note:** This notebook requires **Ludwig >= 0.14** (PR #4092). The `hypernetwork`\n",
"> combiner type is not available in earlier releases. Install with:\n",
"> `pip install \"ludwig>=0.14\"`\n",
"\n",
"This notebook demonstrates the `HyperNetworkCombiner`, which lets one feature\n",
"(**the conditioning feature**) generate the weights of the layers that process all\n",
"other features — rather than simply concatenating everyone together.\n",
"\n",
"Based on **HyperFusion** ([arXiv 2403.13319](https://arxiv.org/abs/2403.13319), 2024).\n",
"\n",
"**What we cover:**\n",
"\n",
"1. Why concatenation is not always enough\n",
"2. Generating a synthetic multi-modal sensor dataset\n",
"3. Baseline: concat combiner\n",
"4. HyperNetworkCombiner\n",
"5. Comparing results and understanding why hypernetwork wins"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a1b2c3d4-0002-0000-0000-000000000002",
"metadata": {},
"outputs": [],
"source": [
"!pip install \"ludwig>=0.14\" --quiet"
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0003-0000-0000-000000000003",
"metadata": {},
"source": [
"## The problem: context-dependent features\n",
"\n",
"Imagine a network of industrial sensors. Each sensor reports three readings\n",
"(`sensor_a`, `sensor_b`, `sensor_c`), but every sensor belongs to one of three\n",
"measurement types: **temperature**, **pressure**, or **humidity**.\n",
"\n",
"The catch: **the same numerical reading means something completely different**\n",
"depending on the sensor type.\n",
"\n",
"- For a **temperature** sensor, `sensor_a = 3.0` is an anomaly (overheating).\n",
"- For a **pressure** sensor, `sensor_a = 3.0` is perfectly normal.\n",
"- For a **humidity** sensor, the anomaly rule involves the *sum* of all three readings.\n",
"\n",
"A concat combiner encodes all four features independently and then stitches them\n",
"together. The network has to learn — **after** the concatenation — to undo the mixing\n",
"and apply type-specific logic. This is hard because the critical signal (`sensor_type`)\n",
"is buried in a shared representation alongside the numerical readings.\n",
"\n",
"The `hypernetwork` combiner solves this directly:\n",
"\n",
"```\n",
"sensor_type ──► HyperNetwork ──► generates weights W, b\n",
" │\n",
"sensor_a ────────────────────► FC(W, b) ──►\n",
"sensor_b ────────────────────► FC(W, b) ──► combined repr.\n",
"sensor_c ────────────────────► FC(W, b) ──►\n",
"```\n",
"\n",
"`sensor_type` does not contribute a feature vector — it **rewrites the entire\n",
"transformation** applied to the numerical sensors."
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0004-0000-0000-000000000004",
"metadata": {},
"source": [
"## Dataset\n",
"\n",
"We generate a synthetic dataset with three sensor types. Each type has its own\n",
"normal operating range and its own anomaly rule, making the sensor type an\n",
"essential piece of context for correct classification."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a1b2c3d4-0005-0000-0000-000000000005",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"RNG = np.random.default_rng(42)\n",
"\n",
"N_PER_TYPE = 600\n",
"SENSOR_TYPES = [\"temperature\", \"pressure\", \"humidity\"]\n",
"\n",
"\n",
"def make_samples(sensor_type: str, n: int, rng: np.random.Generator) -> pd.DataFrame:\n",
" \"\"\"Generate n samples for a single sensor type with type-specific anomaly rules.\"\"\"\n",
" if sensor_type == \"temperature\":\n",
" # Normal: readings near 0; anomaly: sensor_a > 2.5 (overheating)\n",
" sensor_a = rng.normal(0.0, 1.0, n)\n",
" sensor_b = rng.normal(0.0, 1.0, n)\n",
" sensor_c = rng.normal(0.0, 1.0, n)\n",
" anomaly = (sensor_a > 2.5).astype(int)\n",
" elif sensor_type == \"pressure\":\n",
" # Normal: readings near 1; anomaly: sensor_b drops below -0.5 (leak)\n",
" sensor_a = rng.normal(1.0, 0.8, n)\n",
" sensor_b = rng.normal(1.0, 0.8, n)\n",
" sensor_c = rng.normal(1.0, 0.8, n)\n",
" anomaly = (sensor_b < -0.5).astype(int)\n",
" else: # humidity\n",
" # Normal: readings near -1; anomaly: combined level exceeds threshold\n",
" sensor_a = rng.normal(-1.0, 0.9, n)\n",
" sensor_b = rng.normal(-1.0, 0.9, n)\n",
" sensor_c = rng.normal(-1.0, 0.9, n)\n",
" anomaly = ((sensor_a + sensor_b + sensor_c) > 0).astype(int)\n",
"\n",
" return pd.DataFrame(\n",
" {\n",
" \"sensor_a\": sensor_a,\n",
" \"sensor_b\": sensor_b,\n",
" \"sensor_c\": sensor_c,\n",
" \"sensor_type\": sensor_type,\n",
" \"anomaly\": anomaly,\n",
" }\n",
" )\n",
"\n",
"\n",
"frames = [make_samples(t, N_PER_TYPE, RNG) for t in SENSOR_TYPES]\n",
"df = pd.concat(frames, ignore_index=True).sample(frac=1, random_state=42).reset_index(drop=True)\n",
"\n",
"# Train / validation / test split (70 / 15 / 15)\n",
"n = len(df)\n",
"split = np.full(n, 2, dtype=int) # default: test\n",
"idx = np.arange(n)\n",
"RNG.shuffle(idx)\n",
"split[idx[: int(0.70 * n)]] = 0\n",
"split[idx[int(0.70 * n) : int(0.85 * n)]] = 1\n",
"df[\"split\"] = split\n",
"\n",
"print(f\"Total rows: {n}\")\n",
"print(f\"Overall anomaly rate: {df['anomaly'].mean():.1%}\")\n",
"print()\n",
"print(\"Anomaly rate by sensor type:\")\n",
"print(df.groupby(\"sensor_type\")[\"anomaly\"].mean().rename(\"anomaly_rate\"))\n",
"print()\n",
"df.head(10)"
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0006-0000-0000-000000000006",
"metadata": {},
"source": [
"Notice that the three sensor types have **different anomaly rates** and, more\n",
"importantly, the anomaly rules are structurally different. The same value of\n",
"`sensor_a = 3.0` triggers an anomaly for `temperature` but not for `pressure`.\n",
"A model that treats all features symmetrically will struggle with this."
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0007-0000-0000-000000000007",
"metadata": {},
"source": [
"## Baseline: concat combiner\n",
"\n",
"The default Ludwig combiner concatenates all encoder outputs and passes them\n",
"through fully-connected layers. `sensor_type` is just another input — its\n",
"embedding is concatenated alongside the numerical sensor values."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a1b2c3d4-0008-0000-0000-000000000008",
"metadata": {},
"outputs": [],
"source": [
"import yaml\n",
"\n",
"from ludwig.api import LudwigModel\n",
"\n",
"config_concat_str = \"\"\"\n",
"model_type: ecd\n",
"\n",
"input_features:\n",
" - name: sensor_a\n",
" type: number\n",
" preprocessing:\n",
" normalization: zscore\n",
" - name: sensor_b\n",
" type: number\n",
" preprocessing:\n",
" normalization: zscore\n",
" - name: sensor_c\n",
" type: number\n",
" preprocessing:\n",
" normalization: zscore\n",
" - name: sensor_type\n",
" type: category\n",
"\n",
"output_features:\n",
" - name: anomaly\n",
" type: binary\n",
"\n",
"combiner:\n",
" type: concat\n",
" fc_layers:\n",
" - output_size: 128\n",
" - output_size: 64\n",
"\n",
"trainer:\n",
" epochs: 30\n",
" learning_rate: 0.001\n",
"\"\"\"\n",
"\n",
"config_concat = yaml.safe_load(config_concat_str)\n",
"\n",
"model_concat = LudwigModel(config_concat, logging_level=30)\n",
"model_concat.train(dataset=df)\n",
"\n",
"test_df = df[df[\"split\"] == 2].copy()\n",
"preds_concat, _ = model_concat.predict(dataset=test_df)\n",
"\n",
"acc_concat = (preds_concat[\"anomaly_predictions\"].values == test_df[\"anomaly\"].values).mean()\n",
"print(f\"Concat combiner — test accuracy: {acc_concat:.4f}\")"
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0009-0000-0000-000000000009",
"metadata": {},
"source": [
"## HyperNetworkCombiner\n",
"\n",
"Now we switch to `type: hypernetwork`. The combiner reads `sensor_type` (the last\n",
"feature listed in `input_features`) through a hyper-network and uses the output to\n",
"generate the weight matrix and bias of the layer that processes `sensor_a`,\n",
"`sensor_b`, and `sensor_c`.\n",
"\n",
"Key parameters:\n",
"\n",
"| Parameter | Role |\n",
"|---|---|\n",
"| `hidden_size` | Size of the main processing layer |\n",
"| `hyper_hidden_size` | Hidden size of the hyper-network itself |\n",
"| `output_size` | Dimension of the combined representation passed to decoders |"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a1b2c3d4-0010-0000-0000-000000000010",
"metadata": {},
"outputs": [],
"source": [
"config_hypernetwork_str = \"\"\"\n",
"model_type: ecd\n",
"\n",
"input_features:\n",
" - name: sensor_a\n",
" type: number\n",
" preprocessing:\n",
" normalization: zscore\n",
" - name: sensor_b\n",
" type: number\n",
" preprocessing:\n",
" normalization: zscore\n",
" - name: sensor_c\n",
" type: number\n",
" preprocessing:\n",
" normalization: zscore\n",
" - name: sensor_type\n",
" type: category\n",
"\n",
"output_features:\n",
" - name: anomaly\n",
" type: binary\n",
"\n",
"combiner:\n",
" type: hypernetwork\n",
" hidden_size: 128\n",
" hyper_hidden_size: 64\n",
" output_size: 128\n",
"\n",
"trainer:\n",
" epochs: 30\n",
" learning_rate: 0.001\n",
"\"\"\"\n",
"\n",
"config_hypernetwork = yaml.safe_load(config_hypernetwork_str)\n",
"\n",
"model_hypernetwork = LudwigModel(config_hypernetwork, logging_level=30)\n",
"model_hypernetwork.train(dataset=df)\n",
"\n",
"preds_hyper, _ = model_hypernetwork.predict(dataset=test_df)\n",
"\n",
"acc_hyper = (preds_hyper[\"anomaly_predictions\"].values == test_df[\"anomaly\"].values).mean()\n",
"print(f\"HyperNetworkCombiner — test accuracy: {acc_hyper:.4f}\")"
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0011-0000-0000-000000000011",
"metadata": {},
"source": [
"## Comparison"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a1b2c3d4-0012-0000-0000-000000000012",
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"\n",
"# Per-type breakdown\n",
"rows = []\n",
"for stype in SENSOR_TYPES:\n",
" mask = test_df[\"sensor_type\"] == stype\n",
" true = test_df.loc[mask, \"anomaly\"].values\n",
"\n",
" acc_c = (preds_concat.loc[mask, \"anomaly_predictions\"].values == true).mean()\n",
" acc_h = (preds_hyper.loc[mask, \"anomaly_predictions\"].values == true).mean()\n",
" rows.append({\"sensor_type\": stype, \"concat\": round(acc_c, 4), \"hypernetwork\": round(acc_h, 4)})\n",
"\n",
"rows.append({\"sensor_type\": \"OVERALL\", \"concat\": round(acc_concat, 4), \"hypernetwork\": round(acc_hyper, 4)})\n",
"\n",
"results_df = pd.DataFrame(rows)\n",
"print(results_df.to_string(index=False))\n",
"\n",
"# Bar chart\n",
"fig, ax = plt.subplots(figsize=(8, 4))\n",
"x = np.arange(len(rows))\n",
"width = 0.35\n",
"ax.bar(x - width / 2, results_df[\"concat\"], width, label=\"Concat\", color=\"steelblue\")\n",
"ax.bar(x + width / 2, results_df[\"hypernetwork\"], width, label=\"HyperNetwork\", color=\"darkorange\")\n",
"ax.set_xticks(x)\n",
"ax.set_xticklabels(results_df[\"sensor_type\"])\n",
"ax.set_ylabel(\"Test accuracy\")\n",
"ax.set_ylim(0.5, 1.0)\n",
"ax.set_title(\"Sensor anomaly detection: concat vs HyperNetworkCombiner\")\n",
"ax.legend()\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0013-0000-0000-000000000013",
"metadata": {},
"source": [
"## Why hypernetwork wins\n",
"\n",
"### The problem with concatenation\n",
"\n",
"When we use `concat`, the model receives a vector like:\n",
"\n",
"```\n",
"[enc(sensor_a), enc(sensor_b), enc(sensor_c), enc(sensor_type)]\n",
"```\n",
"\n",
"The fully-connected layers after the concat have to learn — from scratch — that\n",
"`enc(sensor_type)` should *gate* the interpretation of the numerical sensors.\n",
"Effectively the network must implement a conditional logic as a series of\n",
"multiplications and additions over the entire concatenated vector. This is\n",
"theoretically possible but inefficient: many capacity-bearing parameters in the\n",
"FC layers end up implementing the routing rather than the actual anomaly detection.\n",
"\n",
"### What hypernetwork does instead\n",
"\n",
"The `HyperNetworkCombiner` separates the two roles explicitly:\n",
"\n",
"1. **Hyper-network** — a small MLP that reads `sensor_type` and emits a vector\n",
" of weights `W` and biases `b`.\n",
"2. **Main network** — a linear layer `FC(W, b)` applied to the concatenation of\n",
" `[sensor_a, sensor_b, sensor_c]` using the *dynamically generated* `W` and `b`.\n",
"\n",
"Because `W` and `b` are different for each sensor type, the transformation\n",
"applied to the numerical sensors is literally different per context. For\n",
"`temperature`, the generated `W` learns to make `sensor_a` highly predictive;\n",
"for `pressure`, the generated `W` shifts attention to `sensor_b`; for\n",
"`humidity`, it learns to combine all three.\n",
"\n",
"### When to use it\n",
"\n",
"- The conditioning feature is a **type, class, mode, or context** that changes\n",
" the semantics of other features qualitatively.\n",
"- You have enough samples per conditioning category (roughly 200+ per class) to\n",
" learn meaningful per-context transformations.\n",
"- The target signal requires different logic for different contexts, not just\n",
" different magnitudes.\n",
"\n",
"### When to stick with concat\n",
"\n",
"- All features contribute on equal footing with no hierarchical conditioning.\n",
"- The dataset is very small — the hyper-network adds parameters.\n",
"- The extra complexity is not warranted (always start simple)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.12.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}