SabaPivot/repro-logsumexp-artifacts / scripts /claim2_strong_convexity.py
SabaPivot's picture
download
raw
8.53 kB
"""Claim 2 -- Lemma 2.7: f_rho is rho-strongly convex and its conjugate f_rho* is
(1/rho)-smooth.
Independent tests:
T1 second derivative of f_rho on its domain: analytic minimiser t=1/(2rho) and the
numerical minimum of f_rho'' (predicted exactly 4 rho).
T2 defining chord inequality of m-strong convexity for m = rho (claim), m = 4 rho
(tight), m = 4.05 rho (must fail) -- executable tightness audit.
T3 (f_rho*)'' bound: analytic maximiser s = -log rho, numerical max (predicted
1/(4 rho)); Lipschitz-gradient inequality with L = 1/rho (claim), L = 1/(4 rho)
(tight) and L = 1/(4.05 rho) (must fail).
T4 Legendre duality identity (f*)''(s) = 1 / f''(t) at t = (f*)'(s) -- this is the
independent re-derivation of "m-strongly convex <=> (1/m)-smooth conjugate".
T5 biconjugate check (f*)* = f.
T6 multivariate transfer: Hessian eigenvalues of v -> sum_i w_i f_rho*(v_i) and of
the parametric objective G_rho(theta,alpha) of Sec. 2.2 for a linear-model loss.
T7 boundary audit: rho -> 1^- and rho >= 1 (outside the paper's 0 < rho < 1).
"""
import json
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from common import (
F_rho,
d2_f_rho,
d2_f_rho_star,
d_f_rho,
d_f_rho_star,
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
rng = np.random.default_rng(SEED)
res = {"seed": SEED}
RHOS = [1e-4, 1e-3, 1e-2, 0.03, 0.1, 0.3, 0.5, 0.7, 0.9, 0.99]
# ------------------------------------------------------------------ T1
t1 = {}
for rho in RHOS:
tt = np.linspace(1e-10, 1.0 / rho * (1 - 1e-10), 2000001)
d2 = d2_f_rho(tt, rho)
k = int(np.argmin(d2))
t1[str(rho)] = {
"min_f2": float(d2.min()),
"argmin_t": float(tt[k]),
"predicted_argmin_1_over_2rho": 1.0 / (2 * rho),
"min_over_rho": float(d2.min() / rho),
"rho_SC_holds(min>=rho)": bool(d2.min() >= rho - 1e-12),
}
res["T1_second_derivative"] = t1
# ------------------------------------------------------------------ T2
def sc_violations(rho, m, n=20000):
lo, hi = 1e-9, 1.0 / rho * (1 - 1e-9)
t1_ = rng.uniform(lo, hi, n)
t2_ = rng.uniform(lo, hi, n)
lam = rng.uniform(0, 1, n)
mid = lam * t1_ + (1 - lam) * t2_
lhs = f_rho(mid, rho)
rhs = (
lam * f_rho(t1_, rho)
+ (1 - lam) * f_rho(t2_, rho)
- 0.5 * m * lam * (1 - lam) * (t1_ - t2_) ** 2
)
viol = lhs > rhs + 1e-9
return int(viol.sum()), float(np.max(lhs - rhs))
t2 = {}
for rho in RHOS:
a, ma = sc_violations(rho, rho)
b, mb = sc_violations(rho, 4 * rho)
c, mc = sc_violations(rho, 4.05 * rho)
t2[str(rho)] = {
"m=rho": {"violations": a, "max_excess": ma},
"m=4rho": {"violations": b, "max_excess": mb},
"m=4.05rho": {"violations": c, "max_excess": mc},
}
res["T2_strong_convexity_chords"] = t2
# ------------------------------------------------------------------ T3
t3 = {}
for rho in RHOS:
ss = np.linspace(-40 - np.log(rho), 40 - np.log(rho), 2000001)
d2s = d2_f_rho_star(ss, rho)
k = int(np.argmax(d2s))
out = {
"max_f_star2": float(d2s.max()),
"argmax_s": float(ss[k]),
"predicted_argmax_minus_log_rho": float(-np.log(rho)),
"predicted_max_1_over_4rho": 1.0 / (4 * rho),
"smooth_1_over_rho_holds": bool(d2s.max() <= 1.0 / rho + 1e-9),
}
for name, L in [
("L=1/rho", 1.0 / rho),
("L=1/(4rho)", 1.0 / (4 * rho)),
("L=1/(4.05rho)", 1.0 / (4.05 * rho)),
]:
s1 = rng.uniform(-30 - np.log(rho), 30 - np.log(rho), 20000)
s2 = rng.uniform(-30 - np.log(rho), 30 - np.log(rho), 20000)
lhs = np.abs(d_f_rho_star(s1, rho) - d_f_rho_star(s2, rho))
rhs = L * np.abs(s1 - s2)
out[name] = {
"violations": int(np.sum(lhs > rhs * (1 + 1e-12) + 1e-15)),
"max_ratio_lhs_over_L_times_ds": float(
np.max(lhs / np.maximum(rhs, 1e-300))
),
}
t3[str(rho)] = out
res["T3_conjugate_smoothness"] = t3
# ------------------------------------------------------------------ T4 duality identity
t4 = {"max_rel_err": 0.0, "n": 0}
for rho in RHOS:
ss = np.linspace(-25 - np.log(rho), 25 - np.log(rho), 2001)
tstar = d_f_rho_star(ss, rho) # t = (f*)'(s)
inv = 1.0 / d2_f_rho(tstar, rho)
got = d2_f_rho_star(ss, rho)
rel = np.max(np.abs(inv - got) / np.maximum(np.abs(got), 1e-300))
t4["max_rel_err"] = max(t4["max_rel_err"], float(rel))
t4["n"] += ss.size
# also: s = f'(t) inverts (f*)'
tt = np.linspace(1e-6, 1.0 / rho * (1 - 1e-6), 2001)
back = d_f_rho_star(d_f_rho(tt, rho), rho)
t4[f"inverse_map_max_abs_err_rho{rho}"] = float(np.max(np.abs(back - tt)))
res["T4_legendre_identity"] = t4
# ------------------------------------------------------------------ T5 biconjugate
t5 = {"max_abs_err": 0.0}
for rho in [0.03, 0.1, 0.3, 0.9]:
tt = np.linspace(1e-4, 1.0 / rho * (1 - 1e-4), 400)
ss = np.linspace(-60, 60, 400001)
fs = f_rho_star(ss, rho)
bic = np.array([np.max(t * ss - fs) for t in tt])
err = float(np.max(np.abs(bic - f_rho(tt, rho))))
t5[f"rho{rho}"] = err
t5["max_abs_err"] = max(t5["max_abs_err"], err)
res["T5_biconjugate"] = t5
# ------------------------------------------------------------------ T6 multivariate
t6 = {}
for rho in [1e-2, 0.1, 0.3]:
n = 40
w = rng.dirichlet(np.ones(n))
v = rng.normal(0, 3, n)
diag = w * d2_f_rho_star(v, rho) # Hessian diagonal of sum_i w_i f*(v_i)
ev = diag
t6[f"separable_rho{rho}"] = {
"max_curvature_per_unit_weight": float(np.max(diag / w)),
"bound_1_over_4rho": 1.0 / (4 * rho),
"bound_1_over_rho": 1.0 / rho,
}
# G_rho(theta,alpha) = mean_i [ alpha + (lam/rho) softplus((l_i(theta)-alpha)/lam + log rho) ]
# with l_i(theta) = (y_i - x_i^T theta)^2 ; compare ||Hess|| to (1/rho) * L(loss)
d = 5
X = rng.normal(size=(200, d))
y = rng.normal(size=200)
theta = rng.normal(size=d) * 0.3
lam = 1.0
def G(z):
th, al = z[:d], z[d]
l = (y - X @ th) ** 2
return np.mean(
al + (lam / rho) * np.logaddexp(0.0, (l - al) / lam + np.log(rho))
)
z0 = np.concatenate([theta, [1.0]])
eps = 1e-4
H = np.zeros((d + 1, d + 1))
for i in range(d + 1):
for j in range(d + 1):
zpp, zpm, zmp, zmm = z0.copy(), z0.copy(), z0.copy(), z0.copy()
zpp[i] += eps
zpp[j] += eps
zpm[i] += eps
zpm[j] -= eps
zmp[i] -= eps
zmp[j] += eps
zmm[i] -= eps
zmm[j] -= eps
H[i, j] = (G(zpp) - G(zpm) - G(zmp) + G(zmm)) / (4 * eps**2)
ev = np.linalg.eigvalsh(0.5 * (H + H.T))
Lloss = 2 * np.max(
np.linalg.eigvalsh(X.T @ X / len(X))
) # smoothness of mean squared loss
t6[f"G_rho_rho{rho}"] = {
"min_eig": float(ev.min()),
"max_eig": float(ev.max()),
"loss_smoothness_L": float(Lloss),
"max_eig_over_L": float(ev.max() / Lloss),
"one_over_rho": 1.0 / rho,
"convex(min_eig>=-1e-6)": bool(ev.min() >= -1e-6),
}
res["T6_multivariate"] = t6
# ------------------------------------------------------------------ T7 boundary
t7 = {}
phi = rng.normal(0, 1.5, 200)
w = np.full(200, 1 / 200)
for rho in [0.9, 0.99, 0.999, 0.9999]:
v, a = F_rho(phi, rho, w)
t7[f"F_rho_rho{rho}"] = {
"F_rho": v,
"limit_mean_phi_minus_1": float(np.sum(w * phi) - 1.0),
}
t7["mean_phi_minus_1"] = float(np.sum(w * phi) - 1.0)
# rho >= 1: domain [0,1/rho] cannot contain a density with mean 1 unless rho == 1 exactly
t7["rho_ge_1_infeasible"] = {
"explanation": "for rho > 1 the constraint dnu/dmu <= 1/rho < 1 is incompatible with "
"int dnu/dmu dmu = 1, so D_rho == +inf and the variational problem (5) is vacuous",
"f_rho(1)_at_rho1.5_is_inf": bool(np.isinf(f_rho(np.array([1.0]), 1.5)[0])),
}
# strong-convexity modulus 4rho -> 4 as rho -> 1
t7["min_f2_at_rho0.999"] = float(
np.min(d2_f_rho(np.linspace(1e-9, 1 / 0.999 * (1 - 1e-9), 200001), 0.999))
)
res["T7_boundary"] = t7
with open(os.path.join(OUT, "claim2_strong_convexity.json"), "w") as fh:
json.dump(res, fh, indent=2, default=float)
print(json.dumps(res, indent=2, default=float)[:4000])

Xet Storage Details

Size:
8.53 kB
·
Xet hash:
31f4bd06ffd35e1273f5b9865c297ac3af7fd0bfa5141dee600fc54f5dbf91bb

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