您能在此处(以列表或其他形式)显示向量,然后进行数学运算,让我们看看它是如何工作的吗?
12 回答
这里有两个非常短的文本来比较:
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
不过,我们对这些词本身并不感兴趣。我们只对这两个垂直的计数向量感兴趣。例如,每个文本中有两个“我”实例。我们将通过计算这两个向量的一个函数,即它们之间夹角的余弦来确定这两个文本彼此之间的接近程度。
这两个向量又是:
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)
wherecos(a)
对应于x
值和sin(a)
值y
,对于第一个向量等。唯一的问题是x
,y
等不完全是cos
andsin
值,因为这些值需要在单位圆上读取。这就是公式的分母起作用的地方:通过除以这些向量长度的乘积,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));
}
}
}
为简单起见,我正在减少向量 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 度。
这段 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))
使用@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)))
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));
}
让我尝试用 Python 代码和一些图形数学公式来解释这一点。
假设我们的代码中有两个非常短的文本:
texts = ["I am a boy", "I am a girl"]
我们想比较下面的查询文本,看看查询与上面的文本有多接近,使用快速余弦相似度分数:
query = ["I am a boy scout"]
我们应该如何计算余弦相似度分数?首先,让我们在 Python 中为这些文本构建一个 tfidf 矩阵:
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(texts)
接下来,让我们检查一下 tfidf 矩阵的值及其词汇表:
print(tfidf_matrix.toarray())
# output
array([[0.57973867, 0.81480247, 0. ],
[0.57973867, 0. , 0.81480247]])
在这里,我们得到一个 tfidf 矩阵,其 tfidf 值为 2 x 3,或 2 个文档/文本 x 3 个术语。这是我们的 tfidf 文档术语矩阵。让我们通过调用来看看这 3 个术语是什么vectorizer.vocabulary_
print(vectorizer.vocabulary_)
# output
{'am': 0, 'boy': 1, 'girl': 2}
这告诉我们 tfidf 矩阵中的 3 个术语是“am”、“boy”和“girl”。“am”在第 0 列,“boy”在第 1 列,“girl”在第 2 列。术语“I”和“a”已被矢量化器删除,因为它们是停用词。
现在我们有了 tfidf 矩阵,我们想将查询文本与我们的文本进行比较,看看我们的查询与我们的文本有多接近。为此,我们可以计算查询的余弦相似度分数与文本的 tfidf 矩阵。但首先,我们需要计算查询的 tfidf:
query = ["I am a boy scout"]
query_tfidf = vectorizer.transform([query])
print(query_tfidf.toarray())
#output
array([[0.57973867, 0.81480247, 0. ]])
在这里,我们计算了查询的 tfidf。我们的 query_tfidf 有一个 tfidf 值向量[0.57973867, 0.81480247, 0. ]
,我们将使用它来计算我们的余弦相似度乘法分数。如果我没记错的话,query_tfidf 值或vectorizer.transform([query])
值是通过从 tfidf_matrix 中选择与查询匹配最多的单词的行或文档来派生的。例如,tfidf_matrix 的第 1 行或文档/文本 1 与包含 "am" (0.57973867) 和 "boy" (0.81480247) 的查询文本的匹配词最多,因此 tfidf_matrix 的第 1 行[0.57973867, 0.81480247, 0. ]
值被选择为query_tfidf 的值。 (注意:如果有人可以帮助进一步解释这一点,那就太好了)
在计算出我们的 query_tfidf 之后,我们现在可以将我们的 query_tfidf 向量与我们的文本 tfidf_matrix 矩阵相乘或点积,以获得余弦相似度分数。
回想一下余弦相似度得分或公式等于以下内容:
cosine similarity score = (A . B) / ||A|| ||B||
这里,A = 我们的 query_tfidf 向量,B = 我们的 tfidf_matrix 的每一行
请注意: A 。B = A * B^T,或 A 点积 B = A 乘以 B Transpose。
知道公式后,让我们手动计算 query_tfidf 的余弦相似度分数,然后将我们的答案与 sklearn.metrics cosine_similarity 函数提供的值进行比较。让我们手动计算:
query_tfidf_arr = query_tfidf.toarray()
tfidf_matrix_arr = tfidf_matrix.toarray()
cosine_similarity_1 = np.dot(query_tfidf_arr, tfidf_matrix_arr[0].T) /
(np.linalg.norm(query_tfidf_arr) * np.linalg.norm(tfidf_matrix_arr[0]))
cosine_similarity_2 = np.dot(query_tfidf_arr, tfidf_matrix_arr[1].T) /
(np.linalg.norm(query_tfidf_arr) * np.linalg.norm(tfidf_matrix_arr[1]))
manual_cosine_similarities = [cosine_similarity_1[0], cosine_similarity_2[0]]
print(manual_cosine_similarities)
#output
[1.0, 0.33609692727625745]
我们手动计算的余弦相似度得分为[1.0, 0.33609692727625745]
. 让我们用 sklearn.metrics cosine_similarity 函数提供的答案值检查我们手动计算的余弦相似度分数:
from sklearn.metrics.pairwise import cosine_similarity
function_cosine_similarities = cosine_similarity(query_tfidf, tfidf_matrix)
print(function_cosine_similarities)
#output
array([[1.0 , 0.33609693]])
输出值都是一样的!手动计算的余弦相似度值与函数计算的余弦相似度值相同!
因此,这个简单的解释显示了如何计算余弦相似度值。希望这个解释对您有所帮助。
两个向量 A 和 B 存在于 2D 空间或 3D 空间中,这些向量之间的角度是 cos 相似度。
如果角度更大(可以达到最大 180 度),即 Cos 180=-1,最小角度为 0 度。cos 0 =1 意味着向量彼此对齐,因此向量相似。
cos 90=0(这足以得出向量 A 和 B 根本不相似的结论,并且由于距离不能为负,余弦值将从 0 到 1。因此,更多的角度意味着减少相似性(也可视化它说得通)
这是一个计算余弦相似度的简单 Python 代码:
import math
def dot_prod(v1, v2):
ret = 0
for i in range(len(v1)):
ret += v1[i] * v2[i]
return ret
def magnitude(v):
ret = 0
for i in v:
ret += i**2
return math.sqrt(ret)
def cos_sim(v1, v2):
return (dot_prod(v1, v2)) / (magnitude(v1) * magnitude(v2))