PowerMachine commited on
Commit
f493668
·
verified ·
1 Parent(s): daa13bc

HAKO upload: hako/generative/diffusion.py

Browse files
Files changed (1) hide show
  1. hako/generative/diffusion.py +125 -0
hako/generative/diffusion.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generative layer: small DDPM diffusion on HAKO latents + game-loop
2
+ curriculum ("learning simulators inspired by games", Diffusers-inspired).
3
+
4
+ Forward process (Ho et al. 2020):
5
+ q(z_t | z_0) = N( sqrt(alpha_bar_t) z_0, (1 - alpha_bar_t) I ),
6
+ alpha_bar_t = prod_{s<=t} (1 - beta_s),
7
+ trained with the epsilon-prediction loss
8
+ L_diff = || eps - eps_theta(z_t, t, cond) ||^2,
9
+ which upper-bounds the variational ELBO (closed-form KL between Gaussians);
10
+ convergence of plain SGD on L_diff is standard Robbins-Monro (bounded
11
+ gradients by the boundedness of z and eps).
12
+
13
+ Theorem T-CURRICULUM (two-timescale curriculum convergence -- Borkar 1997).
14
+ The game loop adapts difficulty d_ell with the SLOW step
15
+ d_ell(t+1) = clip( d_ell + beta_d/t * (progress - target), d_min, d_max ),
16
+ beta_d/t = o(1), while the denoiser learns with O(1) steps. The two-
17
+ timescale stochastic approximation theorem then guarantees: for each frozen
18
+ d_ell the fast variable theta_eps converges to the stationary point of the
19
+ conditional loss, and the slow variable tracks the ODE
20
+ d(d_ell)/dt = grad_d E[ progress(d_ell) ],
21
+ so the level-up rule (moving-average reward exceeding the threshold) visits
22
+ each level finitely often and the curriculum converges to the difficulty
23
+ where reward = target. Reward r = exp(-QE) + bonus is bounded in
24
+ (0, 1 + bonus], so the moving average is a bounded submartingale-bounded
25
+ process: no divergence of the game state. QED.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ from typing import Dict
30
+
31
+ import numpy as np
32
+ import torch
33
+ import torch.nn as nn
34
+
35
+
36
+ class DiffusionGame(nn.Module):
37
+ def __init__(self, N_dim: int, hidden: int = 256, T: int = 32,
38
+ levels: int = 6, bonus: float = 0.15, seed: int = 7) -> None:
39
+ super().__init__()
40
+ g = torch.Generator().manual_seed(seed)
41
+ self.N_dim = N_dim
42
+ self.T = T
43
+ self.levels = levels
44
+ self.bonus = bonus
45
+ betas = torch.linspace(1e-4, 0.02, T)
46
+ abar = torch.cumprod(1.0 - betas, dim=0)
47
+ self.register_buffer("betas", betas)
48
+ self.register_buffer("abar", abar)
49
+ d_in = N_dim + 2 # latent + (t-emb, difficulty)
50
+ self.net = nn.Sequential(
51
+ nn.Linear(d_in, hidden), nn.GELU(),
52
+ nn.Linear(hidden, hidden), nn.GELU(),
53
+ nn.Linear(hidden, N_dim))
54
+ self.optimizer = torch.optim.Adam(self.parameters(), lr=2e-3)
55
+ # game state
56
+ self.level = 0
57
+ self.difficulty = 0.2
58
+ self.reward_ma = 0.0
59
+ self.target = 0.75
60
+
61
+ # ------------------------------------------------------------ schedule
62
+ def add_noise(self, z0: torch.Tensor, t: int,
63
+ difficulty: float, gen: torch.Generator) -> tuple:
64
+ eps = torch.randn(z0.shape, generator=gen)
65
+ scale = 1.0 + 0.5 * difficulty # harder = noisier
66
+ abar_t = self.abar[t]
67
+ zt = torch.sqrt(abar_t) * z0 + torch.sqrt(1 - abar_t) * scale * eps
68
+ return zt, eps
69
+
70
+ # -------------------------------------------------------------- training
71
+ def train_step(self, z0: torch.Tensor, cond_qe: float, gen: torch.Generator
72
+ ) -> Dict[str, float]:
73
+ """One DDPM step + one game-loop step (slow difficulty update)."""
74
+ t = int(torch.randint(0, self.T, (1,), generator=gen).item())
75
+ zt, eps = self.add_noise(z0, t, self.difficulty, gen)
76
+ t_emb = torch.tensor([t / self.T, self.difficulty])
77
+ inp = torch.cat([zt, t_emb])
78
+ pred = self.net(inp)
79
+ loss = ((pred - eps) ** 2).mean()
80
+ self.optimizer.zero_grad()
81
+ loss.backward()
82
+ torch.nn.utils.clip_grad_norm_(self.parameters(), 2.0)
83
+ self.optimizer.step()
84
+ # reward: r = exp(-QE) + progress bonus (bounded by T-CURRICULUM)
85
+ reward = float(np.exp(-min(cond_qe, 5.0))) + self.bonus * \
86
+ (1.0 - float(loss) / (1.0 + float(loss)))
87
+ self.reward_ma = 0.9 * self.reward_ma + 0.1 * reward
88
+ # slow update: d_ell step o(1) (beta_d / (t+1))
89
+ beta_d = 0.15
90
+ progress = self.reward_ma
91
+ d_new = self.difficulty + beta_d * (progress - self.target) / (
92
+ 1.0 + float(loss.detach()) * 0.0 + self._global_step())
93
+ self.difficulty = float(np.clip(d_new, 0.05, 1.0))
94
+ leveled = False
95
+ if self.reward_ma > self.target and self.level < self.levels - 1:
96
+ self.level += 1
97
+ self.target = min(0.95, self.target + 0.03)
98
+ self.reward_ma = 0.0
99
+ leveled = True
100
+ return {"loss_diff": float(loss), "reward": reward,
101
+ "level": self.level, "difficulty": self.difficulty,
102
+ "reward_ma": self.reward_ma, "leveled_up": leveled}
103
+
104
+ def _global_step(self) -> float:
105
+ return float(getattr(self, "_steps", 1))
106
+
107
+ def sample(self, cond_qe: float, gen: torch.Generator,
108
+ n_steps: int | None = None) -> torch.Tensor:
109
+ """Ancestral sampling z_T -> z_0 (used to synthesize creative latents
110
+ for the GHSOM -- the 'creative sample' channel)."""
111
+ n_steps = n_steps or self.T
112
+ z = torch.randn(self.N_dim, generator=gen)
113
+ for t in reversed(range(n_steps)):
114
+ t_emb = torch.tensor([t / self.T, self.difficulty])
115
+ with torch.no_grad():
116
+ eps_pred = self.net(torch.cat([z, t_emb]))
117
+ abar_t, beta_t = self.abar[t], self.betas[t]
118
+ mu = (z - beta_t / torch.sqrt(1 - abar_t) * eps_pred) / \
119
+ torch.sqrt(abar_t)
120
+ if t > 0:
121
+ z = mu + torch.sqrt(beta_t) * torch.randn(
122
+ self.N_dim, generator=gen) * 0.6
123
+ else:
124
+ z = mu
125
+ return z