让我们将余弦相似度分成几个部分,看看它是如何工作的以及为什么它有效。
两个向量 - a
和 b
- 之间的余弦被定义为:
cos(a, b) = sum(a .* b) / (length(a) * length(b))
这里的.*
表示逐元素相乘。分母只是用于归一化,因此我们可以简单地将其称为L
。使用它,我们的函数变为:
cos(a, b) = sum(a .* b) / L
这可以进一步重写为:
cos(a, b) = (a[1]*b[1] + a[2]*b[2] + ... + a[k]*b[k]) / L =
= a[1]*b[1]/L + a[2]*b[2]/L + ... + a[k]*b[k]/L
让我们更加抽象化,用函数g(x, y)
替换x * y / L
(这里的L
是常数,因此我们不将其作为函数参数)。因此,我们的余弦函数变为:
cos(a, b) = g(a[1], b[1]) + g(a[2], b[2]) + ... + g(a[n], b[n])
那就是说,每对元素
(a[i], b[i])
都被
单独处理,结果就是所有处理的简单
总和。这对你的情况很有帮助,因为你不希望不同的对(不同的顶点)相互干扰:如果用户1只访问了顶点2而用户2只访问了顶点1,那么他们之间没有任何共同之处,他们之间的相似度应该为零。你实际上不喜欢的是如何计算个体对之间的相似度 - 即函数
g()
。
用余弦函数计算个体对之间的相似度如下:
g(x, y) = x * y / L
其中x
和y
表示用户在顶点上花费的时间。这里的主要问题是:乘法是否能很好地表示个体对之间的相似性?我认为不行。在某个顶点上花费90秒的用户应该与在那里花费70或110秒的用户更接近,但距离在那里花费1000或0秒的用户更远。乘法(即使通过L
进行归一化)在这里完全具有误导性。两个时间段相乘意味着什么?
好消息是,你将设计相似度函数。我们已经决定满足独立处理对(顶点),并且我们只想要个体相似度函数g(x, y)
来合理处理其参数。那么用于比较时间段的合理函数是什么?我认为减法是一个不错的选择:
g(x, y) = abs(x - y)
这不是相似度函数,而是距离函数 - 值越接近,
g()
的结果越小 - 但最终的思想是相同的,因此需要时我们可以交换它们。
我们可能还希望通过平方差异来增加大偏差的影响:
g(x, y) = (x - y)^2
嘿!我们刚刚重新发明了(均方误差)!现在我们可以继续使用MSE来计算距离,或者我们可以继续寻找好的g()
函数。
有时候我们可能不想增加差异,而是想要平滑差异。在这种情况下,我们可以使用log
:
g(x, y) = log(abs(x - y))
我们可以像这样对零使用特殊处理:
g(x, y) = sign(x)*sign(y)*abs(x - y)
或者我们可以通过反转差异,从距离到相似性回归:
g(x, y) = 1 / abs(x - y)
注意,最近的选项中我们没有使用归一化因子。实际上,您可以为每种情况想出一些好的规范化方法,或者只是省略它——规范化并不总是必要或好的。例如,在余弦相似度公式中,如果您将规范化常数
L=length(a) * length(b)
更改为
L=1
,则会得到不同但仍合理的结果。例如:
cos([90, 90, 90]) == cos(1000, 1000, 1000) # measuring angle only
cos_no_norm([90, 90, 90]) < cos_no_norm([1000, 1000, 1000]) # measuring both - angle and magnitude
总结这个又长又无聊的故事,我建议重新编写余弦相似度/距离,使用两个向量中变量之间的某种差异。