GAN勉強用メモ　第一回

# はじめに
<!-- 発端や概要を記載 -->
実践GAN　敵対的生成ネットワークによる深層学習(マイナビ出版)を読んで勉強した備忘録(身内むけ)
本文のコードは以下の「公式コード」のgitからです
- [実践GAN(英語版)](https://www.manning.com/books/gans-in-action/)
- [実践GAN(日本語版)](https://book.mynavi.jp/ec/products/detail/id=113324)
- [公式コード](https://github.com/GANs-in-Action/gans-in-action)


<!-- 各チャプター -->
<a id="#Chapter1"></a>
# そもそもGANとは
GAN=Generative Adversarial Networks
日本語でいうところの敵対的生成ネットワーク．

生成器と識別器の2つのネットワークからなるモデル．
(図説などは調べればいくらでもでてくるので適宜Google先生に)

生成器は新しくデータを作り出す．もちろん訓練データをもとに新しく生成する．
りんごの画像を生成したいならりんごの画像で訓練させるなど，元となる訓練データによって変わる．

識別器は，本物のデータと偽物のデータを見分ける．
訓練データに含まれる本物のデータと，生成器が作り出した偽物のデータを見分ける，いわば「鑑定士」の役割

この２つのネットワークが競合して相手を互いに出し抜こうとするため，敵対的生成ネットワークという名前がつけられている．


<a id="#Chapter2"></a>
# 何がすごいのか
機械が人間のように振る舞い，考えることができるのかという問いは昔からある．
チューリングテストなどが最たる例．
チューリングテストと同じようにSiriやアレクサちゃんと会話をしてみると，
コンピュータがまだまだ人間のように振る舞うことは不可能だと思われる．

しかし，他のタスクだったらコンピュータが人間を追い越している．
顔認識や囲碁などではすでに人間より上と言えるだろう．
機械学習はデータの中からパターンを見つけるような分類タスク，回帰タスクに関して特に優れている
一方で，何かを新しく生成するようなタスクに関しては苦手である

ところがGANはこれまでコンピュータが苦手であった新しいものを生成するタスクを解消した．
GANは2014年に開発され，2つの分離したニューラルネットワークを使用することで，リアルなデータ生成を可能とした．
現在では高解像度の人間の顔を生成することも可能．
走る馬をしまうまに変換したりなど，人工のシステムでは不可能だと思われていたタスクを可能にした．
以上が簡単なGANの歴史．


<a id="#Chapter3"></a>
# 生成器と識別器

生成器
目的：訓練データとの要素と見分けがつかないくらいの偽のデータを生成する
入力：乱数からなるベクトル
出力：できるだけ本物に見えるように作成した偽のサンプル
訓練と改善：生成器の重みとバイアスは，識別器が偽のサンプルx’を本物の入力と間違う確率を最大化するように更新する
詳細な訓練ステップ：
１新しいベクトルzを生成し，生成器ネットワークを用いて偽のサンプルx'を生成
２識別器ネットワークを用いてx'が本物であるかを推定
３分類誤差を計算し，バックプロパゲーションで生成器ネットワークの訓練可能なパラメータを更新，識別器の誤差を最大化



識別器
目的：生成器からくる偽のデータと，訓練データからくる本物のデータを見分けること
入力：訓練データからくる本物のサンプルと，生成が作成した偽物のサンプルの2つ
出力入力サンプルが本物である推定確率
訓練と改善：識別器の重みとバイアスは，識別性能を最大化するように更新．つまり訓練データのxを本物，偽のサンプルx'を偽物であると判断する確率を最大化するように更新すること．
詳細な訓練ステップ：
１訓練データからランダムに本物のサンプルXを取り出す
２新しい乱数ベクトルzを生成し，生成器ネットワークを用いて偽のサンプルx'を生成
３識別器ネットワークを使ってxとx’を分類
４分類誤差を計算し，バックプロパゲーションで識別器の訓練可能なパラメータを更新，分類誤差を最小化


<a id="#Chapter4"></a>
# 実装（手書き文字の出力）
チュートリアルとしてmnistのような手書き文字風の画像を生成してみる．
環境はColab．
訓練データはもちろんmnistをつかいます．
テキストの第3章にあたる部分(P55から)．

```python
%matplotlib inline

import matplotlib.pyplot as plt
import numpy as np

from keras.datasets import mnist
from keras.layers import Dense, Flatten, Reshape
from keras.layers.advanced_activations import LeakyReLU
from keras.models import Sequential
from tensorflow.keras.optimizers import Adam
```

Adamのインポートはテキストにあるようにfrom keras.optimizers import Adamだとエラー．
tensorflowから指定してあげないとだめ．


ここから中身の実装部分．
生成器も識別器も2層からなるネットワーク．
訓練中は生成器も識別器も，自分自身のパラメータのみ更新する．

生成器は，識別器の偽陽性(FP)を最大化するように訓練する．
識別器は，偽陽性(FP)と偽陰性(FN)を最小化するように訓練する．
説明したいことは全てコメントに書いてあるのでそちらを参考に．
テキストはコード部分にいくつか誤植もあるので注意．

```python
# モデルとデータセットの入力次元を指定する
# MNISTに含まれる各画像の解像度は28*28
# 各ピクセルはグレースケールの1チャンネルのみもつ
img_rows =28
img_cols =28
channels = 1

# 入力画像の次元
img_shape = (img_rows, img_cols, channels)

#生成器の入力として使われるノイズベクトルの次元 
z_dim = 100

# ここから生成器の実装部分
# 中間層は1つとした．活性化関数はLeakyReLUを使用→必ず正の勾配を返す関数（勾配消失を防ぐ）
# zが入力で，28*28*1の画像を生成(出力)する
# 出力層ではtanh関数を使用→出力を[-1,1]の範囲に収められる，[0,1]で返すシグモイドよりはっきりとした画像が得られる(らしい)
def build_generator(img_shape, z_dim):
  # SequentialでどんなNNかを定義する．addで層を追加したりできる
  model = Sequential()

  # 全結合層なのでDense．input_dim=ノード数，ノード数は入力データの次元数と一致
  model.add(Dense(128, input_dim=z_dim))

  # LeakyLeRUによる活性化(関数への入力値が0よりしたの場合には出力値が入力値をalpha倍した値とする)
  model.add(LeakyReLU(alpha=0.01)) 
  
  # tanh関数を使った出力層を定義．出力の次元数は28*28*1（入力画像と同じ）
  model.add(Dense(28*28*1, activation='tanh'))

  # 生成器の出力が画像サイズになるようにreshapeする
  model.add(Reshape(img_shape))

  return model


# ここから識別器の実装部分
# 入力は28*28*1の画像で，出力は入力が本物である確率
# ここでは2層のNNとして，中間層には128個のユニットが存在し，ReLUを使用する
# 確率を出力するので出力層にはシグモイド関数を使用
def build_discriminator(img_shape):
  model = Sequential()

  # 入力画像を1列に並べる(1次元配列にする)
  model.add(Flatten(input_shape=img_shape))

  # 全結合層なのでDense
  model.add(Dense(128))

  # LeakyReLUによる活性化(詳しくは生成器の説明に)
  model.add(LeakyReLU(alpha=0.01))

  # 出力にシグモイドをかませる
  model.add(Dense(1, activation='sigmoid'))

  return model


# ここから生成器と識別器を生成してコンパイルして，モデルを作成する
# 生成器に訓練させるときはdiscriminator.trainableをFalseにして識別器のパラメータを固定して更新されないようにする
# 識別器を訓練させるときは別にコンパイルされたモデルによって訓練させる
# 使用する損失関数は二値交差エントロピを使用
def build_gan(generator, discriminator):

  model = Sequential()

  # 生成器と識別器の統合
  model.add(generator)
  model.add(discriminator)

  return model


# 識別器の構築とコンパイル
# compileメソッドは(損失関数，オプティマイザー名，metrics:訓練時とテスト時にモデルにより評価される評価関数のリスト)
# 参考：https://keras.io/ja/models/model/
discriminator = build_discriminator(img_shape)
discriminator.compile(loss='binary_crossentropy', optimizer=Adam(), metrics=['accuracy'])

# 生成器の構築
generator = build_generator(img_shape, z_dim)

# 生成器の構築中は識別器のパラメータを固定
discriminator.trainable = False

gan = build_gan(generator, discriminator)
gan.compile(loss='binary_crossentropy', optimizer=Adam())


# ここからGANの訓練ループの実装
# MNIST画像からランダムに取り出したミニバッチが本物のサンプル
# ノイズベクトルからなるミニバッチが偽のサンプル
losses = []
accuracies = []
iteration_checkpoints = []

def train(iterations, batch_size, sample_interval):
  # MNISTデータセットの読み込み
  (X_train, _), (_, _) = mnist.load_data()

  # [0, 255]の範囲のグレースケール画素値を[-1, 1]にスケーリング
  X_train = X_train / 127.5 - 1.0
  X_train = np.expand_dims(X_train, axis=3)

  # 本物の画像のラベルは全て1とする
  real = np.ones((batch_size, 1))

  # 偽の画像のラベルは全て0とする
  fake = np.zeros((batch_size, 1))

  for iteration in range(iterations):
    # 識別器の訓練
    # 本物の画像をランダムに切り出したバッチを作成(0 * X_train.shape[0] * batch_sizeの配列の乱数を生成)
    idx = np.random.randint(0, X_train.shape[0], batch_size)
    imgs = X_train[idx]

    # 偽の画像のバッチを作成(正規分布からzを生成)
    z = np.random.normal(0, 1, (batch_size, 100))
    gen_imgs = generator.predict(z)

    # 識別器の訓練
    d_loss_real = discriminator.train_on_batch(imgs, real)
    d_loss_fake = discriminator.train_on_batch(gen_imgs, fake)
    d_loss, accuracy = 0.5 * np.add(d_loss_real, d_loss_fake)

    # 生成器の訓練
    # 偽の画像のバッチを生成する
    z = np.random.normal(0, 1, (batch_size, 100))
    gen_imgs = generator.predict(z)

    # 生成器の訓練
    g_loss = gan.train_on_batch(z, real)

    if (iteration + 1) % sample_interval ==0:

      # 訓練終了後に図時するために，損失と精度をセーブしておく
      losses.append((d_loss, g_loss))
      accuracies.append(100.0 * accuracy)
      iteration_checkpoints.append(iteration + 1)

      # 訓練の進捗を出力する
      print("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (iteration + 1, d_loss, 100.0 * accuracy, g_loss))

      # 生成された画像のサンプルを出力する
      # sample_intervalおきに呼ばれてその時点で生成された4*4枚の画像を出力
      sample_images(generator)


# 生成された画像の表示部分
def sample_images(generator, image_grid_rows=4, image_grid_columns=4):

  # ランダムノイズのサンプリング
  z = np.random.normal(0, 1, (image_grid_rows * image_grid_columns, z_dim))

  # ランダムノイズを使って画像を生成
  gen_imgs = generator.predict(z)

  # 画像の画素値を[0, 1]の範囲にスケールする
  gen_imgs = 0.5 * gen_imgs + 0.5

  # 画像をグリッドに並べる
  fig, axs = plt.subplots(image_grid_rows, image_grid_columns, figsize=(4, 4), sharey=True, sharex=True)

  cnt = 0
  for i in range(image_grid_rows):
    for j in range(image_grid_columns):
      # 並べた画像を出力する
      axs[i, j].imshow(gen_imgs[cnt, :, :, 0], cmap='gray')
      axs[i, j].axis('off')
      cnt += 1


# モデルの実行部分
# ミニバッチの数などハイパーパラメータは適宜変更
iterations = 20000
batch_size = 128
sample_interval = 1000

# 設定した反復回数だけGANの訓練を実行
train(iterations, batch_size, sample_interval)
```

学習時間は結構かかった．
結果の画像の最初の方がこちら
![実行後その1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1268861/65016174-025c-9e82-8a1b-05f71812fc97.png)

最後の方がこちら
![実行後その2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1268861/27afa6f0-681e-4574-6a53-318d350e442c.png)

最初はランダムノイズっぽい感じだったが，最後の方はなんとなくそれっぽくなっている．

<a id="#Chapter4"></a>
# まとめ
今回は生成器も識別器も2層からなる単純な構造のGANを実装した．
次回はDCGANに続く(予定)



<a id="#reference"></a>
# 参考文献
- [使用したテキスト](https://book.mynavi.jp/ec/products/detail/id=113324)
- [オートエンコーダ](https://jp.mathworks.com/discovery/autoencoder.html)
- [公式コード](https://github.com/GANs-in-Action/gans-in-action)