Home > OS >  Improving computation performance of a matrix or array
Improving computation performance of a matrix or array

Time:12-09

I want to optimize my matrix computation.

My code generates an array (p x q x n x n).

Let x : n x p matrix, v: q x 1 vector

 
f1 <- function(i){
  sapply(seq_along(1:n), function(j) outer(x[i,]-x[j,], v, "*")^2, simplify = "array") 
}

sapply(seq_along(1:n), FUN = f1, simplify = "array")

Due to the large size of the array, I found this code should spend much memory space.

Then, improving its performance is limited by parallel computing or other apply methods.

How can I improve the computation performance?

CodePudding user response:

The following cut down execution time by 40% on my machine by:

  • avoiding redundant calculation (squared outer products are identical, regardless whether x[i,]-x[j,] or v. v. enters the equation)
  • supplying a zero matrix for squared outer products where i == j
  • pre-dimensioning the result array and ...
  • finally populating the result array at the correct positions

original function:

f_original <- function(){
  f1 <- function(i){
    sapply(seq_along(1:n), function(j) outer(x[i,]-x[j,], v, "*")^2,
           simplify = "array") 
  }
  sapply(seq_along(1:n), FUN = f1, simplify = "array")
}

alternative:

f_alternative <- function(){
  ## dimension the resulting array in advance:
  res = array(NA, c(p, q, n, n))
  ## these are the possible outcomes, because
  ## the order of i and j does not matter: 
  values <- combn(1:n,
                  m = 2,
                  FUN = function(ab){
                    a = ab[1]; b = ab[2]
                    outer(x[a,] - x[b,], v, '*')^2
                  }
                  )
  ## create all combinations of i and j, where i != j
  indices <- t(combn(1:n, m = 2))
  ## fill the result array at the positions ,,i,j and ,,j,i:
  for(r in 1:nrow(indices)){
    a = indices[r, 1]
    b = indices[r, 2]
    res[, , a, b] = res[ , , b , a] = values[ , , r]
  }
  ## fill the positions ,,i,j and ,,j,i with a zero matrix,
  ## (results from i == j):
  for(r in 1:n) res[, , r, r] = array(0, p * q)

  return(res)
}

benchmarking:

library(microbenchmark)

## initialize:
n = 30
p = 2
q = 5
x = matrix(rnorm(n * p), n, p)
v = rnorm(q)

microbenchmark(f_original(), times = 1e3)
## Unit: milliseconds
##          expr    min      lq     mean  median      uq    max neval
##  f_original() 7.4636 8.10685 9.274144 9.08785 9.97285 74.976  1000

microbenchmark(f_alternative(), times = 1e3)
## Unit: milliseconds
##             expr    min      lq     mean median     uq     max neval
## f_alternative() 4.7369 5.08235 5.941499 5.3563 6.7484 60.7656  1000

CodePudding user response:

Context

Before you even get to parallel computing with multiple cores and/or multiple machines, consider simplifying your calculation.

require( purrr )

# --- For illustration, let
p   <- 3
q   <- 4
n   <- 5
x   <- array( data = 1:( n * p ), dim = c( n, p ) )
V   <- matrix( q:1, nrow = q )

# --- The answer you seek is a multi-dimensional array, such as this
X <- array( data = NA, dim = c( p, q, n, n ) )

Simplifications

Do n calculations at once

Note that wherever i == j, the difference X[ i, ] - X[ j, ] will be a matrix of zeroes, e.g., matrix_of_zeroes <- matrix( data = 0, nrow = n, ncol = p )

and

outer( matrix_of_zeroes, V, "*" )^2

will be a vector of length q containing all zeros, e.g.,

zero_vector <- matrix( 0, ncol = q )

So you can take care of n calculations all at once as follows:

seq( n ) %>% walk( ~{ X[ , , .x, .x ] <<- zero_vector })

Calculate half the elements, re-use the results for the other half

Note that, since the last thing your function does is to square the result

f1( i, j ) == f1( j, i )

so if you calculate the upper triangle, you can use the results in the corresponding elements of the lower triangle, that is, set X[ , , j, i ] equal to element X[ , , i, j ].

Calculating elements of the upper triangle

index <- matrix( c( 1:n ), n, n, byrow = TRUE )

index
#     [,1] [,2] [,3] [,4] [,5]
#[1,]    1    2    3    4    5
#[2,]    1    2    3    4    5
#[3,]    1    2    3    4    5
#[4,]    1    2    3    4    5
#[5,]    1    2    3    4    5

# --- For index j, take everything above the diagonal
j <- index[ upper.tri( index, diag = FALSE ) ]

# j
# [1] 2 3 3 4 4 4 5 5 5 5

# --- For index i, build sequences 1, 12, 123, 1234, ...
2:( n - 1 ) %>%
  map( ~seq( from = 1, to = .x )) %>%
    unlist -> i
# --- Prepend the first i
i <- c( 1, i )

i
# [1] 1 1 2 1 2 3 1 2 3 4

elements <- data.frame( i = i, j = j )

elements
#   i j
#1  1 2
#2  1 3
#3  2 3
#4  1 4
#5  2 4
#6  3 4
#7  1 5
#8  2 5
#9  3 5
#10 4 5

# --- The function that does the work
fij <- function( i, j ) ( ( x[ i, ] - x[ j, ] ) %o%  V )^2

# --- Show what will happen when we walk along these elements.

walk2( elements$i, elements$j, ~cat( sprintf( "fij( %d, %d )\n", .x, .y ) ))
#fij( 1, 2 )
#fij( 1, 3 )
#fij( 2, 3 )
#fij( 1, 4 )
#fij( 2, 4 )
#fij( 3, 4 )
#fij( 1, 5 )
#fij( 2, 5 )
#fij( 3, 5 )
#fij( 4, 5 )

# --- Now actually walk along and calculate, updating the result outside of the function
#     by using the parent environment assignment operator, `<<-`

# --- To calculate the results and store them in the upper triangle:
# walk2( elements$i, elements$j, ~( X[ , , .x, .y ] <<- fij( .x, .y ) ))

# --- To calculate the results and store them in both upper and lower triangles:
walk2(
    elements$i                        # Take one index i
  , elements$j                        #  and one index j
  , ~{
          fij( .x, .y ) ->>           # Calculate the results for i,j
        X[ , , .x, .y ] ->>           # Assign the result for upper-triangle
        X[ , , .y, .x ]               # Assign the result for lower-triangle
     }
)

Putting it all together

require( purrr )
# --- For illustration, let

p   <- 3
q   <- 4
n   <- 5
x   <- array( data = 1:( n * p ), dim = c( n, p ) )
V   <- matrix( q:1, nrow = q )

# --- The answer you seek is a multi-dimensional array, such as this
X <- array( data = NA, dim = c( p, q, n, n ) )

# --- Take care of cases where i == j
zero_vector <- matrix( 0, ncol = q )
seq( n ) %>% walk( ~{ X[ , , .x, .x ] <<- zero_vector })

index <- matrix( c( 1:n ), n, n, byrow = TRUE )

# --- For index j, take everything above the diagonal
j <- index[ upper.tri( index, diag = FALSE ) ]

# --- For index i, build sequences 1, 12, 123, 1234, ...
2:( n - 1 ) %>%
  map( ~seq( from = 1, to = .x )) %>%
    unlist -> i

# --- Prepend the first i
i <- c( 1, i )

elements <- data.frame( i = i, j = j )

# --- The function that does the work
fij <- function( i, j ) ( ( x[ i, ] - x[ j, ] ) %o%  V )^2

# --- To calculate the results and store them in both upper and lower triangles:
walk2(
    elements$i                        # Take one index i
  , elements$j                        #  and one index j
  , ~{
          fij( .x, .y ) ->>           # Calculate the results for i,j
        X[ , , .x, .y ] ->>           # Assign the result for upper-triangle
        X[ , , .y, .x ]               # Assign the result for lower-triangle
     }
)
  • Related