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

114 lines
3.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Compare multi-task loss balancing strategies on a joint classification + regression task.
The dataset is UCI Wine Quality (red) with two output features:
- ``quality`` — the usual 010 score, trained as number regression.
- ``recommended`` — a synthetic binary target set to ``quality >= 6``, trained as binary
classification. The two outputs share everything except the final decoder head, so they
compete for the combiner's representational capacity.
For each balancer in :data:`STRATEGIES` the script trains the same model end-to-end and
records validation metrics. The summary table prints the per-task scores plus a
balance-aware geometric mean so you can see which strategy gets both tasks right.
Requires Ludwig 0.15 / PR #4092 for ``nash_mtl``.
Run: ``python compare_balancers.py``
"""
from __future__ import annotations
import logging
import math
from pathlib import Path
import pandas as pd
import yaml
from ludwig.api import LudwigModel
from ludwig.datasets import wine_quality
HERE = Path(__file__).parent
# Strategies to compare. nash_mtl is included only on the future-capabilities branch.
STRATEGIES = [
"none",
"log_transform",
"uncertainty",
"famo",
"gradnorm",
"nash_mtl",
]
def add_binary_target(df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
df["recommended"] = (df["quality"] >= 6).astype(int)
return df
def build_config(balancer: str) -> dict:
with (HERE / "config_nash_mtl.yaml").open() as f:
config = yaml.safe_load(f)
config["trainer"]["loss_balancing"] = balancer
return config
def run(balancer: str, dataset: pd.DataFrame) -> dict[str, float]:
config = build_config(balancer)
model = LudwigModel(config=config, logging_level=logging.WARNING)
result = model.train(
dataset=dataset,
output_directory=str(HERE / f"results_{balancer}"),
skip_save_processed_input=True,
skip_save_progress=True,
skip_save_unprocessed_output=True,
skip_save_predictions=True,
skip_save_model=True,
)
val = result.train_stats.validation or {}
quality_rmse = min(val["quality"].get("root_mean_squared_error", [float("nan")]))
recommended_acc = max(val["recommended"].get("accuracy", [float("nan")]))
quality_loss = min(val["quality"].get("loss", [float("nan")]))
recommended_loss = min(val["recommended"].get("loss", [float("nan")]))
# Geometric mean of losses is a balance-aware aggregate: a strategy that wrecks one task
# to win the other pays more than a strategy that keeps both reasonable.
geomean = math.sqrt(quality_loss * recommended_loss) if quality_loss and recommended_loss else float("nan")
return {
"quality_rmse": quality_rmse,
"recommended_acc": recommended_acc,
"geomean_loss": geomean,
}
def main() -> None:
dataset = add_binary_target(wine_quality.load())
rows = []
for balancer in STRATEGIES:
print(f"\n=== Training with loss_balancing: {balancer} ===")
try:
scores = run(balancer, dataset)
except Exception as exc:
print(f"[skip] {balancer}: {exc}")
continue
rows.append({"balancer": balancer, **scores})
if not rows:
raise SystemExit("No balancer runs completed successfully.")
summary = pd.DataFrame(rows).set_index("balancer")
summary = summary.sort_values("geomean_loss")
print("\nResults (best-of-training per task, sorted by geomean_loss):")
print(summary.to_string(float_format=lambda v: f"{v:.4f}"))
csv_path = HERE / "balancer_comparison.csv"
summary.to_csv(csv_path)
print(f"\nWrote {csv_path}")
if __name__ == "__main__":
main()