Несколько лет назад кто-то опубликовал в Active State Recipes для целей сравнения, три функции Python / NumPy; каждый из них принимал одни и те же аргументы и возвращал один и тот же результат - матрицу расстояний .
Два из них были взяты из опубликованных источников; они оба - или они кажутся мне - идиоматическим кодом. Повторяющиеся вычисления, необходимые для создания матрицы расстояний, основаны на элегантном синтаксисе индекса numpy. Вот один из них:
from numpy.matlib import repmat, repeat
def calcDistanceMatrixFastEuclidean(points):
numPoints = len(points)
distMat = sqrt(sum((repmat(points, numPoints, 1) -
repeat(points, numPoints, axis=0))**2, axis=1))
return distMat.reshape((numPoints,numPoints))
Третий создал матрицу расстояний, используя один цикл (который, очевидно, является большим количеством циклов, учитывая, что матрица расстояний всего 1000 2D точек имеет миллион записей). На первый взгляд эта функция выглядела как код, который я использовал для написания, когда изучал NumPy, и я писал бы код NumPy, сначала написав код Python, а затем переводя его построчно.
Через несколько месяцев после публикации Active State результаты тестов производительности, сравнивающие три, были опубликованы и обсуждены в потоке в списке рассылки NumPy.
Функция с циклом фактически значительно превзошла два других:
from numpy import mat, zeros, newaxis
def calcDistanceMatrixFastEuclidean2(nDimPoints):
nDimPoints = array(nDimPoints)
n,m = nDimPoints.shape
delta = zeros((n,n),'d')
for d in xrange(m):
data = nDimPoints[:,d]
delta += (data - data[:,newaxis])**2
return sqrt(delta)
Один из участников потока (Кейр Мирле) предложил причину, по которой это могло бы быть правдой:
Причина, по которой я подозреваю, что это будет быстрее, очевидно, много циклов, учитывая, что матрица расстояний, состоящая всего из 1000 2D точек, имеет миллион записей). На первый взгляд эта функция выглядела как код, который я использовал для написания, когда изучал NumPy, и я писал бы код NumPy, сначала написав код Python, а затем переводя его построчно.
Через несколько месяцев после публикации Active State результаты тестов производительности, сравнивающие три, были опубликованы и обсуждены в потоке в списке рассылки NumPy.
Функция с циклом фактически значительно превзошла два других:
from numpy import mat, zeros, newaxis def calcDistanceMatrixFastEuclidean2(nDimPoints): nDimPoints = array(nDimPoints) n,m = nDimPoints.shape delta = zeros((n,n),'d') for d in xrange(m): data = nDimPoints[:,d] delta += (data - data[:,newaxis])**2 return sqrt(delta)
Один из участников потока (Кейр Мирле) предложил причину, по которой это могло бы быть правдой:
Причина, по которой я подозреваю, что это будет быстрее, очевидно, много циклов, учитывая, что матрица расстояний, состоящая всего из 1000 2D точек, имеет миллион записей). На первый взгляд эта функция выглядела как код, который я использовал для написания, когда изучал NumPy, и я писал бы код NumPy, сначала написав код Python, а затем переводя его построчно.
Через несколько месяцев после публикации Active State результаты тестов производительности, сравнивающие три, были опубликованы и обсуждены в потоке в списке рассылки NumPy.
Функция с циклом фактически значительно превзошла два других:
from numpy import mat, zeros, newaxis def calcDistanceMatrixFastEuclidean2(nDimPoints): nDimPoints = array(nDimPoints) n,m = nDimPoints.shape delta = zeros((n,n),'d') for d in xrange(m): data = nDimPoints[:,d] delta += (data - data[:,newaxis])**2 return sqrt(delta)
Один из участников потока (Кейр Мирле) предложил причину, по которой это могло бы быть правдой:
Причина, по которой я подозреваю, что это будет быстрее, что он имеет лучшую локальность, полностью завершив вычисление на относительно небольшой рабочий набор, прежде чем перейти к следующему. Один вкладыши приходится многократно втягивать потенциально большой массив MxN в процессор.
По его собственным словам, его замечание является лишь подозрением, и, похоже, что оно не обсуждалось дальше.
Любые другие мысли о том, как учитывать эти результаты?
В частности, есть ли полезное правило - относительно того, когда выполнять цикл, а когда индексировать - которое можно извлечь из этого примера в качестве руководства при написании кода Numpy?
Для тех, кто не знаком с NumPy, или кто не смотрел на код, это сравнение не основано на крайнем случае - мне, конечно, не было бы так интересно, если бы оно было. Вместо этого это сравнение включает в себя функцию, которая выполняет общую задачу в матричном вычислении (то есть, создает массив результатов с учетом двух предшественников); более того, каждая функция, в свою очередь, состоит из наиболее распространенных встроенных встроенных функций.
TL; DR Второй приведенный выше код выполняет цикл только по количеству измерений точек (3 раза в цикле for для трехмерных точек), поэтому цикла здесь не так много. Реальное ускорение второго кода выше заключается в том, что он лучше использует возможности Numpy, чтобы избежать создания дополнительных матриц при обнаружении различий между точками. Это снижает используемую память и вычислительные затраты.
Подробное объяснение
Я думаю, что функция calcDistanceMatrixFastEuclidean2
, возможно, обманывает вас своим циклом. Это только цикл по количеству размеров точек. Для 1D точек цикл выполняется только один раз, для 2D - дважды и для 3D - трижды. На самом деле это не так уж много зацикливания.
Давайте немного проанализируем код, чтобы понять, почему один быстрее другого. calcDistanceMatrixFastEuclidean
Я назову fast1
, а calcDistanceMatrixFastEuclidean2
будет fast2
.
fast1
основан на способе работы Matlab, о чем свидетельствует функция repmap
. Функция repmap
в этом случае создает массив, состоящий только из исходных данных, повторяемых снова и снова. Однако если вы посмотрите на код функции, он очень неэффективен. Для этого он использует множество функций Numpy (3 reshape
s и 2 repeat
s). Функция repeat
также используется для создания массива, содержащего исходные данные, причем каждый элемент данных повторяется много раз.Если наши входные данные - [1,2,3]
, то мы вычитаем [1,2,3,1,2,3,1,2,3]
из ] [1,1,1,2,2,2,3,3,3]
. Numpy пришлось создать множество дополнительных матриц между запуском кода C Numpy, чего можно было бы избежать.
fast2
использует больше тяжелой работы Numpy, не создавая столько матриц между вызовами Numpy. fast2
проходит через каждое измерение точек, выполняет вычитание и сохраняет промежуточную сумму квадратов разностей между каждым измерением. Только в конце получается квадратный корень. Пока что это может показаться не таким эффективным, как fast1
, но fast2
избегает выполнения работы repmat
с помощью индексации Numpy. Давайте посмотрим на одномерный случай для простоты. fast2
создает одномерный массив данных и вычитает его из двухмерного (N x 1) массива данных. Это создает матрицу разностей между каждой точкой и всеми другими точками без использования repmat
и repeat
и, таким образом, позволяет избежать создания большого количества дополнительных массивов. В этом, на мой взгляд, и заключается реальная разница в скорости. fast1
создает много дополнительных промежутков между матрицами (и они создаются с большими вычислительными затратами), чтобы найти различия между точками, в то время как fast2
лучше использует возможности Numpy, чтобы избежать этого.
Кстати, вот немного более быстрая версия fast2
:
def calcDistanceMatrixFastEuclidean3(nDimPoints):
nDimPoints = array(nDimPoints)
n,m = nDimPoints.shape
data = nDimPoints[:,0]
delta = (data - data[:,newaxis])**2
for d in xrange(1,m):
data = nDimPoints[:,d]
delta += (data - data[:,newaxis])**2
return sqrt(delta)
Разница в том, что мы больше не создаем дельту как матрицу нулей.
dis
для развлечения:
dis.dis(calcDistanceMatrixFastEuclidean)
2 0 LOAD_GLOBAL 0 (len)
3 LOAD_FAST 0 (points)
6 CALL_FUNCTION 1
9 STORE_FAST 1 (numPoints)
3 12 LOAD_GLOBAL 1 (sqrt)
15 LOAD_GLOBAL 2 (sum)
18 LOAD_GLOBAL 3 (repmat)
21 LOAD_FAST 0 (points)
24 LOAD_FAST 1 (numPoints)
27 LOAD_CONST 1 (1)
30 CALL_FUNCTION 3
4 33 LOAD_GLOBAL 4 (repeat)
36 LOAD_FAST 0 (points)
39 LOAD_FAST 1 (numPoints)
42 LOAD_CONST 2 ('axis')
45 LOAD_CONST 3 (0)
48 CALL_FUNCTION 258
51 BINARY_SUBTRACT
52 LOAD_CONST 4 (2)
55 BINARY_POWER
56 LOAD_CONST 2 ('axis')
59 LOAD_CONST 1 (1)
62 CALL_FUNCTION 257
65 CALL_FUNCTION 1
68 STORE_FAST 2 (distMat)
5 71 LOAD_FAST 2 (distMat)
74 LOAD_ATTR 5 (reshape)
77 LOAD_FAST 1 (numPoints)
80 LOAD_FAST 1 (numPoints)
83 BUILD_TUPLE 2
86 CALL_FUNCTION 1
89 RETURN_VALUE
dis.dis(calcDistanceMatrixFastEuclidean2)
2 0 LOAD_GLOBAL 0 (array)
3 LOAD_FAST 0 (nDimPoints)
6 CALL_FUNCTION 1
9 STORE_FAST 0 (nDimPoints)
3 12 LOAD_FAST 0 (nDimPoints)
15 LOAD_ATTR 1 (shape)
18 UNPACK_SEQUENCE 2
21 STORE_FAST 1 (n)
24 STORE_FAST 2 (m)
4 27 LOAD_GLOBAL 2 (zeros)
30 LOAD_FAST 1 (n)
33 LOAD_FAST 1 (n)
36 BUILD_TUPLE 2
39 LOAD_CONST 1 ('d')
42 CALL_FUNCTION 2
45 STORE_FAST 3 (delta)
5 48 SETUP_LOOP 76 (to 127)
51 LOAD_GLOBAL 3 (xrange)
54 LOAD_FAST 2 (m)
57 CALL_FUNCTION 1
60 GET_ITER
>> 61 FOR_ITER 62 (to 126)
64 STORE_FAST 4 (d)
6 67 LOAD_FAST 0 (nDimPoints)
70 LOAD_CONST 0 (None)
73 LOAD_CONST 0 (None)
76 BUILD_SLICE 2
79 LOAD_FAST 4 (d)
82 BUILD_TUPLE 2
85 BINARY_SUBSCR
86 STORE_FAST 5 (data)
7 89 LOAD_FAST 3 (delta)
92 LOAD_FAST 5 (data)
95 LOAD_FAST 5 (data)
98 LOAD_CONST 0 (None)
101 LOAD_CONST 0 (None)
104 BUILD_SLICE 2
107 LOAD_GLOBAL 4 (newaxis)
110 BUILD_TUPLE 2
113 BINARY_SUBSCR
114 BINARY_SUBTRACT
115 LOAD_CONST 2 (2)
118 BINARY_POWER
119 INPLACE_ADD
120 STORE_FAST 3 (delta)
123 JUMP_ABSOLUTE 61
>> 126 POP_BLOCK
8 >> 127 LOAD_GLOBAL 5 (sqrt)
130 LOAD_FAST 3 (delta)
133 CALL_FUNCTION 1
136 RETURN_VALUE
Я не эксперт по dis
, но кажется, что вам нужно посмотреть на функции, которые вызывает первая, чтобы понять, почему они выполняются долго. В Python также есть инструмент профилировщика производительности, cProfile
.