Python中的可增量尺寸Numpy数组

4
我刚刚遇到了在Python中使用增量Numpy数组的需求,由于我没有找到任何东西,所以我自己实现了它。我想知道我的方法是否是最好的,或者你能否提出其他想法。
问题是,我有一个2D数组(程序处理nD数组),其大小事先未知,并且需要在一个方向上连接可变数量的数据(假设我必须多次调用np.vstak)。每次连接数据时,我需要取出数组,沿轴0对其进行排序并进行其他操作,因此我无法构建一个长数组列表,然后一次性地将其np.vstak起来。由于内存分配很昂贵,因此我转向增量数组,在其中将数组的大小增加比我需要的大小更大的量(我使用50%增量),以便最小化分配次数。
我编写了这个代码,您可以在以下代码中看到:
class ExpandingArray:

    __DEFAULT_ALLOC_INIT_DIM = 10   # default initial dimension for all the axis is nothing is given by the user
    __DEFAULT_MAX_INCREMENT = 10    # default value in order to limit the increment of memory allocation

    __MAX_INCREMENT = []    # Max increment
    __ALLOC_DIMS = []       # Dimensions of the allocated np.array
    __DIMS = []             # Dimensions of the view with data on the allocated np.array (__DIMS <= __ALLOC_DIMS)

    __ARRAY = []            # Allocated array

    def __init__(self,initData,allocInitDim=None,dtype=np.float64,maxIncrement=None):
        self.__DIMS = np.array(initData.shape)

        self.__MAX_INCREMENT = maxIncrement
        if self.__MAX_INCREMENT == None:
            self.__MAX_INCREMENT = self.__DEFAULT_MAX_INCREMENT

        # Compute the allocation dimensions based on user's input
        if allocInitDim == None:
            allocInitDim = self.__DIMS.copy()

        while np.any( allocInitDim < self.__DIMS  ) or np.any(allocInitDim == 0):
            for i in range(len(self.__DIMS)):
                if allocInitDim[i] == 0:
                    allocInitDim[i] = self.__DEFAULT_ALLOC_INIT_DIM
                if allocInitDim[i] < self.__DIMS[i]:
                    allocInitDim[i] += min(allocInitDim[i]/2, self.__MAX_INCREMENT)

        # Allocate memory 
        self.__ALLOC_DIMS = allocInitDim
        self.__ARRAY = np.zeros(self.__ALLOC_DIMS,dtype=dtype)

        # Set initData 
        sliceIdxs = [slice(self.__DIMS[i]) for i in range(len(self.__DIMS))]
        self.__ARRAY[sliceIdxs] = initData

    def shape(self):
        return tuple(self.__DIMS)

    def getAllocArray(self):
        return self.__ARRAY

    def getDataArray(self):
        """
        Get the view of the array with data
        """
        sliceIdxs = [slice(self.__DIMS[i]) for i in range(len(self.__DIMS))]
        return self.__ARRAY[sliceIdxs]

    def concatenate(self,X,axis=0):
        if axis > len(self.__DIMS):
            print "Error: axis number exceed the number of dimensions"
            return

        # Check dimensions for remaining axis 
        for i in range(len(self.__DIMS)):
            if i != axis:
                if X.shape[i] != self.shape()[i]:
                    print "Error: Dimensions of the input array are not consistent in the axis %d" % i
                    return

        # Check whether allocated memory is enough 
        needAlloc = False
        while self.__ALLOC_DIMS[axis] < self.__DIMS[axis] + X.shape[axis]:
            needAlloc = True
            # Increase the __ALLOC_DIMS 
            self.__ALLOC_DIMS[axis] += min(self.__ALLOC_DIMS[axis]/2,self.__MAX_INCREMENT)

        # Reallocate memory and copy old data 
        if needAlloc:
            # Allocate 
            newArray = np.zeros(self.__ALLOC_DIMS)
            # Copy 
            sliceIdxs = [slice(self.__DIMS[i]) for i in range(len(self.__DIMS))]
            newArray[sliceIdxs] = self.__ARRAY[sliceIdxs]
            self.__ARRAY = newArray

        # Concatenate new data 
        sliceIdxs = []
        for i in range(len(self.__DIMS)):
            if i != axis:
                sliceIdxs.append(slice(self.__DIMS[i]))
            else:
                sliceIdxs.append(slice(self.__DIMS[i],self.__DIMS[i]+X.shape[i]))

        self.__ARRAY[sliceIdxs] = X
        self.__DIMS[axis] += X.shape[axis]

这段代码比vstack/hstack拼接多个随机大小的数组要表现得更好。

我想知道的是:这是最好的方法吗?在numpy中已经有类似的方法了吗?

进一步地,如果能够重载np.array的切片赋值运算符,那么只要用户分配的内容超出了实际维度,就会执行ExpandingArray.concatenate()。如何进行这样的重载?

测试代码:我在此贴出了一些用于比较vstack和我的方法的代码。我添加了最大长度为100的随机数据块。

import time

N = 10000

def performEA(N):
    EA = ExpandingArray(np.zeros((0,2)),maxIncrement=1000)
    for i in range(N):
        nNew = np.random.random_integers(low=1,high=100,size=1)
        X = np.random.rand(nNew,2)
        EA.concatenate(X,axis=0)
        # Perform operations on EA.getDataArray()
    return EA

def performVStack(N):
    A = np.zeros((0,2))
    for i in range(N):
        nNew = np.random.random_integers(low=1,high=100,size=1)
        X = np.random.rand(nNew,2)
        A = np.vstack((A,X))
        # Perform operations on A
    return A

start_EA = time.clock()
EA = performEA(N)
stop_EA = time.clock()

start_VS = time.clock()
VS = performVStack(N)
stop_VS = time.clock()

print "Elapsed Time EA: %.2f" % (stop_EA-start_EA)
print "Elapsed Time VS: %.2f" % (stop_VS-start_VS)

不要使用三引号字符串作为注释...那不是它们的用途... - mgilson
好的,知道了。我刚看到它 :) 谢谢 - Daniele Bigoni
@mgilson:嘿,这是Guido认可的:[链接](https://twitter.com/gvanrossum/status/112670605505077248)。我自己也这样做,尽管价值不大。 :^) - DSM
1
@DSM -- 这让我感到震惊,Guido 竟然支持它... 我仍然坚持我的原话。我想知道他们是只为 Cpython 生成代码还是其他版本也一样。 - mgilson
@mgilson:PyPy似乎也避免生成字节码。 - DSM
2个回答

2

我认为处理这些小数组的最常见的设计模式就是使用列表。当然,你可以做一些像动态调整大小的事情(如果你想疯狂一点,你也可以尝试使用调整array大小的方法)。我认为一个典型的方法是始终将大小加倍,当你真的不知道事物的大小时。当然,如果你知道数组将增长到多大,那么直接分配整个空间是最简单的。

def performVStack_fromlist(N):
    l = []
    for i in range(N):
        nNew = np.random.random_integers(low=1,high=100,size=1)
        X = np.random.rand(nNew,2)
        l.append(X)
    return np.vstack(l)

我相信在某些情况下,扩展数组可能会很有用(例如当附加的数组都非常小的时候),但是使用上述模式处理此循环似乎更好。优化主要是关于您需要多少次复制所有内容,并且像这样进行列表(除了列表本身)的操作只需一次。因此,通常速度要快得多。


我实际上避免采用这种列表方法,因为每次我连接东西时,我还需要在数组上执行其他操作(如排序和许多其他操作)。我已经在示例中编辑了需要执行其他操作的注释。 - Daniele Bigoni

2
当我面临类似的问题时,我使用了ndarray.resize()函数(http://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.resize.html#numpy.ndarray.resize)。大部分情况下,它会避免重新分配和复制。虽然我不能保证它一定更快(但很可能更快),但它更简单。
至于你的第二个问题,我认为覆盖切片赋值以扩展数组不是一个好主意。这个运算符是用来给已有的元素/切片赋值的。如果你想改变这个行为,在某些情况下它不会立即清楚你希望它如何表现,例如:
a = MyExtendableArray(np.arange(100))
a[200] = 6  # resize to 200? pad [100:200] with what?
a[90:110] = 7  # assign to existing items AND automagically-allocated items?
a[::-1][200] = 6 # ...

我的建议是,切片赋值和数据追加应该保持分开。

+1 对于覆盖建议。关于调整大小,我喜欢这个建议,但是“引用数组会防止调整大小...”而且我可能需要在外部引用它。 - Daniele Bigoni

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