torch.manual_seed(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)
data, test = mix.sample(2000), mix.sample(2000)
class ScoreNet(nn.Module):
"""A two-dimensional score model: a multilayer perceptron with two outputs."""
def __init__(self, hidden=128):
super().__init__()
self.net = nn.Sequential(nn.Linear(2, hidden), nn.SiLU(),
nn.Linear(hidden, hidden), nn.SiLU(),
nn.Linear(hidden, 2))
def forward(self, x):
return self.net(x)
def fisher_divergence(score, x):
"""0.5 E||s(x) - grad log p(x)||^2 on the given points."""
with torch.no_grad():
return 0.5 * ((score(x) - mix.score(x)) ** 2).sum(1).mean().item()
def score_matching_loss(score, x):
"""Hyvarinen's objective E[0.5 ||s||^2 + tr(ds/dx)], trace by autograd."""
x = x.clone().requires_grad_(True)
s = score(x)
# trace = sum_i ds_i/dx_i: the gradient of the batch sum of s_i with respect
# to x has row b equal to ds_i(x_b)/dx_b, of which entry i is the diagonal term
trace = sum(torch.autograd.grad(s[:, i].sum(), x, create_graph=True)[0][:, i]
for i in range(x.shape[1]))
return (0.5 * (s ** 2).sum(1) + trace).mean()
def objective_on(score, x, chunk=20_000): # average over a large sample
return sum(score_matching_loss(score, c).item() * len(c)
for c in x.split(chunk)) / len(x)
torch.manual_seed(1)
big = mix.sample(200_000)
const = 0.5 * (mix.score(big) ** 2).sum(1).mean().item()
objective = objective_on(mix.score, big)
print(f'objective at the true score: {objective:.3f}, '
f'constant: {const:.3f}, sum: {objective + const:.3f}')