|
| 1 | +"""This is an encode-only implementation of the TopK autoencoder. |
| 2 | +The training code lives in the x-tabdeveloping/latent_terms GitHub repo""" |
| 3 | + |
| 4 | +import warnings |
| 5 | +from functools import partial |
| 6 | +from typing import Optional |
| 7 | + |
| 8 | +import numpy as np |
| 9 | +import scipy.sparse as spr |
| 10 | +from sklearn.base import BaseEstimator, TransformerMixin |
| 11 | +from tqdm import trange |
| 12 | + |
| 13 | +try: |
| 14 | + import jax.numpy as jnp |
| 15 | + from jax import jit |
| 16 | + from jax.lax import top_k |
| 17 | +except ModuleNotFoundError: |
| 18 | + warnings.warn("JAX not found, continuing with NumPy implementation.") |
| 19 | + jnp = np |
| 20 | + |
| 21 | + # Dummy JIT as the identity function |
| 22 | + def jit(f): |
| 23 | + return f |
| 24 | + |
| 25 | + # NumPy implementation of the TopK activation function. |
| 26 | + def top_k(a, k, *, axis=-1): |
| 27 | + if axis is None: |
| 28 | + axis_size = a.size |
| 29 | + else: |
| 30 | + axis_size = a.shape[axis] |
| 31 | + index_array = np.argpartition(a, axis_size - k, axis=axis) |
| 32 | + topk_indices = np.take(index_array, -np.arange(k) - 1, axis=axis) |
| 33 | + topk_values = np.take_along_axis(a, topk_indices, axis=axis) |
| 34 | + return topk_values, topk_indices |
| 35 | + |
| 36 | + |
| 37 | +def top_k_activation(z, k: int): |
| 38 | + values, indices = top_k(z, k=k, axis=-1) |
| 39 | + threshold = jnp.min(values, axis=-1) |
| 40 | + condition = threshold[:, None] <= z |
| 41 | + return jnp.where(condition, z, 0) |
| 42 | + |
| 43 | + |
| 44 | +def encode(params, x, k: int): |
| 45 | + z = x @ params["W_e"] + params["b_e"] |
| 46 | + return top_k_activation(z, k) |
| 47 | + |
| 48 | + |
| 49 | +class TopKAutoEncoder(BaseEstimator, TransformerMixin): |
| 50 | + def __init__( |
| 51 | + self, |
| 52 | + n_latent: int = 32768, |
| 53 | + top_k: int = 16, |
| 54 | + lr: float = 1e-3, |
| 55 | + batch_size: int = 4096, |
| 56 | + n_epochs: int = 10, |
| 57 | + alpha: float = 0.03, |
| 58 | + show_progress_bar: bool = True, |
| 59 | + random_state: Optional[int] = None, |
| 60 | + ): |
| 61 | + self.random_state = random_state |
| 62 | + self.n_latent = n_latent |
| 63 | + self.lr = lr |
| 64 | + self.alpha = alpha |
| 65 | + self.top_k = top_k |
| 66 | + self.batch_size = batch_size |
| 67 | + self.n_epochs = n_epochs |
| 68 | + self.show_progress_bar = show_progress_bar |
| 69 | + |
| 70 | + def fit(self, X, y=None): |
| 71 | + # Training is implemented here: https://github.com/x-tabdeveloping/latent_terms |
| 72 | + return self |
| 73 | + |
| 74 | + def to_dict(self) -> dict: |
| 75 | + return dict( |
| 76 | + attr=self.get_params(), |
| 77 | + params=self._params, |
| 78 | + loss_curve=self.loss_curve_, |
| 79 | + ) |
| 80 | + |
| 81 | + @classmethod |
| 82 | + def from_dict(cls, data): |
| 83 | + obj = cls(**data["attr"]) |
| 84 | + params = data["params"] |
| 85 | + obj.coef_ = np.array(params["W_e"]) |
| 86 | + obj.coef_d_ = np.array(params["W_d"]) |
| 87 | + obj.intercept_ = np.array(params["b_e"]) |
| 88 | + obj.intercept_d_ = np.array(params["b_d"]) |
| 89 | + obj.loss_curve_ = data["loss_curve"] |
| 90 | + return obj |
| 91 | + |
| 92 | + @property |
| 93 | + def _params(self): |
| 94 | + return { |
| 95 | + "W_e": self.coef_, |
| 96 | + "b_e": self.intercept_, |
| 97 | + "W_d": self.coef_d_, |
| 98 | + "b_d": self.intercept_d_, |
| 99 | + } |
| 100 | + |
| 101 | + def transform(self, X): |
| 102 | + if spr.issparse(X): |
| 103 | + X = X.todense() |
| 104 | + Z = [] |
| 105 | + _encode = jit(partial(encode, params=self._params, k=self.top_k)) |
| 106 | + for batch_start in trange( |
| 107 | + 0, |
| 108 | + X.shape[0], |
| 109 | + self.batch_size, |
| 110 | + leave=False, |
| 111 | + desc="Going through all batches", |
| 112 | + disable=not self.show_progress_bar, |
| 113 | + ): |
| 114 | + batch_end = batch_start + self.batch_size |
| 115 | + batch_x = X[batch_start:batch_end] |
| 116 | + batch_z = _encode(x=batch_x) |
| 117 | + Z.append(spr.csr_array(batch_z)) |
| 118 | + return spr.vstack(Z, format="csr") |
| 119 | + |
| 120 | + def fit_transform(self, X, y=None): |
| 121 | + return self.fit(X, y).transform(X) |
0 commit comments