
Andrei • 2024-03-20
在网络上检索隐私安全文档
在一篇最近的博文中,我们介绍了 llama-index-networks
库扩展,它使得构建用户可以查询的 RAG 系统网络成为可能。这种网络的好处是显而易见的:连接到用户可能无法访问的多样化知识存储,意味着对更广泛的查询提供更准确的响应。
然而,这些网络的一个主要注意事项是,网络上共享的数据应该是隐私安全的。在这篇博文中,我们将演示如何将私密敏感数据转化为隐私安全版本,然后可以安全地在网络上共享。为此,我们将依赖于隐私增强技术领域的一些最新发展。
Alex、Bob 和 Beth 的故事仍在继续
为了说明这一切,我们将再次使用我们虚构的三个角色:Alex、Bob 和 Beth。快速回顾一下,Alex 是一个数据消费者,他想访问 Bob 和 Beth 拥有并愿意提供的数据源。
我们展示了如何通过 llama-index-networks
采取以下步骤来实现这样的数据协作
- Bob 和 Beth 都构建了各自的 QueryEngine(LlamaIndex 术语中的 RAG)
- Bob 和 Beth 都通过 ContributorService 暴露他们的 QueryEngine
- Alex 构建了一个连接到 Bob 和 Beth 的 ContributorService 的 NetworkQueryEngine
在故事的第二部分,我们增加了一个复杂之处:Bob 和 Beth 拥有私密敏感数据,在分享给 Alex 之前必须进行仔细保护。换句话说,我们需要在上述步骤之前增加一个步骤 0.,即对私有数据集应用保护措施。
保护数据(或更具体地说,数据主体)的措施取决于使用案例因素,例如数据涉及什么以及打算如何共享和最终处理。通常会应用去匿名化技术,例如擦除 PII(即个人可识别信息)。然而,在这篇博文中,我们重点介绍另一种隐私增强技术,称为差分隐私。

第 2 部分:Alex、Bob 和 Beth 的故事。这一次 Bob 和 Beth 有敏感数据想要共享,但除非在网络上共享之前应用保护措施,否则不能共享。
侧边栏:差分隐私入门
简而言之,差分隐私是一种方法,它提供数学保证(在一定概率水平内),即使对手只看到将私有数据集通过受保护的数据处理步骤运行后的输出,也无法确定特定个体是否属于该私有数据集。换句话说,无法从差分隐私算法的输出中得知个体是否包含在私有数据集中。
通过防御数据集包含的威胁,我们降低了对手能够将私有数据与其外部来源链接以了解更多关于数据主体信息并可能导致更多隐私损害(如扭曲)的风险。

差分隐私简要介绍。
回到 Alex、Bob 和 Beth 的故事,为了保护 Bob 和 Beth 的数据,我们将利用一种算法,该算法使用预训练的 LLM 创建满足差分隐私数学保证的私有数据合成副本。该算法由 Xinyu Tang 等人提出,发表在 ICLR 2024 的论文《Privacy-preserving in-context learning with differentially private few-shot generation》中。正是这些合成副本可以用于在网络上共享!
至此,我们有了额外的隐私顾虑,而我们的差分隐私方法意味着我们必须采取以下步骤来促进这次数据协作。
- Bob 和 Beth 创建他们私有数据集的隐私安全合成副本
- Bob 和 Beth 都基于他们的合成数据集构建了各自的 QueryEngine
- Bob 和 Beth 都通过 ContributorService 暴露他们的 QueryEngine
- Alex 构建了一个连接到 Bob 和 Beth 的 ContributorService 的 NetworkQueryEngine
创建私有数据集的差分隐私合成副本
幸运的是,对于步骤 0.,我们可以使用 DiffPrivateSimpleDataset
包。
from llama_index.core.llama_datasets.simple import LabelledSimpleDataset
from llama_index.packs.diff_private_simple_dataset.base import PromptBundle
from llama_index.packs.diff_private_simple_dataset import DiffPrivateSimpleDatasetPack
from llama_index.llms.openai import OpenAI
import tiktoken
# Beth uses `DiffPrivateSimpleDatasetPack` to generate synthetic copies
llm = OpenAI(
model="gpt-3.5-turbo-instruct",
max_tokens=1,
logprobs=True,
top_logprobs=5, # OpenAI only allows for top 5 next token
) # as opposed to entire vocabulary
tokenizer = tiktoken.encoding_for_model("gpt-3.5-turbo-instruct")
beth_private_dataset: LabelledSimpleDataset = ... # a dataset that contains
# examples with two attributes
# `text` and `reference_label`
beth_synthetic_generator = DiffPrivateSimpleDatasetPack(
llm=llm,
tokenizer=tokenizer,
prompt_bundle=prompt_bundle, # params for preparing required prompts
simple_dataset=simple_dataset, # to generate the synthetic examples
)
beth_synthetic_dataset = await beth_synthetic_generator.arun(
size=3, # number of synthetic observations to create
sigma=0.5 # param that determines the level of privacy
)
有了合成数据集,Bob 和 Beth 就可以应用我们在前一篇博文中介绍的步骤来构建他们的隐私安全 QueryEngine。值得在这里提到的是,正如论文作者所说,合成副本可以在下游任务中随意使用多次,而不会产生额外的隐私成本!(这是因为差分隐私的后处理特性。)
示例:Symptom2Disease
在本博文的这一部分,我们将介绍一个实际的隐私安全网络应用示例,该示例基于 Symptom2Disease 数据集。该数据集包含 1,200 个示例,每个示例都包含“症状”描述以及相关的“疾病”标签——数据集包含 24 个不同疾病标签的观察结果。我们将数据集分成两个不相交的子集,一个用于训练,另一个用于测试。此外,我们将这个原始数据集视为私有的,需要在网络上共享之前采取保护措施。
生成 Symptom2Disease 的隐私安全合成观察结果
我们使用训练子集并对其应用 DiffPrivateSimpleDatasetPack
,以便生成隐私安全、合成的观察结果。但在这样做之前,我们首先需要将原始的 Symptom2Disease 数据集转换为 LabelledSimpleDataset
对象。
import pandas as pd
from sklearn.model_selection import train_test_split
from llama_index.core.llama_dataset.simple import (
LabelledSimpleDataExample,
LabelledSimpleDataset,
)
from llama_index.core.llama_dataset.base import CreatedBy, CreatedByType
# load the Symptom2Disease.csv file
df = pd.read_csv("Symptom2Disease.csv")
train, test = train_test_split(df, test_size=0.2)
# create a LabelledSimpleDataset (which is what the pack works with)
examples = []
for index, row in df.iterrows():
example = LabelledSimpleDataExample(
reference_label=row["label"],
text=row["text"],
text_by=CreatedBy(type=CreatedByType.HUMAN),
)
examples.append(example)
simple_dataset = LabelledSimpleDataset(examples=examples)
现在我们可以使用 llama-pack 来创建我们的合成观察结果。
import llama_index.core.instrumentation as instrument
from llama_index.core.llama_dataset.simple import LabelledSimpleDataset
from llama_index.packs.diff_private_simple_dataset.base import PromptBundle
from llama_index.packs.diff_private_simple_dataset import DiffPrivateSimpleDatasetPack
from llama_index.llms.openai import OpenAI
import tiktoken
from .event_handler import DiffPrivacyEventHandler
import asyncio
import os
NUM_SPLITS = 3
T_MAX = 150
llm = OpenAI(
model="gpt-3.5-turbo-instruct",
max_tokens=1,
logprobs=True,
top_logprobs=5,
)
tokenizer = tiktoken.encoding_for_model("gpt-3.5-turbo-instruct")
prompt_bundle = PromptBundle(
instruction=(
"You are a patient experiencing symptoms of a specific disease. "
"Given a label of disease type, generate the chosen type of symptoms accordingly.\n"
"Start your answer directly after 'Symptoms: '. Begin your answer with [RESULT].\n"
),
label_heading="Disease",
text_heading="Symptoms",
)
dp_simple_dataset_pack = DiffPrivateSimpleDatasetPack(
llm=llm,
tokenizer=tokenizer,
prompt_bundle=prompt_bundle,
simple_dataset=simple_dataset,
)
synthetic_dataset = await dp_simple_dataset_pack.arun(
sizes=3,
t_max=T_MAX,
sigma=1.5,
num_splits=NUM_SPLITS,
num_samples_per_split=8, # number of private observations to create a
) # synthetic obsevation
synthetic_dataset.save_json("synthetic_dataset.json")
创建一个有两个贡献者的网络
接下来,我们设想有两个贡献者,他们各自拥有自己的 Symptom2Disease 数据集。特别是,我们将 24 类疾病分成两个不相交的集合,并认为每个贡献者只拥有其中一个集合。请注意,我们在完整的训练集上创建了合成观察结果,尽管我们也可以轻松地在拆分后的数据集上进行此操作。
现在我们有了合成观察结果,我们可以遵循 Alex、Bob 和 Beth 故事中定义的步骤 1 到 3 的稍微修改版本。这里的修改是我们使用了 Retriever 而不是 QueryEngine(Retriever 或 QueryEngine 的选择完全取决于用户)。
步骤 1:贡献者在他们的合成数据集之上构建 Retriever。
import os
from llama_index.core import VectorStoreIndex
from llama_index.core.llama_dataset.simple import LabelledSimpleDataset
from llama_index.core.schema import TextNode
# load the synthetic dataset
synthetic_dataset = LabelledSimpleDataset.from_json(
"./data/contributor1_synthetic_dataset.json"
)
nodes = [
TextNode(text=el.text, metadata={"reference_label": el.reference_label})
for el in synthetic_dataset[:]
]
index = VectorStoreIndex(nodes=nodes)
similarity_top_k = int(os.environ.get("SIMILARITY_TOP_K"))
retriever = index.as_retriever(similarity_top_k=similarity_top_k)
步骤 2:贡献者通过 ContributorRetrieverService 暴露他们的 Retriever。
from llama_index.networks.contributor.retriever.service import (
ContributorRetrieverService,
ContributorRetrieverServiceSettings,
)
settings = ContributorRetrieverServiceSettings() # loads from .env file
service = ContributorRetrieverService(config=settings, retriever=retriever)
app = service.app
步骤 3:定义连接到 ContributorRetrieverService 的 NetworkRetriever。
from llama_index.networks.network.retriever import NetworkRetriever
from llama_index.networks.contributor.retriever import ContributorRetrieverClient
from llama_index.postprocessor.cohere_rerank import CohereRerank
# ContributorRetrieverClient's connect to the ContributorRetrieverService
contributors = [
ContributorRetrieverClient.from_config_file(
env_file=f"./client-env-files/.env.contributor_{ix}.client"
)
for ix in range(1, 3)
]
reranker = CohereRerank(top_n=5)
network_retriever = NetworkRetriever(
contributors=contributors, node_postprocessors=[reranker]
)
建立 NetworkRetriever
后,我们可以根据查询从两个贡献者的数据中检索合成观察结果。
related_records = network_retriever.aretrieve("Vomitting and nausea")
print(related_records) # contain symptoms/disease records that are similar to
# to the queried symptoms.
评估 NetworkRetriever
为了评估 NetworkRetriever
的有效性,我们利用测试集来计算两个传统的检索指标,即:命中率(hit rate)和平均倒数排名(mean reciprocal rank)。
- 命中率:如果任何检索到的节点与测试查询(症状)具有相同的疾病标签,则发生命中。命中率是总命中数除以测试集的大小。
- 平均倒数排名:类似于命中率,但现在我们考虑与测试查询共享相同疾病标签的第一个检索到的节点的位置。如果没有这样的检索节点,则该测试的倒数排名等于 0。平均倒数排名只是测试集中所有倒数排名的平均值。
除了评估 NetworkRetriever
,我们还考虑了两个基线,它们分别代表仅从单个贡献者的合成数据集中进行检索。

在上图中,我们观察到 NetworkRetriever 在测试集中优于两个单独的贡献者 Retriever。然而,这并不难理解,因为网络检索器可以访问更多数据,因为它既可以访问贡献者的合成观察结果——这毕竟是网络的意义所在!
对这些结果进行检查后,可以得出另一个重要的观察。也就是说,隐私安全的合成观察结果确实在保护隐私的同时,仍然保持了原始数据集的可用性。这通常是在应用差分隐私等隐私措施时的一个担忧,因为差分隐私会加入噪声来保护数据。过多的噪声会提供高水平的隐私,但同时可能会使数据在下游任务中变得无用。从上表中我们可以看出,至少对于这个例子(尽管它也证实了论文的结果),合成观察结果仍然与测试集匹配得很好,而测试集确实是真实的观察结果(即不是合成生成的)。
最后,可以通过噪声参数 sigma
来控制这种隐私级别。在上面的示例中,我们使用了 1.5 的 sigma
值,这对于该数据集来说相当于一个 epsilon
(即隐私损失度量)值 1.3。(隐私损失水平在 0 到 1 之间通常被认为是相当私密的。)下面,我们分享了使用 0.5 的 sigma
值所产生的评估结果,这相当于一个 epsilon
值 15.9——更高的 epsilon
值或隐私损失意味着更低的隐私。
# use the `DiffPrivacySimpleDatasetPack` to get the value of epsilon
epsilon = dp_simple_dataset_pack.sigma_to_eps(
sigma=0.5,
mechanism="gaussian",
size=3*24,
max_token_cnt=150 # number of max tokens to generate per synthetic example
)

因此,在比较不同隐私水平下的评估指标后,我们发现当我们使用隐私水平较高的合成观察结果时,性能会受到一些影响,表现为命中率和平均倒数排名的下降。这确实是隐私权衡的一个例证。如果我们看看合成数据集中的一些例子,也许就能明白为什么会发生这种情况。
# synthetic example epsilon = 1.3
{
"reference_label": "Psoriasis",
"text": "[RESULTS] red, scalloped patches on skin; itching and burning sensation; thick, pitted nails on fingers and toes; joint discomfort; swollen and stiff joints; cracked and painful skin on palms and feet",
"text_by": {
"model_name": "gpt-3.5-turbo-instruct",
"type": "ai"
}
},
# synthetic example epsilon = 15.9
{
"reference_label": "Migraine",
"text": "Intense headache, sensitivity to light and sound, nausea, vomiting, vision changes, and fatigue.",
"text_by": {
"model_name": "gpt-3.5-turbo-instruct",
"type": "ai"
}
},
我们可以看到,与隐私水平较低的合成数据集相比,隐私水平较高的合成数据集在文本中标点符号方面不如干净。这是有道理的,因为差分隐私算法会向下一词元生成的机制中添加噪声。因此,大幅扰动这个过程会影响 LLM 的指令遵循能力。
总结
- 我们使用差分隐私创建了隐私安全的合成观察结果,以允许原本不可能实现的私有数据协作。
- 我们展示了 NetworkRetriever 的优势,它比单个贡献者 Retriever 可以访问更多数据。
- 我们展示了不同隐私程度对合成观察结果的影响,并进一步影响了 NetworkRetriever。
了解更多!
为了更深入地了解这篇博文的内容,我们在下方分享了一些链接