加具留矢流余

かぐるやるよ

Dropbox上の画像を直接アノテーションできるサービスを作った。Part 2

前回に引き続きanntというサービスを作った話です。 part.2 では主に技術面の話について書いていきます。

目次

フロントエンドの話

今回始めてvue.jsを使ったのですが本当に便利です。何が便利かってサイトを構成するパーツを分けて実装できるのが本当に楽でした。anntはvue-routerを使ってSPAとして作成したのですが、思ったよりぬるぬる動いてこれは良いものだとなりました。実装していく際にも各ページ毎にComponentとして実装していくので、特にファイルが肥大化することもなく読みやすく作れます。勢いで作ったので一部は肥大化してしまいましたが。

これまでjs周りでjquery以上の何かを使ったことが無かったので、個人用のツール作るときは読みづらい+肥大化する⇒放置というパターンに陥っていたのですが、適当な粒度でパーツ毎に切り出していけるのでダイアログとか作るのはすごい楽でした。

f:id:theflyingcat28:20200621212417p:plain
vueで作ったダイアログ

正直Reactなどの他の仮想DOMライブラリとの違いを把握していませんが、何か新しいWebアプリ作りたいってなったら学習コスト含めてもこの手のライブラリを使っていくのが良いのかなぁと思いました。

個人的に気になるのはこの先どのくらいサポートされるのだろうか...という点です。なんかしょっちゅう新しいライブラリが出てきているイメージがあるので、徐々に廃れていって突如開発中止になるのは怖いなぁと思います。まぁでもこれだけ勢いのあるライブラリであればしばらくは心配する必要も無いのでしょうか。

canvas周りについて

実際にアノテーション作業を行う画面はほとんどcanvasでできています。作業の大半の時間はここを作るために費やされた気がします。大変だったのは座標の変換処理とBoundingBoxを作るところでした。

座標の処理を作っている際は発狂していました。addEventListenerでclickなどのeventの座標を取得⇒canvas上の座標に変換⇒画像上の座標に変換、と言葉にしてしまえば3ステップですが、画像を回転させたりすると変換がどんどん複雑になって沼にハマりました。本当はこの辺の座標変換の処理を上手く適当なクラスに隠蔽して画像上の座標だけを意識して作業できるようにすれば良かったんですが、気がついたら後戻りできないところにいた気がしたので力づくで成し遂げました。リファクタリングは今後の課題です。

BoundingBoxはいつか見た下のリンクに非常に参考になる実装があったので、ありがたく勉強させて頂きました。

hashrock.hatenablog.com

これまでペイントツールとかで良く使っていたもののはずが、いざ実装してみるとBoundingBoxのことを全然理解していないということに気が付きました。特に画面外に持っていこうとしたときの処理とか全然作れなくて、結局動いているのかどうか怪しい実装になってしまった気がします。

DropboxAPI

DropboxAPIを公開しているのでoauthで認可してもらえれば普通に叩けます。ドキュメントがしっかりしているのでそこまで苦労はしませんでしたが、APIによって引数の渡し方が違ったりするとこで少し躓きました。でもドキュメントをしっかり読んでれば回避できたことなので、ドキュメントをしっかり読みましょうという教訓を得ました。あと作りきった後にSDKが存在していることを知りました。下調べをしっかりしましょうという教訓も得ました。ここに関しては反省しかないです。

バックエンドの話

firebaseでjavascriptを使いました。どうしてもクライアント側に持たせたくない処理だけをサーバー側で持つようにしました。なので全部で500行行ってなかったです。firebaseは安いし楽だし便利なのですが、個人的にはKey-Value型のやつがよくわからないのでRDBが使いたかったです。それ以外は満足です。

pythonライブラリの話

特に書くことも無いのですが、こだわったポイントとしてはプロパティを使って, x, y, width, heightという表記でも, left, top, right, bottomという表記でもどちらでも使えるようになっています。widthを書き換えればrightの表記も書き換わるので割と便利だと思います。ぜひ使ってみてください。自分の環境でしかテストが出来ていないので動くかが不安ですが。

画像の読み込みだったり表示だったりはopencvでやっているので全部でこちらも500行くらいでした。READMEだったりsetup.py書く時間の方が長かったです。

まとめ

いろんなライブラリがあって本当に便利な世の中だなぁと思いました。なので作業する前にちゃんと公式のライブラリが無いか探してドキュメントを読む癖をつけましょう。お兄さんとの約束です。作り終わった後に必要だったライブラリ見つけると本当に時間を無駄にした感がすごいです。

mikebird28.hatenablog.jp

mikebird28.hatenablog.jp

Dropbox上の画像を直接アノテーションできるサービスを作った。Part 1

個人開発でサービス作りました。宣伝かねて苦労話・モチベーションとか個人開発に興味がある方に向けて公開します。 まずはPart 1ということで、コンセプトだったり動機だったりとメンタル面の話です。(技術の話も少し触れます)

何を作ったの

anntというブラウザ上で動作するアノテーションツールです。

f:id:theflyingcat28:20200618235123j:plain
anntのアノテーション画面

特徴としては

という点でvottとかlabelimgとかとは差別化できてるのかなと思います。開発期間は約1ヶ月+数日です。

作った動機

一番の動機は自分の技術を具体的な形で世に送り出したかったということだと思います。要するに承認欲求です。今まで割と趣味でプログラムを書いてきたんですけど競馬の予測だったり、自分で使うためのAI用のライブラリだったり自分の中だけで完結していました。そんな中で突如自分が作ってきたものは独りよがりのゴミではとネガティブな感情が湧いてきて気がついたらサービス開発を始めていました。

そんな中、既存のアノテーションツールがイケてないなと思っているうちに(vott使っていました)、気がついたら開発を始めていました。割と勢いって大事だなとこのとき思いました。あとお金ですね。一発うまく当てたら儲かるんじゃないかという下心です。思い返してみると100%下心というかドロドロした理由で開発が始まっていました。

当初の構想と開発の流れ

割と勢いで始まったプロジェクトですが、割と最初にコンセプトは考えていました。vott使っていて微妙だなーと思っていたのが、

  • jsonに画像のパスが絶対パスで書かれていて移動しづらい
  • 出力結果のパーサー作るのめんどい(調べてないだけでライブラリがあるかも)
  • iPad買ったのでiPadでも作業したい

というような点でした。じゃあこれ解決した僕の考えた最強のアノテーションツールを作ればいいってことじゃん!って思ってまずはコンセプトをまとめます。

Step.1 コンセプト設計

  • どこでも作業ができて
  • 作業が終わったらすぐにAIの開発に集中できて
  • 一度アノテーションしたらフォルダのコピーとかも簡単にできる!

これでコンセプト完成!やったぜっ!ってなります。コンセプト考えるのそんなに大事?作りながら考えれば?という意見もありますが、個人的には何をしたいか・譲れない点を明確にしておいたほうが挫折しにくくなると考えています。 一部のスーパープログラマーでも無い限り理想の姿に実装速度は追いつきません。そこで大事になってくるのが機能の実装をいかに上手く諦められるか、だと思います。

例に取るとanntの当初の機能要件は

  • DropboxとOneDriveとGoogleDriveで動作して
  • PC・スマホタブレットでも動作して
  • 出力は独自形式はもちろんのことPACAL VOCとかもサポートして
  • BoundingBoxはもちろん他のタスクにも対応して
  • 専用pythonライブラリでは基本的な前処理もサポート etc...

というキラキラの夢物語でした。実際にはDropboxでしか使えなくて、基本はPC上(一部タブレットも)、出力は独自形式。というような感じのものになったのでいかに夢に向かって歩く中で要らない機能が切り捨てられたかがわかるかと思います。人は強くないので最初にコンセプトを明確にして、あとで辛くなったときに必要の無い機能は実装しないという逃げ道を用意しておくのがいいと思います。

Step.2 使用技術の選定

使ったのはfirebase+vueです。すべてjavascriptです。今までjavascriptなんて最低限しか触ったことが無かったので最初は大変でした。

じゃあ何でjavascriptにしたの?って話になりますが、主に作業負荷の軽減です。まず最初にコスト的な面でfirebaseを採用するのは決めていました。小さいアプリであればスケールもして安いfirebase等のサービスを使うに限ります。firebase使うとなるとあまり複雑な処理はサーバー側でしたくないなと思ってSPAにすることにしました。じゃあvueでいいやとノリで決めて、サーバー再度も頭の中で言語切り替えるのめんどくさいのでjavascriptでいいやとなりました。適当ですね。あまり後悔していないのでまぁいいかと思ってます。

Step.3 もくもくと開発

あとは正直もくもくと開発していくだけです。開発の細かい話はまた今度書けたらと思っています。

モチベーションの維持

完成に近づいてない実感が一番モチベーションが折れます。完成に近づいてない気がしてくると、このサービスゴミでは...他の競合があるので使ってくれないのでは...という魔が入り込んできます。ほっておくとシャドウになってゲームオーバーになるので、まずは最短で何かしら完成するのがいいのかなと思っています。ちなみに無事公開を終えた今は晴れ晴れとした気持ちです。

折れそうになったら他の人の開発記とか読むとMPが回復します。 例) uyamazak.hatenablog.com

www.memory-lovers.blog

shinyorke.hatenablog.com

逆に似たことやってる人見つけるとMPが削れるので注意してください。 例)企業なので比にならないレベルで良さげなものを作っています。

engineer.dena.com

個人開発やってみたいと思っている人へ

自分で実際にやってみて思ったのは、考えているだけじゃなくてまずは手を出してみること、終わらない...もうマジ無理...ってなったときは切り捨てることだと思います。実はanntの前にサービスを作っていたのですが、割と重めのバックエンドでの処理+画像認識モデル3つが必要になるサービスで70%ぐらい完成したところで諦めました。実際に作り始めてみると開発はなかなか進みません。なのでまずは現実的なゴールを設定して走り切るのが大事なのかなと思っています。まだ生まれたてのサービスしか作ってない癖に偉そうなこと言って申し訳ないですが、ぜひ個人開発やってみてください。

さいごに

締めと見せかけて宣伝です。anntは現在プロジェクトを3つまで作成できますが。将来的には無料枠は1プロジェクト、それ以上は有料という形式にしようとしています。ただ今始めていただければ将来的にも3プロジェクトまで無料で使えるようにしようと考えているので、ぜひ皆さん登録してください!(必死)

mikebird28.hatenablog.jp

mikebird28.hatenablog.jp

パッケージを作成してpypiにアップロード(2020/06)

作成したライブラリをpypiに公開しようとしたが情報過多で迷子になった。
やるべきことをトップダウンに書き出せば後世の人々が救われる気がしたのでメモを残す。
困ったことがあれば公式のチュートリアルを見るのが一番。

docs.python.org

  1. ライブラリの準備
  2. 必要なファイルの準備

    • setup.pyを作成
    • setup.cfgの作成
    • LICENCEファイルの準備
    • READMEの準備
  3. 配布物の作成

    • sdistの準備
    • bdist, wheelの準備
  4. 公開の準備

    • pypiに登録
    • 配布物のアップロード

ライブラリの準備

以下3つのライブラリが最低限必要 * setuptools * wheel * twine 脳死で以下のコマンドを実行すればOK。

python -m pip install setuptools wheel twine

必要なファイルの準備

LICENCEやREADMEなどは正直用意して配置するだけなので取り立てての説明は必要に無いと思うが、setup.pyとsetup.cfgに関しては歴史的な経緯で見るサイトによって様々な方法が書いてあるように思う。2020年6月の時点ではsetup.pyは最低限にして、setup.cfgに必要な情報を記載していくのがいいようだ。

setup.py

かつてのdistutilやsetuptoolsではsetup.py内に必要なメタデータをすべて記載しなければならなかったが、setuptoolsのバージョン30あたりを皮切りに自動でsetup.cfgに記載されているデータを読み込んでくれるようになったようだ。なのでsetup.pyは以下のように最低限必要な内容(3行)にして、静的な情報はsetup.cfgに記載するのが管理しやすいように思う。素人なのでよくわからないがインストール時に動的に変更したい内容などがある場合にはsetup.pyに記載するのがいいのだろうか。

#!/usr/bin/env python
from setuptools import setup
setup()

setup.cfg

[metadata]
name = project name
version = 0.0.1
author = Taro Tanaka
author_email = example@example.com
description = project short description
long_description = file:README.md
long_description_content_type = text/markdown
url = https://mikebird28.hatenablog.jp/
licence = file: LICENCE
python_requires = >=3.5
classifiers = 
  Development Status :: 2 - Pre-Alpha
  License :: OSI Approved :: MIT License
  Programming Language :: Python :: 3.5
  Programming Language :: Python :: 3.6
  Programming Language :: Python :: 3.7
  Programming Language :: Python :: 3.8
  Programming Language :: Python :: 3.9

上みたいな感じで必要な情報を記載していけばいい。long_descriptionやlicenceなど情報量が多くなりそうなところは、file: という表記を使って外部ファイルを読み込むことができる。分かりづらいのはclasifiersとlong_descriptionだろうか。

classifiersに指定した情報はユーザーがpypi内で検索するときなどに使われる。Classifiers · PyPIに使用可能なclassifier一覧があるのでここからコピーして貼り付けて行けばいい。種類がありすぎてざっとしか目を通せなかったが、開発状況(アルファ、ベータなど)、自然言語(日本語、英語など)、プログラミング言語pythonのバージョンだけでなく、CやRなどの他の言語も指定できる)、ライセンス、その他トピックなどが指定できるようだ。

long_descriptionには直接setup.cfgに記載してもfile:でREADMEを読み込んでもどちらでも大丈夫だが、MARKDOWNファイルの場合にはlong_description_content_typeでファイルの形式を明示的に指定する必要がある。

README.md

README。気合入れて書く。この記事参考にして頑張った。 deeeet.com

LICENCE

ライセンスを選ぶ。GPLBSD、MIT等々好きなやつを選んでLICENCE.txtに記載する。

配布物の準備

これまでの作業で必要なファイルの準備が整ったので、公開に向けてパッケージを配布可能な形式に変換する。pypiでライブラリを公開するには以下2種類のアーカイブを用意する必要がある

これらのファイルをpypiにアップロードすると現在のpipではwheel形式を優先的にインストールする。

sdist

python setup.py sdist

bdist

python3 setup.py bdist_wheel

dist/以下にアーカイブされた配布用ファイルが配置されている。

  • dist/package_name.whl
  • dist/package_name.tar.gz

最悪sdistのみで配布して、ユーザー側でインストールするときにビルドしてもらえば最悪大丈夫?
ただwheel形式で配布することで不利益を被ることは無さそうなので、作成しない選択肢は無さそう。

pypiへの登録

pypiは本番環境とテスト環境の2つがあるので、それぞれアカウントを作成する。

アカウントの準備が整ったらtwineを使ってテスト環境にパッケージをアップロードする

テスト環境へのアップロード

python -m twine upload --repository testpypi dist/*

本番環境へのアップロード

python3 -m twine upload --repository pypi dist/*

最後にpypiにログインして正しくアップロードができているかを確認すれば終了。 ちなみに今回自分が作成したパッケージは↓なので興味があれば見てみてください。現時点では使い物にならないです。 pypi.org

参考文献

Python パッケージングの標準を知ろう - Tech Blog - Recruit Lifestyle Engineer

過去に計算したDataFrameと内容が同じか高速に判定したい

同じ前処理を実行するたびに数十分取られるのが非常に辛く感じる。 特に同じ入力を入れているのに実行するたびに計算をしなおすのが馬鹿らしく感じる。 かといってDataFrameを丸々キャッシュして、関数呼び出すたびに前回のDataFrameと同じか検証するものアホらしい。

もやもやしつつfeatherとhashでうまいことやれば高速に検証できね?って思ったので頑張ってみた。

コンセプトは以下の通り 1. featherを使ってDataFrameをbytesに変換 2. 高速なハッシュアルゴリズムxxhashを使ってbytesのハッシュ値を計算。 3. 初回はハッシュ値とDataFrameを何かしらの形でファイルに保存する。 4. 2回目以降はハッシュ値を計算して、前回のハッシュ値と同じだったら保存されている計算結果を読み込み。

pyarrowを使うとfeatherの出力先にfile-like objectが使えるので、シリアライズしたDataFrameをメモリ上(io.BytesIO)に吐き出せて良い。

以下実装

import io
import pyarrow.feather
import xxhash
import os
import gc
import pandas as pd

def get_hash(df):
    fp = io.BytesIO()
    pyarrow.feather.write_feather(df, fp)
    b = fp.getvalue()
    fp.close()
    h = xxhash.xxh64(b).hexdigest()
    return h

def dfcacher(func, df, name, path="./", verbose=False):
    h = get_hash(df)
    h_path = os.path.join(path, name+".xxhash")
    f_path = os.path.join(path, name+".feather")
    
    # 前回のハッシュ値を読み込み
    try:
        with open(h_path, "r") as fp:
            saved_h = fp.read().rstrip('\n')
    except FileNotFoundError:
        saved_h = ""
        
    if h == saved_h:
        # 前回とハッシュ値が変わらないのであれば計算結果をキャッシュから読み込み
        if verbose: print(f"Hash value of  {name} is not changed. Load dataframe from cache.")
        del(df)
        gc.collect()
        df = pyarrow.feather.read_feather(f_path)
    else:
        if verbose: print(f"Hash value of  {name} is changed.")
        # ハッシュ値が変わっていれば計算して計算結果を保存
        df = func(df)
        with open(h_path, "w") as fp:
            fp.write(h)
        df.to_feather(f_path)
    return df

# テストケース1
df1 = pd.DataFrame([[1, 2, 3], [4, 5, 6]])
df2 = pd.DataFrame([[1, 2, 3], [4, 5, 6]])
assert get_hash(df1) == get_hash(df2), "ハッシュ値が異なります"

# テストケース2
df1 = pd.DataFrame([[1, 2, 3], [4, 5, 6]])
df2 = pd.DataFrame([[1, 2, 4], [4, 5, 6]])
assert get_hash(df1) != get_hash(df2), "ハッシュ値が等しいです"

# テストケース3
def test_func1(df):
    df = df.apply(lambda x: x+2)
    return df

df1 = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=["c1", "c2", "c3"])
df2 = pd.DataFrame([[1, 2, 4], [4, 5, 6]], columns=["c1", "c2", "c3"])
df = dfcacher(test_func1, df1, "test_func1", verbose=True)
df = dfcacher(test_func1, df1, "test_func1", verbose=True)
df = dfcacher(test_func1, df2, "test_func1", verbose=True)

問題点としては高速にシリアライズするためにfeatherを使ってるので、DataFrameがfeatherフォーマットの条件を満たしてないとうまく動かない。 あと余りしっかり検証していないこと。まだ実際に使ってないので使ってる途中になにか問題が出てきそうな気がする。 ただ体感としてhash値の計算はかなり高速に動作している。今度なにか機会があったら速度の検証をやろうと思っている。

深層学習+Hough変換で紙の領域検出

kaggleやるよ何て名前をつけておいて1年半くらいkaggleやってませんでした。

その間何やってたかって言うとAI使ったWebサービスを作ろうとして挫折してを繰り返してました。 今のところ何の生産物も無いっていう悲惨な状況なんですけど、しょうもないノウハウ達は手に入れたのでせめてもの手向けとして公開しようかなと思う。

① 紙の領域検出

画像の中に含まれる領域を検出するメジャーな方法としてはCanny特徴量でエッジ検出+Hough変換で直線の検出っていう組み合わせがメジャーだと思います。 ただCanny特徴量は紙以外の物体が入っていたり紙面に色々描かれていたりすると、余分なエッジまでも検出しちゃってその後の工程が失敗しちゃう可能性があったりします。

f:id:theflyingcat28:20191109214911j:plain
Cannyでのエッジ検出の例
なので深層学習とかで都合よく検出できたりしないかなーって探していたら、偉大なDropboxが技術ブログを公開してました。

blogs.dropbox.com

今のChromeには素晴らしい翻訳機能があるので紹介するまでもないとは思いますが、彼らがやってることとしては、

  1. 何らかの深層学習の方法(各ピクセルのエッジらしさを0~1の小数で返す)でエッジを検出
  2. 検出したエッジに対してHough変換で直線を検出
  3. 得られた直線から構成可能な四角形の候補をリストアップ
  4. DNNの出力(0~1の実数)を四角形の辺に沿って平均してスコアとする。
  5. スコアの最も高かった四角形を、紙の領域として提案

というような感じでした。 ただ肝心な「何からの深層学習の方法」ということが示されていなかったので適当にCNNを使ったエッジ検出方法を見ていたら何個か見つかりました。

HED(Hostically-Nested Edge Detection)

画像をCNNで畳み込んで行き、各レイヤーの出力をSide Layerと呼ばれる層で逆畳み込みしてまとめて結合して出力という方法。画像中に含まれるエッジのピクセルの数は非エッジのピクセルに比べて圧倒的に少ないので、エッジピクセルの数に応じて誤差関数を重み付けしたりエッジ検出に特化した工夫がなされている。詳細は参考URLを見てください。この手法のメリットはVGG16を転移学習して使うので学習コストが軽い点だと思います。逆にデメリットはネットワークの構造的に画像の情報を余り深く理解できていなさそうなとこ。ただ紙の認識では雑に紙のエッジを検出してくれれば大丈夫なので、そんなデメリットは気にしなくていい気もします。高みを目指したい人はネットワークの構造をM2DetみたいにPyramid構造とかに変更したら精度あがるんじゃないかと思う(妄想)。

f:id:theflyingcat28:20191014235129p:plain
HEDの論文から引用

論文URL: https://arxiv.org/abs/1504.06375

参考URL: http://blog.livedoor.jp/tmako123-programming/archives/51956636.html

CASENet

HEDは画像の各ピクセルに対してエッジかそうでないかを学習する手法でしたが、CASENetはラベルが付けられた複数種類のエッジを検出するための手法です(Semantic Edge Detection)。HEDは二値分類問題を前提としているので、複数種類の矩形物体を検知したい場合にはこちらを検討することになると思います。自分では試してないので何とも得いませんが、論文を読む感じ基本的なネットワークのコンセプトはHEDに近しいものを感じます(当然細部は全く異っていますが)。 参考URL: https://arxiv.org/abs/1705.09759

HEDは2015年、CASENetは2017年の論文なので最新の論文を漁ればもっと良さげな手法もあるかと思いますが、とりあえず紙を検出するくらいであれば、この辺の手法で行けるのではと思います(CASENetの方は試してないので何とも言えませんが)。

試してみた

HEDの中間層を適当に変えたり出力層のチャネル数変えたり色々改造したもの(細かい条件は忘れた)を使って実際の紙を認識できるか試してみました。 f:id:theflyingcat28:20191109215322p:plain Cannyは紙だけでなくペンのエッジも拾ってるのがわかります。 一方、HED使って予測したものはペンのエッジは検出されず、紙の輪郭だけをしっかり拾っているのが確認できます(ペンの部分を補完することはできていませんが)。今回はシンプルな画像で試しましたが色々写り込んでいてCannyでうまくエッジの矩形の検出ができないときは、深層学習+Hough変換でもいいかもねっていう記事でした。

Docker使って手軽にGCEで機械学習できる環境を整えた

最近、強化学習的なとこに手を出してみたけど手元のMacbook AirじゃメモリやCPU的に厳しいのでクラウドで実行することを検討した。 最初はAmazon EC2借りようかなーと考えていたけれど、Google Compute Engine(GCE) だとプリエンプティブという格安実行モードがあることを知り、GCEで実行することにした。プリエンプティブモードで実行すると、1日しかインスタンスを実行できない、突然インスタンスが停止するなどのデメリットがある代わりに、通常価格より50%以上安くインスタンスを利用できる。

しかし毎回ファイルを転送してsshで繋いで実行するのは正直面倒くさい。そこでDockerを使って効率的にプログラムをGCEで実行する環境を整えた。 具体的には機械学習用イメージのビルド→イメージのアップロード→コンテナの起動・実行をコマンドラインからささっと実行できるようにした。

環境構築にあたって以下の2つのサービスを使用した。

  • Google Compute Engine
  • Google Container Registry(Dockerイメージのアップロード先)

Google Container Registryは使用したネットワークとストレージ分だけ課金されるので、少しでも課金額を抑えたい場合には1レジストリだけ無料で使えるDocker Hubなどを使った方がいいかもしれない。

注意: 今回使用するスクリプトGoogleクラウドサービスを使用するので、当然のことながら課金が必要になります。特にコンテナの実行までシェルスクリプトで実行してしまうので、変な設定で実行すると青天井の請求が来る可能性があります。ここに載っている内容を実行する場合には自己責任で実行してください。

Step.0 前準備

gcloudのインストール

Google Compute Engineをコマンドラインから使うためにglcoudをインストールする・ ここでは導入方法の説明は省くがGCPのgcloudコマンドをインストールする - Qiitaを参考にすればインストールできると思う。

プロジェクトの作成・Google Container RegistryのAPI有効化

Google Cloud Platformにアクセスして新しいプロジェクトを作成する。 左のサイドーからContainer Registryを選択してAPIを有効化する。(Google Container Registry以外のサービスを使う人、既に有効化している人はこの作業をする必要はありません)

スクリプトで使用する用の環境変数の設定

今回の説明ではプロジェクト名や、イメージの名前を環境変数に設定して使用する(スクリプトにベタ打ちしてGithubにアップしちゃうのが怖いので)。 CLOUDML_HOST_REGIONにはasia.gcr.ioeu.gcr.ious.gcr.ioなどから選べる(参考:Pushing and Pulling Images  |  Container Registry  |  Google Cloud)。CLOUDML_PROJECTIDには作成した自分のプロジェクトIDを、CLOUDML_IMAGE_NAMEには適当にイメージの名前を設定する。

export CLOUDML_HOST_REGION="REGION TO UPLOAD"
export CLOUDML_PROJECT_ID="YOUR PROJECT ID"
export CLOUDML_IMAGE_NAME="YOUR IMAGE NAME"

Step1. Dockerfileの準備

普通にDockerfileを作成する。今回はtensorflow、keras、lightgbmのCPU版をインストールしている。また実行するファイルをビルドするときに変えられるように実行ファイル名を変数として持たせている。注意点としてpythonの引数に-uを指定しないと出力がフラッシュされなくて、Stack Driverにログが正しく表示されないのでちゃんと指定する。

Step2. アップロード用のスクリプトの準備

DockerfileからイメージをビルドしてGoogle Container Repositoryにアップロードするスクリプトを作る。

実行したいファイルを引数に渡すとそのファイルを実行するイメージをビルドしてくれる。main.pyというスクリプトを実行するイメージを作成したい場合には以下のように実行する。

./deploy.sh main.py

実行に成功すると最後にレポジトリの名前を出力してくれる。

Step3. 実行用のスクリプトの準備

使用するイメージの名前、使うインスタンスの名前を指定するとGCE上で実行してくれるスクリプトを作る。 細かいインスタンスの設定はgcloud beta compute instances create-with-containerの引数で行う。 今回は4コアCPU、メモリ20GB、ディスクサイズ50GB、プリエンプティブ有効の設定で実行した。 他の設定で実行したい場合には一度手動でインスタンスを作成して課金額を確認したあとに https://cloud.google.com/sdk/gcloud/reference/beta/compute/instances/create-with-containerを参考にして引数を決定するのが良いと思う。

2018年11月4日現在gcloud beta compute instances create-with-containerは実行することができるが、ベータ版のコマンドなので今後のアップデートで実行できなくなる可能性がある。

実行してみる

今回作成したスクリプトを使用してkerasのmnistを実行してみる。今回作成したスクリプト類はGitHub - mikebird28/cloudml-templateに置いてある。環境変数などを設定したあとにdeploy.shを実行すると長々とビルドメッセージが流れたあと最後にレポジトリの名前を教えてくれる。

./deploy.sh main.py

deploy.shの実行が終わったら次のようにしてコンテナを実行する。今回はインスタンス名としてtest-instance1を指定したが、自分で実行するときは好きな名前を使用して問題ない。

./exec_container.sh test-instance1 レポジトリ名

コンテナの実行に成功するとインスタンスIPアドレス、CPU数などの詳細が出力される。ブラウザ上でGCEのインスタンス一覧を確認するとコンテナが実行されていることが確認できる。またインスタンスを選んでログを表示すればプログラムが実行されていることが確認できる。

f:id:theflyingcat28:20181104212909p:plain f:id:theflyingcat28:20181105020524p:plain

今回はCPUしか使用してないがDockerfileとexec_container.shを修正すればGPUも使用できるようになるはず。現状exec_container.pyにバグがあるみたいで、処理が一通り終わるともう一度コンテナがはじめから実行されてしまう。時間があったら今後修正する予定。

参考

機械学習の前処理を効率的にするPreprepっていうライブラリを作った

最近kaggleは放置して機械学習を使った競馬の予想をやってるんですけど、少しパラメータを変えただけのコードを実行するだけなのに、毎回一から前処理をしていると時間がかかりすぎて死ぬという問題が発生していました。

そんな悩みを解決するために、実行するときに前処理をキャッシュしておいて、次に実行するときには以前と変化した部分だけを計算するPreprepというライブラリを作りました。

GitHub - mikebird28/preprep

よく何言ってるかわからないと思うので実際に例を見てもらったほうが早い気がします。

import preprep
import pandas as pd


def func1(df):
    return df*2

def func2(df):
    return df*3

def func3(df):
    return df*4

def func4(df):
    return df*5

def func5(df,n = 1):
    return df*n



df = pd.DataFrame([[1,2,3],[4,5,6]])

#キャッシュするディレクトリを指定してインスタンスを作成
p = preprep.Preprep("./cache_files")

#前処理の関数を登録 func1からfunc3を順番に実行していく
p = p.add(func1,name = "func1")
p = p.add(func2,name = "func2")
p = p.add(func3,name = "func3")

#fit_geneで実際に前処理を実行
df = p.fit_gene(df,verbose = True)
print(df.head())

1回目の出力

[*] start running graph
[*] no cache exists for func1, calculate
[*] no cache exists for func2, calculate
[*] no cache exists for func3, calculate
    0    1    2
0  24   48   72
1  96  120  144

2回目の出力

[*] start running graph
[*] available cache for func1 exists, skip calculation
[*] available cache for func2 exists, skip calculation
[*] available cache for func3 exists, skip calculation
    0    1    2
0  24   48   72
1  96  120  144

1回目に実行したときはキャッシュが存在していないので全ての前処理を計算していますが、2回目に実行するときにはキャッシュが存在しているので計算をスキップしています。キャッシュが存在している状態で、fit_geneに渡すDataFrameが変化すると次のようになります。

#DataFrameの値を変更
#df = pd.DataFrame([[1,2,3],[4,5,6]])
df = pd.DataFrame([[1,2,3],[4,5,7]])

#キャッシュするディレクトリを指定してインスタンスを作成
p = preprep.Preprep("./cache_files")

#前処理の関数を登録
p = p.add(func1,name = "func1")
p = p.add(func2,name = "func2")
p = p.add(func3,name = "func3")

#fit_geneで実際に前処理を実行
df = p.fit_gene(df,verbose = True)
print(df.head())
[*] start running graph
[*] saved cache for func1 exists, but dataset hash value has changed, calculate
[*] saved cache for func2 exists, but dataset hash value has changed, calculate
[*] saved cache for func3 exists, but dataset hash value has changed, calculate
    0    1    2
0  24   48   72
1  96  120  168

ちゃんと入力されたDataFrameが変化していることを検出して計算をやり直しています。またfunc3中身を書き換えたり、他の関数に変えたりすると、func3以前はキャッシュから読み込んで、func3以降だけ計算をやり直してくれます。ちゃんと内部で依存グラフを作ってそれを解決しているので、今回のような関数を順番に実行していくだけでなく、枝分かれしたり、結合したりみたいな複雑なものでもちゃんと必要なとこだけ計算をやり直してくれます。


df = pd.DataFrame([[1,2,3],[4,5,7]])

p = preprep.Preprep("./cache_files")

#func3をfunc4に変更
p = p.add(func1,name = "func1")
p = p.add(func2,name = "func2")
#p = p.add(func3,name = "func3")
p = p.add(func4,name = "func3")

#fit_geneで実際に前処理を実行
df = p.fit_gene(df,verbose = True)
print(df.head())

出力

[*] start running graph
[*] available cache for func1 exists, skip calculation
[*] available cache for func2 exists, skip calculation
[*] saved cache for func3 exists, but op hash value has changed, calculate
     0    1    2
0   30   60   90
1  120  150  210

実際に自分で使っていますが、以前に比べて飛躍的に実験を回すスピードが上がったと思います。これまで前処理に毎回30分くらいかかっていましたが、preprepのおかげで変化したとこだけ実行するので今ではほとんど前処理に時間を取られません(入力するDataFrameが変わったりすると、1から計算しなおさないといけないのでやっぱり時間がかかりますが)。

インストールの方法ですがpipで配布しているので下記のコマンドでインストールできる(はず)です。

pip install preprep

まだ生まれたばっかのライブラリなのでバグが多かったりドキュメントが存在してなかったりするので今後その辺はこつこつ直していこうかなと思います。バグとか見つけたらここかgithubでぜひ教えていください。