Векторизованное пространственное расстояние в питоне с использованием NumPy

1

У меня есть массив numpy в python, который содержит лоты (10k+) трехмерных вершинных точек (векторы с координатами [x, y, z]). Мне нужно рассчитать расстояние между всеми возможными парами этих точек.

это легко сделать, используя scipy:

import scipy
D = spdist.cdist(verts, verts)

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

Поэтому я придумал этот наивный код:

def vert_dist(self, A, B):
    return ((B[0]-A[0])**2+(B[1]-A[1])**2+(B[2]-A[2])**2)**(1.0/2)

# Pairwise distance between verts
#Use SciPy, otherwise use fallback
try:
    import scipy.spatial.distance as spdist
    D = spdist.cdist(verts, verts)
except ImportError:
    #FIXME: This is VERY SLOW:
    D = np.empty((len(verts), len(verts)), dtype=np.float64)
    for i,v in enumerate(verts):
        #self.app.setStatus(_("Calculating distance %d of %d (SciPy not installed => using SLOW AF fallback method)"%(i,len(verts))), True)
        for j in range(i,len(verts)):
            D[j][i] = D[i][j] = self.vert_dist(v,verts[j])

vert_dist() вычисляет трехмерное расстояние между двумя вершинами, а остальная часть кода выполняет итерацию по вершинам в массиве 1D, и для каждого из них он вычисляет расстояние до каждого другого в том же массиве и создает 2D-массив расстояний.

Но это очень медленно (1000 раз) по сравнению с scipy родным кодом C. Интересно, могу ли я ускорить его с помощью чистого numpy. по крайней мере, до некоторой степени.

Дополнительная информация: https://github.com/scipy/scipy/issues/9172

BTW я пробовал PyPy JIT-компилятор, и он был еще медленнее (в 10 раз), чем чистый питон.

ОБНОВЛЕНИЕ: мне удалось немного ускорить ситуацию:

    def vert_dist_matrix(self, verts):
            #FIXME: This is VERY SLOW:
            D = np.empty((len(verts), len(verts)), dtype=np.float64)
            for i,v in enumerate(verts):
                    D[i] = D[:,i] = np.sqrt(np.sum(np.square(verts-verts[i]), axis=1))
            return D

Это устраняет внутренний цикл, вычисляя сразу всю строку, что делает материал довольно быстрым, но все же заметно медленнее, чем scipy. Поэтому я все еще смотрю на решение @Divakar

  • 0
    Вы ищете что-то подобное? stackoverflow.com/a/51352819/4909087
  • 0
    этот ответ также довольно интересен, так как обсуждается производительность: stackoverflow.com/a/37903795/8069403
Показать ещё 2 комментария
Теги:
numpy
multidimensional-array
scipy
vertex

2 ответа

3
Лучший ответ

Там пакет eucl_dist (отказ от ответственности: я его автор), который в основном содержит два метода решения проблемы вычисления квадратов эвклидовых расстояний, которые более эффективны, чем SciPy cdist, особенно для больших массивов (с приличным и большим количеством столбцов).

Мы будем использовать некоторые коды из source code чтобы адаптироваться к нашей проблеме здесь, чтобы дать нам два подхода.

Подход №1

Следуя wiki contents, мы могли бы использовать matrix-multiplication и некоторые NumPy specific implementations для нашего первого подхода, например:

def pdist_squareformed_numpy(a):
    a_sumrows = np.einsum('ij,ij->i',a,a)
    dist = a_sumrows[:,None] + a_sumrows -2*np.dot(a,a.T)
    np.fill_diagonal(dist,0)
    return dist

Подход №2

Еще одним методом было бы создание "расширенных" версий входных массивов, которые снова подробно обсуждались в этой ссылке на исходный код github, чтобы иметь наш второй подход, который лучше подходит для меньших столбцов, как здесь, например,

def ext_arrs(A,B, precision="float64"):
    nA,dim = A.shape
    A_ext = np.ones((nA,dim*3),dtype=precision)
    A_ext[:,dim:2*dim] = A
    A_ext[:,2*dim:] = A**2

    nB = B.shape[0]
    B_ext = np.ones((dim*3,nB),dtype=precision)
    B_ext[:dim] = (B**2).T
    B_ext[dim:2*dim] = -2.0*B.T
    return A_ext, B_ext

def pdist_squareformed_numpy_v2(a):
    A_ext, B_ext = ext_arrs(a,a)
    dist = A_ext.dot(B_ext)
    np.fill_diagonal(dist,0)
    return dist

Обратите внимание, что это дает нам квадрат расстояния eucludean. Итак, для фактических расстояний мы хотим использовать np.sqrt() если это необходимо для окончательного вывода.

Образцы прогона -

In [380]: np.random.seed(0)
     ...: a = np.random.rand(5,3)

In [381]: from scipy.spatial.distance import cdist

In [382]: cdist(a,a)
Out[382]: 
array([[0.  , 0.29, 0.42, 0.2 , 0.57],
       [0.29, 0.  , 0.58, 0.42, 0.76],
       [0.42, 0.58, 0.  , 0.45, 0.9 ],
       [0.2 , 0.42, 0.45, 0.  , 0.51],
       [0.57, 0.76, 0.9 , 0.51, 0.  ]])

In [383]: np.sqrt(pdist_squareformed_numpy(a))
Out[383]: 
array([[0.  , 0.29, 0.42, 0.2 , 0.57],
       [0.29, 0.  , 0.58, 0.42, 0.76],
       [0.42, 0.58, 0.  , 0.45, 0.9 ],
       [0.2 , 0.42, 0.45, 0.  , 0.51],
       [0.57, 0.76, 0.9 , 0.51, 0.  ]])

In [384]: np.sqrt(pdist_squareformed_numpy_v2(a))
Out[384]: 
array([[0.  , 0.29, 0.42, 0.2 , 0.57],
       [0.29, 0.  , 0.58, 0.42, 0.76],
       [0.42, 0.58, 0.  , 0.45, 0.9 ],
       [0.2 , 0.42, 0.45, 0.  , 0.51],
       [0.57, 0.76, 0.9 , 0.51, 0.  ]])

Сроки на 10k -

In [385]: a = np.random.rand(10000,3)

In [386]: %timeit cdist(a,a)
1 loop, best of 3: 309 ms per loop

# Approach #1
In [388]: %timeit pdist_squareformed_numpy(a) # squared eucl distances
1 loop, best of 3: 668 ms per loop

In [389]: %timeit np.sqrt(pdist_squareformed_numpy(a)) # actual eucl distances
1 loop, best of 3: 812 ms per loop

# Approach #2
In [390]: %timeit pdist_squareformed_numpy_v2(a) # squared eucl distances
1 loop, best of 3: 237 ms per loop

In [391]: %timeit np.sqrt(pdist_squareformed_numpy_v2(a)) # actual eucl distances
1 loop, best of 3: 395 ms per loop

Второй подход кажется близким к cdist по производительности!

  • 0
    Спасибо! Хорошее редактирование. Я удалил свои предыдущие комментарии и вернусь, чтобы удалить этот позже.
  • 0
    @Divakar Это действительно круто и невероятно быстро. Спасибо! но в pdist_squareformed_numpy_v2() мне пришлось заменить return dist на return np.abs(np.nan_to_num(dist)) . Теперь он работает лучше, но в некоторых случаях дает неточные результаты. Этот код составлен таким образом, что вершины, близкие к 1е-5, можно назвать идентичными. Но мне нужно снизить точность до 1e-1, чтобы получить те же результаты, что и со scipy. Вероятно, есть некоторая потеря остроты.
Показать ещё 11 комментариев
0

Вы можете использовать numpy.linalg.norm:

from numpy.linalg import norm

a = np.random.rand(10000, 3)
b = np.random.rand(10000, 3)

c = norm(a-b, axis=1)  # will return a np.array of distances

Я не оценил это, но n=10K случай работал мгновенно для меня.

  • 0
    Это не то, что я имел в виду. Но спасибо. Это уже решено.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню