最近在写深度学习开源库时,遇到了读取语料并预处理的 API,于是趁此机会整理一下之前积累的关于高性能的内容。全文包括两个部分:第一部分主要聚焦在常用工具 Pandas 处理数据时不同操作方法的性能;第二部分主要介绍一些加速数值计算的工具;第三部分主要介绍一些能够辅助增加性能的工具。
如果懒得看过程,可以直接看结论:
- 数值计算任务:
numba
或Cython
加速;使用.values
计算 - 遍历的非数值计算任务:
df.itertuples
或df.apply
方法 - 数据太大 Pandas 处理不了的可以使用 Arrow 和 Polars,再不行了使用 Spark
所有测试代码在:Note_DS/Performance.ipynb
Pandas 性能对比
数据集
本文的数据集使用 今日头条中文新闻(文本)分类数据集,此数据集样例如下:
1 | $ head -2 toutiao_cat_data.txt |
该数据集是短文本分类任务,包含 15 个类别,共 382688 条样本。每行为一条数据,以_!_
分割的个字段,从前往后分别是 新闻 ID,分类 code,分类名称,新闻字符串(仅含标题),新闻关键词。为了提升处理效率,我们只选择 1 万条数据。
循环任务
假设我们需要得到 title 的分词结果,这里我们使用结巴分词。
For循环
最基本直观的操作:
1 | def tokenize_forloop(df): |
执行结果如下:
1 | CPU times: user 3.06 s, sys: 220 ms, total: 3.28 s |
iterrows方法
1 | def tokenize_iterrows(df): |
执行结果如下:
1 | CPU times: user 2.76 s, sys: 45.3 ms, total: 2.8 s |
itertuple方法
1 | def tokenize_itertuples(df): |
执行结果如下:
1 | CPU times: user 1.26 s, sys: 13.8 ms, total: 1.28 s |
Apply方法
1 | %time df["tokens"] = df["title"].apply(lambda x: jieba.lcut(x)) |
执行结果如下:
1 | CPU times: user 1.41 s, sys: 19.1 ms, total: 1.43 s |
可以发现 itertuple
方法出奇的快,甚至超过了 apply
方法。
批量任务
假设我们需要对长度进行最小最大值范围限定:
1 | def reset_len(df, series, min_len, max_len): |
这种情况可以进行批量处理,也有两种不同的方式:
Series
1 | %timeit reset_len(df, df["length"], 5, 20) |
Numpy
1 | %timeit reset_len(df, df["length"].values, 5, 20) |
可以发现转成 Numpy 快了将近一倍。我们再尝试用 apply
处理一下:
1 | %timeit df["length"] = df["length"].apply(lambda x: deal_length(x, 5, 20)) |
这个差距有 10 倍了。
小结一下:如果是循环任务(非数字、需要其他复杂处理的),可以使用 apply
或 itertuple
,后者对于处理数据集尤其有用;如果是批量任务,可以充分利用矩阵计算快速处理,唯一要做的仅仅只是添加一个 .values
的后缀而已。
加速数据任务
如果任务中有大量数值计算和循环,可以使用 numba
,JAX
,Cython
等进行加速,其性能会远超循环方法。
加速器
构造一个简单的例子:
1 | import numpy as np |
numba
不使用 numba
:
1 | df = pd.DataFrame(np.random.randint(1,100,size=(100000, 3)),columns=['a', 'b', 'N']) |
使用 numba.jit
:
1 | df = pd.DataFrame(np.random.randint(1,100,size=(100000, 3)),columns=['a', 'b', 'N']) |
再看一个批量的例子:
1 | def double_every_value_nonumba(x): |
不使用 numba
:
1 | df = pd.DataFrame(np.random.randint(1,100,size=(100000, 3)),columns=['a', 'b', 'N']) |
使用 numba.jit
和 apply
:
1 | df = pd.DataFrame(np.random.randint(1,100,size=(100000, 3)),columns=['a', 'b', 'N']) |
直接批量计算:
1 | df = pd.DataFrame(np.random.randint(1,100,size=(100000, 3)),columns=['a', 'b', 'N']) |
使用 .values
批量计算:
1 | df = pd.DataFrame(np.random.randint(1,100,size=(100000, 3)),columns=['a', 'b', 'N']) |
使用 numba.vectorize
:
1 | df = pd.DataFrame(np.random.randint(1,100,size=(100000, 3)),columns=['a', 'b', 'N']) |
使用 numba.vectorize
和 .values
:
1 | df = pd.DataFrame(np.random.randint(1,100,size=(100000, 3)),columns=['a', 'b', 'N']) |
使用 numba.jit
和 .values
:
1 | df = pd.DataFrame(np.random.randint(1,100,size=(100000, 3)),columns=['a', 'b', 'N']) |
JAX
依然使用上面批量的例子:
1 | from jax import jit |
使用 jax
:
1 | df = pd.DataFrame(np.random.randint(1,100,size=(100000, 3)),columns=['a', 'b', 'N']) |
使用 jax
和 .values
:
1 | df = pd.DataFrame(np.random.randint(1,100,size=(100000, 3)),columns=['a', 'b', 'N']) |
通过简单的例子对比可以发现,numba
比 JAX
要快一些。
此外还有其他一些加速器,可参考:Make your Python code fly at transonic speeds!
Cython
我们使用前面 numba
的 integrate
方法:
1 | %load_ext Cython |
纯 Cython
:
1 | df = pd.DataFrame(np.random.randint(1,100,size=(100000, 3)),columns=['a', 'b', 'N']) |
比直接使用 Python 稍微快了一点。
添加类型:
1 | df = pd.DataFrame(np.random.randint(1,100,size=(100000, 3)),columns=['a', 'b', 'N']) |
这性能已经超过了使用 numba.jit
,至少也是相差无几了。
使用 ndarray
:
1 | df = pd.DataFrame(np.random.randint(1,100,size=(100000, 3)),columns=['a', 'b', 'N']) |
可以看到已经很好了,这里还是用了 numpy
的 array,总而言之,.values
值得被使用。
eval
这是一个实验功能,看起来还是很强大,我们使用 double_every_value
方法进行测试:
1 | df = pd.DataFrame(np.random.randint(1,100,size=(100000, 3)),columns=['a', 'b', 'N']) |
可以看到这个比使用了 numba
的 apply
还是快很多,但和 .values
和批量方法相比还是有差距。
并发
我们使用最一开始的文本处理任务:
1 | from pnlp import concurring |
不使用并发:
1 | df = get_data() |
可以看到和 apply
差不多:
1 | df = get_data() |
多进程:
1 | df = get_data() |
多线程:
1 | df = get_data() |
多线程不适用并发快一些,但多进程反而会慢,每个进程导入包可能会有影响。
最后看下 Pandarallel:
1 | df = get_data() |
还是有一定的提升。
其他数据处理工具
除了 Pandas,还有两个工具值得推荐:
- 如果数据对 Pandas 来说太大,但对 Spark 来说又太小,可以使用: Polars
- 当然,还有它使用的高性能数据处理引擎:Apache Arrow