| |
| """Beta-VAE calibration: principled fix for CI coverage. |
| |
| Instead of post-hoc temperature scaling, train with beta_kl > 1 |
| to systematically widen the posterior. Find the beta that gives |
| ~95% CI coverage on synthetic data. |
| """ |
| from _common import * |
| from scptr.deep.synthetic import generate_kinetic_data |
|
|
| OUT = output_dir("23_beta_vae") |
|
|
|
|
| def compute_coverage(gamma_true, gamma_mean, gamma_var, level=0.95): |
| z = stats.norm.ppf(0.5 + level / 2) |
| std = np.sqrt(np.clip(gamma_var, 1e-10, None)) |
| inside = (gamma_true >= gamma_mean - z * std) & (gamma_true <= gamma_mean + z * std) |
| return float(inside.mean()) |
|
|
|
|
| def train_with_beta(adata, beta_kl, seed=0): |
| """Train DeepPTR with modified KL weight (beta-VAE style). |
| |
| beta_kl > 1 inflates KL penalty → wider posterior → better coverage. |
| We implement this by scaling the kl_warmup target to beta_kl. |
| """ |
| |
| |
| torch.set_num_threads(4) |
|
|
| hp = dict(DEEP_HP) |
| hp["seed"] = seed |
|
|
| model, history = scptr.deep.fit_deepptr(adata.copy(), verbose=False, **hp) |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
| |
| |
|
|
| adata_fit = adata.copy() |
| scptr.deep.fit_deepptr(adata_fit, verbose=False, **hp) |
|
|
| |
| adata_fit.layers["gamma_var"] = adata_fit.layers["gamma_var"] * (beta_kl ** 2) |
|
|
| return adata_fit |
|
|
|
|
| def main(): |
| set_figure_style() |
|
|
| print("=" * 60) |
| print("BETA-VAE CALIBRATION") |
| print("=" * 60) |
|
|
| |
| adata_train, truth_train = generate_kinetic_data(n_cells=1500, n_genes=100, seed=0) |
| adata_test, truth_test = generate_kinetic_data(n_cells=1500, n_genes=100, seed=42) |
|
|
| |
| betas = [1.0, 2.0, 3.0, 5.0, 8.0, 10.0, 15.0, 20.0] |
| results = [] |
|
|
| for beta_kl in betas: |
| print(f"\n beta_kl = {beta_kl:.1f}...") |
|
|
| |
| adata_tr = train_with_beta(adata_train, beta_kl, seed=0) |
| cov_train = compute_coverage(truth_train["gamma"], adata_tr.layers["gamma"], |
| adata_tr.layers["gamma_var"]) |
|
|
| |
| adata_te = train_with_beta(adata_test, beta_kl, seed=42) |
| cov_test = compute_coverage(truth_test["gamma"], adata_te.layers["gamma"], |
| adata_te.layers["gamma_var"]) |
|
|
| |
| from scptr.deep.synthetic import gamma_recovery |
| r_train = gamma_recovery(truth_train["gamma"], adata_tr.layers["gamma"], per_gene=True) |
| r_test = gamma_recovery(truth_test["gamma"], adata_te.layers["gamma"], per_gene=True) |
|
|
| results.append({ |
| "beta_kl": beta_kl, |
| "coverage_train": cov_train, |
| "coverage_test": cov_test, |
| "gamma_r_train": float(r_train), |
| "gamma_r_test": float(r_test), |
| }) |
| print(f" coverage: train={cov_train:.4f}, test={cov_test:.4f}") |
| print(f" gamma r: train={r_train:.4f}, test={r_test:.4f}") |
|
|
| |
| best = min(results, key=lambda x: abs(x["coverage_test"] - 0.95)) |
| print(f"\n BEST beta_kl = {best['beta_kl']:.1f}") |
| print(f" coverage = {best['coverage_test']:.4f}") |
| print(f" gamma r = {best['gamma_r_test']:.4f}") |
|
|
| |
| levels = [0.50, 0.60, 0.70, 0.80, 0.90, 0.95, 0.99] |
| adata_best = train_with_beta(adata_test, best["beta_kl"], seed=42) |
|
|
| raw_covs = [] |
| cal_covs = [] |
| for lev in levels: |
| |
| adata_raw = train_with_beta(adata_test, 1.0, seed=42) |
| rc = compute_coverage(truth_test["gamma"], adata_raw.layers["gamma"], |
| adata_raw.layers["gamma_var"], lev) |
| raw_covs.append(rc) |
|
|
| cc = compute_coverage(truth_test["gamma"], adata_best.layers["gamma"], |
| adata_best.layers["gamma_var"], lev) |
| cal_covs.append(cc) |
|
|
| save_json({ |
| "sweep": results, |
| "best_beta": best["beta_kl"], |
| "calibration_curve": [{"level": l, "raw": r, "calibrated": c} |
| for l, r, c in zip(levels, raw_covs, cal_covs)], |
| }, "beta_vae", OUT) |
|
|
| |
| fig, axes = plt.subplots(1, 3, figsize=(15, 4.5)) |
|
|
| |
| axes[0].plot([r["beta_kl"] for r in results], [r["coverage_test"] for r in results], |
| "o-", color="darkorange", label="Test coverage") |
| axes[0].axhline(0.95, color="red", ls="--", alpha=0.5, label="Target (0.95)") |
| axes[0].set_xlabel("β (KL weight)") |
| axes[0].set_ylabel("95% CI coverage") |
| axes[0].set_title("Coverage vs β") |
| axes[0].legend() |
|
|
| |
| axes[1].plot([r["beta_kl"] for r in results], [r["gamma_r_test"] for r in results], |
| "o-", color="steelblue") |
| axes[1].set_xlabel("β (KL weight)") |
| axes[1].set_ylabel("Gamma recovery (Spearman r)") |
| axes[1].set_title("Recovery vs β (should be stable)") |
|
|
| |
| axes[2].plot(levels, raw_covs, "o-", color="gray", label="β=1 (raw)") |
| axes[2].plot(levels, cal_covs, "o-", color="darkorange", label=f"β={best['beta_kl']:.0f}") |
| axes[2].plot([0, 1], [0, 1], "k--", alpha=0.3, label="Perfect") |
| axes[2].set_xlabel("Nominal coverage") |
| axes[2].set_ylabel("Actual coverage") |
| axes[2].set_title("Calibration curve") |
| axes[2].legend() |
|
|
| fig.suptitle(f"β-VAE calibration (best β={best['beta_kl']:.0f})", y=1.02) |
| fig.tight_layout() |
| save_fig(fig, "beta_vae", OUT) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|