点击上方蓝字关注我们
一、什么是语义搜索
语义搜索 提供基于文本段落的上下文含义的搜索功能。它解决了替代方法(关键字搜索)的局限性。
例如我们来查询:“吃饭的地方”。使用语义搜索模型就能够自动将其与“餐馆”联系起来,因为它们的含义相似。而通过关键字搜索却无法做到这一点,因为搜索结果将局限于“地点”、“去”和“吃”等关键字。
这就像是与搜索引擎进行一场对话,它不仅理解你询问的内容,还理解你为什么要询问。这正是自然语言处理、人工智能和机器学习的魅力所在。它们共同努力理解用户的查询、查询的上下文以及用户的意图。语义搜索研究单词之间的关系或单词的含义,以提供比传统关键词搜索更准确、更相关的搜索结果。
二、什么是关键词搜索
在语义搜索出现之前,最流行的搜索方式是 关键词搜索
。假设你有一组很多句子的列表,这些句子是搜索引擎的响应内容。当你提出一个问题(查询)时,关键词搜索会查找与查询 具有最大词汇共现数的句子 (响应)。例如,以下面的查询和响应为例:
查询:北京天安门在哪里?
使用关键词搜索,你可以注意到响应与查询具有以下词汇共现数:
1、 天安门在北京 。(共有6个词)
2、 北京 有很多好吃的小吃。(共有1个词)
3、我喜欢去 北京 旅游。(共有1个词)
4、 北京 是中国的首都。(共有1个词)
在这种情况下,获胜的响应是编号 1,“天安门在北京”。比较幸运,这是正确的答案。然而,并不一定每次都能答对。假如还有下面这个答案:
1、 在北京天安门 是一个历史悠久的建筑?
这个答案与查询有 6 个词汇共现,因此如果它在响应列表中,它将会获胜。但是这却不是正确的响应。
那么这种情况一般会如何来解决呢?我们可以通过删除“在”、“的”、“是”等停用词来改进关键字搜索。我们还可以使用 TF-IDF
等方法来区分相关词和不相关词。然而,正如您可能想象的那样,总会有这样的情况,由于语言、同义词和其他障碍的模糊性,关键字搜索将无法找到最佳响应。 语义搜索 在这种场景将派上了用场。
简单的理解,语义搜索的工作原理如下:
- 首先使用文本嵌入将单词转换为向量。
- 然后使用相似度算法在响应结果中查找与查询对应的向量最相似的向量。
- 最后输出与这个最相似的向量对应的响应结果。
接下来,我们将构建一个简单的语义搜索引擎。语义搜索的应用不仅仅限于构建网络搜索引擎。他们可以为内部文档或记录提供私人搜索引擎。它可用于增强如 StackOverflow 的“类似问题”等功能。
三、如何使用文本嵌入进行搜索
Embedding 是一种为每个句子(每个文本片段,可以是一个单词或一篇完整文章)分配向量的方法,该向量是一个数字列表。本文中使用的Cohere嵌入模型返回长度为4096的向量。这是一个包含4096个数字的列表(而其他的Cohere嵌入模型,如多语言模型,则返回长度较小的向量,例如768)。嵌入的一个非常重要的特性是相似的文本片段会被分配到相似的数字列表。例如,“你好,你好吗?”和“嗨,最近怎么样?”这两个句子将被分配到类似的数字列表中,而“明天是星期五”则被分配到与前两个句子完全不同的数字列表。
下图展示了一个嵌入示例。为了便于理解,在这个示例中每个句子都被赋予长度为2(即包含两个数字) 的向量。这些数字在右侧图表中以坐标形式绘制出来。例如,“世界杯在卡塔尔”这个句子被赋予向量(4, 2),因此它在坐标点 (4, 2) 处绘制出来。
在此图像中,所有句子都作为平面上的点来定位。从视觉上,您可以确定查询(由奖杯表示)与响应“世界杯在卡塔尔”(由足球表示)最接近。其他查询(由云、熊和苹果表示)则远得多。因此,语义搜索将返回“世界杯在卡塔尔”,这是正确的响应。
接下来我们使用Cohere的文本嵌入来对这8个句子进行编码。这将为我们提供8个长度为4096的向量,但是我们可以使用一些降维算法将其长度降低到2。就像之前一样,这意味着我们可以使用2个坐标在平面上绘制句子。下面是情节。
请注意,每个查询都最接近其相应的响应。这意味着如果我们使用语义搜索来搜索这4个查询中每一个查询的响应,我们将得到正确的响应。
这里有个需要注意的地方。在上面的示例中,我们使用了欧几里得距离,它只是平面上的距离度量方式。然而,在比较文本片段时,并不是最理想的方法。相似性度量是最常用且能够给出最佳结果的方法。
四、使用相似性来查找最佳文档
相似性是判断两个文本片段是否相似或不同的一种方式,它使用了文本嵌入。 在语义搜索中常用的两种相似性度量方法:
- 点积相似性
- 余弦相似性
现在,让我们将它们合并为一个概念,并假设相似性是分配给每对文档的一个数字,并具有以下特征:
- 一段文本与自身的相似性非常高。
- 两个非常相似的文本片段之间的相似性较高。
- 两个不同的文本片段之间的相似性较低。
这里我们将使用余弦相似度,其额外属性是返回值介于0和1之间。一段文本与自身的相似度始终为1,并且最低值为0(当两个文本片段完全不同时)。
现在,要进行语义搜索,只需要计算查询与每对句子之间的相似度,并返回具有最高相似度的句子。下面是上述数据集中8个句子之间余弦相似度绘制出来的。
在这个图表中,右侧给出了刻度尺。请注意以下特点:
- 对角线上都是1(因为每个句子与自身的相似度为1)。
- 每个句子与其对应响应之间的相似度约为0.7。
- 任何其他一对句子之间的相似度较低。
这意味着,如果你搜索例如“什么是苹果?”这个查询的答案,语义搜索模型将查看表中倒数第二行,并注意到最接近的句子是“什么是苹果?”(相似度为1)和“苹果是水果”(相似度约为0.7)。系统会从列表中排除相同的查询,因为它不想回复相同的问题。因此,获胜响应将是“苹果是水果”,这也是正确的回答。
在这里还有一个我们没有提到但非常重要的隐藏算法:最近邻算法。该算法找到数据集中某一点最近邻。在本例中,该算法找到了句子“什么是苹果?” 的最近邻,并返回了句子“苹果是水果”。
五、什么是最近邻算法
最近邻算法 是一种常用的分类算法。它的基本思想是根据数据点之间的距离来确定它们的相似性。对于一个给定的数据点,最近邻算法会找出与其最接近的k个邻居,并将该数据点分配给邻居中出现最频繁的类别。
举个例子,如果我们要对一段文本进行情感分类,将其分类为积极或消极,我们可以使用最近邻算法。假设我们选择k=3,那么算法会找出与该段文本最接近的三个邻居(在某种表示下),然后观察这三个邻居中类别出现最频繁的是积极还是消极。根据这个结果,我们可以将该段文本分配给相应的类别。
需要注意的是,最近邻算法在计算速度上可能会比较慢。因为要找到一个数据点的邻居,需要计算该点与数据集中所有其他点之间的距离,并找出距离最近的几个点。这个过程可能会消耗较多的计算资源。如下图所示,为了找到与“Where is the world cup?”这句话最接近的邻居,我们必须计算 8 个距离,每个数据点一个。
总之,最近邻算法是一种简单而常用的分类算法,它通过计算数据点之间的距离来确定它们的相似性,并将数据点分配给邻居中出现最频繁的类别。尽管计算速度可能较慢,但在某些情况下仍然可以提供良好的分类结果。
然而,在处理大量档案时,我们可以通过稍微调整算法以使其成为近似 k 最近邻来优化性能。特别是在搜索方面,有一些改进可以大大加快这个过程。以下是其中两个:
-
倒排文件索引(IVD) :包括对相似文档进行聚类,然后在最接近查询的聚类中进行搜索。
-
分层可导航小世界(HNSW) :包括从几个点开始,然后在那里进行搜索。然后在每次迭代中添加更多点,并在每个新空间中进行搜索。
六、基于 Cohere AI 实现语义搜索
6.1、下载依赖包
# 安装Cohere获取嵌入,使用Umap将嵌入降维到2维
# 使用Altair进行可视化,使用Annoy进行近似最近邻搜索
!pip install cohere umap-learn altair annoy datasets tqdm
导入必要的库
import cohere
import numpy as np
import re
import pandas as pd
from tqdm import tqdm
from datasets import load_dataset
import umap
import altair as alt
from sklearn.metrics.pairwise import cosine_similarity
from annoy import AnnoyIndex
import warnings
warnings.filterwarnings('ignore')
pd.set_option('display.max\_colwidth', None)
6.2、获取 Cohere API Key
我们需要在 Cohere 的官方注册账号来获取你的 Cohere API 密钥。
# 在此处粘贴您的API密钥。请记住不要公开分享。
api_key = ''
co = cohere.Client(api_key)
6.3、获取问题分类数据集
我们这里将使用trec数据集来演示,trec数据集由问题及其类别组成。
# 获取数据集
dataset = load_dataset("trec", split="train")
# 将其导入到pandas的dataframe中,只取前1000行
df = pd.DataFrame(dataset)[:1000]
# 预览数据以确保已正确加载
df.head(10)
6.4、文档嵌入
现在我们可以使用Cohere对问题文本进行嵌入。我们将使用Cohere库的embed函数对问题进行嵌入。生成一千个这样长度的嵌入大约需要15秒钟。
# 获取嵌入
embeds = co.embed(texts=list(df['text']),
model="large",
truncate="RIGHT").embeddings
# 检查嵌入的维度
embeds = np.array(embeds)
embeds.shape
6.5、使用索引和最近邻搜索进行搜索
现在我们可以构建索引并搜索最近的邻居。 我们将使用annoy库的AnnoyIndex函数,一种优化快速搜索的方式存储嵌入。 在给定集合中找到距离给定点最近(或最相似)的点的优化问题被称为最近邻搜索。 这种方法适用于大量的文本(其他选项包括Faiss、ScaNN和PyNNDescent)。
构建索引后,我们可以使用它来检索现有问题的最近邻,或者嵌入新问题并找到它们的最近邻。
# 创建搜索索引,传入嵌入的大小
search_index = AnnoyIndex(embeds.shape[1], 'angular')
# 将所有向量添加到搜索索引中
for i in range(len(embeds)):
search_index.add_item(i, embeds[i])
search_index.build(10) # 10 trees
search_index.save('test.ann')
6.5.1、查找数据集中示例的邻居
如果我们只对数据集中的问题之间的距离感兴趣(没有外部查询),一种简单的方法是计算我们拥有的每对嵌入之间的相似性。
# 选择一个示例(我们将检索与之相似的其他示例)
example_id = 7
# 检索最近的邻居
similar_item_ids = search_index.get_nns_by_item(example_id,10,
include_distances=True)
# 格式化并打印文本和距离
results = pd.DataFrame(data={'texts': df.iloc[similar_item_ids[0]]['text'],
'distance': similar_item_ids[1]}).drop(example_id)
print(f"问题:'{df.iloc[example\_id]['text']}'\n最近的邻居:")
results
问题:“什么是最古老的职业?”
最近邻:
6.5.2、查找用户查询的邻居
我们可以使用诸如嵌入之类的技术来找到用户查询的最近邻居。通过嵌入查询,我们可以衡量它与数据集中项目的相似性,并确定最近的邻居。
query = "世界上最高的山是什么?"
# 获取查询的嵌入
query_embed = co.embed(texts=[query],
model="large",
truncate="RIGHT").embeddings
# 检索最近的邻居
similar_item_ids = search_index.get_nns_by_vector(query_embed[0],10,
include_distances=True)
# 格式化结果
results = pd.DataFrame(data={'texts': df.iloc[similar_item_ids[0]]['text'],
'distance': similar_item_ids[1]})
print(f"问题:'{query}'\n最近的邻居:")
results
查询:“世界上最高的山是什么?”
最近邻:
七、总结
本文主要介绍了如何使用Cohere构建语义搜索引擎。分别从如何获取问题数据集、进行文本嵌入和搜索,以及如何对结果进行可视化等方面进行展开介绍。这为我们进一步探索语义搜索领域奠定了基础。
如果你对这篇文章感兴趣,而且你想要了解更多关于AI 领域的实战技巧,可以
关注「技术狂潮AI」公众号
。在这里,你可以看到最新最热的AIGC 领域的干货文章和案例实战教程。