时间序列数据预处理 - 利用NumPy步幅技巧节省内存

3
我正在对时间序列数据集进行预处理,将其形状从二维(数据点,特征)改变为三维(数据点,时间窗口,特征)。
在这样的视角下,时间窗口(有时也称为回溯)表示涉及作为输入变量以预测下一个时间段的前几个时间步骤/数据点的数量。换句话说,时间窗口是机器学习算法考虑单个未来预测所需的过去数据量。
这种方法(或者至少是我的实现方式)的问题在于它在内存使用方面相当低效,因为它会导致窗口之间的数据冗余,使输入数据变得非常沉重。
以下是我一直在使用的函数,将输入数据重新塑造为三维结构。
from sys import getsizeof

def time_framer(data_to_frame, window_size=1):
    """It transforms a 2d dataset into 3d based on a specific size;
    original function can be found at:
    https://machinelearningmastery.com/time-series-prediction-lstm-recurrent-neural-networks-python-keras/
    """
    n_datapoints = data_to_frame.shape[0] - window_size
    framed_data = np.empty(
        shape=(n_datapoints, window_size, data_to_frame.shape[1],)).astype(np.float32)

    for index in range(n_datapoints):
        framed_data[index] = data_to_frame[index:(index + window_size)]
        print(framed_data.shape)

    # it prints the size of the output in MB
    print(framed_data.nbytes / 10 ** 6)
    print(getsizeof(framed_data) / 10 ** 6)

    # quick and dirty quality test to check if the data has been correctly reshaped        
    test1=list(set(framed_data[0][1]==framed_data[1][0]))
    if test1[0] and len(test1)==1:
        print('Data is correctly framed')

    return framed_data

我被建议使用numpy的步幅技巧来克服这个问题并减小重塑数据的大小。不幸的是,到目前为止我找到的任何资源都集中在实现该技巧在二维数组上,就像这个优秀教程一样。我的用例涉及三维输出,我一直在努力。这是我最好的结果;然而,它既没有成功地减小framed_data的大小,也没有正确地框定数据,因为它没有通过质量测试。
我相当确定我的错误在于我没有完全理解步幅参数。 new_strides是我成功地向as_strided提供的唯一值。
from numpy.lib.stride_tricks import as_strided

def strides_trick_time_framer(data_to_frame, window_size=1):

    new_strides = (data_to_frame.strides[0],
                   data_to_frame.strides[0]*data_to_frame.shape[1] ,
                   data_to_frame.strides[0]*window_size)

    n_datapoints = data_to_frame.shape[0] - window_size
    print('striding.....')
    framed_data = as_strided(data_to_frame, 
                             shape=(n_datapoints, # .flatten() here did not change the outcome
                                    window_size,
                                    data_to_frame.shape[1]),                   
                                    strides=new_strides).astype(np.float32)
    # it prints the size of the output in MB
    print(framed_data.nbytes / 10 ** 6)
    print(getsizeof(framed_data) / 10 ** 6)

    # quick and dirty test to check if the data has been correctly reshaped        
    test1=list(set(framed_data[0][1]==framed_data[1][0]))
    if test1[0] and len(test1)==1:
        print('Data is correctly framed')

    return framed_data

任何帮助都将不胜感激!

我已经修改了问题,因为我实际上为了节省空间而转换为float32。 我不知道这是否会改变任何东西。 - Gbsbvm
2个回答

2
你可以使用我在这里创建的步幅模板函数window_nd。然后,如果你只需要跨越第一维,你只需要:
framed_data = window_nd(data_to_frame, window_size, axis = 0)

目前还没有内置的窗口函数可以跨任意轴工作,所以除非在scipy.signalskimage中最近实现了新的函数,否则这可能是您最好的选择。

编辑:要查看内存节省情况,您需要使用@ali_m描述的方法,该方法在此链接中,因为基本的ndarray.nbytes对共享内存太过简单。

def find_base_nbytes(obj):
    if obj.base is not None:
        return find_base_nbytes(obj.base)
    return obj.nbytes

新数组通过了质量检查,但从内存大小的角度来看并没有改善。 - Gbsbvm
1
哦,ndarray.nbytes似乎是一个天真的ndarray.itemsize * ndarray.size。它根本没有考虑共享元素。如果你想确定分布式数组的实际大小,请查看https://dev59.com/QZLea4cB1Zd3GeqP6L5b上的方法。 - Daniel F
只有当我使用 sys.getsizeof(基本属性无法改善)并且保持数据类型为 float64 时,才能看到内存的改进。如果我使用 float32 以节省更多内存,则生成的数组没有基本属性,并且在内存方面也没有改进。 - Gbsbvm
1
在使用getsizeof函数时要小心数组:https://stackoverflow.com/questions/52129595/why-the-size-of-numpy-array-is-different - hpaulj

2

关于这个 X

In [734]: X = np.arange(24).reshape(8,3)
In [735]: X.strides
Out[735]: (24, 8)

这个 as_strided 产生的数组与你的 time_framer 相同。

In [736]: np.lib.stride_tricks.as_strided(X, 
            shape=(X.shape[0]-3, 3, X.shape[1]), 
            strides=(24, 24, 8))
Out[736]: 
array([[[ 0,  1,  2],
        [ 3,  4,  5],
        [ 6,  7,  8]],

       [[ 3,  4,  5],
        [ 6,  7,  8],
        [ 9, 10, 11]],

       [[ 6,  7,  8],
        [ 9, 10, 11],
        [12, 13, 14]],

       [[ 9, 10, 11],
        [12, 13, 14],
        [15, 16, 17]],

       [[12, 13, 14],
        [15, 16, 17],
        [18, 19, 20]]])

它沿着最后一维度移动,就像X一样。倒数第二个也是如此。第一个向前移动一行,所以它也得到了X.strides[0]。因此,窗口大小只影响形状,而不影响步幅。
因此,在您的as_strided版本中,只需使用:
 new_strides = (data_to_frame.strides[0],
                data_to_frame.strides[0] ,
                data_to_frame.strides[1])

小修正。将默认窗口大小设置为2或更大。1会在测试中产生索引错误。
framed_data[0,1]==framed_data[1,0]

关于 getsizeof 的用法:

In [754]: sys.getsizeof(X)
Out[754]: 112
In [755]: X.nbytes
Out[755]: 192

等一下,为什么X的尺寸比nbytes小? 因为它是一个view(请参见上面的第[734]行)。

In [756]: sys.getsizeof(X.copy())
Out[756]: 304

正如在另一个SO中所指出的那样,getsizeof必须谨慎使用:

为什么numpy数组的大小不同?

现在来看扩展副本:

In [757]: x2=time_framer(X,4)
...
In [758]: x2.strides
Out[758]: (96, 24, 8)
In [759]: x2.nbytes
Out[759]: 384
In [760]: sys.getsizeof(x2)
Out[760]: 512

以及步幅版本

In [761]: x1=strides_trick_time_framer(X,4)
...
In [762]: x1.strides
Out[762]: (24, 24, 8)
In [763]: sys.getsizeof(x1)
Out[763]: 128
In [764]: x1.astype(int).strides
Out[764]: (96, 24, 8)
In [765]: sys.getsizeof(x1.astype(int))
Out[765]: 512
x1的大小就像一个视图(因为它是3D的,所以大小为128)。但是,如果我们尝试更改其dtype,它会进行复制,而且步幅和大小与x2相同。
许多对x1的操作将失去步进大小优势,例如x1.ravel()x1+1等。主要是像meansum这样的缩减操作可以产生实际的空间节省。

通过使用 sys.getsizeof,我看到了改进,但是当我编辑时,实际上我将 dtype 转换为 float32 以节省内存;作为 float32,"strided" 数组并没有变得更轻。 - Gbsbvm
1
as_strided 数组是原始数组的 view。也就是说,它使用了原始数据缓冲区。astype 强制它进行复制,并且它将是完整的。比较带有和不带有 astypestrides 属性。在创建完整副本之前,可以对 as_strided 数组执行有限数量的操作。 - hpaulj
我添加了一些 getsizeof 测试。 - hpaulj
所以,getsizeof对于视图并不有用 - 这就是步幅技巧返回的内容; 在视图上使用astype会创建原始副本 - 抵消了步幅技巧的好处; @Daniel F指出,nbytes是一个天真的ndarray.itemsize * ndarray.size,它没有考虑共享元素; - Gbsbvm
1
目前还没有一个有意义的方式来衡量使用as_strided所节省的内存。作为视图,它不会占用额外的内存(除了数组对象开销),而且复制品会被扩展到完整大小。 - hpaulj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接