35  Neural Networks in rtemis

setup_MLP() fits multilayer perceptrons with the torch package, and setup_TabNet() fits TabNet models with the tabnet package. Both run on the device named by the execution configuration; the examples here use the CPU. This chapter fits MLPs to the diabetes and heart failure data, shows early stopping on a validation set, and tunes the architecture and weight decay.

db <- read_diabetes()
hf <- read_heart_failure()
cv <- setup_KFold(n_resamples = 10L, seed = 2026L)
ex <- setup_SerialExecution(seed = 2026L, device = "cpu")

35.1 Configuration

The architecture is given by hidden_units, one width per hidden layer, or generated from a shape. By default, three layers taper from a width derived from the number of inputs (“funnel”). Training uses the AdamW optimizer, a learning rate of 0.001, batches of 256 cases, and at most 100 epochs. Numeric features are standardized, and factors are represented by learned embeddings:

setup_MLP(hidden_units = c(32L, 16L), dropout = 0.1, weight_decay = 0.01, seed = 2026L)
<MLPHyperparameters>
        hyperparameters: 
                              hidden_units: <int> 32, 16
                                activation: <chr> relu
                                norm_first: <lgc> FALSE
                                      bias: <lgc> TRUE
                                  residual: <lgc> FALSE
                                   dropout: <nmr> 0.10
                             input_dropout: <nmr> 0.00
                              weight_decay: <nmr> 0.01
                                l1_penalty: <nmr> 0.00
                                embeddings: <lgc> TRUE
                         embedding_dropout: <nmr> 0.00
                                 optimizer: <chr> adamw
                                        lr: <nmr> 1e-03
                                batch_size: <int> 256
                                max_epochs: <int> 100
                                  patience: <int> 10
                                      seed: <int> 2026
                               num_workers: <int> 0
                                 drop_last: <lgc> FALSE
                                       ifw: <lgc> FALSE
                  unset: <chr> shape, shape_layers, shape_max_units, norm, embedding_dim, beta1, (+6 more)
tunable_hyperparameters: <int> 19
  fixed_hyperparameters: <int> 13
                  tuned: <int> -1
              resampled: <int> 0
              n_workers: <int> 1

  No search values defined for tunable hyperparameters.

35.2 Early stopping

With a validation set, training stops when the validation loss has not improved for patience epochs, and the weights of the best epoch are kept. A network trained on 80% of the diabetes cases, with the remaining 20% for validation:

split <- resample(db, setup_StratSub(n_resamples = 1L, train_p = 0.8, seed = 2026L))
mod_es <- train(
  db[split[[1]]],
  dat_validation = db[-split[[1]]],
  hyperparameters = setup_MLP(
    hidden_units = c(64L, 32L),
    lr = 0.01,
    max_epochs = 500L,
    patience = 30L,
    seed = 2026L
  ),
  execution_config = ex,
  verbosity = 0L
)
plot_learning(mod_es)

The training and validation losses fall together over the first epochs. The validation loss reaches its minimum at epoch 48 and training stops 30 epochs later, while the training loss continues to fall.

35.3 Cross-validated performance

mlp_configs <- list(
  `MLP, default` = setup_MLP(seed = 2026L),
  `MLP, 32-16, lr 0.001` = setup_MLP(
    hidden_units = c(32L, 16L), dropout = 0.1, weight_decay = 0.01,
    lr = 0.001, seed = 2026L
  ),
  `MLP, 32-16, lr 0.01` = setup_MLP(
    hidden_units = c(32L, 16L), dropout = 0.1, weight_decay = 0.01,
    lr = 0.01, seed = 2026L
  )
)
mlp_db <- rbindlist(lapply(mlp_configs, function(hp) {
  mod <- train(
    db,
    hyperparameters = hp,
    outer_resampling_config = cv,
    execution_config = ex,
    verbosity = 0L
  )
  data.table(
    rsq_training = mod@metrics_training@mean_metrics[["rsq"]],
    rsq_test = mod@metrics_test@mean_metrics[["rsq"]]
  )
}), idcol = "model")
mlp_db
                  model rsq_training rsq_test
                 <char>        <num>    <num>
1:         MLP, default       0.4944   0.4552
2: MLP, 32-16, lr 0.001      -1.3291  -1.3546
3:  MLP, 32-16, lr 0.01       0.5519   0.5072

The network with two layers of 32 and 16 units, dropout and weight decay reaches a training \(R^2\) of only -1.329 with the default learning rate of 0.001; a training \(R^2\) below 0 shows that the optimization has not converged in 100 epochs. With a learning rate of 0.01, the same network reaches a training \(R^2\) of 0.552 and a test \(R^2\) of 0.507. The learning rate and the number of epochs determine whether training converges, and are tuned with the architecture.

mod_mlp_hf <- train(
  hf,
  hyperparameters = setup_MLP(seed = 2026L),
  outer_resampling_config = cv,
  execution_config = ex,
  question = hf_question
)
<Resampled Classification Model>
MLP (Multilayer perceptron (torch))
⟳ Tested using 10 independent folds.

  <Resampled Classification Training Metrics>
  Aggregate Confusion Matrix across resamples.
                     Predicted
          Reference  Yes  No    
                Yes  210   276
                 No   64  2060

  Showing mean (sd) across resamples.
        Sensitivity: 0.431 (0.144)
        Specificity: 0.970 (0.020)
  Balanced Accuracy: 0.701 (0.064)
                PPV: 0.784 (0.070)
                NPV: 0.883 (0.024)
                 F1: 0.539 (0.109)
           Accuracy: 0.870 (0.016)
                AUC: 0.873 (0.021)
        Brier Score: 0.098 (0.005)

  <Resampled Classification Test Metrics>
  Aggregate Confusion Matrix across resamples.
                     Predicted
          Reference  Yes  No   
                Yes   10   44
                 No   17  219

  Showing mean (sd) across resamples.
        Sensitivity: 0.193 (0.189)
        Specificity: 0.928 (0.041)
  Balanced Accuracy: 0.560 (0.099)
                PPV: 0.365 (0.329)
                NPV: 0.834 (0.041)
                 F1: NA (NA)
           Accuracy: 0.790 (0.054)
                AUC: 0.705 (0.102)
        Brier Score: 0.148 (0.032)

35.4 Tuning

Architectures are tuned by listing candidate vectors in tune_over(), each a complete architecture (Chapter 21):

mod_mlp_tuned <- train(
  db,
  hyperparameters = setup_MLP(
    hidden_units = tune_over(c(16L), c(32L, 16L), c(64L, 32L, 16L)),
    lr = tune_over(0.001, 0.01),
    dropout = 0.1,
    weight_decay = 0.01,
    seed = 2026L
  ),
  tuner_config = setup_GridSearch(
    resampler_config = setup_KFold(n_resamples = 5L, seed = 2027L)
  ),
  outer_resampling_config = cv,
  execution_config = ex,
  question = diabetes_question,
  verbosity = 0L
)
rbindlist(lapply(mod_mlp_tuned@models, function(m) {
  hp <- m@hyperparameters@hyperparameters
  list(
    hidden_units = paste(hp[["hidden_units"]], collapse = "-"),
    lr = hp[["lr"]]
  )
}), idcol = "fold")
       fold hidden_units    lr
     <char>       <char> <num>
 1:  Fold_1     64-32-16  0.01
 2:  Fold_2     64-32-16  0.01
 3:  Fold_3     64-32-16  0.01
 4:  Fold_4        32-16  0.01
 5:  Fold_5     64-32-16  0.01
 6:  Fold_6     64-32-16  0.01
 7:  Fold_7     64-32-16  0.01
 8:  Fold_8        32-16  0.01
 9:  Fold_9     64-32-16  0.01
10: Fold_10     64-32-16  0.01

35.5 TabNet

mod_tabnet <- train(
  db,
  hyperparameters = setup_TabNet(epochs = 200L),
  outer_resampling_config = cv,
  execution_config = ex,
  verbosity = 0L
)

35.6 Comparison

data.table(
  model = c("GLM", names(mlp_configs), "MLP, tuned", "TabNet"),
  rsq_test = c(
    rsq_glm,
    mlp_db[["rsq_test"]],
    mod_mlp_tuned@metrics_test@mean_metrics[["rsq"]],
    mod_tabnet@metrics_test@mean_metrics[["rsq"]]
  )
)
                  model rsq_test
                 <char>    <num>
1:                  GLM   0.4935
2:         MLP, default   0.4552
3: MLP, 32-16, lr 0.001  -1.3546
4:  MLP, 32-16, lr 0.01   0.5072
5:           MLP, tuned   0.5018
6:               TabNet   0.1301

On the diabetes data, the networks with a converged optimization perform comparably to the linear model, within the variation across folds; TabNet is far below it with 400 training cases. On the heart failure data, the default MLP has a mean test AUC of 0.705 and Brier score of 0.148. With a few hundred cases and ten features, the additional flexibility of a network does not translate into better predictions than a linear or additive model, and it brings optimization hyperparameters that must be tuned.