双编码器可能是建立语义问答系统的最有效方法。 这种架构依赖于相同的神经模型,该模型为问题和答案创建向量嵌入。 假设是,问题和答案在潜在空间中应该有接近彼此的表示。 应该是这样的,因为它们应该都描述相同的语义概念。然而,这不适用于像“是”或“否”这样的答案,但标准的常见问题类问题稍微容易一些,因为这两种文本之间通常存在重叠。 不一定是用词方面,但在其语义上。

是的,你需要自带嵌入,才能开始。获取它们的方法有很多,但使用 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工具实现QA搜索系统
我们不想为神经编码器维护自己的服务,也不想设置一个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 | 问题 | 上下文 | 长答案 | 最终决定 |
|---|---|---|---|---|
| 18802997 | 钙保护素能否预测炎症性肠病复发风险… | … | 测量钙保护素可能有助于识别溃疡性结肠炎… | 可能 |
| 20538207 | 应在肾脏…期间监测温度吗 | … | 新的存储可以提供更稳定的温度… | 不 |
| 25521278 | 清盘是否是肥胖的危险因素? | … | 在进食时清空盘子的倾向 … | 是 |
| 17595200 | 子宫内对肥胖有影响吗? | … | 母子与父子比较.. | 否 |
| 15280782 | 在HIV患者中不安全性行为是否在增加… | … | 没有证据表明不安全性行为的趋势… | 没有 |
使用Cohere和Qdrant构建答案数据库
为了开始生成嵌入,您需要创建一个Cohere帐户。这将开始您的试用期,因此您将能够免费将文本向量化。登录后,您的默认API密钥将在设置中提供。我们将需要它来调用co.embed API,使用官方的python包。
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 准确率:
| 指标 | 值 |
|---|---|
| 准确率@1 | 0.877 |
| 准确率@2 | 0.921 |
| 准确率@3 | 0.942 |
| 准确率@4 | 0.950 |
| 准确率@5 | 0.956 |
| 准确率@6 | 0.960 |
| 准确率@7 | 0.964 |
| 准确率@8 | 0.971 |
| 精确度@9 | 0.976 |
| 准确率@10 | 0.977 |
即使我们仅考虑第一个结果,系统似乎也表现得相当不错,距离是最小的。我们大约在 12% 的问题上失败了。但随着 k 值的提高,数字变得更好。检查我们的系统未能回答的问题、它们的完美匹配和我们的猜测也可能很有价值。
我们成功地在仅仅几行代码内实现了一个有效的问答系统。如果您对所取得的结果满意,那么您可以立即开始使用它。不过,如果您觉得还需要稍微改进,那么微调模型是一个不错的选择。如果您想查看完整的源代码,它可以在谷歌Colab上找到。

