Chainer に IoU評価関数 を追加する（Evaluatorを自作）

# 更新履歴

- 2018/11/17 集計時に誤差が発生するバグを修正しました

# はじめに

*こんにちは、のんびりエンジニアのたっつーです。*
*[ブログ](https://www.fast-system.jp/)を運営しているのでよろしければ見てください。*

Chainer に IoU の評価関数を追加したかったので Evaluator を自作してみました。以下にように、学習中ログに IoU が追加して表示されるようになります。

詳細は、[Chainer に IoU評価関数 を追加する（Evaluatorを自作）](https://www.fast-system.jp/chainer-%e3%81%ab-iou%e8%a9%95%e4%be%a1%e9%96%a2%e6%95%b0-%e3%82%92%e8%bf%bd%e5%8a%a0%e3%81%99%e3%82%8b%ef%bc%88evaluator%e3%82%92%e8%87%aa%e4%bd%9c%ef%bc%89/) をご参照ください。

```
epoch       iou           main/loss   main/accuracy  ...
1           0             0.042866    0.993138       ...
2           0.000329707   0.0347965   0.993241       ...
3           0.00700626    0.0307309   0.993857       ...
```

# 使用例

## iouのログを追加

```python
trainer.extend(IouEvaluator(test_iter, model, device=gpu_id))
```

## 画面にiouを表示

```python
trainer.extend(extensions.PrintReport(['epoch', 'iou', 'main/loss', 'main/accuracy', 'validation/main/loss', 'validation/main/accuracy', 'elapsed_time']))
```

## ファイルにiouグラフを保存する

```python
trainer.extend(extensions.PlotReport(['iou'], x_key='epoch', file_name='iou.png'))
```

# ソースコード

## IouEvaluator.py 本体

```python:IouEvaluator.py
import chainer
from chainer import reporter as reporter_module
from chainer.training import extensions
from chainer import function
import numpy as np

class IouEvaluator(extensions.Evaluator):
    
    def evaluate(self):
        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()
        
        and_count = 0.
        or_count = 0.
        
        for batch in 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)
                        ac, oc = self.iou(in_arrays)
                    elif isinstance(in_arrays, dict):
                        eval_func(**in_arrays)
                        ac, oc = self.iou(in_arrays)
                    else:
                        eval_func(in_arrays)
                        ac, oc = self.iou(in_arrays)
                    and_count = and_count + ac
                    or_count = or_count + oc
            
            # print(observation)
            summary.add(observation)
        
        iou_observation = {}
        if(or_count == 0):
            iou_observation['iou'] = 0.
        else:
            iou_observation['iou'] = float(and_count) / or_count
        summary.add(iou_observation)
        
        return summary.compute_mean()
    
    def iou(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)
        # print(y)
        y = y.argmax(axis=1)
        
        # print('labels', labels)
        # print('predct', y)
        and_count = (labels & y).sum()
        or_count = (labels | y).sum()
        return and_count, or_count
```

## 呼び出し側

```python
def train(model_object, batchsize=64, gpu_id=0, max_epoch=20, dataset_func=None):
    
    # 1. Dataset
    dataset_name=dataset_func.__name__
    train, test = dataset_func()
    print('train=' + str(len(train)) + ' test=' + str(len(test)))
    # print(train)
    img, label = train[0]
    print(img.shape)
    
    # 2. Iterator
    train_iter = iterators.SerialIterator(train, batchsize)
    test_iter = iterators.SerialIterator(test, batchsize, False, False)
    
    # 3. Model
    model = L.Classifier(model_object)
    if gpu_id >= 0:
        model.to_gpu(gpu_id)
    
    # 4. Optimizer
    optimizer = optimizers.Adam()
    optimizer.setup(model)
    
    # 5. Updater
    updater = training.StandardUpdater(train_iter, optimizer, device=gpu_id)
    
    # 6. Trainer
    model_name = model_object.__class__.__name__
    outdir = '{}_{}'.format(model_name, dataset_name)
    if not os.path.exists(outdir):
        os.makedirs(outdir)
    
    epoch_npz = '{}_{}_{}.npz'.format(model_name, dataset_name, '{.updater.epoch}')
    final_npz = '{}_{}_{}.npz'.format(model_name, dataset_name, 'fin')
    # print(epoch_npz)
    # print(final_npz)
    
    trainer = training.Trainer(updater, (max_epoch, 'epoch'), out=outdir)
    
    # 7. Evaluator
    # TestModeEvaluator
    
    trainer.extend(extensions.LogReport())
    trainer.extend(IouEvaluator(test_iter, model, device=gpu_id))
    # trainer.extend(SemanticSegmentationEvaluator(test_iter, model, label_names=['negative','positive']))
    trainer.extend(extensions.PrintReport(['epoch', 'iou', 'main/loss', 'main/accuracy', 'validation/main/loss', 'validation/main/accuracy', 'elapsed_time']))
    trainer.extend(extensions.PlotReport(['main/loss', 'validation/main/loss'], x_key='epoch', file_name='loss.png'))
    trainer.extend(extensions.PlotReport(['main/accuracy', 'validation/main/accuracy'], x_key='epoch', file_name='accuracy.png'))
    trainer.extend(extensions.PlotReport(['iou'], x_key='epoch', file_name='iou.png'))
    trainer.extend(extensions.snapshot_object(model, epoch_npz))
    trainer.run()
    del trainer
    
    # save model
    serializers.save_npz(outdir + '/' + final_npz, model)
    
    return model
```

# 終わりに

*よければ ブログ「[初心者向けUnity情報サイト](https://www.fast-system.jp/)」の方にも色々記載しているのでぜひご参照いただければと思います。*