likes
comments
collection
share

「余弦相似度」工作原理使用搜索引擎搜索“向量比较方法”,我们会发现余弦相似度几乎成为了标准答案。近期,我经常处理 LLM

作者站长头像
站长
· 阅读数 30

使用搜索引擎搜索“向量比较方法”,我们会发现余弦相似度几乎成为了标准答案。近期,我经常处理 LLM 嵌入相关的向量工作,衡量两个嵌入之间的相似程度几乎已经成为我工作流的关键步骤。但余弦相似度的计算过程究竟是怎样的呢?

我之前一直都是机械地复制、粘贴计算余弦相似度的代码,却未曾深究其原理。为了彻底弄懂它,我想解答以下几个问题:

  • 余弦相似度的计算公式是怎样的?

  • 公式的各个部分的含义是什么?

  • 为什么这种方法用于比较 LLM 嵌入时非常有效?

01 大家谈论的“向量”究竟是什么?

在深入探讨余弦相似度之前,我们先来明确一下“向量”的定义。在本文中,“嵌入”和“向量”这两个词是可以互换的。以下是我之前的一篇文章中的描述:

“嵌入”这个概念源自大语言模型(LLM)技术,它是 ChatGPT 等工具的核心技术。其核心思想是将一段文本(比如一篇博客文章)转换成一个由数字组成的数组,即向量。这个向量被称为“嵌入”,它代表了文本的“含义(meaning)”。

简单来说 🍳,嵌入就是向量,向量就是一组数字。如果你也像我一样,习惯将代码转换成 JavaScript,那么我们所说的向量实际上就是数组。比如,const vector = [0.1, 0.2, 0.3, 0.4, 0.5]; 就是一个向量实例。

在 LLM 中创建的嵌入向量是由大量数字组成的数组。以 OpenAI 的 ada-002 模型为例,其生成的嵌入包含了 1,536 个数字,从数学角度来看,该嵌入可以描述为一个存在于 1,536 维空间中的向量。虽然在 LLM 嵌入的领域,二维向量(也就是只有两个数字的数组)的实用性较低,但它们的原理是相同的,我们可以通过它们来解释余弦相似度的计算过程。

02 为什么在比较向量时余弦相似度很有用?

在处理嵌入时,比较两个向量之间的相似程度是关键工作。计算余弦相似度正是进行这种比较的首选方法。

假设我们处于一个超简化的二维空间,此处的向量都仅包含两个数值。那么,两个向量之间的夹角就是它们在坐标系中所形成的两条直线之间的夹角。这些两条直线从坐标原点(0, 0)延伸至向量的终点,将向量的两个数值视为 x 轴和 y 轴上的坐标。

需要注意的是:如果向量包含三个数值,则是在三维空间中进行操作。四个数值对应四维空间,以此类推。不论是在多少维度的空间中,余弦相似度的基本原理都是一致的。

「余弦相似度」工作原理使用搜索引擎搜索“向量比较方法”,我们会发现余弦相似度几乎成为了标准答案。近期,我经常处理 LLM

θ(theta)值代表两个向量之间的夹角。要使一个向量与另一个向量对齐,需要旋转的角度即为 θ。这个角度的余弦值(cos(θ))就是我们所求的余弦相似度,这是一个范围在-1 到 1 之间的数值。

当两个向量的方向完全一致时,余弦相似度达到最大值 1。如果它们相互垂直,相似度则为 0。而如果它们方向完全相反,相似度会降至-1。

余弦相似度计算公式不考虑向量的长度

在计算余弦相似度时,我们只关注向量之间的夹角,而不考虑它们的长度。因此,即使两个向量的长度不同,只要它们同向,余弦相似度依旧能够达到 1。

03 深挖余弦相似度公式

余弦相似度公式如下所示:

「余弦相似度」工作原理使用搜索引擎搜索“向量比较方法”,我们会发现余弦相似度几乎成为了标准答案。近期,我经常处理 LLM

其中:

  • A⋅B 表示向量 A 和向量 B 的点积。
  • ||A|| 代表向量 A 的长度(模)。
  • ||B|| 代表向量 B 的长度(模)。
  • θ 是两个向量之间的夹角。

这个公式看起来很简单,但要将其转换成 JavaScript 代码,还需要解答几个问题:

什么是两个向量的“点积”?

“点积”实际上就是“对应元素相乘后再求和”。两个向量的点积就是它们各自对应元素相乘后的总和。

「余弦相似度」工作原理使用搜索引擎搜索“向量比较方法”,我们会发现余弦相似度几乎成为了标准答案。近期,我经常处理 LLM

例如,向量 [1, 2, 3] 和 [4, 5, 6] 的点积为 1 × 4 + 2 × 5 + 3 × 6 = 4 + 10 + 18 = 32。给定向量 a 和 b,可以用 JS 这样计算点积:

const dotProduct = a.reduce((acc, cur, i) => acc + cur * b[i], 0);

向量的“模”是什么?

简单来说,向量的“模”就是它的长度。在数学术语中,它是向量元素平方和的平方根。

「余弦相似度」工作原理使用搜索引擎搜索“向量比较方法”,我们会发现余弦相似度几乎成为了标准答案。近期,我经常处理 LLM

例如,向量 [1, 2, 3] 的“模”为 √(12 + 22 + 32) = √(1 + 4 + 9) = √14。给定一个向量 a,其“模”可以用 JS 这样计算:

const magnitude = Math.sqrt(a.reduce((acc, cur) => acc + cur ** 2, 0));

但是,我们不是应该忽略向量的“模”吗?

在计算向量的“模”时,我们可能会感到有些反常,因为我们本想要忽略它们的“模”。然而,通过在公式中加入向量“模”的乘积,我们可以对点积进行归一化处理,这样就能确保相似度度量不受向量“模”的影响。

使用 JavaScript 编写的完整余弦相似度计算函数

现在,我们已经逐一理解了该公式的各组成部分,接下来我们可以将它们整合起来,编写一个 JavaScript 函数计算任意两个向量之间的余弦相似度。

「余弦相似度」工作原理使用搜索引擎搜索“向量比较方法”,我们会发现余弦相似度几乎成为了标准答案。近期,我经常处理 LLM

export const cosineSimilarity = (a, b) => {
    const dotProduct = a.reduce((acc, cur, i) => acc + cur * b[i], 0);

    const magnitudeA = Math.sqrt(a.reduce((acc, cur) => acc + cur ** 2, 0));
    const magnitudeB = Math.sqrt(b.reduce((acc, cur) => acc + cur ** 2, 0));

    const magnitudeProduct = magnitudeA * magnitudeB;
    if (magnitudeProduct === 0) return 0; // Prevent division by zero

    const similarity = dotProduct / magnitudeProduct;
    return similarity;
};

为什么该函数没有调用 Math.cos() ?

我们一直在讨论“余弦”的概念,但我们的 cosineSimilarity 函数却并未调用 JavaScript 自带的 Math.cos()函数,实在令人惊讶。这是因为该函数是直接计算出夹角的余弦值,而无需先求出夹角本身。如果使用 Math.cos(),我们就需要先用诸如 Math.acos()之类的反三角函数来求出角度,这不仅没有必要,而且计算成本更高。

04 那么,余弦相似度在比较 LLM 嵌入时究竟有何优势?

余弦相似度之所以成为比较 LLM 嵌入的首选方法,是有其深刻原因的。嵌入的核心在于其方向而非长度。如果两个嵌入向量指向同一个方向,那么在模型看来,它们就具有相同的“含义”。

由于余弦相似度衡量的是向量间的方向相似程度,而忽略它们的长度,因此它成为了比较嵌入向量的理想选择。此外,它的计算成本较低,这也是一个加分项。正如前文所展示的那样,只需几行 JavaScript 代码就能实现这一功能。

嵌入向量的强大之处在于它们能够处理多维数据。这些向量可能很长,向量中数字之间的关联也可能非常复杂。但无论操作的是多少维度的数据,余弦相似度的基本原理都不会改变。它是一种简洁而高效的向量比较方法。

转载自:https://juejin.cn/post/7411362233941327898
评论
请登录