Subscribed unsubscribe Subscribe Subscribe

ISUCON6 予選敗退で終わってしまった

ISUCON

ISUCON6id:uzulla さんと id:moznion さんとチーム「[=======> ] 80%」で出場して、予選通過できず敗退しました。

残念…。

前日までの準備

かくかくしかじかでこの3人で出場することには決まったものの、とにかく2人とは一緒に仕事もしたことないし 顔合わせして練習が必要ですね、ということで8月末と9月前半、2回集まって過去問を使って練習会をした。

まず使用言語。2人はPHP強いけど僕は1行も書いたことないし、Goは3人ともある程度は出来そうだけどまぁやっぱりPerlが(最近は全然さわってないにしても)業務でも使って慣れているし安心かな、ということでPerlで。

役割分担としては、

という感じで。

インフラ・ミドルウェア周り完全にuzullaさんにお任せすることになってしまい、負担が大きくなかなかアプリのコードまで見てもらう余裕を作れず申し訳なかったです。ベンチマークまわしてログを解析してボトルネックを見つけ出す、とかもほぼ任せっきりでやってもらってしまいましたが とにかくそのへんのオペレーションやチューニングは完全に信頼できてとても良い仕事をしていただいて、本当に感謝しております。

アプリのコードはmoznionさんと練習もして息を合わせて作業することができて良かったです。お互いローカル環境でも動作確認程度には動かせるようにしつつコードを読んで動作・仕様を把握し。ログからボトルネックを確認できたらまずそこを解消するためにどう変更するか方針をちゃんと相談し、分担できるところは分担して作業。出来るだけちゃんとプルリクを作ってレビューした上でmerge。お互い無駄なミスや手戻りすることなくスムーズに出来たんじゃないかと思っています。

場当たり的に重そうなところを変更していくのではなく、しっかりログを解析してボトルネックを把握して潰し効果を確認し、また次のボトルネックが分かったらそこを潰す、という流れを繰り返す。2回の練習でその感覚はかなりついて手応えはあった(し、実際当日も効果あった)ので 練習会は本当にやっておいて良かったと思います。

当日・前半

気兼ねなく声出して議論しながら作業できる会議室を確保できたのでそこに集合、準備万端で競技開始。

uzullaさんがAzureについてはかなり使い込んで詳しくなってくれていたおかげで最初のサーバ立ち上げや初期セッティングは練習通りでスムーズに。

こちら側も練習通りにコードをGitHub repositoryに入れてローカルで環境作って…(Perl 24.0なんて入ってなかった!!1 plenv installからだ!!!)

アプリ・DBが2個に分かれてるのか…面倒だな、はやめに統一できるならしてしまいたい、ということでボトルネック計測と並行でその作業から。 isutarの方はごっそり移行できそうだったので慎重に変更しつつisudaに統一して2アプリ間での無駄なやりとりを削減。 (11:30頃)

MySQLクエリ解析の結果keywordを長さ順に全件取得してるのが重い、ということが分かったので、これはとりあえず変更されるものではないしlengthカラムを別に保持してそれで引くようにしよう、と。 (11:45頃)

これだけでそれなりの効果が出て、18,515点で一気に断トツ首位へ。このへんまでの流れ練習通りでとてもスムーズに出来て良かった。 f:id:sugyan:20160918225859p:plain

そもそもこのkeywordの長さ順クエリってhtmlify時に置換すべきキーワードを探すための正規表現を作るだけのためのものだし、毎回引く必要はなくて正規表現はアプリで保持しておいてentryが追加・削除されたときだけ(実際にはベンチマークで削除操作は無かったっぽい?)作り直せばいいじゃないか、ということでPOST時に作ってRedisに正規表現文字列を持たせるよう変更。encode_utf8とかdecode_utf8を挟まないとRedisに入らない、とかでハマってPerlムズい、ってなったりしたけど、そんなに無駄にハマり続けたりすることもなく完了 (12:40頃)

これもしっかり効果が出て、追いついてきていた2位チームを一気に引き離して 40,321点に。 f:id:sugyan:20160918230952p:plain

ここまでは良かった…。

当日・後半

ここからtotal_entriesをRedisに載せたり starをRedisに載せる闇改修をmoznionさんが行ったりしている間に、14時過ぎに10万点超えのチームに一気に抜かされ。

やはりhtmlifyの結果そのものをキャッシュしていかないとダメだ、という話になったが、entryが追加されるごとに正規表現も変更されhtmlifyの結果も変更されないといけないはずだから、単純にはキャッシュできないよな…と悩む(ここでもっと更新されるべき内容を精査するとか考えを巡らせるべきだった)

とにかくアクセスの多いのは/だから、ここで表示される10件だけなら毎回POSTされるたびに作っても1秒以内のレスポンスは出来るし効果があるでしょう、ということで新規entryが投稿されるたびに/で表示される10件だけhtmlifyの結果をRedisに載せて使うように。 (15:10頃)

これで64,434点くらいまでは伸びたが、10万点にはとても届きそうにない…

htmlifyの結果を全部をキャッシュに載せようとするとどうしてもPOST時に関連entryの結果を更新できそうにない、別プロセスでどうにか出来ないか…?と取り組み始めて、アプリ内でforkしてみたりRedisをJobQueue代わりに使って裏のワーカープロセスでhtmlify結果を作ってキャッシュに載せる、とかを試みたが、どれも上手くいかずスコアは伸びず… 結局この悪戦苦闘が17時過ぎまでいっても大きな効果を出せず、すべて戻すことに…

その間にサーバ側で様々なチューニングを行ったuzullaさんの変更がプラスされて、再起動テストなどしていたところ 72,018点という結果が出たのでそれを最終スコアとして時間切れで競技終了。

最終的なコードなどはこちら : https://github.com/uzulla/isucon6-q

反省

予選通過ラインは9〜10万点ということで、どうにもそのラインまで届かせることはできなかった。 序盤で確実にスコアを伸ばしていったのは良かったが、そこから先の大幅な点数アップに辿り着けなかったのは力不足だった。

ある程度のリンク作成ミスがあってもスコア計算としてはとにかく成功GETレスポンスが多ければそのミスを上回る効果があった、という話を後で聞いて、もっとミスを恐れずに貪欲にキャッシュしていくべきだったか、と思ったり。そのへんは試算が足りていなかったし、moznionさんも提案して実装しようとしてくれたところに「元の実装と挙動が変わることになるのは納得いかない」と変なこだわりを持って撥ね除けてしまったのが良くなかった。 そもそもstarのキャッシュとかで結構元の実装と挙動違う実装にしていてベンチマークを通していたのだからそんなこだわりを捨ててとにかくベンチマークが通って高い得点を出すことができるのなら色々試してみても良かった。

というかキャッシュに関しても「POST時に全部作ってGET時は読むだけ」みたいな形でやり始めて思考停止してしまっていたので良くなかった。「POST時には関連するものだけ破棄して、GET時に作ってキャッシュする」という形をちゃんと想定していたら正しい実装でももっとスコアは伸ばせていたのかもしれない。どういうわけか当日はこういう考えが思い浮かんでいなかった。

感想

取り組み甲斐のある良い問題で、楽しく挑戦することができました。 他の出場者の方々もレベル高く、やはり簡単には勝てないのだな、と痛感いたしました。様々な参加エントリを読んで復習していきたいと思います。

出題・運営の皆様ありがとうございました!

一緒にチームを組んで戦ってくださった id:uzulla さん、 id:moznion さん、ありがとうございました!!

Links

TOKYO IDOL FESTIVAL 2016 のタイムテーブル画像化ツールを作った

Ruby JavaScript

日本最大規模のアイドルの祭典・"TOKYO IDOL FESTIVAL"(通称TIF)。

今年は明日からの8/5〜8/7の3日間の日程で、始まります。

TOKYO IDOL FESTIVAL 2016

で、こういったフェスってステージが複数あって観たいステージが幾つもあるなか、どの時間にどれを見るか とか決めるのが大変で、そういうのを解決するためのツールみたいなのを多くの人たちが作っていたりしますね。 今回のTIF 2016でも幾つかそういうのが有志によって公開されています。

今年はメインのタイテ情報がJSONで取得できるようになっていたのでこういったツールも作りやすい環境になっていたかと思います。

で、自分も何か作ろうかな…と思って

自分の場合、タイテ情報ってけっこう画像をそのままダウンロードしたりWebで載ってるものをスクリーンショットで保存して使うことが多くて。ネットワークの繋がらないところでもすぐに確認できるのが便利なので。

なので自分で組んだ自分だけのタイテを一枚絵の画像でダウンロードできるようなのがあればいいな、と思ったのでそういうのを作ってみた。

条件を指定して絞り込んで表示した一覧から自分が行きたいものだけを選択すると、それだけを抽出してこういった画像を生成する、というだけのもの。

↓例 f:id:sugyan:20160804230856p:plain

技術的にも難しいことは特にしてなくて

  • サーバサイド: RailsJSON APIを用意
    • 公式タイムテーブル情報は定期タスクでfetch & parseして整形しキャッシュに突っ込んでおいてそれを読むだけ
    • 選択したものを元にRMagickでテキスト描画しつつ画像を生成
  • クライアントサイド: ReactでUI生成
    • データは全件取得した上で絞り込みによる表示変更
    • 生成結果の画像を表示切り替えで雑にSPAっぽく

という感じ。

Repository : https://github.com/sugyan/tif2016-mytt

JSのビルドには最近webpackを使うようにしていて、開発時にはwebpack-dev-serverを使った。

などを参考にして、webpack-dev-server --inlineなどでソース変更時に再ビルドとリロードが自動で走るようにしつつ配信しておき、Rails側では

module ApplicationHelper
  def javascript_include_tag(*sources)
    if Rails.env.development?
      opts = {
        src: '//localhost:8080/javascripts/main.js'
      }
      return content_tag(:script, '', opts)
    end
    super(*sources)
  end
end

みたいな感じで開発時のみJSがそっちを向くように設定したら捗った。

Asset Piplineを使わずに/public以下に直接成果物を配置すればいいかな、と思ったけど本番の更新時にキャッシュとか制御できるか不安だったので結局一度/assets以下に吐いてAsset Piplineでfingerprint付きのpathで配信するようにした…。


と、こんな感じで作ったアプリをHerokuにdeployしてちょっとお知らせしてみたところ 色んな界隈のヲタクの方々から400RTくらい拡散していただいて。どれくらいPVあったかは分からないけど 使ってくれたヒトが作った画像を載せてくれて「それどうやって作ったの?」「ここから!」みたいにTwitter上でクチコミで広まったりして、思っていた以上に使ってもらえて、作った甲斐あったわー という感じでとても嬉しい。

「TensorFlowはじめました」を読んだ

Book TensorFlow

著者の有山さんとは、TensorFlowでの独自の画像データセットの分類に取り組む同士(?)として勉強会などでお話する機会があり、そんな縁もありまして有り難いことに献本ということで読ませていただくことができました。

第1章の「TensorFlowの基礎」では最初にまずデータフローグラフの「構築」と「実行」で分かれているという概念について、丁寧に説明されていてとても良かったです。いきなり何も知らずに公式Tutorialだけ始めていた自分は、こういう概念について理解するのが遅かった…。

第2章ではCIFAR-10の学習モデルと評価。公式Tutorialの英語を問題なく読めて ある程度のCNNの知識があれば困らないかもしれないけど こうやって日本語で解説されているものがあるというのは(数ヶ月前の自分のように)新しく始める人にとってはとても有り難いだろうなと思います。

第3章ではもうちょっと踏み込んで、TensorFlowでのデータ保存・読込や可視化について。

第4章「CIFAR-10奮闘記」がとても面白かった。 モデル構成や学習データを変えつつ評価結果を確認し、と試行錯誤を繰り返しながら少しずつ正答率を上げる方法を模索していく。自分もアイドル顔識別でそういった試行錯誤を続けていたので とても共感できる内容でした。何をどうすれば正答率が上がる、ってなかなか感覚的な部分もあったりして 最良の道はそう簡単には見つけだせないんですよね…。


というわけで新たにTensorFlowを使って機械学習・画像分類とかをやってみたい、というヒトにはとてもオススメの1冊でした。本当にあと数ヶ月はやくこれが出ていれば僕も最初の躓きが少なかっただろうに…!

巻末の参考文献に私の「すぎゃーんメモ」も載せていただいていて、非常に光栄であります。 この先も続く挑戦の道のり、楽しみにしています!ありがとうございました!!

TensorFlowで顔識別モデルに最適化した入力画像を生成する

Python TensorFlow

f:id:sugyan:20160710002948p:plain

動機

elix-tech.github.io

の記事を読んで、「可視化」の項が面白いなーと思って。 引用されている図によると、人間の目にはまったく出力クラスとは関係なさそうに見える画像でもCNNによる分類器は騙されてしまう、ということのようだ。

なるほど分類モデルの方を固定しておいて入力を変数として最適化していけば任意の出力に最適な入力を得ることができるのか、と。 自分でもやってみることにした。

分類モデル

TensorFlowによるDeep Learningでのアイドル顔識別モデルの性能評価と実験 - すぎゃーんメモ の記事で使ったモデルとデータセットで、ここではCross Validation用にデータを分けずに7,200件すべてを学習に使い20,000 step進めたものを用意した。

このモデルは学習したアイドルたちの顔画像に対してはかなりハッキリと分類できるようになっていて、試しに幾つかを入力して得た分類結果の上位3件をtf.nn.top_k(tf.nn.softmax(logits), k=3)で出力してみると

・例1: エルフロート・マアヤさん (label_index: 10)

f:id:sugyan:20160705200332j:plain

[0.9455398321151733, 0.016151299700140953, 0.013260050676763058] [10, 38, 7]
[0.9314587712287903, 0.02145007625222206, 0.0140310600399971] [10, 7, 38]
[0.9718993306159973, 0.0045845722779631615, 0.0037077299784868956] [10, 2, 17]
[0.9961466789245605, 0.001244293642230332, 0.0008690679096616805] [10, 7, 31]
[0.9985087513923645, 0.0003244238905608654, 0.0003135611186735332] [10, 30, 7]

・例2: フラップガールズスクール・横山未蘭さん (label_index: 13)

f:id:sugyan:20160705200355j:plain

[0.9963579773902893, 0.0019185648998245597, 0.0008565362659282982] [13, 20, 25]
[0.9986739158630371, 0.0006054828991182148, 0.00040348240872845054] [13, 19, 31]
[0.9996882677078247, 0.00011850777082145214, 6.301575194811448e-05] [13, 31, 20]
[0.9860101938247681, 0.006886496674269438, 0.0037682976108044386] [13, 19, 20]
[0.9992870688438416, 0.0002755637979134917, 0.00010664769797585905] [13, 19, 20]

・例3: じぇるの!・針谷早織さん (label_index: 24)

f:id:sugyan:20160705200406j:plain

[0.9933986663818359, 0.004436766263097525, 0.0004516197368502617] [24, 2, 36]
[0.9997298121452332, 6.973237032070756e-05, 5.891052205697633e-05] [24, 8, 2]
[0.9980282187461853, 0.000929205387365073, 0.000297865248285234] [24, 2, 36]
[0.9958142638206482, 0.0027367006987333298, 0.0004832764097955078] [24, 21, 20]
[0.991788923740387, 0.002572949742898345, 0.0013722123112529516] [24, 2, 26]

という具合に、正しいindexの番号の出力が0.9以上になるくらいのものとなっている。

このモデルを騙して誤識別させるような画像を生成する、というのが今回のテーマ。

inputs

今回は入力画像が変数となるので、そのサイズ(今回の場合は96 x 96 x 3)の変数を用意する。取り得る値の範囲は0.0 - 1.0とする。

import tensorflow as tf

with tf.variable_scope('input') as scope:
    v = tf.get_variable('input', shape=(96, 96, 3), initializer=tf.random_uniform_initializer(0.0, 1.0))

inference

学習済みのモデルにこの変数を入力として与え、分類結果を得る。 ただ、元々このモデルはJPEGなどの画像から復元した0 - 255の値をとるtf.uint8Tensortf.image.per_image_whiteningによって変換したものを入力として取るようにしていたので、それに従って同じように変換する。

image = tf.mul(tf.clip_by_value(v, 0.0, 1.0), 255.5)
input_image = tf.image.per_image_whitening(image)
# 以前までの記事で使っていた識別器。今回はbatch sizeを1とする
r = Recognizer(batch_size=1)
logits = r.inference(input_image)

loss

上記で得られた結果と、「理想とする出力」の差分が今回の損失の値になる。 単純に引き算なんかでも良いかもしれないけど、分類モデルの学習と同様にCross Entropyを使ってみることにする。

losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits, [FLAGS.target_class])

train

定義したlossを最小化する手続き。これも適当にAdamOptimizerを使っておく。 学習によって値を更新していくのは入力変数vのみ。

train_op = tf.train.AdamOptimizer().minimize(losses, var_list=[v])

replace tf.image.per_image_whitening

これだけで学習させていけるかなーと思ったのだけど、実際にSession作ってtrain_opを実行してみると

...
tensorflow.python.framework.errors.InvalidArgumentError: We only handle up to Tensor::dims() up to 8, not 0
         [[Node: gradients/Relu_grad/ReluGrad = ReluGrad[T=DT_FLOAT, _device="/job:localhost/replica:0/task:0/cpu:0"](gradients/Sqrt_grad/mul_1, Relu)]]
Caused by op 'gradients/Relu_grad/ReluGrad', defined at:
...

みたいなエラーが出てしまう。 よく分からないのだけど、入力に変数を使っているときにReluがあると傾きを計算できないの…?TensorFlowのバグなんだろうか、、

tf.image.per_image_whiteningがやっているのは画像の各画素値から平均値とか分散とか求めて引いたり割ったりしているだけなので、そのあたりのソースを参考にtf.nn.reluを使っているところだけ除外して

# image = tf.image.per_image_whitening(image)
mean, variance = tf.nn.moments(image, [0, 1, 2])
pixels = tf.reduce_prod(tf.shape(image))
stddev = tf.sqrt(tf.maximum(variance, 0))
input_image = tf.sub(image, mean)
input_image = tf.div(input_image, tf.maximum(stddev, tf.inv(tf.sqrt(tf.cast(pixels, tf.float32)))))

と書き換えてみたら無事に学習できるようになった。

結果

これで学習を進めていくと、1000 step程度でlossは十分に減少し、softmaxの目標indexの出力は0.999を超えるくらいになる。

$ python optimal_inputs.py --target_class 10
0000 - loss: 4.388829 (0.012415)
0001 - loss: 4.170918 (0.015438)
0002 - loss: 3.950892 (0.019238)
0003 - loss: 3.728565 (0.024027)
0004 - loss: 3.509432 (0.029914)
0005 - loss: 3.291546 (0.037196)

...

0997 - loss: 0.000944 (0.999057)
0998 - loss: 0.000942 (0.999058)
0999 - loss: 0.000941 (0.999060)

で、この学習終了後の変数vを画像として出力してみると。

output_image = tf.image.convert_image_dtype(v, tf.uint8, saturate=True)
filename = 'target-%03d.png' % FLAGS.target_class
with open(filename, 'wb') as f:
    f.write(sess.run(tf.image.encode_png(output_image)))

(ちなみにこういったランダム要素の多い画像をjpeg出力するときはchroma_downsamplingオプションをFalseにしないとかなり情報が落ちてしまうようなので注意。結構ハマった)

f:id:sugyan:20160709224905p:plain

いちおう拡大してみると

f:id:sugyan:20160709225048p:plain

にわかには信じがたいけど、このデタラメ模様にしか見えないような画像が 今回の分類器に最適化された入力画像、となる。 実際にこの画像から分類器にかけてtf.nn.top_k(tf.nn.softmax(logits), k=3)を出力してみると、

with open('target-010.png', 'rb') as f:
    img = tf.image.decode_png(f.read())
img.set_shape([96, 96, 3])
inputs = tf.expand_dims(tf.image.per_image_whitening(img), 0)
logits = r.inference(inputs, FLAGS.num_classes)
softmax = tf.nn.softmax(logits)

...

with tf.Session() as sess:
    print(*[x.flatten().tolist() for x in sess.run(tf.nn.top_k(softmax, k=3))])

結果は

[0.9992352724075317, 0.00031209649750962853, 0.00014285057841334492] [10, 31, 18]

となり、確かにindex 10のものが0.999以上の値になっている。 つまり、この分類器からすると

f:id:sugyan:20160709235237j:plainf:id:sugyan:20160709224905p:plain

どちらも非常に高い確度で同じ人物だと認識する、ということになる。マジかよ。

こんなデタラメ模様のどこに特徴が隠れているんだ…。

実験

ちなみに一応、他のindexに最適化された画像もそれぞれ生成してみた。

f:id:sugyan:20160710001524p:plain

という感じで、どれもそれぞれ対応するindexに対し0.999くらいのsoftmax出力になる画像なのだけど、人間の目にはデタラメ模様にしか見えない。

ということは、ランダムな値ではなく別の画像を初期値として そこに人間の目には分からないような最適化を加えたものを作ることもできるわけで。 先ほどのマアヤさん (label_index: 10)の画像を変数の初期値として、異なるindexに最適化された画像を作ると

フラップガールズスクール・横山未蘭さん (label_index: 13)

f:id:sugyan:20160710002258p:plain

[0.9998733997344971, 4.3967520468868315e-05, 2.7816629881272092e-05] [13, 20, 31]

・じぇるの!・針谷早織さん (label_index: 24)

f:id:sugyan:20160710002305p:plain

[0.9999223947525024, 1.816232907003723e-05, 1.359641919407295e-05] [24, 4, 2]

というように、ちょっとしたノイズが載っているだけのように見えるけれど 分類器の結果は完全に別の人物としての高確度の識別をしてしまう。 不思議〜〜〜。

実際、顔画像を収集している中でたまにOpenCVによる顔検出で誤検出された壁紙の模様とか「まったく人間の顔ではないもの」が たまに高確度であるアイドルさんとして識別されることがあったりして不思議に思っていたけど、こういう結果を見るとまぁ起こり得るんだろうなぁと納得できる。むしろこんな意味わからん誤認識をするくせに94%とかの精度が出る方が不思議だわ…。

Source code

github.com

EC2のGPU instanceで Ubuntu 16.04 + TensorFlow 0.9.0 の環境をつくる

AWS

memo.sugyan.com

の続き(?)。 この記事を書いたところ、「Ubuntu 16.04でもこうすれば簡単にCUDAインストールできるよ」とアドバイスをいただきました。ありがとうございます。

qiita.com

というわけで これを使ってやってみた。

g2.2xlargeで、Ubuntu 16.04 LTS (Xenial Xerus)のAMIを使ってインスタンスを立ち上げ、上記記事の通りに操作するだけでCUDA 7.5, cuDNN 4がインストールされる。

あとは(自分はPython3.5を使うので)

$ sudo yum install python3-pip
$ sudo pip3 install --upgrade https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow-0.9.0-cp35-cp35m-linux_x86_64.whl

とやるだけで TensorFlow 0.9.0 がGPUで動いた。やっぱりソースから自分でビルドする必要はなさそうだ。

EC2のGPU instanceでTensorFlow動かすのにもうソースからのビルドは必要ないっぽい?

めも。

aws ec2, g2-2xlarge, ubuntu14.04にtensorflowを導入する。 - Qiita などを参考にしてg2.2xlargeでの環境を作っていたけど、どうやら 0.8.0 くらいからは 「Cuda compute capabilityうんぬんの問題があるので TF_UNOFFICIAL_SETTING=1 ./configure指定してソースからビルドして」というところは必要なさそう…? 実際にCuda Toolkit 7.5、cuDNN v4だけ入れている状態から公式に配布されているpipパッケージでインストールして動かしてみたところ、tensorflow 0.7.1だとcompute capabilityがサポート外ってことでGPUが有効にならなかったが、0.8.0以降だと普通に動かせるっぽい。

Releases · tensorflow/tensorflow · GitHubだけ見てもよく分からないけど、0.7.1〜0.8.0の間の変更でそのへん対応されていたのかな…?

このへん?

github.com

最新のEC2 GPU instanceでのTensorFlowセットアップ記事はこのへんのようだ。

ただrunfileからCUDA 7.5をinstallする場合?はdriverが古いとhangしてしまうなど問題があるっぽいのでそこは注意する必要がありそう。

0.9.0が正式リリースされたらまた真っさらなUbuntuから環境つくりなおしてみようっと。

EC2 Spot Instanceの価格変動をターミナルでモニタリングする

JavaScript AWS Node.js

というものを作った。こんな感じの。

f:id:sugyan:20160623232900p:plain

Repository : https://github.com/sugyan/spot-price-watcher

背景

TensorFlowを使ったDeep Learningでアイドルの顔識別なんかをずっとやってきたけど、やっぱりCPUマシンだけでやっていくのは厳しいな、と思い、

ということでAmazon EC2にてGPU instanceを使って動かすことにした。

g2.2xlargeインスタンスでTensorFlowを動かせるようにするのは、下記記事なんかを参考にしながらやってみたら何とか出来た。 ソースからビルドする必要があるのは時間かかってつらかったけど…

qiita.com

まぁ1回環境を構築してAMIを作ってしまえばその後は苦労せずに同じ環境を再現できるので便利ですね。

で、g2.2xlargeインスタンスを普通に作ると一番安いUSリージョンでも $0.65 per Hour かかるのだけど、これを出来るだけ節約したい。 今までEC2まともに使ったことなくて今回はじめて知ったのだけど、Spot Instancesというのがある。

スポットインスタンスでは、スポットインスタンスリクエストに入札価格を指定して、支払うことができるインスタンス時間あたりの価格を選択します。入札価格がその時点のスポット価格以上であれば、リクエストが受理されてインスタンスを実行できるようになります。このインスタンスの実行は、お客様がインスタンスを終了した時点と、スポット価格が入札価格を上回った時点のいずれか早い方までとなります。スポット価格は Amazon EC2 によって設定され、スポットインスタンス容量に対する需要と供給に応じて定期的に変動します。

製品の詳細 - Amazon EC2 スポットインスタンス - Amazon EC2 | AWS

これを使って、スポット価格が安い時間帯に実行すれば通常インスタンスよりかなり安く済ませられる。 ここ数日の大雑把な感覚だと、US regionでのg2.2xlargeだと $0.1 - $0.2 per Hour のときがあり、最大7〜8割お得になる計算。

ただこのスポット価格は刻一刻と変化しており、いつどのAvailability Zoneがどんな価格に変化するか分からないし、予測できない(これをDeep Learningで予測する、とかも課題としては面白いかもしれないw けどどんな要素が絡んでいるかもよく分からないしなぁ…)。

スポット価格が自分の入札価格を上回ってしまうとその時点でインスタンスは強制中断されてしまうので 中断されるのがイヤな場合はできるだけ高額の入札価格にしておけば良いのだけど、勿論そのスポット価格の分は課金されることになるわけで。

自分の場合は個人の趣味レベルでやってることだし アイドル顔認識モデルの学習なんかは数千step実行すれば十分で、g2.2xlargeインスタンスを使うとGPUの力で1stepあたり0.6秒弱、だから1時間あれば6000stepは進むのでそれくらいの間だけ動いてくれれば十分だったりする(CPUだと一晩以上かかっていた)。 たとえ最後まで終了する前に強制中断させられても「うーん残念。仕方ない」で割り切って またタイミングを見計らってやり直せばいいし、途中経過はちょいちょい書き出しているので実行中に様子を見つつそのcheckpointファイルを吸い出しておけば無駄になることは少ない。

なので、とにかく何も考えず「一番安くなっているもの」を使ってギリギリの入札価格で使いたい。 AMIさえコピーして転送しておけば使えるのでRegionもどこでも構わない。

で、EC2のWebコンソールからは「価格設定履歴」というのを開いて各Availability Zoneの価格の遷移を見ることができるのだけど、

f:id:sugyan:20160623234609p:plain

これだと

  • 選択しているRegionの中のAvailability Zoneでしか一覧できない
  • 高騰しているものも含めてすべて描画されて縮尺を変更できない
    • 最低価格付近だけを見たいのに…
  • 直近24時間より短い範囲で見られない

とか ちょっと不満があった。 何より、いちいちブラウザ開いてポチポチしないとチェックできないのは面倒。

  • ターミナルからコマンド一発で
  • 複数Regionで横断的に
  • 最低価格のあたりの直近の変動・値だけを見たい

と思ったので、そういうものを作った次第。

実装

まずコマンドラインからスポット価格の履歴を取得するのはAWS CLIで出来るし、

AWS コマンドラインインターフェイス | AWS

各言語から使うSDKも様々なものが公式から提供されている。

AWS のツール | AWS

どれを使うか迷ったけど、

  1. 各Regionから取得するのは並列で処理したい
  2. せっかくだからターミナル上にグラフを描画したい

1.はGoかNodeかな、という感じで 2.でbleesed-contribっていうのが以前話題になったのを思い出し、

https://github.com/yaronn/blessed-contrib

これを使ってみたかったというのと Goでも近いものでtermuiというものが実装されていたようだったけど こちらは複数データのLine Chartが未実装ということだったのでNodeを使うことにした。

最近ぜんぜんマトモにNode触っていなかったのでPromiseとかも初めて使ってみたくらいだったけど、勉強しながら書いてみました。 こういうのも公開されていて本当にありがたいです。

JavaScript Promiseの本

複数Regionから並列で各Availability Zoneの価格設定履歴を取得できれば、あとはそのデータを使って描画するだけ。 blessed-contribのバグも見事に踏んでしまったのでp-r送ったりもした。

せっかくなのでキー操作で縮尺・表示範囲を変えたり 最新データに更新できるようにしたりした。

f:id:sugyan:20160623232910g:plain

とりあえずこんなんで最新の価格と細かい値動きはターミナルから一覧できるようになって、便利。

もうちょっと汎用的に使えるように出来たかもしれないけど 多分こんなの自分しか使わないだろうし、ってことで対象RegionはUSの3つのみにしている(EU, APにもGPUインスタンスはあるけど常時高騰している)し 調べるのはg2.2xlargeの価格だけにしているし、AWS CLIのためのCredentialも既にaws configureとかで設定している前提で作っている。 ちょっと雑。

npmへのpublishは自重しておく。