numpy 배열의 그룹 이름을 인덱스에 매핑하는 가장 빠른 방법은 무엇입니까?


9

Lidar의 3D pointcloud로 작업하고 있습니다. 포인트는 다음과 같은 numpy 배열로 제공됩니다.

points = np.array([[61651921, 416326074, 39805], [61605255, 416360555, 41124], [61664810, 416313743, 39900], [61664837, 416313749, 39910], [61674456, 416316663, 39503], [61651933, 416326074, 39802], [61679969, 416318049, 39500], [61674494, 416316677, 39508], [61651908, 416326079, 39800], [61651908, 416326087, 39802], [61664845, 416313738, 39913], [61674480, 416316668, 39503], [61679996, 416318047, 39510], [61605290, 416360572, 41118], [61605270, 416360565, 41122], [61683939, 416313004, 41052], [61683936, 416313033, 41060], [61679976, 416318044, 39509], [61605279, 416360555, 41109], [61664837, 416313739, 39915], [61674487, 416316666, 39505], [61679961, 416318035, 39503], [61683943, 416313004, 41054], [61683930, 416313042, 41059]])

나는 크기의 큐브로 그룹화 내 데이터를 보관하고 싶은 50*50*50모든 큐브가 일부 해쉬 인덱스 내의 NumPy와 인덱스 보존하도록 points에 포함을 . 분할하기 위해 cubes = points \\ 50출력을 다음에 할당 합니다.

cubes = np.array([[1233038, 8326521, 796], [1232105, 8327211, 822], [1233296, 8326274, 798], [1233296, 8326274, 798], [1233489, 8326333, 790], [1233038, 8326521, 796], [1233599, 8326360, 790], [1233489, 8326333, 790], [1233038, 8326521, 796], [1233038, 8326521, 796], [1233296, 8326274, 798], [1233489, 8326333, 790], [1233599, 8326360, 790], [1232105, 8327211, 822], [1232105, 8327211, 822], [1233678, 8326260, 821], [1233678, 8326260, 821], [1233599, 8326360, 790], [1232105, 8327211, 822], [1233296, 8326274, 798], [1233489, 8326333, 790], [1233599, 8326360, 790], [1233678, 8326260, 821], [1233678, 8326260, 821]])

원하는 출력은 다음과 같습니다.

{(1232105, 8327211, 822): [1, 13, 14, 18]), 
(1233038, 8326521, 796): [0, 5, 8, 9], 
(1233296, 8326274, 798): [2, 3, 10, 19], 
(1233489, 8326333, 790): [4, 7, 11, 20], 
(1233599, 8326360, 790): [6, 12, 17, 21], 
(1233678, 8326260, 821): [15, 16, 22, 23]}

내 실제 포인트 클라우드에는 최대 수억 개의 3D 포인트가 포함되어 있습니다. 이런 종류의 그룹화를 수행하는 가장 빠른 방법은 무엇입니까?

나는 다양한 솔루션의 대부분을 시도했습니다. 다음은 포인트 크기가 약 2 천만이고 개별 큐브의 크기가 1 백만이라고 가정하고 시간 계산을 비교 한 것입니다.

팬더 [tuple (elem)-> np.array (dtype = int64)]

import pandas as pd
print(pd.DataFrame(cubes).groupby([0,1,2]).indices)
#takes 9sec

Defauldict [elem.tobytes () 또는 튜플-> 목록]

#thanks @abc:
result = defaultdict(list)
for idx, elem in enumerate(cubes):
    result[elem.tobytes()].append(idx) # takes 20.5sec
    # result[elem[0], elem[1], elem[2]].append(idx) #takes 27sec
    # result[tuple(elem)].append(idx) # takes 50sec

numpy_indexed [int-> np.array]

# thanks @Eelco Hoogendoorn for his library
values = npi.group_by(cubes).split(np.arange(len(cubes)))
result = dict(enumerate(values))
# takes 9.8sec

팬더 + 차원 축소 [int-> np.array (dtype = int64)]

# thanks @Divakar for showing numexpr library:
import numexpr as ne
def dimensionality_reduction(cubes):
    #cubes = cubes - np.min(cubes, axis=0) #in case some coords are negative 
    cubes = cubes.astype(np.int64)
    s0, s1 = cubes[:,0].max()+1, cubes[:,1].max()+1
    d = {'s0':s0,'s1':s1,'c0':cubes[:,0],'c1':cubes[:,1],'c2':cubes[:,2]}
    c1D = ne.evaluate('c0+c1*s0+c2*s0*s1',d)
    return c1D
cubes = dimensionality_reduction(cubes)
result = pd.DataFrame(cubes).groupby([0]).indices
# takes 2.5 seconds

그것은 다운로드 가능 cubes.npz파일을 여기 와 명령을 사용하여

cubes = np.load('cubes.npz')['array']

성능 시간을 확인하십시오.


결과의 각 목록에 항상 같은 수의 인덱스가 있습니까?
Mykola Zotko

예, 항상 동일합니다 : 위에서 언급 한 모든 솔루션에 대해 983234 개의 별개의 큐브.
mathfux

1
최적화를 위해 많은 노력을 기울 였기 때문에 간단한 접근 방식으로 이러한 간단한 팬더 솔루션을 이길 가능성은 거의 없습니다. Cython 기반 접근 방식이 아마도 접근 할 수는 있지만 그것을 능가할지는 의문입니다.
norok2

1
@mathfux 사전으로 최종 결과물을 가져야합니까, 아니면 그룹과 색인을 두 개의 결과물로 갖는 것이 좋을까요?
Divakar

@ norok2도 numpy_indexed접근합니다. 맞습니다. 내가 사용하는 pandas현재 나의 분류 프로세스.
mathfux 2014

답변:


6

그룹당 일정한 인덱스 수

접근법 # 1

1D 배열 dimensionality-reduction로 축소 할 수 있습니다 cubes. 이것은 주어진 큐브 데이터를 n-dim 그리드에 매핑하여 선형 인덱스 등가물을 계산하는 것을 기반으로합니다 here. 그런 다음 선형 지수의 고유성에 따라 고유 그룹과 해당 색인을 분리 할 수 ​​있습니다. 따라서 이러한 전략을 따르면 다음과 같은 하나의 솔루션이 있습니다.

N = 4 # number of indices per group
c1D = np.ravel_multi_index(cubes.T, cubes.max(0)+1)
sidx = c1D.argsort()
indices = sidx.reshape(-1,N)
unq_groups = cubes[indices[:,0]]

# If you need in a zipped dictionary format
out = dict(zip(map(tuple,unq_groups), indices))

대안 # 1 : 정수 값 cubes이 너무 큰 경우, dimensionality-reduction더 짧은 범위의 치수가 기본 축으로 선택되도록 할 수 있습니다 . 따라서, 이러한 경우에, 우리가 얻을 수있는 절감 단계를 수정할 수 c1D있도록처럼 -

s1,s2 = cubes[:,:2].max(0)+1
s = np.r_[s2,1,s1*s2]
c1D = cubes.dot(s)

접근법 # 2

다음은, 우리가 사용할 수있는 Cython-powered kd-tree빠른 가까운 이웃 조회에 이웃 지수에서 가장 가까운 얻을 따라서과 같이 우리의 경우를 해결하기 위해 -

from scipy.spatial import cKDTree

idx = cKDTree(cubes).query(cubes, k=N)[1] # N = 4 as discussed earlier
I = idx[:,0].argsort().reshape(-1,N)[:,0]
unq_groups,indices = cubes[I],idx[I]

일반적인 경우 : 그룹당 가변 인덱스 수

우리는 원하는 출력을 얻기 위해 argsort 기반 방법을 약간의 분할로 확장 할 것입니다-

c1D = np.ravel_multi_index(cubes.T, cubes.max(0)+1)

sidx = c1D.argsort()
c1Ds = c1D[sidx]
split_idx = np.flatnonzero(np.r_[True,c1Ds[:-1]!=c1Ds[1:],True])
grps = cubes[sidx[split_idx[:-1]]]

indices = [sidx[i:j] for (i,j) in zip(split_idx[:-1],split_idx[1:])]
# If needed as dict o/p
out = dict(zip(map(tuple,grps), indices))

cubes키 그룹의 1D 버전 사용

cubes사전 생성 과정을 단순화하고 효율적으로 만들기 위해 키 그룹으로 이전에 나열된 방법을 확장 합니다.

def numpy1(cubes):
    c1D = np.ravel_multi_index(cubes.T, cubes.max(0)+1)        
    sidx = c1D.argsort()
    c1Ds = c1D[sidx]
    mask = np.r_[True,c1Ds[:-1]!=c1Ds[1:],True]
    split_idx = np.flatnonzero(mask)
    indices = [sidx[i:j] for (i,j) in zip(split_idx[:-1],split_idx[1:])]
    out = dict(zip(c1Ds[mask[:-1]],indices))
    return out

다음으로 numba패키지를 사용하여 최종 해시 가능한 사전 출력을 반복합니다. 그것에 따라 두 가지 솔루션이 있습니다-하나는 키와 값을 개별적으로 사용 numba하고 주 호출은 압축하여 dict로 변환하는 반면 다른 하나는 numba-supporteddict 유형 을 생성 하므로 주 호출 기능에 필요한 추가 작업은 없습니다. .

따라서 첫 번째 numba해결책이 있습니다.

from numba import  njit

@njit
def _numba1(sidx, c1D):
    out = []
    n = len(sidx)
    start = 0
    grpID = []
    for i in range(1,n):
        if c1D[sidx[i]]!=c1D[sidx[i-1]]:
            out.append(sidx[start:i])
            grpID.append(c1D[sidx[start]])
            start = i
    out.append(sidx[start:])
    grpID.append(c1D[sidx[start]])
    return grpID,out

def numba1(cubes):
    c1D = np.ravel_multi_index(cubes.T, cubes.max(0)+1)
    sidx = c1D.argsort()
    out = dict(zip(*_numba1(sidx, c1D)))
    return out

두 번째 numba해결책은 다음과 같습니다.

from numba import types
from numba.typed import Dict

int_array = types.int64[:]

@njit
def _numba2(sidx, c1D):
    n = len(sidx)
    start = 0
    outt = Dict.empty(
        key_type=types.int64,
        value_type=int_array,
    )
    for i in range(1,n):
        if c1D[sidx[i]]!=c1D[sidx[i-1]]:
            outt[c1D[sidx[start]]] = sidx[start:i]
            start = i
    outt[c1D[sidx[start]]] = sidx[start:]
    return outt

def numba2(cubes):
    c1D = np.ravel_multi_index(cubes.T, cubes.max(0)+1)    
    sidx = c1D.argsort()
    out = _numba2(sidx, c1D)
    return out

cubes.npz데이터 타이밍 -

In [4]: cubes = np.load('cubes.npz')['array']

In [5]: %timeit numpy1(cubes)
   ...: %timeit numba1(cubes)
   ...: %timeit numba2(cubes)
2.38 s ± 14.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
2.13 s ± 25.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
1.8 s ± 5.95 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

대안 # 1 : 우리는 다음 과 같이 numexpr큰 배열을 계산 하여 더 빠른 속도를 달성 할 수 있습니다 .c1D

import numexpr as ne

s0,s1 = cubes[:,0].max()+1,cubes[:,1].max()+1
d = {'s0':s0,'s1':s1,'c0':cubes[:,0],'c1':cubes[:,1],'c2':cubes[:,2]}
c1D = ne.evaluate('c0+c1*s0+c2*s0*s1',d)

이것은 필요한 모든 장소에 적용 할 수 있습니다 c1D.


답변 주셔서 감사합니다! 나는 여기에서 cKDTree의 사용이 기대되지 않았다. 그러나 여전히 # Approach1에 문제가 있습니다. 출력 길이는 915791입니다. 나는 이것이 갈등의 어떤 종류의 추측 dtypes int32int64
mathfux

@mathfux 나는 number of indices per group would be a constant number의견을 모았다고 가정 합니다. 안전한 가정일까요? 또한 ? cubes.npz의 출력 을 테스트 하고 915791있습니까?
Divakar

네 저도 그렇습니다. 그룹 이름의 순서가 다를 수 있으므로 그룹당 인덱스 수를 테스트하지 않았습니다. 나는 출력 사전의 길이 cubes.npz만 테스트하고 983234내가 제안한 다른 접근법 에 대한 것이었다 .
mathfux 2014

1
@mathfux Approach #3 가변 개수의 인덱스에 대한 일반적인 경우를 확인하십시오 .
Divakar

1
@mathfux Yup은 최소값이 0보다 작 으면 일반적으로 오프셋이 필요합니다. 정밀도를 잘 잡아냅니다!
Divakar

5

각 요소의 색인을 반복하여 해당 목록에 추가 할 수 있습니다.

from collections import defaultdict

res = defaultdict(list)

for idx, elem in enumerate(cubes):
    #res[tuple(elem)].append(idx)
    res[elem.tobytes()].append(idx)

키를 튜플로 변환하는 대신 tobytes () 를 사용하여 런타임을 더욱 향상시킬 수 있습니다 .


현재 성능 시간을 검토하려고합니다 (2 천만 포인트). 반복을 피하기 때문에 내 솔루션이 시간면에서 더 효율적인 것 같습니다. 메모리 소비가 엄청나다는 데 동의합니다.
mathfux

다른 제안 res[tuple(elem)].append(idx)res[elem[0], elem[1], elem[2]].append(idx)30 초가 걸리는 판에 비해 50 초가 걸렸습니다.
mathfux

3

Cython을 사용할 수 있습니다.

%%cython -c-O3 -c-march=native -a
#cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True, infer_types=True

import math
import cython as cy

cimport numpy as cnp


cpdef groupby_index_dict_cy(cnp.int32_t[:, :] arr):
    cdef cy.size_t size = len(arr)
    result = {}
    for i in range(size):
        key = arr[i, 0], arr[i, 1], arr[i, 2]
        if key in result:
            result[key].append(i)
        else:
            result[key] = [i]
    return result

그러나 팬더보다 더 빠르지는 않지만, 그 이후로는 (그리고 아마도 numpy_index기반 솔루션) 가장 빠르며 메모리 패널티는 제공되지 않습니다. 지금까지 제안 된 것들이 여기에 있습니다 .

OP의 머신에서 실행 시간이 ~ 12 초에 가까워 야합니다.


1
고마워요, 나중에 테스트하겠습니다.
mathfux 2014
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.