Metaclasses, Class for Classes
Python Metaclasses and Bayesian Statistics, an unexpected marriage
See PR 158 of AePPL.
In AePPL, variables from which we can obtain a log-probability function need to be instances of a
RandomVariables are perhaps what comes first to mind when we think of object classes that inherit from
MeasurableVariable, but other
Ops also fall under this umbrella, e.g.
CumOp, etc. Note that the later two come under the form of
MeasurableCumsum, but that’s a whole other story.
However, there are instances where we do not want to extract the log-probability of a variable, notably components in a mixture model. The reason is because we just need the log-probability of the mixture and we “pull” the component-specific log-probability function using indexing. In AePPL mixtures, components are “turned into” instances of
X is the original class of the variable/component. These classes are created dynamically, that is they are created “on the fly” when calling
assign_custom_measurable_outputs. For instance, consider the following example:
import aesara.tensor as at X_rv = at.random.normal(5., 3., name="X") Y_rv = at.random.normal(-5., 3., name="Y") unmeasurable_X = assign_custom_measurable_outputs(X_rv.owner).op # <aesara.tensor.random.basic.UnmeasurableNormalRV at 0x1684808e0> unmeasurable_Y = assign_custom_measurable_outputs(Y_rv.owner).op # <aesara.tensor.random.basic.UnmeasurableNormalRV at 0x168483550>
Y_rv are normal random variables, i.e. instances created from the same
unmeasurable_X are their unmeasurable counterpart. However, while
assign_custom_measurable_outputs dynamically creates
UnmeasurableNormalRV at each call, these seemingly same object classes are not identical; notice already the difference memory addresses (0x1684808e0 and 0x168483550). This discrepancy was by design, i.e. they could have been the same class but we chose not.
However, the creation of new classes every time posed the issue of having each
UnmeasurableNormalRV to not be “equal”, which we would want. Here, the use of language can be confusing; we want each
UnmeasurableNormalRV classes to have the same hash but different ids. Having the same hash allows the ops to be “equal” by Python standards.
hash(unmeasurable_X) == hash(unmeasurable_Y) # True: 4967640381975027986 == 4967640381975027986 id(unmeasurable_X) == id(unmeasurable_Y) # False: 6044493248 == 6044530000 unmeasurable_X == unmeasurable_Y # True, same hashes unmeasurable_X is unmeasurable_Y # False, different ids
To allow object classes to be dynamically created yet have the same hash, we resort to metaclasses, a class whose instances are classes themselves. The example above stems from the use of a newly created Python metaclass which I called
UnmeasurableMeta, which inherits from
aesara.graph.utils.MetaType which itself inherits from
abc.ABCMeta, an Inception-like concept and the supreme lord of Python metaclasses.
class UnmeasurableMeta(MetaType): def __new__(cls, name, bases, dict): if "id_obj" not in dict: dict["id_obj"] = None return super().__new__(cls, name, bases, dict) def __eq__(self, other): if isinstance(other, UnmeasurableMeta): return hash(self.id_obj) == hash(other.id_obj) return False def __hash__(self): return hash(self.id_obj) class UnmeasurableVariable(metaclass=UnmeasurableMeta): """ id_obj is an attribute, i.e. tuple of length two, of the unmeasurable class object. e.g. id_obj = (NormalRV, noop_measurable_outputs_fn) """
Effectively, the dunder methods
__hash__ are overwritten to allow the behaviour above. For more information on the behaviour of unmeasurable variables, please see the newly added tests in
tests/test_abstract.py of the AePPL repository.