跳到主要内容

使用Qdrant和Few-Shot Learning对OpenAI模型进行检索增强生成(RAG)的微调

nbviewer

本笔记的目的是通过一个全面的示例,演示如何对OpenAI模型进行检索增强生成(RAG)的微调。

我们还将集成Qdrant和Few-Shot Learning来提升模型的性能并减少幻觉。这可以作为一个实用指南,供对利用OpenAI模型解决特定用例感兴趣的ML从业者、数据科学家和AI工程师参考。🤩

为什么你应该阅读这篇博客?

您想学习如何: - 为特定用例微调OpenAI模型 - 使用Qdrant来提升您的RAG模型的性能 - 使用微调来提高您的RAG模型的正确性并减少幻觉

首先,我们选择了一个数据集,其中我们保证检索是完美的。我们选择了SQuAD数据集的一个子集,这是关于维基百科文章的问题和答案的集合。我们还包括了答案不在上下文中的样本,以演示RAG如何处理这种情况。

目录

  1. 设置环境

零样本学习部分

  1. 数据准备:SQuADv2数据集
  2. 使用基础gpt-3.5-turbo-0613模型进行回答
  3. 微调并使用微调模型进行回答
  4. 评估:模型表现如何?

少样本学习部分

  1. 使用Qdrant改进RAG提示

  2. 使用Qdrant微调OpenAI模型

  3. 评估

  4. 结论

    • 综合结果
    • 观察

术语、定义和参考资料

检索增强生成(RAG)是什么? “检索增强生成(RAG)”一词来源于Facebook AI的Lewis等人最近的一篇论文。这个想法是使用一个预训练的语言模型(LM)生成文本,但使用一个单独的检索系统来查找相关文档,以对LM进行条件设定。

Qdrant是什么? Qdrant是一个开源的向量搜索引擎,允许您在大型数据集中搜索相似的向量。它是用Rust构建的,我们将使用Python客户端与其交互。这是RAG中的检索部分。

什么是少样本学习? 少样本学习是一种机器学习类型,其中模型通过在少量数据上进行训练或微调来“改进”。在这种情况下,我们将使用它来在SQuAD数据集的少量示例上微调RAG模型。这是RAG中的增强部分。

什么是零样本学习? 零样本学习是一种机器学习类型,其中模型通过在没有任何特定数据集信息的情况下进行训练或微调来“改进”。

什么是微调? 微调是一种机器学习类型,其中模型通过在少量数据上进行训练或微调来“改进”。在这种情况下,我们将使用它来在SQuAD数据集的少量示例上微调RAG模型。LM是RAG中生成部分的关键。

1. 设置环境

安装和导入依赖项

!pip install pandas openai tqdm tenacity scikit-learn tiktoken python-dotenv seaborn --upgrade --quiet

import json
import os
import time

import pandas as pd
from openai import OpenAI
import tiktoken
import seaborn as sns
from tenacity import retry, wait_exponential
from tqdm import tqdm
from collections import defaultdict
import numpy as np
import matplotlib.pyplot as plt
import numpy as np
from sklearn.metrics import confusion_matrix

import warnings
warnings.filterwarnings('ignore')

tqdm.pandas()

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "<your OpenAI API key if not set as env var>"))


设置您的密钥

这里获取您的OpenAI密钥,在创建免费集群后在这里获取您的Qdrant密钥。

os.environ["QDRANT_URL"] = "https://xxx.cloud.qdrant.io:6333"
os.environ["QDRANT_API_KEY"] = "xxx"

第二部分 数据准备:SQuADv2数据子集

为了演示目的,我们将从SQuADv2数据集的训练集和验证集中提取一小部分数据。该数据集包含了问题和上下文,其中答案不在上下文中,以帮助我们评估LLM如何处理这种情况。

我们将从JSON文件中读取数据,并创建一个包含以下列的数据框:questioncontextansweris_impossible

下载数据

# !mkdir -p local_cache
# !wget https://rajpurkar.github.io/SQuAD-explorer/dataset/train-v2.0.json -O local_cache/train.json
# !wget https://rajpurkar.github.io/SQuAD-explorer/dataset/dev-v2.0.json -O local_cache/dev.json

读取JSON到DataFrame

def json_to_dataframe_with_titles(json_data):
qas = []
context = []
is_impossible = []
answers = []
titles = []

for article in json_data['data']:
title = article['title']
for paragraph in article['paragraphs']:
for qa in paragraph['qas']:
qas.append(qa['question'].strip())
context.append(paragraph['context'])
is_impossible.append(qa['is_impossible'])

ans_list = []
for ans in qa['answers']:
ans_list.append(ans['text'])
answers.append(ans_list)
titles.append(title)

df = pd.DataFrame({'title': titles, 'question': qas, 'context': context, 'is_impossible': is_impossible, 'answers': answers})
return df

def get_diverse_sample(df, sample_size=100, random_state=42):
"""
从每个标题中进行抽样,获取数据框的多样化样本。
"""
sample_df = df.groupby(['title', 'is_impossible']).apply(lambda x: x.sample(min(len(x), max(1, sample_size // 50)), random_state=random_state)).reset_index(drop=True)

if len(sample_df) < sample_size:
remaining_sample_size = sample_size - len(sample_df)
remaining_df = df.drop(sample_df.index).sample(remaining_sample_size, random_state=random_state)
sample_df = pd.concat([sample_df, remaining_df]).sample(frac=1, random_state=random_state).reset_index(drop=True)

return sample_df.sample(min(sample_size, len(sample_df)), random_state=random_state).reset_index(drop=True)

train_df = json_to_dataframe_with_titles(json.load(open('local_cache/train.json')))
val_df = json_to_dataframe_with_titles(json.load(open('local_cache/dev.json')))

df = get_diverse_sample(val_df, sample_size=100, random_state=42)

3. 使用基础gpt-3.5-turbo-0613模型进行回答

3.1 无样本提示

让我们从使用基础gpt-3.5-turbo-0613模型来回答问题开始。这个提示是问题和上下文的简单连接,中间用分隔符标记:\n\n。我们的提示中有一个简单的指示部分:

根据上下文回答以下问题。只能从上下文中回答。如果你不知道答案,说 ‘我不知道’。

还有其他可能的提示,但这是一个很好的起点。我们将使用这个提示来回答验证集中的问题。

# 获取提示消息的函数
def get_prompt(row):
return [
{"role": "system", "content": "You are a helpful assistant."},
{
"role": "user",
"content": f"""Answer the following Question based on the Context only. Only answer from the Context. If you don't know the answer, say 'I don't know'.
Question: {row.question}\n\n
Context: {row.context}\n\n
Answer:\n""",
},
]

3.2 使用零提示回答

接下来,您需要一些可重复使用的函数,这些函数会调用OpenAI API并返回答案。您将使用API的ChatCompletion.create端点,该端点接受一个提示并返回完成的文本。

# 以坚韧不拔的精神进行多次重试
@retry(wait=wait_exponential(multiplier=1, min=2, max=6))
def api_call(messages, model):
return client.chat.completions.create(
model=model,
messages=messages,
stop=["\n\n"],
max_tokens=100,
temperature=0.0,
)


# 主要功能是回答问题
def answer_question(row, prompt_func=get_prompt, model="gpt-3.5-turbo"):
messages = prompt_func(row)
response = api_call(messages, model)
return response.choices[0].message.content

运行时间:约3分钟,🛜 需要互联网连接

# 使用 `progress_apply` 结合 `tqdm` 来显示进度条
df["generated_answer"] = df.progress_apply(answer_question, axis=1)
df.to_json("local_cache/100_val.json", orient="records", lines=True)
df = pd.read_json("local_cache/100_val.json", orient="records", lines=True)

df

title question context is_impossible answers
0 Scottish_Parliament What consequence of establishing the Scottish ... A procedural consequence of the establishment ... False [able to vote on domestic legislation that app...
1 Imperialism Imperialism is less often associated with whic... The principles of imperialism are often genera... True []
2 Economic_inequality What issues can't prevent women from working o... When a person’s capabilities are lowered, they... True []
3 Southern_California What county are Los Angeles, Orange, San Diego... Its counties of Los Angeles, Orange, San Diego... True []
4 French_and_Indian_War When was the deportation of Canadians? Britain gained control of French Canada and Ac... True []
... ... ... ... ... ...
95 Geology In the layered Earth model, what is the inner ... Seismologists can use the arrival times of sei... True []
96 Prime_number What type of value would the Basel function ha... The zeta function is closely related to prime ... True []
97 Fresno,_California What does the San Joaquin Valley Railroad cros... Passenger rail service is provided by Amtrak S... True []
98 Victoria_(Australia) What party rules in Melbourne's inner regions? The centre-left Australian Labor Party (ALP), ... False [The Greens, Australian Greens, Greens]
99 Immune_system The speed of the killing response of the human... In humans, this response is activated by compl... False [signal amplification, signal amplification, s...

100 rows × 5 columns

4. 微调和使用微调模型进行回答

有关完整的微调过程,请参考OpenAI Fine-Tuning文档

4.1 准备微调数据

我们需要为微调准备数据。我们将使用与之前相同数据集的训练集中的一些样本,但我们将在上下文中添加答案。这将帮助模型学习从上下文中检索答案。

我们的指令提示与之前相同,系统提示也是如此。

def dataframe_to_jsonl(df):
def create_jsonl_entry(row):
answer = row["answers"][0] if row["answers"] else "I don't know"
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{
"role": "user",
"content": f"""Answer the following Question based on the Context only. Only answer from the Context. If you don't know the answer, say 'I don't know'.
Question: {row.question}\n\n
Context: {row.context}\n\n
Answer:\n""",
},
{"role": "assistant", "content": answer},
]
return json.dumps({"messages": messages})

jsonl_output = df.apply(create_jsonl_entry, axis=1)
return "\n".join(jsonl_output)

train_sample = get_diverse_sample(train_df, sample_size=100, random_state=42)

with open("local_cache/100_train.jsonl", "w") as f:
f.write(dataframe_to_jsonl(train_sample))

提示:💡 验证微调数据

您可以查看这个食谱以获取有关如何准备微调数据的更多详细信息。

4.2 微调OpenAI模型

如果您是OpenAI模型微调的新手,请参考如何微调Chat模型笔记本。您也可以参考OpenAI微调文档以获取更多详细信息。

class OpenAIFineTuner:
"""
微调OpenAI模型的类
"""
def __init__(self, training_file_path, model_name, suffix):
self.training_file_path = training_file_path
self.model_name = model_name
self.suffix = suffix
self.file_object = None
self.fine_tuning_job = None
self.model_id = None

def create_openai_file(self):
self.file_object = client.files.create(
file=open(self.training_file_path, "r"),
purpose="fine-tune",
)

def wait_for_file_processing(self, sleep_time=20):
while self.file_object.status != 'processed':
time.sleep(sleep_time)
self.file_object.refresh()
print("File Status: ", self.file_object.status)

def create_fine_tuning_job(self):
self.fine_tuning_job = client.fine_tuning.jobs.create(
training_file=self.file_object["id"],
model=self.model_name,
suffix=self.suffix,
)

def wait_for_fine_tuning(self, sleep_time=45):
while self.fine_tuning_job.status != 'succeeded':
time.sleep(sleep_time)
self.fine_tuning_job.refresh()
print("Job Status: ", self.fine_tuning_job.status)

def retrieve_fine_tuned_model(self):
self.model_id = client.fine_tuning.jobs.retrieve(self.fine_tuning_job["id"]).fine_tuned_model
return self.model_id

def fine_tune_model(self):
self.create_openai_file()
self.wait_for_file_processing()
self.create_fine_tuning_job()
self.wait_for_fine_tuning()
return self.retrieve_fine_tuned_model()

fine_tuner = OpenAIFineTuner(
training_file_path="local_cache/100_train.jsonl",
model_name="gpt-3.5-turbo",
suffix="100trn20230907"
)

运行时间:约10-20分钟,🛜 需要互联网连接

model_id = fine_tuner.fine_tune_model()
model_id

4.2.1 尝试使用微调模型

让我们在与之前相同的验证集上尝试使用微调模型。您将使用与之前相同的提示,但是这次将使用微调模型而不是基础模型。在这样做之前,您可以简单地调用一下,以了解微调模型的表现如何。

completion = client.chat.completions.create(
model=model_id,
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"},
{"role": "assistant", "content": "Hi, how can I help you today?"},
{
"role": "user",
"content": "Can you answer the following question based on the given context? If not, say, I don't know:\n\nQuestion: What is the capital of France?\n\nContext: The capital of Mars is Gaia. Answer:",
},
],
)

print(completion.choices[0].message)

4.3 使用微调模型进行回答

这与之前的操作相同,但是您将使用微调模型而不是基础模型。

运行时间:约5分钟,🛜 需要互联网连接

df["ft_generated_answer"] = df.progress_apply(answer_question, model=model_id, axis=1)

5. 评估:模型表现如何?

为了评估模型的表现,需要将预测的答案与实际答案进行比较 – 如果预测的答案中包含任何实际答案,则表示匹配成功。我们还创建了错误类别,以帮助您了解模型存在哪些问题。

当我们知道上下文中存在正确答案时,可以衡量模型的表现,有3种可能的结果:

  1. 回答正确:模型回答了正确答案。它可能还包括上下文中没有的其他答案。
  2. 跳过:模型回答“我不知道”(IDK),而答案实际上在上下文中。这比给出错误答案要好。在我们的设计中,我们知道真实答案存在,因此我们能够衡量它 – 这并不总是情况。这是一个模型错误。我们将其从总体错误率中排除。
  3. 错误:模型回答了错误答案。这是一个模型错误

当我们知道上下文中不存在正确答案时,可以衡量模型的表现,有2种可能的结果:

  1. 幻觉:模型回答了一个答案,而预期的是“我不知道”。这是一个模型错误
  2. 我不知道:模型回答“我不知道”(IDK),而答案不在上下文中。这是一个模型胜利
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

class Evaluator:
def __init__(self, df):
self.df = df
self.y_pred = pd.Series() # 初始化为空序列
self.labels_answer_expected = ["✅ Answered Correctly", "❎ Skipped", "❌ Wrong Answer"]
self.labels_idk_expected = ["❌ Hallucination", "✅ I don't know"]

def _evaluate_answer_expected(self, row, answers_column):
generated_answer = row[answers_column].lower()
actual_answers = [ans.lower() for ans in row["answers"]]
return (
"✅ Answered Correctly" if any(ans in generated_answer for ans in actual_answers)
else "❎ Skipped" if generated_answer == "i don't know"
else "❌ Wrong Answer"
)

def _evaluate_idk_expected(self, row, answers_column):
generated_answer = row[answers_column].lower()
return (
"❌ Hallucination" if generated_answer != "i don't know"
else "✅ I don't know"
)

def _evaluate_single_row(self, row, answers_column):
is_impossible = row["is_impossible"]
return (
self._evaluate_answer_expected(row, answers_column) if not is_impossible
else self._evaluate_idk_expected(row, answers_column)
)

def evaluate_model(self, answers_column="generated_answer"):
self.y_pred = pd.Series(self.df.apply(self._evaluate_single_row, answers_column=answers_column, axis=1))
freq_series = self.y_pred.value_counts()

# 为每个场景计数行数
total_answer_expected = len(self.df[self.df['is_impossible'] == False])
total_idk_expected = len(self.df[self.df['is_impossible'] == True])

freq_answer_expected = (freq_series / total_answer_expected * 100).round(2).reindex(self.labels_answer_expected, fill_value=0)
freq_idk_expected = (freq_series / total_idk_expected * 100).round(2).reindex(self.labels_idk_expected, fill_value=0)
return freq_answer_expected.to_dict(), freq_idk_expected.to_dict()

def print_eval(self):
answer_columns=["generated_answer", "ft_generated_answer"]
baseline_correctness, baseline_idk = self.evaluate_model()
ft_correctness, ft_idk = self.evaluate_model(self.df, answer_columns[1])
print("When the model should answer correctly:")
eval_df = pd.merge(
baseline_correctness.rename("Baseline"),
ft_correctness.rename("Fine-Tuned"),
left_index=True,
right_index=True,
)
print(eval_df)
print("\n\n\nWhen the model should say 'I don't know':")
eval_df = pd.merge(
baseline_idk.rename("Baseline"),
ft_idk.rename("Fine-Tuned"),
left_index=True,
right_index=True,
)
print(eval_df)

def plot_model_comparison(self, answer_columns=["generated_answer", "ft_generated_answer"], scenario="answer_expected", nice_names=["Baseline", "Fine-Tuned"]):

results = []
for col in answer_columns:
answer_expected, idk_expected = self.evaluate_model(col)
if scenario == "answer_expected":
results.append(answer_expected)
elif scenario == "idk_expected":
results.append(idk_expected)
else:
raise ValueError("Invalid scenario")


results_df = pd.DataFrame(results, index=nice_names)
if scenario == "answer_expected":
results_df = results_df.reindex(self.labels_answer_expected, axis=1)
elif scenario == "idk_expected":
results_df = results_df.reindex(self.labels_idk_expected, axis=1)

melted_df = results_df.reset_index().melt(id_vars='index', var_name='Status', value_name='Frequency')
sns.set_theme(style="whitegrid", palette="icefire")
g = sns.catplot(data=melted_df, x='Frequency', y='index', hue='Status', kind='bar', height=5, aspect=2)

# Annotating each bar
for p in g.ax.patches:
g.ax.annotate(f"{p.get_width():.0f}%", (p.get_width()+5, p.get_y() + p.get_height() / 2),
textcoords="offset points",
xytext=(0, 0),
ha='center', va='center')
plt.ylabel("Model")
plt.xlabel("Percentage")
plt.xlim(0, 100)
plt.tight_layout()
plt.title(scenario.replace("_", " ").title())
plt.show()


# Compare the results by merging into one dataframe
evaluator = Evaluator(df)
# evaluator.evaluate_model(answers_column="ft_generated_answer")
# evaluator.plot_model_comparison(["generated_answer", "ft_generated_answer"], scenario="answer_expected", nice_names=["Baseline", "Fine-Tuned"])

# 可选择将结果保存到 JSON 文件中。
df.to_json("local_cache/100_val_ft.json", orient="records", lines=True)
df = pd.read_json("local_cache/100_val_ft.json", orient="records", lines=True)

evaluator.plot_model_comparison(["generated_answer", "ft_generated_answer"], scenario="answer_expected", nice_names=["Baseline", "Fine-Tuned"])

请注意,微调模型更经常跳过问题 – 并且犯的错误更少。这是因为微调模型更加保守,当不确定时会跳过问题。

evaluator.plot_model_comparison(["generated_answer", "ft_generated_answer"], scenario="idk_expected", nice_names=["Baseline", "Fine-Tuned"])

注意,经过微调的模型学会了更好地说“我不知道”而不是直接跳过问题。或者说,模型擅长跳过问题了。

观察结果

  1. 经过微调的模型更擅长说“我不知道”
  2. 幻觉从100%下降到15%经过微调
  3. 错误答案从17%下降到6%经过微调

正确答案也从83%下降到60%经过微调 - 这是因为经过微调的模型更加保守,更经常说“我不知道”。这是件好事,因为说“我不知道”总比给出错误答案要好。

话虽如此,我们希望提高模型的正确性,即使这会增加幻觉。我们希望找到一个既正确又保守的模型,在两者之间取得平衡。我们将使用Qdrant和Few-Shot Learning来实现这一目标。

💪 你已经完成了3分之2的任务!继续加油!

B部分:少样本学习

我们将从数据集中选择一些例子,包括答案不在上下文中的情况。然后,我们将使用这些例子创建一个提示,用于微调模型。然后我们将衡量微调模型的性能。

接下来是什么?

  1. 使用Qdrant对OpenAI模型进行微调 6.1 嵌入微调数据 6.2 嵌入问题
  2. 使用Qdrant改进RAG提示
  3. 评估

6. 使用Qdrant对OpenAI模型进行微调

到目前为止,我们一直在使用OpenAI模型来回答问题,而没有使用答案的示例。前面的步骤使其在上下文示例中表现更好,而这一步则帮助其泛化到未见数据,并尝试学习何时说“我不知道”以及何时给出答案。

这就是少样本学习的用武之地!

少样本学习是一种迁移学习类型,允许我们回答答案不在上下文中的问题。我们可以通过提供一些答案示例来实现这一点,模型将学会回答答案不在上下文中的问题。

5.1 嵌入训练数据

嵌入是将句子表示为浮点数数组的一种方式。我们将使用这些嵌入来找到与我们正在寻找的问题最相似的问题。

import os
from qdrant_client import QdrantClient
from qdrant_client.http import models
from qdrant_client.http.models import PointStruct
from qdrant_client.http.models import Distance, VectorParams

现在我们已经导入了Qdrant所需的库,

qdrant_client = QdrantClient(
url=os.getenv("QDRANT_URL"), api_key=os.getenv("QDRANT_API_KEY"), timeout=6000, prefer_grpc=True
)

collection_name = "squadv2-cookbook"

# 创建集合,此操作仅执行一次。
# qdrant_client.recreate_collection(
# 集合名称=集合名称,
# vectors_config=VectorParams(size=384, distance=Distance.COSINE),
# 很抱歉,您没有提供需要翻译的英文段落。请提供具体的文本,我将为您进行翻译。

from fastembed.embedding import DefaultEmbedding
from typing import List
import numpy as np
import pandas as pd
from tqdm.notebook import tqdm

tqdm.pandas()

embedding_model = DefaultEmbedding()

5.2 嵌入问题

接下来,您将嵌入整个训练集中的问题。您将使用问题之间的相似度来找到与我们正在寻找的问题最相似的问题。这是在RAG中使用的工作流程,以利用OpenAI模型具有更多示例的上下文学习能力。这就是我们在这里称之为少样本学习。

❗️⏰ 重要提示:此步骤可能需要长达3小时才能完成。请耐心等待。如果看到内存不足错误或内核崩溃,请将批量大小减小到32,重新启动内核并再次运行笔记本。此代码只需要运行一次。

generate_points_from_dataframe 函数的功能分解

  1. 初始化batch_size = 512total_batches 设置了一次处理多少个问题。这是为了防止内存问题。如果您的机器可以处理更多,请随时增加批量大小。如果内核崩溃,请将批量大小减小到32,然后重试。
  2. 进度条tqdm 提供了一个漂亮的进度条,以防您打瞌睡。
  3. 批处理循环:for循环遍历批次。start_idxend_idx 定义了要处理的DataFrame的切片。
  4. 生成嵌入batch_embeddings = embedding_model.embed(batch, batch_size=batch_size) - 这就是魔法发生的地方。您的问题被转换为嵌入。
  5. 生成 PointStruct:使用.progress_apply,它将每一行转换为一个PointStruct对象。这包括一个ID、嵌入向量和其他元数据。

返回PointStruct对象的列表,可用于在Qdrant中创建一个集合。

def generate_points_from_dataframe(df: pd.DataFrame) -> List[PointStruct]:
batch_size = 512
questions = df["question"].tolist()
total_batches = len(questions) // batch_size + 1

pbar = tqdm(total=len(questions), desc="Generating embeddings")

# 批量生成嵌入以提升性能
embeddings = []
for i in range(total_batches):
start_idx = i * batch_size
end_idx = min((i + 1) * batch_size, len(questions))
batch = questions[start_idx:end_idx]

batch_embeddings = embedding_model.embed(batch, batch_size=batch_size)
embeddings.extend(batch_embeddings)
pbar.update(len(batch))

pbar.close()

# 将嵌入转换为列表的列表
embeddings_list = [embedding.tolist() for embedding in embeddings]

# 创建一个临时DataFrame,用于存放嵌入向量和现有DataFrame的列
temp_df = df.copy()
temp_df["embeddings"] = embeddings_list
temp_df["id"] = temp_df.index

# 使用DataFrame的apply方法生成PointStruct对象
points = temp_df.progress_apply(
lambda row: PointStruct(
id=row["id"],
vector=row["embeddings"],
payload={
"question": row["question"],
"title": row["title"],
"context": row["context"],
"is_impossible": row["is_impossible"],
"answers": row["answers"],
},
),
axis=1,
).tolist()

return points

points = generate_points_from_dataframe(train_df)

将嵌入上传到Qdrant

请注意,配置Qdrant不在本笔记本的范围之内。请参考 Qdrant 获取更多信息。我们在上传时使用了600秒的超时,并使用了grpc压缩来加快上传速度。

operation_info = qdrant_client.upsert(
collection_name=collection_name, wait=True, points=points
)
print(operation_info)

6. 使用Qdrant改进RAG提示

现在我们已经将嵌入上传到Qdrant,我们可以使用Qdrant找到与我们正在寻找的问题最相似的问题。我们将使用前5个最相似的问题创建一个提示,以便我们可以用来微调模型。然后,我们将在相同的验证集上使用少量提示来衡量微调模型的性能!

我们的主要函数get_few_shot_prompt作为生成少样本学习提示的工具,它通过使用嵌入模型从Qdrant - 一个向量搜索引擎中检索相似的问题来实现这一点。以下是高级工作流程:

  1. 从Qdrant中检索类似的问题,其中答案在上下文中存在
  2. 从Qdrant中检索类似的问题,其中答案是不可能的,即期望的答案是“我不知道”,以便在上下文中找到
  3. 使用检索到的问题创建提示
  4. 使用提示微调模型
  5. 使用相同的提示技术在验证集上评估微调后的模型
def get_few_shot_prompt(row):

query, row_context = row["question"], row["context"]

embeddings = list(embedding_model.embed([query]))
query_embedding = embeddings[0].tolist()

num_of_qa_to_retrieve = 5

# 向Qdrant查询有答案的类似问题
q1 = qdrant_client.search(
collection_name=collection_name,
query_vector=query_embedding,
with_payload=True,
limit=num_of_qa_to_retrieve,
query_filter=models.Filter(
must=[
models.FieldCondition(
key="is_impossible",
match=models.MatchValue(
value=False,
),
),
],
)
)

# 向Qdrant查询类似问题,这些问题是无法回答的
q2 = qdrant_client.search(
collection_name=collection_name,
query_vector=query_embedding,
query_filter=models.Filter(
must=[
models.FieldCondition(
key="is_impossible",
match=models.MatchValue(
value=True,
),
),
]
),
with_payload=True,
limit=num_of_qa_to_retrieve,
)


instruction = """根据上下文回答以下问题。仅从上下文中回答。如果你不知道答案,请说“我不知道”。"""
# 如果还有下一个最佳问题,请将其添加到提示中。

def q_to_prompt(q):
question, context = q.payload["question"], q.payload["context"]
answer = q.payload["answers"][0] if len(q.payload["answers"]) > 0 else "I don't know"
return [
{
"role": "user",
"content": f"""问题:{问题}

上下文:{上下文}

答案:"""
},
{"role": "assistant", "content": answer},
]

rag_prompt = []

if len(q1) >= 1:
rag_prompt += q_to_prompt(q1[1])
if len(q2) >= 1:
rag_prompt += q_to_prompt(q2[1])
if len(q1) >= 1:
rag_prompt += q_to_prompt(q1[2])



rag_prompt += [
{
"role": "user",
"content": f"""问题:{query}

上下文:{row_context}

答案:"""
},
]

rag_prompt = [{"role": "system", "content": instruction}] + rag_prompt
return rag_prompt

# ⏰ 时间:2分钟
train_sample["few_shot_prompt"] = train_sample.progress_apply(get_few_shot_prompt, axis=1)

7. 使用Qdrant对OpenAI模型进行微调

7.1 上传微调数据到OpenAI

# 准备OpenAI文件格式,即JSONL格式的train_sample。
def dataframe_to_jsonl(df):
def create_jsonl_entry(row):
messages = row["few_shot_prompt"]
return json.dumps({"messages": messages})

jsonl_output = df.progress_apply(create_jsonl_entry, axis=1)
return "\n".join(jsonl_output)

with open("local_cache/100_train_few_shot.jsonl", "w") as f:
f.write(dataframe_to_jsonl(train_sample))

7.2 微调模型

运行时间:~15-30分钟

fine_tuner = OpenAIFineTuner(
training_file_path="local_cache/100_train_few_shot.jsonl",
model_name="gpt-3.5-turbo",
suffix="trnfewshot20230907"
)

model_id = fine_tuner.fine_tune_model()
model_id

# Let's try this out
completion = client.chat.completions.create(
model=model_id,
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{
"role": "user",
"content": "Can you answer the following question based on the given context? If not, say, I don't know:\n\nQuestion: What is the capital of France?\n\nContext: The capital of Mars is Gaia. Answer:",
},
{
"role": "assistant",
"content": "I don't know",
},
{
"role": "user",
"content": "Question: Where did Maharana Pratap die?\n\nContext: Rana Pratap's defiance of the mighty Mughal empire, almost alone and unaided by the other Rajput states, constitute a glorious saga of Rajput valour and the spirit of self sacrifice for cherished principles. Rana Pratap's methods of guerrilla warfare was later elaborated further by Malik Ambar, the Deccani general, and by Emperor Shivaji.\nAnswer:",
},
{
"role": "assistant",
"content": "I don't know",
},
{
"role": "user",
"content": "Question: Who did Rana Pratap fight against?\n\nContext: In stark contrast to other Rajput rulers who accommodated and formed alliances with the various Muslim dynasties in the subcontinent, by the time Pratap ascended to the throne, Mewar was going through a long standing conflict with the Mughals which started with the defeat of his grandfather Rana Sanga in the Battle of Khanwa in 1527 and continued with the defeat of his father Udai Singh II in Siege of Chittorgarh in 1568. Pratap Singh, gained distinction for his refusal to form any political alliance with the Mughal Empire and his resistance to Muslim domination. The conflicts between Pratap Singh and Akbar led to the Battle of Haldighati. Answer:",
},
{
"role": "assistant",
"content": "Akbar",
},
{
"role": "user",
"content": "Question: Which state is Chittorgarh in?\n\nContext: Chittorgarh, located in the southern part of the state of Rajasthan, 233 km (144.8 mi) from Ajmer, midway between Delhi and Mumbai on the National Highway 8 (India) in the road network of Golden Quadrilateral. Chittorgarh is situated where National Highways No. 76 & 79 intersect. Answer:",
},
],
)
print("Correct Answer: Rajasthan\nModel Answer:")
print(completion.choices[0].message)

运行时间:5-15分钟

df["ft_generated_answer_few_shot"] = df.progress_apply(answer_question, model=model_id, prompt_func=get_few_shot_prompt, axis=1)
df.to_json("local_cache/100_val_ft_few_shot.json", orient="records", lines=True)

8. 评估

但是模型的表现如何呢?让我们来比较一下到目前为止我们看过的3种不同模型的结果:

evaluator = Evaluator(df)
evaluator.plot_model_comparison(["generated_answer", "ft_generated_answer", "ft_generated_answer_few_shot"], scenario="answer_expected", nice_names=["Baseline", "Fine-Tuned", "Fine-Tuned with Few-Shot"])

这真是太神奇了——我们能够兼得两全其美!我们能够让模型既正确又保守:

  1. 模型的正确率为83%——与基础模型相同
  2. 模型仅在8%的情况下给出错误答案——相比基础模型的17%有所下降

接下来,让我们来看一下幻觉现象。我们希望减少幻觉现象,但不以牺牲正确性为代价。我们希望在两者之间取得平衡。在这里,我们取得了一个很好的平衡:

  1. 模型出现幻觉的时间为53% —— 从基础模型的100% 下降
  2. 模型在47%的时间内会说“我不知道” —— 从基础模型的从不 上升
evaluator.plot_model_comparison(["generated_answer", "ft_generated_answer", "ft_generated_answer_few_shot"], scenario="idk_expected", nice_names=["Baseline", "Fine-Tuned", "Fine-Tuned with Few-Shot"])

使用Qdrant进行少样本微调是控制和引导RAG系统性能的好方法。在这里,我们通过使用Qdrant找到相似问题,使模型比零样本更加自信。

您也可以使用Qdrant使模型更加保守。我们通过提供答案不在上下文中的问题示例来实现这一点。这会让模型更倾向于更频繁地说“我不知道”。

类似地,您也可以使用Qdrant通过提供答案在上下文中的问题示例来使模型更加自信。这会让模型更倾向于更频繁地给出答案。不过,这样做的代价是模型也会更频繁地产生幻觉。

您可以通过调整训练数据:问题和示例的分布,以及从Qdrant检索的示例的种类和数量来权衡这一点。

9. 结论

在这个笔记本中,我们演示了如何针对特定用例微调OpenAI模型。我们还演示了如何使用Qdrant和少样本学习来提高模型的性能。

聚合结果

到目前为止,我们已经分别查看了每个场景的结果,即每个场景总和为100。让我们将结果作为一个整体来查看,以更广泛地了解模型的表现:

类别 基础 微调 使用Qdrant微调
正确 44% 32% 44%
跳过 0% 18% 5%
错误 9% 3% 4%
幻觉 47% 7% 25%
我不知道 0% 40% 22%

观察结果

与基础模型相比

  1. 使用Qdrant进行少样本微调的模型在回答上下文中存在答案的问题时与基础模型一样好。
  2. 使用Qdrant进行少样本微调的模型在上下文中不存在答案时更擅长说“我不知道”。
  3. 使用Qdrant进行少样本微调的模型在减少幻觉方面表现更好。

与微调模型相比

  1. 使用Qdrant进行少样本微调的模型比微调模型获得更多正确答案:83% 的问题被正确回答,而微调模型为60%
  2. 使用Qdrant进行少样本微调的模型在决定在上下文中不存在答案时何时说“我不知道”方面表现更好。普通微调模式的跳过率为34%,而使用Qdrant进行少样本微调的模型为9%

现在,您应该能够:

  1. 注意正确答案数量和幻觉之间的权衡,以及训练数据集选择如何影响这一点!
  2. 为特定用例微调OpenAI模型,并使用Qdrant来提高您的RAG模型性能
  3. 开始评估您的RAG模型的性能。