【问题标题】:3D Matrix Multiplication in RR中的3D矩阵乘法
【发布时间】:2019-05-06 16:50:04
【问题描述】:

我有一个简单的问题。我想在不使用 for 循环的情况下将 3D 数组乘以 R 中的另一个 3D 数组。

举例说明

假设我有一个 1x3 矩阵 A:

[A1, A2, A3] 

我有一个 3x3 矩阵 B:

[B1, B2, B3 \\
 B4, B5, B6 \\
 B7, B8, B9]

我的主要操作是A %*% B,得到一个 1x3 矩阵。

但现在我想重复这个过程 10,000 次,每次都有不同的 A 和 B,尺寸与上述相同。我可以使用for循环

for (i in 1:10000) {
     A[i] %*% B[i]
}

然后我可以存储 10,000 个值。

但是有什么方法可以在不使用 for 循环的情况下实现相同的目标。我正在考虑可能是 3D 数组乘法。但我不确定如何在 R 中做到这一点。

Matrix A: 1 x 3 x 10000

[A1, A2, A3] 

Matrix B: 3 x 3 x 10000

[B1, B2, B3
 B4, B5, B6
 B7, B8, B9]

另外,矢量化有帮助吗?

你们能帮忙吗?谢谢!

【问题讨论】:

    标签: r matrix tensor


    【解决方案1】:

    有几种方法可以通过数组乘法来完成此操作。您付出的代价是将矩阵重新格式化为具有许多零的更大张量。根据定义,这些是稀疏的,因此主要成本是转换的开销。当你有 10,000 个数组要相乘时,它实际上优于循环。

    n 为 (A,B) 对的数量,k=3 为维度。

    最时尚的解决方案似乎是将An 行(n by k 矩阵)重组为n*k by n*k k by @ 块对角矩阵987654330@ 块。块ii=1..n,在其顶行包含A 的行i,否则为零。将此(右侧)乘以B(排列为k*n 乘以k 矩阵,由n 维度块kk 的“堆栈”组成)计算所有单个产品,将它们存放在结果的第 1、k+1、2k+1、...行,在那里它们可以被挑选出来。

    f3 <- function(a, b) {
      require(RcppArmadillo) # sparseMatrix package
      n <- dim(b)[3]
      k <- dim(b)[2]
      i0 <- (1:n-1)*k+1
      i <- rep(i0, each=k)
      j <- 1:(k*n)
      aa <- sparseMatrix(i, j, x=c(t(a)), dims=c(n*k, n*k))
      bb <- matrix(aperm(b, c(1,3,2)), nrow=n*k)
      t((aa %*% bb)[i0, ])
    }
    

    如您所见,数组操作是基本的:创建稀疏矩阵、转置数组(使用apermt)和乘法。它以k by n 数组(如果您愿意,可以转置)返回结果,每列一个结果向量。

    作为测试,这里是一个使用相同数组数据结构的暴力循环。

    f1 <- function(a, b) sapply(1:nrow(a), function(i) a[i,] %*% b[,,i])
    

    我们可以将这些解决方案应用于相同的输入并比较结果:

    #
    # Create random matrices for testing.
    #
    k <- 3
    n <- 1e6  # Number of (a,B) pairs
    a <- matrix(runif(k*n), ncol=k)
    b <- array(runif(k^2*n), dim=c(k,k,n))
    
    system.time(c1 <- f1(a,b)) # 4+ seconds
    system.time(c3 <- f3(a,b)) # 2/3 second
    
    mean((c1-c3)^2) # Want around 10^-32 or less
    

    结果并不完全相等,但它们的均方差小于 10^-32,表明它们可以被认为是相同的,直到浮点舍入误差。

    面向数组的过程f3 最初比循环过程f1 慢,但在n 为10,000 时赶上。之后,它的速度大约是原来的两倍或更好(在这台机器上;YMMV)。两种算法都应该在 n 中线性扩展(而且时间表明它们确实如此,至少扩展到 n=10,000,000)。

    【讨论】:

    • sparseMatrix() 不是由 RcppArmadillo 包定义的。您是否打算改为使用 Matrix 包?
    • +1 用于简洁的稀疏矩阵解决方案。不过,循环方法可以更有效地实施——请参阅我的答案。
    【解决方案2】:

    如果你的ABlists,你可以使用mapply()

    > nn <- 1e1
    > set.seed(1)
    > A <- replicate(nn,matrix(rnorm(3),nrow=1),simplify=FALSE)
    > B <- replicate(nn,matrix(rnorm(9),nrow=3),simplify=FALSE)
    > head(mapply("%*%",A,B,SIMPLIFY=FALSE),3)
    [[1]]
              [,1]      [,2]       [,3]
    [1,] -1.193976 0.1275999 -0.6831007
    
    [[2]]
             [,1]     [,2]      [,3]
    [1,] 1.371143 1.860379 -1.639078
    
    [[3]]
              [,1]       [,2]     [,3]
    [1,] 0.8250047 -0.6967286 1.949236
    

    【讨论】:

      【解决方案3】:

      for循环比你想象的更高效

      您的n (A,B) 对相乘问题并不等同于通常意义上的张量乘法,尽管 whuber 提出了一种非常巧妙的方法,通过将 B 作为块堆叠在稀疏矩阵。

      您曾说过要避免使用 for 循环,但是当高效编程时,for-loop 方法实际上非常具有竞争力,我建议您重新考虑一下。

      我将使用与 whuber 相同的符号,A 的维度为 n x k,B 的维度为 k x k x n,例如:

      n <- 1e4
      k <- 3
      A <- array(rnorm(k*n),c(n,k))
      B <- array(rnorm(k*k*n),c(k,k,n))
      

      一个简单有效的for循环解决方案应该是这样的

      justAForLoop <- function(A,B) {
        n <- nrow(A)
        for (i in 1:n) A[i,] <- A[i,] %*% B[,,i]
        A
      }
      

      产生一个 n x k 矩阵的结果。

      我修改了whuber的f3函数加载Matrix包,否则sparseMatrix函数不可用。我的f3 版本比原来的版本快得多,因为我在返回结果之前已经消除了最后一个矩阵转置。 通过此修改,它会向justAForLoop 返回相同的数值结果。

      f3 <- function(a, b) {
        require(Matrix)
        n <- dim(b)[3]
        k <- dim(b)[2]
        i0 <- (1:n-1)*k+1
        i <- rep(i0, each=k)
        j <- 1:(k*n)
        aa <- sparseMatrix(i, j, x=c(t(a)), dims=c(n*k, n*k))
        bb <- matrix(aperm(b, c(1,3,2)), nrow=n*k)
        (aa %*% bb)[i0, ]
      }
      

      现在我在新的 R 会话中重新运行 whuber 的模拟:

      > k <- 3
      > n <- 1e6
      > a <- matrix(runif(k*n), ncol=k)
      > b <- array(runif(k^2*n), dim=c(k,k,n))
      > 
      > system.time(c1 <- f1(a,b))
         user  system elapsed 
         3.40    0.09    3.50 
      > system.time(c3 <- f3(a,b))
      Loading required package: Matrix
         user  system elapsed 
         1.06    0.24    1.30 
      > system.time(c4 <- justAForLoop(a,b))
         user  system elapsed 
         1.27    0.00    1.26 
      

      for-loop 方法实际上是最快的,差一点。它比依赖sapplyf1 快得多。 (我的机器是运行 R 3.6.0 的具有 32Gb RAM 的 Windows 10 PC)。

      如果我再次运行所有三个方法,那么f3 将成为最快的,因为这一次 Matrix 包已经在搜索路径中并且不必重新加载:

      > system.time(c1 <- f1(a,b))
         user  system elapsed 
         3.23    0.04    3.26 
      > system.time(c3 <- f3(a,b))
         user  system elapsed 
         0.33    0.20    0.53 
      > system.time(c4 <- justAForLoop(a,b))
         user  system elapsed 
         1.28    0.01    1.30 
      

      但是f3 使用的 RAM 比 for 循环多。在我的电脑上,我可以使用n=1e8 成功运行justAForLoop,而f1f3 都内存不足而失败。

      总结

      直接的for循环方法比sapply高效得多。

      对于 n=10,000 矩阵乘法的问题,运行 for 循环既简单又高效,耗时

      对于介于 1-1000 万之间的 n,whuber 的稀疏矩阵解决方案开始表现出色,尤其是在 Matrix 包已经加载的情况下。

      for 循环使用三种方法中最少的 RAM。对于n 在我的具有 32Gb RAM 的 PC 上的 1 亿,只有 for 循环方法有效。

      【讨论】:

      • 做得很好——谢谢。您可能已经知道我特别欣赏对计算资源权衡的分析。 :-) 我希望稀疏矩阵有大约 100% 到 200+% 的存储开销,具体取决于其索引的数据类型。
      • 感谢两位的回答,非常感谢!当 k = 3 时,您的稀疏矩阵解决方案确实优于简单的 for 循环方法,但是当 k > 10 时,它变得比简单的 for 循环更差。你们知道为什么吗?
      • 我记得用 k 到 50 左右测试这些程序。稀疏矩阵方法在我的机器上仍然保持更快,但只有两倍左右。原因是这些技巧的收获较少,因为大部分工作都由矩阵乘法组成,并且在 k 较大的情况下,无论您实施哪种解决方案,都已经非常有效地完成了。
      • @whuber 你用的是哪台机器?在我的机器上,您的稀疏矩阵解决方案在 k=15 时变慢了。你知道这是为什么吗?我使用了您在此处发布的确切代码。对于 k=3,我机器上的计算时间与您在上面评论的时间非常接近。
      • 这是一个很好的问题——这就是我在回答中写“YMMV”的原因。对于这些基准测试,我在一个四核 Xeon 机器、24 GB RAM 上使用了 Windows 中的 Microsoft R。对于某些线性代数运算,此设置比标准 R 分布快约三倍。我还没有研究过它如何与大数据一起扩展。
      猜你喜欢
      • 2020-04-09
      • 1970-01-01
      • 2021-04-11
      • 2013-04-16
      • 1970-01-01
      • 2016-05-11
      • 1970-01-01
      • 2022-06-30
      • 2021-01-31
      相关资源
      最近更新 更多