在 R 中快速生成 ~ 10^9 个随机过程步骤
Rapidly generating ~ 10^9 steps of a random process in R
我有以下任务要执行:
Generate 10^9 steps of the process described by the formula:
X(0)=0
X(t+1)=X(t)+Y(t)
where Y(t)
are independent random variables with the distribution N(0,1)
. Calculate in what percentage of indices t
the value of X(t)
was negative.
我尝试了以下代码:
x<-c(0,0)
z<-0
loop<-10^9
for(i in 2:loop) {
x[1]<-x[2]
x[2]<-x[1]+rnorm(1, 0, 1)
if (x[2]<0) {z<-z+1}
}
但是,速度很慢。我怎样才能加快速度?
这应该会快得多,但是十亿的任何东西都可能需要一段时间。用较小的长度值来测试它可能会很好 - 比如 10^6.
length = 10^9
Y = rnorm(length)
sum(cumsum(Y)<0)/length
编辑
根据@user3666197 的评论,我对此进行了测试,他是正确的。
该解决方案适用于较小的数字,但一旦步数变得太大,它就会失败。
我根据 OP 的代码测试了我的 "vectorized" 版本。当随机游走的长度为 10^8 时,我的代码花费了大约 7 秒,而 OP 的代码花费了 131 秒(在我的笔记本电脑上)。但是,当我将长度增加到 10^9(根据原始问题)时,我的版本导致大量磁盘交换,我不得不终止该进程。此解决方案在 OP 要求的规模下失败。
鉴于随机源在技术上被构造为确定性硬件的能力,以满足生成流的可重复性要求以及"generated"-随机性的所有条件。 -random generator algorithm,这种随机源不容易从纯 [SERIAL]
转换为任何形式的 "just"-[CONCURRENT]
或 true-[PARALLEL]
作案手法。
也就是说,PRG 步是任何尝试重新定义纯 [SERIAL]
代码执行的中心点(阻塞)。
这不会改变(非)负 X(t)
值的百分比,而只是确定对于给定的 PRG 硬件实现,没有更短的方法,但是纯 [SERIAL]
相互(串行)相关值的生成顺序。
展开 "slow" 循环或准( 因为值仍然是串行相关的)-向量化处理(R 语言实现具有利用但几乎硬件 CPU-指令集级别的技巧——所以不是语言游戏规则的改变者,而是绕过一些明知缓慢的代码执行构造函数)是最可能发生的事情。
一般来说,对于像这样的问题,您可以使用 Rcpp 包将您的函数一对一地翻译成 C++。这应该会带来相当大的加速。
首先是R版本:
random_sum <- function(loop = 1000) {
x<-c(0,0)
z<-0
for(i in 2:loop) {
x[1]<-x[2]
x[2]<-x[1]+rnorm(1, 0, 1)
if (x[2]<0) {z<-z+1}
}
z / loop
}
set.seed(123)
random_sum()
# [1] 0.134
现在的C++版本:
library("Rcpp")
cppFunction("
double random_sum_cpp(unsigned long loop = 1000) {
double x1 = 0;
double x2 = 0;
double z = 0;
for (unsigned long i = 2; i < loop; i++) {
x1 = x2;
x2 = x1 + Rcpp::rnorm(1)[0];
if (x2 < 0) z = z+1;
}
return z/loop;
}")
set.seed(123)
random_sum_cpp()
# [1] 0.134
为了完整起见,我们还考虑一下提出的矢量化版本:
random_sum_vector <- function(loop = 1000) {
Y = rnorm(loop)
sum(cumsum(Y)<0)/loop
}
set.seed(123)
random_sum_vector()
# [1] 0.134
我们看到它对相同的随机种子给出了相同的结果,因此它似乎是一个可行的竞争者。
在基准测试中,C++ 版本和矢量化版本的性能相似,矢量化版本略优于 C++ 版本:
> microbenchmark(random_sum(100000),
random_sum_vector(100000),
random_sum_cpp(100000))
Unit: milliseconds
expr min lq mean median uq max neval
random_sum(1e+05) 184.205588 199.859266 209.220232 205.137043 211.026740 274.47615 100
random_sum_vector(1e+05) 6.320690 6.631704 7.273645 6.799093 7.334733 18.48649 100
random_sum_cpp(1e+05) 8.950091 9.362303 10.663295 9.956996 11.079513 21.30898 100
但是,向量化版本牺牲了速度和内存,C++ 版本几乎不使用内存。
对于 10^9 个步骤,C++ 版本在我的机器上运行大约需要 2 分钟(110 秒)。我没有尝试 R 版本。根据较短的基准测试,可能需要大约 7 个小时。
> microbenchmark(random_sum_cpp(10^9), times = 1)
Unit: seconds
expr min lq mean median uq max neval
random_sum_cpp(10^9) 110.2182 110.2182 110.2182 110.2182 110.2182 110.2182 1
一种解决方案是使用@G5W 提出的矢量化,但将其分成更小的块以避免任何内存溢出问题。这为您提供了矢量化解决方案的速度,但通过管理块大小,您可以控制进程使用的内存量。
下面将问题分解为 1e+07 块,循环 100 次总共得到 1e+09。
在第一个块的末尾,记录低于 0 的时间百分比和结束点。然后结束点被送入下一个区块,你记录低于0的时间百分比,以及新的结束点。
最后,计算 100 次运行的平均值,使总时间低于零。 while循环中调用cat
是为了监控进度,查看进度,这个可以注释掉
funky <- function(start, length = 1e+07) {
Y <- rnorm(length)
Z <- cumsum(Y)
c(sum(Z<(-start))/length, (tail(Z, 1) + start))
}
starttime <- Sys.time()
resvect <- vector(mode = "numeric", length = 100)
result <- funky(0)
resvect[1] <- result[1]
i <- 2
while (i < 101) {
cat(result, "\n")
result <- funky(result[2])
resvect[i] <- result[1]
i <- i + 1
}
mean(resvect)
# [1] 0.1880392
endtime <- Sys.time()
elapsed <- endtime - starttime
elapsed
# Time difference of 1.207566 mins
使用向量通常会比 for 循环产生更好的性能。非常大的数字(即 10^9)的问题是内存限制。由于您只对负指数的最终百分比感兴趣,因此以下将起作用(以 10^9 步长花费几分钟)。
update_state <- function (curr_state, step_size) {
n <- min(curr_state$counter, step_size)
r <- rnorm(min(curr_state$counter, step_size))
total <- curr_state$cum_sum + cumsum(r)
list('counter' = curr_state$counter - n,
'neg_count' = curr_state$neg_count + length(which(total < 0)),
'cum_sum' = curr_state$cum_sum + sum(r))
}
n <- 10^9
curr_state <- list('counter' = n, 'neg_count' = 0, 'cum_sum' = 0)
step_size <- 10^8
while (curr_state$counter > 0) {
curr_state <- update_state(curr_state = curr_state, step_size = step_size)
}
print(curr_state)
print(curr_state$neg_count/ n)
我有以下任务要执行:
Generate 10^9 steps of the process described by the formula:
X(0)=0 X(t+1)=X(t)+Y(t)
where
Y(t)
are independent random variables with the distributionN(0,1)
. Calculate in what percentage of indicest
the value ofX(t)
was negative.
我尝试了以下代码:
x<-c(0,0)
z<-0
loop<-10^9
for(i in 2:loop) {
x[1]<-x[2]
x[2]<-x[1]+rnorm(1, 0, 1)
if (x[2]<0) {z<-z+1}
}
但是,速度很慢。我怎样才能加快速度?
这应该会快得多,但是十亿的任何东西都可能需要一段时间。用较小的长度值来测试它可能会很好 - 比如 10^6.
length = 10^9
Y = rnorm(length)
sum(cumsum(Y)<0)/length
编辑
根据@user3666197 的评论,我对此进行了测试,他是正确的。 该解决方案适用于较小的数字,但一旦步数变得太大,它就会失败。
我根据 OP 的代码测试了我的 "vectorized" 版本。当随机游走的长度为 10^8 时,我的代码花费了大约 7 秒,而 OP 的代码花费了 131 秒(在我的笔记本电脑上)。但是,当我将长度增加到 10^9(根据原始问题)时,我的版本导致大量磁盘交换,我不得不终止该进程。此解决方案在 OP 要求的规模下失败。
鉴于随机源在技术上被构造为确定性硬件的能力,以满足生成流的可重复性要求以及"generated"-随机性的所有条件。 -random generator algorithm,这种随机源不容易从纯 [SERIAL]
转换为任何形式的 "just"-[CONCURRENT]
或 true-[PARALLEL]
作案手法。
也就是说,PRG 步是任何尝试重新定义纯 [SERIAL]
代码执行的中心点(阻塞)。
这不会改变(非)负 X(t)
值的百分比,而只是确定对于给定的 PRG 硬件实现,没有更短的方法,但是纯 [SERIAL]
相互(串行)相关值的生成顺序。
展开 "slow" 循环或准( 因为值仍然是串行相关的)-向量化处理(R 语言实现具有利用但几乎硬件 CPU-指令集级别的技巧——所以不是语言游戏规则的改变者,而是绕过一些明知缓慢的代码执行构造函数)是最可能发生的事情。
一般来说,对于像这样的问题,您可以使用 Rcpp 包将您的函数一对一地翻译成 C++。这应该会带来相当大的加速。
首先是R版本:
random_sum <- function(loop = 1000) {
x<-c(0,0)
z<-0
for(i in 2:loop) {
x[1]<-x[2]
x[2]<-x[1]+rnorm(1, 0, 1)
if (x[2]<0) {z<-z+1}
}
z / loop
}
set.seed(123)
random_sum()
# [1] 0.134
现在的C++版本:
library("Rcpp")
cppFunction("
double random_sum_cpp(unsigned long loop = 1000) {
double x1 = 0;
double x2 = 0;
double z = 0;
for (unsigned long i = 2; i < loop; i++) {
x1 = x2;
x2 = x1 + Rcpp::rnorm(1)[0];
if (x2 < 0) z = z+1;
}
return z/loop;
}")
set.seed(123)
random_sum_cpp()
# [1] 0.134
为了完整起见,我们还考虑一下提出的矢量化版本:
random_sum_vector <- function(loop = 1000) {
Y = rnorm(loop)
sum(cumsum(Y)<0)/loop
}
set.seed(123)
random_sum_vector()
# [1] 0.134
我们看到它对相同的随机种子给出了相同的结果,因此它似乎是一个可行的竞争者。
在基准测试中,C++ 版本和矢量化版本的性能相似,矢量化版本略优于 C++ 版本:
> microbenchmark(random_sum(100000),
random_sum_vector(100000),
random_sum_cpp(100000))
Unit: milliseconds
expr min lq mean median uq max neval
random_sum(1e+05) 184.205588 199.859266 209.220232 205.137043 211.026740 274.47615 100
random_sum_vector(1e+05) 6.320690 6.631704 7.273645 6.799093 7.334733 18.48649 100
random_sum_cpp(1e+05) 8.950091 9.362303 10.663295 9.956996 11.079513 21.30898 100
但是,向量化版本牺牲了速度和内存,
对于 10^9 个步骤,C++ 版本在我的机器上运行大约需要 2 分钟(110 秒)。我没有尝试 R 版本。根据较短的基准测试,可能需要大约 7 个小时。
> microbenchmark(random_sum_cpp(10^9), times = 1)
Unit: seconds
expr min lq mean median uq max neval
random_sum_cpp(10^9) 110.2182 110.2182 110.2182 110.2182 110.2182 110.2182 1
一种解决方案是使用@G5W 提出的矢量化,但将其分成更小的块以避免任何内存溢出问题。这为您提供了矢量化解决方案的速度,但通过管理块大小,您可以控制进程使用的内存量。
下面将问题分解为 1e+07 块,循环 100 次总共得到 1e+09。
在第一个块的末尾,记录低于 0 的时间百分比和结束点。然后结束点被送入下一个区块,你记录低于0的时间百分比,以及新的结束点。
最后,计算 100 次运行的平均值,使总时间低于零。 while循环中调用cat
是为了监控进度,查看进度,这个可以注释掉
funky <- function(start, length = 1e+07) {
Y <- rnorm(length)
Z <- cumsum(Y)
c(sum(Z<(-start))/length, (tail(Z, 1) + start))
}
starttime <- Sys.time()
resvect <- vector(mode = "numeric", length = 100)
result <- funky(0)
resvect[1] <- result[1]
i <- 2
while (i < 101) {
cat(result, "\n")
result <- funky(result[2])
resvect[i] <- result[1]
i <- i + 1
}
mean(resvect)
# [1] 0.1880392
endtime <- Sys.time()
elapsed <- endtime - starttime
elapsed
# Time difference of 1.207566 mins
使用向量通常会比 for 循环产生更好的性能。非常大的数字(即 10^9)的问题是内存限制。由于您只对负指数的最终百分比感兴趣,因此以下将起作用(以 10^9 步长花费几分钟)。
update_state <- function (curr_state, step_size) {
n <- min(curr_state$counter, step_size)
r <- rnorm(min(curr_state$counter, step_size))
total <- curr_state$cum_sum + cumsum(r)
list('counter' = curr_state$counter - n,
'neg_count' = curr_state$neg_count + length(which(total < 0)),
'cum_sum' = curr_state$cum_sum + sum(r))
}
n <- 10^9
curr_state <- list('counter' = n, 'neg_count' = 0, 'cum_sum' = 0)
step_size <- 10^8
while (curr_state$counter > 0) {
curr_state <- update_state(curr_state = curr_state, step_size = step_size)
}
print(curr_state)
print(curr_state$neg_count/ n)