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

340 lines
10 KiB
Python

#!/usr/bin/env python
"""
Uncertainty Quantification with Ludwig: MC Dropout and Temperature Scaling.
Trains three models on the UCI Wine Quality dataset and compares:
1. Baseline — no calibration
2. Temperature Scaling — post-hoc calibration via a learned temperature scalar
3. MC Dropout — per-sample uncertainty estimates via stochastic inference
Usage:
python train.py
"""
import logging
import os
import shutil
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from ludwig.api import LudwigModel
logging.basicConfig(level=logging.WARNING)
# ---------------------------------------------------------------------------
# Dataset
# ---------------------------------------------------------------------------
WINE_URL = "https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv"
WINE_FEATURES = [
"fixed_acidity",
"volatile_acidity",
"citric_acid",
"residual_sugar",
"chlorides",
"free_sulfur_dioxide",
"total_sulfur_dioxide",
"density",
"pH",
"sulphates",
"alcohol",
]
def load_dataset() -> pd.DataFrame:
"""Download and prepare the wine quality dataset."""
print("Downloading wine quality dataset...")
df = pd.read_csv(WINE_URL, sep=";")
# Rename columns: replace spaces with underscores
df.columns = [c.replace(" ", "_") for c in df.columns]
# Binarise: quality >= 7 is "good" (True), otherwise "bad" (False)
df["quality"] = (df["quality"] >= 7).astype(int)
print(f" {len(df)} rows | positive class (quality>=7): {df['quality'].mean():.1%}")
return df
# ---------------------------------------------------------------------------
# Ludwig configs
# ---------------------------------------------------------------------------
def _input_features() -> list:
return [{"name": feat, "type": "number", "preprocessing": {"normalization": "zscore"}} for feat in WINE_FEATURES]
BASE_CONFIG = {
"model_type": "ecd",
"input_features": _input_features(),
"output_features": [
{
"name": "quality",
"type": "binary",
"decoder": {
"type": "mlp_classifier",
"num_fc_layers": 1,
"output_size": 64,
"dropout": 0.1,
},
"loss": {"type": "binary_weighted_cross_entropy"},
}
],
"combiner": {
"type": "concat",
"num_fc_layers": 2,
"output_size": 128,
"dropout": 0.1,
},
"trainer": {"epochs": 30, "learning_rate": 0.001, "batch_size": 128},
}
CALIBRATED_CONFIG = {
**BASE_CONFIG,
"output_features": [
{
**BASE_CONFIG["output_features"][0],
"decoder": {
**BASE_CONFIG["output_features"][0]["decoder"],
"calibration": "temperature_scaling",
},
}
],
}
MC_DROPOUT_CONFIG = {
**BASE_CONFIG,
"output_features": [
{
**BASE_CONFIG["output_features"][0],
"decoder": {
**BASE_CONFIG["output_features"][0]["decoder"],
"dropout": 0.3,
"mc_dropout_samples": 20,
},
}
],
"combiner": {
**BASE_CONFIG["combiner"],
"dropout": 0.2,
},
}
# ---------------------------------------------------------------------------
# Calibration metrics
# ---------------------------------------------------------------------------
def expected_calibration_error(
probabilities: np.ndarray,
labels: np.ndarray,
n_bins: int = 10,
) -> float:
"""Compute Expected Calibration Error (ECE).
Args:
probabilities: Predicted probability for the positive class, shape (N,).
labels: Ground-truth binary labels, shape (N,).
n_bins: Number of equally-spaced confidence bins.
Returns:
ECE as a float in [0, 1].
"""
bins = np.linspace(0.0, 1.0, n_bins + 1)
ece = 0.0
n = len(probabilities)
for lo, hi in zip(bins[:-1], bins[1:]):
mask = (probabilities >= lo) & (probabilities < hi)
if mask.sum() == 0:
continue
conf = probabilities[mask].mean()
acc = labels[mask].mean()
ece += mask.sum() / n * abs(conf - acc)
return float(ece)
def reliability_diagram(
probabilities_dict: dict,
labels: np.ndarray,
n_bins: int = 10,
output_path: str | None = None,
) -> None:
"""Plot reliability diagrams for one or more models.
Args:
probabilities_dict: Mapping from model name to probability arrays.
labels: Ground-truth binary labels.
n_bins: Number of confidence bins.
output_path: If provided, save the figure to this path.
"""
bins = np.linspace(0.0, 1.0, n_bins + 1)
bin_centers = 0.5 * (bins[:-1] + bins[1:])
fig, ax = plt.subplots(figsize=(6, 6))
ax.plot([0, 1], [0, 1], "k--", label="Perfect calibration", linewidth=1.5)
colors = ["tab:red", "tab:blue", "tab:green"]
for (name, probs), color in zip(probabilities_dict.items(), colors):
accs = []
for lo, hi in zip(bins[:-1], bins[1:]):
mask = (probs >= lo) & (probs < hi)
if mask.sum() == 0:
accs.append(float("nan"))
else:
accs.append(labels[mask].mean())
ece = expected_calibration_error(probs, labels, n_bins)
ax.plot(
bin_centers,
accs,
marker="o",
label=f"{name} (ECE={ece:.3f})",
color=color,
)
ax.set_xlabel("Mean predicted probability")
ax.set_ylabel("Fraction of positives")
ax.set_title("Reliability Diagram")
ax.legend(loc="upper left")
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
plt.tight_layout()
if output_path:
os.makedirs(os.path.dirname(output_path), exist_ok=True)
plt.savefig(output_path, dpi=150)
print(f" Saved reliability diagram to {output_path}")
else:
plt.show()
plt.close(fig)
def uncertainty_histogram(
uncertainty: np.ndarray,
output_path: str | None = None,
) -> None:
"""Plot distribution of MC Dropout uncertainty estimates."""
fig, ax = plt.subplots(figsize=(6, 4))
ax.hist(uncertainty, bins=40, edgecolor="white", color="tab:green")
ax.set_xlabel("Uncertainty (variance across MC samples)")
ax.set_ylabel("Count")
ax.set_title("MC Dropout Uncertainty Distribution")
plt.tight_layout()
if output_path:
os.makedirs(os.path.dirname(output_path), exist_ok=True)
plt.savefig(output_path, dpi=150)
print(f" Saved uncertainty histogram to {output_path}")
else:
plt.show()
plt.close(fig)
# ---------------------------------------------------------------------------
# Training helpers
# ---------------------------------------------------------------------------
def train_and_evaluate(
name: str,
config: dict,
dataset: pd.DataFrame,
output_dir: str,
) -> tuple[LudwigModel, pd.DataFrame, np.ndarray]:
"""Train a Ludwig model and return predictions on the test split.
Returns:
(model, predictions_df, labels)
"""
result_dir = os.path.join(output_dir, name)
shutil.rmtree(result_dir, ignore_errors=True)
print(f"\n--- Training: {name} ---")
model = LudwigModel(config=config, logging_level=logging.WARNING)
model.train(
dataset=dataset,
experiment_name="uncertainty",
model_name=name,
output_directory=result_dir,
)
print(f" Evaluating {name}...")
_, predictions, _ = model.predict(dataset=dataset)
return model, predictions
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
output_dir = "./results"
viz_dir = "./visualizations"
os.makedirs(viz_dir, exist_ok=True)
# Load dataset
df = load_dataset()
labels = df["quality"].values.astype(int)
# Train models
_, baseline_preds = train_and_evaluate("baseline", BASE_CONFIG, df, output_dir)
_, calibrated_preds = train_and_evaluate("calibrated", CALIBRATED_CONFIG, df, output_dir)
mc_model, mc_preds = train_and_evaluate("mc_dropout", MC_DROPOUT_CONFIG, df, output_dir)
# Extract probabilities
baseline_probs = baseline_preds["quality_probability_True"].values
calibrated_probs = calibrated_preds["quality_probability_True"].values
mc_probs = mc_preds["quality_probability_True"].values
# Compute ECE
baseline_ece = expected_calibration_error(baseline_probs, labels)
calibrated_ece = expected_calibration_error(calibrated_probs, labels)
mc_ece = expected_calibration_error(mc_probs, labels)
print("\n=== Expected Calibration Error (ECE) ===")
print(f" Baseline: ECE = {baseline_ece:.4f}")
print(f" Temperature Scaling: ECE = {calibrated_ece:.4f}")
print(f" MC Dropout: ECE = {mc_ece:.4f}")
print()
# Reliability diagram
reliability_diagram(
{
"Baseline": baseline_probs,
"Temperature Scaling": calibrated_probs,
},
labels,
output_path=os.path.join(viz_dir, "reliability_diagram.png"),
)
# MC Dropout uncertainty
if "quality_uncertainty" in mc_preds.columns:
uncertainty = mc_preds["quality_uncertainty"].values
print("MC Dropout uncertainty stats:")
print(f" mean={uncertainty.mean():.4f}, std={uncertainty.std():.4f}, max={uncertainty.max():.4f}")
uncertainty_histogram(
uncertainty,
output_path=os.path.join(viz_dir, "mc_dropout_uncertainty.png"),
)
# Show high-uncertainty predictions
threshold = np.percentile(uncertainty, 80)
high_unc_mask = uncertainty >= threshold
print(f"\nHigh-uncertainty samples (top 20%, threshold={threshold:.4f}):")
high_unc_preds = mc_preds["quality_predictions"].values[high_unc_mask].astype(bool)
high_unc_labels = labels[high_unc_mask].astype(bool)
high_unc_acc = (high_unc_preds == high_unc_labels).mean()
print(f" count={high_unc_mask.sum()}, accuracy on these samples: {high_unc_acc:.2%}")
else:
print("Note: 'quality_uncertainty' column not found in predictions.")
print("Make sure mc_dropout_samples > 0 and the decoder has dropout > 0.")
print(f"\nDone. Plots saved to {viz_dir}/")
if __name__ == "__main__":
main()