アプリとサービスのすすめ

アプリやIT系のサービスを中心に書いていきます。たまに副業やビジネス関係の情報なども気ままにつづります

高性能版GANの「styleGAN」で本物そっくりの画像を生成してみた【keras・機械学習】

今回は論文で紹介されてたNVIDIAが開発したstyleGANを実装してみた。

普通のGANとは生成過程も違うし、生成画像の出来の精度も比較にならないぐらい高くて、驚いた。
仕事で使う機会があったので、その生成過程をまとめてく。


目次
1.styleGANについて
2.styleGANコード詳細
3.訓練
4.生成画像
5.まとめ


1.styleGANについて

styleGANはNVIDIAが開発した、本物と見分けがつかないくらいの画像が作れる、超高精度のGAN。
qiita.com


"Progressive-Growing of GANs”というGANの亜種のgeneratorの部分を発展させたもの。

メインのメカニズムは、低い解像度の層から順に学習して、高精度の画像の生成していく仕組みらしい。


【従来のGANとstyleGANの違い】
f:id:trafalbad:20190707152347j:plain

generatorの部分がAdaptive Instance Normalization (AdaIN)でかなり改造してあるのが普通のGANと大きく異なる点。

ちなみにdiscriminatorは普通のGANと同じ。


【Generator部分】
f:id:trafalbad:20190707152336j:plain




2.styleGANコード詳細

input画像



入力画像はgoogle検索でもってきた景色の画像3枚を120枚に増幅。
f:id:trafalbad:20190707154813p:plain


Macの動画ソフト「Quick time player」で動画にして、ffmpegで静止画に変換。120枚くらいに増やした。

$ ffmpeg -i 元動画.avi -ss 144 -t 148 -r 24 -f image2 %06d.jpg

かなり簡単に画像が大量に作れるので便利。

他にもopencvで動画から、静止画を作るやり方もある。

input画像等、ハイパーパラメータの条件はこんな感じ

・画像の合計120枚

・shape=(256, 256, 3)

・Batch =10

・255で割って正規化

・1000~2000エポック

各景色画像30枚ずつで、計120枚を10batchずつ回して訓練してく。

ちなみにoptimizerを含めて、今回のパラメータは、サイズが256の時にこのパラメータでうまくいった。

けど、512とか1024は同じパラメータでは上手くいくかわからない。




Generator



generatorはinputが3つあって、2つは論文にあるように、ノイズを入れるところになってる。

AdaINが普通のGANとの違いがでかい。

from AdaIN import AdaInstanceNormalization

im_size = 256
latent_size = 512

def g_block(inp, style, noise, fil, u = True):

    b = Dense(fil)(style)
    b = Reshape([1, 1, fil])(b)
    g = Dense(fil)(style)
    g = Reshape([1, 1, fil])(g)

    n = Conv2D(filters = fil, kernel_size = 1, padding = 'same', kernel_initializer = 'he_normal')(noise)

    if u:
        out = UpSampling2D(interpolation = 'bilinear')(inp)
        out = Conv2D(filters = fil, kernel_size = 3, padding = 'same', kernel_initializer = 'he_normal')(out)
    else:
        out = Activation('linear')(inp)

    out = AdaInstanceNormalization()([out, b, g])
    out = add([out, n])
    out = LeakyReLU(0.01)(out)

    b = Dense(fil)(style)
    b = Reshape([1, 1, fil])(b)
    g = Dense(fil)(style)
    g = Reshape([1, 1, fil])(g)

    n = Conv2D(filters = fil, kernel_size = 1, padding = 'same', kernel_initializer = 'he_normal')(noise)

    out = Conv2D(filters = fil, kernel_size = 3, padding = 'same', kernel_initializer = 'he_normal')(out)
    out = AdaInstanceNormalization()([out, b, g])
    out = add([out, n])
    out = LeakyReLU(0.01)(out)

    return out

def generator():

    inp_s = Input(shape = [latent_size])
    sty = Dense(512, kernel_initializer = 'he_normal')(inp_s)
    sty = LeakyReLU(0.1)(sty)
    sty = Dense(512, kernel_initializer = 'he_normal')(sty)
    sty = LeakyReLU(0.1)(sty)

    inp_n = Input(shape = [im_size, im_size, 1])
    noi = [Activation('linear')(inp_n)]
    curr_size = im_size
    while curr_size > 4:
        curr_size = int(curr_size / 2)
        noi.append(Cropping2D(int(curr_size/2))(noi[-1]))

    inp = Input(shape = [1])
    x = Dense(4 * 4 * 512, kernel_initializer = 'he_normal')(inp)
    x = Reshape([4, 4, 512])(x)
    x = g_block(x, sty, noi[-1], 512, u=False)

    if(im_size >= 1024):
        x = g_block(x, sty, noi[7], 512) # Size / 64
    if(im_size >= 512):
        x = g_block(x, sty, noi[6], 384) # Size / 64
    if(im_size >= 256):
        x = g_block(x, sty, noi[5], 256) # Size / 32
    if(im_size >= 128):
        x = g_block(x, sty, noi[4], 192) # Size / 16
    if(im_size >= 64):
        x = g_block(x, sty, noi[3], 128) # Size / 8

    x = g_block(x, sty, noi[2], 64) # Size / 4
    x = g_block(x, sty, noi[1], 32) # Size / 2
    x = g_block(x, sty, noi[0], 16) # Size
    x = Conv2D(filters = 3, kernel_size = 1, padding = 'same', activation = 'sigmoid')(x)
    return Model(inputs = [inp_s, inp_n, inp], outputs = x)


Discriminator



Discriminatorは従来のと変わらない。

今回は、1024サイズにも対応できるようにするために、有名なGANのDiscriminatorを使った。

def discriminator():
    inp = Input(shape = [im_size, im_size, 3])

    x = d_block(inp, 16) #Size / 2
    x = d_block(x, 32) #Size / 4
    x = d_block(x, 64) #Size / 8

    if (im_size > 32):
       x = d_block(x, 128) #Size / 16

    if (im_size > 64):
        x = d_block(x, 192) #Size / 32

    if (im_size > 128):
        x = d_block(x, 256) #Size / 64

    if (im_size > 256):
        x = d_block(x, 384) #Size / 128

    if (im_size > 512):
        x = d_block(x, 512) #Size / 256

    x = Flatten()(x)
    x = Dense(128)(x)
    x = Activation('relu')(x)
    x = Dropout(0.6)(x)
    x = Dense(1)(x)
    
    return Model(inputs = inp, outputs = x)


disganとadganの実装


disganでdiscriminatorを訓練。adganでgeneratorを訓練する。

G = generator()
D = discriminator()

# Dは更新して、Gは更新しない
D.trainable = True
for layer in D.layers:
    layer.trainable = True

G.trainable = False
for layer in G.layers:
    layer.trainable = False

ri = Input(shape = [im_size, im_size, 3])
dr = D(ri)

gi = Input(shape = [latent_size])
gi2 = Input(shape = [im_size, im_size, 1])
gi3 = Input(shape = [1])
df = D(G([gi, gi2, gi3]))
da = D(ri)
disgan = Model(inputs=[ri, gi, gi2, gi3], outputs=[dr, df, da])

# Gは更新、Dは更新しない
D.trainable = False
for layer in D.layers:
    layer.trainable = False

G.trainable = True
for layer in G.layers:
    layer.trainable = True

gi = Input(shape = [latent_size])
gi2 = Input(shape = [im_size, im_size, 1])
gi3 = Input(shape = [1])
df = D(G([gi, gi2, gi3]))
adgan = Model(inputs = [gi, gi2, gi3], outputs = df)



lossとoptimizer


lossはGANには珍しくMSEを使った。

optimizerはAdamで学習率(lr)は下のように設定。

・Disgan => lr=0.0002

・Adage => lr=0.0001

両方0.0001よりdisganを少し大きくした方が精度がいい結果になった。

def gradient_penalty_loss(y_true, y_pred, averaged_samples, weight):
    gradients = K.gradients(y_pred, averaged_samples)[0]
    gradients_sqr = K.square(gradients)
    gradient_penalty = K.sum(gradients_sqr,
                              axis=np.arange(1, len(gradients_sqr.shape)))
    return K.mean(gradient_penalty * weight)

partial_gp_loss = partial(gradient_penalty_loss, averaged_samples = ri, weight = 5)

disgan.compile(optimizer=Adam(lr=0.0002, beta_1 = 0, beta_2 = 0.99, decay = 0.00001), loss=['mse', 'mse', partial_gp_loss])

adgan.compile(optimizer = Adam(lr=0.0001, beta_1 = 0, beta_2 = 0.99, decay = 0.00001), loss = 'mse')

3.訓練

主にdisganとadganのtrain。
従来のGANと訓練方法は同じだけど、generatorが違う分、disganとadgan共に独特な値を入れてる。

# Noise 
def noise(n):
    return np.random.normal(0.0, 1.0, size = [n, latent_size])

def noiseImage(n):
    return np.random.uniform(0.0, 1.0, size = [n, im_size, im_size, 1])

# train disgan
real_images  = x_train[idx*batch_size:(idx+1)*batch_size]
train_data = [real_images, noise(batch_size), noiseImage(batch_size), np.ones((batch_size, 1))]
d_loss = disgan.train_on_batch(train_data, [np.ones((batch_size, 1)),
                                                      -np.ones((batch_size, 1)), np.ones((batch_size, 1))])

# train adgan
g_loss = adgan.train_on_batch([noise(batch_size), noiseImage(batch_size), np.ones((batch_size, 1))], 
                                        np.zeros((batch_size, 1), dtype=np.float32))


4.生成画像

生成した画像はこんな感じ。論文通り、本物とそっくりでびっくりの超高精度。

【生成画像】f:id:trafalbad:20190707155211p:plain

生成過程は、初めは黒から始まって、ゴッホみたいな油絵みたいになっていき、本物に近づいてく感じ。


f:id:trafalbad:20190707155057g:plain




5.まとめ

同じ画像が多いとかなり早くできる。今回は3枚×40=120枚で、700エポックにはもう本物っぽいのが出来始めてた。


ただバリエーションが増えると(例えば100枚のスニーカーで、100枚全部違う画像の場合)、とんでもなく時間がかかる。

なるべく生成したい画像は同じやつを何枚も入れとくと早く生成できる。論文みたいな顔のバリエーションだと、とんでもない時間(10万エポックくらい)かかる気がする。

GANの訓練はかなり繊細なので、styleGANのパラメータ調整は普通のGAN以上に繊細だった。

Unet(Auto encoder)でスニーカー画像からロゴを生成してみた【機械学習】

よく元画像から別の画像を生成したりするのに使うautoencoderの亜種「Unet」を使ってみた。
今回やるのはadidasのスニーカーを入力して、ロゴを出力するように学習させるタスク。

autoencoderを使うのは初めてなので、作業過程などをまとめてく。

目次
1.概要
2.コード部分
3.生成画像
・まとめ

1.概要


autoencoderは簡単に言うと画像とか文章を入力して、別のものに変換して出力するモデル。

例は、画像→画像、文章→文章、画像→文章、みたいにたくさんある。

f:id:trafalbad:20190703072233j:plain


Unetはautoencoderの一種。

U-Netが強力なのはEncoderとDecoderとの間に「Contracting path(スキップコネクション)」があるからで、Residual Network(ResNet)と同じ効果を発揮するらしい。

f:id:trafalbad:20190703072149j:plain


ResNetを多様するDeep Unetもあるらしいが、今回は普通のUnetで試した。

今回試すことは、「adidasのスニーカー画像を入力→adidasのロゴ画像を出力する」タスクをやってみた。

adidasのスニーカー】
f:id:trafalbad:20190703072311p:plain




adidasのロゴ
f:id:trafalbad:20190703072335p:plain




2.コード部分

Unet

主に変えた部分は、

・層を二つ追加した
・BatchNormalizationを全層に加えた

の2点。

INPUT_SIZE =(256,256,3)
def unet(input_size = INPUT_SIZE):
    inputs = Input(input_size)

    conv1_1 = Conv2D(32, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(inputs)
    conv1_1 = Conv2D(32, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv1_1)
    norm1_1 =  BatchNormalization()(conv1_1)
    pool1_1 = MaxPooling2D(pool_size=(2, 2))(norm1_1)

    conv1 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool1_1)
    conv1 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv1)
    norm1 =  BatchNormalization()(conv1)
    pool1 = MaxPooling2D(pool_size=(2, 2))(norm1)
    conv2 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool1)
    conv2 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv2)
    norm2 = BatchNormalization()(conv2)
    pool2 = MaxPooling2D(pool_size=(2, 2))(norm2)
    conv3 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool2)
    conv3 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv3)
    norm3 = BatchNormalization()(conv3)
    pool3 = MaxPooling2D(pool_size=(2, 2))(norm3)
    conv4 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool3)
    conv4 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv4)
    norm4 = BatchNormalization()(conv4)
    pool4 = MaxPooling2D(pool_size=(2, 2))(norm4)

    conv5 = Conv2D(1024, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool4)
    conv5 = Conv2D(1024, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv5)
    norm5 = BatchNormalization()(conv5)

    up6 = Conv2D(512, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(norm5))
    merge6 = concatenate([norm4,up6], axis = 3)
    conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge6)
    conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv6)
    norm6 = BatchNormalization()(conv6)

    up7 = Conv2D(256, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(norm6))
    merge7 = concatenate([norm3,up7], axis = 3)
    conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge7)
    conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv7)
    norm7 = BatchNormalization()(conv7)

    up8 = Conv2D(128, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(norm7))
    merge8 = concatenate([norm2,up8], axis = 3)
    conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge8)
    conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv8)
    norm8 = BatchNormalization()(conv8)

    up9 = Conv2D(64, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(norm8))
    merge9 = concatenate([norm1,up9], axis = 3)
    conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge9)
    conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv9)
    conv9 = Conv2D(2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv9)
    norm9 = BatchNormalization()(conv9)

    up9_1 = Conv2D(32, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(norm9))
    merge9_1 = concatenate([norm1_1, up9_1], axis = 3)
    conv9_1 = Conv2D(32, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge9_1)
    conv9_1 = Conv2D(32, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv9_1)
    conv9_1 = Conv2D(2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv9_1)
    norm9_1 = BatchNormalization()(conv9_1)

    conv10 = Conv2D(3, 1, activation = 'sigmoid')(conv9_1)
    return Model(input = inputs, output = conv10)


Contracting pathがあるので、層を厚くしても特徴抽出の効果が増した気がする。





画像入力



今回は画像は増幅できないので、kerasのImagegeneratorメソッドは使わず、generatorを自作。

class trainGenerator(object):
    def __init__(self):
        self.img_generator=[]
        self.mask_generator=[]
        self.reset()
   
    def reset(self):
        self.img_generator=[]
        self.mask_generator=[]
      
    def flow_from_dir(self, img_path, mask_path, batch_size=10):
        while True:
            for imgs, mask in zip(os.listdir(img_path), os.listdir(mask_path)):
                imgs=cv2.imread(img_path+'/'+imgs)
                mask=cv2.imread(mask_path+'/'+mask)
                if imgs is not None:
                  self.img_generator.append(imgs/ 255)
                if mask is not None:
                  self.mask_generator.append(mask/ 255)
                    
                  if len(self.img_generator)==batch_size:
                      input_img=np.array(self.img_generator, dtype=np.float32)
                  if len(self.mask_generator)==batch_size:
                      input_mask=np.array(self.mask_generator, dtype=np.float32)
                      self.reset()
                      yield input_img, input_mask

class valGenerator(trainGenerator):
    def __init__(self):
        self.img_generator=[]
        self.mask_generator=[]
        self.reset()

flow_from_dirのimg_pathが入力、mask_pathが出力画像の教師画像になってる。

validationデータ用のvalGeneratorクラスはtrainGeneratorクラスを継承してる。
あと、指定したバッチサイズに切り出されるようにした。

これでmodel.fitgenerator()メソッドに入れればOK。




訓練部分


lossは、Unetで頻繁に使われるDice loss(dice_coef_loss)を使った。optimizerは代表的なAdam。

import keras.backend as K
import tensorflow as tf
import numpy as np

# loss function
def dice_coef_loss(y_true, y_pred):
    return 1.0 - dice_coef(y_true, y_pred)


def dice_coef(y_true, y_pred):
    y_true = K.flatten(y_true)
    y_pred = K.flatten(y_pred)
    intersection = K.sum(y_true * y_pred)
    return 2.0 * intersection / (K.sum(y_true) + K.sum(y_pred) + 1)

あと、keras.callbackに自作のクラスを入れて、5エポックごとにloss曲線のpng画像が保存されるように工夫した。

from dice_coefficient_loss import dice_coef_loss, dice_coef
from HistoryCallbackLoss import HistoryCheckpoint

callback=[]
callback.append(HistoryCheckpoint(filepath='tb/LearningCurve_{history}.png', verbose=1, period=10))
callback.append(TensorBoard(log_dir='tb/'))
callback.append(ModelCheckpoint('logss/{epoch:02d}_unet.hdf5', monitor='loss', verbose=1))

model = unet()
# model.load_weights("10_unet.hdf5")
model.compile(loss=dice_coef_loss, optimizer=Adam(lr=1e-4), metrics=[dice_coef])           
model.summary()

model.fit_generator(training, steps_per_epoch=2000, 
                    epochs=10,
                    callbacks=callback,
                    validation_data=validations, 
                    validation_steps=200)


工夫した点



今回はよく論文に登場する精度の高い「〜ネットワーク」みたいな、飛び道具的なアルゴリズムを使わずに、ニューラルネットワーク自体を変えて、精度改善の工夫をした。


・Unetの層を増やす

・Dice lossを使う

・validation dataを使う

・callbackメソッドを使い、損失関数を自動調節する関数を作成

・正規化→L1, L2正規化、画像を255で割る

の5点を主に工夫した。派手なことせず、地道なテクで、かなり精度が上がった。

ニューラルネット系は人気で精度が高い「飛び道具」的なアルゴリズムでも、実際に実装してみると、全然使えないこともある。

なので、臨機応変に使いこなしたり、ニューラルネットワーク自体をいじって、堅実に精度向上の工夫ができるテクニックは必須。




3.生成画像

生成した画像

f:id:trafalbad:20190703072405p:plain


出来が少し荒い反省点としては、


・真っ黒の画像をいくつか訓練データに含んでしまった(訓練データが荒い)

・訓練枚数(140枚)少ない、訓練回数が少ない

とかが原因。

画像枚数1万枚以上とか、ガチでやってる人のUnetの結果はすごいのが多い。

なので、逆に140枚でこれだけお手軽に結構な精度の画像が生成できるのはすごい。




まとめ

今回はautoencoderのUnetを試してみた。

ガチでニューラルネットワーク自体を変えて精度向上の工夫したのは初めだった。

Variational Autoencoder(VAE)を使った記事も機会があれば書こうと思う。

circleCIとgithubを連携して、簡単にコードをGCRにpushできるようにしてみた

コードとかの管理ツールとして、circleCIを使っている。

というのもcircleCIを使えば、コードを書き換えて、githubにuploadすれば、circleCI経由で勝手にGCRにDocker imageがpushされるから。

簡単に言うと便利すぎるから使ってる感じ。

今回、githubとcircleCIを連携して、GCRにdocker pushするまでの過程をまとめた。

目次
・circleCIの何が美味しいの?

1.githubとcircleCIを連携させる

2.Cloud SDKやprojectの設定

3.GCRのAPIを有効にし、発行したJSONキーをcircleCIの環境変数に登録

4.発行したJSONキーをcircleCIに登録

5.githubに.circleci/config.ymlファイルを作成

6.コードをgithubにuploadして、GCRにPushする

circleCIの何が美味しいの?

circleCIを使えば、各APIを利用し、コード管理、自動化とかデプロイまでの作業を簡単にしたりと、いろんなことができる。

circleCIの自動化の利点は図で表すとこんな感じ

circleCI導入前
sshでログインしてデプロイコマンドを叩く必要あり
f:id:trafalbad:20190531141822j:plain


circleCI導入後
デプロイコマンド1つでデプロイが完了
f:id:trafalbad:20190531141451j:plain

個人的にTerraform+github+circleCI+atlasでのインフラ管理(記事)がとても便利で好き。

自分はコードを変更するたびにdocker imageを作成してdocker pushするのが面倒すぎたので、
github+circleCI+GCRで連携
→変更したコードをgithubにあげる
→GCRにdocker pushされる

使い方をした。以下ではそのやり方を書いてく。



1.githubとcircleCIを連携させる

circleCIのサイトに行き、githubアカウントで連携

f:id:trafalbad:20190531141342j:plain

連携方法はこのサイトを参考にした。

主にgithubの連携したいレポジトリを選択して、プランを決めれる。
普通に使うなら、プランはフリーでOK。


2.Cloud SDKやprojectの設定

自分はローカルから使う環境だったので、cloud SDK(gcloudコマンド)をinstallして、GCPのprojectを指定した。

# gcloudコマンド のインストール 
$ curl https://sdk.cloud.google.com | bash
$ exec -l $SHELL

# 認証
$ gcloud init

# 権限承認
$ gcloud auth login

# project認証
$ gcloud config set project PROJECT_ID


3.GCRのAPIを有効にし、発行したJSONキーをcircleCIの環境変数に登録

GCPのコンソール画面から、左の「IAMと管理」→「サービスアカウント」を選択し、アカウント作成。

IAMサービスの「サービスアカウント作成」をクリックして(名前・パスワードは適当)→役割を「ストレージ」にして作成→JSONキーを発行。

左の項目のStorageから、「artifacts.[プロジェクト名].appspot.com」のバケットを選択して、権限ボタンをクリック。

さっき作成したIAMサービスアカウントにそのバケットの「ストレージオブジェクト閲覧者」の権限を付与する。





4.発行したJSONキーをcircleCIに登録

circleCIにアクセスし、左の"ADD PROJECTS"ボタンから連携したgithubのレポジトリを選択し、"Set Up Project"をクリック

f:id:trafalbad:20190531141913j:plain



そのあと、Environment Valiableの「Add variable」をクリック。
f:id:trafalbad:20190531141929j:plain



NAMEは"GCLOUD_SERVICE_KEY"と入力、VALUEにさっき発行したJSONキーの文字列をコピペ。

f:id:trafalbad:20190531141948j:plain




5.githubに.circleci/config.ymlファイルを作成

githubで直接「Create new file」で選択して、githubに".circleci/config.yml"ファイルを作成。

config.ymlのコード内容はこのサイトを参考にした。


自分のconfig.yml

version: 2
jobs:
  build:
    docker:
      - image: google/cloud-sdk
      
    working_directory: /go/src/github.com/[githubのアカウント名]/[対象レポジトリ名]
    steps:
      - checkout
      - setup_remote_docker:
          version: 18.06.0-ce
      - run:
          name: Setup CLOUD SDK
          command: |
            echo $GCLOUD_SERVICE_KEY | gcloud auth activate-service-account --key-file=-
            gcloud --quiet config set project [PROJECT_ID]
            gcloud --quiet config set compute/zone asia-east1-b
            gcloud --quiet auth configure-docker
      - run:
          name: Push docker image
          command: |
            docker build -t go-web-server .
            LATEST_TAG=gcr.io/[PROJECT_ID]/go-web-server:latest
            docker tag go-web-server $LATEST_TAG
            docker push $LATEST_TAG            

6.コードをgithubにuploadして、GCRにPushする

今回はGoの簡単なアプリ用コードをgithubにupload。

Dockerfile

FROM golang:1.11-alpine
WORKDIR /go/src/github.com/naoty/go-web-server
COPY . .
RUN go install github.com/naoty/go-web-server
CMD ["go-web-server"]

main.go

package main
import (
  "fmt"
  "net/http"
)

func main() {
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello")
  })
  http.ListenAndServe(":8080", nil)
}


このコードをgit pushでアップロード

$ git add .
$ git commit -m “コメント”
$ git push origin master

git pushコマンドとcircleCIが連動して、GCRにdocker pushされんのかな。

circleCIで”SUCCESS”が出て、
f:id:trafalbad:20190531142018p:plain



GCRにちゃんとdocker imageがpushされてる。
f:id:trafalbad:20190531142035p:plain


circleCIはECRとかいろんなデプロイツールとして連携できるので、おすすめ。

Terraform × ApexでLambdaとCloudwatchログのスケジュールを管理してみた【機械学習・AWSインフラ】

Terraformが人気のインフラ管理ツールということで是非習得したかったので、TerraformとそのラッパーのApexを使って、LambdaとCloudwatchログのスケジュール管理をしてみた。

Apexを使えばLambda管理がラクにできるので、運用をイメージしてTerraformでlambdaを含めたAWSインフラを管理するまでのログをまとめてく。

目次
1.Terraformで管理するインフラの全体像
2.コード&ファイル
3.実行コマンド
4.まとめ

1.Terraformで管理するインフラの全体像

AWSインフラリソースの全体像


TerraformとApexで管理するAWSインフラの全体像を表すと下の図の通り。
f:id:trafalbad:20190518092716j:plain


Apexを使うと嬉しいことは2つ。

・Lambdaの管理がラクになる
terraformでLambdaを管理するには、

AWSのオブジェクト管理(Lambda Function、IAM、triggerのためのCloudWatch Eventsなど)

②Lambda関数のソースコード

③Lambda関数のソースコードに依存するライブラリたち

ソースコードとライブラリを固めたzip

が必要だけど、Apexなら②~④をまとめてやってくれる。


・Lambda用のコードをS3バケットではなく、直接Lambdaにアップロードできる
Apexが上の②〜④をやってくれるおかげで、zipを入れるS3バケットの管理や、terraformでのzipファイルの管理が必要なくなる。




運用イメージ


運用イメージは
・CloudWatchログのログをLambdaで「他のリソース」に送りつけること
を想定してる。


例1.Datadog docsにCloudWatchログのログをLambdaで送りつける
f:id:trafalbad:20190518083246p:plain



例2.ElasticseachにCloudWatchログのログをLambdaで送りつける
f:id:trafalbad:20190518083306j:plain


2.コード

ディレクトリ構成

./project.json # プロジェクト全体の定義
./functions/datadog_logs/function.json # 関数の定義
./functions/datadog_logs/datadog_logs.py # Lambdaで実行するコード(今回はPythonのサンプル)
./infrastructure/prod/main.tf # Terraformで管理するAWSリソースの定義</div>

functions配下の「datadog_logs」フォルダはLambda関数名に合わせる。
またinfrastructure配下の「prod」フォルダは環境の意味(prod=env)で、環境ごとにLambdaを作り分けられるApexの機能。

f:id:trafalbad:20190518083333p:plain



project.json


{
    "name": "event_driven_job",
    "description": "event driven job",
    "memory": 1024,
    "timeout": 120,
    "role": "arn:aws:iam::{各自のAWS Account ID}:role/datadog_logs",
    "environment": {}
}

fuction.jsonとproject.jsonの共通項目はproject.jsonに書けば上書きできるので、role、memory、timeoutとかの共通項目はproject.jsonに書いた。



function.json


{
    "description": "datadog_logs",
    "runtime": "python3.6",
    "handler": "datadog_logs.lambda_handler",
    "environment":
    {
        "ENV": "dev"
    }
}

“runtime”と”handler"さえ良ければOK。あと、関数名は「datadog_logs」。



datadog_logs.py


def lambda_handler(event, context):
    json = {"statusCode": 200, "body": "hello world"}
    return json

Lambda関数用のpython.3.6のサンプルファイル。今回の記事ではほとんど関係ないのでテキトーに作った。



main.tf(resource、variableとかの解説)


・インフラ管理リソース設定
terraform

provider “aws

・Apexに必要
variable "apex_function_datadog_logs" {}


・IAMロールの作成に必要
resource "aws_iam_role" “datadog_logs_lambda_role"

data "aws_iam_policy_document" "datadog_logs_lambda_policy_doc"

resource "aws_iam_role_policy" “datadog_logs_lambda_policy"


・Cloudwatchログのログの出力先
output “datadog_logs_lambda_role"


・Cloudwatchログのロググループ名を定義
data "aws_cloudwatch_log_group" “log_group"


・Cloudwatchログからのログのストリーム設定
resource "aws_cloudwatch_log_subscription_filter" “datadog_logs_filter"


・Lambdaへのアクセス許可
resource "aws_lambda_permission" “datadog_logs_filter"





3.実行コマンド

1.セットアップ

MacにApexをinstall

$ curl https://raw.githubusercontent.com/apex/apex/master/install.sh | sh

・cloudwatchログにロググループ作成(参考サイト
グループ名→「datadog_logs」



・東京regionのS3バケット「apex-test-bucket」を作る


AWS環境変数の設定

$ export AWS_ACCESS_KEY_ID=****
$ export AWS_SECRET_ACCESS_KEY=****
$ export AWS_DEFAULT_REGION=ap-northeast-1
$ export AWS_REGION=ap-northeast-1


2.terraformのバックグランド初期化


$ apex infra init


3.IAMロールの作成


$ apex infra apply -target aws_iam_role.datadog_logs_lambda_role -target aws_iam_policy_document.datadog_logs_lambda_policy_doc -target aws_iam_role_policy.datadog_logs_lambda_policy

-targetオプションではmain.tfのIAMロール作成に必要なリソースを指定してる。

実行後は、IAMロールで「data_logs (AWS サービス: lambda)」ができてる。

f:id:trafalbad:20190518083430p:plain




4.Lambdaのデプロイ


$ apex deploy

S3やcloudwatchログが紐付いたLambda関数が出来てる。関数名は”{{.Project.Name}}_{{.Function.Name}}”。

f:id:trafalbad:20190518083510p:plain




5.残りのAWSリソース(Lambdaのトリガー)のデプロイ


$ apex infra apply

Lambda関数のトリガーにcloudwatchログが追加されてる
f:id:trafalbad:20190518083548p:plain




6.後片付け


$ apex infra destroy
$ apex delete

IAMロール & Lambda関数がキレイに破壊されてる。

f:id:trafalbad:20190518083805j:plain





4.まとめ

terraformコマンド集

# terraformインストール
$ brew install terraform

# バージョン確認
$ terraform --version
$ terraform init   

# インフラ環境を新しく作るため、変更内容を確認
$ terraform plan

# planで確認した内容が実行
$ terraform apply

# 進行状況の確認
$ terraform show

# リソースの全削除
$ terraform destroy


理解するためにやったこと



・terraformでEC2だけ作ってみる

・Lambdaにzipファイルをアップロードしてみる

GUIでCloudWatch イベントでAWS Lambda 関数をスケジュールしてみる

・サイト「Apex+Terraformでサーバレスアーキテクチャをフルコード化」を一通りやってみる



参考サイト



Apex+Terraformでサーバレスアーキテクチャをフルコード化

Providers - Terraform by HashiCorp(Terraform公式サイト)

Apex公式サイト

Node.jsでサーバを構築し、Tensorflow.jsでCNNの画像分類してみた【機械学習・Javascript】

今回はgoogle colaboratory(colab)で訓練したCNNの訓練済みモデルとtensorflow.jsを使い、Node.jsのサーバ上で画像分類してみた。

Node.jsを使ってjavascript用のサーバを立てたり、javascriptを使ったりと学ぶことが多かった。colab上で訓練→javascript上でmodelの読み込み→予測結果表示までの過程を備忘録もかねてまとめた。


目次
・この記事の概要

1.CNNで訓練・モデルの保存

2.tensorflowjs_converterで保存したモデルの変換

3.Node.jsで立てたサーバ上でモデルの読み込み

4.アップロードした画像を予測し、正解率を表示


この記事の概要


やったことを簡単にまとめると、まず「adidas, nike, new balance, vans」の4つのスニーカーを判別する画像分類モデルをCNNで作った。

自分のPCのスペックがカスなので、google colaboratoryを使って訓練。保存したモデル(h5ファイル)をtensorflowjs_converterでjavascript上で使えるように変換。


Node.jsでサーバを構築し、javascript用のサーバ上で変換した学習したモデルを読み込み、ローカルからアップロードした画像を予測し、各正解率を表示する。
その作業過程の中でまとめたいこと、知識などを時事系列でまとめてく。

ちなみにNode.jsのサーバ上にアクセスした時の画面はこんな感じ。
f:id:trafalbad:20190514234945p:plain



1.CNNで訓練・モデルの保存

自分のPCのスペックが心配なので、CNNの訓練はGPUが無料で使える神ツール Google Colaboratory(以下’colab’) を使った。

colabの使い方記事はこちらの記事を参考にした。


訓練に使ったpythonコード

tf.enable_resource_variables()

model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu',
                        input_shape=(100, 100, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(4, activation='softmax'))

model.compile(loss='categorical_crossentropy',
              optimizer='Adam',
              metrics=['acc'])

# fit_generatorで画像を増幅
train_datagen = ImageDataGenerator(
    rescale=1./255,
    rotation_range=40,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True,)

history = History()
callback=[]
callback.append(history)
callback.append(ModelCheckpoint(filepath='/Users/downloads/tfjs/ep{epoch:02d}model.h5', verbose=0, save_best_only=False,
                save_weights_only=False, mode='auto', period=1))


train_generator = train_datagen.flow(image, labels)

init = tf.global_variables_initializer()
sess = tf.InteractiveSession()
sess.run(init)
history = model.fit_generator(train_generator,  
                                  steps_per_epoch=100, callbacks=callback, epochs=50)

keras.CallbackのModelCheckpointメソッドでepochごとに拡張子がh5のファイルを保存する。colabではモデルの訓練・保存のみ行った。

tensorflowjsでのモデル変換はローカルで仮想環境を構築してやった。



2.tensorflowjs_converterで保存したモデルの変換

作業してるとtensorflowとtensorflowjs(以下’tfjs’)のバージョンが合わないとダメとか書いてある記事が多かった。

しかし、今回はtensorflowとtfjs共に、最新versionでモデル変換できた。
ちなみにtfjsを使うには、tfjsセットアップ用サイトでscriptタグのリンクをコピペしてhtmlに貼り付けるだけ。


ググってると、

・訓練モデルをsaved_model形式でpbファイルに保存
javascript上で読み込めるようにtensorflowjs_converterで変換
javascript上ではtf.loadGraphModel()で読み込む

やり方をしてる記事が結構多かった。

自分はそのやり方だと、エラーでつまづいた。結局、次のやり方でやった。

・普通にCNNで訓練してh5ファイルを保存
・tensorflowjs_converterで変換
javascript上ではtf.loadLayersModel()で読み込む



まず、jupyterで仮想環境を作り、tfjsとかをinstall。

$ pip install --upgrade pip
$ pip install tensorflow keras tensorflowjs


その後tfjsのtensorflowjs_converterを使い、以下のコマンドでh5ファイルを変換。

$ tensorflowjs_converter --input_format keras --output_format tfjs_layers_model ep02model.h5 web_model/

input_formatはkeras、 output_formatはtfjs_layers_model。

変換後はh5ファイルがバイナリファイル(group1-shard1of2.bin)とJSONファイル(model.json)に変換される。

f:id:trafalbad:20190514235124p:plain




3.Node.jsで立てたサーバ上でモデルの読み込み

javascriptの作業環境としてはgoogle chromeが人気だけど、モデルの読み込みとかは、エラーが発生し、かなり面倒。

変換モデルを読み込むためには、Google chrome用に「web server for chrome」でサーバを立ててやる必要があったり、拡張機能を追加しなければならなかった。(モデルを読み込む際の拡張機能についてはこのサイトの動画に詳しく載ってる)



結局、Node.jsでローカルにサーバ立てて、safarijavascriptの動作確認をした。

Node.jsはサーバサイドで動くJavaScriptのことで、Javascript用のサーバを構築できる。
Node.jsはNode.jsのサイトからダウンロード。そうすれば、npmコマンドが使えるようになる。



後はサーバ用のファイル(server.js)を用意して、Node.js用パッケージをダウンロードし、サーバを起動。

$ npm install
$ npm start

起動後のサーバには「http://localhost:8080」でアクセスできる。


server.js

let express = require("express");
let app = express();

app.use(express.static("./static"));

app.listen(process.env.PORT || 8080, function(){
    console.log("Serving at 8080")
});


そして、サーバ上ではtf.loadLayersModel()でさっきのJSONファイル(model.json)を読み込んだ。

let model;
async function loadModel() {
	model=await tf.loadLayersModel(`http://localhost:8080/web_model/model.json`);
	document.getElementById('predict-button').classList.remove('is-loading');
};


4.アップロードした画像を予測し、正解率を表示

サーバにアクセスして画像をアップロードしたときの画面はこんな感じ
f:id:trafalbad:20190514235232p:plain

今回は、コード内でtf.tidy()を使い、tfjsで使うGPUの不要なメモリを掃除してる。

まずローカルのnikeのスニーカー画像をアップロード。htmlではcanvasタグに表示

<!--画面表示領域-->
<canvas id="canvas" width="100" height="100" style="border: 2px solid;"></canvas>

<!--アップロードボタン-->
<label for="image"><b>画像</b></label>
<input type="file" accept="image/*" id="getfile">
var file = document.querySelector('#getfile');
const inputWidth = inputHeight = 100;

document.getElementById('getfile').onchange = function(e) {
  var img = new Image();
  img.onload = draw;
  img.src = URL.createObjectURL(this.files[0]);};

function draw() {
  var canvas = document.getElementById('canvas');
  canvas.width = this.width;
  canvas.height = this.height;
  var ctx = canvas.getContext('2d');
  ctx.drawImage(this, 0,0);}


・「Start」ボタンでモデルの読み込み

$("#start-button").click(function(){
	loadModel();
});


・「Predict」ボタンで予測し、各正解率を表示。

function getAccuracyScores() {
	var score = tf.tidy(() => {
		// resize
		const tmpcanvas = document.getElementById('canvas').getContext('2d');
    var image = tmpcanvas.getImageData(0, 0, inputWidth, inputHeight)
		var tensor = tf.browser.fromPixels(image).resizeNearestNeighbor([inputWidth, inputHeight]).toFloat();
		var offset = tf.scalar(255);
		var tensor_iamge = tensor.div(offset).expandDims();
		return model.predict(tensor_iamge);
	  });

	return score;
}

function prediction() {
	var accuracyScores = getAccuracyScores();
	const accuraylists = accuracyScores.data();
	var index = 0
	accuraylists.then(function(e){
		const elements = document.querySelectorAll(".accuracy");
		elements.forEach(el => {
    el.parentNode.classList.remove('is-selected');
    const rowIndex = Number(el.dataset.rowIndex);
    if (index===rowIndex){
			el.parentNode.classList.add('is-selected');
		}
		el.innerText = e[index];
		index++;
	  });
	});
}

・他にも、アップロード前の画面にリセットする「Resetボタン」、画像を左右に90度回転させるボタンも設置



サーバにアクセスしてから、予測して正解率を表示するまではこんな感じ。
f:id:trafalbad:20190515003014g:plain



完成までかなり大変だったけど、colab、Javascript、Node.js、tensorflowjsとか、かなり得るものが多かった。

今回の記事は、上のスキルがペーペーの人間がNode.jsのサーバ上で画像分類できるまでを備忘録を兼ねて書いた。



参考記事
・画像アップロードブラウザでローカル画像をリサイズしてアップロード

・画像のresetClearing an HTML file upload field via JavaScript

大容量データの音声認識(CNN)をCPU上でやった作業ログ【機械学習・ディープラーニング】

今回は音声認識のデータセット「ESC-50」をCNNで分類した。

特にこだわったのが、GPUでも普通にやったらOOMエラーが出るくらいの大容量のデータセットを、kerasのfit_generatorメソッドを使ってCPU上でもできるようにしたこと。

あとは音声認識は触れたことなかったので、前処理から学習するまでの作業ログ。

目次
1.音声データセット(ESC-50)
2.音声データの水増し(Augmentation)
3.水増した音声データの保存と読み込み
4.データ前処理とCPU上で学習(CNN)


1.音声データセット(ESC-50)


今回は音声データセットESC-50」を使う。

ESC-50の音声は環境音・自然音からなる声を含まない音。
動物の鳴き声、雨の音、人間の咳、時計のアラーム、エンジン音など50クラス。それをCNNで分類してみる。

ファイル形式は拡張子が.wavの音声。サイトで動画が音源のmp3やmp4もwavに変換することができる類のやつで、macで普通に再生できる。

自分のPCスペックだと普通にダウンロードできなかったので、linuxコマンドで一晩かけてダウンロード。

$ nohup wget [URL]

nohupコマンドを使えば、PCがスリープモードになっても動き続けてくれる。

import librosa
import librosa.display
import matplotlib.pyplot as plt
import seaborn as sn
from sklearn import model_selection
from sklearn import preprocessing
import IPython.display as ipd

# データのロード
esc_dir = os.path.join(base_dir, "ESC-50-master")
meta_file = os.path.join(esc_dir, "meta/esc50.csv")
audio_dir = os.path.join(esc_dir, "audio/")

# ラベルとその名前のDataFrame
class_dict = {}
for i in range(data_size[0]):
    if meta_data.loc[i,"target"] not in class_dict.keys():
        class_dict[meta_data.loc[i,"target"]] = meta_data.loc[i,"category"]
class_pd = pd.DataFrame(list(class_dict.items()), columns=["labels","classes"])

# ダウンロードした音声データの読み込み用関数
# load a wave data
def load_wave_data(audio_dir, file_name):
    file_path = os.path.join(audio_dir, file_name)
    x, fs = librosa.load(file_path, sr=44100)
    return x,fs

# change wave data to mel-stft
def calculate_melsp(x, n_fft=1024, hop_length=128):
    stft = np.abs(librosa.stft(x, n_fft=n_fft, hop_length=hop_length))**2
    log_stft = librosa.power_to_db(stft)
    melsp = librosa.feature.melspectrogram(S=log_stft,n_mels=128)
    return melsp

# display wave in plots
def show_wave(x):
    plt.plot(x)
    plt.show()
    
    
# display wave in heatmap
def show_melsp(melsp, fs):
    librosa.display.specshow(melsp, sr=fs)
    plt.colorbar()
    plt.show()




2.音声データの水増し(Augmentation)

1stステップでデータを音声認識用に変換し、2ndステップで水増しする。




1stステップ:変換


まずただの音声をメル周波数に変換。

メル周波数は人間の聴覚でも感知できるように考慮された尺度のこと。メル周波数に変換すれば、人間の耳でもなんの音かわかるようになる。

そのあとは分類しやすくするためフーリエ変換で次元圧縮。フーリエ変換での次元圧縮は画像でもよく使われてて、音声でも同じことができる。

フーリエ変換すると、音声を表す2次元グラフの横軸が時間だったのを、周波数のものに変換する。
f:id:trafalbad:20190429132833j:plain


直感的には、N次元の音声を人間にもわかるよう(メル周波数)に変換し、学習しやすいようにフーリエ変換で次元圧縮するイメージ。

# サンプル音声データをロード
x, fs = load_wave_data(audio_dir, meta_data.loc[0,"filename"])

# 音声をメル周波数に変換
melsp = calculate_melsp(x)
print("wave size:{0}\nmelsp size:{1}\nsamping rate:{2}".format(x.shape, melsp.shape, fs))
show_wave(x)
show_melsp(melsp, fs)

# 実際にjupyter上で音声が聞ける
ipd.Audio(x, rate=fs)

>>>output
wave size:(220500,)
melsp size:(128, 1723)
samping rate:44100


音声データ図
f:id:trafalbad:20190429132905j:plain




2nd:水増し


今回は元の音声をホワイトノイズ、シフトサウンド、ストレッチサウンドに変換し、水増し。

f:id:trafalbad:20190429144250j:plain


【ホワイトノイズ】
ホワイトノイズは広い音域に同程度の強度の「シャー」と聞こえるノイズ音を加える。

# add white noise
def add_white_noise(x, rate=0.002):
    return x + rate*np.random.randn(len(x))

x_wn = add_white_noise(x)
melsp = calculate_melsp(x_wn)
print("wave size:{0}\nmelsp size:{1}\nsamping rate:{2}".format(x_wn.shape, melsp.shape, fs))
show_wave(x_wn)
show_melsp(melsp, fs)

>>>output
# shapeは同じ
wave size:(220500,)
melsp size:(128, 1723)


【シフトサウンド
音声の周波数を変える。

def shift_sound(x, rate=2):
    return np.roll(x, int(len(x)//rate))

x_ss = shift_sound(x)
melsp = calculate_melsp(x_ss)


【ストレッチサウンド
周波数はそのままで、テンポ(音声のなる時間)を変える。
例えば、10秒間の音声を8秒間に変換するとか。

def stretch_sound(x, rate=1.1):
    input_length = len(x)
    x = librosa.effects.time_stretch(x, rate)
    if len(x)>input_length:
        return x[:input_length]
    else:
        return np.pad(x, (0, max(0, input_length - len(x))), "constant")
    
x_st = stretch_sound(x)
melsp = calculate_melsp(x_st)





3.水増した音声データの保存と読み込

まず、普通のtrainとtestデータを保存。
(ファイル名:"esc_melsp_train_raw.npz")

freq = 128
time = 1723

# save wave data in npz, with augmentation
def save_np_data(filename, x, y, aug=None, rates=None):
    np_data = np.zeros(freq*time*len(x)).reshape(len(x), freq, time)
    np_targets = np.zeros(len(y))
    for i in range(len(y)):
        _x, fs = load_wave_data(audio_dir, x[i])
        if aug is not None:
            _x = aug(x=_x, rate=rates[i])
        _x = calculate_melsp(_x)
        np_data[i] = _x
        np_targets[i] = y[i]
    np.savez(filename, x=np_data, y=np_targets)

# get training dataset and target dataset
x = list(meta_data.loc[:,"filename"])
y = list(meta_data.loc[:, "target"])

x_train, x_test, y_train, y_test = model_selection.train_test_split(x, y, test_size=0.25, stratify=y)
print("x train:{0}\ny train:{1}\nx test:{2}\ny test:{3}".format(len(x_train), len(y_train), len(x_test), len(y_test)))

>>>output
x train:1500
y train:1500
x test:500
y test:500


次の3つはtrainデータをホワイト・ストレッチ・シフトで水増し保存。(ファイル名:"esc_melsp_train_ss.npz","esc_melsp_train_st.npz", "esc_melsp_train_wn.npz")

# save training dataset with white noise
if not os.path.exists("esc_melsp_train_wn.npz"):
    rates = np.random.randint(1,50,len(x_train))/10000
    save_np_data("esc_melsp_train_wn.npz", x_train,  y_train, aug=add_white_noise, rates=rates)

# save training dataset with sound shift
if not os.path.exists("esc_melsp_train_ss.npz"):
    rates = np.random.choice(np.arange(2,6),len(y_train))
    save_np_data("esc_melsp_train_ss.npz", x_train,  y_train, aug=shift_sound, rates=rates)

# save training dataset with stretch
if not os.path.exists("esc_melsp_train_st.npz"):
    rates = np.random.choice(np.arange(80,120),len(y_train))/100
    save_np_data("esc_melsp_train_st.npz", x_train,  y_train, aug=stretch_sound, rates=rates)

最後はホワイトノイズした後、ストレッチかシフトを組み合わせた水増しデータを保存(”esc_melsp_train_com.npz")。

# save training dataset with combination of white noise and shift or stretch
if not os.path.exists("esc_melsp_train_com.npz"):
    np_data = np.zeros(freq*time*len(x_train)).reshape(len(x_train), freq, time)
    np_targets = np.zeros(len(y_train))
    for i in range(len(y_train)):
        x, fs = load_wave_data(audio_dir, x_train[i])
        x = add_white_noise(x=x, rate=np.random.randint(1,50)/1000)
        if np.random.choice((True,False)):
            x = shift_sound(x=x, rate=np.random.choice(np.arange(2,6)))
        else:
            x = stretch_sound(x=x, rate=np.random.choice(np.arange(80,120))/100)
        x = calculate_melsp(x)
        np_data[i] = x
        np_targets[i] = y_train[i]
    np.savez("esc_melsp_train_com.npz", x=np_data, y=np_targets)



5つのファイルをまとめてtrain_filesにまとめる。ファイル名“esc_melsp_train_com.npz”に保存
テストデータのファイル名は"esc_melsp_test.npz"。

# dataset files
train_files = ["esc_melsp_train_raw.npz",  "esc_melsp_train_ss.npz", "esc_melsp_train_st.npz", 
               "esc_melsp_train_wn.npz", "esc_melsp_train_com.npz"]
test_file = "esc_melsp_test.npz"


4.データ前処理とCPU上で学習(CNN)


前処理


振動数、テンポの設定をして、データ読み込み
振動数(または周波数):freq = 128
テンポ(音のなる時間):time =1723


CNNに読み込ませるデータshapeは
x_train:(7500, 128, 1723, 1):(batch_size, freq, time, 1)
y_train:(7500, 50):(batch_size, classes)

freq = 128
time = 1723

train_num = 1500
test_num = 500

# データセット用placeholderの定義
x_train = np.zeros(freq*time*train_num*len(train_files)).reshape(train_num*len(train_files), freq, time)
y_train = np.zeros(train_num*len(train_files))

# ファイルからtrain/testデータ読み込み
for i in range(len(train_files)):
    data = np.load(train_files[i])
    x_train[i*train_num:(i+1)*train_num] = data["x"]
    y_train[i*train_num:(i+1)*train_num] = data["y"]

test_data = np.load(test_file)
x_test = test_data["x"]
y_test = test_data["y"]

# ラベルをone-hotに変換
classes = 50
y_train = keras.utils.to_categorical(y_train, classes)
y_test = keras.utils.to_categorical(y_test, classes)

# CNN用にデータを(batch_size, freq, time, 1)にreshape
x_train = x_train.reshape(train_num*5, freq, time, 1)
x_test = x_test.reshape(test_num, freq, time, 1)

print("x train:{0}\ny train:{1}\nx test:{2}\ny test:{3}".format(x_train.shape, y_train.shape, x_test.shape, y_test.shape))

>>>output
x train:(7500, 128, 1723, 1)
y train:(7500, 50)
x test:(500, 128, 1723, 1)
y test:(500, 50)


CNNで学習


割と音声認識用でよく使われてるCNNを使用。

f:id:trafalbad:20190429144148j:plain

def cba(inputs, filters, kernel_size, strides):
    x = Conv2D(filters, kernel_size=kernel_size, strides=strides, padding='same')(inputs)
    x = BatchNormalization()(x)
    x = Activation("relu")(x)
    return x

# Callbackとか
model_dir = "~/ESC-50-master/dir"
if not os.path.exists(model_dir):
    os.mkdir(model_dir)

es_cb = EarlyStopping(monitor='val_loss', patience=10, verbose=1, mode='auto')
chkpt = os.path.join(model_dir, 'esc50_.{epoch:02d}_{val_loss:.4f}_{val_acc:.4f}.hdf5')
cp_cb = ModelCheckpoint(filepath = chkpt, monitor='val_loss', verbose=1, save_best_only=True, mode='auto')

# CNN
inputs = Input(shape=(x_train.shape[1:]))

x_1 = cba(inputs, filters=32, kernel_size=(1,8), strides=(1,2))
x_1 = cba(x_1, filters=32, kernel_size=(8,1), strides=(2,1))
x_1 = cba(x_1, filters=64, kernel_size=(1,8), strides=(1,2))
x_1 = cba(x_1, filters=64, kernel_size=(8,1), strides=(2,1))

x_2 = cba(inputs, filters=32, kernel_size=(1,16), strides=(1,2))
x_2 = cba(x_2, filters=32, kernel_size=(16,1), strides=(2,1))
x_2 = cba(x_2, filters=64, kernel_size=(1,16), strides=(1,2))
x_2 = cba(x_2, filters=64, kernel_size=(16,1), strides=(2,1))

x_3 = cba(inputs, filters=32, kernel_size=(1,32), strides=(1,2))
x_3 = cba(x_3, filters=32, kernel_size=(32,1), strides=(2,1))
x_3 = cba(x_3, filters=64, kernel_size=(1,32), strides=(1,2))
x_3 = cba(x_3, filters=64, kernel_size=(32,1), strides=(2,1))

x_4 = cba(inputs, filters=32, kernel_size=(1,64), strides=(1,2))
x_4 = cba(x_4, filters=32, kernel_size=(64,1), strides=(2,1))
x_4 = cba(x_4, filters=64, kernel_size=(1,64), strides=(1,2))
x_4 = cba(x_4, filters=64, kernel_size=(64,1), strides=(2,1))

x = Add()([x_1, x_2, x_3, x_4])

x = cba(x, filters=128, kernel_size=(1,16), strides=(1,2))
x = cba(x, filters=128, kernel_size=(16,1), strides=(2,1))

x = GlobalAveragePooling2D()(x)
x = Dense(classes)(x)
x = Activation("softmax")(x)

model = Model(inputs, x)

# initiate Adam optimizer with amsgrad
opt = keras.optimizers.adam(lr=0.00001, decay=1e-6, amsgrad=True)

model.compile(loss='categorical_crossentropy',
              optimizer=opt,
              metrics=['accuracy'])

model.summary()

>>>output
input_2 (InputLayer) (None, 128, 1723, 1) 0 
~~~~~
activation_38 (Activation)      (None, 50) 




model.fit_generator()で、大容量でCPUでも訓練できるようにした。

# class data generator

class MixupGenerator():
    def __init__(self, x_train, y_train, batch_size=16, alpha=0.2, shuffle=True):
        self.x_train = x_train
        self.y_train = y_train
        self.batch_size = batch_size
        self.alpha = alpha
        self.shuffle = shuffle
        self.sample_num = len(x_train)

    def __call__(self):
        while True:
            indexes = self.__get_exploration_order()
            itr_num = int(len(indexes) // (self.batch_size * 2))

            for i in range(itr_num):
                batch_ids = indexes[i * self.batch_size * 2:(i + 1) * self.batch_size * 2]
                x, y = self.__data_generation(batch_ids)

                yield x, y

    def __get_exploration_order(self):
        indexes = np.arange(self.sample_num)

        if self.shuffle:
            np.random.shuffle(indexes)

        return indexes

    def __data_generation(self, batch_ids):
        _, h, w, c = self.x_train.shape
        _, class_num = self.y_train.shape
        x1 = self.x_train[batch_ids[:self.batch_size]]
        x2 = self.x_train[batch_ids[self.batch_size:]]
        y1 = self.y_train[batch_ids[:self.batch_size]]
        y2 = self.y_train[batch_ids[self.batch_size:]]
        l = np.random.beta(self.alpha, self.alpha, self.batch_size)
        x_l = l.reshape(self.batch_size, 1, 1, 1)
        y_l = l.reshape(self.batch_size, 1)

        x = x1 * x_l + x2 * (1 - x_l)
        y = y1 * y_l + y2 * (1 - y_l)

        return x, y

# 訓練
batch_size = 16
epochs = 10

training_generator = MixupGenerator(x_train, y_train)()
model.fit_generator(generator=training_generator, steps_per_epoch=x_train.shape[0] // batch_size,
                    validation_data=(x_test, y_test), epochs=epochs, 
                    verbose=1, shuffle=True, 
                    callbacks=[es_cb, cp_cb])


今回は訓練を終えて、精度出すところまで行かなかったけど、これと同じやり方なら80%を超えて、人間の精度を超えるらしい。

音声認識は触れたことなかったので、学習までのデータの加工とか、水増し、CPU上で大容量データの訓練方法とかのいいアウトプットになった。

参考:ディープラーニングで音声分類 - Qiita

pythonで強化学習のモデルフリーの手法、学習法・コードまとめ【機械学習】

強化学習はモデルベースとモデルフリーに分類できて、前回はモデルベースの手法をまとめた。
今回はモデルフリーのメインの手法をまとめてく。

モデルベースの手法はこちら。
trafalbad.hatenadiary.jp



目次
1.変数、関数、環境、エージェントの定義
2.モデルフリーにおける3つの問題とその解決法
3.まとめ



1.変数、関数、環境、エージェントの定義

まずモデルフリーの定義について

「モデルフリー」= サッカーのようなスポーツのように、「どんな行動をとれば報酬に繋がるかわからない(遷移関数と報酬関数がわからない)環境」のこと。


モデルフリーは「遷移関数」と「報酬関数」を定義しない代わりに、「エージェント(環境におけるプレイヤー)」を定義する。


モデルフリーはモデルベースとは違い、エージェントが行動しながら、価値を最大化するように学習する仕組みで、メインで定義するのは「行動、状態、環境、エージェント」


モデルベースとモデルフリーの大きな違いは「エージェントを定義するか、してないか」だ。

# モデルフリーの環境(コイントスゲーム)の定義
class CoinToss():

    def __init__(self, head_probs, max_episode_steps=30):
        self.head_probs = head_probs
        self.max_episode_steps = max_episode_steps
        self.toss_count = 0
      # 略
            return reward, done

# エージェントの定義
class Agent():
  # 略


モデルフリーにおける学習の概略図
f:id:trafalbad:20190425221644p:plain



2.モデルフリーにおける3つの問題とその解決法

モデルフリーではエージェントが行動することによって「経験」を蓄積していき、報酬を最大化する仕組み。
そこで、焦点になる問題が3つある。



モデルフリーにおける3つの問題の関係図
f:id:trafalbad:20190425221709p:plain


1.経験を蓄積するか活用するか

モデルフリーでエージェントの「経験」を使い、報酬の総和を最大化するためには

・経験を蓄積して、よりよい遷移状態に行けるかどうか知る(以下「探索」)

・報酬を得るためには、経験を活用する(以下「活用」)

の2つの使い方をバランスよく行う必要がある。


経験における「探索と活用はトレードオフの関係」にあるため、理想は探索しながら経験を活用して報酬を得ること。


そのために「探索と活用のバランス」を上手くとる手法として「epsilon-greedy法」がある。

# epsilon-greedy法での学習

# EpsilonGreedy法のエージェントの定義
class EpsilonGreedyAgent():

    def __init__(self, epsilon):
        self.epsilon = epsilon
        self.V = []
        
 # コイントスゲームを行う処理
    def play(self, env):
        # 略
        return rewards
        


# コイントスゲーム環境と上のエージェントを使う
env = CoinToss([0.1, 0.5, 0.1, 0.9, 0.1])
epsilons = [0.0, 0.1, 0.2, 0.5, 0.8]
game_steps = list(range(10, 310, 10))
result = {}
for e in epsilons:
    agent = EpsilonGreedyAgent(epsilon=e)
    means = []
    for s in game_steps:
        env.max_episode_steps = s
        rewards = agent.play(env)
        means.append(np.mean(rewards))
    result["epsilon={}".format(e)] = means
result["coin toss count"] = game_steps
result = pd.DataFrame(result)
result.set_index("coin toss count", drop=True, inplace=True)
result.plot.line(figsize=(10, 5))
plt.show()


コイントス環境の引数の値を変えて、ベストな値を探し、探索と活用のバランスをとる。

epsilon-greedy法の値による報酬の推移は以下の図の通り。0.1か0.2がベスト値となってる。
f:id:trafalbad:20190425222733p:plain




2.エージェントの行動の修正をどのように行うか

エージェントの行動の修正は

実績で行うか(実績で修正)

実績で修正:エピソードが終わったときの報酬の総和で修正する(主な手法は「モンテカルロ法」)

予測で行うか(予測で修正)

予測で修正:エピソードの終了を待たずに途中で修正する(主な手法は「TD法」)


以下に実績で修正した場合と、予測で修正した場合の報酬獲得の推移をまとめた。



実績で修正した(モンテカルロ法の)エピソード数の獲得報酬平均の推移

f:id:trafalbad:20190425222756p:plain




予測で修正した(TD法を利用した学習法「Q-learning」の)エピソード数の獲得報酬平均の推移

f:id:trafalbad:20190425223428p:plain

どっちも上手く行ってる。



***エージェント行動の修正のメイン手法***

・1回の行動直後に修正する=TD(0)

モンテカルロ法(TD(1))

・2とから3回の行動直後に修正する=Multi-step Learning

・各ステップの値を合成し、誤差を計算する=TD(λ)法

強化学習の深層学習の手法ではMulti-step Learningがよく使われてる。




3.経験を戦略(policyベース)、状態評価(valueベース)、その両方の更新に利用するか

モデルフリーの学習方法には「経験」を

・価値を最大化する行動選択(valueベース)のために更新するか

・戦略に基づく行動選択(policyベース)のために更新するか

・互いに弱点を補いながら、valueベースとpolicyベースの両方(Actor Critic)を更新するか

の3つがある。


モデルフリーの3つの学習法「policyベース」・「valueベース」・「Actor Critic」



valueベース】

valueベースを主に使う学習法はQ-learning。
さっきも載っけたけど、再掲。


Q-learningの学習のエピソード数と獲得平均報酬の推移
f:id:trafalbad:20190425223428p:plain




【policyベース】
policyベースを主に使う学習法はSARSAがある



SARSAの学習のエピソード数と獲得平均報酬の推移
f:id:trafalbad:20190425224343p:plain

今回のコイントス環境では、Q-learningの状態評価の方がいい結果になった。

これはリスクのある行動をとらない状態評価の効果が、今回の環境に上手く反映された結果っぽい。



【Actor Critic法】

もう一つの学習法は、valueベースとpolicyベースを組み合わせた「Actor Critic法」。

戦略担当Actorと状態評価担当Criticを相互に更新して学習する仕組み。


Actor Critic法の学習のエピソード数と獲得報酬平均の推移
f:id:trafalbad:20190425224430p:plain

今までの手法より、エピソード数が長くなってるけど、その分最終的には安定して報酬を獲得できてる。



3.まとめ
モデルフリーの学習の流れをまとめると

①変数、関数、環境、エージェントの定義

②3つの問題を考慮し、手法を考える

1.経験を蓄積するか活用するかのバランスを取る手法「epsilon-greedy法」

2.エージェントの行動の修正をどのように行うか

3.経験を戦略(policyベース)、状態評価(valueベース)、またはそのどちらか(Actor Critic)の更新に利用するか

valueベース、policyベースまたは、両方を組み合わせた「Actor Critic法」で学習する



強化学習は今、すべて深層学習になってるけと、モデルフリーに関して紹介した手法やモデルベースの記事での手法は強化学習の基礎&メインの手法で、深層学習でも使われてる。

強化学習の基礎的かつメインの手法を学習して備忘録としてまとめた。モデルベースの記事とモデルフリーのこの記事で、強化学習の基礎的 & 主要な手法はほぼ網羅してる。