Ускорение расстояния L1 между всеми парами в наземном наборе

У меня есть матрица NxM (обычно 10k X 10k элементов), описывающая набор оснований. Каждая строка представляет объект и каждый столбец конкретную функцию. Например, в матрице

   f1 f2 f3
x1 0  4  -1
x2 1  0  5
x3 4  0  0
x4 0  1  0

объект x1 имеет значение 0 в функции 1, значение 4 в функции 1 и значение 0 в элементе -1. Значения этого представляют собой общие действительные числа (double).

Мне нужно вычислить несколько пользовательских расстояний/различий между всеми парами объектов (все пары линий). Для сравнения, я хочу вычислить L1 (manhattan) и L2 (евклидовых) расстояний.

У меня есть библиотека Eigen для выполнения большинства моих вычислений. Для вычисления L2 (евклидова) я использую следующее наблюдение: для двух векторов a и b размера n мы имеем:

||a - b||^2 = (a_1 - b_1)^2 + (a_2 - b_2)^2 + ... +(a_n - b_n)^2
            = a_1^2 + b_1^2 - 2 a_1 b_1 + a_2^2 + b_2^2 - 2 a_2 b_2 + ... + a_n^2 + b_n^2 - 2 a_n b_n
            = a . a + b . b - 2ab

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

В любом случае, это позволяет писать фэнтезийный код с использованием Eigen (в С++):

Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic> matrix, XX, D;

// Load matrix here, for example
// matrix << 0, 4, -1,
//           1, 0,  5,
//           4, 0,  0,
//           0, 1,  0;

const auto N = matrix.rows();

XX.resize(N, 1);
D.resize(N, N);

XX = matrix.array().square().rowwise().sum();

D.noalias() = XX * Eigen::MatrixXd::Ones(1, N) +
              Eigen::MatrixXd::Ones(N, 1) * XX.transpose();

D -= 2 * matrix * matrix.transpose();
D = D.cwiseSqrt();

Для матрицы 10k X 10k мы можем вычислить расстояние L2 для всех пар объектов/линий менее чем за 1 минуту (2 ядра /4 потока), что я лично считаю хорошим показателем для моих целей. Eigen может комбинировать операции и использовать несколько оптимизаций низкого/высокого уровня для выполнения этих вычислений. В этом случае Eigen использует все доступные ядра (и, конечно же, мы можем это настроить).

Однако мне все еще нужно вычислить расстояние L1, но я не мог найти хорошую алгебраическую форму для использования с Eigen и получить хорошую производительность. До сих пор у меня есть следующее:

const auto N = matrix.rows();
for(long i = 0; i < N - 1; ++i) {
    const auto &row = matrix.row(i);

    #ifdef _OPENMP
    #pragma omp parallel for shared(row)
    #endif
    for(long j = i + 1; j < N; ++j) {
        distance(i, j) = (row - matrix.row(j)).lpNorm<1>();
    }
}

Но это занимает гораздо больше времени: для той же матрицы 10k X 10k этот код использует 3,5 минуты, что намного хуже, учитывая, что L1 и L2 очень близки в их первоначальном виде:

L1(a, b) = sum_i |a_i - b_i|
L2(a, b) = sqrt(sum_i |a_i - b_i|^2)

Любая идея, как изменить L1 на использование быстрых вычислений с Eigen? Или лучшая форма для этого, и я просто не понял.

Большое спасибо за вашу помощь!

Карлос

Ответы

Ответ 1

Позволяет просто рассчитать оба расстояния в одно и то же время. Они только действительно разделяют разницу в строках (в то время как оба могут быть абсолютной разницей, эвклидовое расстояние использует квадрат, который на самом деле не эквивалентен). Итак, теперь мы только перебираем строки n ^ 2.

const auto N = matrix.rows();
for(long i = 0; i < N - 1; ++i) {
    const auto &row = matrix.row(i);

    #ifdef _OPENMP
    #pragma omp parallel for shared(row)
    #endif
    for(long j = i + 1; j < N; ++j) {
        const auto &rowDiff = row - matrix.row(j);
        distanceL1(i, j) = rowDiff.cwiseAbs().sum(); // or .lpNorm<1>(); if it faster
        distanceL2(i, j) = rowDiff.norm()
    }
}

EDIT еще один интенсивный/непроверенный метод памяти может состоять в том, чтобы вычислить целую строку расстояния на каждой итерации (не знаю, будет ли это улучшение или нет)

const auto N = matrix.rows();
#ifdef _OPENMP
#pragma omp parallel for shared(matrix)
#endif
for(long i = 0; i < N - 1; ++i) {
    const auto &row = matrix.row(i);
    // you could use matrix.block(i,j,k,l) to cut down on the number of unnecessary operations
    const auto &mat = matrix.rowwise() - row;

    distanceL1(i) = mat.cwiseAbs().sum().transpose();
    distanceL2(i) = mat.rowwise().norm().transpose();
}

Ответ 2

Это две очень распространенные операции обработки изображений. Первый Sum of Squared Differences (SSD), второй - Сумма абсолютных различий (SAD).

Вы правильно определили, что для SSD просто требуется вычислить cross-correlation между двумя сериями в качестве основного вычисления. Однако вы можете рассмотреть возможность использования FFT для вычисления этих терминов a.b, это сократит количество операций, необходимых для случая L2, но насколько я не знаю, это зависит от того, матричный алгоритм умножения, который использует Eigen.) Если вам нужно, чтобы я объяснил это, я могу, но я полагаю, вы также можете найти его в качестве стандартного использования FFTs. OpenCV имеет (довольно плохую/багги) реализацию сопоставления шаблонов, что вам нужно при использовании CV_TM_SQDIFF.Забастовкa >

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

Вы также можете просмотреть примитивы Imaging Primaging для кросс-корреляции и быстрые библиотеки FFT, такие как FFTW и CUFFT. Если вы не можете позволить себе Intel Imaging Primitves, вы можете использовать инструкции SSE, чтобы значительно ускорить обработку до почти того же эффекта.забастовкa >