Chainerで適合率, 再現率を算出する評価関数を作った(Evaluator自作)


# 概要
- chainer標準のEvaluatorを継承して,Evaluator関数を自作しました
- 分類タスク時の,epoch毎の学習結果をConfusionMatrixにし,適合率, 再現率を算出する評価関数を作成しました

下記の様な出力がされ, cmrecall, cmaccuracyが適合率と再現率を表しています。

```
epoch       main/loss   main/accuracy  elapsed_time  cmrecall    cmaccuracy
1           11.7993     0.616536       205.322       0.991       0.648       
2           0.608019    0.652384       403.316       0.975       0.668       
3           0.518719    0.707642       602.897       0.941       0.91        
.
.
.

```

# Confusion Matrix
ConfusionMatrixの説明は,以下のページがわかりやすいです。

ConfusionMatrixとは,クラス分類の結果をまとめた表のことです。
作成した識別器の,**誤識別の傾向**を見たいときに参考になります。取りこぼしが多いのか,それとも誤認識が多いのか。

例えば,製品の傷の有無を調べるといった異常検知タスクだと,

**誤認識の種類**
1. 傷がついているけれど, スルーしてしまう
2. 傷がついていないけれど, 傷がついていると判定してしまう

といったことが考えられます。この場合だと,1をなくすことが重要なのではないかと考えます。この様に,タスクによってどちらを優先して減らすべきかが変わってくるのでこういった指標を取り入れることは重要です。


> ![cm.png](https://qiita-image-store.s3.amazonaws.com/0/234523/00dc5298-b838-689d-1d75-405d945baf42.png)
[F値 - 機械学習の「朱鷺の杜Wiki」](http://ibisforest.org/index.php?F%E5%80%A4)
[混同行列（Confusion Matrix）](http://popo.ara.black/etc/confusionmatrix.htm)

###再現率
先の例で言うと,実際に傷があったものの内,正しく傷があったと判定した割合になります。

```math
\frac{TP}{TP + FN}
```
###適合率
こちらも同じく先の例で言うと,傷があると予測したものの内,正しく傷があった割合になります。

```math
\frac{TP}{TP + FP}
```

# 使い方
以下のように,通常の拡張機能を追加する様に書いていただければ適合率, 再現率を算出することができます。


###再現率,適合率のログを追加
```
trainer.extend(CMEvaluator(valid_iter, model, device=gpu_id))
```

###標準出力に再現率,適合率を表示
```
trainer.extend(extensions.PrintReport( entries=['epoch', 'main/loss', 'main/accuracy', 'elapsed_time', 'cmrecall', 'cmaccuracy' ]))
```

# 実装
chainer標準のEvaluatorを継承して以下のEvaluator関数を自作しました。

```CMEvaluator.py
import chainer
from chainer import reporter as reporter_module
from chainer.training import extensions
from chainer import function
import numpy as np

from sklearn.metrics import accuracy_score, confusion_matrix, classification_report, precision_score, recall_score

class CMEvaluator(extensions.Evaluator):
    """
    Confusion Matrixで再現率、適合率を算出するEvaluator
    """
    default_name="mycm"
    def evaluate(self):
        # iterator, modelを設定
        iterator = self._iterators['main']
        model = self._targets['main']
        # 別で行う関数があるなら設定
        eval_func = self.eval_func or model

        if self.eval_hook:
            self.eval_hook(self)

        if hasattr(iterator, 'reset'):
            iterator.reset()
            it = iterator
        else:
            it = copy.copy(iterator)

        summary = reporter_module.DictSummary()

        recall_count = 0
        accuracy_count = 0
        lcount = 0
        for i, batch in enumerate(it):
            observation = {}
            with reporter_module.report_scope(observation):
                in_arrays = self.converter(batch, self.device)
                with function.no_backprop_mode():
                    if isinstance(in_arrays, tuple):
                        eval_func(*in_arrays)
                        re, ac = self.cm(in_arrays)
                    elif isinstance(in_arrays, dict):
                        eval_func(**in_arrays)
                        re, ac = self.cm(in_arrays)
                    else:
                        eval_func(in_arrays)
                        re, ac = self.cm(in_arrays)
                    recall_count = recall_count + re
                    accuracy_count = accuracy_count + ac

            summary.add(observation)
            lcount = i

        cm_observation = {}

        cm_observation["cmrecall"] = round(recall_count / (lcount + 1) ,3)
        cm_observation["cmaccuracy"] = round(accuracy_count / (lcount + 1), 3)
        # print(cm_observation)
        # print(summary.compute_mean())
        summary.add(cm_observation)

        return summary.compute_mean()




    def cm(self, in_arrays):
        model = self._targets['main']

        _, labels = in_arrays
        if self.device >= 0:
            labels = chainer.cuda.to_cpu(labels)

        y = model.y.data
        if self.device >= 0:
            y = chainer.cuda.to_cpu(y)
        y = y.argmax(axis=1)
        cmatrix = np.zeros((2,2))
        cmatrix = np.array(confusion_matrix(labels,y))
        # print(cmatrix)
        recall = round(cmatrix[0,0] / (cmatrix[0,0] + cmatrix[0,1]), 3)
        accuracy = round(cmatrix[0,0] / (cmatrix[0,0] + cmatrix[1,0]),3)

        return recall, accuracy
```

[chainer: Evaluatorを自作してトレーニング中のモデルの評価を柔軟に行う](http://mizti.hatenablog.com/entry/2017/10/24/011003)