pymc4のソースコード読んでみた - Remove treedict dependency [10ea1aa]
TL;DR
model nesting が不要になったので、 treedict
が削除されたみたいです。
コミット
2018/05/31のコミットです。
以前は Model
クラスで treedict
クラスが使用されていましたが、その処理が削除されています。その理由や流れを関連issueを読みながらまとめてみます。
10ea1aa - Remove treedict dependency
以下のissueで言及されています。
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]
TL;DR
pep8のlint系の対応やrequirements.txtの更新など、雑多なコミットが多かったので、1つの記事にまとめました。途中で jupyter notebook
で test.ipynb
を起動してもライブラリが無いとか、そもそもシンタックスエラー等があるので、コミット c1ea662
以降に動作確認した方がいいです。
また大きな変更としては、 treedict
の処理が削除されています。これにより、Model
内の named_vars
の処理で parent
を考慮しない作りになったようです。この辺りは別途issueで言及されているので、次回に確認します。
コミット
2018/05/30から2018/05/31の間のコミットです。
- Edit notebook · pymc-devs/pymc4@8c1d02a · GitHub
- Remove .vscode files · pymc-devs/pymc4@d932437 · GitHub
- Fix pep8 lint errors · pymc-devs/pymc4@a0879d4 · GitHub
- Add all · pymc-devs/pymc4@2b5946c · GitHub
- Remove treedict dependency · pymc-devs/pymc4@10ea1aa · GitHub
- fix pylint errors, unused imports · pymc-devs/pymc4@9086d1a · GitHub
- update requirements.txt · pymc-devs/pymc4@c1ea662 · GitHub
各コミットの簡単な補足説明です。
- 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
- requirements.txtを更新
10ea1aa - Remove treedict dependency
以下のissueで言及されています。次回内容を読みます。
参考資料
pymc4のソースコード読んでみた - “Model”の動作確認, Initial Model Class, sampling and random variable [aafa32d]
コミット
2018/05/30のコミットです。
Initial Model Class, sampling and random variable · pymc-devs/pymc4@aafa32d · GitHub
主に、pymc4の根幹となる Model
と RandomVariable
クラスが作成されています。
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
- [ ]
RandomVariable
のインスタンス生成部分のAttributeError
の解消
AttributeError: 'RandomVariable' object has no attribute 'sample'
References
pymc4のソースコード読んでみた - (仮)図でまとめ, Initial Model Class, sampling and random variable [aafa32d]
まとめてみた(内容はまだ自信がないので、後日またチェックしてみる)
メモ
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
を最後まで辿って重複しないかチェックしている)
pymc4のソースコード読んでみた - “Model”, Initial Model Class, sampling and random variable [aafa32d]
コミット
2018/05/30のコミットです。
Initial Model Class, sampling and random variable · pymc-devs/pymc4@aafa32d · GitHub
主に、pymc4の根幹となる Model
と RandomVariable
クラスが作成されています。
以下ファイルが追加されています。
- .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
をセットしています。
kwargs
のキーワード引数にmodel
が存在する場合:parent
にそのmodel
インスタンスをセット- クラスメソッドの
get_contexts
が存在する場合:parent
にget_contexts
の末尾をセット - それ以外:
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_contexts
と get_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]
コミット
2018/05/30のコミットです。
Initial Model Class, sampling and random variable · pymc-devs/pymc4@aafa32d · GitHub
主に、pymc4の根幹となる Model
と RandomVariable
クラスが作成されています。
以下ファイルが追加されています。
- .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
メソッドが定義されているのでまずはこちらを読みます。 meth
は withparent
から渡ってきた引数の 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
以下の処理では meth
が dict.__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]
コミット
2018/05/30のコミットです。
Initial Model Class, sampling and random variable · pymc-devs/pymc4@aafa32d · GitHub
主に、pymc4の根幹となる Model
と RandomVariable
クラスが作成されています。
以下ファイルが追加されています。
- .vscode/settings.json
- pymc4/init.json
- pymc4/model.py
- pymc4/random_variable.py
- pymc4/sample.py
- test.ipynb
一つ一つを細かくみたいので、今回は model.py
の Model
クラスだけを見てみようと思ったのですが、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__
処理に注目してみます。
- super().init で初期化
parent
がdict
もしくは None であることをassertチェックparent
がdict
もしくはNone
であれば引数のparent
をself
にセットする
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
parent
が dict
インスタンスの場合
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 の処理を見てみます。
parent
がtreedict
の場合: 対象の値がself
内、、もしくはparent
に存在するかチェックparent
がdict
の場合: 対象の値がself
内、、もしくはparent
に存在するかチェック- それ以外の場合(=
parent
の探索が不要): 対象の値がself
内に存在するかチェック
1
と 2
の違いは、 parent
が treedict
か dict
かによって呼ぶメソッドを切り替えているだけです。