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

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

pymc4のソースコード読んでみた - Remove treedict dependency [10ea1aa]

f:id:yukinagae:20171122095115p:plain

TL;DR

model nesting が不要になったので、 treedict が削除されたみたいです。

コミット

2018/05/31のコミットです。

以前は Model クラスで treedict クラスが使用されていましたが、その処理が削除されています。その理由や流れを関連issueを読みながらまとめてみます。

10ea1aa - Remove treedict dependency

以下のissueで言及されています。

see: Model context manager, primitive default sampling, random variable class by sharanry · Pull Request #1 · pymc-devs/pymc4 · GitHub

The reason treedict is used in pymc3 is model nesting. Once we were thinking about building blocks that can contan variables. Model definition would come in init like in regular context manager

以前のバージョンpymc3で treedict が使用されていたのは model nesting が理由でした。

@ferrine We never used model nesting and I think we should drop it.

もう model nesting は使わない から treedict はなくていいよ。

実際にコードで動作の違いを見てみます。実際にjupyte notebookで実行して比較してみました。

see: pymc4_code_reading/summary-10ea1aa.ipynb at master · yukinagae/pymc4_code_reading · GitHub

  • treedictクラスに依存している場合、ModelがネストするとparentにもRandomVariableが伝搬する
    • model1(parent): [rv1, rv2, rv3]
    • model2(child): [rv2, rv3]
with WithTreeModel(name="model1") as model1:
    rv1 = WithTreeRandomVariable(name="rv1")
    with WithTreeModel(name="model2") as model2:
        rv2 = WithTreeRandomVariable(name="rv2")
        rv3 = WithTreeRandomVariable(name="rv3")
        print("model1: {}".format([v for v in model1.named_vars]))
        print("model2: {}".format([v for v in model2.named_vars]))

# 出力:
# => model1: ['rv1', 'rv2', 'rv3']
# => model2: ['rv2', 'rv3']
  • treedictクラスに依存していない場合、ModelがネストしてもparentにRandomVariableが伝搬しない
    • model1(parent): [rv1]
    • model2(child): [rv2, rv3]
with NoTreeModel(name="model1") as model1:
    rv1 = NoTreeRandomVariable(name="rv1")
    with NoTreeModel(name="model2") as model2:
        rv2 = NoTreeRandomVariable(name="rv2")
        rv3 = NoTreeRandomVariable(name="rv3")
        print("model1: {}".format([v for v in model1.named_vars]))
        print("model2: {}".format([v for v in model2.named_vars]))

# 出力:
# => model1: ['rv1']
# => model2: ['rv2', 'rv3']

参考資料

pymc4のソースコード読んでみた - lintや依存ライブラリ情報の更新などもろもろ [8c1d02a, d932437, a0879d4, 2b5946c, 10ea1aa, 9086d1a, c1ea662]

f:id:yukinagae:20171122095115p:plain

TL;DR

pep8のlint系の対応やrequirements.txtの更新など、雑多なコミットが多かったので、1つの記事にまとめました。途中で jupyter notebooktest.ipynb を起動してもライブラリが無いとか、そもそもシンタックスエラー等があるので、コミット c1ea662 以降に動作確認した方がいいです。

また大きな変更としては、 treedict の処理が削除されています。これにより、Model 内の named_vars の処理で parent を考慮しない作りになったようです。この辺りは別途issueで言及されているので、次回に確認します。

コミット

2018/05/30から2018/05/31の間のコミットです。

各コミットの簡単な補足説明です。

  • 8c1d02a
    • jupyter notebookの修正(この時点のコードは上手く動作しない)
  • d932437
    • vscodeの設定ファイルを削除
  • a0879d4
    • __init__.py 内に残っていたコミットのdiffを削除
    • pep8のlintエラー対応
      • 不要なimportの削除
      • インデントや空白、改行の調整
  • 2b5946c
    • __all__ でexportするモジュール名のリストを明示する
  • 10ea1aa
    • 後述(次回に持ち越し)
  • 9086d1a
    • pep8のlintエラー対応
  • c1ea662
    • requirements.txtを更新
      • tensorflow==1.8.0
      • xarray==0.10.4
      • numpy==1.14.3
      • tqdm==4.23.3
      • tfp-nightly==0.0.1.dev20180515

10ea1aa - Remove treedict dependency

以下のissueで言及されています。次回内容を読みます。

see: Model context manager, primitive default sampling, random variable class by sharanry · Pull Request #1 · pymc-devs/pymc4 · GitHub

参考資料

pymc4のソースコード読んでみた - “Model”の動作確認, Initial Model Class, sampling and random variable [aafa32d]

f:id:yukinagae:20171122095115p:plain

コミット

2018/05/30のコミットです。

Initial Model Class, sampling and random variable · pymc-devs/pymc4@aafa32d · GitHub

主に、pymc4の根幹となる ModelRandomVariable クラスが作成されています。

Model クラスの動作を確認してみます。

Model - model.py

with でModelインスタンスを生成する度に、stackの末尾にそのmodelインスタンスが生成され、parent が設定されるテスト

print("before: ", Model.get_contexts())

with Model(name="model1") as model1:
    print("[1] inside with", ["{}:{}".format(x.name, getattr(x, 'parent', None)) for x in Model.get_contexts()])
    with Model(name="model2") as model2:
        print("[2] inside with", ["{}:{}".format(x.name, getattr(x, 'parent', None)) for x in Model.get_contexts()])
        print("model2's parent: ", model2.parent.name)
    
with Model(name="model3") as model3:
    print("[3] inside with", ["{}:{}".format(x.name, getattr(x, 'parent', None)) for x in Model.get_contexts()])

print("after: ", Model.get_contexts())

出力:

before:  []
[1] inside with ['model1:None']
[2] inside with ['model1:None', 'model2:<__main__.Model object at 0x1225e8048>']
model2's parent:  model1
[3] inside with ['model3:None']
after:  []

TODO

AttributeError: 'RandomVariable' object has no attribute 'sample'

References

pymc4のソースコード読んでみた - (仮)図でまとめ, Initial Model Class, sampling and random variable [aafa32d]

f:id:yukinagae:20171122095115p:plain

まとめてみた(内容はまだ自信がないので、後日またチェックしてみる)

メモ

  • Model クラスは Context クラスを継承している
  • Model は、クラスのスコープで contexts.stack を保持している
  • with でmodelインスタンスを生成する度に、stackの末尾にそのmodelインスタンスが追加される
  • contexts.stack 内の、1つ前のmodelインスタンスparent になる(0番目のmodelインスタンスparent はNoneになる)
  • RandomVariable(RV)インスタンスが生成される度に、 contexts.stack の末尾のmodelインスタンスにそのRVインスタンスが追加される(その際に parent を最後まで辿って重複しないかチェックしている)

f:id:yukinagae:20180906090637j:plain

pymc4のソースコード読んでみた - “Model”, Initial Model Class, sampling and random variable [aafa32d]

f:id:yukinagae:20171122095115p:plain

コミット

2018/05/30のコミットです。

Initial Model Class, sampling and random variable · pymc-devs/pymc4@aafa32d · GitHub

主に、pymc4の根幹となる ModelRandomVariable クラスが作成されています。

以下ファイルが追加されています。

  • .vscode/settings.json
  • pymc4/init.json
  • pymc4/model.py
  • pymc4/random_variable.py
  • pymc4/sample.py
  • test.ipynb

やっと Model クラスを読んでみます。

Model - model.py

Model クラスのメソッドは以下の通りです。

  • new
  • init
  • model - @property
  • description - @property
  • get_contexts - @classmethod
  • get_context - @classmethod
  • add_random_variable

まずは __new__ メソッドのインスタンス生成部分です。単にインスタンス生成した後は以下3つのパターンで parent をセットしています。

  1. kwargs のキーワード引数に model が存在する場合: parent にその model インスタンスをセット
  2. クラスメソッドの get_contexts が存在する場合: parentget_contexts の末尾をセット
  3. それ以外: parent にNoneをセット
def __new__(cls, *args, **kwargs):
    instance = super(Model, cls).__new__(cls)
    if kwargs.get('model') is not None:
        instance.parent = kwargs.get('model')
    elif cls.get_contexts():
        instance.parent = cls.get_contexts()[-1]
    else:
        instance.parent = None

__init__ メソッドでは named_vars フィールドを treedict インスタンスをセットしているだけみたいです。 parent がある場合にはそれもセットしています。

def __init__(self, name="", model=None, ):
    self.name = name
    if self.parent is not None:
        self.named_vars = treedict(parent=self.parent.named_vars)
    else:
        self.named_vars = treedict()

@property 属性が指定されている以下メソッドは単なるgetterとして用意されているみたいなので、まだどのように使われるかわからないので飛ばします。 description メソッドは実装もされていません。

@property
def model(self):
    return self
    
@property
def decription(self):
    return

get_contextsget_context は同時に読みます。

cls.contexts でクラス単位のフィールドをチェックしていますが、これは継承元の Context クラスで定義されている threading.local() と等価です。またコメントに記載あるように、クラス単位の contexts フィールドはスレッドセーフなオブジェクトとなっています。

get_contexts では単に contexts フィールドの有無をチェックし、 stack を初期化 or stack を返却しているだけです。

get_context ではcontexts.stack の末尾のインスタンスを返却しています。配列にアクセスしているため、 IndexError が発生した場合を想定しています。

@classmethod
def get_contexts(cls):
    # no race-condition here, cls.contexts is a thread-local object
    # be sure not to override contexts in a subclass however!
    if not hasattr(cls.contexts, 'stack'):
        cls.contexts.stack = []
    return cls.contexts.stack

@classmethod
def get_context(cls):
    """Return the deepest context on the stack."""
    try:
        return cls.get_contexts()[-1]
    except IndexError:
        raise TypeError("No context on context stack")

最後に add_random_variable メソッドです。

named_vars フィールド内に引数の var インスタンス(= RandomVariable)をセットしています。すでに存在する場合には ValueError を投げます。 tree_contains メソッドを呼んでいるので、 parent まで辿って存在の有無をチェックをしています。

def add_random_variable(self, var):
    """Add a random variable to the named variables of the model."""
    if self.named_vars.tree_contains(var.name):
        raise ValueError(
            "Variable name {} already exists.".format(var.name))
    self.named_vars[var.name] = var

TODO

  • [ ] Model クラス内で __new__ メソッド内で parent の値をセットしているが、 __init__ 内で実行する場合との違いは? __init__ 内で実行しても問題ないのでは?

References

pymc4のソースコード読んでみた - “withparent”, Initial Model Class, sampling and random variable [aafa32d]

f:id:yukinagae:20171122095115p:plain

コミット

2018/05/30のコミットです。

Initial Model Class, sampling and random variable · pymc-devs/pymc4@aafa32d · GitHub

主に、pymc4の根幹となる ModelRandomVariable クラスが作成されています。

以下ファイルが追加されています。

  • .vscode/settings.json
  • pymc4/init.json
  • pymc4/model.py
  • pymc4/random_variable.py
  • pymc4/sample.py
  • test.ipynb

前回飛ばした withparent のところを継続して読んでみます。

withparent - model.py

withparent メソッドは以下の treedict クラスの内部で呼ばれています(メソッド内ではなく単体で呼ばれています)

親の dict クラスの __setitem__update メソッドを上書きしているように見えます。 withparent という名前からすると、値のセット・更新時に parent の値も拡張するようにしていると推測できます。

# typechecking here works bad
__setitem__ = withparent(dict.__setitem__)
update = withparent(dict.update)

実際に withparent メソッドを読んでみます。

def withparent(meth):
    """Helper wrapper that passes calls to parent's instance"""
    def wrapped(self, *args, **kwargs):
        res = meth(self, *args, **kwargs)
        if getattr(self, 'parent', None) is not None:
            getattr(self.parent, meth.__name__)(*args, **kwargs)
        return res
    # Unfortunately functools wrapper fails
    # when decorating built-in methods so we
    # need to fix that improper behaviour
    wrapped.__name__ = meth.__name__
    return wrapped

コメントに Helper wrapper that passes calls to parent's instance とあるように、 parentインスタンスを呼ぶヘルパーメソッドのようです。このあたりは以前に読んだ tree_contains と似たようなものだと思えます。

withparent 内の以下の wrapped メソッドが定義されているのでまずはこちらを読みます。 methwithparent から渡ってきた引数の dict.__setitem__dict.update になります。

def wrapped(self, *args, **kwargs):
    res = meth(self, *args, **kwargs)
    if getattr(self, 'parent', None) is not None:
        getattr(self.parent, meth.__name__)(*args, **kwargs)
    return res

以下の処理では methdict.__setitem__ と同等だとすると、単に self.__setitem__ を呼んでいるのと一緒のはずです。

res = meth(self, *args, **kwargs)

その後に tree_contains と同様に、 parent の有無をチェックしてもし存在するなら parent__setitem__ を呼び出しています。

if getattr(self, 'parent', None) is not None:
     getattr(self.parent, meth.__name__)(*args, **kwargs)

最後に wrapped メソッドの __name__ を上書きしています。以下のように試しに名前変更前後の値を見てみると、もともと wrapped という名前が __setitem__ という名前に変更されていることがわかります。

print(wrapped.__name__) # <= wrapped
wrapped.__name__ = meth.__name__
print(wrapped.__name__) # <= __setitem__
return wrapped

ちなみに def __setitem__def update を単に treedict クラス内で作成して、親の dict クラスの処理を上書きしても同様のことができるはずです。

References

pymc4のソースコード読んでみた - “treedict”, Initial Model Class, sampling and random variable [aafa32d]

f:id:yukinagae:20171122095115p:plain

コミット

2018/05/30のコミットです。

Initial Model Class, sampling and random variable · pymc-devs/pymc4@aafa32d · GitHub

主に、pymc4の根幹となる ModelRandomVariable クラスが作成されています。

以下ファイルが追加されています。

  • .vscode/settings.json
  • pymc4/init.json
  • pymc4/model.py
  • pymc4/random_variable.py
  • pymc4/sample.py
  • test.ipynb

一つ一つを細かくみたいので、今回は model.pyModel クラスだけを見てみようと思ったのですが、Model クラス内で使用している treedict クラスがあるのでまずはこっちを読んでみます(できるだけコード量が少なく、依存性が小さいクラスから読み進める方針です)

treedict - model.py

以下が treedict クラスです。

class treedict(dict):
    """A dict that passes mutable extending operations used in Model
    to parent dict instance.
    Extending treedict you will also extend its parent
    """
    def __init__(self, iterable=(), parent=None, **kwargs):
        super(treedict, self).__init__(iterable, **kwargs)
        assert isinstance(parent, dict) or parent is None
        self.parent = parent
        if self.parent is not None:
            self.parent.update(self)
    # typechecking here works bad
    __setitem__ = withparent(dict.__setitem__)
    update = withparent(dict.update)

    def tree_contains(self, item):
        # needed for `add_random_variable` method
        if isinstance(self.parent, treedict):
            return (dict.__contains__(self, item) or
                    self.parent.tree_contains(item))
        elif isinstance(self.parent, dict):
            return (dict.__contains__(self, item) or
                    self.parent.__contains__(item))
        else:
            return dict.__contains__(self, item)
  • まずはクラスのコメントから読んでみます。

A dict that passes mutable extending operations used in Model to parent dict instance. Extending treedict you will also extend its parent

つまり、 python組み込みの dict クラスを継承して、親クラス(=parent)の dict まで拡張するように変更した自作クラスのようです。また Model クラス内で使用されているとのことです。

次に __init__ 処理に注目してみます。

  1. super().init で初期化
  2. parentdict もしくは None であることをassertチェック
  3. parentdict もしくは None であれば引数の parentself にセットする
    1. もし親の parentdict インスタンスであれば、その親インスタンスにも key:value をセットする
def __init__(self, iterable=(), parent=None, **kwargs):
    super(treedict, self).__init__(iterable, **kwargs)
    assert isinstance(parent, dict) or parent is None
    self.parent = parent
    if self.parent is not None:
        self.parent.update(self)

例えば以下のようになります。

parent がNoneの場合

child1 = treedict({'a': 1, 'b': 2})
child1 # => {'a': 1, 'b': 2}
child1.parent # => None

parentdict インスタンスの場合

child2 = treedict({'a': 1, 'b': 2}, parent={'x': 1, 'y': 2})
child2 # => {'a': 1, 'b': 2}
child2.parent # => {'a': 1, 'b': 2, 'x': 1, 'y': 2}
  • 別メソッドの withparent を呼んでいるみたいなので、次回のコードリーディングに持ち越します。
# typechecking here works bad
__setitem__ = withparent(dict.__setitem__)
update = withparent(dict.update)
  • tree_contains メソッドを見てみます。組み込みの dict クラスの __contains__ メソッドは対象の値が自分の key: value の中に存在するかチェックできますが、 parent まで値をたどることができません。ですので、この treedict メソッドでは親の parent まで値をたどるようにしているようです。
def tree_contains(self, item):
    # needed for `add_random_variable` method
    if isinstance(self.parent, treedict):
        return (dict.__contains__(self, item) or
                self.parent.tree_contains(item))
    elif isinstance(self.parent, dict):
        return (dict.__contains__(self, item) or
                self.parent.__contains__(item))
    else:
        return dict.__contains__(self, item)

3つの if-elif-else の処理を見てみます。

  1. parenttreedict の場合: 対象の値が self 内、、もしくは parent に存在するかチェック
  2. parentdict の場合: 対象の値が self 内、、もしくは parent に存在するかチェック
  3. それ以外の場合(= parent の探索が不要): 対象の値が self 内に存在するかチェック

12 の違いは、 parenttreedictdict かによって呼ぶメソッドを切り替えているだけです。

References