A T-solver is a forward selection algorithm on the enlarged design
\([\boldsymbol X, \boldsymbol D]\) of
predictors and dummies that stops as soon as T_stop dummies
are active. The T-Rex selectors run one inside every random experiment,
and every T-solver is available as a class of its own, for a single
terminated path, for a study of the solvers, or as a building block. The
terminated form of each algorithm follows the T-Rex paper [1], the
greedy family the terminating greedy pursuits [2].
| Family | Classes | Algorithm |
|---|---|---|
| LARS | TLARS_Solver, TLASSO_Solver,
TSTEPWISE_Solver |
least angle regression and its lasso and stepwise modifications [3], [4] |
| LARS | TSTAGEWISE_Solver |
forward stagewise [5], [6] |
| LARS | TENET_Solver, TENETAug_Solver |
the elastic net [7], Gram based and row augmented |
| LARS | TIENET_Solver, TIENETAug_Solver |
the informed elastic net [8], pathwise and row augmented |
| LARS | TIKNET_Solver |
the Tikhonov net, pathwise |
| OMP | TOMP_Solver, TOOLS_Solver |
orthogonal matching pursuit and orthogonal least squares [9]–[11] |
| OMP | TGP_Solver, TACGP_Solver |
gradient and approximate conjugate gradient pursuit [12] |
| OMP | TMP_Solver, TNCGMP_Solver |
matching pursuit [13] and non-negative constrained generalized matching pursuit [14] |
| AFS | TAFS_Solver |
adaptive forward stepwise [15] |
| CD | TCDLASSO_Solver, TCDENET_Solver,
TCDIENET_Solver, TCDTIKNET_Solver |
pathwise coordinate descent [16], [17] with exact minimizers at every dummy crossing |
The path solvers (LARS and CD families) walk a regularization path,
the greedy solvers (OMP and AFS families) add one variable per step. The
informed solvers take a groups vector, the Tikhonov solvers
a penalty matrix tikhonov_K.
set.seed(31)
n <- 100
p <- 50
L <- 50
true_support <- c(2, 9, 17, 33)
beta <- numeric(p)
beta[true_support] <- c(2, -2, 1.5, -1.5)
X <- matrix(rnorm(n * p), n, p)
D <- matrix(rnorm(n * L), n, L)
y <- as.numeric(X %*% beta + rnorm(n))A solver is constructed on X, D,
y and an options list, and execute_step() runs
the path until T_stop dummies are active. The path can be
continued by calling execute_step() again with a larger
T_stop.
solver <- TLARS_Solver$new(X, D, y)
solver$execute_step(T_stop = 3)
solver$get_num_steps()
#> [1] 11
solver$get_active_predictor_indices()
#> [1] 9 2 33 17 16 23 49 47
solver$get_active_dummy_indices()
#> [1] 2 3 10Indices are 1-based and the dummies continue after the predictors, so the active set of the enlarged design runs from 1 to \(p + L\).
solver$get_actives()
#> [1] 9 2 33 17 16 23 49 47
solver$get_dummy_start_index()
#> [1] 51
solver$get_actions()[1:4]
#> [[1]]
#> [1] 9
#>
#> [[2]]
#> [1] 2
#>
#> [[3]]
#> [1] 33
#>
#> [[4]]
#> [1] 17get_actions() records the path as signed codes per step,
+j for an addition and -j for a removal.
get_beta() is the coefficient vector of the last step and
get_beta_path() the whole path, one column per step with
the all-zero start.
dim(solver$get_beta_path())
#> [1] 100 12
round(solver$get_beta()[true_support], 3)
#> [1] 1.715 -1.872 1.079 -1.184
path_diag <- data.frame(lambda = solver$get_lambda(), rss = solver$get_rss(),
r2 = solver$get_r2(), cp = solver$get_cp(), dof = solver$get_dof())
round(head(path_diag, 5), 3)
#> lambda rss r2 cp dof
#> 1 23.844 1066.332 0.125 776.431 2
#> 2 20.403 558.071 0.542 362.592 3
#> 3 10.417 490.974 0.597 309.696 4
#> 4 9.238 133.327 0.891 19.083 5
#> 5 2.896 130.379 0.893 18.671 6
solver$restore() # give X and D back before the next solver claims themThe coefficient paths of a terminated run show what the solver does.
A predictor enters at a step and its coefficient grows from there, and a
dummy that enters marks one of the T_stop crossings that
end the path. One solver runs to T_stop = 1, keeps a
snapshot of its path, and continues to T_stop = 3, so the
second panel extends the first.
library(ggplot2)
snapshot <- function(solver, label) {
path <- solver$get_beta_path() # (p + L) x (steps + 1)
entering <- abs(vapply(solver$get_actions(), function(a) a[1], numeric(1)))
moved <- which(rowSums(path != 0) > 0)
kind <- ifelse(moved > p, "dummy", ifelse(moved %in% true_support, "true predictor", "other predictor"))
lines <- data.frame(panel = label, variable = rep(moved, each = ncol(path)),
step = rep(seq_len(ncol(path)) - 1, times = length(moved)),
coefficient = as.vector(t(path[moved, , drop = FALSE])),
kind = rep(kind, each = ncol(path)))
marks <- data.frame(panel = label, step = seq_along(entering), dummy = entering > p,
label = ifelse(entering > p, paste0("D", entering - p), entering))
list(lines = lines, marks = marks)
}
s <- TLARS_Solver$new(X, D, y)
s$execute_step(T_stop = 1)
first <- snapshot(s, "T_stop = 1")
s$execute_step(T_stop = 3)
second <- snapshot(s, "T_stop = 3, continued")
s$restore()
lines <- rbind(first$lines, second$lines)
marks <- rbind(first$marks, second$marks)
lines$panel <- factor(lines$panel, levels = c("T_stop = 1", "T_stop = 3, continued"))
marks$panel <- factor(marks$panel, levels = levels(lines$panel))
inclusions <- marks[marks$dummy, ]
inclusions$kind <- "dummy inclusion"
kinds <- c("true predictor", "other predictor", "dummy", "dummy inclusion")
ggplot(lines, aes(step, coefficient, group = variable, colour = kind, linetype = kind,
linewidth = kind)) +
geom_vline(data = inclusions, aes(xintercept = step, colour = kind, linetype = kind,
linewidth = kind), inherit.aes = FALSE) +
geom_line() +
geom_text(data = marks, aes(x = step, y = Inf, label = label), inherit.aes = FALSE,
vjust = 1.4, size = 2.4, colour = "grey30") +
facet_wrap(~ panel, scales = "free_x") +
scale_colour_manual(values = c("true predictor" = "#336C68", "other predictor" = "grey45",
"dummy" = "#D1495B", "dummy inclusion" = "#D1495B"),
breaks = kinds) +
scale_linetype_manual(values = c("true predictor" = "solid", "other predictor" = "solid",
"dummy" = "dashed", "dummy inclusion" = "dotted"),
breaks = kinds) +
scale_linewidth_manual(values = c("true predictor" = 0.9, "other predictor" = 0.5,
"dummy" = 0.6, "dummy inclusion" = 0.4), breaks = kinds) +
labs(x = "Step", y = "Coefficient", colour = NULL, linetype = NULL, linewidth = NULL) +
theme_bw(base_size = 11) +
theme(legend.position = "right", panel.grid.minor = element_blank(),
legend.background = element_rect(fill = "white", colour = "grey60"))The true predictors enter first and keep growing, further predictors follow with small coefficients, and the dummies end the path. The second panel repeats the first up to its last step and continues from there, because the continuation walks the stored path on. The label over each step names the variable that enters.
tsolver_options() holds every input except the data, the
same list for every solver. A solver reads the fields it needs and
ignores the rest.
| Field | Meaning |
|---|---|
normalize, intercept,
scaling_mode |
center and scale the columns of X and D in
place ("L2" or "ZSCORE") and center
y |
precentered |
the caller centered the data: the active set is capped at \(n - 1\) and the intercept counts in the degrees of freedom |
tol |
the numerical tolerance of the comparisons (machine epsilon by default) |
tie_seed |
the seed of the tie-break engine (0 by default, so exact ties resolve reproducibly) |
n_threads |
the thread budget of the solver’s own passes (1 serial, at least 1, and a pass splits only above about a million entries of the design) |
exch_tie_alpha, exch_tie_floor |
the exchangeable-tie band of the greedy solvers |
lambda2 |
the quadratic penalty of the elastic nets (0 by default) |
rho_afs, ncgmp_variant,
tenet_aug_use_lars |
solver-specific switches |
verbose |
print the steps |
With normalize = TRUE or intercept = TRUE
(the defaults) a solver is a writer. It centers and scales the columns
of X and D in place, holds the statistics, and
writes the original values back when restore() is called or
when the object is garbage collected. A second writer on a claimed
matrix is an error, because a second in-place transformation would move
the data by rounding. y is never written.
X_before <- X + 0
w <- TLASSO_Solver$new(X, D, y)
max(abs(X - X_before)) # the scaled columns
#> [1] 4.137846
w$restore()
max(abs(X - X_before)) # the original values
#> [1] 1.110223e-16A pipeline that runs several solvers on one design scales the data
once in front with a scaler and constructs readers, solvers with
normalize = FALSE and intercept = FALSE that
never write.
sx <- MLLpNormScaler$new(norm_type = 2, center = TRUE)
invisible(sx$fit_transform_inplace(X))
sd <- MLLpNormScaler$new(norm_type = 2, center = TRUE)
invisible(sd$fit_transform_inplace(D))
y_c <- y - mean(y)
reader_opts <- tsolver_options(normalize = FALSE, intercept = FALSE, precentered = TRUE)With the data pre-scaled, every family runs on the same design.
constructors <- list(
TLARS = TLARS_Solver, TLASSO = TLASSO_Solver, TSTEPWISE = TSTEPWISE_Solver,
TSTAGEWISE = TSTAGEWISE_Solver, TENET = TENET_Solver, TENETAug = TENETAug_Solver,
TOMP = TOMP_Solver, TOOLS = TOOLS_Solver, TGP = TGP_Solver, TACGP = TACGP_Solver,
TMP = TMP_Solver, TNCGMP = TNCGMP_Solver, TAFS = TAFS_Solver,
TCDLASSO = TCDLASSO_Solver, TCDENET = TCDENET_Solver)
rows <- lapply(names(constructors), function(nm) {
s <- constructors[[nm]]$new(X, D, y_c, options = reader_opts)
s$execute_step(T_stop = 3)
data.frame(solver = nm, steps = s$get_num_steps(),
predictors = paste(s$get_active_predictor_indices(), collapse = " "))
})
do.call(rbind, rows)
#> solver steps predictors
#> 1 TLARS 11 9 2 33 17 16 23 49 47
#> 2 TLASSO 11 9 2 33 17 16 23 49 47
#> 3 TSTEPWISE 7 9 2 33 17
#> 4 TSTAGEWISE 11 9 2 33 17 16 23 49 47
#> 5 TENET 11 9 2 33 17 16 23 49 47
#> 6 TENETAug 11 9 2 33 17 16 23 49 47
#> 7 TOMP 7 9 2 33 17
#> 8 TOOLS 7 9 2 33 17
#> 9 TGP 10 9 2 33 17 47 3
#> 10 TACGP 9 9 2 33 17 47 3
#> 11 TMP 9 9 2 33 17
#> 12 TNCGMP 9 9 2 33 17
#> 13 TAFS 7 9 2 33 17
#> 14 TCDLASSO 3 2 9 16 17 23 33 47 49
#> 15 TCDENET 3 2 9 16 17 23 33 47 49The informed solvers and the Tikhonov solvers take their structure as an extra argument.
groups <- rep(1:5, each = 10)
ien <- TIENET_Solver$new(X, D, y_c, groups = groups,
options = tsolver_options(normalize = FALSE, intercept = FALSE,
precentered = TRUE, lambda2 = 0.5))
ien$execute_step(T_stop = 3)
ien$get_active_predictor_indices()
#> [1] 9 2 33 17 16 23
tik <- TCDTIKNET_Solver$new(X, D, y_c, tikhonov_K = diag(p),
options = tsolver_options(normalize = FALSE, intercept = FALSE,
precentered = TRUE, lambda2 = 0.5))
tik$execute_step(T_stop = 3)
tik$get_active_predictor_indices()
#> [1] 2 9 16 17 19 25 29 33save() serializes the state of a solver to a file and
load() restores it into a solver constructed on the same
data in the same state. The loaded solver is a reader and continues the
path where it was saved.
f <- tempfile(fileext = ".bin")
a <- TLARS_Solver$new(X, D, y_c, options = reader_opts)
a$execute_step(T_stop = 2)
a$save(f)
b <- TLARS_Solver$new(X, D, y_c, options = reader_opts)
b$load(f)
b$get_num_steps()
#> [1] 8
b$execute_step(T_stop = 4)
c <- TLARS_Solver$new(X, D, y_c, options = reader_opts)
c$execute_step(T_stop = 4)
identical(b$get_actives(), c$get_actives())
#> [1] TRUE
unlink(f)get_memory_bytes() reports the heap bytes a solver holds
(its copy of y, the residual, the correlations, the
Cholesky factor, the coefficient path). X and
D are borrowed and not counted.
The TRexSelector_Examples repository holds runnable demos and Monte Carlo studies of every T-solver and five performance studies, with their results and a README per demo, in R, Python and C++.