Я пытаюсь вычислить косинусное сходство между двумя разреженными векторами измерения 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)) ;
}
Скажи это N = 169647, и размеры двух на практике м, N, соответственно.
По поводу ваших вопросов:
Оригинальная сложность Θ (N2).
Сложность предложенного вами решения O ((m + n) log (max (m, n)), который, вероятно, будет гораздо меньше; с помощью std::unordered_map
вместо этого вы можете уменьшить это до ожидаемого O (m + n).
Звучит хорошо, но, как всегда, — YMMV. Вы должны профилировать как эту операцию в контексте всего вашего приложения (чтобы увидеть, является ли это проблемой), так и шаги в этой операции.
Общее представление разреженных векторов представляет собой простой массив индексов и одно из значений или иногда массив пар индексов и значений, поскольку обычно вам необходим доступ к индексу вместе со значением (кроме случаев, когда вам не нравится длина вектора / нормализация или аналогичная). Были предложены две другие формы: использование 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
был неоптимальным.