ludwig-ai--ludwig
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
114 行
3.6 KiB
Python
114 行
3.6 KiB
Python
"""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 0–10 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()
|