Python之并行--基于joblib

Python之并行–基于joblib

Python的并行远不如Matlab好用。比如Matlab里面并行就直接把for改成parfor就行(当然还要注意迭代时下标的格式),而Python查 一查并行,各种乱七八糟的方法一大堆,而且最不爽的一点就是只能对函数进行并行。

当然,这点困难也肯定不能就难倒我们,该克服也得克服,毕竟从本质上讲,也就只是实现的方式换一换而已。

大名鼎鼎的sklearn里面集成了很方便的并行计算,这在之前的机器学习教程里面也有12

仔细查看其代码发现就是用joblib实现的,并且用法还挺巧。

from joblib import Parallel, delayed

parallel = Parallel(n_jobs=self.n_jobs, verbose=self.verbose,
                            pre_dispatch=self.pre_dispatch)
out = parallel(delayed(_fit_and_score)(clone(base_estimator),
                                                       X, y,
                                                       train=train, test=test,
                                                       parameters=parameters,
                                                       **fit_and_score_kwargs)
                               for parameters, (train, test)
                               in product(candidate_params,
                                          cv.split(X, y, groups)))

这段代码的意思非常简单,即是用n_jobs个CPU来计算_fit_and_score函数,其中参数为clone(base_estimator), X, y,train=train, test=test,parameters=parameters,**fit_and_score_kwargs而这里只有parameters,(train,test)作为被枚举的变量,其它参数始终保持不变。至于里为何要用clone函数是因为如果直接将base_estimator传入的话,这个模型在外部也将会被改变。具体原因可以参看其它文档。

这里就简单回顾下joblib的用法:

使用之前可以在自己的环境里先安装好这个库:

pip install joblib
1、简单示例

首先joblib里面最常用到的一个类和一个方法分别是ParalleldelayedParallel主要用于初始化并行计算时需要用到的参数,而delayed则主要用来指定需要被并行的参数。比如官方给出的以下示例:

from math import sqrt
from joblib import Parallel, delayed
Parallel(n_jobs=2)(delayed(sqrt)(i ** 2) for i in range(10))
[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]

这段代码其实已经基本看出方法的主要使用模式了。

解释一下:

  • Parallel(n_jobs=2): 指定两个CPU(默认是分配给不同的CPU)
  • 后面的delayed(sqrt)表示要用的函数是sqrt,这里这种用法就非常类似C++里面的委托(delegate)
  • (i ** 2) for i in range(10): 这里注意(i**2)的括号和delayed(sqrt)是紧挨着的。这一小段表示要传递给delayed中指定的函数的参数是i^2

那么结合这么一小段程序,其实已经能大致理解它的使用方法了。这里最开始可能主要不习惯的是要用到了Python里面的内部函数机制。

当然,作为 调包侠 工程师的我们,先不管这么多,用起来再说。比如写点简单的hello world

c = 'hello world'
Parallel(n_jobs=2)(delayed(print)(i) for i in c)

Out: [None, None, None, None, None, None, None, None, None, None, None]

这里就出现问题了,因为直接用print函数并没有返回值。所以这种实现方式是不正确的。要解决这个问题可以用另外一种方式来实现,比如:

def test_print(c):
    for i in c:
        print(i)
    return c

Parallel(n_jobs=2)(delayed(test_print)(i) for i in test_print(c))
h
e
l
l
o
 
w
o
r
l
d
Out[8]: ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd']

这里注意,真正实现了print功能的是后面的函数test_print(c)

2、多参数并行

由于Python提供了很好的多参数同时并行机制,可以用productzip等方法将多个集合放在一起,那么就可以实现多参数的并行。比如:

import numpy as np 

def test_mvar(a,b):
    return a + b
Parallel(n_jobs=5)(delayed(test_mvar)(i,j) for i,j in zip(np.random.rand(10),np.random.rand(10)))


[0.8787518714894541,
 1.3086203058912202,
 1.3478235487198926,
 1.1963547010071447,
 1.2705711575828578,
 0.5354314753331146,
 1.0794490655604165,
 0.6707125925684075,
 0.8861808920187632,
 1.1512627654053902]

当然注意到开头的代码,函数中可以只并行一部分参数,这里我们也可以如此,比如:

Parallel(n_jobs=5)(delayed(test_mvar)(i,1) for i,j in zip(np.random.rand(10),np.random.rand(10)))

[1.2137475752125968,
 1.4543891439818082,
 1.952684349026534,
 1.3565884407873496,
 1.5411521108342199,
 1.0820532116263255,
 1.8668685545516257,
 1.397012511283467,
 1.1645324713909715,
 1.899119157699394]

下面这段代码就只计算数字1和一系列随机数的和。

那么到这里可以简单再分析一下用法:delayed函数指定了被运行的主函数test_mvar,而其后的参数则是由元组形式给出(i,1),后面需要进行for循环的参数只要保持和前面元组中相同的变量名即可。另外,需要注意的是变量的顺序必须和被运行的主函数保持一致。

3、并行时CPU是怎么分配的

这个问题相对比较麻烦。在joblib中默认是采用loky实现并行,这种方式最为简单直接,它会自然地将任务分配到多个CPU上去运行,同时更加稳定。当然除此之外还有其它的一些方式,比如多进程。这些方法之间的区别在我们大多数时候是相对比较细微的(追求极致并行的除外),我们只需要对它有个简单直接的了解即可。

再看一个例子:

from joblib import Parallel, delayed

import numpy as np

def test_non(a):
    return a

c = range(20)

Parallel(n_jobs=2,backend='multiprocessing')(delayed(test_non)(i) for i in c)

注意,这里什么也没有发生。我们把并行方式改成了多进程,出现的结果就是一直卡着。这里主要就是分配机制的问题。所以一般而言,直接采用默认方式就好

4、何时选用并行

这个问题其实是最关键的。并行其实也不一定一直会很快。因为并行的处理流程实际上是这样的:

原任务
拆分任务
各CPU分别完成任务
合并任务,返回结果

这里拆分、合并和CPU之间的通信都是会产生时间消耗的。那么很容易想到,如果本身的任务很小,其实消耗的时间反而会更多。另外,往往实际的并行不能达到几个CPU就有几倍速度也正是这个原因。比如:

  • 直接循环
import time 
st = time.time()
for i in range(20):
    i
et = time.time()
print(et-st)
0.0
  • 双核
st = time.time()
Parallel(n_jobs=2)(delayed(test_non)(i) for i in c)
et = time.time()
print(et-st)
0.010970830917358398
  • 四核
st = time.time()
Parallel(n_jobs=-1)(delayed(test_non)(i) for i in c)
et = time.time()
print(et-st)
0.23437237739562988

可以很清楚地看到,这时核心数越多,反而时间越慢。

因此在使用并行时,也要严格根据自己的需求来。


  1. sklearn快速入门教程:(四)模型自动调参 ↩︎

  2. sklearn快速入门教程:(五)集成学习 ↩︎

  • 5
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

半个冯博士

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值