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

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

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)を使った記事も機会があれば書こうと思う。

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法」で学習する



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

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

pythonで強化学習のモデルベースの手法とその学習法をまとめてみた【機械学習】

書籍「pythonで学ぶ強化学習」を読んだ。強化学習のアウトプットの機会がないので、とりあえず学んだ内容を備忘録も兼ねてまとめてみた。

主に強化学習は「モデルベース」と「モデルフリー」に分類でき、今回はモデルベースの手法のまとめ。

目次
1.モデルベース強化学習のメイン「変数・関数・環境」
2.モデルベースとモデルフリー
3.強化学習の学習法「policyベース」と「valueベース」
4.まとめ


1.モデルベース強化学習のメイン「変数・関数・環境」

モデルベースの強化学習で主に定義するのは、行動、状態、遷移関数、報酬関数、環境。

例:
環境:12マスのゲーム、緑にたどり着ければ報酬ゲット
f:id:trafalbad:20190425080644p:plain

行動(a):右、左、上、下

状態(s):行列のセルの位置

遷移関数(T(s, a)):行動と状態を引数にとり、次に移動するセル(遷移先:s')とそこへ遷移する確率を返す関数

報酬関数(R(s', s)):状態と遷移先(s')を引数に、赤のセルなら-1と、緑のセルなら+1を返す関数


変数、関数、環境の概略コード

# 状態
class State():
    # 略
     return self.row == other.row and self.column == other.column

# 行動
class Action(Enum):
    UP = 1
    DOWN = -1
    LEFT = 2
    RIGHT = -2

# 環境
class Environment():
    def __init__(self, grid, move_prob=0.8):

   # 遷移関数
   def transit_func(self, state, action):
        # 略
        return transition_probs

   # 報酬関数
   def reward_func(self, state):
        # 略
        return reward, done

   return next_state, reward, done


***ちなみに***
強化学習は「遷移先の状態は直前の状態と、そこでの行動のみに依存する。報酬は直前の状態と遷移先に依存する」という決まり(「マルコフ決定過程(MDP)」)がある。
つまり、強化学習は「行動、状態、遷移関数、報酬関数、環境を与えることで報酬の総和(≒評価値)が決まる」という決まりになってる。


強化学習マルコフ決定過程の図
f:id:trafalbad:20190425080625p:plain



2.モデルベースとモデルフリー

強化学習はモデルベースとモデルフリーに分類できる。

モデルベース




モデルベース=「どんな行動をとれば報酬が得られるか(遷移関数と報酬関数)がはっきりわかってる環境」
のこと

下図の環境は
・緑のセル→プラスの報酬
・オレンジのセル→マイナスの報酬

と「どんな行動をとれば報酬がもらえるのか」はっきりしているからモデルベースの一例。

f:id:trafalbad:20190425080644p:plain


モデルフリー



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


スポーツなら全般(例えばサッカーとかバドミントンとか)が当てはまり、有名なアルファ碁は対戦相手によって報酬が決まらないからモデルフリー。

モデルフリーは遷移関数と報酬関数を定義せず、エージェント(環境におけるプレイヤー)を定義する。




モデルベース、モデルフリーにおける報酬の総和(価値)の計算



モデルベース、モデルフリーの環境はどちらも
・環境における制限時間(エピソード)終了時の報酬の総和(以下「価値」)

を計算するときは、

・過去の計算値を使う
・(行動)確率をかけて期待値で表す

という2つテク(Bellman Equation)を使って計算する。

強化学習はこの「価値」を最大化することが目的なので、重要な部分。

def V(s, gamma=0.99):
    V = R(s) + gamma * max_V_on_next_state(s)
    return V


def R(s):
    if s == "happy_end":
        return 1
    elif s == "bad_end":
        return -1
    else:
        return 0

def max_V_on_next_state(s):
    # 略
    return max(values)

def transit_func(s, a):
    # 略
        return {
            next_state(s, a): MOVE_PROB,
            next_state(s, opposite): 1 - MOVE_PROB}




3.強化学習の学習法「policyベース」と「valueベース」

強化学習の学習目的で一番の焦点は

・Bellman Equationで表した「価値」をどうやって最大化するか

この目的に合わせて、主に「policyベース」と「valueベース」2つの学習方法がある。(2つ両方使う、二刀流的な学習方法もある)




policyベース
=> どのように行動するか(戦略)を基準に学習する

イメージ

policyベース→参謀長みたいな策略家で現実的な思考で学習する感じ


valueベース
=> 「特定の行動をとる=評価になる」ということだけ学習する。(評価=行動選択)

イメージ
valueベース→いいならいい、ダメならダメと勝手に決めて学んでくので、楽観的な思考で学習する感じ




# valueベースとpolicyベースで共通のPlannerを定義
class Planner():

    def __init__(self, env):
        self.env = env
        self.log = []

    def initialize(self):
        self.env.reset()
        self.log = []

    def plan(self, gamma=0.9, threshold=0.0001):
        raise Exception("Planner have to implements plan method.")

    def transitions_at(self, state, action):
        transition_probs = self.env.transit_func(state, action)
        for next_state in transition_probs:
            prob = transition_probs[next_state]
            reward, _ = self.env.reward_func(next_state)
            yield prob, next_state, reward

    def dict_to_grid(self, state_reward_dict):
        grid = []
        for i in range(self.env.row_length):
            row = [0] * self.env.column_length
            grid.append(row)
        for s in state_reward_dict:
            grid[s.row][s.column] = state_reward_dict[s]

        return grid

# Plannerを継承した「valueベース」
class ValuteIterationPlanner(Planner):
    # 略
    return V_grid


# Plannerを継承した「policyベース」
class PolicyIterationPlanner(Planner):
    # 略

 # 戦略の価値計算用関数
 def plan(self, gamma=0.9, threshold=0.0001):
       # 略
        return V_grid



ちなみさっきのセルのモデルベース環境でvalueベースで訓練した結果。上手く緑にたどり着けてる。

f:id:trafalbad:20190425080711p:plain

環境が単純だからpolicyベースでもほとんど変わらない結果だった。



4.まとめ

モデルベースの強化学習の学習・評価までの流れは

①環境の定義に加えて、行動、状態、遷移関数、報酬関数の定義



②「エピソード終了時の報酬の総和(価値)」の定義

強化学習では学習目的がこの「価値」の最大化であり、この「価値」は、
・過去の計算値を使う
・(行動)確率をかけて期待値で表す

というテク「Bellman Equation」を使って計算される。



③「policyベース」か「valueベース」の手法で学習する
policyベース、valueベースまたは、その両方を使った手法を使って学習・評価する。




今の主流はvalueベースとpolicyベースでお互いの弱点を補いながら併用する手法がメインぽい。強化学習の実用されてるのは全てディープラーニングだけど、基礎的な手法自体はここで書いたやり方と同じ。

今回は「モデルベース」のメインの手法をまとめた。

「モデルフリー」の手法をまとめた次記事はこちら
trafalbad.hatenadiary.jp

CNNの訓練済みモデルで特徴抽出して、faissによる類似画像検索してみた【機械学習】

メルカリの画像分類で、end-to-endで学習した学習済みモデルを使って、特徴抽出してから、faissで類似画像検索する手法が使われてた。

詳しくは語られてないけど、下の図の流れ。
f:id:trafalbad:20190306192449j:plain

手順でいうとこんな感じ

→データセット画像(indexing dataset)とクエリ画像(query image)を用意
→訓練済みモデルを使い特徴抽出(feature extraction)
→抽出した特徴量をPCAで次元削減(PCA)
→faissでクエリ画像と似てる画像をデータセット画像から類似画像を検索(faiss)

この手法を前からやってみたかったので、今回は上の図の手法通りに、訓練済みモデルを使い、faissで類似画像検索をしてみた。

目次
・今回の手法の概要
・faissで類似画像検索の行程
1.データセット画像とクエリ画像の用意
2.訓練済みモデルで特徴抽出
3.PCAで次元削減について
4.faissで類似画像検索

今回の手法の概要
以前書いた画像分類記事のときに、inception-resnet-v2でend-to-end学習した時の、学習済みモデルを使った。

inception-resnet-v2を使ったモデルで、200ラベルの精度は50%くらいだった。ただ4種のスニーカー

・vans
・new-balance
adidas
nike

は形が同じで、ロゴが違うだけなので、各々の画像の分類精度はよくなかった。

そこでメルカリの類似画像検索手法で、さらに精度よく分類できるか試してみたのが今回の記事。


特徴抽出は一般的に訓練済みモデルを使い、より汎用的な特徴を取り出すことが目的。CNNは出力層の全結合層前の、より深い部分の畳み込み層から取り出した方が、特徴量という意味ではより汎用性が高い。



一方、faissクラスタリングとか、類似画像検索ができるライブラリらしい。今回は類似画像検索に使ってみた。全体の順序としては、下図のメルカリの画像通りの手順でやってみる。

f:id:trafalbad:20190306192449j:plain

手順

1.データセットの画像とクエリ画像を用意する

2.訓練済みのモデルで畳み込み層('conv_7b')から特徴抽出した出力と、入力画像から新しいモデルを作り、予測

3.次元削減(PCA)

4.faissでクエリ画像と似てる画像をデータセットから検索

faissで類似画像検索の行程


1.データセット画像とクエリ画像の用意

end-to-endで学習した学習済みモデルでは、スニーカー4種類「vans, new-balance, adidas, nike」の精度は10〜50%までと低い。今回は類似画像検索でこれらを上手く分類できるか試す。

まず4種類のデータセット画像を、Googleから適当に集めた。その後、画像を左右反転して水増し、合計928枚用意した。

# image dataset
# ex:new balance
path='/Users/desktop/imgs/new_balance'
new_b=[cv2.resize(cv2.imread(path+'/'+i),dsize=(150, 150)) for i in os.listdir(path)]
new_b=[preprocess_input(x) for x in new_b]
new_b=np.reshape(new_b, (len(new_b),150,150,3))
# 画像枚数水増し
flip=[cv2.flip(i, 1) for i in new_b]
new_bs = np.r_[new_b, flip]

# 他省略
# 4つを結合
ext_img = np.r_[new_bs, nikes, adidass, vanss]
print('total shape {}'.format(ext_img.shape))
>>>
(266, 150, 150, 3)
(244, 150, 150, 3)
(218, 150, 150, 3)
(200, 150, 150, 3)
total shape (928, 150, 150, 3)

# ラベル(0~3)も作成
ext_label = np.r_[new_bl, nikel, adidasl, vansl]
print('total lenth {}'.format(len(ext_label)))
>>>
total lenth 928

クエリ画像は以下の6枚。この画像と同じラベルの画像と似た画像を、データセット画像から検索できるか試す

# クエリラベル
(6, 150, 150, 3)
query image1 name    nike.jpeg
query image2 name   adidas2.jpeg
query image3 name   vans2.jpeg
query image4 name   vans1.jpeg
query image5 name   nikes.jpeg
query image6 name   adidas.jpeg
query_label:[1, 2, 3, 3, 1, 2]

・クエリ画像
f:id:trafalbad:20190306221251p:plain




2.訓練済みモデルで特徴抽出

特徴抽出は前述の通り深い層からより汎用的な特徴を抽出する方法。

以下の手順で特徴抽出して出力と入力画像から、新しいモデルを作成して、そのモデルで予測するまでをやってみる。

sess = tf.Session(config=tf.ConfigProto(allow_soft_placement=True, log_device_placement=False))
K.set_session(sess)
# Inception-resnet-v2呼び出す
train_model= InceptionResNetV2(Input((150, 150, 3)))

# 訓練済みモデル読み込み
train_model.load_weights('/Users/downloads/saved_model.h5')

# 畳み込み層の呼び出し
feature = train_model.get_layer('conv_7b')
print(type(feature))  # <class 'keras.layers.convolutional.Conv2D'>
print(feature.name, feature.output_shape) # conv_7b (None, 3, 3, 1536)

# Globalpooling層の追加
output = GlobalMaxPooling2D()(feature.output)
# 新しいモデル作成
model = Model(inputs=train_model.input, outputs=output)

print(model.output_shape) # (None, 1536)
model.summary()
"""
global_max_pooling2d_1 (GlobalM (None, 1536)           conv_7b[0][0]                    
================================
Total params: 54,332,128
Trainable params: 54,274,656
Non-trainable params: 57,472
"""
# predict()でデータセット画像の特徴抽出
features = model.predict(ext_img)
print(features.shape) # (1564, 1536)

# クエリ画像の特徴抽出
query = model.predict(query_imgs)
print('query shape:{}'.format(query.shape)) # query shape:(6, 1536)

これでデータセット画像とクエリ画像の特徴抽出ができた


3.PCAで次元削減について

PCAを行うのは計算量を減らすため。クエリ画像が数万枚〜数百万以上のときとか、特徴抽出した特徴量を使い、新しいモデルで再訓練するときに行う。

メルカリは多分、特徴抽出してから新しいモデルで再訓練してるから、そのときに次元削減してるかもしれない。

今回はPCAはしないけど、
・クエリ画像が数万枚以上あるとき

・特徴抽出して、新しいモデルで再訓練する

際のコードを試したので、書いとく

・クエリ画像が数万枚以上あるときのPCA実行

"""
クエリ画像が数千枚以上ある場合はPCAで圧縮して、高速化(今回はしない)
"""
from sklearn.decomposition import PCA
pca = decomp.PCA(n_components = 700)
pca.fit(features)

# 主成分分析の結果に基づき、次元削減する。
ex_features = pca.transform(features)
print('extracted feature shape {}'.format(ex_features.shape))

E = pca.explained_variance_ratio_        # 寄与率
cumsum_explained = np.cumsum(E)[::-1][0]  # 累積寄与率

print('累積寄与率{}'.format(cumsum_explained))
# extracted feature shape (928, 700)
# 累積寄与率0.9828264117240906

・特徴抽出後、PCAして(簡易なモデルを使い)再訓練する

# PCA
pca = decomp.PCA(n_components = 700)
pca.fit(features)
ex_features = pca.transform(features)

# ラベル
ext_label = np.r_[new_bl, nikel, adidasl, vansl]
n_labels = len(np.unique(ext_label))  
test_y=np.eye(n_labels)[ext_label]
test_y=np.reshape(test_y, (len(test_y), 4))

# train
ext_model=models.Sequential()
ext_model.add(Dense(2048, input_dim=400, activation='relu'))
ext_model.add(Dense(1024, activation='relu'))
ext_model.add(Dense(512, activation='relu'))
ext_model.add(Dense(256, activation='relu'))
ext_model.add(Dense(4, activation='softmax'))
ext_model.compile(optimizer=SGD(lr=0.01, momentum=0.9, decay=0.001, nesterov=True),
                    loss='categorical_crossentropy', metrics=['accuracy'])
history=ext_model.fit(ex_features, test_y, epochs=20, callbacks=callback)
"""
loss: 1.2685 → loss: 0.0013
acc: 0.4073 → acc: 1.0000
"""


4.faissで類似画像検索

あとはクエリ画像と似てる画像をデータセット画像から検索する。
jupyter上で試すので、まずfaissのインストール

$ conda install -c pytorch faiss-cpu

クエリ画像とデータセット画像と同じ次元数にする(dim=1536)。
あとは類似画像検索を実行。

import faiss
import collections

dim = 1536
index = faiss.IndexFlatL2(dim)   
index.add(features)

# Search
candidate=10
for q in query:
    D, I = index.search(np.expand_dims(q, axis=0), candidate)
    for i in I:
        print(ext_label[i])
        c = collections.Counter(ext_label[i])
        print(c)

パーフォマンス結果は以下の感じ
クエリ画像、そのラベル、そのスニーカータイプ

f:id:trafalbad:20190306221251p:plain
query_label:[1, 2, 3, 3, 1, 2]

query images namenike.jpeg, adidas2.jpeg, vans2.jpeg, vans1.jpeg, nikes.jpeg, adidas.jpeg

・各クエリ画像の検索結果

# candidate(検索数)=1のとき
[1]
Counter({1: 1})
[2]
Counter({2: 1})
[3]
Counter({3: 1})
[3]
Counter({3: 1})
[1]
Counter({1: 1})
[0]
Counter({0: 1})

# candidate=10のとき(左から類似してる順)

[1 1 2 0 2 2 1 1 0 2]
Counter({1: 4, 2: 4, 0: 2})
[2 2 2 0 2 2 2 2 2 2]
Counter({2: 9, 0: 1})
[3 3 3 3 3 2 3 3 3 3]
Counter({3: 9, 2: 1})
[3 1 3 3 3 1 3 3 1 3]
Counter({3: 7, 1: 3})
[1 2 0 3 0 1 1 1 1 0]
Counter({1: 5, 0: 3, 2: 1, 3: 1})
[0 1 3 3 3 1 2 2 0 0]
Counter({0: 3, 3: 3, 1: 2, 2: 2})

スニーカーがドアップのやつはちゃんと検索できてる。最後のスニーカーが2枚写ってるadidasのは、

・データセットに含まれてない

・データセットが足りないか
のどちらかで、多分前者。

結果的に、前回の精度が10%〜50%くらいの画像を、ほぼ間違いなく検索できてる。



画像分類はend-to-endで学習したら終わりだと思ってた。けど「特徴抽出、ファインチューニング、転移学習」などをすることで、より精度が向上しそう。end-to-endで学習することしか画像分類手法としては知らなかったので、今回はいいアウトプットになった。


追記:GlobalAveragePooling2Dでの精度再検証
GlobalAveragePooling2Dは平均プーリング演算で、maxPoolingよりも、メモリ消費量が少なくて、識別率も同じか、少し上回る精度らしい。

特徴抽出をGlobalAveragePooling2Dに置き換えて、faissでの精度を再検証してみた。

・特徴抽出部分でGlobalAveragePooling2Dに変える

output = GlobalAveragePooling2D()(feature.output)
model = Model(inputs=train_model.input, outputs=output)

print(model.output_shape)
model.summary()

"""
# Total paramsはmaxPoolingと変化なし

conv_7b (Conv2D)                (None, 3, 3, 1536)   3194880     block8_10[0][0]                  
_____________________
global_average_pooling2d_1 (Glo (None, 1536)         0           conv_7b[0][0]                    
============
Total params: 54,332,128
Trainable params: 54,274,656
Non-trainable params: 57,472
"""


・faissでcandidate=10で再検証

[1 0 1 3 2 1 1 0 2 2]
Counter({1: 4, 2: 3, 0: 2, 3: 1})
[2 2 2 2 2 2 2 1 2 0]
Counter({2: 8, 1: 1, 0: 1})
[3 3 3 3 3 3 3 3 3 3]
Counter({3: 10})
[3 3 1 3 3 1 3 3 3 0]
Counter({3: 7, 1: 2, 0: 1})
[2 1 1 0 1 0 2 0 0 0]
Counter({0: 5, 1: 3, 2: 2})
[2 0 3 0 3 1 3 2 3 2]
Counter({3: 4, 2: 3, 0: 2, 1: 1})

query_label:[1, 2, 3, 3, 1, 2]
f:id:trafalbad:20190306221251p:plain


maxPoolingで間違えてた、「最後の画像=Adidasの2枚写ってる画像」が正確に判断できてる。

代わりに、最後から二番目のnikeの画像が間違えてる、多分ロゴマークが鮮明でないからだと思う。でも上位3つ出す使い方すれば、当ってる事になるし、GlobalAveragePooling2Dの方がパフォーマンス的にベターみたい。

InceptionResNetV2も本来、使ってるのはMaxPoolingではなくGlobalAveragePooling2Dだし、本来の精度出すにはGlobalAveragePooling2D使った方がいい。



参考サイト
Facebook Researchのfaissで類似検索
Deep Features と Faiss

kubenetesでDjangoのアプリをデプロイする手順と作業ログ(on GKE)

pythonフレームワークでflaskが人気みたいだけど、せっかくDjango勉強したし、フレームワーク的に仕組みも同じなので、どうせならDjangoで作ったアプリをGKE上でデプロイしようと思い、やってみた。

前回、ローカルで画像の予測ラベルを表示するDjangoのアプリを作ったので、それを使用。

これに、redisとceleryのworker処理を加えた。ローカルで動かすとredis 用、worker用、Django用に3つのターミナルを使って処理を行う。

f:id:trafalbad:20190216004311p:plain

ローカルではちゃんとworker処理が行われて予測ラベルが表示される。worker部分は本題でないので詳細はコードを参照。

やったことはdjango_redisなるライブラリを使って、だいたい
このサイト


celeryチュートリアル
を参考にして作った。


けど、worker処理ありのDjangoをGKE上でデプロイしようとすると、redis用のpodを作るあたりでうまくいかない。

なのでworker処理なしの、前作った普通の画像ラベル予測用のDjangoアプリをGKE上でデプロイすることにした。


GCPでのDjangoデプロイの全体像
f:id:trafalbad:20190330134444j:plain


そのデプロイ手順と作業ログを書いていく。

目次
1.DjangoをGKE上でデプロイする

2.workerジョブのDjangoをGKE上でデプロイしようとした時の作業ログ


f:id:trafalbad:20190215203319j:plain

1.DjangoをGKE上でデプロイする

worker処理のない普通のDjangoアプリをデプロイ

worker処理を行うときは、フロントエンドに直結するファイルのコードに、worker処理用のdelayメソッドを使う。
今回、フロントエンドに直結するのはmain.pyというファイル。worker処理ありとなしのコードは以下の通り。

・main.py(worker処理なし)のdelayメソッド部分

from image_pred.tasks import add_numbers   # workerから関数の呼び出し

def pred(img_path):
    x=np.asarray(Image.open(img_path))
    x=np.resize(x,(150,150,3))
    x = np.expand_dims(x, axis=0)
    image = preprocess_input(x)
    image_dump=base64.b64encode(image.dumps()).decode()
    id = add_numbers(image_dump)
    return id

・main.py(worker処理あり)のdelayメソッド部分

from image_pred.tasks import add_numbers # workerから関数の呼び出し
def pred(img_path):
    x=np.asarray(Image.open(img_path))
    x=np.resize(x,(150,150,3))
    x = np.expand_dims(x, axis=0)
    image = preprocess_input(x)
    image_dump=base64.b64encode(image.dumps()).decode()
    id = add_numbers.delay(image_dump)
    time.sleep(70) # 70秒待つ
    ids=id.result
    return ids


delayメソッドを使うか、使わないかだけで、他のworker用のceleryファイルはそのままにしてある。



GKE上でデプロイするためにローカル用のDjangoの変更点

GKE上でデプロイするにはGoogleのチュートリアルを参考にして、足りないところをDjangoアプリのサンプルコードを使って改良した。

これをもとに自分のDjangoのコードで変更したところを列挙してく。

Djangoのproject名:image_pred(チュートリアルの場合、mysite)
アプリ名:myapp(チュートリアルではpolls)

・DockefileのCMDコマンドを変更

CMD gunicorn -b :$PORT image_pred.wsgi



・polls.yamlの内容を変更

・metadata.name
・metadata.labels.app
・spec.metadata.app

の三ヶ所をpollsからmyappに変更



・必要ファイルの必要箇所を追記・変更

・polls.admin.py

・mysite.urls.py

・mysite.settings.py

の内容を追記変更



・myapp/migrations/0001_initial.pyを追加 & 内容変更

githubのpolls/migrations/0001_initial.pyと同じように、0001_initial.pyのmygrations.AddField()内の以下の部分を変更

"to='polls.Question')"
# 以下に変更(pollsを自分のアプリ名に変更する)。

"to='myapp.Question')"


このファイルがないと

$python manage.py makemigrations

実行時にうまくいかない。


他は足りないファイルを追加したりした。


GKE上でデプロイする手順

あとはGKE上でデプロイするための手順を書いてく。
リージョン:us-central1 、ゾーン:us-central1-c、インスタンス:myapp-bccプロジェクト名:sturdy-willow-167902、connectionName の値:sturdy-willow-167902:us-central1:myapp-bcc

1.ローカルにDjango アプリを作成 (at local)

2.ローカル環境の設定

# SQLプロキシをインストールする at local(macos64ビット)
$ curl -o cloud_sql_proxy https://dl.google.com/cloudsql/cloud_sql_proxy.darwin.amd64 && chmod +x cloud_sql_proxy

# Cloud SQL(PostgreSQL) インスタンスを作成する at GKE
"""
SQL→インスタンス作成→PostgreSQL選択→インスタンスID myapp-bcc パスワードk8s リージョン us-central1 設定オプションから[マシンタイプとストレージの設定]でcpu(コア数) 2とメモリ→[作成]
"""

# connectionNameの確認
"""
「SQL」をクリック、インスタンスの一覧が表示されるので、使用するインスタンスをクリック、概要の下にある「インスタンス接続名」があるので確認(sturdy-willow-167902:us-central1:myapp-bcc)
"""

# 新しい Cloud SQL ユーザーとデータベースの作成 at GKE
・DB(gcloud sql databases create [DATABASE_NAME] ―instance=[INSTANCE_NAME])
$ gcloud sql databases create redis --instance=myapp-bcc

・DBユーザーを作成する(by GCU)
"""
GCPメイン画面で、[SQL]を選択、データベースを作成したいインスタンスをクリック、ユーザー」タブをクリック、「ユーザーアカウントを作成」をクリック、「ユーザー名」(bc)と「パスワード」(k8s)を入力して「作成」
"""

3.サービス アカウント(ユーザー & パスワード)を作成する at GKE

「IAMと管理」の中から「サービスアカウント」を選択、上部の「サービスアカウントを作成」をクリック


1.サービスアカウント名(bc)
2.役割に「Cloud SQL クライアント」を設定して「続行」をクリック
3.「キーを作成」をクリックし、キーのタイプにて「JSON」を選択して「作成」をクリックと認証ファイルがダウンロードされる
4.キーが表示されていることを確認して、「完了」をクリック
5. サービスアカウントの一覧に戻るので、作成したアカウントが存在することを確認する
6.JSONキーをmanage.pyと同じパスに移動させる

# Cloud SQL インスタンスを初期化する at local
# プロキシの開始コマンド
$ ./cloud_sql_proxy -instances=インスタンス接続名=tcp:5432 -credential_file=key.json

# 例
$ ./cloud_sql_proxy -instances=sturdy-willow-167902:us-central1:myapp-bcc=tcp:5432 -credential_file=sturdy-willow-167902-beaf39d3bdbd.json


4.データベースを構成する at local

# ローカルでDBにアクセスするため環境変数を設定(DBユーザー名作成時の「ユーザー名」(bc)と「パスワード」(k8s)を使う)
$ export DATABASE_USER=bc && export DATABASE_PASSWORD=k8s

5.GKE を構成する at local

# マニフェストファイルpolls.yaml で、以下のところを変更
your-project-id =>実際のプロジェクトID(プロジェクトID:sturdy-willow-167902)

your-cloudsql-connection-string => connectionName の値: sturdy-willow-167902:myapp-bcc


6.ローカルコンピュータでアプリを実行する

# 孤立した Python 環境を作成し、依存関係をインストール(at local)
$ brew update && brew install mysql   # mysqlをインストール
$ virtualenv venv && source venv/bin/activate && pip install -r requirements.txt


# モデルを設定するために Django の移行を実行 at GKE
"""
=>Cloudsql APIを有効にする
APIとサービス -> ダッシュボード -> APIを有効にする -> 「cloud sql api」 で検索 -> 有効にするをクリック

=> ローカルのDjangoファイルのmysite.settings.pyのDATABASEのNAMEをredisに変更 at local
"""

# celery用のデータベースマイグレーション 
$ python manage.py migrate django_celery_results

# マイグレーションファイルを作成 & 実行(データベースの同期)
$ python manage.py makemigrations && python manage.py migrate 

# 試しにローカル ウェブサーバーを起動
$ python manage.py runserver  # 「http://127.0.0.1:8000/myapp/」 にアクセス & Ctrl+Cキーで停止

参考
Cloudsql APIを有効にする
django_celery_results用のマイグレーション



7.Django 管理コンソールを使用する at local

# スーパーユーザー(管理画面を利用できる管理者ユーザー)の作成
$ python manage.py createsuperuser

# 実行
$ python manage.py runserver
# 「http://127.0.0.1:8000/admin/」にアクセス

# UsernameとPasswordを入力し、管理サイトにログイン

8.アプリを GKE にデプロイする at GKE

# Cloud Storage バケットを作成 & 公開
$ gsutil mb gs://polls-images && gsutil defacl set public-read gs://polls-images

# => gcloudコマンドインストールし、ローカルで実行 (gsutil rsync -R static/ gs://<your-gcs-bucket>/static)&  staticフォルダにあるmyappに移動
$ cd myapp
$ gsutil rsync -R static/ gs://polls-images/static

# mysite/settings.py で、STATIC_URL の値をこの URL「http://storage.googleapis.com/<your-gcs-bucket>/static/」に設定
=http://storage.googleapis.com/polls-images/static/

# GKE クラスタを作成 at GKE
$ gcloud container clusters create myapp --scopes "https://www.googleapis.com/auth/userinfo.email","cloud-platform" --num-nodes 4 --zone "us-central1-c"


# GKEクラスタ操作権限get(kubectl が正しいクラスタとやり取りするように構成されていることを確認)
$ gcloud container clusters get-credentials myapp --zone "us-central1-c"


# シークレットの作成=> JSONキーアップロード & アクセス用にシークレットを作成
$ kubectl create secret generic cloudsql-oauth-credentials --from-file=credentials.json=sturdy-willow-167902-beaf39d3bdbd.json

# データベース アクセスに必要なシークレット作成
$ kubectl create secret generic cloudsql --from-literal=username=bc --from-literal=password=k8s

# configmap作成
$ kubectl create configmap redishost --from-literal=REDISHOST=10.0.0.3
# configmap確認
$ kubectl get configmaps redishost -o yaml


# Cloud SQL プロキシのパブリック Docker イメージを取得 at GKE
$ docker pull b.gcr.io/cloudsql-docker/gce-proxy:1.05

# From here command run at local

# Docker イメージを作成(each Dockerfileを移動)
$ docker build -t gcr.io/sturdy-willow-167902/myapp .

# 認証ヘルパーとしてgcloud を使用するようにdockerを構成。GCRにイメージを pushできる
$ gcloud auth configure-docker

# docker push
$ docker push gcr.io/sturdy-willow-167902/myapp

# GKE上でpod.yamlをアップロード at GKE
$ kubectl create -f myapp.yaml


# 動作確認 at GKE
$ kubectl get pods or $ kubectl describe pod <your-pod-id>
$ kubectl get services myapp

# 'http://[EXTERNAL-IP]/myapp/'にアクセス。

ちゃんとローカルと同じくデプロイできた。

f:id:trafalbad:20190216133734p:plain


2.worker処理ありのDjangoをGKE上でデプロイしようとした時の作業ログ

worker処理ありのDjangoアプリ(ローカルでworker(celery)、redis、Django用にターミナルを3つ使って行うやつ)をGKE上でデプロイしようと思ったけど、GKE上でredisへのアクセスがうまくいかないため、「GKE上でredisを使うためにやった作業」のログ。

ちなみにcelery用ファイルはimage_predフォルダ内に入れてあるので、celeryのworker起動用のファイルは以下の通り。

celery.yamlcelery_docker/Dockefile、run.sh、celery.py


実験1.Dockerfileでapt-getでredis-serverインストール & CMDで起動


エラー1:なぜか、レスポンス返ってこない。

エラー2:Dockefile一つにCMDコマンドでredis-server、celery、Django3つ一気に起動するのはNG。
多分、ローカルでターミナル3つ必要だったから、Dockerでも一気に3つ起動は無理だからだと思う。



実験2.redisインスタンスを使用する(k8sクラスタはipエイリアスとそうじゃないの両方試した)


Googleチュートリアル通りにやった前の記事を参考に、redisインスタンスを設定し、「redis://10.0.0.3:6379」にアクセス。

エラー1:IPの範囲設定を前の記事通りすると、サーバにアクセスできない

エラー2:IP範囲設定せずに、configmap使ったらアクセスできたものの、結果が返ってこない。おそらくredisインスタンスにアクセスできてない



実験3.redisクラスターの使用


以下の手順でredisクラスターを作成

# githubからclone
$ git clone https://github.com/sanderploegsma/redis-cluster

# GCPにファイルアップロードし、起動
$ kubectl apply -f redis-cluster.yaml

# 下記コマンド実行してredisクラスターを作成
$ git clone https://github.com/antirez/redis.git && cd redis && make && sudo make install
$ apt-get update && apt-get install ruby vim wget redis-tools && gem install redis && wget http://download.redis.io/redis-stable/src/redis-trib.rb && chmod 755 redis-trib.rb

# podが6つできるので、実行
$ kubectl exec -it redis-cluster-0 -- redis-cli --cluster create --cluster-replicas 1 $(kubectl get pods -l app=redis-cluster -o jsonpath='{range.items[*]}{.status.podIP}:6379 ')

# 本来、マニフェストファイル内では、cluster IPがredisのIPになる

エラー:なぜか今まで発生してないpandasのエラーがでてきて、解決策講じたものの通用しないので中断


この記事だと、一番現実的なのはredisクラスターかなと思った。k8s用のredisクラスターは事例がgithubにたくさんある。

まあ仕組み理解しないとこの問題は解決しないなと感じた。

今回は普通のDjangoアプリケーションをGKE上でデプロイするまでの内容 & 手順と、ちょっとした作業ログのまとめを備忘録もかねて書いた。



個人的メモ

Djangowsgi.pyファイルに関係する「サーバソケット」について

ソケットは一般にクライアントとサーバーの対話で使用される。
クライアントとサーバーとの間のデータ交換は、 クライアントがソケットを経由してサーバーに接続しているときに行われる。



・gunicornとその-bオプション(-b BIND, --bind=BIND )について

Gunicornとは、Python製のWSGIサーバ(WSGI(Web Server Gateway Interface)でサポートされているWebサーバとWebアプリケーションをつなぐサーバ)。

pipでインストールして、

$ gunicorn myproject.wsgi

とすれば、走る。

− bオプションはバインド(結合)するサーバソケットを指定する。

サーバソケット(BIND)は「$(HOST)」、「$(HOST):$(PORT)」 or 「unix:$(PATH)」で指定。IPは有効なものを指定。


・dockerイメージ全削除コマンド

$ docker images -aq | xargs docker rmi


・プロキシサーバについて

Webブラウザ)の代わりに、ホームページにアクセスしてくれるコンピュータ(サーバ)のこと。

f:id:trafalbad:20190325114444p:plain

ここでは、kubenetesとローカルマシンを繋いでくれる中継地点の役割を果たしてる