Вычислить расстояние между картами, которые представляют разреженные векторы С++

Введение и исходный код

Я пытаюсь вычислить косинусное сходство между двумя разреженными векторами размерности 169647. В качестве входных данных два вектора представлены в виде строки формы <index, value>. Только ненулевые элементы вектора получают индекс.

x = "1:0.1 43:0.4 100:0.43 10000:0.9"
y = "200:0.5 500:0.34 501:0.34"

Сначала мы преобразуем каждый из x и y в два vectors<float>. с помощью функции splitVector. Затем мы вычисляем расстояние с помощью функции cosine_similarity. Неважно, split функция. Я использую его на тот случай, если вы захотите запустить код.

#include <iostream>
#include <string>
#include <vector> 
#include <algorithm>

using namespace std;

void split(const string& s, char c,vector<string>& v) {
   string::size_type i = 0;
   string::size_type j = s.find(c);

   while (j != string::npos) {
      v.push_back(s.substr(i, j-i));
      i = ++j;
      j = s.find(c, j);

      if (j == string::npos)
         v.push_back(s.substr(i, s.length()));
   }
}

float cosine_similarity(const std::vector<float> & A,const std::vector<float> & B)
{
    float dot = 0.0, denom_a = 0.0, denom_b = 0.0 ;
    for(unsigned int i = 0; i < A.size(); ++i)
    {
        dot += A[i] * B[i] ;
        denom_a += A[i] * A[i] ;
        denom_b += B[i] * B[i] ;
    }
    return dot / (sqrt(denom_a) * sqrt(denom_b)) ;
}

void splitVector(const vector<string> & v, vector<float> & values)
{
    vector<string> tmpv;
    string parsed;
    for(unsigned int i = 0; i < v.size(); i++)
    {
        split(v[i], ':', tmpv);
        int idx = atoi(tmpv[0].c_str());
        float val = atof(tmpv[1].c_str()); 
    tmpv.clear();
    values[idx] = val;
    }//end for;
}//end function

int main()
{
   //INPUT VECTORS.
   vector<string> x {"1:0.1","43:0.4","50:0.43","90:0.9"};
   vector<string> y {"20:0.5","40:0.34","50:0.34"};
   
   //STEP 1: Initialize vectors
   int dimension = 169647;
   vector<float> X;
   X.resize(dimension, 0.0);
   
   vector<float> Y;
   Y.resize(dimension, 0.0);
   
   //STEP 2: CREATE FLOAT VECTORS
   splitVector(x, X);
   splitVector(y, Y);
   
   //STEP 3: COMPUTE COSINE SIMILARITY
   cout << cosine_similarity(X,Y) << endl;
}

Проблема и предлагаемое решение

Инициализация и заполнение vector<float> является проблемой. Это действительно занимает так много времени на выполнение. Я думал об использовании структуры std::map<int,float> в С++. где X и Y будут представлены:

std::map<int,float> x_m{ make_pair(1,0.1), make_pair(43,0.4), make_pair(50,0.43), make_pair(90,0.9)};
std::map<int,float> y_m{ make_pair(20,0.5), make_pair(40,0.34), make_pair(50,0.34)};

Для этого я использовал следующую функцию:

float cosine_similarity(const std::map<int,float> & A,const std::map<int,float> & B)
{
    float dot = 0.0, denom_a = 0.0, denom_b = 0.0 ;
    for(auto &a:A)
    { 
      denom_a += a.second * a.second ;
    }
    
    for(auto &b:B)
    { 
      denom_b += b.second * b.second ;
    }
    
    for(auto &a:A)
    {  
        if(B.find(a.first) != B.end())
        {
          dot +=  a.second * B.find(a.first)->second ;
        }  
    }
    return dot / (sqrt(denom_a) * sqrt(denom_b)) ;
}

Вопрос

  • Можете ли вы помочь мне с математикой сложности?
  • Снизит ли сложность вторая предложенная функция, использующая карты?
  • Что вы думаете о решении?

person Hani Goc    schedule 20.01.2016    source источник
comment
Рассматривали ли вы просто использование существующей библиотеки для вычисления этого?   -  person Jørgen Fogh    schedule 20.01.2016
comment
Я думаю, что вычисление с использованием векторов может быть быстрее, чем карты ... в чем проблема с векторами?   -  person Humam Helfawi    schedule 20.01.2016
comment
нет @JørgenFogh Я бы очень хотел. Раньше я использовал питон. Но, как видите, я перешел на C++.   -  person Hani Goc    schedule 20.01.2016
comment
лучше использовать матлаб :)   -  person dynamic    schedule 20.01.2016
comment
@ Мне нужно заполнить векторы. Я буду создавать разреженные векторы размерности 169647, а затем вычислять расстояние между ними.   -  person Hani Goc    schedule 20.01.2016
comment
ваша проблема аналогична слиянию двух отсортированных массивов (поиск слияния, чтобы найти пример merge): в вашем случае у вас будет четыре массива, два с (отсортированными) индексами и два со значениями по этим индексам. выполнить итерацию по обоим массивам индексов, одновременно сравнивая значения в одинаковых элементах индекса и заменяя все несовпадающие индексы в одном с нулевым значением в другом.   -  person BeyelerStudios    schedule 20.01.2016
comment
@BeyelerStudios Если я правильно понимаю, но разве MAP не делает то, о чем вы говорите?   -  person Hani Goc    schedule 20.01.2016
comment
@HaniGoc map имеет ненужное время построения O(nlogn), если ваши индексы уже в порядке (как в большинстве разреженных векторных представлений), поэтому он будет доминировать в сравнении расстояний, которое равно O(n). Тогда есть накладные расходы на итерацию по двум картам вместо увеличения двух индексов (или четырех указателей).   -  person BeyelerStudios    schedule 20.01.2016
comment
@HaniGoc Думаю, дело в том, что если реализовать и поддерживать более быструю версию массива так же просто или проще, зачем возиться с картой?   -  person BeyelerStudios    schedule 20.01.2016
comment
@BeyelerStudios ну да. на самом деле карта добавляла коду дополнительную сложность программирования. ты прав.   -  person Hani Goc    schedule 20.01.2016
comment
Ваша функция split очень неэффективна, потому что она создает много динамически выделяемых строк. Вместо этого попробуйте использовать std::istringstream.   -  person gudok    schedule 20.01.2016
comment
Что касается самого косинусного алгоритма, правильная реализация зависит от того, сколько ненулевых элементов в типичных векторах. Если почти все элементы отличны от нуля, просто используйте std::vector<float> с фиксированной длиной 169647. Если есть только несколько ненулевых элементов (как в вашем примере), используйте std::vector<std::pair<int, float> >, заполните его split, отсортируйте по индексу измерения, а затем реализуйте косинусное сходство, одновременно сканируя оба вектора слева направо (аналогично этапу слияния сортировки слиянием).   -  person gudok    schedule 20.01.2016
comment
@gudok На самом деле я использую boost::split(parts, tfidf, boost::is_any_of()); но только здесь, чтобы иметь возможность запускать код. независимо от библиотеки   -  person Hani Goc    schedule 20.01.2016
comment
@гудок. Подожди. Предположим, я сохраняю вектор‹float› размерности 169647 заполненным нулем. И я вычисляю сходство косинусов. Будет ли это быстрее и проще, чем использование карты? о, хорошо, я просто сохраню это. Все-таки это действительно сложно.   -  person Hani Goc    schedule 20.01.2016
comment
Я думал, что с помощью карты я уменьшу сложность алгоритма.   -  person Hani Goc    schedule 20.01.2016
comment
Я сомневаюсь, что вы сможете уменьшить сложность алгоритма с помощью карты. Если ваши векторы маленькие, то все дело в доступе к памяти, а не в асимптотике... В любом случае, если вы хотите попробовать карту, 1) используйте std::unordered_map вместо std::map (в идеале, с правильно угаданной начальной емкостью); 2) создать только одну карту для двух векторов, например. std::unordered_map<int, std::pair<float, float> > -- тогда в алгоритме косинусного сходства потребуется только один проход по этой карте.   -  person gudok    schedule 20.01.2016


Ответы (2)


Предположим, что N = 169647, а размеры двух на практике равны m, n соответственно.

Что касается ваших вопросов:

  • Исходная сложность равна (N2).

  • Сложность предлагаемого вами решения составляет O((m + n) log(max(m, n)), что, вероятно, будет намного меньше; использование std::unordered_map вместо этого вы можете уменьшить это значение до ожидаемого O(m + n).

  • Звучит неплохо, но, как всегда - YMMV. Вы должны профилировать как эту операцию в контексте всего вашего приложения (чтобы увидеть, является ли это проблемой), так и шаги внутри этой операции.

person Ami Tavory    schedule 25.01.2016

Обычное представление разреженных векторов — это простой массив индексов и одно из значений или иногда массив пар индексов и значений, поскольку обычно вам нужно получить доступ к индексу вместе со значением (за исключением случаев, когда вам не нравится длина вектора / нормализация или что-то подобное). Были предложены две другие формы: с использованием std::map и std::unordered_map.

Пожалуйста, найдите вывод в конце.

Ориентир

Я реализовал длину векторных операций и внутренний продукт (точечный продукт) для этих четырех представлений. Кроме того, я реализовал внутренний продукт очень прямолинейно, предложенный в вопросе OP, и улучшил вычисление косинусного расстояния для реализации пар векторов.

Полный код

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

Полученные результаты

Explanation of the output of the benchmark:
  pairs: implementation using (sorted) std::vector of pairs
  map'd: implementation using std::map
  hashm: implementation using std::unordered_map
  class: implementation using two separate std::vector for indices and values respectively
  specl dot (naive map): dot product using map.find instead of proper iteration
  specl cos (optimised): cosine distance iterating only once over both vectors

Columns are the percentage of non-zeros in the random sparse vector (on average).
Values are in terms of the vector of pairs implementation
(1: equal runtime, 2: took twice as long, 0.5: took half as long).

                    inner product (dot)
            5%          10%          15%          25%
map'd       3.3          3.5          3.7          4.0
hashm       3.6          4.0          4.8          5.2
class       1.1          1.1          1.1          1.1
special[1]  8.3          9.8         10.7         10.8

                    norm squared (len2)
            5%          10%          15%          25%
map'd       6.9          7.6          8.3         10.2
hashm       2.3          3.6          4.1          4.8
class       0.98         0.95         0.93         0.75

                    cosine distance (cos)
            5%          10%          15%          25%
map'd       4.0          4.3          4.6          5.0
hashm       3.2          3.9          4.6          5.0
class       1.1          1.1          1.1          1.1
special[2]  0.92         0.95         0.93         0.94

Тестируемая реализация

За исключением случая special[2], я использовал следующую функцию косинусного расстояния:

template<class Vector>
inline float CosineDistance(const Vector& lhs, const Vector& rhs) {
    return Dot(lhs, rhs) / std::sqrt(LenSqr(lhs) * LenSqr(rhs));
}

Контейнеры пары

Вот реализация Dot для отсортированных vector<pair<size_t,float>> и map<size_t,float>:

template<class PairContainerSorted>
inline float DotPairsSorted(const PairContainerSorted& lhs, const PairContainerSorted& rhs) {
    float dot = 0;
    for(auto pLhs = lhs.begin(), pRhs = rhs.begin(), endLhs = lhs.end(), endRhs = rhs.end(); pRhs != endRhs;) {
        for(; pLhs != endLhs && pLhs->first < pRhs->first; ++pLhs);
        if(pLhs == endLhs)
            break;
        for(; pRhs != endRhs && pRhs->first < pLhs->first; ++pRhs);
        if(pRhs == endRhs)
            break;
        if(pLhs->first == pRhs->first) {
            dot += pLhs->second * pRhs->second;
            ++pLhs;
            ++pRhs;
        }
    }
    return dot;
}

Это реализация для Dot как для неупорядоченной карты, так и для special[1] (равная реализации OP):

template<class PairMap>
inline float DotPairsMapped(const PairMap& lhs, const PairMap& rhs) {
    float dot = 0;
    for(auto& pair : lhs) {
        auto pos = rhs.find(pair.first);
        if(pos != rhs.end())
            dot += pair.second * pos->second;
    }
    return dot;
}

Реализация LenSqr:

template<class PairContainer>
inline float LenSqrPairs(const PairContainer& vec) {
    float dot = 0;
    for(auto& pair : vec)
        dot += pair.second * pair.second;
    return dot;
}

Пара векторов

Обратите внимание, что я упаковал пару векторов в структуру (или class) SparseVector (подробности см. в полном коде):

inline float Dot(const SparseVector& lhs, const SparseVector& rhs) {
    float dot = 0;
    if(!lhs.idx.empty() && !rhs.idx.empty()) {
        const size_t *itIdxLhs = &lhs.idx[0], *endIdxLhs = &lhs.idx[0] + lhs.idx.size();
        const float *itValLhs = &lhs.val[0], *endValLhs = &lhs.val[0] + lhs.val.size();
        const size_t *itIdxRhs = &rhs.idx[0], *endIdxRhs = &rhs.idx[0] + rhs.idx.size();
        const float *itValRhs = &rhs.val[0], *endValRhs = &rhs.val[0] + rhs.val.size();
        while(itIdxRhs != endIdxRhs) {
            for(; itIdxLhs < endIdxLhs && *itIdxLhs < *itIdxRhs; ++itIdxLhs, ++itValLhs);
            if(itIdxLhs == endIdxLhs)
                break;
            for(; itIdxRhs < endIdxRhs && *itIdxRhs < *itIdxLhs; ++itIdxRhs, ++itValRhs);
            if(itIdxRhs == endIdxRhs)
                break;
            if(*itIdxLhs == *itIdxRhs) {
                dot += (*itValLhs) * (*itValRhs);
                ++itIdxLhs;
                ++itValLhs;
                ++itIdxRhs;
                ++itValRhs;
            }
        }
    }
    return dot;
}

inline float LenSqr(const SparseVector& vec) {
    float dot = 0;
    for(float v : vec.val)
        dot += v * v;
    return dot;
}

special[2] просто вычисляет квадрат нормы обоих векторов, перебирая их во время внутреннего произведения (подробности см. в полном коде). Я добавил это, чтобы доказать: попадание в кеш имеет значение. Я могу победить наивный подход векторов пар с парами векторов один, если я просто более эффективно обращаюсь к своей памяти (то же самое было бы верно, если бы вы, конечно, оптимизировали другие пути).

Вывод

Обратите внимание, что все протестированные реализации (за исключением special[1] с поведением O(k*logk)) демонстрируют теоретическое поведение во время выполнения O(k), где k — количество ненулевых элементов в разреженном векторе: это тривиально увидеть для map и vector как реализацию Dot < strong>то же самое, и неупорядоченная карта достигает этого путем реализации find в O(1) с амортизацией.

Почему тогда карта — неправильный инструмент для разреженного вектора? Для std::map ответом являются накладные расходы на итерацию древовидной структуры, для std::unordered_map шаблон случайного доступа к памяти для find, что приводит к огромным накладным расходам при промахах кеша.

Чтобы демистифицировать теоретическое преимущество std::unordered_map над std::map, проверьте результаты для special[1]. Это реализация, которую std::unordered_map превосходит не потому, что она лучше подходит для решения проблемы, а потому, что реализация с использованием std::map была неоптимальной.

person BeyelerStudios    schedule 26.01.2016