双编码器可能是搭建语义问答系统最有效的方式。这种架构依赖于同一个神经网络模型来为问题和答案创建向量嵌入。其假设是,问题和答案在潜在空间中的表示应该彼此接近。这应该成立,因为它们都应该描述相同的语义概念。但对于像“是”或“否”这样的答案,这不适用,不过标准的类似 FAQ 的问题要简单一些,因为通常两种文本之间存在重叠。不一定是在措辞上,而是在它们的语义上。
是的,要开始这项工作,您需要自带嵌入。获取嵌入的方法有很多种,但使用 Cohere co.embed API 可能是最简单便捷的方法。
为什么 co.embed API 和 Qdrant 配合得如此默契?
维护一个大型语言模型可能既困难又昂贵。根据流量变化进行扩展和缩减需要更多精力,而且变得难以预测。这对于任何语义搜索系统来说都可能是一个障碍。但如果您想立即开始,可以考虑使用 SaaS 模型,特别是 Cohere 的co.embed API。它提供最先进的语言模型,作为高可用 HTTP 服务提供,无需训练或维护您自己的服务。由于所有通信都使用 JSON 完成,您可以简单地将 co.embed 的输出作为 Qdrant 的输入提供。
# Putting the co.embed API response directly as Qdrant method input
qdrant_client.upsert(
collection_name="collection",
points=rest.Batch(
ids=[...],
vectors=cohere_client.embed(...).embeddings,
payloads=[...],
),
)
这两个工具都易于结合,因此您可以在几分钟而不是几天内开始使用语义搜索。
如果您的需求非常特定,需要微调通用模型怎么办?Co.embed API 不仅提供预训练编码器,还允许您提供一些自定义数据集,使用您自己的数据定制嵌入模型。这样,您无需担心基础设施,就能获得领域特定模型的质量。
系统架构概览
在实际系统中,答案被向量化并存储在高效的向量搜索数据库中。我们通常甚至不需要提供特定的答案,只需使用文本的句子或段落并将其向量化。即使如此,如果一段稍长的文本包含特定问题的答案,它与问题嵌入的距离也不应该太远。而且肯定比所有其他不匹配的答案更近。将答案嵌入存储在向量数据库中,可以使搜索过程容易得多。
查找正确答案
一旦我们的数据库正常工作,所有答案嵌入都已就位,我们就可以开始查询它了。我们基本上对给定的问题执行相同的向量化操作,并请求数据库提供一些近邻。我们依赖于彼此接近的嵌入,因此我们期望在潜在空间中距离最小的点包含正确答案。
使用 SaaS 工具实现问答搜索系统
我们既不想维护自己的神经编码器服务,甚至也不想搭建 Qdrant 实例。对于这两者都有 SaaS 解决方案——Cohere 的 co.embed API 和 Qdrant Cloud,所以我们将使用它们而不是本地部署工具。
基于生物医学数据的问答
我们将实现一个用于生物医学数据的问答系统。有一个名为 pubmed_qa 的数据集,其中的 pqa_labeled 子集包含 1,000 个由领域专家标注的问题和答案示例。我们的系统将使用 co.embed API 生成的嵌入进行喂入,并将它们加载到 Qdrant。在此处使用 Qdrant Cloud 与您自己的实例差异不大。连接到云实例的方式有细微差别,但所有其他操作都以相同的方式执行。
from datasets import load_dataset
# Loading the dataset from HuggingFace hub. It consists of several columns: pubid,
# question, context, long_answer and final_decision. For the purposes of our system,
# we’ll use question and long_answer.
dataset = load_dataset("pubmed_qa", "pqa_labeled")
pubid | 问题 | 上下文 | long_answer | 最终决定 |
---|---|---|---|---|
18802997 | 粪钙卫蛋白能否预测炎症性肠病复发风险?... | … | 测量粪钙卫蛋白可能有助于识别溃疡性结肠炎... | 可能 |
20538207 | 肾脏手术期间是否应监测体温?... | … | 新的存储方式可提供更稳定的温度... | 否 |
25521278 | 盘子清空是肥胖的危险因素吗? | … | 吃饭时清空盘子的倾向... | 是 |
17595200 | 是否存在宫内因素影响肥胖? | … | 母婴和父婴比较... | 否 |
15280782 | 高危人群中不安全性行为是否在增加?... | … | 没有证据表明不安全性行为存在趋势... | 否 |
使用 Cohere 和 Qdrant 构建答案数据库
要开始生成嵌入,您需要创建一个 Cohere 账户。这将启动您的试用期,以便您可以免费对文本进行向量化。登录后,您的默认 API 密钥将在设置中可用。我们将需要它来使用官方 python 包调用 co.embed API。
import cohere
cohere_client = cohere.Client(COHERE_API_KEY)
# Generating the embeddings with Cohere client library
embeddings = cohere_client.embed(
texts=["A test sentence"],
model="large",
)
vector_size = len(embeddings.embeddings[0])
print(vector_size) # output: 4096
首先连接到 Qdrant 实例,并创建一个配置正确的集合,以便稍后将一些嵌入放入其中。
# Connecting to Qdrant Cloud with qdrant-client requires providing the api_key.
# If you use an on-premise instance, it has to be skipped.
qdrant_client = QdrantClient(
host="xyz-example.eu-central.aws.cloud.qdrant.io",
prefer_grpc=True,
api_key=QDRANT_API_KEY,
)
现在我们可以对所有答案进行向量化了。它们将构成我们的集合,因此我们也可以将它们连同载荷和标识符一起放入 Qdrant。这将使我们的数据集易于搜索。
answer_response = cohere_client.embed(
texts=dataset["train"]["long_answer"],
model="large",
)
vectors = [
# Conversion to float is required for Qdrant
list(map(float, vector))
for vector in answer_response.embeddings
]
ids = [entry["pubid"] for entry in dataset["train"]]
# Filling up Qdrant collection with the embeddings generated by Cohere co.embed API
qdrant_client.upsert(
collection_name="pubmed_qa",
points=rest.Batch(
ids=ids,
vectors=vectors,
payloads=list(dataset["train"]),
)
)
就这样。甚至无需自己搭建单个服务器,我们就创建了一个可以轻松提问的系统。我不想称之为无服务器,因为这个术语已经被使用了,但 co.embed API 与 Qdrant Cloud 使得一切都更容易维护。
使用语义搜索回答问题 — 质量
是时候查询我们的数据库并提出一些问题了。衡量系统整体质量可能很有趣。在这种类型的问题中,我们通常使用 top-k 准确率。如果正确答案出现在前 k 个结果中,则我们认为系统的预测是正确的。
# Finding the position at which Qdrant provided the expected answer for each question.
# That allows to calculate accuracy@k for different values of k.
k_max = 10
answer_positions = []
for embedding, pubid in tqdm(zip(question_response.embeddings, ids)):
response = qdrant_client.search(
collection_name="pubmed_qa",
query_vector=embedding,
limit=k_max,
)
answer_ids = [record.id for record in response]
if pubid in answer_ids:
answer_positions.append(answer_ids.index(pubid))
else:
answer_positions.append(-1)
保存的答案位置使我们能够计算不同 k 值的指标。
# Prepared answer positions are being used to calculate different values of accuracy@k
for k in range(1, k_max + 1):
correct_answers = len(
list(
filter(lambda x: 0 <= x < k, answer_positions)
)
)
print(f"accuracy@{k} =", correct_answers / len(dataset["train"]))
以下是不同 k 值下的 top-k 准确率值
指标 | 值 |
---|---|
accuracy@1 | 0.877 |
accuracy@2 | 0.921 |
accuracy@3 | 0.942 |
accuracy@4 | 0.950 |
accuracy@5 | 0.956 |
accuracy@6 | 0.960 |
accuracy@7 | 0.964 |
accuracy@8 | 0.971 |
accuracy@9 | 0.976 |
accuracy@10 | 0.977 |
看起来我们的系统即使只考虑距离最低的第一个结果,表现也相当不错。我们在大约 12% 的问题上失败了。但随着 k 值越高,数字变得更好。检查系统未能回答的问题,它们的完美匹配以及我们的猜测,也很有价值。
我们仅用几行代码就实现了一个可用的问答系统。如果您对取得的结果满意,那么您可以立即开始使用它。不过,如果您觉得需要稍作改进,那么微调模型是可行的方法。如果您想查看完整的源代码,可在 Google Colab 上获取。