R 中的代数:矢量化与 foreach 与 sapply

Algebra in R: Vectorization vs. foreach vs. sapply

我有一系列包含向量、矩阵和数组的和与乘积的方程组,例如这个:

Y_i = \sum_{s=1}^S (1-alpha_{i,s})*R_i,

其中 YR 是长度为 I 且元素分别为 Y_iR_i 的向量,而 alpha 是一个具有 I 行和 S 列的矩阵。

现在我想在 R 中实现这些方程式,但要以 'mathematical readability' 的合理水平实现。特别是,我不是在寻找最短或执行速度最快的代码块,而是寻找能够直观地反映原始数学表达式的代码块。对于上面的示例,我知道计算向量 Y 的一种快速简便的方法是向量化:

Y <- rowSums((1-alpha)*R)

然而,考虑到更复杂的表达式、更多的操作和更多的维度,我发现使用 foreach 遍历所涉及的维度的循环来基本上复制纸上的方程更直观:

library(foreach)
Y <- foreach(i = 1:I, .combine = c) %:%
    foreach(s = 1:S, .combine = sum) %do% {
        (1-alpha[i,s])*R[i]
    }

我很喜欢这里的结构和.combine 参数,代码还是有些简洁的。不幸的是,这种方法的性能很糟糕,令人遗憾的是它不可行。然后我尝试了 sapply 循环:

Y <- sapply(1:I, function(i) {
    sum(
        sapply(1:S, function(s) {
            (1-alpha[i,s])*R[i]
        })
    )
})

这种方法既快速(不如矢量化方法快,但比 foreach 方法快得多)并且在数学上很直观;然而,代码阅读起来非常笨拙(七行代码只有两个维度)。因此我想问:你能想出一种更好的替代方法来解决这个问题(及其更复杂的变体)而不牺牲太多的计算速度、数学直觉或代码可读性吗?

1) for 仅对内部循环进行向量化会得到非常接近原始循环的结果。 (我们使用最后注释中的输入。)

I <- nrow(alpha)
Y <- numeric(I)
for(i in 1:I) Y[i] <- sum((1 - alpha[i, ]) * R[i])
## [1] -144 -240  -44 -144 -260 -112

2) sapply 这也适用于类似的方法:

I <- nrow(alpha)
Y <- sapply(1:I, function(i) sum((1 - alpha[i, ]) * R[i]))
## [1] -144 -240  -44 -144 -260 -112

3) fn$ 使用 gsubfn 包中的 fn$ 作为函数的前缀将允许传入参数的函数指定为公式,因此我们可以这样写:

library(gsubfn)

I <- nrow(alpha)
S <- ncol(alpha)
fn$sapply(1:I, i ~ sum(fn$sapply(1:S, s ~ (1 - alpha[i, s]) * R[i])))
## [1] -144 -240  -44 -144 -260 -112

或者为了更简洁,我们定义 iter 如图所示,并且还使用 gsubfn 的多重赋值工具一次定义 I 和 S。

library(gsubfn)

iter <- fn$sapply
list[I, S] <- dim(alpha)

iter(1:I, i ~ sum(iter(1:S, s ~ (1 - alpha[i, s]) * R[i])))
## [1] -144 -240  -44 -144 -260 -112

4) Comprehensions CRAN 上有 3 个包支持 python-like comprehensions,并对语法进行了某些修改。还有一些代码已发布 here and here and a github only package lc here,我们不会对其进行审查。这些软件包按字母顺序列在下面。

4a) 理解

library(comprehenr)
packageVersion("comprehenr") # be sure to use version 0.6.9 or later

I <- nrow(alpha)
to_vec(for(i in 1:I) sum((1-alpha[i, ])*R[i]))
## [1] -144 -240  -44 -144 -260 -112

或者有两个索引:

I <- nrow(alpha)
J <- ncol(alpha)
to_vec(for(i in 1:I) sum(to_vec(for(j in 1:J) (1-alpha[i, j])*R[i])))
## [1] -144 -240  -44 -144 -260 -112

4b) eList 有一个新的包,eList,支持列表和向量理解。这个包的一个显着特点(这里没有显示)是它支持并行处理,参数只有很小的变化。

library(eList)
packageVersion("eList") # be sure to use version 0.2,0 or later

I <- nrow(alpha)
Num(for (i in 1:I) sum((1-alpha[i, ])*R[i]))
## [1] -144 -240  -44 -144 -260 -112

或同时使用 i 和 s:

library(eList)

I <- nrow(alpha)
S <- ncol(alpha)

Num(for(i in 1:I) Sum(for(s in 1:S) (1-alpha[i,s]) * R[i]))
## [1] -144 -240  -44 -144 -260 -112

4c) listcompr 这是另一个支持理解的包。它的语法与上述两个略有不同,更接近 Python.

library(listcompr)

I <- nrow(alpha)
gen.vector(sum((1-alpha[i, ])*R[i]), i = 1:I)
## [1] -144 -240  -44 -144 -260 -112

或同时使用两个索引:

I <- nrow(alpha)
J <- ncol(alpha)
gen.vector(sum(gen.vector((1-alpha[i, j])*R[i], j = 1:J)), i = 1:I)
## [1] -144 -240  -44 -144 -260 -112

5) nimble 如果以上方法不够快,我们可以考虑使用 nimble 包,它将类似 R 的代码和一些类型定义翻译成 C++。

library(nimble)

calc <- nimbleFunction(
  run = function(alpha = double(2), R = double(1)) {
    I <- dim(alpha)[1]
    Y <- numeric(I)
    for(i in 1:I) Y[i] <- sum((1 - alpha[i, ]) * R[i])
    return(Y)
    returnType(double(1))
  }
)

Ccalc <- compileNimble(calc)

# test
Ccalc(alpha, R)
## [1] -144 -240  -44 -144 -260 -112

6) einsum einsum 包支持爱因斯坦张量符号。第一个参数的左侧用逗号分隔成两组,每组在后续参数的一个输入中定义索引。右侧的索引是输出的相应索引。此包具有生成 C++ 代码然后执行它的能力(此处未显示)。

library(einsum)

einsum("ij,i -> i", 1-alpha, R)
## [1] -144 -240  -44 -144 -260 -112

备注

用于测试的一些输入:

alpha <- matrix(1:24, 6)
R <- c(4, 6, 1, 3, 5, 2)

更新

根据新版本和其他发现重新安排了演示文稿,添加了其他方法并更新了理解部分。