日本語BERT文脈埋め込み分析

【概要】 BERTは文脈を考慮した単語埋め込み技術で、同一単語でも文脈により異なるベクトル表現を生成する。日本語BERT(tohoku-nlp/bert-base-japanese-v3)を用いて「手」の多義性を分析。身体部位としての「手」と方法・手段としての「手」のベクトル類似度を計算し、文脈による意味の違いを数値化。コサイン類似度とヒートマップで可視化し、BERTの文脈理解能力を実験的に確認する。


1. 概要

主要技術:BERT(Bidirectional Encoder Representations from Transformers)

出典:Devlin, J., Chang, M. W., Lee, K., & Toutanova, K. (2019). BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. In Proceedings of the 2019 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies (pp. 4171-4186).

技術の特徴:BERTは文章中の各単語を前後の文脈を考慮して理解する技術である。従来の単語埋め込みでは単語に固定的なベクトルを割り当てていたが、BERTでは同じ単語でも使われる文脈によって異なるベクトル表現を生成する。これにより多義語の意味識別が可能となる。

アプリ例:文書分類、質問応答、感情分析、機械翻訳

体験価値:「手」という単語が文脈により異なる意味(身体部位/方法)を持つことを、ベクトルの類似度として数値的に観察できる。

2. 事前準備

Python, Windsurfをインストールしていない場合の手順(インストール済みの場合は実行不要)。

  1. 管理者権限でコマンドプロンプトを起動する(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)。
  2. 以下のコマンドをそれぞれ実行する(winget コマンドは1つずつ実行)。
REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Windsurf をシステム領域にインストール
winget install --scope machine --id Codeium.Windsurf -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
REM Windsurf のパス設定
set "WINDSURF_PATH=C:\Program Files\Windsurf"
if exist "%WINDSURF_PATH%" (
    echo "%PATH%" | find /i "%WINDSURF_PATH%" >nul
    if errorlevel 1 setx PATH "%PATH%;%WINDSURF_PATH%" /M >nul
)

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

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


pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install transformers scikit-learn matplotlib numpy unidic-lite fugashi japanize-matplotlib

3. プログラムコード


# プログラム名: 日本語BERT文脈埋め込み分析プログラム
# 特徴技術名: BERT (Bidirectional Encoder Representations from Transformers)
# 出典: Devlin, J., Chang, M. W., Lee, K., & Toutanova, K. (2019). BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. In Proceedings of the 2019 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 1 (Long and Short Papers) (pp. 4171-4186).
# 特徴機能: 文脈依存的な単語埋め込み生成 - 双方向Transformerにより単語の前後文脈を考慮した768次元の動的埋め込みベクトルを生成
# 学習済みモデル: tohoku-nlp/bert-base-japanese-v3 - 日本語Wikipedia及びCC-100コーパスで事前学習された日本語BERTモデル、Unidic 2.1.2形態素解析器使用、語彙サイズ32,768、URL: https://huggingface.co/tohoku-nlp/bert-base-japanese-v3
# 方式設計:
#   - 関連利用技術:
#     - Transformers (Hugging Face): BERTモデルとトークナイザーのロード・実行
#     - PyTorch: テンソル演算とモデル推論
#     - scikit-learn: コサイン類似度計算
#     - matplotlib/japanize_matplotlib: 類似度ヒートマップの可視化
#   - 入力と出力: 入力: プログラム内で定義された日本語文章リスト、出力: コンソールへの分析結果表示とヒートマップ画像
#   - 処理手順: 1)文章をトークン化、2)BERTで各トークンの埋め込みを取得、3)対象単語の埋め込みを抽出、4)コサイン類似度を計算、5)結果を可視化
#   - 前処理、後処理: 前処理: Unidic形態素解析によるサブワード分割、後処理: 複数トークンの場合は平均ベクトルを使用
#   - 追加処理: 対象単語が複数トークンに分割された場合の平均化処理により、サブワード分割の影響を軽減
#   - 調整を必要とする設定値: TARGET_WORD(分析対象の単語、デフォルト: "手")、MODEL_NAME(使用するBERTモデル、デフォルト: "tohoku-nlp/bert-base-japanese-v3")
# 将来方策: TARGET_WORDの自動検出機能 - 入力文章群から共通して出現し、かつ文脈により意味が異なる可能性のある多義語を自動的に検出・提案する機能
# その他の重要事項: 日本語BERTモデルはサイズが大きいため初回実行時にダウンロードに時間がかかる
# 前準備:
#   - pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
#   - pip install transformers scikit-learn matplotlib numpy unidic-lite fugashi japanize-matplotlib

import torch
from transformers import AutoTokenizer, AutoModel
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
import matplotlib
matplotlib.use('TkAgg')  # Windows対応のバックエンド
import matplotlib.pyplot as plt
import japanize_matplotlib
import warnings

warnings.filterwarnings('ignore')

# 調整可能な設定値
MODEL_NAME = 'tohoku-nlp/bert-base-japanese-v3'  # 使用するBERTモデル
TARGET_WORD = '手'  # 分析対象の単語
RANDOM_SEED = 42  # 再現性のための乱数シード
RESULT_FILE = 'result.txt'  # 結果保存ファイル名
HEATMAP_VMIN = 0.7  # ヒートマップの最小値
HEATMAP_VMAX = 1.0  # ヒートマップの最大値
FONT_SIZE_TITLE = 14  # グラフタイトルのフォントサイズ
FIGURE_SIZE = (10, 8)  # グラフのサイズ

# テスト文章(「手」が異なる意味で使われている)
SENTENCES = [
    '手を洗って清潔にする',      # 身体部位
    '手にけがを負った',          # 身体部位
    '問題解決の手を考える',      # 方法・手段
    '良い手が見つからない'       # 方法・手段
]

# 再現性のための乱数シード設定
np.random.seed(RANDOM_SEED)
torch.manual_seed(RANDOM_SEED)
if torch.cuda.is_available():
    torch.cuda.manual_seed(RANDOM_SEED)

# 結果を保存するリスト
results = []

# 出力処理の関数
def print_and_save(text, results_list):
    print(text)
    results_list.append(text)

# プログラム開始時の説明
print_and_save('=== 日本語BERT文脈埋め込み分析プログラム ===', results)
print_and_save('BERTを使用して多義語の文脈依存的な意味の違いを分析します', results)
print_and_save('対象単語が異なる文脈でどのように異なる埋め込みベクトルを持つかを可視化します\n', results)

# 対象単語の存在確認
word_found = False
for sentence in SENTENCES:
    if TARGET_WORD in sentence:
        word_found = True
        break

if not word_found:
    print(f'警告: 対象単語「{TARGET_WORD}」が文章に含まれていません')
    exit()

# モデルとトークナイザーの読み込み
try:
    print('モデルをロード中...')
    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
    model = AutoModel.from_pretrained(MODEL_NAME)
    print('モデルのロードが完了しました')
except Exception as e:
    print(f'モデルのダウンロードに失敗しました: {MODEL_NAME}')
    print(f'エラー: {e}')
    exit()

# デバイスの自動選択
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
print_and_save(f'使用デバイス: {device}', results)

print_and_save(f'使用モデル: {MODEL_NAME}', results)
print_and_save('トークナイザー: Unidic 2.1.2 + WordPiece', results)

print_and_save('\n=== 入力文章 ===', results)
for i, sentence in enumerate(SENTENCES):
    print_and_save(f'文{i+1}: {sentence}', results)

# メイン処理
vectors = []
print_and_save('\n=== トークン化結果 ===', results)

for i, sentence in enumerate(SENTENCES):
    # トークン化
    inputs = tokenizer(sentence, return_tensors='pt', padding=True)
    inputs = {k: v.to(device) for k, v in inputs.items()}  # デバイスに転送

    # BERTで埋め込みを取得
    with torch.no_grad():
        outputs = model(**inputs)
        hidden_states = outputs.last_hidden_state

    # トークンを確認
    tokens = tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])
    print_and_save(f'\n文{i+1}のトークン: {tokens}', results)

    # 「手」の位置を特定
    # WordPieceトークナイザーは単語を複数のサブワードに分割する可能性がある
    positions = []
    for j, token in enumerate(tokens):
        # 完全一致または部分一致(サブワード)
        if token == TARGET_WORD or token.replace('##', '') == TARGET_WORD:
            positions.append(j)
        elif TARGET_WORD in token.replace('##', ''):
            # サブワードの一部の場合
            positions.append(j)

    print_and_save(f'「{TARGET_WORD}」のトークン位置: {positions}', results)

    if positions:
        # 複数トークンの場合は平均
        vector = hidden_states[0, positions].mean(dim=0).cpu().numpy()
    else:
        # 見つからない場合(本来は発生しないはず)
        print_and_save(f'警告: 文{i+1}で対象単語のトークンが見つかりません', results)
        vector = hidden_states[0, 1:-1].mean(dim=0).cpu().numpy()

    vectors.append(vector)
    print_and_save(f'ベクトル取得成功: 次元数 {vector.shape[0]}', results)

# コサイン類似度の計算
similarity_matrix = cosine_similarity(vectors)

# 結果出力
print_and_save(f'\n=== 「{TARGET_WORD}」のベクトル間のコサイン類似度 ===', results)

# ヘッダー行
header = '     '
for i in range(len(SENTENCES)):
    header += f'文{i+1}   '
print_and_save(header, results)

# 類似度行列の表示
for i in range(len(SENTENCES)):
    row = f'文{i+1}: '
    for j in range(len(SENTENCES)):
        row += f'{similarity_matrix[i,j]:.3f} '
    print_and_save(row, results)

# ヒートマップの作成
plt.figure(figsize=FIGURE_SIZE)
im = plt.imshow(similarity_matrix, cmap='coolwarm', vmin=HEATMAP_VMIN, vmax=HEATMAP_VMAX)
plt.colorbar(im)
plt.xticks(range(len(SENTENCES)), [f'文{i+1}' for i in range(len(SENTENCES))])
plt.yticks(range(len(SENTENCES)), [f'文{i+1}' for i in range(len(SENTENCES))])
plt.title(f'「{TARGET_WORD}」の文脈依存類似度 - BERT日本語モデル', fontsize=FONT_SIZE_TITLE)

# 各セルに数値を表示
for i in range(len(SENTENCES)):
    for j in range(len(SENTENCES)):
        plt.text(j, i, f'{similarity_matrix[i,j]:.3f}',
                ha='center', va='center', color='black')

plt.tight_layout()
plt.show()

# 分析結果
print_and_save('\n=== 分析結果 ===', results)
print_and_save(f'文1「{SENTENCES[0]}」と文2「{SENTENCES[1]}」の「{TARGET_WORD}」の類似度: {similarity_matrix[0,1]:.3f}', results)
print_and_save(f'文1「{SENTENCES[0]}」と文3「{SENTENCES[2]}」の「{TARGET_WORD}」の類似度: {similarity_matrix[0,2]:.3f}', results)
print_and_save(f'文3「{SENTENCES[2]}」と文4「{SENTENCES[3]}」の「{TARGET_WORD}」の類似度: {similarity_matrix[2,3]:.3f}', results)

# 文脈識別能力の評価
same_context = similarity_matrix[0,1]  # 同じ意味での使用
diff_context = similarity_matrix[0,2]  # 異なる意味での使用
discrimination = same_context - diff_context

print_and_save('\n=== 文脈識別評価 ===', results)
print_and_save(f'同一文脈内類似度: {same_context:.3f}', results)
print_and_save(f'異文脈間類似度: {diff_context:.3f}', results)
print_and_save(f'文脈識別スコア: {discrimination:.3f}', results)
print_and_save(f'評価: {"文脈依存性あり" if discrimination > 0.05 else "文脈依存性が限定的"}', results)

# モデル情報
print_and_save('\n=== モデル情報 ===', results)
print_and_save(f'語彙サイズ: {len(tokenizer.vocab)}', results)
print_and_save(f'埋め込み次元数: {vectors[0].shape[0]}', results)

# 結果をファイルに保存
with open(RESULT_FILE, 'w', encoding='utf-8') as f:
    f.write('\n'.join(results))
print(f'\n{RESULT_FILE}に保存しました')

4. 使用方法

  1. 上記のプログラムを実行する
  2. 実行結果として、「手」という単語の文脈による意味の違いがコサイン類似度として数値化され、ヒートマップで視覚化される。コサイン類似度は-1から1の値を取り、1に近いほど2つのベクトルが似た方向を向いている(意味が近い)ことを示す。

5. 実験・探求のアイデア

実験要素

  1. 対象単語の変更:「目」(身体部位/目的)、「線」(物理的線/路線)など他の多義語で実験する。
  2. 文章数の増加:同じ意味で使われる文章を4つ用意し、クラスタリングの精度を観察する。
  3. 類似度閾値の探索:同一文脈と判定する閾値を実験的に決定する。
  4. AIモデル選択:プログラムコード内のコメントに記載されている各モデルの特徴を参考に、以下のモデルで同じ実験を行い、文脈識別能力を比較する。
    • tohoku-nlp/bert-base-japanese-v2(旧版)
    • tohoku-nlp/bert-large-japanese-v2(大規模版)
    • tohoku-nlp/bert-base-japanese-char-v3(文字レベル版)

新発見を促す実験

  1. 慣用句での観察:「手を打つ」「手が回らない」での「手」のベクトルを観察し、物理的な手との関係を探る。
  2. 応用実験:取得したベクトルを使って文書分類器を作成し、文脈理解の実用性を体験する。
  3. 層別分析:BERTの各層(1層目、6層目、12層目)での埋め込みを比較し、文脈理解がどの層で形成されるか観察する。