# Training non default regression models

#### Etienne Becht

#### June 2020

# Introduction

This vignette explains how to specify non-default machine learning frameworks and their hyperparameters when applying Infinity Flow. We will assume here that the basic usage of Infinity Flow has already been read, if you are not familiar with this material I suggest you first look at the basic usage vignette

This vignette will cover:

1. Loading the example data
2. Note on package design
3. The `regression_functions` argument
4. The `extra_args_regression_params` argument
5. Neural networks

# Loading the example data

Here is a single R code chunk that recapitulates all of the data preparation covered in the basic usage vignette.

    if(!require(devtools)){
        install.packages("devtools")
    }
    if(!require(infinityFlow)){
        library(devtools)
        install_github("ebecht/infinityFlow")
    }

    library(infinityFlow)

    data(steady_state_lung)
    data(steady_state_lung_annotation)
    data(steady_state_lung_backbone_specification)

    dir <- file.path(tempdir(), "infinity_flow_example")
    input_dir <- file.path(dir, "fcs")
    write.flowSet(steady_state_lung, outdir = input_dir)
    #> [1] "/tmp/RtmpYcaYiC/infinity_flow_example/fcs"

    write.csv(steady_state_lung_backbone_specification, file = file.path(dir, "backbone_selection_file.csv"), row.names = FALSE)

    path_to_fcs <- file.path(dir, "fcs")
    path_to_output <- file.path(dir, "output")
    path_to_intermediary_results <- file.path(dir, "tmp")
    backbone_selection_file <- file.path(dir, "backbone_selection_file.csv")

    targets <- steady_state_lung_annotation$Infinity_target
    names(targets) <- rownames(steady_state_lung_annotation)
    isotypes <- steady_state_lung_annotation$Infinity_isotype
    names(isotypes) <- rownames(steady_state_lung_annotation)

    input_events_downsampling <- 1000
    prediction_events_downsampling <- 500
    cores = 1L

# Note on package design

The `infinity_flow()` function which encapsulates the complete Infinity Flow computational pipeline uses two arguments to respectively select regression models and their hyperparameters. These two arguments are both lists, and should have the same length. The idea is that the first list, `regression_functions` will be a list of model templates (XGBoost, Neural Networks, SVMs…) to train, while the second will be used to specify their hyperparameters. The list of templates is then fit to the data using parallel computing with socketing (using the `parallel` package through the `pbapply` package), which is more memory efficient.

# The `regression_functions` argument

This argument is a list of functions which specifies how many models to train per well and which ones. Each type of machine learning model is supported through a wrapper in the infinityFlow package, and has a name of the form `fitter_*`. See below for the complete list:

    print(grep("fitter_", ls("package:infinityFlow"), value = TRUE))
    #> [1] "fitter_glmnet"  "fitter_linear"  "fitter_nn"      "fitter_svm"
    #> [5] "fitter_xgboost"

fitter_ function | Backend | Model type
--- | --- | ---
fitter_xgboost | XGBoost | Gradient boosted trees
fitter_nn | Tensorflow/Keras | Neural networks
fitter_svm | e1071 | Support vector machines
fitter_glmnet | glmnet | Generalized linear and polynomial models
fitter_lm | stats | Linear and polynomial models

These functions rely on optional package dependencies (so that you do not need to install e.g. Keras if you are not planning to use it). We need to make sure that these dependencies are however met:

           optional_dependencies <- c("glmnetUtils", "e1071")
           unmet_dependencies <- setdiff(optional_dependencies, rownames(installed.packages()))
           if(length(unmet_dependencies) > 0){
               install.packages(unmet_dependencies)
           }
           for(pkg in optional_dependencies){
               library(pkg, character.only = TRUE)
           }

In this vignette we will train all of these models. Note that if you do it on your own data, it make take quite a bit of memory (remember that the output expression matrix will be a numeric matrix of size `(prediction_events_downsampling x number of wells) rows x (number of wells x number of models)`.

To train multiple models we create a list of these fitter_* functions and assign this to the `regression_functions` argument that will be fed to the `infinity_flow` function. The names of this list will be used to name your models.

    regression_functions <- list(
        XGBoost = fitter_xgboost, # XGBoost
        SVM = fitter_svm, # SVM
        LASSO2 = fitter_glmnet, # L1-penalized 2nd degree polynomial model
        LM = fitter_linear # Linear model
    )

# The `extra_args_regression_params` argument

This argument is a list of list (so of the form `list(list(...), list(...), etc.)`) of length `length(regression_functions)`. Each element of the extra_args_regression_params object is thus a list. This lower-level list will be used to pass named arguments to the machine learning fitting function. The list of `extra_args_regression_params` is matched with the list of machine learning models `regression_functions` using the order of the elements in these two lists (e.g. the first regression model is matched with the first element of the list of arguments, then the seconds elements are matched together, etc…).

    backbone_size <- table(read.csv(backbone_selection_file)[,"type"])["backbone"]
    extra_args_regression_params <- list(
         ## Passed to the first element of `regression_functions`, e.g. XGBoost. See ?xgboost for which parameters can be passed through this list
        list(nrounds = 500, eta = 0.05),

        # ## Passed to the second element of `regression_functions`, e.g. neural networks through keras::fit. See https://keras.rstudio.com/articles/tutorial_basic_regression.html
        # list(
        #         object = { ## Specifies the network's architecture, loss function and optimization method
        #             model = keras_model_sequential()
        #             model %>%
        #                 layer_dense(units = backbone_size, activation = "relu", input_shape = backbone_size) %>%
        #                 layer_dense(units = backbone_size, activation = "relu", input_shape = backbone_size) %>%
        #                 layer_dense(units = 1, activation = "linear")
        #             model %>%
        #                 compile(loss = "mean_squared_error", optimizer = optimizer_sgd(lr = 0.005))
        #             serialize_model(model)
        #         },
        #         epochs = 1000, ## Number of maximum training epochs. The training is however stopped early if the loss on the validation set does not improve for 20 epochs. This early stopping is hardcoded in fitter_nn.
        #         validation_split = 0.2, ## Fraction of the training data used to monitor validation loss
        #         verbose = 0,
        #         batch_size = 128 ## Size of the minibatches for training.
        # ),

        # Passed to the third element, SVMs. See help(svm, "e1071") for possible arguments
        list(type = "nu-regression", cost = 8, nu=0.5, kernel="radial"),

        # Passed to the fourth element, fitter_glmnet. This should contain a mandatory argument `degree` which specifies the degree of the polynomial model (1 for linear, 2 for quadratic etc...). Here we use degree = 2 corresponding to our LASSO2 model Other arguments are passed to getS3method("cv.glmnet", "formula"),
        list(alpha = 1, nfolds=10, degree = 2),

        # Passed to the fourth element, fitter_linear. This only accepts a degree argument specifying the degree of the polynomial model. Here we use degree = 1 corresponding to a linear model.
        list(degree = 1)
    )

We can now run the pipeline with these custom arguments to train all the models.

    if(length(regression_functions) != length(extra_args_regression_params)){
        stop("Number of models and number of lists of hyperparameters mismatch")
    }
    imputed_data <- infinity_flow(
        regression_functions = regression_functions,
        extra_args_regression_params = extra_args_regression_params,
        path_to_fcs = path_to_fcs,
        path_to_output = path_to_output,
        path_to_intermediary_results = path_to_intermediary_results,
        backbone_selection_file = backbone_selection_file,
        annotation = targets,
        isotype = isotypes,
        input_events_downsampling = input_events_downsampling,
        prediction_events_downsampling = prediction_events_downsampling,
        verbose = TRUE,
        cores = cores
    )
    #> Using directories...
    #>  input: /tmp/RtmpYcaYiC/infinity_flow_example/fcs
    #>  intermediary: /tmp/RtmpYcaYiC/infinity_flow_example/tmp
    #>  subset: /tmp/RtmpYcaYiC/infinity_flow_example/tmp/subsetted_fcs
    #>  rds: /tmp/RtmpYcaYiC/infinity_flow_example/tmp/rds
    #>  annotation: /tmp/RtmpYcaYiC/infinity_flow_example/tmp/annotation.csv
    #>  output: /tmp/RtmpYcaYiC/infinity_flow_example/output
    #> Parsing and subsampling input data
    #>  Downsampling to 1000 events per input file
    #>  Concatenating expression matrices
    #>  Writing to disk
    #> Logicle-transforming the data
    #>  Backbone data
    #>  Exploratory data
    #>  Writing to disk
    #>  Transforming expression matrix
    #>  Writing to disk
    #> Harmonizing backbone data
    #>  Scaling expression matrices
    #>  Writing to disk
    #> Fitting regression models
    #>  Randomly selecting 50% of the subsetted input files to fit models
    #>  Fitting...
    #>      XGBoost
    #>
    #>  11.73122 seconds
    #>      SVM
    #>
    #>  0.9072952 seconds
    #>      LASSO2
    #>
    #>  7.502817 seconds
    #>      LM
    #>
    #>  0.07756186 seconds
    #> Imputing missing measurements
    #>  Randomly drawing events to predict from the test set
    #>  Imputing...
    #>      XGBoost
    #>
    #>  0.9056804 seconds
    #>      SVM
    #>
    #>  0.9425871 seconds
    #>      LASSO2
    #>
    #>  1.19878 seconds
    #>      LM
    #>
    #>  0.04382563 seconds
    #>  Concatenating predictions
    #>  Writing to disk
    #> Performing dimensionality reduction
    #> 17:51:41 UMAP embedding parameters a = 1.262 b = 1.003
    #> 17:51:41 Read 5000 rows and found 17 numeric columns
    #> 17:51:41 Using Annoy for neighbor search, n_neighbors = 15
    #> 17:51:41 Building Annoy index with metric = euclidean, n_trees = 50
    #> 0%   10   20   30   40   50   60   70   80   90   100%
    #> [----|----|----|----|----|----|----|----|----|----|
    #> **************************************************|
    #> 17:51:42 Writing NN index file to temp file /tmp/RtmpYcaYiC/file1c9aea10f29fb5
    #> 17:51:42 Searching Annoy index using 1 thread, search_k = 1500
    #> 17:51:43 Annoy recall = 100%
    #> 17:51:43 Commencing smooth kNN distance calibration using 1 thread with target n_neighbors = 15
    #> 17:51:44 Initializing from normalized Laplacian + noise (using irlba)
    #> 17:51:44 Commencing optimization for 1000 epochs, with 102092 positive edges using 1 thread
    #> 17:52:00 Optimization finished
    #> Exporting results
    #>  Transforming predictions back to a linear scale
    #>  Exporting FCS files (1 per well)
    #> Plotting
    #>  Chopping off the top and bottom 0.005 quantiles
    #>  Shuffling the order of cells (rows)
    #>  Producing plot
    #> Background correcting
    #>  Transforming background-corrected predictions. (Use logarithm to visualize)
    #>  Exporting FCS files (1 per well)
    #> Plotting
    #>  Chopping off the top and bottom 0.005 quantiles
    #>  Shuffling the order of cells (rows)
    #>  Producing plot

Our model names are appended to the predicted markers in the output. For more discussion about the outputs (including output files written to disk and plots), see the basic usage vignette

       print(imputed_data$bgc[1:2, ])
    #>      FSC-A      FSC-H      FSC-W   SSC-A      SSC-H      SSC-W CD69-CD301b
    #> 2 41378.61 -0.6566746 -0.8820758 2121.94 -0.9589057 -1.4518346   0.8732715
    #> 4 43608.00 -0.7837915  0.1891402 3692.46 -0.2470512 -0.3560733   1.0538001
    #>       Zombie      MHCII         CD4        CD44        CD8      CD11c
    #> 2 -101.63795  0.6099976  0.08146431 0.008106949 -0.2739487 -0.6413340
    #> 4  -99.66527 -0.9704427 -0.95104767 0.090037378 -0.4566827  0.2052232
    #>        CD11b        F480       Ly6C     Lineage    CD45a488 FJComp-PE(yg)-A
    #> 2 -0.6270077 -0.78911193 -1.4483736 -0.03390842  0.08994746       0.3295537
    #> 4  0.9194257 -0.04682779 -0.5376685 -1.03319473 -0.55376185       1.3290365
    #>         CD24      CD103     Time CD137.LASSO2_bgc CD137.LM_bgc CD137.SVM_bgc
    #> 2  0.8657763 0.00111465 2405.506      -0.04792694  -0.03124259     0.2152218
    #> 4 -0.6889606 0.10049367 2466.706       0.18986985   0.05245893     0.4663906
    #>   CD137.XGBoost_bgc CD28.LASSO2_bgc CD28.LM_bgc CD28.SVM_bgc CD28.XGBoost_bgc
    #> 2       -0.05588577      0.08452474 -0.04123823   -0.0970863      -0.08942195
    #> 4        0.04605989      0.02893708  0.10207903    0.2371528       0.07900966
    #>   CD49b(pan-NK).LASSO2_bgc CD49b(pan-NK).LM_bgc CD49b(pan-NK).SVM_bgc
    #> 2               -0.2016910           -0.1103609            -0.5279791
    #> 4                0.4479062            0.2907326             1.4334856
    #>   CD49b(pan-NK).XGBoost_bgc KLRG1.LASSO2_bgc KLRG1.LM_bgc KLRG1.SVM_bgc
    #> 2                -0.3584650       0.04220486   -0.1457060    -0.1047714
    #> 4                 0.4486807       0.16660230    0.1453845     0.4173929
    #>   KLRG1.XGBoost_bgc Ly-49c/F/I/H.LASSO2_bgc Ly-49c/F/I/H.LM_bgc
    #> 2        0.14649852               0.0138331          0.03089164
    #> 4       -0.02661375               0.1704880          0.18979252
    #>   Ly-49c/F/I/H.SVM_bgc Ly-49c/F/I/H.XGBoost_bgc Podoplanin.LASSO2_bgc
    #> 2           -0.6350853               -0.2001596           -0.05479615
    #> 4            1.1595562                0.4748323            0.18629325
    #>   Podoplanin.LM_bgc Podoplanin.SVM_bgc Podoplanin.XGBoost_bgc SHIgG.LASSO2_bgc
    #> 2       -0.08341477        -0.09375937             -0.2411681    -1.085268e-15
    #> 4        0.28912138         0.56124412              0.6846517    -4.572309e-16
    #>   SHIgG.LM_bgc SHIgG.SVM_bgc SHIgG.XGBoost_bgc SSEA-3.LASSO2_bgc SSEA-3.LM_bgc
    #> 2 3.023187e-15 -3.816235e-16     -1.818714e-15         0.1047338   0.009341444
    #> 4 9.820668e-16 -7.545205e-16     -3.310301e-15        -0.2110692  -0.180610666
    #>   SSEA-3.SVM_bgc SSEA-3.XGBoost_bgc TCR Vg3.LASSO2_bgc TCR Vg3.LM_bgc
    #> 2     0.06362389          0.1734343         0.35347736     0.19322552
    #> 4    -0.21413587         -0.2501127         0.09005298     0.06586005
    #>   TCR Vg3.SVM_bgc TCR Vg3.XGBoost_bgc rIgM.LASSO2_bgc   rIgM.LM_bgc
    #> 2       0.1247000           0.2477775   -4.492286e-16  9.362609e-16
    #> 4       0.8566089           0.9606679   -2.922194e-16 -3.198131e-16
    #>    rIgM.SVM_bgc rIgM.XGBoost_bgc    UMAP1    UMAP2 PE_id
    #> 2 -1.272078e-16    -4.354317e-16 901.7764 224.9627     1
    #> 4 -1.272078e-16     1.926053e-16 656.1270 537.8730     1

# Neural networks

Neural networks won’t build in knitr for me but here is an example of the syntax if you want to use them.

Note: there is an issue with serialization of the neural networks and socketing since I updated to R-4.0.1. If you want to use neural networks, please make sure to set

    cores = 1L

    optional_dependencies <- c("keras", "tensorflow")
    unmet_dependencies <- setdiff(optional_dependencies, rownames(installed.packages()))
    if(length(unmet_dependencies) > 0){
         install.packages(unmet_dependencies)
    }
    for(pkg in optional_dependencies){
        library(pkg, character.only = TRUE)
    }

    invisible(eval(try(keras_model_sequential()))) ## avoids conflicts with flowCore...

    if(!is_keras_available()){
         install_keras() ## Instal keras unsing the R interface - can take a while
    }

    if (!requireNamespace("BiocManager", quietly = TRUE)){
        install.packages("BiocManager")
    }
    BiocManager::install("infinityFlow")

    library(infinityFlow)

    data(steady_state_lung)
    data(steady_state_lung_annotation)
    data(steady_state_lung_backbone_specification)

    dir <- file.path(tempdir(), "infinity_flow_example")
    input_dir <- file.path(dir, "fcs")
    write.flowSet(steady_state_lung, outdir = input_dir)

    write.csv(steady_state_lung_backbone_specification, file = file.path(dir, "backbone_selection_file.csv"), row.names = FALSE)

    path_to_fcs <- file.path(dir, "fcs")
    path_to_output <- file.path(dir, "output")
    path_to_intermediary_results <- file.path(dir, "tmp")
    backbone_selection_file <- file.path(dir, "backbone_selection_file.csv")

    targets <- steady_state_lung_annotation$Infinity_target
    names(targets) <- rownames(steady_state_lung_annotation)
    isotypes <- steady_state_lung_annotation$Infinity_isotype
    names(isotypes) <- rownames(steady_state_lung_annotation)

    input_events_downsampling <- 1000
    prediction_events_downsampling <- 500

    ## Passed to fitter_nn, e.g. neural networks through keras::fit. See https://keras.rstudio.com/articles/tutorial_basic_regression.html
    regression_functions <- list(NN = fitter_nn)

    backbone_size <- table(read.csv(backbone_selection_file)[,"type"])["backbone"]
    extra_args_regression_params <- list(
            list(
            object = { ## Specifies the network's architecture, loss function and optimization method
            model = keras_model_sequential()
            model %>%
            layer_dense(units = backbone_size, activation = "relu", input_shape = backbone_size) %>%
            layer_dense(units = backbone_size, activation = "relu", input_shape = backbone_size) %>%
            layer_dense(units = 1, activation = "linear")
            model %>%
            compile(loss = "mean_squared_error", optimizer = optimizer_sgd(lr = 0.005))
            serialize_model(model)
            },
            epochs = 1000, ## Number of maximum training epochs. The training is however stopped early if the loss on the validation set does not improve for 20 epochs. This early stopping is hardcoded in fitter_nn.
            validation_split = 0.2, ## Fraction of the training data used to monitor validation loss
            verbose = 0,
            batch_size = 128 ## Size of the minibatches for training.
        )
    )

    imputed_data <- infinity_flow(
        regression_functions = regression_functions,
        extra_args_regression_params = extra_args_regression_params,
        path_to_fcs = path_to_fcs,
        path_to_output = path_to_output,
        path_to_intermediary_results = path_to_intermediary_results,
        backbone_selection_file = backbone_selection_file,
        annotation = targets,
        isotype = isotypes,
        input_events_downsampling = input_events_downsampling,
        prediction_events_downsampling = prediction_events_downsampling,
        verbose = TRUE,
        cores = 1L
    )

# Conclusion

Thank you for following this vignette, I hope you made it through the end without too much headache and that it was informative. General questions about proper usage of the package are best asked on the Bioconductor support site to maximize visibility for future users. If you encounter bugs, feel free to raise an issue on infinityFlow’s github.

# Information about the R session when this vignette was built

    sessionInfo()
    #> R version 4.2.1 (2022-06-23)
    #> Platform: x86_64-pc-linux-gnu (64-bit)
    #> Running under: Ubuntu 20.04.5 LTS
    #>
    #> Matrix products: default
    #> BLAS:   /home/biocbuild/bbs-3.16-bioc/R/lib/libRblas.so
    #> LAPACK: /home/biocbuild/bbs-3.16-bioc/R/lib/libRlapack.so
    #>
    #> Random number generation:
    #>  RNG:     L'Ecuyer-CMRG
    #>  Normal:  Inversion
    #>  Sample:  Rejection
    #>
    #> locale:
    #>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C
    #>  [3] LC_TIME=en_GB              LC_COLLATE=C
    #>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8
    #>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C
    #>  [9] LC_ADDRESS=C               LC_TELEPHONE=C
    #> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
    #>
    #> attached base packages:
    #> [1] stats     graphics  grDevices utils     datasets  methods   base
    #>
    #> other attached packages:
    #> [1] e1071_1.7-12       glmnetUtils_1.1.8  infinityFlow_1.8.0 flowCore_2.10.0
    #>
    #> loaded via a namespace (and not attached):
    #>  [1] gtools_3.9.3        shape_1.4.6         terra_1.6-17
    #>  [4] xfun_0.34           bslib_0.4.0         pbapply_1.5-0
    #>  [7] splines_4.2.1       lattice_0.20-45     generics_0.1.3
    #> [10] htmltools_0.5.3     stats4_4.2.1        yaml_2.3.6
    #> [13] survival_3.4-0      rlang_1.0.6         jquerylib_0.1.4
    #> [16] uwot_0.1.14         BiocGenerics_0.44.0 sp_1.5-0
    #> [19] xgboost_1.6.0.1     matrixStats_0.62.0  foreach_1.5.2
    #> [22] stringr_1.4.1       RProtoBufLib_2.10.0 cytolib_2.10.0
    #> [25] raster_3.6-3        codetools_0.2-18    evaluate_0.17
    #> [28] Biobase_2.58.0      knitr_1.40          fastmap_1.1.0
    #> [31] class_7.3-20        irlba_2.3.5.1       parallel_4.2.1
    #> [34] Rcpp_1.0.9          matlab_1.0.4        cachem_1.0.6
    #> [37] S4Vectors_0.36.0    jsonlite_1.8.3      png_0.1-7
    #> [40] digest_0.6.30       stringi_1.7.8       grid_4.2.1
    #> [43] cli_3.4.1           tools_4.2.1         magrittr_2.0.3
    #> [46] RcppAnnoy_0.0.20    sass_0.4.2          proxy_0.4-27
    #> [49] glmnet_4.1-4        Matrix_1.5-1        data.table_1.14.4
    #> [52] rmarkdown_2.17      iterators_1.0.14    R6_2.5.1
    #> [55] compiler_4.2.1