Python/Jupyter Notebook Python 减少内存消耗的方法

Python/Jupyter Notebook Python 减少内存消耗的方法

基本思路

在 Jupyter Notebook 中运行 Python 代码时,如果使用了太多的内存,那么会报Memory Error的错误。这表示前面的变量使用了太多的内存。

事后减少内存

在有一次数据建模中,由于前面进行了很多数据处理的工作,创建了非常多临时的DataFrame,运行到后面的代码时,前面大部分的变量都已经用不上了,但是它们还占据着内存,因此产生了内存不足的错误。


在报错后,需要想办法减少变量占用的内存,以便腾出空间来运行接下来的代码。

使用如下语句,可以查看当前进程总共占用了多少内存。

1
2
3
4
import os,psutil

process = psutil.Process(os.getpid())
print('Used Memory:',process.memory_info().rss / 1024 / 1024,'MB')

可以使用dir()命令查看在内存中的所有变量。

或者使用globals().keys()查看全局变量。globals()函数会以字典类型返回当前位置的全部全局变量

下面这个函数可以查看所有变量占用的内存,按照内存占用大小降序展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import pandas as pd
from sys import getsizeof
def get_memory(threshold=1048576):
'''查看变量占用内存情况

:param threshold: 仅显示内存数值大于等于threshold的变量, 默认为 1MB=1048576B
'''
memory_df=pd.DataFrame(columns=['name', 'memory', 'convert_memory'])
i=0
for key in list(globals().keys()):
memory = eval("getsizeof({})".format(key))
if memory<threshold:
continue
if(memory>1073741824):# GB
unit='GB'
convert_memory=round(memory/1073741824)
elif(memory>1048576):# MB
unit='MB'
convert_memory=round(memory/1048576)
elif(memory>1024):# KB
unit='KB'
convert_memory=round(memory/1024)
else:
unit='B'
convert_memory = memory
memory_df.loc[i]=[key, memory, str(convert_memory)+unit]
i=i+1
# 按照内存占用大小降序排序
memory_df.sort_values("memory",inplace=True,ascending=False)
return memory_df

memory_df = get_memory()
memory_df

然后使用del删除过大的不必要变量。

需要注意的是:使用del删除自己创建的变量后,还需要使用gc.collect()方法强制进行垃圾回收。

1
2
3
4
5
6
7
8
9
import gc

del a
del b
del c
...
...
...
gc.collect()

事前减少DataFrame占用的内存

上面的方法是在报Memory Error的错误后,才去删除变量减少内存占用。另一种思路是事前减少变量的内存占用。由于内存占用比较大的变量多数是DataFrame。因此这里使用一种专门针对DataFrame的内存优化的方法。

DataFrame中,每个字段的类型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Data type   Description
bool_ Boolean (True or False) stored as a byte
int_ Default integer type (same as C long; normally either int64 or int32)
intc Identical to C int (normally int32 or int64)
intp Integer used for indexing (same as C ssize_t; normally either int32 or int64)
int8 Byte (-128 to 127)
int16 Integer (-32768 to 32767)
int32 Integer (-2147483648 to 2147483647)
int64 Integer (-9223372036854775808 to 9223372036854775807)
uint8 Unsigned integer (0 to 255)
uint16 Unsigned integer (0 to 65535)
uint32 Unsigned integer (0 to 4294967295)
uint64 Unsigned integer (0 to 18446744073709551615)
float_ Shorthand for float64.
float16 Half precision float: sign bit, 5 bits exponent, 10 bits mantissa
float32 Single precision float: sign bit, 8 bits exponent, 23 bits mantissa
float64 Double precision float: sign bit, 11 bits exponent, 52 bits mantissa
complex_ Shorthand for complex128.
complex64 Complex number, represented by two 32-bit floats (real and imaginary components)
complex128 Complex number, represented by two 64-bit floats (real and imaginary components)

可以看到,在int 中,就有intcintpint8int16int32int64等多种类型,越往后能表示的数据范围就越大,同时内存占用也随之增大。但很多时候,某个字段中的数据达不到这个范围,使用大的类型来表示是浪费的。比如一个字段类型是int64,但是这个字段中的数据都比较小,范围在-30000 to 30000之间,那么这时只需要使用int32来表示即可。

而 pandas 在读取数据时,由于不知道实际的数据范围,数据列为int类型时,默认是int64float类型默认是float64。因此我们需要根据实际的数据情况,为字段设置尽量小的数据类型。

因此这里构造一个函数,专门用于压缩int类型和float类型的数据,其他类型不好压缩,因此不处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def reduce_mem(df):
start_mem = df.memory_usage().sum() / 1024 ** 2
for col in df.columns:
col_type = df[col].dtypes
# 如果当前列的类型不是 object,剩下就是 int,int32, float,float32 等
if col_type != object:
# 取出这一列的最小值
c_min = df[col].min()
# 取出这一列的最大值
c_max = df[col].max()
# 如果类型是 int 开头的
if str(col_type)[:3] == 'int':
# 如果最大值和最小值都在 int8 的范围内,则转为 int8
if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
df[col] = df[col].astype(np.int8)
# 如果最大值和最小值都在 int16 的范围内,则转为 int16
elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
df[col] = df[col].astype(np.int16)
# 如果最大值和最小值都在 int32 的范围内,则转为 int32
elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
df[col] = df[col].astype(np.int32)
# 如果最大值和最小值都在 int64 的范围内,则转为 int64
elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
df[col] = df[col].astype(np.int64)
elif str(col_type)[:3] == 'float':# 如果类型是 float 开头的
# 如果最大值和最小值都在 float16 的范围内,则转为 float16
if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
df[col] = df[col].astype(np.float16)
# 如果最大值和最小值都在 float32 的范围内,则转为 float32
elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
df[col] = df[col].astype(np.float32)
else:
df[col] = df[col].astype(np.float64)
end_mem = df.memory_usage().sum() / 1024 ** 2 # 计算用了多少 M
print('{:.2f} Mb, {:.2f} Mb ({:.2f} %)'.format(start_mem, end_mem, 100 * (start_mem - end_mem) / start_mem))
gc.collect()
return df

参考

评论