An Introduction To RcppEigen
An Introduction To RcppEigen
net/publication/265085117
An Introduction to RcppEigen
Article
CITATIONS READS
0 754
1 author:
Douglas M. Bates
University of Wisconsin–Madison
136 PUBLICATIONS 57,814 CITATIONS
SEE PROFILE
Some of the authors of this publication are also working on these related projects:
Matrix: R package for classed matrix computations, notably sparse decompositions View project
All content following this page was uploaded by Douglas M. Bates on 16 November 2014.
Abstract
The RcppEigen package provides access from R to the Eigen C++ template library for numerical linear
algebra. Rcpp (Eddelbuettel and François, 2011b) classes and specializations of the C++ templated
functions as and wrap from Rcpp provide the “glue” for passing objects from R to C++ and back.
1 Introduction
As stated in the Rcpp (Eddelbuettel and François, 2011a) vignette, “Extending Rcpp”
Rcpp facilitates data interchange between R and C++ through the templated functions Rcpp::as
(for conversion of objects from R to C++) and Rcpp::wrap (for conversion from C++ to R).
The RcppEigen package provides the header files composing the Eigen C++ template library and imple-
mentations of Rcpp::as and Rcpp::wrap for the C++ classes defined in Eigen.
The Eigen classes themselves provide high-performance, versatile and comprehensive representations
of dense and sparse matrices and vectors, as well as decompositions and other functions to be applied to
these objects. In the next section we introduce some of these classes and show how to interface to them
from R.
2 Eigen classes
Eigen (http://eigen.tuxfamily.org) is a C++ template library providing classes for many forms of
matrices, vectors, arrays and decompositions. These classes are flexible and comprehensive allowing for
both high performance and well structured code representing high-level operations. C++ code based on
Eigen is often more like R code, working on the “whole object”, than compiled code in other languages
where operations often must be coded in loops.
As in many C++ template libraries using template meta-programming (Abrahams and Gurtovoy,
2004), the templates themselves can be very complicated. However, Eigen provides typedef’s for common
classes that correspond to R matrices and vectors, as shown in Table 1. We will use these typedef’s
throughout this document.
The C++ classes shown in Table 1 are in the Eigen namespace, which means that they must be
written as Eigen::MatrixXd. However, if we preface our use of these class names with a declaration like
using Eigen : : MatrixXd ;
we can use these names without the qualifier.
Table 1: Correspondence between R matrix and vector types and classes in the Eigen namespace.
R object type Eigen class typedef
numeric matrix MatrixXd
integer matrix MatrixXi
complex matrix MatrixXcd
numeric vector VectorXd
integer vector VectorXi
complex vector VectorXcd
Matrix::dgCMatrix SparseMatrix<double>
1
2.1 Mapped matrices in Eigen
Storage for the contents of matrices from the classes shown in Table 1 is allocated and controlled by the
class constructors and destructors. Creating an instance of such a class from an R object involves copying
its contents. An alternative is to have the contents of the R matrix or vector mapped to the contents of
the object from the Eigen class. For dense matrices we use the Eigen templated class Map. For sparse
matrices we use the Eigen templated class MappedSparseMatrix.
We must, of course, be careful not to modify the contents of the R object in the C++ code. A
recommended practice is always to declare mapped objects as const.
2
Listing 1: transCpp: Transpose a matrix of integers
using Eigen : : Map ;
using Eigen : : MatrixXi ;
// Map t h e i n t e g e r m a t r i x AA from R
const Map<MatrixXi> A( as<Map<MatrixXi> >(AA) ) ;
// e v a l u a t e and r e t u r n t h e t r a n s p o s e o f A
const MatrixXi At (A. t r a n s p o s e ( ) ) ;
return wrap ( At ) ;
[,1] [,2]
[1,] 1 4
[2,] 2 5
[3,] 3 6
> str(A)
int [1:3, 1:2] 1 2 3 4 5 6
and, in Listing 1, use the transpose() method for the Eigen::MatrixXi class to return its transpose.
The R matrix in the SEXP AA is mapped to an Eigen::MatrixXi object then the matrix At is constructed
from its transpose and returned to R. We check that it works as intended.
> ftrans <- cxxfunction(signature(AA="matrix"), transCpp, plugin="RcppEigen")
> (At <- ftrans(A))
[,1] [,2] [,3]
[1,] 1 2 3
[2,] 4 5 6
> stopifnot(all.equal(At, t(A)))
For numeric or integer matrices the adjoint() method is equivalent to the transpose() method. For
complex matrices, the adjoint is the conjugate of the transpose. In keeping with the conventions in the
Eigen documentation we will, in what follows, use the adjoint() method to create the transpose of
numeric or integer matrices.
3
Listing 3: crossprodCpp: Cross-product and transposed cross-product of a single matrix
using Eigen : : Map ;
using Eigen : : MatrixXi ;
using Eigen : : Lower ;
4
Listing 4: cholCpp: Cholesky decomposition of a cross-product
using Eigen : : Map ;
using Eigen : : MatrixXd ;
using Eigen : : LLT ;
using Eigen : : Lower ;
elements such that A = LDL0 . Statisticians often write the decomposition as A = R0 R where R is an
upper triangular matrix. Of course, this R is simply the transpose of L from the “LLt” form.
The templated Eigen classes for the LLt and LDLt forms are called LLT and LDLT. In general we would
preserve the objects from these classes so that we could use them for solutions of linear systems. For
illustration we simply return the matrix L from the “LLt” form.
Because the Cholesky decomposition involves taking square roots we switch to numeric matrices
> storage.mode(A) <- "double"
before applying the code in Listing 4.
> fchol <- cxxfunction(signature(AA = "matrix"), cholCpp, "RcppEigen")
> (ll <- fchol(A))
$L
[,1] [,2]
[1,] 3.741657 0.000000
[2,] 8.552360 1.963961
$R
[,1] [,2]
[1,] 3.741657 8.552360
[2,] 0.000000 1.963961
> stopifnot(all.equal(ll[[2]], chol(crossprod(A))))
5
Listing 5: cholDetCpp: Determinant of a cross-product using the Cholesky decomposition
using Eigen : : Lower ;
using Eigen : : Map ;
using Eigen : : MatrixXd ;
using Eigen : : VectorXd ;
where the model matrix, X, is n × p (n ≥ p) and y is an n-dimensional response vector. There are
several ways based on matrix decompositions, to determine such a solution. We have already seen
two forms of the Cholesky decomposition: “LLt” and “LDLt”, that can be used to solve for β. b Other
decompositions that can be used are the QR decomposition, with or without column pivoting, the singular
value decomposition and the eigendecomposition of a symmetric matrix.
Determining a least squares solution is relatively straightforward. However, in statistical computing
we often require additional information, such as the standard errors of the coefficient estimates. Cal-
−1
culating these involves evaluating the diagonal elements of (X 0 X) and the residual sum of squares,
2
ky − X βk
b .
6
Listing 6: lltLSCpp: Least squares using the Cholesky decomposition
using Eigen : : LLT ;
using Eigen : : Lower ;
using Eigen : : Map ;
using Eigen : : MatrixXd ;
using Eigen : : VectorXd ;
7
Listing 7: QRLSCpp: Least squares using the unpivoted QR decomposition
using Eigen : : HouseholderQR ;
In the descriptions of other methods for solving least squares problems, much of the code parallels that
shown in Listing 6. We will omit the redundant parts and show only the evaluation of the coefficients, the
rank and the standard errors. Actually, we only calculate the standard errors up to the scalar multiple
of s, the residual standard error, in these code fragments. The calculation of the residuals and s and
the scaling of the coefficient standard errors is the same for all methods. (See the files fastLm.h and
fastLm.cpp in the RcppEigen source package for details.)
8
[1] 4.309225e+16
> rcond(mm) # alternative evaluation, the reciprocal condition number
[1] 2.320603e-17
> (c(rank=qr(mm)$rank, p=ncol(mm))) # rank as computed in R's qr function
rank p
11 12
> set.seed(1)
> dd$y <- mm %*% seq_len(ncol(mm)) + rnorm(nrow(mm), sd = 0.1)
> # lm detects the rank deficiency
> fm1 <- lm(y ~ f1 * f2, dd)
> writeLines(capture.output(print(summary(fm1), signif.stars=FALSE))[9:22])
Coefficients: (1 not defined because of singularities)
Estimate Std. Error t value Pr(>|t|)
(Intercept) 0.97786 0.05816 16.81 3.41e-09
f1B 12.03807 0.08226 146.35 < 2e-16
f1C 3.11722 0.08226 37.90 5.22e-13
f1D 4.06852 0.08226 49.46 2.83e-14
f2b 5.06012 0.08226 61.52 2.59e-15
f2c 5.99759 0.08226 72.91 4.01e-16
f1B:f2b -3.01476 0.11633 -25.92 3.27e-11
f1C:f2b 7.70300 0.11633 66.22 1.16e-15
f1D:f2b 8.96425 0.11633 77.06 < 2e-16
f1B:f2c NA NA NA NA
f1C:f2c 10.96133 0.11633 94.23 < 2e-16
f1D:f2c 12.04108 0.11633 103.51 < 2e-16
The lm function for fitting linear models in R uses a rank-revealing form of the QR decomposition.
When the model matrix is determined to be rank deficient, according to the threshold used in R’s QR
decomposition, the model matrix is reduced to rank (X) columns by pivoting selected columns (those
that are apparently linearly dependent on columns to their left) to the right hand side of the matrix.
A solution for this reduced model matrix is determined and the coefficients and standard errors for the
redundant columns are flagged as missing.
An alternative approach is to evaluate the “pseudo-inverse” of X from the singular value decomposition
(SVD) of X or the eigendecomposition of X 0 X. The SVD is of the form
X = U DV 0 = U1 D1 V 0
where U is an orthogonal n × n matrix and U1 is its leftmost p columns, D is n × p and zero off the main
diagonal so that D1 is a p × p diagonal matrix with non-decreasing non-negative diagonal elements, and
V is a p × p orthogonal matrix. The pseudo-inverse of D1 , written D1+ is a p × p diagonal matrix whose
first r = rank(X) diagonal elements are the inverses of the corresponding diagonal elements of D1 and
whose last p − r diagonal elements are zero.
The tolerance for determining if an element of the diagonal of D is considered to be (effectively) zero
is a multiple of the largest singular value (i.e. the (1, 1) element of D).
In Listing 8 we define a utility function, Dplus, to return the pseudo-inverse as a diagonal matrix,
given the singular values (the diagonal of D) and the apparent rank. To be able to use this function with
the eigendecomposition where the eigenvalues are in increasing order we include a Boolean argument rev
indicating whether the order is reversed.
9
Listing 8: DplusCpp: Create the diagonal matrix D + from the array of singular values d
using Eigen : : D i a g o n a l M a t r i x ;
using Eigen : : Dynamic ;
The standard errors of the coefficient estimates in the rank-deficient case must be interpreted carefully.
The solution with one or more missing coefficients, as returned by the lm.fit function in R and the
column-pivoted QR decomposition described in Section 4.6 does not provide standard errors for the
missing coefficients. That is, both the coefficient and its standard error are returned as NA because the
least squares solution is performed on a reduced model matrix. It is also true that the solution returned
by the SVD method is with respect to a reduced model matrix but the p coefficient estimates and their p
standard errors don’t show this. They are, in fact, linear combinations of a set of r coefficient estimates
and their standard errors.
X 0 X = V ΛV 0
where V , the matrix of eigenvectors, is a p × p orthogonal matrix and Λ is a p × p diagonal matrix with
non-increasing, non-negative diagonal elements, called the eigenvalues of X 0 X. When the eigenvalues
are distinct this V is the same as that in the SVD. Also the eigenvalues of X 0 X are the squares of the
singular values of X.
With these definitions we can adapt much of the code from the SVD method for the eigendecompo-
sition, as shown in Listing 10.
XP = QR = Q1 R1
where, as before, Q is n × n and orthogonal and R is n × p and upper triangular. The p × p matrix P is
a permutation matrix. That is, its columns are a permutation of the columns of Ip . It serves to reorder
the columns of X so that the diagonal elements of R are non-increasing in magnitude.
10
Listing 10: SymmEigLSCpp: Least squares using the eigendecomposition
using Eigen : : S e l f A d j o i n t E i g e n S o l v e r ;
const S e l f A d j o i n t E i g e n S o l v e r <MatrixXd>
VLV( MatrixXd ( p , p ) . s e t Z e r o ( ) . s e l f a d j o i n t V i e w <Lower >. rankUpdate (X. a d j o i n t ( ) ) ) ;
const ArrayXd D( e i g . e i g e n v a l u e s ( ) ) ;
const i n t r ( (D > D[ p − 1 ] * t h r e s h o l d ( ) ) . count ( ) ) ;
const MatrixXd VDp(VLV. e i g e n v e c t o r s ( ) * Dplus (D. s q r t ( ) , r , true ) ) ;
const VectorXd b e t a h a t (VDp * VDp. a d j o i n t ( ) * X. a d j o i n t ( ) * y ) ;
const VectorXd s e ( s * VDp. rowwise ( ) . norm ( ) ) ;
An instance of the class Eigen::ColPivHouseholderQR has a rank() method returning the compu-
tational rank of the matrix. When X is of full rank we can use essentially the same code as in the
unpivoted decomposition except that we must reorder the standard errors. When X is rank-deficient we
evaluate the coefficients and standard errors for the leading r columns of XP only.
In the rank-deficient case the straightforward calculation of the fitted values, as X β,
b cannot be used.
We could do some complicated rearrangement of the columns of X and the coefficient estimates but it is
conceptually (and computationally) easier to employ the relationship
I 0
b = Q1 Q01 y = Q r
y Q0 y
0 0
11
Listing 11: ColPivQRLSCpp: Least squares using the pivoted QR decomposition
using Eigen : : ColPivHouseholderQR ;
typedef ColPivHouseholderQR<MatrixXd > : : PermutationType Permutation ;
Call:
fastLm.formula(formula = y ~ f1 * f2, data = dd, method = 4L)
12
The coefficients from the symmetric eigendecomposition method are the same as those from the SVD
> print(summary(fmVLV <- fastLm(y ~ f1 * f2, dd, method=5L)), signif.stars=FALSE)
Call:
fastLm.formula(formula = y ~ f1 * f2, data = dd, method = 5L)
13
Table 2: lmBenchmark results on a desktop computer for the default size, 100, 000 × 40, full-rank model
matrix running 20 repetitions for each method. Times (Elapsed, User and Sys) are in seconds. The BLAS
in use is a single-threaded version of Atlas (Automatically Tuned Linear Algebra System).
Method Relative Elapsed User Sys
LLt 1.000000 1.227 1.228 0.000
LDLt 1.037490 1.273 1.272 0.000
SymmEig 2.895681 3.553 2.972 0.572
QR 7.828036 9.605 8.968 0.620
PivQR 7.953545 9.759 9.120 0.624
arma 8.383048 10.286 10.277 0.000
lm.fit 13.782396 16.911 15.521 1.368
SVD 54.829666 67.276 66.321 0.912
GSL 157.531377 193.291 192.568 0.640
An SVD method using the Lapack SVD subroutine, dgesv, may be faster than the native Eigen
implementation of the SVD, which is not a particularly fast method.
5 Delayed evaluation
A form of delayed evaluation is used in Eigen. That is, many operators and methods do not force the
evaluation of the object but instead return an “expression object” that is evaluated when needed. As an
example, even though we write the X 0 X evaluation using .rankUpdate(X.adjoint()) the X.adjoint()
part is not evaluated immediately. The rankUpdate method detects that it has been passed a matrix
that is to be used in its transposed form and evaluates the update by taking inner products of columns
of X instead of rows of X 0 .
Occasionally the method for Rcpp::wrap will not force an evaluation when it should. This is at least
what Bill Venables calls an “infelicity” in the code, if not an outright bug. In the code for the transpose
of an integer matrix shown in Listing 1 we assigned the transpose as a MatrixXi before returning it with
wrap. The assignment forces the evaluation. If we skip this step, as in Listing 12 we get an answer with
the correct shape but incorrect contents.
> Ai <- matrix(1:6, ncol=2L)
> ftrans2 <- cxxfunction(signature(AA = "matrix"), badtransCpp, "RcppEigen")
> (At <- ftrans2(Ai))
[,1] [,2] [,3]
[1,] 1 3 5
[2,] 2 4 6
> all.equal(At, t(Ai))
[1] "Mean relative difference: 0.4285714"
Another recommended practice is to assign objects before wrapping them for return to R.
6 Sparse matrices
Eigen provides sparse matrix classes. An R object of class dgCMatrix (from the Matrix (Bates and
Maechler, 2011) package) can be mapped as in Listing 13.
14
Listing 13: sparseProdCpp: Transpose and product with sparse matrices
using Eigen : : Map ;
using Eigen : : MappedSparseMatrix ;
using Eigen : : SparseMatrix ;
using Eigen : : VectorXd ;
References
David Abrahams and Aleksey Gurtovoy. C++ Template Metaprogramming: Concepts, Tools and Tech-
niques from Boost and Beyond. Addison-Wesley, Boston, 2004.
Douglas Bates and Martin Maechler. Matrix: Sparse and Dense Matrix Classes and Methods, 2011. URL
http://CRAN.R-Project.org/package=Matrix. R package version 1.0-2.
Dirk Eddelbuettel and Romain François. Rcpp: Seamless R and C++ Integration, 2011a. URL http:
//CRAN.R-Project.org/package=Rcpp. R package version 0.9.4.
Dirk Eddelbuettel and Romain François. Rcpp: Seamless R and C++ integration. Journal of Statistical
Software, 40(8):1–18, 2011b. URL http://www.jstatsoft.org/v40/i08/.
Romain François and Dirk Eddelbuettel. RcppGSL: Rcpp integration for GNU GSL vectors and matrices,
2011. URL http://CRAN.R-Project.org/package=RcppGSL. R package version 0.1.1.
Romain François, Dirk Eddelbuettel, and Douglas Bates. RcppArmadillo: Rcpp integration for Armadillo
templated linear algebra library, 2011. URL http://CRAN.R-Project.org/package=RcppArmadillo. R
package version 0.2.18.
Oleg Sklyar, Duncan Murdoch, Mike Smith, Dirk Eddelbuettel, and Romain François. inline: Inline C,
C++, Fortran function calls from R, 2010. URL http://CRAN.R-Project.org/package=inline. R
package version 0.3.8.
15