【问题标题】:Algebra in R: Vectorization vs. foreach vs. sapplyR中的代数:向量化与foreach与sapply
【发布时间】:2021-01-11 13:50:41
【问题描述】:

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

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

其中YR 是长度为I 的向量,其元素分别为Y_iR_ialpha 是具有I 行和S 列的矩阵。

现在我想在 R 中实现这些方程,但这样做要具有合理的“数学可读性”水平。特别是,我不是在寻找最短或最快执行的代码块,而是直观地反映原始数学表达式的代码块。对于上面的示例,我知道计算向量 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 方法快)和数学直观;但是,代码读起来很笨拙(只有二维七行)。因此我想问一下:你能想出一个更好的替代方法来解决这个问题(以及更复杂的变体)而不牺牲太多的计算速度、数学直觉或代码可读性吗?

【问题讨论】:

  • "preferable" 含糊不清,以至于过于基于意见。如果你可以向量化计算并且它比其他方法更快,为什么不坚持呢?直觉和可读性来自经验。 R 本身并不是最直观和易读的编程语言。如果您找到了一种惯用的(在 R 意义上)使其工作的方式,何必担心呢?
  • 我不相信你的目标是合理的(甚至一般可以实现)。在任何情况下,都需要更加明确和具体地定义它。此外,您还需要考虑浮点精度。在某些应用程序中,这比您的所有其他问题更重要。
  • 如果你想要代码可读性或数学直觉,最好直接将数学语言翻译成你的代码,例如嵌套for循环。如果您更关心计算速度,则应该重新编写数学语言并对其进行优化作为第一步,这是以可读性为代价的。两个方面是取舍。取决于你真正追求什么。
  • 您可以做的一件事是创建更具可读性的自定义函数。我一直这样做。然后你就有了可读的顶行代码,你可以深入研究。
  • 感谢大家的意见。我主要担心的是可能有一个(我已经在做的功能/包/变体)我还不知道,但它完全符合我的要求。不幸的是,我想情况并非如此。我同意@JohnColeman 的观点,最终,只要有一些经验,就可以很容易地立即写下矢量化版本而无需考虑太多。现在,我将继续使用sapply 循环来测试我的代码,然后通过在我认为合适的地方引入矢量化来修改它。感谢您的讨论!

标签: r algebra


【解决方案1】:

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 的理解,但对语法进行了某些修改。还发布了一些代码 herehere 以及一个仅限 github 的包 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)

更新

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

【讨论】:

  • 谢谢,特别是在eListnimble 中提示我。快速查询:如何使用eList 语法实现完整的嵌套循环?我到了Num(for (i in 1:I) for (s in 1:S) (1-alpha[i,s])*R[i]),但不知道如何在循环内对s 维度求和,而不只是应用外部rowSums
  • 如果有一个Sum 运算符并且如果它工作它大概是Num(for(i in 1:I) Sum(for(s in 1:S) (1-alpha[i,s]) * R[i])) 但似乎这种嵌套理解在包版本0.0.1.0 中不起作用。在我回答问题时,我确实向作者发送了一封指出问题的电子邮件。
  • 谢谢,我也试过了。 SumProd 作为运营商确实很方便。无论如何,我仍然需要看看我能多容易地习惯各种不同的方法。我目前的策略可能是从 eList/sapply 开始以获得数学直觉,然后尝试尽可能矢量化,并最终切换到 nimble/Rcpp 以获得最终的性能提升。我会将这个问题留待更长时间,以便其他人发表他们的想法。
  • 添加了 einsum 包。
猜你喜欢
  • 1970-01-01
  • 2016-07-11
  • 1970-01-01
  • 1970-01-01
  • 2011-01-17
  • 1970-01-01
  • 1970-01-01
  • 2011-11-28
  • 1970-01-01
相关资源
最近更新 更多