独自の情報を抽出する:SpaCyによるカスタムエンティティ抽出ルール構築
はじめに
業務で扱うテキストデータから、特定の種類の情報、例えば製品の固有コード、プロジェクト名、特定のフォーマットで記述された日付や金額、顧客レビュー中の特定の属性といった情報を抽出したい、というニーズは少なくないかと存じます。自然言語処理(NLP)の分野では、このような特定の情報群を「固有表現(Named Entity)」と呼び、その抽出は固有表現抽出(Named Entity Recognition; NER)というタスクとして広く研究・応用されています。
汎用的な固有表現抽出モデル(人名、組織名、地名など)は多くのNLPライブラリで提供されていますが、皆さんの業務固有のテキストデータに現れる独自の情報を抽出するには、標準的なモデルだけでは不十分な場合があります。正規表現も強力なツールですが、テキストのバリエーションに対応するには複雑になりすぎたり、限界があったりします。
本記事では、Pythonで広く使われているNLPライブラリであるSpaCyを活用し、独自のルールに基づいてテキストから特定の情報を抽出する「ルールベースカスタムエンティティ抽出」の手法について解説します。NLPライブラリの利用経験が少なくても、Pythonでの開発経験があれば理解できるよう、具体的なコード例を豊富に交えながら進めてまいります。
カスタムエンティティ抽出とは
一般的な固有表現抽出が、あらかじめ定義されたカテゴリ(人名、組織名、日付など)に属する単語やフレーズを識別するのに対し、カスタムエンティティ抽出は、ユーザーが定義した独自のカテゴリに属する情報をテキストから見つけ出すプロセスです。
例えば、以下のようなテキストから特定の情報を抽出したいとします。
注文番号 SP789-2023Q4 が本日出荷されました。
→ 注文番号SP789-2023Q4
を抽出製品 XYZ-Alpha の改善提案(チケット ID: #12345)を受け付けました。
→ 製品名XYZ-Alpha
とチケットID#12345
を抽出【定例会議議事録】日時: 2024/03/15 10:00 場所: Web会議
→ 日時2024/03/15 10:00
、場所Web会議
を抽出
これらの情報は、標準的なNERモデルでは「固有表現」として認識されない可能性が高いです。しかし、特定のパターンやキーワード、構造に着目することで、コンピュータに識別させることが可能になります。
なぜルールベースアプローチか?
カスタムエンティティ抽出の手法としては、大きく分けて以下の2つが考えられます。
- 統計的手法/機械学習アプローチ: 大量の教師データ(人間が正解のエンティティにタグ付けしたテキスト)を用意し、そのデータに基づいてモデルを学習させる方法です。高精度が期待できますが、データ準備にコストがかかり、なぜその結果になったのかがブラックボックスになりがちです。
- ルールベースアプローチ: あらかじめ人間がテキストの特徴を分析し、特定のパターンやキーワードの組み合わせといった「ルール」を定義する方法です。
本記事で取り上げるルールベースアプローチには、以下のような利点があります。
- 直感的で制御しやすい: テキストがどのように解析され、どのようなルールが適用されているかが見えやすいため、結果の解釈やデバッグが容易です。
- データ準備コストが低い: 大規模な教師データを必要としません。対象となるテキストの構造やパターンを分析するだけでルールを構築できます。
- 特定のパターンに強い: 正規表現のように厳密なパターンにマッチさせたい場合に有効です。
- ドメイン知識を反映しやすい: その業務領域に特有の知識や用語、フォーマットを直接ルールとして組み込むことができます。
一方で、テキスト表現の多様性に対応するのが難しい、ルールの数が増えるとメンテナンスが大変になる、といった限界もあります。しかし、比較的フォーマットが決まっているテキスト(ログ、報告書、特定の入力フォームからのデータなど)や、対象となるエンティティの種類が限られている場合には、非常に効果的なアプローチです。
SpaCyによるルールベースカスタムエンティティ抽出
SpaCyは、ルールベースのマッチング機能として主に以下の2つを提供しています。これらを活用することで、柔軟なルール定義が可能になります。
Matcher
: トークンレベルのパターンマッチングを行います。単語そのものだけでなく、品詞、形態、フラグ(数字であるか、大文字であるかなど)といったトークンの属性に基づいた複雑なパターンを定義できます。PhraseMatcher
: 単語のリストや句のリストに一致するテキストを見つけます。特定の用語集や辞書に基づいてエンティティを抽出したい場合に適しています。
Matcher
を使ったパターン定義
Matcher
は、トークンのシーケンスに対するパターンを定義します。パターンは辞書のリストで表現され、各辞書はシーケンス内の1つのトークンに適用される属性と値を指定します。
例えば、「"数量: " に続く数字」というパターンを考えます。
import spacy
# 日本語モデルをロード
# なければ python -m spacy download ja_core_news_sm を実行
nlp = spacy.load("ja_core_news_sm")
from spacy.matcher import Matcher
# Matcherオブジェクトを初期化。共有の語彙(Vocab)を渡す
matcher = Matcher(nlp.vocab)
# パターン定義
# トークン属性のリストとして定義します。
# 各辞書が1トークンに対応
pattern = [
{"LOWER": "数量"}, # 1つ目のトークンは小文字で"数量"
{"IS_PUNCT": True, "OP": "?"}, # 句読点が0個または1個(例: ":")
{"IS_SPACE": True, "OP": "*"}, # スペースが0個以上
{"LIKE_NUM": True} # 数字のように見えるトークン
]
# マッチャーにパターンを追加
# 第一引数はルールのID(文字列)、第二引数はマッチした際に実行する関数(今回はNone)、第三引数以降はパターン
matcher.add("QUANTITY_PATTERN", None, pattern)
# テキストの処理
text = "在庫リスト:数量: 150個、単価: 200円"
doc = nlp(text)
# マッチングの実行
matches = matcher(doc)
# 結果の表示
print("Matched Spans:")
for match_id, start, end in matches:
span = doc[start:end] # マッチした範囲のSpanオブジェクトを取得
print(f" Match ID: {nlp.vocab.strings[match_id]}, Span: {span.text}, Start: {start}, End: {end}")
このコードでは、Matcher
を使って「数量」という単語に続いて数字が来るパターンを定義しています。LIKE_NUM
は「150個」のような数字を含むトークンにもマッチするため、より実用的なパターンを記述できます。OP
キーは、そのトークンパターンがテキスト中で出現する回数を制御します (?
: 0回または1回, *
: 0回以上, +
: 1回以上)。
PhraseMatcher
を使った辞書マッチング
特定の単語リストやフレーズリストに一致するテキストを抽出したい場合は、PhraseMatcher
が便利です。これは内部的に効率的なTrieデータ構造を利用しており、大きな辞書でも高速にマッチングが可能です。
例えば、特定の製品名のリストから製品名を抽出する場合を考えます。
import spacy
nlp = spacy.load("ja_core_news_sm")
from spacy.matcher import PhraseMatcher
# PhraseMatcherオブジェクトを初期化
matcher = PhraseMatcher(nlp.vocab)
# 抽出したい製品名のリスト
product_list = ["製品 XYZ-Alpha", "サービス Beta-V2", "旧モデル ABC"]
# リストの各要素をSpaCyのDocオブジェクトに変換
patterns = [nlp(text) for text in product_list]
# マッチャーにパターンを追加
# 第一引数はルールのID(文字列)、第二引数はマッチした際に実行する関数(今回はNone)、第三引数以降はパターン(Docオブジェクトのリスト)
matcher.add("PRODUCT_LIST", None, *patterns) # *patterns でリストを展開して渡す
# テキストの処理
text = "先日購入した製品 XYZ-Alpha が期待通りでした。新しいサービス Beta-V2 も試したいです。"
doc = nlp(text)
# マッチングの実行
matches = matcher(doc)
# 結果の表示
print("Matched Spans:")
for match_id, start, end in matches:
span = doc[start:end]
print(f" Match ID: {nlp.vocab.strings[match_id]}, Span: {span.text}, Start: {start}, End: {end}")
PhraseMatcher
は、単語リスト内のフレーズと完全に一致するテキスト部分を見つけます。これは、特定のキーワードや用語集に基づいたエンティティ抽出に非常に有効です。
マッチ結果をエンティティとして追加
Matcher
やPhraseMatcher
で見つけた一致箇所を、SpaCyのDocオブジェクトの.ents
属性にカスタムエンティティとして追加することで、後続の処理で活用しやすくなります。これは通常、カスタムパイプラインコンポーネントとして実装します。
import spacy
from spacy.matcher import Matcher, PhraseMatcher
from spacy.tokens import Span
# 日本語モデルをロード
nlp = spacy.load("ja_core_news_sm")
# マッチャーとフレーズマッチャーを準備
matcher = Matcher(nlp.vocab)
phrase_matcher = PhraseMatcher(nlp.vocab)
# 例:特定の注文番号パターン (例: XX999-YYYYQZ)
order_pattern = [{"TEXT": {"REGEX": "[A-Z]{2}[0-9]{3}"}}, {"TEXT": "-"}, {"TEXT": {"REGEX": "[0-9]{4}Q[1-4]"}}]
matcher.add("ORDER_ID", None, order_pattern)
# 例:特定の部署名リスト
department_list = ["営業部", "開発部", "広報部"]
department_patterns = [nlp(text) for text in department_list]
phrase_matcher.add("DEPARTMENT", None, *department_patterns)
# カスタムパイプラインコンポーネントの定義
@spacy.Language.component("custom_entity_extractor")
def custom_entity_extractor_component(doc):
"""MatcherとPhraseMatcherの結果をエンティティとしてDocに追加するカスタムコンポーネント"""
# Matcherによるマッチング
matches = matcher(doc)
# PhraseMatcherによるマッチング
phrase_matches = phrase_matcher(doc)
# 新しいエンティティのリストを作成
# 既存のエンティティ(もしあれば)は残しておく
new_ents = list(doc.ents)
# Matcherの結果をSpanオブジェクトに変換し、新しいエンティティリストに追加
for match_id, start, end in matches:
span_text = doc[start:end].text
# 重複や既存エンティティとの重なりを避ける処理(ここではシンプルに直接追加)
# 実際には、既存のdoc.entsと新しいspanの範囲を比較し、
# 重複しないか、あるいは優先順位に基づいて追加するかを判断します。
try:
# 新しいSpanオブジェクトを作成し、エンティティタイプを指定
new_ent = Span(doc, start, end, label=nlp.vocab.strings[match_id])
new_ents.append(new_ent)
except ValueError:
# 同一範囲に既存エンティティがあるなどでSpan作成に失敗する場合がある
print(f"Warning: Could not add span '{span_text}' as entity.")
pass
# PhraseMatcherの結果をSpanオブジェクトに変換し、新しいエンティティリストに追加
for match_id, start, end in phrase_matches:
span_text = doc[start:end].text
try:
new_ent = Span(doc, start, end, label=nlp.vocab.strings[match_id])
new_ents.append(new_ent)
except ValueError:
print(f"Warning: Could not add span '{span_text}' as entity.")
pass
# Docオブジェクトのエンティティを更新
# 重複や競合を適切に処理するためには、スパンのソートや重複チェックが必要です。
# SpaCy v3.xでは、EntSegmenterなどのコンポーネントを組み合わせるのが推奨される場合もあります。
# ここでは簡易的にセットに変換して重複をなくし、元のdoc.entsも加えて結合します。
# SpaCyのSpanオブジェクトはハッシュ化可能ですが、エンティティリストとして扱う際は注意が必要です。
# 理想的には、範囲とラベルをキーにした辞書などを使うと重複排除やソートが容易です。
# 例:start, end, label をタプルにしてsetで重複排除後、再度Spanに変換し直す
seen_spans = set()
final_ents = []
for ent in sorted(new_ents, key=lambda e: (e.start, e.end)): # 開始位置でソート
span_key = (ent.start, ent.end, ent.label)
if span_key not in seen_spans:
final_ents.append(ent)
seen_spans.add(span_key)
doc.ents = final_ents
return doc
# パイプラインにカスタムコンポーネントを追加
# 既存の'ner'コンポーネントより前に実行することで、カスタムエンティティを先に検出できます。
# 'after': 'ner' とすると、既存のNERの結果を上書き/拡張する形になります。
nlp.add_pipe("custom_entity_extractor", after="ner") # 既存のNERの後に追加する例
# テキストの処理
text = "開発部の山田さんが注文番号 AB123-2023Q3 の件で営業部と連携中です。"
doc = nlp(text)
# 結果の表示
print("Entities:")
for ent in doc.ents:
print(f" {ent.text} ({ent.label_})")
この例では、Matcher
で注文番号のパターンを、PhraseMatcher
で部署名のリストを定義し、これらのマッチ結果をcustom_entity_extractor
というカスタムパイプラインコンポーネント内でdoc.ents
に追加しています。このようにパイプラインに組み込むことで、標準のトークナイゼーションやその他の処理フローの中でカスタム抽出を実行できるようになります。
より高度なパターン定義
Matcher
のパターン定義では、トークンの様々な属性や、正規表現を使用できます。これにより、より複雑なテキスト構造に対応できます。
よく使われるトークン属性の例:
TEXT
: トークンの完全一致LOWER
,UPPER
: 小文字/大文字にした場合の完全一致POS
: 品詞タグ (例: 'NOUN', 'VERB')TAG
: 詳細な品詞タグ (例: '名詞', '動詞')DEP
: 依存関係ラベル (例: 'nsubj', 'dobj')IS_ALPHA
,IS_DIGIT
,IS_PUNCT
,IS_SPACE
: アルファベット、数字、句読点、スペースかどうかの真偽LIKE_NUM
: 数字のように見えるか ('123', '一二三', '数千')LIKE_URL
,LIKE_EMAIL
: URLやメールアドレスのように見えるかREGEX
: トークンのテキストに対する正規表現
これらの属性とオペレーター(OP
: ?
, *
, +
, !
- 否定)を組み合わせることで、柔軟なパターンを作成できます。例えば、「動詞に続く名詞句」といった依存構造に基づいたパターンも定義可能です(ただし、依存構造解析の結果が正確であることが前提となります)。
実務への応用例
ルールベースカスタムエンティティ抽出は、様々な業務課題に応用できます。
- 顧客レビューからの特定情報抽出: 製品に関する特定の不満点(例:「バッテリーの持ちが悪い」、「UIが分かりにくい」)や要望(例:「〇〇機能を追加してほしい」)など、パターン化できる意見を抽出・集計する。特定のキーワードや、そのキーワードに関連する否定・肯定表現などをルール化します。
- ログデータからのエラーパターン検出: アプリケーションログやサーバーログから、特定のフォーマットを持つエラーメッセージ、リクエストID、処理時間超過の警告などを抽出する。正規表現と組み合わせることで、より頑健な抽出が可能です。
- 社内文書からの情報収集: 報告書、議事録、メールなどから、特定のプロジェクト名、担当者、期日、決定事項といった構造化されていないテキスト中の重要な情報を抽出・整理する。特定のキーワードの周辺情報や、箇条書きのパターンなどがルール化の対象になります。
- 契約書・請求書からのキー情報抽出: 文書全体としては自由形式でも、特定の項目(契約主体、金額、日付、項目名)は一定のパターンやキーワードの近くに出現する場合が多く、これをルールで捉えることができます。
パフォーマンスと考慮事項
ルールベースアプローチは、ルールの数や複雑さ、そして対象となるテキストの量によってパフォーマンスが変動します。
PhraseMatcher
のパフォーマンス: 辞書のサイズが大きくなっても、内部構造により比較的効率的なマッチングが可能です。ただし、非常に巨大な辞書の場合はメモリ使用量に注意が必要です。Matcher
のパフォーマンス: 定義するパターンの数と複雑さがパフォーマンスに影響します。特に、*
や+
といった繰り返しオペレーターを多用する複雑なパターンは、処理に時間がかかる可能性があります。- ルールのメンテナンス: 業務の変化やテキストパターンの変更に伴い、ルールを更新・追加する必要があります。ルールの数が多くなると、その管理とデバッグが煩雑になる可能性があります。
- 曖昧性の問題: 人間の言語は曖昧さを持ちます。定義したルールが意図しないテキストにマッチしたり、逆にマッチすべきテキストを見逃したりすることが起こり得ます。これを完全に防ぐのは難しく、テストデータを用いてルールの精度を評価・改善していく作業が重要になります。
- ルール間の競合: 複数のルールが同じテキスト範囲にマッチした場合、どのルールを優先するか、あるいは両方を許可するかといった競合解決のロジックが必要になることがあります。前述のカスタムパイプラインコンポーネント内でこの処理を実装するか、SpaCyのより高度な機能(例えば、複数のSpanグループを扱う機能など)を検討する必要があります。
- ハイブリッドアプローチ: ルールベースアプローチには限界がある場合でも、機械学習モデルによる抽出結果と組み合わせることで、より頑健なシステムを構築できることがあります。例えば、機械学習モデルで大まかな範囲を特定し、その中でルールベースで詳細な情報を抽出するといった連携が考えられます。
システムに組み込む際は、抽出処理にかかる時間を考慮し、バッチ処理にするかリアルタイム処理にするか、あるいは非同期処理を取り入れるかなどを検討する必要があります。また、抽出結果の品質を継続的にモニタリングし、必要に応じてルールを改善していく運用体制も重要です。
まとめ
本記事では、SpaCyライブラリを用いたルールベースのカスタムエンティティ抽出手法について解説しました。Matcher
とPhraseMatcher
を活用することで、業務固有のテキストデータから特定の情報を効率的に抽出するシステムを構築することが可能です。
- 特定のパターンにマッチさせる
Matcher
- 辞書に含まれるフレーズにマッチさせる
PhraseMatcher
- これらの結果をSpaCyの処理パイプラインに組み込み、エンティティとして扱う方法
これらの手法は、正規表現だけでは対応が難しいが、統計的手法のための大量データ準備は困難、といった場合に強力な選択肢となります。コード例を参考に、ぜひ皆様の業務課題におけるテキストからの情報抽出に挑戦していただければ幸いです。ルールの構築とメンテナンスには労力が伴いますが、対象ドメインの専門知識を活かせる点が大きな魅力と言えるでしょう。
複雑なケースや高い汎用性が求められる場合には、機械学習アプローチや、ルールベースと統計的手法を組み合わせたハイブリッドなアプローチも視野に入れることになりますが、まずはルールベースでスモールスタートしてみるのも良い方法です。