テキストデータ抽出テクニック

PythonとNLPでテキスト中の参照関係を捉える:共参照解析の実践手法と情報抽出への活用

Tags: Python, 自然言語処理, NLP, 情報抽出, 共参照解析, SpaCy, coreferee

はじめに:情報抽出の課題と共参照解析の必要性

業務で扱うテキストデータから、特定の情報や構造を自動的に抽出することは、データ分析、業務効率化、意思決定支援など、様々な目的に不可欠です。しかし、単にキーワードやパターンを抽出するだけでは、テキストの持つ複雑な意味や文脈を見落としてしまうことがあります。

特に、テキスト中では同じ実体(人物、組織、場所、物など)が、異なる表現で繰り返し参照されることが頻繁に起こります。例えば、「山田さんが報告書を提出した。彼はその内容について説明を行った。」という文では、「彼」は「山田さん」を指しています。このような参照関係を正確に理解できなければ、「報告書を提出した人物」と「説明を行った人物」が同一であるという重要な情報を捉えることができません。

このような、同じ実体を指す異なる表現を特定し、それらの間の参照関係を明らかにするタスクを共参照解析(Coreference Resolution)と呼びます。共参照解析は、自然言語理解の高度なタスクの一つであり、その結果は、情報抽出の精度を飛躍的に向上させる可能性を秘めています。

この記事では、Pythonと自然言語処理ライブラリを活用した共参照解析の実践方法について解説します。特に、Webエンジニアの皆様が既存の技術スキルを活かしつつ、具体的なテキスト抽出課題に応用できるよう、基本的な考え方から具体的なコード例、そして実務での考慮事項までを網羅的にご紹介します。

共参照解析とは何か?

共参照解析は、テキスト中に現れる複数の表現が、同じ実体を参照しているか否かを判断し、参照している表現同士をグループ化するタスクです。ここでいう「実体」は、人名、組織名といった固有表現だけでなく、一般的な名詞句(「その報告書」「新しいシステム」)や代名詞(「彼」「彼女」「それ」「これ」)なども含まれます。

共参照関係を構成する要素として、アンテセデント(Antecedent)アナフォラ(Anaphora)があります。アンテセデントは先行詞とも呼ばれ、最初に実体を参照する表現です。アナフォラは後方照応詞とも呼ばれ、アンテセデントと同じ実体を後から参照する表現です。先ほどの例「山田さんが報告書を提出した。彼はその内容について説明を行った。」では、「山田さん」がアンテセデント、「彼」がアナフォラとなります。

共参照解析の目標は、テキスト中の参照表現を網羅的に抽出し、同じ実体を指す表現の集合(共参照クラスター)を生成することです。例えば、「山田さん」と「彼」は一つの共参照クラスターにまとめられます。

この解析結果が得られれば、ある情報(例:「説明を行った」)がどの実体(例:「山田さん」)に関するものなのかを正確に特定できるようになります。これは、単に「説明を行った」という動詞とその周辺の単語だけを抽出するよりも、はるかに深いレベルでの情報理解、ひいては高精度な情報抽出を可能にします。

Pythonにおける共参照解析ライブラリ

共参照解析は複雑なタスクであり、高度な機械学習モデルや言語学的知識が必要となります。幸いなことに、Pythonには共参照解析を実行するためのライブラリが存在します。主要なNLPライブラリであるSpaCyの拡張機能として提供されているものが、比較的導入しやすく、よく利用されています。

過去にはneuralcorefというプラグインがSpaCy v2系で利用されていましたが、SpaCy v3系以降は主にcorefereeという別のプラグインが公式にサポートされています。本記事では、SpaCy v3系に対応したcorefereeを中心に解説を進めます。

corefereeは、Transformerベースのモデルなどを活用しており、比較的高い精度で共参照解析を実行することができます。

corefereeを使った共参照解析の実践

まず、必要なライブラリとモデルをインストールします。Pythonのpipを使ってインストールが可能です。

pip install spacy coreferee
python -m spacy download en_core_web_sm # 例: 英語の小型モデル
python -m spacy download ja_core_news_sm # 例: 日本語の小型モデル
python -m coreferee download en # coreferee用の英語モデル
python -m coreferee download ja # coreferee用の日本語モデル

(※モデル名は適宜、使用する言語や要件に合わせて変更してください。smは小型モデル、mdlgは中型・大型モデルです。)

インストールとモデルのダウンロードが完了したら、Pythonスクリプトから利用できます。基本的な使い方は、SpaCyのパイプラインにcorefereeを追加し、テキストを処理するだけです。

以下に、簡単な英語のテキストを使った共参照解析の例を示します。

import spacy
import coreferee

# SpaCyと言語モデルをロードし、corefereeパイプラインを追加
# 日本語の場合は 'ja_core_news_sm' に変更し、coreferee download ja を実行
nlp = spacy.load("en_core_web_sm")
nlp.add_pipe("coreferee")

text = "John gave Mary a present. He was very happy. She thanked him."

# テキストを処理
doc = nlp(text)

# 共参照クラスターの情報を取得
print("--- Coreference Clusters ---")
for cluster in doc._.coref_clusters:
    print(cluster)

# 各トークンが属するクラスター情報を確認(オプション)
print("\n--- Token Coreference Info ---")
for token in doc:
    print(f"Token: '{token.text}', Index: {token.i}, Coref_clusters: {token._.coref_clusters}")

上記のコードを実行すると、テキスト中の参照関係がクラスターとして表示されます。例えば、「John」「He」「him」が同じ人物を、「Mary」「She」が同じ人物を指していることが示されるでしょう。(モデルの精度やバージョンによって出力形式や結果は異なる場合があります。)

出力例(corefereeのバージョンやモデルにより異なる可能性があります):

--- Coreference Clusters ---
[John, He, him]
[Mary, She]

--- Token Coreference Info ---
Token: 'John', Index: 0, Coref_clusters: [John, He, him]
Token: 'gave', Index: 1, Coref_clusters: []
Token: 'Mary', Index: 2, Coref_clusters: [Mary, She]
Token: 'a', Index: 3, Coref_clusters: []
Token: 'present', Index: 4, Coref_clusters: []
Token: '.', Index: 5, Coref_clusters: []
Token: 'He', Index: 6, Coref_clusters: [John, He, him]
Token: 'was', Index: 7, Coref_clusters: []
Token: 'very', Index: 8, Coref_clusters: []
Token: 'happy', Index: 9, Coref_clusters: []
Token: '.', Index: 10, Coref_clusters: []
Token: 'She', Index: 11, Coref_clusters: [Mary, She]
Token: 'thanked', Index: 12, Coref_clusters: []
Token: 'him', Index: 13, Coref_clusters: [John, He, him]
Token: '.', Index: 14, Coref_clusters: []

doc._.coref_clustersには、coreferee.coref_cluster.CorefClusterオブジェクトのリストが格納されています。各CorefClusterオブジェクトは、そのクラスターに属するスパン(単語やフレーズの範囲)のリストを持っています。この情報を使って、どの表現が同じ実体を指しているのかをプログラムで扱えるようになります。

情報抽出への応用例

共参照解析の結果は、様々な情報抽出タスクで利用できます。ここでは、業務テキストを想定した具体的な応用例をいくつかご紹介します。

例1:ログデータからのアクション実行者特定

システムログや操作履歴のようなテキストデータから、特定のアクション(例:「設定変更」「ファイル削除」)を誰が行ったのかを抽出したい場合を考えます。ログにはユーザー名が毎回明示的に書かれているとは限らず、代名詞や省略で参照されることもあります。

import spacy
import coreferee

nlp = spacy.load("en_core_web_sm")
nlp.add_pipe("coreferee")

log_text = """
User 'admin' logged in from IP 192.168.1.100.
He accessed the system configuration page.
The administrator updated the database schema.
They finished the process.
"""

doc = nlp(log_text)

# アクション(ここでは動詞句の一部)とその実行者候補を抽出する簡単なロジック
extracted_actions = []

# 共参照クラスターを辞書形式でアクセスしやすくする
# クラスター内の全てのスパンが、そのクラスターの代表スパン(通常は最初のスパン)を指すようにマッピング
coref_map = {}
for cluster in doc._.coref_clusters:
    # クラスターの代表スパン(例: 'admin')
    main_span = cluster.main
    for mention_span in cluster.mentions:
        coref_map[mention_span.start] = main_span # スパンの開始位置をキーとする

# 動詞とその主語(nlpの依存構造解析も利用)を基にアクションと実行者を推測
for token in doc:
    # 動詞を見つけたら、その主語を探す
    if token.pos_ == "VERB":
        subject = None
        for child in token.children:
            if child.dep_ == "nsubj": # 名詞的主語
                subject = child
                break

        if subject:
            # 主語が代名詞などの場合、共参照解析の結果を適用して元の実体を探す
            resolved_subject = subject
            # 主語のスパンがcoref_mapに存在するか確認
            # coref_mapのキーはスパンの開始トークンインデックス
            if subject.start in coref_map:
                resolved_subject = coref_map[subject.start] # 代表スパンに置き換え

            extracted_actions.append({
                "action": token.text, # 動詞
                "subject": resolved_subject.text # 解析済み主語
            })

print("--- Extracted Actions ---")
for action_info in extracted_actions:
    print(f"Action: '{action_info['action']}', Subject: '{action_info['subject']}'")

この例では、SpaCyの依存構造解析で主語を特定し、その主語が共参照関係を持つ場合はcorefereeの結果を使って本来の実体に置き換えています。これにより、「He accessed」や「They finished」の「He」「They」が「admin」や「The administrator」(共参照解析の結果次第)を指していることを理解し、正確な実行者を抽出できます。

例2:顧客フィードバックからの属性と評価の関連付け

製品レビューやフィードバックから、特定の製品属性(例:「バッテリー」「画面」「デザイン」)に関する意見(肯定、否定)を抽出したい場合、共参照解析が役立ちます。

import spacy
import coreferee

# 日本語モデルを使用する例
try:
    nlp = spacy.load("ja_core_news_sm")
    nlp.add_pipe("coreferee")
except OSError:
    print("Japanese model not found. Please run 'python -m spacy download ja_core_news_sm' and 'python -m coreferee download ja'")
    exit()


feedback_text = """
このスマートフォンのバッテリー持ちは非常に良いです。それは一日中使っても充電が不要でした。
カメラの性能も素晴らしい。しかし、そのデザインは少し古く感じます。
"""

doc = nlp(feedback_text)

# 簡易的な属性と評価抽出ロジック
# (実際にはより複雑な依存構造解析や辞書が必要になります)
attribute_evaluations = []

# 共参照マップを作成
coref_map = {}
for cluster in doc._.coref_clusters:
     # クラスターの代表スパン
    main_span = cluster.main
    for mention_span in cluster.mentions:
        coref_map[mention_span.start] = main_span # スパンの開始位置をキーとする


# 特定の属性キーワードと、評価を示す形容詞などを紐付ける
# 例: バッテリー持ち (attribute) -> 良い (evaluation)
#     カメラの性能 (attribute) -> 素晴らしい (evaluation)
#     デザイン (attribute) -> 古く (evaluation)

# 簡単な属性リストと、評価キーワードの例
attributes = ["バッテリー持ち", "カメラの性能", "デザイン"]
evaluation_keywords = ["良い", "素晴らしい", "不要", "古い"] # 簡易的な例

# docを文ごとに処理
for sent in doc.sents:
    current_attribute = None

    # 文中の属性キーワードを探す
    for attr in attributes:
        # 属性キーワードのスパンを検索(単純な文字列一致)
        # 実際にはより高度なパターンマッチングが必要
        attr_span = None
        for match in spacy.matcher.PhraseMatcher(nlp.vocab)([(attr, [nlp(attr)[0].lemma_])], sent):
            attr_span = sent[match[1]:match[2]]
            break # 最初に見つかったものを使用

        if attr_span:
             current_attribute = attr_span
             break # 属性が見つかったらループを抜ける

    # 文中の評価キーワードを探す
    found_evaluations = []
    for token in sent:
        if token.text in evaluation_keywords:
            found_evaluations.append(token)

    # 属性と評価が見つかった場合、共参照解析の結果を考慮して関連付け
    if current_attribute and found_evaluations:
        for eval_token in found_evaluations:
             # 評価対象が共参照で参照されているか確認
             # 評価キーワードの周辺(ここでは評価キーワード自体)のスパンがcoref_mapに存在するか確認
             # 実際には、評価対象を示す名詞句などが共参照されることが多いが、
             # ここでは評価キーワード自体が属する文脈全体が属性を指す可能性を考慮
             resolved_subject = current_attribute.text # デフォルトは文中で見つかった属性

             # より洗練されたロジックとしては、評価キーワードの主語や修飾対象を探し、
             # それらが共参照されているかをチェックする
             # ここでは簡易的に、評価キーワードが含まれる文全体が
             # 前述の属性を指している可能性を考慮する

             # 共参照クラスターの中にcurrent_attributeが含まれているか確認
             # もし含まれていれば、そのクラスターの代表スパンを対象とする
             # 簡易的な例として、評価キーワードの周辺トークンが属するクラスターを辿る
             target_span = None
             # 評価トークンが属するクラスターを探す
             if eval_token.i in coref_map:
                 # 評価トークンが直接共参照解決されるわけではないが、
                 # 評価の対象となる名詞句などが共参照される可能性が高い
                 # ここでは、評価トークンの「親」などの依存関係を辿るか、
                 # あるいは文全体の共参照をチェックする
                 # より現実的には、依存構造解析で評価対象となる名詞を見つけ、
                 # その名詞が共参照解決されるかを見る

                 # 簡易的な代替案:文頭から属性までのスパンが共参照解決されるか見る
                 # これは正確ではないが、概念を示す
                 pass # この例では、文中で属性が見つかった場合はそれを優先する


             attribute_evaluations.append({
                 "attribute": resolved_subject, # 解決済み(ここでは単純)属性
                 "evaluation": eval_token.text,
                 "sentence": sent.text
             })
    elif found_evaluations:
        # 文中に属性が直接見つからなかったが評価キーワードがあった場合
        # 共参照解析結果から、この文が指しうる先行属性を探す
        # 例えば、「それは一日中使っても充電が不要でした。」の「それ」が
        # 前の文の「バッテリー持ち」を指している場合
        for eval_token in found_evaluations:
             # 評価キーワードを含む文が指しうる共参照対象を探す
             # この文中のスパンが、前の文の属性を指しているか?
             resolved_subject = "不明" # デフォルト

             # この文のスパンがcoref_mapに含まれており、
             # かつその代表スパンが前の文にある属性と一致するか確認
             # 評価トークン自体、あるいはその周辺のスパンが共参照解決されているか
             span_to_check = eval_token # 評価トークン自体をチェック

             # 評価トークンが属するクラスターを探す
             cluster_for_eval_token = None
             for cluster in doc._.coref_clusters:
                  for mention_span in cluster.mentions:
                      if span_to_check.i >= mention_span.start and span_to_check.i < mention_span.end:
                           cluster_for_eval_token = cluster
                           break
                  if cluster_for_eval_token: break

             if cluster_for_eval_token:
                  # このクラスターの代表スパンが、属性リストに含まれるか、
                  # または属性リストに含まれるスパンと同じクラスターに属しているか
                  # これはより複雑なロジックが必要
                  # 簡易的に、代表スパンのテキストを属性リストと照合
                  if cluster_for_eval_token.main.text in attributes:
                       resolved_subject = cluster_for_eval_token.main.text
                  # あるいは、属性スパンが属するクラスターと一致するかチェック
                  # 実際には、属性スパン自体をSpaCyで解析した結果を使ってクラスターを探す必要がある
                  # 例えば、doc.vocab[attr]..___.coref_clusters など

             attribute_evaluations.append({
                 "attribute": resolved_subject, # 解決済み属性(共参照を考慮)
                 "evaluation": eval_token.text,
                 "sentence": sent.text
             })


print("--- Extracted Attribute Evaluations ---")
for item in attribute_evaluations:
    print(f"Sentence: '{item['sentence']}' -> Attribute: '{item['attribute']}', Evaluation: '{item['evaluation']}'")

上記のコード例は、共参照解析の結果を応用する概念を示すための簡易的なものです。実際のシステムでは、SpaCyの依存構造解析、品詞タグ、固有表現抽出などの結果を組み合わせ、より精緻なルールや機械学習モデルを使って属性と評価の関係性を抽出する必要があります。しかし、共参照解析の結果を使うことで、「それ」や「その」といった指示代名詞が指す実体(この場合は属性)を特定できるようになり、抽出漏れや誤った関連付けを防ぐのに役立ちます。

パフォーマンスと実務での考慮事項

共参照解析は、一般的に計算コストの高いタスクです。特に、長いテキストや大量のテキストデータを処理する場合、実行時間やメモリ使用量が問題となる可能性があります。

まとめ

この記事では、Pythonと自然言語処理ライブラリcorefereeを使った共参照解析の実践方法と、それがテキストからの情報抽出タスクにおいていかに重要であるかをご紹介しました。

共参照解析によって、テキスト中の異なる表現が同じ実体を指している関係性を明らかにすることで、主語・目的語の特定や、属性と評価の紐付けといった、単なるキーワード抽出やパターンマッチングでは難しかった情報抽出をより高精度に行うことが可能になります。

corefereeのようなライブラリを利用すれば、複雑な共参照解析モデルを自前で開発することなく、既存のSpaCyパイプラインに組み込む形で導入できます。ただし、計算コストやモデルの精度といった実務的な考慮事項も存在します。

業務で扱うテキストデータからより深いインサイトを得たり、複雑な情報を構造化して抽出したりする必要がある場合に、共参照解析は強力なツールとなり得ます。ぜひこの記事を参考に、ご自身のプロジェクトで共参照解析の活用を検討してみてください。