R︱foreach+doParallel并行+联用迭代器优化内存+并行机器学习算法

本文详细介绍R语言中foreach包的使用方法,包括基本概念、参数解读及应用案例,涵盖从入门到进阶的并行计算技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

要学的东西太多,无笔记不能学~~ 欢迎关注公众号,一起分享学习笔记,记录每一颗“贝壳”~

———————————————————————————


      接着之前写的并行算法parallel包,parallel相比foreach来说,相当于是foreach的进阶版,好多东西封装了。而foreach包更为基础,而且可自定义的内容很多,而且实用性比较强,可以简单的用,也可以用得很复杂。笔者将自己的学习笔记记录一下。

      R︱并行计算以及提高运算效率的方式(parallel包、clusterExport函数、SupR包简介)


——————————————————————————————————————




一、foreach包简介与主要函数解读


      foreach包是revolutionanalytics公司贡献给R开源社区的一个包,它能使R中的并行计算更为方便。大多数并行计算都主要完成三件事情:将问题分割小块、对小块问题进行并行计算、合并计算结果。foreach包中,迭代器完成分割工作,”%dopar%“函数实现对小块的并行计算,”.combine”函数完成合并工作。

foreach(..., .combine, .init, .final=NULL, .inorder=TRUE,
       .multicombine=FALSE,
       .maxcombine=if (.multicombine) 100 else 2,
       .errorhandling=c('stop', 'remove', 'pass'),
       .packages=NULL, .export=NULL, .noexport=NULL,
       .verbose=FALSE)
when(cond)
e1 %:% e2
obj %do% ex
obj %dopar% ex
times(n)

参数解读:

      (1)%do%严格按照顺序执行任务(所以,也就非并行计算),%dopar%并行执行任务,%do%时候就像sapply或lapply,%dopar%就是并行启动器

      (2.combine:运算之后结果的显示方式,default是list,“c”返回vector, cbind和rbind返回矩阵,"+"和"*"可以返回rbind之后的“+”或者“*”,帮你把数据整合起来,太良心了!!
      (3).init:.combine函数的第一个变量
      (4).final:返回最后结果
      (5).inorder:TRUE则返回和原始输入相同顺序的结果(对结果的顺序要求严格的时候),FALSE返回没有顺序的结果(可以提高运算效率)。这个参数适合于设定对结果顺序没有需求的情况。
      (6).muticombine:设定.combine函数的传递参数,default是FALSE表示其参数是2,TRUE可以设定多个参数
      (7).maxcombine:设定.combine的最大参数
      (8).errorhandling:如果循环中出现错误,对错误的处理方法
      (9).packages:指定在%dopar%运算过程中依赖的package(%do%会忽略这个选项),用于并行一些机器学习算法。
      (10).export:在编译函数的时候需要预先加载一些内容进去,类似parallel的clusterExport


      如果你不知道自己的机器有没有启动并行,你可以通过以下的函数来进行查看,帮助你理解自己电脑的核心数:

getDoParWorkers( )    #查看注册了多少个核,配合doMC package中的registerDoMC( )使用
getDoParRegistered( ) # 查看doPar是否注册;如果没有注册返回FALSE
getDoParName( )       #查看已经注册的doPar的名字
getDoParVersion( )    #查看已经注册的doPar的version

本节内容主要参考:R语言处理大数据


——————————————————————————————————————




二、新手教程:foreach应用


1、最简单模式——堪比lapply


foreach(a=1:3, b=rep(10, 3)) %do% {
  a + b
}
## [[1]]
## [1] 11
## 
## [[2]]
## [1] 12
## 
## [[3]]
## [1] 13
      这个并不是并行,只是有着类似lapply的功能。
      foreach返回的是list格式值,list格式是默认的数据格式。来看看上面的内容怎么用lapply实现:

lapply(cbind(1:3,rep(10,3),function(x,y) x+y ))


但是有个小细节就是,%do%之后的{}可以随意写中间赋值过程,譬如c<-a+b,这个用lapply不是特别好写。所以这个我超级喜欢!

这里需要注意的一点是:a, b叫循环变量,循环次数取两者长度中最小的。譬如a=1,2,3 b=1,2,也就只能循环两次。


2、参数:.combine——定义输出结果的整合


      默认是foreach之后返回的是list,你可以指定自己想要的格式。.combine选项连接了“c”函数,该函数的功能是连接所有返回值组成向量。此外,我们可以使用“cbind”将生成的多个向量组合成矩阵,例如生成四组随机数向量,进而按列合并成矩阵:


foreach(i=1:4, .combine="cbind") %do% rnorm(4)
##      result.1 result.2 result.3 result.4
## [1,]  0.26634 -0.73193 -0.25927   0.8632
## [2,]  0.54132  0.08586  1.46398  -0.6995
## [3,] -0.15619  0.85427 -0.47997   0.2160
## [4,]  0.02697 -1.40507 -0.06972   0.2252

      运算之后结果的显示方式,default是list,“c”返回vector, cbind和rbind返回矩阵,"+"和"*"可以返回rbind之后的“+”或者“*”,帮你把数据整合起来。

      .combine还可以接上自己编译的函数,这点很人性化,譬如:


cfun <- function(a, b)  a+b
foreach(i=1:4, .combine="cfun") %do% rnorm(4)


.combine帮你把输出结果,再要调整的问题一次性解决了,并且将数据整合也进行并行加速,棒!


一些关于.combine的c,rbind,cbind,*,+其他案例:


 x <- foreach(a=1:3, b=rep(10, 3), .combine="c") %do%
 {
    x1<-(a + b);
     x2<-a*b;
     c(x1,x2);  
 }
> x
[1] 11 10 12 20 13 30
> x <- foreach(a=1:3, b=rep(10, 3), .combine="rbind") %do%
 {
    x1<-(a + b);
     x2<-a*b;
     c(x1,x2);  
 }
> x
         [,1] [,2]
result.1   11   10
result.2   12   20
result.3   13   30
> x <- foreach(a=1:3, b=rep(10, 3), .combine="cbind") %do%
 {
     x1<-(a + b);
     x2<-a*b;
     c(x1,x2);  
 }
> x
     result.1 result.2 result.3
[1,]       11       12       13
[2,]       10       20       30
> x <- foreach(a=1:3, b=rep(10, 3), .combine="+") %do%
 {
     x1<-(a + b);
     x2<-a*b;
     c(x1,x2);  
 }
> x
[1] 36 60
> x <- foreach(a=1:3, b=rep(10, 3), .combine="*") %do%
 {
    x1<-(a + b);
     x2<-a*b;
     c(x1,x2);  
 }
> x
[1] 1716 6000


3、参数.inorder——定义输出结果的顺序


      .inorder:TRUE则返回和原始输入相同顺序的结果(对结果的顺序要求严格的时候),FALSE返回没有顺序的结果(可以提高运算效率)。这个参数适合于设定对结果顺序没有需求的情况。


      顺序这东西,写过稍微复杂的函数都知道,特别在数据匹配时尤为重要,因为你需要定义一些rownames的名称,这时候输出的顺序万一不匹配,可能后面还要花时间匹配过来。


foreach(i=4:1, .combine='c', .inorder=FALSE) %dopar% {
  Sys.sleep(3 * i)
  i
}
## [1] 4 3 2 1


——————————————————————————————————————




三、中级教程:利用doParallel并行+联用迭代器优化内存

1、利用doParallel并行——%dopar%

      foreach包创作是为了解决一些并行计算问题,将”%do%“更改为“%dopar%”前面例子就可以实现并行计算。在并行之前,需要register注册集群:

library(foreach)
library(doParallel)

cl<-makeCluster(no_cores)
registerDoParallel(cl)
      要记得最后要结束集群(不是用stopCluster()):stopImplicitCluster()


2、参数when——按条件运算

foreach(a=irnorm(1, count=10), .combine='c') %:% when(a >= 0) %do% sqrt(a)

      其中when是通过%:%来导出,而且%:%之后,还可以接%do%

qsort <- function(x) {
  n <- length(x)
  if (n == 0) {
    x
    } else {
      p <- sample(n, 1)
      smaller <- foreach(y=x[-p], .combine=c) %:% when(y <= x[p]) %do% y
      larger <- foreach(y=x[-p], .combine=c) %:% when(y > x[p]) %do% y
      c(qsort(smaller), x[p], qsort(larger))
      }
}

qsort(runif(12))
##  [1] 0.1481 0.2000 0.2769 0.4729 0.4747 0.5730 0.6394 0.6524 0.8315 0.8325
## [11] 0.8413 0.8724


3、联用iterators——优化、控制内存


      iterators是为了给foreach提供循环变量,每次定义一个iterator,它都内定了“循环次数”和“每次循环返回的值”,因此非常适合结合foreach的使用。
      iter(obj, ...):可以接受iter, vector, matrix, data.frame, function。
      nextElem(obj, ...):接受iter对象,显示对象数值。

      以matrix为例,

iter(obj, by=c('column', 'cell', 'row'), chunksize=1L, checkFunc=function(...) TRUE, recycle=FALSE, ...)

      参数解读:
      by:按照什么顺序循环;
      matrix和data.frame都默认是“row”,“cell”是按列依次输出(所以对于“cell”,chunksize只能指定为默认值,即1)
      chunksize:每次执行函数nextElem后,按照by的设定返回结果的长度。如果返回结构不够,将取剩余的全部。
      checkFunc=function(...) TRUE:执行函数checkFun,如果返回TRUE,则返回;否则,跳过。
      recycle:设定在nextElem循环到底(“错误: StopIteration”)是否要循环处理,即从头再来一遍。

(1)iter+function迭代输出


      来看一个iter案例,帮你把函数分块给你,不用一次性导入计算,耗费内存:

> a
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    5    9   13   17
[2,]    2    6   10   14   18
[3,]    3    7   11   15   19
[4,]    4    8   12   16   20
> i2 <- iter(a, by = "column", checkFunc=function(x) sum(x) > 50)
> nextElem(i2)
     [,1]
[1,]   13
[2,]   14
[3,]   15
[4,]   16
> nextElem(i2)
     [,1]
[1,]   17
[2,]   18
[3,]   19
[4,]   20
> nextElem(i2)
错误: StopIteration
      不过,如果没有next了,就会出现报错,这时候就需要稍微注意一下。

      iter(function()rnorm(1)),使用nextElem可以无限重复;但是iter(rnorm(1)),只能来一下。
更有意思的是对象如果是iter,即test1 <- iter(obj); test2 <- iter(test1),那么这两个对象是连在一起的,同时变化。

      (2)生成随机数


      irnorm(..., count)irunif(..., count)irbinom(..., count)irnbinom(..., count)irpois(..., count)是内部生成iterator的工具,分别表示从normal,uniform,binomial,negativity binomial和Poisson分布中随机选取N个元素,进行count次。

      其中,negative binomial分布:其概率积累函数(probability mass function)为掷骰子,每次骰子为3点的概率为p,在第r+k次恰好出现r次的概率。

      icount(count)可以生成1:conunt的iterator;如果count不指定,将从无休止生成1:Inf
      icountn(vn)比较好玩,vn是指一个数值向量(如果是小数,则向后一个数取整,比如2.3 --> 3)。循环次数为prod(vn),每次返回的向量中每个元素都从1开始,不超过设定 vn,变化速率从左向右依次递增。

      idiv(n, ..., chunks, chunkSize)返回截取从1:n的片段长度,“chunks”和“chunkSize”不能同时指定,“chunks”为分多少片段(长度从大到小),“chunkSize”为分段的最大长度(长度由大到小)
      iapply(X, MARGIN):与apply很像,MARGIN中1是row,2是column
      isplit(x, f, drop=FALSE, ...):按照指定的f划分矩阵

> i2 <- icountn(c(3.4, 1.2))
> nextElem(i2)
[1] 1 1
> nextElem(i2)
[1] 2 1
> nextElem(i2)
[1] 3 1
> nextElem(i2)
[1] 4 1
> nextElem(i2)
[1] 1 2
> nextElem(i2)
[1] 2 2
> nextElem(i2)
[1] 3 2
> nextElem(i2)
[1] 4 2
> nextElem(i2)
错误: StopIteration


——————————————————————————————————————


四、高级教程:并行机器学习算法


      并行计算一些小任务会比按顺序运算它们花费更多的时间,所以当普通运算足够快的时候,并没有必要使用并行计算模式改进其运算效率。

      同时,最适合并行莫过于随机森林算法了。

#生成矩阵x作为输入值,y作为目标因子
x <- matrix(runif(500), 100)
y <- gl(2, 50)
#导入randomForest包
require(randomForest)


1、独立循环运行随机森林算法


      如果我们要创建一个包含1200棵树的随机森林模型,在6核CPU电脑上,我们可以将其分割为六块执行randomForest函数六次,同时将ntree参赛设为200,最后再将结果合并。

rf <- foreach(ntree=rep(200, 6), .combine=combine) %do%
  randomForest(x, y, ntree=ntree)
rf
## 
## Call:
##  randomForest(x = x, y = y, ntree = ntree) 
##                Type of random forest: classification
##                      Number of trees: 1200
## No. of variables tried at each split: 2

      分开来运行6个200树的随机森林算法。


2、参数.packages——并行运行随机森林算法


      将%do%改为“%dopar%”,同时使用.packages调用randomForest:

rf <- foreach(ntree=rep(200,6), .combine=combine, .packages="randomForest") %dopar% 
  randomForest(x, y, ntree=ntree)
rf
      通过.packages来将函数包导入其中,类似parallel中的clusterEvalQ,但是foreach在一个函数里面包含了函数、包的导入过程。

      当然还可以使用一些其他包,使用.packages参数来加载包,比如说:.packages = c("rms", "mice")

3、参数.export——将doParallel并行写入函数


      写入函数有个问题就是,运行函数的时候,运用不了R外面内存环境的变量。而且会报错:
test <- function (exponent) {
  foreach(exponent = 2:4, 
          .combine = c)  %dopar%  
    base^exponent
}
test()

 Error in base^exponent : task 1 failed - "object 'base' not found" 


      所以需要在写函数的时候,将一些外面的内存函数,写到函数之中,通过.export,而不需要使用clusterExport。注意的是,他可以加载最终版本的变量,在函数运行前,变量都是可以改变的:

base <- 2
cl<-makeCluster(2)
registerDoParallel(cl)
 
base <- 4
test <- function (exponent) {
  foreach(exponent = 2:4, 
          .combine = c,
          .export = "base")  %dopar%  
    base^exponent
}
test()
 
stopCluster(cl)

 [1]  4  8 16


——————————————————————————————————————

应用一:并行时.dopar,如何输入多个常规值,.export输入多个值?


本节想解决的问题:

1、能不能输出每次迭代的时间?(参考:R语言︱函数使用技巧(循环、if族/for、switch、repeat、ifelse、stopifnot)

2、并行的时候,如何导入多个数值型变量?


注意:

.export需要输入方程中没有的值,而且必须是一个文本型,可以用list的方式。


library(foreach)
registerDoParallel(makeCluster(2))  

a=c(1,2,3)
b=c(3,4,5,5,5,5)
d=c(2,4,35,5,5,2)
i=0
t1 = Sys.time()

foreach(x=a, .combine="rbind",.export="list(b,d,i,t1)") %dopar% {
  cc=b+d
  a+cc
  i=i+1
  print(paste("迭代了",i,"次",sep=""))
  print(difftime(Sys.time(), t1, units = 'sec'))  
}

注意代码中,设置了2个核心。

.combine可以将数据合并起来,之后步骤在所有结果出来之后再进行相应的合并。所以结果出来,会有result.1等项目:

[1] "迭代了1次"
Time difference of 0.03000283 secs
[1] "迭代了2次"
Time difference of 0.031003 secs
[1] "迭代了3次"
Time difference of 0.031003 secs
               [,1]
result.1 0.03000283
result.2 0.03100300
result.3 0.03100300




——————————————————————————————————————

参考文献:


1、R语言中的并行计算:foreach,iterators, doParallel包

2、foreach包简介     /    FROM:《Using The foreach Package》

3、 R语言︱大数据集下运行内存管理

4、R︱并行计算以及提高运算效率的方式(parallel包、clusterExport函数、SupR包简介)


<think>好的,我现在需要帮助用户解决如何用伪代码简述算法过程和逻辑的问题。首先,用户提到他们想参考之前的回答中的引用,但根据引用内容,用户提供的引用[1]是关于SVM算法和KKT条件、SMO算法以及Python实现的,引用[2]是关于Raft算法的日志复制和配置示例。不过用户现在的问题是关于伪代码的简述,可能需要结合之前的快速排序和位移加速技术的例子。 用户之前的提问中,我给出了快速排序的Python代码示例,并生成了相关问题。后来用户又要求增加一个实例,于是我提供了位移加速技术的详细解析,包含数学公式、伪代码步骤和引用。现在用户可能希望看到如何用伪代码来简述一个算法的过程和逻辑,可能需要参考之前的格式,但更简洁。 需要明确用户的具体需求。用户可能希望了解如何将算法步骤转化为伪代码,或者需要示例。用户提到了“伪代码的简述”,所以重点在于结构清晰、逻辑明确,而不是具体的编程语言细节。 考虑到用户之前的问题中提到了快速排序和位移加速技术,可能需要选取一个常见算法作为例子,比如快速排序或者Raft算法的日志复制部分,用伪代码展示其过程。根据引用[2]中的Raft伪代码示例,用户可能对分布式算法感兴趣,但用户现在的问题更通用,可能需要一个通用例子。 另外,用户可能希望伪代码的结构包括关键步骤,如初始化、循环、条件判断等,并且要符合逻辑流程。同时,用户可能希望伪代码中带有注释,解释每一步的作用,类似于引用[2]中的处理方式。 需要注意伪代码的格式,使用合适的缩进和关键字,如“function”、“if”、“else”、“for”、“while”等,但不需要具体的语法,保持简洁易懂。可能需要分步骤说明,比如在快速排序中,选择基准元素,分区,递归调用等步骤。 需要检查是否符合用户之前的格式要求,比如在回答中使用中文,行内公式用$...$,独立公式用$$...$$,但伪代码部分应该用代码块包裹,使用三个反引号,并注明是伪代码。例如: ```pseudocode function quick_sort(arr): if length(arr) <= 1: return arr pivot = select_pivot(arr) left = elements < pivot right = elements >= pivot return quick_sort(left) + [pivot] + quick_sort(right) ``` 此外,用户可能希望伪代码之后有相关的问题,类似之前的结构。因此,生成的问题需要围绕伪代码的编写方法、优化、应用场景等。 需要确保伪代码示例正确,逻辑清晰,并且涵盖用户可能关心的关键点。例如,在快速排序中选择基准元素的不同方法(如第一个元素、随机选择、三数取中等),或者分区过程的细节。但用户要求的是“简述”,所以可能需要简化,只保留核心步骤。 总结来说,用户需要的是一个用伪代码简述算法过程和逻辑的示例,可能类似之前的快速排序或位移加速的实例,但更注重伪代码的结构和关键步骤的呈现。需要确保伪代码简洁易懂,逻辑正确,并且符合用户之前的问题格式。</think>##### **算法伪代码设计方法与实例解析** 伪代码的核心目标是**清晰展示算法逻辑**,需平衡数学严谨性与可读性。以下是设计要点及进阶案例: --- ### **一、伪代码设计三原则** 1. **结构化表示** 使用标准控制结构(顺序/分支/循环),如: ```pseudocode function 算法名称(输入参数): // 初始化阶段 变量 = 初始值 while 终止条件未满足: // 核心计算步骤 if 条件判断: 执行分支A else: 执行分支B // 状态更新 更新关键变量 return 输出结果 ``` 2. **关键操作显式标注** 突出算法特色操作,如梯度下降中的学习率更新: ```pseudocode function 随机梯度下降(数据集, 学习率α, 迭代次数T): 初始化权重向量 w for t in 1..T: 随机选取样本 (x_i, y_i) 计算梯度 ∇L = 损失函数的梯度 w = w - α * ∇L // 参数更新核心步骤 return w ``` 3. **复杂度标记** 在关键操作后添加时间复杂度注释: ```pseudocode function 归并排序(arr): if arr长度 ≤ 1: return arr // O(1) mid = 长度/2 left = 归并排序(arr[0..mid]) // T(n/2) right = 归并排序(arr[mid+1..]) // T(n/2) return merge(left, right) // O(n) ``` --- ### **二、进阶实例:Raft共识算法日志复制** 结合引用[2]的Raft协议,其日志复制伪代码可优化为: ```pseudocode // Leader节点处理客户端写请求 function Leader.handle_write(data): current_term = 持久化存储.current_term new_entry = LogEntry(term=current_term, data=data) // 阶段1:日志持久化 local_log.append(new_entry) // O(1) parallel foreach follower in cluster: // 并行优化 send_append_entries(follower, new_entry) // 阶段2:等待多数派确认 confirmed_count = 1 // 自身已持久化 while confirmed_count < majority: wait_for_acknowledge() // 非阻塞等待 if 收到新ACK: confirmed_count += 1 // 阶段3:提交并通知 commit_index = new_entry.index notify_followers_to_commit(commit_index) // O(n)网络操作 return "SUCCESS" ``` **关键优化点**: 1. 并行发送日志条目(降低网络延迟影响) 2. 非阻塞等待确认(提升吞吐量) 3. 批量提交通知(减少RPC调用次数) --- ### **三、伪代码复杂度分析框架** 建立可量化的评估标准: $$ T(n) = \sum_{操作} (执行次数 \times 单次耗时) $$ 以快速排序为例: ```pseudocode function quick_sort(arr): if len(arr) ≤ 1: return arr // O(1) pivot = median_of_three(arr) // O(1) left, right = partition(arr, pivot) // O(n) return quick_sort(left) + [pivot] + quick_sort(right) // 2T(n/2) ``` 推导时间复杂度: $$ T(n) = O(n) + 2T(\frac{n}{2}) \Rightarrow T(n) = O(n \log n) $$ --- ### **四、伪代码到工程代码的转换** 以布隆过滤器为例展示不同实现层级: | 层级 | 伪代码表示 | Python实现片段 | |------------|---------------------------------------|-----------------------------------| | 算法逻辑 | ``` | ``` | | | function add(element): | def add(self, element): | | | for i in 1..k: | for i in range(self.k): | | | index = hash_i(element) | idx = self.hash_funcs[i]( | | | bit_array[index] = 1 | element) % self.size | | | | self.bit_array[idx] = 1 | | | ``` | ``` | **设计启示**: - 哈希函数数量 `k` 需满足 $$ k = \frac{m}{n} \ln 2 $$(m为位数组大小,n为元素数量) - 误判率计算公式:$$ P_{fp} \approx \left(1 - e^{-kn/m}\right)^k $$ --- ##### **相关问题** 1. 如何评估伪代码与实际代码的性能差异? 2. 在分布式系统中如何设计拜占庭容错算法的伪代码? 3. 伪代码中如何准确表示机器学习模型的训练过程?
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值