読者です 読者をやめる 読者になる 読者になる

のどあめ

ググってもなかなか出てこなかった情報を垂れ流すブログ。

【Tensorflow】TFRecordファイルでshuffle_batchしたときの偏り調査

Tensorflowでは色々な形式のデータのRead/Writeに対応しています。 これらの入力形式の中で最もスタンダードなのがTFRecord形式です。(とドキュメントに書いてました)

TFRecord形式のファイルには、1つのファイルに複数のデータを格納できるので、 毎日大量に生成されるようなデータを扱う場合は、日毎にデータを作ればとてもファイルの整理がしやすそうです。

一方で、Tensorflowにはミニバッチを使った学習を簡単に行うための、tf.train.shuffle_batch関数が用意されています。 これは、複数の入力ファイル内のデータをshuffleしてミニバッチにしてくれる便利関数で、 大量データを扱うことを前提にした実装をするため、擬似的にshuffleしたバッチを作ります。

この「擬似的」というのが曲者で、 うっかり正例・負例を一つのファイルにそれぞれまとめて、 サンプルコード通りのパラメータでshuffle_batchを使ったりすると、 バッチ内が全部ラベルが同じであるミニバッチができてしまったりします。 (Reading data  |  TensorFlowをちゃんと読めば分かる話なのですが…)

今回は、TFRecordファイルでshuffle_batchを使う場合に、どのようにデータを保存すべきか、パラメータはどう選べばよいかを調査しました。

結論としては、以下の2点がわかりました。

  1. shuffle_batchを使いたいときは、 1つのTFRecordにパラメータcapacityより十分少ない数のデータしか入れてはいけない。
  2. バッチ作成のパフォーマンスを上げるときは、num_threadsパラメータを1以上にするか、shuffle_batch_joinを使う。

では、以下に詳細を述べます。

事前知識

TFRecord形式

TFRecordはTensorflowのstandardなファイル形式で、中身はProtocol Bufferっぽいです。 1ファイルに、等形式の異なる値で構成されるデータ(int, floatの組等)を1ファイルに複数書き込むことができます。

公式ドキュメント: https://www.tensorflow.org/versions/r0.12/api_docs/python/python_io.html#tfrecords-format-details

tf.train.shuffle_batch

Tensorflowでshuffleしたミニバッチを使いたいときに利用する関数です。

公式ドキュメント: https://www.tensorflow.org/versions/r0.12/api_docs/python/io_ops.html#shuffle_batch

先に述べたとおりshuffleと書いているので、よしなに入力ファイルをshuffleしてくれような気がしますが、 正確なshuffleではなく(大量データを想定して)Queueをつかった擬似的なshuffleでミニバッチを作ります。

その為、入力データの1ファイルに複数データが入っている場合は、 パラメータを適切な値にしなければ、ミニバッチに含まれるデータに偏りが生まれます。

tf.train.shuffle_batchの偏り検証

使用したスクリプト

github.com

TFRecordの作成

https://github.com/ykicisk/minibatch_TFRecord/blob/master/create_tfrecords.py

詳細は省略しますが、このスクリプトを動かすと、以下のように10個のTFRecordファイルができます。 それぞれには、5000個ずつデータが入っています。

今回はあとでミニバッチを作るときに、どのデータがどのファイルの何番目に入っていたかをわかりやすくするため、 データの中にfile_idx(ファイルが何番目), record_idx(何個目のデータ)、ランダムなnumpy.ndarray(2x4)を格納しています。

$ mkdir data
$ ./create_tfrecords.py data
wirte: data/file00.tfrecord
wirte: data/file01.tfrecord
wirte: data/file02.tfrecord
wirte: data/file03.tfrecord
wirte: data/file04.tfrecord
wirte: data/file05.tfrecord
wirte: data/file06.tfrecord
wirte: data/file07.tfrecord
wirte: data/file08.tfrecord
wirte: data/file09.tfrecord

$ tree data
data
├── file00.tfrecord
├── file01.tfrecord
├── file02.tfrecord
├── file03.tfrecord
├── file04.tfrecord
├── file05.tfrecord
├── file06.tfrecord
├── file07.tfrecord
├── file08.tfrecord
└── file09.tfrecord

0 directories, 10 files

TFRecordの偏りの可視化

https://github.com/ykicisk/minibatch_TFRecord/blob/master/visualize_mini_batch.py

上記のcreate_tfrecords.pyで作ったTFRecordファイルでミニバッチを作り、その偏りを可視化するスクリプトです。 ミニバッチを作るときのパラメータを引数で指定できます。

tf.train.shuffle_batchのパラメータ

詳細は後述しますが、とりあえずざっくりどんなものがあるかだけ先に並べておきます。

パラメータ ざっくりした説明
batch_size ミニバッチのサイズ
min_after_dequeue Queue(後述のExampleQueue)からdequeueしたときにQueueに最低でも残るデータ数
capacity Queueのサイズ
num_threads 入力Queue(後述のFilenameQueue)のenqueueするthread数

上記のスクリプトでは、パラメータcapacityについてはドキュメントに習いcapacity = min_after_dequeue + 3 * batch_sizeで固定、他のパラメータはスクリプトで指定します。

また、引数--joinで、shuffle_batchの代わりにshuffle_batch_join(後述)を使います。 この場合、引数--num_threadsで指定した数のTFRecordReaderを作成します。

動作例

# パラメータnum_threadsを指定して、shuffle_batch_joinを利用する
# dataは入力ファイル、tb_logはTensorBoardのログファイルを書き出すパス
$ ./visualize_mini_batch.py --num_threads 10 --join data tb_log

f:id:ykicisk:20161218164734p:plain

横軸がバッチのインデックス、縦軸がレコードのインデックス、色がファイルを表します。 散布図の一点が1データを表し、何番目のバッチでどのファイルの何番目のデータが出てきたかを示しています。(全データ点の5%のみ表示します)

上記の例では、バッチ前半では各ファイルの前半のデータがよく出てきて、バッチ後半で後半のデータが出てくることがわかります。 つまり偏っています。

幾つかパラメータを変えて検証

検証では、偏りにはあまり関係ないbatch_sizeを10に固定して、 他のパラメータを幾つか変えてみます。

(検証1)公式ドキュメントの例と同じパラメータ

Reading data  |  TensorFlowに書かれたパラメータを試します。

$ ./visualize_mini_batch.py --min_after_dequeue 10000 --num_threads 1 data tb_log_default

f:id:ykicisk:20161218164748p:plain

1ファイル内でのshuffleは若干後半のデータが後に出てくる傾向がありますが、それなりにできている気がします。

ただし、バッチをつくるときに同じファイルのデータしか出ていないことがわかります。 上記の結果の場合はfile01, file08のデータが前半のバッチによく出現して、file04, file09のデータは後半のバッチによく出現しています。

(検証2) shuffle_batch_joinを利用

Reading data  |  TensorFlowを読むと、 「ファイル間でもっとシャッフルしたいなら、tf.train.shuffle_batch_joinを使う(超意訳)」みたいなことが書いています。

とりあえず使ってみます。

$ ./visualize_mini_batch.py --min_after_dequeue 10000 --num_threads 10 --join data tb_log_t10_join

f:id:ykicisk:20161218164734p:plain

今度はファイル間のシャッフルはいい感じになっていますが、 バッチ前半でファイル前半データが、バッチ後半でファイル後半のデータが出るようになりました。

検証3| num_threadsを利用

公式ドキュメントをさらに読むと「 他にはtf.train.shuffle_batchnum_threadsパラメータを1より大きくする手もある(超意訳)」 みたいなことが書いています。

とりあえず使ってみます。

$ ./visualize_mini_batch.py --min_after_dequeue 10000 --num_threads 10 data tb_log_t10

f:id:ykicisk:20161218165154p:plain

一見ではデフォルトパラメータと何も変わっていない様に見えます。

考察

shuffle_batchの挙動

Reading data  |  TensorFlowとプログラムの挙動を見る限り、 shuffle_batchは、以下のように動いているようです。

  1. ファイル単位でshuffleして、Filename Queueにenqueueする。
  2. Filename Queueからファイルを1つずつReaderが読み込み、Readerが指定したDecoderをつかってデータをデコードする。
  3. Readerはデコードしたデータ、ExampleQueueにenqueueする。
  4. shuffle_batchは、ExampleQueueが一定数(min_after_dequeue + batch_size?)溜まったら、ExampleQueueをshuffleして上からbatch_size個を出力する

コードではこんな感じになります。

# filenamesから、shuffle済みfilename_queueを作成
filenames = ["fileA", "fileB", "fileC"]
filename_queue = tf.train.string_input_producer(filepaths, shuffle=True)

# shuffle済みfilename_queueから、ファイルを1つずつ読み込むReaderを作成する
reader = tf.TFRecordReader()
_, selialized_data = reader.read(file_queue)

# decoder
data_def = {"key": tf.FixedLenFeature([2, 4], tf.float32)}  # データ形式を指定
example = tf.parse_single_example(selialized_data, features=data_def)

# パラメータを指定してバッチ作成
data_batch = tf.train.shuffle_batch(
    example,
    batch_size=batch_size,
    capacity=capacity,
    min_after_dequeue=min_after_dequeue,
    num_threads=num_threads,
    allow_smaller_final_batch=True
)

何故偏ってしまったか?

Reading data  |  TensorFlowのGIFを使って説明します。

shuffle_batchは入力された全データではなく、ExampleQueueをshuffleします。 shuffle_batchの中身が偏っていたのは、そもそもExampleQueueが偏っていたのが原因です。

今回は1ファイルに5000個のファイルが入っていて、 ExampleQueueのサイズcapacitymin_after_dequeue + 3 * batch_size = 10000 + 30 = 10030でした。

この場合、ReaderがFilenameQueueから3つ目のファイルを読み込み始めたあたりでExampleQueueが一杯になり、 shuffle_batchがほぼ2つ目のファイルのデータしか入っていないExampleQueueをshuffleしてミニバッチを作る→一部のファイルのデータしかバッチに含まれない、となっていたと思われます。

(再掲) f:id:ykicisk:20161218164748p:plain

shuffle_batch_joinの挙動

shuffle_batch_joinでは、先に述べた「Filename Queueからファイルを1つずつReaderが読み込む」というところが、 並列化されて複数のReaderで複数のファイルを同時に読み込んでExampleQueueにデータをenqueueします。

ただし、全Readerがデータの1/5程度(全体では、5000 * 10 / 5 = 10000 ≒ capacity) を読み込んだ時点で ExampleQueueが一杯になってしまうため、最初の方のバッチはファイル前半のデータで占められてしまっっていたと思われます。

(再掲) f:id:ykicisk:20161218164734p:plain

num_threadsを1より大きくした場合の挙動

Reading data  |  TensorFlowを良く読むと、 「num_threadsを1より大きくすると、1つのファイルをReaderが同時に読むようになって早くなるよ」的なことが書いてあります。 How toのGIF的にいえば、Reader1, Reader2というのがまさにこれに当たります。

複数スレッドでReaderが動いたとしても、1つのファイルからデータを読み込んでいるので、ExampleQueue内の偏りは解消できません。 そのため、データの偏りを可視化したときは、デフォルトパラメータと同じような結果になったと思われます。

では、num_threadsをいつ使えばよいかというと、ファイルのロードやdecodeに時間がかかる場合に、shuffle_batchが学習のボトルネックになるのを防ぐ目的で使えそうです。 この様子はReading data  |  TensorFlowにある通り、Tensorboardをみればわかります。

shuffle_batchをつかうと、TensorBoardのEVENTSにqueueというグラフが追加されます。

$ tensorboard --logdir=tb_log_default 

f:id:ykicisk:20161218175238p:plain

ドキュメントが見つからなかったので詳細は良くわかりませんが、 どうやらqueue/shuffle_batch/fraction_over_10000_of_30_fullというのが、ExampleQueueがいっぱいになっている割合みたいなものを表しているようです。

この場合は、全ステップで1より小さいので、ExampleQueueがいっぱいになっているタイミングが無い=Readerがファイルをロード・デコードする部分がボトルネックになっている、 ということがわかります。

queue/input_producer/fraction_of_32_fullというのは、FilenameQueueの方を表しているみたいです。 バッチを読み込んでいくに連れて1つずつファイルが読み込まれていることを表していると思われます。縦軸はよくわかりません。

これに対して、num_threadsを10にした場合は、これが常に1になっているので、 Readerの読み込みが十分早くボトルネックが解消されていることがわかります。

$ tensorboard --logdir=tb_log_t10 

f:id:ykicisk:20161218175251p:plain

ちなみに、shuffle_batch_joinを使ったときも、Readerが複数あるので同様にファイルのロード・デコードが早くなっています。

$ tensorboard --logdir=tb_log_t10_join

f:id:ykicisk:20161218175259p:plain

num_threadsを指定した場合は同ファイルのデータが同じバッチに入りやすくなり、shuffle_batch_joinを使用した場合は同ファイルの前半・後半のデータが同バッチに入りにくくなります。 この性質に注意してどちらを使うべきかを選べばよいかと思います。

結局どうすればよかったか?

今回は、ExampleQueueのcapacityが小さすぎたのが問題なので、 一番簡単な方法はパラメータcapacityを大きくしてやれば良いです。

# 全データが入るサイズまでcapacityを大きくする
$ ./visualize_mini_batch.py --min_after_dequeue 500000 --num_threads 1 data tb_log_m500000

f:id:ykicisk:20161218181634p:plain

ただし、これは全入力データを一旦メモリに載せるという話なので、大量データを使う場合には使えません。

他の解決方法としては、1つのTFRecordファイルに入れるデータの量を減らすという方法があります。 そもそもTFRecordで複数データをまとめるとファイルの管理が楽そうだというモチベーションでTFRecordを使い始めましたが、運用上の問題が無い限り複数データを1ファイルに入れないほうが良さそうです。

やむを得ず1ファイルに複数データを入れる場合でも、1つのTFRecordにはcapacityより十分少ない数のデータしか入れてはいけません。

まとめ

今回は、Tensorflowのshuffle_batchを使うときのパラメータと、それを変化させたときのバッチの偏りについて調査しました。

その結果、以下の2つがわかりました。

  1. shuffle_batchを使いたいときは、 1つのTFRecordにはcapacityより十分少ない数のデータしか入れてはいけない。
  2. バッチ作成のパフォーマンスを上げるときは、num_threadsパラメータを1以上にするか、shuffle_batch_joinを使う。

参考にしたページ

開発合宿で作ったアプリをリリース && 地名から緯度経度を取得するAPIの比較

5月の連休中に、友人と温泉旅館でswift開発合宿をしました。
そこで作ったアプリを少しずつ肉付けして、本日ようやくAppleの審査を通過してリリースできました。

作ったアプリ: どこでも展望台

どこでも展望台

どこでも展望台

  • Yohei Iseki
  • ナビゲーション
  • 無料

アプリ説明用画像

f:id:ykicisk:20160611215048p:plain

どこでも展望台について

アプリ「どこでも展望台」は、画像を見てもらえばだいたいわかると思いますが、 登録したランドマークの方角と距離をARで表示するアプリです。

主に展望台あっちに富士山が見えるはず・・・みたいな時に利用することを想定しています。
無料なのでぜひ使ってみてくださいね!

このアプリでは、サイドメニューから地名を入力してランドマークを登録するのですが、 この地名→緯度経度を取得するところで結構右往左往してしまいました。

ということで今回は、どこでも展望台を開発する家庭で得られた 地名から緯度経度を取得するAPIの比較について紹介します。

地名からの緯度経度情報を取得方法の検討

結論から先にいうと、Google Maps APIのPlaces API Web Serviceを使うのが一番よいです。

以下、検討の詳細です。

ジオコーディングと逆ジオコーディング

「地名 緯度経度 取得」みたいなクエリでググると、 ジオコーディングと逆ジオコーディングなるものがあることがわかりました。

ざっくりいうと

  • ジオコーディング: 住所→緯度経度の変換
  • 逆ジオコーディング: 緯度経度→住所の変換

のことです。

今回は、ジオコーディングっぽいですが、住所ではなく地名を直接緯度経度に変換することが目的です。
今回扱う地名→緯度経度の変換の名称は結局わからないままでした。

地名→緯度経度の変換方法

今回は、swiftでiosアプリを作ることを前提としています。また、お金は出したくないです。
この制約の下で使える地名→緯度経度の変換方法について以下の4つを検討しました。
※ 順番は検討した順番

比較した内容

検討では、以下の観点について比較を行いました。
※ 何れも2016/06/09時点の比較です

  1. カバレッジ・精度(いくつかの地名クエリについて、正しい結果が返せるか)
  2. APIの制限(1日何回つかえるか)

また、性質の違いがわかりやすいクエリとして、「東京タワー」「富士山」「六本木一丁目駅」「ランドマークタワー」の結果も記載します。
(結果自体をのせるのはダメっぽいので成功・失敗についてのみ記載)

結果

概要

方法 カバレッジ・精度 APIの制限 備考
Geocoding API 5秒に1回以上のアクセスはNG 1クエリに対して1Result。
レスポンスがやや遅い(3〜5秒)
CLGeocoder 制限はあるが具体的な数値は書いてない -
YOLP コンテンツジオコーダAPI 1日50000回以下(実際は不明?) -
Google Maps API 1日100回。ただしクレカ登録で1日15000回までUP クレカ登録ではお金は発生しない

詳細

Geocoding API

  • APIのトップに仕様・利用規約が書かれています
    • 裏でGoogle Maps APIを叩いている?ので精度はかなり良いようです。
    • 5秒に1回以上のアクセスは禁止しています。

地名→緯度経度の変換例

クエリ 成功 or 失敗 備考
東京タワー -
富士山 -
六本木一丁目駅 -
ランドマークタワー -

CLGeocoder

  • 純正のジオコーダ。基本iphone限定になってしまう。
  • 精度はかなりイマイチな感じ。今後に期待。
  • API制限についてはドキュメントに以下のように記載されています。

    Geocoding requests are rate-limited for each app, so making too many requests in a short period of time may cause some of the requests to fail.

地名→緯度経度の変換例

クエリ 成功 or 失敗 備考
東京タワー -
富士山 -
六本木一丁目駅 麻布十番一丁目がHIT
ランドマークタワー HITしない。
横浜ランドマークタワー」ならHIT

YOLP コンテンツジオコーダAPI

  • 住所検索とランドマークの検索ができる
  • YOLPの「ランドマーク」に定義されていないもの(山など)は取れない?
  • 利用制限については、APIの合計で50000回 && API単体制限回数があるとのことですが、 コンテンツジオコーダ自体のAPI制限についての記載は見当たりませんでした。
クエリ 成功 or 失敗 備考
東京タワー -
富士山 富士山駅がHIT
六本木一丁目駅 六本木駅がHIT。
ランドマークタワー HITしない。
横浜ランドマークタワー」だと横浜駅がHIT

Google Maps API

使用制限は、Google 周辺検索サービスと Google プレイス テキスト検索サービスとで共通ですが、テキスト検索サービスには 10 倍の乗数が適用されます。つまり、テキスト検索リクエスト 1 回で、リクエスト 10 回分の割り当て量を使用することになります。Google Maps API for Work の契約の一部として Google Places API を購入した場合、乗数は異なります。詳しくは、Google Maps API for Work のドキュメントをご覧ください。

クエリ 成功 or 失敗 備考
東京タワー -
富士山 -
六本木一丁目駅 -
ランドマークタワー -

考察

地名から緯度経度情報が欲しい時、最低限の精度を満たすのは、Geocoding APIGoogle Maps APIだと思います。

ただし、Geocoding APIは元データがGoogle Maps APIですし、 大量のAPI利用はできないので、普通にGoogle Maps APIを使っておけば良さそうです。

結論

地名から緯度経度情報がほしい時は、今のところGoogle Maps API一択で良いと思います。

何れの方法を使うにしても、必ず利用規約をよく読んで正しく使いましょう。
私の場合は、Google Maps APIに以下の制限があるのに後で気づいて実装やり直しが何回か発生してしまいました。。

  • 地図表示を使わない場合はpowered by Googleのロゴを表示する
  • 緯度経度はアプリに保存するのはNG(例外あり)
  • 利用規約とプライバシーポリシーにGoogle Map APIを使用していることを明記

Dockerでagent forwardingが有効な開発環境の構築 (OS X)

今まではVagrant + chef or ansibleで開発環境を構築していましたが、 docker runの軽快さに惚れてDockerに乗り換えを考えています。

今回はdocker-machineを使って、Mac上で動かす開発環境構築(ubuntu)を作りました。 agent forwardingを有効にする設定で意外とハマるポイントがあったので記事にしました。

環境構築要のDockerfileとchefはこちら。 github.com

注意事項

  • ホストはMac OSを前提としています。
  • Dockerとdocker-machineがインストールされていることを前提とします。

Dockerでchefを使う

特にハマるポイントはありませんでした。
今回は以下のページを参考にしました。

Dockerでagent forwarding

Dockerでagent forwardingを有効にするのは意外と手間がかかるようです。

大別すると以下の2つに別れるのですが、両方ともハマるポイントがありました。

  1. docker buildでagent forwarding
  2. docker runでagent forwarding

docker buildでagent forwarding

dockerのイメージ作成の途中で、ホスト側の鍵を使いたい場合があります。
しかし、docker runではやりようがある(後述)のですが、docker build時には難しいようです。 Dockerfileで鍵をCOPYすれば可能かも知れませんが、鍵をイメージに置くのは気が引けます。

参考ページ

これに関しては、個人的にdocker buildssh鍵を使いたい場面が、「設定ファイル等をgit cloneするときだけ」ということに着目して、 設定ファイルの入ったリポジトリをDockerfileを管理するリポジトリのsubmoduleとすることで対処しました。

Dockerfileでは以下のようにDockerfileを管理するリポジトリごと設定ファイルをイメージにCOPYしています。

COPY . /home/docker/.docker

docker_dev/Dockerfile at master · ykicisk/docker_dev · GitHub

buildするときはこんな感じ。

$ git clone --recursive git@github.com:ykicisk/docker_dev.git
$ cd docker_dev
$ docker build -t centos:dev .

dcoker runssh-gent forwarding

dockerコンテナ内でホストのssh鍵を使う方法は2つあります。

  1. $SSH_AUTH_SOCKをコンテナにマウントする
  2. コンテナでsshdを起動してssh -Aで接続する
1. $SSH_AUTH_SOCKをコンテナにマウントする

こちらは、docker-machineを使っている場合は、 一度docker-machine sshする必要があり面倒です。

さらに、一旦exitすると$SSH_AUTH_SOCKの値が変わってしまうため(?) agent forwardingが切れてしまうことがわかりました。

$ docker-machine ssh default -A
                        ##         .
                  ## ## ##        ==
               ## ## ## ## ##    ===
           /"""""""""""""""""\___/ ===
      ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ /  ===- ~~~
           \______ o           __/
             \    \         __/
              \____\_______/
 _                 _   ____     _            _
| |__   ___   ___ | |_|___ \ __| | ___   ___| | _____ _ __
| '_ \ / _ \ / _ \| __| __) / _` |/ _ \ / __| |/ / _ \ '__|
| |_) | (_) | (_) | |_ / __/ (_| | (_) | (__|   <  __/ |
|_.__/ \___/ \___/ \__|_____\__,_|\___/ \___|_|\_\___|_|
Boot2Docker version 1.10.3, build master : 625117e - Thu Mar 10 22:09:02 UTC 2016
Docker version 1.10.3, build 20f81dd

$ docker run -it -v $SSH_AUTH_SOCK:/ssh-agent -e SSH_AUTH_SOCK=/ssh-agent centos:dev /bin/zsh

docker at <container id> in ~
$ ssh-add -l
# agent forwardingが有効

# ctl-p ctl-qで抜ける

$ docker attach <container id>

docker at <container id> in ~
$ ssh-add -l
# agent forwardingが無効

ということで、こちらは諦めました。

参考ページ

2. コンテナでsshdを起動してssh -Aで接続する

基本はdocker-machineでport forwardingすれば良いのですが、 docker-machineを動かすのにvirtual boxを使用しているので、 virtual box側でもport forwardingの設定をする必要があります。

f:id:ykicisk:20160410230953p:plain

また、最初はcentos7で開発環境を作ろうと思っていましたが、systemctlを動かすところでかなりハマりました。 centosにそこまでこだわりがあったわけではなかったので、開発環境をcentosからubuntuに変更しました。
※うまくやれば使えそうなので、こだわりがある人は頑張れば良いと思います。

参考ページ

最終的な使用感

sshをするたびに、ssh -p 2222みたいな指定をするのが面倒なので、 ~/.ssh/configを以下のように書いておきます。

Host docker
    HostName localhost
    User docker
    StrictHostKeyChecking no
    Port 2222

開発環境を作るときは以下のコマンドを打てばOKです。

$ docker build -t ubuntu:dev .
$ docker run -d -p 2222:22 ubuntu:dev
$ ssh docker

docker at <container id> in ~
$ ssh-add -l
# agent forwardingが有効

今回のまとめ

  • とりあえずDockerでagent forwardingできる開発環境を構築できた。
    • docker build時: git submoduleを使うことで問題を回避
    • docker run時: コンテナにsshする。port-forwarding設定は.ssh/configに隠蔽。
  • 最近環境構築だけで満足してしまうので、その先にも手をつけられるようにしたいです。

MessagePackでgolang-python間でデータ受け渡しする

データをバイナリファイルとして保存すると読み込みが早くて良いですが、 OSや言語が変わると読み込むことができません。

研究室時代には、この問題に対処するためにJSONとバイナリを保存しておいて、 バイナリファイルの読み込みに失敗したらJSONを読み込むという意味不明な実装をしていました。

しかし、最近同僚にMessagePackというものがあることを教えてもらいました。

公式サイトからの引用

MessagePack is an efficient binary serialization format.
It lets you exchange data among multiple languages like JSON. But it's faster and smaller.

つまり、これを使えば万事解決するわけですね。 (同僚いわく、MessagePackを知らないエンジニアはモグリだとか・・・はい。)

今回は、試しにMessagePack(とredis)を使ってgoとpython間でデータのやりとりに挑戦します。

なお、あとで紹介するコードはこちらのgithubにも公開しています。 github.com

準備

データの書き出し先としてredisを使うので、Redisのインストールして起動しておきます。

$ redis-server

また、pythonとgoでMessagePackを扱うパッケージをインストールします。

$ pip install msgpack-python
$ go get gopkg.in/vmihailenco/msgpack.v2

さらに、goでRedisを扱うパッケージを入れます。

$ go get gopkg.in/redis.v3

redigoというパッケージもありますが、 こちらのほうが使いやすいです(個人的に)。名前に惑わされてはいけません。

実装

  • python or goの一方でRedisにList, Mapを書き込む
  • 他方でRedisからList, Mapを受け取る

というプログラムを実装します。

python → go

put.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import msgpack
import redis


if __name__ == "__main__":
    r = redis.StrictRedis(host='localhost', port=6379, db=0)
    # packing
    packed_ls_int = msgpack.packb(range(10))
    packed_ls_float = msgpack.packb([-1.0, 2.02, 5.04])
    packed_dic = msgpack.packb({u'pykey1': u'pyval1', u'pykey2': u'pyval2'})
    # put
    r.set("py_ls_int", packed_ls_int)
    r.set("py_ls_float", packed_ls_float)
    r.set("py_map", packed_dic)

get.go

package main

import (
    "fmt"
    "gopkg.in/redis.v3"
    "gopkg.in/vmihailenco/msgpack.v2"
)

func check(e error) {
    if e != nil {
        panic(e)
    }
}

func main() {
    redis_client := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // no password set
        DB:       0,  // use default DB
    })
    var unpacked_map map[string]string
    var unpacked_ls_int []int
    var unpacked_ls_float []float64
    {
        msg, msgpack_err := redis_client.Get("py_map").Bytes()
        check(msgpack_err)
        msgpack.Unmarshal(msg, &unpacked_map)
    }
    {
        msg, msgpack_err := redis_client.Get("py_ls_int").Bytes()
        check(msgpack_err)
        msgpack.Unmarshal(msg, &unpacked_ls_int)
    }
    {
        msg, msgpack_err := redis_client.Get("py_ls_float").Bytes()
        check(msgpack_err)
        msgpack.Unmarshal(msg, &unpacked_ls_float)
    }
    fmt.Println(unpacked_map)
    fmt.Println(unpacked_ls_int)
    fmt.Println(unpacked_ls_float)
}

実行結果

$ python put.py
$ go run get.go
map[pykey1:pyval1 pykey2:pyval2]
[0 1 2 3 4 5 6 7 8 9]
[-1 2.02 5.04]

go -> python

put.go

package main

import (
    "gopkg.in/redis.v3"
    "gopkg.in/vmihailenco/msgpack.v2"
)

func check(e error) {
    if e != nil {
        panic(e)
    }
}

func main() {
    redis_client := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // no password set
        DB:       0,  // use default DB
    })
    dic := map[string]string{"gokey1": "goval1", "gokey2": "goval2"}
    ls_int := []int{1, 3, 5, 8}
    ls_float := []float64{2.3, 6.4, 5.5}
    {
        msg, msgpack_err := msgpack.Marshal(dic)
        check(msgpack_err)
        check(redis_client.Set("go_map", msg, 0).Err())
    }
    {
        msg, msgpack_err := msgpack.Marshal(ls_int)
        check(msgpack_err)
        check(redis_client.Set("go_ls_int", msg, 0).Err())
    }
    {
        msg, msgpack_err := msgpack.Marshal(ls_float)
        check(msgpack_err)
        check(redis_client.Set("go_ls_float", msg, 0).Err())
    }
}

get.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import msgpack
import redis


if __name__ == "__main__":
    r = redis.StrictRedis(host='localhost', port=6379, db=0)

    packed = r.get("go_map")
    dic = msgpack.unpackb(packed, encoding='utf-8')
    packed = r.get("go_ls_int")
    ls_int = msgpack.unpackb(packed, encoding='utf-8')
    packed = r.get("go_ls_float")
    ls_float = msgpack.unpackb(packed, encoding='utf-8')

    print dic
    print ls_int
    print ls_float

実行結果

$ go run put.go
$ python get.py
{u'gokey2': u'goval2', u'gokey1': u'goval1'}
[1, 3, 5, 8]
[2.3, 6.4, 5.5]

まとめ

複雑な構造体などはパーサを定義する必要があるみたいですが、 リストやマップ程度であればこのように簡単に受け渡しできます。

試してはいませんが、他言語間でもいい感じに使えるのではないでしょうか。

くれぐれも俺俺フォーマットなんて作らず、MessagePackを使っていきましょう!

golang + anacondaでTwitter Streamimg APIを使う

goのTwitter APIライブラリの一つとして、anacondaがあります。

最近まで(?)Streaming APIには対応されていなかったようですが、 ソースを見る限りすでにStreaming APIに対応するための実装があるようです。

GoDocを見ながら適当に実装してみたらちゃんと動いたので、 今回はその方法について書きます。

準備

$ go get github.com/ChimeraCoder/anaconda

実装

github.com

package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "github.com/ChimeraCoder/anaconda"
    "io/ioutil"
)

type ApiConf struct {
    ConsumerKey       string `json:"consumer_key"`
    ConsumerSecret    string `json:"consumer_secret"`
    AccessToken       string `json:"access_token"`
    AccessTokenSecret string `json:"access_token_secret"`
}

func check(e error) {
    if e != nil {
        panic(e)
    }
}

func main() {
    var apiConf ApiConf
    {
        apiConfPath := flag.String("conf", "config.json", "API Config File")
        flag.Parse()
        data, err_file := ioutil.ReadFile(*apiConfPath)
        check(err_file)
        err_json := json.Unmarshal(data, &apiConf)
        check(err_json)
    }

    anaconda.SetConsumerKey(apiConf.ConsumerKey)
    anaconda.SetConsumerSecret(apiConf.ConsumerSecret)
    api := anaconda.NewTwitterApi(apiConf.AccessToken, apiConf.AccessTokenSecret)

    twitterStream := api.PublicStreamSample(nil)
    for {
        x := <-twitterStream.C
        switch tweet := x.(type) {
        case anaconda.Tweet:
            fmt.Println(tweet.Text)
            fmt.Println("-----------")
        case anaconda.StatusDeletionNotice:
            // pass
        default:
            fmt.Printf("unknown type(%T) : %v \n", x, x)
        }
    }
}

簡単に説明すると、twitterStream.Cからどんどんツイートを取得して表示するだけです。

twitterStream.Canaconda.Tweet型(ツイート本体)と anaconda.StatusDeletionNotice型(ツイート消されてますNotice?)が流れてきます。 消されたツイートはどうでもよいので今回はスルーしています。

goを使うとJsonパースが楽なのも良いですね。

実行

$ go run main.go -conf=config.json

追記:環境によっては-conf="config.json"とする必要があるそうです

世界中のツイート(の一部)がどんどん流れてきます。 これを使って何かおもしろいことができればよいですね!

OpenCV 3.0で人物追跡したい!

最近社内でライトニングトークなるものがありました。 そのうち画像を使う機会がありそうで復習したかった半分、 OpenCV3.0使ってみたかった半分で単カメラの人物追跡をテーマに軽く解説をしてきました。

研究室時代ではいろんな論文の実装を組み込んで何とか実装していた単カメラ人物追跡ですが、 OpenCV3.0を使えば、100行くらいで人物追跡できます。素晴らしい限りです。

今回は、その実装について説明したいと思います。

ただし、以下の点には注意です。

  • 理論などはすべて省略します
  • 高速化は考えません(本当は大事)
  • 使っている技術は古いです(2005-2006くらいの技術)

用いた環境

  • C++11
  • OpenCV 3.0
  • boost 1.58 (ファイル読み込みのみ)

人物追跡

ここでいう人物追跡とは、単一のカメラ映像に写る人物の軌跡を取得することです。 人物検出をして、その結果を追跡することで実現できます。

人物検出

画像の中から人物の矩形を取得します。

OpenCVでは、cv::HOGDescriptor::getDefaultPeopleDetectorというズバリなものが用意されていますので、 今回はこれを利用します。

(人物)追跡

人物検出の結果を起点に、そのものが映像中に動いた奇跡を追跡します。 (人物以外でも使えます)

OpenCVでは、cv::Trackerというインタフェースが用意されていますので、 今回はこれを利用します。

実装

実装はこんな感じです。 ykicisk/tracking · GitHub

#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/tracking/tracker.hpp>
#include <boost/filesystem.hpp>

const cv::Size MAX_DETECT_SIZE = cv::Size(100, 200);
const int MAX_MISS_FRAME = 10;
const double MIN_NEW_DETECT_INTERSECTION_RATE = 0.5;

class MyTracker {
private:
    static int next_id;
    int id;
    int n_miss_frame = 0;
    cv::Rect2d rect;
    cv::Ptr<cv::Tracker> cv_tracker;
public:
    // フレーム画像と追跡対象(Rect)で初期化
    MyTracker(const cv::Mat& _frame, const cv::Rect2d& _rect) 
        : id(next_id++), rect(_rect) {
        cv_tracker = cv::Tracker::create("BOOSTING"); //  or "MIL"
        cv_tracker->init(_frame, _rect);
    }
    // 次フレームを入力にして、追跡対象の追跡(true)
    // MAX_MISS_FRAME以上検出が登録されていない場合は追跡失敗(false)
    bool update(const cv::Mat& _frame){
        n_miss_frame++;
        return cv_tracker->update(_frame, rect) && n_miss_frame < MAX_MISS_FRAME;
    }
    // 新しい検出(Rect)を登録。現在位置と近ければ受理してn_miss_frameをリセット(true)
    // そうでなければ(false)
    bool registerNewDetect(const cv::Rect2d& _new_detect){
        double intersection_rate = 1.0 * (_new_detect & rect).area() / (_new_detect | rect).area();
        bool is_registered = intersection_rate > MIN_NEW_DETECT_INTERSECTION_RATE;
        if (is_registered) n_miss_frame = 0;
        return is_registered;
    }
    // trackerの現在地を_imageに書き込む
    void draw(cv::Mat& _image) const{
        cv::rectangle(_image, rect, cv::Scalar(255, 0, 0), 2, 1);
        cv::putText(_image, cv::format("%03d", id), cv::Point(rect.x + 5, rect.y + 17), 
                cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(255,0,0), 1, CV_AA);
    }
};
int MyTracker::next_id = 0;


int main(int argc, char* argv[]){
    if(argc != 2){
        std::cout << "usage: " << argv[0] << " videodir" << std::endl;
        exit(1);
    }
    // フレーム画像列のパスを取得
    namespace fs = boost::filesystem;
    std::vector<std::string> frame_paths;
    for(auto it = fs::directory_iterator(argv[1]); it != fs::directory_iterator(); ++it){
        frame_paths.push_back(it->path().string());
    }
    // detector, trackerの宣言
    cv::HOGDescriptor detector;
    detector.setSVMDetector(cv::HOGDescriptor::getDefaultPeopleDetector());
    std::vector<MyTracker> trackers;
    // 1フレームずつループ
    for (auto& frame_path : frame_paths){
        std::cout << "frame : " << frame_path << std::endl;
        cv::Mat frame = cv::imread(frame_path);
        // 人物検出
        std::vector<cv::Rect> detections;
        detector.detectMultiScale(frame, detections);
        // trackerの更新(追跡に失敗したら削除)
        for (auto t_it = trackers.begin(); t_it != trackers.end();){
            t_it = (t_it->update(frame)) ? std::next(t_it) : trackers.erase(t_it);
        }
        // 新しい検出があればそれを起点にtrackerを作成。(既存Trackerに重なる検出は無視)
        for(auto& d_rect : detections){
            if (d_rect.size().area() > MAX_DETECT_SIZE.area()) continue;
            bool exists = std::any_of(trackers.begin(), trackers.end(), 
                    [&d_rect](MyTracker& t){return t.registerNewDetect(d_rect);});
            if(!exists) trackers.push_back(MyTracker(frame, d_rect));
        }
        // 人物追跡と人物検出の結果を表示
        cv::Mat image = frame.clone();
        for(auto& t : trackers) t.draw(image);
        for(auto& d_rect : detections) cv::rectangle(image, d_rect, cv::Scalar(0, 255, 0), 2, 1);
        cv::imshow("demo", image);
        cv::waitKey(1);
    }
    return 0;
}

動画のフレームごとに人物検出と追跡を繰り返して実現します。

そのままだとTrackerがどんどん増えていくので、 Trackerと重なる検出がでてこなくなったTrackerは消すようにしています。

実験

PETS2009 S2.L1のView001で使って動かしてみました。

緑が人物検出の結果、青が追跡結果です。

考察

思ったよりイマイチですね。

人物検出の誤検出・未検出について

OpenCV3.0のdetectMultiScaleを使った人物検出ではこれが限界だと思います。

もっとリッチな人物検出手法をつかったり、 シーンの知識(どの当たりにどんな大きさの人が写るなど)を利用すればもっと検出がよくなると思います。

追跡ミスについて

汎用追跡器を使ったこともありますが、オクルージョンにめちゃくちゃ弱いです。 真ん中のポールを通過した人はもれなくおかしなことになっていますね。

これに解決するためには、もっと人物に特化した追跡器を使ったり、 関連付けられた人物画像を一旦バラバラにして最適化で同一人物をくっつけなおしたりすると良いと思います。

まとめ

  • OpenCV3.0を使えば簡単に単カメラ人物追跡できる
  • 結果はいまいち。実際のカメラは更にいろいろあるので更におかしい事が起こる
  • 人物追跡にかぎらず、この辺りの分野は闇が深い

次回があれば、今回取得した人物画像列について、 複数カメラでの対応付けを行う方法について説明します。

でも次回はありません。

Web画像検索で画像を収集する

沢山の画像を利用して何やらしたい時があります.
たいていはImageNetやFlickrで収集すれば良いのですが, 時には普通のWebで画像検索した結果を利用したいこともあります.

以前に,Web画像検索した結果を収集したい時の最適解はなんだろうと検討したことがありましたので, その結果をメモついでに記事にしました.

画像収集に利用するサービスの検討

Google画像検索API

制限が厳しくなり,同じクエリに対して10枚×10ページの合計100枚までしか取得できないようです. 画像を収集するという意味では使いにくいですね.

基本的な使い方は

Bing Search API

同じクエリでも1000件以上収集できます(上限は確認していません).
無料枠でも月5000回Getできるので画像収集として使えますね.
今回はBing Search APIが最適解という結論に至りました.

API利用までの流れなどは以下の記事で詳しく紹介されています.
Bing Search APIを使ってWeb検索を行うには(Json編) | garicchi.com

画像収集スクリプト(Python)

今回はWindowsで利用したのでcp932とか書いてますが, 違う環境の場合は適宜修正してください.

# coding: utf-8
import sys, os
import argparse
import json
import urllib
import urllib2
import requests

#proxy setting
proxy_dict =  {"http":"your.proxy:8080"}
proxy = urllib2.ProxyHandler(proxy_dict)
opener = urllib2.build_opener(proxy)
urllib2.install_opener(opener)

#primary account key
api_key="*************************************"

def download_urllist(urllist,outpath):
    for url in urllist:
        try:
            print"url:"+url
            with open(outpath+"/"+os.path.basename(url),'wb') as f:
                img=urllib2.urlopen(url,timeout=5).read()
                f.write(img)
        except urllib2.URLError:
            print("URLError")
        except IOError:
            print("IOError")
        except UnicodeEncodeError:
            print("EncodeError")
        except OSError:
            print("OSError")

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("-q","--query", required=True, 
            help="query")
    parser.add_argument("-o","--outpath", required=True,
            help="output path")
    args = parser.parse_args()


    #Bing Image Search
    query = args.query.decode('cp932').encode('utf-8')
    query = urllib2.quote(query)

    step = 20
    num = 50
    
    url_param_dict={
            "Query":"'%s'"%(query),
            "Market":"'ja-JP'",
            "Adult":"'Off'",
            }
    url_param_base = urllib.urlencode(url_param_dict)
    url_param_base = url_param_base + "&$format=json&$top=%d&$skip="%(num)


    for skip in range(0,num*step,num):
        url_param = url_param_base + str(skip)
        url="https://api.datamarket.azure.com/Bing/Search/Image?"+url_param
        print url

        response = requests.get(url, 
                            auth=(api_key, api_key), 
                            headers={'User-Agent': 'My API Robot'},
                            proxies=proxy_dict)
        response=response.json()

        urllist=[item['MediaUrl'] for item in response['d']['results']]
        download_urllist(urllist,args.outpath)

画像収集スクリプトは以下の記事を参考にしました.
Bing Search API を使いたいと思ったのでPythonでラッパーを作ってみた - [[ともっくす alloc] init]

使い方は

python collect_images.py -q Query -o outputdir

です.

-qで指定したクエリについて,step*num件の画像をダウンロードします.
ダウンロードした結果にはゴミも多いので,いい感じにクリーニングして利用しましょう.