标准方式
让我们举一个简单的例子,我们有一个图像数据集,可能有数千甚至数百万张图像!出于时间方面的考虑,我们只使用 1000 张。在将所有图像传到深度神经网络之前,我们想要将它们的大小调整为 600×600。以下是 GitHub 上经常可以看到的一些非常标准的 Python 处理代码。
这个程序遵循的是数据处理脚本中常见的简单模式:
从要处理的文件(或其他数据)列表开始。使用 for 循环逐个处理数据,并在每个循环迭代上进行预处理。
让我们使用一个包含 1000 个 jpeg 文件的文件夹来测试这个程序,看看需要多长时间:
在我的机器(6 个内核的 i7-8700k CPU)上运行时间为 7.9864 秒!对于这样高端的 CPU 来说,这看起来有点慢了。让我们看看可以做些什么来加快速度。
更快的方式
为了理解 Python 如何并行处理事物,我们可以先了解并行处理本身是怎么回事。假设我们要将 1000 颗钉子钉入一块木头,钉一颗钉子需要 1 秒钟,如果是 1 个人做,那么总的需要 1000 秒时间。但如果团队中有 4 个人,我们将钉子分成四份,让每个人负责钉自己的钉子。使用这种方法,我们只需 250 秒即可完成任务!
同样,我们可以使用 Python 来处理上面的 1000 张图像:
-
将 jpg 文件列表拆分为 4 组。
-
运行 4 个单独的 Python 解释器实例。
-
让每个 Python 实例处理 4 组图像中的一个。
-
合并 4 个实例的处理结果,得到最终结果。
Python 为我们处理了所有“脏活”。我们只需要告诉它想要运行哪个函数,以及使用多少个 Python 实例,然后它会完成所有其他操作!我们只需修改 3 行代码。
在上面的代码中:
启动与 CPU 核心一样多的 Python 进程,这里是 6 个。实际进行处理的是这行代码:
executor.map() 将你想要运行的函数以及要处理的图像列表作为输入。因为我们有 6 个核心,所以可以同时处理列表中的 6 张图像!
如果我们再次运行我们的程序:
运行时间为 1.14265 秒,快了 6 倍!
注意:生成更多的 Python 进程并在它们之间 shuffle 数据会带来一些额外的开销,所以不一定总是会得到这么大的速度提升。但总的来说,速度的提升是非常明显的。
它总会这么快吗?
当你要对数据集中的每个数据点执行相似的计算时,使用 Python 并行进程池是一个很好的办法。但是,它并不总是完美的解决方案。并行进程池不会按照任何可预测的顺序来处理数。如果你需要处理结果按特定顺序排列,那么这个方法可能就不适合你。
Python 需要知道如何“pickle”你要处理的数据类型。所运的是,Python 官方文档提到了这些类型:
-
None、True、False;
-
整数、浮点数、复数;
-
字符串、字节、字节数组;
-
仅包含可处理的对象的元组、列表、集合和词典;
-
在模块顶层定义的函数(使用 def 而不是 lambda 进行定义);
-
在模块顶层定义的内置函数;
-
在模块顶层定义的类;
-
__dict__ 或 __getstate()__ 调用结果是可处理的类实例。