notebooks/zh-CN/llm_gateway_pii_detection.ipynb (568 lines of code) (raw):

{ "cells": [ { "cell_type": "markdown", "id": "be3d6762-59dc-4339-8118-d0f3cf158cc5", "metadata": {}, "source": [ "# 个人身份信息(PII)检测的 LLM 网关\n", "*作者: [Anthony Susevski](https://github.com/asusevski)*" ] }, { "cell_type": "markdown", "id": "e8e0d0f7-25e1-4fc7-a4c1-0c5196f776c5", "metadata": {}, "source": [ "采用大语言模型(LLM)进行企业级应用时,常见的投诉之一就是数据隐私问题,尤其是对于处理敏感数据的团队来说。尽管开源模型通常是一个不错的选择,*如果可能的话应该尝试使用*,但有时我们只想快速演示一下,或者有充分的理由使用 LLM API。在这种情况下,最好采用某种网关来处理个人身份信息(PII)数据的清洗,从而降低 PII 泄露的风险。\n", "\n", "总部位于加拿大多伦多的金融科技公司 **Wealthsimple** 已经为了这个目的 [开源了一个代码库](https://github.com/wealthsimple/llm-gateway)。在本 Notebook 中,我们将探索如何利用这个代码库,在向 LLM 提供商发出 API 调用之前,对数据进行清洗。为此,我们将使用来自[AI4Privacy](https://huggingface.co/datasets/ai4privacy/pii-masking-200k)的 PII 数据集,并使用 Cohere 的 [Command R+](https://huggingface.co/CohereForAI/c4ai-command-r-plus)模型的[免费试用 API](https://cohere.com/blog/free-developer-tier-announcement),演示 Wealthsimple 的 PII 清洗功能。\n", "\n", "首先,请按照 [README](https://github.com/wealthsimple/llm-gateway) 中的说明进行安装:\n", "1. 安装 Poetry 和 Pyenv\n", "2. 安装 pyenv 版本 3.11.3\n", "3. 安装项目所需的依赖:\n", "```\n", "brew install gitleaks\n", "poetry install\n", "poetry run pre-commit install\n", "```\n", "4. 运行 `cp .envrc.example .envrc` 并用 API 密钥更新配置。" ] }, { "cell_type": "code", "execution_count": 88, "id": "aed2caf7-41dd-427b-b69a-18d44d554319", "metadata": {}, "outputs": [], "source": [ "import os\n", "from llm_gateway.providers.cohere import CohereWrapper\n", "from datasets import load_dataset\n", "import cohere\n", "import types\n", "import re" ] }, { "cell_type": "code", "execution_count": 70, "id": "85d8bcd4-37a6-4add-a83d-43909bbe9c87", "metadata": {}, "outputs": [], "source": [ "COHERE_API_KEY = os.environ['COHERE_API_KEY']\n", "DATABASE_URL = os.environ['DATABASE_URL'] # default database url: \"postgresql://postgres:postgres@postgres:5432/llm_gateway\"" ] }, { "cell_type": "markdown", "id": "ae399eb7-f7a5-44d4-bfd5-cc38570b2360", "metadata": {}, "source": [ "## LLM 包装器\n", "\n", "包装器对象是一个简单的封装器,它在发起 API 调用之前,将“清洗器”应用到输入的提示语上。使用包装器发起请求时,我们将获得一个响应和一个 `db_record` 对象。在深入了解更多细节之前,让我们先来看一下它的实际应用。" ] }, { "cell_type": "code", "execution_count": 113, "id": "b0882af5-42bc-460a-a42e-9bd336dc7292", "metadata": {}, "outputs": [], "source": [ "wrapper = CohereWrapper()" ] }, { "cell_type": "code", "execution_count": 73, "id": "f7f055e4-c290-4ac1-9869-2e8712355c49", "metadata": {}, "outputs": [], "source": [ "example = \"Michael Smith (msmith@gmail.com, (+1) 111-111-1111) committed a mistake when he used PyTorch Trainer instead of HF Trainer.\"" ] }, { "cell_type": "code", "execution_count": 74, "id": "3f2ae809-cf5b-444c-9bff-34947575a428", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'data': ['Michael Smith made a mistake by using PyTorch Trainer instead of HF Trainer.'], 'return_likelihoods': None, 'meta': {'api_version': {'version': '1'}, 'billed_units': {'input_tokens': 48, 'output_tokens': 14}}}\n" ] } ], "source": [ "response, db_record = wrapper.send_cohere_request(\n", " endpoint=\"generate\",\n", " model=\"command-r-plus\",\n", " max_tokens=25,\n", " prompt=f\"{example}\\n\\nSummarize the above text in 1-2 sentences.\",\n", " temperature=0.3,\n", ")\n", "\n", "print(response)" ] }, { "cell_type": "markdown", "id": "1c4459a1-0fad-48a8-88e9-6a145502df04", "metadata": {}, "source": [ "响应返回的是 LLM 的输出;在这个例子中,由于我们要求模型对一个已经很简短的句子进行总结,它返回了以下消息:\n", "\n", "`['Michael Smith made a mistake by using PyTorch Trainer instead of HF Trainer.']`" ] }, { "cell_type": "code", "execution_count": 75, "id": "536aa495-43ab-4570-8142-b040b85f4cf9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'user_input': 'Michael Smith ([REDACTED EMAIL ADDRESS], (+1) [REDACTED PHONE NUMBER]) committed a mistake when he used PyTorch Trainer instead of HF Trainer.\\n\\nSummarize the above text in 1-2 sentences.', 'user_email': None, 'cohere_response': {'data': ['Michael Smith made a mistake by using PyTorch Trainer instead of HF Trainer.'], 'return_likelihoods': None, 'meta': {'api_version': {'version': '1'}, 'billed_units': {'input_tokens': 48, 'output_tokens': 14}}}, 'cohere_model': 'command-r-plus', 'temperature': 0.3, 'extras': '{}', 'created_at': datetime.datetime(2024, 6, 10, 2, 16, 7, 666438), 'cohere_endpoint': 'generate'}\n" ] } ], "source": [ "print(db_record)" ] }, { "cell_type": "markdown", "id": "04dde01f-60f4-4917-a881-3f57baf44c9f", "metadata": {}, "source": [ "第二个返回项是数据库记录。该代码库是为使用 Postgres 后端而设计的;实际上,代码库自带一个使用 Docker 构建的完整前端。Postgres 数据库用于存储网关的聊天历史记录。然而,它也非常有用,因为它展示了每个请求中实际发送的数据。如我们所见,提示语经过了清洗,实际发送的内容如下:\n", "\n", "`Michael Smith ([REDACTED EMAIL ADDRESS], (+1) [REDACTED PHONE NUMBER]) committed a mistake when he used PyTorch Trainer instead of HF Trainer.\\n\\nSummarize the above text in 1-2 sentences.`\n", "\n", "但等等,我听到你在想。Michael Smith 不就是 PII 吗?确实是。但是这个代码库实际上并没有实现姓名清洗功能。接下来,我们将探讨在提示语中应用了哪些清洗器:\n", "\n", "> [!提示] \n", "> Cohere 的 `generate` 端点实际上已经被弃用,因此,如果有人能为 Cohere API 的新的 Chat 端点创建并提交集成,这将是一个非常棒的开源贡献。" ] }, { "cell_type": "markdown", "id": "10b182a1-c8a3-4a18-90ae-b8256efea50e", "metadata": {}, "source": [ "## 清洗器!\n", "\n", "根据他们的代码库,以下是实现的清洗器:\n", "\n", "```python\n", "ALL_SCRUBBERS = [\n", " scrub_phone_numbers,\n", " scrub_credit_card_numbers,\n", " scrub_email_addresses,\n", " scrub_postal_codes,\n", " scrub_sin_numbers,\n", "]\n", "```\n", "\n", "网关会依次应用每个清洗器。\n", "\n", "这虽然有些 “hacky”(不够优雅),但如果你确实需要实现另一个清洗器,可以通过修改包装器方法来实现。以下是一个演示:\n", "\n", "> [!提示] \n", "> 作者提到,SIN(社会保险号)清洗器特别容易误清洗数据,因此它会被放在最后,以确保其他与数字相关的 PII 先被清洗。" ] }, { "cell_type": "code", "execution_count": 114, "id": "e5ca6d12-0663-4311-9b6f-e3596fc2a36e", "metadata": {}, "outputs": [], "source": [ "def my_custom_scrubber(text: str) -> str:\n", " \"\"\"\n", " Scrub Michael Smith in text\n", "\n", " :param text: Input text to scrub\n", " :type text: str\n", " :return: Input text with any mentions of Michael Smith scrubbed\n", " :rtype: str\n", " \"\"\"\n", " return re.sub(\n", " r\"Michael Smith\",\n", "\n", " \n", " \"[REDACTED PERSON]\",\n", " text,\n", " re.IGNORECASE\n", " )" ] }, { "cell_type": "code", "execution_count": 115, "id": "1c31aed5-f934-4498-8a91-e509eb041a91", "metadata": {}, "outputs": [], "source": [ "original_method = wrapper.send_cohere_request\n", "\n", "def modified_method(self, **kwargs):\n", " self._validate_cohere_endpoint(kwargs.get('endpoint', None)) # Unfortunate double validate cohere endpoint call\n", " prompt = kwargs.get('prompt', None)\n", " text = my_custom_scrubber(prompt)\n", " kwargs['prompt'] = text\n", " return original_method(**kwargs)\n", "\n", "# Assign the new method to the instance\n", "wrapper.send_cohere_request = types.MethodType(modified_method, wrapper)" ] }, { "cell_type": "code", "execution_count": 116, "id": "3434529d-517d-48e6-a891-35108366fb90", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'data': ['[REDACTED PERSON] made an error by using PyTorch Trainer instead of HF Trainer. They can be contacted at [RED'], 'return_likelihoods': None, 'meta': {'api_version': {'version': '1'}, 'billed_units': {'input_tokens': 52, 'output_tokens': 25}}}\n" ] } ], "source": [ "response, db_record = wrapper.send_cohere_request(\n", " endpoint=\"generate\",\n", " model=\"command-r-plus\",\n", " max_tokens=25,\n", " prompt=f\"{example}\\n\\nSummarize the above text in 1-2 sentences.\",\n", " temperature=0.3,\n", ")\n", "\n", "print(response)" ] }, { "cell_type": "code", "execution_count": 117, "id": "b4fcd05a-6648-4a15-9195-ab865b2d5f05", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'user_input': '[REDACTED PERSON] ([REDACTED EMAIL ADDRESS], (+1) [REDACTED PHONE NUMBER]) committed a mistake when he used PyTorch Trainer instead of HF Trainer.\\n\\nSummarize the above text in 1-2 sentences.', 'user_email': None, 'cohere_response': {'data': ['[REDACTED PERSON] made an error by using PyTorch Trainer instead of HF Trainer. They can be contacted at [RED'], 'return_likelihoods': None, 'meta': {'api_version': {'version': '1'}, 'billed_units': {'input_tokens': 52, 'output_tokens': 25}}}, 'cohere_model': 'command-r-plus', 'temperature': 0.3, 'extras': '{}', 'created_at': datetime.datetime(2024, 6, 10, 2, 59, 58, 733195), 'cohere_endpoint': 'generate'}\n" ] } ], "source": [ "print(db_record)" ] }, { "cell_type": "markdown", "id": "8c8da471-285c-4c6f-9d37-d27eb5479a6b", "metadata": {}, "source": [ "如果你确实需要这样做,请务必记住,清洗器是按顺序应用的,因此,如果你的自定义清洗器与任何默认清洗器发生冲突,可能会导致一些意外行为。\n", "\n", "例如,针对姓名的清洗,实际上有[其他清洗库](https://github.com/kylemclaren/scrub)可以探索,这些库采用更复杂的算法来清洗 PII。这个代码库涵盖了更多的PII类型,例如 [IP 地址、主机名等](https://github.com/kylemclaren/scrub/blob/master/scrubadubdub/scrub.py)。然而,如果你仅仅需要删除特定的匹配项,你仍然可以使用上述代码进行处理。" ] }, { "cell_type": "markdown", "id": "cf338c5d-cd2c-4f9a-9ea2-ea18b352c60c", "metadata": {}, "source": [ "## 数据集\n", "\n", "让我们在一个完整的数据集上演示这个包装器的实际应用。" ] }, { "cell_type": "code", "execution_count": 126, "id": "47166d9f-d4e1-4895-b3b9-9a5fbb399e9c", "metadata": { "scrolled": true }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "409122f13cd748c78f9b1be3cbfac4f8", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Downloading readme: 0%| | 0.00/12.8k [00:00<?, ?B/s]" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "8a61bbc66ab649e39af020347df2331a", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Downloading data: 0%| | 0.00/73.8M [00:00<?, ?B/s]" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "2f33d9cb809c4aada68fc35a7a2b68a1", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Downloading data: 0%| | 0.00/116M [00:00<?, ?B/s]" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "b358959872654504b91421352adfd3b1", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Downloading data: 0%| | 0.00/97.8M [00:00<?, ?B/s]" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "46a96422ad954882b1658f211521bf16", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Downloading data: 0%| | 0.00/93.1M [00:00<?, ?B/s]" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "c6771a1fdfbd40a488bd4fab81cc88b0", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Generating train split: 0 examples [00:00, ? examples/s]" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "pii_ds = load_dataset(\"ai4privacy/pii-masking-200k\")" ] }, { "cell_type": "code", "execution_count": 141, "id": "7df53225-9880-4f04-8c8e-bed5de6aa52c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "\"I need the latest update on assessment results. Please send the files to Valentine4@gmail.com. For your extra time, we'll offer you Kip 100,000 but please provide your лв account details.\"" ] }, "execution_count": 141, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pii_ds['train'][36]['source_text']" ] }, { "cell_type": "code", "execution_count": 140, "id": "d2f1c854-7fe3-4db3-8b88-635dc5894b38", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'data': [\"The person is requesting an update on assessment results and is offering Kip 100,000 in exchange for the information and the recipient's account details.\"], 'return_likelihoods': None, 'meta': {'api_version': {'version': '1'}, 'billed_units': {'input_tokens': 64, 'output_tokens': 33}}}\n" ] } ], "source": [ "example = pii_ds['train'][36]['source_text']\n", "\n", "response, db_record = wrapper.send_cohere_request(\n", " endpoint=\"generate\",\n", " model=\"command-r-plus\",\n", " max_tokens=50,\n", " prompt=f\"{example}\\n\\nSummarize the above text in 1-2 sentences.\",\n", " temperature=0.3,\n", ")\n", "\n", "print(response)" ] }, { "cell_type": "code", "execution_count": 142, "id": "2c8189d8-9878-430b-b053-0d340acbe008", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'user_input': \"I need the latest update on assessment results. Please send the files to V[REDACTED EMAIL ADDRESS]. For your extra time, we'll offer you Kip 100,000 but please provide your лв account details.\\n\\nSummarize the above text in 1-2 sentences.\", 'user_email': None, 'cohere_response': {'data': [\"The person is requesting an update on assessment results and is offering Kip 100,000 in exchange for the information and the recipient's account details.\"], 'return_likelihoods': None, 'meta': {'api_version': {'version': '1'}, 'billed_units': {'input_tokens': 64, 'output_tokens': 33}}}, 'cohere_model': 'command-r-plus', 'temperature': 0.3, 'extras': '{}', 'created_at': datetime.datetime(2024, 6, 10, 3, 10, 51, 416091), 'cohere_endpoint': 'generate'}\n" ] } ], "source": [ "print(db_record)" ] }, { "cell_type": "markdown", "id": "4aab8583-daf4-4374-89b9-b596715425b8", "metadata": {}, "source": [ "## 常规输出\n", "\n", "如果我们直接将文本发送到端点而不进行任何清洗,摘要结果如下所示:" ] }, { "cell_type": "code", "execution_count": 145, "id": "d381c8ea-4105-4def-878e-634100ea7d78", "metadata": {}, "outputs": [], "source": [ " co = cohere.Client(\n", " api_key=os.environ['COHERE_API_KEY']\n", ")\n", "\n", "response_vanilla = co.generate(\n", " prompt=f\"{example}\\n\\nSummarize the above text in 1-2 sentences.\",\n", " model=\"command-r-plus\",\n", " max_tokens=50,\n", " temperature=0.3\n", ")" ] }, { "cell_type": "code", "execution_count": 148, "id": "15fad584-75f3-4955-bd31-06e9adff9dd1", "metadata": {}, "outputs": [ { "data": { "text/html": [ "<table border=\"1\" class=\"dataframe\">\n", " <thead>\n", " <tr style=\"text-align: right;\">\n", " <th></th>\n", " <th>prompt</th>\n", " <th>text</th>\n", " </tr>\n", " </thead>\n", " <tbody>\n", " <tr>\n", " <th>0</th>\n", " <td>I need the latest update on assessment results. Please send the files to Valentine4@gmail.com. For your extra time, we'll offer you Kip 100,000 but please provide your лв account details.\n", "\n", "Summarize the above text in 1-2 sentences.</td>\n", " <td>The text is a request for an update on assessment results to be sent to Valentine4@gmail.com, with an offer of Kip 100,000 in exchange for the information and account details.</td>\n", " </tr>\n", " </tbody>\n", "</table>" ], "text/plain": [ "Generations([cohere.Generation {\n", " \tid: f3b759b4-2a58-467c-af9d-288e769a5a44\n", " \tprompt: I need the latest update on assessment results. Please send the files to Valentine4@gmail.com. For your extra time, we'll offer you Kip 100,000 but please provide your лв account details.\n", " \n", " Summarize the above text in 1-2 sentences.\n", " \ttext: The text is a request for an update on assessment results to be sent to Valentine4@gmail.com, with an offer of Kip 100,000 in exchange for the information and account details.\n", " \tlikelihood: None\n", " \tfinish_reason: COMPLETE\n", " \ttoken_likelihoods: None\n", " }])" ] }, "execution_count": 148, "metadata": {}, "output_type": "execute_result" } ], "source": [ "response_vanilla" ] }, { "cell_type": "markdown", "id": "7103c697-6479-4907-a646-ed5ac2c7dd09", "metadata": {}, "source": [ "总结一下,在 Notebook 中,我们演示了如何使用 Wealthsimple 开源的 PII 检测示例网关,并在此基础上添加了自定义清洗器。如果你真的需要可靠的 PII 检测,确保运行自己的测试,验证你所采用的清洗算法是否真正覆盖了你的应用场景。最重要的是,尽可能在你自己托管的基础设施上部署开源模型,这将始终是构建 LLM 应用时最安全、最可靠的选择 :)" ] } ], "metadata": { "kernelspec": { "display_name": "nlp", "language": "python", "name": "nlp" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.5" } }, "nbformat": 4, "nbformat_minor": 5 }