---
title: "The ML methods"
output:
  rmarkdown::html_vignette:
    toc: true
    css: vignette_styles.css
    includes:
      before_body: vignette_nav.html
bibliography: references.bib
csl: ieee.csl
link-citations: true
vignette: >
  %\VignetteIndexEntry{The ML methods}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.align = "center",
                      fig.width = 6, fig.height = 4, fig.retina = 2)
options(width = 80)
library(TRexSelector)
old_threads <- trex_set_num_threads(min(2L, trex_get_max_threads()))
```

# The building blocks

The machine learning (ML) methods are the numerical building blocks the selectors use: scalers, ridge
and elastic-net regression with cross-validation (the $\lambda_2$ tuners), the singular value
decomposition (SVD) and principal component analysis (PCA) behind the sparse PCA selector, and the
hierarchical clustering behind the dependency-aware and the grouped selectors. They are exposed as classes
and functions of their own, built on Eigen [@guennebaud2010eigen].

```{r data}
set.seed(8)
n <- 120
p <- 30
X <- matrix(rnorm(n * p), n, p)
beta <- c(2, -1.5, 0, 0, 1, rep(0, p - 5))
y <- as.numeric(X %*% beta + rnorm(n))
```

# Scalers

`MLZScoreScaler` centers and divides by the sample standard deviation, `MLLpNormScaler` centers and
divides by the L1 or L2 norm. Both work in place on a double matrix (or a memory-mapped one) and can undo
their transformation.

```{r scalers}
z <- MLZScoreScaler$new()
z$fit(X)
round(z$get_centers()[1:3], 3)
round(z$get_scales()[1:3], 3)
X_before <- X + 0
invisible(z$transform_inplace(X))
round(colMeans(X)[1:3], 12)
round(apply(X, 2, sd)[1:3], 12)
invisible(z$inverse_transform_inplace(X))
max(abs(X - X_before))
```

`fit_transform_inplace()` does both in one call, `get_dropped_indices()` names constant columns, and
`save()` / `load()` persist the statistics.

```{r lp}
l2 <- MLLpNormScaler$new(norm_type = 2, center = TRUE)
invisible(l2$fit_transform_inplace(X))
sqrt(colSums(X^2))[1:3]
invisible(l2$inverse_transform_inplace(X))
```

# Ridge regression

`MLRidge` solves $\min_{\boldsymbol\beta} \|\boldsymbol y - \boldsymbol X \boldsymbol\beta\|_2^2 +
\lambda \|\boldsymbol\beta\|_2^2$ for one penalty [@hoerl2000ridge], by the primal or the dual Cholesky
path depending on the shape of `X`. `MLRidgeCV` chooses the penalty by $K$-fold cross-validation over a
geometric grid and reports the minimum and the one-standard-error rule.

```{r ridge}
ridge <- MLRidge$new()
round(ridge$solve(X, y, lambda = 1)[1:5], 3)
cv <- MLRidgeCV$new()
cv$fit(X, y, n_folds = 5, n_lambda = 50)
c(lambda_min = cv$cv_min(), lambda_1se = cv$cv_1se())
```

```{r ridge-plot}
#| fig.cap: "Cross-validation error of the ridge penalty with the minimum and the one-standard-error choice."
library(ggplot2)
curve <- data.frame(lambda = cv$get_lambdas(), error = cv$get_cv_errors())
choice <- data.frame(lambda = c(cv$cv_min(), cv$cv_1se()), rule = c("minimum", "one standard error"))
ggplot(curve, aes(lambda, error)) +
  geom_line(aes(colour = "CV error, 5 folds"), linewidth = 0.9) +
  geom_vline(data = choice, aes(xintercept = lambda, linetype = rule), colour = "#D1495B") +
  scale_x_log10() +
  scale_colour_manual(values = c("CV error, 5 folds" = "#336C68")) +
  labs(x = "Lambda", y = "CV error", colour = NULL, linetype = NULL) +
  theme_bw(base_size = 11) +
  theme(legend.position = "inside", legend.position.inside = c(0.02, 0.98),
        legend.justification = c(0, 1), legend.background = element_blank(),
        legend.box.background = element_rect(fill = "white", colour = "grey60"),
        legend.box.margin = margin(4, 6, 4, 6))
```

`MLRidgeCV$new(low_memory = TRUE)` runs the dual (kernel) form on column blocks of `X` read in place,
with memory $O(n^2)$ instead of $O(np)$, the route for $p \gg n$.

# The elastic net

`MLElasticNet` fits the elastic-net path [@zou2005elasticnet] by coordinate descent over a glmnet-style
grid [@friedman2010glmnet]. `alpha = 1` is the lasso, `alpha = 0` the ridge. The coefficients are
reported on the original scale.

```{r enet}
en <- MLElasticNet$new()
en$fit(X, y, alpha = 0.5, n_lambda = 50)
dim(en$get_coef())
length(en$get_lambdas())
en$converged()
B <- en$get_coef()               # the path can stop before n_lambda points
round(B[1:5, ncol(B)], 3)
round(en$predict(X[1:3, ])[, ncol(B)], 3)
```

`MLElasticNetCV` selects the penalty by cross-validation on a shared grid. The strong rules
[@tibshirani2012strong] speed up the sweeps.

```{r enet-cv}
encv <- MLElasticNetCV$new()
encv$fit(X, y, alpha = 0.5, n_folds = 5, n_lambda = 50)
c(lambda_min = encv$cv_min(), lambda_1se = encv$cv_1se())
```

Both classes accept `low_memory = TRUE` and then run on `X` itself, a memory-mapped matrix included,
without an allocation of the size of `X`.

# SVD and PCA

`MLSVD$new()$compute(X, M)` returns the top-$M$ truncated SVD, by a direct, a Gram or a randomized
path depending on the shape of `X`.

```{r svd}
svd_res <- MLSVD$new()$compute(X, M = 3)
names(svd_res)
dim(svd_res$U)
round(svd_res$S, 3)
```

`MLPCA` fits the top-$M$ components. It centers and normalizes `X` in place at construction, the same
rule as the T-solvers, and `restore()` undoes it.

```{r pca}
X_before <- X + 0
pca <- MLPCA$new(X, M = 3, center = TRUE, normalize = TRUE)
pca$fit()
dim(pca$get_scores())
dim(pca$get_loadings())
round(pca$get_explained_variance(), 3)
round(pca$transform(X_before[1:2, ]), 3)
pca$restore()
max(abs(X - X_before))
```

# Hierarchical clustering

`ml_agglomerative_cluster()` runs agglomerative clustering on the rows of a matrix with a linkage from
`ml_linkage_method` (Ward, average, complete, single, WPGMA, median, centroid) and a metric from
`ml_distance_metric` (Euclidean, correlation, Manhattan, and two locality-sensitive hashing (LSH)
variants of the correlation distance for very wide data). Single linkage runs the SLINK algorithm
[@sibson1973slink], the other linkages the nearest-neighbour chain [@benzecri1982nnchain;
@murtagh2012overview] with the Lance-Williams update [@lance1967linkage]. The result is a linkage
matrix, and `ml_cut_tree()` cuts it into a given number of clusters.

```{r hac}
set.seed(9)
centers <- rep(c(-3, 0, 3), each = 20)
Z <- centers + matrix(rnorm(60 * 4), 60, 4)
linkage <- ml_agglomerative_cluster(Z, method = ml_linkage_method$Ward,
                                    metric = ml_distance_metric$Euclidean)
dim(linkage)
labels <- ml_cut_tree(linkage, num_orig_objs = 60, num_clusters = 3)
table(labels, rep(1:3, each = 20))
```

To cluster variables instead of observations, cluster the transposed design with the correlation
distance, which is what the grouped and the dependency-aware selectors do.

```{r hac-vars}
link_vars <- ml_agglomerative_cluster(t(X), method = ml_linkage_method$Single,
                                      metric = ml_distance_metric$Correlation)
head(ml_cut_tree(link_vars, num_orig_objs = p, num_clusters = 5))
```

`use_mmap = TRUE` keeps the intermediate distances in memory-mapped files, and a memory-mapped `data`
matrix is clustered in place.

# Examples

The [TRexSelector_Examples](https://github.com/trexselector/TRexSelector_Examples/tree/main)
repository holds runnable demos and Monte Carlo studies of the scalers, ridge and elastic-net
cross-validation, PCA, SVD and clustering, with their results and a README per demo, in
[R](https://github.com/trexselector/TRexSelector_Examples/tree/main/R/ml_methods),
[Python](https://github.com/trexselector/TRexSelector_Examples/tree/main/Python/ml_methods) and
[C++](https://github.com/trexselector/TRexSelector_Examples/tree/main/cpp/ml_methods).

```{r teardown, include = FALSE}
trex_set_num_threads(old_threads)
```

# Bibliography
