画像検索の評価指標についてまとめてみた（コード付き）

画像検索の評価指標は、検索結果の正確さ、ランキングの質、クラスタリングの効果などを測定します。この記事では、画像検索評価において使用される評価指標について調べたことをまとめて、コードも実装してみました。

- [Precision@k](https://qiita.com/insilicomab/items/472578d9a85abc391dac#precisionk)
- [Average Precision@k (AP@k)](https://qiita.com/insilicomab/items/472578d9a85abc391dac#average-precisionk-apk)
- [Mean Average Precision@k (mAP@k)](https://qiita.com/insilicomab/items/472578d9a85abc391dac#mean-average-precisionk-mapk)
- [Mean Reciprocal Rank@k (MRR@k)](https://qiita.com/insilicomab/items/472578d9a85abc391dac#mean-reciprocal-rankk-mrrk)
- [Adjusted Mutual Information (AMI)](https://qiita.com/insilicomab/items/472578d9a85abc391dac#adjusted-mutual-information-ami)
- [Normalized Mutual Information (NMI)](https://qiita.com/insilicomab/items/472578d9a85abc391dac#normalized-mutual-information-nmi)

# Precision@k
### 概要
Precision@k は、検索結果やレコメンデーションシステムの精度を評価するために使われる指標の一つです。
この指標は、ユーザーに提示される上位 k 個の結果の中で、**どれだけの項目が正解（関連があるもの）であるか**を評価します。

### 特徴
- **k の選択**：k の値によって結果が、大きく変わる可能性があります。一般的には k を 1、3、5、10 といった小さめの値に設定することが多いです。例えば、ユーザーが最初に表示される10件の検索結果だけを重要視する場合は、Precision@10 を使って評価します
<br>

- **直感的な解釈**：上位 k 個のレコメンデーションのうち、何割が適切だったかを示します

<br>

- **局所的な評価**：Precision@k は、上位 k 件の結果のみに焦点を当てるため、全体の結果の正確さを示すものではありません。システムが大量の結果を返す場合でも、上位の結果だけを評価対象とするため、ユーザーの注目が限られている場合（例：トップ10のレコメンドのみを表示する場合）に適しています

### 計算方法

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/471633/21740101-1a28-6037-be72-f1880636f1c3.png)

ここで、Relevant items in top k は、上位 k 件の中で正解とされる（関連性のある）アイテムの数、Total items in top k は上位 k 件の結果の総数（つまり k ）を示します。
<br>

例えば、上位10個の推薦アイテムのうち、ユーザーが5個のアイテムと相互作用した場合、
Precision@10 = 5 /10 = 0.5 (= 50%)
となり、システムが50%の精度で関連アイテムをレコメンドしたことを示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/471633/4fb66a67-37c6-845e-6dae-dbcb6b12e54e.png)
引用：https://www.evidentlyai.com/ranking-metrics/precision-recall-at-k


### コード実装例

```python
import numpy as np

def precision_at_k(y_true, y_score, k, higher_is_better=True):
    # 入力をNumPy配列に変換
    y_true = np.asarray(y_true)
    y_score = np.asarray(y_score)

    # スコアでソート（higher_is_better に応じて昇順or降順）
    if higher_is_better:
        top_k_indices = np.argsort(y_score)[::-1][:k]
    else:
        top_k_indices = np.argsort(y_score)[:k]

    # 上位k個に対応する真のラベルを取得
    top_k_true = y_true[top_k_indices]

    # Precision@kを計算
    precision = np.sum(top_k_true) / k

    return precision
```

#### サンプルデータ（スコアが高いほど類似、例：確率）

```python
y_true_high = [1, 0, 1, 1, 0, 1, 0, 0]
y_scores_high = [0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2]
```
- Precision@3

```python
print(f"Precision@3: {precision_at_k(y_true_high, y_scores_high, 3):.4f}")
```

```sh:出力
Precision@3: 0.6667
```

- Precision@5

```python
print(f"Precision@5: {precision_at_k(y_true_high, y_scores_high, 5):.4f}")
```

```sh:出力
Precision@5: 0.6000
```

#### サンプルデータ（スコアが低いほど類似、例：距離）

```python
y_true_low = [1, 0, 1, 1, 0, 1, 0, 0]
y_scores_low = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]
```

- Precision@3

```python
print(f"Precision@3: {precision_at_k(y_true_low, y_scores_low, 3, higher_is_better=False):.4f}")
```

```sh:出力
Precision@3: 0.6667
```

- Precision@5

```python
print(f"Precision@5: {precision_at_k(y_true_low, y_scores_low, 5, higher_is_better=False):.4f}")
```

```sh:出力
Precision@5: 0.6000
```

# Average Precision@k (AP@k)
### 概要

検索結果やレコメンデーションシステムの精度を評価するために使われる指標の一つで Precision@k の発展形です。具体的には、上位 k で関連アイテムが出現するたびにPrecision@kを計算し、その平均を取ったものが AP@k となります。AP@k は、**各関連アイテムがランキングのどの位置にあるかを反映するため、 Precision@k よりも結果全体の質を詳細に評価**できます。

### 特徴
- **順位を考慮**：関連性のあるアイテムが上位に表示されるほど、精度に大きく寄与します。逆に、関連性のあるアイテムが下位に表示されるとスコアへの寄与は小さくなります。したがって、関連するアイテムが上位に集まっている場合、高いスコアが得られます
<br>

- **部分的な結果評価**：AP@k は、上位 k 件の結果だけを評価します。したがって、重要な上位の結果に焦点を当てることが可能です

### 計算方法
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/471633/1d27b9ed-9350-965c-3b8f-ade2e064afa3.png)

ここで m は関連アイテムの総数、P(i) は i 番目のアイテムまでの精度（Precision@i）、rel(i)は i 番目のアイテムが関連している場合は1、そうでない場合は0となります。

例えば、上位6個のアイテム（k=6）で、1番目と4番目と5番目が関連アイテムだった場合、

1\.	P(1) = 1/1 =1
2\.	P(4) = 2/4 = 0.5
3\.	P(5) = 3/5 = 0.6

AP@6 = (1+0.5+0.6) / 3 = 0.7

となります。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/471633/258255e2-a9cc-2533-b1c4-0792b909ab08.png)
引用：https://www.evidentlyai.com/ranking-metrics/mean-average-precision-map


### コード実装例

```python
import warnings
import numpy as np

# 各レコードごとのaverage precisionを計算する関数
def average_precision_at_k(y_true, y_score, k=10, higher_is_better=True):
    # リストをNumPy配列に変換
    y_true = np.asarray(y_true)
    y_score = np.asarray(y_score)

    # スコアでソート（higher_is_better に応じて昇順or降順）
    if higher_is_better:
        top_k_indices = np.argsort(y_score)[::-1][:k]
    else:
        top_k_indices = np.argsort(y_score)[:k]

    # 上位k個に対応する真のラベルを取得
    top_k_true = y_true[top_k_indices]

    sum_precision = 0.0
    num_hits = 0.0

    for i, p in enumerate(top_k_true):
        if p == 1:
            num_hits += 1
            precision = num_hits / (i + 1)
            sum_precision += precision

    if num_hits == 0:
        warnings.warn(
            "No positive class found in y_true."
        )
        return 0.0

    return sum_precision / min(np.sum(y_true), k)
```

#### サンプルデータ（スコアが高いほど類似、例：確率）

```python
y_true = [1, 0, 1, 0, 1]
y_score = [0.9, 0.75, 0.6, 0.85, 0.7]
```

- AP@5

```python
apk = average_precision_at_k(y_true, y_score, k=5, higher_is_better=True)
print(f"AP@k: {apk}")
```

```sh:出力
AP@k: 0.7000000000000001
```

#### サンプルデータ（スコアが低いほど類似、例：距離）

```python
y_true = [0, 1, 1, 0, 0]
y_score = [0.9, 0.75, 0.6, 0.85, 0.7]
```

- AP@5

```python
apk = average_precision_at_k(y_true, y_score, k=5, higher_is_better=False)
print(f"AP@k: {apk}")
```

```sh:出力
AP@k: 0.8666666666666667
```

#### [scikit-learn](https://scikit-learn.org/1.5/modules/generated/sklearn.metrics.average_precision_score.html)を使って計算

scikit-learnでも実装されていますが、値は0から1の間で、スコアが高いほど良い場合にのみ使えます。

```python
from sklearn.metrics import average_precision_score


y_true = [1, 0, 1, 0, 1]
y_score = [0.9, 0.75, 0.6, 0.85, 0.7]

print(average_precision_score(y_true, y_score))
```

```sh:出力
0.7
```

# Mean Average Precision@k (mAP@k)

### 概要
MAP@kは、複数のクエリに対する Average Precision@k (AP@k) の平均値で、情報検索やレコメンデーションシステムの評価によく使用される指標です。**複数のクエリやユーザーリクエストに対してシステム全体の精度を評価する**ための有効な指標です。関連アイテムが上位に表示されることを重視し、ユーザー体験に直接影響する要素を評価するのに適しています。

### 特徴
AP@k と同様、順序を考慮した評価が可能、上位 k 個のアイテムのみを評価をするという特徴に加えて、以下の特徴があります。

- **複数クエリやユーザーの総合的な評価**：1つのクエリや検索結果のパフォーマンスではなく、複数のクエリにわたって全体の平均精度を評価するため、検索エンジンや推薦システムの全体的な性能を評価します

### 計算方法

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/471633/1a64f64b-420e-b9ed-38cd-396b1f8c1da3.png)

ここで、Q はクエリやユーザーリクエストの総数（つまり評価対象となる全検索クエリの数）、AP@k<sub>i</sub> はクエリ i に対する Average Precision@k です。

ユーザーが 100 人いる場合は、各ユーザーの AP@k を合計して 100 で割ります。

# Mean Reciprocal Rank@k (MRR@k)

### 概要
MRR@k は、検索システムや推薦システムで、ランク付けされたリスト内の最初の関連項目の位置を評価する指標です。MRR@k は、正解（関連性がある）なアイテムが最初に登場する順位（ランク）の逆数（Reciprocal Rank）を用いて評価します。このため、**ユーザーの求めるアイテムがどれだけ早く見つかるか（上位に表示されるか）に重き**を置いており、検索システムの使い勝手を測るのに有効な指標です。

### 特徴
- **最初の関連アイテムのみを考慮**：システムが最初に関連アイテムを提示する能力を評価します。各クエリに対して最初に現れる関連アイテムの順位を基準にするため、ユーザーが求める情報に素早くたどり着けるかを評価できます

<br>

- **順位の重要性**：より上位にランクされた関連アイテムに対して高いスコアが与えられます

### 計算方法

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/471633/62118332-b5b5-a45a-cd4a-32ae3b7dfbfc.png)

ここで、Q はクエリやユーザーリクエストの総数、rank<sub>i</sub> は、クエリ（ユーザー） i に対して、最初に関連するアイテムが現れた順位。もし rank<sub>i</sub> が上位 k 件に存在しなければ、そのクエリは 0 となります。

より具体的に説明します。

次のような 4 つのクエリがあり、各クエリに対する関連アイテムの出現順位が以下とします（k=10）。

クエリ1：正解アイテムが 1 番目に出現
クエリ2: 正解アイテムが 3 番目に出現
クエリ3: 正解アイテムが 6 番目に出現
クエリ4: 正解アイテムが 2 番目に出現

それぞれの Reciprocal Rank は次のようになります。

クエリ1：Reciprocal Rank = 1/1 = 1.0
クエリ2：Reciprocal Rank = 1/3 = 0.33
クエリ3：Reciprocal Rank = 1/6 = 0.17
クエリ4：Reciprocal Rank = 1/2 = 0.5

MRR@10 = (1.0+0.33+0.17+0.5) / 4 = 2/4 = 0.5

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/471633/a8eaeac3-68f9-6c39-f828-af9f1b25fd90.png)
引用：https://www.evidentlyai.com/ranking-metrics/mean-reciprocal-rank-mrr

### コード実装例

```python
import numpy as np


def reciprocal_rank_at_k(y_true, y_score, k=10, higher_is_better=True):
    # リストをNumPy配列に変換
    y_true = np.asarray(y_true)
    y_score = np.asarray(y_score)

    # スコアでソート（higher_is_better に応じて昇順or降順）
    if higher_is_better:
        top_k_indices = np.argsort(y_score)[::-1][:k]
    else:
        top_k_indices = np.argsort(y_score)[:k]

    # 上位k個に対応する真のラベルを取得
    top_k_true = y_true[top_k_indices]

    for i, num in enumerate(top_k_true):
        if num == 1:
            return 1 / (i + 1)

    return 0
```

#### サンプルデータ

```python
test_lists = [
    [0, 0, 1, 0, 1],
    [1, 0, 0, 0, 0],
    [0, 1, 0, 0, 1],
    [0, 0, 0, 0, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 0, 1, 0],
    [0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1],
]
y_scores_high = [0.9, 0.8, 0.7, 0.6, 0.5]
y_scores_low = [0.5, 0.4, 0.3, 0.2, 0.1]
```

#### スコアが高いほど類似（例：確率）

```python
results = []
for l in test_lists:
    result = reciprocal_rank_at_k(y_true=l, y_score=y_scores_high, k=5, higher_is_better=True)
    results.append(result)
    print(f"Reciprocal Rank@k: {result}\n")

print(f"Mean Reciprocal Rank@k: {np.mean(results)}")
```

```sh：出力
Reciprocal Rank@k: 0.3333333333333333

Reciprocal Rank@k: 1.0

Reciprocal Rank@k: 0.5

Reciprocal Rank@k: 0

Reciprocal Rank@k: 0.3333333333333333

Reciprocal Rank@k: 0.25

Reciprocal Rank@k: 0.2

Reciprocal Rank@k: 1.0

Mean Reciprocal Rank@k: 0.4520833333333333
```

#### スコアが低いほど類似（例：距離）

```python
results = []
for l in test_lists:
    result = reciprocal_rank_at_k(y_true=l, y_score=y_scores_low, k=5, higher_is_better=False)
    results.append(result)
    print(f"Reciprocal Rank@k: {result}\n")

print(f"Mean Reciprocal Rank@k: {np.mean(results)}")
```

```sh:出力
Reciprocal Rank@k: 1.0

Reciprocal Rank@k: 0.2

Reciprocal Rank@k: 1.0

Reciprocal Rank@k: 0

Reciprocal Rank@k: 0.3333333333333333

Reciprocal Rank@k: 0.5

Reciprocal Rank@k: 1.0

Reciprocal Rank@k: 1.0

Mean Reciprocal Rank@k: 0.6291666666666667
```

# Adjusted Mutual Information (AMI)
### 概要
AMIは、Mutual Information (MI)と呼ばれる情報理論に基づく指標を調整したもので、**元のクラスタリング結果（グラウンドトゥルース）と予測されたクラスタリング結果がどれだけ一致しているか**を評価します。MIの問題点として、ランダムなクラスタリング結果でも高いスコアが出る可能性があります。AMIはこれを修正するために、ランダムなクラスタリングから期待されるスコアを引き算して調整します。正確さを調整した形で算出されるため、異なるクラスタリング方法間の比較がしやすくなっています。

### 特徴
- **調整**：通常のMIは、クラスタ数やデータの規模によって値が変わることがあり、単純な比較が難しくなります。AMIはこれを調整し、クラスタ数やサンプル数が異なる場合でも比較が可能になります

<br>

- **範囲**：0から1の範囲の値をとり、1に近いほど2つのクラスタリングの一致度が高いことを示します。場合によっては負の値になることもあります

<br>

- **ランダムな期待値の引き算**：MIでは、データのサイズやクラスタ数が変動することでスコアが変化しますが、AMIではランダムなクラスタリング結果から得られる期待スコアを引き算するため、偶然の一致による影響を補正した正確な評価が可能です

### 計算方法
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/471633/8c06aa5d-c32e-322f-71f8-712962110451.png)

ここで、U と V は2つのクラスタリング結果、H(U) および H(V) はそれぞれのエントロピー、E[MI(U,V)] はランダムクラスタリングに基づく期待値です。

### 使用例
- **クラスタリングアルゴリズムの比較**：異なるアルゴリズムの性能を評価する際に使用できます

<br>

- **最適なクラスタ数の決定**：クラスタ数を変えながらAMIスコアを計算し、最適な数を見つけることができます

<br>

- **データセットの品質評価**：ラベル付けの一貫性をチェックするのに役立ちます

<br>

- **画像検索システムの評価**：類似画像のグルーピング性能を評価できます

### コード実装例
[scikit-learn](https://scikit-learn.org/stable/modules/clustering.html#mutual-information-based-scores)で実装されています。

```python
from sklearn.metrics.cluster import adjusted_mutual_info_score

labels_true = [0, 0, 0, 1, 1, 1]
labels_pred = [0, 0, 1, 1, 2, 2]
adjusted_mutual_info_score(labels_true, labels_pred)
```

```sh:出力
0.2987924581708901
```

予測ラベルの 0 と 1 を並べ替え、2 を 3 に名前変更すると、同じスコアが得られます。

```python
labels_pred = [1, 1, 0, 0, 3, 3]
adjusted_mutual_info_score(labels_true, labels_pred)
```

```sh:出力
0.2987924581708901
```

#### 非正のスコアの例

```python
labels_true = [0, 1, 2, 0, 3, 4, 5, 1]
labels_pred = [1, 1, 0, 0, 2, 2, 2, 2]
adjusted_mutual_info_score(labels_true, labels_pred)
```

```sh:出力
-0.16666666666666655
```

# Normalized Mutual Information (NMI)

### 概要
NMI は、クラスタリングの評価に広く使われる指標で、グラウンドトゥルースとクラスタリング結果の類似度を測るために使われます。Mutual Information (MI) に基づいており、異なるクラスタリング結果を比較する際に便利です。NMI は、MI をエントロピーで正規化することで、クラスタ数やデータセットのサイズに依存しない評価が可能となります。ただし、NMI は偶然の一致による影響を補正しません。

### 特徴
- **範囲**：0から1の範囲の値をとり、1に近いほど2つのクラスタリングの一致度が高いことを示します

<br>

- **正規化**：異なるデータセットや異なるクラスタ数の比較が可能

### 計算方法
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/471633/6e2e8126-2749-0310-65ac-e10875bd9667.png)

H(U) はグラウンドトゥルースクラスタU のエントロピー、H(V) は予測されたクラスタ のエントロピーです。Mutual Information (MI) は、2つのクラスタ間で共有される情報量を示します。

### 使用例
- **クラスタリングアルゴリズムの比較**：異なるアルゴリズムの性能を評価する際に使用できます

<br>

- **最適なクラスタ数の決定**：クラスタ数を変えながらAMIスコアを計算し、最適な数を見つけることができます

<br>

- **データセットの品質評価**：ラベル付けの一貫性をチェックするのに役立ちます

<br>

- **画像検索システムの評価**：類似画像のグルーピング性能を評価できます

### コード実装例
[scikit-learn](https://scikit-learn.org/stable/modules/clustering.html#mutual-information-based-scores)で実装されています。

```python
from sklearn.metrics.cluster import normalized_mutual_info_score


labels_true = [0, 0, 0, 1, 1, 1]
labels_pred = [0, 0, 1, 1, 2, 2]
normalized_mutual_info_score(labels_true, labels_pred)
```

```sh:出力
0.5158037429793889
```

```python
labels_pred = [1, 1, 0, 0, 3, 3]
normalized_mutual_info_score(labels_true, labels_pred)
```

```sh:出力
0.5158037429793889
```