Cách nhanh nhất để ánh xạ tên nhóm của mảng numpy vào các chỉ số là gì?


9

Tôi đang làm việc với 3D pointcloud của Lidar. Các điểm được cho bởi mảng numpy trông như thế này:

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

Tôi muốn giữ dữ liệu của mình được nhóm thành các khối có kích thước 50*50*50sao cho mỗi khối đều giữ một số chỉ số có thể băm và các chỉ số khó hiểu của pointsnó chứa . Để có được sự phân tách, tôi chỉ định cubes = points \\ 50đầu ra nào cho:

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

Đầu ra mong muốn của tôi trông như thế này:

{(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 thực sự của tôi chứa tới vài trăm triệu điểm 3D. Cách nhanh nhất để làm loại nhóm này là gì?

Tôi đã thử phần lớn các giải pháp khác nhau. Dưới đây là so sánh thời gian tính toán giả định kích thước của các điểm là khoảng 20 triệu và kích thước của các hình khối riêng biệt là khoảng 1 triệu:

Gấu trúc [tuple (elem) -> np.array (dtype = int64)]

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

Defauldict [elem.tobytes () hoặc 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

Gấu trúc + giảm kích thước [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

Có thể tải cubes.npztập tin ở đây và sử dụng lệnh

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

để kiểm tra thời gian thực hiện.


Bạn có luôn có cùng số chỉ số trong mỗi danh sách trong kết quả của mình không?
Mykola Zotko

Vâng, nó luôn luôn giống nhau: 983234 khối riêng biệt cho tất cả các giải pháp nêu trên.
mathfux

1
Không chắc rằng một giải pháp Pandas đơn giản như vậy sẽ bị đánh bại bởi một cách tiếp cận đơn giản, vì rất nhiều nỗ lực đã được dành để tối ưu hóa nó. Một cách tiếp cận dựa trên Cython có thể có thể tiếp cận nó, nhưng tôi nghi ngờ nó sẽ tốt hơn nó.
norok2

1
@mathfux Bạn có phải có đầu ra cuối cùng là một từ điển hay không nếu có các nhóm và chỉ số của chúng là hai đầu ra thì có ổn không?
Divakar

@ norok2 numpy_indexedchỉ tiếp cận nó quá. Tôi đoán nó đúng. Tôi sử dụng pandascho các quá trình phân loại của tôi hiện nay.
mathfux

Câu trả lời:


6

Số chỉ số không đổi trong mỗi nhóm

Cách tiếp cận số 1

Chúng ta có thể thực hiện dimensionality-reductionđể giảm cubesxuống một mảng 1D. Điều này dựa trên ánh xạ của dữ liệu hình khối đã cho vào lưới n-dim để tính toán các tương đương chỉ số tuyến tính, được thảo luận chi tiết here. Sau đó, dựa trên tính duy nhất của các chỉ số tuyến tính đó, chúng ta có thể tách riêng các nhóm duy nhất và các chỉ số tương ứng của chúng. Do đó, theo các chiến lược đó, chúng tôi sẽ có một giải pháp, như vậy -

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

Giải pháp thay thế số 1: Nếu các giá trị nguyên trong cubesquá lớn, chúng tôi có thể muốn thực hiện dimensionality-reductionsao cho các kích thước có phạm vi ngắn hơn được chọn làm trục chính. Do đó, đối với những trường hợp đó, chúng ta có thể sửa đổi bước giảm để có được c1D, như vậy -

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

Cách tiếp cận số 2

Tiếp theo, chúng ta có thể sử dụng Cython-powered kd-treeđể tra cứu hàng xóm gần nhất nhanh chóng để có được các chỉ số lân cận gần nhất và từ đó giải quyết trường hợp của chúng ta như vậy -

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]

Trường hợp chung: Số lượng chỉ số khác nhau cho mỗi nhóm

Chúng tôi sẽ mở rộng phương thức dựa trên argsort với một số phân tách để có được đầu ra mong muốn của chúng tôi, như vậy -

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

Sử dụng các phiên bản 1D của các nhóm cubeslàm khóa

Chúng tôi sẽ mở rộng phương thức được liệt kê trước đó với các nhóm cubeslàm khóa để đơn giản hóa quá trình tạo từ điển và cũng làm cho nó hiệu quả với nó, như vậy -

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

Tiếp theo, chúng tôi sẽ sử dụng numbagói để lặp lại và đi đến đầu ra từ điển có thể băm cuối cùng. Đi cùng với nó, sẽ có hai giải pháp - Một giải pháp sử dụng các khóa và giá trị riêng biệt bằng cách sử dụng numbavà cuộc gọi chính sẽ nén và chuyển đổi thành dict, trong khi giải pháp còn lại sẽ tạo ra một numba-supportedkiểu chính tả và do đó không cần thêm công việc của chức năng gọi chính .

Vì vậy, chúng tôi sẽ có numbagiải pháp đầu tiên :

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

numbagiải pháp thứ hai là:

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

Thời gian với cubes.npzdữ liệu -

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)

Giải pháp thay thế số 1: Chúng ta có thể tăng tốc hơn nữa với numexprcác mảng lớn để tính toán c1D, như vậy -

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)

Điều này sẽ được áp dụng tại tất cả những nơi yêu cầu c1D.


Cảm ơn rất nhiều vì đã phản hồi! Tôi không mong đợi việc sử dụng cKDTree là có thể ở đây. Tuy nhiên, vẫn còn một số vấn đề với # Cách tiếp cận1 của bạn. Độ dài của đầu ra chỉ là 915791. Tôi đoán đây là một số loại xung đột giữa dtypes int32int64
mathfux

@mathfux Tôi giả sử number of indices per group would be a constant numberrằng tôi đã thu thập ý kiến. Đó sẽ là một giả định an toàn? Ngoài ra, bạn đang thử nghiệm cubes.npzcho đầu ra của 915791?
Divakar

Vâng tôi đồng ý. Tôi đã không kiểm tra số lượng chỉ số cho mỗi nhóm vì thứ tự tên nhóm có thể khác nhau. Tôi chỉ kiểm tra độ dài của từ điển đầu ra cubes.npzvà nó 983234dành cho các phương pháp khác mà tôi đề xuất.
mathfux

1
@mathfux Hãy xem Approach #3 trường hợp chung đó có số lượng chỉ số thay đổi.
Divakar

1
@mathfux Yup rằng việc bù đắp thường là cần thiết nếu mức tối thiểu nhỏ hơn 0. Bắt chính xác!
Divakar

5

Bạn chỉ có thể lặp lại và thêm chỉ mục của từng thành phần vào danh sách tương ứng.

from collections import defaultdict

res = defaultdict(list)

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

Thời gian chạy có thể được cải thiện hơn nữa bằng cách sử dụng tobytes () thay vì chuyển đổi khóa thành một tuple.


Hiện tại tôi đang cố gắng đánh giá thời gian thực hiện (cho 20 triệu điểm). Có vẻ như giải pháp của tôi hiệu quả hơn về mặt thời gian vì tránh lặp lại. Tôi đồng ý, tiêu thụ bộ nhớ là rất lớn.
mathfux

một đề xuất khác res[tuple(elem)].append(idx)mất 50 giây so với phiên bản của nó res[elem[0], elem[1], elem[2]].append(idx)mất 30 giây.
mathfux

3

Bạn có thể sử dụng 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

nhưng nó sẽ không làm cho bạn nhanh hơn những gì Pandas làm, mặc dù nó là nhanh nhất sau đó (và có lẽ là numpy_indexgiải pháp dựa trên), và không đi kèm với hình phạt bộ nhớ của nó. Một bộ sưu tập những gì đã được đề xuất cho đến nay là ở đây .

Trong máy của OP sẽ có thời gian thực hiện gần ~ 12 giây.


1
Cảm ơn rất nhiều, tôi sẽ kiểm tra nó sau.
mathfux
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.