为什么这个Python逻辑索引占用这么多内存

2 投票
3 回答
1260 浏览
提问于 2025-04-17 14:42

我有一个一维的时间序列,长度是1亿(也就是100,000,000个元素)。这里是我在Dropbox上使用的数据链接。(这个文件有382 MB。)

更新

根据内存分析,错误发生在这一行:

data[absolute(data-dc)< m*std(data)]=dc.

更具体来说,操作absolute(data-dc)消耗了所有的内存。data就是上面提到的那个,而dc是一个常量。也许这是一个微妙的语法错误?

我想要去掉一些异常值和伪影,并用中位数替换这些值。我尝试用以下函数来实现这个目标。

 from numpy import *

 from sys import argv

 from scipy.io import savemat
 from scipy.stats import scoreatpercentile

 def reject_outliers(data,dc,m=3):
      data[data==0] = dc
      data[bp.absolute(data-dc) < m*np.std(data)] = dc
      return data

 def butter_bandpass(lowcut,highcut,fs,order=8):
    nyq = 0.5*fs
    low = lowcut/nyq
    high = highcut/nyq

    b,a= butter(order, [low, high], btype='band')
    return b,a

 def butter_bandpass_filter(data,lowcut,highcut,fs,order=8):
    b,a = butter_bandpass(lowcut,highcut,fs,order=order)
    return lfilter(b,a,data) 

 OFFSET = 432
 filename = argv[1]
 outname = argv[2]  

 print 'Opening '+ filename
 with open(filename,'rb') as stream:
      stream.seek(OFFSET)
      data=fromfile(stream,dtype='int16')
 print 'Removing Artifacts, accounting for zero-filling'
 dc = median(data)
 data = reject_outliers(data,dc)

 threshold = scoreatpercentile(absolute(data),85)   
 print 'Filtering and Detrending'
 data = butter_bandpass_filter(data,300,7000,20000)
 savemat(outname+'.mat',mdict={'data':data})

调用这个函数处理一个文件时,消耗了4 GB的内存和3 GB的虚拟内存。我确信问题出在这个函数的第二行,因为我一步步调试我写的脚本,发现它总是在这一部分卡住。我甚至可以在OS X的Finder中看到可用的硬盘空间每秒都在急剧下降。

时间序列的长度并不足以解释这个问题。那么reject-outliers的第二行到底出了什么问题呢?

3 个回答

4

这是你数据和修改过的 @mbatchkarov的代码 的结果:

$ python mbatchkarov.py 
Filename: mbatchkarov.py

Line #    Mem usage    Increment   Line Contents
================================================
     5                             @profile
     6     15.74 MB      0.00 MB   def go(m=3):
     7     15.74 MB      0.00 MB       header_size = 432
     8     15.74 MB      0.00 MB       with open('ch008.ddt', 'rb') as file:
     9     15.75 MB      0.00 MB           file.seek(header_size)
    10    380.10 MB    364.36 MB           data = np.fromfile(file, dtype=np.int16) # 2 bytes per item                                                             
    11    380.20 MB      0.10 MB       dc = np.median(data)
    12                             
    13                                 # data[np.absolute(data - dc) < m*np.std(data)] = dc                                                                        
    14                                 # `data - dc` => temporary array 8 bytes per item                                                                           
    15    744.56 MB    364.36 MB       t = data.copy()
    16    744.66 MB      0.09 MB       t -= dc
    17    744.66 MB      0.00 MB       np.absolute(t, t)
    18    926.86 MB    182.20 MB       b = t < m*np.std(data) # boolean => 1 byte per item                                                                         
    19    926.87 MB      0.01 MB       data[b] = dc
    20    926.87 MB      0.00 MB       return data

data - dc 这个操作会需要更多的内存:200M个数据项,每个数据项占8个字节,也就是说,data - dc 会因为广播的原因创建一个或两个临时的双精度数组。为了避免这种情况,可以先明确地复制一份数据,然后在原地进行减法操作:

t = data.copy() # 200M items x 2 bytes per item
t -= dc

看起来 memory_profiler 并没有显示临时数组的内存使用情况。程序的最大内存使用量大约是3GB。

5

我刚刚生成了1亿个随机的小数,并且做了你提到的那种索引。整个过程的内存使用量一直都在1GB以下。你的代码还有其他我们不知道的操作吗?可以试试用一个很棒的工具memory_profiler来运行你的代码,看看内存使用情况。


编辑:我添加了代码和memory_profiler的输出:

from numpy.random import uniform
import numpy

@profile
def go(m=3):
    data = uniform(size=100000000)
    dc = numpy.median(data)
    data[numpy.absolute(data-dc) < m*numpy.std(data)] = dc
    return data

if __name__ == '__main__':
    go()

输出:

Filename: example.py

Line #    Mem usage    Increment   Line Contents
================================================
     3                             @profile
     4     15.89 MB      0.00 MB   def go(m=3):
     5    778.84 MB    762.95 MB    data = uniform(size=100000000)
     6    778.91 MB      0.06 MB    dc = numpy.median(data)
     7    874.34 MB     95.44 MB    data[numpy.absolute(data-dc) < m*numpy.std(data)] = dc
     8    874.34 MB      0.00 MB    return data

正如你所看到的,1亿个小数并没有占用那么多内存。

3

Memory_profiler 记录的是在执行某一行代码之后,Python 虚拟机的内存状态。因此,如果在一行代码中创建和销毁的数组,它们不会出现在内存使用情况的报告中。

以 @mbatchkarov 的例子为例,你可以把 "data[numpy.absolute(data-dc) < m*numpy.std(data)] = dc" 这一行拆分成更小的部分,这样就能看到临时数组对内存的影响:

from numpy.random import uniform
import numpy

@profile
def go(m=3):
    data = uniform(size=100000000)
    dc = numpy.median(data)
    t1 = data-dc
    t2 = numpy.absolute(t1) < m*numpy.std(data)
    data[t2] = dc
    return data

if __name__ == '__main__':
    go()

这样就能得到

$ python -m memory_profiler t1.py 
Filename: t1.py

Line #    Mem usage    Increment   Line Contents
================================================
     4                             @profile
     5     16.61 MB      0.00 MB   def go(m=3):
     6    779.56 MB    762.95 MB       data = uniform(size=100000000)
     7    779.62 MB      0.06 MB       dc = numpy.median(data)
     8   1542.57 MB    762.95 MB       t1 = data-dc
     9   1637.99 MB     95.42 MB       t2 = numpy.absolute(t1) < m*numpy.std(data)
    10   1638.00 MB      0.02 MB       data[t2] = dc
    11   1638.00 MB      0.00 MB       return data

在这里很明显,"data-dc" 这一操作会重复占用内存。解决这个问题的方法是直接在原数组上进行减法,也就是说,把 "t1 = data - dc" 改成 "data -= dc":

$ python -m memory_profiler t1.py 
Filename: t1.py

Line #    Mem usage    Increment   Line Contents
================================================
     4                             @profile
     5     16.61 MB      0.00 MB   def go(m=3):
     6    779.56 MB    762.95 MB       data = uniform(size=100000000)
     7    779.62 MB      0.06 MB       dc = numpy.median(data)
     8    779.63 MB      0.01 MB       data -= dc
     9    875.05 MB     95.42 MB       t2 = numpy.absolute(data) < m*numpy.std(data)
    10    875.07 MB      0.02 MB       data[t2] = dc
    11    875.07 MB      0.00 MB       return data

如你所见,"data -= dc" 现在几乎不会增加内存使用。

撰写回答