LoginSignup
5
4

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

Last updated at Posted at 2022-08-01

1. 概要

  • 元論文

1-1. 経緯

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

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

1-2. 解決策

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

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

1-3. 結論

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

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

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

2. 詳細

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

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

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上で確認したい場合は*ココ*をクリックして下さい

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化されて入っています。
以下のような感じ。

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 以下の関数については、以下の記事で説明がなされていました。

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


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

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

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

torch.nn.init.kaiming_normal_のドキュメント

kaiming_normal_ メソッドの概要

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

本メソッドの各引数については、上記 kaiming_normal_ のドキュメントと、pytorchkaiming_normal_ソース を見て、確認しました。

第一引数

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

引数 a

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

引数 mode

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

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

ソースコードはこちら

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

引数 nonlinearity

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

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

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

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

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)

各引数の意味合いは公式ドキュメントを見るとすぐにわかる。

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の論文に記載されているものとは異なります。

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

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

学習時間がかかってしょうがないので、今回は省略しました(;˘ω˘)スッ…スヤァ…

4. 感想など

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

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

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

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

5. 参考

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

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

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

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

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4