有人可以用非常简单的图形方式给出余弦相似度的例子吗?


201

维基百科上的余弦相似度文章

您可以在此处(列表或其他形式)显示矢量,然后进行数学运算,然后让我们看看它是如何工作的吗?

我是初学者。


1
尝试获取Widdows的Geometry and Meaning的副本(press.uchicago.edu/presssite/…),我读了一段时间,希望几年前能收到它,并且是很棒的介绍性文字。
内森·豪威尔

Answers:


463

这里有两个非常简短的文本可以比较:

  1. Julie loves me more than Linda loves me

  2. Jane likes me more than Julie loves me

我们想知道这些文本有多么相似,仅就字数而言(并忽略字序)。我们首先列出两个文本中的单词:

me Julie loves Linda than more likes Jane

现在我们计算这些单词在每个文本中出现的次数:

   me   2   2
 Jane   0   1
Julie   1   1
Linda   1   0
likes   0   1
loves   2   1
 more   1   1
 than   1   1

但是我们对单词本身不感兴趣。我们只对计数的这两个垂直向量感兴趣。例如,每个文本中有两个“ me”实例。我们将通过计算这两个向量的一个函数,即它们之间的夹角的余弦值,来确定这两个文本彼此之间的接近程度。

这两个向量分别是:

a: [2, 0, 1, 1, 0, 2, 1, 1]

b: [2, 1, 1, 0, 1, 1, 1, 1]

它们之间夹角的余弦约为0.822。

这些向量是8维的。显然,使用余弦相似度的优点是,它将人类无法想象的问题转换为可以解决的问题。在这种情况下,您可以将其视为大约35度的角度,该角度与零或完美一致有些“距离”。


12
这正是我想要的。究竟。这被认为是“向量空间模型”的最简单形式吗?
TIMEX

2
我真的很高兴,这对您有用,Alex。很抱歉延迟回复。我有一段时间没有访问StackOverflow了。实际上,这是“内部产品空间”的一个示例。关于维基百科有一个基本的讨论。
比尔·贝尔

1
有什么方法可以规范文档长度吗?
SINθ

1
您必须使用长度归一化,并且在此之前,请尝试对所有项向量使用对数频率加权。如果您已经在处理归一化向量,那么它就是AB的点积
Ali Gajani 2014年

4
使用长度归一化和TF-IDF的更详细的示例:site.uottawa.ca/~diana/csi4107/cosine_tf_idf_example.pdf
Mike B.

121

我猜想您更想了解“ 为什么 ”余弦相似性起作用(为什么它提供相似性的良好指示),而不是“ 如何 ”计算它(用于计算的特定操作)。如果您对后者感兴趣,请参阅本文中Daniel指示的参考以及相关的SO Question

为了解释如何以及什至是为什么,首先,它有助于简化问题并仅在两个维度上起作用。一旦以2D形式获得,就更容易在三个维度上进行思考,当然也很难在更多的维度上进行想象,但是到那时,我们可以使用线性代数进行数值计算,并帮助我们以即使我们无法绘制这些线/向量/“平面” /“球体”的n个维度。

因此,在两个维度上:关于文本相似性,这意味着我们将专注于两个不同的术语,例如单词“伦敦”和“巴黎”,并且我们将计算每个单词在每个单词中被发现了多少次。我们希望比较的两个文件。对于每个文档,这为我们提供了xy平面中的一个点。例如,如果Doc1有一次巴黎,而伦敦有四次,则(1,4)处的点将显示此文档(关于文档的这种小型评估)。或者,就矢量而言,此Doc1文档将是从原点到点(1,4)的箭头。考虑到此图像,让我们考虑两个文档相似的含义以及它与向量的关系。

非常相似的文件(同样是关于这组有限的维度)将具有相同数量的巴黎参考编号,以及具有相同数量的伦敦参考编号,或者它们可能具有相同的比例。一个文档Doc2,其中有2篇引用到巴黎,8篇引用到了伦敦,也将非常相似,只是文本较长或以某种方式更重复城市名称,但比例相同。也许这两个文件都是伦敦的指南,只是提及巴黎(以及这座城市有多酷;-)而已!

现在,不太相似的文件可能还包括对这两个城市的引用,但比例不同。也许Doc2只会引用一次巴黎,而只会引用七次伦敦。

回到我们的xy平面,如果我们绘制这些假设文档,我们会看到,当它们非常相似时,它们的向量会重叠(尽管某些向量可能会更长),并且由于它们的共同点越来越少,这些向量开始发生分歧,使它们之间有更大的角度。

通过测量向量之间的角度,我们可以很好地了解它们的相似性,并使事情变得更加容易,通过获取该角度的余弦值,我们可以得到一个很好的0到1或-1到1的值,该值表示这种相似性取决于我们所考虑的方式和方式。角度越小,余弦值越大(接近1),相似度也越高。

在极端情况下,如果Doc1仅引用巴黎,而Doc2仅引用伦敦,则这些文档绝对没有共同点。Doc1的向量在x轴上,Doc2在y轴上,夹角为90度,余弦为0。在这种情况下,我们可以说这些文档彼此正交。

添加维度
通过直观的相似感(以小角度(或大余弦)表示),我们现在可以想象3维的事物,例如将“阿姆斯特丹”一词混入其中,并很好地可视化包含两个文档的文档对每个引用的引用都有一个朝着特定方向的向量,我们可以看到该方向如何与引用巴黎和伦敦的文档分别进行三遍比较,而不是阿姆斯特丹等。如上所述,我们可以尝试并想象这种幻想可容纳10或100个城市。很难绘制,但是易于概念化。

我只想对公式本身几句话就结束了。正如我已经说过的,其他参考文献提供了有关计算的良好信息。

首先是二维。两个向量之间的角度的余弦的公式由三角差(角度a和角度b之间)得出:

cos(a - b) = (cos(a) * cos(b)) + (sin (a) * sin(b))

该公式看起来与点积公式非常相似:

Vect1 . Vect2 =  (x1 * x2) + (y1 * y2)

其中cos(a)对应于x值和sin(a)所述y值,所述第一向量,等等。唯一的问题是,xy等不完全cossin值,这些值需要被在单位圆上读出。这就是公式的分母所在的位置:通过除以这些向量的长度的乘积,xy坐标将被标准化。


25

这是我在C#中的实现。

using System;

namespace CosineSimilarity
{
    class Program
    {
        static void Main()
        {
            int[] vecA = {1, 2, 3, 4, 5};
            int[] vecB = {6, 7, 7, 9, 10};

            var cosSimilarity = CalculateCosineSimilarity(vecA, vecB);

            Console.WriteLine(cosSimilarity);
            Console.Read();
        }

        private static double CalculateCosineSimilarity(int[] vecA, int[] vecB)
        {
            var dotProduct = DotProduct(vecA, vecB);
            var magnitudeOfA = Magnitude(vecA);
            var magnitudeOfB = Magnitude(vecB);

            return dotProduct/(magnitudeOfA*magnitudeOfB);
        }

        private static double DotProduct(int[] vecA, int[] vecB)
        {
            // I'm not validating inputs here for simplicity.            
            double dotProduct = 0;
            for (var i = 0; i < vecA.Length; i++)
            {
                dotProduct += (vecA[i] * vecB[i]);
            }

            return dotProduct;
        }

        // Magnitude of the vector is the square root of the dot product of the vector with itself.
        private static double Magnitude(int[] vector)
        {
            return Math.Sqrt(DotProduct(vector, vector));
        }
    }
}

太棒了,谢谢,我喜欢您如何解释Magnitude =)
liminal18

太好了,但是如果我们使用文件或字符串呢?
塔拉(Talha)

21

为简单起见,我减小向量a和b:

Let :
    a : [1, 1, 0]
    b : [1, 0, 1]

然后余弦相似度(Theta):

 (Theta) = (1*1 + 1*0 + 0*1)/sqrt((1^2 + 1^2))* sqrt((1^2 + 1^2)) = 1/2 = 0.5

那么cos 0.5的倒数是60度。


18

这段Python代码是我快速而肮脏的实现算法的尝试:

import math
from collections import Counter

def build_vector(iterable1, iterable2):
    counter1 = Counter(iterable1)
    counter2 = Counter(iterable2)
    all_items = set(counter1.keys()).union(set(counter2.keys()))
    vector1 = [counter1[k] for k in all_items]
    vector2 = [counter2[k] for k in all_items]
    return vector1, vector2

def cosim(v1, v2):
    dot_product = sum(n1 * n2 for n1, n2 in zip(v1, v2) )
    magnitude1 = math.sqrt(sum(n ** 2 for n in v1))
    magnitude2 = math.sqrt(sum(n ** 2 for n in v2))
    return dot_product / (magnitude1 * magnitude2)


l1 = "Julie loves me more than Linda loves me".split()
l2 = "Jane likes me more than Julie loves me or".split()


v1, v2 = build_vector(l1, l2)
print(cosim(v1, v2))

您能否解释为什么在行“ all_items = set(counter1.keys())。union(set(counter2.keys()))”中使用set。
Ghos3t

@ Ghos3t,即从两个文档中获取不同单词的列表
Jobs

7

使用@Bill Bell示例,在[R]中有两种方法

a = c(2,1,0,2,0,1,1,1)

b = c(2,1,1,1,1,0,1,1)

d = (a %*% b) / (sqrt(sum(a^2)) * sqrt(sum(b^2)))

或利用crossprod()方法的性能...

e = crossprod(a, b) / (sqrt(crossprod(a, a)) * sqrt(crossprod(b, b)))

5

这是Python实现余弦相似度的简单代码。

from scipy import linalg, mat, dot
import numpy as np

In [12]: matrix = mat( [[2, 1, 0, 2, 0, 1, 1, 1],[2, 1, 1, 1, 1, 0, 1, 1]] )

In [13]: matrix
Out[13]: 
matrix([[2, 1, 0, 2, 0, 1, 1, 1],
        [2, 1, 1, 1, 1, 0, 1, 1]])
In [14]: dot(matrix[0],matrix[1].T)/np.linalg.norm(matrix[0])/np.linalg.norm(matrix[1])
Out[14]: matrix([[ 0.82158384]])

3
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * 
* @author Xiao Ma
* mail : 409791952@qq.com
*
*/
  public class SimilarityUtil {

public static double consineTextSimilarity(String[] left, String[] right) {
    Map<String, Integer> leftWordCountMap = new HashMap<String, Integer>();
    Map<String, Integer> rightWordCountMap = new HashMap<String, Integer>();
    Set<String> uniqueSet = new HashSet<String>();
    Integer temp = null;
    for (String leftWord : left) {
        temp = leftWordCountMap.get(leftWord);
        if (temp == null) {
            leftWordCountMap.put(leftWord, 1);
            uniqueSet.add(leftWord);
        } else {
            leftWordCountMap.put(leftWord, temp + 1);
        }
    }
    for (String rightWord : right) {
        temp = rightWordCountMap.get(rightWord);
        if (temp == null) {
            rightWordCountMap.put(rightWord, 1);
            uniqueSet.add(rightWord);
        } else {
            rightWordCountMap.put(rightWord, temp + 1);
        }
    }
    int[] leftVector = new int[uniqueSet.size()];
    int[] rightVector = new int[uniqueSet.size()];
    int index = 0;
    Integer tempCount = 0;
    for (String uniqueWord : uniqueSet) {
        tempCount = leftWordCountMap.get(uniqueWord);
        leftVector[index] = tempCount == null ? 0 : tempCount;
        tempCount = rightWordCountMap.get(uniqueWord);
        rightVector[index] = tempCount == null ? 0 : tempCount;
        index++;
    }
    return consineVectorSimilarity(leftVector, rightVector);
}

/**
 * The resulting similarity ranges from −1 meaning exactly opposite, to 1
 * meaning exactly the same, with 0 usually indicating independence, and
 * in-between values indicating intermediate similarity or dissimilarity.
 * 
 * For text matching, the attribute vectors A and B are usually the term
 * frequency vectors of the documents. The cosine similarity can be seen as
 * a method of normalizing document length during comparison.
 * 
 * In the case of information retrieval, the cosine similarity of two
 * documents will range from 0 to 1, since the term frequencies (tf-idf
 * weights) cannot be negative. The angle between two term frequency vectors
 * cannot be greater than 90°.
 * 
 * @param leftVector
 * @param rightVector
 * @return
 */
private static double consineVectorSimilarity(int[] leftVector,
        int[] rightVector) {
    if (leftVector.length != rightVector.length)
        return 1;
    double dotProduct = 0;
    double leftNorm = 0;
    double rightNorm = 0;
    for (int i = 0; i < leftVector.length; i++) {
        dotProduct += leftVector[i] * rightVector[i];
        leftNorm += leftVector[i] * leftVector[i];
        rightNorm += rightVector[i] * rightVector[i];
    }

    double result = dotProduct
            / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
    return result;
}

public static void main(String[] args) {
    String left[] = { "Julie", "loves", "me", "more", "than", "Linda",
            "loves", "me" };
    String right[] = { "Jane", "likes", "me", "more", "than", "Julie",
            "loves", "me" };
    System.out.println(consineTextSimilarity(left,right));
}
}

3

简单的JAVA代码来计算余弦相似度

/**
   * Method to calculate cosine similarity of vectors
   * 1 - exactly similar (angle between them is 0)
   * 0 - orthogonal vectors (angle between them is 90)
   * @param vector1 - vector in the form [a1, a2, a3, ..... an]
   * @param vector2 - vector in the form [b1, b2, b3, ..... bn]
   * @return - the cosine similarity of vectors (ranges from 0 to 1)
   */
  private double cosineSimilarity(List<Double> vector1, List<Double> vector2) {

    double dotProduct = 0.0;
    double normA = 0.0;
    double normB = 0.0;
    for (int i = 0; i < vector1.size(); i++) {
      dotProduct += vector1.get(i) * vector2.get(i);
      normA += Math.pow(vector1.get(i), 2);
      normB += Math.pow(vector2.get(i), 2);
    }
    return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
  }

1
这不是“简单的图形方式”,而仅仅是代码。尽管其他人也犯了同样的错误:/
Skrylar '18

-1

两个向量A和B存在于2D空间或3D空间中,这些向量之间的角度为cos相似度。

如果角度更大(可以达到最大180度),则Cos 180 = -1,最小角度为0度。cos 0 = 1表示向量彼此对齐,因此向量相似。

cos 90 = 0(足以得出向量A和B根本不相似并且距离不能为负,因此余弦值将介于0到1之间的信息。说得通)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.