宣布 LlamaCloud 全面上市(以及我们的 1900 万美元 A 轮融资)!
LlamaIndex

Roey Ben Chaim 2024-03-13

PII 检测器:在 RAG 中侵犯隐私

几天前,我在 DataStax 总部有机会参加 LlamaIndex RAG-A-THON。在整个周末期间,我们必须实现一个利用检索增强生成 (RAG) 技术的解决方案。

由于我有网络安全背景,我倾向于关注 RAG 技术的安全隐患和障碍。首先想到的一个问题是,许多非结构化数据未经清理,可能包含敏感数据。

PII:是什么?为什么?

PII 是 Personally Identifiable Information(个人身份信息)的缩写。它指的是任何可以用来识别特定个人的信息。
这可能包括姓名、地址、电话号码、电子邮件地址、社会安全号码和财务信息。

处理 PII 很重要的原因有几个

  1. 隐私:PII 通常包含敏感和私密的详细信息(如地址),因此保护它可以维护客户的隐私。
  2. 身份盗窃:PII 也可能导致身份盗窃(例如,某人的社会安全号码被泄露)。
  3. 法律合规:保护 PII 也是法律要求。许多国家和地区已经颁布了要求组织保护 PII 的法律法规。欧盟的 GDPR(通用数据保护条例)或美国的 HIPAA(健康保险可携性和责任法案)都规范了我们处理 PII 的方式。
  4. 信任和声誉:数据泄露或 PII 处理不当会严重损害一个人的声誉和信任。
  5. 财务安全:PII 可能包含财务信息,如信用卡号和银行详细信息。PII 泄露可能导致欺诈性交易。
  6. 国家安全问题:以上所有内容在主权环境中都至关重要。

RAG 中的 PII

列出的所有内容几乎适用于所有利用 RAG 的应用程序。请记住,RAG 技术包含两个组件——模型和向量数据库。因此,每个组件都需要解决 PII 问题。

模型

语言模型是基于可能包含真实世界数据的大型数据集进行训练的,这些数据可能包含 PII 和客户数据。当模型生成文本时,存在生成包含 PII 内容的风险。如果您正在创建一个多租户应用程序并希望防止数据泄露,这一点更为关键。可以通过过滤或匿名化响应来减轻这种风险。通过对已删除任何敏感信息的匿名数据训练模型,是防止 PII 泄露的更好方法。

向量数据库

向量数据库,就像普通数据库一样,不应明文存储敏感信息。此类信息应仅使用加密、哈希、加盐和访问控制进行存储。话虽如此,也应确保数据库返回的相似性搜索不会检索个人数据。

此外,GDPR 和 HIPAA 等各种法规也适用于此处。因此,如果原始数据包含 PII,您可能需要在欧洲或根据法规在任何其他区域添加另一个实例。持久化数据应加密或哈希处理(并额外加盐)。

介绍:Presidio

Presidio 是由微软维护的开源库(请参阅我们的 GitHub 仓库)。它来源于拉丁词 praesidium,意思是“保护”或“驻军”。

  • 它使组织能够使用统一的 SDK 保护隐私。
  • 它提供快速的识别匿名化模块,用于文本和图像中的私人实体,例如信用卡号、姓名、位置、社会安全号码、比特币钱包、美国电话号码、财务数据等等。

免责声明:没有任何东西是万无一失的。确保敏感数据匿名化是您的责任。

Presidio 如何工作?

  1. 预定义自定义 PII 识别器利用命名实体识别 (NER)正则表达式基于规则的逻辑校验和(例如比特币地址验证)。
  2. 它是可扩展的,因此您可以添加自己的实体和检测机制。
  3. 它是可定制的,因此您可以创建自己的匿名化器,并排除/包含某些实体(例如,排除地理位置的匿名化)。

LlamaIndex 后处理器

已经有一些使用 NER 模型和 LLM 的 PII 集成!这些被实现为在管道末端运行的后处理器

from llama_index.postprocessor import NERPIINodePostprocessor
from llama_index import ServiceContext
from llama_index.schema import TextNode

text = """
My name is Roey Ben Chaim and my credit card number is 4095-2609-9393-4932. 
My email is robo@presidio.site and I live in Amsterdam.
Have you been to a Pálmi Einarsson concert before?
What is the limit for card 4158112277712? My IBAN is GB90YNTU67299444055881. 
What's your last name? Bob, it's Bob.
My great great grandfather was called Yulan Peres, 
and my great great grandmother was called Jennifer Holst
I can't browse to your site, keep getting address 179.177.214.91 blocked error
Just posted a photo https://www.FilmFranchise.dk/
"""

node = TextNode(text=text)

service_context = ServiceContext.from_defaults()
processor = NERPIINodePostprocessor(service_context=service_context)

from llama_index.schema import NodeWithScore

new_nodes = processor.postprocess_nodes([NodeWithScore(node=node)])
print(new_nodes[0].node.get_text())

运行上述代码的结果如下

My name is [PER_12] and my credit card number is 4095-2609-9393-4932. 
My email is robo@presidio.site and I live in [LOC_123].
Have you been to a [PER_153] concert before?
What is the limit for card 4158112277712? My IBAN is GB90YNTU67299444055881. 
What's your last name? [PER_286], it's [PER_286].
My great great grandfather was called [PER_339], 
and my great great grandmother was called [PER_395]
I can't browse to your site, keep getting address 179.177.214.91 blocked error
Just posted a photo https://www.[ORG_521].dk/

如本例所示,虽然 NER 模型在检测 PII 方面做得不错,但它们可能会遗漏一些实体,如 IBAN 代码、信用卡号、电子邮件、医疗许可证等。

Presidio 检测到的即时可用实体比传统模型更多。这是可能的,因为 Presidio 利用了几种方法来检测 PII——从 NER 模型到正则表达式和基于规则的逻辑。

将 Presidio 与 LlamaIndex 集成

我最终集成了 PresidioPIINodePostprocessor,它将文本作为输入并对其进行掩码。使用 Presidio 的分析器和匿名化器可以实现这一点

from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine

analyzer = AnalyzerEngine(supported_languages=["en"])
results = analyzer.analyze(text=text, language='en')
engine = AnonymizerEngine()
new_text = engine.anonymize(text=text, analyzer_results=results)

这非常有趣且简单。然而,给定输入文本“Alice and Bob are friends”,输出将是:“<PERSON> and <PERSON> are friends”。我不能接受这样。

所以,我添加了一个计数器,并将原始值与掩码值进行映射,确保当实体再次出现时,使用之前请求的值

def anonymize_function(origin, entity_type):
    nonlocal pii_counter
    nonlocal inverted_mapping
    nonlocal mapping
    if entity_type not in inverted_mapping:
        inverted_mapping[entity_type] = {}
    typed_mapping = inverted_mapping[entity_type]
    if origin in typed_mapping:
        return typed_mapping[origin]
    new_value = f"<{entity_type}_{pii_counter}>"
    typed_mapping[origin] = new_value
    mapping[new_value]=origin
    pii_counter+=1
    return typed_mapping[origin]

from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
from presidio_anonymizer.entities import OperatorConfig

analyzer = AnalyzerEngine(supported_languages=["en"])
results = analyzer.analyze(text=text, language='en')
engine = AnonymizerEngine()
new_text = engine.anonymize(text=text, analyzer_results=results, 
                            operators={"DEFAULT": OperatorConfig("custom", 
                            params={"lambda": anonymize_function})})

注意:目前 presidio 不包含实体类型作为 lambda 函数的输入参数,所以我不得不添加这个功能。

测试和基准

一旦这一切启动并运行,我就可以使用上一次运行的文本调用新添加的 presidio 后处理器

from llama_index.postprocessor import PresidioPIINodePostprocessor
from llama_index import ServiceContext
from llama_index.schema import TextNode

text = """
My name is Roey Ben Chaim and my credit card number is 4095-2609-9393-4932. 
My email is robo@presidio.site and I live in Amsterdam.
Have you been to a Pálmi Einarsson concert before?
What is the limit for card 4158112277712? My IBAN is GB90YNTU67299444055881. 
What's your last name? Bob, it's Bob.
My great great grandfather was called Yulan Peres, 
and my great great grandmother was called Jennifer Holst
I can't browse to your site, keep getting address 179.177.214.91 blocked error
Just posted a photo https://www.FilmFranchise.dk/
"""

node = TextNode(text=text)

service_context = ServiceContext.from_defaults()
processor = PresidioPIINodePostprocessor(service_context=service_context)

from llama_index.schema import NodeWithScore

new_nodes = processor.postprocess_nodes([NodeWithScore(node=node)])
print(new_nodes[0].node.get_text())

运行上述代码的结果如下

My name is <PERSON_12> and my credit card number is <CREDIT_CARD_11>. 
My email is <EMAIL_ADDRESS_10> and I live in <LOCATION_9>.
Have you been to a <PERSON_8> concert before?
What is the limit for card <CREDIT_CARD_7>? My IBAN is <IBAN_CODE_6>. 
What's your last name? <PERSON_5>, it's <PERSON_5>.
My great great grandfather was called <PERSON_4>, 
and my great great grandmother was called <PERSON_3>
I can't browse to your site, keep getting address <IP_ADDRESS_2> blocked error
Just posted a photo <URL_1>

总体而言,Presidio 检测到 12 个实体,而另一个 NER 解决方案检测到 8 个。请注意,信用卡号、电子邮件地址、IBAN、IP 地址和 URL(至少其中一部分)没有被检测到。

我很好奇这些字符串在 LLM 上的解析效果如何,因此我填充了索引并查询了以下内容

from llama_index import VectorStoreIndex

index = VectorStoreIndex([n.node for n in new_nodes])
response = index.as_query_engine().query(
    "What is my name?"
)
print(response)

结果如下

Your name is <PERSON_12>.

结果如何

无论如何,这个项目在 RAG-A-THON 中获得了第三名(连续赛道)。

注意:这张图片不包含 PII

更新

Presidio 现已作为后处理器完全集成到 LlamaIndex 中,请参阅此 notebook,了解如何使用 Presidio 进行 PII 掩码。接下来的步骤将是添加更多定制和匿名化选项。