数学翻译
让我(尝试)将你所拥有的东西翻译成一个更数学的公式。从那里应该更容易。
OriginalTropes
是某个游戏的转义数,称之为A
。然后NewTropes
是其他游戏的比喻,称之为B
。然后Similarities
就是A
和的交集B
。你的公式是:
|Intersect(A, B)| / ((|A| - |Intersect(A, B)|) + (|B| - |Intersect(A, B)|) + |Intersect(A, B)|)
简化,我们有:
|Intersect(A, B)| / (|A| + |B| - |Intersect(A, B)|)
换句话说,您说的相似度是共同项目数除以项目总数减去共同项目数的比率。
现在让我们来看几个特殊情况。拿A = B
。然后我们有:
|Intersect(A, B)| = |A| = |B|
. 你的公式是:
|A| / (|A| + |A| - |A|) = 1
限制
现在让我们说集合A
和B
大小相等。但是,他们只有一半的共同点。换句话说,
|A| = |B| = 2 |Intersect(A, B)|
你的相似度得分是:
1/2 |A| / (2|A| - 1/2|A|) = 1/3
理想情况下,这应该是1/2
,而不是1/3
。如果您考虑任何集合 where|A| = |B| = n
和 where |Intersect(A, B)| = n * p
for ,您会得到类似的结果0 <= p <= 1
。
一般来说,对于上述形式的集合,您的相似性算法最终会低估两组之间的相似性。这看起来像下图中的紫色曲线。蓝色曲线是余弦相似度所给出的。因此,如果 50% 是常见的并且它们的大小相同,则这两个集合的相似性为0.5
. 同样,如果它们有 90% 的共同点,那么它的相似性为0.9
。
余弦相似度
您可能希望的是类似于两组之间的角度。考虑整个元素集,Intersect(A, B)
并定义N = |Intersect(A, B)|
。让a
和b
是 和 的N
维度表示A
,B
其中每个元素1
如果存在于原始集合中或0
不存在,则具有值。
然后你使用角度的余弦为:
Cos(theta) = Dot(a, b) / (||a|| * ||b||)
请注意,符号||a||
指的是欧几里得长度,而不是集合的大小。这可能比您以前使用的具有更好的属性。
例子
这是一个例子。比方说:
A = { "Big Swords", "Male Hero", "No Cars" }
B = { "Male Hero", "Trains", "No Dragons" }
然后完整的不同集Union(A, B)
被给出为:
Union(A, B) = { "Big Swords", "Male Hero", "No Cars", "Trains", "No Dragons" }
这意味着N = |Union(A, B) = 5
. 棘手的一方变成了如何适当地索引这些中的每一个。您实际上可以使用字典和计数器来索引元素。我会把这个留给你试试。现在,我们将使用Union(A, B)
. 然后a
和b
给出为:
a = { 1, 1, 1, 0, 0 }
b = { 0, 1, 0, 1, 1 ]
在这一点上,它变成了标准数学:
Dot(a, b) = 1
|a| = sqrt(3)
|b| = sqrt(3)
Similarity = 1 / 3
示例实现
public double Compare(IEnumerable<String> A, IEnumerable<String> B)
{
// Form the intersection between A and B
var C = A.Intersect(B);
// a and b are N (C.Length) dimensional bi-valued (0 or 1) vectors
var a = new List<int>(C.Length);
var b = new List<int>(C.Length);
var map = new Dictionary<String, int>();
// Map from the original key to an index in the intersection
for (int i = 0; i < C.Length; i++)
{
var key = C[i];
map[key] = i;
}
// Set the 1's in the N-dimensional representation of A
foreach (var element in A)
{
var i = map[element];
a[i] = 1;
}
// And do the same for B
foreach (var element in B)
{
var i = map[element];
b[i] = 1;
}
int dot = 0;
// Easy part :) Standard vector dot product
for (int i = 0; i < C.Length; i++)
dot += a[i] * b[i];
// It suffices to take the length because the euclidean norm
// of a and b are, respectively, the length of A and B
return dot / Math.Sqrt((double) A.Length * B.Length);
}