一、目的
在我们进行机器学习数据预处理的时候,为了加快数据的预处理,我们很多时候会用并行来进行加速。但是同时会增大内存的开销(甚至引起OOM)。本文的目的就是为了解决在资源有限的情况下,加速数据预处理。
二、为什么最终优化效率要用并行
一个流程、或者一个函数或者一个项目,可以划分成2部分,可优化部分和不可优化部分。
按照Amdal's Law
可以依据上图计算出加速比。
S
=
T
o
l
d
/
T
n
e
w
=
1
(
1
−
a
)
+
a
k
S=T_{old}/T_{new} = \frac{1}{(1-a) + \frac{a}{k}}
S=Told/Tnew=(1−a)+ka1
当K趋向无穷时,即将可优化部分优化到几乎不花时间,这时候我们可得到极限加速比
S
=
1
1
−
a
S=\frac{1}{1-a}
S=1−a1,当不可优化部分占比0.6时,极限加速比为2.5。所以在这个时候想要,再提升性能,就需要并发,即将不可优化部分分成多部分,进行并行运行。
三、预处理并行优化
用的智慧海洋比赛的数据
下面只写出了并行部分的脚本, 全部脚本可以参考:笔者的github:并行协程优化
数据地址:https://download.csdn.net/download/Scc_hy/16743283
加载并准备辅助函数。
import pandas as pd
import numpy as np
import os
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
import psutil
import time, datetime
import warnings
warnings.filterwarnings(action='ignore')
def detect_total_memory(return_flag=True):
mem_info = psutil.virtual_memory()
mem_used = mem_info.used/ 1024 / 1024 / 1024
mem_used_rate = mem_info.percent
print(f'当前进程的内存使用情况:{mem_used_rate:.2f}% | {mem_used:.5f} GB')
if return_flag:
return mem_used, mem_used_rate
def detect_process_memory():
now_p_mem = psutil.Process(os.getpid()).memory_info().rss / 1024 / 1024 / 1024
print (f'当前进程的内存使用:{now_p_mem:.5f} GB | {now_p_mem * 1024:.3f} MB')
@clock
def parallel_deal(func_, params_list, run_flag=True):
workers = os.cpu_count() * 9 // 10
p_ = ProcessPoolExecutor(workers)
tasks = [p_.submit(func_, parami) for parami in params_list]
# 我们可以得知,在提交线程任务的时候就会将参数全部提交进入内存中
print('--'*20)
print('After submit to process')
detect_process_memory()
if run_flag:
print('Start parellel process')
res = [task.result() for task in as_completed(tasks)]
return res
return []
@clock
def res2df(res):
if isinstance(res, list):
return pd.DataFrame(res)
return pd.DataFrame(list(res))
我们可以看出在提交进程任务的时候就会将参数全部提交进入内存中,这时候占用的内存总df数*单个df的大小
。当数据不是很多的时候,不会内存溢出。但是如果非常大的时候就回存在这样的问题。
def main(only_detect_preocess_memory_flag=False):
print('start processing')
detect_process_memory()
detect_total_memory()
train_data_root = r'D:\Python_data\My_python\Projects\智慧海洋\data\hy_round1_train_20200102'
params_list = (pd.read_csv( os.path.join(train_data_root, i) ) for i in os.listdir(train_data_root)[:200])
run_ = not only_detect_preocess_memory_flag
print('run_: ', run_)
res = parallel_deal(zhhy_preprocess, params_list, run_flag=run_)
if not only_detect_preocess_memory_flag:
print('--'*20)
print('start transform to dataFrame')
res_df = res2df(res)
print('res_df simple view')
print('res_df.shape: ', res_df.shape)
print('res_df.head: \n', res_df.head(2))
print('Done')
if __name__ == '__main__':
main(False)
"""
start processing
当前进程的内存使用:0.06974 GB | 71.414 MB
当前进程的内存使用情况:79.00% | 6.23040 GB
run_: True
----------------------------------------
After submit to process
当前进程的内存使用:0.08219 GB | 84.164 MB
Start parellel process
parallel_deal Done. | It costs 12.538s
----------------------------------------
start transform to dataFrame
res2df Done. | It costs 0.026s
res_df simple view
res_df.shape: (200, 33)
res_df.head:
0 1 2 3 4 5 6 7 8 9 10 11 12 ... 20 21 22 23 24 25 26 27 28 29 30 31 32
0 0 拖网 4 0 23 15 9.39 0.265966 -6.80 6.31 0.006271 0.070218 0.857143 ... 0.053269 0.944310 2.181406 0.029993 0.019370 0.944310 0.0 4745.887438 87.089644 5.125652 0.136680 4.783 6.580
1 1 拖网 4 0 23 19 10.47 1.607922 -3.19 4.58 0.010391 0.359375 0.276042 ... 0.184896 0.632812 4.886008 0.462122 0.153646 0.632812 0.0 5828.114792 494.874699 5.232657 0.740035 3.977 3.833
main Done. | It costs 15.043s
进程中总共消耗了12.75MB的内存
"""
针对智慧海洋这个数据,内存的优化有两个方法。两个方法的思路是一样的,只有在进程启动运行的时候才将数据传入内存中。
四、内存优化
进程任务中读取数据
只用进行简单修改就可以,这时候不会将所有的数据直接传入内存中。在执行提交的并行任务的时候才会读取,并占用内存,这个时候占用的内存为进程数*单个df的大小
。
def main(only_detect_preocess_memory_flag=False):
...
params_list = (os.path.join(train_data_root, i) for i in os.listdir(train_data_root)[:200])
...
if __name__ == '__main__':
main(False)
"""
start processing
当前进程的内存使用:0.06967 GB | 71.344 MB
当前进程的内存使用情况:79.70% | 6.28205 GB
run_: True
----------------------------------------
After submit to process
当前进程的内存使用:0.07072 GB | 72.414 MB
Start parellel process
parallel_deal Done. | It costs 12.788s
----------------------------------------
start transform to dataFrame
res2df Done. | It costs 0.045s
res_df simple view
res_df.shape: (200, 33)
res_df.head:
0 1 2 3 4 5 6 7 8 9 10 11 12 ... 20 21 22 23 24 25 26 27 28 29 30 31 32
0 0 拖网 4 0 23 15 9.39 0.265966 -6.80 6.31 0.006271 0.070218 0.857143 ... 0.053269 0.944310 2.181406 0.029993 0.019370 0.944310 0.0 4745.887438 87.089644 5.125652 0.136680 4.783 6.580
1 1 拖网 4 0 23 19 10.47 1.607922 -3.19 4.58 0.010391 0.359375 0.276042 ... 0.184896 0.632812 4.886008 0.462122 0.153646 0.632812 0.0 5828.114792 494.874699 5.232657 0.740035 3.977 3.833
[2 rows x 33 columns]
Done
进程中总共消耗了1MB左右的内存
"""
当然,这其实是属于较特殊的情况,有时候数据均已经在内存中。改成生成器优化之后,虽然减少了内存,但是提交到进程中还是会内存溢出,这个时候就需要对多进程任务进行优化了。
协程优化
可以看出每次占用的是1.2MB,相比于未优化前,内存降低了非常多。运行效率上看(均15秒左右),并没有降低。
def parallel_deal_generator(func_, params_list, chunksize = None):
workers = os.cpu_count() * 9 // 10
if not chunksize:
chunksize = workers
p_ = ProcessPoolExecutor(workers)
tasks = []
chunk_n = 1
n = 1
while True:
try:
tasks.append(p_.submit(func_, next(params_list)))
n += 1
if chunksize == n:
print('chunk_n:', chunk_n)
chunk_n += 1
n = 1
print('--'*20)
print('After submit to process')
detect_process_memory()
print('Start parellel process')
yield [task.result() for task in as_completed(tasks)]
tasks = []
except:
if len(tasks) > 0:
yield [task.result() for task in as_completed(tasks)]
def main_g():
print('start processing')
detect_process_memory()
detect_total_memory()
train_data_root = r'D:\Python_data\My_python\Projects\智慧海洋\data\hy_round1_train_20200102'
params_list = (os.path.join(train_data_root, i) for i in os.listdir(train_data_root)[:200])
res_g = parallel_deal_generator(zhhy_preprocess, params_list)
res_df_list = []
while True:
try:
print('--'*20)
print('start transform to dataFrame')
res_df = res2df(next(res_g))
res_df_list.append(res_df)
print('res_df simple view')
print('res_df.shape: ', res_df.shape)
# print('res_df.head: \n', res_df.head(2))
except:
break
res_fdf = pd.concat(res_df_list)
print('res_fdf.shape: ', res_fdf.shape)
print('Done')
if __name__ == '__main__':
# main(False)
main_g()
"""
start processing
当前进程的内存使用:0.06995 GB | 71.629 MB
当前进程的内存使用情况:76.60% | 6.04265 GB
...
chunk_n: 33
----------------------------------------
After submit to process
当前进程的内存使用:0.07118 GB | 72.891 MB
Start parellel process
res2df Done. | It costs 0.007s
res_df.shape: (6, 33)
----------------------------------------
res2df Done. | It costs 0.006s
res_df.shape: (2, 33)
----------------------------------------
res_fdf.shape: (200, 33)
Done
main_g Done. | It costs 14.847s
"""