简体   繁体   English

R:可以矢量化/加速这个双循环吗?

[英]R: Is it possible to vectorise / speed-up this double loop?

This is a high-level, general question. 这是一个高层次的普遍问题。 There are some similar ones around with different, and more concise, examples. 周围也有一些相似的例子,但有不同且更简洁的示例。 Perhaps it cannot be answered. 也许无法回答。 conn is a matrix. conn是一个矩阵。

     for (i in 2:dim(conn)[1]) {
        for (j in 2:dim(conn)[1]) {
          if ((conn[i, 1] == conn[1, j]) & conn[i, 1] != 0) {
              conn[i, j] <- 1
              conn[j, i] <- 1
              }
              else {
                conn[i, j] <- 0
                conn[j, i] <- 0
                }
           }
      }

This comes straight out of cluscomp from the clusterCons package. 这都直出cluscomp从clusterCons包。

My question is simply: is it possible to speed up the loop or to vectorise it? 我的问题很简单:是否可以加快循环速度或使其向量化? As an R beginner, I cannot see it and don't want to end up with frustration because it may not be possible. 作为R的初学者,我看不到它,也不想以失败而告终,因为它可能无法实现。 I'll accept any answer that can say yes or no and hint towards the potential amount of effort involved. 我会接受任何可以回答“是”或“否”的答案,并暗示可能涉及的工作量。

Here is how I would have written it, using outer as a substitute for the double loop. 这是我用outer代替双循环的方式编写的。 Note that it is still doing more computations than needed, but is certainly faster. 请注意,它仍在进行比所需更多的计算,但是肯定更快。 I have assumed conn is a square matrix. 我假设conn是一个方矩阵。

The original code: 原始代码:

f1 <- function(conn) {
   for (i in 2:dim(conn)[1]) {
      for (j in 2:dim(conn)[1]) {
         if ((conn[i, 1] == conn[1, j]) & conn[i, 1] != 0) {
            conn[i, j] <- 1
            conn[j, i] <- 1
         } else {
            conn[i, j] <- 0
            conn[j, i] <- 0
         }
      }
   }
   return(conn)
}

My suggestion: 我的建议:

f2 <- function(conn) {
   matches <- 1*outer(conn[-1,1], conn[1,-1], `==`)
   matches[conn[-1,1] == 0, ] <- 0
   ind <- upper.tri(matches)
   matches[ind] <- t(matches)[ind]
   conn[-1,-1] <- matches
   return(conn)
}

Some sample data: 一些样本数据:

set.seed(12345678)
conn <- matrix(sample(1:2, 5*5, replace=TRUE), 5, 5)
conn
#      [,1] [,2] [,3] [,4] [,5]
# [1,]    2    2    1    2    1
# [2,]    1    1    2    2    1
# [3,]    2    2    1    2    1
# [4,]    2    2    2    2    1
# [5,]    1    1    2    2    1

The results: 结果:

f1(conn)
#      [,1] [,2] [,3] [,4] [,5]
# [1,]    2    2    1    2    1
# [2,]    1    0    1    1    0
# [3,]    2    1    0    0    1
# [4,]    2    1    0    1    0
# [5,]    1    0    1    0    1

identical(f1(conn), f2(conn))
# [1] TRUE

A bigger example, with time comparison: 一个更大的例子,带有时间比较:

set.seed(12345678)
conn <- matrix(sample(1:2, 1000*1000, replace=TRUE), 1000, 1000)

system.time(a1 <- f1(conn))
# user  system elapsed 
# 59.840   0.000  57.094 

system.time(a2 <- f2(conn))
# user  system elapsed 
# 0.844   0.000   0.950 

identical(a1, a2)
# [1] TRUE

Maybe not the fastest method you can get (I have no doubt other people here can find much faster using eg compiler or Rcpp), but short and fast enough for you I hope. 也许不是您可以获得的最快方法(我毫无疑问,这里的其他人可以使用编译器或Rcpp找到更快的方法),但我希望它足够简短。


Edit: since it has been pointed out (from the context of where this code was pulled from) that conn is a symmetric matrix, my solution can be shortened a bit: 编辑:由于已经指出(从提取此代码的上下文中) conn是对称矩阵,因此我的解决方案可以缩短一点:

f2 <- function(conn) {
   matches <- outer(conn[-1,1], conn[1,-1],
                    function(i,j)ifelse(i==0, FALSE, i==j)) 
   conn[-1,-1] <- as.numeric(matches)
   return(conn)
}

Non-matrixy solution - should be pretty damn fast, assuming conn is non-negative and symmetric... 非矩阵解-假设conn为非负且对称的...

connmake = function(conn){
  ordering = order(conn[,1])
  breakpoints = which(diff(conn[ordering,1]) != 0)
  if (conn[ordering[1], 1] != 0){
    breakpoints = c(1, breakpoints + 1, nrow(conn) + 1)
  } else {
    breakpoints = c(breakpoints + 1, nrow(conn) +1)
  }
  output = matrix(0, nrow(conn), nrow(conn))

  for (i in 1:(length(breakpoints) - 1)){
    output[ ordering[breakpoints[i]:(breakpoints[i+1] -1)],
        ordering[breakpoints[i]:(breakpoints[i+1] -1)]] =  1
  }
  output[,1] = conn[,1]
  output[1,] = conn[,1]
  output
}

Some test code using earlier benchmarking. 一些使用早期基准测试的测试代码。 (Original code is implemented as orig() , f2() is earlier suggestion.) (原始代码实现为orig()f2()是较早的建议。)

size = 2000
conn  = matrix(0, size, size)
conn[1,] = sample( 1:20, size, replace = T)
conn[,1] = conn[1,]

system.time(orig(conn) -> out1)
#user  system elapsed 
#20.54    0.00   20.54 
system.time(f2(conn) -> out2)
#user  system elapsed
#0.39    0.02    0.41 
system.time(connmake(conn) -> out3)
#user  system elapsed 
#0.02    0.00    0.01 
identical(out1, out2)
#[1] TRUE
identical(out1, out3)
#[1] TRUE

Note that f2 actually fails for conn containing 0, but not my problem, eh? 请注意,对于包含0的conn,f2实际上失败,但是不是我的问题,是吗? conn with negative values can be dealt with simply by eg increasing the relevant values by a safe offset. 带有负值的conn可以简单地通过例如将相关值增加一个安全偏移来处理。 Non-symmetric conn will require more thought, but should be doable... 非对称conn需要更多思考,但应该可行。

The general lesson is that sort is fast compared to pairwise comparison. 一般的教训是,与成对比较相比,排序速度更快。 Pairwise comparison is O(N^2), while the slowest sort algorithm in R is O(N^4/3). 成对比较是O(N ^ 2),而R中最慢的排序算法是O(N ^ 4/3)。 Once the data is sorted, comparisons become trivial. 数据排序后,比较变得无关紧要。

Several things come to mind. 我想到了几件事。

First, you can cut the time about in half by only looping through the entries below the diagonal or above the diagonal. 首先,您可以仅循环浏览对角线以下或对角线上方的条目,从而将时间缩短一半。 If the matrix is square either will work. 如果矩阵是正方形,则两者都可以工作。 If dim(conn)[1] > dim(conn)[2] then you'll want to loop through the bottom-left triangle using something like 如果dim(conn)[1] > dim(conn)[2]则需要使用类似以下的方法遍历左下三角形

for (j in 2:dim(conn)[2]) {
  for (i in j:dim(conn)[1]) {
    ...
  }
}

Second, one might try to use apply and it's ilk because they usually generate significant time decreases. 其次,人们可能会尝试使用apply ,这是很麻烦的,因为它们通常会减少大量时间。 However, in this case each [i,j] cell refers back to both the column head [1,j] and the row head [i,1] , which means we can't just send a cell, row or column to *pply. 但是,在这种情况下,每个[i,j]单元格都指向列头[1,j]和行头[i,1] ,这意味着我们不能只将单元格,行或列发送给* pply。 For code clarity, I would probably keep the for loops. 为了清楚起见,我可能会保留for循环。 Any *pply-based trick that worked would be so clever that I'd forget how it worked a year from now. 任何基于* pply的技巧都非常聪明,以至于我忘了一年后它是如何运作的。

Finally, this seems like a classic example of something that would be much, much faster using C called from R. This might seem like a lot of work, but it's a lot easier than you might think, even (for this particular example) if you don't know C. The first brief example of calling C from R that made sense to me was here , but it doesn't leverage Rcpp, so I wouldn't stop there. 最后,这似乎是一个经典的示例,它使用从R调用的C可以使速度大大提高。这似乎是很多工作,但比您想象的要容易得多,即使(对于此特定示例)如果您不了解C。对我来说,从R调用C的第一个简短示例在这里 ,但是它没有利用Rcpp,因此我不会就此止步。 Alternatively, if you start with any simple example of working Rcpp code then you could modify it to do what you want here. 另外,如果您从工作Rcpp代码的任何简单示例开始,那么您可以对其进行修改以执行您想要的操作。 If you just want to modify someone else's code, start with this StackOverflow thread . 如果您只想修改其他人的代码,请从此StackOverflow线程开始。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM