2

I am not a user of PyMC myself, but recently I stumbled upon this article that showed a snippet of some PyMC model:

def linear_regression(x):
    scale = yield tfd.HalfCauchy(0, 1)
    coefs = yield tfd.Normal(tf.zeros(x.shape[1]), 1, )
    predictions = yield tfd.Normal(tf.linalg.matvec(x, coefs), scale)
    return predictions

The author suggested that users

would be uncomfortable with bar = yield foo

Uncomfortable indeed I am. I tried to make sense of this generator, but couldn't see how it can be used.

This is my thought process. If I do foo = linear_regression(bar) and execute foo (e.g. next(foo)), it will return the value of scale to me. However, this will also turn the local variable scale to None. Similarly, if foo is executed again, I can get the value of coefs, but the local coefs would become None. With both local scale and coefs being None, how can predictions be evaluated?

Or is there a way to evaluate foo without triggering the yield on scale and coefs, and directly yield on predictions?

What is the black magic here? Help needed.

Fanchen Bao
  • 3,310
  • 1
  • 21
  • 34

1 Answers1

4

Disclosure: I'm the author of the original linked article.

I think your main misunderstanding is this: Python generators can not only yield values to you, but you can also send back values to generators using generator.send(). Thus, bar = yield foo would yield foo to you; the generator will wait until you send it another value (which can be None, which is what happens if you just call next(generator)!), assign that value to bar, and then continue running the generator.

Here's a simple example:

>>> def add_one_generator():
...     x = 0
...     while True:
...         x = yield x + 1
...
>>> gen = add_one_generator()
>>> y = gen.send(None)  # First sent value must be None, to start the generator
>>> print(y)
1
>>> z = gen.send(2)
>>> print(z)
3

Notice that when I send(2), the generator assigns the sent value to x, and then resumes execution. In this case, that just means yield x + 1 again, which is why the yielded z is 3.

For more info on this pattern and why it might be useful, take a look at this StackOverflow answer.

Here's some pseudocode that brings us closer to how things will (probably) work in PyMC4:

>>> def linear_regression(x):
...     scale = yield tfd.HalfCauchy(0, 1)
...     coefs = yield tfd.Normal(tf.zeros(x.shape[1]), 1, )
...     predictions = yield tfd.Normal(tf.linalg.matvec(x, coefs), scale)
...     return predictions
>>> model = linear_regression(data)
>>> next_distribution = model.send(None)
>>> scale = pymc_do_things(next_distribution)
>>> coefs = pymc_do_things(model.send(scale))
>>> predictions = pymc_do_things(model.send(coefs))
eigenfoo
  • 56
  • 5
  • 2
    Very surprised to have the author of the article answer my question. Thanks for the explanation. I suppose the reason PyMC4 constructs API this way is that user can inject `scale` and `coefs` as they wish, right? – Fanchen Bao Apr 23 '20 at 15:47
  • 2
    Almost: it allows the _PyMC4 inference engine_ to inject `scale` and `coefs` as it wishes. The user-specified model generator yields the distributions to PyMC4, and PyMC4 will interact with these distributions and re-inject the distribution into the model generator. – eigenfoo Apr 24 '20 at 16:50
  • over a year later, this is such a wholesome conversation, and I love it! Yay, stackoverflow! – Sam Hughes Oct 28 '21 at 18:55