|
| 1 | +from functools import partial |
| 2 | +import numpy as np |
| 3 | + |
| 4 | +import jax |
| 5 | +import jax.numpy as jnp |
| 6 | +from jax import random, grad, jit, vmap |
| 7 | +import jax_dataclasses as jdc |
| 8 | + |
| 9 | +# Set random seed for reproducibility |
| 10 | +key = random.PRNGKey(0) |
| 11 | + |
| 12 | +# setup model structure using jax dataclass |
| 13 | +@jdc.pytree_dataclass |
| 14 | +class Model: |
| 15 | + """ |
| 16 | + DeltaNet-like model for solving pathfinder task |
| 17 | + """ |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | +# --- 1. Define Data Structures --- |
| 22 | + |
| 23 | +@dataclass |
| 24 | +class ModelParams: |
| 25 | + # Input Embeddings |
| 26 | + pos_embeds: jnp.ndarray # (16, 16, embed_dim) |
| 27 | + seq_embeds: jnp.ndarray # (seq_len, embed_dim) |
| 28 | + |
| 29 | + # DeltaNet Layers |
| 30 | + delta_layers: list # List of layer-specific parameters |
| 31 | + |
| 32 | + # Output Head |
| 33 | + output_w: jnp.ndarray # (embed_dim, 1) |
| 34 | + output_b: jnp.ndarray |
| 35 | + |
| 36 | +@dataclass |
| 37 | +class DeltaLayerParams: |
| 38 | + wq: jnp.ndarray # (num_heads, embed_dim, head_dim) |
| 39 | + wk: jnp.ndarray # (num_heads, embed_dim, head_dim) |
| 40 | + wv: jnp.ndarray # (num_heads, embed_dim, head_dim) |
| 41 | + wo: jnp.ndarray # (num_heads * head_dim, embed_dim) |
| 42 | + norm: jnp.ndarray |
| 43 | + B: jnp.ndarray # (num_heads,) - NEW: Learnable per-head parameter |
| 44 | + |
| 45 | +@dataclass |
| 46 | +class TrainingConfig: |
| 47 | + learning_rate: float |
| 48 | + batch_size: int |
| 49 | + num_iterations: int |
| 50 | + # Model dimensions |
| 51 | + embed_dim: int |
| 52 | + num_heads: int |
| 53 | + num_layers: int |
| 54 | + patch_size: int |
| 55 | + image_size: int |
| 56 | + # Precision amplification factor |
| 57 | + alpha: float |
| 58 | + |
| 59 | +# --- 2. Model Initialization --- |
| 60 | + |
| 61 | +def init_model(key, config): |
| 62 | + head_dim = config.embed_dim // config.num_heads |
| 63 | + seq_len = (config.image_size // config.patch_size)**2 |
| 64 | + |
| 65 | + keys = random.split(key, 2 + config.num_layers * 6) # Adjusted for new B param |
| 66 | + |
| 67 | + delta_layers = [] |
| 68 | + for i in range(config.num_layers): |
| 69 | + layer_params = DeltaLayerParams( |
| 70 | + wq=random.normal(keys[i*6+0], (config.num_heads, config.embed_dim, head_dim)), |
| 71 | + wk=random.normal(keys[i*6+1], (config.num_heads, config.embed_dim, head_dim)), |
| 72 | + wv=random.normal(keys[i*6+2], (config.num_heads, config.embed_dim, head_dim)), |
| 73 | + wo=random.normal(keys[i*6+3], (config.num_heads * head_dim, config.embed_dim)), |
| 74 | + norm=jnp.ones(config.embed_dim), |
| 75 | + B=jnp.ones(config.num_heads) # NEW: Initialize B for each head |
| 76 | + ) |
| 77 | + delta_layers.append(layer_params) |
| 78 | + |
| 79 | + return ModelParams( |
| 80 | + pos_embeds=random.normal(keys[-4], (config.image_size // config.patch_size, config.image_size // config.patch_size, config.embed_dim)), |
| 81 | + seq_embeds=random.normal(keys[-3], (seq_len, config.embed_dim)), |
| 82 | + delta_layers=delta_layers, |
| 83 | + output_w=random.normal(keys[-2], (config.embed_dim, 1)), |
| 84 | + output_b=random.normal(keys[-1], (1,)) |
| 85 | + ) |
| 86 | + |
| 87 | +# --- 3. Core Model Logic --- |
| 88 | + |
| 89 | +def z_score(x, axis=-1, eps=1e-5): |
| 90 | + mean = jnp.mean(x, axis=axis, keepdims=True) |
| 91 | + std = jnp.std(x, axis=axis, keepdims=True) |
| 92 | + return (x - mean) / (std + eps) |
| 93 | + |
| 94 | +def delta_net_layer_forward(params: DeltaLayerParams, x: jnp.ndarray, S: jnp.ndarray, alpha: float): |
| 95 | + |
| 96 | + def scan_fn(carry, token_x): |
| 97 | + S_prev = carry |
| 98 | + |
| 99 | + # Z-score input token |
| 100 | + token_x_norm = z_score(token_x) |
| 101 | + |
| 102 | + # Project to Q, K, V for each head |
| 103 | + q_h = jnp.einsum('d,hdm->hm', token_x_norm, params.wq) |
| 104 | + k_h = jnp.einsum('d,hdm->hm', token_x_norm, params.wk) |
| 105 | + v_h = jnp.einsum('d,hdm->hm', token_x_norm, params.wv) |
| 106 | + |
| 107 | + # Sigmoid nonlinearity |
| 108 | + q_h, k_h = jax.nn.sigmoid(q_h), jax.nn.sigmoid(k_h) |
| 109 | + |
| 110 | + # --- Per-head recurrent update (vmapped) --- |
| 111 | + def head_update(S_h, B_h, q_token, k_token, v_token): |
| 112 | + # NEW: Prediction includes learnable parameter B |
| 113 | + v_pred = B_h * (S_h @ k_token) |
| 114 | + error = v_token - v_pred |
| 115 | + |
| 116 | + # State update |
| 117 | + dS = jnp.outer(error, k_token) |
| 118 | + S_curr = S_h + dS |
| 119 | + |
| 120 | + # --- Mathematical Equivalence Comment --- |
| 121 | + # The update S_curr = S_prev + outer(v - B * (S_prev @ k), k) |
| 122 | + # expands to: S_curr = S_prev + v @ k.T - B * S_prev @ k @ k.T |
| 123 | + # which rearranges to: S_curr = S_prev @ (I - B * k @ k.T) + v @ k.T |
| 124 | + # This is the DeltaNet update rule. |
| 125 | + # ----------------------------------------- |
| 126 | + |
| 127 | + # Precision amplification |
| 128 | + temp = jnp.exp(-alpha * jnp.linalg.norm(error)) + 1e-6 |
| 129 | + |
| 130 | + # Efferent signal |
| 131 | + efferent = jax.nn.softmax((S_curr @ q_token) / temp) |
| 132 | + return S_curr, efferent |
| 133 | + |
| 134 | + S_next, efferent_h = vmap(head_update)(S_prev, params.B, q_h, k_h, v_h) |
| 135 | + |
| 136 | + # Concatenate heads and project out |
| 137 | + efferent_flat = efferent_h.flatten() |
| 138 | + output_token = efferent_flat @ params.wo |
| 139 | + |
| 140 | + # Skip connection |
| 141 | + final_token = token_x + output_token |
| 142 | + |
| 143 | + return S_next, final_token |
| 144 | + |
| 145 | + final_S, output_sequence = jax.lax.scan(scan_fn, S, x) |
| 146 | + return output_sequence, final_S |
| 147 | + |
| 148 | +def forward_pass(params: ModelParams, batch_patches: jnp.ndarray, config: TrainingConfig): |
| 149 | + # Add positional and sequential embeddings |
| 150 | + batch_size, seq_len, _ = batch_patches.shape |
| 151 | + pos_embeds_flat = params.pos_embeds.reshape(seq_len, config.embed_dim) |
| 152 | + x = batch_patches + pos_embeds_flat[jnp.newaxis, :, :] + params.seq_embeds[jnp.newaxis, :, :] |
| 153 | + |
| 154 | + # Initialize recurrent states (one per layer) |
| 155 | + head_dim = config.embed_dim // config.num_heads |
| 156 | + initial_S = jnp.zeros((config.num_layers, config.num_heads, head_dim, head_dim)) |
| 157 | + |
| 158 | + # Propagate through layers |
| 159 | + current_x = x |
| 160 | + for i in range(config.num_layers): |
| 161 | + layer_params = params.delta_layers[i] |
| 162 | + S_layer = initial_S[i] |
| 163 | + |
| 164 | + # Vmap across the batch dimension |
| 165 | + batch_layer_fwd = vmap(delta_net_layer_forward, in_axes=(None, 0, None, None)) |
| 166 | + current_x, _ = batch_layer_fwd(layer_params, current_x, S_layer, config.alpha) |
| 167 | + current_x = jax.nn.layer_norm(current_x, use_scale=False) * layer_params.norm |
| 168 | + |
| 169 | + # Readout from the mean of the final sequence |
| 170 | + final_representation = jnp.mean(current_x, axis=1) # (batch, embed_dim) |
| 171 | + logits = final_representation @ params.output_w + params.output_b |
| 172 | + return jnp.squeeze(logits) |
| 173 | + |
| 174 | +# --- 4. Loss and Training Functions --- |
| 175 | + |
| 176 | +def loss_fn(params: ModelParams, batch_patches, batch_labels, config: TrainingConfig): |
| 177 | + logits = forward_pass(params, batch_patches, config) |
| 178 | + loss = jnp.mean(optax.sigmoid_binary_cross_entropy(logits, batch_labels)) |
| 179 | + return loss |
| 180 | + |
| 181 | +@partial(jit, static_argnums=(3,)) |
| 182 | +def train_step(params: ModelParams, opt_state, batch, config: TrainingConfig): |
| 183 | + batch_patches, batch_labels = batch |
| 184 | + loss_val, grads = grad(loss_fn, has_aux=False)(params, batch_patches, batch_labels, config) |
| 185 | + updates, opt_state = optimizer.update(grads, opt_state, params) |
| 186 | + params = optax.apply_updates(params, updates) |
| 187 | + return params, opt_state, loss_val |
| 188 | + |
| 189 | +def train(config: TrainingConfig, model_params: ModelParams, train_images, train_labels): |
| 190 | + print("--- Starting Training ---") |
| 191 | + opt_state = optimizer.init(model_params) |
| 192 | + key = random.PRNGKey(42) |
| 193 | + |
| 194 | + # Data preprocessing function |
| 195 | + def preprocess_images(images): |
| 196 | + p = config.patch_size |
| 197 | + b, h, w = images.shape |
| 198 | + # (B, H/p, W/p, p, p) |
| 199 | + patches = images.reshape(b, h // p, p, w // p, p).transpose(0, 1, 3, 2, 4) |
| 200 | + # (B, H/p * W/p, p*p) |
| 201 | + patches = patches.reshape(b, (h // p) * (w // p), p * p) |
| 202 | + return patches / 255.0 |
| 203 | + |
| 204 | + for i in range(config.num_iterations): |
| 205 | + # Create a mini-batch |
| 206 | + key, subkey = random.split(key) |
| 207 | + indices = random.permutation(subkey, train_images.shape[0])[:config.batch_size] |
| 208 | + batch_images = train_images[indices] |
| 209 | + batch_labels = train_labels[indices] |
| 210 | + |
| 211 | + batch_patches = preprocess_images(batch_images) |
| 212 | + |
| 213 | + model_params, opt_state, loss_val = train_step(model_params, opt_state, (batch_patches, batch_labels), config) |
| 214 | + |
| 215 | + if i % 10 == 0: |
| 216 | + print(f"Iteration {i:03d} | Loss: {loss_val:.4f}") |
| 217 | + |
| 218 | + print("--- Training Finished ---") |
| 219 | + return model_params |
| 220 | + |
| 221 | + |
| 222 | +# --- 5. Main Execution --- |
| 223 | +if __name__ == '__main__': |
| 224 | + # Create dummy data files for demonstration |
| 225 | + print("Creating dummy data files 'ims.npy' and 'labels.npy'...") |
| 226 | + dummy_ims = np.random.randint(0, 256, size=(100, 64, 64), dtype=np.uint8) |
| 227 | + dummy_labels = np.random.randint(0, 2, size=(100,), dtype=np.int32) |
| 228 | + np.save('ims.npy', dummy_ims) |
| 229 | + np.save('labels.npy', dummy_labels) |
| 230 | + print("Dummy data created.") |
| 231 | + |
| 232 | + # Configuration |
| 233 | + config = TrainingConfig( |
| 234 | + learning_rate=1e-4, |
| 235 | + batch_size=8, |
| 236 | + num_iterations=101, |
| 237 | + embed_dim=64, |
| 238 | + num_heads=4, |
| 239 | + num_layers=3, |
| 240 | + patch_size=4, |
| 241 | + image_size=64, |
| 242 | + alpha=1.0, |
| 243 | + ) |
| 244 | + |
| 245 | + # Initialize optimizer |
| 246 | + optimizer = optax.adam(config.learning_rate) |
| 247 | + |
| 248 | + # Initialize model |
| 249 | + key = random.PRNGKey(0) |
| 250 | + model_params = init_model(key, config) |
| 251 | + |
| 252 | + # Load data |
| 253 | + try: |
| 254 | + images_data = np.load('ims.npy') |
| 255 | + labels_data = np.load('labels.npy') |
| 256 | + print(f"Loaded data: images shape {images_data.shape}, labels shape {labels_data.shape}") |
| 257 | + except FileNotFoundError: |
| 258 | + print("Error: Could not find 'ims.npy' or 'labels.npy'.") |
| 259 | + exit() |
| 260 | + |
| 261 | + # Run training |
| 262 | + trained_params = train(config, model_params, jnp.array(images_data, dtype=jnp.float32), jnp.array(labels_data)) |
0 commit comments