note

一、openai api + redis的使用

1. 设计思路

数据:Kaggle提供的Quora数据集:FAQ Kaggle dataset! | Data Science and Machine Learning。有字段Index(['Questions', 'Followers', 'Answered', 'Link'], dtype='object')

把Link当做答案构造数据对。基本的流程如下:

  • 对每个Question计算Embedding(借助Openai接口)
  • 存储Embedding,同时存储每个Question对应的答案
    • 如果几万条内,可存储为文件,每次服务启动时直接加载到内存or缓存里
  • 从存储的地方检索最相似的Question(找到最相似的问题的答案)

2. 代码实战

当question很多则不适合直接将embedding存入。

(1)使用docker直接运行:

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

执行后,docker会自动从hub把镜像拉到本地,默认是6379端口。
(2)安装redis的python客户端pip install redis用Python和Redis进行交互了
(3)建立索引(定义一组schema,告诉redis你的字段、属性),生成embedding存入redis

import openai
from openai.embeddings_utils import get_embedding, cosine_similarity
from redis.commands.search.query import Query
from redis.commands.search.field import TextField, VectorField
import redis
import pandas as pd
import numpy as np

OPENAI_API_KEY = '...'
openai.api_key = OPENAI_API_KEY
MODEL = "gpt-3.5-turbo"

# redis test
# docker拉取镜像, 默认是6379端口号
r = redis.Redis()
r.set("key", "value")
r.get("key")

# 1. 建立索引
VECTOR_DIM = 12288
INDEX_NAME = "faq"

# 2. 建好要存字段的索引,针对不同属性字段,使用不同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)

# 3. 如果需要删除已有文档的话,可以使用下面的命令
index.dropindex(delete_documents=True)

# 4. 把数据存储到redis中
df = pd.read_csv("/home/andy/torch_rechub_n/hug_llm/content/dataset/Kaggle related questions on Qoura - Questions.csv")
# df.shape

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)

# 5. 构造查询输入
query = "kaggle alive?"
embed_query = get_embedding(query)
params_dict = {"query_embedding": np.array(embed_query).astype(dtype=np.float32).tobytes()}

k = 3
base_query = f"* => [KNN {k} @embedding $query_embedding AS similarity]"
return_fields = ["question", "answer", "similarity"]
query = (
    Query(base_query)
     .return_fields(*return_fields)
     .sort_by("similarity")
     .paging(0, k)
     .dialect(2)
)

# 6. 查询
res = index.search(query, params_dict)
for i,doc in enumerate(res.docs):
    score = 1 - float(doc.similarity)
    print(f"{doc.id}, {doc.question}, {doc.answer} (Score: {round(score ,3) })")

二、聚类和降维可视化

暂略。

三、推荐系统和QA

1. 设计思路

QA使用的是用户的Question去匹配已有知识库,而推荐是使用用户的浏览记录去匹配。但是很明显,推荐相比QA要更复杂一些,主要包括以下几个方面:

  • 刚开始用户没有记录时的推荐(一般行业称为冷启动问题)。
  • 除了相似还有其他要考虑的因素:比如热门内容、新内容、内容多样性、随时间变化的兴趣变化等等。
  • 编码(Embedding输入)问题:我们应该取标题呢,还是文章,还是简要描述或者摘要,还是都要计算。
  • 规模问题:推荐面临的量级一般会远超QA,除了横向扩展机器,是否能从流程和算法设计上提升效率。
  • 用户反馈对推荐系统的影响问题:用户反感或喜欢与文章本身并没有直接关系,比如用户喜欢体育新闻但讨厌中国足球。
  • 线上实时更新问题。

整体设计如下:

  • 用户注册登录时,让其选择感兴趣的类型(如体育、音乐、时尚等),我们通过这一步将用户框在一个大的范围内,同时用来顺道解决冷启动问题。
  • 给用户推荐内容时,在知道类别(用户注册时选择+浏览记录)后,应依次考虑时效性、热门程度、多样性等。
  • 考虑到性能问题,可以编码「标题+摘要」。
  • 对大类别进一步细分,只在细分类别里进行相似度计算。
  • 记录用户实时行为(如浏览Item、浏览时长、评论、收藏、点赞、转发等)。
  • 动态更新内容库,更新用户行为库。

数据:AG_news新闻数据集,每条数据有四个字段’Class Index’, ‘Title’, ‘Description’, ‘embedding’,其中类型四个分别是:1-World, 2-Sports, 3-Business, 4-Sci/Tech,AG News Classification Dataset | Kaggle
任务:基于新闻推荐的一个简单召回

2. 代码实战

@dataclass是python3.7版本以上出的一个装饰器,可以简化数据类的定义,自动为类添加__init____repr____eq__等方法属性,但是@dataclass装饰器生成的方法和属性可能不能满足所有需求,这时需要手动编写。

from dataclasses import dataclass
import pandas as pd
from typing import List
from openai.embeddings_utils import get_embedding, cosine_similarity
from sklearn.metrics.pairwise import cosine_similarity
import openai
import numpy as np
import random

# 1. 观察数据
df = pd.read_csv("/home/andy/torch_rechub_n/hug_llm/content/dataset/AG_News.csv")
df.shape
# 1-World, 2-Sports, 3-Business, 4-Sci/Tech
df["Class Index"].value_counts()  # 每个类别都是3w
sdf = df.sample(100)  # 抽样只用100条数据

# 2. 维护一个用户偏好和行为记录
@dataclass
class User:
    user_name: str

@dataclass
class UserPrefer:
    user_name: str
    prefers: List[int]

@dataclass
class Item:
    item_id: str
    item_props: dict

@dataclass
class Action:
    action_type: str
    action_props: dict

@dataclass
class UserAction:
    user: User
    item: Item
    action: Action
    action_time: str

# 3. 一个用户的历史记录
u1 = User("u1")
up1 = UserPrefer("u1", [1, 2])
# sdf.iloc[1] 正好是sport(类别为2)
i1 = Item("i1", {
    "id": 1,
    "catetory": "sport",
    "title": "Swimming: Shibata Joins Japanese Gold Rush",
    "description": "\
    ATHENS (Reuters) - Ai Shibata wore down French teen-ager  Laure Manaudou to win the women's 800 meters \
    freestyle gold  medal at the Athens Olympics Friday and provide Japan with  their first female swimming \
    champion in 12 years.",
    "content": "content"
})
a1 = Action("浏览", {
    "open_time": "2023-04-01 12:00:00",
    "leave_time": "2023-04-01 14:00:00",
    "type": "close",
    "duration": "2hour"
})
ua1 = UserAction(u1, i1, a1, "2023-04-01 12:00:00")

# 4. 计算所有文本的embedding
OPENAI_API_KEY = "..."
openai.api_key = OPENAI_API_KEY

sdf["embedding"] = sdf.apply(lambda x:
                             get_embedding(x.Title + x.Description, engine="text-embedding-ada-002"), \
                             axis=1)

# 5. recall 召回
class Recall:

    def __init__(self, df: pd.DataFrame):
        self.data = df

    def user_prefer_recall(self, user, n):
        up = self.get_user_prefers(user)
        idx = random.randrange(0, len(up.prefers))
        return self.pick_by_idx(idx, n)

    def hot_recall(self, n):
        # 随机进行示例
        df = self.data.sample(n)
        return df

    def user_action_recall(self, user, n):
        actions = self.get_user_actions(user)
        interest = self.get_most_interested_item(actions)
        recoms = self.recommend_by_interest(interest, n)
        return recoms

    def get_most_interested_item(self, user_action):
        """
        可以选近一段时间内用户交互时间、次数、评论(相关属性)过的Item
        """
        # 就是sdf的第2行,idx为1的那条作为最喜欢(假设)
        # 是一条游泳相关的Item
        idx = user_action.item.item_props["id"]
        im = self.data.iloc[idx]
        return im

    def recommend_by_interest(self, interest, n):
        cate_id = interest["Class Index"]
        q_emb = interest["embedding"]
        # 确定类别
        base = self.data[self.data["Class Index"] == cate_id]
        # 此处可以复用QA那一段代码,用给定embedding计算base中embedding的相似度
        base_arr = np.array(
            [v.embedding for v in base.itertuples()]
        )
        q_arr = np.expand_dims(q_emb, 0)
        sims = cosine_similarity(base_arr, q_arr)
        # 排除掉自己
        idxes = sims.argsort(0).squeeze()[-(n + 1):-1]
        return base.iloc[reversed(idxes.tolist())]

    def pick_by_idx(self, category, n):
        df = self.data[self.data["Class Index"] == category]
        return df.sample(n)

    def get_user_actions(self, user):
        dct = {"u1": ua1}
        return dct[user.user_name]

    def get_user_prefers(self, user):
        dct = {"u1": up1}
        return dct[user.user_name]

    def run(self, user):
        ur = self.user_action_recall(user, 5)
        if len(ur) == 0:
            ur = self.user_prefer_recall(user, 5)
        hr = self.hot_recall(3)
        # 拼接用户召回+热点召回
        return pd.concat([ur, hr], axis=0)

r = Recall(sdf)
rd = r.run(u1)
# 共8个,5个用户行为推荐、3个热门

用户行为召回、热点召回的结果:

在这里插入图片描述

Reference

[1] openai-cookbook/Semantic_text_search_using_embeddings.ipynb at main · openai/openai-cookbook
[2] openai-cookbook/getting-started-with-redis-and-openai.ipynb at main · openai/openai-cookbook
[3] openai-cookbook/Visualizing_embeddings_in_3D.ipynb at main · openai/openai-cookbook
[4] https://github.com/datawhalechina/hugging-llm
[5] facebookresearch/faiss: A library for efficient similarity search and clustering of dense vectors.
[6] milvus-io/milvus: Vector database for scalable similarity search and AI applications.
[7] Vector similarity | Redis
[8] https://redis.io/docs/stack/search/reference/stopwords/

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐