PyTorch CNNモデル再現性問題

# はじめに
最近ずっと悩んでいたのが、PyTorchで乱数の種を固定したつもりでも、結果が一定しない問題。環境はGoogle ColabのGPUです。
(2021-04-10) 対策もわかったので修正
(2021-07-06) 従来の方式でGoogle Colabでエラーになったので対策を追記
(2021-08-01) pytorchのバージョンアップに伴い、関数が変わったのでコード修正

# どのような問題か
次のようなコードで定義したCNNモデルです。
CIFAR-10学習用に作ったモデルです。

```
class CNN_v2(nn.Module):
    def __init__(self, num_classes):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 32, 3, padding=(1,1), padding_mode='replicate')
        self.conv2 = nn.Conv2d(32, 32, 3, padding=(1,1), padding_mode='replicate')
        self.conv3 = nn.Conv2d(32, 64, 3, padding=(1,1), padding_mode='replicate')
        self.conv4 = nn.Conv2d(64, 64, 3, padding=(1,1), padding_mode='replicate')
        self.conv5 = nn.Conv2d(64, 128, 3, padding=(1,1), padding_mode='replicate')
        self.conv6 = nn.Conv2d(128, 128, 3, padding=(1,1), padding_mode='replicate')
        self.relu = nn.ReLU(inplace=True)
        self.flatten = nn.Flatten()
        self.maxpool = nn.MaxPool2d((2,2))
        self.classifier1 = nn.Linear(4*4*128, 128)
        self.classifier2 = nn.Linear(128, 10)

        self.features = nn.Sequential(
            self.conv1,
            self.relu,
            self.conv2,
            self.relu,
            self.maxpool,
            self.conv3,
            self.relu,
            self.conv4,
            self.relu,
            self.maxpool,
            self.conv5,
            self.relu,
            self.conv6,
            self.relu,
            self.maxpool,
            )

        self.classifier = nn.Sequential(
            self.classifier1,
            self.relu,
            self.classifier2
        )

    def forward(self, x):
        x1 = self.features(x)
        x2 = self.flatten(x1)
        x3 = self.classifier(x2)
        return x3
```

# 最初の試み
最初にやった乱数固定は次のコードでした。

```
def seed_torch(seed=42):
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)

seed_torch()
```

これで、こんな形でモデルインスタンスを作ったのですが、結果的にまったくうまくいきませんでした。

```
# モデルインスタンスの生成
lr = 0.01
net = CNN_v2(n_output).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=lr)
```

# 次の試み
いろいろ調べるとGPUを使うと、上のコードだけでは結果を固定できない。次のように修正する必要があるとの記載が見つかります。なんでもパフォーマンスをよくするため、再現性が犠牲になるのだとか。
で、乱数固定コードを次のように修正しました。


```
def seed_torch(seed=42):
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.backends.cudnn.deterministic = True

seed_torch()
```

しかし、これでも結果は一定になりませんでした。。。

# 最後の試み
本当にdeterministicになっているかどうかチェックするために、次のような``set_deterministic(True)``呼び出しを入れるとよいと、どこかに記載がありました。


```
def seed_torch(seed=42):
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.backends.cudnn.deterministic = True
    torch.set_deterministic(True)

seed_torch()
```

そこで、乱数固定コードを上のように修正して、再度同じ学習をしたところ、下のようなエラー表示が。。。


```
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-80-229841503302> in <module>()
      2 
      3 num_epochs = 50
----> 4 history = fit(net, optimizer, criterion, num_epochs, train_loader, test_loader, device, history)

2 frames
/content/pythonlibs/torch_lib1/__init__.py in fit(net, optimizer, criterion, num_epochs, train_loader, test_loader, device, history)
     70 
     71             # 勾配計算
---> 72             loss.backward()
     73 
     74             # 重み変更

/usr/local/lib/python3.7/dist-packages/torch/tensor.py in backward(self, gradient, retain_graph, create_graph, inputs)
    243                 create_graph=create_graph,
    244                 inputs=inputs)
--> 245         torch.autograd.backward(self, gradient, retain_graph, create_graph, inputs=inputs)
    246 
    247     def register_hook(self, hook):

/usr/local/lib/python3.7/dist-packages/torch/autograd/__init__.py in backward(tensors, grad_tensors, retain_graph, create_graph, grad_variables, inputs)
    145     Variable._execution_engine.run_backward(
    146         tensors, grad_tensors_, retain_graph, create_graph, inputs,
--> 147         allow_unreachable=True, accumulate_grad=True)  # allow_unreachable flag
    148 
    149 

RuntimeError: replication_pad2d_backward_cuda does not have a deterministic implementation, but you set 'torch.use_deterministic_algorithms(True)'. You can turn off determinism just for this operation if that's acceptable for your application. You can also file an issue at https://github.com/pytorch/pytorch/issues to help us prioritize adding deterministic support for this operation.
```

どうやらConv2dのモジュール定義にオプションで入れていたpaddingがいけないようなメッセージです。


# 検証
上の予想が正しいか、確認するためpaddingオプションを取ったモデルを作り直してみます。

```
class CNN_v3(nn.Module):
    def __init__(self, num_classes):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 32, 3)
        self.conv2 = nn.Conv2d(32, 32, 3)
        self.conv3 = nn.Conv2d(32, 64, 3)
        self.conv4 = nn.Conv2d(64, 64, 3)
        self.conv5 = nn.Conv2d(64, 128, 3)
        self.conv6 = nn.Conv2d(128, 128, 3)
        self.relu = nn.ReLU(inplace=True)
        self.flatten = nn.Flatten()
        self.maxpool = nn.MaxPool2d((2,2))
        self.classifier1 = nn.Linear(1*1*128, 128)
        self.classifier2 = nn.Linear(128, 10)

        self.features = nn.Sequential(
            self.conv1,
            self.relu,
            self.conv2,
            self.relu,
            self.maxpool,
            self.conv3,
            self.relu,
            self.conv4,
            self.relu,
            self.maxpool,
            self.conv5,
            self.relu,
            self.conv6,
            self.relu,
            #self.maxpool,
            )

        self.classifier = nn.Sequential(
            self.classifier1,
            self.relu,
            self.classifier2
        )

    def forward(self, x):
        x1 = self.features(x)
        x2 = self.flatten(x1)
        x3 = self.classifier(x2)
        return x3
```

```
# モデルインスタンスの生成
lr = 0.01
net = CNN_v3(n_output).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=lr)
```

今度はうまくいきました。先ほどのようなエラーも起きませんし、損失関数値の結果もいつも同じにできました。
どうやら、Conv2dのpaddingオプションの実装に問題があるっぽいというのが結論です。

# 最終対策 
(2021-04-10 追記)
真の原因がわかりました。paddingオプション自体はいいのですが、次の追加オプションを入れるとdeterministicでなくなるようです。

``padding_mode='replicate'``

最終的に次のモデルにすることで、オリジナルと構造は変えずに再現可能なモデルを作れるようになりました。


```
class CNN_v4(nn.Module):
    def __init__(self, num_classes):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 32, 3, padding=(1,1))
        self.conv2 = nn.Conv2d(32, 32, 3, padding=(1,1))
        self.conv3 = nn.Conv2d(32, 64, 3, padding=(1,1))
        self.conv4 = nn.Conv2d(64, 64, 3, padding=(1,1))
        self.conv5 = nn.Conv2d(64, 128, 3, padding=(1,1))
        self.conv6 = nn.Conv2d(128, 128, 3, padding=(1,1))
        self.relu = nn.ReLU(inplace=True)
        self.flatten = nn.Flatten()
        self.maxpool = nn.MaxPool2d((2,2))
        self.classifier1 = nn.Linear(4*4*128, 128)
        self.classifier2 = nn.Linear(128, 10)

        self.features = nn.Sequential(
            self.conv1,
            self.relu,
            self.conv2,
            self.relu,
            self.maxpool,
            self.conv3,
            self.relu,
            self.conv4,
            self.relu,
            self.maxpool,
            self.conv5,
            self.relu,
            self.conv6,
            self.relu,
            self.maxpool,
            )

        self.classifier = nn.Sequential(
            self.classifier1,
            self.relu,
            self.classifier2
        )

    def forward(self, x):
        x1 = self.features(x)
        x2 = self.flatten(x1)
        x3 = self.classifier(x2)
        return x3
```

```
# モデルインスタンスの生成
lr = 0.01
net = CNN_v4(n_output).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=lr)
```

# 2021-07-06 追記
Google Colabでは、従来の実装でエラーが発生するようになりました。
その対策は、下記のセルをNotebookの冒頭に追加し、実行することとなります。
 (エラーの発生理由や、この設定で何が変わるのかまでは未調査)

```
# 2021-07-04 この環境変数設定をしないと、
# torch.set_deterministic(True) がエラーになった

%env CUBLAS_WORKSPACE_CONFIG=:16:8
```

# 2021-08-01 更に追記
Google ColabのPytorchバージョンが変わった(1.9.0+cu102)ことにより関数名が変更になりました。
現在の呼び出し方は、以下になります。

```
# 乱数初期化

def torch_seed(seed=123):
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.use_deterministic_algorithms = True
    torch.backends.cudnn.deterministic = True

```

# 2021-10-10 追記
ここで説明したノウハウを含めてPyTorchの書籍を出版しました。
紹介記事をqiitaに掲載しましたので、こちらもあわせてご参照いただけると幸いです。

[書籍「最短コースでわかる PyTorch &深層学習プログラミング」紹介](https://qiita.com/makaishi2/items/2c40fe43c01b35acb8c4)