Answers:
这里有两个非常简短的文本可以比较:
Julie loves me more than Linda loves me
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度的角度,该角度与零或完美一致有些“距离”。
我猜想您更想了解“ 为什么 ”余弦相似性起作用(为什么它提供相似性的良好指示),而不是“ 如何 ”计算它(用于计算的特定操作)。如果您对后者感兴趣,请参阅本文中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
值,所述第一向量,等等。唯一的问题是,x
,y
等不完全cos
和sin
值,这些值需要被在单位圆上读出。这就是公式的分母所在的位置:通过除以这些向量的长度的乘积,x
和y
坐标将被标准化。
这是我在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));
}
}
}
这段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))
这是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]])
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));
}
}
简单的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));
}