当前位置:   article > 正文

【文本特征表征(3)】embedding 应用在QA应答系统,使用 Redis 作为语义检索,可以使用向量数据库(可选)_embedding qa

embedding qa

1、什么是QA应答系统

QA是问答的意思,Q表示Question,A表示Answer,QA是NLP非常基础和常用的任务。简单来说,就是当用户提出一个问题时,我们能从已有的问题库中找到一个最相似的,并把它的答案返回给用户。这里有两个关键点:

事先需要有一个QA库。
用户提问时,系统要能够在QA库中找到一个最相似的。
  • 1
  • 2

ChatGPT(或生成方式)做这类任务相对有点麻烦,尤其是当:

QA库非常庞大时
给用户的答案是固定的,不允许自由发挥时
  • 1
  • 2

生成方式做起来是事倍功半。但是Embedding确实天然的非常适合,因为该任务的核心就是在一堆文本中找出给定文本最相似的。简单来说,其实就是个相似度计算问题。

2、什么是Redis 作为语义检索

当Question非常多,比如上百万甚至上亿时,这种方式就不合适了。一个是内存里可能放不下,另一个是算起来也很慢。这时候就必须借助一些专门用来做语义检索的工具了。

比较常用的工具有:

  • facebookresearch/faiss: A library for efficient similarity search and clustering of dense vectors.
  • milvus-io/milvus: Vector database for scalable similarity search and AI applications.
  • Vector similarity | Redis

此处,我们以Redis为例,其他工具用法类似。

首先,我们需要一个redis,建议使用docker直接运行:

docker run -p 6379:6379 -it redis/redis-stack:latest
  • 1

执行后,docker会自动从hub把镜像拉到本地,默认是6379端口。

然后安装redis-py,也就是Redis的Python客户端:

pip install redis
  • 1

这样我们就可以用Python和Redis进行交互了。

先来个最简单的例子:

import redis
r = redis.Redis()
r.set("key", "value")
  • 1
  • 2
  • 3
True
  • 1
r.get("key")
  • 1
b'value'
  • 1

大家使用过ElasticSearch,接下来的内容会非常容易理解。总的来说,和刚刚的步骤差不多,但是这里我们需要先建索引,然后生成Embedding并把它存储到Redis,再进行使用(从索引中搜索)。不过由于我们使用了工具,具体步骤会略微不同。

索引的概念和数据库中的索引有点相似,就是要定义一组Schema,告诉Redis你的字段是什么,有哪些属性。

3、 什么是向量数据库

可以看看这个 milvus-io/milvus: Vector database for scalable similarity search and AI applications.
感觉为了用途可能广泛!数据库交给商用会好一点。

4、 怎么做

我们使用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
  • 1
  • 2
  • 3
  • 4

这里,我们就把Link当做答案构造数据对。基本的流程如下:

对每个Question计算Embedding
存储Embedding,同时存储每个Question对应的答案
从存储的地方检索最相似的Question
  • 1
  • 2
  • 3

第一步我们将借助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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
[0.665769204766594,
 0.8711775410642538,
 0.7489853201153621,
 0.7384357684745508,
 0.7287129153982224]
  • 1
  • 2
  • 3
  • 4
  • 5

我们返回第二个即可:

vec_base[1]["question"], vec_base[1]["answer"]

('Is Kaggle dead?', '/Is-Kaggle-dead')
  • 1
  • 2
  • 3

当然,在实际中,我们不建议使用循环,大家可以使用NumPy进行批量计算。

arr = np.array(
    [v["embedding"] for v in vec_base]
)
arr.shape
  • 1
  • 2
  • 3
  • 4
(5, 12288)
  • 1
q_arr = np.expand_dims(q_emb, 0)
q_arr.shape
  • 1
  • 2
(1, 12288)
  • 1
from sklearn.metrics.pairwise import cosine_similarity

cosine_similarity(arr, q_arr)
  • 1
  • 2
  • 3
array([[0.6657692 ],
       [0.87117754],
       [0.74898532],
       [0.73843577],
       [0.72871292]])
  • 1
  • 2
  • 3
  • 4
  • 5

不过,当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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
b'OK'
  • 1

接下来就是把数据存到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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

然后我们就可以进行搜索查询了,这一步构造查询输入稍微有一点麻烦。

# 构造查询输入
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)
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
KNN(K最近邻算法),简单来说就是对未知点,分别和已有的点算距离,挑距离最近的K个点。
  • 1
# 查询
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) })")
  • 1
  • 2
  • 3
  • 4
  • 5
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)
  • 1
  • 2
  • 3

上面,我们通过几种不同的方法为大家介绍了如何使用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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/703023
推荐阅读
相关标签
  

闽ICP备14008679号