---
title: "Getting Started with kindling"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Getting Started with kindling}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
    collapse = TRUE,
    comment = "#>",
    eval = identical(Sys.getenv("NOT_CRAN"), "true"),
    fig.width = 7,
    fig.height = 5,
    warning = FALSE,
    message = FALSE
)

fansi::set_knit_hooks(knitr::knit_hooks)
```

## Introduction

`{kindling}` bridges the gap between `{torch}` and `{tidymodels}`, providing a streamlined interface for building, training, and tuning deep learning models. This vignette will guide you through the basic usage.

## Installation

You can install `{kindling}` on CRAN:

``` r
install.packages('kindling')
```

Or install the development version from GitHub:

```{r eval = FALSE}
# install.packages("pak")
pak::pak("joshuamarie/kindling")
## devtools::install_github("joshuamarie/kindling") 
```

## Before using {kindling}

```{r}
library(kindling)
```

Before starting, you need to install LibTorch first, the backend of PyTorch, which is also the backend of `{torch}` R package:

``` r
torch::install_torch()
```

## Main Features

Current `{kindling}` supports the following: 

-   Code generation of `{torch}` expression
-   Multiple architectures available

    -  Base models interface: feedforward networks (MLP/DNN/FFNN) and recurrent variants (RNN, LSTM, GRU)
    -  Generalized neural network trainer that has the same topology as MLPs

-   Native support for R ML workflows and pipelines (currently `{tidymodels}`; `{mlr3}` planned)
-   Fine-grained control over network depth, layer sizes, and activation functions
-   GPU acceleration support via `{torch}` tensors

### What it doesn't support

As of `{kindling}` >0.3.0, it supports most of NN architectures thanks to its versatility, as long as they follow typical MLP's topology. This package, however, does not support the following:

1.  Residual Networks (ResNet)
2.  Automatic Integration (AutoInt)
3.  Self-Attention and Inter-sample Attention Transformer (Saint)

To use all of these, you might want to take an interest towards `{brulee}` package instead. The said NN architectures above are available on version 1.0.0 (and later) release. 

## Usage: Three Levels of Interaction

`{kindling}` is powered by R's metaprogramming capabilities through *code generation*. Generated `torch::nn_module()` expressions power the training functions, which in turn serve as engines for `{tidymodels}` integration. This architecture gives you flexibility to work at whatever abstraction level suits your task.

### Level 1: Code Generation for `torch::nn_module`

At the lowest level, you can generate raw `torch::nn_module` code for maximum customization. Functions ending with `_generator` return unevaluated expressions you can inspect, modify, or execute.

Here's how to generate a feedforward network specification:

```{r, collapse=FALSE, comment=""}
ffnn_generator(
    nn_name = "MyFFNN",
    hd_neurons = c(64, 32, 16),
    no_x = 10,
    no_y = 1,
    activations = 'relu'
)
```

This creates a three-hidden-layer network (64 - 32 - 16 neurons) that takes 10 inputs and produces 1 output. Each hidden layer uses ReLU activation, while the output layer remains "untransformed".

### Level 2: Direct Training Interface

Skip the code generation and train models directly with your data. This approach handles all the `{torch}` boilerplate when training the models internally.

Let's classify iris species:

```{r, comment="", collapse=FALSE} 
model = ffnn(
    Species ~ .,
    data = iris,
    hidden_neurons = c(10, 15, 7),
    activations = act_funs(relu, softshrink[lambd = 0.5], elu), 
    loss = "cross_entropy",
    epochs = 100
)

model
```

> For parametric activation functions like softshrink, which contains `"lambd"` ($\lambda$) as its parameter (the default is 1), use indexed syntax (available on v0.3.x+) e.g. `softshrink[lambd = 0.5]` or `softshrink[0.5]`, or a string literal expression e.g. `"softshrink(lambd = 0.5)"`, to transmute the parameter value. See `?kindling::act_funs()` for more details. 

Evaluate the prediction through `predict()`. The `predict()` method is extended for fitted models through its `newdata` argument. 

Two kinds of `predict()` usage: 

1.  **Without `newdata`** predictions default to the training data. 

    ```{r}
    predict(model) |>
        (\(x) table(actual = iris$Species, predicted = x))()
    ```


2.  **With `newdata`** simply pass the new data frame as the new reference. 

    ```{r}
    sample_iris = dplyr::slice_sample(iris, n = 10, by = Species)

    predict(model, newdata = sample_iris) |>
        (\(x) table(actual = sample_iris$Species, predicted = x))()
    ```


### Level 3: Conventional tidymodels Integration

Work with neural networks just like any other `{parsnip}` model. This unlocks the entire `{tidymodels}` toolkit for preprocessing, cross-validation, and model evaluation.

```{r}
# library(kindling)
# library(parsnip)
# library(yardstick)
box::use(
    kindling[mlp_kindling, rnn_kindling, act_funs, args],
    parsnip[fit, augment],
    yardstick[metrics]
)
data(Ionosphere, package = "mlbench")

ionosphere_data = Ionosphere[, -2]

# Train a feedforward network with parsnip
mlp_kindling(
    mode = "classification",
    hidden_neurons = c(128, 64),
    activations = act_funs(relu, softshrink[lambd = 0.5]),
    epochs = 100
) |>
    fit(Class ~ ., data = ionosphere_data) |>
    augment(new_data = ionosphere_data) |>
    metrics(truth = Class, estimate = .pred_class)

# Or try a recurrent architecture (demonstrative example with tabular data)
rnn_kindling(
    mode = "classification",
    hidden_neurons = c(128, 64),
    activations = act_funs(relu, elu),
    epochs = 100,
    rnn_type = "gru"
) |>
    fit(Class ~ ., data = ionosphere_data) |>
    augment(new_data = ionosphere_data) |>
    metrics(truth = Class, estimate = .pred_class)
```

## Hyperparameter Tuning & Resampling

The package has integration with `{tidymodels}`, so it supports hyperparameter tuning via `{tune}` with searchable parameters.

The current searchable parameters under `{kindling}`: 

-   Layer widths (neurons per layer)
-   Network depth (number of hidden layers)
-   Activation function combinations
-   Output activation
-   Optimizer (Type of optimization algorithm)
-   Bias (choose between the presence and the absence of the bias term)
-   Validation Split Proportion
-   Bidirectional (boolean; only for RNN)

The searchable parameters outside from `{kindling}`, i.e. under `{dials}` package such as `learn_rate()` also supported. 

Here's an example: 

```{r, collapse=FALSE, comment=""}
# library(tidymodels)
box::use(
    kindling[
        mlp_kindling, hidden_neurons, activations, output_activation, grid_depth
    ],
    parsnip[fit, augment],
    recipes[recipe],
    workflows[workflow, add_recipe, add_model],
    rsample[vfold_cv],
    tune[tune_grid, tune, select_best, finalize_workflow],
    dials[grid_random],
    yardstick[accuracy, roc_auc, metric_set, metrics]
)

mlp_tune_spec = mlp_kindling(
    mode = "classification",
    hidden_neurons = tune(),
    activations = tune(),
    output_activation = tune()
)

iris_folds = vfold_cv(iris, v = 3)
nn_wf = workflow() |>
    add_recipe(recipe(Species ~ ., data = iris)) |>
    add_model(mlp_tune_spec)

nn_grid_depth = grid_depth(
    hidden_neurons(c(32L, 128L)),
    activations(c("relu", "elu")),
    output_activation(c("sigmoid", "linear")),
    n_hlayer = 2,
    size = 10,
    type = "latin_hypercube"
)

# This is supported but limited to 1 hidden layer only
## nn_grid = grid_random(
##     hidden_neurons(c(32L, 128L)),
##     activations(c("relu", "elu")),
##     output_activation(c("sigmoid", "linear")),
##     size = 10
## )

nn_tunes = tune::tune_grid(
    nn_wf,
    iris_folds,
    grid = nn_grid_depth
)

best_nn = select_best(nn_tunes)
best_nn
final_nn = finalize_workflow(nn_wf, best_nn)
final_nn_model = fit(final_nn, data = iris)
final_nn_model

final_nn_model |>
    augment(new_data = iris) |>
    metrics(truth = Species, estimate = .pred_class)
```

Resampling strategies from `{rsample}` will enable robust cross-validation workflows, orchestrated through the `{tune}` and `{dials}` APIs.

## Variable Importance

`{kindling}` integrates with established variable importance methods from `{NeuralNetTools}` and `{vip}` to interpret trained neural networks. Two primary algorithms are available:

1.  Garson's Algorithm

    ```{r}
    garson(model, bar_plot = FALSE)
    ```

2.  Olden's Algorithm

    ```{r}
    olden(model, bar_plot = FALSE)
    ```

### Integration with {vip}

For users working within the `{tidymodels}` ecosystem, `{kindling}` models work seamlessly with the `{vip}` package:

```{r, fig.height=6, fig.width=8}
#| label: example
#| fig.alt: "Variable Importance Plot"
box::use(
    vip[vi, vip]
)

vi(model) |> 
    vip()
```

*Note: Weight caching increases memory usage proportional to network size. Only enable it when you plan to compute variable importance multiple times on the same model.*

## Learn More

- Visit the package website: https://kindling.joshuamarie.com
- Report issues: https://github.com/joshuamarie/kindling/issues
