วิธีที่เร็วที่สุดในการแมปชื่อกลุ่มของอาร์เรย์ numpy กับดัชนีคืออะไร


9

ฉันทำงานกับ 3D pointcloud ของ Lidar คะแนนจะได้รับจากอาร์เรย์ 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เพื่อให้ทุกคิวบ์รักษาดัชนีที่ไม่สามารถเข้าถึงได้และดัชนีดัชนีของฉันที่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]}

pointcloud ที่แท้จริงของฉันมีคะแนน 3D มากถึงไม่กี่ร้อยล้าน วิธีที่เร็วที่สุดในการจัดกลุ่มแบบนี้คืออะไร?

ฉันได้ลองใช้วิธีแก้ปัญหาต่าง ๆ เป็นส่วนใหญ่แล้ว นี่คือการเปรียบเทียบการคำนวณเวลาโดยสมมติว่าขนาดของคะแนนเป็นอาร์เรย์ 20 ล้านและขนาดของก้อนที่แตกต่างกันคือ 1 ล้าน:

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

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

Defauldict [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

การลดขนาดของ Pandas + [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
ไม่น่าเป็นไปได้ที่โซลูชันของ Pandas ที่เรียบง่ายดังกล่าวจะถูกเอาชนะด้วยวิธีการง่ายๆเนื่องจากมีการใช้ความพยายามอย่างมากในการปรับให้เหมาะสม วิธีที่อิง Cython อาจเข้าใกล้ แต่ฉันสงสัยว่ามันจะมีประสิทธิภาพสูงกว่า
norok2

1
@mathfux คุณต้องมีผลลัพธ์สุดท้ายเป็นพจนานุกรมหรือมันจะไม่เป็นไรที่จะมีกลุ่มและดัชนีของพวกเขาเป็นสองผลลัพธ์?
Divakar

@ norok2 numpy_indexedใกล้แค่นี้ด้วยเช่นกัน ฉันเดาว่าถูกต้อง ฉันใช้pandasสำหรับกระบวนการจำแนกของฉันในขณะนี้
mathfux

คำตอบ:


6

จำนวนดัชนีคงที่ต่อกลุ่ม

วิธีการ # 1

เราสามารถทำการdimensionality-reductionลดcubesเป็น 1D array ได้ นี้ขึ้นอยู่กับการทำแผนที่ของข้อมูลที่ได้รับก้อนลงบนตาราง heren-สลัวเพื่อคำนวณเทียบเท่าเชิงเส้นดัชนีการหารือในรายละเอียด จากนั้นขึ้นอยู่กับเอกลักษณ์ของดัชนีเชิงเส้นเหล่านั้นเราสามารถแยกกลุ่มที่ไม่ซ้ำกันและดัชนีที่เกี่ยวข้อง ดังนั้นตามกลยุทธ์เหล่านี้เราจะมีทางออกหนึ่งอย่างเช่น -

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แพคเกจเพื่อทำซ้ำและไปที่เอาต์พุตพจนานุกรม hashable สุดท้าย จะไปด้วยมันจะมีสองวิธี - หนึ่งที่ได้รับคีย์และค่าแยกต่างหากโดยใช้numbaและการโทรหลักจะซิปและแปลงเป็น Dict ในขณะที่อีกคนหนึ่งจะสร้างnumba-supportedประเภท Dict และจึงไม่จำเป็นต้องทำงานพิเศษโดยฟังก์ชั่นการโทรหลัก .

ดังนั้นเราจะมี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 int32และint64
mathfux

@ Mathfux ฉันสมมติnumber of indices per group would be a constant numberว่าฉันรวบรวมความคิดเห็น นั่นจะเป็นสมมติฐานที่ปลอดภัยหรือไม่? นอกจากนี้คุณกำลังทดสอบcubes.npzการส่งออกของ915791?
Divakar

ใช่ฉันทำ. ฉันไม่ได้ทดสอบจำนวนดัชนีต่อกลุ่มเพราะลำดับของชื่อกลุ่มอาจแตกต่างกัน ฉันทดสอบความยาวของพจนานุกรมของเอาต์พุตcubes.npzเท่านั้นและเป็น983234แนวทางอื่นที่ฉันแนะนำ
mathfux

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 ()แทนการแปลงคีย์เป็น tuple


ฉันพยายามทบทวนเวลาแสดงในขณะนี้ (สำหรับคะแนน 20M) ดูเหมือนว่าโซลูชันของฉันจะมีประสิทธิภาพมากขึ้นในแง่ของเวลาเพราะหลีกเลี่ยงการทำซ้ำ ฉันเห็นด้วยว่าการใช้หน่วยความจำนั้นมหาศาล
mathfux

อีกข้อเสนอres[tuple(elem)].append(idx)ใช้เวลา 50 วินาทีเทียบกับฉบับres[elem[0], elem[1], elem[2]].append(idx)ที่ใช้เวลา 30 วินาที
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

แต่มันจะไม่ทำให้คุณเร็วกว่าที่ Pandas ทำแม้ว่ามันจะเร็วที่สุดหลังจากนั้น (และอาจเป็นnumpy_indexโซลูชั่นพื้นฐาน) และไม่ได้มาพร้อมกับบทลงโทษจากหน่วยความจำของมัน คอลเลกชันของสิ่งที่ได้รับการเสนอชื่อเพื่อให้ห่างไกลที่นี่

ในเครื่องของ OP ที่ควรเข้าใกล้เวลาดำเนินการ ~ 12 วินาที


1
ขอบคุณมากฉันจะทดสอบในภายหลัง
mathfux
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.