PyTorchのテストとその構成

#はじめに
PyTorch 1.1.0のテストコードで説明する。個別テストの一部も例として示す。
すべてのテストは、[testディレクトリ](https://github.com/pytorch/pytorch/tree/v1.1.0/test)配下にある。スクリプト実行は、以下のように行う。テスト名を付加した結果が欲しい場合は、`-v`を追加する。

```
python test/run_test.py
```
個別テストは、以下のようにして行う。(run_test.pyから指定する場合と個別テストから指定する場合を例示する)

```python
python test/run_test.py -i sparse -- TestSparse.test_factory_size_check
```

```python
python test/test_nn.py TestNN.test_affine_grid
```
#構成
##前置き
- Pythonのunittestを拡張してPyTorch/testは作られている。このためunittestの基本知識があることが望ましい。([本資料の文末](https://qiita.com/sakaia/private/855fc174732832dedc1f#unittest)に簡単な説明をつけた。)

##全体
- すべてのテストは、run_test.pyよりtest_xxxx.pyを起動して行う。
- すべてのテストの共通設定ファイルは、common_utils.pyである。
   - unittestの大元となるTestCaseクラスを、[定義している。](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_utils.py#L305)
   - テンソルの標準フォーマット(torch.DoubleTensor)も[定義している。](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_utils.py#L42)
   - テンソルの計算精度(precision=1e-5)もこのTestCaseクラス内で設定される。
   - unittestの実行は、各テストファイル(test_xxxx.py)にてcommon_utils.run_testsを呼び出すことによって行う。なお、[common_utils.run_tests](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_utils.py#L57)がunittest.mainを起動している。
- 個別のテストコードは、test_xxxx.pyである。
  - 逆伝搬計算は、test_autograd.pyおよびtest_nn.pyでチェックを行っている。
     - 典型的には、微分(dx)が、1e-6で、精度がy-y'が1e-5であることを要請している。ただし、このパラメータは関数により変動している。

##個別テスト
### test_autograd
誤差逆伝播の計算チェックを行うテストセットである。
関数リストは、2種類ある。

-  [TestAutogradクラス](https://github.com/pytorch/pytorch/blob/v1.1.0/test/test_autograd.py#L71)に定義しているtest_xxxxメソッド群
-  common_methods_invocations.pyの[method_testsメソッド](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_methods_invocations.py#L114)で定義し、[add_testメソッド](https://github.com/pytorch/pytorch/blob/v1.1.0/test/test_autograd.py#L3071)でTestAutogradクラスに登録したメソッド群

#### テストコード例
前者のテスト例として[test_grad](https://github.com/pytorch/pytorch/blob/v1.1.0/test/test_autograd.py#L255)メソッドを示す。大まかには、演算を行って、assertXXXXで値のチェックを行うという構成になっている。

```
    def test_grad(self):
        x = torch.randn(2, 2, requires_grad=True)
        y = torch.randn(2, 2, requires_grad=True)
        z = x ** 2 + y * x + y ** 2
        z.backward(torch.ones(2, 2), create_graph=True)

        x_grad = 2 * x.data + y.data
        y_grad = x.data + 2 * y.data
        self.assertEqual(x.grad.data, x_grad)
        self.assertEqual(y.grad.data, y_grad)

        grad_sum = 2 * x.grad + y.grad
        x_hv = torch.autograd.grad(
            outputs=[grad_sum], grad_outputs=[torch.ones(2, 2)],
            inputs=[x], create_graph=True)
        expected_x_hv = torch.ones(2, 2) * 5
        expected_y_hv = torch.ones(2, 2) * 4

        self.assertEqual(x_hv[0].data, expected_x_hv)
        self.assertEqual(x.grad.data, x_grad)
        self.assertEqual(y.grad.data, y_grad)
```
後者は、add_test内で、[do_test関数](https://github.com/pytorch/pytorch/blob/v1.1.0/test/test_autograd.py#L3094)を定義し登録している。
実際のテストは、以下のように定義する

- 関数名　ここではmul
- テンソルサイズ(S, S, S)。なお、S=5, M=10, L=20である
- 引数((S, S),)
- 演算属性(broadcast_rhs)

```
        ('mul', (S, S, S), ((S, S),), 'broadcast_rhs', (True,)),
```
### test_nn
torch.nn.XXXXXという層を提供する関数に対するテストセットである。
テストする関数リストは、common_nn.pyおよびtest_nn.pyの

- (モジュール系)[module_tests](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_nn.py#L45)および[new_module_tests](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_nn.py#L872)
- (損失関数系)[criterion_tests](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_nn.py#L2675)および[new_criterion_tests (test_nn.pyで定義)](https://github.com/pytorch/pytorch/blob/v1.1.0/test/test_nn.py#L8097)

にある。これらを、[TestBaseクラス](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_nn.py#L3051)を継承した、NewModuleTestクラスや、NewCriterionTestクラス等を経由して、[add_testメソッド](https://github.com/pytorch/pytorch/blob/v1.1.0/test/test_nn.py#L8067)に渡す。そして、[TestNNクラス](https://github.com/pytorch/pytorch/blob/v1.1.0/test/test_nn.py#L498)に登録する。クラス関数を登録しているので、`__call__`メソッドがテストコードとして実行される。モジュール関数の場合、テストコードは、ModuleTestクラスの[`__call__`メソッド](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_nn.py#L3140)である。損失関数の場合、テストコードは、CriterionTestクラスの[`__call__`メソッド](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_nn.py#L3308)である。
これらの定義後、run_testsメソッド経由ですべてのテストコードを実行する。

#### テストコード例
テストコードでは、dictを定義して行う。
ModuleTestで定義するmodule_testsの各要素は、以下のように定義する。

- module_nameは、(self.)constructorにクラス名として引き継がれる。NewModuleTestクラス生成時に設定する。
- constructor_argsは、constructorの引数として渡される。
- input_sizeは、テンソルのサイズであり、torch.randnによりテンソルが生成されテストされる。
- reference_fnは、計算結果の比較対象となる関数である。

```
    dict(
        module_name='LogSoftmax',
        constructor_args=(1,),
        input_size=(10, 20),
        reference_fn=lambda i, _: torch.exp(i).div_(torch.exp(i).sum(1, True).expand(10, 20)).log_(),
    ),
```
NewModuleTestのnew_module_testsの各要素は以下のように定義する。

- fullnameは、テスト名であり、テストファイルがtest_fullnameとなる。
- constructor/input_sizeは、上記と同じなので省略する。
- cudnnは、cudnnライブラリで処理するかのフラグでTrue or Falseを設定する。

```
    dict(
        fullname='Conv1d_groups',
        constructor=lambda: nn.Conv1d(4, 6, kernel_size=3, groups=2),
        input_size=(2, 4, 6),
        cudnn=True,
    ),
```


なお上記パラメータが実行されるメソッドは、NewModuleTestの場合、それの親クラスであるModuleTestの以下の処理である。

```python
    def __call__(self, test_case):
        module = self.constructor(*self.constructor_args)
        input = self._get_input()

        if self.reference_fn is not None:
            out = test_case._forward(module, input)
            ref_input = deepcopy(input)
            expected_out = self.reference_fn(ref_input, test_case._get_parameters(module)[0])
            test_case.assertEqual(out, expected_out)
        self.test_noncontig(test_case, module, input)

        if self.should_test_pickle:
            # TODO: do this with in-memory files as soon as torch.save will support it
            with TemporaryFile() as f:
                test_case._forward(module, input)
                torch.save(module, f)
                f.seek(0)
                module_copy = torch.load(f)
                test_case.assertEqual(test_case._forward(module, input), test_case._forward(module_copy, input))

        self._do_test(test_case, module, input)
```
test_nnのNewModuleTestクラスの_do_testメソッドは、以下である。CPU/CUDA/CUDNN等かつ半精度や倍精度のテストをしていることがわかる。

```Python
    def _do_test(self, test_case, module, input):
        test_case.check_jacobian(module, input, self.jacobian_input)

        if self.check_gradgrad:
            # could probably unify check_jacobian above with this.
            params = tuple(x for x in module.parameters())
            _assertGradAndGradgradChecks(test_case,
                                         lambda x, *args, **kw: test_case._forward(module, x), (input,) + params)

        # check if module can be printed
        module.__repr__()

        if self.check_inplace:
            # check if the inplace variant of the module gives the same result
            # as the out-of-place

            module_ip = self.constructor(*self.constructor_args, inplace=True)

            input_version = input._version
            with freeze_rng_state():
                output = module(input)
            test_case.assertEqual(input._version, input_version)

            input_ip = deepcopy(input)
            input_ip_clone = input_ip.clone()
            with freeze_rng_state():
                output_ip = module_ip(input_ip_clone)
            test_case.assertNotEqual(input_ip_clone._version, input_version)
            test_case.assertEqual(output, output_ip)
            grad = output.data.clone().normal_()
            input.grad.data.zero_()
            output.backward(grad)
            output_ip.backward(grad)
            test_case.assertEqual(input.grad, input_ip.grad)

        if isinstance(input, torch.LongTensor) and TEST_CUDA:
            # check that cuda() moves module parameters to correct GPU device,
            # and that float() casts parameters correctly

            input = input.cuda()
            module.float().cuda()
            module(input)
            for p in module.parameters():
                test_case.assertIsInstance(p, torch.cuda.FloatTensor)
                test_case.assertEqual(p.get_device(), 0)

            if torch.cuda.device_count() > 1:
                input = input.cuda(1)
                module.cuda(1)
                with torch.cuda.device(1):
                    module(input)
                for p in module.parameters():
                    test_case.assertIsInstance(p, torch.cuda.FloatTensor)
                    test_case.assertEqual(p.get_device(), 1)
        else:
            # check that float()/double() casters work correctly

            # to float
            if not isinstance(input, torch.LongTensor):
                input = input.float()
            module.float()
            module(input)
            for p in module.parameters():
                test_case.assertIsInstance(p, torch.FloatTensor)

            # and back to double
            if not isinstance(input, torch.LongTensor):
                input = input.double()
            module.double()
            module(input)
            for p in module.parameters():
                test_case.assertIsInstance(p, torch.DoubleTensor)

            if TEST_CUDA and self.should_test_cuda:
                # check that cuda() moves module parameters to correct GPU device,
                # and that float() casts parameters correctly

                # to GPU0
                input = input.float().cuda()
                module.float().cuda()
                module(input)
                for p in module.parameters():
                    test_case.assertIsInstance(p, torch.cuda.FloatTensor)
                    test_case.assertEqual(p.get_device(), 0)

                # to CPU
                input = input.cpu()
                module.cpu()
                module(input)
                for p in module.parameters():
                    test_case.assertIsInstance(p, torch.FloatTensor)

                # back to GPU0
                input = input.cuda()
                module.cuda()
                module(input)
                for p in module.parameters():
                    test_case.assertIsInstance(p, torch.cuda.FloatTensor)
                    test_case.assertEqual(p.get_device(), 0)

                # test that forwards of module runs correctly without cuDNN
                if self.cudnn:
                    with torch.backends.cudnn.flags(enabled=False):
                        module(input)
                        for p in module.parameters():
                            test_case.assertIsInstance(p, torch.cuda.FloatTensor)
                            test_case.assertEqual(p.get_device(), 0)

                if torch.cuda.device_count() >= 2:
                    # test cross-GPU transfer works
                    # to GPU1
                    input = input.cuda(1)
                    module.cuda(1)
                    with torch.cuda.device(1):
                        module(input)
                    for p in module.parameters():
                        test_case.assertIsInstance(p, torch.cuda.FloatTensor)
                        test_case.assertEqual(p.get_device(), 1)

                if not self.skip_double:
                    # test double()
                    input = input.double().cuda()
                    module.double().cuda()
                    module(input)
                    for p in module.parameters():
                        test_case.assertIsInstance(p, torch.cuda.DoubleTensor)
                        test_case.assertEqual(p.get_device(), 0)

                # test half()
                input = input.half().cuda()
                module.half().cuda()
                module(input)
                for p in module.parameters():
                    test_case.assertIsInstance(p, torch.cuda.HalfTensor)
                    test_case.assertEqual(p.get_device(), 0)

```


### test_torch
torch.xxxメソッドに対して行うテストセットである。

### test_indexing/test_indexing_cuda
テスト項目は、両テストともtest_indexingで定義している。

- test_indexing単体は、CPUデバイス上で定義したテンソルでのテストである。
- test_indexing_cudaは、デフォルトテンソルをtorch.DoubleTensorから(CUDAデバイス用の)torch.cuda.FloatTensorに変更して、テストを行っている。

### test_tensorboard
TensorBoard用に追加された関数群(torch.utils.tensorboard配下)の動作確認テストである。なお、本テストではrunsディレクトリ書き込むだけであり、終了時に該当ディレクトリを削除する。もし、出力をtensorboardで確認したい場合、ディレクトリの削除処理をテストコードから取り除いて実行する必要がある。

##注意事項
- [テストで生成するデフォルトテンソルは、torch.DoubleTensorである。](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_utils.py#L42)これは、逆伝搬計算のチェックを高精度で行うためである。テンソルがtorch.float64でない場合は、Warningが表示される。
- 逆伝搬の解析結果と数値演算の比較は、PRECISIONまたはprecisionという変数で比較される。関数名としては、[gradcheck](https://github.com/pytorch/pytorch/blob/v1.1.0/torch/autograd/gradcheck.py#L187)や[gradgradcheck](https://github.com/pytorch/pytorch/blob/v1.1.0/torch/autograd/gradcheck.py#L324)等を参照のこと。
  - なお、test_nnの中のModuleTestでは、[precisionが2e-4](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_nn.py#L3138)と設定されている。ただし、[precisionパラメータを個別に設定](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_nn.py#L1847)しているテストもある。
- [正式サポートではない](https://github.com/pytorch/pytorch/blob/v1.1.0/CONTRIBUTING.md)が、`-pt`を追加するとunittestの代わりにpytestでの実行が出来る。なお、pytestでもunittestモジュールがインポートされ、unittest用のテストコードが実行出来る。

#参考資料
##ソースコード
- テスト全体
  - [pytorch/test/run_test.py](https://github.com/pytorch/pytorch/blob/v1.1.0/test/run_test.py)(全テストを起動するスクリプト test_XXXXX.pyを起動する また、モジュールごと実行しない関数はここで指定する。)
  - 各テストでの共通設定
     - [pytorch/test/common_utils.py](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_utils.py)(テストの共通設定 テンソルの標準フォーマットや標準精度)
     - [TestCase@common_utils.py](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_utils.py#L305) (このクラスを83カ所で継承している。)
         - [TestCase@expecttest.py](https://github.com/pytorch/pytorch/blob/v1.1.0/test/expecttest.py#L160)
             - unittest.TestCase 
- テスト個別
  - [pytorch/test/test_autograd.py](https://github.com/pytorch/pytorch/blob/v1.1.0/test/test_autograd.py)
     - [pytorch/test/common_methods_invocations.py](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_methods_invocations.py)
  - [pytorch/test/test_nn.py](https://github.com/pytorch/pytorch/blob/v1.1.0/test/test_nn.py)
     - [pytorch/test/common_nn.py](https://github.com/pytorch/pytorch/blob/v1.1.0/test/common_nn.py)
- 逆伝搬計算の数値演算と解析結果の比較(PRECISION等で探すと様々な値でチェックしていることがわかる)
  - [pytorch/torch/autograd/gradcheck.py](https://github.com/pytorch/pytorch/blob/v1.1.0/torch/autograd/gradcheck.py)(autogradの逆伝搬チェックスクリプト。gradcheckとgradgradcheckがある。)

##文書等
###pytorch
- [Unit testing](https://github.com/pytorch/pytorch/blob/v1.1.0/CONTRIBUTING.md#unit-testing)

###UnitTest
- [class unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase)(初期化時および終了時のメソッドなど)
  - [debug()](https://docs.python.org/3/library/unittest.html#unittest.TestCase.debug)出力結果の比較用メソッド群
- unittestの大まかな知識
   - unittest.TestCaseに具体的なテストとしてtest_xxxxメソッドを追加する。また、初期化および終了処理は、同クラス内のsetUp/tearDownメソッドで行う。
       - test_xxxxメソッドでは、assertXXXX等により値の正当性を確認する。
   - 上記で定義したメソッドは、unittest.main()の延長で実行される。 

###pytest
- [Configuration Options](https://docs.pytest.org/en/5.0.1/reference.html#configuration-options)(pytestの設定ファイルおよびそのオプション)
- [Usage and Invocations](https://docs.pytest.org/en/latest/usage.html#specifying-tests-selecting-tests)(pytest配下で個別テストの指定方法)
- [Conventions for Python test discovery](https://docs.pytest.org/en/5.0.1/goodpractices.html#conventions-for-python-test-discovery)(pytestでのテストコード検出順番。unittest.TestCaseも検出できる。)
   - pytestのテスト収集処理は、unittestモジュール向けのテストも呼び出せる。
- [stdout/stderrをテスト毎に出力する(`-s`, `--capture=`など)](https://docs.pytest.org/en/5.0.1/capture.html)

### Python
- [3. Data model](https://docs.python.org/3/reference/datamodel.html) (`__call__`によるクラスを呼び出し可能にする仕組みなど)