Оптимизация поиска ближайших четырех элементов в двух трехмерных массивах

У меня есть два пустых массива, заполненных трехмерными координатами (x, y, z). Для каждой точки первого массива («целевой» массив) мне нужно найти 4 ближайшие точки второго массива («исходный» массив). У меня нет проблем с поиском фактических результатов с использованием различных методов, но я хочу максимально ускорить процесс.

Мне это нужно, потому что я работаю над инструментом Maya, который передает информацию, хранящуюся в каждой вершине сетки, во вторую сетку, и у них может быть разное количество вершин.

На данный момент, однако, это становится больше проблемой Python, чем проблемой Maya, поскольку моим основным узким местом является время, затрачиваемое на поиск совпадений вершин.

Количество элементов может варьироваться от нескольких сотен до сотен тысяч, и я хочу быть уверен, что найду лучший способ ускорить поиск. Я хотел бы, чтобы мой инструмент работал как можно быстрее, так как он может использоваться очень часто, и минуты ожидания каждый раз, когда он должен запускаться, были бы довольно раздражающими.

Я нашел несколько полезных ответов, которые привели меня в правильном направлении:

Здесь я узнал о KDTrees и различных алгоритмах и здесь Я нашел несколько полезных соображений по многопоточности.

Вот некоторый код, имитирующий сценарий, с которым я буду работать, и несколько решений, которые я пробовал.

import timeit
import numpy as np
from multiprocessing.pool import ThreadPool
from scipy import spatial

# brut Froce
def bruteForce():
    results = []
    for point in sources:
        dists = ((targets - [point]) ** 2).sum(axis=1)  # compute distances
        ndx = dists.argsort()  # indirect sort
        results.append(zip(ndx[:4], dists[ndx[:4]]))
    return results

# Thread Pool Implementation
def threaded():
    def worker(point):
        dists = ((targets - [point]) ** 2).sum(axis=1)  # compute distances
        ndx = dists.argsort()  # indirect sort
        return zip(ndx[:4], dists[ndx[:4]])


    pool = ThreadPool()
    return pool.map(worker, sources)

# KDTree implementation
def kdTree():
    tree = spatial.KDTree(targets, leafsize=50)
    return [tree.query(point, k=4) for point in sources]

# define the number of points for the two arrays
n_targets = 40000  
n_sources = 40000  

#pick some random points
targets = np.random.rand(n_targets, 3) * 100
sources = np.random.rand(n_sources, 3) * 100



print 'KDTree:   %s' % timeit.Timer(lambda: kdTree()).repeat(1, 1)[0]
print 'bruteforce:   %s' % timeit.Timer(lambda: bruteForce()).repeat(1, 1)[0]
print 'threaded:   %s' % timeit.Timer(lambda: threaded()).repeat(1, 1)[0]

Мои результаты:

KDTree:       10.724864464  seconds
bruteforce:   211.427750433 seconds
threaded:     47.3280865123 seconds

Наиболее перспективным методом кажется KDTree. Сначала я думал, что, используя некоторые потоки для разделения работы KDTree на отдельные задачи, я мог бы еще больше ускорить процесс. Однако после быстрого тестирования с использованием базовой реализации threading.Thread оказалось, что она работает еще хуже, когда KDTree вычисляется в потоке. Читая этот пример scipy, я вижу, что KDTrees не очень подходят для использования в параллельных потоках, но я не очень понял, как.

Тогда мне было интересно, есть ли какой-либо другой способ оптимизировать этот код, чтобы он работал быстрее, возможно, используя многопроцессорность или какой-либо другой трюк для параллельного анализа моих массивов.

Заранее спасибо за помощь!


person TheArcadeFire    schedule 23.05.2019    source источник
comment
Как правило, Python плохо справляется с многопоточностью, потому что многие обращения к структурам данных синхронизируются глобальной блокировкой интерпретатора. multiprocessing может помочь здесь, но это должно быть сделано осторожно, чтобы копирование структур данных работало и избегалось ненужное копирование (особенно в Windows могут быть проблемы из-за отсутствия функции fork ОС).   -  person Michael Butscher    schedule 24.05.2019
comment
Не могли бы вы привести пример многопроцессорной обработки с помощью KDTree?   -  person TheArcadeFire    schedule 24.05.2019


Ответы (2)


Есть одна очень простая, но чрезвычайно эффективная вещь, которую вы можете сделать, это переключиться с KDTree на cKDTree. Последний является заменой Cython первого, который реализован на чистом Python.

Также обратите внимание, что .query векторизовано, нет необходимости в понимании списка.

import scipy.spatial as ss

a = np.random.random((40000,3))
b = np.random.random((40000,3))

tree_py = ss.KDTree(a)
tree_cy = ss.cKDTree(a)

timeit(lambda: tree_cy.query(b, k=4), number=10)*100
# 71.06744810007513
timeit(lambda: tree_py.query(b, k=4), number=1)*1000
# 13309.359921026044

Так что это почти 200x ускорение бесплатно.

person Paul Panzer    schedule 23.05.2019
comment
Причина понимания списка заключается в том, что мне в любом случае нужно что-то делать, пока я просматриваю свои точки. Таким образом, в окончательной реализации я все равно буду перебирать каждую точку, находить ближайшие 4, а затем делать некоторые другие вещи, которые я здесь пропустил, чтобы сопоставить значения этих 4 точек с пунктом назначения. Поэтому я подумал, что было бы лучше использовать реальный цикл в тесте. - person TheArcadeFire; 24.05.2019
comment
Я обязательно попробую cKDTree. Однако из ваших результатов я уже вижу, что вы получаете гораздо более медленные вычисления, чем я, возможно, потому, что вы не указываете размер листа. Я заметил, что изменение размера листа может иметь огромное значение в производительности, но документация на самом деле не объясняет, что он делает и каковы предлагаемые значения. Есть ли у вас какое-либо представление о том, как следует установить размер листа для достижения наилучшей скорости? - person TheArcadeFire; 24.05.2019
comment
К сожалению, нет, но я только что немного поиграл с ним, и компромиссы не кажутся такими же для KDTree (где это помогает, но немного (‹2x) ваше значение, 50, кажется не слишком далеким от оптимального для размера задачи) и для cKDTree (где я пытался пять минут, я не мог найти значение, которое превосходит значение по умолчанию). Кажется, это указывает на то, что накладные расходы по делению пополам более значительны в чистом Python, чем в Cython, что, я бы сказал, имеет смысл. - person Paul Panzer; 24.05.2019

Для достаточно большого количества исходных точек многопроцессорная обработка может дать выигрыш в скорости. Важным моментом является то, что каждый подпроцесс должен содержать копию файла KDTree. В Linux (поддерживающем fork) это делается автоматически при создании подпроцессов после построения дерева.

Для Windows дерево должно быть либо отправлено pickled подпроцессам, как это делается автоматически при отправке параметров в подпроцесс (что работает только для cKDTree, но не для KDTree), либо дерево должно быть создано с нуля в каждом процессе.

В следующем коде показан вариант травления с несколькими процессами cKDTree по сравнению с одним процессом.

import timeit
import numpy as np
from multiprocessing.pool import Pool
from scipy import spatial


# cKDTree implementation
def ckdTree():
    tree = spatial.cKDTree(targets, leafsize=50)
    return [tree.query(point, k=4) for point in sources]


# Initialization to transfer kdtree
def setKdTree(tree):
    global kdtree

    kdtree = tree

# Worker must not be in another function for multiprocessing
def multiprocKd_worker(point):
    return kdtree.query(point, k=4)


# cKDTree process pool implementation
def multiprocCKd():
    tree = spatial.cKDTree(targets, leafsize=50)

    pool = Pool(initializer=setKdTree, initargs=(tree,))
    return pool.map(multiprocKd_worker, sources)


if __name__ == "__main__":
    # define the number of points for the two arrays
    n_targets = 40000
    n_sources = 40000

    #pick some random points
    targets = np.random.rand(n_targets, 3) * 100
    sources = np.random.rand(n_sources, 3) * 100


    print('cKDTree:   %s' % timeit.Timer(lambda: ckdTree()).repeat(1, 1)[0])
    print('multiprocCKd:   %s' % timeit.Timer(lambda: multiprocCKd()).repeat(1, 1)[0])
person Michael Butscher    schedule 25.05.2019