CycleGANをpytorchで実装してみた

# TL;DL
CycleGANとして有名な，"Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks"をpytorchで実装してみました．
実装については下記Gitを確認してください．
https://github.com/DH-rgb/cycle-gan

論文タイトルにある"Unpaired Image-to-Image Translation"が示す通り，画像変換をペアデータの無いデータセットでも実現したという研究ですね．ドメイン間の1対1変換という制約があるとはいえ，ペアデータを作ることはかなり困難な分野であり，この論文がどれほど衝撃的かつ重要なモノかは想像に難くないですね（5295cites...~~ばけもの~~...）

有名な例は馬⇔シマウマの変換．これ，コンピュータビジョンに関わっている人で知らない人いないんじゃないですかね？

今回の記事ではCycleGANの凄いところを簡単にまとめながら，GANの実装方法を同時に勉強しました．所謂"The Devils in the Details"が多くて大変だった...


# 概要
GANの詳細についてはこの記事には書いていません．気になる方はQiitaで探すなり，原論文読むなりして調べてみてください．
ここでは，個人的に「CylceGAN凄いなぁ」と思った点とCycleGANの構造について書いていきます．
### 凄いところその1：Unparedデータで画像変換
この研究の目的は，二つの画像ドメインX,Yに対して，画像変換G:X->Yを行うモデルGを実現することです．
CycleGANの出現前にも同様に画像変換を行うネットワーク（pix2pix等）はありましたが，その多くがペアデータ，つまり画像変換前後のペア，が必要なモノでした．

CycleGANは画像変換タスクをペアデータなしで，つまり2ドメインの画像だったらなんでも良いという条件で実現しました．ここがこの論文の第一の貢献！![paired & unpaired](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682751/df104e0f-e0e8-df75-72ee-31434a368434.png)



### 凄いところその2：Cycle Consistentという新しいロス概念
CycleGANの貢献としてUnparedな画像変換の実現ばかりが注目されがちですが，個人的にはこのCycle Consistentという考え方がこの論文の重要なところだと思っています．

まずは背景から...

GANの出現は教師なし学習の敷居を下げることになりましたが，画像変換にGANをそのまま使うにはいくつかの問題点がありました．
一つ目が，ドメイン間の対応が正しく行われないこと．別のドメインに変換できたとしても，画像内容が全く変わってしまったら意味がありません．
二つ目が，複数画像が同じ画像に変換されてしまうこと．別のドメインに変換できたとしても，すべての画像が同じ画像に変換されては意味がありません．
これらの問題を解決するためには，GANのAdversarial Lossのほかに何か別の概念をロスとして表現する必要がありました．CycleGANではこの課題に対し，一度変換した画像を再度変換元のドメインに変換すること，つまりX→Y→Xというサイクルで画像を変換することを行い，サイクル後の画像が正確にもと画像を再現しているかを損失として考慮しています．
ここに込められたお気持ちとしては，「しっかりドメイン変換できるのなら，元に戻すこともできるでしょ？？」というあたりでしょうか．

これがCycle Consistency Loss，2ドメインを移動するサイクルにおいての一貫性を評価したロスの考え方です．（別解釈もあるかもしれません）

### CycleGAN
それではCycleGANの具体的な構造について見ていきます．

論文中から抜粋した構造図は以下のよう．「え，これだけ？」と思ってしまいそうですが，モデル自体とてもシンプルなのでこれだけでも十分．重要なのは，先にも述べたように，ドメイン間で行って帰ってくるサイクルがあることです（a,b,cすべてでそれが強調されていますね）![image of cycle gan](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682751/8d7cd8bb-22a4-63e2-dea5-4e13e5a91ab8.png)

損失関数には，通常のAdversarial LossにCycle Consistency Lossを加えたものとなります．式に書くと以下のよう．

$L(G,F,D_x, D_y)=L_{GAN}(G,D_{Y},X,Y) + L_{GAN}(G,D_{X},Y,X) + λL_{cyc}(G,F)$

Cycle Consistency Lossの式は以下のようです．単純に，元画像とサイクル後の復元画像のL1ノルムを取っている形ですね．λはAdversarial Lossに対する相対的な重みのようです．これが強いとより形状を保持しようとするようです．

$L_{cyc}(G,F) = E_{y∼pdata(y)}[||G(F(y))-y||]+E_{x∼pdata(x)}[||F(G(x))-x||]$


# 実装
これだけ強力なモデルであるのに関わらず，提案手法とインプリ記述部分がたった1.5ページしかないことにまず驚き．CycleGANがどれだけシンプルなモデルであるかをまざまざ見せつけられ感じがします（自分もシンプル＆つよつよネットワーク作りたい...）

Appendixにインプリの詳しいことが書かれているのも親切すぎる（こういう実装を細かく書くのは意外とめんどくさい）．
いろいろな面で神論文．

### 各種パラメータ
まずは細かいパラメータだけ列挙します．
（これを間違えると，再現実装が不可能．CNNのストライドとかは後述）

- λ=10 : cycle consistency lossの係数
- learning rate = 0.0002 (all model) : 200epochの学習の内，初め100epochは固定，その後100epochで0まで下げる（線形に）
- 重みはガウス分布に基づいて正規化（0,0.02)

## Generatorの構造
Reflection Paddingの位置に注意．備忘録参照．（論文中だとさも全てのpaddingがReflection Paddingのように書いている）．
generatorの最初と最終層だけpaddingサイズは3，他は1（discriminatorも）
太字は実装からしか分からない情報．


1. Reflection Padding size 3
2. CNN: 64 channel, 7x7 kernel, 1 stride
2. InstanceNorm
3. ReLU
4. CNN: 128 ch, 3x3 kernel, 2 stride, 1 padding, bias
5. InstanceNorm
6. ReLU
7. CNN: 256 ch, 3x3 kernel, 2 stride, 1 padding, bias
8. InstanceNorm
9. ReLU
10. Resnet Block x Block num(6 or 9)
    1. Reflection Padding size 1
    1. 256 ch, 3x3 kernel, **1 stride**(論文中に言及無し，変えてよいかも）
    2. **InstanceNorm**
    3. **ReLU**([https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix/blob/5fd11c27a1b9a5d79ef4ab879146c6b065f6e2a8/models/networks.py#L365](https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix/blob/5fd11c27a1b9a5d79ef4ab879146c6b065f6e2a8/models/networks.py#L365) から読み取った）
    4. Reflection Padding size 1
    4. 256 ch, 3x3 kernel, **1 stride**(論文中に言及無し，変えてよいかも）
    5. **InstanceNorm**
11. CNN: 128 ch, 3x3 kernel, 1/2 stride, 1 padding, bias
12. InstanceNorm
13. ReLU
14. CNN: 64 ch, 3x3 kernel, 1/2 stride, 1 padding, bias
15. InstanceNorm
16. ReLU
17. Reflection Padding size 3
17. CNN: 3 channel, 7x7 kernel, 1 stride
19. **Tanh**

# Discriminatorの構造
Leaky ReLUの負の部分の傾きは0.2

1. CNN: 64 channel, 4x4 kernel, 2 stride, 1 padding
2. leaky_ReLU
3. CNN: 128 channel, 4x4 kernel, 2 stride, 1 padding, bias
4. InstanceNorm
5. leaky_ReLU
6. CNN: 256 channel, 4x4 kernel, 2 stride, 1 padding, bias
7. InstanceNorm
8. leaky_ReLU
9. CNN: 512 channel, 4x4 kernel, ~~2 stride~~  **1 stride(になっている**）, 1 padding, bias
10. InstanceNorm
11. leaky_ReLU
12. **CNN: 1 channel, 4x4 kernel, stride 1**, 1 padding

さて疑問が一つだけ．PatchGANを用いると言っているのですが，いまいちどこで使っているのかが分からない．最終出力が一次元な辺りからしてこれは，標準的なAdversarial Lossなのでは？と思ったけど，どうやらこれでいいらしいようです（Githubのissueより）．256x256の画像入力の場合，最終的に30x30のマップが出力されました．
それと，Appendixだと全てのストライドが２となっていますが，実は最終層とその前だけは１だったりします（devils in details)

## Discriminator Loss
本来はDiscriminatorLossを最大化，つまり以下の式を最大化することが目的ですが，

$L_{GAN}(G,D_{Y},X,Y)=E_{y∼pdata(y)}[logD_Y(y)]+E_{x∼pdata(x)}[log(1−D_Y(G(x)))]$

実装上は基本的にロスを下げる方向に学習する必要があるため，上式を以下のように変えます.

$L_{GAN}(G,D_{Y},X,Y)=E_{y∼pdata(y)}[log(1-D_Y(y))]+E_{x∼pdata(x)}[logD_Y(G(x))]$

注意したいのは，実装上の式の場合Generatorはロスを最大化する方向に動くということ.
（この式を使ってminGを考えると，generatorはdiscriminatorが判別しやすいように嘘っぽい画像を生成するようになってしまう）

### Discriminatorの更新
論文では過去50の画像を利用してDiscriminatorを更新していると書いてありますが，具体的な実装については何一つ書かれていなかったので，コードを確認．

以下のようなものを発見．
https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix/blob/f5834b3ed339ec268f40cf56928234eed8dfeb92/util/image_pool.py#L5
とりあえずGeneratorで生成したものについてはランダムにバッファに格納していき，最大50の画像をfakeとしてDiscriminatorを学習するっぽい．バッファに格納する画像についてもランダムに選択している点に要注意．

##備忘録＆The Devils in the Details
実装時に困ったところを羅列していく（誰かの参考のために）．

- Adversarial loss計算のための1,0テンソル用意部分
[https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix/blob/f5834b3ed339ec268f40cf56928234eed8dfeb92/models/networks.py#L240-L275](https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix/blob/f5834b3ed339ec268f40cf56928234eed8dfeb92/models/networks.py#L240-L275)
- 論文中だとRefectionPadding使ってると書いてあるが，どこに使っているかは明記していない．実際にはGeneratorの最初の層と最後の層とresblockのみのよう.
- Conv層ではデフォルトのpaddingを用いている（padding=1）．
- Generator最終層はTanh

# 結果
画像はoriginal -> fake -> after cycleの順番．
![A_7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682751/2cfb7dca-91a0-8ac1-fb92-b8fb9d4d9fcc.png)
![A_9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682751/739f49a1-0609-75e7-d332-d8364f57d138.png)
![B_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682751/5b8e3de3-259e-800e-3a3e-ec814944f21f.png)
![B_16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682751/80d423e2-28b1-7036-7390-fcaaedebc349.png)

# Additional
趣味で集めているマイクラの建築物画像と，それと似ている（かなぁと勝手に思っている）画像をFlickrなどで集めて，マイクラ⇔実写変換できるか試してみました．
結果はこんな感じ．
実写画像変換
![実写](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682751/a89d2c8e-28f1-9546-b498-f73f2fa3a789.png)
マイクラ画像変換
![マイクラ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682751/7c3f6c1f-1e4d-f81c-be27-8b6cbe5fe1c7.png)

...微妙....
なんかいい感じにブロック感でたり，ブロック感なくなったりするかなぁと夢見てたけど実際は難しそう．CycleGANではConsistency Lossによって形状変化に対応できずテクスチャ変換のみになってしまう問題点が指摘されていて，ブロック感出すための形状変化はやっぱりできないみたいです（猫犬変換が上手くできないのはこの理由）．ただ，マイクラ画像を実写風に変換したときに，色合いや影はいい感じに実写感出ていると個人的には感じました．

係数の調整とかでもう少しましになるかもだけど，そもそもデータセットのクリーニングも済んでないのでそこからか...(いつかまでの宿題）

# Todo
就活で時間がとられてしまった...
今年のCVPRを見ていたら，3次元オブジェクトを微分レンダラー等で2次元に落とし込んで学習させているものが多くみられるので，
CycleGANに組み合わせてオブジェクトのテクスチャ変換ができないか試してみたいです．