SabaPivot's picture
download
raw
7.62 kB
"""Linear regression under log loss and under the beta-divergence loss
(beta = 1.5), plus the four step-size tuning rules that Algorithm 1 dispatches on.
beta-divergence for a Gaussian working model f(y;theta,z)=N(y; z'theta, sigma^2):
l(theta;x) = -1/(beta-1) f^{beta-1} + (1/beta) int f(y';theta,z)^beta dy'
The integral is (2 pi sigma^2)^{-(beta-1)/2} beta^{-1/2}, which does NOT depend on
theta, so it drops out of every derivative (the authors' released code estimates
it by Monte Carlo; the closed form used here is exact). With r = y - z'theta,
grad l = -f^{beta-1} r z / sigma^2
hess l = f^{beta-1} [ 1 - (beta-1) r^2 / sigma^2 ] z z' / sigma^2
"""
import numpy as np
from scipy.optimize import minimize
class LinReg:
def __init__(self, X, y, sigma=1.0, loss="log", beta=1.5, z_samples=None):
self.X = np.asarray(X, float)
self.y = np.asarray(y, float)
self.N, self.D = self.X.shape
self.sigma = sigma
self.loss = loss
self.beta = beta
self.Gamma = np.zeros((self.D, self.D)) # no prior, as in Table 3
# 'beta_mc' replicates the authors' released implementation, which replaces
# the (theta-independent) integral (1/beta) int f(z;theta)^beta dz by the
# plain average (1/beta) mean_j f(z_j;theta)^beta over FIXED draws
# z_j ~ N(0,(5 sigma)^2). That average is not an unbiased estimator of the
# integral and, unlike the integral, does depend on theta, so it changes the
# loss. Both variants are reported.
self.z = None if z_samples is None else np.asarray(z_samples, float)
def _f_pow(self, r):
s2 = self.sigma**2
c = (2 * np.pi * s2) ** (-0.5)
return (c ** (self.beta - 1)) * np.exp(-(self.beta - 1) * r**2 / (2 * s2))
def _mc_terms(self, mu):
"""returns (dOmega/dmu, d2Omega/dmu^2) for the authors' fixed-z surrogate"""
s2 = self.sigma ** 2
c = (2 * np.pi * s2) ** (-0.5)
d = self.z[None, :] - mu[:, None] # (N, n_mc)
fb = (c ** self.beta) * np.exp(-self.beta * d ** 2 / (2 * s2))
g = (fb * d / s2).mean(1)
h = (fb / s2 * (self.beta * d ** 2 / s2 - 1.0)).mean(1)
return g, h
def value(self, th):
r = self.y - self.X @ th
if self.loss == "log":
return float((r**2).mean() / (2 * self.sigma**2))
v = float((-self._f_pow(r) / (self.beta - 1)).mean())
if self.loss == "beta_mc":
s2 = self.sigma ** 2
c = (2 * np.pi * s2) ** (-0.5)
mu = self.X @ th
d = self.z[None, :] - mu[:, None]
fb = (c ** self.beta) * np.exp(-self.beta * d ** 2 / (2 * s2))
v += float((fb.mean(1) / self.beta).mean())
return v
def grad_n(self, th, X=None, y=None):
X = self.X if X is None else X
y = self.y if y is None else y
r = y - X @ th
if self.loss == "log":
w = -r / self.sigma**2
else:
w = -self._f_pow(r) * r / self.sigma**2
if self.loss == "beta_mc":
mu = X @ th
s2 = self.sigma ** 2
c = (2 * np.pi * s2) ** (-0.5)
d = self.z[None, :] - mu[:, None]
fb = (c ** self.beta) * np.exp(-self.beta * d ** 2 / (2 * s2))
w = w + (fb * d / s2).mean(1)
return w[:, None] * X
def hess_n(self, th):
r = self.y - self.X @ th
if self.loss == "log":
w = np.ones(self.N) / self.sigma**2
else:
w = (
self._f_pow(r)
* (1 - (self.beta - 1) * r**2 / self.sigma**2)
/ self.sigma**2
)
if self.loss == "beta_mc":
w = w + self._mc_terms(self.X @ th)[1]
return w[:, None, None] * np.einsum("nd,ne->nde", self.X, self.X)
def fit(self, th0=None):
if self.loss == "log":
return np.linalg.lstsq(self.X, self.y, rcond=None)[0]
th0 = np.linalg.lstsq(self.X, self.y, rcond=None)[0] if th0 is None else th0
r = minimize(
self.value,
th0,
jac=lambda t: self.grad_n(t).mean(0),
method="L-BFGS-B",
options=dict(maxiter=5000, ftol=1e-16, gtol=1e-12),
)
return r.x
# ------------------------------------------------------------------
def sgd_paths(self, th0, Lam, B, n_iters, R, rng, burn_frac=0.5):
"""R independent preconditioned-SGD chains; returns per-chain tail covariance."""
X, y = self.X, self.y
TH = np.repeat(np.asarray(th0, float)[None, :], R, 0)
LamT = np.asarray(Lam, float).T
start = int(burn_frac * n_iters)
acc = np.zeros((R, self.D, self.D))
s = np.zeros((R, self.D))
cnt = 0
diverged = np.zeros(R, bool)
s2 = self.sigma**2
for t in range(n_iters):
IDX = rng.integers(0, self.N, size=(R, B))
Xb, yb = X[IDX], y[IDX]
r = yb - np.einsum("rbd,rd->rb", Xb, TH)
if self.loss == "log":
w = -r / s2
else:
w = -self._f_pow(r) * r / s2
if self.loss == "beta_mc":
mu = np.einsum("rbd,rd->rb", Xb, TH)
c = (2 * np.pi * s2) ** (-0.5)
dd = self.z[None, None, :] - mu[:, :, None]
fb = (c ** self.beta) * np.exp(-self.beta * dd**2 / (2 * s2))
w = w + (fb * dd / s2).mean(2)
g = np.einsum("rb,rbd->rd", w, Xb) / B
TH = TH - g @ LamT
bad = ~np.isfinite(TH).all(1) | (np.abs(TH).max(1) > 1e8)
diverged |= bad
TH[bad] = 0.0
if t >= start:
acc += np.einsum("rd,re->rde", TH, TH)
s += TH
cnt += 1
covs = acc / cnt - np.einsum("rd,re->rde", s / cnt, s / cnt)
return covs, diverged
# ---------------------------------------------------------------- tunings --
def sandwich(J, I, N):
Ji = np.linalg.inv(J)
return Ji @ I @ Ji / N
def tune_Lambda(
method,
H,
S,
Ihat,
Jhat,
B,
nm=None,
A_cov=None,
sig2=None,
beta_temp=np.inf,
sigma=1.0,
):
"""Return (Lambda, residual, closed_form_start) for one row of Algorithm 1 line 6.
LR+WS uses Ziyin et al. (2022) Eq. (6),
C = B^{-1} ( A Sigma A + Tr[A Sigma] A + sigma_r^2 A ),
which the paper states for the sigma = 1 parameterisation of the Gaussian
working model. For a general working scale sigma the per-sample Hessian is
z z' / sigma^2 and the score is -r z / sigma^2, so every term carries an extra
sigma^{-4}; that scale-covariant form is what is implemented here (it reduces
to Eq. (6) exactly when sigma = 1).
"""
from common import solve_Lambda
s4 = sigma**4
if method == "CT":
# Eq. (3): Lambda = (Sigma H + H Sigma) Chat^{-1}, Chat = Cov(G_1(that)) = Ihat/B
Lam = (S @ H + H @ S) @ np.linalg.inv(Ihat / B)
return Lam, 0.0, Lam
if method == "DQ+const":
Cf = lambda Sig: Jhat / B # noqa: E731
elif method == "LR+WS":
Cf = lambda Sig: (
A_cov @ Sig @ A_cov
+ np.trace(A_cov @ Sig) * A_cov # noqa: E731
+ sig2 * A_cov
) / (B * s4)
elif method == "DQ+exact":
Cf = lambda Sig: nm.C(Sig, B) # noqa: E731
else:
raise ValueError(method)
Lam, res, ok, Lam0 = solve_Lambda(H, S, Cf, beta=beta_temp)
return Lam, res, Lam0

Xet Storage Details

Size:
7.62 kB
·
Xet hash:
be0b9e8a953ca4035e7c3f289f90c4be420f424d28cbe78201bb177242e9317b

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