【问题标题】:Compute distance between maps that represent sparse vectors c++计算表示稀疏向量c ++的地图之间的距离
【发布时间】:2016-04-26 04:52:50
【问题描述】:

简介及源码

我正在尝试计算两个维度为 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"

首先,我们使用函数splitVector 将x 和y 分别转换为两个vectors<float>.。然后我们使用函数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&lt;float&gt; 是个问题。它真的需要很多执行时间。我正在考虑在 C++ 中使用 std::map&lt;int,float&gt; 结构。其中 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)) ;
}

问题

  • 您能帮我计算一下复杂性吗?
  • 使用地图的第二个提议函数会降低复杂性吗?
  • 您认为解决方案如何?

【问题讨论】:

  • 您是否考虑过仅使用现有库来计算它?
  • 我认为使用向量计算它可能比地图更快。向量有什么问题?
  • 如果使用matlab会更好:)
  • @HaniGoc map 有一个不必要的 O(nlogn) 构造时间如果你的索引已经有序(在大多数稀疏向量表示中它们是)所以它将主导距离比较,即@987654334 @。然后是遍历两个映射而不是增加两个索引(或四个指针)的开销。
  • @HaniGoc 我想重点是,如果实现和维护更快的数组版本一样容易或更容易,为什么还要麻烦地图?

标签: c++ dictionary vector distance cosine-similarity


【解决方案1】:

稀疏向量的常见表示是一个简单的索引数组和一个值,或者有时是索引和值对的数组,因为通常您需要与值一起访问索引(除非您不喜欢矢量长度/归一化或类似)。建议使用另外两种形式:使用std::mapstd::unordered_map

请在最后找到结论。

基准测试

我为这四种表示实现了向量运算长度和内积(点积)。此外,我以 OP 问题中建议的非常直接的方式实现了内积,并改进了向量对实现的余弦距离计算。

完整代码

我已经对这些实现进行了基准测试。您可以从这个link 中查看我的代码,我从中获取了以下数字(尽管比率与我自己机器上的运行非常匹配,只有更高的RunCount 才能更均匀地分布随机输入向量) .结果如下:

结果

基准输出的解释:
  对:使用(排序的)std::vector 对的实现
  map'd:使用 std::map 实现
  hashm:使用 std::unordered_map 实现
  类:使用两个单独的 std::vector 分别用于索引和值的实现
  specl dot (naive map):使用 map.find 而不是正确迭代的点积
  specl cos(优化):余弦距离仅在两个向量上迭代一次

列是随机稀疏向量中非零的百分比(平均)。
值是根据对实现的向量
(1:相同的运行时间,2:花费两倍的时间,0.5:花费一半的时间)。

                    内积(点)
            5% 10% 15% 25%
映射 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%
映射 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%
映射 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]-case 我使用了以下余弦距离函数:

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

一对容器

这是Dot 的实现,用于排序的vector&lt;pair&lt;size_t,float&gt;&gt;map&lt;size_t,float&gt;

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;
}

这是无序映射和special[1]Dot 的实现(等于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;
}

向量对

请注意,我将这对向量打包到结构中(或classSparseVector(查看完整代码了解详细信息):

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] 简单地计算两个向量的平方范数,同时在内积期间迭代它们(查看完整代码以获取详细信息)。我添加了这一点来证明一点:缓存命中很重要。如果我只是更有效地访问我的内存(当然,如果您优化其他路径也是如此),我可以用向量对来击败向量对的幼稚方法。

结论

请注意,所有测试的实现(除了具有O(k*logk) 行为的special[1] 之外)表现出O(k) 的理论运行时行为,其中k 是稀疏向量中非零的数量:这对参见 map 和 vector,因为 Dot 的实现 是相同的,而无序映射通过在 O(1) amortised 中实现 find 来实现这一点。

那么为什么地图是稀疏矢量的错误工具?对于std::map,答案是迭代树结构的开销,对于std::unordered_mapfind 的随机内存访问模式,两者都会导致缓存未命中期间的巨大开销。

要揭开std::unordered_map 相对于std::map 的理论优势,请检查special[1] 的结果。这是 std::unordered_map 正在击败的实现,不是因为它更适合该问题,而是因为使用 std::map 的实现不是最佳的。

【讨论】:

    【解决方案2】:

    n = 169647 em>,两者在实践中的尺寸分别是 M EM>, N EM>。

    关于您的问题:

    • 原始复杂性是θ(n 2 sup>) em>。

    • 所提出的解决方案的复杂性是 O((m + n)log(max(m,n)) em>,这可能远小;使用std::unordered_map您可以将其降低到预期的 O(m + n) em>。

    • 听起来很好,但是,始终 - 是的 - YMMV。您应该在整个应用程序的上下文中介绍此OP(查看它是一个问题),以及此OP中的步骤。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-03-07
      • 1970-01-01
      • 1970-01-01
      • 2017-01-13
      • 1970-01-01
      • 1970-01-01
      • 2020-06-02
      相关资源
      最近更新 更多