sparsetrace commited on
Commit
cc26f43
·
verified ·
1 Parent(s): 30a8bf8

Create TART.py

Browse files
Files changed (1) hide show
  1. TART.py +453 -0
TART.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import Optional
3
+
4
+ import numpy as np
5
+ import jax
6
+ import jax.numpy as jnp
7
+ import flax.linen as nn
8
+ import optax
9
+ from flax.training import train_state
10
+
11
+ def zscore(x: np.ndarray, eps: float = 1e-6):
12
+ x = np.asarray(x, dtype=np.float32)
13
+ mu = x.mean(axis=0, keepdims=True)
14
+ sd = x.std(axis=0, keepdims=True) + eps
15
+ return (x - mu) / sd, (mu, sd)
16
+
17
+
18
+ def make_nextstep_windows(X: np.ndarray, L: int):
19
+ """
20
+ Simple 1-step-ahead dataset:
21
+ input X_in[i] = X[i : i+L] (L, D)
22
+ target Y_out[i] = X[i+L] (D,)
23
+ """
24
+ X = np.asarray(X, dtype=np.float32)
25
+ T, D = X.shape
26
+ if T <= L:
27
+ raise ValueError(f"T={T} must be > L={L}")
28
+ N = T - L
29
+ X_in = np.stack([X[i:i+L] for i in range(N)], axis=0) # (N, L, D)
30
+ Y_out = np.stack([X[i+L] for i in range(N)], axis=0) # (N, D)
31
+ return X_in, Y_out
32
+
33
+
34
+ def choose_heads(d_model: int, target_head_dim: int = 32, max_heads: int = 8) -> int:
35
+ """Pick a reasonable number of heads given d_model."""
36
+ n_heads = max(1, min(max_heads, d_model // target_head_dim))
37
+ # ensure divisibility
38
+ while d_model % n_heads != 0 and n_heads > 1:
39
+ n_heads -= 1
40
+ return n_heads
41
+
42
+ def build_rotary_inv_freq(head_dim: int, base: float = 10000.0):
43
+ assert head_dim % 2 == 0, "head_dim must be even for RoPE."
44
+ return 1.0 / (base ** (jnp.arange(0, head_dim, 2, dtype=jnp.float32) / head_dim))
45
+
46
+
47
+ def apply_rope(x, inv_freq):
48
+ """
49
+ Apply RoPE to q/k.
50
+
51
+ x: (batch, heads, seq_len, head_dim)
52
+ inv_freq: (head_dim/2,)
53
+ """
54
+ b, h, t, d = x.shape
55
+ half = d // 2
56
+ # positions
57
+ positions = jnp.arange(t, dtype=jnp.float32) # (t,)
58
+ freqs = jnp.einsum("i,j->ij", positions, inv_freq) # (t, half)
59
+ cos = jnp.cos(freqs)[None, None, :, :] # (1,1,t,half)
60
+ sin = jnp.sin(freqs)[None, None, :, :]
61
+
62
+ x1 = x[..., :half] # (b,h,t,half)
63
+ x2 = x[..., half:] # (b,h,t,half)
64
+
65
+ x1_rot = x1 * cos - x2 * sin
66
+ x2_rot = x1 * sin + x2 * cos
67
+ x_rot = jnp.concatenate([x1_rot, x2_rot], axis=-1) # (b,h,t,d)
68
+ return x_rot
69
+
70
+ class MultiHeadSelfAttention(nn.Module):
71
+ d_model: int
72
+ n_heads: int
73
+ dropout: float = 0.0
74
+ use_rope: bool = True
75
+
76
+ @nn.compact
77
+ def __call__(self, x, deterministic: bool):
78
+ """
79
+ x: (batch, seq_len, d_model)
80
+ returns: (batch, seq_len, d_model)
81
+ """
82
+ b, t, d_model = x.shape
83
+ assert d_model == self.d_model
84
+ assert self.d_model % self.n_heads == 0, "d_model must be divisible by n_heads"
85
+ head_dim = self.d_model // self.n_heads
86
+ assert head_dim % 2 == 0, "head_dim must be even for RoPE"
87
+
88
+ # project to qkv
89
+ qkv = nn.Dense(3 * self.d_model, use_bias=False, name="qkv")(x) # (b, t, 3*d_model)
90
+ qkv = qkv.reshape(b, t, 3, self.n_heads, head_dim)
91
+ qkv = qkv.transpose(2, 0, 3, 1, 4) # (3, b, h, t, head_dim)
92
+ q, k, v = qkv[0], qkv[1], qkv[2] # each: (b, h, t, head_dim)
93
+
94
+ if self.use_rope:
95
+ inv_freq = build_rotary_inv_freq(head_dim)
96
+ q = apply_rope(q, inv_freq)
97
+ k = apply_rope(k, inv_freq)
98
+
99
+ # scaled dot-product attention with causal mask
100
+ scale = 1.0 / math.sqrt(head_dim)
101
+ attn_logits = jnp.einsum("bhqd, bhkd -> bhqk", q, k) * scale # (b,h,t,t)
102
+
103
+ # causal mask
104
+ mask = jnp.tril(jnp.ones((t, t), dtype=jnp.bool_))
105
+ attn_logits = jnp.where(mask, attn_logits, -1e9)
106
+
107
+ attn_weights = nn.softmax(attn_logits, axis=-1)
108
+ attn_weights = nn.Dropout(rate=self.dropout)(attn_weights, deterministic=deterministic)
109
+
110
+ attn_output = jnp.einsum("bhqk, bhkd -> bhqd", attn_weights, v) # (b,h,t,head_dim)
111
+ attn_output = attn_output.transpose(0, 2, 1, 3).reshape(b, t, self.d_model) # (b,t,d_model)
112
+ out = nn.Dense(self.d_model, name="out_proj")(attn_output)
113
+ out = nn.Dropout(rate=self.dropout)(out, deterministic=deterministic)
114
+ return out
115
+
116
+ class FeedForward(nn.Module):
117
+ d_model: int
118
+ mlp_ratio: float = 4.0
119
+ dropout: float = 0.0
120
+
121
+ @nn.compact
122
+ def __call__(self, x, deterministic: bool):
123
+ hidden_dim = int(self.d_model * self.mlp_ratio)
124
+ x = nn.Dense(hidden_dim)(x)
125
+ x = nn.gelu(x)
126
+ x = nn.Dropout(rate=self.dropout)(x, deterministic=deterministic)
127
+ x = nn.Dense(self.d_model)(x)
128
+ x = nn.Dropout(rate=self.dropout)(x, deterministic=deterministic)
129
+ return x
130
+
131
+
132
+ class TransformerBlock(nn.Module):
133
+ d_model: int
134
+ n_heads: int
135
+ dropout: float = 0.0
136
+ mlp_ratio: float = 4.0
137
+ use_rope: bool = True
138
+
139
+ @nn.compact
140
+ def __call__(self, x, deterministic: bool):
141
+ # Self-attention
142
+ h = nn.LayerNorm()(x)
143
+ h = MultiHeadSelfAttention(
144
+ d_model=self.d_model,
145
+ n_heads=self.n_heads,
146
+ dropout=self.dropout,
147
+ use_rope=self.use_rope,
148
+ )(h, deterministic=deterministic)
149
+ x = x + h
150
+
151
+ # FFN
152
+ h2 = nn.LayerNorm()(x)
153
+ h2 = FeedForward(
154
+ d_model=self.d_model,
155
+ mlp_ratio=self.mlp_ratio,
156
+ dropout=self.dropout,
157
+ )(h2, deterministic=deterministic)
158
+ x = x + h2
159
+ return x
160
+
161
+ class TARTModel(nn.Module):
162
+ """
163
+ Core autoregressive Transformer that maps
164
+ (batch, L, D_in) -> (batch, L, D_in)
165
+ and we train it to predict the *next* token at each step.
166
+ For forecasting, we use the last position as the next-step prediction.
167
+ """
168
+ input_dim: int
169
+ d_model: int
170
+ n_heads: int
171
+ depth: int
172
+ max_len: int
173
+ dropout: float = 0.0
174
+ use_learned_pos: bool = True
175
+ use_rope: bool = True
176
+
177
+ @nn.compact
178
+ def __call__(self, x, deterministic: bool = True):
179
+ """
180
+ x: (batch, seq_len, input_dim)
181
+ returns: (batch, seq_len, input_dim)
182
+ """
183
+ b, t, d_in = x.shape
184
+ assert d_in == self.input_dim
185
+ if t > self.max_len:
186
+ raise ValueError(f"seq_len={t} exceeds max_len={self.max_len}")
187
+
188
+ # Project input to d_model
189
+ h = nn.Dense(self.d_model, name="token_embed")(x)
190
+
191
+ # Learned positional embedding
192
+ if self.use_learned_pos:
193
+ pos_emb = self.param(
194
+ "pos_emb",
195
+ nn.initializers.normal(stddev=0.02),
196
+ (self.max_len, self.d_model),
197
+ ) # (max_len, d_model)
198
+ positions = jnp.arange(t)[None, :] # (1, t)
199
+ h = h + pos_emb[positions, :] # broadcast to (b,t,d_model)
200
+
201
+ # Stacked transformer blocks
202
+ for i in range(self.depth):
203
+ h = TransformerBlock(
204
+ d_model=self.d_model,
205
+ n_heads=self.n_heads,
206
+ dropout=self.dropout,
207
+ mlp_ratio=4.0,
208
+ use_rope=self.use_rope,
209
+ name=f"block_{i}",
210
+ )(h, deterministic=deterministic)
211
+
212
+ h = nn.LayerNorm(name="final_ln")(h)
213
+ out = nn.Dense(self.input_dim, name="out_proj")(h) # (b,t,D_in)
214
+ return out
215
+
216
+ class TART:
217
+ """
218
+ Time-series AutoReg Transformer (TART)
219
+
220
+ Usage:
221
+ forecaster = TART(R_tX, L=128, d_model=64, depth=4, ...)
222
+ preds = forecaster(F_cX, steps=200)
223
+
224
+ - R_tX: training time-series, shape (T, D)
225
+ - L: context length (sequence length seen by the Transformer)
226
+ """
227
+
228
+ def __init__(self,
229
+ R_tX: np.ndarray,
230
+ L: int,
231
+ d_model: int = 64,
232
+ depth: int = 4,
233
+ n_heads: Optional[int] = None,
234
+ use_learned_pos: bool = True,
235
+ use_rope: bool = True,
236
+ dropout: float = 0.0,
237
+ seed: int = 0,
238
+ val_split: float = 0.1,
239
+ batch_size: int = 64,
240
+ max_epochs: int = 50,
241
+ init_lr: float = 3e-4,
242
+ min_lr: float = 1e-5,
243
+ lr_decay: float = 0.5,
244
+ tol_rel_improve: float = 1e-3,
245
+ patience: int = 5):
246
+
247
+ R_tX = np.asarray(R_tX, dtype=np.float32)
248
+ if R_tX.ndim == 1:
249
+ R_tX = R_tX[:, None]
250
+ T, D = R_tX.shape
251
+ if T <= L:
252
+ raise ValueError(f"T={T} must be > L={L}")
253
+
254
+ self.L = int(L)
255
+ self.D = int(D)
256
+
257
+ # choose d_model / n_heads
258
+ d_model = int(max(16, d_model))
259
+ if n_heads is None:
260
+ n_heads = choose_heads(d_model)
261
+ self.d_model = d_model
262
+ self.n_heads = int(n_heads)
263
+ self.depth = int(depth)
264
+
265
+ # normalize training data
266
+ F_norm, (mu, sd) = zscore(R_tX)
267
+ self._mu = mu
268
+ self._sd = sd
269
+
270
+ # build 1-step-ahead dataset
271
+ X_in, Y_out = make_nextstep_windows(F_norm, L=self.L) # (N,L,D), (N,D)
272
+ N = X_in.shape[0]
273
+ n_val = max(1, int(val_split * N))
274
+ n_tr = N - n_val
275
+ Xtr, Ytr = X_in[:n_tr], Y_out[:n_tr]
276
+ Xva, Yva = X_in[n_tr:], Y_out[n_tr:]
277
+
278
+ print(f"[TART] Train N={N} (train={n_tr}, val={n_val}) | "
279
+ f"L={self.L} D={self.D} | d_model={d_model} heads={n_heads} depth={depth}")
280
+
281
+ # build model
282
+ max_len = self.L # always feed sequences of length L
283
+ model = TARTModel(
284
+ input_dim=self.D,
285
+ d_model=self.d_model,
286
+ n_heads=self.n_heads,
287
+ depth=self.depth,
288
+ max_len=max_len,
289
+ dropout=dropout,
290
+ use_learned_pos=use_learned_pos,
291
+ use_rope=use_rope,
292
+ )
293
+ self.model = model
294
+ self.max_len = max_len
295
+
296
+ rng = jax.random.PRNGKey(seed)
297
+ dummy_x = jnp.zeros((1, self.L, self.D), dtype=jnp.float32)
298
+ params = model.init(rng, dummy_x, deterministic=True)["params"]
299
+
300
+ def create_state(lr):
301
+ tx = optax.adamw(learning_rate=lr, weight_decay=0.0)
302
+ return train_state.TrainState.create(apply_fn=model.apply,
303
+ params=params,
304
+ tx=tx)
305
+
306
+ state = create_state(init_lr)
307
+ self._rng = rng
308
+
309
+ @jax.jit
310
+ def train_step(state, x_batch, y_batch, rng):
311
+ """One training step (MSE on next-step prediction of last token)."""
312
+ dropout_rng, new_rng = jax.random.split(rng)
313
+
314
+ def loss_fn(p):
315
+ preds = state.apply_fn({"params": p},
316
+ x_batch,
317
+ deterministic=False,
318
+ rngs={"dropout": dropout_rng})
319
+ pred_last = preds[:, -1, :] # (B,D)
320
+ loss = jnp.mean((pred_last - y_batch) ** 2)
321
+ return loss
322
+
323
+ loss, grads = jax.value_and_grad(loss_fn)(state.params)
324
+ new_state = state.apply_gradients(grads=grads)
325
+ return new_state, loss, new_rng
326
+
327
+ @jax.jit
328
+ def eval_step(state, x_batch, y_batch):
329
+ preds = state.apply_fn({"params": state.params},
330
+ x_batch,
331
+ deterministic=True)
332
+ pred_last = preds[:, -1, :]
333
+ loss = jnp.mean((pred_last - y_batch) ** 2)
334
+ return loss
335
+
336
+ # move training data to JAX arrays once
337
+ Xtr_j = jnp.asarray(Xtr)
338
+ Ytr_j = jnp.asarray(Ytr)
339
+ Xva_j = jnp.asarray(Xva)
340
+ Yva_j = jnp.asarray(Yva)
341
+
342
+ best_params = state.params
343
+ best_val = float("inf")
344
+ curr_lr = init_lr
345
+ epochs_no_gain = 0
346
+
347
+ num_batches = lambda N: int(np.ceil(N / batch_size))
348
+
349
+ for epoch in range(1, max_epochs + 1):
350
+ # shuffle training indices
351
+ idx = np.arange(n_tr)
352
+ np.random.default_rng(epoch + seed).shuffle(idx)
353
+
354
+ # training loop
355
+ train_losses = []
356
+ rng = self._rng
357
+ for bi in range(num_batches(n_tr)):
358
+ s = bi * batch_size
359
+ e = min((bi + 1) * batch_size, n_tr)
360
+ batch_idx = idx[s:e]
361
+ xb = Xtr_j[batch_idx]
362
+ yb = Ytr_j[batch_idx]
363
+ state, loss, rng = train_step(state, xb, yb, rng)
364
+ train_losses.append(float(loss))
365
+
366
+ self._rng = rng
367
+ tr_loss = float(np.mean(train_losses))
368
+
369
+ # validation
370
+ val_losses = []
371
+ for bi in range(num_batches(n_val)):
372
+ s = bi * batch_size
373
+ e = min((bi + 1) * batch_size, n_val)
374
+ xb = Xva_j[s:e]
375
+ yb = Yva_j[s:e]
376
+ val_losses.append(float(eval_step(state, xb, yb)))
377
+ va_loss = float(np.mean(val_losses))
378
+
379
+ print(f"[TART] epoch {epoch:03d} | train {tr_loss:.6e} | val {va_loss:.6e} | lr {curr_lr:.2e}")
380
+
381
+ # track best
382
+ if va_loss + 1e-8 < best_val:
383
+ rel_gain = (best_val - va_loss) / max(best_val, 1e-8) if best_val < float("inf") else 1.0
384
+ best_val = va_loss
385
+ best_params = state.params
386
+ epochs_no_gain = 0
387
+ else:
388
+ rel_gain = 0.0
389
+ epochs_no_gain += 1
390
+
391
+ # LR schedule on plateau
392
+ if epochs_no_gain >= patience:
393
+ if curr_lr > min_lr * (1.0 + 1e-9):
394
+ curr_lr = max(min_lr, curr_lr * lr_decay)
395
+ print(f"[TART] plateau → lowering LR to {curr_lr:.2e}")
396
+ # rebuild optimizer with new LR, keep params
397
+ params = state.params
398
+ state = train_state.TrainState.create(
399
+ apply_fn=state.apply_fn,
400
+ params=params,
401
+ tx=optax.adamw(learning_rate=curr_lr, weight_decay=0.0),
402
+ )
403
+ epochs_no_gain = 0
404
+ else:
405
+ print(f"[TART] early stop: lr at min and no improvement (best val {best_val:.6e})")
406
+ break
407
+
408
+ # store best params
409
+ self.state = state.replace(params=best_params)
410
+ self.params = self.state.params
411
+
412
+ # parameter count (for paper)
413
+ self.param_count = sum(p.size for p in jax.tree_util.tree_leaves(self.params))
414
+ print(f"[TART] params: {self.param_count:,}")
415
+
416
+ def __call__(self, F_cX: np.ndarray, steps: int) -> np.ndarray:
417
+ """
418
+ Forecast `steps` points given any context slice F_cX that ENDS at
419
+ the forecast start. Uses last L points (with left-padding if needed).
420
+ """
421
+ X = np.asarray(F_cX, dtype=np.float32)
422
+ if X.ndim == 1:
423
+ X = X[:, None]
424
+ if X.shape[1] != self.D:
425
+ raise ValueError(f"Expected D={self.D}, got {X.shape[1]}")
426
+
427
+ # take last L rows (pad on the left if needed)
428
+ if X.shape[0] < self.L:
429
+ pad_len = self.L - X.shape[0]
430
+ pad = np.repeat(X[:1], repeats=pad_len, axis=0)
431
+ ctx = np.concatenate([pad, X], axis=0)
432
+ else:
433
+ ctx = X[-self.L:]
434
+
435
+ # normalize with training stats
436
+ ctx_n = (ctx - self._mu) / self._sd
437
+ ctx_n = jnp.asarray(ctx_n[None, :, :]) # (1, L, D)
438
+
439
+ preds_n = []
440
+ params = self.params
441
+
442
+ for _ in range(steps):
443
+ # deterministic forward (no dropout)
444
+ out = self.model.apply({"params": params}, ctx_n, deterministic=True) # (1,L,D)
445
+ next_n = out[:, -1, :] # (1,D)
446
+ preds_n.append(np.array(next_n[0]))
447
+
448
+ # update context: drop oldest, append new
449
+ ctx_n = jnp.concatenate([ctx_n[:, 1:, :], next_n[:, None, :]], axis=1)
450
+
451
+ preds_n = np.stack(preds_n, axis=0) # (steps, D) normalized
452
+ preds = preds_n * self._sd + self._mu # unnormalize
453
+ return preds