のどあめ

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

macOS SierraでKarabinerの「EISUU to Escape(入力ソースが日本語以外の場合のみ)」を実現する

macOS SierraからKarabinerが使えなくなりました。 公式サイトでは、Karabiner-Elementsへの移行を呼びかけていますが、 2017/09/16現在ではKarabinerの全ての機能を使うことはできないようです。

私が愛用していた「EISUU to Escape(入力ソースが日本語以外の場合のみ)」(下図)もその一つです。 今回は、HammerspoonとKarabiner-Elementsを使って擬似的にこの設定を再現しました。

f:id:ykicisk:20170916162357p:plain

注意

失敗するとKarabiner-Elementsの設定ファイル(~/config/karabiner/karabiner.json)が消えてしまいます。 必ずバックアップをとってから試すようにして下さい

実現方法

こちらの記事とほぼ同じ方法を使います。 日本語入力の切替時に、HammerspoonでKarabiner-Elementsの設定を切り替えます。

qiita.com

必要なアプリケーションをインストール

以下の2つをインストールします。

Karabiner-Elementsのプロファイルを作る

上の記事を参考に、Karabiner-Elementsのプロファイルを作成します。

今回は、日本語入力以外の設定である「Default」と 日本語入力中の設定である「Japanese」の2つのプロファイルを作成します。 設定項目はなんでも良いですが、 Defaultの方のみに、英数キーをEscapeにマッピングする設定をします。

f:id:ykicisk:20170916162440p:plain

また、Miscから「show profile name in menu bar」をONにしておくと、切り替わりがわかりやすくて良いと思います。

Karabinerの設定を変更するスクリプトを実装

以下のスクリプト~/.hammerspoon/karabiner-switcher.pyに配置します。

※要実行権限

#!/usr/bin/env python
# -*- encode: utf-8 -*-
import os
import sys
import json


def main():
    if len(sys.argv) != 2:
        print("usage: {0} profile_name".format(sys.argv[0]))
        exit(1)

    home = os.environ['HOME']
    fpath = os.path.join(home, ".config/karabiner/karabiner.json")

    with open(fpath) as f:
        conf = json.load(f)

    profile_name = sys.argv[1]
    select_flag = False
    for c in conf["profiles"]:
        c["selected"] = c["name"] == profile_name
        select_flag |= c["selected"]

    if not select_flag:
        print("profile_name[{0}] is not found".format(profile_name))
        exit(1)

    with open(fpath, "w") as f:
        f.write(json.dumps(conf, indent=4, sort_keys=True))


if __name__ == "__main__":
    main()

Hammerspoonの設定

入力ソース切替で上のスクリプトを実行するよう、 ~/.hammerspoon/init.luaを実装します。

以下はgoogle日本語入力の場合です。他のIMEを使っている場合はif文のところを変更して下さい。 IMEのSouceIDはコメントアウトしているprint文をconsoleから確認できます。

local home = os.getenv("HOME")

function switchKarabiner()
  source_id = hs.keycodes.currentSourceID()
  -- print("source_id: " .. source_id .. "\n")
  if source_id == "com.google.inputmethod.Japanese.base" then
    hs.execute(home .. '/.hammerspoon/karabiner-switcher.py Japanese')
  else
    hs.execute(home .. '/.hammerspoon/karabiner-switcher.py Default')
  end
end

hs.keycodes.inputSourceChanged(switchKarabiner)

Hammerspoonの設定を反映させる

HammerspoonのReload Configを選びます。

これで日本語入力のON/OFFでKarabiner-ElementsのプロファイルがJapanese/Default が切り替わっていれば成功です。

まとめ

  • HammerspoonでKarabiner-Elementsの設定を切り替えることで、 macOS SierraでKarabinerの「EISUU to Escape(入力ソースが日本語以外の場合のみ)」を実現しました。
  • たまに設定がおかしくなったときは、手動でKarabinerの設定を切り替えてください。

【Rust×WebAssembly】木構造グラフの描画でWebAssemblyの処理速度を検証

概要

  • 力学的モデルを使った木構造グラフの描画を対象に
    JavaScriptとWebAssembly(WASM)の処理速度比較をした
  • ノード数が大きいグラフの描画ではWebAssemblyのほうが1.7倍くらい早かった。

やりたいこと

過去に何人かの方がWebAssemblyとJavaScriptの処理速度をされています。

彼らによると、フィボナッチ数列やループ処理などのベンチマーク的なスクリプトは、WebAssemblyのほうが数倍程度早いそうです。

今回は、WebAssemblyが実際に使われそうな例で処理時間の比較を行いたいと思います。

検証対象

数倍の処理速度向上で恩恵を受ける例として描画計算があります。

今回は、グラフ描画アルゴリズムの一つである力学的モデルを用いた 木構造グラフの描画時間を、JavaScriptとWebAssemblyで比較したいと思います。

アルゴリズム参考:力学モデル (グラフ描画アルゴリズム) - Wikipedia

検証に用いたスクリプト

github.com

f:id:ykicisk:20170514184326p:plain

ざっくりした使い方

  1. num_nodesに木構造グラフをのノード数を入力する
  2. [Init Graph]を押下してグラフのノード位置を初期化する
  3. [Update by JavaScript]または[Update by WASM]を押下して、力学モデルを使ってノード位置を更新する
  4. 実行にかかった時間がtimeに表示される

※ animation flagをOFFにした場合は、ノード位置が収束するまで描画しない。

実装概要

dist/index.html

https://github.com/ykicisk/wasm_graph/blob/master/dist/index.html

  • 諸々に利用するjQuery, 描画に利用するjCanvasをロードします。
  • dist/wasm/*dist/js/bundle.jsの順番にスクリプトをロードします。

src/js/main.js

https://github.com/ykicisk/wasm_graph/blob/master/src/js/main.js

※実際はBabelでdist/js/bundle.jsにトランスパイルされます。

  • 指定されたノード数で木構造グラフを生成します。
    • 描画に利用するノード・エッジの情報はWebAssemblyで利用しやすいように、ArrayではなくFloat32ArrayやInt32Arrayで定義します。
  • グラフ初期化関数・グラフ描画関数を定義します。
  • index.htmlにグラフ操作用のIF要素を追加します。
    • 指定したノード数のグラフを生成します。
    • 後述のGraphUpadaterを使ってグラフのノード位置を更新します。

src/js/GraphUpdater.js

https://github.com/ykicisk/wasm_graph/blob/master/src/js/main.js

※実際はBabelでdist/js/bundle.jsにトランスパイルされます。

  • グラフのノード位置を更新するGraphUpadaterクラス。
    • GraphUpdaterBase: 共通部分を実装した基底クラス
      • 共通で利用するノードの速度を格納するFloat32Arrayの確保
      • animation_flagによる処理切り替え
      • _update_loop()関数でイテレーション1回分の更新を行う。
    • JavaScriptGraphUpdater: JavaScriptでグラフ更新するクラス
    • RustGraphUpdater: WASMでグラフ更新するクラス
JavaScriptのグラフ更新関数

ノードの座標{x,y}_list、速度{vx,vy}_listを更新して、 ノードの運動エネルギー合計を返します。

スクリプト内で使われている変数は以下の通り

  • COULOMB, BOUNCE, ATTENUATION: 定数
  • {x,y}_list: 現在のノードのx, y座標(Float32Array)
  • {fx,fy}_list: ノードにかかる力のx, y成分(Float32Array)
  • {vx,vy}_list: ノードの速度のx, y成分(Float32Array)
  • tmp_{x,y}_list: 前ステップのノードのx, y座標(Float32Array)
  • edge_list: エッジ情報(i+1番目のノードとedge_list[i]番目のノードにエッジが貼ってある)(Int32Array)
_update_loop() {
    // init
    this.tmp_x_list.set(this.x_list);
    this.tmp_y_list.set(this.y_list);
    this.fx_list.fill(0.0);
    this.fy_list.fill(0.0);

    let energy = 0.0;
    for (let idx1 = 0; idx1 < this.num_nodes-1; idx1++){
        for (let idx2 = idx1 + 1; idx2 < this.num_nodes; idx2++){
            let dist_x = this.tmp_x_list[idx1] - this.tmp_x_list[idx2];
            let dist_y = this.tmp_y_list[idx1] - this.tmp_y_list[idx2];
            let rsq = Math.pow(dist_x, 2) + Math.pow(dist_y, 2);
            this.fx_list[idx1] += this.COULOMB * dist_x / rsq;
            this.fy_list[idx1] += this.COULOMB * dist_y / rsq;
            this.fx_list[idx2] -= this.COULOMB * dist_x / rsq;
            this.fy_list[idx2] -= this.COULOMB * dist_y / rsq;
        }
    }
    for (var i = 0; i < (this.num_nodes - 1);i++) {
        let idx1 = this.edge_list[i];
        let idx2 = i+1;

        let dist_x = this.tmp_x_list[idx2] - this.tmp_x_list[idx1];
        let dist_y = this.tmp_y_list[idx2] - this.tmp_y_list[idx1];

        this.fx_list[idx1] += this.BOUNCE * dist_x;
        this.fy_list[idx1] += this.BOUNCE * dist_y;
        this.fx_list[idx2] -= this.BOUNCE * dist_x;
        this.fy_list[idx2] -= this.BOUNCE * dist_y;
    }
    for (let idx = 0; idx < this.num_nodes; idx++){
        this.vx_list[idx] = (this.vx_list[idx] + this.fx_list[idx]) * this.ATTENUATION;
        this.vy_list[idx] = (this.vy_list[idx] + this.fy_list[idx]) * this.ATTENUATION;
        this.x_list[idx] += this.vx_list[idx];
        this.y_list[idx] += this.vy_list[idx];
        energy += Math.pow(this.vx_list[idx], 2) * Math.pow(this.vy_list[idx], 2);
    }
    return energy;
}
WASMのグラフ更新関数

処理をWebAssemblyに委任します。

constructor() {
    ...
    this.wasm_update_loop = Module.cwrap('wasm_update_loop',
                                            'number',
                                            ['number', 'number',
                                             'number', 'number',
                                             'number', 'number']);
    ...
}
_update_loop() {
    return this.wasm_update_loop(this.num_nodes, this.edge_list.byteOffset,
                                 this.x_list.byteOffset, this.y_list.byteOffset,
                                 this.vx_list.byteOffset, this.vy_list.byteOffset);
}

src/main.rs(WebAssembly)の実装はほぼJavaScriptと同じです。

#[no_mangle]
pub extern "C" fn wasm_update_loop(num_nodes_i32: i32,
                                   edge_list_ptr: *const i32,
                                   x_list_ptr: *mut f32,
                                   y_list_ptr: *mut f32,
                                   vx_list_ptr: *mut f32,
                                   vy_list_ptr: *mut f32)
                                   -> f32 {
    let mut energy: f32 = 0.0;

    unsafe {
        let num_nodes = num_nodes_i32 as usize;
        let mut x_list: &mut [f32] = std::slice::from_raw_parts_mut(x_list_ptr, num_nodes);
        let mut y_list: &mut [f32] = std::slice::from_raw_parts_mut(y_list_ptr, num_nodes);
        let mut vx_list: &mut [f32] = std::slice::from_raw_parts_mut(vx_list_ptr, num_nodes);
        let mut vy_list: &mut [f32] = std::slice::from_raw_parts_mut(vy_list_ptr, num_nodes);
        let edge_list: &[i32] = std::slice::from_raw_parts(edge_list_ptr, num_nodes - 1);
        let mut fx_list = vec![0.0; num_nodes];
        let mut fy_list = vec![0.0; num_nodes];
        let mut tmp_x_list = vec![0.0; num_nodes];
        let mut tmp_y_list = vec![0.0; num_nodes];
        tmp_x_list.clone_from_slice(x_list);
        tmp_y_list.clone_from_slice(y_list);
        for idx1 in 0..num_nodes - 1 {
            for idx2 in idx1 + 1..num_nodes {
                let dist_x = x_list[idx1] - tmp_x_list[idx2];
                let dist_y = y_list[idx1] - tmp_y_list[idx2];
                let rsq = dist_x * dist_x + dist_y * dist_y;
                fx_list[idx1] += COULOMB * dist_x / rsq;
                fy_list[idx1] += COULOMB * dist_y / rsq;
                fx_list[idx2] -= COULOMB * dist_x / rsq;
                fy_list[idx2] -= COULOMB * dist_y / rsq;
            }
        }
        for i in 0..num_nodes - 1 {
            let idx1 = edge_list[i] as usize;
            let idx2 = i + 1;

            let dist_x = tmp_x_list[idx2] - tmp_x_list[idx1];
            let dist_y = tmp_y_list[idx2] - tmp_y_list[idx1];

            fx_list[idx1] += BOUNCE * dist_x;
            fy_list[idx1] += BOUNCE * dist_y;
            fx_list[idx2] -= BOUNCE * dist_x;
            fy_list[idx2] -= BOUNCE * dist_y;
        }
        for idx in 0..num_nodes {
            vx_list[idx] = (vx_list[idx] + fx_list[idx]) * ATTENUATION;
            vy_list[idx] = (vy_list[idx] + fy_list[idx]) * ATTENUATION;
            x_list[idx] += vx_list[idx];
            y_list[idx] += vy_list[idx];
            energy += vx_list[idx].powf(2.0) * vy_list[idx].powf(2.0);
        }
    }
    energy
}

検証方法

ノード位置をランダムに初期化した木構造グラフに対して力学的モデルを適用します。 ノード数が同じグラフを3個生成して、ノード位置が収束して描画が完了するまでの時間の平均を比較します。

先のスクリプトのanimation flagはOFFで比較します。

検証結果

結果は下表です。 WebAssemblyを使うことで、ノード数が大きいときの処理時間が60%程度に抑えられることが読み取れます。

f:id:ykicisk:20170514192228p:plain

力学的モデルを利用した木構造グラフの描画においても、 WebAssemblyを用いることで計算時間短縮の恩恵を得られることがわかりました。

感想

  • WebAssemblyを使っても処理が数倍早い程度なので、応用先は考える必要がありそう。
  • WebAssemblyはまだまだ安定していないようにみえる。
    • 不具合があったときに、エラー文でググっても出てこない
    • 再起動すると動くようになる謎バグとかが多い
  • 当たり前だがJavaScriptmallocの相性が悪い。例えばClassにDestructorがないのでメモリ管理が大変。
  • WebAssemblyに状態をもたせるより、JavaScriptで状態を一元管理してWebAssemblyに引数で必要な情報だけ渡したほうが良さそう。
  • Rustの実装がunsafeのオンパレードになるなら、普通にCやC++で実装したほうがわかりやすそう。

参考ページ

【Rust×WebAssembly】Rust JavaScript間でデータをやり取りする

概要

  • WebAssemblyを使ってJavaScriptからRustの関数を使うときの話
  • JavaScript<->Rustのデータのやり取りをする方法をまとめました。
  • 数値、String、Arrayで書き方が違うみたいです。

github.com

参考にしたページ

Rust, Webassemblyとは

省略

必要なもの

データやり取りの方法

共通

  1. src/main.rswasm/wasm_test.wasm,wasm/wasm_test.jsコンパイルします。
  2. index.htmlwasm/wasm_test.wasmをロードしたらsrc/main.jsが実行されます。
  3. src/main.jsから、src/main.rsで実装したrustの関数を呼び出します。

index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>wasm test</title>
    <script>
      var Module = {};
 
      fetch('wasm/wasm_test.wasm')
        .then((response) => response.arrayBuffer())
        .then((wasm) => {
          Module.wasmBinary = wasm;
 
          const scriptElem = document.createElement('script');
          scriptElem.src = 'wasm/wasm_test.js';
          scriptElem.addEventListener('load', (e) => {
              const mainScriptElem = document.createElement('script');
              mainScriptElem.src = 'src/main.js';
              document.body.appendChild(mainScriptElem);
          });
          document.body.appendChild(scriptElem);
        });
    </script>
  </head>
  <body>
    <div id="number"></div>
    <div id="string"></div>
    <div id="array"></div>
  </body>
</html>

想定する結果

  • JavaScriptからRustに渡された値はprintln!マクロによってconsole.logに出力します。
  • RustからJavaScriptに渡された値はdivに出力されます。
  • githubのコードを動かすと↓のようになります。

f:id:ykicisk:20170430193518p:plain

数値の受け渡し

数値(js<->rust)

数値を受け渡す場合は、JavaScript、Rustともに普通に関数を書けば良いです。 i32f64等数値っぽい型はそのまま渡せますが、usizeなんかは渡せませんでした。

src/main.js

// number: js <-> rust
const wasm_number = Module.cwrap('wasm_number', 'number', ['number', 'number']);
document.getElementById('number').innerText = wasm_number(3, 4.2);

src/main.rs

#[no_mangle]
pub extern "C" fn wasm_number(n: i32, m: f64) -> f64 {
    m + (n as f64) * 2.0
}

文字列の受け渡し

文字列(js->rust)

JavaScriptの文字列はRustで*c_char型として受け取ります。 これをRustのString型に変換する必要があります。

src/main.js

const wasm_string_to_rust = Module.cwrap('wasm_string_to_rust', null, ['string']);
wasm_string_to_rust("string from js");

src/main.rs

use std::os::raw::c_char;
use std::ffi::CStr;
use std::str;
#[no_mangle]
pub extern "C" fn wasm_string_to_rust(c_buf: *const c_char) {
    let c_str: &CStr = unsafe { CStr::from_ptr(c_buf) };
    let buffer: &[u8] = c_str.to_bytes();
    let rust_str: &str = str::from_utf8(buffer).unwrap();
    let rust_string: String = rust_str.to_owned();
    println!("{}", rust_string);
}

文字列(rust->js)

rustの文字列をjsに渡すためには、同様に*c_char型で返せば良いですが、 Rustの関数内で作った文字列のアドレスをas_ptrで返してしまうと、 関数から抜けたときに文字列のメモリが開放されてしまいます。

今回は、lazy_staticを利用してstaticなStringを保持して、 勝手にメモリが開放されないようにしています。

JavaScriptから関数を呼ぶ場合はこの処理をなくしても動作するそうですが、 Rustから関数を呼ぶとメモリ開放されて値を受け取れないので、念のためやっておいた方が良いと思います。

src/main.js

const wasm_string_to_js = Module.cwrap('wasm_string_to_js', 'string', []);
document.getElementById('string').innerText = wasm_string_to_js();

src/main.rs

lazy_static! {
    static ref HEAP_STRING: Mutex<String> = Mutex::new(String::new());
}
#[no_mangle]
pub extern "C" fn wasm_string_to_js() -> *const c_char {
    let s: String = "string from rust".to_string();

    let mut output = HEAP_STRING.lock().unwrap();
    output.clear();
    write!(output, "{}\0", s).unwrap();
    output.as_ptr() as *const c_char
}

配列の受け渡し

配列(js->rust)

配列の受け渡しをする時もメモリ管理は重要です。 JavaScriptの方で配列用のメモリを確保し、Rust側でその値を*f32として受け取ります。

JavaScript側で普通にnew FloatArray(5)みたいな感じで配列を作っちゃうと動きません。

src/main.js

// array: js -> rust
const wasm_array_to_rust = Module.cwrap('wasm_array_to_rust', null, ['number', 'number']);
var len = 5;
var bufsize = len * 4;
var bufptr = Module._malloc(bufsize);
Module._memset(bufptr, 0, bufsize)
var buf = new Float32Array(Module.HEAPF32.buffer, bufptr, len);
for (var i = 0; i < len;i ++){
    buf[i] = 1.1 * i;
}
wasm_array_to_rust(len, buf.byteOffset);
Module._free(bufptr); 

src/main.rs

#[no_mangle]
pub extern "C" fn wasm_array_to_rust(_len: i32, ptr: *const f32) {
    let len = _len as usize;
    let slice: &[f32] = unsafe { std::slice::from_raw_parts(ptr, len) };
    for n in slice {
        println!("{}", n);
    }
}

配列(rust->js)

RustからJavaScriptに配列を渡すときも、配列自体はJavaScriptで確保して、Rust側で値を埋めるようにします。

※Rustが1.16.0だとうまく行かなかったので、動かないときはRustのバージョンを上げるとよいかもです。

src/main.js

// array: rust -> js
const wasm_array_to_js = Module.cwrap('wasm_array_to_js', null, ['number', 'number']);
var len = 6;
var bufsize = len * 4;
var bufptr = Module._malloc(bufsize);
Module._memset(bufptr, 0, bufsize)
var buf = new Float32Array(Module.HEAPF32.buffer, bufptr, len);
wasm_array_to_js(len, buf.byteOffset);
document.getElementById('array').innerText = buf;
Module._free(bufptr); 

src/main.rs

#[no_mangle]
pub extern "C" fn wasm_array_to_js(_len: i32, ptr: *mut f32) {
    let len = _len as usize;
    let mut v: Vec<f32> = vec![0.5; len];
    v[3] = 99.3;
    let mut res: &mut [f32] = unsafe { std::slice::from_raw_parts_mut(ptr, len) };
    res.clone_from_slice(&v.as_slice())
}

感想

  • 就活生にWebAssemblyについて質問されたが、うまく答えられなかったのでWebAssembly勉強したい。
  • Rust×WebAssemblyで遊ぼうと思ったが、ここでかなり詰まってしまった。
  • せっかくJavaScriptとRust使っているのにCみたいにメモリ管理するの辛い。
    • ここを乗り切れば平和な世界が訪れる?
  • JavaScriptもRustも超初心者なので何か間違ってる気がする

【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を使っていきましょう!