R:总结相邻矩阵元素。如何加快速度?

时间:2016-06-07 16:03:11

标签: r matrix openmp rcpp armadillo

我正在处理大约2500x2500x50(lonxlatxtime)的大型矩阵。矩阵只包含1和0.我需要知道每个时间步长24个周围元素的总和。到目前为止,我是这样做的:

xdim <- 2500
ydim <- 2500
tdim <- 50
a <- array(0:1,dim=c(xdim,ydim,tdim))
res <- array(0:1,dim=c(xdim,ydim,tdim))

for (t in 1:tdim){
  for (x in 3:(xdim-2)){
    for (y in 3:(ydim-2)){
      res[x,y,t] <- sum(a[(x-2):(x+2),(y-2):(y+2),t])
    }
  }
}

这样可行,但对我的需求来说太慢了。有人请建议如何加快速度?

3 个答案:

答案 0 :(得分:6)

简介

我不得不说,只有数组的设置背后有很多隐藏的东西。然而问题的其余部分是微不足道的。因此,有两种方法可以实现:

  1. @Alex给出的强力(用C ++编写)
  2. 观察复制模式
  3. 使用OpenMP的Bruteforce

    如果我们想要蛮力&#39;然后我们可以使用@Alex给出的建议来使用OpenMP和Armadillo

    #include <RcppArmadillo.h>
    
    // [[Rcpp::depends(RcppArmadillo)]]
    
    // Add a flag to enable OpenMP at compile time
    // [[Rcpp::plugins(openmp)]]
    
    // Protect against compilers without OpenMP
    #ifdef _OPENMP
      #include <omp.h>
    #endif
    
    // [[Rcpp::export]]
    arma::cube cube_parallel(arma::cube a, arma::cube res, int cores = 1) {
    
      // Extract the different dimensions
      unsigned int tdim = res.n_slices;
    
      unsigned int xdim = res.n_rows;
    
      unsigned int ydim = res.n_cols;
    
      // Same calculation loop
      #pragma omp parallel for num_threads(cores)
      for (unsigned int t = 0; t < tdim; t++){
        // pop the T
        arma::mat temp_mat = a.slice(t);
    
        // Subset the rows
        for (unsigned int x = 2; x < xdim-2; x++){
    
          arma::mat temp_row_sub = temp_mat.rows(x-2, x+2);
    
          // Iterate over the columns with unit accumulative sum
          for (unsigned int y = 2; y <  ydim-2; y++){
            res(x,y,t) = accu(temp_row_sub.cols(y-2,y+2));
          }
        }
      }
    
      return res;
    }
    

    复制模式

    但是,更聪明的方法是了解array(0:1, dims)的构建方式。

    最值得注意的是:

    • 案例1:如果xdim是偶数,则只有矩阵的行交替。
    • 案例2:如果xdim为奇数且ydim为奇数,则行交替以及矩阵交替。
    • 案例3:如果xdim为奇数且ydim为偶数,则只有行备用

    实施例

    让我们看看实际观察模式的案例。

    案例1:

    xdim <- 2
    ydim <- 3
    tdim <- 2
    a <- array(0:1,dim=c(xdim,ydim,tdim))
    

    <强>输出

    , , 1
    
         [,1] [,2] [,3]
    [1,]    0    0    0
    [2,]    1    1    1
    
    , , 2
    
         [,1] [,2] [,3]
    [1,]    0    0    0
    [2,]    1    1    1
    

    案例2:

    xdim <- 3
    ydim <- 3
    tdim <- 3
    a <- array(0:1,dim=c(xdim,ydim,tdim))
    

    <强>输出:

    , , 1
    
         [,1] [,2] [,3]
    [1,]    0    1    0
    [2,]    1    0    1
    [3,]    0    1    0
    
    , , 2
    
         [,1] [,2] [,3]
    [1,]    1    0    1
    [2,]    0    1    0
    [3,]    1    0    1
    
    , , 3
    
         [,1] [,2] [,3]
    [1,]    0    1    0
    [2,]    1    0    1
    [3,]    0    1    0
    

    案例3:

    xdim <- 3
    ydim <- 4
    tdim <- 2
    a <- array(0:1,dim=c(xdim,ydim,tdim))
    

    <强>输出:

    , , 1
    
         [,1] [,2] [,3] [,4]
    [1,]    0    1    0    1
    [2,]    1    0    1    0
    [3,]    0    1    0    1
    
    , , 2
    
         [,1] [,2] [,3] [,4]
    [1,]    0    1    0    1
    [2,]    1    0    1    0
    [3,]    0    1    0    1
    

    模式黑客

    好的,基于上面的讨论,我们选择利用这种独特模式制作一些代码。

    创建交替向量

    在这种情况下,交替向量在两个不同的值之间切换。

    #include <RcppArmadillo.h>
    // [[Rcpp::depends(RcppArmadillo)]]
    
    // ------- Make Alternating Vectors
    
    arma::vec odd_vec(unsigned int xdim){
    
      // make a temporary vector to create alternating 0-1 effect by row.
      arma::vec temp_vec(xdim);
    
      // Alternating vector (anyone have a better solution? )
      for (unsigned int i = 0; i < xdim; i++) {
        temp_vec(i) = (i % 2 ? 0 : 1);
      }
    
      return temp_vec;
    }
    
    arma::vec even_vec(unsigned int xdim){
    
      // make a temporary vector to create alternating 0-1 effect by row.
      arma::vec temp_vec(xdim);
    
      // Alternating vector (anyone have a better solution? )
      for (unsigned int i = 0; i < xdim; i++) {
        temp_vec(i) = (i % 2 ? 1 : 0); // changed
      }
    
      return temp_vec;
    }
    

    创建矩阵

    的三种情况

    如上所述,矩阵有三种情况。偶数,第一奇数和第二奇数。

    // --- Handle the different cases 
    
    // [[Rcpp::export]]
    arma::mat make_even_matrix(unsigned int xdim, unsigned int ydim){
    
      arma::mat temp_mat(xdim,ydim);
    
      temp_mat.each_col() = even_vec(xdim);
    
      return temp_mat;
    }
    
    // xdim is odd and ydim is even
    // [[Rcpp::export]]
    arma::mat make_odd_matrix_case1(unsigned int xdim, unsigned int ydim){
    
      arma::mat temp_mat(xdim,ydim);
    
      arma::vec e_vec = even_vec(xdim);
      arma::vec o_vec = odd_vec(xdim);
    
      // Alternating column 
      for (unsigned int i = 0; i < ydim; i++) {
        temp_mat.col(i) = (i % 2 ? o_vec : e_vec);
      }
    
      return temp_mat;
    }
    
    // xdim is odd and ydim is odd    
    // [[Rcpp::export]]
    arma::mat make_odd_matrix_case2(unsigned int xdim, unsigned int ydim){
    
      arma::mat temp_mat(xdim,ydim);
    
      arma::vec e_vec = even_vec(xdim);
      arma::vec o_vec = odd_vec(xdim);
    
      // Alternating column 
      for (unsigned int i = 0; i < ydim; i++) {
        temp_mat.col(i) = (i % 2 ? e_vec : o_vec); // slight change
      }
    
      return temp_mat;
    }
    

    计算引擎

    与之前的解决方案相同,只是没有t,因为我们不再需要重复计算。

    // --- Calculation engine
    
    // [[Rcpp::export]]
    arma::mat calc_matrix(arma::mat temp_mat){
    
      unsigned int xdim = temp_mat.n_rows;
    
      unsigned int ydim = temp_mat.n_cols;
    
      arma::mat res = temp_mat;
    
      // Subset the rows
      for (unsigned int x = 2; x < xdim-2; x++){
    
        arma::mat temp_row_sub = temp_mat.rows(x-2, x+2);
    
        // Iterate over the columns with unit accumulative sum
        for (unsigned int y = 2; y <  ydim-2; y++){
          res(x,y) = accu(temp_row_sub.cols(y-2,y+2));
        }
      }
    
      return res;
    }
    

    调用主函数

    这是将所有内容组合在一起的核心功能。这为我们提供了所需的距离阵列。

    // --- Main Engine
    
    // Create the desired cube information
    // [[Rcpp::export]]
    arma::cube dim_to_cube(unsigned int xdim = 4, unsigned int ydim = 4, unsigned int tdim = 3) {
    
      // Initialize values in A
      arma::cube res(xdim,ydim,tdim);
    
      if(xdim % 2 == 0){
        res.each_slice() = calc_matrix(make_even_matrix(xdim, ydim));
      }else{
    
        if(ydim % 2 == 0){
    
          res.each_slice() = calc_matrix(make_odd_matrix_case1(xdim, ydim));
    
        }else{
    
          arma::mat first_odd_mat = calc_matrix(make_odd_matrix_case1(xdim, ydim));
    
          arma::mat sec_odd_mat = calc_matrix(make_odd_matrix_case2(xdim, ydim));
    
          for(unsigned int t = 0; t < tdim; t++){
            res.slice(t) = (t % 2 ? sec_odd_mat : first_odd_mat);
          }
    
        }
    
      }
    
      return res;
    }
    

    时序

    现在,真正的事实是它的表现如何:

    Unit: microseconds
           expr      min        lq       mean    median        uq       max neval
        r_1core 3538.022 3825.8105 4301.84107 3957.3765 4043.0085 16856.865   100
     alex_1core 2790.515 2984.7180 3461.11021 3076.9265 3189.7890 15371.406   100
      cpp_1core  174.508  180.7190  197.29728  194.1480  204.8875   338.510   100
      cpp_2core  111.960  116.0040  126.34508  122.7375  136.2285   162.279   100
      cpp_3core   81.619   88.4485  104.54602   94.8735  108.5515   204.979   100
      cpp_cache   40.637   44.3440   55.08915   52.1030   60.2290   302.306   100
    

    用于计时的脚本:

    cpp_parallel = cube_parallel(a,res, 1)
    alex_1core = alex(a,res,xdim,ydim,tdim)
    cpp_cache = dim_to_cube(xdim,ydim,tdim)
    op_answer = cube_r(a,res,xdim,ydim,tdim)
    
    all.equal(cpp_parallel, op_answer)
    all.equal(cpp_cache, op_answer)
    all.equal(alex_1core, op_answer)
    
    xdim <- 20
    ydim <- 20
    tdim <- 5
    a <- array(0:1,dim=c(xdim,ydim,tdim))
    res <- array(0:1,dim=c(xdim,ydim,tdim))
    
    
    ga = microbenchmark::microbenchmark(r_1core = cube_r(a,res,xdim,ydim,tdim),
                                        alex_1core = alex(a,res,xdim,ydim,tdim),
                                        cpp_1core = cube_parallel(a,res, 1), 
                                        cpp_2core = cube_parallel(a,res, 2), 
                                        cpp_3core = cube_parallel(a,res, 3),
                                        cpp_cache = dim_to_cube(xdim,ydim,tdim))
    

答案 1 :(得分:2)

这是一个对大型阵列来说速度快的解决方案:

res <- apply(a, 3, function(a) t(filter(t(filter(a, rep(1, 5), circular=TRUE)), rep(1, 5), circular=TRUE)))
dim(res) <- c(xdim, ydim, tdim)

我使用rep(1,5)过滤数组作为沿每个维度的权重(即,在2的邻域内的和值)。然后我修改了dim属性,因为它最初是作为矩阵出现的。

请注意,这会在数组的边缘处包裹总和(这可能是有意义的,因为您正在查看纬度和经度;如果没有,我可以修改我的答案。)

具体例子:

xdim <- 500
ydim <- 500
tdim <- 15
a <- array(0:1,dim=c(xdim,ydim,tdim))

以及您目前正在使用的内容(边缘有NA)以及此示例在我的笔记本电脑上显示的时间:

f1 <- function(a, xdim, ydim, tdim){
  res <- array(NA_integer_,dim=c(xdim,ydim,tdim))
  for (t in 1:tdim){
    for (x in 3:(xdim-2)){
      for (y in 3:(ydim-2)){
        res[x,y,t] <- sum(a[(x-2):(x+2),(y-2):(y+2),t])
      }
    }
  }
  return(res)
}

system.time(res1 <- f1(a, xdim, ydim, tdim))
#   user  system elapsed
# 14.813   0.005  14.819

这里与我描述的版本进行了比较:

f2 <- function(a, xdim, ydim, tdim){
  res <- apply(a, 3, function(a) t(filter(t(filter(a, rep(1, 5), circular=TRUE)), rep(1, 5), circular=TRUE)))
  dim(res) <- c(xdim, ydim, tdim)
  return(res)
}

system.time(res2 <- f2(a, xdim, ydim, tdim))
#  user  system elapsed
# 1.188   0.047   1.236

你可以看到它有显着的速度提升(对于大型阵列)。并检查它是否提供了正确的解决方案(注意我添加了NA,因此两个结果都匹配,因为我以循环方式提供了过滤器):

## Match NAs
res2NA <- ifelse(is.na(res1), NA, res2)

all.equal(res2NA, res1)
# [1] TRUE

我补充说你的完整阵列(2500x2500x50)花了不到一分钟(大约55秒),虽然它确实在这个过程中使用了大量内存,仅供参考。

答案 2 :(得分:1)

您当前的代码在冗余子集和计算方面有很多开销。如果你想要更快的速度,请清理它。

  • xdim <- ydim <- 20; tdim <- 5,我看到我的机器加速了23%。
  • xdim <- ydim <- 200; tdim <- 10,我看到加速率提高了25%。

这需要额外内存的少量成本,这可以通过检查下面的代码来实现。

xdim <- ydim <- 20; tdim <- 5
a <- array(0:1,dim=c(xdim,ydim,tdim))
res <- array(0:1,dim=c(xdim,ydim,tdim))

microbenchmark(op= {
  for (t in 1:tdim){
    for (x in 3:(xdim-2)){
      for (y in 3:(ydim-2)){
        res[x,y,t] <- sum(a[(x-2):(x+2),(y-2):(y+2),t])
      }
    }
  }
},
alex= {
  for (t in 1:tdim){
    temp <- a[,,t]
    for (x in 3:(xdim-2)){
      temp2 <- temp[(x-2):(x+2),]
      for (y in 3:(ydim-2)){
        res[x,y,t] <- sum(temp2[,(y-2):(y+2)])
      }
    }
  }
}, times = 50)

Unit: milliseconds
 expr      min       lq     mean   median       uq      max neval cld
   op 4.855827 5.134845 5.474327 5.321681 5.626738 7.463923    50   b
 alex 3.720368 3.915756 4.213355 4.012120 4.348729 6.320481    50  a 

进一步改进:

  1. 如果你用 C ++ 写这个,我的猜测是认识res[x,y,t] = res[x,y-1,t] - sum(a[...,y-2,...]) + sum(a[...,y+2,...])会为你节省更多时间。在R中,它没有在我的计时测试中。
  2. 这个问题也令人尴尬地平行。您无法分割t维度以更多地使用多核架构。
  3. 这两个都留给读者/ OP。