Почему здесь происходит клонирование индексации?

Несколько лет назад кто-то опубликовал в 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 двумерных точек имеет один миллион записей). На первый взгляд эта функция выглядела как код, который я использовал для записи, когда я изучал NumPy, и я бы написал код NumPy, сначала написав код Python, а затем переведя его, построчно.

Спустя несколько месяцев после публикации Active State результаты тестов производительности, сравнивающих три, были опубликованы и обсуждены в thread в списке рассылки 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)

Один участник в теме (Keir Mierle) предложил причину, почему это может быть правдой:

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

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

Любые другие мысли о том, как объяснить эти результаты?

В частности, существует ли полезное правило - относительно того, когда цикл и когда индексировать - который можно извлечь из этого примера в качестве руководства при написании numpy-кода?

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

Ответ 1

TL; DR Второй код, приведенный выше, представляет собой только цикл по количеству измерений точек (3 раза по циклу for для трехмерных точек), поэтому петли не так много. Реальное ускорение во втором коде выше - это то, что он лучше использует силу Numpy, чтобы избежать создания некоторых дополнительных матриц при поиске различий между точками. Это уменьшает использование памяти и вычислительные усилия.

Дольше Объяснение Я думаю, что функция calcDistanceMatrixFastEuclidean2 обманывает вас, возможно, своим циклом. Это только цикл по числу измерений точек. Для 1D точек цикл выполняется один раз, для 2D, дважды, и для 3D - трижды. Это действительно не так много циклов.

Проанализируйте код немного, чтобы понять, почему он быстрее, чем другой. calcDistanceMatrixFastEuclidean Я буду называть fast1, а calcDistanceMatrixFastEuclidean2 будет fast2.

fast1 основан на способе Matlab делать вещи, о чем свидетельствует функция repmap. Функция repmap создает массив в этом случае, это только оригинальные данные, повторяющиеся снова и снова. Однако, если вы посмотрите на код функции, это очень неэффективно. Для этого используется множество функций Numpy (3 reshape и 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 пришлось создавать множество дополнительных матриц между запущенным кодом Numpy C, которого можно было бы избежать.

fast2 использует больше тяжелого подъема Numpy, не создавая столько матриц между вызовами Numpy. fast2 проходит через каждый размер точек, выполняет ли вычитание и сохраняет общее количество квадратов различий между каждым измерением. Только в конце делается квадратный корень. Пока что это может показаться не столь эффективным, как fast1, но fast2 избегает делать вещи repmat, используя индексирование Numpy. Рассмотрим простоту для случая 1D. fast2 создает 1D-массив данных и вычитает его из массива 2D (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)

Разница в том, что мы больше не создаем delta как матрицу нулей.

Ответ 2

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.