更快地计算余弦相似度

时间:2013-06-26 19:23:21

标签: java search-engine k-means cosine-similarity

我想在我的IR项目中使用余弦相似性但是因为矢量的大小很大并且必须多次浮动多次,所以需要很长时间。

有没有办法更快地计算余弦相似度?

这是我的代码:

private double diffrence(HashMap<Integer, Float> hashMap,
 HashMap<Integer, Float> hashMap2 ) {
    Integer[] keys = new Integer[hashMap.size()];
    hashMap.keySet().toArray(keys);

     float ans = 0;

    for (int i = 0; i < keys.length; i++) {
        if (hashMap2.containsKey(keys[i])) {
             ans += hashMap.get(keys[i]) * hashMap2.get(keys[i]);

        }
    }

     float hashLength = 0;
    for (int i = 0; i < keys.length; i++) {
         hashLength += (hashMap.get(keys[i]) * hashMap.get(keys[i]));
    }
     hashLength = (float) Math.sqrt(hashLength);

    Integer[] keys2 = new Integer[hashMap2.size()];
    hashMap2.keySet().toArray(keys2);

     float hash2Length = 0;
    for (int i = 0; i < keys2.length; i++) {

         hash2Length += hashMap2.get(keys2[i]) * hashMap2.get(keys2[i]);

    }
     hash2Length = (float) Math.sqrt(hash2Length);

    return (float) (ans /(hash2Length*hashLength));
}

5 个答案:

答案 0 :(得分:7)

通常在IR中,一个向量的非零元素远少于另一个向量(通常查询向量是较稀疏的元素,但即使对于文档向量也是如此)。您可以通过循环遍历稀疏向量的键来节省时间,即较小的哈希映射,在较大的哈希映射中查找它们。

至于pkacprzak建议的查找表和你的内存不足:意识到可以在余弦相似度计算之前进行归一化。对于每个向量,在存储之前,计算其范数并将每个元素除以该范数。然后,您可以计算点积并获得余弦相似度。

即,余弦相似度通常定义为

x·y / (||x|| × ||y||)

但这等于

(x / ||x||) · (y / ||y||)

其中/是逐元素划分。如果您每个都x替换x / ||x||,那么您只需要计算x·y

如果将这两个建议结合起来,就会得到一个余弦相似度算法,只需要在两个输入中较小的一个上进行一次循环。

通过使用更智能的sparse vector结构,可以进一步改进;哈希表在查找和迭代中都有 lot 的开销。

答案 1 :(得分:1)

通常有太多的向量来预先计算每对的余弦相似度,但是您可以预先计算每个向量的长度并使用查找表存储它。这减少了计算两个向量的余弦相似度的常数因子 - 实际上它节省了大量的时间,因为有很多浮点运算。

我假设你不是通过在向量中存储零来浪费记忆。

答案 2 :(得分:1)

除了像其他人建议的那样预先标准化你的矢量并假设你的矢量列表没有改变,将它们转换为数组一次(在相似性函数之外)并按照它们排序关键指标,例如:

Integer[] keys = new Integer[hashMap.size()];
Float values[] = new Float[keys.size()];
int i = 0;
float norm = ...;    
for (Map.Entry<Integer, Float> entry : new TreeMap<Integer, Float>(hashMap).entrySet())
{
   keys[i] = entry.getKey();
   values[i++] = entry.getValue() / norm;
}

然后进行实际的相似度计算(假设你然后传递keys1valueskeys2values2而不是两个HashMaps),你的内心循环减少到:

float ans = 0;
int i,j = 0;
while (i < keys1.length && j < keys2.length)
{
  if (keys1[i] < keys2[j])
    ++i;
  else if (keys1[i] > keys2[j])
    ++j;
  else
    // we have the same key in 1 and 2
    ans += values1[i] * values2[j];
}

您甚至可以考虑将所有矢量的所有keysvalues连续存储在intfloat的大数组中,将另一个数组与索引保持在第一个的位置:

int sumOfAllVectorLengths = ...;
int allKeys[] = new int[sumOfAllVectorLengths];
float allValues[] = new float[sumOfAllVectorLengths];
int firstPos = new int[numberOfVectors + 1]; 
firstPos[numberOfVectors] = sumOfAllVectorLengths;

int nextFirstPos = 0;
int index = 0;

for (HashMap<Integer, Float> vector : allVectors)
{
   firstPos[index] = nextFirstPos;

   float norm = ...;    
   for (Map.Entry<Integer, Float> entry : new TreeMap<Integer, Float>(hashMap).entrySet())
   {
      keys[nextFirstPos] = entry.getKey();
      values[nextFirstPos++] = entry.getValue() / norm;
   }

   ++index; 
}

然后将数组和向量的索引传递给比较函数。

答案 3 :(得分:0)

您可以查看项目simbase https://github.com/guokr/simbase,它是一个矢量相似性nosql数据库。

Simbase使用以下概念:

  • 矢量集:一组矢量
  • 基础:向量的基础,一个向量集中的向量具有相同的基础
  • 建议:具有相同基础的两个向量集之间的单向二元关系

写操作在每个基础上以单个线程处理,并且需要在任何两个向量之间进行比较,因此写操作在O(n)处缩放。

我们对i7-cpu Macbook上的密集向量进行了非最终性能测试,它可以轻松处理100k 1k维向量,每次写入操作在0.14秒内;如果线性比例可以保持,则意味着Simbase可以处理700k密集向量,每次写入操作不到1秒。

答案 4 :(得分:-2)

我可以清楚地看到至少有一个地方,你只是在浪费CPU周期:

for (int i = 0; i < keys.length; i++) {
    if (hashMap2.containsKey(keys[i])) {
         ans += hashMap.get(keys[i]) * hashMap2.get(keys[i]);
    }
}

float hashLength = 0;
for (int i = 0; i < keys.length; i++) {
     hashLength += (hashMap.get(keys[i]) * hashMap.get(keys[i]));
}

这里你在同一个2个hashMaps上有2个相同边界的循环。你为什么不在一个周期内完成它:

float hashLength = 0;
int hm = 0;
for (int i = 0; i < keys.length; i++) {
    hm = hashMap.get(keys[i])*hashMap2.get(keys[i]);
    hashLength += hm;
    if (hashMap2.containsKey(keys[i])) {
         ans += hm;
    }
}

顺便问一下,使用hashMap有什么特殊原因吗?或者你可以使用一些更简单的数组?