Apa cara tercepat untuk memetakan nama grup array numpy ke indeks?

9

Saya bekerja dengan pointcloud 3D dari Lidar. Poin diberikan oleh array numpy yang terlihat seperti ini:

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]])

Saya ingin menjaga data saya dikelompokkan ke dalam ukuran kubus 50*50*50sehingga setiap kubus mempertahankan beberapa indeks hashable dan indeks numpy dari pointsisinya . Untuk mendapatkan pemisahan, saya menetapkan cubes = points \\ 50output mana ke:

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]])

Output yang saya inginkan terlihat seperti ini:

{(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 saya yang sebenarnya berisi beberapa ratus juta poin 3D. Apa cara tercepat untuk melakukan pengelompokan semacam ini?

Saya sudah mencoba mayoritas dari berbagai solusi. Berikut adalah perbandingan perhitungan waktu dengan asumsi ukuran poin adalah sekitar 20 juta dan ukuran kubus berbeda adalah sekitar 1 juta:

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 () atau tuple -> list]

#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

Pengurangan panda + dimensi [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

Anda dapat mengunduh cubes.npzfile di sini dan menggunakan perintah

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

untuk memeriksa waktu kinerja.

mathfux
sumber
Apakah Anda selalu memiliki jumlah indeks yang sama di setiap daftar di hasil Anda?
Mykola Zotko
Ya, selalu sama: 983234 kubus berbeda untuk semua solusi yang disebutkan di atas.
mathfux
1
Tidak mungkin bahwa solusi Pandas yang sederhana seperti itu akan dikalahkan oleh pendekatan sederhana, karena banyak upaya telah dilakukan untuk mengoptimalkannya. Pendekatan berbasis Cython mungkin bisa mendekatinya, tapi saya ragu itu akan mengungguli itu.
norok2
1
@ mathfux Apakah Anda harus memiliki hasil akhir sebagai kamus atau apakah boleh menggunakan grup dan indeksnya sebagai dua hasil?
Divakar
@ norok2 numpy_indexedhanya mendekatinya juga. Saya kira itu benar. Saya menggunakan pandasuntuk proses klasifikasi saya saat ini.
mathfux

Jawaban:

6

Jumlah konstan indeks per grup

Pendekatan # 1

Kita dapat melakukan dimensionality-reductionuntuk mengurangi cubeske array 1D. Ini didasarkan pada pemetaan data kubus yang diberikan ke grid n-dim untuk menghitung persamaan indeks linear, dibahas secara rinci here. Kemudian, berdasarkan keunikan dari indeks linear tersebut, kita dapat memisahkan grup unik dan indeks terkaitnya. Oleh karena itu, mengikuti strategi-strategi itu, kita akan memiliki satu solusi, seperti -

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))

Alternatif # 1: Jika nilai integer cubesterlalu besar, kita mungkin ingin melakukan dimensionality-reductionsedemikian rupa sehingga dimensi dengan tingkat yang lebih pendek dipilih sebagai sumbu utama. Karenanya, untuk kasus-kasus tersebut, kita dapat memodifikasi langkah reduksi untuk mendapatkannya c1D, seperti -

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

Pendekatan # 2

Selanjutnya, kita dapat menggunakan Cython-powered kd-treepencarian tetangga terdekat terdekat untuk mendapatkan indeks tetangga terdekat dan karenanya menyelesaikan kasus kita seperti ini -

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]

Kasus umum: Jumlah variabel indeks per grup

Kami akan memperluas metode berbasis argsort dengan beberapa pemisahan untuk mendapatkan hasil yang diinginkan, seperti -

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))

Menggunakan versi 1D grup cubessebagai kunci

Kami akan memperluas metode yang terdaftar sebelumnya dengan kelompok cubessebagai kunci untuk menyederhanakan proses pembuatan kamus dan juga membuatnya efisien dengan itu, seperti begitu -

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

Selanjutnya, kita akan menggunakan numbapaket untuk beralih dan mendapatkan hasil akhir kamus hashable. Bersamaan dengan itu, akan ada dua solusi - Satu yang mendapatkan kunci dan nilai secara terpisah menggunakan numbadan panggilan utama akan zip dan dikonversi ke dict, sementara yang lain akan membuat numba-supportedtipe dict dan karenanya tidak ada pekerjaan tambahan yang diperlukan oleh fungsi panggilan utama .

Dengan demikian, kita akan memiliki numbasolusi pertama :

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

Dan numbasolusi kedua sebagai:

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

Pengaturan waktu dengan cubes.npzdata -

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)

Alternatif # 1: Kita dapat mencapai peningkatan lebih lanjut dengan numexpruntuk array besar untuk dihitung c1D, seperti -

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)

Ini akan berlaku di semua tempat yang membutuhkan c1D.

Divakar
sumber
Terima kasih banyak atas tanggapannya! Saya tidak berharap penggunaan cKDTree dimungkinkan di sini. Namun, masih ada beberapa masalah dengan # Approach1 Anda. Panjang output hanya 915791. Saya kira ini adalah semacam konflik antara dtypes int32danint64
mathfux
@ mathfux Saya berasumsi number of indices per group would be a constant numberbahwa saya mengumpulkan komentar. Apakah itu asumsi yang aman? Juga, apakah Anda menguji cubes.npzoutput 915791?
Divakar
Ya saya lakukan. Saya tidak menguji jumlah indeks per grup karena urutan nama grup mungkin berbeda. Saya menguji panjang kamus cubes.npzhanya dari output dan itu 983234untuk pendekatan lain yang saya sarankan.
mathfux
1
@ mathfux Periksa Approach #3 untuk kasus umum dari jumlah variabel indeks.
Divakar
1
@ mathfux Yup bahwa penyeimbangan diperlukan secara umum jika minimum kurang dari 0. Tangkapan yang bagus pada presisi!
Divakar
5

Anda mungkin hanya mengulang dan menambahkan indeks setiap elemen ke daftar yang sesuai.

from collections import defaultdict

res = defaultdict(list)

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

Runtime dapat lebih ditingkatkan dengan menggunakan tobytes () alih-alih mengubah kunci menjadi tuple.

abc
sumber
Saya sedang mencoba melakukan review waktu kinerja saat ini (untuk 20 juta poin). Tampaknya solusi saya lebih efisien dalam hal waktu karena iterasi dihindari. Saya setuju, konsumsi memori sangat besar.
mathfux
proposal lain res[tuple(elem)].append(idx)butuh 50 detik vs edisi res[elem[0], elem[1], elem[2]].append(idx)yang butuh 30 detik.
mathfux
3

Anda bisa menggunakan 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

tetapi itu tidak akan membuat Anda lebih cepat dari apa yang dilakukan Pandas, meskipun itu adalah yang tercepat setelah itu (dan mungkin numpy_indexsolusi yang berdasarkan), dan tidak datang dengan hukuman memori itu. Koleksi apa yang telah diusulkan sejauh ini ada di sini .

Di mesin OP yang seharusnya mendekati ~ 12 detik waktu eksekusi.

norok2
sumber
1
Terima kasih banyak, saya akan mengujinya nanti.
mathfux