初心者でもGAN(敵対的生成ネットワーク)を使えるか？〜英単語判定機２〜

###はじめに
Google Colaboratoryとkerasを使って初歩的な機械学習をやってみた記録です。
筆者自身が機械学習の初心者なので、ところどころ間違いがあるかもしれないのでご了承ください。指摘等大歓迎です。

今回は、一世を風靡したGANと呼ばれる敵対的生成ネットワークを、自分で用意したデータセットに対して実際に行ってみることで、これがどのようなものかを学んでいきます。

####想定する読者像
GANを用いた機械学習に興味があるが、実装の経験がない人。
####前提
[以前の記事](https://qiita.com/kagamidokei/items/cf595854099613e9e950)の続編なので、前回の内容を知っていること。

####環境
動作環境:Google Colaboratory
OS:Windows10

####今回の目標
判定を行う機械学習モデルが実装可能な課題について、その判定器に対する生成モデルを実装できるようになること。

####やること
Kerasを用いて[前回](https://qiita.com/kagamidokei/items/cf595854099613e9e950)実装した英単語判定器を応用して、GANによる英単語生成器を実装すること。

***
###GANとは？
GANとはGenerative Adversarial Network（敵対的生成ネットワーク）の略で、「生成」の名の通り、訓練データをもとに、それらと同様の特徴をもつ新たなデータを生成することができる機械学習モデルの一種です。利用例としては、たくさんの顔写真画像を学習させることで、現実に存在しない人間の顔写真画像を新たに生成する、というような技術が知られています。
今回はGANを用いて英単語にありそうな５文字のアルファベット列「英単語もどき」の実装を目指します。  
  
<details><summary>GANの考え方とは？（初学者向け）</summary><div>
　　
GANの詳細で正確な説明は他の優秀な方々の記事に譲るとして、ここでは考え方を簡単に、顔画像生成を例に解説します。  
まず任意のランダムなベクトルを変換して画像を生成する「生成器」と画像が顔写真かどうか判断する「判定器」をそれぞれ用意します。といっても、最初はそれぞれランダムなパラメタで初期化されているので、正しい生成や判定はできません。
そこで、まず判定器に「本物の顔写真」と「生成器が生成した偽画像」をラベル付きでそれぞれいくつか与えます。これを学習することで「判定器」は顔写真とランダム画像を少し区別できるようになります。
次に生成器に画像を生成させ、その画像を判定器に入力した時、例えばそれが0.6の確率で「生成器が生成した偽画像」だと判定したのであれば、この0.6を誤差として生成器のパラメタを更新します。こうすることで、少しでも判定器を騙すという方向に学習が進むことになります。
これをひたすら繰り返すと、判定器は生成器が生成した偽画像を見破れるように、逆に生成器は判定器を騙せるようにまるで切磋琢磨してお互いを高め合うライバル同士のように学習がすすんでいくことになります。こうすることで最終的に、本物の顔写真と見紛うような画像を生成できる生成器が得られる、というわけです。

</div></details>

###実装方針
「生成器と判定器を敵対的に学習させるネットワークの実装」と聞くと、特に私のような初心者は「また何か難しいライブラリをインストールしてこなければならないのか」などと考えてしまうものですが、実はその必要はありません。
前回と同じようにKerasを使い、ただそのモデルの構成において少し工夫を加えるだけで、GANは簡単に実装することができます。（今回実際にやってみて最も驚いたのはここでした。）

ひとまず下準備として、前回と全く同じ流れで判定器（＝前回のmodelと同じ）に入力するための訓練データの生成と諸関数の用意を行います。
この部分は前回既に扱っているので、説明は省略しコードのみを載せていきます。説明は適宜前回の記事を参照してください。

太字で注意を書いている部分のみ前回から変更を加えているので、前回のコードを流用する場合は必ず参照してください。

###下準備（概ね前回の繰り返し）
コードのみ掲載します。詳細な説明は前回記事を参照してください。


```Python:GoogleDriveをインポート
from google.colab import drive
drive.mount('/content/drive')
```
```Python:データの読み込み
base='drive/MyDrive/ML/train/'#train.txtを配置したフォルダを指定します。
f = open(base+'train.txt', 'r')
rawdata = f.read()
data=rawdata.split()
print(data[:5])
f.close()
```
```Python:アルファベットと数値の変換
char="abcdefghijklmnopqrstuvwxyz"
dic={}
for i in range(len(char)):
  dic[char[i]]=i
print(dic['c'])#cは3番目なので、2が出力されるはず。（リストの番号は0,1,2...なので）
```
```Python:「存在する英単語」データの作成
import re
def correct_generate(rawdata,data_size=10000,length=5):#lengthは生成する単語の長さです。今回は５文字に設定しています。
  alpha_data=[]
  for word in rawdata:
    word=word.lower()
    res = re.sub(r'[^a-zA-Z]', '', word)#正規表現を使って、数字や記号などアルファベット以外を''に置換（つまり消去）している
    if len(res)==length:
      alpha_data.append(res)#長さ５文字の単語だけを集めています
  set_alpha=list(set(alpha_data))#setを使って重複を取り除き、listに戻します

  beta_data=[]
  for word in set_alpha:
    num=[]
    for id in range(len(word)):
      c=word[id]
      num.append(dic[c])
    beta_data.append(num)#単語に対応する数字を入れていきます。例:"apple"->[0,15,15,11,4]
  size=len(beta_data)
  return set_alpha, beta_data, size

_,beta,size=correct_generate(data)
print(size)
print(beta[:5])

```

**注意：前回と異なり、「ランダムな文字列」は生成器が生成してくれるので、その作成は不要。**
**x_trainにも、正しい英単語のデータのみ入れておけばよい。**

```Python:訓練データの元を作成
x_train_rawlist=beta
y_train_list=[]
for i in range(len(beta)):
  y_train_list.append(1)
print(x_train_rawlist[0])
```

```Python:one_hotエンコーディング
import numpy as np
x_train_list=[]
for data in x_train_rawlist:
  item=[]
  for num in data:
    one_hot=np.zeros(26)#26次元零ベクトルの生成
    one_hot[num]=1
    one_hot_list=one_hot.tolist()
    item.append(one_hot_list)
  x_train_list.append(item)
```

```Python:listをnumpyに
x_train=np.array(x_train_list)
y_train=np.array(y_train_list)
print(x_train.shape)
print(y_train.shape)
```

```Python:データの確認
def nothing_val(word):
  for w in alpha:
    if word==w:
      return False
  return True
nothing_val('beast')
```

```Python:単語とベクトルの変換
def word2num(word):
  num=[]
  for id in range(len(word)):
    num.append(dic[word[id]])
  return num
def num2word(nums):
  word=''
  for i in nums:
    word+=char[i]
  return word
print(num2word([1,4, 0, 18, 19]))
print(word2num("beast"))
```


###簡易的テストの準備

判定器の学習の進捗をリアルタイムで確かめるため、簡易的なテストデータを作ります。
訓練データに含めていない英単語である"beast","blade","joker"と、ランダムな文字列"kdjus","adjsa","flakq"
をそれぞれ判定器に入力できるone_hotエンコーディングしたベクトルの形で用意しておきます。
前回と異なるポイントとして、y_testもone_hotエンコーディングした形で用意する必要があることに注意してください。

```Python:テストデータ
test_words=["beast","blade","joker","kdjus","adjsa","flakq"]
y_test=[[0,1],[0,1],[0,1],[1,0],[1,0],[1,0]]
x_test=[]
for word in test_words:
  vec=word2num(word)
  item=[]
  for num in vec:
    data=np.zeros(26,dtype=int)
    data[num]=1
    data_list=data.tolist()
    item.append(data_list)
  x_test.append(item)
```

###GANモデルの実装

ここからついにGANモデルの実装に入っていきます。
まずはライブラリの準備です。

```Python:ライブラリの準備
import numpy as np
import math
from keras.datasets import mnist
from keras.layers import Dense, Flatten, Reshape
from keras.models import Sequential
import tensorflow as tf
from tensorflow.keras.models import Sequential
```
続いて生成器、判定器のモデルを作っていきます。各引数は以下の通りです。
shape:判定器に入力するデータのshape
all:判定器に入力するデータ１つあたりのパラメタの総数
dim:生成器に入力する初期ベクトルの次元

例えば40×30の大きさ画像生成なら、shape=(40,30) all=1200です。
dimはとりあえず適当に100とかで問題ありません。

```Python:生成器のモデル
def create_generator(shape,all,dim):
  model = Sequential()
  model.add(Dense(128, input_dim=dim))
  model.add(tf.keras.layers.Dropout(0.1))
  model.add(Dense(128, input_dim=dim))
  model.add(tf.keras.layers.Dropout(0.1))
  model.add(Dense(128, input_dim=dim))
  model.add(tf.keras.layers.Dropout(0.1))
  model.add(Dense(128, input_dim=dim))
  model.add(tf.keras.layers.Dropout(0.1))
  model.add(Dense(all, activation='tanh'))
  model.add(Reshape(shape))
  return model
```

```Python:判定器のモデル
def create_discriminatior(shape):
  model = Sequential()
  model.add(Flatten(input_shape=shape))
  model.add(tf.keras.layers.Dense(128, activation='relu'))
  model.add(tf.keras.layers.Dropout(0.2))
  model.add(tf.keras.layers.Dense(128, activation='relu'))
  model.add(tf.keras.layers.Dropout(0.2))
  model.add(tf.keras.layers.Dense(128, activation='relu'))
  model.add(tf.keras.layers.Dropout(0.2))
  model.add(Dense(2, activation='sigmoid'))
  return model
```

生成器、判定器ともに、最後の出力層さえ合っていれば層の数や各中間層のニューロン数などは基本的に適当で構いません。
しかし一つだけ大きな注意点があります。
それは、**生成器と判定器で、大体似たような層の数、ニューロンの数になるようにする**ということです。
これはどういうことかというと、例えば生成器と判定器のどちらか片一方、例えば判定器の学習が先に進みすぎると、もう一方の生成器が一向に判定器を騙すことができず、学習が進まなくなってしまうという問題が起きるからです。（筆者はこれで２時間悩みました）

この生成器と判定器の学習バランスを学習時に確認する方法については後で述べますが、とりあえずこのことを気に留めておくようにしましょう。

続いて、生成器と判定器を「くっつけた」モデル（以下統合モデル）を定義します。
この統合モデルにランダムな入力を与えると、それは生成器に入力され、生成物（例えば写真っぽい画像、今回だと英単語もどき）が出力されます。さらにその生成物がそのまま判定器に入力され、今度はその生成物が「本物である確率」「生成器の偽者である確率」をそれぞれ出力します。つまり、ランダムな入力から一気に「判定器の判断結果」が得られるのです。そしてそれは判定器に見破られた割合つまり、生成器にとっての「誤差」でもあるのです。この誤差を用いてこの統合モデルを更新します。こうすることで、「判定器の出力に基づいて生成器を更新する」ことができるのです。

しかし一つ疑問が残ります。それだと生成器と一緒に「判定器」も更新されてしまいそうです。これは困ります。生成器にとっての誤差は「見破られた割合」、判定器にとっての誤差は「騙された割合」で、それぞれ違うもの（というか真逆のもの）だからです。そこでこの統合モデルの更新の際には、「判定器の方は更新しないでね」という設定をあらかじめ行っておく必要があります。

```Python:GANのモデル
def create_gan(generator, discriminator):
  model = Sequential()
  model.add(generator)
  model.add(discriminator)
  return model
```

###学習

今回のメインである学習です。少し長いコードになりますが落ち着いて進めましょう。
trainの各引数は以下の通りです。
x_train:訓練データ（正しい英単語のみをベクトルに変換したデータの集合）
shape:判定器に入力するデータのshape
all:判定器に入力するデータ１つあたりのパラメタの総数
dim:生成器に入力する初期ベクトルの次元
optimizer:最適化手法。"adam","sgd"など文字列で指定すればよい。
batch_size:生成器、判定器それぞれが学習を行うターンを交互に繰り返す、その１ターン分で学習に使うデータの数。
sample_interval:何ターンごとに学習の経過状況を出力するか
iterations:学習を行う総ターン数

注意すべき点は、先にも述べた通り統合モデルについては判定器のパラメタを更新しないように設定しなければならない点です。これは、discriminator.trainable=Falseのようにして行います。
モデルをコンパイルする時点でdiscriminator.trainableが何に設定されているかでパラメタ更新の可否が決まるので、判定器discriminatorのコンパイル時にはTrueに、統合モデルであるganのコンパイル時にはFalseにします。



```Python:ライブラリの準備
def train(x_train,shape ,all , dim, optimizer, batch_size, sample_interval,iterations ):
#学習の記録用
  loss_m=[]
  acc_m=[]
#各モデルの生成とコンパイル
  discriminator = create_discriminatior(shape)
  discriminator.trainable=True
  discriminator.compile(loss='binary_crossentropy', optimizer=optimizer, metrics=['accuracy']) 
  discriminator.trainable=False
  generator = create_generator(shape,all, dim)
  gan = create_gan(generator, discriminator)
  gan.compile(loss='hinge', optimizer=optimizer,metrics=['accuracy'])
  x_train = np.expand_dims(x_train, axis=3)
#ラベルの作成
  real=[]
  fake=[]
  for i in range(batch_size):
    real.append([0,1])
    fake.append([1,0])
  real=np.array(real)
  fake=np.array(fake)
#学習
  for iteration in range(iterations):
#訓練データをランダムに取り出す
    idx = np.random.randint(0, x_train.shape[0], batch_size)
    words = x_train[idx]
#生成器に入力するランダムなベクトルz
    z = np.random.normal(0, 1, (batch_size, dim))
    gen_words = generator.predict(z)

    d_loss_f = discriminator.train_on_batch(gen_words, fake)
    d_loss_r = discriminator.train_on_batch(words, real)
#誤差と正解率の全体平均を取る
    d_loss, acc = 0.5 * np.add(d_loss_r, d_loss_f)
    z = np.random.normal(0, 1, (batch_size, 100))
#g_lossは生成器の学習（生成器としては判定器に本物と判定されるのが正解なので、ラベルはreal）
    g_loss = gan.train_on_batch(z, real)

#sample_intervalごとに損失値と精度を保存
    if (iteration+1) % sample_interval == 0:
      print(f"iteration:{iteration+1}")
      print(f"loss:{d_loss},{g_loss[1]} accuracy:{acc}")
      loss_m.append((d_loss, g_loss[1]))
      acc_m.append(acc)
      test_loss, test_acc = discriminator.evaluate(x_test, y_test, verbose=0)
      print(f"testloss:{test_loss}")
      predictions = discriminator.predict(x_test)
      #print(predictions)  #test_lossが減らないときはコメントアウトを外して状況を見る

#セーブ
  generator.save('dcgan_generator.h5')
  discriminator.save('dcgan_discriminator.h5')
  gan.save('dcgan.h5')
  return generator,discriminator,gan,loss_m,acc_m
generator,discriminator,gan,loss_m,acc_m=train(x_train,(5,26),130,100,'adam',batch_size=1000, sample_interval=10,iterations=1000)
```

この学習には1000ターンで約５分程度の時間がかかります。コーヒーでも飲んで待っていてください。
学習はうまく行ったでしょうか。
先に述べた学習のバランスを確認する上で重要なのがg_lossとd_lossの値です。これが両者とも似たような値（オーダーがあっていればまあOK？）になっていれば、バランスよく学習が進んでいることになります。
なお、コメントアウトを外してprint(prediction)を見た時に、すべての単語に対して0.9のような高い確率で本物だと判定する、というような状況がずっと続いて変化しない場合も、学習バランスがおかしい可能性が高いです。

testlossがある程度（0.01くらい？）まで小さくなっていて、なおかつg_lossとd_lossがバランスしていれば、学習はうまく行っていると判断してよいのではないでしょうか。

あとは実際に学習済み生成器モデルを動かすだけです！

###学習済み英単語もどき生成器の利用

生成器モデルを使う前に、生成器の出力をアルファベット列に変換する関数を用意します。
「num2wordじゃだめなのか」と思うかもしれませんが、これは入力が少し違います。

```Python:生成器の出力をアルファベット列に変換
def convert(vectors):
  res=""
  for vec in vectors:
    res+=char[np.argmax(np.array(vec))]
  return res
```

```:num2wordの入力例
[1,4,0,18,19]
```

```:convertの入力例（生成器の出力例）
[[0.05, 0.4,...,0.02,0.04],
 [ 0.1,0.02,...,0.01,0.01],
 [ 0.3,0.01,...,0.03,0.03],
 [ 0.1,0.05,...,0.05,0.02],
 [0.07, 0.1,...,0.04,0.01]]

```


では実際に生成器に30個ほどランダムなベクトルを入れて、生成される単語を見てみましょう。

```Python:生成器の利用
def generation(size=30,dim=100):
  z = np.random.normal(0, 1, (size, dim))
  gen_words = generator.predict(z)
  prediction = discriminator.predict(gen_words)
  for i in range(size):
    print(convert(gen_words[i]))
generation()
```


```:出力
deems
beioy
loien
craoy
buncn
drfes
foule
telie
pians
berms
beehe
duecr
beneg
bueen
beres
lrlls
taiet
tauze
auiat
lousx
fnaoy
beeen
cailt
benen
burhs
pjeas
foatt
sagnt
dreus
anleg
```

結果はどうでしょうか？これを「うまく行った！」と捉えるかどうかは人によるとは思いますが、ひとまず敵対的生成ネットワークを用いて英単語もどき生成器を実装することができました。
前回の記事で最後に作成した、「数撃ちゃ当たる」的な発想の英単語もどき生成器と比較しても、少なくともかなり高速に英単語もどきを生成できるようになったのではないかと思います。
同じような発想でいろいろなGANを用いたモデルが作れると思いますので、ぜひ作ってみてください。
↓コードが置いてあります
https://colab.research.google.com/github/mirrormouse/machine_learning/blob/main/GAN_english_words.ipynb