Chainer v2のenable_backpropの挙動について

# TL;DR

* `enable_backprop`は `backward()` 実行時ではなくforwardグラフ構築時に適用される。
    - ただし、動きがあまり直感的でないため、グラフ中に何度も`enable_backprop`を変更するような操作はしないほうが良い。
* `chainer.configuration`において`train`は`enable_backprop`を内包しないので気をつける。
    - 標準ライブラリの `chainer.extensions.Evaluator`はこれらを適切に設定してくれるので安心して大丈夫。
    - 自前の`chainer.extensions.Evaluator`みたいなものを書くときは両方の`configurations`を変更すること

# 目的

とうとう重い腰をあげてChainerとオレオレヘルパーライブラリをv2にあげようとしたところ、`enable_backprop`の挙動がきになったのでもメモ。
特に、下記の質問について考えてみる。

* グラフの途中で`enable_backprop`が変わったらどうなるのだろうか。 `.unchain_backward()`みたいなことはできるのだろうか?
* `train`をFalseにしたら、`enable_backprop`も自動的にFalseになるのだろうか?
* 標準ライブラリの `chainer.extensions.Evaluator`は信用して使って大丈夫なのだろうか?

ちなみにこの投稿はv2.0.0時点のものです。

# 調査

## グラフの途中で`enable_backprop`が変わったらどうなる?

contextmanagerを使った実装だと、グラフの途中で`enable_backprop`が変わったらどうなるのかがとても気になります。
もし、`no_backprop_mode`が`backward()`実行時に効果を持ち、forward計算時には`no_backprop_mode`を使っていたのにbackward計算を`no_backprop_mode`で囲い忘れたがために異常に良い精度がでてしまった...では困ります[^1]。

[^1]: もちろん、optimizerを呼ばないと実際にはデータが変わらないので、実用上このミスをおかしても問題ありませんが。

この答えは、[function_node.py](https://github.com/chainer/chainer/blob/1933b272bd27804c10af1a9e144a304a6edb85d9/chainer/function_node.py#L239-L258)にあります。ここでの記載のとおり、`no_backprop_mode`の効果は計算グラフ構築時に親を登録しないところにあるので、**forward計算時に指定した範囲で**効果を持ちます。

通常の実行

```python
a = chainer.Variable(np.array([0.1], dtype=np.float32))
with chainer.configuration.using_config('enable_backprop', True):
    chainer.config.show()
    b = a * 2.0
b.backward()
print a.grad
```

```:出力
cudnn_deterministic  False
debug                False
enable_backprop      True
keep_graph_on_report False
train                True
type_check           True
use_cudnn            auto
[ 2.]
```

backwardが範囲外にあってもちゃんと効果があることがわかります。

```python
a = chainer.Variable(np.array([0.1], dtype=np.float32))
with chainer.configuration.using_config('enable_backprop', False):
    chainer.config.show()
    b = a * 2.0
b.backward()
print a.grad
```

```:出力
cudnn_deterministic  False
debug                False
enable_backprop      False
keep_graph_on_report False
train                True
type_check           True
use_cudnn            auto
None
```
 
また、上記で述べたように、`enable_backprop`は**親**との接続を断ち切ります。したがってcontextmanager内で新しく作られた変数ではなく、その親の勾配が0になります。

```python
a = chainer.Variable(np.array([0.1], dtype=np.float32))
with chainer.configuration.using_config('enable_backprop', False):
    b = a * 2.0
c = b + 0.5
c.backward()
print a.grad  # None
print b.grad  # [ 1.]
```

しかも、**backwardが呼ばれた時のconfigurationに完全に依存しないかというと、[そうではありません](https://github.com/chainer/chainer/issues/3368)**。なので、1つの計算グラフの中で何度も`enable_backprop`を使うような実装はしないで、素直に`unchain_backward()`を使ったほうがよさそうです。

## `train`をFalseにしたら、`enable_backprop`も自動的にFalseになる?

**なりません。**

```python
a = chainer.Variable(np.array([0.1], dtype=np.float32))
with chainer.configuration.using_config('train', False):
    chainer.config.show()
    b = a * 2.0
b.backward()
print a.grad
```

```:出力
cudnn_deterministic  False
debug                False
enable_backprop      True
keep_graph_on_report False
train                False
type_check           True
use_cudnn            auto
[ 2.]
```

なので、自分で`extensions.Evaluator`みたいなコードを書く場合は`enable_backprop`と`train`の両方を`False`にする必要があります。

## 標準ライブラリの `chainer.extensions.Evaluator`は信用して使って大丈夫?

**大丈夫そうです。** `enable_backprop`と`train`の両方を`False`にしてくれています。

```python
import chainer
import chainer.functions as F
import chainer.links as L
from chainer import training
from chainer.training import extensions

# Network definition
class MLP(chainer.Chain):
    def __init__(self, n_out):
        super(MLP, self).__init__()
        with self.init_scope():
            self.l1 = L.Linear(None, n_out)

    def __call__(self, x):
        chainer.config.show()
        print ""
        return self.l1(x)

model = L.Classifier(MLP(10))

optimizer = chainer.optimizers.Adam()
optimizer.setup(model)

# Load the MNIST dataset
train, test = chainer.datasets.get_mnist()
test = chainer.datasets.split_dataset(test, 1)[0]

train_iter = chainer.iterators.SerialIterator(train, 32)
test_iter = chainer.iterators.SerialIterator(test, 1,
                                             repeat=False, shuffle=False)
# Set up a trainer
updater = training.StandardUpdater(train_iter, optimizer)
trainer = training.Trainer(updater, (1, 'iteration'))
trainer.extend(extensions.Evaluator(test_iter, model), trigger=(1, 'iteration'))

# Run the training
trainer.run()
```
```:出力
cudnn_deterministic  False
debug                False
enable_backprop      True
keep_graph_on_report False
train                True
type_check           True
use_cudnn            auto

cudnn_deterministic  False
debug                False
enable_backprop      False
keep_graph_on_report False
train                False
type_check           True
use_cudnn            auto
```

ご覧のとおり`enable_backprop`と`train`の両方が`False`になっています。コードでいうと[このあたり](https://github.com/chainer/chainer/blob/v2.0.2/chainer/training/extensions/evaluator.py#L175-L181)が該当しています。