A learner is the model half of a candidate: a representation says how
the record is reduced, and a learner says what is fitted to the
reduction. timesift ships fifteen learners, from a
penalised logistic regression to a residual convolutional network. Every
one of them goes through the same pair of functions, the same folds and
the same scoring as a learner written for a single study, so adding a
model never means changing the code that fits or scores.
This article lists what ships, what each learner can be handed, how the main ones are set, how the neural encoders are trained and tuned, and how a learner, a response type or a metric of one’s own is added. It ends with what each learner costs on a small record and where each is the wrong choice.
The examples use one simulated record throughout: 150 units read every six hours for a year, and four presence-absence responses that depend on the record through a lagged mechanism acting at the weekly grain. In this simulation the response reads each unit’s departure from its own mean, so a learner can find the signal only at a grain fine enough to keep that departure.
Installation
# Install from CRAN
install.packages("timesift")
# Or install the development version from GitHub
# install.packages("pak")
pak::pak("gcol33/timesift")# Install from PyPI
pip install timesift
# with the torch encoders, the contrasts and the plots
pip install "timesift[torch,contrasts,plot]"
# Or install the development version from GitHub
pip install git+https://github.com/gcol33/timesift
sim <- simulate_records(n = 150L, mechanism = "lag", variables = 4L, days = 365L,
step_hours = 6, prevalence = 0.3, auc = 0.85, seed = 4L)
targets <- data.frame(unit = rownames(sim$y), sim$y)
colSums(sim$y)
#> v01 v02 v03 v04
#> 46 55 40 51One interface
A learner is a list carrying a fit function, a
predict function and three declarations: what it reads,
whether it covers the responses jointly or one at a time, and which
packages it needs. Printing one shows the declarations and the settings
it carries, with NULL where a setting takes its default at
the fit.
elasticnet()
#> <timesift learner> elasticnet
#> reads : tabular ; one model per response: yes, separate
#> data : every representation of the run
#> settings: alpha = 0.5, n_inner = 10, squares = TRUE, s = lambda.1se, n_lambda = 100, tol = 1e-08, threads = 1, seed = 1
forest(balance = TRUE)
#> <timesift learner> forest
#> reads : tabular ; one model per response: yes, separate
#> data : every representation of the run
#> settings: trees = NULL, mtry = NULL, min_node = NULL, balance = TRUE, preset = default, seed = 1, threads = 1fit is handed a [unit, bin, channel] array
and the response matrix of the same units, and predict
returns one column per response. A learner that fits one model per
response is still handed the whole response matrix, so nothing above the
learner layer needs to know which kind it was. c() binds
learners into a set, which timesift() takes as
learners.
c(elasticnet(), forest(), cnn())
#> <timesift models> 3 learners
#> elasticnet reads tabular, separate
#> forest reads tabular, separate
#> cnn reads sequence, jointThe learners that ship
Every shipped learner is registered under its own name, and its declarations can be read off the constructor:
shipped <- learners()
built <- lapply(shipped, function(nm) getExportedValue("timesift", nm)())
data.frame(learner = shipped,
reads = vapply(built, `[[`, "", "reads"),
multi = vapply(built, `[[`, "", "multi"),
needs = vapply(built, function(l) paste(l$needs, collapse = ", "), ""))
#> learner reads multi needs
#> 1 additive tabular separate
#> 2 boosting tabular separate
#> 3 cnn sequence joint torch
#> 4 discriminant tabular separate
#> 5 elasticnet tabular separate
#> 6 envelope tabular separate
#> 7 forest tabular separate
#> 8 hierarchical tabular separate
#> 9 linear tabular separate
#> 10 mars tabular separate
#> 11 maxent tabular separate
#> 12 mlp tabular joint torch
#> 13 perceptron tabular separate
#> 14 rescnn sequence joint torch
#> 15 tree tabular separateTwelve learners read a tabular block and fit one model per response.
mlp() also reads a tabular block, flattening the channels,
but fits every response jointly through one shared embedding, as the two
convolutional encoders do. Only cnn() and
rescnn() read the bins as a sequence. The three encoders
need torch. Every other learner runs on the compiled core
the package ships, which the Python package calls as well, so the two
languages fit the same model.
Each compiled learner is written from the published method and is checked in the test fixtures against the package whose numbers it reproduces. The biomod2 article maps each of them to the biomod2 algorithm it corresponds to, with the option sets biomod2 uses.
| learner | model | numbers checked against | responses |
|---|---|---|---|
elasticnet() |
elastic net over every column and its square, penalty chosen by inner cross-validation | glmnet | every head |
linear() |
generalised linear model, terms searched by AIC | MASS stepAIC(), glm()
|
every head |
tree() |
classification and regression tree, pruned by inner cross-validation | rpart | every head |
forest() |
random forest | randomForest | every head |
boosting() |
gradient boosted trees, first or second order | gbm, xgboost | every head |
maxent() |
maximum entropy over maxnet’s feature classes | maxnet | presence-absence |
envelope() |
surface range envelope | biomod2 bm_SRE()
|
presence-absence |
mars() |
multivariate adaptive regression splines | earth | every head |
discriminant() |
flexible discriminant analysis on a MARS basis | mda fda(method = mars)
|
presence-absence |
additive() |
additive model of thin plate regression splines | mgcv gam(method = "GCV.Cp")
|
every head |
perceptron() |
network of one hidden layer, fitted by BFGS | nnet | every head |
hierarchical() |
Bayesian logistic model, optional unit intercepts and spatial field | none | presence-absence |
mlp() |
fully connected encoder, joint head | torch | every head |
cnn() |
convolutional encoder, joint head | torch | every head |
rescnn() |
dilated residual convolutional encoder, joint head | torch | every head |
“Every head” means the learner fits whatever loss the response head
names: a logistic model under presence-absence, a Gaussian one under the
continuous, abundance and ordinal heads, and a Poisson one under the
count head. hierarchical() has no reference package. It
finds the posterior by Laplace’s method and integrates its
hyperparameters over a grid around their mode (Rue, Martino and Chopin
2009), and with spatial = "nngp" or "hsgp" it
adds a Gaussian-process field over the coordinates that
timesift(coords = ) names.
What a learner may be handed
A tabular learner reads the bins as a block of predictors, one column
per bin of each channel. Given native(), the record
unreduced, it would be handed one column per reading, so the pair is
refused. When the learner is pinned to native() through
data =, the refusal comes before the record is read:
timesift(targets, sim$readings, y = starts_with("v"), id = unit, time = time,
learners = forest(data = native()), verbose = FALSE)
#> Error:
#> ! `forest()` reads a tabular representation; `native()` gives it one column per reading. Use `grain()`, `multigrain()` or `lookback()`.Inside a sift the same pair is skipped and the run goes
on. The candidate table keeps the skipped pair, with the reason in
note.
mixed <- timesift(targets, sim$readings, y = starts_with("v"), id = unit, time = time,
learners = c(elasticnet(), forest()),
sift = c(native(), grain("month")),
resampling = cv(v = 5), n_inner = NULL, ensemble = FALSE, verbose = FALSE)
mixed$candidates[c("candidate", "bins", "status")]
#> candidate bins status
#> 1 elasticnet / native 1460 not applicable
#> 2 elasticnet / month 12 fitted
#> 3 forest / native 1460 not applicable
#> 4 forest / month 12 fittedThe native representation of this record has 1460 bins, one per
reading. A sequence learner is refused the opposite case: a
representation that gives one row of features, such as
multigrain(), or one whose record turns out to hold a
single bin. That refusal is shown with the encoders below.
Several learners in one run
learners takes a set or a named list. A named list
reports each candidate under its own name, which is how two settings of
one learner sit side by side. Every learner without a
data = runs at every grain of sift;
linear() is pinned to the monthly grain here, and the
elastic net is given a looser stopping tolerance, both to keep the run
short. The section on settings shows what the tolerance changes.
models <- list(
elasticnet = elasticnet(tol = 1e-4),
linear = linear(data = grain("month")),
tree = tree(),
forest = forest(),
forest_balanced = forest(balance = TRUE),
gbm = boosting(),
xgboost = boosting(method = "xgboost"),
maxent = maxent()
)
run_time <- system.time(
fit <- timesift(targets, sim$readings, y = starts_with("v"), id = unit, time = time,
learners = models, sift = grains("week", "month"),
resampling = cv(v = 5), n_inner = 3L, verbose = FALSE)
)
run_time[["elapsed"]]
#> [1] 30.54The run took 31 seconds for 15 candidates. With three inner folds in each of five outer folds, every candidate is fitted 5 x (3 + 1) = 20 times, three inner fits and one outer fit in each outer fold, and once more on every target.
summary() lists the candidates in order of their mean
score on the outer folds, with the number of responses on which each
scored highest, and then the held-out score of the procedure that
chooses and weights them.
summary(fit)
#> timesift 150 targets, 4 responses, 5-fold random CV, roc_auc
#>
#> candidates, scored on the outer folds
#> candidate mean won responses
#> forest_balanced / month 0.459 0 separate
#> forest / month 0.461 0 separate
#> tree / month 0.480 0 separate
#> gbm / month 0.483 0 separate
#> xgboost / month 0.503 0 separate
#> elasticnet / month 0.523 0 separate
#> tree / week 0.537 0 separate
#> forest_balanced / week 0.593 0 separate
#> forest / week 0.595 0 separate
#> xgboost / week 0.611 0 separate
#> maxent / month 0.626 0 separate
#> linear / month 0.634 0 separate
#> gbm / week 0.637 0 separate
#> elasticnet / week 0.700 0 separate
#> maxent / week 0.750 4 separate
#>
#> procedure, chosen and weighted inside each outer training fold
#> selected 0.741 se 0.048
#> ensemble 0.740 se 0.040
#> selected maxent / week in 4, elasticnet / week in 1 of 5 folds
#>
#> choice on every target maxent / week
#> weights on every target maxent / week 0.80 gbm / week 0.10 linear / month 0.09 elasticnet / week 0.02Each learner run at both grains scores higher at the weekly one,
which is the grain the simulation acts at. At the monthly grain the
tree, both forests and gbm score below 0.5, so they find nothing there.
maxent / week has the highest mean, 0.750, and scored
highest on all four responses. The procedure rows are the scores to
report: inside each outer training fold the inner folds chose a
candidate and fitted the stack’s weights, and the outer test fold then
scored that choice once, 0.741 for the selected candidate and 0.740 for
the stack. The highest candidate mean was picked out on the same folds
it is scored on.
The candidate table records how each candidate was built: its learner, its grain, the number of bins and channels, whether it was fitted, and, for a tuned learner, the settings it chose.
One learner on one array
fit_learner() fits a learner to an array from
grain_matrix() without the folds and the scoring around it,
and predict() on the result takes an array of the same bins
and channels. The units below are split in half once. The two arrays are
built from the two halves of the record, over the same calendar
weeks.
train <- rownames(sim$y)[1:75]
test <- rownames(sim$y)[76:150]
in_train <- sim$readings$unit %in% train
week_train <- grain_matrix(sim$readings[in_train, ], unit, time, reading, grain = "week")
week_test <- grain_matrix(sim$readings[!in_train, ], unit, time, reading, grain = "week")
f <- fit_learner(elasticnet(), week_train, sim$y[train, ])
f
#> <timesift fit> elasticnet at the week grain
#> channels: mean
#> response: presence_absence on 4 variables
f$model[c("unfitted", "stopped")]
#> $unfitted
#> character(0)
#>
#> $stopped
#> character(0)unfitted names any response with too few presences to
choose a penalty on, which is then predicted its share among the fitting
units, and stopped names any response whose penalty path
ended early. Neither applies here. The fit carries the bins it was made
on, and an array with other bins is refused:
month_train <- grain_matrix(sim$readings[in_train, ], unit, time, reading, grain = "month")
month_test <- grain_matrix(sim$readings[!in_train, ], unit, time, reading, grain = "month")
predict(f, month_test)
#> Error:
#> ! the representation predicted on has different channels or bins from the fitted one: 12 bins here and 53 bins in the fit. A calendar grain is read by its bins' instants, so a fit predicts a record over the same period; a lookback reads a span relative to each target and predicts any period.Settings of the main learners
The settings below are the ones most often changed; each constructor’s documentation lists the rest.
elasticnet() fits one penalised model per response over
every column and, with squares = TRUE, its square.
alpha mixes the two penalties, 1 being the
lasso and 0 ridge. s is where the penalty path
is read: "lambda.1se", the largest penalty within one
standard error of the least held-out deviance, is glmnet’s default,
"lambda.min" is the penalty of least deviance, and a number
is a penalty given directly. tol is where the coordinate
descent stops; the default leaves the fit as close to the optimum as
glmnet’s own default does, and a looser one is faster.
forest() grows 500 trees by default, with
mtry the square root of the column count under
presence-absence. balance = TRUE draws as many units of
each class into every tree as the smaller class holds, so a rare
response’s presences are half of each tree’s draw.
boosting() fits gbm’s first-order trees by default and
XGBoost’s second-order trees with method = "xgboost", whose
lambda and gamma are the penalty on a leaf and
the least gain a split must make. maxent() builds maxnet’s
feature classes, the letters of classes: l
linear, q quadratic, p pairwise products,
h hinges and t thresholds. Left
NULL, the classes follow each response’s presence count, as
maxnet chooses them.
The function below fits a learner on the training half and returns its AUC on the other half, averaged over the four responses.
held_out_auc <- function(l) {
f <- fit_learner(l, week_train, sim$y[train, ])
p <- predict(f, week_test)
mean(vapply(colnames(p), function(v) roc_auc(sim$y[test, v], p[, v]), numeric(1)))
}
variants <- list(
`elasticnet()` = elasticnet(),
`elasticnet(s = "lambda.min")` = elasticnet(s = "lambda.min"),
`elasticnet(alpha = 1)` = elasticnet(alpha = 1),
`elasticnet(tol = 1e-4)` = elasticnet(tol = 1e-4),
`forest()` = forest(),
`forest(balance = TRUE)` = forest(balance = TRUE),
`boosting()` = boosting(),
`boosting(method = "xgboost")` = boosting(method = "xgboost"),
`maxent()` = maxent(),
`maxent(classes = "lq")` = maxent(classes = "lq")
)
round(vapply(variants, held_out_auc, numeric(1)), 3)
#> elasticnet() elasticnet(s = "lambda.min")
#> 0.631 0.680
#> elasticnet(alpha = 1) elasticnet(tol = 1e-4)
#> 0.608 0.631
#> forest() forest(balance = TRUE)
#> 0.516 0.514
#> boosting() boosting(method = "xgboost")
#> 0.606 0.555
#> maxent() maxent(classes = "lq")
#> 0.665 0.693On this split the elastic net read at "lambda.min"
scores 0.680 against 0.631 at the default "lambda.1se", and
the lasso scores 0.608. The looser tolerance gives the same AUC as the
default to three decimals, and the timings at the end of the article
show what it saves. maxent(classes = "lq") scores 0.693
against 0.665 for the classes maxnet chooses, and both forests score
close to 0.5 at this grain on 75 units. A single split of 75 units is a
noisy comparison, so a difference of this size is worth checking under
timesift()’s folds, with the two settings as two named
candidates, before acting on it.
Neural encoders
The encoders separate architecture from training. mlp(),
cnn() and rescnn() take the layer widths, the
kernel and the dropout, and train_control() holds every
training setting: epochs, batch size, learning rate, optimiser,
schedule, early stopping and device. train_control() is the
one place a training setting is defaulted. A run hands its control to
every neural learner, and a training setting given to one learner
overrides the run’s control for that learner alone, on the settings it
names.
train_control(epochs = 5L, device = "cpu")
#> <timesift control>
#> epochs 5
#> batch_size 64 (default)
#> learning_rate 0.001 (default)
#> weight_decay 1e-04 (default)
#> optimizer adamw (default)
#> penalty 0 (default)
#> alpha 0.5 (default)
#> schedule cosine (default)
#> plateau_factor 0.1 (default)
#> plateau_patience 10 (default)
#> early_stopping Inf (default)
#> val_frac 0 (default)
#> device cpu
#> seed 1 (default)
#> swa FALSE (default)
#> swa_start 0.7 (default)
cnn(channels = c(8L, 16L), kernel = 3L, epochs = 10L)
#> <timesift learner> cnn
#> reads : sequence ; one model per response: no, joint
#> data : every representation of the run
#> settings: channels = 8/16, kernel = 3, dropout = 0.3
#> training: epochs = 10
#> needs : torchThe control marks the settings that were not named as
(default), and the learner lists under
training the one setting it overrides. The run below trains
both encoders at the weekly grain for a handful of epochs on the
processor. The run’s control sets five epochs and the convolutional
encoder overrides it with ten. This keeps the example quick and leaves
both networks undertrained.
nets <- timesift(targets, sim$readings, y = starts_with("v"), id = unit, time = time,
learners = c(mlp(hidden = c(32L, 16L)),
cnn(channels = c(8L, 16L), kernel = 3L, epochs = 10L)),
sift = grains("week"), control = train_control(epochs = 5L, device = "cpu"),
resampling = cv(v = 3), n_inner = NULL, ensemble = FALSE, verbose = FALSE)
summary(nets)
#> timesift 150 targets, 4 responses, 3-fold random CV, roc_auc
#>
#> candidates, scored on the outer folds
#> candidate mean won responses
#> mlp / week 0.495 2 joint
#> cnn / week 0.508 2 joint
#>
#> choice on every target cnn / weekAfter so few epochs both encoders score close to 0.5, as an
undertrained network does. The responses column reads
joint: one network covers all four responses, and each
candidate is still reported once.
A sequence learner pinned to a representation that gives one row of features is refused before the record is read:
timesift(targets, sim$readings, y = starts_with("v"), id = unit, time = time,
learners = cnn(data = multigrain(c("week", "month"))), verbose = FALSE)
#> Error:
#> ! `cnn()` reads a sequence; `multigrain()` gives one row of features.A fitted encoder holds its weights as plain arrays and the device as
the setting it was given, so a fit saved with saveRDS() on
a machine with a graphics card predicts after readRDS() on
one without.
Tuning
tune() wraps a learner so that, each time it is fitted,
it cross-validates a grid of its own settings on the units it is handed
and fits the setting that scored best. In a run each outer fold tunes on
its own training units, so the setting a fold is scored with was never
chosen on that fold’s held-out units. A grid names settings the learner
carries, and every combination is tried.
tuned <- tune(forest(trees = 200L), list(mtry = c(2L, 8L), min_node = c(1L, 5L)), n_inner = 3L)
tf <- fit_learner(tuned, week_train, sim$y[train, ])
tf$model$chosen
#> $mtry
#> [1] 8
#>
#> $min_node
#> [1] 5
tf$model$table
#> settings score
#> 1 mtry = 2, min_node = 1 0.5355025
#> 2 mtry = 8, min_node = 1 0.5438940
#> 3 mtry = 2, min_node = 5 0.5415086
#> 4 mtry = 8, min_node = 5 0.5666238The search chose mtry = 8 and min_node = 5,
the combination with the highest mean inner AUC. Under
timesift(), the settings each tuned candidate chose on all
targets are in the settings column of the candidate
table.
Given no grid, tune() searches the grid
registered under the learner’s name. Eight shipped learners carry the
grids that biomod2’s BIOMOD_Tuning() searches:
tunings()
#> [1] "boosting" "discriminant" "envelope" "forest" "mars"
#> [6] "maxent" "mlp" "perceptron"register_tuning() adds a grid, as a named list or as a
function of the learner and the array for a grid that depends on the
data, such as one bounded by the number of columns. The elastic net has
no grid registered; the one below searches its mixing.
register_tuning("elasticnet", list(alpha = c(0.25, 1)))
te <- fit_learner(tune(elasticnet(), n_inner = 3L), month_train, sim$y[train, ])
te$model$table
#> settings score
#> 1 alpha = 0.25 0.5088889
#> 2 alpha = 1 0.5158333alpha = 1, the lasso, scored higher on the three inner
folds, and the tuned learner was fitted with it. The registration lasts
for the session, and replacing a grid already registered takes
overwrite = TRUE.
A learner of one’s own
learner() takes a fit and a
predict function, and fit is handed what it
declares. A fit with an argument called
weights receives the response head’s case weights, one per
cell of the response. One with head receives the head
itself, whose loss says what to fit toward. One with
control receives the resolved train_control(),
and one with group receives the grouping the outer folds
keep whole, so that a split drawn inside the fit keeps it too. A
fit that declares none of these is fitted unweighted, with
nothing but the array and the response.
The learner below reduces every channel to its mean over the bins and fits one generalised linear model per response, in the family the head’s loss names.
channel_means <- function(data = NULL) {
learner(
"channel_means",
fit = function(x, y, head, weights, ...) {
f <- data.frame(apply(unclass(x), c(1L, 3L), mean))
family <- switch(head$loss,
binary_cross_entropy = stats::quasibinomial(),
squared_error = stats::gaussian(),
poisson_deviance = stats::quasipoisson())
lapply(seq_len(ncol(y)), function(j) {
stats::glm(y[, j] ~ ., data = f, weights = weights[, j], family = family)
})
},
predict = function(model, x) {
f <- data.frame(apply(unclass(x), c(1L, 3L), mean))
vapply(model, function(m) stats::predict(m, f, type = "response"), numeric(nrow(f)))
},
data = data
)
}
channel_means()
#> <timesift learner> channel_means
#> reads : tabular ; one model per response: yes, separate
#> data : every representation of the runregister_learner() makes the constructor available by
name, beside the shipped learners, and a registered learner can then be
given to timesift() or fit_learner() as its
name alone. A fit made by a registered learner is saved as the learner’s
name and settings, so a session that reads the fit back with
readRDS() registers the learner again before predicting
from it.
register_learner("channel_means", channel_means)
"channel_means" %in% learners()
#> [1] TRUE
own <- timesift(targets, sim$readings, y = starts_with("v"), id = unit, time = time,
learners = c(elasticnet(), channel_means()),
sift = grains("week", "month"),
resampling = cv(v = 5), n_inner = NULL, ensemble = FALSE, verbose = FALSE)
summary(own)
#> timesift 150 targets, 4 responses, 5-fold random CV, roc_auc
#>
#> candidates, scored on the outer folds
#> candidate mean won responses
#> channel_means / month 0.461 0 separate
#> channel_means / week 0.461 0 separate
#> elasticnet / month 0.524 0 separate
#> elasticnet / week 0.701 4 separate
#>
#> choice on every target elasticnet / weekchannel_means scores 0.461 at both grains. The mean of a
channel over the whole year is nearly the same number whichever bins it
is averaged from, and in this simulation the responses read each unit’s
departure from its own level, which a yearly mean averages away. The
weekly elastic net, which reads every bin, scores 0.701.
Other responses, and other metrics
The response head says what the values are, how each learner fits
them and which cells a score is defined on. Besides
"presence_absence", four heads ship:
responses()
#> [1] "abundance" "continuous" "count" "ordinal"
#> [5] "presence_absence"A count is fitted under the Poisson deviance. The counts below are drawn from each unit’s mean reading, and the forest fits them with no change to the learner.
week <- grain_matrix(sim$readings, unit, time, reading, grain = "week")
rate <- exp(0.5 + 0.8 * as.numeric(scale(apply(week, 1L, mean))))
counts <- matrix(rpois(150, rate), ncol = 1L, dimnames = list(rownames(sim$y), "count"))
cf <- fit_learner(forest(), week_train, counts[train, , drop = FALSE], response = "count")
round(head(predict(cf, week_test), 3), 2)
#> count
#> d001u00076 2.26
#> d001u00077 1.23
#> d001u00078 1.29maxent(), envelope(),
discriminant() and hierarchical() need a
presence-absence response and refuse any other head:
fit_learner(maxent(), week_train, counts[train, , drop = FALSE], response = "count")
#> Error:
#> ! maxnet fits a presence-absence response, under a head whose loss is the binary cross-entropy; this head's loss is poisson_deviance.register_response() adds a head. Its specification names
prepare, activation, loss,
metric and cells, and optionally
weights. The activation and the loss are each one of the
three the learners know: "sigmoid", "identity"
or "exp", and "binary_cross_entropy",
"squared_error" or "poisson_deviance". The
shipped presence-absence head weights each presence by the ratio of
absences to presences among the fitting units, capped at 50 by default,
through positive_weights(). The weight of a presence in
each response of this record:
w <- positive_weights(sim$y)
apply(w, 2L, max)
#> v01 v02 v03 v04
#> 2.260870 1.727273 2.750000 1.941176A head with a different cap reaches every learner through one
registration. With a cap of 2, the presences of v01 and
v03 weigh 2 and those of v02 and
v04 keep the weights above.
register_response("presence_absence_cap2", list(
prepare = function(y) {
y <- as.matrix(y)
storage.mode(y) <- "double"
if (!all(y %in% c(0, 1))) stop("a presence-absence response is 0/1.", call. = FALSE)
y
},
activation = "sigmoid",
loss = "binary_cross_entropy",
metric = "roc_auc",
cells = function(y, folds) scorable_cells(y, folds),
weights = function(y, fitting) positive_weights(y, cap = 2, fitting = fitting)
))A metric is a function of the observed values and the predictions of
one held-out cell. register_metric() adds one by name, and
timesift(metric = ) then chooses and reports by it. The
Brier score below has its sign reversed, so that a higher score is
better, as the choice of a candidate assumes of every metric.
register_metric("neg_brier", function(y, p) -mean((y - p)^2))
capped <- timesift(targets, sim$readings, y = starts_with("v"), id = unit, time = time,
learners = c(elasticnet(), forest()), sift = grains("month"),
response = "presence_absence_cap2", metric = "neg_brier",
resampling = cv(v = 5), n_inner = NULL, ensemble = FALSE, verbose = FALSE)
summary(capped)
#> timesift 150 targets, 4 responses, 5-fold random CV, neg_brier
#>
#> candidates, scored on the outer folds
#> candidate mean won responses
#> forest / month -0.259 0 separate
#> elasticnet / month -0.239 4 separate
#>
#> choice on every target elasticnet / monthThe run is chosen and reported by the Brier score under the capped weights. The elastic net, at -0.239, scored above the forest, at -0.259, and scored highest on all four responses.
Choosing learners
The cost of a learner depends on how many columns its representation flattens to, and on how much of its fit is an inner search. The timings below fit each tabular learner once on all 150 units and four responses at the weekly grain, which flattens to 53 columns.
timed <- list(elasticnet = elasticnet(), `elasticnet(tol = 1e-4)` = elasticnet(tol = 1e-4),
linear = linear(), tree = tree(), forest = forest(), boosting = boosting(),
xgboost = boosting(method = "xgboost"), maxent = maxent(),
envelope = envelope(), mars = mars(), discriminant = discriminant(),
perceptron = perceptron(), hierarchical = hierarchical())
seconds <- vapply(timed, function(l) {
system.time(fit_learner(l, week, sim$y))[["elapsed"]]
}, numeric(1))
sort(round(seconds, 2), decreasing = TRUE)
#> linear elasticnet forest
#> 10.42 3.96 0.47
#> maxent elasticnet(tol = 1e-4) discriminant
#> 0.40 0.31 0.17
#> xgboost perceptron tree
#> 0.08 0.07 0.03
#> boosting mars hierarchical
#> 0.03 0.02 0.01
#> envelope
#> 0.00additive() is not in that list, because at its default
k = 10 an additive model of 53 columns needs more
coefficients than there are units:
fit_learner(additive(), week, sim$y)
#> Error:
#> ! an additive model of 478 coefficients has more than the 150 units it is fitted to; fewer columns or a smaller `k` fit.At the monthly grain, 12 columns, it fits, and its cost depends on
k:
month <- grain_matrix(sim$readings, unit, time, reading, grain = "month")
additive_seconds <- c(
`k = 10` = system.time(fit_learner(additive(), month, sim$y))[["elapsed"]],
`k = 5` = system.time(fit_learner(additive(k = 5L), month, sim$y))[["elapsed"]])
round(additive_seconds, 2)
#> k = 10 k = 5
#> 21.94 0.55With k = 10 the fit took 21.9 seconds, and with
k = 5 it took 0.5 seconds.
The elastic net is the learner to start with. It reads every column
without a selection step, chooses its penalty inside the units it is
fitted on, and is the arm the package measures a network against. At its
default tolerance it took 4 seconds here, and tol = 1e-4
took 0.3, at the same held-out AUC on the split above.
linear() was the slowest at 10.4 seconds: its two-way
search refits the model for every term it could add or drop at every
step, and the weekly grain offers it 106 terms, each column and its
square. Pinned to a coarse grain, or searched forward under a bound as
linear(select = "forward", terms = "column", max_terms = 3)
does, the search has fewer steps to take. Every other tabular learner
took at most 0.47 seconds.
What a learner can be fitted on depends on the number of units against the number of columns its representation flattens to:
- The additive model holds one coefficient for the intercept and
k - 1for each column, and refuses a fit with more coefficients than units. On a small sample it needs a coarse grain or a smallk, and its cost grows withk, as the timings above show. -
linear(select = "none")fits every term without selecting, and over more columns than units it separates any response it is given. It is meant for a coarse grain such asdata = grain("season"). - The envelope keeps a unit inside only where every column agrees, so each added column leaves fewer presences inside. It too is meant for a coarse grain.
- The elastic net, the forest and boosting keep a wide block fittable on few units through the penalty, the column draw and the shrinkage, which makes them the first learners to run at a fine grain on a small sample.
-
maxent()builds a hinge per column per knot, so its design grows with the grain. A fit whose design would passmax_designgigabytes is refused with the size it would have taken, and a coarser grain or fewer classes brings it within the limit. -
perceptron()reads the columns in their own units unlessstandardise = TRUE, and a record in degrees saturates its hidden units sooner the wider its range.
The encoders fit every response through one network, so a rarer response borrows from the common ones. That pooling is what they are for, and it needs many responses and enough units to train a network on. In the Schrankogel study, 894 plots and 101 species read hourly for three years, a fully connected network on the series was level with a penalised logistic model on 188 hand-built features, and the gain came from convolution reading the series at a coarse grain. A study with few units and few responses may gain little from the encoders over the elastic net, and pays for the training.
A learner is the wrong choice where it cannot read the response or
the representation. maxent(), envelope(),
discriminant() and hierarchical() fit
presence-absence alone. A tabular learner cannot read
native(), and a sequence learner cannot read a
multigrain() block or a representation of one bin. A
learner can also be handed a reduction that has already discarded the
signal: channel_means above was fitted correctly and scored
below 0.5, because a yearly mean keeps nothing of a weekly departure. No
learner recovers what the representation dropped, which is why
timesift() compares learners across grains.