tensorflow が遅くなる話

masm11 です。

最近、tensorflow を使って機械学習しています。 tensorflow がだんだん遅くなることがあって、気づいたことがあるので、書いてみます。 ただし、以下は私の想像であることをはじめにお断りしておきます。

まず、

a = tf.Variable(...)
b = tf.placeholder(...)

を実行します。この時、以下のように、2つのオブジェクトができて、 a, b がそれぞれのオブジェクトを指します。

f:id:masm11:20180404104135p:plain

次に、

c = a * b

を実行します。この時点では、実際の掛け算は行われません。掛け算を表すオブジェクトから a, b への参照ができます。以下のようになります。

f:id:masm11:20180404104150p:plain

そして、placeholder に値を設定して c を評価します。

d = c.eval(session=sess, feed_dict={b:...})

この時、ようやく実際に掛け算が行われます。

機械学習をしている時、epoch ごとに精度を評価することもあると思います。

for epoch in range(epochs):
    # 学習する
    # ...
    # 評価する
    d = c.eval(session=sess, feed_dict={b:...})

これは問題はありません。

ここで、以下のように書き換えてみます。

for epoch in range(epochs):
    # 学習する
    # ...
    # 評価する
    c = a * b
    d = c.eval(session=sess, feed_dict={b:...})

一見、変数 c を直前に作っているだけで、何も問題はないように思えます。

しかし、

c = a * b

この部分は、実際に掛け算を行わず、掛け算を表すオブジェクトを作るので、 以下のようにたくさんのオブジェクトが作られてしまいます。

f:id:masm11:20180404104206p:plain

さて、ここまで、コードのイメージでしか説明しませんでしたので、 実際のコードで時間を測定してみます。

まずは、ループの前で c を作った場合:

$ cat fast.py
#!/usr/bin/env python

import tensorflow as tf

a = tf.constant(2, dtype=tf.float32)
b = tf.constant(3, dtype=tf.float32)

sess = tf.Session()
sess.run(tf.global_variables_initializer())

c = a * b
for i in range(1000):
    c.eval(session=sess)
$ for i in `seq 5`; do time python fast.py; done

real    0m1.092s
user    0m1.092s
sys 0m0.200s

real    0m1.110s
user    0m1.104s
sys 0m0.208s

real    0m1.108s
user    0m1.116s
sys 0m0.196s

real    0m1.106s
user    0m1.100s
sys 0m0.208s

real    0m1.094s
user    0m1.080s
sys 0m0.216s
$ 

次に、ループの中で c を作った場合です:

$ cat slow.py
#!/usr/bin/env python

import tensorflow as tf

a = tf.constant(2, dtype=tf.float32)
b = tf.constant(3, dtype=tf.float32)

sess = tf.Session()
sess.run(tf.global_variables_initializer())

for i in range(1000):
    c = a * b
    c.eval(session=sess)
$ for i in `seq 5`; do time python slow.py; done

real    0m7.812s
user    0m7.808s
sys 0m0.196s

real    0m7.915s
user    0m7.896s
sys 0m0.208s

real    0m7.834s
user    0m7.824s
sys 0m0.200s

real    0m7.841s
user    0m7.812s
sys 0m0.224s

real    0m7.852s
user    0m7.812s
sys 0m0.232s
$ 

7倍も時間がかかっています。

以上の結果から、tensorflow のオブジェクト(式)は使い捨てにしない方が良さそうです。

Elasticsearch v2.3 から v.5.6 にバージョンアップしました

永田です。

ようやく。

先日おこなった Elasticsearch のバージョンアップについて書きます。

ちょうど年末に Cookpad の中の方がとても良い記事を書いていらっしゃいます。

qiita.com

私も色々と参考にさせていただきました。

ただバージョンアップといっても、使っている機能によって気にするところが色々違います。 この記事も今からバージョンアップされる方の参考になればと思います。

はじめに

2系から5系って、メジャーバージョンが3つもあがってるやん、って思いますが実はそうではないです。 Elasticsearchが2系のときに、関連製品である(いわゆる Elastic Stack)Kibana や Logstash とバージョンを合わせるため、一気にすべてが5系に上がったと記憶しています。

そしてもう Elasticsearch は6系が出ています。

概要

大きく以下のステップを実施しました。

  • jdk のバージョンアップ
  • elasticsearch 5.6 バージョンアップ
  • elasticsearch-rails gem バージョンアップ
  • deprecated なコードを修正

jdkのバージョンアップ

elasticsearch 5系は java8 が必須です。 2系を使っていた頃は open-jdk7 を使っていたのですが、これを機に、oracle-jdk8 に変えました(もう世の中ではJava10と騒いでいますが) まずは 2系を oracle-jdk8 で動かすようにしました。

elasticsearch 5.6 バージョンアップ

移行プラグインで事前調査

まずは移行プラグインで、移行に際して修正すべき点をチェックします。 2系から5系への移行の場合は、 https://github.com/elastic/elasticsearch-migration/tree/2.x を、2系の plugin でインストールします。

./bin/plugin install https://github.com/elastic/elasticsearch-migration/releases/download/v2.0.4/elasticsearch-migration-2.0.4.zip

以下、そのプラグインで挙がった指摘事項です。

Site plugin are no longer supported

サイトプラグインというのは、画面を持つプラグインということでしょう。 2系では、head と inquisitor を使っていましたが、もうプラグインとしては使えなくなります。

ここではひとまず使えないということを理解しておいて、5系ではインストールしないようにするということに注意します。

At least 65536 file descriptors must be available to Elasticsearch

2系利用時は 65535 に設定していました・・・・。

/etc/security/limits.conf の 65535 を 65536 に変更しました。

                soft    nofile          65536
                hard    nofile          65536

bootstrap.mlockall is set to true but mlockall has failed (elasticsearch.yml)

もともとの mlockall をコメントアウトし、memory_lock を付け足して有効にしました

#bootstrap.mlockall: true
bootstrap.memory_lock: true

index.mapper.dynamic can no longer be set in the config file (elasticsearch.yml)

コメントアウトしました。

#index.mapper.dynamic: false

discovery.zen.ping.timeout has been renamed to discovery.zen.ping_timeout (elasticsearch.yml)

言われたとおりにリネーム。

#discovery.zen.ping.timeout: 5s
discovery.zen.ping_timeout: 5s

以上で、移行プラグインの指摘の修正はおわりです。

その他の設定ファイルについて

  • 2系では、使っていた logging.yml は不要になります。代わりに、log4j2.properties を使うことになります。このファイルはES5 に同梱されています。
  • 2系では、elasticsearch.in.sh というスクリプトを用意し、その中で JAVA_OPTS をいじっていましたがこれも不要です。JAVA_OPTSの変更系の設定は jvm.options というファイルでおこないます。これもES5に同梱されています。

起動オプション

  • -Des.default.path.data から -Edefault.path.data という指定の仕方に変わっています。

メインのアップグレード作業

メジャーバージョンの変更なので Rolling Upgrade はできません。 下記のリンクにしたがって、Full Cluster Restart Upgrade をしましょう。

www.elastic.co

我々のサービスでは、Elasticsearch への一切の更新をとめるために計画停止をおこないました。 既存の2系のElasticsearch をすべてシャットダウンし、1台ずつ5系に更新しました。

elasticsearch-rails gem バージョンアップ

-gem 'elasticsearch-rails', '~> 2.0.1'
-gem 'elasticsearch-model', '~> 2.0.1'
+gem 'elasticsearch-rails', '~> 5.0.2'
+gem 'elasticsearch-model', '~> 5.0.2'

deprecated なコードを修正

クエリの修正

missing query はES5からはサポートされなくなったとのこと。

https://www.bountysource.com/issues/47368001-elasticsearch-5-transporterror-400-parsing_exception-no-query-registered-for-missing

{ missing: { field: 'field_name' } }

としていたところを、

{ bool: { must_not: { exists: { field: 'field_name' } } } }

としました。

elasticsearch_deprecation.log

ES5 にすると自動的にこのログをはいてくれます。

Deprecated field [lowercase_expanded_terms] used, replaced by [Decision is now made by the analyzer]

2系では以下のURLにあるように lowercase_expanded_terms というオプションが使えましたが、使えなくなったので外しました。

www.elastic.co

おわりに

上述の Qiita の記事では平均レスポンスタイムが2倍も速くなったとありましたが、うちではちゃんと計測できてませんが、体感はそこまでの変化はありません。 うちのサービスでは通常の検索はそこそこ速いですが、データ量に依存して遅いこともあり、検索レスポンスについてはまだまだ課題を抱えています。

いつもの

エンジニア募集してます! Ruby もやりたいし、Elasticsearch も触ってみたい!ってひとは声かけてくださいねー。

グラフの平行移動について

こんにちは、masm11 です。

ブログに数式が書けるとのことで、今回はプログラミングから離れて、ガチ数学の話をしたいと思います。 といっても難しい話はしません。

高校の数学で、

\displaystyle
 y = ax^{2} + bx + c

を式変形して

\displaystyle
 y = a(x - d)^{2} + e

の形にして、グラフの放物線の頂点が  (d, e) にある、という計算を何度もやらされました。

その中で、疑問に思っていたことがあります。

何故  d の前は  - e の前は  + なのだろう?

ということです。

確かに得られる結果は正しいのですが、釈然としませんでした。

この疑問は高校時代には解決しませんでした。

あれから30年以上経った最近、機械学習について学んでいます。その機械学習の数学をやっている中で、ふと気づきました。

実は、

\displaystyle
 y - e = a(x - d)^{2}

こうなのですね。これなら、 d e も、前が  - です。

 x - d x 軸方向に  d だけ移動していることを表し、同様に  y - e y 軸方向に  e だけ移動していることを表しているわけです。

たったこれだけですが、30年以上の長きにわたる"長年の疑問"がようやく解決しました!

memcached を監視する

永田です。

まずはこちら。

blog.ingage.jp

よろしくお願いします。

さて弊社ではとあるデータのキャッシュには memcached を使っています。 めちゃくちゃ速いので助かっています。

保存するデータの容量はそんなに多くはなくて、 /etc/conf.d/memcached には、

MEMUSAGE="256"

と 256メガバイトを指定しています。キャッシュを登録する際は期限も設定しているのでだいたいこれで回っていますが、キャッシュするデータ量が256MBを超えることがたまにあります。 この場合、キャッシュの量が上記のように設定したサイズを超えるわけではなく、古いキャッシュが消されていくことになります。

こうなったことは検知しておきたかったので、スクリプトを作ることにしました。 (コマンド呼出し部分とか雑ですが・・・)

#!/usr/bin/env perl

use v5.18;

my $cmd_path = "/usr/bin/memcached-tool";
my @cmd_args = ('localhost', 'stats');

unless (-f $cmd_path) {
    die "$cmd_path not found";
}

my $command = $cmd_path . ' ' . join(' ', @cmd_args);
my $output  = `$command`;

my $limit_maxbytes = 0;
my $bytes = 0;
my $curr_items = 0;
my $evictions  = 0;

for my $line (split("\n", $output)) {
    if ($line =~ /\s+limit_maxbytes\s+(\d+)/) {
        $limit_maxbytes = $1;
    }
    if ($line =~ /\s+bytes\s+(\d+)/) {
        $bytes = $1;
    }
    if ($line =~ /\s+curr_items\s+(\d+)/) {
        $curr_items = $1;
    }
    if ($line =~ /\s+evictions\s+(\d+)/) {
        $evictions = $1;
    }
}
if ($evictions > 0) {
  # ここで通知する
}

いくつかデータはとっていますが、容量がいっぱいになって退去(eviction)させられた古いキャッシュの件数が1以上になったら通知してくれたらいいので evictions のチェックのみをしています。

このスクリプトを定期的に流しており、この通知がきたら、memcached を再起動して様子をみています。 (memcached が空になったら、データベースに一斉にアクセスがいくわけですが、今回のケースはそこまで負荷にはなっていないです)

いくら再起動してもこの通知が来るようになったらメモリの容量が足らないと判断できます。

bash script で並列度を制限しながら並列処理する

masm11 です。

bash script で、時間がかかる仕事を複数の CPU core を有効活用して処理したくなったので、今回はその方法を考えてみました。

要件は以下の通りです。

  • bash script で書く。
  • 時間のかかる仕事やあまりかからない仕事がたくさんある。
  • 各仕事は CPU core を1つしか使わない。
  • CPU core は複数あるので、複数の仕事を並列に処理したい。
  • 各仕事は bash script 内で関数で定義されている。

man bash してたどり着いた結論は、jobs %?ce を使う、ということです。 マニュアルには ce としか書いてありませんが、何でも構いません。 例えば jobs %?foo と実行すると、jobs の実行結果のうち foo を含むものだけを出力してくれます。 foo を含むものがあれば $? が 0 に、なければ 1 になります。

この機能を使うことにしました。 コードは以下のようになります。

#!/bin/sh

job_body () {
    id=$1
    # やりたい仕事。ここでは仮に引数に渡された時間だけ sleep することにする。
    echo "$(date +%H:%M:%S) $id enter."
    sleep $2
    echo "$(date +%H:%M:%S) $id leave."
}

process_pool () {
    JOBIDS=([0]=JOBID_a [1]=JOBID_b)
    while :; do
        for id in ${JOBIDS[*]}; do
            if jobs %?${id} > /dev/null 2>&1; then
                # このジョブはまだ実行中。
                :
            else
                if read args; then
                    # 実行が終了している。新たなジョブを投入。
                    eval "job_body $id $args &"
                else
                    # 新たなジョブもない。
                    if jobs %?JOBID_ > /dev/null 2>&1; then
                        # 何らかのジョブがまだ実行中。
                        :
                    else
                        # 新たなジョブもないし、実行中のジョブもない。終了。
                        return
                    fi
                fi
            fi
        done
        sleep 1
    done
}

# process_pool にジョブを渡す
cat <<EOT | process_pool
10
20
30
40
EOT

ジョブは2つまで並列実行可能として、JOBID_a, JOBID_b とします。 最初にその ID を配列で定義しておいて、あとは以下を繰り返しています。

  1. ジョブがまだ実行中なら何もしない。
  2. ジョブが終了しているなら、新たなジョブがあれば起動する。
  3. 新たなジョブがなければ、実行中のジョブがあるか確認して、あれば待機。
  4. それもなければ、何もないので終了。

関数名が process_pool になっていますが、process pool でもなんでもなく、 単に終了したら新たに起動しているだけです。 id が不要であれば、jobs の個数を数えるだけでも良いかもしれません。

結果は以下の通り。

% bash ./test.sh
22:53:29 JOBID_a enter.
22:53:29 JOBID_b enter.
22:53:39 JOBID_a leave.
22:53:40 JOBID_a enter.
22:53:49 JOBID_b leave.
22:53:50 JOBID_b enter.
22:54:10 JOBID_a leave.
22:54:30 JOBID_b leave.
% 

JOBID_aJOBID_b が並列に動作しているのがわかります。うまくいってますね。

上記のコードで細かいことを言えば、個人的には eval を使っているのが気に入っていません。このままでは たまたま $argsJOBID_a が含まれていた場合に誤動作してしまいます。 ここは今後の課題です。

ボツ案として、実は以下のようなものも考えました。

worker () {
    id=$1
    while read a; do
        job_body $id $a
    done
}

process_pool () {
    worker a &
    worker b &
    wait
}

しかし、read が一行だけ read() してくれる可能性が低いので、諦めました。 こちらの方が process pool っぽいのですが。

エンジニア募集しています!

永田です。

今日はCTOとして記事を書きます。

エンジニア募集しています!(切実)

募集内容は 、

中途採用|株式会社インゲージ 採用情報

に載せています。

  • Webアプリケーションエンジニア(サーバーサイドエンジニア)
  • フロントエンジニア
  • インフラエンジニア
  • iOSエンジニア

です。

大阪にある会社で、Ruby (Rails) で仕事ができて、自社サービスで、というのはなかなかないと思います。

どんなサービスを開発しているかは、

最新のメール管理システム|問合せ管理の新基準 Re:lation(リレーション)

を見ていただければわかりますが、今のところサイトがあまりいけてないので(!)、わからないことがあれば説明します。

ただ弊社としては、今、求める人材としては上記ページにも書いていますが、まだまだ小さい会社なので、自己管理ができて、自分から行動してくれる人となります。

もし興味がありましたら、Twitter (@kizashi1122) にメンションください。