---
title: "Introduction to densemlp"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Introduction to densemlp}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

```{r setup}
library(densemlp)
```

## What densemlp is

`densemlp` is a compact dense feedforward neural network (multilayer perceptron) for regression,
classification and survival analysis on tabular data. Forward propagation, backpropagation, and
Adam optimization are implemented natively in C++ via `RcppArmadillo` - there is no
`torch`/`libtorch` dependency, which is what makes it fast to install and fast to train.

Hidden layers are `Linear -> BatchNorm -> ReLU -> [gate] -> [dropout] -> [residual]` (set
`batch_norm = FALSE` to drop the normalization step), with a plain `Linear -> task activation`
output layer (linear for regression, sigmoid for binary classification, softmax for multiclass,
a linear risk score for survival). Numeric predictors are centered/scaled and categorical
predictors are one-hot encoded internally, so `x` can be a mixed-type data frame.

## Fitting a single model

```{r}
set.seed(1)
n <- 300
x <- data.frame(a = rnorm(n), b = rnorm(n), c = rnorm(n))
y <- 2 * x$a - x$b + 0.5 * x$a * x$c + rnorm(n, sd = 0.2)

train <- sample.int(n, floor(0.8 * n))
test <- setdiff(seq_len(n), train)

fit <- densemlp(x[train, ], y[train], hidden_units = c(32, 16), epochs = 60)
pred <- predict(fit, x[test, ])
sqrt(mean((pred - y[test])^2))
```

`task = "auto"` (the default) infers `"regression"` from a numeric outcome, or `"binary"` /
`"multiclass"` from a factor/character outcome with 2 or more than 2 levels respectively.

```{r}
y_class <- factor(ifelse(y > median(y), "high", "low"))
fit_class <- densemlp(x[train, ], y_class[train], hidden_units = c(32, 16), epochs = 60)
predict(fit_class, x[test, ], type = "prob")[1:5, ]
predict(fit_class, x[test, ], type = "class")[1:5]
```

## Architecture options

All accuracy-oriented options are opt-in and default to off, so a plain `densemlp(x, y)` call keeps
the smallest, fastest architecture.

```{r}
fit_opts <- densemlp(
  x[train, ], y[train], hidden_units = c(32, 16),
  residual = TRUE,          # learned skip connection per hidden block
  gated = TRUE,              # learned sigmoid gate per hidden block
  dropout = 0.05,            # inverted dropout per hidden layer
  batch_norm = TRUE,         # batch normalization inside each hidden block
  input_projection = 8,      # linear map of the encoded inputs before layer 1
  ema_decay = 0.99,          # moving-average weights for eval/final predictions
  lr_schedule = "cosine",
  epochs = 60
)
```

`input_projection = k` inserts a bare linear layer (no activation, no batch normalization) that
maps the encoded predictors to `k` dimensions before the first hidden block; it cannot be
combined with `interaction`.

`interaction = TRUE` (an efficient learned cross-feature layer) is also available, but is
currently numerically unstable on small-n/high-p regression data and is not recommended - see
`NEWS.md` for details. It is left out of `tune_densemlp()`'s search grid for the same reason.

## Internal ensembles

Setting `ensemble > 1` fits several bootstrap-resampled members internally and averages their
predictions transparently - `predict()` still returns a single vector/matrix. Use `ncores` to
fit members in parallel (via `parallel::mclapply` on Unix-alikes; falls back to serial on
Windows).

```{r}
fit_ens <- densemlp(
  x[train, ], y[train], hidden_units = c(32, 16), epochs = 60,
  ensemble = 5, ncores = 2
)
sqrt(mean((predict(fit_ens, x[test, ]) - y[test])^2))
```

## Cross-validation

`cv_densemlp()` fits `densemlp()` on each of `folds` splits and scores the held-out fold with
`densemlp_metrics()` (RMSE/NRMSE/R² for regression; accuracy/balanced accuracy/macro AUC/log loss for
classification). Extra arguments are forwarded to every fold's `densemlp()` call.

```{r}
cv <- cv_densemlp(x, y, folds = 5, hidden_units = c(32, 16), epochs = 40)
cv
```

## Hyperparameter tuning

`tune_densemlp()` grid-searches over `hidden_units`, `dropout`, `residual`, `gated`, `ema_decay`,
`lr_schedule`, `epochs`, `batch_size`, and `lr`. Each candidate is repeated (`repeats`) with
successive seeds and ranked by mean best-epoch validation loss; the best configuration is refit on
the full data by default.

```{r}
tuned <- tune_densemlp(
  x, y, repeats = 2,
  grid = list(
    hidden_units = list(c(16), c(32, 16)),
    lr = c(1e-3, 3e-3)
  )
)
tuned$best_config
```

## Survival outcomes

With `task = "survival"` the response is a `survival::Surv(time, event)` object (or a two-column
`(time, event)` matrix). Two losses are available: `loss = "cox"` (the default) trains a single
linear risk score with a batch-wise Breslow-tie Cox partial likelihood, and `loss = "brier"`
trains a discrete-time hazard head against the IPCW integrated Brier score.

```{r, eval = requireNamespace("survival", quietly = TRUE)}
library(survival)
data(lung, package = "survival")
lung <- na.omit(lung[, c("time", "status", "age", "sex", "ph.ecog", "ph.karno", "wt.loss")])
sy <- Surv(lung$time, lung$status == 2)
sx <- lung[, c("age", "sex", "ph.ecog", "ph.karno", "wt.loss")]

sfit <- densemlp(sx, sy, task = "survival", hidden_units = c(16, 8), epochs = 60)
risk <- predict(sfit, sx, type = "response")
densemlp_metrics(sy, risk, task = "survival")
```

## Variable importance

`perm_importance()` gives model-agnostic permutation importance: each predictor is shuffled in
turn and the drop in `densemlp_metrics()` performance is recorded.

```{r}
imp <- perm_importance(fit, x[test, ], y[test])
imp$data
```

## Visualizing training

```{r, fig.width = 6, fig.height = 4}
plot(fit)          # or, equivalently, plot_history(fit)
```

## Learn more

See `?densemlp`, `?cv_densemlp`, `?tune_densemlp`, `?densemlp_metrics` and `?perm_importance` for
full argument documentation, and `NEWS.md` for the changelog.
