自动合并检索器¶
在本 Notebook 中,我们将展示 AutoMergingRetriever
,它查看一组叶节点并递归地“合并”引用超过给定阈值的父节点的叶节点子集。这使我们能够将潜在地分散的、较小的上下文整合为可能有助于合成的较大上下文。
您可以自己在一组文档上定义这种层次结构,或者利用我们全新的文本解析器:一个 HierarchicalNodeParser,它接收一组候选文档并输出整个节点层次结构,从“粗粒度”到“细粒度”。
%pip install llama-index-llms-openai
%pip install llama-index-readers-file pymupdf
%load_ext autoreload
%autoreload 2
如果您在 Colab 上打开此 Notebook,可能需要安装 LlamaIndex 🦙。
!pip install llama-index
加载数据¶
首先加载 Llama 2 论文:https://arxiv.org/pdf/2307.09288.pdf。这将是我们的测试数据。
!mkdir -p 'data/'
!wget --user-agent "Mozilla" "https://arxiv.org/pdf/2307.09288.pdf" -O "data/llama2.pdf"
from pathlib import Path
from llama_index.readers.file import PDFReader
from llama_index.readers.file import PyMuPDFReader
loader = PyMuPDFReader()
# docs0 = loader.load_data(file=Path("./data/llama2.pdf"))
docs0 = loader.load(file_path=Path("./data/llama2.pdf"))
默认情况下,PDF reader 为每一页创建一个单独的文档。为了本 Notebook 的目的,我们将多个文档拼接成一个文档。这将帮助我们更好地突出自动合并功能,它可以在后续将“拼接”分块。
from llama_index.core import Document
doc_text = "\n\n".join([d.get_content() for d in docs0])
docs = [Document(text=doc_text)]
从文本中解析分块层次结构,加载到存储¶
在本节中,我们将使用 HierarchicalNodeParser
。它将输出一个节点层次结构,从具有较大分块大小的顶层节点到具有较小分块大小的子节点,其中每个子节点都有一个具有较大分块大小的父节点。
默认情况下,层次结构如下:
- 第一层:分块大小 2048
- 第二层:分块大小 512
- 第三层:分块大小 128
然后我们将这些节点加载到存储中。叶节点通过向量存储进行索引和检索 - 这些是首先通过相似性搜索直接检索到的节点。其他节点将从文档存储中检索。
from llama_index.core.node_parser import (
HierarchicalNodeParser,
SentenceSplitter,
)
node_parser = HierarchicalNodeParser.from_defaults()
nodes = node_parser.get_nodes_from_documents(docs)
len(nodes)
1029
这里我们导入一个简单的辅助函数,用于在节点列表中获取“叶”节点。这些是没有自己的子节点的节点。
from llama_index.core.node_parser import get_leaf_nodes, get_root_nodes
leaf_nodes = get_leaf_nodes(nodes)
len(leaf_nodes)
795
root_nodes = get_root_nodes(nodes)
# define storage context
from llama_index.core.storage.docstore import SimpleDocumentStore
from llama_index.core import StorageContext
from llama_index.llms.openai import OpenAI
docstore = SimpleDocumentStore()
# insert nodes into docstore
docstore.add_documents(nodes)
# define storage context (will include vector store by default too)
storage_context = StorageContext.from_defaults(docstore=docstore)
llm = OpenAI(model="gpt-3.5-turbo")
## Load index into vector index
from llama_index.core import VectorStoreIndex
base_index = VectorStoreIndex(
leaf_nodes,
storage_context=storage_context,
)
定义检索器¶
from llama_index.core.retrievers import AutoMergingRetriever
base_retriever = base_index.as_retriever(similarity_top_k=6)
retriever = AutoMergingRetriever(base_retriever, storage_context, verbose=True)
# query_str = "What were some lessons learned from red-teaming?"
# query_str = "Can you tell me about the key concepts for safety finetuning"
query_str = (
"What could be the potential outcomes of adjusting the amount of safety"
" data used in the RLHF stage?"
)
nodes = retriever.retrieve(query_str)
base_nodes = base_retriever.retrieve(query_str)
> Merging 4 nodes into parent node. > Parent node id: caf5f81c-842f-46a4-b679-6be584bd6aff. > Parent node text: We conduct RLHF by first collecting human preference data for safety similar to Section 3.2.2: an...
len(nodes)
3
len(base_nodes)
6
from llama_index.core.response.notebook_utils import display_source_node
for node in nodes:
display_source_node(node, source_length=10000)
节点 ID: d4d67180-71c8-4328-b3f1-1e98fa42ab69
Similarity 0.8694979150607424
文本: 我们还在表 35 中列出了安全和有益奖励模型之间不一致的两个定性示例。A.4.2 安全数据扩展的定性结果 在第 4.2.3 节中,我们定量研究了在模型 RLHF 中添加更多安全数据的影响。在这里,我们展示了几个样本,以定性检查我们在表 36、37 和 38 中扩展安全数据时模型行为的演变。总的来说,我们观察到 Llama 2-Chat 在使用更多安全数据后对不安全提示的响应变得更安全。
节点 ID: caf5f81c-842f-46a4-b679-6be584bd6aff
Similarity 0.86168727941324
文本: 我们通过首先收集安全的人类偏好数据进行 RLHF,类似于第 3.2.2 节:标注者编写他们认为会引发不安全行为的提示,然后比较多个模型对提示的响应,根据一组准则选择最安全的响应。然后我们使用人类偏好数据训练安全奖励模型(见第 3.2.2 节),并在 RLHF 阶段重用对抗性提示从模型中采样。在不损害有益性的情况下提高长尾安全鲁棒性 安全本质上是一个长尾问题,挑战来自于少数非常具体的案例。我们通过选取两个中间的 Llama 2-Chat 检查点——一个在 RLHF 阶段没有对抗性提示,另一个有——并使用我们的安全和有益奖励模型评估它们在我们测试集上的响应,来研究安全 RLHF 的影响。在图 14 中,我们绘制了安全 RM 在安全测试集上的得分分布变化(左)以及有益性 RM 在有益性测试集上的得分分布变化(右)。在图的左侧,我们观察到安全 RM 在安全集上的得分分布在进行安全微调并结合 RLHF 后向更高奖励分数偏移,并且接近零的长尾分布变薄。左上角出现了一个明显的聚类,表明模型安全性的提升。在右侧,我们在图 14 的右侧 y = x 线下方没有观察到任何聚集模式,这表明有益性得分分布在进行安全微调并结合 RLHF 后得以保留。换句话说,在有足够的有益性训练数据的情况下,增加额外的安全缓解阶段不会对模型在有益性上的表现产生任何明显的负面影响。表 12 显示了一个定性示例。安全数据扩展的影响。先前的研究(Bai et al., 2022a)已经观察到 LLM 的有益性和安全性之间存在紧张关系。为了更好地理解增加安全训练数据如何影响总体模型性能,特别是其有益性,我们通过调整 RLHF 阶段使用的安全数据量来研究安全数据扩展的趋势。
节点 ID: d9893bef-a5a7-4248-a0a1-d7c28800ae59
Similarity 0.8546977459150967
文本: 0 0.2 0.4 0.6 0.8 1.0 安全 RLHF 前的有益性 RM 得分 0.0 0.2 0.4 0.6 0.8 1.0 安全 RLHF 后的有益性 RM 得分 0 1000 0 1000 图 14:通过奖励模型得分分布衡量的安全 RLHF 影响。左:Meta 安全测试集上生成的安全奖励模型得分。样本在左上角聚类表明模型安全性的提升。
for node in base_nodes:
display_source_node(node, source_length=10000)
节点 ID: 16328561-9ff7-4307-8d31-adf6bb74b71b
Similarity 0.8770715326726375
文本: 表 12 显示了一个定性示例。安全数据扩展的影响。先前的研究(Bai et al., 2022a)已经观察到 LLM 的有益性和安全性之间存在紧张关系。为了更好地理解增加安全训练数据如何影响总体模型性能,特别是其有益性,我们通过调整 RLHF 阶段使用的安全数据量来研究安全数据扩展的趋势。
节点 ID: e756d327-1a28-4228-ac38-f8a831b1bf77
Similarity 0.8728111844788112
文本: 左上角出现了一个明显的聚类,表明模型安全性的提升。在右侧,我们在图 14 的右侧 y = x 线下方没有观察到任何聚集模式,这表明有益性得分分布在进行安全微调并结合 RLHF 后得以保留。换句话说,在有足够的有益性训练数据的情况下,增加额外的安全缓解阶段不会对模型在有益性上的表现产生任何明显的负面影响。表 12 显示了一个定性示例。安全数据扩展的影响。
节点 ID: d4d67180-71c8-4328-b3f1-1e98fa42ab69
Similarity 0.8697379697028405
文本: 我们还在表 35 中列出了安全和有益奖励模型之间不一致的两个定性示例。A.4.2 安全数据扩展的定性结果 在第 4.2.3 节中,我们定量研究了在模型 RLHF 中添加更多安全数据的影响。在这里,我们展示了几个样本,以定性检查我们在表 36、37 和 38 中扩展安全数据时模型行为的演变。总的来说,我们观察到 Llama 2-Chat 在使用更多安全数据后对不安全提示的响应变得更安全。
节点 ID: d9893bef-a5a7-4248-a0a1-d7c28800ae59
Similarity 0.855087365309258
文本: 0 0.2 0.4 0.6 0.8 1.0 安全 RLHF 前的有益性 RM 得分 0.0 0.2 0.4 0.6 0.8 1.0 安全 RLHF 后的有益性 RM 得分 0 1000 0 1000 图 14:通过奖励模型得分分布衡量的安全 RLHF 影响。左:Meta 安全测试集上生成的安全奖励模型得分。样本在左上角聚类表明模型安全性的提升。
节点 ID: d62ee107-9841-44b5-8b70-bc6487ad6315
Similarity 0.8492541852986794
文本: 在不损害有益性的情况下提高长尾安全鲁棒性 安全本质上是一个长尾问题,挑战来自于少数非常具体的案例。我们通过选取两个中间的 Llama 2-Chat 检查点——一个在 RLHF 阶段没有对抗性提示,另一个有——并使用我们的安全和有益奖励模型评估它们在我们测试集上的响应,来研究安全 RLHF 的影响。
节点 ID: 312a63b3-5e28-4fbf-a3e1-4e8dc0c026ea
Similarity 0.8488371951811564
文本: 我们通过首先收集安全的人类偏好数据进行 RLHF,类似于第 3.2.2 节:标注者编写他们认为会引发不安全行为的提示,然后比较多个模型对提示的响应,根据一组准则选择最安全的响应。然后我们使用人类偏好数据训练安全奖励模型(见第 3.2.2 节),并在 RLHF 阶段重用对抗性提示从模型中采样。
插入查询引擎¶
from llama_index.core.query_engine import RetrieverQueryEngine
query_engine = RetrieverQueryEngine.from_args(retriever)
base_query_engine = RetrieverQueryEngine.from_args(base_retriever)
response = query_engine.query(query_str)
> Merging 4 nodes into parent node. > Parent node id: 3671b20d-ea5e-4afc-983e-02be6ee8302d. > Parent node text: We conduct RLHF by first collecting human preference data for safety similar to Section 3.2.2: an...
print(str(response))
Adjusting the amount of safety data used in the RLHF stage could potentially have the following outcomes: 1. Improved model safety: Increasing the amount of safety data used in RLHF may lead to improvements in model safety. This means that the model becomes better at responding to unsafe prompts and avoids generating unsafe or harmful outputs. 2. Thinning out of the long tail of safety RM scores: Increasing the amount of safety data may result in a shift in the distribution of safety reward model (RM) scores towards higher reward scores. This means that the model becomes more consistent in generating safe responses and reduces the occurrence of low safety scores. 3. Preservation of helpfulness performance: Adjusting the amount of safety data used in RLHF is not expected to negatively impact model performance on helpfulness. This means that the model's ability to generate helpful responses is maintained even after incorporating additional safety training. 4. Gathering pattern in helpfulness RM scores: There is no observed gathering pattern below the y = x line in the distribution of helpfulness RM scores after safety tuning with RLHF. This suggests that the helpfulness score distribution is preserved, indicating that the model's helpfulness performance is not significantly degraded by the addition of safety mitigation measures. Overall, adjusting the amount of safety data used in the RLHF stage aims to strike a balance between improving model safety without compromising its helpfulness performance.
base_response = base_query_engine.query(query_str)
print(str(base_response))
Adjusting the amount of safety data used in the RLHF stage could potentially lead to improvements in model safety. This can be observed by a clear cluster appearing on the top-left corner, suggesting enhanced model safety. Additionally, it is indicated that the helpfulness score distribution is preserved after safety tuning with RLHF, indicating that the addition of safety data does not negatively impact model performance on helpfulness.
from llama_index.core.evaluation import DatasetGenerator, QueryResponseDataset
from llama_index.llms.openai import OpenAI
import nest_asyncio
nest_asyncio.apply()
# NOTE: run this if the dataset isn't already saved
# Note: we only generate from the first 20 nodes, since the rest are references
eval_llm = OpenAI(model="gpt-4")
dataset_generator = DatasetGenerator(
root_nodes[:20],
llm=eval_llm,
show_progress=True,
num_questions_per_chunk=3,
)
eval_dataset = await dataset_generator.agenerate_dataset_from_nodes(num=60)
eval_dataset.save_json("data/llama2_eval_qr_dataset.json")
# optional
eval_dataset = QueryResponseDataset.from_json(
"data/llama2_eval_qr_dataset.json"
)
比较结果¶
我们对每个检索器运行评估:正确性、语义相似性、相关性和忠实度。
import asyncio
import nest_asyncio
nest_asyncio.apply()
from llama_index.core.evaluation import (
CorrectnessEvaluator,
SemanticSimilarityEvaluator,
RelevancyEvaluator,
FaithfulnessEvaluator,
PairwiseComparisonEvaluator,
)
from collections import defaultdict
import pandas as pd
# NOTE: can uncomment other evaluators
evaluator_c = CorrectnessEvaluator(llm=eval_llm)
evaluator_s = SemanticSimilarityEvaluator(llm=eval_llm)
evaluator_r = RelevancyEvaluator(llm=eval_llm)
evaluator_f = FaithfulnessEvaluator(llm=eval_llm)
# pairwise_evaluator = PairwiseComparisonEvaluator(llm=eval_llm)
from llama_index.core.evaluation.eval_utils import (
get_responses,
get_results_df,
)
from llama_index.core.evaluation import BatchEvalRunner
eval_qs = eval_dataset.questions
qr_pairs = eval_dataset.qr_pairs
ref_response_strs = [r for (_, r) in qr_pairs]
pred_responses = get_responses(eval_qs, query_engine, show_progress=True)
base_pred_responses = get_responses(
eval_qs, base_query_engine, show_progress=True
)
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 60/60 [00:07<00:00, 8.17it/s]
import numpy as np
pred_response_strs = [str(p) for p in pred_responses]
base_pred_response_strs = [str(p) for p in base_pred_responses]
evaluator_dict = {
"correctness": evaluator_c,
"faithfulness": evaluator_f,
"relevancy": evaluator_r,
"semantic_similarity": evaluator_s,
}
batch_runner = BatchEvalRunner(evaluator_dict, workers=2, show_progress=True)
eval_results = await batch_runner.aevaluate_responses(
eval_qs, responses=pred_responses, reference=ref_response_strs
)
base_eval_results = await batch_runner.aevaluate_responses(
eval_qs, responses=base_pred_responses, reference=ref_response_strs
)
results_df = get_results_df(
[eval_results, base_eval_results],
["Auto Merging Retriever", "Base Retriever"],
["correctness", "relevancy", "faithfulness", "semantic_similarity"],
)
display(results_df)
名称 | 正确性 | 相关性 | 忠实度 | 语义相似性 | |
---|---|---|---|---|---|
0 | 自动合并检索器 | 4.266667 | 0.916667 | 0.95 | 0.962196 |
1 | 基线检索器 | 4.208333 | 0.916667 | 0.95 | 0.960602 |
分析:结果大致相同。
我们还尝试使用成对评估器查看 GPT-4 偏好哪个答案。
batch_runner = BatchEvalRunner(
{"pairwise": pairwise_evaluator}, workers=10, show_progress=True
)
pairwise_eval_results = await batch_runner.aevaluate_response_strs(
eval_qs,
response_strs=pred_response_strs,
reference=base_pred_response_strs,
)
pairwise_score = np.array(
[r.score for r in pairwise_eval_results["pairwise"]]
).mean()
pairwise_score
0.525
分析:成对比较得分衡量了候选答案(使用自动合并检索器)与基线答案(使用基线检索器)相比被偏好的百分比。这里我们看到结果大致持平。