R/decomp_Autoencoder.R

Defines functions autoencoder_holdout autoencoder_device autoencoder_scores autoencoder_tensor autoencoder_fit_module autoencoder_module autoencoder_matrix autoencoder_standardize autoencoder_batch_size autoencoder_hidden_units vae_objective

# decomp_Autoencoder.R
# ::rtemis::
# 2026- EDG rtemis.org

# Torch autoencoders as decompositions. The training loop, the vocabularies and
# the serialization helpers live in `065_Torch.R`; what is here is the
# autoencoder itself: how the hidden widths are decided, how the inputs are
# standardized, the module, and the fit object that survives `saveRDS()`.
#
# The methods are defined on `AutoencoderBaseConfig`, so every autoencoder leaf
# inherits them through S7 dispatch.

# %% AutoencoderFit ----
#' @title AutoencoderFit
#'
#' @description
#' A fitted autoencoder as plain R data. A torch module holds external pointers
#' and fails at first use after `readRDS()`, so the fit stores the serialized
#' parameters and everything needed to rebuild the module around them, and
#' `apply_decomp_()` and `reconstruct_()` rebuild it. Every architectural
#' setting is stored here, so the fit is readable on its own.
#'
#' @field state Raw: Serialized module parameters, from `torch_state()`.
#' @field features Character: Input features, in the order the fit used.
#' @field k Integer: Latent width.
#' @field hidden_units Integer: Encoder hidden widths, input side first.
#' @field activation,norm Character: Layer settings; `norm` NULL for none.
#' @field dropout,input_dropout,input_noise Numeric: Regularization settings.
#' @field variational Logical: Whether the module is a variational autoencoder.
#' @field beta Optional Numeric: KL weight a variational fit was trained with;
#'   NULL for a plain one.
#' @field center,scale Numeric: Training statistics the inputs are standardized
#'   with, one per feature.
#' @field device Character: Device the fit was trained on.
#' @field n_validation Integer: Cases held out for early stopping.
#' @field epochs_trained,best_epoch Integer: Epochs run, and the epoch whose
#'   weights were kept.
#' @field history data.frame: Per-epoch training and validation loss.
#'
#' @author EDG
#' @keywords internal
#' @noRd
AutoencoderFit <- new_class(
  name = "AutoencoderFit",
  package = "rtemis",
  properties = list(
    state = class_raw,
    features = class_character,
    k = class_integer,
    hidden_units = class_integer,
    activation = class_character,
    norm = NULL | class_character,
    dropout = class_numeric,
    input_dropout = class_numeric,
    input_noise = class_numeric,
    variational = class_logical,
    beta = NULL | class_numeric,
    center = class_numeric,
    scale = class_numeric,
    device = class_character,
    n_validation = class_integer,
    epochs_trained = class_integer,
    best_epoch = class_integer,
    history = class_data.frame
  ),
  validator = function(self) {
    p <- length(self@features)
    if (length(self@center) != p || length(self@scale) != p) {
      return("center and scale need one value per feature.")
    }
    if (any(self@scale <= 0)) {
      return("scale must be positive.")
    }
    if (self@variational == is.null(self@beta)) {
      return("beta is set exactly when the fit is variational.")
    }
    NULL
  }
) # /rtemis::AutoencoderFit


# %% repr.AutoencoderFit ----
method(repr, AutoencoderFit) <- function(x, pad = 0L, output_type = NULL) {
  paste0(
    repr_S7name("AutoencoderFit", pad = pad, output_type = output_type),
    repr_ls(
      list(
        features = length(x@features),
        hidden_units = x@hidden_units,
        k = x@k,
        variational = x@variational,
        beta = x@beta,
        device = x@device,
        epochs_trained = x@epochs_trained,
        best_epoch = x@best_epoch
      ),
      pad = pad,
      output_type = output_type
    )
  )
} # /rtemis::repr.AutoencoderFit


# %% print.AutoencoderFit ----
method(print, AutoencoderFit) <- function(x, output_type = NULL, ...) {
  cat(repr(x, output_type = output_type))
  invisible(x)
} # /rtemis::print.AutoencoderFit


# %% autoencoder_beta ----
#' The KL weight of a variational autoencoder config, or NULL
#'
#' The one thing the fitting method needs to know about the leaf: NULL trains a
#' plain autoencoder, a number a variational one with that weight. Dispatched,
#' so the shared `decomp_()` method never tests for a class.
#'
#' @param config `AutoencoderBaseConfig` object.
#'
#' @return Numeric scalar, or NULL.
#'
#' @author EDG
#' @keywords internal
#' @noRd
autoencoder_beta <- new_generic("autoencoder_beta", "config")


# %% autoencoder_beta.AutoencoderBaseConfig ----
method(autoencoder_beta, AutoencoderBaseConfig) <- function(config) {
  NULL
} # /rtemis::autoencoder_beta.AutoencoderBaseConfig


# %% autoencoder_beta.VariationalAutoencoderConfig ----
method(autoencoder_beta, VariationalAutoencoderConfig) <- function(config) {
  config[["beta"]]
} # /rtemis::autoencoder_beta.VariationalAutoencoderConfig


# %% vae_objective ----
#' The per-case objective of a variational autoencoder
#'
#' For each case, the reconstruction loss summed over features plus `beta`
#' times the KL divergence of the case's latent normal from the standard
#' normal, in closed form `-0.5 * sum(1 + logvar - mu^2 - exp(logvar))` over
#' the latent dimensions. Summed over features, not averaged, so `beta` weighs
#' the prior against the whole reconstruction, as in the beta-VAE literature.
#' `torch_fit()` takes the weighted mean over cases.
#'
#' @param loss Character: One of `TORCH_REGRESSION_LOSSES`.
#' @param beta Numeric: KL weight.
#'
#' @return Function of the module's output, `list(reconstruction, mu, logvar)`,
#' and the target, returning a tensor with one loss per case.
#'
#' @author EDG
#' @keywords internal
#' @noRd
vae_objective <- function(loss, beta) {
  reconstruction_loss <- torch_loss_module(loss)
  function(output, target) {
    mu <- output[[2L]]
    logvar <- output[[3L]]
    reconstruction <- reconstruction_loss(output[[1L]], target)[["sum"]](
      dim = 2L
    )
    kl <- -0.5 *
      (1 + logvar - mu[["pow"]](2) - logvar[["exp"]]())[["sum"]](dim = 2L)
    reconstruction + beta * kl
  }
} # /rtemis::vae_objective


# %% autoencoder_hidden_units ----
#' Resolve the encoder's hidden widths
#'
#' Given widths are used as they are. Unset gives one hidden layer whose width
#' is the geometric mean of the input and latent widths -- halfway, on the log
#' scale, between the data and the bottleneck -- held between 32 and 512 and
#' never below `k`. The floor is measured: on iris (`k = 2`), a 3-unit layer
#' (the unfloored mean) reconstructed nothing and an 8-unit one trailed PCA,
#' while 32 units matched it (2026-09-29).
#'
#' @param hidden_units Integer vector or NULL: Widths given in the config.
#' @param n_features Integer: Input width.
#' @param k Integer: Latent width.
#' @param verbosity Integer: If > 0, print messages.
#'
#' @return Integer vector: One width per encoder hidden layer.
#'
#' @author EDG
#' @keywords internal
#' @noRd
autoencoder_hidden_units <- function(
  hidden_units,
  n_features,
  k,
  verbosity = 1L
) {
  if (!is.null(hidden_units)) {
    msg0(
      "Encoder hidden layers, as given: ",
      paste(hidden_units, collapse = ", "),
      "...",
      verbosity = verbosity
    )
    return(as.integer(hidden_units))
  }
  width <- max(
    as.integer(k),
    min(512L, max(32L, as.integer(round(sqrt(n_features * k)))))
  )
  msg0(
    "Encoder hidden layer, derived from ",
    n_features,
    " features and k = ",
    k,
    ": ",
    width,
    "...",
    verbosity = verbosity
  )
  width
} # /rtemis::autoencoder_hidden_units


# %% autoencoder_batch_size ----
#' Resolve the batch size
#'
#' Unset gives a tenth of the training cases, held between 16 and 256, so every
#' epoch takes about ten optimization steps until the data are large. A fixed
#' 256 gives 150 cases one step per epoch, and a 100-epoch fit on iris then
#' reconstructs nothing; a fixed 32 gave 10,000 cases seven times the training
#' time of 256 for the same reconstruction (2026-09-29).
#'
#' @param batch_size Integer or NULL: Batch size given in the config.
#' @param n_training Integer: Training cases.
#'
#' @return Integer.
#'
#' @author EDG
#' @keywords internal
#' @noRd
autoencoder_batch_size <- function(batch_size, n_training) {
  if (!is.null(batch_size)) {
    return(as.integer(batch_size))
  }
  as.integer(min(256L, max(16L, n_training %/% 10L)))
} # /rtemis::autoencoder_batch_size


# %% autoencoder_standardize ----
#' Standardize a data matrix with a fit's training statistics
#'
#' @param xm Numeric matrix: Cases by features, in the fit's feature order.
#' @param center,scale Numeric: One value per feature.
#'
#' @return Numeric matrix.
#'
#' @author EDG
#' @keywords internal
#' @noRd
autoencoder_standardize <- function(xm, center, scale) {
  sweep(sweep(xm, 2L, center, FUN = "-"), 2L, scale, FUN = "/")
} # /rtemis::autoencoder_standardize


# %% autoencoder_matrix ----
#' Take the features a fit was trained on as a numeric matrix
#'
#' The only point on the apply and reconstruct paths that sees the data before
#' torch does, so a missing feature or a missing value is rejected here, naming
#' it: a missing value would otherwise propagate through the network as NaN.
#'
#' @param x Tabular data.
#' @param fit `AutoencoderFit` object.
#'
#' @return Numeric matrix, cases by `fit@features`.
#'
#' @author EDG
#' @keywords internal
#' @noRd
autoencoder_matrix <- function(x, fit) {
  x <- as.data.frame(x)
  absent <- setdiff(fit@features, names(x))
  if (length(absent) > 0L) {
    rtemis.core::abort(
      "Data is missing ",
      length(absent),
      " feature(s) the autoencoder was fit on: ",
      paste0("'", absent, "'", collapse = ", "),
      ".",
      class = c("rtemis_value_error", "rtemis_input_error")
    )
  }
  xm <- as.matrix(x[, fit@features, drop = FALSE])
  incomplete <- fit@features[colSums(is.na(xm)) > 0L]
  if (length(incomplete) > 0L) {
    rtemis.core::abort(
      "The autoencoder cannot encode missing values; ",
      length(incomplete),
      " feature(s) have them: ",
      paste0("'", incomplete, "'", collapse = ", "),
      ".",
      class = c("rtemis_value_error", "rtemis_input_error")
    )
  }
  xm
} # /rtemis::autoencoder_matrix


# %% autoencoder_module ----
#' Build the autoencoder module
#'
#' An `encoder` and a `decoder` submodule, each callable on its own, so encoding
#' and decoding are `torch_forward()` over one of them. Each hidden layer is a
#' linear map, the activation, the normalization and dropout, in that order (as
#' `mlp_module()` with `norm_first = FALSE`); the latent layer and the output
#' layer are linear.
#'
#' A variational module's encoder holds the hidden layers and two linear heads,
#' for the means and the log variances of the latent normal; called on its own
#' it returns the means, which are the components. The full module returns
#' `list(reconstruction, mu, logvar)`, decoding a reparameterized draw
#' `mu + exp(logvar / 2) * noise` in training mode and `mu` in eval mode, so
#' the validation loss and every output after training are deterministic.
#' Every noise draw goes through `torch_seeded_randn()`, so a seeded fit
#' reproduces on mps too.
#'
#' The corruption that makes the model denoising is applied by the full module
#' to its input, and only in training mode: Gaussian noise of standard
#' deviation `input_noise`, then dropout at `input_dropout`. Training passes the
#' clean data as the target, so the model learns to undo the corruption; the
#' validation loss, `apply_decomp_()` and `reconstruct_()` run in eval mode and
#' see clean inputs.
#'
#' The generator is created inside the function because `torch` is a
#' Suggests-gated backend.
#'
#' @param n_features Integer: Input width.
#' @param hidden_units Integer vector: Encoder hidden widths, input side first.
#' @param k Integer: Latent width.
#' @param activation Character: One of `TORCH_ACTIVATIONS`.
#' @param norm Character or NULL: One of `TORCH_NORMS`, or NULL for none.
#' @param dropout Numeric: Dropout after every hidden layer.
#' @param input_dropout Numeric: Masking probability on the training input.
#' @param input_noise Numeric: Gaussian noise level on the training input.
#' @param variational Logical: Whether to build a variational autoencoder.
#'
#' @return `nn_module` object.
#'
#' @author EDG
#' @keywords internal
#' @noRd
autoencoder_module <- function(
  n_features,
  hidden_units,
  k,
  activation,
  norm,
  dropout,
  input_dropout,
  input_noise,
  variational = FALSE
) {
  layers <- function(widths) {
    blocks <- lapply(seq_len(length(widths) - 1L), function(i) {
      list(
        torch::nn_linear(widths[[i]], widths[[i + 1L]]),
        torch_activation_module(activation),
        torch_norm_module(norm, widths[[i + 1L]]),
        torch::nn_dropout(dropout)
      )
    })
    unlist(blocks, recursive = FALSE)
  }
  variational_encoder <- torch::nn_module(
    classname = "VariationalEncoder",
    initialize = function(widths, k) {
      self[["trunk"]] <- do.call(torch::nn_sequential, layers(widths))
      self[["mu"]] <- torch::nn_linear(widths[[length(widths)]], k)
      self[["logvar"]] <- torch::nn_linear(widths[[length(widths)]], k)
    },
    forward = function(x) {
      self[["mu"]](self[["trunk"]](x))
    }
  )
  generator <- torch::nn_module(
    classname = "Autoencoder",
    initialize = function(
      n_features,
      hidden_units,
      k,
      input_dropout,
      input_noise,
      variational
    ) {
      self[["input_noise"]] <- input_noise
      self[["variational"]] <- variational
      self[["corrupt"]] <- torch::nn_dropout(input_dropout)
      encoder_widths <- c(n_features, hidden_units)
      decoder_widths <- c(k, rev(hidden_units))
      self[["encoder"]] <- if (variational) {
        variational_encoder(encoder_widths, k)
      } else {
        do.call(
          torch::nn_sequential,
          c(
            layers(encoder_widths),
            list(torch::nn_linear(encoder_widths[[length(encoder_widths)]], k))
          )
        )
      }
      self[["decoder"]] <- do.call(
        torch::nn_sequential,
        c(
          layers(decoder_widths),
          list(torch::nn_linear(
            decoder_widths[[length(decoder_widths)]],
            n_features
          ))
        )
      )
    },
    forward = function(x) {
      if (self[["training"]]) {
        if (self[["input_noise"]] > 0) {
          x <- x + self[["input_noise"]] * torch_seeded_randn(x)
        }
        x <- self[["corrupt"]](x)
      }
      if (!self[["variational"]]) {
        return(self[["decoder"]](self[["encoder"]](x)))
      }
      encoder <- self[["encoder"]]
      h <- encoder[["trunk"]](x)
      mu <- encoder[["mu"]](h)
      logvar <- encoder[["logvar"]](h)
      z <- if (self[["training"]]) {
        mu + (0.5 * logvar)[["exp"]]() * torch_seeded_randn(mu)
      } else {
        mu
      }
      list(self[["decoder"]](z), mu, logvar)
    }
  )
  generator(
    n_features = n_features,
    hidden_units = hidden_units,
    k = k,
    input_dropout = input_dropout,
    input_noise = input_noise,
    variational = variational
  )
} # /rtemis::autoencoder_module


# %% autoencoder_fit_module ----
#' Rebuild a fit's module and load its parameters
#'
#' @param fit `AutoencoderFit` object.
#'
#' @return `nn_module` object in eval mode.
#'
#' @author EDG
#' @keywords internal
#' @noRd
autoencoder_fit_module <- function(fit) {
  torch_restore(
    autoencoder_module(
      n_features = length(fit@features),
      hidden_units = fit@hidden_units,
      k = fit@k,
      activation = fit@activation,
      norm = fit@norm,
      dropout = fit@dropout,
      input_dropout = fit@input_dropout,
      input_noise = fit@input_noise,
      variational = fit@variational
    ),
    fit@state
  )
} # /rtemis::autoencoder_fit_module


# %% autoencoder_tensor ----
#' A numeric matrix as a float tensor
#'
#' @param xm Numeric matrix.
#'
#' @return `torch_tensor` object.
#'
#' @author EDG
#' @keywords internal
#' @noRd
autoencoder_tensor <- function(xm) {
  torch::torch_tensor(unname(xm), dtype = torch::torch_float())
} # /rtemis::autoencoder_tensor


# %% autoencoder_scores ----
#' Encode data with a fitted module
#'
#' The one encoding path: `decomp_()` computes its components through it after
#' the fit, and `apply_decomp_()` through it on new data, so applying a fit to
#' its own training data reproduces the fitted components exactly.
#'
#' @param module `nn_module` object: The fitted autoencoder.
#' @param fit `AutoencoderFit` object.
#' @param x Tabular data.
#' @param algorithm Character: Algorithm name, which prefixes the column names.
#' @param device Character: Torch device name.
#'
#' @return Numeric matrix, cases by `k`.
#'
#' @author EDG
#' @keywords internal
#' @noRd
autoencoder_scores <- function(module, fit, x, algorithm, device) {
  standardized <- autoencoder_standardize(
    autoencoder_matrix(x, fit),
    fit@center,
    fit@scale
  )
  transformed <- torch_forward(
    module[["encoder"]],
    list(autoencoder_tensor(standardized)),
    device = device
  )
  colnames(transformed) <- paste0(algorithm, "_", seq_len(NCOL(transformed)))
  transformed
} # /rtemis::autoencoder_scores


# %% autoencoder_device ----
#' The torch device name for an execution config's requested device
#'
#' @param execution_config Optional `ExecutionConfig` object.
#'
#' @return Character.
#'
#' @author EDG
#' @keywords internal
#' @noRd
autoencoder_device <- function(execution_config = NULL) {
  requested <- execution_device(execution_config)
  torch_device_name(torch_training_device(requested), requested)
} # /rtemis::autoencoder_device


# %% autoencoder_holdout ----
#' Draw the cases held out for early stopping
#'
#' Drawn from R's random stream, which `decomp()` seeds from the execution
#' config. Too few cases to hold out one while keeping one for training
#' disables early stopping.
#'
#' @param n Integer: Number of cases.
#' @param fraction Numeric \[0, 1): Fraction to hold out.
#' @param verbosity Integer: If > 0, print messages.
#'
#' @return Integer vector of held-out row indices, possibly empty.
#'
#' @author EDG
#' @keywords internal
#' @noRd
autoencoder_holdout <- function(n, fraction, verbosity = 1L) {
  n_validation <- as.integer(round(fraction * n))
  if (fraction > 0 && (n_validation == 0L || n_validation >= n)) {
    msg0(
      "Too few cases to hold out ",
      format(fraction),
      " of them; training for the full epoch budget without early stopping...",
      verbosity = verbosity
    )
    return(integer())
  }
  if (n_validation == 0L) {
    return(integer())
  }
  sort(sample.int(n, n_validation))
} # /rtemis::autoencoder_holdout


# %% decomp_.AutoencoderBaseConfig ----
#' Autoencoder decomposition
#'
#' @param config `AutoencoderBaseConfig` object.
#' @param x Tabular data: Numeric features.
#' @param execution_config Optional `ExecutionConfig` object: Threads, device
#' and, through `decomp()`, the seed.
#' @param verbosity Integer: Verbosity level.
#'
#' @return List with `decom` (`AutoencoderFit`), `transformed` and `config`,
#' the config with the hidden widths the fit resolved.
#'
#' @author EDG
#' @keywords internal
#' @noRd
method(decomp_, AutoencoderBaseConfig) <- function(
  config,
  x,
  execution_config = NULL,
  verbosity = 1L
) {
  # Checks ----
  check_dependencies("torch")
  check_unsupervised_data(x = x, allow_missing = FALSE, verbosity = verbosity)

  # Standardize ----
  xm <- as.matrix(x)
  features <- colnames(xm)
  center <- colMeans(xm)
  scale <- apply(xm, 2L, stats::sd)
  # A constant feature carries nothing to scale; it is centered to zero.
  scale[!is.finite(scale) | scale == 0] <- 1
  standardized <- autoencoder_standardize(xm, center, scale)

  # Architecture ----
  k <- config[["k"]]
  hidden_units <- autoencoder_hidden_units(
    config[["hidden_units"]],
    n_features = length(features),
    k = k,
    verbosity = verbosity
  )

  # Resources ----
  set_torch_threads(algorithm_threads(execution_config), verbosity = verbosity)
  device <- autoencoder_device(execution_config)
  check_mps_reproducible(
    device,
    seed = if (is.null(execution_config)) NULL else execution_config@seed,
    dropout = c(config[["dropout"]], config[["input_dropout"]])
  )

  # Randomness ----
  # Both from R's stream, which `decomp()` seeds from the execution config:
  # the held-out cases, then torch's own generator, which `set.seed()` does not
  # reach. Seeded before the module is built, since that draws the weights.
  validation_index <- autoencoder_holdout(
    NROW(xm),
    config[["validation_fraction"]],
    verbosity = verbosity
  )
  torch::torch_manual_seed(sample.int(.Machine[["integer.max"]], 1L))

  # Train ----
  msg("Training", config@algorithm, "on", device, "...", verbosity = verbosity)
  beta <- autoencoder_beta(config)
  variational <- !is.null(beta)
  module <- autoencoder_module(
    n_features = length(features),
    hidden_units = hidden_units,
    k = k,
    activation = config[["activation"]],
    norm = config[["norm"]],
    dropout = config[["dropout"]],
    input_dropout = config[["input_dropout"]],
    input_noise = config[["input_noise"]],
    variational = variational
  )
  training <- standardized[
    setdiff(seq_len(NROW(xm)), validation_index),
    ,
    drop = FALSE
  ]
  validate <- length(validation_index) > 0L
  batch_size <- autoencoder_batch_size(config[["batch_size"]], NROW(training))
  # Batch normalization needs more than one case per batch to compute a
  # variance, so a trailing batch of one aborts the fit partway through.
  drop_last <- identical(config[["norm"]], "batch_norm") &&
    NROW(training) %% batch_size == 1L
  if (drop_last) {
    msg(
      "Dropping the last batch: batch normalization cannot use a batch of one case.",
      verbosity = verbosity
    )
  }
  training_tensor <- autoencoder_tensor(training)
  validation_tensor <- if (validate) {
    autoencoder_tensor(standardized[validation_index, , drop = FALSE])
  }
  fitted <- torch_fit(
    module = module,
    inputs = list(training_tensor),
    target = training_tensor,
    weights = torch::torch_ones(NROW(training), 1L),
    inputs_validation = if (validate) list(validation_tensor),
    target_validation = validation_tensor,
    weights_validation = if (validate) {
      torch::torch_ones(length(validation_index), 1L)
    },
    loss = config[["loss"]],
    objective = if (variational) vae_objective(config[["loss"]], beta),
    optimizer = config[["optimizer"]],
    lr = config[["lr"]],
    weight_decay = config[["weight_decay"]],
    betas = torch_betas(config[["beta1"]], config[["beta2"]]),
    eps = config[["eps"]],
    momentum = config[["momentum"]],
    lr_scheduler = config[["lr_scheduler"]],
    batch_size = batch_size,
    max_epochs = config[["max_epochs"]],
    patience = config[["patience"]],
    max_grad_norm = config[["max_grad_norm"]],
    drop_last = drop_last,
    device = device,
    verbosity = verbosity
  )

  # Components ----
  # Encoded after the fit, in eval mode, on the training device and through the
  # helper `apply_decomp_()` uses, so applying the fit to these cases
  # reproduces them.
  decom <- AutoencoderFit(
    state = torch_state(fitted[["module"]]),
    features = features,
    k = k,
    hidden_units = hidden_units,
    activation = config[["activation"]],
    norm = config[["norm"]],
    dropout = config[["dropout"]],
    input_dropout = config[["input_dropout"]],
    input_noise = config[["input_noise"]],
    variational = variational,
    beta = beta,
    center = unname(center),
    scale = unname(scale),
    device = device,
    n_validation = length(validation_index),
    epochs_trained = as.integer(fitted[["epochs_trained"]]),
    best_epoch = as.integer(fitted[["best_epoch"]]),
    history = fitted[["history"]]
  )
  transformed <- autoencoder_scores(
    fitted[["module"]],
    decom,
    x,
    algorithm = config@algorithm,
    device = device
  )
  # Resolved here, from the data, so the record would otherwise report unset
  # values the fit demonstrably used.
  config@hidden_units <- hidden_units
  config@batch_size <- batch_size
  list(decom = decom, transformed = transformed, config = config)
} # /rtemis::decomp_.AutoencoderBaseConfig


# %% apply_decomp_.AutoencoderBaseConfig ----
#' Apply a fitted autoencoder to new data
#'
#' @param config `AutoencoderBaseConfig` object.
#' @param decom `AutoencoderFit` object.
#' @param new_data Tabular data: The fit's features.
#' @param execution_config Optional `ExecutionConfig`: Where the fit is applied.
#' @param verbosity Integer: Verbosity level.
#'
#' @return Numeric matrix of components.
#'
#' @keywords internal
#' @noRd
method(apply_decomp_, AutoencoderBaseConfig) <- function(
  config,
  decom,
  new_data,
  execution_config = NULL,
  verbosity = 1L
) {
  check_dependencies("torch")
  check_is_S7(decom, AutoencoderFit)
  set_torch_threads(algorithm_threads(execution_config), verbosity = verbosity)
  autoencoder_scores(
    autoencoder_fit_module(decom),
    decom,
    new_data,
    algorithm = config@algorithm,
    device = autoencoder_device(execution_config)
  )
} # /rtemis::apply_decomp_.AutoencoderBaseConfig


# %% reconstruct_.AutoencoderBaseConfig ----
#' Decode autoencoder components back to input space
#'
#' @details
#' The decoder's output is in standardized units; the training statistics
#' stored on the fit map it back to the units of the data. Standardization is
#' per feature, so `x` is not needed.
#'
#' @param config `AutoencoderBaseConfig` object.
#' @param decom `AutoencoderFit` object.
#' @param transformed Numeric matrix: Components, cases by `k`.
#' @param x Tabular data: The data being reconstructed.
#' @param execution_config Optional `ExecutionConfig`: Where the work runs.
#' @param verbosity Integer: Verbosity level.
#'
#' @return Numeric matrix: Reconstruction in input units, cases by features.
#'
#' @keywords internal
#' @noRd
method(reconstruct_, AutoencoderBaseConfig) <- function(
  config,
  decom,
  transformed,
  x,
  execution_config = NULL,
  verbosity = 1L
) {
  check_dependencies("torch")
  check_is_S7(decom, AutoencoderFit)
  set_torch_threads(algorithm_threads(execution_config), verbosity = verbosity)
  decoded <- torch_forward(
    autoencoder_fit_module(decom)[["decoder"]],
    list(autoencoder_tensor(as.matrix(transformed))),
    device = autoencoder_device(execution_config)
  )
  reconstructed <- sweep(
    sweep(decoded, 2L, decom@scale, FUN = "*"),
    2L,
    decom@center,
    FUN = "+"
  )
  colnames(reconstructed) <- decom@features
  reconstructed
} # /rtemis::reconstruct_.AutoencoderBaseConfig


# %% training_device.AutoencoderBaseConfig ----
#' The device an autoencoder fit will run on
#'
#' Resolved once here for `decomp()`'s resources line and again in `decomp_()`
#' for the fit; the resolution is deterministic, so the two agree. NULL when
#' libtorch is absent, since `decomp_()` is about to report that.
#'
#' @param x `AutoencoderBaseConfig` object.
#' @param requested Optional `DeviceConfig` object.
#'
#' @return Character or NULL.
#'
#' @author EDG
#' @keywords internal
#' @noRd
method(training_device, AutoencoderBaseConfig) <- function(
  x,
  requested = NULL
) {
  torch_training_device(requested)
} # /rtemis::training_device.AutoencoderBaseConfig

Try the rtemis package in your browser

Any scripts or data that you put into this service are public.

rtemis documentation built on Oct. 20, 2026, 1:06 a.m.