Documentation Index
Fetch the complete documentation index at: https://wb-21fd5541-john-wbdocs-2044-rename-serverless-products.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
これはインタラクティブ ノートブックです。ローカルで実行するか、以下のリンクを使用できます。
このガイドでは、個人を特定できる情報 (PII) データのプライバシーを保ちながら、W&B Weave を使用する方法を学びます。このガイドでは、PII データを特定し、マスクし、匿名化するための以下の方法を紹介します。
- PII データを特定してマスクするための正規表現。
- Python ベースのデータ保護 SDK である Microsoft の Presidio。このツールでは、マスキングと置換の機能を利用できます。
- 偽データを生成する Python ライブラリ Faker。Presidio と組み合わせることで、PII データを匿名化できます。
さらに、PII のマスキングと匿名化をワークフローに統合するために、weave.op input/output logging customization と autopatch_settings の使い方も学びます。詳細は、Customize logged inputs and outputsを参照してください。
開始するには、以下の手順に従ってください。
- Overview セクションを確認します。
- prerequisites を完了します。
- PII データの特定、マスキング、匿名化に使用できる available methods を確認します。
- Apply the methods to Weave calls を確認します。
以下のセクションでは、weave.op を使用した入出力のロギングの概要と、Weave で PII データを扱う際のベストプラクティスを紹介します。
Weave Ops では、入力と出力の後処理関数を定義できます。これらの関数を使用すると、LLM call に渡すデータや Weave にログされるデータを変更できます。
次の例では、2 つの後処理関数を定義し、それらを weave.op() の引数として渡します。
from dataclasses import dataclass
from typing import Any
import weave
# 入力ラッパークラス
@dataclass
class CustomObject:
x: int
secret_password: str
# まず、入力と出力の後処理関数を定義します:
def postprocess_inputs(inputs: dict[str, Any]) -> dict[str, Any]:
return {k:v for k,v in inputs.items() if k != "hide_me"}
def postprocess_output(output: CustomObject) -> CustomObject:
return CustomObject(x=output.x, secret_password="REDACTED")
# 次に、`@weave.op` デコレーターを使用する際に、これらの処理関数をデコレーターの引数として渡します:
@weave.op(
postprocess_inputs=postprocess_inputs,
postprocess_output=postprocess_output,
)
def some_llm_call(a: int, hide_me: str) -> CustomObject:
return CustomObject(x=a, secret_password=hide_me)
PII データで Weave を使用する際のベストプラクティス
PII データで Weave を使用する前に、以下のベストプラクティスを確認してください。
- PII の検出を確認するため、匿名化したデータをログする
- Weave トレースで PII の処理プロセスをトラッキングする
- 実際の PII を露出させずに、匿名化のパフォーマンスを測定する
- 未加工のPIIは絶対にログしない
- ログする前に機微なフィールドを暗号化する
- 後で復号する必要があるデータには、可逆暗号化を使用します
- 元に戻す必要のない一意の ID には、一方向ハッシュを使用します
- 暗号化したまま分析する必要があるデータには、専用の暗号化方式の利用を検討します
- まず、必要なパッケージをインストールします。
%%capture
# @title 必要なPythonパッケージ:
!pip install cryptography
!pip install presidio_analyzer
!pip install presidio_anonymizer
!python -m spacy download en_core_web_lg # PresidioはspaCy NLP推論エンジンを使用する
!pip install Faker # FakerでPIIデータを偽データに置換する
!pip install weave # トレースを活用するため
!pip install set-env-colab-kaggle-dotenv -q # 環境変数用
!pip install anthropic # Sonnetを使用するため
!pip install cryptography # データを暗号化するため
-
次の場所でAPIキーを作成します:
%%capture
# @title APIキーを正しく設定する
# 使用方法については https://pypi.org/project/set-env-colab-kaggle-dotenv/ を参照してください。
from set_env import set_env
_ = set_env("ANTHROPIC_API_KEY")
_ = set_env("WANDB_API_KEY")
- Weaveプロジェクトを初期化します。
import weave
# 新しい Weave プロジェクトを開始する
WEAVE_PROJECT = "pii_cookbook"
weave.init(WEAVE_PROJECT)
- 10 個のテキストブロックを含むデモ用 PII データセットを読み込みます。
import requests
url = "https://raw.githubusercontent.com/wandb/docs/main/weave/cookbooks/source/10_pii_data.json"
response = requests.get(url)
pii_data = response.json()
print('PII data first sample: "' + pii_data[0]["text"] + '"')
セットアップ を完了したら、次のことができます
PII データを検出して保護するために、以下の方法で PII データを特定してマスクし、必要に応じて匿名化します。
- 正規表現を使用して PII データを特定し、マスクします。
- Microsoft Presidio。マスキングと置換の機能を備えた、Python ベースのデータ保護 SDK です。
- Faker。偽データを生成するための Python ライブラリです。
正規表現 (正規表現) は、PII データを特定してマスクするための最もシンプルなmethodです。正規表現 を使うと、電話番号、メールアドレス、社会保障番号などの機密情報のさまざまな形式に一致するパターンを定義できます。正規表現 を使えば、より複雑な NLP 手法を使わなくても、大量のテキストをスキャンして情報を置き換えたりマスクしたりできます。
import re
# 正規表現を使用してPIIデータをクリーニングする関数を定義する
def redact_with_regex(text):
# 電話番号パターン
# \b : 単語境界
# \d{3} : 正確に3桁
# [-.]? : ハイフンまたはドット(省略可)
# \d{3} : さらに3桁
# [-.]? : ハイフンまたはドット(省略可)
# \d{4} : 正確に4桁
# \b : 単語境界
text = re.sub(r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b", "<PHONE>", text)
# メールパターン
# \b : 単語境界
# [A-Za-z0-9._%+-]+ : メールユーザー名に使用できる1文字以上の文字
# @ : @記号(リテラル)
# [A-Za-z0-9.-]+ : ドメイン名に使用できる1文字以上の文字
# \. : ドット(リテラル)
# [A-Z|a-z]{2,} : 2文字以上の大文字または小文字(TLD)
# \b : 単語境界
text = re.sub(
r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "<EMAIL>", text
)
# SSNパターン
# \b : 単語境界
# \d{3} : 正確に3桁
# - : ハイフン(リテラル)
# \d{2} : 正確に2桁
# - : ハイフン(リテラル)
# \d{4} : 正確に4桁
# \b : 単語境界
text = re.sub(r"\b\d{3}-\d{2}-\d{4}\b", "<SSN>", text)
# 簡易的な名前パターン(網羅的ではない)
# \b : 単語境界
# [A-Z] : 大文字1文字
# [a-z]+ : 1文字以上の小文字
# \s : 空白文字1文字
# [A-Z] : 大文字1文字
# [a-z]+ : 1文字以上の小文字
# \b : 単語境界
text = re.sub(r"\b[A-Z][a-z]+ [A-Z][a-z]+\b", "<NAME>", text)
return text
サンプルテキストを使ってこの関数をテストしてみましょう:
# 関数をテストする
test_text = "My name is John Doe, my email is john.doe@example.com, my phone is 123-456-7890, and my SSN is 123-45-6789."
cleaned_text = redact_with_regex(test_text)
print(f"Raw text:\n\t{test_text}")
print(f"Redacted text:\n\t{cleaned_text}")
方法 2: Microsoft Presidio を使用してマスクする
次の方法では、Microsoft Presidio を使用して PII データを完全に除去します。Presidio は PII をマスクし、PII のタイプを表すプレースホルダーに置き換えます。たとえば、Presidio は "My name is Alex" 内の Alex を <PERSON> に置き換えます。
Presidio では、一般的な Entities が標準でサポートされています。以下の例では、PHONE_NUMBER、PERSON、LOCATION、EMAIL_ADDRESS、US_SSN のいずれかに該当するすべての Entities をマスクします。Presidio による処理は関数にまとめられています。
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
# アナライザーを設定する。NLPモジュール(デフォルトではspaCyモデル)およびその他のPII認識器を読み込む。
analyzer = AnalyzerEngine()
# アノニマイザーを設定する。アナライザーの結果を使用してテキストを匿名化する。
anonymizer = AnonymizerEngine()
# Presidioのマスキング処理を関数にカプセル化する
def redact_with_presidio(text):
# テキストを解析してPIIデータを特定する
results = analyzer.analyze(
text=text,
entities=["PHONE_NUMBER", "PERSON", "LOCATION", "EMAIL_ADDRESS", "US_SSN"],
language="en",
)
# 特定されたPIIデータを匿名化する
anonymized_text = anonymizer.anonymize(text=text, analyzer_results=results)
return anonymized_text.text
サンプルテキストを使って、この関数をテストしてみましょう:
text = "My phone number is 212-555-5555 and my name is alex"
# 関数をテストする
anonymized_text = redact_with_presidio(text)
print(f"Raw text:\n\t{text}")
print(f"Redacted text:\n\t{anonymized_text}")
method 3: Faker と Presidio を使用した置換による匿名化
テキストをマスクする代わりに、Faker Python ライブラリで生成したダミーデータを使用し、MS Presidio で名前や電話番号などの PII を置き換えて匿名化できます。たとえば、次のようなデータがあるとします。
"My name is Raphael and I like to fish. My phone number is 212-555-5555"
Presidio と Faker を使用してデータを処理すると、次のようになります。
"My name is Katherine Dixon and I like to fish. My phone number is 667.431.7379"
Presidio と Faker を効果的に併用するには、独自のオペレーター への参照を指定する必要があります。これらのオペレーター は、PII をダミーデータに置き換える役割を持つ Faker の function を Presidio が使用できるようにします。
from faker import Faker
from presidio_anonymizer import AnonymizerEngine
from presidio_anonymizer.entities import OperatorConfig
fake = Faker()
# faker関数を作成する(値を受け取る必要があることに注意)
def fake_name(x):
return fake.name()
def fake_number(x):
return fake.phone_number()
# PERSONおよびPHONE_NUMBEREntities用のカスタムオペレーターを作成する
operators = {
"PERSON": OperatorConfig("custom", {"lambda": fake_name}),
"PHONE_NUMBER": OperatorConfig("custom", {"lambda": fake_number}),
}
text_to_anonymize = (
"My name is Raphael and I like to fish. My phone number is 212-555-5555"
)
# アナライザーの出力
analyzer_results = analyzer.analyze(
text=text_to_anonymize, entities=["PHONE_NUMBER", "PERSON"], language="en"
)
anonymizer = AnonymizerEngine()
# 上記のオペレーターをアノニマイザーに渡すことを忘れずに
anonymized_results = anonymizer.anonymize(
text=text_to_anonymize, analyzer_results=analyzer_results, operators=operators
)
print(f"Raw text:\n\t{text_to_anonymize}")
print(f"Anonymized text:\n\t{anonymized_results.text}")
コードを1つのクラスにまとめ、Entitiesの一覧に、先ほど特定した追加項目も含めましょう。
from typing import ClassVar
from faker import Faker
from presidio_anonymizer import AnonymizerEngine
from presidio_anonymizer.entities import OperatorConfig
# Fakerを拡張してフェイクデータを生成するカスタムクラス
class MyFaker(Faker):
# faker関数を作成する(値を受け取る必要があることに注意)
def fake_address(self):
return fake.address()
def fake_ssn(self):
return fake.ssn()
def fake_name(self):
return fake.name()
def fake_number(self):
return fake.phone_number()
def fake_email(self):
return fake.email()
# Entitiesのカスタムオペレーターを作成する
operators: ClassVar[dict[str, OperatorConfig]] = {
"PERSON": OperatorConfig("custom", {"lambda": fake_name}),
"PHONE_NUMBER": OperatorConfig("custom", {"lambda": fake_number}),
"EMAIL_ADDRESS": OperatorConfig("custom", {"lambda": fake_email}),
"LOCATION": OperatorConfig("custom", {"lambda": fake_address}),
"US_SSN": OperatorConfig("custom", {"lambda": fake_ssn}),
}
def redact_and_anonymize_with_faker(self, text):
anonymizer = AnonymizerEngine()
analyzer_results = analyzer.analyze(
text=text,
entities=["PHONE_NUMBER", "PERSON", "LOCATION", "EMAIL_ADDRESS", "US_SSN"],
language="en",
)
anonymized_results = anonymizer.anonymize(
text=text, analyzer_results=analyzer_results, operators=self.operators
)
return anonymized_results.text
サンプルテキストでこの関数をテストしてみましょう:
faker = MyFaker()
text_to_anonymize = (
"My name is Raphael and I like to fish. My phone number is 212-555-5555"
)
anonymized_text = faker.redact_and_anonymize_with_faker(text_to_anonymize)
print(f"Raw text:\n\t{text_to_anonymize}")
print(f"Anonymized text:\n\t{anonymized_text}")
Method 4: autopatch_settings を使用する
autopatch_settings を使用すると、サポートされる1つ以上のLLMインテグレーションについて、初期化時にPII処理を直接設定できます。この方法の利点は次のとおりです。
- PII処理ロジックを初期化時に一元化して適用できるため、各所に分散したカスタムロジックの必要性を減らせます。
- PII処理のワークフローは、特定のインテグレーションごとにカスタマイズしたり、完全に無効化したりできます。
autopatch_settings を使用してPII処理を設定するには、サポートされるLLMインテグレーションのいずれかについて、op_settings で postprocess_inputs および/または postprocess_output を定義します。
def postprocess(inputs: dict) -> dict:
if "SENSITIVE_KEY" in inputs:
inputs["SENSITIVE_KEY"] = "REDACTED"
return inputs
client = weave.init(
...,
autopatch_settings={
"openai": {
"op_settings": {
"postprocess_inputs": postprocess,
"postprocess_output": ...,
}
},
"anthropic": {
"op_settings": {
"postprocess_inputs": ...,
"postprocess_output": ...,
}
}
},
)
以下の例では、PII のマスキングおよび匿名化のメソッドを Weave Models に統合し、その結果を Weave トレース で確認します。
まず、Weave Model を作成します。Weave Model は、設定、モデルの重み、モデルの動作を定義するコードなどの情報を組み合わせたものです。
このモデルには、Anthropic API を呼び出す predict 関数を含めます。トレース を使用して LLM calls をトレースしながら、Anthropic の Claude Sonnet で感情分析を実行します。Claude Sonnet はテキストブロックを受け取り、次の感情分類のいずれか 1 つを出力します: positive、negative、または neutral。さらに、PII データが LLM に送信される前にマスクまたは匿名化されるようにするため、後処理関数も含めます。
このコードを実行すると、Weave のプロジェクトページへのリンクと、実行した特定のトレース (LLM calls) へのリンクが表示されます。
最も単純なケースでは、regex を使用して元のテキスト内の PII データを検出し、マスクできます。
import json
from typing import Any
import anthropic
import weave
# モデル予測 Weave Op に対して正規表現によるマスキングを適用する入力後処理関数を定義する
def postprocess_inputs_regex(inputs: dict[str, Any]) -> dict:
inputs["text_block"] = redact_with_regex(inputs["text_block"])
return inputs
# Weave モデル / 予測関数
class SentimentAnalysisRegexPiiModel(weave.Model):
model_name: str
system_prompt: str
temperature: int
@weave.op(
postprocess_inputs=postprocess_inputs_regex,
)
async def predict(self, text_block: str) -> dict:
client = anthropic.AsyncAnthropic()
response = await client.messages.create(
max_tokens=1024,
model=self.model_name,
system=self.system_prompt,
messages=[
{"role": "user", "content": [{"type": "text", "text": text_block}]}
],
)
result = response.content[0].text
if result is None:
raise ValueError("モデルからの応答がありません")
parsed = json.loads(result)
return parsed
python
# system prompt を使用して LLM モデルを作成する
model = SentimentAnalysisRegexPiiModel(
name="claude-3-sonnet",
model_name="claude-3-5-sonnet-20240620",
system_prompt='あなたは感情分析分類器です。テキストをその感情に基づいて分類します。入力はテキストのブロックです。次の評価オプションのいずれか1つで回答してください["positive", "negative", "neutral"]。回答は JSON 形式の1単語にしてください: {classification}。有効な JSON であることを確認してください。',
temperature=0,
)
print("Model: ", model)
# テキストブロックごとに、まず匿名化してから予測する
for entry in pii_data:
await model.predict(entry["text"])
Presidio を使用したマスキング method
次に、Presidio を使用して元のテキスト内の PII データを特定し、マスクします。
from typing import Any
import weave
# モデル予測 Weave Op に Presidio マスキングを適用する入力後処理関数を定義する
def postprocess_inputs_presidio(inputs: dict[str, Any]) -> dict:
inputs["text_block"] = redact_with_presidio(inputs["text_block"])
return inputs
# Weave モデル / 予測関数
class SentimentAnalysisPresidioPiiModel(weave.Model):
model_name: str
system_prompt: str
temperature: int
@weave.op(
postprocess_inputs=postprocess_inputs_presidio,
)
async def predict(self, text_block: str) -> dict:
client = anthropic.AsyncAnthropic()
response = await client.messages.create(
max_tokens=1024,
model=self.model_name,
system=self.system_prompt,
messages=[
{"role": "user", "content": [{"type": "text", "text": text_block}]}
],
)
result = response.content[0].text
if result is None:
raise ValueError("No response from model")
parsed = json.loads(result)
return parsed
python
# system prompt を使用して LLM モデルを作成する
model = SentimentAnalysisPresidioPiiModel(
name="claude-3-sonnet",
model_name="claude-3-5-sonnet-20240620",
system_prompt='You are a Sentiment Analysis classifier. You will be classifying text based on their sentiment. Your input will be a block of text. You will answer with one the following rating option["positive", "negative", "neutral"]. Your answer should be one word in json format: {classification}. Ensure that it is valid JSON.',
temperature=0,
)
print("Model: ", model)
# テキストブロックごとに、まず匿名化してから予測する
for entry in pii_data:
await model.predict(entry["text"])
Faker と Presidio の置換 method
この例では、Faker を使用して匿名化された置換用の PII データを生成し、Presidio を使用して元のテキスト内の PII データを特定し、置換します。
from typing import Any
import weave
# モデル予測 Weave Op に Faker の匿名化と Presidio のマスキングを適用する入力後処理関数を定義する
faker = MyFaker()
def postprocess_inputs_faker(inputs: dict[str, Any]) -> dict:
inputs["text_block"] = faker.redact_and_anonymize_with_faker(inputs["text_block"])
return inputs
# Weave モデル / 予測関数
class SentimentAnalysisFakerPiiModel(weave.Model):
model_name: str
system_prompt: str
temperature: int
@weave.op(
postprocess_inputs=postprocess_inputs_faker,
)
async def predict(self, text_block: str) -> dict:
client = anthropic.AsyncAnthropic()
response = await client.messages.create(
max_tokens=1024,
model=self.model_name,
system=self.system_prompt,
messages=[
{"role": "user", "content": [{"type": "text", "text": text_block}]}
],
)
result = response.content[0].text
if result is None:
raise ValueError("No response from model")
parsed = json.loads(result)
return parsed
python
# system prompt を指定して LLM モデルを作成する
model = SentimentAnalysisFakerPiiModel(
name="claude-3-sonnet",
model_name="claude-3-5-sonnet-20240620",
system_prompt='You are a Sentiment Analysis classifier. You will be classifying text based on their sentiment. Your input will be a block of text. You will answer with one the following rating option["positive", "negative", "neutral"]. Your answer should be one word in json format: {classification}. Ensure that it is valid JSON.',
temperature=0,
)
print("Model: ", model)
# テキストブロックごとに匿名化してから予測する
for entry in pii_data:
await model.predict(entry["text"])
autopatch_settings method
次の例では、初期化時に anthropic の postprocess_inputs を postprocess_inputs_regex() 関数() に設定します。postprocess_inputs_regex 関数は、Method 1: Regular Expression Filtering で定義された redact_with_regex method を適用します。これにより、redact_with_regex がすべての anthropic モデルへの入力に適用されます。
from typing import Any
import weave
client = weave.init(
...,
autopatch_settings={
"anthropic": {
"op_settings": {
"postprocess_inputs": postprocess_inputs_regex,
}
}
},
)
# モデル予測 Weave Op に対して正規表現マスキングを適用する入力後処理関数を定義する
def postprocess_inputs_regex(inputs: dict[str, Any]) -> dict:
inputs["text_block"] = redact_with_regex(inputs["text_block"])
return inputs
# Weave モデル / 予測関数
class SentimentAnalysisRegexPiiModel(weave.Model):
model_name: str
system_prompt: str
temperature: int
async def predict(self, text_block: str) -> dict:
client = anthropic.AsyncAnthropic()
response = await client.messages.create(
max_tokens=1024,
model=self.model_name,
system=self.system_prompt,
messages=[
{"role": "user", "content": [{"type": "text", "text": text_block}]}
],
)
result = response.content[0].text
if result is None:
raise ValueError("モデルからの応答がありません")
parsed = json.loads(result)
return parsed
python
# system prompt を使用して LLM モデルを作成する
model = SentimentAnalysisRegexPiiModel(
name="claude-3-sonnet",
model_name="claude-3-5-sonnet-20240620",
system_prompt='あなたは感情分析分類器です。テキストの感情に基づいて分類を行います。入力はテキストブロックです。次の評価オプションのいずれか1つで回答してください["positive", "negative", "neutral"]。回答は JSON 形式の1単語にしてください: {classification}。有効な JSON であることを確認してください。',
temperature=0,
)
print("Model: ", model)
# テキストブロックごとに、まず匿名化してから予測する
for entry in pii_data:
await model.predict(entry["text"])
PII の匿名化に加えて、cryptography ライブラリの Fernet 対称暗号化を使ってデータを暗号化することで、セキュリティをさらに高められます。この方法では、匿名化したデータが傍受された場合でも、暗号鍵がなければ内容を読み取ることはできません。
import os
from cryptography.fernet import Fernet
from pydantic import BaseModel, ValidationInfo, model_validator
def get_fernet_key():
# 環境変数にキーが存在するか確認する
key = os.environ.get('FERNET_KEY')
if key is None:
# キーが存在しない場合、新しいキーを生成する
key = Fernet.generate_key()
# キーを環境変数に保存する
os.environ['FERNET_KEY'] = key.decode()
else:
# キーが存在する場合、バイト形式であることを確認する
key = key.encode()
return key
cipher_suite = Fernet(get_fernet_key())
class EncryptedSentimentAnalysisInput(BaseModel):
encrypted_text: str = None
@model_validator(mode="before")
def encrypt_fields(cls, values):
if "text" in values and values["text"] is not None:
values["encrypted_text"] = cipher_suite.encrypt(values["text"].encode()).decode()
del values["text"]
return values
@property
def text(self):
if self.encrypted_text:
return cipher_suite.decrypt(self.encrypted_text.encode()).decode()
return None
@text.setter
def text(self, value):
self.encrypted_text = cipher_suite.encrypt(str(value).encode()).decode()
@classmethod
def encrypt(cls, text: str):
return cls(text=text)
def decrypt(self):
return self.text
# 新しい EncryptedSentimentAnalysisInput を使用するよう sentiment_analysis_model を変更
class sentiment_analysis_model(weave.Model):
model_name: str
system_prompt: str
temperature: int
@weave.op()
async def predict(self, encrypted_input: EncryptedSentimentAnalysisInput) -> dict:
client = AsyncAnthropic()
decrypted_text = encrypted_input.decrypt() # カスタムクラスを使用してテキストを復号する
response = await client.messages.create(
max_tokens=1024,
model=self.model_name,
system=self.system_prompt,
messages=[
{ "role": "user",
"content":[
{
"type": "text",
"text": decrypted_text
}
]
}
]
)
result = response.content[0].text
if result is None:
raise ValueError("モデルからの応答がありません")
parsed = json.loads(result)
return parsed
model = sentiment_analysis_model(
name="claude-3-sonnet",
model_name="claude-3-5-sonnet-20240620",
system_prompt="あなたは感情分析の分類器です。テキストをその感情に基づいて分類します。入力はテキストのブロックです。次の評価オプションの中から1つで回答してください[\"positive\", \"negative\", \"neutral\"]。回答はJSON形式のdictで、キーはclassificationとして1単語で返してください。",
temperature=0
)
for entry in pii_data:
encrypted_input = EncryptedSentimentAnalysisInput.encrypt(entry["text"])
await model.predict(encrypted_input)