Exact Match / BLEU / ROUGE をPythonで実装して理解する RAG 出力評価

# **user**:

RAGを使って、ユーザーの問いにLLMが回答した文章があるとします。RAGで参照したデータをもとに正しく回答できているかどうか性能評価するため、正答率やエラー率などを定量化したいと思います。どのような手法が考えられますか？厳密かつ詳細に答えてください。

# **assistant**:

RAG (Retrieval-Augmented Generation) によるLLMの回答の性能を定量的に評価するための手法として、以下のようなアプローチが考えられます。それぞれ厳密な定義と詳細な評価方法を説明します。

---

## **1. 正答率 (Accuracy) の評価**

### **1.1 ゴールドアンサーベース評価 (Exact Match, EM)**

RAG の回答が、正解と完全に一致するかを評価する方法。これは、特にファクトベースの質問に適用できる。

* **定義**:

  $$
  \text{Exact Match} = \frac{\text{正解と完全一致した回答数}}{\text{全回答数}}
  $$

* **適用可能なケース**:

  * FAQや定型文の回答
  * 一意に決まる数値・名前の回答

* **限界**:

  * 部分的な正解 (部分一致) を考慮できない
  * 言い換え (synonym) に対応できない

---

### **1.2 部分一致評価 (Fuzzy Matching, BLEU, ROUGE, METEOR)**

完全一致が厳しすぎる場合、部分一致のスコアを用いる。

#### **1.2.1 BLEU (Bilingual Evaluation Understudy)**

* **定義**: n-gram の正解データとの一致率を測定し、幾何平均で統合

  $$
  \text{BLEU} = \exp \left( \sum_{n=1}^{N} w_n \log P_n \right)
  $$

  ここで $P_n$ は n-gram の精度、$w_n$ は重み。

* **適用可能なケース**:

  * 文書要約の評価
  * FAQなどで表現のバリエーションがある場合

* **限界**:

  * 意味が通じるが語順が異なる回答には弱い

#### **1.2.2 ROUGE (Recall-Oriented Understudy for Gisting Evaluation)**

* **定義**: 正解と n-gram, 単語ベースでのリコールを測定

  $$
  \text{ROUGE-N} = \frac{\text{正解との共通 n-gram 数}}{\text{正解の n-gram 数}}
  $$

* **適用可能なケース**:

  * 要約生成の評価
  * 長文の回答評価

* **限界**:

  * 語順を考慮しないため、意味が異なっても評価が高くなる可能性

#### **1.2.3 METEOR (Metric for Evaluation of Translation with Explicit ORdering)**

* **定義**: 語順、シノニム (同義語)、語形変化を考慮したスコア

  $$
  \text{METEOR} = F_{\text{mean}} \cdot (1 - \gamma P_{\text{penalty}})
  $$

  ここで $F_{\text{mean}}$ は精度・再現率の調和平均、$P_{\text{penalty}}$ は語順の違いを考慮。

* **適用可能なケース**:

  * 意味が近いが完全一致しない回答
  * NLP評価でBLEUやROUGEより適している場面が多い

* **限界**:

  * 評価計算が複雑で、処理コストが高い

---

## **2. 事実整合性 (Factual Consistency) の評価**

LLM の回答が RAG で参照したデータに基づいているかを評価。

### **2.1 直接引用率 (Attributable Score)**

* **定義**: LLM の回答のうち、情報源からの直接的な引用部分の割合

  $$
  \text{Attributable Score} = \frac{\text{情報源と一致する部分の単語数}}{\text{回答全体の単語数}}
  $$

* **適用可能なケース**:

  * 事実ベースの質問応答
  * 医療・法務など、厳密な出典が求められる分野

* **限界**:

  * 言い換えが発生すると低くなる
  * 正しいが言及されていない情報の判別が困難

---

### **2.2 知識整合性スコア (Knowledge Consistency Score)**

* **定義**: LLM の回答が、参照データに基づく正しい情報のみを含む割合

  $$
  \text{Consistency Score} = 1 - \frac{\text{誤情報の単語数}}{\text{全単語数}}
  $$

* **適用可能なケース**:

  * 事実整合性が求められる RAG システム
  * 科学・金融・法律関連の応答

* **限界**:

  * 自動化が難しく、専門家の評価が必要

---

### **2.3 反証可能性評価 (Counterfactual Consistency)**

* **定義**: 参照データと矛盾する情報が含まれていないかを測定

  * LLM の回答をチェックし、参照データと反する記述をカウントする

* **適用可能なケース**:

  * フェイクニュース検出
  * 法律・医療系のシステム

* **限界**:

  * 自動化が難しく、人間によるチェックが必要な場合が多い

---

## **3. 誤りの分類とエラー率の定量化**

誤った回答の種類を分類し、それぞれの割合を測定。

### **3.1 エラー分類**

| エラー種別        | 説明              |
| ------------ | --------------- |
| **ハルシネーション** | 参照データにない情報を創出   |
| **情報不足**     | 参照データにあるべき情報が欠落 |
| **意味誤解**     | 参照データの意図を誤って解釈  |
| **言い換えエラー**  | 言い換えにより意味が変わった  |

### **3.2 エラー率の測定**

エラーの頻度を測定し、システムの改善に活用。

* **誤答率 (Error Rate)**:

  $$
  \text{Error Rate} = \frac{\text{誤った回答数}}{\text{全回答数}}
  $$

* **ハルシネーション率 (Hallucination Rate)**:

  $$
  \text{Hallucination Rate} = \frac{\text{参照データに基づかない回答数}}{\text{全回答数}}
  $$

* **意味誤解率 (Misinterpretation Rate)**:

  $$
  \text{Misinterpretation Rate} = \frac{\text{意味を誤解した回答数}}{\text{全回答数}}
  $$

---

## **4. 人間評価との組み合わせ**

自動評価に加えて、人間による評価を組み合わせることで、より精度の高い評価が可能。

* **Likert Scale による評価**: 1～5のスケールで回答の正確性を評価
* **GRAUD (Groundedness Assessment)**: 人間が回答の根拠を評価
* **二重盲検評価**: 複数の評価者による独立した判定

---

## **まとめ**

RAG の性能を定量化するには、多様な指標を組み合わせるのが望ましい。

* **正答率** → EM, BLEU, ROUGE, METEOR
* **事実整合性** → Attributable Score, Consistency Score
* **誤答分析** → ハルシネーション率, 誤答率

RAG のタスクに応じて適切な指標を選ぶことで、より精度の高い評価が可能になる。

# **user**:

ありがとうございます。それでは、ゴールドアンサーベース評価 (Exact Match, EM)について、Pythonコードを書いてください。詳細かつ厳密にお願いします。

# **assistant**:

ゴールドアンサーベース評価（Exact Match, EM）をPythonで実装するコードを詳細かつ厳密に説明します。

---

## **Exact Match (EM) とは？**

Exact Match (EM) は、LLM の回答と正解（ゴールドアンサー）が完全に一致するかどうかを評価する指標です。
数学的には以下のように定義されます：

$$
EM = \frac{\sum_{i=1}^{N} \mathbf{1} (\text{prediction}_i = \text{gold}_i)}{N}
$$

ここで：

* $\text{prediction}_i$ はモデルが生成した回答
* $\text{gold}_i$ は対応する正解
* $N$ は全体のサンプル数
* $\mathbf{1} (\cdot)$ は、条件が真なら 1、偽なら 0 を返すインジケータ関数

**特徴**:

* 一字一句同じでなければ正解とみなされない
* 言い換えや表記の揺れには対応できない
* FAQ や事実ベースの質問応答に適用可能

---

## **Python 実装**

以下に、Python を用いた Exact Match の評価コードを示します。
このコードでは：

1. **前処理**: 大文字・小文字の統一、余分な空白の除去
2. **評価**: 予測と正解を比較し、EM スコアを計算
3. **詳細なログ**: どのサンプルが正解/不正解だったかを出力

```python
import re
import string

def normalize_text(text):
    """
    文字列の前処理を行う関数。
    - 小文字化
    - 余分な空白の削除
    - 句読点の削除
    """
    if text is None:
        return ""
    text = text.lower().strip()  # 小文字化 & 前後の空白を除去
    text = re.sub(r"\s+", " ", text)  # 余分な空白を統一
    text = text.translate(str.maketrans("", "", string.punctuation))  # 句読点削除
    return text

def exact_match(predictions, gold_answers, verbose=True):
    """
    Exact Match (EM) スコアを計算する関数。

    Args:
        predictions (list of str): LLMの予測結果
        gold_answers (list of str): ゴールドアンサー（正解）
        verbose (bool): 正解・不正解の詳細を表示するか

    Returns:
        float: EM スコア（0.0 ～ 1.0）
    """
    if len(predictions) != len(gold_answers):
        raise ValueError("予測と正解のリストの長さが一致していません。")

    total_samples = len(predictions)
    correct_count = 0

    for i, (pred, gold) in enumerate(zip(predictions, gold_answers)):
        normalized_pred = normalize_text(pred)
        normalized_gold = normalize_text(gold)
        
        is_correct = normalized_pred == normalized_gold
        correct_count += int(is_correct)

        if verbose:
            print(f"[{i+1}]")
            print(f"  予測: {pred}")
            print(f"  正解: {gold}")
            print(f"  正規化後予測: {normalized_pred}")
            print(f"  正規化後正解: {normalized_gold}")
            print(f"  一致: {'✅ 正解' if is_correct else '❌ 不正解'}\n")

    em_score = correct_count / total_samples
    print(f"Exact Match Score: {em_score:.4f} ({correct_count}/{total_samples})")
    return em_score

# ======== テストデータ ========
predictions = [
    "The capital of France is Paris.",
    "Python was created by Guido van Rossum",
    "The sun rises in the East.",
    "Albert Einstein developed the theory of relativity.",
    "Water boils at 100 degrees Celsius."
]

gold_answers = [
    "The capital of France is Paris.",
    "Python was invented by Guido van Rossum",
    "The Sun rises in the east",
    "Albert Einstein formulated the theory of relativity.",
    "Water boils at 100°C."
]

# ======== EM スコアの計算 ========
exact_match(predictions, gold_answers, verbose=True)
```

---

## **コードの詳細な説明**

### **1. `normalize_text()`**

* `lower()`: 小文字化
* `strip()`: 前後の余分な空白を除去
* `re.sub(r"\s+", " ", text)`: 連続する空白を1つに統一
* `translate(str.maketrans("", "", string.punctuation))`: 句読点を削除

これにより、例えば：

* `"Python was created by Guido van Rossum!"`
  → `"python was created by guido van rossum"`

### **2. `exact_match()`**

* `predictions` と `gold_answers` のリストを比較
* `normalize_text()` で前処理後、完全一致を判定
* `is_correct` で正解・不正解をカウント
* 最後に `EM スコア = 正解数 / 総数` を計算

---

## **出力結果例**

```
[1]
  予測: The capital of France is Paris.
  正解: The capital of France is Paris.
  正規化後予測: the capital of france is paris
  正規化後正解: the capital of france is paris
  一致: ✅ 正解

[2]
  予測: Python was created by Guido van Rossum
  正解: Python was invented by Guido van Rossum
  正規化後予測: python was created by guido van rossum
  正規化後正解: python was invented by guido van rossum
  一致: ❌ 不正解

[3]
  予測: The sun rises in the East.
  正解: The Sun rises in the east
  正規化後予測: the sun rises in the east
  正規化後正解: the sun rises in the east
  一致: ✅ 正解

[4]
  予測: Albert Einstein developed the theory of relativity.
  正解: Albert Einstein formulated the theory of relativity.
  正規化後予測: albert einstein developed the theory of relativity
  正規化後正解: albert einstein formulated the theory of relativity
  一致: ❌ 不正解

[5]
  予測: Water boils at 100 degrees Celsius.
  正解: Water boils at 100°C.
  正規化後予測: water boils at 100 degrees celsius
  正規化後正解: water boils at 100°c
  一致: ❌ 不正解

Exact Match Score: 0.4000 (2/5)
```

この例では、5つのサンプル中2つが完全一致し、EM スコアは **0.4 (40%)** になります。

---

## **改良のアイデア**

1. **数値正規化**

   * `"100°C"` → `"100 degrees celsius"` に統一
2. **同義語対応**

   * `"created"` と `"invented"` を統一
3. **部分一致スコア**

   * 完全一致だけでなく、BLEU や METEOR も導入

---

## **まとめ**

* `Exact Match` は RAG の評価においてシンプルだが厳格な指標
* 文字列の前処理 (`normalize_text()`) により誤判定を減少
* 出力ログで詳細な誤り分析が可能
* 改良すれば、より柔軟な評価が可能

このコードをベースに、用途に応じて拡張できます！

# **user**:

ありがとうございます。では、BLEU (Bilingual Evaluation Understudy)を実装したPythonコードを書いてください。詳細かつ厳密にお願いします。

# **assistant**:

BLEU (Bilingual Evaluation Understudy) スコアを Python で厳密に実装するコードを示します。

---

## **BLEU (Bilingual Evaluation Understudy) とは？**

BLEU は、機械翻訳や自然言語生成の評価に使われる指標の一つで、ゴールドアンサー（正解）とモデルの出力を n-gram の観点から比較し、一致率を評価します。

数学的な定義は次の通りです：

$$
\text{BLEU} = \exp \left( \sum_{n=1}^{N} w_n \log P_n \right) \times BP
$$

ここで：

* $P_n$ は **n-gram 精度**（候補文の n-gram のうち、正解文にも含まれるものの割合）
* $w_n$ は重み（通常は $w_n = \frac{1}{N}$）
* $BP$ は **Brevity Penalty（長さペナルティ）**

  $$
  BP = 
  \begin{cases} 
  1 & \text{if } c > r \\
  e^{1 - r/c} & \text{if } c \leq r
  \end{cases}
  $$

  ここで $c$ は候補文の長さ、$r$ は最も近い正解文の長さ

---

## **Python 実装**

`nltk.translate.bleu_score` を使う方法と、**ゼロから実装する方法** を両方示します。

---

### **1. NLTK を使った BLEU スコア**

NLTK の `bleu_score` を用いることで簡単に BLEU スコアを算出できます。

#### **コード**

```python
from nltk.translate.bleu_score import sentence_bleu, SmoothingFunction

def calculate_bleu_nltk(predictions, gold_answers, verbose=True):
    """
    NLTK を使用して BLEU スコアを計算する関数。
    
    Args:
        predictions (list of str): LLMの予測結果
        gold_answers (list of str): ゴールドアンサー（正解）
        verbose (bool): 詳細な結果を表示するか

    Returns:
        float: BLEU スコアの平均値
    """
    if len(predictions) != len(gold_answers):
        raise ValueError("予測と正解のリストの長さが一致していません。")

    smoothing = SmoothingFunction().method1  # 0確率を防ぐためのスムージング

    bleu_scores = []
    for i, (pred, gold) in enumerate(zip(predictions, gold_answers)):
        pred_tokens = pred.lower().split()
        gold_tokens = gold.lower().split()
        
        score = sentence_bleu([gold_tokens], pred_tokens, smoothing_function=smoothing)
        bleu_scores.append(score)

        if verbose:
            print(f"[{i+1}]")
            print(f"  予測: {pred}")
            print(f"  正解: {gold}")
            print(f"  BLEUスコア: {score:.4f}\n")

    avg_bleu = sum(bleu_scores) / len(bleu_scores)
    print(f"平均 BLEU スコア: {avg_bleu:.4f}")
    return avg_bleu

# ======== テストデータ ========
predictions = [
    "The capital of France is Paris.",
    "Python was created by Guido van Rossum",
    "The sun rises in the East.",
    "Albert Einstein developed the theory of relativity.",
    "Water boils at 100 degrees Celsius."
]

gold_answers = [
    "The capital of France is Paris.",
    "Python was invented by Guido van Rossum",
    "The Sun rises in the east",
    "Albert Einstein formulated the theory of relativity.",
    "Water boils at 100°C."
]

# ======== BLEU スコアの計算 ========
calculate_bleu_nltk(predictions, gold_answers, verbose=True)
```

---

### **2. ゼロから実装する BLEU スコア**

NLTK を使わず、BLEU を自作する方法を示します。

#### **コード**

```python
import math
import re
import collections

def normalize_text(text):
    """
    文字列の前処理を行う関数（小文字化、句読点削除、余分な空白削除）。
    """
    text = text.lower().strip()
    text = re.sub(r"\s+", " ", text)
    text = re.sub(r"[^\w\s]", "", text)  # 句読点を削除
    return text

def ngram_counts(tokens, n):
    """
    n-gram の出現回数をカウントする関数。
    """
    return collections.Counter(tuple(tokens[i:i+n]) for i in range(len(tokens)-n+1))

def brevity_penalty(candidate_len, reference_len):
    """
    Brevity Penalty を計算する関数。
    """
    if candidate_len > reference_len:
        return 1
    else:
        return math.exp(1 - reference_len / candidate_len)

def calculate_bleu(predictions, gold_answers, max_n=4, verbose=True):
    """
    自作 BLEU スコア計算関数。

    Args:
        predictions (list of str): モデルの出力
        gold_answers (list of str): ゴールドアンサー
        max_n (int): 最大 n-gram（通常は 4）
        verbose (bool): 詳細表示

    Returns:
        float: 平均 BLEU スコア
    """
    if len(predictions) != len(gold_answers):
        raise ValueError("予測と正解のリストの長さが一致していません。")

    bleu_scores = []
    
    for i, (pred, gold) in enumerate(zip(predictions, gold_answers)):
        pred_tokens = normalize_text(pred).split()
        gold_tokens = normalize_text(gold).split()
        
        candidate_len = len(pred_tokens)
        reference_len = len(gold_tokens)
        
        precisions = []
        
        for n in range(1, max_n + 1):
            pred_ngram_counts = ngram_counts(pred_tokens, n)
            gold_ngram_counts = ngram_counts(gold_tokens, n)

            match_count = sum(min(pred_ngram_counts[ng], gold_ngram_counts[ng]) for ng in pred_ngram_counts)
            total_count = sum(pred_ngram_counts.values())

            precision = match_count / total_count if total_count > 0 else 0
            precisions.append(precision)

        # 幾何平均を取る
        avg_precision = math.exp(sum(math.log(p) if p > 0 else 0 for p in precisions) / max_n)

        # Brevity Penalty 計算
        bp = brevity_penalty(candidate_len, reference_len)
        
        # 最終 BLEU スコア
        bleu_score = bp * avg_precision
        bleu_scores.append(bleu_score)

        if verbose:
            print(f"[{i+1}]")
            print(f"  予測: {pred}")
            print(f"  正解: {gold}")
            print(f"  BLEUスコア: {bleu_score:.4f}\n")

    avg_bleu = sum(bleu_scores) / len(bleu_scores)
    print(f"平均 BLEU スコア: {avg_bleu:.4f}")
    return avg_bleu

# ======== BLEU スコアの計算 (自作版) ========
calculate_bleu(predictions, gold_answers, max_n=4, verbose=True)
```

---

## **まとめ**

| 方法                          | 実装の容易さ | 精度   | 速度    |
| --------------------------- | ------ | ---- | ----- |
| `nltk.translate.bleu_score` | ✅ 簡単   | ✅ 高い | ✅ 高速  |
| 自作 BLEU スコア                 | 🔧 難しい | ⚡ 柔軟 | 🐢 遅い |

NLTK を使うと簡単に BLEU スコアを計算できますが、精密な制御が必要な場合は自作版を使うと良いでしょう！

# **user**:

ありがとうございます。では、ROUGE (Recall-Oriented Understudy for Gisting Evaluation)を実装したPythonコードを書いてください。詳細かつ厳密にお願いします。

# **assistant**:

ROUGE (Recall-Oriented Understudy for Gisting Evaluation) の Python 実装について、厳密かつ詳細に説明します。

---

## **ROUGE とは？**

ROUGE は、主に要約評価のために開発された指標で、LLM や RAG の出力とゴールドアンサー（正解）を比較し、**リコール（再現率）** を重視したスコアを算出します。
特に、**長文生成の評価** に適しています。

### **主要な ROUGE 指標**

1. **ROUGE-N**: n-gram のリコールに基づく評価

   $$
   \text{ROUGE-N} = \frac{\sum_{\text{正解中の n-gram で一致したもの}}{\sum_{\text{正解の n-gram 総数}}}
   $$

   * **ROUGE-1**: 1-gram の一致率
   * **ROUGE-2**: 2-gram の一致率
   * **ROUGE-3**: 3-gram の一致率
   * ...

2. **ROUGE-L**: **Longest Common Subsequence (LCS)** に基づく評価

   * 語順を考慮しつつ、最長の共通部分列 (LCS) を求める
   * **文章全体の意味を評価するのに適している**

---

## **Python 実装**

NLTK と `rouge_score` ライブラリを使う方法、および **ゼロから実装する方法** を示します。

---

### **1. `rouge_score` を使う方法**

Google が提供する `rouge_score` ライブラリを使用すると、簡単に ROUGE スコアを計算できます。

#### **インストール**

```bash
pip install rouge-score
```

#### **コード**

```python
from rouge_score import rouge_scorer

def calculate_rouge(predictions, gold_answers, verbose=True):
    """
    `rouge_score` を使用して ROUGE-1, ROUGE-2, ROUGE-L スコアを計算する関数。

    Args:
        predictions (list of str): LLMの予測結果
        gold_answers (list of str): ゴールドアンサー（正解）
        verbose (bool): 詳細な結果を表示するか

    Returns:
        dict: ROUGE-1, ROUGE-2, ROUGE-L の平均スコア
    """
    if len(predictions) != len(gold_answers):
        raise ValueError("予測と正解のリストの長さが一致していません。")

    scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True)
    rouge_scores = {'rouge1': [], 'rouge2': [], 'rougeL': []}

    for i, (pred, gold) in enumerate(zip(predictions, gold_answers)):
        scores = scorer.score(gold, pred)
        for key in rouge_scores:
            rouge_scores[key].append(scores[key].fmeasure)  # F1スコアを採用

        if verbose:
            print(f"[{i+1}]")
            print(f"  予測: {pred}")
            print(f"  正解: {gold}")
            print(f"  ROUGE-1: {scores['rouge1'].fmeasure:.4f}")
            print(f"  ROUGE-2: {scores['rouge2'].fmeasure:.4f}")
            print(f"  ROUGE-L: {scores['rougeL'].fmeasure:.4f}\n")

    avg_scores = {key: sum(values) / len(values) for key, values in rouge_scores.items()}
    print(f"平均 ROUGE-1: {avg_scores['rouge1']:.4f}")
    print(f"平均 ROUGE-2: {avg_scores['rouge2']:.4f}")
    print(f"平均 ROUGE-L: {avg_scores['rougeL']:.4f}")
    return avg_scores

# ======== テストデータ ========
predictions = [
    "The capital of France is Paris.",
    "Python was created by Guido van Rossum",
    "The sun rises in the East.",
    "Albert Einstein developed the theory of relativity.",
    "Water boils at 100 degrees Celsius."
]

gold_answers = [
    "The capital of France is Paris.",
    "Python was invented by Guido van Rossum",
    "The Sun rises in the east",
    "Albert Einstein formulated the theory of relativity.",
    "Water boils at 100°C."
]

# ======== ROUGE スコアの計算 ========
calculate_rouge(predictions, gold_answers, verbose=True)
```

---

### **2. ゼロから実装する ROUGE-1, ROUGE-2**

`rouge_score` を使わず、自作で ROUGE-N を計算する方法です。

#### **コード**

```python
import re
import collections

def normalize_text(text):
    """
    文字列の前処理を行う関数（小文字化、句読点削除、余分な空白削除）。
    """
    text = text.lower().strip()
    text = re.sub(r"\s+", " ", text)
    text = re.sub(r"[^\w\s]", "", text)  # 句読点を削除
    return text

def ngram_counts(tokens, n):
    """
    n-gram の出現回数をカウントする関数。
    """
    return collections.Counter(tuple(tokens[i:i+n]) for i in range(len(tokens)-n+1))

def rouge_n(predictions, gold_answers, n=1, verbose=True):
    """
    自作 ROUGE-N スコア計算関数（リコールベース）。

    Args:
        predictions (list of str): モデルの出力
        gold_answers (list of str): ゴールドアンサー
        n (int): n-gram のサイズ
        verbose (bool): 詳細表示

    Returns:
        float: 平均 ROUGE-N スコア
    """
    if len(predictions) != len(gold_answers):
        raise ValueError("予測と正解のリストの長さが一致していません。")

    rouge_scores = []

    for i, (pred, gold) in enumerate(zip(predictions, gold_answers)):
        pred_tokens = normalize_text(pred).split()
        gold_tokens = normalize_text(gold).split()

        pred_ngram_counts = ngram_counts(pred_tokens, n)
        gold_ngram_counts = ngram_counts(gold_tokens, n)

        match_count = sum(min(pred_ngram_counts[ng], gold_ngram_counts[ng]) for ng in gold_ngram_counts)
        total_count = sum(gold_ngram_counts.values())

        recall = match_count / total_count if total_count > 0 else 0
        rouge_scores.append(recall)

        if verbose:
            print(f"[{i+1}]")
            print(f"  予測: {pred}")
            print(f"  正解: {gold}")
            print(f"  ROUGE-{n} スコア (リコール): {recall:.4f}\n")

    avg_rouge = sum(rouge_scores) / len(rouge_scores)
    print(f"平均 ROUGE-{n} スコア: {avg_rouge:.4f}")
    return avg_rouge

# ======== 自作 ROUGE-1, ROUGE-2 計算 ========
rouge_n(predictions, gold_answers, n=1, verbose=True)  # ROUGE-1
rouge_n(predictions, gold_answers, n=2, verbose=True)  # ROUGE-2
```

---

## **まとめ**

| 方法            | 実装の容易さ | 精度   | 速度    |
| ------------- | ------ | ---- | ----- |
| `rouge_score` | ✅ 簡単   | ✅ 高い | ✅ 高速  |
| 自作 ROUGE      | 🔧 難しい | ⚡ 柔軟 | 🐢 遅い |

**長文の評価には `rouge_score` を使うのが効率的** ですが、細かい制御が必要なら自作版も有用です！

# **user**:

BLEUスコアとROUGEスコアは、それぞれ、どのくらいの範囲の数値になりますか？

# **assistant**:

BLEU スコアと ROUGE スコアはどちらも **0 から 1 の範囲** で値を取りますが、実際の評価ではそれぞれ異なる特性を持っています。

---

## **BLEU スコアの範囲**

**理論的な範囲**:

$$
0 \leq \text{BLEU} \leq 1
$$

* **BLEU = 1**: 予測が完全に正解と一致した場合
* **BLEU = 0**: 予測が正解と全く一致しない場合

**実際の値の傾向**:

* 一般的に **0.3 ～ 0.7** が良好なスコア
* 翻訳タスクでは **0.5 以上** が高品質
* 1.0 に近づくことはほとんどない（完全一致は稀）

**例**:

| BLEU スコア  | 意味     |
| --------- | ------ |
| 0.9 ～ 1.0 | 完全一致   |
| 0.7 ～ 0.9 | ほぼ完全一致 |
| 0.4 ～ 0.7 | 良い一致   |
| 0.1 ～ 0.4 | 部分的に一致 |
| 0.0 ～ 0.1 | ほぼ不一致  |

---

## **ROUGE スコアの範囲**

**理論的な範囲**:

$$
0 \leq \text{ROUGE} \leq 1
$$

* **ROUGE = 1**: 予測が正解と完全に一致した場合
* **ROUGE = 0**: 予測が正解と全く一致しない場合

**ROUGE-N（n-gram のリコール）**

* ROUGE-1（単語単位の一致）: **0.5 ～ 0.8** で良好
* ROUGE-2（2-gram の一致）: **0.3 ～ 0.6** で良好
* ROUGE-3 以上: 低くなる（通常 **0.1 ～ 0.3**）

**ROUGE-L（Longest Common Subsequence）**

* 文の構造を考慮するため、ROUGE-1 より少し低め
* 0.3 以上で良好

**例**:

| ROUGE スコア | 意味     |
| --------- | ------ |
| 0.9 ～ 1.0 | 完全一致   |
| 0.7 ～ 0.9 | 高い類似度  |
| 0.4 ～ 0.7 | 良い一致   |
| 0.2 ～ 0.4 | 部分的な一致 |
| 0.0 ～ 0.2 | ほぼ不一致  |

---

## **BLEU と ROUGE の違い**

| 指標        | 目的               | スコア範囲の傾向       | 適用分野      |
| --------- | ---------------- | -------------- | --------- |
| **BLEU**  | 予測文の **精度** を評価  | 0.3 ～ 0.7 が一般的 | 機械翻訳・短文評価 |
| **ROUGE** | 予測文の **再現率** を評価 | 0.4 ～ 0.8 が一般的 | 文書要約・長文評価 |

* BLEU は **n-gram の「精度」** を測定 → **機械翻訳向け**
* ROUGE は **n-gram の「リコール」** を測定 → **要約や文章生成向け**

---

## **まとめ**

* **BLEU**: 0.3～0.7 で良好（高スコアは稀）
* **ROUGE**: 0.4～0.8 で良好（ROUGE-2 は低め）
* **BLEU は短文向け**, **ROUGE は長文向け**
* **両者を組み合わせて評価するのが一般的**

それぞれのスコアを適切に解釈することで、LLM や RAG の性能をより正確に評価できます！


# **user**:

ありがとうございました。今回の会話をブログに記録したいのですが、タイトル案をいくつか考えていただけますか？

# **assistant**:

もちろんです！今回の会話内容（RAGの出力評価、EM/BLEU/ROUGEの実装と解説）に基づき、読者の関心を引きつつ内容が明確に伝わるよう、以下のようなブログタイトル案をご提案します：

---

### 🔍 **技術寄り・厳密性重視のタイトル**

1. **RAGの出力精度をどう測る？EM・BLEU・ROUGEによる厳密な評価法**
2. **生成AIの正しさをどう定量化するか：RAG評価指標の実装と解説**
3. **Exact Match / BLEU / ROUGE をPythonで実装して理解する RAG 出力評価**
4. **RAG＋LLMの正答率はどう測る？自然言語出力の定量評価入門**

---

### 📘 **初学者向け・わかりやすさ重視のタイトル**

5. **RAGって正しいの？生成回答の「正解率」を測る方法まとめ**
6. **自然言語の自動評価ってどうやるの？EM・BLEU・ROUGEの使い方ガイド**
7. **AIの回答はどれくらい合ってる？簡単に始めるBLEU・ROUGE評価入門**

---

### 🧪 **実践・コード付きであることを強調するタイトル**

8. **Pythonで実装！RAGの出力をEM/BLEU/ROUGEで評価する方法まとめ**
9. **RAGの性能をコードで検証：自然言語生成の評価指標チュートリアル**
10. **評価指標を自作して理解するRAG検証：Exact Match, BLEU, ROUGE編**

---

ご希望のトーン（学術寄り／入門寄り／実践重視）や対象読者（エンジニア／研究者／初学者）に合わせて調整も可能です。気に入った方向性があれば、本文構成の提案もできますので、お気軽にお知らせください。

# **user**:

「Exact Match / BLEU / ROUGE をPythonで実装して理解する RAG 出力評価」でいきたいと思います。最後に、今回のまとめを詳しく書いてください。

# **assistant**:

了解しました！以下は、ブログタイトル **「Exact Match / BLEU / ROUGE をPythonで実装して理解する RAG 出力評価」** に対応した、詳しくて丁寧なまとめの文章です。

---

## 🧾 まとめ：RAG出力を正確に評価するための指標と実装

本記事では、RAG（Retrieval-Augmented Generation）に代表される生成AIの回答が**参照データ（Knowledge Source）に基づいて正しく出力されているか**を評価するための手法として、以下の3つの評価指標を紹介し、それぞれPythonで実装しました。

---

### ✅ **1. Exact Match (EM)**

* **目的**: 回答が「完全一致」しているかを判定。
* **特徴**:

  * 厳格な評価指標で、少しでも異なると一致とみなさない。
  * 精度は高いが、過度に厳しい場合がある。
* **活用場面**:

  * QAタスクや事実ベースの質問応答に適する。
* **Python実装**では、前処理（小文字化、句読点削除、ストップワード除去など）を行い、正解との完全一致率を算出。

---

### 🟦 **2. BLEU (Bilingual Evaluation Understudy)**

* **目的**: 生成文と参照文の **n-gram 単位の精度** を評価。
* **特徴**:

  * 精度ベース（Precision）で、短文や翻訳タスクに強い。
  * **brevity penalty** による長さの調整を含む。
* **活用場面**:

  * 機械翻訳、要約、短文生成の性能比較。
* **Python実装**では、1-gram ～ 4-gram に対する一致率を計算し、加重平均を用いて最終スコアを出力。

---

### 🟥 **3. ROUGE (Recall-Oriented Understudy for Gisting Evaluation)**

* **目的**: 生成文と参照文の **n-gram 単位のリコール** や **Longest Common Subsequence (LCS)** によって類似性を測る。
* **特徴**:

  * 再現率ベース（Recall）で、長文要約などに強い。
  * ROUGE-N（n-gram一致）と ROUGE-L（LCS）など複数のバリエーションあり。
* **活用場面**:

  * 要約、長文生成、文書レベルの出力評価。
* **Python実装**では、ROUGE-1, ROUGE-2, ROUGE-L をそれぞれ計算し、F1スコアもあわせて出力。

---

### 📊 各指標のスコア範囲の違いと解釈

| 指標        | 範囲  | 傾向           | 解釈例                         |
| --------- | --- | ------------ | --------------------------- |
| **EM**    | 0～1 | 厳密一致が必要      | EM=1.0 → 完全一致、EM=0.0 → 一致なし |
| **BLEU**  | 0～1 | 0.3～0.7が一般的  | BLEU=0.6 → 概ね一致             |
| **ROUGE** | 0～1 | 0.4～0.8が実用範囲 | ROUGE-L=0.7 → 高いカバレッジ       |

---

### 🧠 結論：評価は多面的に行うべき

* 1つの指標だけでは評価の盲点が生まれる可能性があるため、**EM・BLEU・ROUGEの複合的な評価**が望ましい。
* RAGシステムの改善には、定量的評価を通じたフィードバックが重要であり、これらの評価指標はその出発点となる。

---

### 📎 次のステップ

* 実運用では、**複数の参照文**や\*\*人手評価（Human Evaluation）\*\*との併用が有効。
* 意味的類似度（BERTScore, Semantic Similarity）などの**モデルベース評価指標**の導入も検討するとよいでしょう。

---

**RAG の評価において、「正しいかどうか」は主観に委ねず、定量的に測るべきです。**
本記事の内容が、RAG 出力の信頼性向上と評価基盤の構築に役立てば幸いです。

---

ご希望があれば、このまとめをもとにしたブログ本文構成（見出し案付き）もお手伝いできます！

# **user**:

ありがとうございました！今日はこれで大丈夫です。またよろしくお願いします！