GANの訓練ループでよく見るdetachは何をしている？

# どんな記事？

以下は["Inside Deep Learning"](https://github.com/EdwardRaff/Inside-Deep-Learning/blob/main/Chapter_9.ipynb)という本に載っているGANの訓練ループの一部です．（見やすいように一部編集しています）

```python
optimizerD = torch.optim.AdamW(D.parameters())
optimizerG = torch.optim.AdamW(G.parameters())

G_losses = []
D_losses = []

for epoch in tqdm(range(num_epochs)):
    for data, class_label in tqdm(train_loader, leave=False):
        real_data = data.to(device)
        y_real = torch.full((batch_size,1), real_label, dtype=torch.float32, device=device)
        y_fake = torch.full((batch_size,1), fake_label, dtype=torch.float32, device=device)
        
        # Discriminatorの訓練
        # readデータで勾配計算
        D.zero_grad()
        errD_real = loss_func(D(real_data), y_real)
        errD_real.backward()
        
        # fakeデータで勾配計算
        z = torch.randn(batch_size, latent_d, device=device)
        fake = G(z)
        # 元のNotebookにあったコメント↓
        #Why do we detach here? Because we don't want the gradient to impact G. 
        #Our goal right now is to update _just_ the discriminator. 
        #BUT, we will re-use this fake data for updating the discriminator, so we want to save the 
        #non-detached version! 
        errD_fake = loss_func(D(fake.detach()), y_fake)
        errD_fake.backward()

        errD = errD_real + errD_fake
        optimizerD.step()

        # Generatorの訓練
        G.zero_grad()
        errG = loss_func(D(fake), y_real)
        errG.backward()
        optimizerG.step()
        
        G_losses.append(errG.item())
        D_losses.append(errD.item())

```

ちょっと気になるのはDiscriminatorの訓練の`errD_fake = loss_func(D(fake.detach()), y_fake)`です．どうしてここで`fake.detach()`するんだ？どうしてGeneratorの訓練では`fake.detach()`しないんだ？"Inside Deep Learning"の本文中とNotebookのコメントにこの`detach()`に関する説明がありましたが，納得がいきません（納得いかない理由は後ほど）．

この`fake.detach()`が出現するのはこの本特有のことではなく，他のところでもよく出てきます（例えば[PyTorchのexample](https://github.com/pytorch/examples/blob/a60bd4e261afc091004ea3cf582d0ad3b2e01259/dcgan/main.py#L230)）．

この記事では，この`fake.detach()`について調べます．

# 結論

> This is not true. Detaching fake from the graph is necessary to avoid forward-passing the noise through G when we actually update the generator. If we do not detach, then, although fake is not needed for gradient update of D, it will still be added to the computational graph and as a consequence of backward pass which clears all the variables in the graph (retain_graph=False by default), fake won't be available when G is updated.

https://github.com/pytorch/examples/issues/116#issuecomment-401579280

つまり，「`fake`をDとGの訓練で使いまわしたいが，detachしないとDの勾配を計算したときに計算グラフが消され，Gの勾配計算ができなくなってしまう」ということらしいです．

もう少し細かく説明すると，
* `fake`はDiscriminatorとGeneratorの訓練の両方で使うので，Discriminatorで計算した`fake = G(z)`をGeneratorの訓練でも再利用したい．
* Dの訓練でdetachせずに`fake`を使って損失を計算（`errD_fake = loss_func(D(fake.detach()), y_fake)`）し，Dの勾配を計算（`errD_fake.backward()`）すると，計算グラフが消されてしまう．
* 計算グラフが消された状態で，Gの訓練時に先ほどの`fake`を再利用してGの損失を計算（`errG = loss_func(D(fake), y_real)`）し，Gの勾配を計算（`errG.backward()`）しようとする．しかし`fake`の計算グラフはDの勾配を計算したときに消えているため，Gの勾配を計算することができない（エラーを吐く）．

つまり，detachする以外の方法として
* DとGの訓練で`fake`を使い回すのをやめて，毎回`fake = G(z)`を計算する．
* `errD_fake.backward(retain_graph=True)`として計算グラフを消さないようにする．（[TORCH.TENSOR.BACKWARD](https://pytorch.org/docs/stable/generated/torch.Tensor.backward.html#:~:text=retain_graph%20(bool%2C%20optional)%20%E2%80%93%20If%20False%2C%20the%20graph%20used%20to%20compute%20the%20grads%20will%20be%20freed.%20Note%20that%20in%20nearly%20all%20cases%20setting%20this%20option%20to%20True%20is%20not%20needed%20and%20often%20can%20be%20worked%20around%20in%20a%20much%20more%20efficient%20way.%20Defaults%20to%20the%20value%20of%20create_graph.)）

などが考えられます（効率が良いかは別）．

自分の認識が合っているか少し自信がないので，もし間違っていらたご指摘ください．


以下蛇足です．

# コメントに対する疑問

先ほどのコードには，`fake.detach()`に関してこんなコメントがありました．

> Why do we detach here? Because we don't want the gradient to impact G. Our goal right now is to update _just_ the discriminator. BUT, we will re-use this fake data for updating the discriminator, so we want to save the non-detached version! 

なるほど．この説明はおそらく次のような意味でしょう．

「Discriminator`D`のfakeデータに対するLoss`errD_fake`の計算は，もしもdetachしないのであれば，`errD_fake = loss_func(D(fake), y_fake)`となります．そもそも`fake`はlatent vector`z`をGenerator`G`に渡すことで生成されていました（`fake = G(z)`）．そのため，勾配を計算（`errD_fake.backward()`）すると，GとDの両方の重みに対して勾配が計算されてしまいます．しかし今はDを訓練しているので，Gの重みは更新したくありません．そこで`fake.detach()`とすることによってGの勾配の計算を行わないようにしています」

一瞬納得しそうになりましたが，よく考えてみるとちょっと納得いきません．

`optimizerD = torch.optim.AdamW(D.parameters()); optimizerG = torch.optim.AdamW(G.parameters())`としているなら，Dの訓練ではDの重みだけ，Gの訓練ではGの重みだけを更新することができているのではないか？

考えてもわからないのでググったらStackOverflowに全く同じ疑問を持っている人がいました．

https://stackoverflow.com/questions/46944629/why-detach-needs-to-be-called-on-variable-in-this-example

> that's because if you don't use fake.detach() in output = netD(fake.detach()).view(-1) then fake is just some middle variable in the whole computational Graph, which tracks gradients in both netG and netD. and when you call netD.backward() the graph will be released. which means no more gradient information about netG() in the computational Graph. then when you use errG.backward() later, it will cause an error something like
"Trying to backward through the graph a second time"
if you don't use fake.detach(), you can use netD.backward(retain_graph=True)

GitHubにも同じような質問がありました．

https://github.com/pytorch/examples/issues/116

> This is not true. Detaching fake from the graph is necessary to avoid forward-passing the noise through G when we actually update the generator. If we do not detach, then, although fake is not needed for gradient update of D, it will still be added to the computational graph and as a consequence of backward pass which clears all the variables in the graph (retain_graph=False by default), fake won't be available when G is updated.

つまり，「結論」で書いたような感じらしいです．

# Inside Deep Learning

detachの説明では混乱しましたが，この本は説明が超丁寧でわかりやすく，僕のように雰囲気でDeep Learningをやっている人にもおすすめです．

* "Inside Deep Learning"（[O'Reilly Learning](https://learning.oreilly.com/library/view/inside-deep-learning/9781617298639/)，[Amazon](https://www.amazon.co.jp/-/en/Edward-Raff/dp/1617298638)，[Manning](https://www.manning.com/books/inside-deep-learning))