key = jax.random.PRNGKey(0)
mix = d2l.GaussianMixture(means=[[-2.5, -1.5], [2.5, -1.5], [0.0, 2.5]],
weights=[0.5, 0.3, 0.2], std=0.5)
key, k1, k2 = jax.random.split(key, 3)
data, test = mix.sample(k1, 2000), mix.sample(k2, 2000)
class ScoreNet(nnx.Module):
"""A two-dimensional score model: a multilayer perceptron with two outputs."""
def __init__(self, hidden=128, rngs=None):
self.h1 = nnx.Linear(2, hidden, rngs=rngs)
self.h2 = nnx.Linear(hidden, hidden, rngs=rngs)
self.out = nnx.Linear(hidden, 2, rngs=rngs)
def __call__(self, x):
return self.out(nnx.silu(self.h2(nnx.silu(self.h1(x)))))
def fisher_divergence(score, x):
"""0.5 E||s(x) - grad log p(x)||^2 on the given points."""
return float(0.5 * ((score(x) - mix.score(x)) ** 2).sum(1).mean())
def score_matching_loss(score, x):
"""Hyvarinen's objective E[0.5 ||s||^2 + tr(ds/dx)], trace by autodiff."""
s = score(x)
f = lambda q: score(q[None])[0] # the field at a single point, for its Jacobian
trace = jax.vmap(lambda q: jnp.trace(jax.jacfwd(f)(q)))(x)
return (0.5 * (s ** 2).sum(1) + trace).mean()
def objective_on(score, x, chunks=10): # average over a large sample
return float(sum(score_matching_loss(score, c)
for c in jnp.array_split(x, chunks)) / chunks)
key, subkey = jax.random.split(key)
big = mix.sample(subkey, 200_000)
const = float(0.5 * (mix.score(big) ** 2).sum(1).mean())
objective = objective_on(mix.score, big)
print(f'objective at the true score: {objective:.3f}, '
f'constant: {const:.3f}, sum: {objective + const:.3f}')