In this vignette, we will show two ways to define a neural network
architecture in mlr3torch. We start with the direct route,
where an existing torch module is turned into an
mlr3::Learner. Afterwards, we show how to build
architectures as mlr3pipelines::Graphs, which allows to
infer shapes from the task and to tune the architecture itself.
From a torch Module to a Learner
If you already have a torch::nn_module, or want to write
one yourself, lrn("classif.module") (or
lrn("regr.module") for regression) wraps it into an
mlr3::Learner. This gives you the training loop, resampling
and tuning infrastructure of mlr3torch without having to
express the architecture as a Graph.
The module generator needs to take task as an argument,
so that dimensions that depend on the data – such as the number of
features or classes – can be inferred during $train(). Its
remaining arguments become hyperparameters of the resulting
Learner.
nn_one_layer = nn_module("nn_one_layer",
initialize = function(task, size_hidden) {
self$first = nn_linear(task$n_features, size_hidden)
self$second = nn_linear(size_hidden, output_dim_for(task))
},
# the argument x corresponds to the ingress token x below
forward = function(x) {
self$second(nnf_relu(self$first(x)))
}
)The second ingredient are the ingress_tokens, which
specify how the features of the task are converted into the tensors that
are passed to the module’s forward() method. Their names
must match the arguments of forward(), so here
ingress_num() collects all numeric features into a single
tensor x.
module_learner = lrn("classif.module",
module_generator = nn_one_layer,
ingress_tokens = list(x = ingress_num()),
size_hidden = 20,
epochs = 10,
batch_size = 16
)
module_learner$train(tsk("iris"))
module_learner$network
#> An `nn_module` containing 163 parameters.
#>
#> ── Modules ─────────────────────────────────────────────────────────────────────
#> • first: <nn_linear> #100 parameters
#> • second: <nn_linear> #63 parametersNote that the parameters of the module generator are by default
inferred as untyped (ParamUty). To make them tunable, pass
an explicit param_set to lrn("classif.module")
that describes them,
e.g. paradox::ps(size_hidden = paradox::p_int(1L, tags = "train")).
Building an Architecture as a Graph
Writing the module directly gives you full control, but the
architecture is then a black box: its layer sizes are fixed in R code
and cannot be tuned, and the input dimensions have to be derived from
the task by hand. Building the network as a Graph instead
addresses both points. To show this, we create a simple CNN for the
tiny-imagenet task, which is a subset of the well-known Imagenet
benchmark.
imagenet = tsk("tiny_imagenet")
imagenet
#>
#> ── <TaskClassif> (110000x2): ImageNet Subset ───────────────────────────────────
#> • Target: class
#> • Properties: multiclass
#> • Features (1):
#> • lt (1): image
#> • Target classes: abacus (0%), academic gown, academic robe, judge's robe (0%),
#> acorn (0%), African elephant, Loxodonta africana (0%), albatross, mollymawk
#> (0%), alp (0%), altar (0%), American alligator, Alligator mississipiensis (0%),
#> American lobster, Northern lobster, Maine lobster, Homarus americanus (0%),
#> apron (0%) + 190 moreThe central ingredients for creating such graphs are
PipeOpTorch operators.
To mark the entry-point of the neural network, we use a
PipeOpTorchIngress, for which three different flavors
exist:
-
po("torch_ingress_num")for numeric data -
po("torch_ingress_categ")for categorical columns -
po("torch_ingress_ltnsr")forlazy_tensors
Because the imagenet task contains only one feature of type
lazy_tensor, we go for the last option:
architecture = po("torch_ingress_ltnsr")We now define a relatively simple convolutional neural network. Note
that in the code below po("nn_relu_1") is equivalent to
nn("relu", id = "nn_relu_1"). This is needed, because
mlr3pipelines::Graphs require that each PipeOp
has a unique ID.
What we can further notice is that we don’t have to specify the input
dimension for the convolutional layers, which are inferred from the task
during $train()ing. This means that our
Learner can be applied to tasks with different image sizes,
each time building up the correct network structure.
architecture = architecture %>>%
nn("conv2d_1", out_channels = 64, kernel_size = 11, stride = 4, padding = 2) %>>%
nn("relu_1") %>>%
nn("max_pool2d_1", kernel_size = 3, stride = 2) %>>%
nn("conv2d_2", out_channels = 192, kernel_size = 5, padding = 2) %>>%
nn("relu_2") %>>%
nn("max_pool2d_2", kernel_size = 3, stride = 2)We can now continue with specifying the classification part of the network, which is a dense network that repeats a layer twice:
In order to repeat a segment from a network multiple times, we can
use nn("block"), which we here repeat twice. Then, we
follow with the output head of the network, where we don’t have to
specify the number of classes, as they can also be inferred from the
task
Next, we can combine the convolutional part with the dense head:
Below, we display the network:
architecture$plot(html = TRUE)To turn this network architecture into an mlr3::Learner
what is left to do is to configure the loss, optimizer, callbacks, and
training arguments, which we do now: We use the standard cross-entropy
loss, SGD as the optimizer and checkpoint our model every 20 epochs.
checkpoint = tempfile()
architecture = architecture %>>%
po("torch_loss", t_loss("cross_entropy")) %>>%
po("torch_optimizer", t_opt("sgd", lr = 0.01)) %>>%
po("torch_callbacks",
t_clbk("checkpoint", freq = 20, path = checkpoint)) %>>%
po("torch_model_classif",
batch_size = 32, epochs = 100L, device = "cuda")
cnn = as_learner(architecture)
cnn$id = "cnn"This created Learner now exposes all configuration
options of the individual PipeOps in its
$param_set, from which we show only a subset for
readability:
as.data.table(cnn$param_set)[c(32, 34, 42), 1:4]
#> id class lower upper
#> <char> <char> <num> <num>
#> 1: block.n_blocks ParamInt 0 Inf
#> 2: block.dropout.p ParamDbl 0 1
#> 3: torch_loss.reduction ParamFct NA NAWe can still change them, or if we wanted to, even tune them! Below, we increase the number of blocks and latent dimension of the dense part, as well as change the learning rate of the SGD optimizer.
cnn$param_set$set_values(
block.n_blocks = 4L,
block.linear.out_features = 4096 * 2,
torch_optimizer.lr = 0.2
)Finally, we train the learner on the task:
cnn$train(imagenet)