カレーの例文でわかる！BERTによる日本語文書埋め込みの作り方

前回の記事では単語の埋め込み表現について解説しました。今回は文脈を考慮した文書全体の埋め込み表現を生成する方法について、具体的な例を交えながら解説します。特に、BERTモデルを使った実践的なアプローチに焦点を当て、「カレー」に関連するテキストを例として使用します。

## はじめに

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/98da64d1-7f80-4f69-82fe-cf589aba9e1a.png)
テキストデータの分析や検索において、文書を数値ベクトルに変換する「埋め込み（Embedding）」技術は非常に重要です。特に、文脈を考慮した埋め込みは、単語の多義性や文脈依存の意味を適切に捉えることができます。

BERTのような事前学習済み言語モデルを活用することで、高品質な文脈化埋め込みを簡単に生成できるようになりました。この記事では、日本語テキストに対してBERTモデルを適用し、文書の埋め込み表現を生成する方法を解説します。

## 環境準備

まずはGoogle Colabで実行するための環境をセットアップしましょう。日本語処理に必要なライブラリとツールをインストールします。

```python
# 必要なライブラリのインストール
!pip install transformers fugashi unidic-lite torch numpy matplotlib scikit-learn pandas

# 日本語フォントと日本語処理ツールのインストール
!apt-get install -y fonts-noto-cjk fonts-noto-cjk-extra
!apt-get install -y mecab mecab-ipadic-utf8 libmecab-dev
!pip install japanize-matplotlib

# MeCabの設定ファイルの存在確認とインストール
!ln -s /etc/mecabrc /usr/local/etc/mecabrc 2>/dev/null || true
```

必要なライブラリをインポートします。

```python
import torch
from transformers import AutoTokenizer, AutoModel
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib  # 日本語フォントの設定
from sklearn.metrics.pairwise import cosine_similarity
import pandas as pd
```

## 日本語BERTモデルの準備

今回は、日本語に特化したBERTモデルを使用します。具体的には、東北大学が公開している「tohoku-nlp/bert-base-japanese-v3」を利用します。

```python
# 日本語BERTモデルとトークナイザーのロード
model_name = "tohoku-nlp/bert-base-japanese-v3"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
```

## テキストの埋め込み関数

次に、テキストから埋め込みベクトルを生成する関数を定義します。この関数は、入力テキストをトークン化し、BERTモデルに通して埋め込みを取得します。ここでは、最後の隠れ層の出力の平均を取ることで、文書全体の埋め込みを生成します。

```python
def get_bert_embedding(text, model, tokenizer):
    # テキストをトークン化
    inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True, max_length=512)
    
    # 勾配計算を無効化して計算効率を向上
    with torch.no_grad():
        outputs = model(**inputs)
    
    # 最後の隠れ層の出力を取得（[CLS]トークンの出力または全トークンの平均）
    # 方法1: [CLS]トークンの出力を使用
    # cls_embedding = outputs.last_hidden_state[:, 0, :].numpy()
    
    # 方法2: 全トークンの平均を使用（パディングを除く）
    # attention_mask を使って、実際のトークンの部分だけを平均計算に含める
    last_hidden_state = outputs.last_hidden_state
    attention_mask = inputs['attention_mask']
    
    # マスクを拡張して隠れ状態の次元に合わせる
    mask_expanded = attention_mask.unsqueeze(-1).expand(last_hidden_state.size()).float()
    
    # マスクを適用して合計
    sum_hidden = torch.sum(last_hidden_state * mask_expanded, 1)
    
    # トークンの数で割って平均を求める
    sum_mask = torch.clamp(mask_expanded.sum(1), min=1e-9)
    mean_hidden = sum_hidden / sum_mask
    
    # numpy配列に変換
    embedding = mean_hidden.numpy()
    
    return embedding[0]  # バッチサイズ1の場合
```

## カレーに関する文書の埋め込み生成

それでは、カレーに関連する様々な文書を準備し、埋め込みを生成してみましょう。

```python
# カレーに関連するテキスト例
curry_texts = [
    "カレーは日本の家庭料理として定着しています。",
    "インドカレーは本場のスパイスを使った本格的な味わいが特徴です。",
    "カレーライスに福神漬けを添えるのは日本独自の食べ方です。",
    "スパイスから手作りするカレーは風味が豊かです。",
    "カツカレーは日本で人気のカレーの一種です。",
    "タイのグリーンカレーはココナッツミルクとハーブが特徴的です。",
    "カレーパンは日本で生まれた独自のパン料理です。",
    "ジャワカレーは甘口で子供にも人気があります。",
    "カレーのルーを使えば簡単に本格的な味が楽しめます。",
    "スープカレーは北海道札幌が発祥と言われています。"
]

# 各テキストの埋め込みを生成
curry_embeddings = []
for text in curry_texts:
    embedding = get_bert_embedding(text, model, tokenizer)
    curry_embeddings.append(embedding)

# 埋め込みをnumpy配列に変換
curry_embeddings = np.array(curry_embeddings)
print(f"埋め込みの形状: {curry_embeddings.shape}")
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/0c4da5ae-747b-4072-9b1e-7b3d2e03e3bf.png)

## 埋め込みの可視化と分析

生成した埋め込みを分析するために、コサイン類似度を計算してみましょう。これにより、テキスト間の意味的な近さを確認できます。

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

# ヒートマップとして可視化
plt.figure(figsize=(10, 8))
plt.imshow(similarity_matrix, cmap='viridis', interpolation='nearest')
plt.colorbar(label='コサイン類似度')
plt.title('カレーに関するテキスト間の類似度')
plt.xticks(np.arange(len(curry_texts)), np.arange(1, len(curry_texts) + 1), rotation=90)
plt.yticks(np.arange(len(curry_texts)), np.arange(1, len(curry_texts) + 1))
plt.tight_layout()
plt.show()

# より見やすく表示するためのDataFrame作成
similarity_df = pd.DataFrame(similarity_matrix, 
                            index=[f"T{i+1}" for i in range(len(curry_texts))],
                            columns=[f"T{i+1}" for i in range(len(curry_texts))])
display(similarity_df.round(3))

# 元のテキストとインデックスの対応を表示
for i, text in enumerate(curry_texts):
    print(f"T{i+1}: {text}")
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/92d6f48e-a360-45b5-bd35-bc798cfe6137.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/6319d8de-ddc5-4f46-8f78-5a2e9a4c0740.png)


## 文書の分類と検索

埋め込みを用いた簡単な文書検索の例を示します。クエリテキストの埋め込みと各文書の埋め込みとの類似度を計算し、最も類似度の高い文書を検索結果として返します。

```python
def search_similar_documents(query_text, document_embeddings, documents, top_n=3):
    # クエリテキストの埋め込みを取得
    query_embedding = get_bert_embedding(query_text, model, tokenizer)
    
    # 各文書との類似度を計算
    similarities = cosine_similarity([query_embedding], document_embeddings)[0]
    
    # 類似度でソートしてインデックスを取得
    top_indices = np.argsort(similarities)[::-1][:top_n]
    
    # 結果を返す
    results = []
    for idx in top_indices:
        results.append({
            "document": documents[idx],
            "similarity": similarities[idx]
        })
    
    return results

# 検索例1: 日本のカレーについて
query1 = "日本の独自のカレー文化について知りたい"
results1 = search_similar_documents(query1, curry_embeddings, curry_texts)

print(f"クエリ: {query1}")
for i, result in enumerate(results1):
    print(f"{i+1}. 類似度: {result['similarity']:.4f}")
    print(f"   {result['document']}")
    print()

# 検索例2: 本格的なスパイスカレー
query2 = "本格的なスパイスの効いたカレーのレシピ"
results2 = search_similar_documents(query2, curry_embeddings, curry_texts)

print(f"クエリ: {query2}")
for i, result in enumerate(results2):
    print(f"{i+1}. 類似度: {result['similarity']:.4f}")
    print(f"   {result['document']}")
    print()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/5df7bf65-15a6-4c56-9db3-c4951a6e5702.png)

## 実用的なシナリオ：カレーレシピの推薦システム

最後に、より実用的なシナリオとして、ユーザーの好みに合わせたカレーレシピの推薦システムを簡単に実装してみましょう。

```python
# カレーレシピのデータ
curry_recipes = [
    {
        "title": "基本の肉じゃがカレー",
        "description": "ジャガイモと牛肉を使った基本の和風カレーです。家庭的な味わいが特徴です。",
        "ingredients": ["牛肉", "じゃがいも", "にんじん", "玉ねぎ", "カレールー"],
        "difficulty": "easy"
    },
    {
        "title": "スパイスから作るインドカレー",
        "description": "10種類以上のスパイスを調合して作る本格的なインドカレーです。独特の風味と深い味わいが楽しめます。",
        "ingredients": ["鶏肉", "玉ねぎ", "トマト", "ヨーグルト", "ガラムマサラ", "クミン", "ターメリック", "コリアンダー"],
        "difficulty": "hard"
    },
    {
        "title": "ココナッツミルクのタイ風グリーンカレー",
        "description": "ココナッツミルクとタイハーブを使った爽やかな辛さのグリーンカレーです。",
        "ingredients": ["鶏肉", "ナス", "ピーマン", "ココナッツミルク", "グリーンカレーペースト", "ナンプラー"],
        "difficulty": "medium"
    },
    {
        "title": "野菜たっぷりキーマカレー",
        "description": "挽き肉と細かく刻んだ野菜をたっぷり使ったヘルシーなキーマカレーです。",
        "ingredients": ["合挽き肉", "玉ねぎ", "にんじん", "ズッキーニ", "トマト", "カレー粉"],
        "difficulty": "easy"
    },
    {
        "title": "札幌風スープカレー",
        "description": "具材の旨みがたっぷり溶け込んだスープと、スパイシーな風味が特徴の北海道札幌風スープカレーです。",
        "ingredients": ["鶏手羽元", "じゃがいも", "かぼちゃ", "ブロッコリー", "スープカレースパイス"],
        "difficulty": "medium"
    }
]

# レシピの説明文からベクトル生成
recipe_descriptions = [recipe["description"] for recipe in curry_recipes]
recipe_embeddings = []

for desc in recipe_descriptions:
    embedding = get_bert_embedding(desc, model, tokenizer)
    recipe_embeddings.append(embedding)

recipe_embeddings = np.array(recipe_embeddings)

# ユーザーの好みに基づくレシピ推薦関数
def recommend_recipes(user_preference, recipe_data, recipe_embeddings, top_n=3):
    # ユーザーの好みをベクトル化
    preference_embedding = get_bert_embedding(user_preference, model, tokenizer)
    
    # 類似度計算
    similarities = cosine_similarity([preference_embedding], recipe_embeddings)[0]
    
    # 類似度順にソート
    top_indices = np.argsort(similarities)[::-1][:top_n]
    
    # 推薦結果
    recommendations = []
    for idx in top_indices:
        recommendations.append({
            "recipe": recipe_data[idx],
            "similarity": similarities[idx]
        })
    
    return recommendations

# 推薦例
user_preference1 = "本格的なスパイスの風味を楽しみたい"
recommendations1 = recommend_recipes(user_preference1, curry_recipes, recipe_embeddings)

print(f"ユーザーの好み: {user_preference1}")
print("おすすめレシピ:")
for i, rec in enumerate(recommendations1):
    print(f"{i+1}. {rec['recipe']['title']} (類似度: {rec['similarity']:.4f})")
    print(f"   説明: {rec['recipe']['description']}")
    print(f"   材料: {', '.join(rec['recipe']['ingredients'])}")
    print(f"   難易度: {rec['recipe']['difficulty']}")
    print()

user_preference2 = "家庭的で優しい味わいのカレーが食べたい"
recommendations2 = recommend_recipes(user_preference2, curry_recipes, recipe_embeddings)

print(f"ユーザーの好み: {user_preference2}")
print("おすすめレシピ:")
for i, rec in enumerate(recommendations2):
    print(f"{i+1}. {rec['recipe']['title']} (類似度: {rec['similarity']:.4f})")
    print(f"   説明: {rec['recipe']['description']}")
    print(f"   材料: {', '.join(rec['recipe']['ingredients'])}")
    print(f"   難易度: {rec['recipe']['difficulty']}")
    print()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/877045aa-2790-4e24-b3c6-4641a0d3c5f9.png)

## まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/66f59625-5e4b-4276-bc13-7916f8a7c3da.png)

この記事では、BERTを用いた文脈化埋め込みの生成方法と、その応用例について解説しました。カレーに関するテキストを例に、文書間の類似度計算や文書検索、さらにはレシピ推薦システムなど、実用的なアプリケーションへの適用方法を紹介しました。

BERTによる文脈化埋め込みの主な利点は以下の通りです：

1. **文脈の考慮**: 同じ単語でも文脈によって意味が変わる場合に適切に対応
2. **高い表現力**: 文書の意味的な類似性を高精度に捉えることが可能
3. **転移学習**: 大量のデータで事前学習されているため、少ないデータでも高性能

今回示した例はGoogle Colabで実行できる簡易なものですが、これをベースに様々な自然言語処理タスクに応用できます。例えば、レシピデータベースの検索機能や、カスタマーサポートのFAQ検索、文書分類など、多くの実用的なシステムに組み込むことが可能です。

## 参考資料

- [HuggingFace Transformersドキュメント](https://huggingface.co/docs/transformers/index)
- [東北大学 日本語BERTモデル](https://huggingface.co/tohoku-nlp/bert-base-japanese-v3)
- [BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding](https://arxiv.org/abs/1810.04805)