
Use Cases for LazyMatrix: Statistical Algorithms
use-cases.RmdLinear Regression with LSQR
Introduction
Several algorithms for iterative least squares algorithms have been
proposed when working with sparse matrices. We have chosen to work with
the method proposed by Paige and Saunders
(1982) that is called LSQR. This iterative algorithm allows us to
use custom methods for matrix operations which allows for our lazy
computations. We will see that the operations %*%,
crossprod and norm are enough for getting
stable estimates of the regression coefficients.
Notation
The authors proposes an algorithm as a set of initalisation
parameters and the design matrix
.
As mentioned by the authors, the matrix
is used only to compute products of the form
and
.
This object will be our LazyMatrix objects and hence, all
computations will be done lazily at every step of the iteration. For
clarity, matrices will be denoted by capital letters
,
vectors by
and scalars by greek letters
.
For vectors, the norm is the Euclidean norm and for matrices we assume
the Frobenius norm
.
Mathematical Background
We will make use of the Lanczos process, for which we will not go into further detail. The main idea is that with the help of a set of initialization parameters, we can with the help of the design matrix update the regression coefficients for every step of the algorithm. The algorithm is explained as
Initialization:
-
For , repeat steps 3–6:
Continue the bidiagonalization:
Construct and apply the next orthogonal transformation:
Update and :
Reset the loop variables:
Check for convergence.
At this point, the authors do not propose a criterion for when we should stop iterating. However, we have chosen to use the criterion that LSQR in scipy (Virtanen et al. 2020; developers 2025) uses. First, compute the residual between the response vector and the estimate as
and then we stop iterating when
tol is a parameter for tolerance level of the difference
that we accept. In our algorithm, it is by default
.
Implementation
Assuming we want to fit regression coefficient vector
on the scaled matrix
with response vector
.
We can compute these estimates using lazy computation. Assuming the
original sparse matrix A, create an instance of
LazyMatrix and run the method
lazymatrix::lsqr.
library(lazymatrix)
#>
#> Attaching package: 'lazymatrix'
#> The following object is masked from 'package:base':
#>
#> norm
# 1. Define sparseMatrix
base::set.seed(123)
n_row <- 50
n_col <- 10
i <- c(
1:50,
base::sample(1:50, 20, replace = TRUE),
base::sample(1:50, 15, replace = TRUE)
)
j <- c(
base::rep_len(1:10, 50),
base::sample(1:10, 20, replace = TRUE),
base::sample(1:10, 15, replace = TRUE)
)
pairs <- base::unique(data.frame(i = i, j = j))
i <- pairs$i
j <- pairs$j
x <- stats::rnorm(length(i))
A <- Matrix::sparseMatrix(i = i, j = j, x = x, dims = c(n_row, n_col))
# 2. Define response vector
y <- stats::rnorm(nrow(A))
# 3. Define LazyMatrix
X <- LazyMatrix(A, "sd", "mean")
# 4. Perform lsqr
lazy_beta <- lazymatrix::lsqr(X, y)Now,the non-lazy approach would be to scale A using
base::scale()and then use something like
stats::lm.fit() to get the estimates. This would imply
creating a copy of A with non-zero elements taking up
unneccessary memory and being much slower generally.
The estimates are equivalent as is shown below.
Gradient Descent
Introduction
In statistical modeling, the classical gradient descent algorithm
remains a fundamental part in any kind of modern machine learning
approach. As an example of the power of lazymatrix, we
propose The approach we propose the simplest form of gradient descent on
a scaled matrix. As many of our implementations show, it is sufficient
to use a small set of matrix operations in order to perform this
algorithm.
Mathematical Background
The algorithm follows the approach as explained by Ng et al. (2025). Assume the cost function defined as
where , being the elements of the design matrix and the parameters. In gradient descent, we usually talk about two types of parameters, weights and biases. In the notation above, they are both included within the parameter vector . This is done by letting so that the first term in includes the additive bias term. is the response vector, so we recognize to be the cost function representing the residual sum of squares. Hence, the goal is to choose to minimize . We start with an initial guess of the parameter and repeatedly update it to make the cost function smaller. This is done with the gradient descent algorithm defined as
where the partial derivative is the derivative of the cost function with regards to the parameter. is the learning rate that usually takes a small value to control how fast the algorithm converges. Computing this derivative is straightforward and we get the expression
providing us with the update rule
Implementation
The following gradient descent algorithm works as helper function for which we will compare the lazy-approach to the non-lazy.
gradient_descent_helper <- function(
x,
y,
w_init,
b_init,
learning_rate,
n_epochs
) {
w <- w_init
b <- Matrix::Matrix(b_init, nrow = nrow(x))
n <- nrow(x)
for (epoch in 1:n_epochs) {
y_pred <- x %*% w + b
error <- y_pred - y
w <- w - (learning_rate * crossprod(x, error)) / n
b <- b - (learning_rate * sum(error)) / n
}
list(w = w, b = b)
}Compared to the definition above, in this algorithm we include the
bias term as b. This results in a list of the two parameter
vectors, weights and biases. n_epochs is the number of time
we choose to iterate. As the result of
LazyMatrix %*% vector results in a
Matrix-object, the addition of the bias term can be handled
completely within the Matrix framework. The key of
lazymatrix is to handle the heavy matrix operations lazily
which is done in this case using %*% and
crossprod. Now, we use the sparse matrix A
from section #1 and it’s LazyMatrix X and set
initial values randomly for the weights and biases and the response
vector y_true.
set.seed(4567)
p <- ncol(X)
w_init <- stats::rnorm(p)
b_init <- stats::rnorm(1)
y_true <- stats::rnorm(nrow(A))
learning_rate <- 0.01
n_epochs <- 10Note that the function returns the parameters from the iterative algorithm.
pars_lazy <- gradient_descent_helper(
x = X,
y = y_true,
w_init = w_init,
b_init = b_init,
learning_rate = learning_rate,
n_epochs = n_epochs
)
print("First 5 parameter weights and the bias term.")
#> [1] "First 5 parameter weights and the bias term."
print(pars_lazy$w[1:5])
#> [1] -0.6537508 -0.7836177 0.2716844 0.5295553 1.2169584
print(pars_lazy$b[1])
#> [1] 1.381793These parameters can now be used for predictions and are comparible
with the non-lazy way of using scale(A)and then running
gradient descent.
Discussion
Nonetheless there exists much more complex versions of the gradient
descent algorithm, this example rather proves that with a few matrix
operations being performed lazily, we can avoid storing heavy copies of
large matrices and perform operations solely when they are needed. The
idea is to implement methods to lazymatrix which allows for
more complex algorithms such as Stochastic gradient descent.
Principal Component Analysis
Introduction
A fundamental statistical algorithm for dimension reduction is
principal component analysis (PCA). In base R, the function used to
perform PCA is stats::prcomp(), which does so by breaking
up the matrix A into it’s singular value decomposition
(SVD) using base::svd(). This implies that we require a
lazy implementation of svd in order for principal component analysis to
work. Moreover, many methods have been proposed to perform svd on sparse
matrices and in lazymatrix we have chosen to follow the
iterative algorithm proposed by Baglama and
Reichel (2005), which the authors have also implemented in the R
package irlba. As this method uses iterative methods, we
can easily include lazymatrix’s methods for
%*% and crossprod in order to get a working
algorithm for svd. However, as the authors themselves say in the paper,
the irlba function will not compute all singular values and
proposes the user to use base::svd if one wishes to compute
all singular values. This would however involve materializing
A breaking the lazy computation in the process. Therefore,
we work solely with the algorithm proposed by irlba.
Mathematical Background
Partial Singular Value Decomposition with irlba
Assume the matrix
being the scaled version of A being
.
We wish to perform a partial singular value decomposition, where the
first step is using a Lanczos process which can be described as
Choose a random unit vector
Compute , transforming from to
Normalize the vector and let . This builds up the matrix as it’s first diagonal entry.
-
Compute the residual vector by first returning to with and let
-
Compute the length of the residual vector and let
-
Continue to build up with as
At the next iteration,
is used as input vector and we start over until we reach convergence
after
steps, resulting in the bidiagonal form of
.
At every point when matrix multiplication is being computed with
or
,
lazy computation through lazymatrix is being implemented.
Once we have performed every iteration in the Lanczos process, we can
stack the right basis vectors
into the matrix
and the left basis vectors
into
resulting in the equations
where we know that is bidiagonal and since , this is a smaller matrix and the decomposition can be written as
where we know that is bidiagonal and since , this is a smaller matrix and the decomposition can be written as
where
$$ \begin{align*} \Sigma_B&=\text{diag}(\sigma_1^B, \sigma_2^B,\ldots,\sigma_m^B)\newline U_B&\in R^{m\times m} \\ V_B&\in R^{m\times m}. \end{align*} $$
This decompoisition can be used to get approximate solutions to the svd of with the relationships
These equations can be validated easily by looking first at the left singular vectors
which implies that satisfies the left singular vector equation. Instead, the second equation , has a residual error if we look at
where it can be shown that
This error term is proportional to the last computation of and residual .
Now, we are not neccessarily interested in all
iterations, but it is sufficient to choose
as the number of triplets
for which we aim to compute. This is also a parameter in the
svd call for lazymatrix. What we do is after
iterations, we choose the
largest singular values
and their corresponding left and right vectors. We see whether these
have converged based on the criterion
and
and tol being a tolerance parameter. If all
have converged, the algorithm is done. If instead we do not have
convergence, we start the Lanczos process again using the subspace
spanned by the
right singular vectors
and from there we run another
runs of the process. This ensures that the previously computed singular
values
are carried over into the new
,
rather than restarting from scratch.
Principal Component Analysis with Partial SVD
As the regular stats::prcomp() function uses
base::svd() for dimension reduction, the equivalent method
for principal component analysis in lazymatrix is identical
but with the custom method using partial SVD with irlba.
Implementation
As before, we define a sparse matrix for which we will perform
singular value decomposition and thereafter principal component
analysis. We use the same sparse matrix from above (A) and
use it do define the lazy matrix X.
#3. SVD
svd_lazy <- svd(X)
# 4. Print singular values
print("The singular values are: ")
#> [1] "The singular values are: "
svd_lazy$d
#> [1] 9.104398 7.842210 7.653368 7.277682 6.978369
print("The first 6 rows of the left singular vectors are: ")
#> [1] "The first 6 rows of the left singular vectors are: "
head(svd_lazy$u)
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 0.01333517 -0.004771403 0.019689873 0.068220394 -0.173749037
#> [2,] 0.01350000 -0.003359592 0.006420480 0.001814066 0.011252310
#> [3,] -0.02927655 0.001950910 0.009877735 0.007683850 0.008493306
#> [4,] 0.21819519 -0.028828949 -0.100755039 0.111868855 0.071569385
#> [5,] 0.02331452 -0.149512123 0.051400116 0.289542383 0.169000315
#> [6,] -0.07461649 -0.073651823 -0.207556881 0.145466353 0.087293952
print("The frist 6 rows of the right singular vectors are: ")
#> [1] "The frist 6 rows of the right singular vectors are: "
head(svd_lazy$v)
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 0.0005177039 -0.02539160 0.08076583 0.36481720 -0.889025448
#> [2,] -0.0349727147 0.39383668 -0.21740956 -0.59873418 -0.308986839
#> [3,] -0.5573476051 0.18356325 0.19092437 0.01085593 0.083917203
#> [4,] 0.4681343605 -0.05644394 -0.20230136 0.21057716 0.110572231
#> [5,] 0.0512182125 -0.37439995 0.62396360 0.06067131 0.002115646
#> [6,] -0.2623446370 -0.18904462 -0.53246233 0.35541508 0.180506467This function has been incorporated within the S4 method for the
LazyMatrix object if we call
lazymatrix::prcomp(). Hence, working with the
LazyMatrix is equivalent to working with a regular matrix
object.
pca_lazy <- prcomp(X)
print("The first 6 rows of the principal components are: ")
#> [1] "The first 6 rows of the principal components are: "
head(pca_lazy$rotation)
#> PC1 PC2 PC3 PC4 PC5 PC6
#> [1,] 0.0005177039 -0.02539160 -0.08076583 -0.36481720 -0.889025448 -0.22840881
#> [2,] -0.0349727147 0.39383668 0.21740956 0.59873418 -0.308986839 -0.11636259
#> [3,] -0.5573476051 0.18356325 -0.19092437 -0.01085593 0.083917203 -0.03513912
#> [4,] 0.4681343605 -0.05644394 0.20230136 -0.21057716 0.110572231 -0.27175921
#> [5,] 0.0512182125 -0.37439995 -0.62396360 -0.06067131 0.002115646 0.16468595
#> [6,] -0.2623446370 -0.18904462 0.53246233 -0.35541508 0.180506467 -0.30508329
#> PC7 PC8 PC9
#> [1,] -0.05356839 0.07535334 0.01491230
#> [2,] 0.22989075 -0.51995123 -0.06941254
#> [3,] -0.23435083 -0.02269329 -0.51967863
#> [4,] -0.43806174 -0.35281652 -0.51250510
#> [5,] 0.08626780 -0.63852801 0.11599341
#> [6,] 0.15492293 -0.34646067 0.34329372Conclusion
What we encounter when using iterative methods for performing sparse
SVD, is that we will not compute all singular values as is the case when
calling base::svd(). One could argue that the method
should, if possible, compute all of them using a regular
base::svd() call. However, this breaks sparsity and the
lazy structure, materializing the matrix and hence loosing the lazy
computation. Since the use case for lazymatrix is mainly
for working with large sparse matrices, where computation of all
singular values may not even be feasible, we recommend not to use this
framework for small matrices where sparsity is not an issue and all
singular values may be computed using base::svd().