テキストの前処理でGiNZAを使って個人情報のマスキングをする

LLMの学習データを用意していた際、人名などの個人情報をマスキングしたくなった。

調べてみると、日本語の自然言語処理ライブラリであるGiNZAで固有表現を抽出できるみたい。

GiNZA - Japanese NLP Library
Universal Dependenciesに基づくオープンソース日本語NLPライブラリ

v5からTransformersモデルを採用しており、解析精度が向上したとのこと。

Transformersモデルによる解析精度の向上

GiNZA v5の解析精度は以前のバージョンから飛躍的な向上を遂げました。精度向上の主たる貢献はTransformers事前学習モデルの導入にあります。

高精度なGiNZAモデルを使って個人情報のマスキングに使えないか試してみた。

まずはインストール。

pip install -U ginza ja_ginza_electra

bashからでも使えるので、ちょっと試してみたい場合はPythonのコードを書かなくてすむ。

$ ginza
銀座でランチをご一緒しましょう。
# text = 銀座でランチをご一緒しましょう。
1	銀座	銀座	PROPN	名詞-固有名詞-地名-一般	_	6	obl	_	SpaceAfter=No|BunsetuBILabel=B|BunsetuPositionType=SEM_HEAD|NP_B|Reading=ギンザ|NE=B-GPE|ENE=B-City
2	で	で	ADP	助詞-格助詞	_	1	case	_	SpaceAfter=No|BunsetuBILabel=I|BunsetuPositionType=SYN_HEAD|Reading=デ
3	ランチ	ランチ	NOUN	名詞-普通名詞-一般	_	6	obj	_	SpaceAfter=No|BunsetuBILabel=B|BunsetuPositionType=SEM_HEAD|NP_B|Reading=ランチ
4	を	を	ADP	助詞-格助詞	_	3	case	_	SpaceAfter=No|BunsetuBILabel=I|BunsetuPositionType=SYN_HEAD|Reading=ヲ
5	ご	ご	NOUN	接頭辞	_	6	compound	_	SpaceAfter=No|BunsetuBILabel=B|BunsetuPositionType=CONT|Reading=ゴ
6	一緒	一緒	VERB	名詞-普通名詞-サ変可能	_	0	root	_	SpaceAfter=No|BunsetuBILabel=I|BunsetuPositionType=ROOT|Reading=イッショ
7	し	する	AUX	動詞-非自立可能	_	6	aux	_	SpaceAfter=No|BunsetuBILabel=I|BunsetuPositionType=SYN_HEAD|Inf=サ行変格,連用形-一般|Reading=シ
8	ましょう	ます	AUX	助動詞	_	6	aux	_	SpaceAfter=No|BunsetuBILabel=I|BunsetuPositionType=SYN_HEAD|Inf=助動詞-マス,意志推量形|Reading=マショウ
9	。	。	PUNCT	補助記号-句点	_	6	punct	_	SpaceAfter=No|BunsetuBILabel=I|BunsetuPositionType=CONT|Reading=。

以下は公式のサンプルコード。

import spacy
nlp = spacy.load('ja_ginza_electra')
doc = nlp('銀座でランチをご一緒しましょう。')
for sent in doc.sents:
    for token in sent:
        print(
            token.i,
            token.orth_,
            token.lemma_,
            token.norm_,
            token.morph.get("Reading"),
            token.pos_,
            token.morph.get("Inflection"),
            token.tag_,
            token.dep_,
            token.head.i,
        )
    print('EOS')

これをもとに、個人情報のマスキング用に書き換えたのがこちら。

import spacy


nlp = spacy.load("ja_ginza_electra")


def mask_text(text):
    doc = nlp(str(text))
    masked_text = ""
    for sent in doc.sents:
        for token in sent:
            if token.pos_ == "PROPN":
                masked_text += "[PROPN]"
            else:
                masked_text += token.text

    return masked_text

[PROPN]は固有名詞(proper noun)を表す。

実際に使ってみるとこんな感じ。

text = "田中太郎さんは、1990年に東京都渋谷区で生まれました。神奈川県横浜市にあるABCカンパニーでエンジニアとして働いています。彼の電話番号は080-1234-5678です。メールアドレスはtanaka.taro@example.comです。"
mask_text(text)

[PROPN][PROPN]さんは、1990年に[PROPN]で生まれました。[PROPN]にあるABCカンパニーでエンジニアとして働いています。彼の電話番号は080-1234-5678です。メールアドレスはtanaka.taro@example.comです。

メールアドレスや電話番号は厳しいみたいなので、正規表現などによるルールベースマッチングが良さそう。

text = "田中太郎さんは東京の鈴木クリニックの山田花子先生からの紹介患者。セカンドオピニオンを求めてABC123大学病院を受診した。大阪のasdfカンパニーで勤務しており、仕事柄XYZ症候群になりやすいと考えられる。"
mask_text(text)

[PROPN][PROPN]さんは[PROPN]の[PROPN]クリニックの[PROPN][PROPN]先生からの紹介患者。セカンドオピニオンを求めてABC123大学病院を受診した。[PROPN]のasdfカンパニーで勤務しており、仕事柄XYZ症候群になりやすいと考えられる。

地名はマスキングできているが、病院名や企業名は微妙?

最終的には目視による確認が必要そうだが、全部手作業するよりは楽といったところか。

Transformersを使って学習を行う際、[PROPN]をトークナイザーのspecial tokenとして追加しても良いかもしれない。

もしくは空白や●●などに置き換えるケースもありそう。


備忘録としてコードを1行ずつ振り返っていく。

ginzaを使うためにはspacyが必要で、pip installした時に一緒に入ってくる。

spaCy · Industrial-strength Natural Language Processing in Python
spaCy is a free open-source library for Natural Language Processing in Python. It features NER, POS tagging, dependency ...

まずはginzaをロードする。

公式サイトを見る限り、nlp = spacy.load(…)と書くのが一般的みたい。

import spacy


nlp = spacy.load("ja_ginza_electra")

次に、入力するテキストをトークンにする。

nlpからコールするとDocオブジェクトが帰ってくる。

Doc · spaCy API Documentation
A container for accessing linguistic annotations.

Doc is a sequence of Token objects. Access sentences and named entities, export annotations to numpy arrays, losslessly serialize to compressed binary strings. The Doc object holds an array of TokenC structs. The Python-level Token and Span objects are views of this array, i.e. they don’t own the data themselves.

doc = nlp(str(text))

Docの中は文単位で区切られており、Doc.sentsのイテレーターで1つ1つの文にアクセスできる。

Linguistic Features · spaCy Usage Documentation
spaCy is a free open-source library for Natural Language Processing in Python. It features NER, POS tagging, dependency ...
for sent in doc.sents:

sentはTokenオブジェクトで構成されており、これまたイテレーターになっているのでループでアクセス可能。

Token · spaCy API Documentation
An individual token — i.e. a word, punctuation symbol, whitespace, etc.
    for token in sent:

Tokenオブジェクトの属性には様々な情報が格納されており、トークナイズされる前のテキストはtext属性、品詞はposまたはpos_属性で取得できる。

text
Verbatim text content.
TYPE: str

pos
Coarse-grained part-of-speech from the Universal POS tag set.
TYPE: int

pos_
Coarse-grained part-of-speech from the Universal POS tag set.
TYPE: str

Universal POS tag setはここで確認できる。

POSはPart of Speechの略で品詞を表す。

Universal POS tags

今回は個人情報(=固有名詞)をマスキングしたかったので、PROPNに一致するトークンかどうかで場合分けすればOK。

今回はpos_属性でstringが一致するかどうかで書いてみた。

            if token.pos_ == "PROPN":
                masked_text += "[PROPN]"
            else:
                masked_text += token.text

もしintで比較したい場合、spacy.symbolsを使うとわかりやすい。

https://github.com/explosion/spaCy/blob/master/spacy/symbols.pyx

シンボルを使う場合、先程のコードは次のように書き換えられる。

アンダーバーの有無に注意。

from spacy.symbols import PROPN

(中略)

            if token.pos == PROPN:
                masked_text += "[PROPN]"
            else:
                masked_text += token.text

以上から出来上がったマスキング関数が冒頭に記載したものになる。

import spacy


nlp = spacy.load("ja_ginza_electra")


def mask_text(text):
    doc = nlp(str(text))
    masked_text = ""
    for sent in doc.sents:
        for token in sent:
            if token.pos_ == "PROPN":
                masked_text += "[PROPN]"
            else:
                masked_text += token.text

    return masked_text

この関数を使う際は、テキストファイルをwith open(…)してからforループを回してもいいし、データフレームならpandasやpolarsでapplyしてもいい。

大規模言語モデル入門

ChatGPT/LangChainによるチャットシステム構築[実践]入門

コメント

タイトルとURLをコピーしました