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

Я пытаюсь вычислить косинусное сходство между двумя разреженными векторами измерения 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)) ;
}

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

3

Решение

Скажи это N = 169647, и размеры двух на практике м, N, соответственно.

По поводу ваших вопросов:

  • Оригинальная сложность Θ (N2).

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

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

1

Другие решения

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

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

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

Полный код

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

Результаты

Пояснения к выводу эталонного теста:
пары: реализация с использованием (отсортировано) std :: vector пар
map'd: реализация с использованием std :: map
hashm: реализация с использованием std :: unordered_map
class: реализация, использующая два отдельных std :: vector для индексов и значений соответственно
specl dot (naive map): продукт точки, использующий map.find вместо правильной итерации
specl cos (оптимизированный): косинусное расстояние, повторяющееся только один раз по обоим векторам

Столбцы - это процент ненулевых значений в случайном разреженном векторе (в среднем).
Значения в терминах вектора реализации пар
(1: равное время выполнения, 2: заняло вдвое больше времени, 0,5: заняло вдвое меньше времени).

внутреннее произведение (точка)
5% 10% 15% 25%
map'd 3.3 3.5 3.7 4.0
хеш 3,6 4,0 4,8 5,2
класс 1.1 1.1 1.1 1.1
специальные [1] 8,3 9,8 10,7 10,8

норма в квадрате (len2)
5% 10% 15% 25%
map'd 6,9 7,6 8,3 10,2
хэш 2,3 3,6 4,1 4,8
класс 0,98 0,95 0,93 0,75

косинусное расстояние (cos)
5% 10% 15% 25%
map'd 4.0 4.3 4.6 5.0
хэш 3,2 3,9 4,6 5,0
класс 1.1 1.1 1.1 1.1
специальные [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] (соответствует реализации ОП):

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 число ненулевых элементов в разреженном векторе: это тривиально, чтобы увидеть карту и вектор как реализацию Dot та же и неупорядоченная карта достигает этого путем реализации find в O(1) амортизируется.

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

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

2

По вопросам рекламы [email protected]