2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Databricks Free Edition で OpenAI privacy-filter を動かして PII をマスキングする

2
Posted at

openai/privacy-filter とは

openai/privacy-filter は、OpenAI が公開したテキスト中の個人情報(PII)を検出・マスキングするためのトークン分類モデルです。Apache 2.0 ライセンスで、商用利用やファインチューニングも可能です。

モデルカードは以下にあります(英語)。

特徴を整理すると次の通りです。

  • 総パラメータ 1.5B、アクティブパラメータ 50M の MoE 構成で、アーキテクチャは gpt-oss 系をベースにしています
  • 自己回帰生成ではなく、入力系列を1回のフォワードパスでラベリングし、制約付き Viterbi デコードで BIOES スパンに整える方式です
  • 128,000 トークンのロングコンテキストに対応し、チャンク分割なしで長文を処理できます
  • ブラウザやノートPCでも動く軽さで、GPU は必須ではありません

検出できるのは次の8カテゴリです。

  • account_number
  • private_address
  • private_email
  • private_person
  • private_phone
  • private_url
  • private_date
  • secret

PII のリダクションを補助するためのモデルであり、これ単体で匿名化やコンプライアンスを保証するものではない、という位置づけです。

Databricks Free Edition で動かせるのか

このモデルを Databricks Free Edition のサーバーレスノートブックで動かせるか試しました。先に結論を言うと、追加設定なしの素直なコードでそのまま動きました。

事前に気にしていた点は次の2つです。

まず GPU です。Free Edition はサーバーレス専用で GPU はサポートされていません。ただしこのモデルはアクティブ 50M かつ単一フォワードパスで、CPU でも実用的に動くため GPU がなくても問題になりません。

もう1つは送信先の制限です。同じく Free Edition は送信インターネットアクセスが信頼済みドメインの限られたセットに制限されています。Hugging Face からの重みダウンロードがここで弾かれる懸念がありましたが、今回の検証時点では from_pretrained での直接ダウンロードがそのまま通りました。

ライブラリのインストール

カスタムの model_type(openai_privacy_filter)に対応した新しめの transformers が必要です。今回はインストール時に transformers 5.12.0 が入り、これで問題なくロードできました。CPU 運用なので torch は CPU 版で十分です。

%pip install -U "transformers>=4.46" torch huggingface_hub
%restart_python

サーバーレスでは %pip install 後に %restart_python で Python を再起動します。

直接ロードして動かす

一番素直な方法です。Hugging Face への送信が許可されていれば、これだけで重みのダウンロードからパイプライン構築まで完了します。

from transformers import pipeline

classifier = pipeline(
    task="token-classification",
    model="openai/privacy-filter",
    aggregation_strategy="simple",
)

Screenshot 2026-06-13 at 7.03.06.JPG

実行すると model.safetensors(約 2.8GB)などをダウンロードしてロードが完了します。途中で次のような警告が出ますが、これは未認証ダウンロードのレート制限に関する注意で、一度きりの検証であれば無視して問題ありません。レート制限に当たる場合は HF_TOKEN を設定してください。

Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.

推論を試す

構築した classifier にテキストを渡すと、検出スパンとカテゴリが返ります。

text = "My name is Harry Potter and my email is harry.potter@hogwarts.edu."
for ent in classifier(text):
    print(f"{ent['entity_group']:<18} score={ent['score']:.4f}  '{ent['word']}'")

実行結果は次の通りです。

private_person     score=1.0000  ' Harry'
private_person     score=1.0000  ' Potter'
private_email      score=1.0000  ' harry.potter@hogwarts'
private_email      score=1.0000  '.edu'

人名とメールアドレスを高いスコアで検出できています。ここで気付くのは、aggregation_strategy="simple" を指定していても、HarryPotter、メールアドレスの前半と .edu がそれぞれ別スパンに分かれている点です。後段でマスキングする際はこの分割を考慮する必要があります。

PII マスキング

検出スパンをカテゴリ名のプレースホルダに置換する素直な実装です。後ろのスパンから順に置換することで、文字位置のズレを防いでいます。

def mask_pii(text: str) -> str:
    entities = classifier(text)
    for ent in sorted(entities, key=lambda x: x["start"], reverse=True):
        label = ent["entity_group"].upper()
        text = text[: ent["start"]] + f"[{label}]" + text[ent["end"] :]
    return text


samples = [
    "Please contact John Smith at john.smith@example.com or 555-0142.",
    "田中太郎さんの住所は東京都港区赤坂9-7-1です。",
]
for s in samples:
    print("原文 :", s)
    print("変換後:", mask_pii(s))
    print()

実行結果は次の通りです。

原文 : Please contact John Smith at john.smith@example.com or 555-0142.
変換後: Please contact[PRIVATE_PERSON][PRIVATE_PERSON] at[PRIVATE_EMAIL][PRIVATE_EMAIL] or [PRIVATE_PHONE][PRIVATE_PHONE].

原文 : 田中太郎さんの住所は東京都港区赤坂9-7-1です。
変換後: [PRIVATE_PERSON][PRIVATE_PERSON]さんの住所は[PRIVATE_ADDRESS][PRIVATE_ADDRESS]です。

Screenshot 2026-06-13 at 7.04.40.JPG

英語の氏名・メール・電話番号に加えて、日本語の氏名と住所もきちんと検出できています。英語中心のモデルとされていますが、日本語でも代表的な PII は拾えるようです。

一方で、素直な実装には2つの粗が出ています。1つは同一カテゴリの隣接スパンが分割されたまま置換されるため、[PRIVATE_PERSON][PRIVATE_PERSON] のようにプレースホルダが重複する点です。もう1つは、スパンの先頭に空白が含まれるため、置換時に直前の空白が巻き込まれて消えてしまう点です(contact[PRIVATE_PERSON] のように間のスペースが失われています)。

マスキングの改良版

隣接する同一カテゴリのスパンをマージし、先頭の空白をマスク対象から外すようにします。

def merge_entities(entities, max_gap: int = 1):
    entities = sorted(entities, key=lambda x: x["start"])
    merged = []
    for e in entities:
        if (
            merged
            and e["entity_group"] == merged[-1]["entity_group"]
            and e["start"] - merged[-1]["end"] <= max_gap
        ):
            merged[-1]["end"] = e["end"]
        else:
            merged.append(dict(e))
    return merged


def mask_pii(text: str, max_gap: int = 1) -> str:
    entities = merge_entities(classifier(text), max_gap)
    for ent in sorted(entities, key=lambda x: x["start"], reverse=True):
        start, end = ent["start"], ent["end"]
        # 先頭の空白はマスク対象から外す
        while start < end and text[start].isspace():
            start += 1
        label = ent["entity_group"].upper()
        text = text[:start] + f"[{label}]" + text[end:]
    return text

max_gap は、間に空白1文字などを挟んで隣接する同一カテゴリのスパンをどこまでまとめるかの閾値です。これで先ほどの英語の例は次のように整います。

Please contact [PRIVATE_PERSON] at [PRIVATE_EMAIL] or [PRIVATE_PHONE].

まとめ

openai/privacy-filter は、Databricks Free Edition のサーバーレスノートブックで追加設定なしに動かせました。ポイントは次の通りです。

  • アクティブ 50M かつ単一フォワードパスのため、GPU 非対応の Free Edition でも CPU で実用的に動きます
  • from_pretrained での直接ダウンロードがそのまま通り、追加のネットワーク設定は不要でした
  • 英語中心のモデルですが、日本語の氏名・住所も検出できました
  • 素直なマスキングは隣接スパンの重複や空白の欠落が出るため、スパンのマージと空白除外を入れると実用的になります

なお本番運用では、リダクションの単層手段として使い、機微なワークフローには人間によるレビュー経路を残すのが安全です。日本語データで使う場合は、in-domain での評価を行ったうえで導入することをおすすめします。

はじめてのDatabricks

はじめてのDatabricks

Databricks無料トライアル

Databricks無料トライアル

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?