SabaPivot's picture
download
raw
11.1 kB
"""Claim 1 -- Def 2.1 / Eqs (3)-(4) safe KL divergence, and Prop 2.4:
the induced approximation satisfies (as printed in the paper) F_rho - O(rho) <= F <= F_rho.
Independent tests:
T0 f_rho is a legal f-divergence generator (convex, finite exactly on [0,1/rho]),
and f_rho -> f_0 = t log t + 1 - t as rho -> 0.
T1 Lemma 2.3: (1/rho) log(1+rho e^s) - 1 really is the convex conjugate of f_rho
(brute-force sup_t s t - f_rho(t) on a fine grid + Newton).
T2 Duality (5) <-> (6): F_rho from the dual formula equals the *primal* value
sup_nu { <phi,nu> - D_rho(nu,mu) } computed by constrained convex optimisation
over the simplex with the hard density bound dnu/dmu <= 1/rho, and the
maximiser matches the closed form e^{phi-a*}/(1+rho e^{phi-a*}).
T3 Prop 2.4(i) monotonicity in rho, (ii) F_rho -> F as rho -> 0 with fitted rate.
T4 DIRECTION AUDIT of the printed sandwich F_rho - O(rho) <= F <= F_rho.
T5 Prop 2.4(iii) and (iv) explicit lower bounds, inside and outside their stated
admissible rho-ranges (boundary audit).
T6 Corollary 2.5 (LogSumExp form) over random inputs.
"""
import json
import os
import sys
import numpy as np
from scipy.optimize import minimize
from scipy.special import logsumexp
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from common import (
F_rho,
F_rho_bruteforce,
F_true,
alpha_star,
d2_f_rho,
d_f_rho,
f_rho,
f_rho_star,
)
OUT = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "outputs"
)
os.makedirs(OUT, exist_ok=True)
SEED = 20260725
res = {"seed": SEED}
rng = np.random.default_rng(SEED)
# ---------------------------------------------------------------- T0
rhos = [1e-4, 1e-3, 1e-2, 0.03, 0.1, 0.3, 0.5, 0.9]
t0 = {
"convexity_min_second_deriv": {},
"domain_ok": True,
"limit_to_f0_max_abs_err": {},
}
for rho in rhos:
tt = np.linspace(1e-9, 1.0 / rho - 1e-9, 200001)
d2 = d2_f_rho(tt, rho)
t0["convexity_min_second_deriv"][str(rho)] = float(d2.min())
if not np.isinf(f_rho(np.array([1.0 / rho + 1e-6]), rho)[0]):
t0["domain_ok"] = False
for rho in [1e-3, 1e-4, 1e-5, 1e-6]:
tt = np.linspace(1e-6, 5.0, 20001)
f0 = tt * np.log(tt) + 1 - tt
t0["limit_to_f0_max_abs_err"][str(rho)] = float(np.max(np.abs(f_rho(tt, rho) - f0)))
res["T0_generator"] = t0
# ---------------------------------------------------------------- T1 conjugate
t1 = {"max_abs_err": 0.0, "n_checks": 0}
for rho in rhos:
for s in np.linspace(-20, 20, 81):
tt = np.linspace(1e-12, 1.0 / rho * (1 - 1e-12), 400001)
sup = np.max(s * tt - f_rho(tt, rho))
# refine with the analytic stationary point t = e^s/(1+rho e^s)
tstar = np.exp(s - np.logaddexp(0.0, s + np.log(rho))) / 1.0
tstar = min(max(tstar, 1e-14), 1.0 / rho - 1e-14)
sup = max(sup, float(s * tstar - f_rho(np.array([tstar]), rho)[0]))
err = abs(sup - float(f_rho_star(np.array([s]), rho)[0]))
t1["max_abs_err"] = max(t1["max_abs_err"], err)
t1["n_checks"] += 1
res["T1_lemma23_conjugate"] = t1
# ---------------------------------------------------------------- T2 primal-dual
t2 = {"cases": [], "max_abs_gap": 0.0, "max_density_err": 0.0}
for trial in range(12):
n = int(rng.integers(4, 8))
phi = rng.normal(0, 2.0, size=n)
w = rng.dirichlet(np.ones(n))
rho = float(rng.choice([0.03, 0.1, 0.3, 0.5]))
dual, a = F_rho(phi, rho, w)
def neg(r):
return -(np.sum(w * phi * r) - np.sum(w * f_rho(r, rho)))
cons = [{"type": "eq", "fun": lambda r: np.sum(w * r) - 1.0}]
bnds = [(1e-10, 1.0 / rho - 1e-10)] * n
best, bestr = -np.inf, None
for _ in range(6):
r0 = rng.uniform(1e-3, 1.0 / rho, size=n)
r0 = r0 / np.sum(w * r0)
r0 = np.clip(r0, 1e-9, 1.0 / rho - 1e-9)
try:
sol = minimize(
neg,
r0,
bounds=bnds,
constraints=cons,
method="SLSQP",
options={"maxiter": 800, "ftol": 1e-14},
)
if sol.success and -sol.fun > best:
best, bestr = -sol.fun, sol.x
except Exception:
pass
closed = np.exp(phi - a) / (1.0 + rho * np.exp(phi - a))
t2["cases"].append(
{
"n": n,
"rho": rho,
"dual_Frho": dual,
"primal_sup": float(best),
"abs_gap": abs(dual - best),
"density_max_abs_err": float(np.max(np.abs(closed - bestr))),
"density_max": float(closed.max()),
"one_over_rho": 1.0 / rho,
}
)
t2["max_abs_gap"] = max(t2["max_abs_gap"], abs(dual - best))
t2["max_density_err"] = max(
t2["max_density_err"], float(np.max(np.abs(closed - bestr)))
)
res["T2_primal_dual"] = t2
# ---------------------------------------------------------------- random instances
def random_instance(rng, kind=None):
kind = kind or rng.choice(["gauss", "heavy", "spiky", "uniform", "bimodal"])
n = int(rng.integers(5, 400))
if kind == "gauss":
phi = rng.normal(0, rng.uniform(0.2, 4.0), size=n)
elif kind == "heavy":
phi = rng.standard_t(2.0, size=n) * rng.uniform(0.5, 3.0)
elif kind == "spiky":
phi = rng.normal(0, 0.3, size=n)
phi[0] += rng.uniform(3, 12)
elif kind == "uniform":
phi = rng.uniform(-6, 6, size=n)
else:
phi = np.concatenate(
[rng.normal(-3, 0.5, n // 2), rng.normal(3, 0.5, n - n // 2)]
)
w = rng.dirichlet(np.ones(n) * rng.uniform(0.3, 3.0))
return phi, w, str(kind)
# ---------------------------------------------------------------- T3 monotone + limit
rho_grid = np.array([0.5, 0.3, 0.1, 0.03, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6])
mono_viol, n_mono = 0, 0
rate_fits = []
for trial in range(400):
phi, w, _ = random_instance(rng)
vals = [F_rho(phi, r, w)[0] for r in rho_grid]
for i in range(len(rho_grid) - 1): # rho_grid decreasing
n_mono += 1
if not (vals[i] <= vals[i + 1] + 1e-10): # F_{rho'} <= F_rho for rho <= rho'
mono_viol += 1
Ftrue = F_true(phi, w)
gaps = np.array([Ftrue - v for v in vals])
small = rho_grid <= 1e-2
p = np.polyfit(np.log(rho_grid[small]), np.log(np.maximum(gaps[small], 1e-300)), 1)
rate_fits.append(float(p[0]))
if trial < 200:
rate_fits[-1] = float(p[0])
res["T3_monotone_and_limit"] = {
"n_pairs": n_mono,
"monotonicity_violations": mono_viol,
"gap_rate_exponent_mean": float(np.mean(rate_fits)),
"gap_rate_exponent_std": float(np.std(rate_fits)),
"gap_rate_exponent_min": float(np.min(rate_fits)),
"gap_rate_exponent_max": float(np.max(rate_fits)),
"note": "exponent of (F - F_rho) ~ rho^p fitted on rho <= 1e-2",
}
# ---------------------------------------------------------------- T4 DIRECTION AUDIT
audit = {
"n": 0,
"F_le_Frho_holds": 0,
"Frho_le_F_holds": 0,
"max_(F - Frho)": -np.inf,
"min_(F - Frho)": np.inf,
"examples": [],
}
for trial in range(3000):
phi, w, kind = random_instance(rng)
rho = float(rng.choice([1e-4, 1e-3, 1e-2, 0.03, 0.1, 0.3, 0.5, 0.7, 0.9]))
Fv = F_true(phi, w)
Fr, _ = F_rho(phi, rho, w)
d = Fv - Fr
audit["n"] += 1
if Fv <= Fr + 1e-12:
audit["F_le_Frho_holds"] += 1
if Fr <= Fv + 1e-12:
audit["Frho_le_F_holds"] += 1
audit["max_(F - Frho)"] = max(audit["max_(F - Frho)"], float(d))
audit["min_(F - Frho)"] = min(audit["min_(F - Frho)"], float(d))
if len(audit["examples"]) < 5 and rho in (0.1, 0.3):
audit["examples"].append(
{
"kind": kind,
"n": int(phi.size),
"rho": rho,
"F": float(Fv),
"F_rho": float(Fr),
"F_minus_Frho": float(d),
}
)
# a fully explicit hand-checkable example: mu = delta_0, phi = 0
rho = 0.1
Fr_pt, _ = F_rho(np.array([0.0]), rho, np.array([1.0]))
closed = np.log(1 - rho) - 1 - np.log(1 - rho) / rho
audit["point_mass_example"] = {
"rho": rho,
"F": 0.0,
"F_rho_numeric": float(Fr_pt),
"F_rho_closed_form": float(closed),
"closed_form": "log(1-rho) - 1 - log(1-rho)/rho",
}
audit["bruteforce_crosscheck"] = float(
abs(F_rho_bruteforce(np.array([0.0]), rho, np.array([1.0])) - Fr_pt)
)
res["T4_direction_audit"] = audit
# ---------------------------------------------------------------- T5 Prop 2.4(iii),(iv)
t5 = {
"iii_inside": {"n": 0, "viol": 0, "min_slack": np.inf},
"iii_outside": {"n": 0, "viol": 0, "min_slack": np.inf},
"iv_inside": {"n": 0, "viol": 0, "min_slack": np.inf},
"iv_outside": {"n": 0, "viol": 0, "min_slack": np.inf},
}
for trial in range(1500):
phi, w, _ = random_instance(rng)
F1 = F_true(phi, w)
F2 = F_true(2 * phi, w)
a_cap = 0.25 * np.exp(2 * F1 - F2) # admissible rho for (iii)
M = float(phi.max())
b_cap = np.exp(F1 - M) # admissible rho for (iv)
for rho in [1e-4, 1e-3, 1e-2, 0.03, 0.1, 0.3, 0.5, 0.9]:
if rho >= 1:
continue
Fr, _ = F_rho(phi, rho, w)
lb3 = F1 + rho / 2 - 4 * rho * np.exp(F2 - 2 * F1)
key = "iii_inside" if rho <= a_cap else "iii_outside"
t5[key]["n"] += 1
slack = Fr - lb3
t5[key]["min_slack"] = min(t5[key]["min_slack"], float(slack))
if slack < -1e-9:
t5[key]["viol"] += 1
lb4 = F1 - rho * np.exp(M - F1)
key = "iv_inside" if rho <= b_cap else "iv_outside"
t5[key]["n"] += 1
slack = Fr - lb4
t5[key]["min_slack"] = min(t5[key]["min_slack"], float(slack))
if slack < -1e-9:
t5[key]["viol"] += 1
for k in t5:
t5[k]["min_slack"] = (
float(t5[k]["min_slack"]) if np.isfinite(t5[k]["min_slack"]) else None
)
res["T5_explicit_bounds"] = t5
# ---------------------------------------------------------------- T6 Corollary 2.5
t6 = {"n": 0, "lower_viol": 0, "upper_viol": 0, "max_gap_over_rho": 0.0, "worst": None}
for trial in range(3000):
n = int(rng.integers(2, 300))
a = rng.normal(0, rng.uniform(0.2, 5.0), size=n)
rho = float(rng.choice([1e-3, 1e-2, 0.03, 0.1, 0.3, 0.6, 0.9]))
lse = float(logsumexp(a))
# inf_alpha alpha - 1 + (1/rho) sum_i log(1 + rho e^{a_i - alpha})
# == log n + F_{rho/n}(id; mu_n) with mu_n uniform -> reuse F_rho with weights 1/n
val = np.log(n) + F_rho(a, rho / n, np.full(n, 1.0 / n))[0]
t6["n"] += 1
if val < lse - rho - 1e-9:
t6["lower_viol"] += 1
if val > lse + 1e-9:
t6["upper_viol"] += 1
g = (lse - val) / rho
if g > t6["max_gap_over_rho"]:
t6["max_gap_over_rho"] = float(g)
t6["worst"] = {
"n": n,
"rho": rho,
"LSE": lse,
"approx": float(val),
"gap": float(lse - val),
}
res["T6_corollary25"] = t6
with open(os.path.join(OUT, "claim1_sandwich.json"), "w") as fh:
json.dump(res, fh, indent=2, default=float)
print(json.dumps(res, indent=2, default=float)[:6000])

Xet Storage Details

Size:
11.1 kB
·
Xet hash:
820503b721de777686205be5e72ceef7a92692a573511cab26ac52513ac7078b

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.