File size: 8,389 Bytes
8099f2d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | #!/usr/bin/env python3
"""Génération par COMPRÉHENSION — le modèle CRÉE des images depuis les règles grokkées.
PRINCIPE : comprendre → générer. Le modèle grok les primitives visuelles et leurs
règles de composition → il peut GÉNÉRER de nouvelles images en composant les
primitives selon les règles comprises. Pas une copie — une CRÉATION depuis la compréhension.
Pipeline :
1. Cross-modal grok : image + texte → concept ID (compréhension, déjà fait)
2. Flow-matching decoder : concept AMV → intégrer du bruit vers l'image
Le decoder apprend : concept → comment composer les primitives visuelles → signal
3. Génération : concept ID → AMV → flow-matching → image générée
4. Vérification : l'image générée est classifiée par le cross-modal → bon concept ?
C'est le crown-jewel à l'envers : grok (a,b)→op(a,b) permet de CALCULER tout résultat.
Grok les règles visuelles → GÉNÉRER toute image du concept.
"""
import torch, torch.nn as nn, torch.nn.functional as F, glob, os, numpy as np, time
from PIL import Image
from sklearn.cluster import MiniBatchKMeans
from ocm26400.spectral_core import SpectralCoreBlock
from ocm26400.amv import D_MODEL, PART
from ocm26400.learned_vocab import LearnedVocab
device = "cuda" if torch.cuda.is_available() else "cpu"
torch.manual_seed(0)
IMG_DIR = "/media/akone/SAVENVME2/Datasets/vision_tinyimagenet"
N_CLUSTERS = 10
PATCH_DIM = 48 # 4x4x3
def extract_patches(path):
"""Image → 4 patches (4x4x3 = 48-dim chacun), puis moyenne."""
im = Image.open(path).convert("RGB").resize((8, 8))
arr = np.array(im, dtype=np.float32) / 255.0
return np.mean([arr[:4, :4].flatten(), arr[:4, 4:].flatten(),
arr[4:, :4].flatten(), arr[4:, 4:].flatten()], axis=0)
# ============================================================
# Flow-matching decoder : concept AMV → génère l'image
# ============================================================
class FlowDecoder(nn.Module):
"""Apprend : concept AMV + temps t + bruit x_t → vélocité v.
À l'inférence : intégrer du bruit vers l'image, conditionné par le concept."""
def __init__(self, cond_dim=D_MODEL, x_dim=PATCH_DIM, hidden=256):
super().__init__()
self.net = nn.Sequential(
nn.Linear(cond_dim + x_dim + 1, hidden), nn.GELU(),
nn.Linear(hidden, hidden), nn.GELU(),
nn.Linear(hidden, x_dim)
)
def forward(self, x, cond, t):
"""x: (B, x_dim), cond: (B, cond_dim), t: (B, 1) → vélocité (B, x_dim)."""
return self.net(torch.cat([x, cond, t], dim=-1))
@torch.no_grad()
def sample(self, cond, n_steps=20):
"""Génère depuis le bruit, conditionné par le concept AMV."""
B = cond.shape[0]
x = torch.randn(B, PATCH_DIM, device=cond.device) # start: bruit
for i in range(n_steps):
t = torch.full((B, 1), i / n_steps, device=cond.device)
v = self.forward(x, cond, t)
x = x + v / n_steps # Euler integration
return x # image générée
def train_generation():
# 1. charger images + clustering (concept IDs)
paths = sorted(glob.glob(os.path.join(IMG_DIR, "*.png")))[:1500]
all_patches = np.array([extract_patches(p) for p in paths], dtype=np.float64)
km = MiniBatchKMeans(n_clusters=N_CLUSTERS, batch_size=256, random_state=0, n_init=3)
km.fit(all_patches)
labels = km.predict(all_patches)
all_patches = all_patches.astype(np.float32) # back to float32 for torch
# concept canonical embeddings
cv = LearnedVocab(n=N_CLUSTERS, dim=PART, init="ortho", seed=0)
cv.freeze()
canon = cv._matrix().to(device) # (N_CLUSTERS, PART)
# 2. cross-modal classifier (pour vérifier la génération)
class Classifier(nn.Module):
def __init__(self):
super().__init__()
self.proj = nn.Linear(PATCH_DIM, D_MODEL)
self.core = SpectralCoreBlock(d_model=D_MODEL, seq_len=1)
self.head = nn.Linear(D_MODEL, PART)
def forward(self, x):
return self.head(self.core(self.proj(x).unsqueeze(1)).squeeze(1))
clf = Classifier().to(device)
opt_c = torch.optim.Adam(clf.parameters(), lr=3e-3)
# 3. flow decoder (générateur)
decoder = FlowDecoder(cond_dim=PART, x_dim=PATCH_DIM).to(device)
opt_d = torch.optim.Adam(decoder.parameters(), lr=3e-3)
# split
perm = np.random.permutation(len(paths))
ntr = int(len(paths) * 0.8)
tr_idx, te_idx = perm[:ntr], perm[ntr:]
patches_t = torch.tensor(all_patches, dtype=torch.float32).to(device)
labels_t = torch.tensor(labels, dtype=torch.long).to(device)
# PHASE 1 : entraîner classifier + decoder SIMULTANÉMENT
print(f"[génération par compréhension] {len(paths)} images, {N_CLUSTERS} concepts", flush=True)
print(f" Phase 1: classifier (comprendre) + flow decoder (générer) simultanés", flush=True)
t0 = time.time()
for step in range(4000):
bi = torch.tensor(np.random.choice(tr_idx, 48, replace=False))
x_real = patches_t[bi] # (48, 48) patches réels
y = labels_t[bi] # concept IDs
cond = canon[y] # (48, PART) concept AMV
# --- classifier loss (comprendre : image → concept) ---
out_c = clf(x_real)
loss_cls = (1 - F.cosine_similarity(out_c, cond).clamp(-1, 1)).mean()
# --- flow-matching loss (générer : concept → image) ---
x_0 = torch.randn_like(x_real) # bruit (48, 48)
t = torch.rand(48, 1, device=device) # (48, 1)
x_t = (1 - t) * x_0 + t * x_real # interpolation (48,48)
v_target = x_real - x_0 # vélocité cible (48,48)
v_pred = decoder(x_t, cond, t)
loss_gen = F.mse_loss(v_pred, v_target)
loss = loss_cls + loss_gen
opt_c.zero_grad(); opt_d.zero_grad()
loss.backward()
opt_c.step(); opt_d.step()
if step % 1000 == 0:
clf.eval()
with torch.no_grad():
ok = sum(1 for i in te_idx[:100]
if (clf(patches_t[i:i+1]) @ canon.t()).argmax(1).item() == labels[i])
print(f" step {step} cls={loss_cls.item():.3f} gen={loss_gen.item():.3f} "
f"clf_test={ok}% t={time.time()-t0:.0f}s", flush=True)
clf.train()
# PHASE 2 : GÉNÉRER des images depuis les concepts
print(f"\n[Phase 2: GÉNÉRATION — concept AMV → flow-matching → image créée]")
decoder.eval(); clf.eval()
n_gen = 5 # générer 5 images par concept
generated = {}
verify_ok = 0; verify_tot = 0
with torch.no_grad():
for ci in range(N_CLUSTERS):
cond = canon[ci:ci+1].expand(n_gen, -1) # (5, PART)
gen_patches = decoder.sample(cond, n_steps=25) # (5, 48) patches générés
generated[ci] = gen_patches.cpu().numpy()
# VÉRIFICATION : le classifier reconnaît-il l'image générée comme le bon concept ?
pred = (clf(gen_patches) @ canon.t()).argmax(1)
hits = (pred == ci).sum().item()
verify_ok += hits; verify_tot += n_gen
gen_acc = verify_ok / max(verify_tot, 1)
print(f"\n=== GÉNÉRATION PAR COMPRÉHENSION ===")
print(f" images générées: {verify_tot} ({n_gen} par concept × {N_CLUSTERS})")
print(f" vérification (généré reconnu comme bon concept): {verify_ok}/{verify_tot} = {gen_acc*100:.0f}%")
print(f" hasard: {100/N_CLUSTERS:.0f}%")
print(f" temps: {time.time()-t0:.0f}s")
print(f" méthode: concept ID → flow-matching → image CRÉÉE (pas copiée)")
print(f" preuve: l'image générée est reconnue par le classifier = génération cohérente")
# save
ckpt = "/media/akone/SAVENVME2/Datasets/ocm26400/generation_trained.pt"
torch.save({"decoder": decoder.state_dict(), "classifier": clf.state_dict(),
"canon": canon, "gen_acc": gen_acc,
"method": "flow-matching generation from comprehension"}, ckpt)
print(f" [SAUVÉ] {ckpt}")
return gen_acc
if __name__ == "__main__":
print("="*60)
print("GÉNÉRATION PAR COMPRÉHENSION — créer depuis les règles grokkées")
print("="*60)
acc = train_generation()
print(f"\nGénération vérifiée: {acc*100:.0f}%")
|