ModernBERT による話者帰属句検出(ソースコードと実行結果)

Python開発環境,ライブラリ類

ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。

Python 3.12 のインストール

インストール済みの場合は実行不要。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。

REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul

関連する外部ページ

Python の公式ページ: https://www.python.org/

AI エディタ Windsurf のインストール

Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。

winget install --scope machine Codeium.Windsurf -e --silent

関連する外部ページ

Windsurf の公式ページ: https://windsurf.com/

必要なライブラリのインストール

コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する


pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install transformers scikit-learn

ModernBERT による話者帰属句検出プログラム

概要

本プログラムは、日本語文章から話者帰属句(「と彼は言った」のような表現)を検出し、その確信度を算出する。ModernBERTの文脈理解機能を用いて、文章の意味的特徴を抽出し、話者帰属句の存在を判定する。

主要技術とその概要

ModernBERTは、BERTアーキテクチャを改良した言語モデルである。RoPE(Rotary Position Embeddings)という位置エンコーディング手法により、トークン間の相対位置関係を回転行列で表現し、長い文脈における依存関係を処理する [1]。本プログラムで使用するsbintuitions/modernbert-ja-130mモデルは、4.39兆の日本語・英語トークンで学習され、最大8192トークンまで処理可能である [2]。

コサイン類似度は、ベクトル間の角度に基づく類似性の尺度である。本プログラムでは、参照話者帰属句群から抽出した特徴ベクトルと入力文の特徴ベクトルとの類似度を計算し、文脈的な類似性を評価する。この手法により、正規表現パターンに一致しない表現についても話者帰属句である可能性を判定する。

参考文献

[1] Warner, B., Chaffin, A., Clavié, B., et al. (2024). Smarter, Better, Faster, Longer: A Modern Bidirectional Encoder for Fast, Memory Efficient, and Long Context Finetuning and Inference. arXiv:2412.13663. https://arxiv.org/abs/2412.13663

[2] SB Intuitions. (2024). sbintuitions/modernbert-ja-130m. Hugging Face. https://huggingface.co/sbintuitions/modernbert-ja-130m

ソースコード


# プログラム名: ModernBERT による話者帰属句検出プログラム
# 特徴技術名: ModernBERT(Modern Bidirectional Encoder Representations from Transformers)
# 出典: Warner, B., Chaffin, A., Clavié, B., et al. (2024). Smarter, Better, Faster, Longer: A Modern Bidirectional Encoder for Fast, Memory Efficient, and Long Context Finetuning and Inference. arXiv:2412.13663. https://arxiv.org/abs/2412.13663
# 特徴機能: RoPE(Rotary Position Embeddings)により絶対位置を回転行列でエンコードし、
#          自己注意機構において明示的な相対位置依存性を自然に組み込む。これにより
#          任意の系列長への柔軟な拡張が可能となり、相対距離の増加に伴う
#          トークン間依存性の減衰を実現。話者帰属句の長距離依存関係を捉える
# 学習済みモデル: sbintuitions/modernbert-ja-130m
#                概要: SB Intuitionsが公開する日本語ModernBERT(130Mパラメータ)
#                特徴: 4.39T日本語・英語トークンで学習、語彙数102,400、最大8192トークン対応、
#                     SentencePieceトークナイザー使用、MITライセンス
#                URL: https://huggingface.co/sbintuitions/modernbert-ja-130m
#
# 方式設計:
#   関連利用技術:
#     - Transformers: HuggingFaceの事前学習済みモデル利用ライブラリ、v4.48.0以上必須
#     - PyTorch: Meta開発のディープラーニングフレームワーク、動的計算グラフと自動微分機能
#     - scikit-learn: 機械学習ライブラリ、コサイン類似度計算等の距離メトリクス機能
#   入力と出力:
#     入力: 日本語文章(文字列)
#     出力: 話者帰属句の確信度(0.0〜1.0の実数値)をprint()で表示.検出されたパターンがあれば併せて表示
#   処理手順:
#     1. ModernBERTモデルと専用トークナイザーの自動ダウンロードと初期化(初回のみ)
#     2. GPU/CPU自動フォールバック機能によるデバイス最適化
#     3. 拡充された参照話者帰属句群からの文脈特徴ベクトル抽出と平均化
#     4. 入力文章の文脈特徴ベクトル抽出(<s>トークンの埋め込み使用)
#     5. 正規表現によるパターンマッチング検出
#     6. コサイン類似度による文脈類似性評価
#     7. パターンマッチングと文脈類似度の統合による確信度計算
#   前処理・後処理:
#     前処理: SentencePieceによるサブワード分割、最大長制限によるトークン化
#     後処理: 確信度の正規化とパターン詳細の出力表示
#   追加処理:
#     - 複数の話者帰属パターンの包括的検出による再現率向上
#     - 参照ベクトルとのコサイン類似度計算による文脈的類似性評価
#     - パターンマッチングと文脈類似度の重み付き統合による精度改善
#     - GPU/CPU自動フォールバック機能による処理速度最適化
#   調整を必要とする設定値:
#     - MAX_LENGTH: 最大入力長(デフォルト8192、短文の場合は512等に削減可能)
#     - PATTERN_WEIGHT/CONTEXT_WEIGHT: パターンと文脈の重み(0.3/0.7、タスクにより調整)
#     - BASE_CONFIDENCE/MAX_CONFIDENCE: 確信度の範囲(0.05〜0.95、厳密性により調整)
#     - CONFIDENCE_THRESHOLD: 話者帰属句と判定する確信度の閾値(0.35、厳密性により調整)
#     将来的に確信度分布のヒストグラム表示機能により最適値の決定が可能
#   その他の重要事項:
#     - Flash Attention 2対応GPUでの実行により推論速度が向上
#     - ModernBERTは次文予測タスクを行わないため<s>トークンを使用
#
# 前準備:
#   pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
#   pip install transformers scikit-learn

import torch
from transformers import AutoTokenizer, AutoModel
import re
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
from datetime import datetime
from collections import defaultdict

# 調整可能な設定値
MAX_LENGTH = 8192  # ModernBERTの最大入力長
PATTERN_WEIGHT = 0.3  # パターンマッチング時の重み
CONTEXT_WEIGHT = 0.7  # 文脈類似度の重み
BASE_CONFIDENCE = 0.05  # パターン未検出時の基本確信度
MAX_CONFIDENCE = 0.95  # パターン検出時の最大確信度
CONFIDENCE_THRESHOLD = 0.35  # 話者帰属句と判定する確信度の閾値

# 参照話者帰属句(モデル学習用)
REFERENCE_TEXTS = [
    # 基本的な話者帰属表現
    'と彼は言った', 'と彼女が述べた', 'と私は話した', '彼は困ったと述べた',
    'と彼が語った', 'と彼女は叫んだ', 'と私は答えた', 'と彼は返答した',
    'と彼女が応じた', 'と私はつぶやいた', 'と彼はささやいた',
    '彼女は驚愕したと述べた', '彼は喜悦したと言った',
    '私は心配したと話した', '彼らは安心したと述べた',

    # 追加の人称代名詞パターン
    'と僕は言った', 'と僕が述べた', 'と私たちは話した', 'と我々が語った',
    'と彼らは言った', 'と彼女らが述べた', 'とあなたは言った', 'と君が話した',
    'とその人は述べた', 'とその人が言った', 'とこちらは答えた', 'とそちらが応じた',

    # 追加の動詞バリエーション
    'と彼は説明した', 'と彼女が解説した', 'と私は発言した', 'と彼が主張した',
    'と彼女は反論した', 'と私が質問した', 'と彼は尋ねた', 'と彼女が問いかけた',
    'と私は伝えた', 'と彼が報告した', 'と彼女は通知した', 'と私が連絡した',
    'と彼は宣言した', 'と彼女が断言した', 'と私は明言した', 'と彼が指摘した',
    'と彼女は提案した', 'と私が要求した', 'と彼は依頼した', 'と彼女が頼んだ',
    'と私は相談した', 'と彼が確認した', 'と彼女は否定した', 'と私が肯定した',
    'と彼は同意した', 'と彼女が賛成した', 'と私は反対した', 'と彼が批判した',

    # 感情を含む表現の追加
    '彼は悲しんだと述べた', '彼女は喜んだと言った', '私は驚いたと話した',
    '彼らは困惑したと述べた', '彼は怒ったと言った', '彼女は安堵したと述べた',
    '私は緊張したと話した', '彼らは興奮したと言った', '彼は落胆したと述べた',
    '彼女は感動したと言った', '私は後悔したと話した', '彼らは満足したと述べた',
    '彼は焦ったと言った', '彼女は慌てたと述べた', '私は迷ったと話した',

    # 助詞のバリエーション
    'と彼も言った', 'と彼女も述べた', 'と私も話した', 'と彼らも語った',
    'と彼だけが言った', 'と彼女こそが述べた', 'と私ばかりが話した',
    'と彼さえも言った', 'と彼女までも述べた', 'と私などが話した',

    # 丁寧語・敬語表現
    'と彼は申した', 'と彼女がおっしゃった', 'と私は申し上げた', 'と彼が仰った',
    'と彼女は申された', 'と私がお話しした', 'と彼はお答えになった',
    'と彼女がご説明された', 'と私はお伝えした', 'と彼がご報告された',

    # 方言・口語表現
    'と彼は言ってた', 'と彼女が話してた', 'と私は言ったんだ', 'と彼が語ったよ',
    'と彼女は言ったわ', 'と私が話したの', 'と彼は言ったぞ', 'と彼女が述べたさ',
    'と私は言っちゃった', 'と彼が話しちゃった', 'と彼女は言っちまった',

    # 時制のバリエーション
    'と彼は言っている', 'と彼女が述べている', 'と私は話している',
    'と彼が言っていた', 'と彼女は述べていた', 'と私が話していた',
    'と彼は言うだろう', 'と彼女が述べるだろう', 'と私は話すだろう',

    # 複合的な表現
    'と彼は声を上げて言った', 'と彼女が小声で述べた', 'と私は大声で話した',
    'と彼がゆっくりと語った', 'と彼女は急いで答えた', 'と私が慎重に返答した',
    'と彼は笑いながら言った', 'と彼女が泣きながら述べた', 'と私は怒りながら話した',

    # 文末表現のバリエーション
    'と彼は言い切った', 'と彼女が述べ終えた', 'と私は話し終わった',
    'と彼が言い放った', 'と彼女は述べ立てた', 'と私が話し出した',
    'と彼は言い返した', 'と彼女が言い直した', 'と私は言い添えた',

    # 引用形式のバリエーション
    'そう彼は言った', 'そう彼女が述べた', 'そう私は話した',
    'こう彼が語った', 'こう彼女は答えた', 'こう私が返答した',

    # 否定形
    'と彼は言わなかった', 'と彼女が述べなかった', 'と私は話さなかった',

    # 受動態
    'と彼によって言われた', 'と彼女によって述べられた', 'と私によって話された'
]

# 話者帰属パターン(正規表現)
ATTR_PATTERNS = [
    # 直接的な話者帰属表現
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち|あなた|君|こちら|そちら|俺|わたし|わたくし|お前|あんた)[はがも]?(?:言[っうい]た|述べた|話した|語[っう]た|叫[んび]だ|答え[た]|返答した|応じた|つぶやいた|ささやいた)',

    # 追加の動詞パターン
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち|あなた|君|俺|わたし)[はがも]?(?:説明した|解説した|発言した|主張した|反論した|質問した|尋ねた|問いかけた)',
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち|あなた|君|俺|わたし)[はがも]?(?:伝えた|報告した|通知した|連絡した|宣言した|断言した|明言した|指摘した)',
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち|あなた|君|俺|わたし)[はがも]?(?:提案した|要求した|依頼した|頼[んみ]だ|相談した|確認した|否定した|肯定した)',
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち|あなた|君|俺|わたし)[はがも]?(?:同意した|賛成した|反対した|批判した)',

    # 敬語・丁寧語パターン
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち)[はがも]?(?:申した|おっしゃった|申し上げた|仰[っい]た|申された|お話しした|お答えになった)',
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち)[はがも]?(?:ご説明された|お伝えした|ご報告された)',

    # 固有名詞を含む表現
    r'と[一-龯]+[はがも]?(?:言[っうい]た|述べた|話した|語[っう]た)',
    r'と[一-龯]+[はがも]?(?:説明した|解説した|発言した|主張した|反論した|質問した|尋ねた|問いかけた)',
    r'と[一-龯]+[はがも]?(?:伝えた|報告した|通知した|連絡した|宣言した|断言した|明言した|指摘した)',

    # 引用符を含む表現
    r'[「『][^「『」』]*[」』]と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち|あなた|君)',
    r'[「『][^「『」』]*[」』]と[一-龯]+[はがも]?(?:言[っうい]た|述べた|話した)',

    # 感情表現を含む話者帰属
    r'(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち|[一-龯]+)[はがも]?[困惑驚愕喜悦心配安心][っう]?たと(?:述べた|言[っうい]た|話した)',
    r'(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち|[一-龯]+)[はがも]?[悲喜怒哀楽苦][しんみ][んだ]と(?:述べた|言[っうい]た|話した)',
    r'(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち|[一-龯]+)[はがも]?(?:落胆|感動|後悔|満足|焦[っり]|慌て|迷[っい])たと(?:述べた|言[っうい]た|話した)',

    # 助詞バリエーション
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち)[だけこそばかりさえまでなど][はがも]?(?:言[っうい]た|述べた|話した|語[っう]た)',

    # 時制・アスペクトのバリエーション
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち)[はがも]?(?:言[っうい]ている|述べている|話している|語[っう]ている)',
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち)[はがも]?(?:言[っうい]ていた|述べていた|話していた|語[っう]ていた)',

    # 複合的な表現
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち)[はがも]?(?:声を上げて|小声で|大声で|ゆっくりと|急いで|慎重に)(?:言[っうい]た|述べた|話した)',
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち)[はがも]?(?:笑いながら|泣きながら|怒りながら)(?:言[っうい]た|述べた|話した)',

    # 文末表現のバリエーション
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち)[はがも]?(?:言い切[っり]た|述べ終えた|話し終わった|言い放[っち]た|述べ立てた|話し出した)',
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち)[はがも]?(?:言い返した|言い直した|言い添えた)',

    # 引用形式のバリエーション
    r'(?:そう|こう|ああ)(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち)[はがも]?(?:言[っうい]た|述べた|話した|語[っう]た)',

    # 口語・方言表現
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち)[はがも]?(?:言って(?:た|る)|話して(?:た|る)|述べて(?:た|る))',
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち)[はがも]?(?:言[っう]たん(?:だ|よ)|話したん(?:だ|よ)|述べたん(?:だ|よ))',
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち)[はがも]?(?:言[っう]ちゃった|話しちゃった|言[っう]ちまった)',

    # 否定形
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち)[はがも]?(?:言わなかった|述べなかった|話さなかった|語らなかった)',

    # 受動態
    r'と(?:彼|彼女|その人|私|僕|彼ら|彼女ら|我々|私たち)によって(?:言われた|述べられた|話された|語られた)'
]

# 話者属性辞書
SPEAKER_ATTRIBUTES = {
    # 一人称代名詞
    '私': {'人称': '一人称', '単複': '単数', '話者との関係': '話者本人'},
    '僕': {'人称': '一人称', '性別指示': '男性', '単複': '単数', '話者との関係': '話者本人'},
    '俺': {'人称': '一人称', '性別指示': '男性', '単複': '単数', '話者との関係': '話者本人', 'スタイル': 'カジュアル'},
    'わたし': {'人称': '一人称', '単複': '単数', '話者との関係': '話者本人'},
    'わたくし': {'人称': '一人称', '単複': '単数', '話者との関係': '話者本人', 'スタイル': 'フォーマル'},
    '私たち': {'人称': '一人称', '単複': '複数', '話者との関係': '話者を含む集団'},
    '私達': {'人称': '一人称', '単複': '複数', '話者との関係': '話者を含む集団'},
    '我々': {'人称': '一人称', '単複': '複数', '話者との関係': '話者を含む集団'},
    'われわれ': {'人称': '一人称', '単複': '複数', '話者との関係': '話者を含む集団'},
    '僕たち': {'人称': '一人称', '性別指示': '男性', '単複': '複数', '話者との関係': '話者を含む集団'},
    '僕ら': {'人称': '一人称', '性別指示': '男性', '単複': '複数', '話者との関係': '話者を含む集団', 'スタイル': 'カジュアル'},
    '俺たち': {'人称': '一人称', '性別指示': '男性', '単複': '複数', '話者との関係': '話者を含む集団', 'スタイル': 'カジュアル'},
    '俺ら': {'人称': '一人称', '性別指示': '男性', '単複': '複数', '話者との関係': '話者を含む集団', 'スタイル': 'カジュアル'},

    # 二人称代名詞
    'あなた': {'人称': '二人称', '単複': '単数', '話者との関係': '聞き手'},
    '君': {'人称': '二人称', '単複': '単数', '話者との関係': '聞き手'},
    'きみ': {'人称': '二人称', '単複': '単数', '話者との関係': '聞き手'},
    'お前': {'人称': '二人称', '単複': '単数', '話者との関係': '聞き手', 'スタイル': 'カジュアル'},
    'あんた': {'人称': '二人称', '単複': '単数', '話者との関係': '聞き手', 'スタイル': 'カジュアル'},
    'そなた': {'人称': '二人称', '単複': '単数', '話者との関係': '聞き手', 'スタイル': '古風'},
    'あなたたち': {'人称': '二人称', '単複': '複数', '話者との関係': '聞き手'},
    'あなた方': {'人称': '二人称', '単複': '複数', '話者との関係': '聞き手', 'スタイル': 'フォーマル'},
    '君たち': {'人称': '二人称', '単複': '複数', '話者との関係': '聞き手'},
    '君ら': {'人称': '二人称', '単複': '複数', '話者との関係': '聞き手', 'スタイル': 'カジュアル'},

    # 三人称代名詞
    '彼': {'人称': '三人称', '性別指示': '男性', '単複': '単数', '話者との関係': '他者'},
    '彼女': {'人称': '三人称', '性別指示': '女性', '単複': '単数', '話者との関係': '他者'},
    'かれ': {'人称': '三人称', '性別指示': '男性', '単複': '単数', '話者との関係': '他者'},
    'かのじょ': {'人称': '三人称', '性別指示': '女性', '単複': '単数', '話者との関係': '他者'},
    '彼ら': {'人称': '三人称', '性別指示': '男性', '単複': '複数', '話者との関係': '他者'},
    '彼女ら': {'人称': '三人称', '性別指示': '女性', '単複': '複数', '話者との関係': '他者'},
    'かれら': {'人称': '三人称', '性別指示': '男性', '単複': '複数', '話者との関係': '他者'},
    'その人': {'人称': '三人称', '単複': '単数', '話者との関係': '他者'},
    'その方': {'人称': '三人称', '単複': '単数', '話者との関係': '他者', 'スタイル': 'フォーマル'},
    'この人': {'人称': '三人称', '単複': '単数', '話者との関係': '他者'},
    'この方': {'人称': '三人称', '単複': '単数', '話者との関係': '他者', 'スタイル': 'フォーマル'},
    'あの人': {'人称': '三人称', '単複': '単数', '話者との関係': '他者'},
    'あの方': {'人称': '三人称', '単複': '単数', '話者との関係': '他者', 'スタイル': 'フォーマル'},

    # 指示代名詞
    'こちら': {'人称': '指示', '単複': '単数', '話者との関係': '話者側'},
    'そちら': {'人称': '指示', '単複': '単数', '話者との関係': '聞き手側'},
    'あちら': {'人称': '指示', '単複': '単数', '話者との関係': '第三者側'},

    # 役職パターン
    '部長': {'役職': '部長'},
    '課長': {'役職': '課長'},
    '係長': {'役職': '係長'},
    '主任': {'役職': '主任'},
    '社長': {'役職': '社長'},
    '専務': {'役職': '専務'},
    '常務': {'役職': '常務'},
    '取締役': {'役職': '取締役'},
    '先生': {'役職': '先生'},
    '教授': {'役職': '教授'},
    '准教授': {'役職': '准教授'},
    '講師': {'役職': '講師'},
    '助教': {'役職': '助教'},
    '医師': {'役職': '医師'},
    '弁護士': {'役職': '弁護士'},
    '議員': {'役職': '議員'}
}

# サンプル文章
SAMPLE_TEXTS = [
    '今日は良い天気ですね。',
    '「今日は良い天気ですね」と彼は言った。',
    '彼女は困ったと述べた。',
    '私は現場で彼と打ち合わせをしていた。'
]

# グローバル変数
tokenizer = None
model = None
device = None
ref_vec = None
results_for_save = []  # 結果保存用リスト

def init_model():
    """ModernBERTモデルとトークナイザーの初期化"""
    global tokenizer, model, device, ref_vec

    if tokenizer is None or model is None:
        # GPU/CPU自動選択
        device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        print(f'使用デバイス: {device}')

        model_name = 'sbintuitions/modernbert-ja-130m'
        print(f'モデル {model_name} をダウンロード中...')

        tokenizer = AutoTokenizer.from_pretrained(model_name)
        model = AutoModel.from_pretrained(model_name)
        model.to(device)
        model.eval()

        # 参照ベクトル作成
        ref_feats = []
        for text in REFERENCE_TEXTS:
            feats, _ = extract_features(text)
            ref_feats.append(feats.cpu().detach().numpy())

        # 未使用のGPUキャッシュメモリを解放
        if torch.cuda.is_available():
            torch.cuda.empty_cache()

        ref_vec = np.mean(ref_feats, axis=0)
        print('モデルのダウンロードと初期化が完了しました。')


def extract_features(text):
    """文脈特徴ベクトルの抽出"""
    inputs = tokenizer(
        text,
        return_tensors='pt',
        truncation=True,
        padding=True,
        max_length=MAX_LENGTH
    )

    # デバイスに配置
    inputs = {k: v.to(device) for k, v in inputs.items()}

    with torch.no_grad():
        outputs = model(**inputs)
        # <s>トークンの埋め込み使用
        pooled = outputs.last_hidden_state[:, 0, :]

    return pooled, inputs


def detect_patterns(text):
    """話者帰属パターンの検出"""
    patterns = []
    for pattern in ATTR_PATTERNS:
        matches = re.findall(pattern, text)
        if matches:
            patterns.extend(matches)

    return len(patterns) > 0, patterns


def extract_speaker(text, patterns):
    """話者表現の抽出"""
    if not patterns:
        return None

    # 役職名のリスト
    role_titles = ['部長', '課長', '係長', '主任', '社長', '専務', '常務', '取締役',
                  '先生', '教授', '准教授', '講師', '助教', '医師', '弁護士', '議員']

    # パターンから話者を抽出
    for pattern in patterns:
        # 固有名詞+役職の抽出を最初にチェック
        for role in role_titles:
            match = re.search(r'([一-龯]+)' + role, pattern)
            if match:
                return match.group(0)

        # 人称代名詞の抽出
        for pronoun, attrs in SPEAKER_ATTRIBUTES.items():
            # 役職名は除外
            if pronoun not in role_titles and pronoun in pattern:
                return pronoun

        # 固有名詞のみの抽出
        match = re.search(r'と([一-龯]+)[はがも]?(?:言|述べ|話し|語)', pattern)
        if match:
            return match.group(1)

    return None


def analyze_speaker_attributes(speaker):
    """話者属性の分析"""
    if not speaker:
        return {}

    attributes = {}

    # 基本属性の取得
    if speaker in SPEAKER_ATTRIBUTES:
        attributes.update(SPEAKER_ATTRIBUTES[speaker])

    # 役職を含む場合の処理
    role_titles = ['部長', '課長', '係長', '主任', '社長', '専務', '常務', '取締役',
                  '先生', '教授', '准教授', '講師', '助教', '医師', '弁護士', '議員']
    for role in role_titles:
        if role in speaker:
            attributes['役職'] = role
            if speaker not in SPEAKER_ATTRIBUTES:
                attributes['人称'] = '固有名詞'
                attributes['単複'] = '単数'
                attributes['話者との関係'] = '他者'

    # 固有名詞の場合
    if not attributes and re.match(r'^[一-龯]+$', speaker):
        attributes = {
            '人称': '固有名詞',
            '単複': '単数',
            '話者との関係': '他者'
        }

    return attributes


def calc_confidence(feats, has_pattern):
    """確信度計算"""
    # コサイン類似度計算
    feats_np = feats.cpu().numpy()
    cosine_sim = cosine_similarity(feats_np, ref_vec.reshape(1, -1))[0][0]

    # 正規化(-1〜1 → 0〜1)
    norm_sim = (cosine_sim + 1) / 2

    # パターン有無で重み付け
    if has_pattern:
        confidence = min(MAX_CONFIDENCE, norm_sim * CONTEXT_WEIGHT + PATTERN_WEIGHT)
    else:
        confidence = max(BASE_CONFIDENCE, norm_sim * 0.4)

    return confidence


def detect_attribution(text):
    """話者帰属句検出のメイン関数"""
    # モデル初期化
    init_model()

    feats, _ = extract_features(text)
    has_pattern, patterns = detect_patterns(text)
    confidence = calc_confidence(feats, has_pattern)

    # 話者属性分析
    speaker = extract_speaker(text, patterns)
    attributes = analyze_speaker_attributes(speaker)

    # 結果表示
    print(f'文: {text}')
    print(f'確信度: {confidence:.3f}')

    # 確信度が閾値以上の場合のみ話者として検出
    if confidence >= CONFIDENCE_THRESHOLD:
        if patterns:
            print(f'検出されたパターン: {patterns}')

        if speaker and attributes:
            print('\n【話者属性分析】')
            print(f'検出された話者表現: {speaker}')
            print('属性情報:')
            for key, value in attributes.items():
                print(f'  - {key}: {value}')
    else:
        print(f'確信度が閾値({CONFIDENCE_THRESHOLD})未満のため、話者帰属句として検出されませんでした。')
        speaker = None  # 閾値未満の場合は話者をNoneに設定
        attributes = {}

    print()

    # 結果を保存用リストに追加(確信度が閾値以上の場合のみ話者情報を保存)
    result = {
        'text': text,
        'confidence': confidence,
        'patterns': patterns if confidence >= CONFIDENCE_THRESHOLD else [],
        'speaker': speaker if confidence >= CONFIDENCE_THRESHOLD else None,
        'attributes': attributes if confidence >= CONFIDENCE_THRESHOLD else {}
    }
    results_for_save.append(result)

    return confidence


def analyze_speaker_network(results):
    """話者間の対話関係を分析"""
    # 話者の連続性を分析(誰の後に誰が話したか)
    speaker_transitions = defaultdict(lambda: defaultdict(int))
    speaker_counts = defaultdict(int)

    # 前の話者を追跡
    prev_speaker = None

    for result in results:
        if result['speaker']:
            current_speaker = result['speaker']
            speaker_counts[current_speaker] += 1

            if prev_speaker and prev_speaker != current_speaker:
                speaker_transitions[prev_speaker][current_speaker] += 1

            prev_speaker = current_speaker

    return speaker_transitions, speaker_counts


def display_network_analysis(transitions, counts):
    """ネットワーク分析結果の表示"""
    print('\n【話者ネットワーク分析】')
    print('\n=== 分析結果の読み方 ===')
    print('話者間の対話関係を「A → B: N回」の形式で表示します。')
    print('これは話者Aの発言の後に話者Bが発言した回数を示します。')
    print('回数が多いほど、その話者間での対話が頻繁であることを意味します。\n')

    print('=== 話者間の遷移パターン ===')

    # 遷移データを整形して表示
    transition_list = []
    for from_speaker, to_speakers in transitions.items():
        for to_speaker, count in to_speakers.items():
            transition_list.append((from_speaker, to_speaker, count))

    # 回数の多い順にソート
    transition_list.sort(key=lambda x: x[2], reverse=True)

    if transition_list:
        for from_sp, to_sp, count in transition_list:
            print(f'{from_sp} → {to_sp}: {count}回')
    else:
        print('話者間の遷移は検出されませんでした。')

    return transition_list


def analyze_speaker_frequency(results):
    """話者の発言頻度統計"""
    speaker_stats = defaultdict(int)
    total_attributions = 0

    for result in results:
        if result['speaker']:
            speaker_stats[result['speaker']] += 1
            total_attributions += 1

    return speaker_stats, total_attributions


def display_frequency_analysis(speaker_stats, total):
    """発言頻度統計の表示"""
    print('\n【話者発言頻度統計】')
    print(f'総発言数: {total}回\n')

    # 発言回数の多い順にソート
    sorted_speakers = sorted(speaker_stats.items(), key=lambda x: x[1], reverse=True)

    frequency_data = []
    for speaker, count in sorted_speakers:
        percentage = (count / total) * 100 if total > 0 else 0
        print(f'{speaker}: {count}回 ({percentage:.1f}%)')
        frequency_data.append((speaker, count, percentage))

    return frequency_data


def save_results_to_file():
    """結果をファイルに保存"""
    if not results_for_save:
        return

    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

    # 集計データの計算
    stats = {
        '人称': defaultdict(int),
        '性別指示': defaultdict(int),
        '単複': defaultdict(int)
    }

    for result in results_for_save:
        if result['attributes']:
            for key in ['人称', '性別指示', '単複']:
                if key in result['attributes']:
                    stats[key][result['attributes'][key]] += 1
                elif key == '性別指示':
                    stats[key]['指定なし'] += 1

    # 話者ネットワーク分析
    transitions, speaker_counts = analyze_speaker_network(results_for_save)

    # 発言頻度統計
    speaker_stats, total_attributions = analyze_speaker_frequency(results_for_save)

    # ファイルに書き込み
    with open('result.txt', 'w', encoding='utf-8') as f:
        f.write('話者帰属句検出結果\n')
        f.write(f'生成日時: {timestamp}\n\n')

        f.write('【概要】\n')
        f.write('本機能は、日本語文章中の話者帰属句から話者の属性情報を抽出します。\n')
        f.write('属性情報には人称、性別指示、単複、役職、話者との関係が含まれます。\n\n')

        f.write('【算出方法】\n')
        f.write('1. 正規表現パターンマッチングにより話者帰属句を検出\n')
        f.write('2. 検出された話者表現(代名詞、固有名詞等)を抽出\n')
        f.write('3. 正規表現と辞書マッチングにより話者を特定\n')
        f.write('4. 事前定義された属性辞書との照合により属性を判定\n')
        f.write('5. 文脈情報を考慮した属性の確定\n\n')

        f.write('【処理結果】\n')
        f.write('=' * 80 + '\n')

        for result in results_for_save:
            f.write(f'入力文: {result["text"]}\n')
            f.write(f'確信度: {result["confidence"]:.3f}\n')
            if result['speaker']:
                f.write(f'話者表現: {result["speaker"]}\n')
                if result['attributes']:
                    f.write('属性:\n')
                    for key, value in result['attributes'].items():
                        f.write(f'  {key}: {value}\n')
            elif result['confidence'] < CONFIDENCE_THRESHOLD:
                f.write(f'確信度が閾値({CONFIDENCE_THRESHOLD})未満のため、話者帰属句として検出されませんでした。\n')
            f.write('\n' + '=' * 80 + '\n')

        # 集計結果
        total_count = len([r for r in results_for_save if r['speaker']])
        if total_count > 0:
            f.write('\n【集計結果】\n')
            f.write(f'総検出数: {total_count}件\n')

            # 人称別
            if stats['人称']:
                f.write('人称別:\n')
                for key, count in stats['人称'].items():
                    percentage = (count / total_count) * 100
                    f.write(f'  {key}: {count}件 ({percentage:.1f}%)\n')

            # 性別指示
            if stats['性別指示']:
                f.write('性別指示:\n')
                for key, count in stats['性別指示'].items():
                    percentage = (count / total_count) * 100
                    f.write(f'  {key}: {count}件 ({percentage:.1f}%)\n')

            # 単複
            if stats['単複']:
                f.write('単複:\n')
                for key, count in stats['単複'].items():
                    percentage = (count / total_count) * 100
                    f.write(f'  {key}: {count}件 ({percentage:.1f}%)\n')

        # 話者ネットワーク分析を追加
        f.write('\n【話者ネットワーク分析】\n')
        f.write('\n=== 分析結果の読み方 ===\n')
        f.write('話者間の対話関係を「A → B: N回」の形式で表示します。\n')
        f.write('これは話者Aの発言の後に話者Bが発言した回数を示します。\n')
        f.write('回数が多いほど、その話者間での対話が頻繁であることを意味します。\n\n')

        f.write('=== 話者間の遷移パターン ===\n')
        transition_list = []
        for from_speaker, to_speakers in transitions.items():
            for to_speaker, count in to_speakers.items():
                transition_list.append((from_speaker, to_speaker, count))

        transition_list.sort(key=lambda x: x[2], reverse=True)

        if transition_list:
            for from_sp, to_sp, count in transition_list:
                f.write(f'{from_sp} → {to_sp}: {count}回\n')
        else:
            f.write('話者間の遷移は検出されませんでした。\n')

        # 発言頻度統計を追加
        f.write('\n【話者発言頻度統計】\n')
        f.write(f'総発言数: {total_attributions}回\n\n')

        sorted_speakers = sorted(speaker_stats.items(), key=lambda x: x[1], reverse=True)
        for speaker, count in sorted_speakers:
            percentage = (count / total_attributions) * 100 if total_attributions > 0 else 0
            f.write(f'{speaker}: {count}回 ({percentage:.1f}%)\n')

    print('結果をresult.txtに保存しました。')


# メインプログラム
print('=== 話者帰属句検出プログラム ===')
print()
print('0: サンプル文章を使用')
print('1: キーボードから文章を入力')
print('2: ファイルから文章を読み込み')
print()

choice = input('選択してください (0, 1, or 2): ')

if choice == '0':
    print()
    print('=== サンプル文章での検出結果 ===')
    print()
    for text in SAMPLE_TEXTS:
        detect_attribution(text)

    # ネットワーク分析と頻度統計を表示
    transitions, counts = analyze_speaker_network(results_for_save)
    display_network_analysis(transitions, counts)

    speaker_stats, total = analyze_speaker_frequency(results_for_save)
    display_frequency_analysis(speaker_stats, total)

    save_results_to_file()

elif choice == '1':
    print()
    while True:
        text = input('文章を入力してください(終了は空Enter): ')
        if not text:
            break
        print()
        detect_attribution(text)

    # ネットワーク分析と頻度統計を表示
    transitions, counts = analyze_speaker_network(results_for_save)
    display_network_analysis(transitions, counts)

    speaker_stats, total = analyze_speaker_frequency(results_for_save)
    display_frequency_analysis(speaker_stats, total)

    save_results_to_file()

elif choice == '2':
    print()
    print('ファイル選択ダイアログを開いています...')

    try:
        import tkinter as tk
        from tkinter import filedialog

        # tkinterのルートウィンドウを非表示で作成
        root = tk.Tk()
        root.withdraw()

        # ファイル選択ダイアログを表示
        file_path = filedialog.askopenfilename(
            title='テキストファイルを選択してください',
            filetypes=[
                ('テキストファイル', '*.txt'),
                ('すべてのファイル', '*.*')
            ]
        )

        if file_path:
            print(f'選択されたファイル: {file_path}')
            print()

            try:
                # ファイルを読み込み(エンコーディングを自動判定)
                encodings = ['utf-8', 'shift_jis', 'euc_jp', 'iso2022_jp']
                file_content = None

                for encoding in encodings:
                    try:
                        with open(file_path, 'r', encoding=encoding) as f:
                            file_content = f.read()
                        print(f'ファイルエンコーディング: {encoding}')
                        break
                    except UnicodeDecodeError:
                        continue

                if file_content is None:
                    print('ファイルの読み込みに失敗しました。エンコーディングを確認してください。')
                else:
                    # 文章を改行で分割して処理
                    sentences = [s.strip() for s in file_content.split('\n') if s.strip()]

                    print(f'検出された文章数: {len(sentences)}')
                    print()
                    print('=== ファイルからの検出結果 ===')
                    print()

                    for i, sentence in enumerate(sentences):
                        print(f'--- 文章 {i+1}/{len(sentences)} ---')
                        detect_attribution(sentence)

                    # ネットワーク分析と頻度統計を表示
                    transitions, counts = analyze_speaker_network(results_for_save)
                    display_network_analysis(transitions, counts)

                    speaker_stats, total = analyze_speaker_frequency(results_for_save)
                    display_frequency_analysis(speaker_stats, total)

                    save_results_to_file()

            except Exception as e:
                print(f'ファイル読み込みエラー: {e}')
        else:
            print('ファイルが選択されませんでした。')

    except ImportError:
        print('tkinterがインストールされていません。')
        print('ファイル選択機能を使用するにはtkinterが必要です。')
    except Exception as e:
        print(f'ファイル選択エラー: {e}')
else:
    print('無効な選択です')
    exit()