Python的并行远不如Matlab好用。比如Matlab里面并行就直接把for改成parfor就行(当然还要注意迭代时下标的格式),而Python查 一查并行,各种乱七八糟的方法一大堆,而且最不爽的一点就是只能对函数进行并行。
当然,这点困难也肯定不能就难倒我们,该克服也得克服,毕竟从本质上讲,也就只是实现的方式换一换而已。
大名鼎鼎的sklearn里面集成了很方便的并行计算,这在之前的机器学习教程里面也有^1
仔细查看其代码发现就是用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里面最常用到的一个类和一个方法分别是Parallel和delayed。Parallel主要用于初始化并行计算时需要用到的参数,而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提供了很好的多参数同时并行机制,可以用product、zip等方法将多个集合放在一起,那么就可以实现多参数的并行。比如:
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就有几倍速度也正是这个原因。比如:直接循环
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
可以很清楚地看到,这时核心数越多,反而时间越慢。
因此在使用并行时,也要严格根据自己的需求来。