Python高效计算库Joblib的详细入门教程
文章目录
- 1. Joblib库是什么?
- 2. 核心功能介绍及演示
- 2.1 高效序列化和反序列化对象
- 2.2 快速磁盘缓存
- 2.3 并行计算
1. Joblib库是什么?
Joblib 是一个用于在 Python 中进行高效计算的开源库,提供了一些用于内存映射和并行计算的工具,能大幅提高科学计算和数据分析的效率,特别适合于需要进行重复计算或大规模数据处理的任务。
(图片来源网络,侵删)Joblib 库的常用关键功能包括对象高效序列化、函数值临时缓存以及并行计算,能够优化数据处理流程。在Python中安装 Joblib 库也非常简单,通过 pip 执行以下命令即可:
pip install joblib
安装完成后,执行如下代码,如果能输出相应版本号,则说明已经成功安装。
import joblib print(joblib.__version__)
2. 核心功能介绍及演示
joblib 库的主要功能体现在以下三方面:
- 高效序列化和反序列化:除特殊Python对象外,Joblib 库能将数值对象高效序列化保存到本地,常常用来保存、加载数据对象和模型对象;
- 快速磁盘缓存:够提供高效的磁盘缓存和延迟加载,可以将函数的返回值缓存到磁盘上以避免重复计算;
- 并行计算:能轻松将代码任务分配到多核处理器上。
2.1 高效序列化和反序列化对象
类似于 pickle 库,Joblib 库提供了 dump 和 load 函数,能够高效地将大型数据对象(例如大型数组、机器学习模型等)保存到本地文件或从本地文件加载回来。Joblib 针对 numpy 数组进行了特定的优化,基于殊序列化格式,相比于通用的序列化更加高效。
如下例子,对比了 pickle 库和 Joblib 库在保存和加载大规模数组上的效率。首先生成 ( 10000 , 10000 ) (10000,10000) (10000,10000) 的数组,两个库分别循环保存和加载数据 5 5 5 次,最终的平均处理时间如下(见注释部分):
import numpy as np import pickle, joblib, time # 生成一个大型的 numpy 数组对象,例如 10000 x 10000 的数组 large_array = np.random.rand(10000, 10000) # 循环5次 n = 5 # 平均处理时间 2.54s for i in range(n): with open(f'pickle_data_{i}.pkl', 'wb') as f: pickle.dump(large_array, f) # 平均处理时间 0.72s for i in range(n): with open(f'pickle_data_{i}.pkl', 'rb') as f: load_large_array = pickle.load(f) # 平均处理时间 2.16s for i in range(n): joblib.dump(large_array, f'joblib_data_{i}.joblib') # 平均处理时间 0.04s for i in range(n): load2_large_array = joblib.load(f'joblib_data_{i}.joblib')
相比于 pickle 库加载 .pkl 文件,Joblib 库加载 .joblib 文件的平均效率极高,保存文件的效率也有一定的优势,此外,Joblib 库的接口更加易用,因此在处理含大量数据的任务时常常用来代替 pickle 库。
这种保存再加载的方式,常用在将训练好的模型或计算的数据集保存后分发给其他用户,还常常用于大规模数据的深拷贝(相比于直接深拷贝,保存后加载的方式常常更快)等场景中。
2.2 快速磁盘缓存
Joblib 库的另一核心功能是能将函数的计算返回值快速缓存到磁盘上(记忆模式),当再次调用该函数时,如果函数的输入参数没有改变,则 Joblib 直接从缓存中加载结果而不是重新计算。
如下例子,通过定义缓存目录,以及创建缓存器,添加指定装饰器后,当我们运行第一次函数时,会将函数计算结果缓存到磁盘,再次调用函数时,如果输入参数相同,则从磁盘调出相应的计算结果,避免重复计算。当然了,很自然的一个想法是,为什么不把函数的计算结果保存为哈希表,传入参数为键,计算结果为值,当然也是可行的,但这会极大占用内存,而 Joblib 是将原本应在内存上的计算结果缓存到磁盘,且缓存和调用的处理非常快。
from joblib import Memory import time cachedir = './my_cache' # 定义缓存目录 memory = Memory(cachedir, verbose=0) @memory.cache def expensive_computation(a, b): print("Computing expensive_computation...") sum_ = 0 for i in range(1000000): sum_ += a * b / 10 + a / b return sum_ # 第一次调用,将计算并缓存结果 result = expensive_computation(20, 3) # 0.0967 s # 第二次调用,将直接从缓存加载结果 result = expensive_computation(20, 3) # 0.000997 s
上述代码的装饰器可以理解为将函数 expensive_computation 作为参数传入 memory.cache() 方法当中,上述写法等价于 memory.cache(expensive_computation())。
显然,对于有大量重复计算的任务,该库能极大地提高处理效率。值得注意的是,上述定义的函数中,存在打印语句 print(...),当首次执行函数时,会执行该打印语句,而函数是重复执行的,则会直接从缓存中继承曾经的计算结果,而不会经过中间具体的计算逻辑,也就不会打印相关语句。
2.3 并行计算
Joblib 的最核心的功能应该是提供了高级的(简单易用)并行化工具,能够使我们轻松地将计算任务分配到多个 CPU 核心上执行。
如下所示,当我们有多个独立的任务需要执行,可以通过 Joblib 的 Parallel 和 delayed 功能并行处理这些任务,从而节省时间。
from joblib import Parallel, delayed import numpy as np def process(i): data = np.random.rand(1000, 1000) # 普通的循环计算 # 5.798 s for i in range(1000): process(i) # Joblib 的并行计算 # 3.237 s Parallel(n_jobs=4)(delayed(process)(i) for i in range(1000))
上述式子在,n_jobs 定义了线程数,若n_jobs=-1,则启用所有可用的 CPU 核心;delayed() 中传入任务(函数)名,而后的 (i) 为任务分配传入参数,在 Joblib 的并行计算下,执行 1000 1000 1000 次任务 process() 的时间为 3.237 s 3.237s 3.237s,而循环依次执行的时间为 5.798 s 5.798s 5.798s。
随着任务的计算复杂度增大、独立任务数增多,并行计算的优势会逐渐明显,但相对于我们开的并行任务数,这种优势有时并不那么显著。原因在于,默认情况下,joblib.Parallel 是启动单独的Python工作进程,以便在分散的CPU上同时执行任务,但由于输入和输出数据需要在队列中序列化以便同工作进程进行通信,可能会导致大量开销。因此,小规模任务下,Joblib 的并行计算效率可能较低。