LUKEによる日本語感情分析(ソースコードと実行結果)


日本国憲法の分析結果

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 opencv-python numpy pillow ruptures

LUKE日本語感情分析プログラム

概要

本プログラムは、日本語テキストから8種類の感情(喜び、悲しみ、期待、驚き、怒り、恐れ、嫌悪、信頼)を自動的に分類する。各文章に対して感情スコアを算出し、文章全体の感情変化を可視化する。

主要技術

プログラムは事前学習済みのLUKEモデル(Mizuiro-sakura/luke-japanese-large-sentiment-analysis-wrime)を使用する。このモデルはWRIMEデータセットでファインチューニングされており、日本語テキストの8感情分類に特化している。処理手順は以下の通りである:

  1. テキストをLUKEトークナイザーで処理
  2. トランスフォーマーモデルで特徴抽出
  3. 8感情分類ヘッドで各感情の生スコアを計算
  4. ソフトマックス関数で確率値に変換

参考文献

[1] Yamada, I., Asai, A., Shindo, H., Takeda, H., & Matsumoto, Y. (2020). LUKE: Deep Contextualized Entity Representations with Entity-aware Self-attention. In Proceedings of the 2020 Conference on Empirical Methods in Natural Language Processing (EMNLP), pages 6442–6454.

[2] Kajiwara, T., Chu, C., Takemura, N., Nakashima, Y., & Nagahara, H. (2021). WRIME: A New Dataset for Emotional Intensity Estimation with Subjective and Objective Annotations. In Proceedings of the 2021 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, pages 2095–2104.


# プログラム名: LUKE日本語感情分析プログラム
# 特徴技術名: LUKE (Language Understanding with Knowledge-based Embeddings)
# 出典: Yamada, I., Asai, A., Shindo, H., Takeda, H., & Matsumoto, Y. (2020). LUKE: Deep Contextualized Entity Representations with Entity-aware Self-attention. In Proceedings of the 2020 Conference on Empirical Methods in Natural Language Processing (EMNLP), pages 6442–6454.
# 特徴機能: エンティティ認識型自己注意機構により、文中の単語とエンティティを独立したトークンとして扱い、それぞれの文脈を考慮した表現を出力する機能
# 学習済みモデル: Mizuiro-sakura/luke-japanese-large-sentiment-analysis-wrime - LUKEの日本語版をWRIMEデータセットで8感情分類にファインチューニングしたモデル - https://huggingface.co/Mizuiro-sakura/luke-japanese-large-sentiment-analysis-wrime
# 方式設計:
#   関連利用技術: Transformers(Hugging Face)、PyTorch、NumPy、OpenCV、Pillow、ruptures
#   入力と出力: 入力: テキストファイル(tkinterでファイル選択)、出力: 感情分析結果をprint()で表示、OpenCV画面で感情変化グラフ表示、終了時result.txt保存
#   処理手順: テキスト読み込み→文章分割→感情分類→スコア計算→変化点検出→グラフ生成
#   前処理、後処理: テキスト正規化、トークン化、ソフトマックス確率変換
#   追加処理: 閾値処理による文章抽出、感情変化点検出処理 - Peltアルゴリズムによる統計的変化点検出を実施。感情スコアの時系列データから有意な変化点を自動検出
#   調整を必要とする設定値: 閾値設定(0.0-1.0)、変化点検出感度(1.0)
# 将来方策: 変化点検出の感度(現在1.0固定)を自動最適化する機能 - 文章全体の感情変化の分散から適切な感度を動的に決定
# その他の重要事項: 8感情順番固定(喜び0、悲しみ1、期待2、驚き3、怒り4、恐れ5、嫌悪6、信頼7)、グラフは各文章の最大感情のみ表示
# 前準備: pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
# pip install transformers opencv-python numpy pillow ruptures

from transformers import AutoTokenizer, AutoModelForSequenceClassification, LukeConfig
import torch
import numpy as np
import cv2
import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageDraw, ImageFont
import ruptures as rpt

# 設定値
CHANGE_POINT_THRESHOLD = 0.2  # 感情変化点検出の閾値(0.0-1.0)※少数データ時のみ使用
CHANGE_POINT_PENALTY = 1.0  # 変化点検出の感度(0.1-1.0、小さいほど敏感)
GRAPH_WIDTH = 1400  # グラフの幅(凡例スペースを含む)
GRAPH_HEIGHT = 800  # グラフの高さ
GRAPH_MARGIN = 80  # グラフの余白
MAX_SEQ_LENGTH = 512  # トークナイザーの最大シーケンス長

# 感情の色定義(BGR形式)
EMOTION_COLORS = [
    (0, 0, 255),    # 赤(喜び)
    (255, 0, 0),    # 青(悲しみ)
    (0, 255, 0),    # 緑(期待)
    (255, 255, 0),  # シアン(驚き)
    (0, 0, 128),    # 暗赤(怒り)
    (128, 0, 128),  # 紫(恐れ)
    (0, 128, 128),  # 暗黄(嫌悪)
    (255, 128, 0)   # 水色(信頼)
]

print('LUKE日本語感情分析プログラム')
print('本プログラムはLUKEモデルを使用してテキストの感情を8種類に分類します')
print('使用方法: テキストファイルを選択し、閾値を設定してください')
print('グラフ表示後、任意のキーを押すとプログラムが終了します')

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

# モデルとトークナイザーの読み込み
print('LUKEモデルを読み込み中...')
try:
    tokenizer = AutoTokenizer.from_pretrained('Mizuiro-sakura/luke-japanese-large-sentiment-analysis-wrime')
    config = LukeConfig.from_pretrained('Mizuiro-sakura/luke-japanese-large-sentiment-analysis-wrime', output_hidden_states=True)
    model = AutoModelForSequenceClassification.from_pretrained('Mizuiro-sakura/luke-japanese-large-sentiment-analysis-wrime', config=config)
    model = model.to(device)
    print('モデル読み込み完了')
except Exception as e:
    print(f'モデルの読み込みに失敗しました: {e}')
    exit()

# ファイル選択
root = tk.Tk()
root.withdraw()
file_path = filedialog.askopenfilename(title='テキストファイルを選択', filetypes=[('Text files', '*.txt')])
if not file_path:
    print('ファイルが選択されていません')
    exit()

# テキスト読み込み
try:
    with open(file_path, 'r', encoding='utf-8') as f:
        text = f.read()
    print(f'ファイル読み込み完了: {file_path}')
except Exception as e:
    print(f'ファイルの読み込みに失敗しました: {e}')
    exit()

# 文章分割
sentences = [s.strip() for s in text.split('。') if s.strip()]
print(f'文章数: {len(sentences)}')

# 感情ラベル
emotion_labels = ['喜び', '悲しみ', '期待', '驚き', '怒り', '恐れ', '嫌悪', '信頼']

# 閾値設定
threshold_input = input('\n閾値を入力(0.0-1.0、未入力の場合は0.0): ').strip()
try:
    threshold = float(threshold_input) if threshold_input else 0.0
    if threshold < 0.0 or threshold > 1.0:
        threshold = 0.0
        print('閾値が範囲外です。0.0に設定します。')
except ValueError:
    threshold = 0.0
    print('無効な閾値です。0.0に設定します。')

print(f'\n感情分析を開始します(閾値: {threshold})')

# 感情分析実行
results = []
max_emotions = []  # 各文章の最大感情のインデックスを保存
max_scores = []    # 各文章の最大感情のスコアを保存

for i, sentence in enumerate(sentences):
    if not sentence:
        results.append(np.zeros(8))  # 空文章用のダミースコアを追加
        max_emotions.append(0)
        max_scores.append(0.0)
        continue

    # トークン化
    token = tokenizer(sentence, truncation=True, max_length=MAX_SEQ_LENGTH, padding='max_length', return_tensors='pt')
    token = {k: v.to(device) for k, v in token.items()}

    # 推論
    with torch.no_grad():
        output = model(**token)
        scores = torch.softmax(output.logits, dim=-1).squeeze().cpu().numpy()

    results.append(scores)

    # 最大感情を特定
    max_emotion_idx = np.argmax(scores)
    max_score = scores[max_emotion_idx]
    max_emotions.append(max_emotion_idx)
    max_scores.append(max_score)

    # 結果表示
    print(f'\n文章{i+1}: {sentence}')

    # 全感情表示
    for j, (emotion, score) in enumerate(zip(emotion_labels, scores)):
        print(f'{emotion}: {score:.3f}')
        if score >= threshold:
            print(f'→ 閾値{threshold}以上: {sentence} ({emotion}: {score:.3f})')

# 変化点検出関数(最大感情の変化を検出)
def detect_emotion_change_points(max_emotions):
    change_points = []
    for i in range(1, len(max_emotions)):
        if max_emotions[i] != max_emotions[i-1]:
            change_points.append(i)
    return change_points

# OpenCVでグラフ生成
print('\n感情変化グラフを生成しています...')

# グラフの読み方説明
print('\n【グラフの読み方】')
print('縦軸: 感情スコア(0.0~1.0)')
print('  - 各感情の強さを0から1の範囲で表示')
print('  - 1.0に近いほどその感情が強い')
print('横軸: 文章番号')
print('  - テキスト内の文章の順番')
print('縦線: 感情変化点')
print('  - 最大感情が変化した箇所')
print('\n【感情スコアの算出方法】')
print('1. 各文章をLUKEモデルでトークン化')
print('2. トランスフォーマーモデルで特徴抽出')
print('3. 8感情分類ヘッドで各感情の生スコアを計算')
print('4. ソフトマックス関数で0-1の確率値に変換')
print('5. 全感情の合計が1.0になるよう正規化')
print('6. 最も高いスコアの感情をグラフに表示')
print('\n【8感情の定義(WRIMEデータセット準拠)】')
print('0. 喜び: 嬉しさ、楽しさ、満足感')
print('1. 悲しみ: 悲しさ、寂しさ、喪失感')
print('2. 期待: 期待、希望、前向きな予測')
print('3. 驚き: 驚き、意外性、予想外の感情')
print('4. 怒り: 怒り、苛立ち、不満')
print('5. 恐れ: 恐怖、不安、心配')
print('6. 嫌悪: 嫌悪感、不快感、拒絶感')
print('7. 信頼: 信頼、安心感、信用')
print('\n【感情と色の対応】')
print('喜び: 赤色')
print('悲しみ: 青色')
print('期待: 緑色')
print('驚き: シアン(水色)')
print('怒り: 暗赤色')
print('恐れ: 紫色')
print('嫌悪: 暗黄色')
print('信頼: 明るい青色')

# グラフ設定(グラフエリアは元のサイズを維持)
graph_area_width = 1200 - 2 * GRAPH_MARGIN  # グラフ描画エリアの幅
graph_height = GRAPH_HEIGHT - 2 * GRAPH_MARGIN

# 画像初期化
img = np.ones((GRAPH_HEIGHT, GRAPH_WIDTH, 3), dtype=np.uint8) * 255

# 軸描画
cv2.line(img, (GRAPH_MARGIN, GRAPH_MARGIN), (GRAPH_MARGIN, GRAPH_HEIGHT - GRAPH_MARGIN), (0, 0, 0), 2)
cv2.line(img, (GRAPH_MARGIN, GRAPH_HEIGHT - GRAPH_MARGIN), (GRAPH_MARGIN + graph_area_width, GRAPH_HEIGHT - GRAPH_MARGIN), (0, 0, 0), 2)

# グリッド描画
for i in range(11):
    y = int(GRAPH_HEIGHT - GRAPH_MARGIN - i * graph_height / 10)
    cv2.line(img, (GRAPH_MARGIN, y), (GRAPH_MARGIN + graph_area_width, y), (200, 200, 200), 1)
    cv2.putText(img, f'{i/10:.1f}', (GRAPH_MARGIN - 40, y + 5), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 0), 1)

# X軸ラベル
for i in range(len(sentences)):
    if i % max(1, len(sentences) // 10) == 0:
        x = int(GRAPH_MARGIN + i * graph_area_width / (len(sentences) - 1 if len(sentences) > 1 else 1))
        cv2.putText(img, str(i + 1), (x - 10, GRAPH_HEIGHT - GRAPH_MARGIN + 25), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 0), 1)

# 感情変化点の検出
change_points = detect_emotion_change_points(max_emotions)

# 変化点の背景描画
for cp in change_points:
    x = int(GRAPH_MARGIN + cp * graph_area_width / (len(sentences) - 1 if len(sentences) > 1 else 1))
    # 太いオレンジ線で描画
    cv2.line(img, (x, GRAPH_MARGIN), (x, GRAPH_HEIGHT - GRAPH_MARGIN), (0, 165, 255), 3)
    # 変化点の文章番号を表示
    cv2.putText(img, f'CP{cp}', (x - 15, GRAPH_MARGIN - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 2)

# 最大感情の折れ線グラフ描画
for i in range(len(sentences) - 1):
    x1 = int(GRAPH_MARGIN + i * graph_area_width / (len(sentences) - 1 if len(sentences) > 1 else 1))
    y1 = int(GRAPH_HEIGHT - GRAPH_MARGIN - max_scores[i] * graph_height)
    x2 = int(GRAPH_MARGIN + (i + 1) * graph_area_width / (len(sentences) - 1 if len(sentences) > 1 else 1))
    y2 = int(GRAPH_HEIGHT - GRAPH_MARGIN - max_scores[i + 1] * graph_height)

    # 現在の文章の最大感情の色で線を描画
    color = EMOTION_COLORS[max_emotions[i]]
    cv2.line(img, (x1, y1), (x2, y2), color, 2)

    # データポイントを描画
    cv2.circle(img, (x1, y1), 5, color, -1)

    # 感情名を点の上に表示(文章数が少ない場合のみ)
    if len(sentences) <= 20:
        cv2.putText(img, emotion_labels[max_emotions[i]], (x1 - 15, y1 - 10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.4, color, 1)

# 最後の点
if len(sentences) > 0:
    x_last = int(GRAPH_MARGIN + (len(sentences) - 1) * graph_area_width / (len(sentences) - 1 if len(sentences) > 1 else 1))
    y_last = int(GRAPH_HEIGHT - GRAPH_MARGIN - max_scores[-1] * graph_height)
    cv2.circle(img, (x_last, y_last), 5, EMOTION_COLORS[max_emotions[-1]], -1)
    if len(sentences) <= 20:
        cv2.putText(img, emotion_labels[max_emotions[-1]], (x_last - 15, y_last - 10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.4, EMOTION_COLORS[max_emotions[-1]], 1)

# 凡例(日本語対応)- グラフエリアの外側に配置
font = ImageFont.truetype("C:/Windows/Fonts/msgothic.ttc", 16)
img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)

# 凡例の位置をグラフエリアの完全外側に配置
legend_x = GRAPH_MARGIN + graph_area_width + 30  # グラフエリアの右端から30ピクセル右
legend_y = 100

# 色名の定義(日本語)
color_names_jp = ['赤', '青', '緑', 'シアン', '暗赤', '紫', '暗黄', '明青']

for i, emotion in enumerate(emotion_labels):
    y_pos = legend_y + i * 30
    # 色付き四角形はOpenCVで描画
    cv2.rectangle(img, (legend_x, y_pos), (legend_x + 20, y_pos + 15), EMOTION_COLORS[i], -1)
    # 日本語テキストはPillowで描画(感情名と色名)
    draw.text((legend_x + 30, y_pos), f'{emotion}({color_names_jp[i]})', font=font, fill=(0, 0, 0))

img = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)

# タイトル
cv2.putText(img, 'Emotion Change - Maximum Emotion per Sentence', (GRAPH_WIDTH // 2 - 200, 40),
            cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)

# 画像表示
cv2.namedWindow('Emotion Analysis Graph', cv2.WINDOW_NORMAL)  # リサイズ可能なウィンドウ
cv2.imshow('Emotion Analysis Graph', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

# グラフ画像保存
cv2.imwrite('emotion_graph.png', img)
print('emotion_graph.pngに保存しました')

# 結果保存
try:
    with open('result.txt', 'w', encoding='utf-8') as f:
        f.write('感情分析結果\n')
        f.write(f'ファイル: {file_path}\n')
        f.write(f'文章数: {len(sentences)}\n')
        f.write(f'閾値: {threshold}\n\n')

        for i, (sentence, scores) in enumerate(zip(sentences, results)):
            f.write(f'文章{i+1}: {sentence}\n')
            for emotion, score in zip(emotion_labels, scores):
                f.write(f'{emotion}: {score:.3f}\n')
            f.write('\n')

    print('result.txtに保存しました')
except Exception as e:
    print(f'結果ファイルの保存に失敗しました: {e}')