सूचकांकों को संख्यात्मक सरणी के समूह नामों को मैप करने का सबसे तेज़ तरीका क्या है?


9

मैं लिडार के 3 डी पॉइंटक्लाउड के साथ काम कर रहा हूं। अंक इस प्रकार दिखाई देने वाले संख्यात्मक सरणी द्वारा दिए गए हैं:

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ताकि हर घन कुछ hashable सूचकांक और मेरे की 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 अंक हैं। इस तरह की ग्रुपिंग करने का सबसे तेज़ तरीका क्या है?

मैंने कई तरह के समाधानों की कोशिश की है। यहाँ समय गणना की तुलना यह मानते हुए कि अंकों का आकार 20 मिलियन है और अलग-अलग क्यूब्स का आकार 1 मिलियन है।

पंडस [टपल (एलएम) -> np.array (dtype = int64)]

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

डिफ़ाल्डडिक्ट [elem.tobytes () या tuple -> सूची]

#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']

प्रदर्शन समय की जांच करने के लिए।


क्या आपके परिणाम में हमेशा प्रत्येक सूची में समान संख्या में सूचकांक होते हैं?
मायकोला जोतको

हां, यह हमेशा समान होता है: उपर्युक्त सभी समाधानों के लिए 983234 अलग-अलग क्यूब्स।
मैथफॉक्स

1
यह संभावना नहीं है कि इस तरह के एक सरल पंडों के समाधान को एक सरल दृष्टिकोण से पीटा जाएगा, क्योंकि इसे अनुकूलित करने में बहुत प्रयास किए गए हैं। साइथॉन-आधारित दृष्टिकोण शायद यह दृष्टिकोण कर सकता है, लेकिन मुझे संदेह है कि यह इसे बेहतर बना देगा।
norok2

1
@mathfux क्या आपको अंतिम आउटपुट एक डिक्शनरी के रूप में देना है या समूह और उनके सूचकांकों को दो आउटपुट के रूप में रखना ठीक होगा?
दिवाकर

@ norok2 numpy_indexedकेवल इसे भी अप्रोच करता है। मुझे लगता है कि यह सही है। मैं pandasवर्तमान में अपनी वर्गीकरण प्रक्रियाओं के लिए उपयोग करता हूं।
मैथफॉक्स 19

जवाबों:


6

प्रति समूह सूचकांकों की लगातार संख्या

दृष्टिकोण # १

हम 1 डी सरणी dimensionality-reductionको कम करने के लिए प्रदर्शन कर सकते हैं cubes। यह विस्तार से चर्चा की गई रैखिक-सूचकांक समकक्षों की गणना करने के लिए एन-मंद ग्रिड पर दिए गए क्यूब्स डेटा की मैपिंग पर आधारित है 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]

सामान्य मामला: प्रति समूह सूचकांकों की परिवर्तनीय संख्या

हम अपने वांछित आउटपुट को प्राप्त करने के लिए कुछ बंटवारे के साथ आर्गसॉर्ट आधारित विधि का विस्तार करेंगे, जैसे -

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और मुख्य कॉलिंग ज़िप करेगा और तानाशाही में बदल जाएगा, जबकि दूसरा एक numba-supportedतानाशाही प्रकार का निर्माण करेगा और इसलिए मुख्य कॉलिंग फ़ंक्शन द्वारा आवश्यक कोई अतिरिक्त कार्य नहीं होगा ।

इस प्रकार, हमारे पास पहला 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 का उपयोग यहाँ संभव है। हालाँकि, आपके # दृष्टिकोण 1 के साथ अभी भी कुछ समस्याएं हैं। आउटपुट की लंबाई केवल 915791 है। मुझे लगता है कि यह किसी तरह का संघर्ष है dtypes int32औरint64
मैथ्यूक्स

@ मैथफक्स मैं मान number of indices per group would be a constant numberरहा हूं कि मैं टिप्पणियों से एकत्र हुआ हूं। क्या यह एक सुरक्षित धारणा होगी? इसके अलावा, आप cubes.npzके उत्पादन के लिए परीक्षण कर रहे हैं 915791?
दिवाकर

हां, है। मैंने प्रति समूह सूचकांकों की संख्या का परीक्षण नहीं किया क्योंकि समूह नामों का क्रम भिन्न हो सकता है। मैं cubes.npzकेवल आउटपुट के शब्दकोश की लंबाई का परीक्षण करता हूं और यह 983234मेरे द्वारा सुझाए गए अन्य दृष्टिकोणों के लिए था ।
मैथ्यूक्स

1
@mathfux Approach #3 सूचकांकों की चर संख्या के उस सामान्य मामले की जाँच करें ।
दिवाकर

1
@ मैथफक्स यूप कि ऑफसेटिंग की आवश्यकता आम तौर पर होती है यदि न्यूनतम 0. से कम हो तो सटीक पर अच्छा पकड़!
दिवाकर

5

आप बस पुनरावृत्ति कर सकते हैं और प्रत्येक तत्व के सूचकांक को संबंधित सूची में जोड़ सकते हैं।

from collections import defaultdict

res = defaultdict(list)

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

ट्यूल की कुंजी को परिवर्तित करने के बजाय टोबाइट्स () का उपयोग करके रनटाइम को और बेहतर बनाया जा सकता है ।


मैं इस समय प्रदर्शन समय की समीक्षा करने की कोशिश कर रहा हूं (20M अंक के लिए)। ऐसा लगता है कि समय के संदर्भ में मेरा समाधान अधिक कुशल है क्योंकि पुनरावृत्ति से बचा जाता है। मैं मानता हूं, मेमोरी की खपत बहुत अधिक है।
गणितफक्स

एक अन्य प्रस्ताव res[tuple(elem)].append(idx)में 50 सेकंड बनाम इसके संस्करण res[elem[0], elem[1], elem[2]].append(idx)में 30 सेकंड लगे।
मैथ्यूक्स

3

आप साइथन का उपयोग कर सकते हैं:

%%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आधारित समाधान) है, और यह स्मृति दंड के साथ नहीं आता है। अब तक जो प्रस्तावित किया गया है उसका एक संग्रह यहां है

ओपी की मशीन में ~ 12 सेकंड निष्पादन समय के करीब होना चाहिए।


1
बहुत बहुत धन्यवाद, मैं इसे बाद में परीक्षण करूँगा।
मैथ्यूक्स
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.