[PyTorch / 深層学習] 重みパラメータの初期値が原因で学習が収束しない現象を体験した

# 1. 概要

- 元論文

https://arxiv.org/abs/1409.1556

## 1-1. 経緯

深層学習論文の再現実装をしたいと思っており、中でもVGGの論文はまぁまぁ簡単だと聞いて、VGGの再現実装に挑戦していたのですが、以下のような経緯で問題にぶつかりました。

1. ネットワークモデルを、 `torchvision` の提供する `torchvision.models.vgg16` と完全に同じ状態にした。
※モデルの状態は、以下の[実装したネットワークモデル]を参照
1. torchvisionの`vgg16`と、自作したVGG16をそれぞれ学習させたところ、**自作のVGG16のみ、学習が全然収束しなかった**。
1. 何回学習させても、「2.」の問題が解消せず。

<details>
<summary>[実装したネットワークモデル]</summary>
<div>

```python:VGG
  VGG(
    (features): Sequential(
      (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (1): ReLU(inplace=True)
      (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (3): ReLU(inplace=True)
      (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
      (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (6): ReLU(inplace=True)
      (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (8): ReLU(inplace=True)
      (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
      (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (11): ReLU(inplace=True)
      (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (13): ReLU(inplace=True)
      (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (15): ReLU(inplace=True)
      (16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
      (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (18): ReLU(inplace=True)
      (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (20): ReLU(inplace=True)
      (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (22): ReLU(inplace=True)
      (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
      (24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (25): ReLU(inplace=True)
      (26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (27): ReLU(inplace=True)
      (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (29): ReLU(inplace=True)
      (30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    )
    (avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
    (classifier): Sequential(
      (0): Linear(in_features=25088, out_features=4096, bias=True)
      (1): ReLU(inplace=True)
      (2): Dropout(p=0.5, inplace=False)
      (3): Linear(in_features=4096, out_features=4096, bias=True)
      (4): ReLU(inplace=True)
      (5): Dropout(p=0.5, inplace=False)
      (6): Linear(in_features=4096, out_features=1000, bias=True)
    )
  )
```

</div>
</details>

## 1-2. 解決策

自分の実装の間違っている箇所を調べるため、Githubに上がっている `torchvision` のソースコードを見に行った。

https://github.com/pytorch/vision/blob/1a1d509c8e6578584e7e9e4bd442654bf39149c8/torchvision/models/vgg.py#L51

その結果、 `モデルの重みパラメータの初期化` 処理の有無が原因であったことが判明した。
重みパラメータの初期化処理を加えたところ、二つのモデルは似たような学習曲線を描いた。
（ただし、途中でColaboratoryの使用時間制限に引っかかって処理が停止したので、最後までは確認できず....）。


## 1-3. 結論

学習が収束しない原因は重みパラメータの初期値だった。
本で読んだことはあったが、実際に体験できたのは良い経験になった。

再現実装するなら、モデルだけじゃなくて`重みパラメータの初期値`も再現しないと、全然異なる学習結果になってしまう。

※当然ではあるが、その他にもハイパーパラメータなども再現しないとダメ


# 2. 詳細

## 2-1. 重みパラメータの初期化処理

torchvision側のソースと、自分が行った実装の違いはここだった。


> ```python:PyTorch の vgg16 に実装されていた処理
> # 重みパラメータを初期化している
>         if init_weights:
>             for m in self.modules():
>                 if isinstance(m, nn.Conv2d):
>                     nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
>                     if m.bias is not None:
>                         nn.init.constant_(m.bias, 0)
>                 elif isinstance(m, nn.BatchNorm2d):
>                     nn.init.constant_(m.weight, 1)
>                     nn.init.constant_(m.bias, 0)
>                 elif isinstance(m, nn.Linear):
>                     nn.init.normal_(m.weight, 0, 0.01)
>                     nn.init.constant_(m.bias, 0)
> ```
> 
> 該当箇所のソースコードをGithub上で確認したい場合は[＊ココ＊](https://github.com/pytorch/vision/blob/1a1d509c8e6578584e7e9e4bd442654bf39149c8/torchvision/models/vgg.py#L52-L63)をクリックして下さい

## 2-2. 解説

わかる範囲で上記ソースを解説します。
適宜、**Githubのソースと比較しながら見ていただけるとわかりやすい**と思います。


### 2-2-1. if init_weights:

最初の `init_weights` は、`torchvision.models.vgg16(weights=None)`にした場合に`True`になります。

`weights`引数に対して、`None`以外の値を与えた場合は、`init_weights=False`になっているようでした。

### 2-2-2. for m in self.modules:

`self.modules`には、各層がmodule化されて入っています。
以下のような感じ。

```python:self.modulesの中身
# from torch import nn が実行されているとする

nn.Sequentials(
    nn.Conv2d(...),
    nn.ReLU(...),
    nn.Conv2d(...),
    ...
)
```

なので、これが`for`ループで回されると、

- 1ループ目が`Conv2d`のmodule
- 2ループ目が`ReLU`のmodule
- ...

という感じになります。

### 2-2-3. if isinstance(m, nn....):

`isinstance`は、pythonの組み込み関数ですね。
`m`には`Conv2d`などが入っています。

この`m`が、 `Conv2d` `BatchNorm2d` `Linear` のいずれかから作られたインスタンスだったときだけ `if`条件がTrue になります。


---

以降の 2-2-4、2-2-5、2-2-6 で取り上げる `nn.init` 以下の関数については、以下の記事で説明がなされていました。

https://pystyle.info/pytorch-parameters-initialization/

この記事はわかりやすいですし、これらの関数を使うだけなら、この記事の説明で十分足りると思います。

---

### 2-2-4. nn.init.kaiming_normal_(m.weights, ...)

恐らく、一番大事なところです。

pytorchの公式ドキュメントがとても親切で、使い方まで含めて記載されているので、リンクを貼っておきます。ぺたり。

> [torch.nn.init.kaiming_normal_のドキュメント](https://pytorch.org/docs/stable/nn.init.html#torch.nn.init.kaiming_normal_)



#### kaiming_normal_ メソッドの概要

いわゆる `Heの初期値` と呼ばれるものを使ってモデルパラメータの初期化を行ってくれるメソッドです。
`normal_`というpostfixがついていることからわかるように、正規分布に従う値でパラメータを初期化してくれます。

本メソッドの各引数については、上記 kaiming_normal_ のドキュメントと、`pytorch`の [kaiming_normal_ソース](https://github.com/pytorch/pytorch/blob/8def154e00f55dd4f7fd4bff20ee50309eaf3132/torch/nn/init.py#L415) を見て、確認しました。


#### 第一引数

`kaiming_normal_`は、第一引数に与えた **Tersor型のパラメータを初期化** してくれます。

#### 引数 `a`

今回`vgg16`では、`mode` `nonlinearity`のみが指定されていたので、引数`a`についての話は省略。

#### 引数 `mode`

デフォルト値の `'fan_in'` か、もしくは `'fan_out'` が選べるようです。

'fan_in'の場合は、一つ前の層の分散の大きさが維持されるように、モデルパラメータを初期化するようです。
対して'fan_out'を指定した場合は、一つ後ろの層の分散の大きさが維持されるようにモデルパラメータを初期化するようです。

[ソースコードはこちら](https://github.com/pytorch/pytorch/blob/8def154e00f55dd4f7fd4bff20ee50309eaf3132/torch/nn/init.py#L284-L300)

**TODO:** なぜVGGで`fan_out`が指定されているのか、わかっていない


#### 引数 `nonlinearity`

`'relu'` か `'leaky_relu'` を指定した方が良いよとドキュメントには書かれています。

今回のvgg16ではReLUを使用したので、`'relu'`を指定しておくので良さそう。

`nonlinearity`引数として他に何が指定できるのか調べてみたところ、`sigmoid`や`tanh`なども指定できるようです。

`nonlinearity`に指定した各引数による動作の違いは、以下のソースを参照。

https://github.com/pytorch/pytorch/blob/8def154e00f55dd4f7fd4bff20ee50309eaf3132/torch/nn/init.py#L67-L119


### 2-2-5. nn.init.constant_(m.bias, 0)

各層のバイアス(`bias`)の初期値を定数(`constant`)で初期化する。

`nn.init.constant_(m.bias, 0)`のように書いた場合は、全て`0`で初期化。

### 2-2-6. nn.init.normal_(m.weight, 0, 0.01)

各引数の意味合いは[公式ドキュメント](
https://pytorch.org/docs/stable/nn.init.html#torch.nn.init.normal_)を見るとすぐにわかる。

> torch.nn.init.normal_(tensor, mean=0.0, std=1.0)

と書かれているので、`nn.init.normal_(m.weight, 0, 0.01)`と記述した場合は、平均`0`、標準偏差`0.01`の正規分布からサンプリングされた値で、第一引数の`m.weight`を初期化してくれる。

# 3. 補足

今回の `torchvision.models.vgg16` で行われている重みパラメータの初期化は[VGGの論文](https://arxiv.org/abs/1409.1556)に記載されているものとは異なります。

VGGの論文では、"3 CLASSIFICATION FRAMEWORK > 3.1 TRAINING" の第2パラグラフで重みパラメータの初期化について触れられており、それによると以下の手順で初期化が行われたようです。

1. まず、比較的層数の少ない設定Aのモデルに、ランダムな初期パラメータを与えて学習
1. Aよりも深い層を持つ設定のモデルを学習させる際は、最初の4つのConvolution層と、最後の3つの全結合層に対して、「1.」で得られたパラメータを適用
1. その他の中間層は、ランダムなパラメータで初期化
1. 「1.」や「3.」において、ランダムなパラメータで初期化する部分には、平均0、分散0.01の正規分布からランダムにサンプリングした値を適用
1. （バイアスは0で初期化）

学習時間がかかってしょうがないので、今回は省略しました(；˘ω˘)ｽｯ…ｽﾔｧ…


# 4. 感想など

VGGの再現実装を行うには `2-2.` に書いたことを全て理解する必要があったらしい。

今回のように **学習が収束しない** 場合、**モデルパラメータの初期値に問題があるかもしれない** とわかったのは、大きな収穫だった。

これ以外にも、論文によっては、モデルのネットワークやハイパーパラメータの他に、前処理、最適化関数や損失関数なども工夫していたりするので、まだまだ学ぶことはたくさんありそうだ。

他の論文も再現実装していくぞ！( \*˙︶˙\*)و


# 5. 参考

Heの初期値についての原著論文(Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification)は、以下のリンクから飛べます。

https://openaccess.thecvf.com/content_iccv_2015/html/He_Delving_Deep_into_ICCV_2015_paper.html

また、VGG論文中で触れられている、重みパラメータのランダムな初期化方法（Glorot & Bengio (2010)）については以下で確認できます。

http://proceedings.mlr.press/v9/glorot10a

より理解を深めるため、どっちも軽く目を通しておきたいです。

その他、論文全体の日本語訳は以下のQiita記事にまとまっていました。
大変ありがたく活用させていただきました！

https://qiita.com/ttomomasa/items/b673a1e0b42a2a14a9d2