はじめに
Google Colaboratoryとkerasを使って初歩的な機械学習をやってみた記録です。
筆者自身が機械学習の初心者なので、ところどころ間違いがあるかもしれないのでご了承ください。指摘等大歓迎です。
今回は、一世を風靡したGANと呼ばれる敵対的生成ネットワークを、自分で用意したデータセットに対して実際に行ってみることで、これがどのようなものかを学んでいきます。
想定する読者像
GANを用いた機械学習に興味があるが、実装の経験がない人。
前提
以前の記事の続編なので、前回の内容を知っていること。
環境
動作環境:Google Colaboratory
OS:Windows10
今回の目標
判定を行う機械学習モデルが実装可能な課題について、その判定器に対する生成モデルを実装できるようになること。
やること
Kerasを用いて前回実装した英単語判定器を応用して、GANによる英単語生成器を実装すること。
GANとは?
GANとはGenerative Adversarial Network(敵対的生成ネットワーク)の略で、「生成」の名の通り、訓練データをもとに、それらと同様の特徴をもつ新たなデータを生成することができる機械学習モデルの一種です。利用例としては、たくさんの顔写真画像を学習させることで、現実に存在しない人間の顔写真画像を新たに生成する、というような技術が知られています。
今回はGANを用いて英単語にありそうな5文字のアルファベット列「英単語もどき」の実装を目指します。
GANの考え方とは?(初学者向け)
実装方針
「生成器と判定器を敵対的に学習させるネットワークの実装」と聞くと、特に私のような初心者は「また何か難しいライブラリをインストールしてこなければならないのか」などと考えてしまうものですが、実はその必要はありません。
前回と同じようにKerasを使い、ただそのモデルの構成において少し工夫を加えるだけで、GANは簡単に実装することができます。(今回実際にやってみて最も驚いたのはここでした。)
ひとまず下準備として、前回と全く同じ流れで判定器(=前回のmodelと同じ)に入力するための訓練データの生成と諸関数の用意を行います。
この部分は前回既に扱っているので、説明は省略しコードのみを載せていきます。説明は適宜前回の記事を参照してください。
太字で注意を書いている部分のみ前回から変更を加えているので、前回のコードを流用する場合は必ず参照してください。
下準備(概ね前回の繰り返し)
コードのみ掲載します。詳細な説明は前回記事を参照してください。
from google.colab import drive
drive.mount('/content/drive')
base='drive/MyDrive/ML/train/'#train.txtを配置したフォルダを指定します。
f = open(base+'train.txt', 'r')
rawdata = f.read()
data=rawdata.split()
print(data[:5])
f.close()
char="abcdefghijklmnopqrstuvwxyz"
dic={}
for i in range(len(char)):
dic[char[i]]=i
print(dic['c'])#cは3番目なので、2が出力されるはず。(リストの番号は0,1,2...なので)
import re
def correct_generate(rawdata,data_size=10000,length=5):#lengthは生成する単語の長さです。今回は5文字に設定しています。
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)#長さ5文字の単語だけを集めています
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にも、正しい英単語のデータのみ入れておけばよい。
x_train_rawlist=beta
y_train_list=[]
for i in range(len(beta)):
y_train_list.append(1)
print(x_train_rawlist[0])
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)
x_train=np.array(x_train_list)
y_train=np.array(y_train_list)
print(x_train.shape)
print(y_train.shape)
def nothing_val(word):
for w in alpha:
if word==w:
return False
return True
nothing_val('beast')
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エンコーディングした形で用意する必要があることに注意してください。
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モデルの実装に入っていきます。
まずはライブラリの準備です。
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:判定器に入力するデータ1つあたりのパラメタの総数
dim:生成器に入力する初期ベクトルの次元
例えば40×30の大きさ画像生成なら、shape=(40,30) all=1200です。
dimはとりあえず適当に100とかで問題ありません。
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
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
生成器、判定器ともに、最後の出力層さえ合っていれば層の数や各中間層のニューロン数などは基本的に適当で構いません。
しかし一つだけ大きな注意点があります。
それは、生成器と判定器で、大体似たような層の数、ニューロンの数になるようにするということです。
これはどういうことかというと、例えば生成器と判定器のどちらか片一方、例えば判定器の学習が先に進みすぎると、もう一方の生成器が一向に判定器を騙すことができず、学習が進まなくなってしまうという問題が起きるからです。(筆者はこれで2時間悩みました)
この生成器と判定器の学習バランスを学習時に確認する方法については後で述べますが、とりあえずこのことを気に留めておくようにしましょう。
続いて、生成器と判定器を「くっつけた」モデル(以下統合モデル)を定義します。
この統合モデルにランダムな入力を与えると、それは生成器に入力され、生成物(例えば写真っぽい画像、今回だと英単語もどき)が出力されます。さらにその生成物がそのまま判定器に入力され、今度はその生成物が「本物である確率」「生成器の偽者である確率」をそれぞれ出力します。つまり、ランダムな入力から一気に「判定器の判断結果」が得られるのです。そしてそれは判定器に見破られた割合つまり、生成器にとっての「誤差」でもあるのです。この誤差を用いてこの統合モデルを更新します。こうすることで、「判定器の出力に基づいて生成器を更新する」ことができるのです。
しかし一つ疑問が残ります。それだと生成器と一緒に「判定器」も更新されてしまいそうです。これは困ります。生成器にとっての誤差は「見破られた割合」、判定器にとっての誤差は「騙された割合」で、それぞれ違うもの(というか真逆のもの)だからです。そこでこの統合モデルの更新の際には、「判定器の方は更新しないでね」という設定をあらかじめ行っておく必要があります。
def create_gan(generator, discriminator):
model = Sequential()
model.add(generator)
model.add(discriminator)
return model
学習
今回のメインである学習です。少し長いコードになりますが落ち着いて進めましょう。
trainの各引数は以下の通りです。
x_train:訓練データ(正しい英単語のみをベクトルに変換したデータの集合)
shape:判定器に入力するデータのshape
all:判定器に入力するデータ1つあたりのパラメタの総数
dim:生成器に入力する初期ベクトルの次元
optimizer:最適化手法。"adam","sgd"など文字列で指定すればよい。
batch_size:生成器、判定器それぞれが学習を行うターンを交互に繰り返す、その1ターン分で学習に使うデータの数。
sample_interval:何ターンごとに学習の経過状況を出力するか
iterations:学習を行う総ターン数
注意すべき点は、先にも述べた通り統合モデルについては判定器のパラメタを更新しないように設定しなければならない点です。これは、discriminator.trainable=Falseのようにして行います。
モデルをコンパイルする時点でdiscriminator.trainableが何に設定されているかでパラメタ更新の可否が決まるので、判定器discriminatorのコンパイル時にはTrueに、統合モデルであるganのコンパイル時にはFalseにします。
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ターンで約5分程度の時間がかかります。コーヒーでも飲んで待っていてください。
学習はうまく行ったでしょうか。
先に述べた学習のバランスを確認する上で重要なのがg_lossとd_lossの値です。これが両者とも似たような値(オーダーがあっていればまあOK?)になっていれば、バランスよく学習が進んでいることになります。
なお、コメントアウトを外してprint(prediction)を見た時に、すべての単語に対して0.9のような高い確率で本物だと判定する、というような状況がずっと続いて変化しない場合も、学習バランスがおかしい可能性が高いです。
testlossがある程度(0.01くらい?)まで小さくなっていて、なおかつg_lossとd_lossがバランスしていれば、学習はうまく行っていると判断してよいのではないでしょうか。
あとは実際に学習済み生成器モデルを動かすだけです!
学習済み英単語もどき生成器の利用
生成器モデルを使う前に、生成器の出力をアルファベット列に変換する関数を用意します。
「num2wordじゃだめなのか」と思うかもしれませんが、これは入力が少し違います。
def convert(vectors):
res=""
for vec in vectors:
res+=char[np.argmax(np.array(vec))]
return res
[1,4,0,18,19]
[[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個ほどランダムなベクトルを入れて、生成される単語を見てみましょう。
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