| """Synthetic datasets for quick experiments.""" |
| from __future__ import annotations |
|
|
| import numpy as np |
|
|
|
|
| def make_induction_head_task(n: int = 100, T: int = 30, V: int = 20, seed: int = 42): |
| np.random.seed(seed) |
| perm = np.random.permutation(V) |
|
|
| X = np.zeros((n, T), dtype=int) |
| Y = np.zeros(n, dtype=int) |
|
|
| X[:, 0] = np.random.randint(0, V, n) |
| for i in range(n): |
| for j in range(1, T - 2): |
| if X[i, j - 1] == 0: |
| X[i, j] = np.random.randint(1, V) |
| else: |
| X[i, j] = np.random.randint(0, V) |
| X[:, -2] = np.random.randint(1, V, n) |
| X[:, -1] = 0 |
|
|
| contains_zero = np.any(X[:, :-1] == 0, axis=1) |
| missing_zero_indices = np.where(~contains_zero)[0] |
| if len(missing_zero_indices) > 0: |
| random_indices = np.random.randint(0, T - 2, size=len(missing_zero_indices)) |
| X[missing_zero_indices, random_indices] = 0 |
|
|
| for i in range(n): |
| Y[i] = X[i][np.argwhere(X[i, :-1] == 0).flatten()[-1] + 1] |
|
|
| vocab = 1.5 ** (1 / 3) * np.linspace(-1, 1, V) |
| vocab = vocab[perm] |
| return vocab[X], vocab[Y] |
|
|
|
|
| def make_induction_head_seq_to_seq_task( |
| n: int = 1, T: int = 1000, V: int = 20, seed: int = 42 |
| ): |
| np.random.seed(seed) |
| perm = np.random.permutation(V) |
|
|
| X = np.zeros((n, T), dtype=int) |
| Y = np.zeros((n, T), dtype=int) |
|
|
| X[:, 0] = np.random.randint(0, V, n) |
| for i in range(n): |
| for j in range(1, T - 2): |
| if X[i, j - 1] == 0: |
| X[i, j] = np.random.randint(1, V) |
| else: |
| X[i, j] = np.random.randint(0, V) |
| X[:, -2] = np.random.randint(1, V, n) |
| X[:, -1] = 0 |
|
|
| contains_zero = np.any(X[:, :-1] == 0, axis=1) |
| missing_zero_indices = np.where(~contains_zero)[0] |
| if len(missing_zero_indices) > 0: |
| random_indices = np.random.randint(0, T - 2, size=len(missing_zero_indices)) |
| X[missing_zero_indices, random_indices] = 0 |
|
|
| for i in range(n): |
| current_val = 0 |
| flag = False |
| for t in range(T): |
| if X[i, t] == 0: |
| flag = True |
| elif flag: |
| current_val = X[i, t] |
| flag = False |
| Y[i, t] = current_val |
|
|
| vocab = 1.5 ** (1 / 3) * np.linspace(-1, 1, V) |
| vocab[0] *= 2 |
| vocab = vocab[perm] |
| return vocab[X], vocab[Y] |
|
|
|
|
| def get_rbf_sample( |
| T: int, |
| *, |
| dt: float = 1.0, |
| lengthscale: float = 10.0, |
| sigma: float = 1.0, |
| mu: float = 0.0, |
| seed: int | None = None, |
| embed_factor: int = 2, |
| jitter: float = 1e-12, |
| clip_neg_eigs: bool = True, |
| ) -> np.ndarray: |
| """ |
| Efficiently sample from a 1D zero-mean GP on an evenly spaced grid using an RBF kernel, |
| via circulant embedding + FFT (O(M log M)). |
| |
| GP prior on indices {0,...,T-1} with covariance: |
| k(i,j) = sigma^2 * exp(-( (i-j)*dt )^2 / (2*lengthscale^2)) |
| |
| Args: |
| T: number of samples |
| dt: grid spacing |
| lengthscale: RBF lengthscale ℓ (>0) |
| sigma: marginal std (>=0) |
| mu: mean |
| seed: RNG seed |
| embed_factor: embedding size multiplier; M is next power of 2 >= embed_factor*T. |
| 2 is standard; increase (e.g. 4) if negative eigenvalues occur. |
| jitter: small diagonal jitter added to k(0) to help numerical stability |
| clip_neg_eigs: if True, clip tiny negative FFT eigenvalues to 0. |
| |
| Returns: |
| z: (T,) sample |
| """ |
| if T <= 0: |
| return np.zeros((0,), dtype=np.float64) |
| if dt <= 0: |
| raise ValueError("dt must be > 0") |
| if lengthscale <= 0: |
| raise ValueError("lengthscale must be > 0") |
| if sigma < 0: |
| raise ValueError("sigma must be >= 0") |
| if embed_factor < 2: |
| raise ValueError("embed_factor should be >= 2 for circulant embedding") |
|
|
| rng = np.random.default_rng(seed) |
|
|
| |
| M_min = embed_factor * T |
| M = 1 << (M_min - 1).bit_length() |
| if M % 2 != 0: |
| M += 1 |
|
|
| |
| d = np.arange(T, dtype=np.float64) * dt |
| k_col = (sigma ** 2) * np.exp(-0.5 * (d / lengthscale) ** 2) |
| k_col[0] += jitter |
|
|
| |
| |
| c = np.zeros(M, dtype=np.float64) |
| c[:T] = k_col |
| c[M - (T - 1) :] = k_col[1:][::-1] |
|
|
| |
| lam = np.real(np.fft.fft(c)) |
| if clip_neg_eigs: |
| lam = np.maximum(lam, 0.0) |
| else: |
| if np.min(lam) < -1e-10: |
| raise ValueError( |
| f"Circulant embedding not PSD (min eigenvalue {np.min(lam)}). " |
| "Increase embed_factor or enable clip_neg_eigs." |
| ) |
| lam = np.maximum(lam, 0.0) |
|
|
| |
| |
| |
| |
| Z = np.zeros(M, dtype=np.complex128) |
|
|
| |
| Z[0] = rng.normal() |
| Z[M // 2] = rng.normal() |
|
|
| |
| re = rng.normal(size=(M // 2 - 1)) |
| im = rng.normal(size=(M // 2 - 1)) |
| Z[1 : M // 2] = (re + 1j * im) / np.sqrt(2.0) |
| |
| Z[M // 2 + 1 :] = np.conj(Z[1 : M // 2][::-1]) |
|
|
| |
| Y = np.sqrt(lam) * Z |
|
|
| |
| x_full = (np.sqrt(M) * np.fft.ifft(Y)).real |
|
|
| return mu + x_full[:T] |
|
|
|
|
| def rbf_kernel_1d(i: int, j: int, *, sigma: float, lengthscale: float, dt: float) -> float: |
| d = (i - j) * dt |
| return (sigma ** 2) * np.exp(-0.5 * (d / lengthscale) ** 2) |
|
|
|
|
| def oracle_mse_rbf_periodic_censor( |
| P: int, |
| L: int, |
| *, |
| sigma: float = 1.0, |
| lengthscale: float = 10.0, |
| dt: float = 1.0, |
| num_periods: int = 50, |
| burn_periods: int = 10, |
| jitter: float = 1e-10, |
| ) -> dict: |
| """ |
| Oracle MSE for the censored-copy task when Z_t ~ GP(mean, RBF kernel). |
| |
| Censoring pattern (matches ``make_censored_task``): |
| observed at index i iff (i % P) < (P//2). |
| But due to the task's lagging, we only ever observe indices >= L (because X is input_signal[L:]). |
| |
| At model time t, the oracle conditions on all observed indices <= (t+L), i.e. fixed-lag smoothing. |
| |
| Returns: |
| { |
| "mse": (T,) oracle per-timestep MSE trace, |
| "avg_mse_last_period": scalar average over the last period (after burn-in), |
| "avg_mse_post_burn": scalar average over all timesteps after burn-in, |
| } |
| """ |
| if P <= 0 or P % 2 != 0: |
| raise ValueError("P must be a positive even integer.") |
| if L < 0: |
| raise ValueError("L must be >= 0") |
| if sigma < 0 or lengthscale <= 0 or dt <= 0: |
| raise ValueError("Require sigma>=0, lengthscale>0, dt>0") |
| if num_periods <= 0: |
| raise ValueError("num_periods must be > 0") |
| if burn_periods < 0 or burn_periods >= num_periods: |
| raise ValueError("burn_periods must be in [0, num_periods-1]") |
|
|
| m = P // 2 |
| T = num_periods * P |
| N = T + L |
| burn_T = burn_periods * P |
|
|
| |
| |
| observed = np.zeros(N, dtype=bool) |
| for i in range(L, N): |
| observed[i] = (i % P) < m |
|
|
| |
| obs_idx: list[int] = [] |
| chol_L = np.zeros((0, 0), dtype=np.float64) |
|
|
| def add_observation(i_new: int): |
| """Add new observed index to the Cholesky factor (noise-free GP with jitter).""" |
| nonlocal chol_L, obs_idx |
| if len(obs_idx) == 0: |
| k_nn = rbf_kernel_1d(i_new, i_new, sigma=sigma, lengthscale=lengthscale, dt=dt) + jitter |
| chol_L = np.array([[np.sqrt(k_nn)]], dtype=np.float64) |
| obs_idx.append(i_new) |
| return |
|
|
| |
| k_vec = np.array( |
| [rbf_kernel_1d(i_new, j, sigma=sigma, lengthscale=lengthscale, dt=dt) for j in obs_idx], |
| dtype=np.float64, |
| ) |
|
|
| |
| w = np.linalg.solve(chol_L, k_vec) |
| k_nn = rbf_kernel_1d(i_new, i_new, sigma=sigma, lengthscale=lengthscale, dt=dt) + jitter |
| diag_sq = k_nn - float(w @ w) |
| diag = np.sqrt(max(diag_sq, jitter)) |
|
|
| |
| M = len(obs_idx) |
| L_new = np.zeros((M + 1, M + 1), dtype=np.float64) |
| L_new[:M, :M] = chol_L |
| L_new[M, :M] = w |
| L_new[M, M] = diag |
|
|
| chol_L = L_new |
| obs_idx.append(i_new) |
|
|
| def posterior_var(test_t: int) -> float: |
| """Var(Z_test_t | observed indices obs_idx), using current Cholesky.""" |
| k_tt = rbf_kernel_1d(test_t, test_t, sigma=sigma, lengthscale=lengthscale, dt=dt) |
|
|
| if len(obs_idx) == 0: |
| return k_tt |
|
|
| if obs_idx[-1] == test_t or test_t in obs_idx: |
| return 0.0 |
|
|
| k_tO = np.array( |
| [rbf_kernel_1d(test_t, j, sigma=sigma, lengthscale=lengthscale, dt=dt) for j in obs_idx], |
| dtype=np.float64, |
| ) |
| |
| alpha = np.linalg.solve(chol_L, k_tO) |
| var = k_tt - float(alpha @ alpha) |
| return max(var, 0.0) |
|
|
| |
| mse = np.zeros(T, dtype=np.float64) |
|
|
| for h in range(N): |
| if observed[h]: |
| add_observation(h) |
|
|
| if h >= L: |
| t = h - L |
| if t < T: |
| mse[t] = posterior_var(t) |
|
|
| |
| post_burn = mse[burn_T:] if burn_T < T else mse |
| avg_post_burn = float(np.mean(post_burn)) if post_burn.size else float(np.mean(mse)) |
|
|
| return avg_post_burn |
|
|
|
|
| def get_ou_sample( |
| T: int, |
| dt: float = 1.0, |
| tau: float = 10.0, |
| sigma: float = 1.0, |
| mu: float = 0.0, |
| seed: int | None = None, |
| ) -> np.ndarray: |
| """ |
| Sample a stationary Ornstein–Uhlenbeck process at discrete times. |
| |
| Continuous-time OU (one common parametrization): |
| dX_t = -(1/tau) (X_t - mu) dt + sigma dW_t |
| |
| Discretization (exact transition): |
| X_{t+dt} = mu + rho (X_t - mu) + eps |
| rho = exp(-dt/tau) |
| eps ~ N(0, q), q = (sigma^2 * tau / 2) * (1 - rho^2) |
| |
| Stationary distribution: |
| X_t ~ N(mu, sigma^2 * tau / 2) |
| |
| Args: |
| T: number of samples to return |
| dt: sampling interval |
| tau: relaxation time constant (> 0) |
| sigma: diffusion scale (>= 0) |
| mu: mean |
| seed: RNG seed |
| |
| Returns: |
| x: (T,) numpy array |
| """ |
| if T <= 0: |
| return np.zeros((0,), dtype=np.float64) |
| if tau <= 0: |
| raise ValueError("tau must be > 0") |
| if sigma < 0: |
| raise ValueError("sigma must be >= 0") |
|
|
| rng = np.random.default_rng(seed) |
|
|
| rho = np.exp(-dt / tau) |
| var_stationary = (sigma**2) * tau / 2.0 |
|
|
| |
| q = var_stationary * (1.0 - rho**2) |
|
|
| x = np.empty((T,), dtype=np.float64) |
| x[0] = mu + np.sqrt(var_stationary) * rng.standard_normal() |
|
|
| if T > 1: |
| noise = np.sqrt(q) * rng.standard_normal(size=T - 1) |
| for t in range(T - 1): |
| x[t + 1] = mu + rho * (x[t] - mu) + noise[t] |
|
|
| return x |
|
|
|
|
| def oracle_mse_censored_task( |
| P: int, |
| L: int, |
| *, |
| dt: float = 1.0, |
| tau: float = 10.0, |
| sigma: float = 1.0, |
| ) -> float: |
| """ |
| Oracle (minimum expected) per-timestep MSE for the censored-copy task |
| under a stationary OU process, with periodic censoring pattern: |
| |
| - Period length: P |
| - First half of each period: uncensored (perfect observation => MSE=0) |
| - Second half: censored (missing observations) |
| - Lookahead (in original time index units): L (the task lag) |
| |
| Assumes the oracle uses all uncensored samples up to time t+L to predict Z_t. |
| Because OU is Gaussian Markov (AR(1)), the per-step conditional variance is: |
| |
| If future endpoint u is NOT yet observed: |
| Var(Z_t | Z_s) = v * (1 - rho^(2 d1)) |
| |
| If future endpoint u IS observed: |
| Var(Z_t | Z_s, Z_u) = v * (1 - a^2 - b^2 + 2ab rho^D) / (1 - rho^(2D)), |
| which simplifies to: |
| v * (1 - rho^(2 d1) - rho^(2 d2) + rho^(2D)) / (1 - rho^(2D)). |
| |
| Here: |
| rho = exp(-dt/tau) |
| v = stationary variance = sigma^2 * tau / 2 |
| m = P/2 (must be integer; require even P) |
| s = last uncensored time in the period, u = first uncensored time next period |
| D = u - s = m + 1 |
| For censored positions: k = 1..m with d1 = k and d2 = D - k. |
| |
| Returns: |
| Long-run average MSE per timestep (averaged over one period). |
| """ |
| if P <= 0: |
| raise ValueError("P must be positive") |
| if P % 2 != 0: |
| raise ValueError("This oracle formula assumes even P (because censoring uses P//2).") |
| if L < 0: |
| raise ValueError("L must be >= 0") |
| if tau <= 0: |
| raise ValueError("tau must be > 0") |
| if sigma < 0: |
| raise ValueError("sigma must be >= 0") |
|
|
| m = P // 2 |
| rho = np.exp(-dt / tau) |
| v = (sigma**2) * tau / 2.0 |
|
|
| D = m + 1 |
| rho2D = rho ** (2 * D) |
| denom = 1.0 - rho2D |
|
|
| mse_sum = 0.0 |
|
|
| |
| for k in range(1, m + 1): |
| d1 = k |
| d2 = D - k |
|
|
| if d2 > L: |
| |
| mse_k = v * (1.0 - rho ** (2 * d1)) |
| else: |
| |
| |
| mse_k = v * (1.0 - rho ** (2 * d1) - rho ** (2 * d2) + rho2D) / denom |
|
|
| mse_sum += mse_k |
|
|
| |
| return mse_sum / P |
|
|
|
|
| def oracle_info_gain( |
| X: np.ndarray, |
| *, |
| lag: int, |
| censor_val: float, |
| dt: float = 1.0, |
| tau: float = 10.0, |
| sigma: float = 1.0, |
| mu: float = 0.0, |
| atol: float = 0.0, |
| eps: float = 1e-12, |
| ) -> np.ndarray: |
| """ |
| Oracle information-gain (salience) trace for the censored OU task. |
| |
| info_gain[t] = 0.5 * log( |
| Var(Z_t | obs <= t+lag-1) / Var(Z_t | obs <= t+lag) |
| ) |
| |
| This measures how much the *newest* input sample at time (t+lag) |
| reduces uncertainty about the current target Z_t. |
| |
| Args: |
| X: (T,) input signal (censored OU samples) |
| lag: fixed lag used in the task |
| censor_val: value used to mark censored samples |
| dt, tau, sigma, mu: OU parameters |
| atol: optional tolerance for detecting censor_val |
| eps: numerical stability |
| |
| Returns: |
| info_gain: (T,) oracle salience / information-gain trace |
| """ |
| X = np.asarray(X, dtype=np.float64) |
| T = X.shape[0] |
|
|
| rho = np.exp(-dt / tau) |
| v = (sigma**2) * tau / 2.0 |
|
|
| |
| N = T + lag |
|
|
| observed = np.zeros(N, dtype=bool) |
| obs_value = np.zeros(N, dtype=np.float64) |
|
|
| if atol > 0: |
| obs_mask = np.abs(X - censor_val) > atol |
| else: |
| obs_mask = X != censor_val |
|
|
| ks = lag + np.arange(T) |
| observed[ks] = obs_mask |
| obs_value[ks] = X |
|
|
| |
| last_obs_leq = np.full(N, -1, dtype=int) |
| last = -1 |
| for k in range(N): |
| if observed[k]: |
| last = k |
| last_obs_leq[k] = last |
|
|
| |
| next_obs_geq = np.full(N, N, dtype=int) |
| nxt = N |
| for k in range(N - 1, -1, -1): |
| if observed[k]: |
| nxt = k |
| next_obs_geq[k] = nxt |
|
|
| def posterior_var(t: int, horizon: int) -> float: |
| """Var(Z_t | uncensored obs with indices <= horizon).""" |
| if horizon < 0: |
| return v |
|
|
| horizon = min(horizon, N - 1) |
|
|
| s = last_obs_leq[min(t, horizon)] if t >= 0 else -1 |
| u = next_obs_geq[t] if t <= horizon and next_obs_geq[t] <= horizon else N |
|
|
| if s == t and s != -1: |
| return 0.0 |
|
|
| if s == -1 and u == N: |
| return v |
| if s == -1: |
| d = u - t |
| return v * (1.0 - rho ** (2 * d)) |
| if u == N: |
| d = t - s |
| return v * (1.0 - rho ** (2 * d)) |
|
|
| d1 = t - s |
| d2 = u - t |
| D = d1 + d2 |
| rho2D = rho ** (2 * D) |
| return v * (1.0 - rho ** (2 * d1) - rho ** (2 * d2) + rho2D) / max( |
| 1.0 - rho2D, eps |
| ) |
|
|
| info_gain = np.zeros(T, dtype=np.float64) |
|
|
| for t in range(T): |
| var_prev = posterior_var(t, t + lag - 1) |
| var_post = posterior_var(t, t + lag) |
| info_gain[t] = 0.5 * np.log((var_prev + eps) / (var_post + eps)) |
|
|
| return info_gain |
|
|
|
|
|
|
|
|
| def oracle_info_gain_rbf( |
| X: np.ndarray, |
| *, |
| lag: int, |
| censor_val: float, |
| dt: float = 1.0, |
| lengthscale: float = 10.0, |
| sigma: float = 1.0, |
| atol: float = 0.0, |
| jitter: float = 1e-10, |
| eps: float = 1e-12, |
| max_obs: int | None = None, |
| ) -> np.ndarray: |
| """ |
| Oracle information-gain trace for the censored task under an RBF-kernel GP prior. |
| |
| Prior: |
| Z ~ GP(mu, k), k(i,j)=sigma^2 * exp(-((i-j)*dt)^2/(2*ell^2)) |
| Observations: |
| Noise-free: observe Z_k exactly at uncensored k; censored => missing. |
| |
| At model time t: |
| horizon h = t + lag (original-time index of current input X[t]) |
| IG_t = 0.5 * log( Var(Z_t | obs<=h-1) / Var(Z_t | obs<=h) ) |
| |
| Args: |
| X: (T,) input stream; X[t] is either Z_{t+lag} (uncensored) or censor_val (censored) |
| lag: task lag L |
| censor_val: sentinel for missing observations |
| dt, lengthscale, sigma: RBF GP kernel params |
| atol: optional tolerance for detecting censor_val |
| jitter: diagonal jitter for numerical stability |
| eps: stability for log ratio |
| max_obs: cap number of retained observed points (approximation). If None, keeps all. |
| |
| Returns: |
| info_gain: (T,) oracle IG trace |
| """ |
| if max_obs is None: |
| max_obs = int(lag + 3 * lengthscale / dt) |
| X = np.asarray(X, dtype=np.float64) |
| T = X.shape[0] |
| if lag < 0: |
| raise ValueError("lag must be >= 0") |
| if dt <= 0 or lengthscale <= 0: |
| raise ValueError("dt and lengthscale must be > 0") |
| if sigma < 0: |
| raise ValueError("sigma must be >= 0") |
|
|
| |
| N = T + lag |
|
|
| |
| observed = np.zeros(N, dtype=bool) |
| if atol > 0: |
| obs_mask = np.abs(X - censor_val) > atol |
| else: |
| obs_mask = X != censor_val |
| ks = lag + np.arange(T) |
| observed[ks] = obs_mask |
|
|
| |
| inv_ell2 = 1.0 / (lengthscale * lengthscale) |
| sigma2 = sigma * sigma |
|
|
| def k_vec(t_idx: int, obs_idx: np.ndarray) -> np.ndarray: |
| d = (t_idx - obs_idx).astype(np.float64) * dt |
| return sigma2 * np.exp(-0.5 * (d * d) * inv_ell2) |
|
|
| def k_tt(_: int) -> float: |
| return sigma2 |
|
|
| |
| obs_idx: list[int] = [] |
| chol_L = np.zeros((0, 0), dtype=np.float64) |
|
|
| def rebuild_cholesky(): |
| nonlocal chol_L |
| if len(obs_idx) == 0: |
| chol_L = np.zeros((0, 0), dtype=np.float64) |
| return |
| idx = np.array(obs_idx, dtype=np.int64) |
| d = (idx[:, None] - idx[None, :]).astype(np.float64) * dt |
| K = sigma2 * np.exp(-0.5 * (d * d) * inv_ell2) |
| K[np.diag_indices_from(K)] += jitter |
| chol_L = np.linalg.cholesky(K) |
|
|
| def add_observation(i_new: int): |
| """Rank-1 append update; if we truncate (drop oldest), rebuild.""" |
| nonlocal chol_L |
|
|
| |
| obs_idx.append(i_new) |
| if max_obs is not None and len(obs_idx) > max_obs: |
| |
| obs_idx.pop(0) |
| rebuild_cholesky() |
| return |
|
|
| |
| if chol_L.shape[0] == 0: |
| chol_L = np.array([[np.sqrt(k_tt(i_new) + jitter)]], dtype=np.float64) |
| return |
|
|
| idx = np.array(obs_idx[:-1], dtype=np.int64) |
| kv = k_vec(i_new, idx) |
| w = np.linalg.solve(chol_L, kv) |
| diag_sq = (k_tt(i_new) + jitter) - float(w @ w) |
| diag = np.sqrt(max(diag_sq, jitter)) |
|
|
| M = chol_L.shape[0] |
| L_new = np.zeros((M + 1, M + 1), dtype=np.float64) |
| L_new[:M, :M] = chol_L |
| L_new[M, :M] = w |
| L_new[M, M] = diag |
| chol_L = L_new |
|
|
| def posterior_var(t_idx: int) -> float: |
| """Var(Z_t | current obs_idx).""" |
| if len(obs_idx) == 0: |
| return k_tt(t_idx) |
| idx = np.array(obs_idx, dtype=np.int64) |
| ktO = k_vec(t_idx, idx) |
| alpha = np.linalg.solve(chol_L, ktO) |
| var = k_tt(t_idx) - float(alpha @ alpha) |
| return max(var, 0.0) |
|
|
| |
| info_gain = np.zeros(T, dtype=np.float64) |
|
|
| for h in range(N): |
| |
| if h >= lag: |
| t = h - lag |
|
|
| |
| var_prev = posterior_var(t) |
|
|
| |
| if observed[h]: |
| add_observation(h) |
|
|
| |
| var_post = posterior_var(t) |
|
|
| info_gain[t] = 0.5 * np.log((var_prev + eps) / (var_post + eps)) |
|
|
| else: |
| |
| |
| if observed[h]: |
| add_observation(h) |
|
|
| return info_gain |
|
|
|
|
|
|
| def make_censored_task( |
| T: int = 1000, |
| lag: int = 10, |
| source: str = "get_ou_sample", |
| censor_period: int = 40, |
| seed: int = 0, |
| censor_val: float = -3.0, |
| **signal_kwargs, |
| ): |
| np.random.seed(seed) |
|
|
| if source == "whitesignal": |
| dt = signal_kwargs.pop("dt", 0.01) |
| freq = signal_kwargs.pop("freq", 1.0) |
| rms = signal_kwargs.pop("rms", 0.5) |
| output_signal = whitesignal(period=(T + lag) * dt, dt=dt, freq=freq, rms=rms, **signal_kwargs) |
| elif source == "ou": |
| output_signal = get_ou_sample(T=(T+lag), **signal_kwargs) |
| elif source == "rbf": |
| output_signal = get_rbf_sample(T=(T+lag), **signal_kwargs) |
| else: |
| raise ValueError(f"Unknown source '{source}'. Use 'whitesignal' or 'ou'.") |
|
|
| input_signal = np.copy(output_signal) |
| mask = (np.arange(T+lag) % censor_period) >= (censor_period // 2) |
|
|
| input_signal[mask] = censor_val |
| |
| return input_signal[lag:], output_signal[:-lag] |
|
|
|
|
| def make_copying_task( |
| T: int = 1000, |
| lag: int = 10, |
| source: str = "get_ou_sample", |
| seed: int = 0, |
| **signal_kwargs, |
| ): |
| """ |
| Build a simple sequence-copying task from a continuous signal. |
| |
| The input is generated either by :func:`whitesignal` or |
| :func:`get_ou_sample`. The output is the same signal shifted forward by |
| ``lag`` time steps, forcing a sequence model to retain information over that |
| window to predict correctly. |
| |
| Parameters |
| ---------- |
| T : int, optional |
| Length of the sequence. |
| lag : int, optional |
| Number of time steps to shift the target output relative to the input. |
| source : {"whitesignal", "ou"}, optional |
| Which generator to use. "ou" selects :func:`get_ou_sample`. |
| seed : int, optional |
| Random seed used for reproducibility. |
| **signal_kwargs : |
| Additional keyword arguments forwarded to the signal generator. |
| |
| Returns |
| ------- |
| input_signal : ndarray, shape (T,) |
| The driving input sequence. |
| target_signal : ndarray, shape (T,) |
| The delayed copy of ``input_signal``. |
| """ |
|
|
| if lag <= 0: |
| raise ValueError("lag must be positive to form a copying task") |
| if lag >= T: |
| raise ValueError("lag must be smaller than T to produce a valid shift") |
|
|
| np.random.seed(seed) |
|
|
| if source == "whitesignal": |
| dt = signal_kwargs.pop("dt", 0.01) |
| freq = signal_kwargs.pop("freq", 1.0) |
| rms = signal_kwargs.pop("rms", 0.5) |
| input_signal = whitesignal(period=T * dt, dt=dt, freq=freq, rms=rms, **signal_kwargs) |
| elif source in {"ou", "get_ou_sample"}: |
| input_signal = get_ou_sample(T=T, **signal_kwargs) |
| else: |
| raise ValueError(f"Unknown source '{source}'. Use 'whitesignal' or 'ou'.") |
|
|
| target_signal = np.zeros_like(input_signal) |
| target_signal[lag:] = input_signal[: T - lag] |
|
|
| return input_signal, target_signal |
|
|
|
|
| def make_multiplexing_task( |
| T: int = 1000, K: int = 3, lag: int = 3, repeat: int = 1, seed: int = 42 |
| ): |
| np.random.seed(seed) |
| assert T % repeat == 0 |
| multiplex_pattern = np.random.randint(0, K, size=T // repeat) |
| multiplex_pattern = np.repeat(multiplex_pattern, repeat) |
|
|
| orig_seqs = np.stack([get_ou_sample(T=T) for _ in range(K)], axis=1) |
| orig_seqs = 1.0 / (1.0 + np.exp(-orig_seqs)) |
|
|
| all_seqs = 0.8 * orig_seqs + 0.1 |
| all_seqs = all_seqs + np.arange(K)[None] |
| all_seqs -= np.mean(all_seqs) |
| all_seqs /= np.std(all_seqs) |
|
|
| X = np.zeros(T) |
| Y = np.zeros((T, K)) |
| histories = np.zeros((K, lag)) |
| task_idx = np.zeros(K, dtype=int) |
| for i, k in enumerate(multiplex_pattern): |
| X[i] = all_seqs[task_idx[k], k] |
| histories[k] = np.roll(histories[k], -1) |
| histories[k, -1] = orig_seqs[task_idx[k], k] |
| task_idx[k] += 1 |
| Y[i] = np.mean(histories, axis=1) |
| return X, Y |
|
|
|
|
| def make_induction_head_multioutput_s2s_task( |
| T: int = 1000, V: int = 30, K: int = 5, seed: int = 42 |
| ): |
| np.random.seed(seed) |
|
|
| X = np.zeros(T, dtype=int) |
| Y = np.zeros((T, K), dtype=int) |
|
|
| X[0] = np.random.randint(0, V) |
| for j in range(1, T): |
| if X[j - 1] < K: |
| X[j] = np.random.randint(K, V) |
| else: |
| X[j] = np.random.randint(0, V) |
|
|
| current_vals = np.zeros(K, dtype=int) |
| flag = False |
| for t in range(T): |
| if t > 0: |
| Y[t] = Y[t - 1] |
| if X[t] < K: |
| flag = True |
| elif flag: |
| flag = False |
| Y[t, X[t - 1]] = X[t] |
|
|
| vocab = 1.5 ** (1 / 3) * np.linspace(-1, 1, V - K) |
| special_vocab = vocab[0] + np.linspace(-3, -1, K) |
| vocab = np.concatenate([special_vocab, vocab], axis=0) |
| return vocab[X], vocab[Y] |
|
|
|
|
| def make_implicit_measure_task(T: int = 100, filter_size: int = 15, seed: int = 42): |
| np.random.seed(seed) |
| t = np.linspace(0, 1, T) |
| clean_signal = np.sin(8 * np.pi * t) |
| mask = np.zeros(T) |
| mask[:-filter_size] = 1.0 |
| observed = clean_signal.copy() |
| noise = 0.05 * np.random.randn(T) |
| observed += noise |
| observed[mask == 0] = -4.0 |
| return observed, clean_signal |
|
|
| def make_simple_repetition_task(T: int = 100, shift: int = 10, seed: int = 42): |
|
|
| np.random.seed(seed) |
|
|
| t = np.linspace(0, 1, T) |
| observed = np.sin(8 * np.pi * t) |
| noise = 0.2 * np.random.randn(T) |
| observed += noise |
|
|
| output = observed.copy() |
| output = output[:-shift] |
| output = np.concatenate([np.zeros(shift), output]) |
|
|
| return observed, output |
|
|
|
|
| def whitesignal(period, dt, freq, rms=0.5, batch_shape=(), seed=None): |
| """ |
| Copied from github.com/state-spaces/s4 |
| |
| Produces output signal of length period / dt, band-limited to frequency freq |
| Output shape (*batch_shape, period/dt) |
| Adapted from the nengo library |
| """ |
| assert not (freq is not None and freq < 1.0 / period) |
| assert freq <= 0.5 / dt |
|
|
| if seed is not None: |
| np.random.seed(seed) |
|
|
| n_coefficients = int(np.ceil(period / dt / 2.0)) |
| shape = batch_shape + (n_coefficients + 1,) |
| sigma = rms * np.sqrt(0.5) |
| coefficients = 1j * np.random.normal(0.0, sigma, size=shape) |
| coefficients[..., -1] = 0.0 |
| coefficients += np.random.normal(0.0, sigma, size=shape) |
| coefficients[..., 0] = 0.0 |
| set_to_zero = np.fft.rfftfreq(2 * n_coefficients, d=dt) > freq |
| coefficients *= 1 - set_to_zero |
| power_correction = np.sqrt(1.0 - np.sum(set_to_zero, dtype=float) / n_coefficients) |
| if power_correction > 0: |
| coefficients /= power_correction |
| coefficients *= np.sqrt(2 * n_coefficients) |
| signal = np.fft.irfft(coefficients, axis=-1) |
| return signal |
|
|
|
|
| def wray_and_green_output(input_signal, a=2.0, m=0.3, k=0.08, tau_max=50, scaling=4e-3): |
| """Implement the system described in Wray and Green (1994).""" |
| T = len(input_signal) |
|
|
| |
| mu = lambda t: a / m * np.exp(-k * t) * np.sin(m * t) |
| tau_vals = np.arange(tau_max) |
| filter = mu(tau_vals)[::-1] |
| filter = scaling * np.outer(filter, filter) |
|
|
| |
| output_signal = np.zeros_like(input_signal) |
| input_signal = np.concatenate([np.zeros(tau_max - 1), input_signal], axis=0) |
|
|
| |
| for i in range(T): |
| input_slice = input_signal[i : i + tau_max] |
| output_signal[i] = np.sum(filter * np.outer(input_slice, input_slice)) |
|
|
| return output_signal |
|
|
|
|
| __all__ = [ |
| "get_ou_sample", |
| "make_copying_task", |
| "make_induction_head_multioutput_s2s_task", |
| "make_induction_head_seq_to_seq_task", |
| "make_induction_head_task", |
| "make_implicit_measure_task", |
| "make_multiplexing_task", |
| "whitesignal", |
| "wray_and_green_output", |
| ] |
|
|