pyenv+Minicondaからdirenvに乗り換える

背景

pyenvを使っていた理由

Minicondaを使っていた理由

  • NumPyやSciPyをバイナリ形式でダウンロードしたかったから
    • Wheel環境が整備されていなかった時代はプロジェクト用のvirtualenvを作って pip install scipy をする毎にNumPyやSciPyのコンパイルのために手が止まってしまっていた

乗り換えようと思った理由

  • Python 2.6を使わなければいけないことが最近ではほとんど無くなったから(homebrewでインストールできるpython2.7とpython3で十分)
  • Wheel環境が整備されてきたのでpipでもバイナリ形式でダウンロードできるから

乗り換え先として試してみたもの

pyenv + pyenv-virtualenv

  • pyenvは残したままでMinicondaだけ捨てる方法
  • pyenv local を使うことで,ディレクトリを移動したときにvirtualenv環境を自動的に変更できる
  • 試してみると,なぜかシェルの動きがもっさりしてしまった(Enterキーを連打してみると分かりやすい。原因不明)
  • プロンプトに出てくるvirtualenv名を消す方法が分からないVIRTUAL_ENV_DISABLE_PROMPT を定義すればいいだけだった…

direnv

  • pyenvもMinicondaも捨てる方法
  • プロンプトに出てくるvirtualenv名を消すためには,source bin/activate するときに VIRTUAL_ENV_DISABLE_PROMPT という環境変数に空文字以外の値を入れておけばよい
  • pip install したときに /usr/local/bin が汚染されるのが怖すぎるので,デフォルトでvirtualenvを有効にするために以下の設定を .zshrc に書いている
if [[ -d "$HOME/.virtualenvs/default" ]]; then
  VIRTUAL_ENV_DISABLE_PROMPT=true source $HOME/.virtualenvs/default/bin/activate
fi
  • ディレクトリ移動時のvirtualenv環境を自動的に変更するには,以下の設定を .envrc に書いておけばよい
source $HOME/.virtualenvs/rime-python2/bin/activate

PyStanのログ出力を消す

背景

PyStanでMCMCするときには標準出力に有益なログが出力されます。

たとえば,サンプリングの途中経過が出力されます。

Iteration:  800 / 2000 [ 40%]  (Warmup)
Iteration:  800 / 2000 [ 40%]  (Warmup)
Iteration:  600 / 2000 [ 30%]  (Warmup)
Iteration:  800 / 2000 [ 40%]  (Warmup)
Iteration: 1000 / 2000 [ 50%]  (Warmup)
Iteration: 1000 / 2000 [ 50%]  (Warmup)
Iteration: 1001 / 2000 [ 50%]  (Sampling)
Iteration: 1001 / 2000 [ 50%]  (Sampling)
Iteration: 1000 / 2000 [ 50%]  (Warmup)
Iteration: 1001 / 2000 [ 50%]  (Sampling)
Iteration:  800 / 2000 [ 40%]  (Warmup)
Iteration: 1200 / 2000 [ 60%]  (Sampling)
Iteration: 1200 / 2000 [ 60%]  (Sampling)

最近になって,サンプリング終了までの時間の予測も出力されるようになりました。

Gradient evaluation took 3e-05 seconds
1000 transitions using 10 leapfrog steps per transition would take 0.3 seconds.
Adjust your expectations accordingly!

これらのログはMCMCによるサンプリングが正しく行えているかどうかチェックするのに非常に有用ですが,ログを出力させたくない場合もあります。私の場合だと,Thompson samplingのための事後分布の推定にStanを使っているのですが,アームを引くたびに事後分布の推定を行う必要があり,何度もStanを呼び出すことになるのでStanのログ出力だけでコンソール出力が埋まってしまうのに悩まされていました。

ダメな解決法1

PyStanのドキュメント を読んでみると,StanModel.sampling のオプションとして refresh というパラメータが見つかります。これはサンプリングの途中経過を表す Iteration: 1001 / 2000 [ 50%] (Sampling) という出力の頻度を調整するパラメータで,0に設定すると途中経過が出力されなくなります。このパラメータの設定によって出力が抑制されるものの,サンプリング終了時に出力される以下のようなログは表示されたままで,かなりの行数が使われてしまい邪魔だと感じてしまいます。

 Elapsed Time: 0.01544 seconds (Warm-up)
               0.013689 seconds (Sampling)
               0.029129 seconds (Total)

ダメな解決法2

Pythoncontextlib モジュールには redirect_stdout というコンテクストマネージャがあり,with文で指定した範囲内で stdout への出力を別のストリームに変更することができます。以下のような関数を作って stdout への出力を /dev/null に変更してみました。

@contextlib.contextmanager
def suppress_stdout():
    with open(os.devnull, "w") as f:
        with contextlib.redirect_stderr(f):
            yield

以下のように使います。

data = ...
stan_model = ...
with suppress_stdout():
    fit = stan_model.sampling(data=data, seed=0)

この方法ではPyStanの出力は抑制できません。PyStanで呼び出されたサンプラーPythonコードの子プロセスとして起動するので,Pythonコード中の stdout が引き継がれるわけではないからです。

うまくいった解決法

以下のGitHub Issuesに「ファイルディスクリプタを書き換えれば良いよ」という書き込みがあったので試してみました。

github.com

今回は stdout への出力を抑制したいだけで stderr の出力はそのまま表示されてほしいので,処理を若干書き換えました。

@contextlib.contextmanager
def suppress_stdout():
    null_fd = os.open(os.devnull, os.O_RDWR)
    save_fd = os.dup(1)
    os.dup2(null_fd, 1)
    yield
    os.dup2(save_fd, 1)
    os.close(null_fd)
    os.close(save_fd)

以下のように使います。

data = ...
stan_model = ...
with suppress_stdout():
    fit = stan_model.sampling(data=data, seed=0)

この方法でPyStanの出力を抑制することができました。

log(1-exp(x)) を安定に計算する

「ベルヌーイ分布のパラメータ x が与えられたときに,裏が出る確率 1-x を計算したい」「1-x ではなく log(1-x) がほしい」「x ではなく log x が与えられている」という状況で,タイトルのように log(1-exp(x)) を計算したい状況になりました。

機械学習では何かの値の対数を計算することがよくありますが,いくつかの計算については数値計算的に安定な方法が提案されています。有名なのは log Σ_i exp(x_i) を計算する logsumexp です。そのまま exp(x_i) を計算するとオーバーフローする可能性がありますが,最終的には対数をとっているので答えはそこまで大きくならないはずで,exp(x_i) のオーバーフローを回避する方法があります。

log(1-exp(x)) についても類似のテクニックがないかTwitterでお聞きしたところ,数名の方から有用な情報を頂いたので紹介します。

今回は Stan のために log(1-exp(x)) が必要だったため,Stan の log1m_exp 関数の中身を調べることにしました。GitHub の stan-dev/math の中で log1m_exp の実装は以下の箇所にあります。

math/log1m_exp.hpp at 8008de4e4d867ae21e6a66d561588a4ebb80e866 · stan-dev/math · GitHub

inline double log1m_exp(double a) {
  using std::log;
  using std::exp;
  if (a >= 0)
    return std::numeric_limits<double>::quiet_NaN();
  else if (a > -0.693147)
    return log(-expm1(a));  // 0.693147 ~= log(2)
  else
    return log1m(exp(a));
}

見たところによると,exp(x) >= 0.5 の場合,すなわち 1-exp(x) が 0 に近い場合は log(-expm1(x)) を計算し,exp(x) < 0.5 の場合,すなわち 1-exp(x) が 1 に近い場合は log1m(exp(x)) を計算しているようです。引き算で生じる桁落ちを防ぎたい気持ちが感じられますね!

ちなみに expm1(x)=exp(x)-1 と log1m(x)=log1p(-x)=log(1-x) の実装はboostのものを使っていて,ソースは以下のページで見られます。

boost/math/special_functions/expm1.hpp - 1.64.0

boost/math/special_functions/log1p.hpp - 1.64.0

数値計算にはいろいろなテクニックがあるので身につけていきたいですね。