Rev 4565 | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed
\documentclass{article}\usepackage{myVignette}\usepackage[authoryear,round]{natbib}\bibliographystyle{plainnat}%%\VignetteIndexEntry{Comparisons of Least Squares calculation speeds}%%\VignetteDepends{Matrix}\begin{document}\SweaveOpts{engine=R,eps=FALSE,pdf=TRUE,width=5,height=3,strip.white=TRUE,keep.source=TRUE}\setkeys{Gin}{width=\textwidth}\title{Comparing Least Squares Calculations}\author{Douglas Bates\\R Development Core Team\\\email{Douglas.Bates@R-project.org}}\date{\today}\maketitle\begin{abstract}Many statistics methods require one or more least squares problemsto be solved. There are several ways to perform this calculation,using objects from the base R system and using objects in theclasses defined in the \code{Matrix} package.We compare the speed of some of these methods on a very smallexample and on a example for which the model matrix is large andsparse.\end{abstract}<<preliminaries, echo=FALSE>>=options(width=75)@\section{Linear least squares calculations}\label{sec:LeastSquares}Many statistical techniques require least squares solutions\begin{equation}\label{eq:LeastSquares}\widehat{\bm{\beta}}=\arg\min_{\bm{\beta}}\left\|\bm{y}-\bX\bm{\beta}\right\|^2\end{equation}where $\bX$ is an $n\times p$ model matrix ($p\leq n$), $\bm{y}$ is$n$-dimensional and $\bm{\beta}$ is $p$ dimensional. Most statisticstexts state that the solution to (\ref{eq:LeastSquares}) is\begin{equation}\label{eq:XPX}\widehat{\bm{\beta}}=\left(\bX\trans\bX\right)^{-1}\bX\trans\bm{y}\end{equation}when $\bX$ has full column rank (i.e. the columns of $\bX$ arelinearly independent) and all too frequently it is calculated inexactly this way.\subsection{A small example}\label{sec:smallLSQ}As an example, let's create a model matrix, \code{mm}, and correspondingresponse vector, \code{y}, for a simple linear regression model usingthe \code{Formaldehyde} data.<<modelMatrix>>=data(Formaldehyde)str(Formaldehyde)(m <- cbind(1, Formaldehyde$carb))(yo <- Formaldehyde$optden)@Using \code{t} to evaluatethe transpose, \code{solve} to take an inverse, and the \code{\%*\%}operator for matrix multiplication, we can translate \ref{eq:XPX} intothe \Slang{} as<<naiveCalc>>=solve(t(m) %*% m) %*% t(m) %*% yo@On modern computers this calculation is performed so quickly that it cannotbe timed accurately in \RR{}\footnote{From R version 2.2.0, \code{system.time()} has default argument\code{gcFirst = TRUE} which is assumed and relevant for all subsequent timings}<<timedNaive>>=system.time(solve(t(m) %*% m) %*% t(m) %*% yo)@and it provides essentially the same results as the standard\code{lm.fit} function that is called by \code{lm}.<<catNaive>>=dput(c(solve(t(m) %*% m) %*% t(m) %*% yo))dput(unname(lm.fit(m, yo)$coefficients))@%$\subsection{A large example}\label{sec:largeLSQ}For a large, ill-conditioned least squares problem, such as thatdescribed in \citet{koen:ng:2003}, the literal translation of(\ref{eq:XPX}) does not perform well.<<KoenNg>>=library(Matrix)data(KNex, package = "Matrix")y <- KNex$ymm <- as(KNex$mm, "matrix") # full traditional matrixdim(mm)system.time(naive.sol <- solve(t(mm) %*% mm) %*% t(mm) %*% y)@Because the calculation of a ``cross-product'' matrix, such as$\bX\trans\bX$ or $\bX\trans\bm{y}$, is a common operation instatistics, the \code{crossprod} function has been provided to dothis efficiently. In the single argument form \code{crossprod(mm)}calculates $\bX\trans\bX$, taking advantage of the symmetry of theproduct. That is, instead of calculating the $712^2=506944$ elements of$\bX\trans\bX$ separately, it only calculates the $(712\cdot713)/2=253828$ elements in the upper triangle and replicates them inthe lower triangle. Furthermore, there is no need to calculate theinverse of a matrix explicitly when solving alinear system of equations. When the two argument form of the \code{solve}function is used the linear system\begin{equation}\label{eq:LSQsol}\left(\bX\trans\bX\right) \widehat{\bm{\beta}} = \bX\trans\by\end{equation}is solved directly.Combining these optimizations we obtain<<crossKoenNg>>=system.time(cpod.sol <- solve(crossprod(mm), crossprod(mm,y)))all.equal(naive.sol, cpod.sol)@On this computer (2.0 GHz Pentium-4, 1 GB Memory, Goto's BLAS, in Spring2004) thecrossprod form of the calculation is about four times as fast as thenaive calculation. In fact, the entire crossprod solution isfaster than simply calculating $\bX\trans\bX$ the naive way.<<xpxKoenNg>>=system.time(t(mm) %*% mm)@Note that in newer versions of \RR{} and the BLAS library (as of summer2007), \RR's \code{\%*\%} is able to detect the many zeros in \code{mm} andshortcut many operations, and is hence much faster for such a sparse matrixthan \code{crossprod} which currently does not make use of suchoptimizations. This is not the case when \RR{} is linked against anoptimized BLAS library such as GOTO or ATLAS.%%Also, for fully dense matrices, \code{crossprod()} indeed remains faster(by a factor of two, typically) independently of the BLAS library:<<fullMatrix_crossprod>>=fm <- mmset.seed(11)fm[] <- rnorm(length(fm))system.time(c1 <- t(fm) %*% fm)system.time(c2 <- crossprod(fm))stopifnot(all.equal(c1, c2, tol = 1e-12))@ % using stopifnot(.) to save output\subsection{Least squares calculations with Matrix classes}\label{sec:MatrixLSQ}The \code{crossprod} function applied to a single matrix takesadvantage of symmetry when calculating the product but does not retainthe information that the product is symmetric (and positivesemidefinite). As a result the solution of (\ref{eq:LSQsol}) isperformed using general linear system solver based on an LUdecomposition when it would be faster, and more stable numerically, touse a Cholesky decomposition. The Cholesky decomposition could be usedbut it is rather awkward<<naiveChol>>=system.time(ch <- chol(crossprod(mm)))system.time(chol.sol <-backsolve(ch, forwardsolve(ch, crossprod(mm, y),upper = TRUE, trans = TRUE)))stopifnot(all.equal(chol.sol, naive.sol))@The \code{Matrix} package uses the S4 class system\citep{R:Chambers:1998} to retain information on the structure ofmatrices from the intermediate calculations. A general matrix indense storage, created by the \code{Matrix} function, has class\code{"dgeMatrix"} but its cross-product has class \code{"dpoMatrix"}.The \code{solve} methods for the \code{"dpoMatrix"} class use theCholesky decomposition.<<MatrixKoenNg>>=mm <- as(KNex$mm, "dgeMatrix")class(crossprod(mm))system.time(Mat.sol <- solve(crossprod(mm), crossprod(mm, y)))stopifnot(all.equal(naive.sol, unname(as(Mat.sol,"matrix"))))@Furthermore, any method that calculates adecomposition or factorization stores the resulting factorization withthe original object so that it can be reused without recalculation.<<saveFactor>>=xpx <- crossprod(mm)xpy <- crossprod(mm, y)system.time(solve(xpx, xpy))system.time(solve(xpx, xpy)) # reusing factorization@The model matrix \code{mm} is sparse; that is, most of the elements of\code{mm} are zero. The \code{Matrix} package incorporates specialmethods for sparse matrices, which produce the fastest results of all.<<SparseKoenNg>>=mm <- KNex$mmclass(mm)system.time(sparse.sol <- solve(crossprod(mm), crossprod(mm, y)))stopifnot(all.equal(naive.sol, unname(as(sparse.sol, "matrix"))))@As with other classes in the \code{Matrix} package, the\code{dsCMatrix} retains any factorization that has been calculatedalthough, in this case, the decomposition is so fast that it isdifficult to determine the difference in the solution times.<<SparseSaveFactor>>=xpx <- crossprod(mm)xpy <- crossprod(mm, y)system.time(solve(xpx, xpy))system.time(solve(xpx, xpy))@\subsection*{Session Info}<<sessionInfo, results=tex>>=toLatex(sessionInfo())@<<from_pkg_sfsmisc, echo=FALSE>>=if(identical(1L, grep("linux", R.version[["os"]]))) { ##----- Linux - only ----Sys.procinfo <- function(procfile){l2 <- strsplit(readLines(procfile),"[ \t]*:[ \t]*")r <- sapply(l2[sapply(l2, length) == 2],function(c2)structure(c2[2], names= c2[1]))attr(r,"Name") <- procfileclass(r) <- "simple.list"r}Scpu <- Sys.procinfo("/proc/cpuinfo")Smem <- Sys.procinfo("/proc/meminfo")} # Linux only@<<Sys_proc_fake, eval=FALSE>>=if(identical(1L, grep("linux", R.version[["os"]]))) { ## Linux - only ---Scpu <- sfsmisc::Sys.procinfo("/proc/cpuinfo")Smem <- sfsmisc::Sys.procinfo("/proc/meminfo")print(Scpu[c("model name", "cpu MHz", "cache size", "bogomips")])print(Smem[c("MemTotal", "SwapTotal")])}@<<Sys_proc_out, echo=FALSE>>=if(identical(1L, grep("linux", R.version[["os"]]))) { ## Linux - only ---print(Scpu[c("model name", "cpu MHz", "cache size", "bogomips")])print(Smem[c("MemTotal", "SwapTotal")])}@\bibliography{Matrix}\end{document}