Векторизация цикла for с повторяющимися индексами в python

Я пытаюсь оптимизировать фрагмент, который вызывается много (миллионы раз), поэтому любое улучшение скорости (надеюсь, удаление цикла for) было бы здорово.

Я вычисляю корреляционную функцию некоторой j-й частицы со всеми остальными

C_j(|r-r'|) = sqrt(E((s_j(r')-s_k(r))^2)) усредненный по k.

Моя идея состоит в том, чтобы иметь переменную corrfun, которая собирает данные в некоторые ячейки (г, определенный в другом месте). Я нахожу, к какой ячейке r принадлежит каждый s_k, и это сохраняется в ind. Таким образом, ind[0] — это индекс r (и, следовательно, corrfun), которому соответствует точка j=0. Несколько точек могут попасть в один и тот же бин (на самом деле я хочу, чтобы бины были достаточно большими, чтобы содержать несколько точек), поэтому я суммирую все (s_j(r')-s_k(r))^2, а затем делю на количество точки в этом бине (хранятся в переменной rw). Код, который я в итоге сделал для этого, следующий (np для numpy):

for k, v in enumerate(ind):
        if j==k:
            continue
        corrfun[v] += (s[k]-s[j])**2
        rw[v] += 1
rw2 = rw
rw2[rw < 1] = 1
corrfun = np.sqrt(np.divide(corrfun, rw2))

Обратите внимание, что дело с rw2 было связано с тем, что я хочу избежать проблем с делением на 0, но я возвращаю массив rw и хочу иметь возможность различать элементы rw=0 и rw=1. Возможно, для этого тоже есть более элегантное решение.

Есть ли способ ускорить цикл for? Хотя я не хотел бы добавлять взаимодействие с самим собой (j==k), я даже согласен с взаимодействием с самим собой, если это означает, что я могу получить значительно более быстрый расчет (длина ind ~ 1E6, поэтому взаимодействие с самим собой, вероятно, в любом случае незначительно).

Благодарю вас!

Илья

Редактировать:

Вот полный код. Обратите внимание, что в полном коде я также усредняю ​​по j.

import numpy as np

def twopointcorr(x,y,s,dr):

    width = np.max(x)-np.min(x)
    height = np.max(y)-np.min(y)

    n = len(x)

    maxR = np.sqrt((width/2)**2 + (height/2)**2)

    r = np.arange(0, maxR, dr)
    print(r)
    corrfun = r*0
    rw = r*0
    print(maxR)
    ''' go through all points'''
    for j in range(0, n-1):
        hypot = np.sqrt((x[j]-x)**2+(y[j]-y)**2)
        ind = [np.abs(r-h).argmin() for h in hypot]

        for k, v in enumerate(ind):
            if j==k:
                continue
            corrfun[v] += (s[k]-s[j])**2
            rw[v] += 1

    rw2 = rw
    rw2[rw < 1] = 1
    corrfun = np.sqrt(np.divide(corrfun, rw2))
    return r, corrfun, rw

Я отлаживаю тест следующим образом

from twopointcorr import twopointcorr
import numpy as np
import matplotlib.pyplot as plt
import time

n=1000
x = np.random.rand(n)
y = np.random.rand(n)
s = np.random.rand(n)

print('running two point corr functinon')

start_time = time.time()
r,corrfun,rw = twopointcorr(x,y,s,0.1)
print("--- Execution time is %s seconds ---" % (time.time() - start_time))

fig1=plt.figure()
plt.plot(r, corrfun,'-x')

fig2=plt.figure()
plt.plot(r, rw,'-x')
plt.show()

Опять же, основная проблема заключается в том, что в реальном наборе данных n~1E6. Конечно, я могу выполнить повторную выборку, чтобы сделать ее меньше, но мне бы хотелось на самом деле прокрутить набор данных.


person Ilya    schedule 26.04.2016    source источник
comment
Можете ли вы предоставить полностью работающую программу, включая этапы установки?   -  person John Zwinck    schedule 26.04.2016
comment
Конечно, сейчас я отредактирую весь пост. Хорошо, код готов   -  person Ilya    schedule 26.04.2016
comment
Спасибо. Кстати, почему range(0, n-1) вместо range(0, n)?   -  person John Zwinck    schedule 26.04.2016
comment
Это может быть полезно для add.at - небуферизованной версии обычного дополнения. docs.scipy.org/doc/ numpy-1.10.1/ссылка/сгенерированный/   -  person hpaulj    schedule 26.04.2016


Ответы (3)


Вот код, который использует трансляцию, гипот, раунд, бинкаунт для удаления всех циклов:

def twopointcorr2(x, y, s, dr):
    width = np.max(x)-np.min(x)
    height = np.max(y)-np.min(y)
    n = len(x)
    maxR = np.sqrt((width/2)**2 + (height/2)**2)
    r = np.arange(0, maxR, dr)    
    osub = lambda x:np.subtract.outer(x, x)

    ind = np.clip(np.round(np.hypot(osub(x), osub(y)) / dr), 0, len(r)-1).astype(int)
    rw = np.bincount(ind.ravel())
    rw[0] -= len(x)
    corrfun = np.bincount(ind.ravel(), (osub(s)**2).ravel())
    return r, corrfun, rw

для сравнения я изменил ваш код следующим образом:

def twopointcorr(x,y,s,dr):
    width = np.max(x)-np.min(x)
    height = np.max(y)-np.min(y)

    n = len(x)

    maxR = np.sqrt((width/2)**2 + (height/2)**2)

    r = np.arange(0, maxR, dr)
    corrfun = r*0
    rw = r*0
    for j in range(0, n):
        hypot = np.sqrt((x[j]-x)**2+(y[j]-y)**2)
        ind = [np.abs(r-h).argmin() for h in hypot]
        for k, v in enumerate(ind):
            if j==k:
                continue
            corrfun[v] += (s[k]-s[j])**2
            rw[v] += 1

    return r, corrfun, rw        

и вот код для проверки результатов:

import numpy as np

n=1000
x = np.random.rand(n)
y = np.random.rand(n)
s = np.random.rand(n)

r1, corrfun1, rw1 = twopointcorr(x,y,s,0.1)
r2, corrfun2, rw2 = twopointcorr2(x,y,s,0.1)

assert np.allclose(r1, r2)
assert np.allclose(corrfun1, corrfun2)
assert np.allclose(rw1, rw2)        

и результаты %timeit:

%timeit twopointcorr(x,y,s,0.1)
%timeit twopointcorr2(x,y,s,0.1)    

выходы:

1 loop, best of 3: 5.16 s per loop
10 loops, best of 3: 134 ms per loop
person HYRY    schedule 26.04.2016
comment
Большое спасибо, это работает отлично и является самым быстрым - person Ilya; 26.04.2016
comment
Хороший - особенно использование np.hypot() и ravel() - это улучшения по сравнению с моим ответом. - person John Zwinck; 27.04.2016

Ваш исходный код в моей системе выполняется примерно за 5,7 секунды. Я полностью векторизовал внутренний цикл и заставил его работать за 0,39 секунды. Просто замените свой цикл «пройти через все точки» на это:

    points = np.column_stack((x,y))
    hypots = scipy.spatial.distance.cdist(points, points)
    inds = np.rint(hypots.clip(max=maxR) / dr).astype(np.int)

    # go through all points            
    for j in range(n): # n.b. previously n-1, not sure why
        ind = inds[j]

        np.add.at(corrfun, ind, (s - s[j])**2)

        np.add.at(rw, ind, 1)
        rw[ind[j]] -= 1 # subtract self                                                                                 

Первое наблюдение заключалось в том, что ваш код hypot вычислял двумерные расстояния, поэтому я заменил его кодом cdist из SciPy, чтобы сделать все это за один вызов. Во-вторых, внутренний цикл for был медленным, и благодаря проницательному комментарию @hpaulj я также векторизовал это, используя np.add.at().


Поскольку вы спросили, как векторизовать внутренний цикл, я сделал это позже. Теперь для запуска требуется 0,25 секунды, что дает общее ускорение более чем в 20 раз. Вот окончательный код:

    points = np.column_stack((x,y))
    hypots = scipy.spatial.distance.cdist(points, points)
    inds = np.rint(hypots.clip(max=maxR) / dr).astype(np.int)

    sn = np.tile(s, (n,1)) # n copies of s                                                                              
    diffs = (sn - sn.T)**2 # squares of pairwise differences
    np.add.at(corrfun, inds, diffs)

    rw = np.bincount(inds.flatten(), minlength=len(r))
    np.subtract.at(rw, inds.diagonal(), 1) # subtract self

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

person John Zwinck    schedule 26.04.2016
comment
Спасибо, я попробую это. Кроме того, если это не слишком сложно, можете ли вы показать, как устранить внешний цикл? Проблема n ^ 2, поэтому, по-видимому, реальная проблема требует на много порядков больше времени, чем тест. - person Ilya; 26.04.2016
comment
@Ilya: я полностью векторизовал его сейчас - см. обновленный ответ. Это было нелегко. Пожалуйста, дайте мне знать, какое ускорение это дает вам на реальных данных в полном размере - мне очень любопытно узнать! - person John Zwinck; 26.04.2016
comment
John Zwinck: Спасибо за помощь, тест занимает около 240 мс с вашей версией и около 100 мс с версией @HYRY! Я все еще не совсем готов обработать все данные (и мне, вероятно, все еще нужно будет выполнить подвыборку, поскольку для набора данных 1E6 и гораздо более точного доктора эта проблема подразумевает несколько дней вычислений......), но я опубликую кое-что. когда я делаю! Еще раз большое спасибо за вашу помощь! - person Ilya; 26.04.2016

Итак, как оказалось, внешние продукты невероятно затратны в памяти, однако, используя ответы от @HYRY и @JohnZwinck, я смог создать код, который по-прежнему примерно линеен по n в памяти и быстро вычисляется (0,5 секунды для тестового случая)

import numpy as np

def twopointcorr(x,y,s,dr,maxR=-1):

    width = np.max(x)-np.min(x)
    height = np.max(y)-np.min(y)

    n = len(x)

    if maxR < dr:
        maxR = np.sqrt((width/2)**2 + (height/2)**2)

    r = np.arange(0, maxR+dr, dr)

    corrfun = r*0
    rw = r*0


    for j in range(0, n):

        ind = np.clip(np.round(np.hypot(x[j]-x,y[j]-y) / dr), 0, len(r)-1).astype(int)
        np.add.at(corrfun, ind, (s - s[j])**2)
        np.add.at(rw, ind, 1)

    rw[0] -= n

    corrfun = np.sqrt(np.divide(corrfun, np.maximum(rw,1)))
    r=np.delete(r,-1)
    rw=np.delete(rw,-1)
    corrfun=np.delete(corrfun,-1)
    return r, corrfun, rw
person Ilya    schedule 27.04.2016