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

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

自然言語処理タスクでいろんなRNN系ニューラルネットでの精度を検証してみた【keras・機械学習】

深層学習を使った自然言語処理のタスクで、ネガポジの2値分類をやった。


その際、自然言語処理向けのいろんなRNN系のニューラルネットワーク(NN)を使ったので、各NNの精度を順にまとめてく。

ちなみに、
・不均衡データに対して損失関数でチューニングする方法

・大容量データ対策

もまとめた


f:id:trafalbad:20190326080327p:plain


目次
・使うデータセット
1.LSTMだけ
2.GRUだけ
3.GRU+dropout + recurrent_dropout
4.GRUベースの双方向RNN
5.1次元CNN
6.不均衡データの精度を損失関数(class-balanced-loss)で上げる方法
・最後に:大容量データ対策にfit_generater()
・追記:LSTM層(GRU層)の多層化で精度再検証


使うデータセット

使うデータは「IMDbデータセット」という映画に関するレビュー文章。

すでに文章はidベクトル化されてて、それのネガポジ判定の2値分類をした際の、NNごとの精度を見ていく。

入力形式は自然言語処理で使うembbedingレイヤーを使うため、ゼロパディングして、ミニバッチ化した。やり方は下記記事を参照。

trafalbad.hatenadiary.jp


IMDbデータセットの「訓練データ数、テストデータ数、共通するパラメータ条件.etc」は以下の通り。

・trainデータ:2000こ(ゼロパディング済み)

・testデータ:1000こ(ゼロパディング済み)

・epoch=5

・batch_size=128

・validation_split=0.2

・ネガポジ判定の2値分類


1.LSTMだけ

まず定番、LSTM。

from keras.datasets import imdb
from keras.preprocessing import sequence
import os
import math
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from keras.optimizers import SGD, RMSprop, Adam
from keras.callbacks import History, LearningRateScheduler, Callback
from keras import layers
from keras.models import Model, Sequential
from keras.layers import Dense, Input, Lambda, LSTM, GRU, Embedding


max_words=88585 # 単語のインデックスの数
model = Sequential()
model.add(Embedding(max_words, 32))
model.add(LSTM(32))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(x_train, y_train,
                    epochs=5, batch_size=128, validation_split=0.2, callbacks=callback)

# 精度検証
_, acc = model.evaluate(x_test, y_test, verbose=1)
print('\nTest accuracy: {0}'.format(acc))


精度は72%。

simple-RNNよりは確実にいい。


2.GRUだけ

max_words=88585 # 単語のインデックスの数
model = Sequential()
model.add(Embedding(max_words, 32))
model.add(GRU(32))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(x_train, y_train,
                    epochs=5, batch_size=128, validation_split=0.2, callbacks=callback)

精度は72.6%。

GRUはLSTMより計算量が少なく、速い。 LSTMとほぼ変わらないけど、計算量少ないからGRU使ってる人が多いのかな。



3.GRU+dropout + recurrent_dropout

今度は過学習防止(dropoutは偶発的な相関関係を破壊する効果がある)のため、dropoutとRNN専用dropoutの「recurrent_dropout」を設定。

max_words=88585 # 単語のインデックスの数
model = Sequential()
model.add(Embedding(max_words, 32))
model.add(GRU(32, dropout=0.1, recurrent_dropout=0.1,))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(x_train, y_train,
                    epochs=5, batch_size=128, validation_split=0.2, callbacks=callback)

精度は73.6%。

dropoutの最適値はいくつか知らないけど、とりあえず0.1あたりに設定。

データ数少ないから、精度にはほとんど反映されてないけど、過学習は確実に防げる。




4.GRUベースの双方向RNN

双方向RNNはBidirectional層を使う。

max_words=88585 # 単語のインデックスの数
model = Sequential()
model.add(Embedding(max_words, 32))
model.add(layers.Bidirectional(GRU(32)))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(x_train, y_train,
                    epochs=5, batch_size=128, validation_split=0.2, callbacks=callback)

精度78.2%。

LSTMやGRUより上がってる。ちなみにLSTMベースの双方向RNNも可能。

LSTMやGRUはベクトルを一方向から計算しないため、一方向の文章構造の特徴しか把握できない。

一方、双方向RNNは逆向きにもベクトルを計算するため、両方向の文章構造の特徴を把握できる。

NN系はベクトル構造による特徴把握が精度に大きく関わるので、逆向きでもベクトル構造の特徴が把握できれば、双方向RNNは普通のRNNより精度はいいことになる。

文章ならベクトルにしてしまえば、逆からでもベクトル構造の特徴に意味が含まれていることがほとんどなので、双方向RNNはより自然言語処理向けのRNNだといえる。



5.1次元CNN

max_words=88585
max_len=1629
model = Sequential()
model.add(layers.Embedding(max_words, 128, input_length=max_len))
model.add(layers.SeparableConv1D(32, 7, activation='relu'))
model.add(layers.MaxPooling1D(5))
model.add(layers.SeparableConv1D(32, 7, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(lr=1e-4),
              loss='binary_crossentropy',
              metrics=['acc'])


精度は49%。

1次元CNNについてわかったこと

1次元CNNは自然言語処理にはあまり向いてない

testデータの2番目の次元をtrainデータと同じにしなければ、予測に使えないため、面倒(x_train shape: (2000, 1629)
x_test shape: (1000, 1629))

気温予測とかの時系列データには、LSTMとかと遜色ない精度出てる

最近では普通の畳み込み層(Conv1Dとか)よりも、dw畳み込み層(SeparableConv1Dとか)を使った方が、「計算量も高く、表現力も高い」。  

なのでCNNでは、普通の畳み込み層より、dw畳み込み層使うべし。


自然言語処理だとRNN系のニューラルネットより、精度低いけど計算量少ない

一次元CNNとRNNを連結させ、長いシーケンスデータを処理(CNNでより汎用的な特徴を抽出した後、それをRNNで処理)するやり方もある



過学習防止のための、L2正規化(& L1正規化 & L2/L1正規化)のkerasでのやり方

# L2正規化 & L1正規化 &  L2/L1正規化
keras.regularizers.l2(0.)
keras.regularizers.l1(0.)
keras.regularizers.l1_l2(l1=0.01, l2=0.01)

# 利用例(https://keras.io/ja/regularizers/)
from keras import regularizers
model.add(Dense(64, input_dim=64, kernel_regularizer=regularizers.l2(0.01),
                activity_regularizer=regularizers.l1(0.01)))


6.不均衡データの精度を損失関数(class-balanced-loss)で上げる方法

今回はほぼ均衡データだけど、不均衡データ(クラス毎のデータ数に偏りがあるデータ)に対してのチューニング方法として、"class-balanced-loss"をがあるので、やってみる。

"class-balanced-loss"はデータ数が少ないクラスの損失関数に重み付けして、データ数が少ないクラスの精度を上げる方法。

やり方

# https://datascience.stackexchange.com/questions/13490/how-to-set-class-weights-for-imbalanced-classes-in-keras (参考記事)

# ラベルのarrayを作成(y_trainはラベル:0 label=506、1 label =494)
np.unique(y_train)
# >>> array([0, 1])

from sklearn.utils import class_weight

class_weights = class_weight.compute_class_weight('balanced', np.unique(y_train),  y_train)

class_weights
# >>> array([1.01071356, 0.98951118])


# model.fit()の引数のclass_weightに格納.
history = model.fit(x_train, y_train,
                    epochs=5, batch_size=128, validation_split=0.2, callbacks=callback, class_weight=class_weights)

本家記事のやり方だと、「現実のデータ数≠リアルのデータ数」として扱っているため、上のコードは本来の"class-balanced-loss"とは少し違う。

けど、チューニング方法としては大体同じ。



結果的に精度は

・双方向RNN => 78.2%

・LSTM => 72%

・GRU => 72.6%

・GRU+dropout + recurrent_dropout => 73.6%

・1次元CNN => 49%


=> 双方向RNN > GRU+dropout + recurrent_dropout > GRU ≒ LSTM > 1次元CNN

結論、「双方向RNNが一番精度が高く、自然言語処理に向いてる」。


ちなみに、データ量が多くてGPUによる分散処理をする場合、one-hotベクトルでNNに入力するとか、パディングなしでミニバッチ化せずにNNに入力すると、GPUの分散処理での学習のとき、文章の依存関係が壊れて精度が悪くなるらしい。

なので、embbedingレイヤーに入るとき、word embbedingでid表現かつゼロパディングしてミニバッチ化するのが、自然言語処理の深層学習テクとしては一般的な手法。


最後に、tree-RNNとか、attentionで拡張したRNNとかは翻訳、応答、文章生成あたりのガチ自然言語処理のタスクで使われてる事例があるけど、kerasで実装してる例がなかったので、今回は割愛した。


最後に:大容量データ対策にfit_generater()

大容量データ対策として、学習時に.fit()の代わりに、.fit_generater()を使う。例えば、画像データをよみこませるとき、一定サイズ毎に区切りながらデータを読み込せて、メモリ問題を解決できる。


.fit_generater()には、Generatorオブジェクト(バッチ単位にデータを提供する仕組みを実装したもの)を渡す(参考:Kerasで大容量データをModel.fit_generatorを使って学習する



自然言語処理でも使ってる事例があるので、データ量がでかいときにオススメ



追記:LSTM層(GRU層)の多層化で精度再検証
GRUベースの双方向RNNと、ただのLSTMを2層に(多層化)して、精度を再検証してみた。
なんか専門用語だとLSTM層の多層化のことを「層のスタック」とか言ってるらしい。

一層目の出力は完全なシーケンスデータを返さなきゃならないので、「return_sequences=True」を指定。

# 2層のLSTM(+ dropout + recurrent_dropout)
max_words=88585 # 単語のインデックスの数
model = Sequential()
model.add(Embedding(max_words, 32))
model.add(LSTM(32, return_sequences=True))
model.add(LSTM(32, dropout=0.1, recurrent_dropout=0.5))
model.add(Dense(1, activation='sigmoid'))


# 2層のGRUベースの双方向RNN( + dropout + recurrent_dropout)
model = Sequential()
model.add(Embedding(max_words, 32))
model.add(layers.Bidirectional(GRU(32, return_sequences=True)))
model.add(layers.Bidirectional(GRU(32, dropout=0.1, recurrent_dropout=0.5)))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(x_train, y_train, epochs=5, batch_size=128, validation_split=0.2, callbacks=callback)

精度は

・2層のLSTM(前の精度72%)
74%

・2層のGRUベース双方向RNN(前の精度78.2%)
78.6%

わかったこと

・多層化はdropoutを使ったとき、性能がボトルネックになるから行うことが多い

自然言語処理では多層でも隠れ層の値(今回は32)は全部の層で同じ

自然言語処理で2層目以降にrelu関数使ったら勾配消失した。(時系列データでは多層でも2層目以降に、relu関数使ってるケースあり。)

結果的に、多層化したら計算量上がるし、精度も上がる。

今回はデータセットが少量だから結果にほとんど反映されなかったけど、わかったことは「LSTMモデルは、精度が高いモデルは多層化してるし、多層化すればたいてい精度は上がる」ということ。

自然言語処理タスクにLSTM層の多層化はdropoutと併用すると、かなり効果あるようだ。


参考記事Kerasで実装するSeq2Seq_その3_多層LSTMとBidirectional

ニューラルネット(RNN, LSTM)で使う自然言語処理の単語埋め込み(word embedding)のやり方まとめ【機械学習】

ニューラルネット(RNNとかLSTM)で自然言語処理をするときに、embbedingレイヤーを使い、単語を入力する。そのとき、単語をidベクトルに変換する「単語埋め込み(word embedding)」という手法を使う。

簡単にいうと、従来の自然言語処理で使うone-hot表現とは違い、深層学習では単語をid表現でベクトル化する。

今回、単語埋め込み(word embedding)のやり方を備忘録もかねてまとめた。

f:id:trafalbad:20190323182023p:plain


目次
・tensorflow_hubで単語埋め込み
・パディングして単語埋め込み


tensorflow_hubで単語埋め込み


日本語の変換

日本語で埋め込みベクトル作成してみる。
tensorflow_hubでやってる例があったので、トライ。

# 日本語ベクトル化
import numpy as np
import tensorflow as tf
import tensorflow_hub as hub
from janome.tokenizer import Tokenizer

# text
example_sentences = [
    "京都の大学",
    "アメリカの美味しい食べ物",
    "機械学習の本",
    "ビル・ゲイツ",
    "御殿場市民"
]

# ベクトル変換
jtok = Tokenizer()

with tf.Graph().as_default():
    embed = hub.Module("https://tfhub.dev/google/nnlm-ja-dim128/1")
    embeddings = embed(list(map(lambda x: ' '.join([y.surface for y in jtok.tokenize(x)]), example_sentences)))

    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        sess.run(tf.tables_initializer())
        example_vectors = sess.run(embeddings)

"""
print('text shape{}'.format(example_vectors.shape))
>>> text shape(5, 128)

# example_vectors[1]の中身

array([ 0.21444249, -0.02066225, -0.02490281,  0.12042393, -0.02669256,
       -0.03639572,  0.0639141 , -0.07621422, -0.09123933,  0.04221497,
       -0.02266158,  0.07067862, -0.0404582 , -0.14392036,  0.02329277,
        0.09088391,  0.02312123,  0.03846002,  0.05741814,  0.00031251,
        0.02235819, -0.23327258,  0.00174309,  0.04039909,  0.01923054,
       -0.20671186,  0.04574473, -0.10783764,  0.15570977,  0.21124859,
        0.23662198,  0.08777227,  0.03669035,  0.02975237, -0.09071992,
       -0.07266812,  0.02674059,  0.03673555, -0.02911181, -0.1486303 ,
        0.02271459,  0.04228514,  0.02575765,  0.01484851, -0.00291231,
       -0.21089311, -0.00445587, -0.21334003, -0.12411128, -0.10119673,
        0.06045113, -0.09723218,  0.08770846, -0.12805086,  0.16502124,
       -0.07979961,  0.2203255 , -0.17222357,  0.01070272,  0.09691209,
       -0.03311934, -0.13294616, -0.14924897, -0.07744226, -0.01559774,
       -0.1402346 ,  0.22744502, -0.07018153,  0.05709712, -0.14845742,
        0.0601044 ,  0.06071291,  0.07477927,  0.02545806, -0.00027584,
        0.04564046, -0.20603304,  0.04277818,  0.07747093,  0.00619286,
        0.14053614, -0.02086988, -0.13657984,  0.03583155, -0.0381945 ,
       -0.15456699, -0.04663824,  0.1366553 , -0.03684065, -0.2111983 ,
       -0.01449677, -0.12352285, -0.03340601,  0.1493544 , -0.11698331,
       -0.04235147, -0.20047963,  0.06850106, -0.00192337,  0.08337143,
        0.0665336 ,  0.06508755, -0.06783675,  0.01749612, -0.02375472,
       -0.04449525, -0.10569633,  0.01875219, -0.0829886 ,  0.03253315,
       -0.01677698,  0.08705967,  0.05160309, -0.06960055, -0.06620288,
       -0.05360216,  0.11966458,  0.01819556,  0.05795261, -0.13429345,
       -0.11908479, -0.0697221 , -0.09247562, -0.02146355,  0.03899785,
       -0.01095748,  0.06306917, -0.01096421], dtype=float32)
"""

中身を見ると、単語がidではなく、ベクトルで表現されてる。embbedingレイヤーに突っ込むときは下のようにした。

# ベクトルのmaxとmin
print(example_vectors.max(), example_vectors.min())     
# max=0.31192207,   min= -0.23901775


# Embeddingレイヤーに入れる時
max=1

model = Sequential()
model.add(Embedding(max, 32))
model.add(LSTM(32))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(example_vectors, y_train[:5],
                    epochs=5, batch_size=128, validation_split=0.2, callbacks=callback)

パディングして単語埋め込み
パディングはマトリックスの行(横)の長さを揃えて、ミニバッチ化する手法。主にゼロでパディングする。



普通の英文

以下の英文を単語埋め込みで変換してみる。

data = [
    ["Could I exchange business cards, if you don’t mind?", 1],
    ["I'm calling regarding the position advertised in the newspaper.", 0],
    ["I'd like to apply for the programmer position.", 0],
    ["Could you tell me what an applicant needs to submit?", 1],
    ["Could you tell me what skills are required?", 1],
    ["We must prepare our financial statement by next Monday.", 0],
    ["Would it be possible if we check the draft?", 1],
    ["The depreciation of fixed assets amounts to $5 million this year.", 0],
    ["Please expedite the completion of the balance sheet.", 0],
    ["Could you increase the maximum lending limit for us?", 1],
    ["We should cut down on unnecessary expenses to improve our profit ratio.", 0],
    ["What percentage of revenue are we spending for ads?", 1],
    ["One of the objectives of internal auditing is to improve business efficiency.", 0],
    ["Did you have any problems finding us?", 1],
    ["How is your business going?", 1],
    ["Not really well. I might just sell the business.", 0],
    ["What line of business are you in?", 1],
    ["He has been a valued client of our bank for many years.", 0],
    ["Would you like for me to show you around our office?", 1],
    ["It's the second door on your left down this hall.", 0],
    ["This is the … I was telling you about earlier.", 0],
    ["We would like to take you out to dinner tonight.", 0],
    ["Could you reschedule my appointment for next Wednesday?", 1],
    ["Would you like Japanese, Chinese, Italian, French or American?", 1],
    ["Is there anything you prefer not to have?", 1],
    ["Please give my regards to the staff back in San Francisco.", 0],
    ["This is a little expression of our thanks.", 0],
    ["Why don’t you come along with us to the party this evening?", 1],
    ["Unfortunately, I have a prior engagement on that day.", 0],
    ["I am very happy to see all of you today.", 0],
    ["It is a great honor to be given this opportunity to present here.", 0],
    ["The purpose of this presentation is to show you the new direction our business is taking in 2009.", 0],
    ["Could you please elaborate on that?", 1],
    ["What's your proposal?", 1],
    ["That's exactly the point at issue here.", 0],
    ["What happens if our goods arrive after the delivery dates?", 1],
    ["I'm afraid that's not accpetable to us.", 0],
    ["Does that mean you can deliver the parts within three months?", 1],
    ["We can deliver parts in as little as 5 to 10 business days.", 0],
    ["We've considered all the points you've put forward and our final offer is $900.", 0],
    ["Excuse me but, could I have your name again, please?", 1],
    ["It's interesting that you'd say that.", 0],
    ["The pleasure's all ours. Thank you for coimng today.", 0],
    ["Could you spare me a little of your time?", 1],
    ["That's more your area of expertise than mine, so I'd like to hear more.", 0],
    ["I'd like to talk to you about the new project.", 0],
    ["What time is convenient for you?", 1],
    ["How’s 3:30 on Tuesday the 25th?", 1],
    ["Could you inform us of the most convenient dates for our visit?", 1],
    ["Fortunately, I was able to return to my office in time for the appointment.", 0],
    ["I am sorry, but we have to postpone our appointment until next month.", 0],
    ["Great, see you tomorrow then.", 0],
    ["Great, see you tomorrow then.", 1],
    ["I would like to call on you sometime in the morning.", 0],
    ["I'm terribly sorry for being late for the appointment.", 0],
    ["Could we reschedule it for next week?", 1],
    ["I have to fly to New York tomorrow, can we reschedule our meeting when I get back?", 1],
    ["I'm looking forward to seeing you then.", 0],
    ["Would you mind writing down your name and contact information?", 1],
    ["I'm sorry for keeping you waiting.", 0],
    ["Did you find your way to our office wit no problem?", 1],
    ["I need to discuss this with my superior. I'll get back to you with our answer next week.", 0],
    ["I'll get back to you with our answer next week.", 0],
    ["Thank you for your time seeing me.", 0],
    ["What does your company do?", 1]]


以下の手順で単語をidに変換、-1でパディングして、マトリックスを作成するまで。

# 英語textを単語IDでベクトル化

N = len(data)
data_x, data_t = [], []
for d in data:
    data_x.append(d[0]) # 文書
    data_t.append(d[1]) # ラベル
    
    
def sentence2words(sentence):
    stopwords = ["i", "a", "an", "the", "and", "or", "if", "is", "are", "am", "it", "this", "that", "of", "from", "in", "on"]
    sentence = sentence.lower() # 小文字化
    sentence = sentence.replace("\n", "") # 改行削除
    sentence = re.sub(re.compile(r"[!-\/:-@[-`{-~]"), " ", sentence) # 記号をスペースに置き換え
    sentence = sentence.split(" ") # スペースで区切る
    sentence_words = []
    for word in sentence:
        if (re.compile(r"^.*[0-9]+.*$").fullmatch(word) is not None): # 数字が含まれるものは除外
            continue
        if word in stopwords: # ストップワードに含まれるものは除外
            continue
        sentence_words.append(word)        
    return sentence_words


# 単語辞書
words = {}
for sentence in data_x:
    sentence_words = sentence2words(sentence)
    for word in sentence_words:
        if word not in words:
            words[word] = len(words)
            
# 文章を単語ID配列にする
data_x_vec = []
for sentence in data_x:
    sentence_words = sentence2words(sentence)
    sentence_ids = []
    for word in sentence_words:
        sentence_ids.append(words[word])
    data_x_vec.append(sentence_ids)
    
    
# 文章の長さを揃えるため、-1でパディングする
max_sentence_size = 0
for sentence_vec in data_x_vec:
    if max_sentence_size < len(sentence_vec):
        max_sentence_size = len(sentence_vec)
for sentence_ids in data_x_vec:
    while len(sentence_ids) < max_sentence_size:
        sentence_ids.insert(0, -1) # 先頭に追加
        
# arrayに変換
data_x_vec = np.array(data_x_vec, dtype="int32")
data_t = np.array(data_t, dtype="int32")


"""
# print(data_x_vec.shape)
>>> (65, 18)

# data_x_vecのマトリックスの中身

array([[ -1,  -1,  -1, ...,   6,   7,   4],
       [ -1,  -1,  -1, ...,  12,  13,   4],
       [ -1,  -1,  -1, ...,  19,  11,   4],
       ...,
       [ -1,  -1,  -1, ...,  35, 236,   4],
       [ -1,  -1,  -1, ..., 243,  21,   4],
       [ -1,  -1,  -1, ..., 259, 260,   4]], dtype=int32)
"""


keras メソッドを使う

kerasメソッドを使えば、簡単にゼロパディングして、id変換のベクトル化ができる。

今回はIMDbデータセットという、映画のレビューを使う。

すで単語がidに変換されており、kerasメソッドを使えば、簡単にembbedingレイヤーの入力形式に変換可能。

# IMDbデータセット
from keras.layers import Embedding
from keras.datasets import imdb

# すでにID変換されてる映画レビューのtextをロード
(x_train, y_train), (x_test, y_test) = imdb.load_data()
x_train=x_train[:2000]
y_train=y_train[:2000]
"""
#  x_train[0]の中身

array([list([1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 22665, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 21631, 336, 385, 39, 4, 172, 4536, 1111, 17, 546, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2025, 19, 14, 22, 4, 1920, 4613, 469, 4, 22, 71, 87, 12, 16, 43, 530, 38, 76, 15, 13, 1247, 4, 22, 17, 515, 17, 12, 16, 626, 18, 19193, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2223, 5244, 16, 480, 66, 3785, 33, 4, 130, 12, 16, 38, 619, 5, 25, 124, 51, 36, 135, 48, 25, 1415, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 10311, 8, 4, 107, 117, 5952, 15, 256, 4, 31050, 7, 3766, 5, 723, 36, 71, 43, 530, 476, 26, 400, 317, 46, 7, 4, 12118, 1029, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 7486, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 5535, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 5345, 19, 178, 32]),
       以下省略
"""


embbedingレイヤーに入れるときは、マイナスではなく、ゼロでパディング。

# kerasのsequenceメソッドで簡単に変換

# 変換前のx_trainは長さ2000のlist
print(len(x_train))
#  >>> 2000


# ゼロパディングでID変換
from keras.preprocessing import sequence
x_train = sequence.pad_sequences(x_train)

# 変換後のマトリックスのshape
print(x_train.shape)
# >>>(2000, 1038)

"""
変換後のx_trainの中身
>>>
array([[    0,     0,     0, ...,    19,   178,    32],
       [    0,     0,     0, ...,    16,   145,    95],
       [    0,     0,     0, ...,     7,   129,   113],
       ...,
       [    0,     0,     0, ...,     9,    35,  2384],
       [    0,     0,     0, ...,    61, 12599, 19290],
       [    0,     0,     0, ...,    18,     6,   250]], dtype=int32)
"""


今回単語埋め込みは

・tensorflow_hubでベクトルに変換(多分、正規のやり方じゃない)

・numpyとかでまともにパディングしてid変換

・kerasのメソッド使って簡単にゼロパディングして、id変換、マトリックス作成

の3つを試してみた。

ちなみに使ったRNNの出力形式は2値分類。

参考記事

Facebook Researchのfaissで類似検索

CNN、RNNで文章分類を実装してみた

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とローカルマシンを繋いでくれる中継地点の役割を果たしてる

2017,18年で便利だったpython, sql,linuxとかのコマンド・コードまとめ

2017〜2018年で頻繁に使ったコードのまとめで、個人的備忘録。
これからも追記はしてきます。

pandas

Q1.pandasで特定の列だけ演算をする場合

下のようなDataFrame(df)で、特定の列('c')を-1する場合。

# df
   a, b, c, d, e
0  1  2  3  2  2
1  2  2  3  2  1
2  3  3  3  4  4
3  2  4  5  3  1


A.apply()を使う

new_df['c'] = df['c'].apply(lambda x:x-1)


Q2.pandasのdataframeをリストの中に辞書を挿入した形式にする方法

>>> import pandas as pd
>>> df = pd.DataFrame({"A":[1,2,3], "B":[4,5,6], "C":[7,8,9]})
>>> df
   A  B  C
0  1  4  7
1  2  5  8
2  3  6  9

>>> tmp = df.to_dict(orient="index")
>>> [tmp[i] for i in df.index]
[{'B': 4, 'A': 1, 'C': 7}, {'B': 5, 'A': 2, 'C': 8}, {'B': 6, 'A': 3, 'C': 9}]   



# 例 factrization machines
[ {'user_id': '23', 'movie_id': '12', 'occupation':'11', 'zip_code': '11'}, 
 {'user_id': '34', 'movie_id': '22', 'occupation':'22', 'zip_code': '33'}, 
 {'user_id': '445', 'movie_id': '33', 'occupation':'44', 'zip_code': '22'}, ]


Q3.列の内容が同じDataFrame同士を連結する方法

A.pandas.merge()を使う

pandas.DataFrameを結合するmerge, join





Q4.pandasでDataFrameの特定の列の値の重複数を入れた新たな列を作成する方法

# df
    x    y
# 0  AA  100
# 1  AA  200  
# 2  AA  200  
# 3  BB  100
# 4  BB  200
# 5  CC  300


そこからdfの列xの該当する値に重複数を入れた新たな重複数の列(z)を含んだ行列(dff)を作る。

# dff
    x     y       z
0  AA  100  3
1  AA  200  3
2  AA  200  3
3  BB  100  2
4  BB  200  2 
5  CC  300  1

A.groupby().transform('count')

重複数のカウントが簡単にできる

import pandas as pd
df = pd.DataFrame({
    'x':['AA','AA','AA','BB','BB','CC'],
    'y':[100,200,200,100,200,300]})

df['z'] = df.groupby('x').transform('count')
print(df)
     x    y      z
0  AA  100  3
1  AA  200  3
2  AA  200  3
3  BB  100  2
4  BB  200  2
5  CC  300  1


Q5.行数の合わない2つのlistをpandasのDataFrameで表にする方法

listA = ['A', 'B', 'C', 'D', 'E', 'F']
listB = ['a', 'b', 'c', 'd']

pd.DataFrame([listA, listB], columns=list(‘ABCDEF'))





Q6.pandasの欠損値以外を1に変える方法

pandasのリスト(図)を作成し、欠損値以外は文字が表示されている。
欠損値を0に変えたあと、文字を全て1に変える処理をしたい場合。

A.notnull()で欠損値以外を検索し、.fillna()で欠損値を指定した値で埋め込む

下記の例は欠損値以外を1に置き換えた後、欠損値を0に置き換えてる

import pandas as pd
from numpy import nan

df = pd.DataFrame([['a', nan], [nan, 'b']])
df[df.notnull()] = 1
df.fillna(0, inplace=True)

Q7.pandasの特定の列を最後に移動させる方法

pandasで表を作ったとき、特定の列の値を最後に移動させる方法。

例:下の図だとt列を最後に移動させる


A.除外してあとで付け加える

参考=>pandasのデータフレームの列を入れ替える

col = df.columns.tolist() # 列名のリスト
col.remove('t') # 't'を削除 ※列名は重複していないものとする
col.append('t') # 末尾に`t`を追加
df = df.ix[:,col]
#df = df[col] でもよい

Gitコマンド


Q8.githubにcommitする方法

# 既存レポジトリ対象のcommand 
git clone URL 
cd jupyter_note(cloneしたフォルダ) 
mkdir detecting_trend_words 
cd detecting_trend_words #ファイル移動 
cp change_detection.ipynb detecting_trend_words/change_detection.ipynb 
git add . 
git commit -m "コメント” 
git push origin master
# 特定のファイルの削除
git reset 
git rm 'ファイル名' 
git commit -m "コメント” 
git push origin master

SQL


Q9.Bigquery(SQL)で5年間分の検索単語数を1日おきに抽出する方法

google bigquery で検索ワードを以下の条件でSQLで出す。


・指定期間は5年間
・1時間ごとの単語数をcountする
・1列で1時間分の「単語数、その時、ワード」の3行分のマトリックスを作成

LSTM用には時系列データとしてshape=(365×5×24,)(例:batch,time_window,dim=365×5,3,21)
の形で抽出するのが目的。


ちなみに、5年分の単語抽出のSQLは以下の通り。

# original_keywords:単語
# time:検索ワードが入力された時間

SELECT original_keywords, time AS t
from TABLE_DATE_RANGE(search_logs.search_log_, TIMESTAMP ('2012-6-10'), TIMESTAMP ('2017-6-10') )
WHERE original_keywords IS NOT NULL


A.集計関数を利用すれば可能


timeがTIMESTAMP型ならこんな感じ。

#standardSQL

SELECT
  original_keywords,
  format_time("%Y%m%d%H", time) AS hours,
  COUNT(*) AS cnt   # ワード、時間、ワード数
fROM  search_logs.search_log_*
WHERE original_keywords IS NOT NULL
AND _TABLE_SUFFIX BETWEEN '20120610' AND20170610' # 5年間でワードがある場合の条件指定
GROUP BY original_keywords, format_timestamp("%Y%m%d%H", time) # ワードと時間で一まとめにする


BIG Queryで使うSQLの例

WHERE句の条件指定では「WHERE al._TABLE_SUFFIX BETWEEN」
を使う。

#standardSQL
select al.id, ial.id, al.id2
al.id3
FROM `reco.log_*` al
JOIN `action.log_*` ial ON al.id=ial.id
WHERE al._TABLE_SUFFIX BETWEEN '20180610' AND '20180611'
group by al.id, ial.id, al.id2, al.id3

Tips


Q10.if __name__ == ‘__main__':の意味


python コード.py」などと直接実行された場合、if __name__ == '__main__':以下が実行される。




Q11.for文でリスト内の要素を辞書に格納する方法

下の用のfor文を使ってdicに自動で格納したい場合。

cat=[['b', 'a1'],
 ['b', 'a2'],
 ['c', 'b1'],
 ['c', 'b2'],
 ['c', 'b3'],
 ['d', 'c1'],
 ['d', 'c2'],
 ['d', 'c3']]

dic={'b': (['a1', 'a2']), 'c': ['b1','b2','b3'], ‘d':['c1','c2','c3']}




A.setdefaultを使う

cat = [['b', 'a1'],
       ['b', 'a2'],
       ['c', 'b1'],
       ['c', 'b2'],
       ['c', 'b3'],
       ['d', 'c1'],
       ['d', 'c2'],
       ['d', 'c3']]

dic = {}
for k,v in cat:
    dic.setdefault(k, []).append(v)

print(dic)  # => {'b': ['a1', 'a2'], 'c': ['b1', 'b2', 'b3'], 'd': ['c1', 'c2', 'c3']}


Linux


Q12.末尾の名前が0~Nまでのディレクトリを作成する

$ for i in $(seq 0 199); do mkdir "label_$i"; done


$(seq 0 199)で0~199までの数字を返し、末尾がその数字のディレクトを作成してる



Q13.ファイルの拡張子の最後を.jpgに変換する

$ for i in $(ls); do mv $i "$i.jpg"; done

例:dog.jpeg.jmg →dog.jpg


インフラ系(AWS, tensorboard)


Q14.s3にファイルのアップロード

# tar cf - ディレクトリ名 | gzip --best | aws s3 cp - s3://バケット/ファイル名

# ex
$ tar cf - model_tar | gzip —best | aws s3 cp - s3://sage/model.tar.gz

Tensorboardの表示方法

qiita.com

$ tensorboard --logdir=[ファイルがあるdir名]
ex:
$ tensorboard --gdir=/Users/〜/Downloads
# 「http://localhost:6006」にaccess

プログラマーの目の酷使・疲れを回復させるオススメの11の方法【視力ケア】

プログラミングで仕事するようになってから、PCや勉強などで目をより使うことになった。

過去の悪き呪いもあり、目が悪くなるのはやばいと思いかなりケアに徹してきた。その結果、個人的に目のケアで普段やってることをまとめておこうと思う。目を悪くしないと意識するだけでもかなり違う。

目次
1.目が悪くなる条件、知識
・環境作り
2.証明とかで日光で明るい環境での作業や読書
3.パソコン画面のズームアップ
4.ブルーライトカットのPCメガネを使う
・作業中のケア
5.まばたきを多めにする
6.直感で目が痛いと感じたら即休む

・アフターケア
7.目をほぐすアイマッサージャーを使う
8.休む日、目を使わない日を作る
9.視力回復効果を持つブルーベリーをとる
10.睡眠をタップリとる、遠慮なく昼寝する
11.マッサージで教えてもらった目のツボを押して筋肉をほぐす
12.+α 風の自己予防法

f:id:trafalbad:20170123070523j:plain

1.目が悪くなる条件、知識

これは目医者に聞いたり、直感で感じたこと。


度数の強い(遠くがよく見える)メガネ・コンタクトで、至近距離のパソコン・物体を直視し続けると、一気に近視が進む。
→通常より弱いか、丁度よいくらいの度数のメガネ・コンタクトがベスト

基準は30分やったら10分休める
→ 目の筋肉は使いすぎると硬直するので、遠くの緑を見るとか、マッサージをする

頑張って見ようとすると眼精疲労が進む
→適正度数なら頑張ってみようとしないので、近視は進まない。物を見ていて変に力んだり、違和感を覚えたら適正度数ではないってこと

目の疲れは直感を信じる
→ 脳信号からのサインを感じ取る。ワンピースの見聞色的な、何となくの延長上の感覚


パソコンをつける前の環境作り


2.証明とかで日光で明るい環境での作業や読書

パソコンをいじるなら、部屋を明るくする環境は必須。漫画とかの読書にも言える。

例えば、曇りの日だと部屋はかなり暗く、画面を見るのに目の負担がでかいから、証明はつけたほうがいい。

パソコンは日の当たるところ・直射日光は避けた方がいいだろう。直射日光は画面の光を乱反射させて、よけいパソコン画面を見辛くしてしまう。

パソコンを置くなら、窓辺近くの日陰とかがベストコンディション。




漫画とかの読書の類は日光を大いに活用した方が個人的にベスト。日光を明かり代わりにして読むとだいぶ目の負担が楽になる。




3.パソコン画面のズームアップ

パソコンのほとんどは文字媒体。特に仕事では文章だらけだから、そうした時は画面を必ずズームアップしよう。

逆にアニメや動画など特別はっきり見えなくていいものは、度が弱いメガネで見るとかなり楽。





4.ブルーライトカットのPCメガネを使う

ブルーライトはPCやスマホから発せられる目に害のある光。PCメガネはそれをカットすることで有名。

f:id:trafalbad:20170123070607j:plain

ネット上では賛否両論だが、実際に使っている経験から言わせてもらうと、かなり効果はある。

買う前はPC作業15分で目が痛くなってきたが、PCメガネでは1時間は持つ。定期的に休憩を入れれば、プログラマーでも休憩を入れれば、一日中PC・スマホの相手ができるほどの優れもの。

値段はフレーム込みで5000円〜1万円程度。実際にメガネに行って、度とサイズが合うものを購入した方がいい。PCメガネ有る無しでは目の普段が驚くほど減る。




パソコン操作中の視力ケア



5.まばたきを1秒に一回する

人間は普通は2秒に一回はまばたきをする。しかしパソコン画面とかを見てるときは、0.5秒〜1秒単位でまばたきを意識的にしてみよう。

主な理由は、目の渇きを抑えることと、パソコンを凝視することを抑える効果がある。

まばたきをしないと、パソコン画面をじっとにらみつけていることが多くなってしまい、結果的に目が疲れやすくなる。

まばたきを意識的にすることで目の負担は大分軽くなる。




6.直感で目が痛いと感じたら即休む

パソコンやスマホを30分いじったら10分休憩するのがベストらしい。
メガネ屋の定員曰く、休憩時間には
・目を閉じる

・遠くを見る

・昼寝をする
とかあるけど、個人的に一番肌感覚であってるものをやるのが一番いい。

目が痛くなったり、違和感を感じたら脳からの危険信号だと思って早めに休憩することをオススメする。
自分は、直感を信じて対処して、特に昼寝を重視してる。


アフターケア



7.目をほぐすアイマッサージャーを使う

アイマッサージャーはamazon で買った。自宅でできるし、ホットマスクと同じ効果がある。しかも充電できるから何回でも使えてコスパが良すぎる。


使ってみたら、1週間もかからず。目のシボシボが取れた。これはもう体験してとしか言うしかないが、効果はかなりあって今では手放せないアイテムになってる。



ホットマスクは面倒なので、充電で済むマシーン系の方が自分は好きなのでこれを選んだ。




8.休む日、目を使わない日を作る

普段はスマホだテレビだ漫画だと、視力を使いまくってしまう。特に陰キャで外に出ないならなおさらだろう。

だから普段から目を酷使しないように意識するのは当たり前として、それでもさらに目を酷使しない日を作ることが大切。

そもそも家にこもってると目を使ってしまうから、温泉でも出かけるとか、外出する日は、そういうきっかけには持ってこいだろう。




9.視力回復効果を持つブルーベリーをとる

視力回復効果を持つ食品はいろいろあるが、誰でも聞いたことあるのは、ブルーベリーなはず。

ブルーベリーに含まれるアントシアニンという成分が、目の網膜のロドブシンの再合成を活発化させる。つまりブルーベリーには目の疲れを回復させる、体の機能を促進させる効果がある。

ジャムやヨーグルトでとると、パンと一緒に食べられるし、ヨーグルトだとデザートにもオススメ。

もっと手軽にサプリメントとして買えるようになっている。自分は普段DHCのブルーベリーエキスのサプリメントを一日2錠飲んでる。

飲まない前と比べると目の疲れや疲労はたしかに取れた。今ならドラッグストアやamazon でも買えるので、自分は60日分はストックしておいて、朝飲むようにしてる。

f:id:trafalbad:20170123070719j:plain:w250
AmazonDHC ブルーベリーエキス




10.睡眠をタップリとる、遠慮なく昼寝する

人間は体の性質上、睡眠が一番健康上効果がある。惰眠とか規則正しい生活しつつも適度に昼寝してると血行も肌のツヤも良くなる。

おまけに、活動・免疫能力、回復能力も上がるので、眼精疲労とかの目の疲れには欠かせない。

最近というか前から自分は、効率重視のため眠くなったら寝るようにしてる。そうすると能率がかなり上がる。

あとは外に出て考えたりとか、目を使わずに仕事をする方法を考えたりと、「目を使う時間&使わない時間」のバランス考えるようになった。



11.マッサージで教えてもらった目のツボを押して筋肉をほぐす

マッサージ師に教えてもらった目のツボがある。言葉では表しにくいので図で示してみた。

目の上の骨盤

だいたい眉毛のあたり。眼球穴が空いてる周りの骨の部分で、眉毛の上あたり

こめかみ

だいたい図の位置。骨のくぼみがあるからそこがツボになってる。

頭の後ろ側(背骨の延長線上にある)


だいたい図のあたり。水平線上に耳があるところかつ、背骨の延長線上にある。


ブログだととても伝わりにくいので、詳しくはマッサージ師に教えてもらうのが一番。

眼精疲労は筋肉が凝り固まって生じるから、マッサージでほぐしてやるとかなり効果がある。

自分でやっても、マッサージ師に揉んでもらう分の何割かは自分でできるのだから、知っといて損はない技術。




12.+α 風の自己予防法

医者に聞いたセルフ式風邪予防のメモ。



・日頃のからうがい手洗いをする。
・あとよく寝る。


・風邪はビタミンcを含む食品とること。
・極力、人混みは必要ないときは避ける。

・風邪の時期はマスクをする。

・熱を引いたときは
→水を多く飲むとか、暑いなら服脱いで熱を逃すとかなどが対策っぽい。





今回はパソコン作業やスマホをいじるのが多くなってきたので、目のケア方法を改めてまとめてみた。
どれも実践すると効果的なものばかりなのでオススメ。

GKE上のkubernetesで機械学習運用環境(MLops)を作成手順・コマンド・知識メモ

機械学習運用環境(MLops)の一部を話題のkubernetes(k8s)で作成してみた。GCPのGKE上でk8sを利用できるので、今回はGKE上で途中まで構築。

今回参考にするMLopsは下の図。どうもこのサイトによるとメルカリでマイクロサービスとして運用されてるらしい  
f:id:trafalbad:20181207154107p:plain




このMLopsのk8sの部分だけ(下図)作成してみた。
f:id:trafalbad:20181207154123p:plain




・作成手順
step1. persistentVolumeを使ってAWSのS3とマウントしたPodをLoad balancerを使って、負荷分散を行う。http://[EXTERNAL-IP]にアクセスして、flaskで’hello, world’を表示させる。


step2.redisインスタンスと結合させたflaskアプリケーション用Podをgoogleチュートリアルに従い、作成


step3.上の図のPersistentVolumeと繋がってるtensorflowの部分の「worker用Pod」を作成。今回は簡易版でworker用のpodをceleryで作成



途中からAmazon sageMakerの方がMLopsを簡単に作れるという噂で、k8sでの構築はstep3で中断。けど、せっかくなのでアウトプットしたログを記事にまとめておこうと思う。


目次
・MLopsの構築

kubernetes関係知識まとめ

・コマンド関連まとめ



MLopsの構築

step1. flask用Podを作成して、で’hello world'を表示

まずflaskアプリケーションで’hello world’を表示させてみる。flaskはDjangoより便利ってことで使った。


・用意したファイル
app.yaml 、main.py(flaskアプリ本体) 、requirements.txt(インストール用のライブラリ)、Dockerfile、flask_pod.yaml(Pod作成用マニフェストファイル)、sample_lb.yaml(load balancer用ファイル)


プロジェクト ID:sturdy-willow-167902


1. Dockerイメージpush & 確認

まずGCPのホーム画面からcloud shell(コンソール)を開いて(右上のとこにある)、kubernetes Engineを選択。 

cloud shell(コンソール)の起動方法は、GCPメイン画面から[プロジェクト]をクリック。

上段の右から6番目のアイコン「cloud shellにアクセス」をクリックし、「起動」で開ける。
f:id:trafalbad:20181207154314p:plain





以下コマンドはコンソール上で実行。

# Dockerイメージ作成して、Container Repositoriesにpush
$ export PROJECT_ID="$(gcloud config get-value project -q)”
$ docker build -t gcr.io/${PROJECT_ID}/visit-counter:v1 .
$ gcloud docker -- push gcr.io/${PROJECT_ID}/visit-counter:v1

#コンソール右上の [ウェブでプレビュー]で ’hello world’ が表示されるか確認
$ docker run --rm -p 8080:8080 gcr.io/${PROJECT_ID}/visit-counter:v1

→ Container Repositoriesに「visit-counter」ができる



2.Podの作成とserviceの定義


ここは単純にkubektlコマンドでPodの作成とサービスの定義(負荷分散)だけ。

そのためにGUIでもCUIでもいいので、kubernetes Engineのクラスタを作成しておく。クラスタを作成したら、コンソールで以下コマンド実行。

# クラスタ操作権限取得
$ gcloud container clusters get-credentials <クラスタ名> --zone=asia-northeast1-a

# 一応、VMインスタンス確認
$ gcloud compute instances list


***注意、マニフェストファイル(flask_pod.yaml)ではイメージ名のプロジェクトIDを展開する参考サイト

asia.gcr.io/$PROJECT_ID/wdpress/hello-world
# $PROJECT_ID は展開する必要があ流ので、上のイメージ名を下のように修正
asia.gcr.io/sturdy-willow-167902/wdpress-123456/hello-world

# Podの作成
$ kubectl apply -f flask_pod.yaml (pod確認  $ kubectl get pod)
# Serviceの定義
$ kubectl apply -f sample_lb.yaml
# EXTERNAL-IP(IPアドレス)の確認
$ kubectl get service

→ http://[EXTERNAL-IP] にブラウザからアクセスして、’hello world’が表示される。



step2.redisインスタンスと結合したflask用Pod作成

redisインスタンスと結合したflaskアプリケーションのPodを作成。
作成するMLopsではこの部分

これはGoogleのチュートリアルがあるが、素でやってもすんなり行かないので、ちょっと修正した。あとはチュートリアル通りに実行。 


チュートリアルで修正した点
インスタンスクラスタのリージョンを同じにする => region=us-central1

・RESERVED_IP_RANGE の部分を、実際のインスタンスの予約済み IP 範囲に置き換える(参考サイト

マニフェストファイル(YAMファイル)のイメージ名のプロジェクトIDを展開



この3点だけ修正。あとはチュートリアル通りに実行。

プロジェクトID:sturdy-willow-167902



1.redisインスタンス作成

# インスタンス作成
$ gcloud redis instances create myinstance --size=2 --region=us-central1

# インスタンス情報確認
$ gcloud redis instances describe myinstance --region=us-central1

>> createTime: '2019-01-06T13:19:26.236748318Z'
currentLocationId: us-central1-c
host: 10.0.0.3
locationId: us-central1-c
memorySizeGb: 2
name: projects/sturdy-willow-167902/locations/us-central1/instances/myinstance
port: 6379
redisVersion: REDIS_3_2
reservedIpRange: 10.0.0.0/29
state: READY
tier: BASIC

# redisインスタンスを削除するとき
$ gcloud redis instances delete myinstance ―region=us-central1




2.CUIクラスタ作成

#  redisインスタンスのリージョンに設定
$ gcloud config set core/project sturdy-willow-167902 && gcloud config set compute/zone us-central1-c
# クラスタ作成
$ gcloud container clusters create visitcount-cluster --num-nodes=3 --enable-ip-alias
# クラスタの権限get
$ gcloud container clusters get-credentials visitcount-cluster --zone us-central1-c --project sturdy-willow-167902





3.RESERVED_IP_RANGE の部分を、実際のインスタンスの予約済み IP 範囲に置き換え

$ git clone https://github.com/bowei/k8s-custom-iptables.git && cd k8s-custom-iptables/

$ TARGETS="10.0.0.0/29 192.168.0.0/16" ./install.sh && cd ..

4.Dockerイメージ作成とpush

$ export PROJECT_ID="$(gcloud config get-value project -q)"
$ docker build -t gcr.io/${PROJECT_ID}/visit-counter:v1 .
$ gcloud docker -- push gcr.io/${PROJECT_ID}/visit-counter:v1

5. configmapの作成

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


6.pod作成, service定義、アクセス

$ kubectl apply -f visit-counter.yaml
$ kubectl get service visit-counter

# アクセスして動作確認
$ curl http://[EXTERNAL-IP]



Step3.celeryでworker用のPodを作成

ここでは下図のworkerのPodを作成。
f:id:trafalbad:20181207154249p:plain

今回はPVと接続せず、簡単にworker用のcelery のpodを作成してみた。
用意したfileは「Dockerfile, run.sh, celery_conf.py, celery_worker.yaml」。


そのためには別途dockerイメージを作成する必要がある。

$export PROJECT_ID="$(gcloud config get-value project -q)"
$ docker build -t gcr.io/${PROJECT_ID}/visit-counter:worker .
$gcloud docker -- push gcr.io/${PROJECT_ID}/visit-counter:worker

各ファイルのコードは以下の通り。
 
・Dockerfile

FROM library/celery
ADD celery_conf.py /app/celery_conf.py
ADD run.sh /usr/local/bin/run.sh

ENV C_FORCE_ROOT 1
CMD ["/bin/bash", "/usr/local/bin/run.sh"]      # run.shのコマンド実行
CMD exec /bin/bash -c "trap : TERM INT; sleep infinity & wait"  # 永続的に起動される


・run.sh

#!/bin/bash
# celery worker起動コマンド
/usr/local/bin/celery -A celery_conf worker -l info


celery_conf.py

from celery import Celery
from flask import Flask

def make_celery(app):
    celery = Celery(app.import_name, backend=app.config['CELERY_RESULT_BACKEND'],
                    broker=app.config['CELERY_BROKER_URL'])
    celery.conf.update(app.config)
    return celery

flask_app = Flask(__name__)
flask_app.config.update(CELERY_BROKER_URL='redis://10.0.0.3:6379', CELERY_RESULT_BACKEND='redis://10.0.0.3:6379')
celery = make_celery(flask_app)

@celery.task()
def add(a, b):
    return a + b


celery_worker.yaml
参考サイトによるとspec.template.metadata.labelsをvisit-counterにすることでflask側のPodと接続させるらいし

apiVersion: v1
kind: ReplicationController
metadata:
  labels:
    component: celery
  name: celery-controllers
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: visit-counter
        component: visit-counter
    spec:
      containers:
      - image: "gcr.io/sturdy-willow-167902/visit-counter:worker"
        name: celery 
        env:
        - name: REDISHOST
          valueFrom:
            configMapKeyRef:
              name: redishost
              key: REDISHOST
        ports:
        - containerPort: 5672
        resources:
          limits:
            cpu: 100m


最終的に作ったPodが

$ kubectl apply -f visit-counter.yaml && kubectl get pod 

で確認したとき、「Running」になっているので、worker用podの完成。


あとは、flask側のpodで

$ from celery_conf import add
$ r=add.delay(1,3)
$ r.result

こんな感じでworker側に投げた値を受け取れるようにする必要がある。
これはserviceを定義するとか、マニフェストファイルを定義するとか、いろいろありそうなので、またの機会にした。

というのも、amazon sagemakerというのが便利との噂を聞いて、k8sからamazon sagemakerに切り替えてMLopsを構築したから。
なので、k8sでの作業は中断し、amazon sagemakerでmlopsを構築した。


ちなみに、Amazon sageMakerでのMLops構築手順の関係記事は別記事にまとめた。

trafalbad.hatenadiary.jp




kubernetes関係知識まとめ

PersistentVolumeについて

k8sのpersistentvolume (以下PV)はPodとは独立した外部ストレージ。

通常のk8sのPVはクラスタの外部の別の場所に作成するが、GCP上ではPVは代替ストレージの「GCEPesistentDisk」がすでに存在してる。PVCで用途に応じて容量・タイプなどを適宜追加・拡張する。




・PVのアクセスモード

  • > ReadWriteOnce: 1つのノードからR/Wでマウントできる
  • > ReadOnlyMany: 複数のノードからReadOnlyでマウントできる
  • > ReadWriteMany: 複数のノードからR/Wでマウントできる


・PVCの作成時、PVのストレージタイプの「標準」か「SSD」をStorageClassで指定

SSD」はマニフェストファイルではstorageClassName: ssdで指定。デフォルトは「標準」



app.yamlPython ランタイムについて

=>アプリケーションのコードと依存関係をインストールして実行するソフトウェアスタック。
標準ランタイムは、app.yaml で以下のように宣言(Docker でビルド)
runtime: python
env: flex


=>アプリケーションの app.yaml で使用するバージョン(Python2 or 3)をruntime_config に指定

runtime: python
env: flex
runtime_config:
    python_version: 3

=>ライブラリのインストール
同じディレクトリ内の requirements.txt ファイルを検索し、pipでrequirements.txt 内のライブラリを全てインストールする。




・PV<=> PVC <=> Pod間の結合関係
PVとPVCとPod間のどこが結合してるか?の関係について、


PV:metadata.name: nfs001, storageClassNameフィールドのslow

PVC:metadata.name: nfs-claim1, storageClassNameフィールドのslow

Pod:metadata.name: my-nginx-pod, persistentVolumeClaim.claimNameフィールドのnfs-claim1

つまりPVとPod、PVCとPodの結合関係は
PVとPod: slow(PV) <=> slow(Pod)
PVCとPod:nfs-claim1(PVC)<=> nfs-claim1(Pod)


DockerfileのENVの環境変数について
ENV命令は、環境変数と値のセット。値はDockerfile から派生する全てのコマンド環境で利用でき、 上書き、変数扱いも可能。

ex: 環境変数に値をセットする場合、

ENV MES Good-bye 

は MES=Good-bye
を意味する。 $MESで環境変数をあらわせる。




・gunicorn
例えば myapp.py というファイル内に app という名前でFlaskアプリがある場合、以下のように gunicorn で起動。

$ gunicorn myapp:app

デフォルトのPORTとHOSTは127.0.0.1と8000



gunicornのportについて
This will start one process running one thread listening on 127.0.0.1:8000.

I wish to run gunicorn on port 80.
→ gunicorn -b 0.0.0.0:80 backend:app




・flask

if __name__ == ‘__main__':
   app.run(debug=False, host='0.0.0.0', port=80)

host='0.0.0.0' の指定が大事。これがなければ、外部からアクセスすることができない。参考サイト



・os.environ.get
環境変数 key が存在すればその値を返し、存在しなければ default を返す。
key、default、および返り値は文字列。
もし環境変数がなくともKeyErrorがraiseされず、第二引数(キーワード引数)のdefaultの値が返る。(指定しなければNoneが返る)



・PORT、HOST
REDISHOST→ネットワークに接続されたコンピュータのこと(redisインスタンス自体)
REDISPORT→redisインスタンスに接続するドア(番号)




・configMap
# マニフェストファイル内での部分

env:
     - name: REDISHOST
        valueFrom:
          configMapKeyRef:
            name: redishost
            key: REDISHOST

keyのREDISHOSTが環境変数

# configmap作成コマンド
$ kubectl create configmap <map-name> <data-source>

# 例
$ kubectl create configmap redishost ―from-literal=REDISHOST=10.0.0.3

redishostがmap-nameで、それ以下がdata-source

# configmap削除
$ kubectl delete configmap <configmap名>

上のコマンドで環境変数のREDISHOSTが10.0.0.3に変更された。

REDISHOST=redisインスタンス自体を環境変数に代入。Dockerfileのデフォルトのクラスタ環境変数をredisインスタンスのPORT(自体)で上書き。




コマンド関連まとめ



(ターミナル上での)k8sクラスタ作成コマンド

$ gcloud container clusters create <クラスタ名> --machine-type=n1-standard-1 ―num-nodes=3

ex: gcloud container clusters create mlops --machine-type=n1-standard-1 --num-nodes=3



クラスタ環境設定の権限取得(cloud shell上で作成したクラスタを操作できる)

$ gcloud container clusters get-credentials <名前> ―zone=<ゾーン>

ex:$ gcloud container clusters get-credentials mlopss --zone=asia-northeast1-a



kubectlコマンドでクラスタ内のノードとpod関係の操作

$ kubectl get nodes (ノードの確認)
$ kubectl get pod  (Podの確認)
# Serviceの情報を確認(クラスタ内で通信可能なIPアドレス)
$ kubectl get services sample-clusterip

# Pod内へのログイン
$ kubectl exec -it sample-pod /bin/bash
→sample-podはmetadata.nameフィールド名



・dockerコマンド関係

$ docker rmi <イメージID>  -f (dockerイメージの削除) 
$ docker images (dockerイメージ確認)
$ docker ps(コンテナが正常に起動しているか確認)
$ docker images -aq | xargs docker rmi(dockerイメージ全部削除)

# コンテナ停止、削除
$ docker stop [コンテナ名]  (全コンテナ停止 docker stop $(docker ps -q))
$ docker rm [コンテ名] (全コンテナ削除 docker rm $(docker ps -q -a))