Label ranking average precision(LRAP)の理論と実装

# Label ranking average precision(LRAP)の理論と実装
## この記事は？
- 精度の評価指標の１つである，Label ranking average precision(LRAP)の理論と実装について解説を試みたものです．
- Kaggleの[Freesound Audio Tagging 2019](https://www.kaggle.com/c/freesound-audio-tagging-2019)の指標で採用されているため，改めて確認しておくための自身の備忘録として．

## 参考
- scikit-learnの説明は下記．  
https://scikit-learn.org/stable/modules/model_evaluation.html#label-ranking-average-precision
- Googleの公式の実装は下記．  
https://colab.research.google.com/drive/1AgPdhSp7ttY18O3fEoHOQKlt_3HJDLi8
- この記事の実装  
https://colab.research.google.com/drive/1OcMrib87WYgEVOAv-jPffwaB73Q8x1N0

## LRAPとは
- モデルの精度の指標の1つ．マルチラベルの分類問題の精度として用いられる．
- Precision（適合率）やRecall（再現率）の代わりにラベルのランキング（Sigmoid関数やSoftmax関数で出力した値に対して，値が大きい順にランク付けしたもの）が用いられる．

## マルチラベルの分類って？
### 2クラス分類
- 2クラス分類では，入力されたデータを2クラスに分類する．
- 出力にはSigmoid関数等が用いられる．予測結果を足すと1になる（確率問題として解く）．  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195903/4126338d-53dd-baa4-adc9-7c73c27513ec.png)

### マルチクラス分類
- マルチクラス分類では，入力されたデータを複数クラスに分類する．  
- 出力にはSoftmax関数等が用いられる．予測結果を足すと1になる（確率問題として解く）．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195903/21ec60a8-4dae-296c-11e5-385d39f79539.png)


### マルチラベル分類
- マルチラベル分類では，入力されたデータに対して複数のラベルに対する確率を算出する．
- Softmax関数ではなく，各出力結果に対してSigmoid関数を適用するのが一般的らしい．なので予測結果を全て足しても1にはならない．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195903/25857a94-4f4c-9b50-c952-6148b85edd59.png)

## LRAPの式
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195903/9164eaca-6ca0-a117-4e54-6fc43dc26c15.png)

- 1,「正解ラベルのランクまでの"1"（正解）の数 / 予測した予測したラベルのランク」を各ラベルごとに求めたものの総和を非0要素の予測ラベル数で割る（平均）
- 2,1ををサンプル数計算して総和し，サンプル数で割る（平均）
- ちなみにKaggleで書かれている「label-weighted」というのは，ラベル（クラス）の出現頻度のスコアへの影響を少なくするために，各ラベルのスコアに重み付き平均を掛けるのだが，重み付き平均のスコアの総和は結局各ラベルに対するスコアの平均に等しい．
- 例）スコア1.0のラベルが1つ，スコア0.5のラベルが4つの場合，それぞれの重みづけスコアは(1*(1/5))，(0.5*4/5)となる．すなわち，
  - (1 * 0.2) + (0.5 * 0.8) =  (1 * 0.2) + (0.5 * 0.8) / 1 = (1 * 0.2) * 5 + (0.5 * 0.8) * 5 / 5 = 1 * 1(出現回数) + 0.5 * 4(出現回数) / 5

## どういうこと？
### 各予測結果の算出
#### 例1
- 月見うどん（ねぎのせ）の場合
- 対象の画像に対して，うどん，卵，ネギが正解ラベルとして付いている
- 予測結果は，うどん，卵，ねぎの順に予測値が高い
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195903/b0c038fc-3757-6d43-a8f7-c94d96858c1f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195903/ef3fd525-da04-93e6-99f1-a0a657d0ddeb.png)

- 例1の精度の計算
  - うどんについて
      - うどんのランク=1
      - うどんのランクまでの正解数=1
      - うどんのスコア=1/1=1
  - 卵について
      - 卵のランク=2
      - 卵のランクまでの正解数=2
      - 卵のスコア=2/2=1
  - ねぎについて
      - ねぎのランク=3
      - ねぎのランクまでの正解数=3
      - ねぎのスコア=3/3=1
  - このサンプルに対するスコア
      - (うどん + 卵 + ねぎ) / 3 = (1+1+1)/3 = 1

- 実装（Googleの実装を利用）

```
# 正解ラベル
# ご飯，パン，うどん，卵，ねぎ
y_true = np.array([[0,0,1,1,1]])

# 予測スコア
# ご飯，パン，うどん，卵，ねぎ
y_score = np.array([[0.2, 0.1, 0.9, 0.7, 0.6]])

pos_class_indices, precision_at_hits = _one_sample_positive_class_precisions(y_score[0], y_true[0])
print("sample 1 のスコア", precision_at_hits)

score, weight = calculate_per_class_lwlrap(y_true, y_score)
print("各クラスのスコア", score)
print("各クラスの重み", weight)

LwLRAP = (score*weight).sum()
print("LwLRAP", LwLRAP)
```

> sample 1 のスコア [1. 1. 1.]
> 各クラスのスコア [0. 0. 1. 1. 1.]
> 各クラスの重み [0.         0.         0.33333333 0.33333333 0.33333333]
> LwLRAP 1.0


#### 例2
- 月見うどん（ねぎのせ）2の場合
- 対象の画像に対して，うどん，卵，ネギが正解ラベルとして付いている
- 予測結果は，うどん，卵，ごはん，ねぎの順に予測値が高い
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195903/7faed605-55ad-d532-59d6-00ce758d13d3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195903/9549260e-0816-6756-45c3-77f58d2a135d.png)

- 例2の精度の計算
  - うどんについて
      - うどんのランク=1
      - うどんのランクまでの正解数=1
      - うどんのスコア=1/1=1
  - 卵について
      - 卵のランク=2
      - 卵のランクまでの正解数=2
      - 卵のスコア=2/2=1
  - ねぎについて
      - ねぎのランク=4
      - ねぎのランクまでの正解数=3
      - ねぎのスコア=3/4=0.75
  - このサンプルに対するスコア
      - (うどん + 卵 + ねぎ) / 3 = (1+1+0.75)/3 = 0.91666...

- 実装（Googleの実装を利用）

```
# 正解ラベル
# ご飯，パン，うどん，卵，ねぎ
y_true_2 = np.array([[0,0,1,1,1]])

# 予測スコア
# ご飯，パン，うどん，卵，ねぎ
y_score_2 = np.array([[0.6, 0.1, 0.9, 0.7, 0.3]])

pos_class_indices_2, precision_at_hits_2 = _one_sample_positive_class_precisions(y_score_2[0], y_true_2[0])
print("月見うどん2のスコア：", precision_at_hits_2)

score_2, weight_2 = calculate_per_class_lwlrap(y_true_2, y_score_2)
print("各クラスのスコア", score_2)
print("各クラスの重み", weight_2)

LwLRAP_2 = (score_2*weight_2).sum()
print("LwLRAP", LwLRAP_2)
```

>月見うどん2のスコア： [1.   1.   0.75]
各クラスのスコア [0.   0.   1.   1.   0.75]
各クラスの重み [0.         0.         0.33333333 0.33333333 0.33333333]
LwLRAP 0.9166666666666666

#### 最終的なスコア

```
# 正解ラベル
# ご飯，パン，うどん，卵，ねぎ
y_true = np.array([[0,0,1,1,1], [0,0,1,1,1]])

# 予測スコア
# ご飯，パン，うどん，卵，ねぎ
y_score = np.array([[0.2, 0.1, 0.9, 0.7, 0.6], [0.6, 0.1, 0.9, 0.7, 0.3]])

score, weight = calculate_per_class_lwlrap(y_true, y_score)
print("各クラスのスコア", score)
print("各クラスの重み", weight)

LwLRAP = (score*weight).sum()
print("LwLRAP", LwLRAP)
```

>各クラスのスコア [0.    0.    1.    1.    0.875]
各クラスの重み [0.         0.         0.33333333 0.33333333 0.33333333]
LwLRAP 0.9583333333333333


## まとめ
- KaggleのFAT2019では，各音源に対して複数楽器がラベリングされているので，楽器数（ラベル数）＝次元数とした配列をつくり，MultiLabelBinarizerなどで0,1表現にしてlabelデータを作ります．
- あとは出力層も同様の次元数にし，各出力にSigmoidを通して上記Google関数を実行することで精度を算出できます．※LOSSはクロスエントロピー等で算出．


以上．