algorise's picture
download
raw
5.06 kB
"""Synthetic distributed problems used to numerically audit FAB's theory.
Bilevel problem (Section 2, Eq. 1): a decentralized ridge-regression
hyperparameter-tuning task in the style of the paper's own "distributed
hyperparameter tuning" experiment (Sec 4, Appendix B). Each agent i holds
local training data (A_i, b_i) and validation data (C_i, e_i):
g_i(x, y) = (1/2 m_i) ||A_i y - b_i||^2 + (mu/2) ||y - x||^2 (lower level, mu-strongly convex in y)
f_i(x, y) = (1/2 p_i) ||C_i y - e_i||^2 + kappa_i (1 - cos(1^T x)) (upper level, smooth but NONconvex in x)
x is a learned "prior/anchor" hyperparameter that the lower-level ridge
regression is regularized towards; y is the regression weight vector.
Because g_i is jointly quadratic, y*(x) is affine in x, which lets us
compute the true hypergradient of F*(x) := (1/n) sum_i f_i(x, y*(x)) in
closed form (via the implicit function theorem) as an exact ground truth --
no need for a separate high-precision centralized solve.
Single-level problem (Eq. 9, used for Theorem 3.6 / Push-Pull only): the same
kind of smooth nonconvex coupling term directly on x, no bilevel structure.
"""
from __future__ import annotations
import numpy as np
class BilevelHyperTuning:
def __init__(self, n: int, d: int, m: int, p: int, mu: float = 1.0,
heterogeneity: float = 0.5, seed: int = 0):
rng = np.random.default_rng(seed)
self.n, self.d, self.m, self.p, self.mu = n, d, m, p, mu
y_true = rng.normal(size=d) / np.sqrt(d)
self.A = np.empty((n, m, d))
self.b = np.empty((n, m))
self.C = np.empty((n, p, d))
self.e = np.empty((n, p))
self.kappa = rng.uniform(0.5, 1.5, size=n)
for i in range(n):
# heterogeneity in (0, 1]: smaller => agents see more different local targets
y_i = y_true + heterogeneity * rng.normal(size=d) / np.sqrt(d)
self.A[i] = rng.normal(size=(m, d)) / np.sqrt(m)
self.b[i] = self.A[i] @ y_i + 0.01 * rng.normal(size=m)
self.C[i] = rng.normal(size=(p, d)) / np.sqrt(p)
self.e[i] = self.C[i] @ y_true + 0.01 * rng.normal(size=p)
# M = (1/n) sum_i (1/m) A_i^T A_i + mu I ; y*(x) = M^{-1}(v + mu x)
M = mu * np.eye(d)
v = np.zeros(d)
for i in range(n):
M += (self.A[i].T @ self.A[i]) / (n * m)
v += (self.A[i].T @ self.b[i]) / (n * m)
self.M = M
self.v = v
self.Minv = np.linalg.inv(M)
def y_star(self, x: np.ndarray) -> np.ndarray:
return self.Minv @ (self.v + self.mu * x)
def f_i(self, i: int, x, y):
r = self.C[i] @ y - self.e[i]
return 0.5 * (r @ r) / self.p + self.kappa[i] * (1 - np.cos(x.sum()))
def g_i(self, i: int, x, y):
r = self.A[i] @ y - self.b[i]
return 0.5 * (r @ r) / self.m + 0.5 * self.mu * np.sum((y - x) ** 2)
def grad_y_f_i(self, i, x, y):
return (self.C[i].T @ (self.C[i] @ y - self.e[i])) / self.p
def grad_x_f_i(self, i, x, y):
return np.full_like(x, self.kappa[i] * np.sin(x.sum()))
def grad_y_g_i(self, i, x, y):
return (self.A[i].T @ (self.A[i] @ y - self.b[i])) / self.m + self.mu * (y - x)
def grad_x_g_i(self, i, x, y):
return self.mu * (x - y)
def true_hypergradient(self, x: np.ndarray) -> np.ndarray:
"""Exact grad F*(x) via the implicit function theorem: dy*/dx = mu * M^{-1}."""
y = self.y_star(x)
grad_y_f_avg = np.mean([self.grad_y_f_i(i, x, y) for i in range(self.n)], axis=0)
grad_x_f_avg = np.mean([self.grad_x_f_i(i, x, y) for i in range(self.n)], axis=0)
dy_dx = self.mu * self.Minv # d x d
return dy_dx.T @ grad_y_f_avg + grad_x_f_avg
def F_star(self, x: np.ndarray) -> float:
y = self.y_star(x)
return float(np.mean([self.f_i(i, x, y) for i in range(self.n)]))
class SingleLevelNonconvex:
"""min_x (1/n) sum_i f_i(x), f_i(x) = kappa_i (1 - cos(1^T x)) + 0.5*c_i ||x - r_i||^2.
Smooth, bounded-below, genuinely nonconvex (periodic term), with per-agent
heterogeneity (r_i, c_i, kappa_i) -- used only for Theorem 3.6 / Prop 3.5's
single-level Push-Pull special case (Eq. 9), no bilevel structure at all.
"""
def __init__(self, n: int, d: int, heterogeneity: float = 0.5, seed: int = 1):
rng = np.random.default_rng(seed)
self.n, self.d = n, d
self.kappa = rng.uniform(0.5, 1.5, size=n)
self.c = rng.uniform(0.5, 1.5, size=n)
self.r = heterogeneity * rng.normal(size=(n, d)) / np.sqrt(d)
def f_i(self, i, x):
return self.kappa[i] * (1 - np.cos(x.sum())) + 0.5 * self.c[i] * np.sum((x - self.r[i]) ** 2)
def grad_i(self, i, x):
return np.full_like(x, self.kappa[i] * np.sin(x.sum())) + self.c[i] * (x - self.r[i])
def F(self, x):
return float(np.mean([self.f_i(i, x) for i in range(self.n)]))
def true_gradient(self, x):
return np.mean([self.grad_i(i, x) for i in range(self.n)], axis=0)

Xet Storage Details

Size:
5.06 kB
·
Xet hash:
81ba9626b62120e68bdf52efb860b747853bd809b5e923f58248ac6d4078889c

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