【问题标题】:calculate cosine similarity faster更快地计算余弦相似度
【发布时间】:2013-06-24 02:28:25
【问题描述】:

我想在我的 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));
}

【问题讨论】:

  • 比什么更快?你真的应该展示一些代码,或者至少描述你的算法。如果您没有使用稀疏向量表示,则说明您做得不够理想。
  • 你的所有内核都在炽热地运行,不是吗?
  • @Ulterior :你是什么意思?
  • 你分析过这段代码吗?你知道哪一部分需要时间吗?有了这么多的哈希表查找,我不会太担心一些浮点操作。 (例如hashMap.get(keys[i]) * hashMap.get(keys[i]) 两次查找完全相同的键,而您可以缓存结果。)
  • @Paniz:这与 O(1) 无关,而与实际性能有关。哈希表是非常慢的向量表示。此外,如果您要进行 n 次查找,然后进行 n 次乘法运算,那么两者总共需要 O(n) 时间。

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


【解决方案1】:

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

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

即余弦相似度通常定义为

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

但这等于

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

其中/ 是逐元素除法。如果你每个人都用x / ||x||替换x,那么你只需要计算x·y

如果您将这两个建议结合起来,您将得到一个余弦相似度算法,该算法只对两个输入中较小的一个进行一个循环。

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

【讨论】:

  • 当我使用其他方法上的值时,我不能像你所说的那样替换。 hashmap 没有任何零值,因为我想在 hashmap 中查找另一个 hashmap 的键,所以我认为它是最好的 DS。不是吗?
  • @Paniz:哈希表中不存在的元素隐式为零,不是吗?不,哈希表不是稀疏向量的理想数据结构,正如我一直在解释的那样。 (索引,值)对的排序列表可以使事情变得更快。如果你想坚持使用哈希表,你永远不会从中获得真正好的性能。
  • 如果我将其更改为(索引,值)列表然后对其进行排序,排序时间会小于此代码吗?
  • @Paniz:排序可以离线完成,因此成本分布在许多余弦相似度计算中。
【解决方案2】:

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

我假设您不会通过在向量中存储零来浪费内存。

【讨论】:

  • 因为我的内存有限,我不能再保存任何东西了:(
  • @Paniz 如果你有 n 个向量,那么存储预先计算的长度只消耗 n 个额外的浮点数。此外,为了节省内存,您可以用更简单的数据结构(如列表)替换 HashMap。
  • 总数约为 42000,因为每次迭代时中心都会发生变化,因此存储中心的大小不会有用。你还说拯救他们会更好吗?是 hashmap 还是 float 乘法比较耗时?
  • @Paniz 你说的中心是什么意思?
  • 我在 k-means 聚类中使用它,每个集群都有一个中心,并且每次迭代时中心都会更新。
【解决方案3】:

除了按照其他人的建议对向量进行预规范化并假设向量列表没有改变之外,将它们转换为数组对一次(在相似度函数之外)并按关键索引,例如:

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

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

【讨论】:

  • 我需要尽快在 hashmap 中查找
  • 所需的总时间实际上取决于你有多少向量n,以及你做了多少(k)相似度计算。如果kn 小很多(相似度计算很少,向量很多),上面的可能不会让你有太多收获。另一方面,如果kn 大得多(例如,您想计算所有可能对的相似性,即k = n^2),则预先计算某些数量(假设它是O(n))以节省时间在k 相似度计算中很可能会加快速度。我的建议是您事先将哈希图转换为线性数组。
  • 我的作品是你所说的第一个案例。时间是因为浮点计算还是使用hashmap?
  • 我现在才看到您想要进行 k-means 聚类,因此您会将每个 n 向量与每个 k 中心进行比较(k 这里与我的不同之前的评论)几次(所以你会做 x 次n * k 比较)。
  • 我的主要问题是它需要时间是因为浮点计算还是因为使用了 hashmap? (你所说的比较完全正确)
【解决方案4】:

可以查看项目simbasehttps://github.com/guokr/simbase,它是一个向量相似度nosql数据库。

Simbase 使用以下概念:

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

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

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

【讨论】:

    【解决方案5】:

    我至少可以清楚地看到一个地方,你只是在浪费 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 个 hashMap 上有 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有什么特别的原因吗?或者你可以用一些更简单的数组来做?

    【讨论】:

    • 计算余弦相似度实际上并不涉及cos函数。它是两个向量的归​​一化点积——这个名字源于这样一个事实,即这样的点积等于向量之间夹角的余弦。
    • @larsmans 如果某个值等于两个向量之间角度的余弦值,那么如果不计算该角度的余弦值,您应该如何找到它?无论如何,预先计算好的表格会让他摆脱浮点运算。
    • 对于向量 x 和 y,x·y/(||x||×||y||) = cos(θ) 其中 θ 是 x 和 y 之间的角度。那只是加法,乘法和倒数。详情请见Introduction to IR
    • ...和sqrt。抱歉,我忘了那个。
    • 我需要hashmap来存储单词的索引及其权重,我需要在恒定时间内查找
    猜你喜欢
    • 2015-05-24
    • 2017-07-07
    • 2018-04-11
    • 2017-02-03
    • 2017-10-19
    • 2021-05-19
    • 2011-05-21
    • 2020-03-16
    相关资源
    最近更新 更多