The mvn
R package provides computationally efficient tools related to the multivariate normal and Student's t distributions.
The tools are generally faster than those provided by other packages, thanks to the use of C++ code through the
Rcpp
\RcppArmadillo
packages and parallelization through the OpenMP
API. The most important functions are:
rmvn()
: simulates multivariate normal random vectors.rmvt()
: simulates Student's t normal random vectors.dmvn()
: evaluates the probability density function of a multivariate normal distribution.dmvt()
: evaluates the probability density function of a multivariate Student's t distribution.maha()
: evaluates mahalanobis distances.In the following sections we will benchmark each function against equivalent functions provided by other packages, while in the final section we provide an example application.
Simulating multivariate normal random variables is an essential step in many Monte Carlo algorithms (such as MCMC or Particle Filters),
hence this operations has to be as fast as possible. Here we compare the rmvn
function with the equivalent function rmvnorm
(from the mvtnorm
package) and mvrnorm
(from the MASS
package). In particular, we simulate \(10^4\) twenty-dimensional random vectors:
library("microbenchmark")
library("mvtnorm")
library("mvnfast")
library("MASS")
# We might also need to turn off BLAS parallelism
library("RhpcBLASctl")
blas_set_num_threads(1)
N <- 10000
d <- 20
# Creating mean and covariance matrix
mu <- 1:d
tmp <- matrix(rnorm(d^2), d, d)
mcov <- tcrossprod(tmp, tmp)
microbenchmark(rmvn(N, mu, mcov, ncores = 2),
rmvn(N, mu, mcov),
rmvnorm(N, mu, mcov),
mvrnorm(N, mu, mcov))
## Unit: milliseconds
## expr min lq mean median
## rmvn(N, mu, mcov, ncores = 2) 2.902814 2.998321 3.612878 3.052274
## rmvn(N, mu, mcov) 5.401385 5.446887 6.449389 5.482576
## rmvnorm(N, mu, mcov) 17.399070 18.128807 22.549732 18.314619
## mvrnorm(N, mu, mcov) 16.760721 17.509000 21.786832 17.637787
## uq max neval cld
## 3.699213 37.83526 100 a
## 5.966471 40.11215 100 a
## 18.691448 52.95715 100 b
## 18.019016 53.70390 100 b
In this example rmvn
cuts the computational time, relative to the alternatives, even when a single core is used. This gain is attributable to several factors: the use of C++ code and efficient numerical algorithms to simulate the random variables. Parallelizing the computation on two cores gives another appreciable speed-up. To be fair, it is necessary to point out that rmvnorm
and mvrnorm
have many more safety check on the user's input than rmvn
. This is true also for the functions described in the next sections.
Notice that this function does not use one of the Random Number Generators (RNGs) provided by R, but one of the parallel cryptographic RNGs described in (Salmon et al., 2011) and available here. It is important to point out that this RNG can safely be used in parallel, without risk of collisions between parallel sequence of random numbers, as detailed in the above reference.
We get similar performance gains when we simulate multivariate Student's t random variables:
# Here we have a conflict between namespaces
microbenchmark(mvnfast::rmvt(N, mu, mcov, df = 3, ncores = 2),
mvnfast::rmvt(N, mu, mcov, df = 3),
mvtnorm::rmvt(N, delta = mu, sigma = mcov, df = 3))
## Unit: milliseconds
## expr min lq
## mvnfast::rmvt(N, mu, mcov, df = 3, ncores = 2) 4.652198 4.726370
## mvnfast::rmvt(N, mu, mcov, df = 3) 8.830104 8.859716
## mvtnorm::rmvt(N, delta = mu, sigma = mcov, df = 3) 21.119516 22.239874
## mean median uq max neval cld
## 7.975439 4.813252 5.747973 77.90991 100 a
## 10.662410 8.898932 9.299192 81.96622 100 a
## 30.576735 22.337058 23.132446 97.08728 100 b
When d
and N
are large, and rmvn
or rmvt
are called several times with the same arguments, it would make sense to create the matrix where to store the simulated random variable upfront. This can be done as follows:
A <- matrix(nrow = N, ncol = d)
class(A) <- "numeric" # This is important. We need the elements of A to be of class "numeric".
rmvn(N, mu, mcov, A = A)
Notice that here rmvn
returns NULL
, not the simulated random vectors! These can be found in the matrix provided by the user:
A[1:2, 1:5]
## [,1] [,2] [,3] [,4] [,5]
## [1,] -8.462332 0.4888049 4.0023860 13.398520 9.964920
## [2,] 4.319361 5.8249954 -0.4042163 2.998746 -2.713987
Pre-creating the matrix of random variables saves some more time:
microbenchmark(rmvn(N, mu, mcov, ncores = 2, A = A),
rmvn(N, mu, mcov, ncores = 2),
times = 200)
## Unit: milliseconds
## expr min lq mean median
## rmvn(N, mu, mcov, ncores = 2, A = A) 2.661931 2.674606 2.685424 2.680439
## rmvn(N, mu, mcov, ncores = 2) 2.901276 2.919331 3.934591 2.926944
## uq max neval cld
## 2.688435 2.998077 200 a
## 3.258934 77.384910 200 b
Don't look at the median time here, the mean is much more affected by memory re-allocation.
Here we compare the dmvn
function, which evaluates the multivariate normal density, with the equivalent function dmvtnorm
(from the mvtnorm
package).
In particular we evaluate the log-density of \(10^4\) twenty-dimensional random vectors:
# Generating random vectors
N <- 10000
d <- 20
mu <- 1:d
tmp <- matrix(rnorm(d^2), d, d)
mcov <- tcrossprod(tmp, tmp)
X <- rmvn(N, mu, mcov)
microbenchmark(dmvn(X, mu, mcov, ncores = 2, log = T),
dmvn(X, mu, mcov, log = T),
dmvnorm(X, mu, mcov, log = T), times = 500)
## Unit: milliseconds
## expr min lq mean
## dmvn(X, mu, mcov, ncores = 2, log = T) 1.453886 1.477213 1.567035
## dmvn(X, mu, mcov, log = T) 2.630782 2.646670 2.713598
## dmvnorm(X, mu, mcov, log = T) 2.444235 2.539010 4.324475
## median uq max neval cld
## 1.497118 1.620458 4.312699 500 a
## 2.660778 2.775283 3.752153 500 b
## 2.576760 3.635029 76.810257 500 c
Again, we get some speed-up using C++ code and some more from the parallelization. We get similar results if we use a multivariate Student's t density:
# We have a namespace conflict
microbenchmark(mvnfast::dmvt(X, mu, mcov, df = 4, ncores = 2, log = T),
mvnfast::dmvt(X, mu, mcov, df = 4, log = T),
mvtnorm::dmvt(X, delta = mu, sigma = mcov, df = 4, log = T), times = 500)
## Unit: milliseconds
## expr min
## mvnfast::dmvt(X, mu, mcov, df = 4, ncores = 2, log = T) 1.622762
## mvnfast::dmvt(X, mu, mcov, df = 4, log = T) 2.852947
## mvtnorm::dmvt(X, delta = mu, sigma = mcov, df = 4, log = T) 2.740013
## lq mean median uq max neval cld
## 1.663794 1.777445 1.739397 1.855927 3.691391 500 a
## 2.880080 2.947171 2.894956 3.012011 5.915347 500 b
## 2.807969 4.471841 2.851654 3.933217 78.926866 500 c
Finally, we compare the maha
function, which evaluates the square mahalanobis distance with the equivalent function mahalanobis
(from the stats
package).
Also in the case we use \(10^4\) twenty-dimensional random vectors:
# Generating random vectors
N <- 10000
d <- 20
mu <- 1:d
tmp <- matrix(rnorm(d^2), d, d)
mcov <- tcrossprod(tmp, tmp)
X <- rmvn(N, mu, mcov)
microbenchmark(maha(X, mu, mcov, ncores = 2),
maha(X, mu, mcov),
mahalanobis(X, mu, mcov))
## Unit: milliseconds
## expr min lq mean median
## maha(X, mu, mcov, ncores = 2) 1.342140 1.359810 1.406688 1.379959
## maha(X, mu, mcov) 2.524762 2.534680 2.574092 2.548229
## mahalanobis(X, mu, mcov) 2.928934 2.960816 4.974597 3.798563
## uq max neval cld
## 1.455877 1.820133 100 a
## 2.613008 2.730864 100 a
## 4.044230 77.806682 100 b
The acceleration is similar to that obtained in the previous sections.
As an example application of the dmvn
function, we implemented the mean-shift mode seeking algorithm.
This procedure can be used to find the mode or maxima of a kernel density function, and it can be used to set up
clustering algorithms. Here we simulate \(10^4\) d-dimensional random vectors from mixture of normal distributions:
set.seed(5135)
N <- 10000
d <- 2
mu1 <- c(0, 0); mu2 <- c(2, 3)
Cov1 <- matrix(c(1, 0, 0, 2), 2, 2)
Cov2 <- matrix(c(1, -0.9, -0.9, 1), 2, 2)
bin <- rbinom(N, 1, 0.5)
X <- bin * rmvn(N, mu1, Cov1) + (!bin) * rmvn(N, mu2, Cov2)
Finally, we plot the resulting probability density and, starting from 10 initial points, we use mean-shift to converge to the nearest mode:
# Plotting
np <- 100
xvals <- seq(min(X[ , 1]), max(X[ , 1]), length.out = np)
yvals <- seq(min(X[ , 2]), max(X[ , 2]), length.out = np)
theGrid <- expand.grid(xvals, yvals)
theGrid <- as.matrix(theGrid)
dens <- dmixn(theGrid,
mu = rbind(mu1, mu2),
sigma = list(Cov1, Cov2),
w = rep(1, 2)/2)
plot(X[ , 1], X[ , 2], pch = '.', lwd = 0.01, col = 3)
contour(x = xvals, y = yvals, z = matrix(dens, np, np),
levels = c(0.002, 0.01, 0.02, 0.04, 0.08, 0.15 ), add = TRUE, lwd = 2)
# Mean-shift
library(plyr)
inits <- matrix(c(-2, 2, 0, 3, 4, 3, 2, 5, 2, -3, 2, 2, 0, 2, 3, 0, 0, -4, -2, 6),
10, 2, byrow = TRUE)
traj <- alply(inits,
1,
function(input)
ms(X = X,
init = input,
H = 0.05 * cov(X),
ncores = 2,
store = TRUE)$traj
)
invisible( lapply(traj,
function(input){
lines(input[ , 1], input[ , 2], col = 2, lwd = 1.5)
points(tail(input[ , 1]), tail(input[ , 2]))
}))
As we can see from the plot, each initial point leads one of two points that are very close to the true mode. Notice that the bandwidth for the kernel density estimator was chosen by trial-and-error, and less arbitrary choices are certainly possible in real applications.
Dirk Eddelbuettel and Romain Francois (2011). Rcpp: Seamless R and C++ Integration. Journal of Statistical Software, 40(8), 1-18. URL http://www.jstatsoft.org/v40/i08/.
Eddelbuettel, Dirk (2013) Seamless R and C++ Integration with Rcpp. Springer, New York. ISBN 978-1-4614-6867-7.
Dirk Eddelbuettel, Conrad Sanderson (2014). RcppArmadillo: Accelerating R with high-performance C++ linear algebra. Computational Statistics and Data Analysis, Volume 71, March 2014, pages 1054-1063. URL http://dx.doi.org/10.1016/j.csda.2013.02.005
John K. Salmon, Mark A. Moraes, Ron O. Dror, and David E. Shaw (2011). Parallel Random Numbers: As Easy as 1, 2, 3. D. E. Shaw Research, New York, NY 10036, USA.