LSGAN（Pytorch）

# 概要
LSGANとは
* 損失関数に平均二乗誤差（MSE）を利用
* 識別器でSigmoid関数を使用しない

以上をGANに組み込むことで、
勾配消失を抑えて学習が効率的に進むように工夫したGANです。

# 損失関数
## Sigmoid関数の問題点
sigmoid関数は以下のような数式とグラフになります
![Sigmoid.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670347/ab3dfafb-04fd-1a59-1816-8c0fbe0880c5.png)
$f(x) = \frac{1}{1+e^{-x}}$

sigmoid関数では出力値が[0~1]の間をとるようになっており、
これを確率と捉え、二値分類でSigmoid関数を使用されることが一般的です。

一方GANの損失関数は以下の通りになります
$min_Gmax_DL(D,C) = E_{x～p_{data}(x)}[logD(x)] + E_{x～p_{z}(x)}[log(1-D(G(z)))]$
識別関数は本物か偽物かの二値分類を行った後、対数を通します。
そのため、右辺第一項は以下の通りに置き換えられます
$ logD(x)  $
$ logD(x) = log \frac{1}{1+e^{-x}} $ 

以上をxの範囲を-10から10まで変化した場合の出力値をグラフにしてみます
![GANSigmoid.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670347/3f884f20-520e-9ec0-61be-411bc3af2003.png)



ある一定の値以上になると出力される損失が0になることが分かります
損失が0だとそもそも学習に必要な勾配がなく学習がほとんど進まない状態になってしまいます

## 平均二乗誤差
Sigmoid関数に対して対数を通す場合、一定以上の値だと損失値が0になってしまうことが分かりました。

この問題点に対して
* 対数の代わりに平均二乗誤差を使用
* Sigmoid関数を使用しない

ことで解決することができます。

平均二乗誤差の式は以下の通りになります
$ MSE = \frac{1}{N}\sum^{N}_{n=1}(p_i - y_i)^2 $
pは予測値、yは正解値になります

GANの損失関数の右辺第一項に**平均二乗誤差の使用**と**Sigmoid関数を適用しない**の二点を落とし込むと以下のよう数式になります<br>
$ (x-1)^2 $
以上の式で、xの範囲を-10から10まで変化した場合の出力値をグラフにしてみます
![GANMSE.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670347/8dd53461-4d31-b9b7-f774-38819a849231.png)
$ log \frac{1}{1+e^{-x}} $の場合と比較すると、損失値が0になる部分がほとんどないことが分かります

# モデル構造
## 生成器
生成器は通常のGANで使用される構造と変わりありません
``` python
class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.sequential1 = nn.Sequential(
            nn.ConvTranspose2d(100 , 64 * 8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(64 * 8),
            nn.ReLU(True)
        )
        self.sequential2 = nn.Sequential(
            nn.ConvTranspose2d(64 *8, 64 * 4, 4  , 2 , 1, bias=False),
            nn.BatchNorm2d( 64 * 4),
            nn.ReLU(True)
        )
        self.sequential3 = nn.Sequential(
            nn.ConvTranspose2d(64 * 4, 64 * 2 , 4  , 2 , 1, bias=False),
            nn.BatchNorm2d(64 * 2),
            nn.ReLU(True)
        )
        self.sequential4 = nn.Sequential(
            nn.ConvTranspose2d( 64 * 2 , 1 , 4  , 2 , 1, bias=False),
            nn.Tanh()
        )


    def forward(self, input):
        x = self.sequential1(input)
        x = self.sequential2(x)
        x = self.sequential3(x)
        x = self.sequential4(x)
        return x
```

``` 出力
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
   ConvTranspose2d-1            [-1, 512, 4, 4]         819,200
       BatchNorm2d-2            [-1, 512, 4, 4]           1,024
              ReLU-3            [-1, 512, 4, 4]               0
   ConvTranspose2d-4            [-1, 256, 8, 8]       2,097,152
       BatchNorm2d-5            [-1, 256, 8, 8]             512
              ReLU-6            [-1, 256, 8, 8]               0
   ConvTranspose2d-7          [-1, 128, 16, 16]         524,288
       BatchNorm2d-8          [-1, 128, 16, 16]             256
              ReLU-9          [-1, 128, 16, 16]               0
  ConvTranspose2d-10            [-1, 1, 32, 32]           2,048
             Tanh-11            [-1, 1, 32, 32]               0
================================================================
Total params: 3,444,480
Trainable params: 3,444,480
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 1.33
Params size (MB): 13.14
Estimated Total Size (MB): 14.47
----------------------------------------------------------------
```

## 識別器
識別器の場合には通常は最後の層にSigmoid関数を使用しますが、
LSGANの場合にはSigmoid関数を使用しません
しかし、それ以外は通常のGANと変わりがありません
``` python
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__() 
        self.sequential1 = nn.Sequential(
            nn.Conv2d(1 , 64, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True)
        )      
        self.sequential2 = nn.Sequential(
            nn.Conv2d(64 , 128, 4, 2, 1, bias=False),
            nn.BatchNorm2d(128),
            nn.LeakyReLU(0.2, inplace=True)
        )   
        self.sequential3 = nn.Sequential(
            nn.Conv2d(128,256, 4, 2, 1, bias=False),
            nn.BatchNorm2d(256),
            nn.LeakyReLU(0.2, inplace=True)
        ) 
        self.sequential4 = nn.Sequential(
            nn.Conv2d(256, 1,  4, 1, 0, bias=False)
        ) 

    def forward(self, input):
        x = self.sequential1(input)
        x = self.sequential2(x)
        x = self.sequential3(x)
        x = self.sequential4(x)
        return x
```
``` 出力
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1           [-1, 64, 16, 16]           1,024
         LeakyReLU-2           [-1, 64, 16, 16]               0
            Conv2d-3            [-1, 128, 8, 8]         131,072
       BatchNorm2d-4            [-1, 128, 8, 8]             256
         LeakyReLU-5            [-1, 128, 8, 8]               0
            Conv2d-6            [-1, 256, 4, 4]         524,288
       BatchNorm2d-7            [-1, 256, 4, 4]             512
         LeakyReLU-8            [-1, 256, 4, 4]               0
            Conv2d-9              [-1, 1, 1, 1]           4,096
================================================================
Total params: 661,248
Trainable params: 661,248
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.53
Params size (MB): 2.52
Estimated Total Size (MB): 3.06
----------------------------------------------------------------
```

# 学習
大まかな学習の流れは以下の通りです.

流れ自体は以下の点を考慮した通常のGANの学習と変わりません
* 識別器→生成器の順で学習を行う
* 識別器の訓練時に偽物と本物はバッチ内で混ぜて学習を行わない。
(バッチ正規化層でまとめて正規化されてしまう恐れが存在するため)
* 生成器の損失関数を$min_G[log(1-D(G(z)))]$ではなく$max_G[log(D(G(z)))]$とする

```python
for epoch in range(num_epochs):
    for i, data in enumerate(dataloader, 0):
        real_cpu = data[0].to(device)
        
        b_size = real_cpu.size(0)
        real_label = torch.full((b_size,), 1, dtype=torch.float, device=device)
        fake_label = torch.full((b_size,), 0, dtype=torch.float, device=device)

        # 識別器学習
        discriminator.zero_grad()    
        
        ## 本物画像に対する損失の計算
        output = discriminator(real_cpu).view(-1)
        errD_real = criterion(output, real_label)
        
        ## 偽物画像に対する損失の計算
        noise = torch.randn(b_size, nz, 1, 1, device=device)
        fake = netG(noise)        
        output = discriminator(fake.detach()).view(-1)
        errD_fake = criterion(output, fake_label)

        ## 損失を用いて学習
        errD = errD_real + errD_fake
        errD.backward()
        optimizerD.step()

        # 生成器学習
        netG.zero_grad()

        ## 損失の計算
        output = discriminator(fake).view(-1)
        errG = criterion(output, fake_label)
        
        ## 損失を用いての学習
        errG.backward()
        optimizerG.step()     
```
変わる点は損失関数です
損失関数は通常のバイナリークロスエントロピーではなく平均二乗誤差を使用します
```python
# 通常は損失関数にバイナリークロスエントロピーを使用する
criterion = nn.BCELoss()

# LSGANでは損失関数に平均二乗誤差を使用する
criterion = nn.MSELoss()
```


# 参考サイト
* [GANディープラーニング実装ハンドブック](https://www.amazon.co.jp/)