使用嵌入进行问答
本文介绍了如何使用 OpenAI 的 GPT-3 模型回答用户的问题,并通过预处理上下文信息、创建嵌入向量、使用文档嵌入和检索等方法来提高模型的回答准确度。作者提供了使用文本嵌入进行搜索和建议、使用自定义嵌入等示例方法。
正文
许多用例需要 GPT-3 以有见地的答案来回答用户问题。 例如,客户支持聊天机器人可能需要提供常见问题的答案。 GPT 模型在训练中获取了很多通用知识,但我们经常需要摄取和使用包含更具体信息的大型库。
在本笔记本中,我们将演示一种方法,通过使用文档嵌入和检索,使 GPT-3 能够使用文本库作为参考来回答问题。 我们将使用有关 2020 年夏季奥运会的维基百科文章的数据集。 请参阅此笔记本以了解数据收集过程。
import numpy as np import openai import pandas as pd import pickle import tiktoken COMPLETIONS_MODEL = "text-davinci-003" EMBEDDING_MODEL = "text-embedding-ada-002"
默认情况下,GPT-3 不是 2020 年奥运会的专家:
prompt = "谁赢得了 2020 年夏季奥运会男子跳高冠军?" openai.Completion.create( prompt=prompt, temperature=0, max_tokens=300, model=COMPLETIONS_MODEL )["choices"][0]["text"].strip(" \n")
"巴西的马塞洛·基耶里吉尼 (Marcelo Chierighini) 在 2020 年夏季奥运会上获得男子跳高金牌。" 马塞洛是一名游泳金牌得主,而且,我们认为,跳高运动员算不上什么! 显然 GPT-3 在这里需要一些帮助。 要解决的第一个问题是模型正在产生幻觉而不是告诉我们"我不知道"。 这很糟糕,因为它让人很难相信模型给我们的答案!
0) 通过及时工程防止幻觉
我们可以通过更明确地提示来解决这个幻觉问题:
prompt = """尽可能如实回答问题,如果您不确定答案,请说"对不起,我不知道"。 问:谁赢得了 2020 年夏季奥运会男子跳高冠军? A:""" openai.Completion.create( prompt=prompt, temperature=0, max_tokens=300, model=COMPLETIONS_MODEL )["choices"][0]["text"].strip(" \n")
"抱歉,我不知道。" 为了帮助模型回答问题,我们在提示中提供了额外的上下文信息。 当所需的上下文总数很短时,我们可以直接将其包含在提示中。 例如,我们可以使用从维基百科获取的信息。 我们更新初始提示以告诉模型明确使用提供的文本。
prompt = """使用提供的文本尽可能如实回答问题,如果答案未包含在下面的文本中,请说"我不知道" 语境: 2020 年夏季奥运会男子跳高比赛于 2021 年 7 月 30 日至 8 月 1 日在奥林匹克体育场举行。 来自24个国家的33名运动员参赛; 可能的总数取决于有多少国家将使用普遍名额 除了通过分数或排名的 32 个资格赛之外,还可以进入运动员(2021 年没有使用普遍名额)。 意大利运动员 Gianmarco Tamberi 和卡塔尔运动员 Mutaz Essa Barshim 成为比赛的联合冠军 当他们清除 2.37m 时,他们两人之间的平局。 坦贝里和巴尔希姆罕见地同意分享金牌 不同国家的运动员同意在奥林匹克历史上分享同一枚奖牌。 特别是 Barshim 被听到问比赛官员"我们能有两个金牌吗?" 作为对被提供的回应 '跳下'。 白俄罗斯的 Maksim Nedasekau 获得铜牌。 这是意大利男子跳高史上的第一枚奖牌, 白俄罗斯为意大利和卡塔尔夺得男子跳高首金,男子跳高三连冠 卡塔尔(全部由 Barshim)。 Barshim 成为继 Patrik Sjöberg 之后第二个在跳高比赛中获得三枚奖牌的人 瑞典(1984 年至 1992 年)。 问:谁赢得了 2020 年夏季奥运会男子跳高冠军? A:"""
openai.Completion.create( prompt=prompt, temperature=0, max_tokens=300, top_p=1, frequency_penalty=0, presence_penalty=0, model=COMPLETIONS_MODEL )["choices"][0]["text"].strip(" \n")
"Gianmarco Tamberi 和 Mutaz Essa Barshim 成为该赛事的联合获胜者。"
只有当模型可能需要知道的额外内容的数据集足够小以适合单个提示时,才可以将额外信息添加到提示中。 当我们需要模型从大量信息中选择相关的上下文信息时,我们该怎么办?
在本笔记本的其余部分,我们将演示一种通过使用文档嵌入和检索,使用大量附加上下文信息来增强 GPT-3 的方法。 此方法分两步回答查询:首先检索与查询相关的信息,然后根据检索到的信息编写针对问题定制的答案。 第一步使用 Embeddings API,第二步使用 Completions API。
步骤是:
- 通过将上下文信息分成块来预处理上下文信息,并为每个块创建一个嵌入向量。
- 收到查询后,将查询嵌入到与上下文块相同的向量空间中,并找到与查询最相似的上下文嵌入。
- 将最相关的上下文嵌入添加到查询提示中。
- 将问题连同最相关的上下文一起提交给 GPT,并收到使用提供的上下文信息的答案。
1)预处理文档库
我们计划使用文档嵌入来获取文档库中最相关的部分,并将它们插入到我们提供给 GPT-3 的提示中。 因此,我们需要将文档库分解为上下文的“部分”,可以单独搜索和检索。
部分应该足够大以包含足够的信息来回答问题; 但小到足以将一个或多个放入 GPT-3 提示符中。 我们发现大约一段文本通常是一个不错的长度,但您应该针对您的特定用例进行试验。 在此示例中,维基百科文章已经分组到语义相关的标题中,因此我们将使用它们来定义我们的部分。 此预处理已在此笔记本中完成,因此我们将加载结果并使用它们。
# 我们已经托管了处理后的数据集,因此您可以直接下载它而无需重新创建它。 # 这个数据集已经被分成了几个部分,维基百科页面的每个部分一行。 df = pd.read_csv('https://cdn.openai.com/API/examples/data/olympics_sections_text.csv') df = df.set_index(["title", "heading"]) print(f"{len(df)} rows in the data.") df.sample(5)
3964 rows in the data.
标题 | 标题 | 内容 | 令 牌 |
---|---|---|---|
牙买加参加2020年夏季奥运会 | 游泳 | 牙买加游泳运动员进一步获得了资格… | 51 |
2020年夏季奥运会射箭 – 女子个人 | 背景 | 这是连续第13次亮相… | 136 |
德国参加2020年夏季奥运会 | 运动攀岩 | 德国将两名运动登山者带入Ol… | 98 |
2020年夏季奥运会自行车 – 女子小轮车比赛 | 比赛形式 | 比赛是三轮比赛,… | 215 |
2020年夏季奥运会排球 – 男子比赛 | 格式 | 初赛是… | 104 |
我们通过为每个部分创建一个嵌入向量来预处理文档部分。嵌入是数字向量,可以帮助我们理解文本在语义上的相似或不同程度。两个嵌入越接近,它们的内容就越相似。有关更多信息,请参阅有关 OpenAI 嵌入的文档。
此索引阶段可以脱机执行,并且仅运行一次以预先计算数据集的索引,以便以后可以检索每条内容。由于这是一个小示例,我们将在本地存储和搜索嵌入。如果您有更大的数据集,请考虑使用矢量搜索引擎(如Pinecone或Weaviate)来支持搜索。
def get_embedding(text: str, model: str=EMBEDDING_MODEL) -> list[float]: result = openai.Embedding.create( model=model, input=text ) return result["data"][0]["embedding"] def compute_doc_embeddings(df: pd.DataFrame) -> dict[tuple[str, str], list[float]]: """ 使用 OpenAI 嵌入 API 为数据框中的每一行创建一个嵌入。 返回在每个嵌入向量和它对应的行的索引之间映射的字典。 """ return { idx: get_embedding(r.content) for idx, r in df.iterrows() }
def load_embeddings(fname: str) -> dict[tuple[str, str], list[float]]: """ 从 CSV 中读取文档嵌入及其密钥。 fname 是具有这些命名列的 CSV 的路径: "title", "heading", "0", "1", ... 直到嵌入向量的长度。 """ df = pd.read_csv(fname, header=0) max_dim = max([int(c) for c in df.columns if c != "title" and c != "heading"]) return { (r.title, r.heading): [r[str(i)] for i in range(max_dim + 1)] for _, r in df.iterrows() }
同样,我们已经为您托管了嵌入,因此您不必从头开始重新计算它们。
document_embeddings = load_embeddings("https://cdn.openai.com/API/examples/data/olympics_sections_document_embeddings.csv") # ===== 或者,取消注释下面的行以从头开始重新计算嵌入。 ======== # document_embeddings = compute_doc_embeddings(df)
# 嵌入示例: example_entry = list(document_embeddings.items())[0] print(f"{example_entry[0]} : {example_entry[1][:5]}... ({len(example_entry[1])} entries)")
('2020 夏季奥运会','摘要'):[0.0037565305829048,-0.0061981128528714,-0.0087078781798481,-0.0071364338509738,-0.0025227521546185] ...(1536 个条目)
因此,我们将文档库拆分为多个部分,并通过创建表示每个块的嵌入向量对它们进行编码。接下来,我们将使用这些嵌入来回答用户的问题。
2) 找到与问题嵌入最相似的文档嵌入
在问答时,为了回答用户的查询,我们计算问题的查询嵌入,并使用它来查找最相似的文档部分。由于这是一个小示例,因此我们在本地存储和搜索嵌入。如果您有更大的数据集,请考虑使用矢量搜索引擎(如Pinecone或Weaviate)来支持搜索。
def vector_similarity(x: list[float], y: list[float]) -> float: """ 返回两个向量之间的相似度。 因为 OpenAI 嵌入被归一化为长度 1,所以余弦相似度与点积相同。 """ return np.dot(np.array(x), np.array(y)) def order_document_sections_by_query_similarity(query: str, contexts: dict[(str, str), np.array]) -> list[(float, (str, str))]: """ 查找提供的查询的查询嵌入,并将其与所有预先计算的文档嵌入进行比较 找到最相关的部分。 返回文档部分列表,按相关性降序排列。 """ query_embedding = get_embedding(query) document_similarities = sorted([ (vector_similarity(query_embedding, doc_embedding), doc_index) for doc_index, doc_embedding in contexts.items() ], reverse=True) return document_similarities
order_document_sections_by_query_similarity("谁赢得了男子跳高比赛?", document_embeddings)[:5]
[(0.884864308450606, ("2020 年夏季奥林匹克运动会田径项目——男子跳高",'总结')), (0.8633938355935518, ("2020 年夏季奥林匹克运动会田径项目——男子撑杆跳","总结"), (0.861639730583851, ("2020 年夏季奥林匹克运动会田径项目——男子跳远","总结"), (0.8560523857031264, ("2020 年夏季奥林匹克运动会田径比赛——男子三级跳远","总结")), (0.8469039130441247, ("2020 年夏季奥运会田径比赛——男子 110 米栏", '概括'))]
order_document_sections_by_query_similarity("谁赢得了女子跳高比赛?", document_embeddings)[:5]
[(0.8726165220223294, ("2020 年夏季奥林匹克运动会田径比赛——女子跳远",'总结')), (0.8682196158313358, ("2020 年夏季奥林匹克运动会田径比赛——女子跳高","总结"), (0.863191526370672, ("2020 年夏季奥运会田径项目——女子撑杆跳","总结"), (0.8609374262115406, ("2020 年夏季奥林匹克运动会田径比赛——女子三级跳远","总结")), (0.8581515607285688, ("2020 年夏季奥运会田径比赛——女子 100 米栏", '概括'))]
我们可以看到,每个问题最相关的文档部分包括男子和女子跳高比赛的摘要 – 这正是我们所期望的。
3) 将最相关的文档部分添加到查询提示中
计算出最相关的上下文片段后,只需将它们附加到提供的查询中即可构建提示。使用查询分隔符来帮助模型区分单独的文本片段很有帮助。
MAX_SECTION_LEN = 500 SEPARATOR = "\n* " ENCODING = "gpt2" # text-davinci-003 的编码 encoding = tiktoken.get_encoding(ENCODING) separator_len = len(encoding.encode(SEPARATOR)) f"Context separator contains {separator_len} tokens"
'上下文分隔符包含 3 个标记'
def construct_prompt(question: str, context_embeddings: dict, df: pd.DataFrame) -> str: """ Fetch relevant """ most_relevant_document_sections = order_document_sections_by_query_similarity(question, context_embeddings) chosen_sections = [] chosen_sections_len = 0 chosen_sections_indexes = [] for _, section_index in most_relevant_document_sections: # Add contexts until we run out of space. document_section = df.loc[section_index] chosen_sections_len += document_section.tokens + separator_len if chosen_sections_len > MAX_SECTION_LEN: break chosen_sections.append(SEPARATOR + document_section.content.replace("\n", " ")) chosen_sections_indexes.append(str(section_index)) # 有用的诊断信息 print(f"Selected {len(chosen_sections)} document sections:") print("\n".join(chosen_sections_indexes)) header = """使用提供的上下文尽可能如实回答问题,如果答案未包含在下面的文本中,请说"我不知道"。\n\n上下文:\n""" return header + "".join(chosen_sections) + "\n\n Q: " + question + "\n A:"
prompt = construct_prompt( "谁赢得了 2020 年夏季奥运会男子跳高冠军?", document_embeddings, df ) print("===\n", prompt)
选定的 2 个文档部分: ("2020 年夏季奥林匹克运动会田径项目——男子跳高","总结") ("2020 年夏季奥林匹克运动会田径项目——男子跳远","总结") === 使用提供的上下文尽可能真实地回答问题,如果答案未包含在下面的文本中,请说"我不知道"。 语境: * 2020 年夏季奥运会男子跳高项目于 2021 年 7 月 30 日至 8 月 1 日在奥林匹克体育场举行。 来自24个国家的33名运动员参赛; 可能的总数取决于除了 32 个通过分数或排名的资格赛(2021 年没有使用通用名额)之外,还有多少国家将使用通用名额来参赛运动员。 意大利运动员 Gianmarco Tamberi 和卡塔尔运动员 Mutaz Essa Barshim 在他们两人以 2.37m 的成绩并列后成为该赛事的联合冠军。 坦贝里和巴尔希姆都同意分享金牌,这在奥运会历史上是罕见的,不同国家的运动员同意分享同一枚奖牌。 特别是 Barshim 被听到问比赛官员"我们能有两个金牌吗?" 作为对"跳下"的回应。 白俄罗斯的 Maksim Nedasekau 获得铜牌。 这枚奖牌是意大利和白俄罗斯在男子跳高项目上的首枚奖牌,意大利和卡塔尔在男子跳高项目上的首枚金牌,以及卡塔尔男子跳高项目连续第三枚奖牌(均由巴尔希姆获得)。 Barshim 成为继瑞典的 Patrik Sjöberg(1984 年至 1992 年)之后第二个在跳高比赛中获得三枚奖牌的人。 * 2020 年夏季奥运会男子跳远比赛于 2021 年 7 月 31 日至 8 月 2 日在日本国家体育场举行。 预计约有 35 名运动员参加比赛; 确切的数字取决于除了 32 个通过时间或排名的资格赛(2016 年使用了 1 个普遍名额)之外,还有多少国家使用普遍名额来进入运动员。 来自20个国家的31名运动员参加了比赛。 Miltiadis Tentoglou 获得了金牌,这是希腊在男子跳远比赛中的第一枚奖牌。 古巴运动员 Juan Miguel Echevarría 和 Maykel Massó 分别获得银牌和铜牌,这是该国自 2008 年以来获得的第一枚奖牌。 问:谁赢得了 2020 年夏季奥运会男子跳高冠军? A:
我们现在获得了与该问题最相关的文档部分。作为最后一步,让我们把它们放在一起以获得问题的答案。
4)根据上下文回答用户的问题。
现在,我们已经检索了相关上下文并构造了提示,我们终于可以使用完成 API 来回答用户的查询。
COMPLETIONS_API_PARAMS = { # 我们使用 0.0 的温度,因为它给出了最可预测的事实答案。 "temperature": 0.0, "max_tokens": 300, "model": COMPLETIONS_MODEL, }
def answer_query_with_context( query: str, df: pd.DataFrame, document_embeddings: dict[(str, str), np.array], show_prompt: bool = False ) -> str: prompt = construct_prompt( query, document_embeddings, df ) if show_prompt: print(prompt) response = openai.Completion.create( prompt=prompt, **COMPLETIONS_API_PARAMS ) return response["choices"][0]["text"].strip(" \n")
answer_query_with_context("谁赢得了 2020 年夏季奥运会男子跳高冠军?", df, document_embeddings)
选定的 2 个文档部分: ("2020 年夏季奥林匹克运动会田径项目——男子跳高","总结") ("2020 年夏季奥林匹克运动会田径项目——男子跳远","总结")
詹马科·坦贝里 (Gianmarco Tamberi) 和穆塔兹·埃萨·巴希姆 (Mutaz Essa Barshim) 在他们两人以 2.37 米的成绩并列后成为了赛事的联合冠军。 Tamberi 和 Barshim 都同意分享金牌。
通过结合嵌入和完成 API,我们创建了一个问答模型,该模型可以使用大量附加知识来回答问题。当它不知道答案时,它也明白!
在本例中,我们使用了维基百科文章的数据集,但该数据集可以替换为书籍、文章、文档、服务手册等。我们迫不及待地想看看您使用 GPT-3 创建的内容!
更多示例
让我们玩得开心,并尝试更多的例子。
query = "为什么 2020 年夏季奥运会最初被推迟?" answer = answer_query_with_context(query, df, document_embeddings) print(f"\nQ: {query}\nA: {answer}")
选定的 1 个文档部分: ("2020 年夏季奥运会的担忧和争议","摘要") 问:为什么 2020 年夏季奥运会最初被推迟? 答:2020 年夏季奥运会最初因 COVID-19 大流行而推迟。
query = "2020年夏季奥运会,获得奖牌最多的国家获得了多少枚金牌?" answer = answer_query_with_context(query, df, document_embeddings) print(f"\nQ: {query}\nA: {answer}")
选定的 2 个文档部分: ('2020年夏季奥运会奖牌榜', '总结') ('2020年夏季奥运会奖牌获得者名单', '摘要') 问:2020年夏季奥运会,获得奖牌最多的国家获得了多少枚金牌? A:美国获得的奖牌总数最多,有 113 枚,金牌最多,有 39 枚。
query = "男子铅球比赛有何不寻常之处?" answer = answer_query_with_context(query, df, document_embeddings) print(f"\nQ: {query}\nA: {answer}")
选定的 2 个文档部分: ("2020 年夏季奥林匹克运动会田径项目——男子铅球","总结") ("2020 年夏季奥林匹克运动会田径比赛——男子铁饼","总结") 问:男子铅球比赛有何不寻常之处? A:相同的三名选手在同一个人项目的背靠背版本中获得相同的奖牌。
query = "2020年夏季奥运会,意大利获得了多少银牌?" answer = answer_query_with_context(query, df, document_embeddings) print(f"\nQ: {query}\nA: {answer}")
选定的 2 个文档部分: ("意大利参加 2020 年夏季奥运会"、"摘要") ("2020 年夏季奥运会的圣马力诺"、"摘要") 问:2020年夏季奥运会,意大利获得了多少银牌? A:10枚银牌。
我们的问答模型不太容易出现幻觉,并且对它知道或不知道什么有更好的感觉。当信息不包含在上下文中时,这有效;当问题无厘头时;或者当问题在理论上可以回答但超出了 GPT-3 的权限时!
query = "法国获得的奖牌总数乘以颁发给所有国家的跆拳道奖牌数是多少?" answer = answer_query_with_context(query, df, document_embeddings) print(f"\nQ: {query}\nA: {answer}")
选定的 4 个文档部分: ("法国参加 2020 年夏季奥运会"、"跆拳道") ('2020 年夏季奥运会跆拳道比赛 – 资格赛'、'资格赛总结') ('2020年夏季奥运会奖牌榜', '奖牌数') ("2020 年夏季奥运会跆拳道比赛 – 男子 80 公斤级","比赛形式") 问:法国获得的奖牌总数乘以所有国家获得的跆拳道奖牌数是多少? 答:我不知道。
query = "世界上最高的山是什么?" answer = answer_query_with_context(query, df, document_embeddings) print(f"\nQ: {query}\nA: {answer}")
选定的 3 个文档部分: ("2020 年夏季奥运会运动攀岩 – 男子全能","路线设定") (《2020年冬季青年奥林匹克运动会滑雪登山——男子个人》,《总结》) (《2020年冬季青年奥林匹克运动会滑雪登山——女子个人》,《总结》) 问:世界上最高的山是什么山? 答:我不知道。
query = "谁赢得了 2020 年夏季奥运会的 grimblesplatch 比赛?" answer = answer_query_with_context(query, df, document_embeddings) print(f"\nQ: {query}\nA: {answer}")
选定的 2 个文档部分: (《2020 年夏季奥林匹克运动会体操比赛——女子蹦床》,《总结》) ("2020 年夏季奥林匹克运动会马术比赛——团体障碍赛"、"总结") 问:谁赢得了 2020 年夏季奥运会的 grimblesplatch 比赛? 答:我不知道。
评论 (0)