If you fit a feed-forward network through parsnip::mlp()
inside a workflow, orbital() works exactly as
it does for any other model: call orbital() on the fitted
workflow and everything else in this package (predict(),
orbital_sql(), orbital_inline(), and so on)
follows the usual pattern described in vignette("orbital").
Nothing below is required for that path.
This vignette instead covers converting a network object directly,
either because you built one yourself with
torch::nn_sequential() or
keras3::keras_model_sequential()/keras3::keras_model(),
or because you want more control than mlp() exposes (a
custom mode, type, or lvl, or
extracting an intermediate hidden layer instead of the final
output).
Supported layers
orbital() walks a network’s layers directly, so it only
supports the layer types it knows how to translate into an equivalent
SQL/dplyr expression:
- A stack of linear/dense layers, each optionally followed by an
activation (
relu,sigmoid,tanh,linear/identity,leaky_relu,elu,gelu, andsoftmaxon the final layer only). -
Dropoutlayers, which are a no-op at prediction time and are skipped. -
BatchNorm/LayerNorm(torch:nn_batch_norm1d,nn_layer_norm; keras3:layer_batch_normalization(),layer_layer_normalization()), applied using their stored evaluation-time statistics.
Anything else (convolutional layers, recurrent layers, attention,
residual/skip connections, embeddings, and so on) is not supported and
orbital() will error rather than silently producing an
incorrect translation.
Input names
Unlike a fitted lm() or
parsnip::model_fit(), a bare torch or keras3 network
carries no record of its input feature names anywhere in the object:
torch tensors are positional, and a keras3 Input() is an
anonymous fixed-width vector. Because of this, input_names
is required whenever you call orbital() on a bare network
directly, in the same order the network was trained on:
library(orbital)
library(torch)
net <- nn_sequential(
nn_linear(4, 8),
nn_relu(),
nn_linear(8, 1)
)
ob <- orbital(net, input_names = c("x1", "x2", "x3", "x4"))
predict(ob, new_data)The same requirement applies to bare keras3 models:
library(keras3)
x <- keras_model_sequential(input_shape = 4) |>
layer_dense(8, activation = "relu") |>
layer_dense(1)
ob <- orbital(x, input_names = c("x1", "x2", "x3", "x4"))Mode, type, and lvl
orbital() infers mode from the network’s
shape: a single output unit with a linear (or no) final activation is
"regression"; anything with more than one output unit, or a
single unit behind a sigmoid, is
"classification". You can override the inferred value with
mode = "regression"/mode = "classification" if
you trained the network with a loss function that doesn’t match this
default (for example, a single-unit classifier with a bare linear final
layer and a manually-implemented loss).
For a classification network, type controls whether
predict() returns class probabilities
(type = "prob", the default) or the predicted class
(type = "class"). lvl names the classes; if
you don’t supply one, they default to class_0,
class_1, and so on, in the network’s own output order:
Extracting an intermediate layer
output_layer lets you stop at a hidden layer instead of
the network’s final output, useful for exposing a learned embedding
rather than a prediction. It takes the 1-based index of the linear/dense
layer to stop at (counting only linear/dense layers, ignoring
activations, dropout, and normalization):
ob <- orbital(net, input_names = c("x1", "x2", "x3", "x4"), output_layer = 1)
predict(ob, new_data)
#> .pred_layer_1_1 .pred_layer_1_2 ... .pred_layer_1_8output_layer is not supported for multi-output networks
(below), since there each head already exposes its own layers
separately.
Multi-output networks
A network with a shared trunk feeding two or more separate output
heads (for example, predicting both a numeric price and a product
category from the same hidden representation) is a branching graph
rather than a single chain, so it needs its own input shape.
mode, type, and lvl each become a
named list keyed by head name instead of a single value, and each head’s
own value is inferred/defaulted independently, exactly as it would be
for a single-output network.
torch
For torch, express the trunk and heads explicitly as a
list(trunk = ..., heads = list(...)), each element a plain
nn_sequential():
net <- list(
trunk = nn_sequential(
nn_linear(4, 8),
nn_relu()
),
heads = list(
price = nn_sequential(nn_linear(8, 1)),
category = nn_sequential(nn_linear(8, 2))
)
)
ob <- orbital(
net,
input_names = c("x1", "x2", "x3", "x4"),
mode = list(price = "regression", category = "classification"),
lvl = list(category = c("standard", "luxury"))
)
preds <- predict(ob, new_data)
names(preds)
#> [1] ".pred_price" ".pred_category_class"orbital() has no way to infer trunk/heads from an
arbitrary torch nn_module’s forward() method,
which is why the shape has to be spelled out this way rather than passed
as a single network object.
keras3
keras3’s functional API already records the model’s graph in its own
config, so there’s no separate trunk/heads argument to fill in: build
the model with keras_model() and a named list of outputs,
and orbital() infers the shared trunk and each head’s own
layers directly from the graph, by finding the longest run of layers
common to every output’s path back to the input.
library(keras3)
inp <- keras_input(shape = 4L)
trunk <- inp |> layer_dense(8, activation = "relu")
price <- trunk |> layer_dense(1)
category <- trunk |> layer_dense(2)
x <- keras_model(inputs = inp, outputs = list(price = price, category = category))
ob <- orbital(
x,
input_names = c("x1", "x2", "x3", "x4"),
mode = list(price = "regression", category = "classification"),
lvl = list(category = c("standard", "luxury"))
)If the outputs list isn’t named, the head names fall back to the outputs’ own layer names.
A functional model must have exactly one input and two or more
outputs; a layer that is called more than once, or that merges more than
one tensor (e.g. layer_concatenate()), is not supported,
since neither has a meaningful trunk/heads split.
