Transgate という Node.js 製エージェントベースのタスクフローフレームワーク

Transgate というNode.js製のエージェントベースのタスクフロフレームワークを作った。

どうして作ったのか?

自宅の家電を操作するためのプログラムを書いていたら、色々なフローがごちゃごちゃになったから。 Dysonのファンから定期的に温度湿度を取得してデータベースに保存したり、Google Home/Assistant + IFTTT から来るメッセージを処理してIRKit を操作する。そのうち温度に従って自動的に IRKit 経由でエアコンを操作したくもなった、さてどう書こうかと?

どんなもの?

突然だけど空港などの荷物の仕分けをイメージしてください。エージェントは、ゲートから出てくるアイテムを受け取り、処理して別のゲートに送る。ゲートの向こう側がどうなっているかは、エージェントは何も知らない。エージェントは空のアイテムを来たら作業を終える。アーキテクチャのイメージはこんな感じです。

エージェントはゲートからアイテムを受け取ることと、別のゲートに新たにアイテムを送ることができる。アイテムはシンプルなオブジェクトだ。エージェントは自身のタスクに集中できる。だから前工程や次工程が増えても減ってもアイテムの構成が変わらなければ問題なく動く。そして入出力がシンプルなためユニットテストも簡単にかける。エージェントはゲートの実体を知らないので、入力元ゲートをスタブに、出力先ゲートをモックに、簡単に置き換えられる。

フレームワークに出てくる概念のまとめ

  • ゲート(Gate)はファイルストレージやデータベース、キュー、APIサービスといった入出力のエンドポイントです。
  • エージェント(Agent)はゲート間でアイテムを処理するタスクワーカーです。ゲートが何に通じているかは関知しません。
  • アイテム(Item)はゲート間を流れるシンプルなオブジェクトでエージェントが処理する対象です。

使用例

今回のフレームワークを作るきっかけになったホームコントロールプログラムを通じて説明してみます。 ちなみにこのプログラムは靴箱の中の Raspberry PI 上でデーモンとして動いています。

構成図

f:id:tilfin:20171123144501p:plain

メインプログラム (main.js)

const {
  Agent,
  HttpClientGate,
  HttpServerGate,
  IntervalGate,
  JointGate,
  StdoutGate,
  duplicator,
  mixer,
} = require('transgate');

const pino = require('pino')();
const config = require('konfig-yaml')();

const MongoGate = require('./lib/mongo_gate');
const IRKitGate = require('./lib/irkit_gate');

// Agent
const AnalysisCommander = require('./lib/analysis_commander');
const DysonCoolLinkRecorder = require('./lib/dyson/cool_link_recorder');
const EnvironmentalAnalyzer = require('./lib/environmental_analyzer');

// Gate
const slackGate = new HttpClientGate({ endpoint: config.slack.webhook_url });
const iftttGate = new HttpServerGate({ port: config.port });
const irkitGate = new IRKitGate(config.irkit.endpoint);
const intervalGate = new IntervalGate(60);
const mongoGate = new MongoGate(config.mongodb.endpoint, config.mongodb.collection);
const drToEaGate = new JointGate();

(async () => {
  try {
    await Agent.all(
      new AnalysisCommander(iftttGate, { irkitGate, slackGate }),
      new DysonCoolLinkRecorder(intervalGate, duplicator(mongoGate, drToEaGate)),
      new EnvironmentalAnalyzer(drToEaGate, { irkitGate, slackGate }),
    );
  } catch(err) {
    pino.error(err);  
    await iftttGate.close();
    await mongoGate.close();
  }

  intervalGate.clear();
})()
.catch(err => {
  pino.error(err);
});

7つのゲート

  • slackGate は slack にテキストメッセージをポストします。HttpClientGate のインスタンスで、アイテムとなるJSON{ "text": "<text message>" } です。
  • iftttGate は IFTTT の webhook から受け取った JSON をアイテムとして利用します。アイテムとなるJSON{ "target": "TV", "text": "<speaking words>" } です。
  • irkitGate はHTTPインターフェイスを備える赤外線送信器に命令します。アイテムとなるJSON{ "command": "celling_light_off" } です。
  • intervalGate は一定の間隔でアイテムを生成します。アイテムは { "time": <Date instance> } です。この場合は 1 分おきにエージェントの処理を走らせます。
  • mongoGate は MongoDB の指定のコレクションに送信されたアイテムを登録します。
  • drToEaGate は後述の DysonCoolLinkRecorder から EnvironmentalAnalyzer にアイテムの流すジョイントです。

3つのエージェント

  • AnalysisCommander は IFTTT の webhook から来た JSON をアイテムとして受け取り、操作対象とテキストから IRKit に対して送信すべき赤外線信号を指定します。slack には文言が解釈できなかったときにポストします。
  • DysonCoolLinkRecorder は Dyson PureCoolLink ファンから1分おきに温度と湿度を取得して、duplicator という複製機を挟んで MongoDB への書き込みとジョイントとなるゲートに送ります。
  • EnvironmentalAnalyzer はそのジョイントを通じて来た温度から閾値を超えていたらエアコンの操作を IRKit に要求します。自動的に操作をしたときは slack に記録します。

エージェントの実装

Agentのサブクラスを作ります。main メソッドで受け取ったアイテムを処理して指定先のゲートに新たなアイテムを送る処理を書きます。before/after のフックメソッドを使って、初期化処理や別に利用するプロセス(例えば headless chrome) をここで制御(起動・停止)します。

下記は EnvironmentalAnalyzer の実装例でです。室温が摂氏17度以下になったらエアコンをオンにします。

const { Agent } = require('transgate');

module.exports = 
class EnvironmentalAnalyzer extends Agent {
  async before() {
    this._preTemp = null;
    this._airconAlive = false;
  }

  async main(item, { irkitGate, slackGate }) {
    const curTemp = item.temp;

    if (this._preTemp && this._preTemp > 17 && curTemp <= 17) {
      if (!this._airconAlive) {
        await irkitGate.sendAll({ command: 'aircon_on' });
        this._airconAlive = true;
        await slackGate.send({ text: `Turn on aircon because temp is down to ${curTemp}` });          
      }
    }

    this._preTemp = curTemp;
  }
}

コンストラクタとアイテムの入力元ゲートが隠蔽されているのは、 null を受け取ると次のゲートに送り、自身は終了するという仕様の実装を意識させないためです。

特徴のまとめ

  • 複雑なデーモンやバッチプログラムに向いている。
  • 同じエージェントを並列で動かすようなことは想定していないので、大量に捌く処理には向いてない。
  • メインプログラムで登場するゲートとエージェントと、アイテムのタスクフローが定義できる。そのためこれだけで全体が把握できる。
  • エージェントの処理は async/await で擬似的に同期に書けつつ、エージェントが多くても Node.js なのでスレッドベースのように重くならない。
  • ゲートの置き換えが容易なので、エージェントのユニットテストが書きやすく、部分的な実行の確認もしやすい。

予想される疑問と答え

参照先サービスは全部ゲートになるのか? 

Noです。ゲート間は一方通行に限定されます。エージェントはその先を知らない。つまりリクエストを投げて、それに対するレスポンスを得ることはできません。往復ではなく、ループにすることは可能ですが、ステートレスなのでどの送り出したアイテム(リクエスト)に対してのレスポンスかはわからないのです。ゲートは、エージェントにとってトリガーとなるものを出す部分と成果を送る部分になります。

一連のフローが終わったら時にキッカーに完了を通知するには?

キューシステムはタスクが完了したら完了通知を送る必要が往往にあります。こういった場合は、アイテムにそのコンテキストを持たせてフローに流して、最後のゲートが完了通知を送る役割を担うようにします。

ロガーはゲートにすべきか?

ログがアウトプットそのものになるならゲートにすべきです。そうすれば後からゲートをさらに Agent にジョイントするものに置き換えて、そこからログ解析サービスに投げるといった修正も容易にできます。

ゲートにどこまでロジックを含めていいのか?

ゲートはできる限りシンプルな方が良いです。エージェントはテストしやすいように設計しますが、ゲートそのものにロジックを入れてしまうと入出力先を付け替えてテストできなくなります。ただプロジェクト共通のロジックでそれがフォーマット程度であれば、ゲートに実装してもいいでしょう。複雑ならばそれ用のエージェントを作ってゲートの前に置き、ジョイントで繋げるだけです。

Transgate に興味を持っていただけたら幸いです。

English version Transgate is Agent-based taskflow framework for Node.js

Windows 上に Python 環境を構築して Tensorflow GPU + Keras で日本古典籍字形の文字認識を試すまで

tilfin.hatenablog.com

こちらの記事で書いたように Windows で Tensorflow を GPU で試してみました。この中で Python をオフィシャルのインストーラから入れて virtualenv で動作環境を作成したのですが、後々 matplotlib や OpenCV を入れようとしたところ Windows のために諸々嵌ることがあったので、より簡単に All-in-One 導入できる Anaconda から入れてみることにしました。

www.procrasist.com

またこちらの記事で見た Keras が、今後 Tensorflow で色々と試す上で非常分かりやすそうだったので、導入してみることにしました。

Anaconda で Python 3.5 をセットアップ

既にインストールしていた Python は「プログラムと機能」からアンインストールしておきました。

Download Anaconda Now! | Continuum から [Python 3.5 version 64-BIT INSTALLER] をダウンロードして実行します。インストール ウィザードで環境変数を通しておきます。(デフォルトのインストール場所ではなく D:\Anaconda35 に自分は入れました。)

Tensorflow + Keras をセットアップ

環境構築

ここから先は PowerShell で行います。

conda で work という名前の環境を作ります。

PS D:\> conda create -n work python=3.5 anaconda

…省略…

Extracting packages ...
[      COMPLETE      ]|##################################################| 100%
Linking packages ...
        1 個のファイルをコピーしました。##################               |  71%
[      COMPLETE      ]|##################################################| 100%
#
# To activate this environment, use:
# > activate work
#
# To deactivate this environment, use:
# > deactivate work
#
# * for power-users using bash, you must source
#

Tensorflow の導入

work 環境にして pip で tensorflow と tensorflow-gpu をインストールします。

PS D:\> activate work

PS D:\> pip install tensorflow
Collecting tensorflow
  Using cached tensorflow-0.12.1-cp35-cp35m-win_amd64.whl
Requirement already satisfied (use --upgrade to upgrade): wheel>=0.26 in d:\anaconda3\lib\site-packages (from tensorflow)
Collecting protobuf>=3.1.0 (from tensorflow)
  Using cached protobuf-3.1.0.post1-py2.py3-none-any.whl
Installing collected packages: protobuf, tensorflow
Successfully installed protobuf-3.1.0.post1 tensorflow-0.12.1

PS D:\> pip install tensorflow-gpu
Collecting tensorflow-gpu
  Using cached tensorflow_gpu-0.12.1-cp35-cp35m-win_amd64.whl
Installing collected packages: tensorflow-gpu
Successfully installed tensorflow-gpu-0.12.1

Keras をセットアップ

Keras Documentation は numpy, scipy, pyyaml OpenCV, HDF5, hdf5 に依存しています。この中で OpenCV は anaconda で入らないので別途インストール必要があります。

OpenCV 3.2 をインストール

hikuichi.hatenablog.com 単純に pip でインストールしても Windows では動かないので、こちらを参考にビルド済みパッケージを使って、OpenCV をインストールします。 http://www.lfd.uci.edu/~gohlke/pythonlibs/ から opencv_python‑3.2.0+contrib‑cp35‑cp35m‑win_amd64.whl をダウンロードして、 pip で絶対パスを指定してインストールします。

PS D:\> pip install D:\opencv_python-3.2.0+contrib-cp35-cp35m-win_amd64.whl
Processing d:\opencv_python-3.2.0+contrib-cp35-cp35m-win_amd64.whl
Installing collected packages: opencv-python
Successfully installed opencv-python-3.2.0+contrib

Keras をインストール

PS D:\> pip install keras
Collecting keras
  Downloading Keras-1.2.0.tar.gz (167kB)
    100% |################################| 174kB 911kB/s
Collecting theano (from keras)
  Downloading Theano-0.8.2.tar.gz (2.9MB)
    100% |################################| 2.9MB 471kB/s
Building wheels for collected packages: keras, theano
  Running setup.py bdist_wheel for keras ... done
  Stored in directory: C:\Users\someone\AppData\Local\pip\Cache\wheels\f8\2c\8e\ffffff128220b1acf6fcd5e692269c6e8d607
44c15fd160c2c
  Running setup.py bdist_wheel for theano ... done
  Stored in directory: C:\Users\someone\AppData\Local\pip\Cache\wheels\96\2b\3d\ffffff24a7171a4afb7144d1e944a7be643b4
48b23a35b9937
Successfully built keras theano
Installing collected packages: theano, keras
Successfully installed keras-1.2.0 theano-0.8.2

Matplotlib, OpenCV, Keras を試す

Matplotlib を試す

REPL からシグモイド関数をグラフ描画して表示します。

PS D:\> python
Python 3.5.2 |Anaconda 4.2.0 (64-bit)| (default, Jul  5 2016, 11:41:13) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy as np
>>> import matplotlib.pylab as plt
>>> def sigmoid(x):
...   return 1 / (1 + np.exp(-x))
...
>>> x = np.arange(-5.0, 5.0, 0.1)
>>> y = sigmoid(x)
>>> plt.plot(x, y)
SetProcessDpiAwareness(2) failed: COM error 0xffffffff80070005  (Unknown error 0x0ffffffff80070005), using 2
[<matplotlib.lines.Line2D object at 0x000002370B81EA20>]
>>> plt.ylim(-0.1,1.1)
(-0.1, 1.1)
>>> plt.show()
>>>
>>> ^Z

f:id:tilfin:20170108213321p:plain

OpenCV を試す

REPL から画像ファイルを開いて表示してみます。

PS D:\> python
Python 3.5.2 |Anaconda 4.2.0 (64-bit)| (default, Jul  5 2016, 11:41:13) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import cv2
>>> cv2.imread('pic.jpg')
>>> img = cv2.imread('./pic.jpg')
>>> cv2.imshow('win1', img)
>>> cv2.waitKey(0)
13
>>>
>>> cv2.destroyWindow('win1')
>>> ^Z

f:id:tilfin:20170108213343p:plain

cv2.waitKey(0) の後、ウィンドウに対して Enter を入力すると表示されます。

日本古典籍字形の文字認識で Keras を試す

www.procrasist.com Keras を実際に試してみます。こちらの記事を参考に日本古典籍字形の文字認識を試してみました。

書名一覧 | 日本古典籍字形データセット の「機械学習による文字認識」から サンプルコード(TAR+GZ 24.57 MB) をダウンロードします。 D:\pmjt_sample_20161116 に展開しました。

PS D:\pmjt_sample_20161116> python .\run.py
Using TensorFlow backend.
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library cublas64_80.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library cudnn64_5.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library cufft64_80.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library nvcuda.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library curand64_80.dll locally
X_train shape: (19909, 28, 28, 1)
19909 train samples
3514 test samples
Train on 19909 samples, validate on 3514 samples
Epoch 1/12
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:885] Found device 0 with properties:
name: GeForce GTX 960
major: 5 minor: 2 memoryClockRate (GHz) 1.253
pciBusID 0000:01:00.0
Total memory: 2.00GiB
Free memory: 1.64GiB
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:906] DMA: 0
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:916] 0:   Y
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:975] Creating TensorFlow device (/gpu:0) -> (device: 0, name: GeForce GTX 960, pci bus id: 0000:01:00.0)
19909/19909 [==============================] - 5s - loss: 1.4426 - acc: 0.5135 - val_loss: 0.7607 - val_acc: 0.7803
Epoch 2/12
19909/19909 [==============================] - 3s - loss: 0.7044 - acc: 0.7817 - val_loss: 0.4843 - val_acc: 0.8677
Epoch 3/12
19909/19909 [==============================] - 3s - loss: 0.5310 - acc: 0.8401 - val_loss: 0.3561 - val_acc: 0.8973
Epoch 4/12
19909/19909 [==============================] - 2s - loss: 0.4451 - acc: 0.8646 - val_loss: 0.3038 - val_acc: 0.9124
Epoch 5/12
19909/19909 [==============================] - 2s - loss: 0.3969 - acc: 0.8815 - val_loss: 0.2888 - val_acc: 0.9121
Epoch 6/12
19909/19909 [==============================] - 2s - loss: 0.3606 - acc: 0.8928 - val_loss: 0.2586 - val_acc: 0.9266
Epoch 7/12
19909/19909 [==============================] - 2s - loss: 0.3389 - acc: 0.9012 - val_loss: 0.2390 - val_acc: 0.9306
Epoch 8/12
19909/19909 [==============================] - 2s - loss: 0.3208 - acc: 0.9065 - val_loss: 0.2283 - val_acc: 0.9308
Epoch 9/12
19909/19909 [==============================] - 2s - loss: 0.2999 - acc: 0.9116 - val_loss: 0.2147 - val_acc: 0.9351
Epoch 10/12
19909/19909 [==============================] - 2s - loss: 0.2819 - acc: 0.9176 - val_loss: 0.2038 - val_acc: 0.9397
Epoch 11/12
19909/19909 [==============================] - 2s - loss: 0.2677 - acc: 0.9181 - val_loss: 0.1995 - val_acc: 0.9394
Epoch 12/12
19909/19909 [==============================] - 2s - loss: 0.2605 - acc: 0.9219 - val_loss: 0.1968 - val_acc: 0.9405
Test score: 0.196767529901
Test accuracy: 0.940523619806

Ubuntu に OpenCV をインストールして Python で画像から顔をクロップするまで

以前に Tensorflow のセットアップをして、付属するサンプルを試すところまでして終わっていましたが、 実際に何か試してみないとということで、よくある顔判別をやるためにデータを用意する仕組みを作ろうとしてます。

Python 3 と OpenCV 3 を使って画像から顔検出してそれを正方形でクロップして同一サイズにするというスクリプトを作ります。

セットアップ

ほぼ下記のサイトの通りやったところうまくいきました。 www.pyimagesearch.com 以前にcudaのライブラリを VirtualBox 上の仮想環境にに入れていたせいでコンパイルエラーになりましたが、 それも下記のオプションを指定して進められました。 codeyarns.com

スクリプト

Python はほとんど書いたことないのですが、色々と調べてなんとかそれらしいものができました。

import cv2
import sys
import os


class FaceCropper(object):
    CASCADE_PATH = "data/haarcascades/haarcascade_frontalface_default.xml"

    def __init__(self):
        self.face_cascade = cv2.CascadeClassifier(self.CASCADE_PATH)

    def generate(self, image_path, show_result):
        img = cv2.imread(image_path)
        if (img is None):
            print("Can't open image file")
            return 0

        #img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        faces = self.face_cascade.detectMultiScale(img, 1.1, 3, minSize=(100, 100))
        if (faces is None):
            print('Failed to detect face')
            return 0

        if (show_result):
            for (x, y, w, h) in faces:
                cv2.rectangle(img, (x,y), (x+w, y+h), (255,0,0), 2)
            cv2.imshow('img', img)
            cv2.waitKey(0)
            cv2.destroyAllWindows()

        facecnt = len(faces)
        print("Detected faces: %d" % facecnt)
        i = 0
        #height, width = img.shape[:2]

        for (x, y, w, h) in faces:
            r = max(w, h) / 2
            centerx = x + w / 2
            centery = y + h / 2
            nx = int(centerx - r)
            ny = int(centery - r)
            nr = int(r * 2)

            faceimg = img[ny:ny+nr, nx:nx+nr]
            lastimg = cv2.resize(faceimg, (32, 32))
            i += 1
            cv2.imwrite("image%d.jpg" % i, lastimg)


if __name__ == '__main__':
    args = sys.argv
    argc = len(args)

    if (argc != 2):
        print('Usage: %s [image file]' % args[0])
        quit()

    detecter = FaceCropper()
    detecter.generate(args[1], True)

CASCADE_PATH は OpenCV に付属している顔検出の XML 定義のファイルパスを指定します。 顔検出は face_cascade.detectMultiScale の部分で最低の検出サイズを 100x100 として、scaleFactor=1.1, minNeighbors=3 を色々試行して良さそうな値としました。 また下記のサイトを参考にさせてもらいました。 物体検出(detectMultiScale)をパラメータを変えて試してみる(minNeighbors編) | Workpiles

スクリプトJPEGファイルパスを指定すると、サイズ 32x32 の imageナンバー.jpg というファイルが検出された数だけ保存されます。 generateメソッドの第2引数を True にすると検出状態をウィンドウで表示します。

実装例によっては一旦グレースケールに変換しているものを見ましたが(上記ではコメントアウトしてます)、手元の画像で試した限り結果に差はなさそうでした。データセットとしてDeep Learningに用いることまで考慮するとモノクロの方が情報削減にはなりそうですが。

TensorFlow が正式に Windows サポートして GPU が使えたので試してみた

Google から正式に Tensorflow が Windows 対応して GPU が使えるとのアナウンスがありました。
https://developers.googleblog.com/2016/11/tensorflow-0-12-adds-support-for-windows.html

セットアップ環境

この環境で TensorFlow を試しみたいと思います。なるべくDドライブにセットアップするようにしています。

CUDA と cuDNN をインストール

CUDA Toolkit 8.0

https://developer.nvidia.com/cuda-downloads

  • Operating System: Windows
  • Architecture: x86_64
  • Version: 10
  • Installer Type: exe (network)

普通にインストーラを実行しました。

cuDNN v5.1

開発者アカウントを登録して利用規約に同意してダウンロードしました。

https://developer.nvidia.com/rdp/cudnn-download cuDNN v5.1 Library for Windows 10 を落として zip 内の cuda フォルダ内を C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0 にコピー展開します。

Python 3.5 のセットアップ

Windows の場合 Anaconda を使った方が後々楽なのでこちらを奨めます。 (2017/01/08 追記)
http://tilfin.hatenablog.com/entry/2017/01/08/220556 内に記載してます。

普通にトップページからダウンロードすると 32bit 版なので、https://www.python.org/downloads/windows/ から Download Windows x86-64 executable installer をダウンロードしてインストールします。 ※ なお、 D:\Python35 にインストールしました。

3.5系は環境変数の追加や pip も同時に入れてくれました。 PowerShell から一応 pip のアップグレードもしました。

virtualenv をインストール

実行環境用の作業フォルダを作れるモジュールを入れます。

PS D:\> pip install --upgrade virtualenv
Collecting virtualenv
  Downloading virtualenv-15.1.0-py2.py3-none-any.whl (1.8MB)
    100% |################################| 1.8MB 646kB/s
Installing collected packages: virtualenv
Successfully installed virtualenv-15.1.0

TensorFlow のパッケージをインストール

PowerShell から pip で tensorflow と tensorflow-gpu を入れます。 https://pypi.python.org/pypi/tensorflow

tensorflow

PS D:\> pip install tensorflow
Processing y:\tensorflow-0.12.0rc0-cp35-cp35m-win_amd64.whl
Collecting six>=1.10.0 (from tensorflow==0.12.0rc0)
  Using cached six-1.10.0-py2.py3-none-any.whl
Collecting protobuf==3.1.0 (from tensorflow==0.12.0rc0)
  Downloading protobuf-3.1.0-py2.py3-none-any.whl (339kB)
    100% |################################| 348kB 2.2MB/s
Collecting wheel>=0.26 (from tensorflow==0.12.0rc0)
  Using cached wheel-0.29.0-py2.py3-none-any.whl
Collecting numpy>=1.11.0 (from tensorflow==0.12.0rc0)
  Downloading numpy-1.11.2-cp35-none-win_amd64.whl (7.6MB)
    100% |################################| 7.6MB 179kB/s
Requirement already satisfied (use --upgrade to upgrade): setuptools in d:\python35\lib\site-packages (from protobuf==3
1.0->tensorflow==0.12.0rc0)
Installing collected packages: six, protobuf, wheel, numpy, tensorflow
Successfully installed numpy-1.11.2 protobuf-3.1.0 six-1.10.0 tensorflow-0.12.0rc0 wheel-0.29.0

tensorflow-gpu

PS D:\> pip install tensorflow-gpu
Collecting tensorflow-gpu
  Downloading tensorflow_gpu-0.12.0rc0-cp35-cp35m-win_amd64.whl (32.5MB)
    100% |################################| 32.5MB 40kB/s
Requirement already satisfied: wheel>=0.26 in d:\python35\lib\site-packages (from tensorflow-gpu)
Requirement already satisfied: numpy>=1.11.0 in d:\python35\lib\site-packages (from tensorflow-gpu)
Requirement already satisfied: six>=1.10.0 in d:\python35\lib\site-packages (from tensorflow-gpu)
Requirement already satisfied: protobuf==3.1.0 in d:\python35\lib\site-packages (from tensorflow-gpu)
Requirement already satisfied: setuptools in d:\python35\lib\site-packages (from protobuf==3.1.0->tensorflow-gpu)
Installing collected packages: tensorflow-gpu
Successfully installed tensorflow-gpu-0.12.0rc0

実行開始

virtualenv で D:\tensorflow に作業フォルダを作ります。

PS D:\> virtualenv --system-site-packages D:\tensorflow
Using base prefix 'd:\\python35'
New python executable in D:\tensorflow\Scripts\python.exe
Installing setuptools, pip, wheel...done.

学習解析サンプルを用意する

適当なところで git clone --recurse-submodules https://github.com/tensorflow/tensorflow します。自分は普段 VirtualBoxLinux を動かしていて SMB でファイル共有するのでそちらでクローンしました。 tensorflow/tensorflow/modelsD:\tensorflow\models となるようにコピーします。

MNIST を試す

手書き数字の解析プログラムを試してみます。

PS D:\> cd tensorflow\tensorflow\models\image\mnist
PS D:\tensorflow\models\image\mnist> python convolutional.py
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfu
lly opened CUDA library cublas64_80.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfu
lly opened CUDA library cudnn64_5.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfu
lly opened CUDA library cufft64_80.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfu
lly opened CUDA library nvcuda.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfu
lly opened CUDA library curand64_80.dll locally
Successfully downloaded train-images-idx3-ubyte.gz 9912422 bytes.
Successfully downloaded train-labels-idx1-ubyte.gz 28881 bytes.
Successfully downloaded t10k-images-idx3-ubyte.gz 1648877 bytes.
Successfully downloaded t10k-labels-idx1-ubyte.gz 4542 bytes.
Extracting data\train-images-idx3-ubyte.gz
Extracting data\train-labels-idx1-ubyte.gz
Extracting data\t10k-images-idx3-ubyte.gz
Extracting data\t10k-labels-idx1-ubyte.gz
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:885] F
ound device 0 with properties:
name: GeForce GTX 960
major: 5 minor: 2 memoryClockRate (GHz) 1.253
pciBusID 0000:01:00.0
Total memory: 2.00GiB
Free memory: 1.64GiB
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:906] D
MA: 0
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:916] 0
:   Y
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:975] C
reating TensorFlow device (/gpu:0) -> (device: 0, name: GeForce GTX 960, pci bus id: 0000:01:00.0)
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:586] C
ould not identify NUMA node of /job:localhost/replica:0/task:0/gpu:0, defaulting to 0.  Your kernel may not have been bu
ilt with NUMA support.
Initialized!
Step 0 (epoch 0.00), 50.9 ms
Minibatch loss: 8.334, learning rate: 0.010000
Minibatch error: 85.9%
Validation error: 84.6%
Step 100 (epoch 0.12), 12.1 ms
Minibatch loss: 3.226, learning rate: 0.010000
Minibatch error: 4.7%
Validation error: 7.3%
Step 200 (epoch 0.23), 12.0 ms
Minibatch loss: 3.404, learning rate: 0.010000
Minibatch error: 10.9%
(省略)
Minibatch loss: 1.609, learning rate: 0.006302
Minibatch error: 0.0%
Validation error: 1.0%
Test error: 0.8%

割と早く終わったので GPU が効いているのでしょう。上手く動いたことは確認できましたが、わかりやすい ImageNet を次に試します。

ImageNet を試す

画像を解析して何の画かを当てる ImageNet です。

PS D:> cd \tensorflow\models\imagenet

まず準備です。 python .\classify_image.py を実行します。

PS D:\tensorflow\models\image\imagenet> python .\classify_image.py
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library cublas64_8
0.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library cudnn64_5.
dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library cufft64_80
.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library nvcuda.dll
 locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library curand64_8
0.dll locally
>> Downloading inception-2015-12-05.tgz 100.0%
Successfully downloaded inception-2015-12-05.tgz 88931400 bytes.
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:885] Found device 0 with properties:
name: GeForce GTX 960
major: 5 minor: 2 memoryClockRate (GHz) 1.253
pciBusID 0000:01:00.0
Total memory: 2.00GiB
Free memory: 1.64GiB
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:906] DMA: 0
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:916] 0:   Y
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:975] Creating TensorFlow device (/gpu:0)
 -> (device: 0, name: GeForce GTX 960, pci bus id: 0000:01:00.0)
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:586] Could not identify NUMA node of /jo
b:localhost/replica:0/task:0/gpu:0, defaulting to 0.  Your kernel may not have been built with NUMA support.
W c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_def_util.cc:332] Op BatchNormWithGlobalNormalization is depr
ecated. It will cease to work in GraphDef version 9. Use tf.nn.batch_normalization().
W c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\bfc_allocator.cc:217] Ran out of memory trying to allocate
 1.91GiB. The caller indicates that this is not a failure, but may mean that there could be performance gains if more memory is available.
giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca (score = 0.89233)
indri, indris, Indri indri, Indri brevicaudatus (score = 0.00859)
lesser panda, red panda, panda, bear cat, cat bear, Ailurus fulgens (score = 0.00264)
custard apple (score = 0.00141)
earthstar (score = 0.00107)

適当に M:\fuji.jpg に富士山の写真をおきました。 python .\classify_image.py --image_file M:\fuji.jpg で解析させます。

PS D:\tensorflow\models\image\imagenet> python .\classify_image.py --image_file M:\fuji.jpg
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library cublas64_8
0.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library cudnn64_5.
dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library cufft64_80
.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library nvcuda.dll
 locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library curand64_8
0.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:885] Found device 0 with properties:
name: GeForce GTX 960
major: 5 minor: 2 memoryClockRate (GHz) 1.253
pciBusID 0000:01:00.0
Total memory: 2.00GiB
Free memory: 1.64GiB
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:906] DMA: 0
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:916] 0:   Y
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:975] Creating TensorFlow device (/gpu:0)
 -> (device: 0, name: GeForce GTX 960, pci bus id: 0000:01:00.0)
E c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:586] Could not identify NUMA node of /jo
b:localhost/replica:0/task:0/gpu:0, defaulting to 0.  Your kernel may not have been built with NUMA support.
W c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\framework\op_def_util.cc:332] Op BatchNormWithGlobalNormalization is depr
ecated. It will cease to work in GraphDef version 9. Use tf.nn.batch_normalization().
W c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\bfc_allocator.cc:217] Ran out of memory trying to allocate
 1.91GiB. The caller indicates that this is not a failure, but may mean that there could be performance gains if more memory is available.
volcano (score = 0.91087)
fire screen, fireguard (score = 0.00192)
alp (score = 0.00162)
lakeside, lakeshore (score = 0.00130)
geyser (score = 0.00077)

volcano (score = 0.91087) 火山と認識されましたね。ちなみに写真は雪化粧してる富士山でした。

W c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\bfc_allocator.cc:217] Ran out of memory trying to allocate
 1.91GiB. The caller indicates that this is not a failure, but may mean that there could be performance gains if more memory is available.

と警告が出ていたのでもっとメモリがあるといいのでしょう。

とりあえず特に嵌らずに動いたのでみなさんもお試しください。

AWS IAM Switch Roleのリストを増やすChrome Extensionを作った

AWS Management Console において、AWSアカウントから他のAWSアカウントにスイッチロールできますが、これの最近の履歴が5つまでしか表示されない。あくまでも最近使ったものという位置付けなんだと思います。

某仕事上、1つのアカウントから多数の他のアカウントに Switch Role にせざる得ないので専用の URL をブックマークしていました。しかし、これでも毎回補完された入力フォームが挟まれて怠い(このページ遷移が結構かかります)。

このような理由で、Switch Role の履歴(リスト)を拡張する Chrome Extension を作りました。Chrome Web Store で公開しているので簡単にインストールできます。

chrome.google.com

f:id:tilfin:20160809233207p:plain

特徴

  • Switch Role の履歴に設定に定義されたプロファイル(アカウント)の分が項目増えるリスト拡張される。
  • リストには <プロファイル名> | <AWSアカウントID>AWSアカウントID がわかりやすくなる。
  • 色指定ができる(固定色ではない)。
  • スイッチロール後に黒いヘッダーの下部に指定色のバーが表示されてより現在のプロファイルが識別しやすくなる。
  • Chrome Sync (端末間) で設定は共有されます。

設定

ブラウザの URL バー右に並ぶ拡張のアイコンをクリックして、ポップアップしたテキストエリアに CLIで同様の設定となる ~/aws/config ファイルと同様のフォーマットで定義して保存するだけです。色指定 (例.color = ffcc99) も CSS ライク(先頭 # なし)で指定できます。GitHubの README を詳しくは参照ください。

GitHub - tilfin/aws-extend-switch-roles: Extend your AWS IAM switching roles by Chrome extension

f:id:tilfin:20160809233023p:plain

GitLab CI から Google Cloud Pub/Sub 経由で自動デプロイ

自分用のちょっとしたWebサービスをさくら VPS環境に構築しています。こんなオレオレサービスでも Git リポジトリの master ブランチに push したら自動的にデプロイできるようにしたい。毎回 SSH して手動で更新はしたくない。

一般的な方法だと、どうしてもデプロイ先のサーバーにリクエストの受け先を用意する必要があります。概ね HTTP 経由で受けないといけないので、そこから派生するアクションの内容から鑑みても、セキュリティ的にグローバル空間に置かれたサーバでやりたくないものです。

そこで自前で Pub/Sub サービスを経由してやってみることにしました。Pub/Sub であれば懸念事項のアクセスを受け入れることなく、Subscriber も Pull 型で設置できます。

Google Cloud Console

まず Google Cloud Console - 認証情報サービスアカウントを作成しておきます。次に Google Cloud Console - Pub/Subトピックを作成します。そしてそのトピック対して用意したサービスアカウントにPub/Sub サブスクライバ―Pub/Sub パブリッシャーの権限を与えます。

自動デプロイの仕組みを作る

スクリプトは Node.js を使います。gcloud モジュールをグローバルで使えるようにしておきます。下記の package.json の定義とおり サブスクライバーとなる agent.js と通知をする publish.js をそれぞれ作ります。

package.json

{
  "name": "myapp",
  "scripts": {
    "agent": "NODE_PATH=`npm root -g` node agent.js",
    "publish": "NODE_PATH=`npm root -g` node publish.js"
  },
  "dependencies": {
    "gcloud": "^0.36.0"
  },
  "private": true
}

通知

GitLab CI から呼び出して Pub/Sub にデプロイメッセージを発行するスクリプトを作ります。

publish.js

gcloudオブジェクトからpubsubオブジェクトを取得して、トピック名からtopicオブジェクトを取得して、通知を行うというシンプルなものです。

'use strict';

const gcloud = require('gcloud')({
  keyFilename: 'サービスアカウントのキーJSONファイルパス',
  projectId: 'Google Cloud ConsoleのプロジェクトID'
});

const pubsub = gcloud.pubsub();
const topic = pubsub.topic('トピックID');

topic.publish({
  data: {
    command: 'deploy'
  }
}, function(err) {
  if (err) {
    console.error(err);
  } else {
    console.info('Published deploy message!');
  }
});

デプロイエージェント

トピックのサブスクライバーとしてメッセージを受信したら、Gitリポジトリから最新の内容を反映してデーモンを再起動するエージェントを作ります。

agent.js

topicオブジェクトを取得して、subscribe(購読)します。このとき1番目の引数が購読名になるため、ホスト名を使って複数のエージェントが存在する状態でもそれぞれに通知されるようにします。同じ名前にしてしまうと同一のサブスクライバーとみなされて、メッセージがいずれかのエージェントにしか飛びません。

'use strict';

const os = require('os');
const path = require('path');
const execFile = require('child_process').execFile;

const gcloud = require('gcloud')({
  keyFilename: 'サービスアカウントのキーJSONファイルパス',
  projectId: 'Google Cloud ConsoleのプロジェクトID'
});

function startDeploy() {
  const child = execFile('deploy.sh',
    (error, stdout, stderr) => {
      if (error) {
        throw error;
      }
      console.log(stderr);
      console.log(stdout);
    });
}

const pubsub = gcloud.pubsub();
const topic = pubsub.topic('トピックID');
const name = 'sub_' + os.hostname();

var opts = {
  autoAck: true, // メッセージを取得したことの承認を自動で行う
  reuseExisting: true, // 再購読時にエラーならないように再利用する
  interval: 60
};

topic.subscribe(name, opts, (err, subscription, apiResponse) => {
  if (err) {
    console.error(err);
    return;
  }

  console.info('Subscribed');

  subscription.on('message', (msg) => {
    const data = msg.data;
    const command = (data && data.command) || null;

    if (command === 'deploy') {
      console.info('Received deploy message id:%s', msg.id);

      try {
        startDeploy();
      } catch (ex) {
        console.error(ex);
      }
    } else {
      console.warn('Received unexpected message:');
      console.warn(msg);
    }
  });
});

注意事項として Pull 型サブスクライバーは接続の度に課金カウンタが上がるので、 interval はできるだけ長くした方が良いです。 メッセージが来たら下記のような deploy.sh スクリプトを用意してリポジトリを更新してサービスをリフレッシュします。

deploy.sh

#!/bin/bash
git fetch --all
git checkout --force origin/master

npm run deploy # サービスリフレッシュ
echo "Deployed"

GitLab CI からキック

下記の設定ファイルをリポジトリに設置するとデプロイステージで発行します。gcloudモジュールのインストールはそれなりの大きさで時間がかかるので、 https://hub.docker.com/r/tilfin/gitlab-deployer/ で公開している Docker で Node.js と gcloud を入れてあります。これコンテナを deploy タグ付きで CI Runner 登録しておく良いでしょう。

.gitlab-ci.yml

stages:
  - deploy

deploy_job:
  type: deploy
  script:
    - npm run publish
  tags:
    - deploy
  only:
    - master

動作確認

実行環境でも Node.js + gcloud をセットアップしておき npm run agent してサブスクライバーを起動しておきます。 GitLab のリポジトリの master に push すると Pub/Sub メッセージを経由してサブスクライバーがデプロイスクリプトを実行します。