SabaPivot's picture
download
raw
7.16 kB
"""Claim 5 -- Sec. 3.2 / Fig. 3: KL-regularised DRO on California Housing.
Objective (17): L(theta) = lam * log( (1/n) sum_i exp( l_i(theta)/lam ) ),
l_i(theta) = (y_i - theta^T x_i)^2, lam = 1.
Baseline (18) : batch-softmax gradient estimator of Levy et al. (2020).
Proposed (19) : safe-KL / SoftPlus estimator with the extra scalar alpha.
Optimiser : SGD with Nesterov momentum 0.9 (PyTorch semantics), init = least squares.
The crux tested here is the STEPSIZE THRESHOLD: the largest eta in the paper's grid
{1e-8,...,1e-4} for which a method still converges, per batch size 10 / 100 / 1000.
"""
import argparse
import itertools
import json
import os
import time
from multiprocessing import Pool
import numpy as np
from scipy.special import expit, logsumexp
from sklearn.datasets import fetch_california_housing
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(ROOT, "outputs")
os.makedirs(OUT, exist_ok=True)
LAM = 1.0
N_EPOCHS = 50
CHECK_EPOCHS = [1, 2, 5, 10, 20, 30, 40, 50]
def get_data():
raw = fetch_california_housing()
X = StandardScaler().fit_transform(raw.data)
y = raw.target.astype(np.float64)
X = np.hstack([X, np.ones((X.shape[0], 1))]) # intercept column
lr = LinearRegression(fit_intercept=True).fit(X[:, :-1], y)
theta0 = np.concatenate([lr.coef_, [lr.intercept_]])
return X, y, theta0
def objective(X, y, theta, lam=LAM):
l = (X @ theta - y) ** 2
return float(lam * (logsumexp(l / lam) - np.log(len(y))))
def run(method, batch, eta, rho, seed, X, y, theta0, lam=LAM, n_epochs=N_EPOCHS):
rng = np.random.default_rng(seed)
n, d = X.shape
theta = theta0.copy()
alpha = 0.0
buf = np.zeros(d)
buf_a = 0.0
mom = 0.9
L0 = objective(X, y, theta, lam)
traj = {0: L0}
diverged_at = None
log_rho = np.log(rho) if rho is not None else None
for ep in range(1, n_epochs + 1):
perm = rng.permutation(n)
for s in range(0, n - batch + 1, batch):
idx = perm[s : s + batch]
Xb, yb = X[idx], y[idx]
r = Xb @ theta - yb
l = r**2
if method == "baseline": # Eq. (18)
p = np.exp(l / lam - logsumexp(l / lam))
g = 2.0 * (Xb.T @ (p * r))
ga = None
else: # Eq. (19)
w = expit((l - alpha) / lam + log_rho) / rho
g = 2.0 * (Xb.T @ (w * r)) / batch
ga = 1.0 - float(np.mean(w))
if not np.all(np.isfinite(g)):
diverged_at = ep
break
buf = mom * buf + g
theta = theta - eta * (g + mom * buf)
if ga is not None:
buf_a = mom * buf_a + ga
alpha = alpha - eta * (ga + mom * buf_a)
if diverged_at is not None:
break
if ep in CHECK_EPOCHS:
v = objective(X, y, theta, lam)
traj[ep] = v
if not np.isfinite(v) or v > L0 + 50:
diverged_at = ep
break
finite = np.isfinite(list(traj.values())).all() and diverged_at is None
final = traj[max(traj)]
return {
"method": method,
"batch": batch,
"eta": eta,
"rho": rho,
"seed": seed,
"L0": L0,
"final": float(final),
"traj": {str(k): float(v) for k, v in traj.items()},
"diverged_at": diverged_at,
"converged": bool(finite and final <= L0 + 1e-9),
"improvement": float(L0 - final) if finite else None,
"alpha": float(alpha),
}
_G = {}
def _init():
_G["data"] = get_data()
def _worker(t):
X, y, th = _G["data"]
return run(*t, X, y, th)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--procs", type=int, default=60)
ap.add_argument("--seeds", type=int, default=10)
args = ap.parse_args()
etas = [1e-8, 1e-7, 1e-6, 1e-5, 1e-4]
batches = [10, 100, 1000]
rhos = [1e-4, 1e-3, 1e-2, 1e-1, 0.5, 0.9]
seeds = list(range(args.seeds))
tasks = [
("baseline", b, e, None, s)
for b, e, s in itertools.product(batches, etas, seeds)
]
tasks += [
("proposed", b, e, r, s)
for b, e, r, s in itertools.product(batches, etas, rhos, seeds)
]
print(f"{len(tasks)} runs", flush=True)
t0 = time.time()
with Pool(args.procs, initializer=_init) as pool:
recs = pool.map(_worker, tasks, chunksize=1)
print("elapsed", time.time() - t0, flush=True)
X, y, th = get_data()
summary = {
"L0": objective(X, y, th),
"n": int(X.shape[0]),
"d": int(X.shape[1]),
"lam": LAM,
"n_epochs": N_EPOCHS,
"seeds": args.seeds,
"runs": recs,
}
with open(os.path.join(OUT, "claim5_kl_dro.json"), "w") as fh:
json.dump(summary, fh)
# ---- stepsize threshold table -------------------------------------------
table = []
for b in batches:
for meth, r in [("baseline", None)] + [("proposed", rr) for rr in rhos]:
row = {"method": meth, "rho": r, "batch": b, "per_eta": {}}
for e in etas:
rs = [
x
for x in recs
if x["method"] == meth
and x["batch"] == b
and x["eta"] == e
and x["rho"] == r
]
ok = sum(x["converged"] for x in rs)
fin = [x["final"] for x in rs if x["converged"]]
row["per_eta"][str(e)] = {
"converged_seeds": ok,
"n_seeds": len(rs),
"mean_final": float(np.mean(fin)) if fin else None,
"std_final": float(np.std(fin)) if fin else None,
}
good = [
e
for e in etas
if row["per_eta"][str(e)]["converged_seeds"] == len(seeds)
]
row["max_stable_eta"] = max(good) if good else None
best = None
for e in etas:
m = row["per_eta"][str(e)]["mean_final"]
if m is not None and (best is None or m < best[1]):
best = (e, m)
row["best_eta"] = best[0] if best else None
row["best_final_objective"] = best[1] if best else None
table.append(row)
with open(os.path.join(OUT, "claim5_threshold_table.json"), "w") as fh:
json.dump(table, fh, indent=2)
for row in table:
print(
row["method"],
row["rho"],
"batch",
row["batch"],
"max_stable_eta",
row["max_stable_eta"],
"best_eta",
row["best_eta"],
"best_obj",
(
None
if row["best_final_objective"] is None
else round(row["best_final_objective"], 4)
),
flush=True,
)
if __name__ == "__main__":
main()

Xet Storage Details

Size:
7.16 kB
·
Xet hash:
37b166fcf87c74dd101394b2556606c3fa179f2527fbd21d8fc710ad38376195

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