"""Torch-backed autoencoder reducers (GH #162).
Six scikit-learn-style dimensionality-reduction classes, each wrapping a
small `torch.nn.Module` autoencoder and its own training loop, registered
in `hypertools.reduce.common.REDUCERS` (resolved lazily -- see
`hypertools.reduce.common.resolve_reducer`, mirroring how `'UMAP'` is
resolved) so `import hypertools` never requires `torch` to be installed.
`torch` ships as the optional `[torch]` extra (`pip install
"hypertools[torch]"`); this module is only ever imported once a caller
actually asks for one of the six names below, at which point a missing
`torch` surfaces as a friendly `ImportError` (see
`hypertools.reduce.common.resolve_reducer`).
Variant selection guidance
---------------------------
- `Autoencoder` -- a single hidden layer encoder/decoder. The default
choice for small-to-medium tabular data; cheapest to train.
- `DeepAutoencoder` -- a configurable multi-layer encoder/decoder
(`hidden_dims=[...]`, geometric defaults between the input and latent
dimensionality when not given). Use when a shallow network underfits.
- `SparseAutoencoder` -- adds an L1 penalty (`sparsity_weight=`) on the
hidden-layer activations, encouraging a sparse (more interpretable)
intermediate representation.
- `ConvolutionalAutoencoder` -- 1-D convolutions slide across the feature
axis, exploiting local structure among adjacent columns (e.g. spatially
or temporally adjacent features/channels) rather than treating every
feature independently as a plain `Linear` layer would.
- `SequenceAutoencoder` -- a GRU seq2seq model that treats the ROWS of `x`
as one time-ordered sequence and produces a per-timepoint latent code
(`transform(x)` still returns `(n_rows, n_components)`, one latent
vector per row/timepoint). Use for genuine timeseries/trajectory data
where row order matters.
- `VariationalAutoencoder` -- a probabilistic encoder (mean + log-variance
heads) trained with a KL-divergence term toward a standard normal prior
via the reparameterization trick; `transform` returns the latent MEANS
(not samples), giving a smooth, generative latent space.
All six share `_fit_torch_model` (Adam optimizer, MSE reconstruction loss
plus a variant-specific extra term) for their training loop, and the
`_TorchAutoencoderBase` mixin for the shared scikit-learn-style contract:
`fit`/`transform`/`fit_transform`/`inverse_transform` (the decoder) plus an
`n_components` attribute (wired to `hypertools.reduce.reduce.reduce`'s
`ndims=`). Inputs are standardized internally at fit time (mean/std) and
the standardization is undone in `inverse_transform`, so training is
stable even on unscaled data.
"""
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.base import BaseEstimator
from sklearn.exceptions import NotFittedError
def _resolve_device(device):
"""Resolve `device='auto'` to `'cuda'` > `'mps'` > `'cpu'` (in that
order of preference); any other value is passed through to
`torch.device` as-is.
Parameters
----------
device : str
`'auto'`, or any string accepted by `torch.device`.
Returns
-------
torch.device
The resolved device.
"""
if device != 'auto':
return torch.device(device)
if torch.cuda.is_available():
return torch.device('cuda')
mps = getattr(torch.backends, 'mps', None)
if mps is not None and mps.is_available():
return torch.device('mps')
return torch.device('cpu')
def _geometric_hidden_dims(n_in, n_components, n_layers):
"""A geometric progression of `n_layers` hidden-layer widths strictly
between `n_in` and `n_components`, used as the default `hidden_dims`
for the multi-layer variants (`DeepAutoencoder`, `VariationalAutoencoder`)
when the caller does not supply one explicitly.
Parameters
----------
n_in : int
Number of input features.
n_components : int
Latent dimensionality.
n_layers : int
Number of hidden layers to generate.
Returns
-------
list of int
`n_layers` widths, decreasing from close to `n_in` down to just
above `n_components`.
"""
if n_layers <= 0:
return []
lo = max(int(n_components) + 1, 2)
hi = max(int(n_in), lo + 1)
if n_layers == 1:
return [max(int(round(np.sqrt(hi * lo))), lo)]
sizes = np.geomspace(hi, lo, num=n_layers + 2)[1:-1]
return [max(int(round(s)), lo) for s in sizes]
def _resolve_hidden_dims(hidden_dims, n_in, n_components, default_layers):
"""Normalize the shared `hidden_dims=` constructor kwarg to a list of
layer widths.
Parameters
----------
hidden_dims : None, int, or sequence of int
`None` computes `default_layers` geometric widths (see
`_geometric_hidden_dims`); an `int` is treated as a single hidden
layer of that width; a sequence is used exactly as given (letting
`DeepAutoencoder`/`VariationalAutoencoder` callers control depth).
n_in : int
Number of input features.
n_components : int
Latent dimensionality.
default_layers : int
Number of layers to generate when `hidden_dims` is `None`.
Returns
-------
list of int
Hidden layer widths, outermost (closest to the input) first.
"""
if hidden_dims is None:
return _geometric_hidden_dims(n_in, n_components, default_layers)
if isinstance(hidden_dims, (int, np.integer)):
return [int(hidden_dims)]
return [int(d) for d in hidden_dims]
def _fit_torch_model(net, Xs, *, epochs, batch_size, lr, device, random_state,
verbose, sequence_mode):
"""Shared training loop for all six autoencoder variants: Adam
optimizer, minimizing `net.compute_loss` (each variant's `nn.Module`
implements this -- MSE reconstruction, plus an L1 activation penalty
for `SparseAutoencoder` or a KL term for `VariationalAutoencoder`).
Parameters
----------
net : torch.nn.Module
A freshly constructed (untrained) autoencoder network implementing
`encode`, `decode`, and `compute_loss`.
Xs : numpy.ndarray
Already-standardized (mean 0, std 1 per feature) training data.
epochs : int
Number of passes over the data (`epochs=0` returns `net`
untouched, at its random initialization -- used by the test suite
as an "untrained" baseline).
batch_size : int
Minibatch size (ignored when `sequence_mode=True`: the whole
sequence is used as one batch every epoch, since shuffling rows
would destroy the sequence's temporal order).
lr : float
Adam learning rate.
device : str
`'auto'`, or any string accepted by `torch.device` (see
`_resolve_device`).
random_state : int or None
When given, seeds `torch.manual_seed` before training so
minibatch order (and any stochastic layers, e.g. the VAE's
reparameterization noise) is reproducible.
verbose : bool
Whether to print a progress line every ~10% of `epochs`.
sequence_mode : bool
`True` for `SequenceAutoencoder`: trains on the full sequence
(no shuffling/minibatching) every epoch.
Returns
-------
(torch.nn.Module, torch.device)
The trained (or, for `epochs=0`, untouched) network in `eval`
mode, and the device it was moved to.
"""
dev = _resolve_device(device)
net = net.to(dev)
if random_state is not None:
torch.manual_seed(random_state)
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
x_t = torch.as_tensor(Xs, dtype=torch.float32, device=dev)
n = x_t.shape[0]
net.train()
for epoch in range(epochs):
if sequence_mode:
optimizer.zero_grad()
loss = net.compute_loss(x_t)
loss.backward()
optimizer.step()
epoch_loss = loss.item()
else:
if random_state is not None:
gen = torch.Generator().manual_seed(random_state * 1_000_003 + epoch)
perm = torch.randperm(n, generator=gen)
else:
perm = torch.randperm(n)
total = 0.0
for start in range(0, n, batch_size):
idx = perm[start:start + batch_size]
batch = x_t[idx]
optimizer.zero_grad()
loss = net.compute_loss(batch)
loss.backward()
optimizer.step()
total += loss.item() * idx.numel()
epoch_loss = total / n
if verbose and (epoch == 0 or epoch == epochs - 1
or (epoch + 1) % max(1, epochs // 10) == 0):
print(f'[hypertools autoencoder] epoch {epoch + 1}/{epochs} '
f'loss={epoch_loss:.6f}')
net.eval()
return net, dev
# --------------------------------------------------------------------- #
# nn.Module network definitions (one per variant)
# --------------------------------------------------------------------- #
class _ShallowAENet(nn.Module):
"""Single-hidden-layer encoder/decoder, used by `Autoencoder`."""
def __init__(self, n_in, n_components, hidden_dim):
super().__init__()
self.enc1 = nn.Linear(n_in, hidden_dim)
self.enc2 = nn.Linear(hidden_dim, n_components)
self.dec1 = nn.Linear(n_components, hidden_dim)
self.dec2 = nn.Linear(hidden_dim, n_in)
self.act = nn.ReLU()
def encode(self, x):
"""Map input `x` to its latent (`n_components`-dim) representation."""
return self.enc2(self.act(self.enc1(x)))
def decode(self, z):
"""Reconstruct the input space from latent code `z`."""
return self.dec2(self.act(self.dec1(z)))
def forward(self, x):
"""Full autoencoder pass: encode `x` then decode back to the input space."""
return self.decode(self.encode(x))
def compute_loss(self, x):
"""MSE reconstruction loss between `x` and its autoencoded reconstruction."""
return F.mse_loss(self.forward(x), x)
class _SparseAENet(nn.Module):
"""Single-hidden-layer encoder/decoder with an L1 penalty on the
hidden-layer activation, used by `SparseAutoencoder`."""
def __init__(self, n_in, n_components, hidden_dim, sparsity_weight):
super().__init__()
self.enc1 = nn.Linear(n_in, hidden_dim)
self.enc2 = nn.Linear(hidden_dim, n_components)
self.dec1 = nn.Linear(n_components, hidden_dim)
self.dec2 = nn.Linear(hidden_dim, n_in)
self.act = nn.ReLU()
self.sparsity_weight = sparsity_weight
def encode(self, x):
"""Map input `x` to its latent (`n_components`-dim) representation."""
return self.enc2(self.act(self.enc1(x)))
def decode(self, z):
"""Reconstruct the input space from latent code `z`."""
return self.dec2(self.act(self.dec1(z)))
def forward(self, x):
"""Full autoencoder pass: encode `x` then decode back to the input space."""
return self.decode(self.encode(x))
def compute_loss(self, x):
"""MSE reconstruction loss plus an L1 sparsity penalty on the hidden-layer activation."""
hidden = self.act(self.enc1(x))
z = self.enc2(hidden)
xhat = self.dec2(self.act(self.dec1(z)))
recon = F.mse_loss(xhat, x)
l1 = hidden.abs().mean()
return recon + self.sparsity_weight * l1
class _DeepAENet(nn.Module):
"""Multi-layer (`hidden_dims`) encoder/decoder, used by
`DeepAutoencoder`."""
def __init__(self, n_in, n_components, hidden_dims):
super().__init__()
dims = [n_in] + list(hidden_dims)
enc_layers = []
for a, b in zip(dims[:-1], dims[1:]):
enc_layers += [nn.Linear(a, b), nn.ReLU()]
enc_layers.append(nn.Linear(dims[-1], n_components))
self.encoder = nn.Sequential(*enc_layers)
rdims = [n_components] + list(reversed(hidden_dims))
dec_layers = []
for a, b in zip(rdims[:-1], rdims[1:]):
dec_layers += [nn.Linear(a, b), nn.ReLU()]
dec_layers.append(nn.Linear(rdims[-1], n_in))
self.decoder = nn.Sequential(*dec_layers)
def encode(self, x):
"""Map input `x` to its latent (`n_components`-dim) representation via the multi-layer encoder."""
return self.encoder(x)
def decode(self, z):
"""Reconstruct the input space from latent code `z` via the multi-layer decoder."""
return self.decoder(z)
def forward(self, x):
"""Full autoencoder pass: encode `x` then decode back to the input space."""
return self.decode(self.encode(x))
def compute_loss(self, x):
"""MSE reconstruction loss between `x` and its autoencoded reconstruction."""
return F.mse_loss(self.forward(x), x)
class _ConvAENet(nn.Module):
"""1-D convolutional encoder/decoder sliding over the feature axis,
used by `ConvolutionalAutoencoder`."""
def __init__(self, n_in, n_components, channels):
super().__init__()
c1, c2 = channels
self.enc_conv = nn.Sequential(
nn.Conv1d(1, c1, kernel_size=3, padding=1), nn.ReLU(),
nn.Conv1d(c1, c2, kernel_size=3, padding=1), nn.ReLU(),
)
self.enc_fc = nn.Linear(c2 * n_in, n_components)
self.dec_fc = nn.Linear(n_components, c2 * n_in)
self.dec_conv = nn.Sequential(
nn.ConvTranspose1d(c2, c1, kernel_size=3, padding=1), nn.ReLU(),
nn.ConvTranspose1d(c1, 1, kernel_size=3, padding=1),
)
self.n_in = n_in
self.c2 = c2
def encode(self, x):
"""Map input `x` to its latent (`n_components`-dim) representation via 1-D convolutions."""
h = self.enc_conv(x.unsqueeze(1))
return self.enc_fc(h.flatten(1))
def decode(self, z):
"""Reconstruct the input space from latent code `z` via transposed 1-D convolutions."""
h = self.dec_fc(z).view(-1, self.c2, self.n_in)
return self.dec_conv(h).squeeze(1)
def forward(self, x):
"""Full autoencoder pass: encode `x` then decode back to the input space."""
return self.decode(self.encode(x))
def compute_loss(self, x):
"""MSE reconstruction loss between `x` and its autoencoded reconstruction."""
return F.mse_loss(self.forward(x), x)
class _SeqAENet(nn.Module):
"""GRU seq2seq encoder/decoder, used by `SequenceAutoencoder`. Treats
its whole input as ONE time-ordered sequence (`(seq_len, n_in)`) and
produces one latent vector per timepoint (`(seq_len, n_components)`)."""
def __init__(self, n_in, n_components, hidden_dim):
super().__init__()
self.encoder_rnn = nn.GRU(n_in, hidden_dim, batch_first=True)
self.enc_fc = nn.Linear(hidden_dim, n_components)
self.decoder_rnn = nn.GRU(n_components, hidden_dim, batch_first=True)
self.dec_fc = nn.Linear(hidden_dim, n_in)
def encode(self, x):
"""Map the sequence `x` (`(seq_len, n_in)`) to one latent vector per timepoint via a GRU encoder."""
h, _ = self.encoder_rnn(x.unsqueeze(0))
return self.enc_fc(h.squeeze(0))
def decode(self, z):
"""Reconstruct the sequence's input space from its per-timepoint latent codes `z` via a GRU decoder."""
h, _ = self.decoder_rnn(z.unsqueeze(0))
return self.dec_fc(h.squeeze(0))
def forward(self, x):
"""Full autoencoder pass: encode sequence `x` then decode back to the input space."""
return self.decode(self.encode(x))
def compute_loss(self, x):
"""MSE reconstruction loss between `x` and its autoencoded reconstruction."""
return F.mse_loss(self.forward(x), x)
class _VAENet(nn.Module):
"""Variational encoder (mean/log-variance heads) + decoder, used by
`VariationalAutoencoder`."""
def __init__(self, n_in, n_components, hidden_dims, kl_weight):
super().__init__()
dims = [n_in] + list(hidden_dims)
enc_layers = []
for a, b in zip(dims[:-1], dims[1:]):
enc_layers += [nn.Linear(a, b), nn.ReLU()]
self.enc_body = nn.Sequential(*enc_layers)
self.mu = nn.Linear(dims[-1], n_components)
self.logvar = nn.Linear(dims[-1], n_components)
rdims = [n_components] + list(reversed(hidden_dims))
dec_layers = []
for a, b in zip(rdims[:-1], rdims[1:]):
dec_layers += [nn.Linear(a, b), nn.ReLU()]
dec_layers.append(nn.Linear(rdims[-1], n_in))
self.decoder = nn.Sequential(*dec_layers)
self.kl_weight = kl_weight
def encode_params(self, x):
"""Map input `x` to the latent Gaussian's parameters `(mu, logvar)`."""
h = self.enc_body(x)
return self.mu(h), self.logvar(h)
def encode(self, x):
"""Map input `x` to its latent MEAN (`mu`), not a stochastic sample (used by `transform`)."""
# transform() returns the latent MEANS, not stochastic samples.
mu, _ = self.encode_params(x)
return mu
def reparameterize(self, mu, logvar):
"""Sample a latent code from `N(mu, exp(logvar))` via the reparameterization trick."""
std = torch.exp(0.5 * logvar)
eps = torch.randn_like(std)
return mu + eps * std
def decode(self, z):
"""Reconstruct the input space from latent code `z`."""
return self.decoder(z)
def forward(self, x):
"""Full VAE pass: encode `x` to `(mu, logvar)`, sample `z`, and decode `z`.
Returns
-------
tuple of (Tensor, Tensor, Tensor)
`(reconstruction, mu, logvar)`.
"""
mu, logvar = self.encode_params(x)
z = self.reparameterize(mu, logvar)
return self.decode(z), mu, logvar
def compute_loss(self, x):
"""MSE reconstruction loss plus a KL-divergence term toward a standard normal prior."""
xhat, mu, logvar = self.forward(x)
recon = F.mse_loss(xhat, x)
kl = -0.5 * torch.mean(1 + logvar - mu.pow(2) - logvar.exp())
return recon + self.kl_weight * kl
# --------------------------------------------------------------------- #
# scikit-learn-style wrapper classes
# --------------------------------------------------------------------- #
class _TorchAutoencoderBase(BaseEstimator):
"""Shared scikit-learn-style contract for the six torch autoencoder
reducers: `fit`/`transform`/`fit_transform`/`inverse_transform`
(the decoder), standardizing inputs at fit time and undoing that
standardization in `inverse_transform`.
Subclasses implement `_build_net(n_features_in)`, returning a freshly
constructed (untrained) `torch.nn.Module` exposing `encode`, `decode`,
and `compute_loss` (see the `_*Net` classes above), and set
`_sequence_mode = True` if training should not shuffle/minibatch rows
(only `SequenceAutoencoder` does this).
Attributes
----------
net_ : torch.nn.Module or None
The fitted network (`None` until `fit`/`fit_transform` runs).
mean_, std_ : numpy.ndarray or None
Per-feature fit-time mean/std used to standardize inputs.
n_features_in_ : int or None
Number of input features, set by `fit`/`fit_transform`.
"""
_sequence_mode = False
def _build_net(self, n_features_in):
raise NotImplementedError
@property
def is_fitted(self):
"""Whether `fit`/`fit_transform` has already been run."""
return self.net_ is not None
def fit(self, X, y=None):
"""Fit the autoencoder on `X` (see `fit_transform`); returns
`self`."""
self.fit_transform(X)
return self
def fit_transform(self, X, y=None):
"""Standardize `X`, train a fresh network on it, and return the
fitted latent codes.
Parameters
----------
X : array-like of shape (n_samples, n_features)
Training data.
Returns
-------
numpy.ndarray of shape (n_samples, n_components)
The latent codes (for `VariationalAutoencoder`, the latent
means).
Raises
------
ValueError
If `X` is not 2-D, or if a training hyperparameter is invalid
(`epochs` must be a non-negative integer, `batch_size` a
positive integer, and `lr` a positive finite number).
"""
# validate training hyperparameters up front: a typo'd `epochs=-5`
# used to run "successfully" and return a finite but UNTRAINED
# embedding with no error or warning (release-1.0 audit,
# D04-gallery-models-010).
if isinstance(self.epochs, bool) \
or not isinstance(self.epochs, (int, np.integer)) \
or self.epochs < 0:
raise ValueError(
f'epochs must be a non-negative integer (0 explicitly '
f'skips training, leaving the network at its random '
f'initialization); got {self.epochs!r}')
if isinstance(self.batch_size, bool) \
or not isinstance(self.batch_size, (int, np.integer)) \
or self.batch_size < 1:
raise ValueError(
f'batch_size must be a positive integer; got '
f'{self.batch_size!r}')
if isinstance(self.lr, bool) \
or not isinstance(self.lr, (int, float, np.integer,
np.floating)) \
or not np.isfinite(self.lr) or self.lr <= 0:
raise ValueError(
f'lr (learning rate) must be a positive finite number; '
f'got {self.lr!r}')
X = np.asarray(X, dtype=np.float64)
if X.ndim != 2:
raise ValueError('X must be a 2D array')
self.n_features_in_ = X.shape[1]
self.mean_ = X.mean(axis=0)
std = X.std(axis=0)
std[std == 0] = 1.0
self.std_ = std
xs = (X - self.mean_) / self.std_
if self.random_state is not None:
torch.manual_seed(self.random_state)
net = self._build_net(self.n_features_in_)
net, device = _fit_torch_model(
net, xs, epochs=self.epochs, batch_size=self.batch_size,
lr=self.lr, device=self.device, random_state=self.random_state,
verbose=self.verbose, sequence_mode=self._sequence_mode)
self.net_ = net
self.device_ = device
return self._encode(xs)
def transform(self, X):
"""Apply the already-fitted network to (new) `X`, without
refitting.
Parameters
----------
X : array-like of shape (n_samples, n_features)
Data to encode.
Returns
-------
numpy.ndarray of shape (n_samples, n_components)
The latent codes.
Raises
------
sklearn.exceptions.NotFittedError
If `fit`/`fit_transform` has not been called yet.
"""
if self.net_ is None:
raise NotFittedError('must fit autoencoder before transforming data')
X = np.asarray(X, dtype=np.float64)
xs = (X - self.mean_) / self.std_
return self._encode(xs)
def _encode(self, xs):
self.net_.eval()
with torch.no_grad():
x_t = torch.as_tensor(xs, dtype=torch.float32, device=self.device_)
z = self.net_.encode(x_t)
return z.detach().cpu().numpy().astype(np.float64)
def inverse_transform(self, Z):
"""Decode latent codes `Z` back to (standardization-undone)
feature space.
Parameters
----------
Z : array-like of shape (n_samples, n_components)
Latent codes (e.g. from `transform`).
Returns
-------
numpy.ndarray of shape (n_samples, n_features)
The reconstructed data.
Raises
------
sklearn.exceptions.NotFittedError
If `fit`/`fit_transform` has not been called yet.
"""
if self.net_ is None:
raise NotFittedError('must fit autoencoder before inverse-transforming data')
self.net_.eval()
with torch.no_grad():
z_t = torch.as_tensor(np.asarray(Z, dtype=np.float64),
dtype=torch.float32, device=self.device_)
xhat = self.net_.decode(z_t).detach().cpu().numpy().astype(np.float64)
return xhat * self.std_ + self.mean_
[docs]
class Autoencoder(_TorchAutoencoderBase):
"""A single-hidden-layer (shallow) autoencoder reducer.
The default, cheapest-to-train autoencoder variant: one `Linear` +
ReLU hidden layer on each side of the bottleneck. Good for small to
medium tabular data where a deeper network is unnecessary.
Parameters
----------
n_components : int
Latent (bottleneck) dimensionality (default: 2). Wired to
`hypertools.reduce.reduce.reduce`'s `ndims=`.
epochs : int
Number of training epochs (default: 100). Must be a non-negative
integer (validated at fit time); `epochs=0` explicitly skips
training, leaving the network at its random initialization
(useful only as an untrained baseline).
batch_size : int
Minibatch size (default: 64). Must be a positive integer
(validated at fit time).
lr : float
Adam learning rate (default: 1e-3). Must be a positive finite
number (validated at fit time).
hidden_dims : None, int, or sequence of int
Hidden layer width. `None` (default) computes a sensible width
geometrically between `n_components` and the number of input
features; an `int` (or single-element sequence) sets it directly.
device : str
`'auto'` (default) picks `'cuda'` > `'mps'` > `'cpu'`; any other
string is passed to `torch.device` as-is.
random_state : int or None
Seeds `torch.manual_seed` for reproducible weight initialization
and minibatch order (default: None, i.e. nondeterministic).
verbose : bool
Print training progress (default: False).
Attributes
----------
net_ : torch.nn.Module or None
The fitted network.
"""
[docs]
def __init__(self, n_components=2, epochs=100, batch_size=64, lr=1e-3,
hidden_dims=None, device='auto', random_state=None,
verbose=False):
self.n_components = n_components
self.epochs = epochs
self.batch_size = batch_size
self.lr = lr
self.hidden_dims = hidden_dims
self.device = device
self.random_state = random_state
self.verbose = verbose
self.net_ = None
self.mean_ = None
self.std_ = None
self.n_features_in_ = None
self.device_ = None
def _build_net(self, n_features_in):
hidden = _resolve_hidden_dims(self.hidden_dims, n_features_in,
self.n_components, default_layers=1)[0]
return _ShallowAENet(n_features_in, self.n_components, hidden)
[docs]
class SparseAutoencoder(_TorchAutoencoderBase):
"""A single-hidden-layer autoencoder with an L1 penalty on the hidden
activation (sparse coding), encouraging a sparser, more interpretable
intermediate representation.
Parameters
----------
n_components : int
Latent (bottleneck) dimensionality (default: 2).
sparsity_weight : float
Weight of the L1 penalty on the mean absolute hidden-layer
activation, added to the MSE reconstruction loss (default: 1e-3).
epochs, batch_size, lr, hidden_dims, device, random_state, verbose
See `Autoencoder`.
Attributes
----------
net_ : torch.nn.Module or None
The fitted network.
"""
[docs]
def __init__(self, n_components=2, sparsity_weight=1e-3, epochs=100,
batch_size=64, lr=1e-3, hidden_dims=None, device='auto',
random_state=None, verbose=False):
self.n_components = n_components
self.sparsity_weight = sparsity_weight
self.epochs = epochs
self.batch_size = batch_size
self.lr = lr
self.hidden_dims = hidden_dims
self.device = device
self.random_state = random_state
self.verbose = verbose
self.net_ = None
self.mean_ = None
self.std_ = None
self.n_features_in_ = None
self.device_ = None
def _build_net(self, n_features_in):
hidden = _resolve_hidden_dims(self.hidden_dims, n_features_in,
self.n_components, default_layers=1)[0]
return _SparseAENet(n_features_in, self.n_components, hidden,
self.sparsity_weight)
[docs]
class DeepAutoencoder(_TorchAutoencoderBase):
"""A multi-layer autoencoder reducer with configurable depth.
Parameters
----------
n_components : int
Latent (bottleneck) dimensionality (default: 2).
hidden_dims : None, int, or sequence of int
Hidden layer widths, outermost (closest to the input) first.
`None` (default) computes 2 geometrically-spaced widths between
the number of input features and `n_components`; a sequence sets
the exact depth and widths (e.g. `[64, 32, 16]`).
epochs, batch_size, lr, device, random_state, verbose
See `Autoencoder`.
Attributes
----------
net_ : torch.nn.Module or None
The fitted network.
"""
[docs]
def __init__(self, n_components=2, hidden_dims=None, epochs=100,
batch_size=64, lr=1e-3, device='auto', random_state=None,
verbose=False):
self.n_components = n_components
self.hidden_dims = hidden_dims
self.epochs = epochs
self.batch_size = batch_size
self.lr = lr
self.device = device
self.random_state = random_state
self.verbose = verbose
self.net_ = None
self.mean_ = None
self.std_ = None
self.n_features_in_ = None
self.device_ = None
def _build_net(self, n_features_in):
hidden_dims = _resolve_hidden_dims(self.hidden_dims, n_features_in,
self.n_components, default_layers=2)
return _DeepAENet(n_features_in, self.n_components, hidden_dims)
[docs]
class ConvolutionalAutoencoder(_TorchAutoencoderBase):
"""A 1-D convolutional autoencoder reducer: two `Conv1d` layers slide
across the feature axis (treating each row as a length-`n_features`,
1-channel signal), exploiting local structure among adjacent features
rather than treating every feature independently.
Parameters
----------
n_components : int
Latent (bottleneck) dimensionality (default: 2).
hidden_dims : None or sequence of 2 int
`(channels_1, channels_2)` for the two `Conv1d`/`ConvTranspose1d`
layers. `None` (default) uses `(8, 16)`.
epochs, batch_size, lr, device, random_state, verbose
See `Autoencoder`.
Attributes
----------
net_ : torch.nn.Module or None
The fitted network.
"""
[docs]
def __init__(self, n_components=2, hidden_dims=None, epochs=100,
batch_size=64, lr=1e-3, device='auto', random_state=None,
verbose=False):
self.n_components = n_components
self.hidden_dims = hidden_dims
self.epochs = epochs
self.batch_size = batch_size
self.lr = lr
self.device = device
self.random_state = random_state
self.verbose = verbose
self.net_ = None
self.mean_ = None
self.std_ = None
self.n_features_in_ = None
self.device_ = None
def _build_net(self, n_features_in):
if self.hidden_dims is None:
channels = (8, 16)
elif isinstance(self.hidden_dims, (int, np.integer)):
channels = (int(self.hidden_dims), int(self.hidden_dims))
else:
dims = [int(d) for d in self.hidden_dims]
channels = (dims[0], dims[1] if len(dims) > 1 else dims[0])
return _ConvAENet(n_features_in, self.n_components, channels)
[docs]
class SequenceAutoencoder(_TorchAutoencoderBase):
"""A GRU seq2seq autoencoder reducer for genuine timeseries/trajectory
data: the ROWS of `x` are treated as one time-ordered sequence, and a
latent vector is produced PER TIMEPOINT (`transform(x)` still returns
`(n_rows, n_components)`, one row per input row).
Because row order is meaningful, training uses the full sequence every
epoch (no row shuffling/minibatching -- `batch_size` is accepted for
interface consistency with the other variants but ignored).
Parameters
----------
n_components : int
Per-timepoint latent dimensionality (default: 2).
hidden_dims : None, int, or sequence of int
GRU hidden size. `None` (default) computes a sensible width
geometrically between `n_components` and the number of input
features; only the first value is used if a sequence is given.
epochs, lr, device, random_state, verbose
See `Autoencoder`.
batch_size : int
Accepted for interface consistency; ignored (see above).
Attributes
----------
net_ : torch.nn.Module or None
The fitted network.
"""
_sequence_mode = True
[docs]
def __init__(self, n_components=2, hidden_dims=None, epochs=100,
batch_size=64, lr=1e-3, device='auto', random_state=None,
verbose=False):
self.n_components = n_components
self.hidden_dims = hidden_dims
self.epochs = epochs
self.batch_size = batch_size
self.lr = lr
self.device = device
self.random_state = random_state
self.verbose = verbose
self.net_ = None
self.mean_ = None
self.std_ = None
self.n_features_in_ = None
self.device_ = None
def _build_net(self, n_features_in):
hidden = _resolve_hidden_dims(self.hidden_dims, n_features_in,
self.n_components, default_layers=1)[0]
return _SeqAENet(n_features_in, self.n_components, hidden)
[docs]
class VariationalAutoencoder(_TorchAutoencoderBase):
"""A variational autoencoder (VAE) reducer: a probabilistic encoder
(mean + log-variance heads) trained with a KL-divergence term toward a
standard normal prior, via the reparameterization trick.
`transform`/`fit_transform` return the latent MEANS (not stochastic
samples), giving a smooth, reproducible, generative latent space.
Parameters
----------
n_components : int
Latent dimensionality (default: 2).
kl_weight : float
Weight of the KL-divergence term, added to the MSE reconstruction
loss (default: 1.0).
hidden_dims : None, int, or sequence of int
Hidden layer widths of the shared encoder body / decoder,
outermost (closest to the input) first. `None` (default) computes
2 geometrically-spaced widths.
epochs, batch_size, lr, device, random_state, verbose
See `Autoencoder`.
Attributes
----------
net_ : torch.nn.Module or None
The fitted network.
"""
[docs]
def __init__(self, n_components=2, kl_weight=1.0, hidden_dims=None,
epochs=100, batch_size=64, lr=1e-3, device='auto',
random_state=None, verbose=False):
self.n_components = n_components
self.kl_weight = kl_weight
self.hidden_dims = hidden_dims
self.epochs = epochs
self.batch_size = batch_size
self.lr = lr
self.device = device
self.random_state = random_state
self.verbose = verbose
self.net_ = None
self.mean_ = None
self.std_ = None
self.n_features_in_ = None
self.device_ = None
def _build_net(self, n_features_in):
hidden_dims = _resolve_hidden_dims(self.hidden_dims, n_features_in,
self.n_components, default_layers=2)
return _VAENet(n_features_in, self.n_components, hidden_dims,
self.kl_weight)
#: names resolvable by `hypertools.reduce.common.resolve_reducer`, lazily
#: importing this module (torch) only when one is actually requested.
AUTOENCODER_NAMES = (
'Autoencoder', 'DeepAutoencoder', 'SparseAutoencoder',
'ConvolutionalAutoencoder', 'SequenceAutoencoder',
'VariationalAutoencoder',
)