オーストラリアで勉強してきたMLデザイナーの口語自由詩

主に、データ分析・機械学習・ベイズ・統計について自由に書く。

PyMC4のInstallation failsというissueに対応するPR送った

TL;DR

次のissueにあるように, 現状だと依存性の解決の部分でfailしてインストールできないのでとりあえずforkして dependency-resolution というbranchで修正してみた.

see: Installation fails · Issue #23 · pymc-devs/pymc4 · GitHub

修正点

これを

tf-nightly==1.9.0.dev20180607
tfp-nightly==0.3.0.dev20180725
tb-nightly==1.9.0a20180613

こうするだけの簡単なお仕事.

tf-nightly
tfp-nightly
tb-nightly

とりあえず本体にPR送った(`・ω・´)

Specified the latest versions of tf/tfp/tfb-nightly by yukinagae · Pull Request #24 · pymc-devs/pymc4 · GitHub

おまけ

pip コマンドよくわかってないオプションがあったので軽く調べた.

pip install --user git+https://github.com/pymc-devs/pymc4.git#egg=pymc4
  • —user
    • ユーザのローカルディレクトリ( ~/.local/ など)に対象のパッケージをインストールする
  • egg=[プロジェクト名を明示的に指定]

また, 次のようにbranch名も指定できる * @[branch名]

pip install --user git+https://github.com/yukinagae/pymc4@dependecny-resolution.git#egg=pymc4

参考資料

Add model.target_log_prob_fn() sampling [a703c21] - pymc4のソースコード読んでみた

f:id:yukinagae:20171122095115p:plain

TL;DR

  • ターゲットとなる unobserved(未観測なRandomVariableインスタンス) の対数確率の合計を返すメソッドを実装しています.

コミット

2018/06/18のコミットです.

以下ファイルが修正されています.

  • pymc4/model/base.py: Model クラスに次の2つのメソッドが追加
    • target_log_prob_fn(self, args, kwargs)
    • unobserved(self) *@property
  • pymc4/util/interceptors.py
    • VariableDescription クラスに rv(RandomVariableのインスタンス) のプロパティが追加されただけです. この追加プロパティは target_log_prob_fn で使用されています(後述).

target_log_prob_fn

unobserved(未観測なrv)log_probability(対数確率) の合計を返します.

※ちなみにちょっとした罠ですが, 変数名の i というのはこのコードの実装では index ではなく, dictの key なので, 単に k などとした方がよいです.

    def target_log_prob_fn(self, *args, **kwargs):
        logp = 0
        for i in self.unobserved.keys():
            print(kwargs.get(i))
            logp += self.unobserved[i].rv.distribution.log_prob(value=kwargs.get(i))
         return logp

unobserved

self.variables (RandomVariableインスタンスのリスト)から unobserved を抽出しているだけです(= observed ではないものを抽出).

ちなみに, わざわざ collections.OrderedDict で順序を保持したdictionary配列にしている理由はこの時点ではよくわかりません.

    @property
    def unobserved(self):
        unobserved = {}
        for i in self.variables:
            if self.variables[i] not in self.observed.values():
                unobserved[i] = self.variables[i]
         unobserved = collections.OrderedDict(unobserved)
        return unobserved

参考資料

python3.5削除 + pep8対応 [bb4de21] - pymc4のソースコード読んでみた

f:id:yukinagae:20171122095115p:plain

TL;DR

  • python3.5は対応せず、3.6以上対応の方針
  • pep8のコードスタイルの修正なので、特に重要な点はなさそうです。今後はlint系のコミットは冗長なので省略するかもしれません。

コミット

2018/06/11のコミットです。

以下ファイルが修正・追加されていますが、基本的にpep8のコードスタイルの修正なので、説明は割愛します。

  • .travis.yml
  • pymc4/distributions/base.py
  • pymc4/inference_sampling_sample.py
  • requirements-dev.txt
  • setup.cfg
  • tests/test_model.py
  • tests/test_nothing.py

python3.5削除(3.6以上対応の方針)

.travis_yml を見ると、pythonのバージョン 3.5 が削除されています。

以下の議論を見ると、基本的に3.6対応していく流れみたいです。

see: Supported Python versions? · Issue #2 · pymc-devs/pymc4 · GitHub

3.6の新機能として、主に以下の f-strings literal と variable annotation があるみたいなので、これらを使用するなら3.5は捨てる必要があります(正確には annotation は3.5では単に無視されるみたいですが、 f-strings が使えないということです)

  • PEP 498, formatted string literals.
  • PEP 526, syntax for variable annotations.

Debian 9 (stable) では3.5が使われているようですが、結論としては miniconda使えばいいじゃん ということになったみたいです。

参考資料

テストサンプルの生成 [e334115, d07338e, 93bc07b] - pymc4のソースコード読んでみた

f:id:yukinagae:20171122095115p:plain

概要

  • Model クラスのサンプル生成のメソッドを読んでみます。
    • test_point

コミット

2018/06/09から2018/06/11の間のコミットです。

Model - pymc4/model/base.py

test_point

まずはこの test_point メソッドがでどのように呼ばれるか調べます。pymc4のプロジェクト内では以下の tests/test_model.py のみで使用されています。

@pm.inline
def model(cfg):
    ed.Normal(0., 1., name='normal')

testval_random = model.test_point()
testval_mode = model.test_point(sample=False)

assert testval_mode['normal'] == 0.
assert testval_mode['normal'] != testval_random['normal']

ここでは2種類の呼び方があるみたいです。

  • test_point(): デフォルトでsample=True
  • test_point(sample=False)

事前情報として、ed.Normal(0., 1., name='normal') というのは、正規分布で平均が0かつ標準偏差が1という意味です。

test_point(sample=True)

Modelに設定されているRandomVariableのインスタンスの確率分布に沿って、1つの乱数が生成されます。

例) 正規分布で平均が0かつ標準偏差が1の確率分布から乱数を生成する

test_point(sample=False)

Modelに設定されているRandomVariableのインスタンスの確率分布に沿って、中央値が返されます。

例) 正規分布で平均が0かつ標準偏差が1の確率分布の中央値は0なので、常に0が返される


さっと test_point メソッドの実装も見てみます。

以下がそのメソッドの全容です。

def test_point(self, sample=True):
    # 1
    def not_observed(var, *args, **kwargs):
        return kwargs['name'] not in self.observed
    values_collector = interceptors.CollectVariables(filter=not_observed)
    chain = [values_collector]

    # 2
    if not sample:
        def get_mode(state, rv, *args, **kwargs):
            return rv.distribution.mode()
        chain.insert(0, interceptors.Generic(after=get_mode))

    # 3
    with self.graph.as_default(), ed.interception(interceptors.Chain(*chain)):
        self._f(self.cfg)

    # 4
    with self.session.as_default():
        returns = self.session.run(list(values_collector.result.values()))
    return dict(zip(values_collector.result.keys(), returns))

一つひとつバラバラに見ていきます。

#1

この辺りの挙動はあまりよくわかっていないので飛ばします。

    def not_observed(var, *args, **kwargs):
        return kwargs['name'] not in self.observed
    values_collector = interceptors.CollectVariables(filter=not_observed)
    chain = [values_collector]

#2

test_point(sample=False) の場合の処理です。やはり予想通り、中央値を返すようになっています。

    if not sample:
        def get_mode(state, rv, *args, **kwargs):
            return rv.distribution.mode()
        chain.insert(0, interceptors.Generic(after=get_mode))

#3

以前、 edward2のinterception処理 の記事で確認したので飛ばします。

    with self.graph.as_default(), ed.interception(interceptors.Chain(*chain)):
        self._f(self.cfg)

#4

tensorflowのsessionを実行して、実際にGraph内の演算処理を実行しています。この辺りも今後もう少し理解できてくるはずです。

    with self.session.as_default():
        returns = self.session.run(list(values_collector.result.values()))
    return dict(zip(values_collector.result.keys(), returns))

The tf.Session.run method is the main mechanism for running a tf.Operation or evaluating a tf.Tensor. You can pass one or more tf.Operation or tf.Tensor objects to tf.Session.run, and TensorFlow will execute the operations that are needed to compute the result.

see: Graphs and Sessions  |  TensorFlow


このコミットを読み続けるのも疲れたので、次回から次のコミットに進みます。


TODO

  • [ ] #1not_observed の処理の内容確認

参考資料

tensorflowのグラフ構造 [e334115, d07338e, 93bc07b] - pymc4のソースコード読んでみた

f:id:yukinagae:20180927082722g:plain

概要

  • まずは Model クラスの初期化処理系のメソッドを読んでみます。
    • _init_variables: 今回はここの self.graph.as_default() の処理を読みます

コミット

2018/06/09から2018/06/11の間のコミットです。

Model - pymc4/model/base.py

_init_variables

今回は _init_variables 内の self.graph.as_default() の処理を見ていきます。

info_collector = interceptors.CollectVariablesInfo() # info_collector() で呼べるcallable
with self.graph.as_default(), ed.interception(info_collector):
    self._f(self.cfg)

self._variables = info_collector.result

self.graphModel クラス内で以下のように定義されているので、実際には self.session.graph == tf.session.graphと同等です。

@property
def graph(self):
    return self.session.graph

また、今回の読む対象に絞って [余計な情報を省略] + [変数を読み替える]、を行うと以下のコードと同等になります。

with tf.Session.graph().as_default():
    ed.Normal(0., 1., name='normal') <= ここはed.XXX()

上記の XX 部分は tensorflow_probabilitydistribution クラスをedward2の RandomVariable クラスでwrapしたものです。

see: probability/generated_random_variables.py at master · tensorflow/probability · GitHub

結局これらを理解するには tensorflow のGraphの動作を理解する方が早そうです。

f:id:yukinagae:20180927082722g:plain

see: Graphs and Sessions  |  TensorFlow

実際に as_default() の使用例を見てみると、 以下のように with スコープで実行された tf.XXX の処理の結果がそのスコープのGraphに追加されていくようです。

g_1 = tf.Graph()
with g_1.as_default():
  # Operations created in this scope will be added to `g_1`.
  c = tf.constant("Node in g_1")

  # Sessions created in this scope will run operations from `g_1`.
  sess_1 = tf.Session()

g_2 = tf.Graph()
with g_2.as_default():
  # Operations created in this scope will be added to `g_2`.
  d = tf.constant("Node in g_2")

# Alternatively, you can pass a graph when constructing a <a href="../api_docs/python/tf/Session"><code>tf.Session</code></a>:
# `sess_2` will run operations from `g_2`.
sess_2 = tf.Session(graph=g_2)

assert c.graph is g_1
assert sess_1.graph is g_1

assert d.graph is g_2
assert sess_2.graph is g_2

つまり以下のコードを簡略的に理解すると、 g1(graph) -> c(constant) という紐付きをGraphとして構造化することができるということです。

with g_1.as_default():
  # Operations created in this scope will be added to `g_1`.
  c = tf.constant("Node in g_1")

同様に以下のコードも、tf.Session.graph() -> ed.Normal() と紐付けることができます。

with tf.Session.graph().as_default():
    ed.Normal(0., 1., name='normal') <= ここはed.XXX()

参考資料

edward2のinterception処理 [e334115, d07338e, 93bc07b] - pymc4のソースコード読んでみた

f:id:yukinagae:20171122095115p:plain

概要

  • まずは Model クラスの初期化処理系のメソッドを読んでみます。
    • _init_variables: 今回はここの ed.interception() の処理を読みます

コミット

2018/06/09から2018/06/11の間のコミットです。

Model - pymc4/model/base.py

_init_variables

今回は _init_variables 内のtensorflow及びtensorflow_probabilityに関連する処理を見ていきます。 ed.interception がわかればなんとなく処理がわかってくるはずです。

info_collector = interceptors.CollectVariablesInfo() # info_collector() で呼べるcallable
with self.graph.as_default(), ed.interception(info_collector):
    self._f(self.cfg)

self._variables = info_collector.result

以下の ed.interception の使い方を見てみましょう。

see: probability/interceptor.py at master · tensorflow/probability · GitHub

def model():
    return ed.Poisson(rate=1.5, name="y")

def interceptor(f, *args, **kwargs):
    if kwargs.get("name") == "y":
        kwargs["value"] = 42
    return interceptable(f)(*args, **kwargs)

with ed.interception(interceptor):
    y = model()

with tf.Session() as sess:
    assert sess.run(y.value) == 42

interceptという名前の通り、 def interceptor(f, *args, **kwargs) のような関数を渡すと、 f を呼んだ結果のインスタンスに値をセットすることができます。

上記の例では、 y というModelインスタンスy.value = 42 の値をセットしています。

他のコードを読んでみると ed.interception(interceptors.Chain(*chain)) という形で使用している箇所もあるので、名前から察するにinterceptの処理を複数重ねていくことができるみたいです。例えば、上記の例で言えば、別のintercept処理を重ねて y.result = [42] のように yインスタンスに新たに result という名前の配列を作成していくこともできるはずです。


以下の _init_variables の処理を見ると、 self._f を実行した戻り値がedward2の RandomVariable クラスのインスタンスであり、そのインスタンスinfo_collector というinterceptの処理をかませていることが見て取れます。前回見た通り、 CollectVariablesInfo 内では 単に f を実行し、info_collectorresult プロパティに RandomVariable の配列を格納していました。

with self.graph.as_default(), ed.interception(info_collector):
    self._f(self.cfg) # <= RandomVariableクラスのインスタンス

結果的に、 with文内で RandomVariableインスタンスの結果を受け取ることなく、 info_collector から結果の配列を受け取ることができます。

self._variables = info_collector.result <= RandomVariableクラスのインスタンス配列

次回は、どう使われているかわからない self.graph.as_default() 部分を読み解いていく予定です。

参考資料

Modelの_init_vatiables内のInterceptor処理 [e334115, d07338e, 93bc07b] - pymc4のソースコード読んでみた

f:id:yukinagae:20171122095115p:plain

概要

  • まずは Model クラスの初期化処理系のメソッドを読んでみます。
    • _init_variables: 今回はここを読みます

コミット

2018/06/09から2018/06/11の間のコミットです。

Model - pymc4/model/base.py

_init_variables

前回わからなかった、 interceptors 周りの処理を読んでいきます。

from pymc4.util import interceptors

(中略)

    def _init_variables(self):
        info_collector = interceptors.CollectVariablesInfo()
        with self.graph.as_default(), ed.interception(info_collector):
            self._f(self.cfg)
        self._variables = info_collector.result

ここでの interceptorsutil 内で自前で定義している CollectVariablesInfo を読んでみます。

CollectVariablesInfo - pymc4/util/interceptors.py

CollectVariablesInfo クラスは Interceptor クラスを継承しているので、まずはそちらを読みます。

VariableDescription = collections.namedtuple('VariableDescription', 'Dist,shape')

class Interceptor(object):
    def name_scope(self):
        return tf.name_scope(self.__class__.__name__.lower())

    def __call__(self, f, *args, **kwargs):
        if kwargs.get('name') is None:
            raise SyntaxError('Every random variable should have a name')
        f, args, kwargs = self.before(f, *args, **kwargs)
        rv = f(*args, **kwargs)
        return self.after(rv, *args, **kwargs)

    def before(self, f, *args, **kwargs):
        return f, args, kwargs

    def after(self, rv, *args, **kwargs):
        return rv

__call__ が定義されているので、おそらく以下のように使用できるはずです。

intercptor = Interceptor() # Interceptorのインスタンス生成
rv = intercptor(f, name="hoge") # intercptor.__call__(f, *args, **kwargs) が呼べる

keyword引数に name が無いと SyntaxError('Every random variable should have a name') になります。

if kwargs.get('name') is None:
    raise SyntaxError('Every random variable should have a name')

あとはただ beforeafter メソッドが呼ばれているだけです。ぱっと見ると、 rvf 関数で生成して、 after で何かしてるようです。

beforeafter は子クラスでoverrideされるメソッドです。)

    f, args, kwargs = self.before(f, *args, **kwargs)
    rv = f(*args, **kwargs)
    return self.after(rv, *args, **kwargs)

    def before(self, f, *args, **kwargs):
        return f, args, kwargs

    def after(self, rv, *args, **kwargs):
        return rv

CollectVariablesInfo も見てみましょう。この中では親クラスの after メソッドがoverrideされています。

class CollectVariablesInfo(Interceptor):
    def __init__(self):
        self.result = collections.OrderedDict()

    def after(self, rv, *args, **kwargs):
        name = kwargs["name"]
        if name not in self.result:
            self.result[name] = VariableDescription(rv.distribution.__class__, rv.shape)
        else:
            raise KeyError(name, 'Duplicate name')
        return rv

親クラスの __call__ メソッド内で既にkeyword引数に name が含まれていることのチェックが行われているため、ここでは単に name = kwargs["name"] で値を取得できることが保証されています。

あとは以下の通り、 result プロパティ(OrderedDict)に name をkeyに、valuerv(RandomVariable) の情報を VariableDescription として rv のクラス名とshapeをセットしています。

self.result[name] = VariableDescription(rv.distribution.__class__, rv.shape)

VariableDescription はただのnamedtuple(名前付きタプル型)です。

VariableDescription = collections.namedtuple('VariableDescription', 'Dist,shape')

次回は _init_variables 内のtensorflow及びtensorflow_probabilityに関連する処理を見ていきます。 ed.interception がわかればなんとなく処理がわかってくるはずです。

see: probability/interceptor.py at master · tensorflow/probability · GitHub

info_collector = interceptors.CollectVariablesInfo() # info_collector() で呼べるcallable
with self.graph.as_default(), ed.interception(info_collector):
    self._f(self.cfg)

参考資料