Numpy dizisinin grup adlarını dizinlerle eşleştirmenin en hızlı yolu nedir?


9

Lidar'ın 3D pointcloud'u ile çalışıyorum. Noktalar aşağıdaki gibi numpy dizisiyle verilir:

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

Ben boyutta küpler halinde gruplandırılmış benim verilerini tutmak istiyorum 50*50*50her küp bazı hashable indeksi ve benim içinde numpy indeksleri korur, böylece pointsiçerdiği . Bölmek için cubes = points \\ 50hangi çıktıları atarım:

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

İstediğim çıktı şöyle görünüyor:

{(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]}

Gerçek noktamda birkaç yüz milyonlarca 3D noktası var. Bu tür gruplamayı yapmanın en hızlı yolu nedir?

Çeşitli çözümlerin çoğunu denedim. Nokta büyüklüğünün 20 milyon ve farklı küplerin büyüklüğünün 1 milyon olduğunu varsayarak zaman tüketiminin karşılaştırılması:

Pandalar [grup (elem) -> np.array (dtype = int64)]

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

Varsayılan [elem.tobytes () veya 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

Pandalar + boyut azalması [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.npzDosyayı buraya indirmek ve bir komut kullanmak mümkündür

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

performans süresini kontrol etmek için.


Sonucunuzdaki her listede her zaman aynı sayıda indeks var mı?
Mykola Zotko

Evet, her zaman aynıdır: 983234 yukarıda belirtilen tüm çözümler için ayrı küpler.
mathfux

1
Bu kadar basit bir Pandas çözümünün basit bir yaklaşımla yenilmesi olası değildir, çünkü onu optimize etmek için çok çaba harcanmıştır. Cython tabanlı bir yaklaşım muhtemelen ona yaklaşabilir, ancak bundan daha iyi performans göstereceğinden şüpheliyim.
norok2

1
@mathfux Son çıktıyı sözlük olarak mı almanız gerekiyor, yoksa grupların ve indekslerinin iki çıktı mı olması uygun olur?
Divakar

@ norok2 numpy_indexedsadece ona da yaklaşır. Sanırım doğru. pandasŞu anda sınıflandırma süreçlerim için kullanıyorum .
mathfux

Yanıtlar:


6

Grup başına sabit endeks sayısı

Yaklaşım # 1

Bir 1D dizisine dimensionality-reductionazaltmak cubesiçin performans gösterebiliriz . Bu, ayrıntılı olarak tartışılan lineer indeks eşdeğerlerini hesaplamak için verilen küp verilerinin bir n-dim ızgarasına eşlenmesine dayanır here. Daha sonra, bu doğrusal endekslerin benzersizliğine dayanarak, benzersiz grupları ve karşılık gelen indeksleri ayırabiliriz. Dolayısıyla, bu stratejileri takip ederek, tek bir çözümümüz olurdu, şöyle -

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: içindeki tamsayı değerleri cubesçok büyükse, dimensionality-reductiondaha kısa ölçülerdeki boyutlar birincil eksen olarak seçilecek şekilde yapmak isteyebiliriz . Dolayısıyla, bu durumlarda için biz almak azaltma adımı değiştirebilir c1Dböylece gibi -

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

Yaklaşım # 2

Sırada, Cython-powered kd-treeen yakın komşu endeksleri almak için hızlı en yakın komşu arama için kullanabiliriz ve böylece durumumuzu böyle çözebiliriz -

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]

Genel durum: Grup başına değişken dizin sayısı

Argsort tabanlı yöntemi, istediğimiz çıktıyı elde etmek için bazı bölünmelerle genişleteceğiz,

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

cubesAnahtar gruplarının 1D sürümlerini anahtar olarak kullanma

Daha önce listelenen yöntemi cubes, sözlük oluşturma sürecini basitleştirmek ve aynı zamanda onunla verimli hale getirmek için anahtar grupları olarak genişleteceğiz.

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

Şimdi, numbatekrarlamak ve son yıkanabilir sözlük çıktısına ulaşmak için paketi kullanacağız . Bununla birlikte, iki çözüm olacaktı - Biri anahtarları ve değerleri ayrı ayrı kullanan numbave ana çağrı kilitlenip dikteye dönüştürülürken, diğeri bir numba-supporteddikte tipi oluşturacak ve bu nedenle ana çağrı işlevi tarafından ekstra bir iş gerektirmeyecek .

Böylece, ilk numbaçözümümüz olurdu :

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

Ve ikinci numbaçözüm:

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.npzVeri ile zamanlamalar -

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: Biz birlikte daha da hızlanması için numexprbilgi işlem büyük diziler için c1Dşöyle, -

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)

Bu, gerekli olan her yerde uygulanabilir c1D.


Yanıtınız için çok teşekkürler! CKDTree kullanımının burada mümkün olacağını beklemiyordum. Ancak # Yaklaşımınız1 ile ilgili hala bazı sorunlar var. Çıktı uzunluğu sadece 915791'dir. Sanırım bu dtypes int32veint64
mathfux

@mathfux number of indices per group would be a constant numberYorumları topladığımı varsayıyorum . Bu güvenli bir varsayım mıdır? Ayrıca, cubes.npzçıktı için test 915791mi ediyorsunuz?
Divakar

Evet ediyorum. Grup adlarının sırası farklı olabileceğinden, grup başına dizin sayısını test etmedim. Çıktı sözlüğünün uzunluğunu cubes.npzsadece denedim ve 983234önerdiğim diğer yaklaşımlar içindi .
mathfux

1
@mathfux Approach #3 Değişken endeks sayısı için genel bir durum olup olmadığına bakın.
Divakar

1
@mathfux Genelde minimum değer 0'dan küçükse dengelemeye ihtiyaç duyulur.
Divakar

5

Her öğenin dizinini ilgili listeye yineleyebilir ve ekleyebilirsiniz.

from collections import defaultdict

res = defaultdict(list)

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

Anahtarı bir tuple dönüştürmek yerine tobytes () kullanarak çalışma zamanı daha da geliştirilebilir .


Şu anda performans süresini gözden geçirmeye çalışıyorum (20M puanları için). Görünüşe göre çözümüm zaman açısından daha verimli çünkü yineleme önlendi. Katılıyorum, bellek tüketimi çok büyük.
mathfux

başka bir teklif res[tuple(elem)].append(idx)ise res[elem[0], elem[1], elem[2]].append(idx)30 saniye süren baskıya karşı 50 saniye sürdü.
mathfux

3

Cython'u kullanabilirsiniz:

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

ancak bundan sonra en hızlı (ve belki de numpy_indextemel çözüm) olmasına rağmen, Panda'nın yaptığıdan daha hızlı olmayacak ve bunun hafıza cezası ile birlikte gelmeyecek. Şimdiye kadar teklif edilenlerin bir koleksiyonu burada .

OP'nin makinesinde ~ 12 sn. Yürütme süresine yaklaşmalıdır.


1
Çok teşekkürler, daha sonra test edeceğim.
mathfux
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.