赞
踩
QA是问答的意思,Q表示Question,A表示Answer,QA是NLP非常基础和常用的任务。简单来说,就是当用户提出一个问题时,我们能从已有的问题库中找到一个最相似的,并把它的答案返回给用户。这里有两个关键点:
事先需要有一个QA库。
用户提问时,系统要能够在QA库中找到一个最相似的。
ChatGPT(或生成方式)做这类任务相对有点麻烦,尤其是当:
QA库非常庞大时
给用户的答案是固定的,不允许自由发挥时
生成方式做起来是事倍功半。但是Embedding确实天然的非常适合,因为该任务的核心就是在一堆文本中找出给定文本最相似的。简单来说,其实就是个相似度计算问题。
当Question非常多,比如上百万甚至上亿时,这种方式就不合适了。一个是内存里可能放不下,另一个是算起来也很慢。这时候就必须借助一些专门用来做语义检索的工具了。
比较常用的工具有:
此处,我们以Redis为例,其他工具用法类似。
首先,我们需要一个redis,建议使用docker直接运行:
docker run -p 6379:6379 -it redis/redis-stack:latest
执行后,docker会自动从hub把镜像拉到本地,默认是6379端口。
然后安装redis-py,也就是Redis的Python客户端:
pip install redis
这样我们就可以用Python和Redis进行交互了。
先来个最简单的例子:
import redis
r = redis.Redis()
r.set("key", "value")
True
r.get("key")
b'value'
大家使用过ElasticSearch,接下来的内容会非常容易理解。总的来说,和刚刚的步骤差不多,但是这里我们需要先建索引,然后生成Embedding并把它存储到Redis,再进行使用(从索引中搜索)。不过由于我们使用了工具,具体步骤会略微不同。
索引的概念和数据库中的索引有点相似,就是要定义一组Schema,告诉Redis你的字段是什么,有哪些属性。
可以看看这个 milvus-io/milvus: Vector database for scalable similarity search and AI applications.
感觉为了用途可能广泛!数据库交给商用会好一点。
我们使用Kaggle提供的Quora数据集:FAQ Kaggle dataset! | Data Science and Machine Learning,先把它给读进来。https://www.kaggle.com/general/183270
import pandas as pd
df = pd.read_csv("dataset/Kaggle related questions on Qoura - Questions.csv")
df.shape
这里,我们就把Link当做答案构造数据对。基本的流程如下:
对每个Question计算Embedding
存储Embedding,同时存储每个Question对应的答案
从存储的地方检索最相似的Question
第一步我们将借助OpenAI的Embedding接口,但是后两步得看实际情况了。如果Question的数量比较少,比如只有几万条甚至几千条,那我们可以把计算好的Embedding直接存储成文件,每次服务启动时直接加载到内存或缓存里就好了。使用时,挨个计算输入问题和存储的所有问题的相似度,然后给出最相似的问题的答案。
为了快速演示,我们只取前5个句子为例:
from openai.embeddings_utils import get_embedding, cosine_similarity import openai import numpy as np OPENAI_API_KEY = "填入专属的API key" openai.api_key = OPENAI_API_KEY vec_base = [] for v in df.head().itertuples(): emb = get_embedding(v.Questions) im = { "question": v.Questions, "embedding": emb, "answer": v.Link } vec_base.append(im) # 然后给定输入,比如:"is kaggle alive?",我们先获取它的Embedding,然后逐个遍历vec_base计算相似度,并取最高的作为响应。 query = "is kaggle alive?" q_emb = get_embedding(query) sims = [cosine_similarity(q_emb, v["embedding"]) for v in vec_base] sims
[0.665769204766594,
0.8711775410642538,
0.7489853201153621,
0.7384357684745508,
0.7287129153982224]
我们返回第二个即可:
vec_base[1]["question"], vec_base[1]["answer"]
('Is Kaggle dead?', '/Is-Kaggle-dead')
当然,在实际中,我们不建议使用循环,大家可以使用NumPy进行批量计算。
arr = np.array(
[v["embedding"] for v in vec_base]
)
arr.shape
(5, 12288)
q_arr = np.expand_dims(q_emb, 0)
q_arr.shape
(1, 12288)
from sklearn.metrics.pairwise import cosine_similarity
cosine_similarity(arr, q_arr)
array([[0.6657692 ],
[0.87117754],
[0.74898532],
[0.73843577],
[0.72871292]])
不过,当Question非常多,比如上百万甚至上亿时,这种方式就不合适了。Redis 专门用来做语义检索的工具
VECTOR_DIM = 12288 INDEX_NAME = "faq" from redis.commands.search.query import Query from redis.commands.search.field import TextField, VectorField from redis.commands.search.indexDefinition import IndexDefinition # 建好要存字段的索引,针对不同属性字段,使用不同Field question = TextField(name="question") answer = TextField(name="answer") embedding = VectorField( name="embedding", algorithm="HNSW", attributes={ "TYPE": "FLOAT32", "DIM": VECTOR_DIM, "DISTANCE_METRIC": "COSINE" } ) schema = (question, embedding, answer) index = r.ft(INDEX_NAME) try: info = index.info() except: index.create_index(schema, definition=IndexDefinition(prefix=[INDEX_NAME + "-"])) Hierarchical Navigable Small Worlds # 如果需要删除已有文档的话,可以使用下面的命令 index.dropindex(delete_documents=True)
b'OK'
接下来就是把数据存到Redis。
for v in df.head().itertuples():
emb = get_embedding(v.Questions)
# 注意,redis要存储bytes或string
emb = np.array(emb, dtype=np.float32).tobytes()
im = {
"question": v.Questions,
"embedding": emb,
"answer": v.Link
}
# 重点是这句
r.hset(name=f"{INDEX_NAME}-{v.Index}", mapping=im)
然后我们就可以进行搜索查询了,这一步构造查询输入稍微有一点麻烦。
# 构造查询输入 query = "kaggle alive?" embed_query = get_embedding(query) params_dict = {"query_embedding": np.array(embed_query).astype(dtype=np.float32).tobytes()} k = 3 # {some filter query}=>[ KNN {num|$num} @vector_field $query_vec] base_query = f"* => [KNN {k} @embedding $query_embedding AS score]" return_fields = ["question", "answer", "score"] query = ( Query(base_query) .return_fields(*return_fields) .sort_by("score") .paging(0, k) .dialect(2) )
KNN(K最近邻算法),简单来说就是对未知点,分别和已有的点算距离,挑距离最近的K个点。
# 查询
res = index.search(query, params_dict)
for i,doc in enumerate(res.docs):
similarity = 1 - float(doc.score)
print(f"{doc.id}, {doc.question}, {doc.answer} (Similarity: {round(score ,3) })")
faq-1, Is Kaggle dead?, /Is-Kaggle-dead (Score: 0.831)
faq-2, How should a beginner get started on Kaggle?, /How-should-a-beginner-get-started-on-Kaggle (Score: 0.735)
faq-3, What are some alternatives to Kaggle?, /What-are-some-alternatives-to-Kaggle (Score: 0.73)
上面,我们通过几种不同的方法为大家介绍了如何使用Embedding进行QA任务。简单回顾一下,要做QA任务首先咱们得有一个QA库,这些QA就是我们的仓库,每当一个新的问题过来时,我们就用这个问题去和咱们仓库里的每一个Q去匹配,然后找到最相似的那个,接下来就把该问题的Answer当做新问题的Answer交给用户。
这个任务的核心就是如何找到这个最相似的,涉及两个知识点:如何表示一个Question,以及如何查找到相似的Question。对于第一点,我们用API提供的Embedding表示,我们可以把它当做一个黑盒子,输入任意长度的文本,输出一个向量。查找相似问题则主要是用到相似度算法,语义相似度一般用cosine距离来衡量。
当然实际中可能会更加复杂一些,比如我们可能除了使用语义匹配,还会使用字词匹配(经典的做法)。而且,一般都会找到topN个相似的,然后对这topN个结果进行排序,选出最可能的那个。不过,这个前面我们举过例子了,完全可以通过ChatGPT来解决,让它帮你选出最好的那个。
https://github.com/datawhalechina/hugging-llm/blob/main/content/ChatGPT%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97%E2%80%94%E2%80%94%E7%9B%B8%E4%BC%BC%E5%8C%B9%E9%85%8D.ipynb
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。