Skip to content

Utility function to assess conditional independence #791

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Nov 3, 2017
8 changes: 8 additions & 0 deletions docs/tex/bib.bib
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,14 @@ @article{lecun1998gradient
publisher={IEEE}
}

@inproceedings{schachter1998bayes,
title={Bayes-Ball: The Rational Pastime (for Determining Irrelevance and Requisite Information in Belief Networks and Influence Diagrams)},
author={Schachter, Ross D},
booktitle={Proceedings of the Fourteenth Conference in Uncertainty in Artificial Intelligence},
pages={480--487},
year={1998}
}

@article{watts1998collective,
title={Collective dynamics of ‘small-world’networks},
author={Watts, Duncan J and Strogatz, Steven H},
Expand Down
4 changes: 3 additions & 1 deletion edward/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
from edward.util import check_data, check_latent_vars, copy, dot, \
get_ancestors, get_blanket, get_children, get_control_variate_coef, \
get_descendants, get_parents, get_session, get_siblings, get_variables, \
Progbar, random_variables, rbf, set_seed, to_simplex, transform
is_independent, Progbar, random_variables, rbf, set_seed, \
to_simplex, transform
from edward.version import __version__, VERSION

from tensorflow.python.util.all_util import remove_undocumented
Expand Down Expand Up @@ -74,6 +75,7 @@
'get_session',
'get_siblings',
'get_variables',
'is_independent',
'Progbar',
'random_variables',
'rbf',
Expand Down
1 change: 1 addition & 0 deletions edward/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
'get_session',
'get_siblings',
'get_variables',
'is_independent',
'Progbar',
'random_variables',
'rbf',
Expand Down
80 changes: 80 additions & 0 deletions edward/util/random_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from copy import deepcopy
from edward.models.random_variable import RandomVariable
from edward.models.random_variables import TransformedDistribution
from edward.models import PointMass
from edward.util.graphs import random_variables
from tensorflow.contrib.distributions import bijectors
from tensorflow.core.framework import attr_value_pb2
Expand Down Expand Up @@ -714,6 +715,85 @@ def get_variables(x, collection=None):
return list(output)


def is_independent(a, b, condition=None):
"""Assess whether a is independent of b given the random variables in
condition.

Implemented using the Bayes-Ball algorithm [@schachter1998bayes].

Args:
a: RandomVariable or list of RandomVariable.
Query node(s).
b: RandomVariable or list of RandomVariable.
Query node(s).
condition: RandomVariable or list of RandomVariable, optional.
Random variable(s) to condition on.

Returns:
bool.
True if a is independent of b given the random variables in condition.

#### Examples

```python
a = Normal(0.0, 1.0)
b = Normal(a, 1.0)
c = Normal(a, 1.0)
assert ed.is_independent(b, c, condition=a)
```
"""
if condition is None:
condition = []
if not isinstance(a, list):
a = [a]
if not isinstance(b, list):
b = [b]
if not isinstance(condition, list):
condition = [condition]
A = set(a)
B = set(b)
condition = set(condition)

top_marked = set()
# The Bayes-Ball algorithm will traverse the belief network
# and add each node that is relevant to B given condition
# to the set bottom_marked. A and B are conditionally
# independent if no node in A is in bottom_marked.
bottom_marked = set()

schedule = [(node, "child") for node in B]
while schedule:
node, came_from = schedule.pop()

if node not in condition and came_from == "child":
if node not in top_marked:
top_marked.add(node)
for parent in get_parents(node):
schedule.append((parent, "child"))

if not isinstance(node, PointMass) and node not in bottom_marked:
bottom_marked.add(node)
if node in A:
return False # node in A is relevant to B
for child in get_children(node):
schedule.append((child, "parent"))

elif came_from == "parent":
if node in condition and node not in top_marked:
top_marked.add(node)
for parent in get_parents(node):
schedule.append((parent, "child"))

elif node not in condition and node not in bottom_marked:
bottom_marked.add(node)
if node in A:
return False # node in A is relevant to B
for child in get_children(node):
schedule.append((child, "parent"))

return True


def transform(x, *args, **kwargs):
"""Transform a continuous random variable to the unconstrained space.

Expand Down
64 changes: 64 additions & 0 deletions tests/util/test_is_independent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import tensorflow as tf

from edward.models import Normal
from edward.util import is_independent


class test_is_independent_class(tf.test.TestCase):

def test_chain_structure(self):
"""a -> b -> c -> d -> e"""
a = Normal(0.0, 1.0)
b = Normal(a, 1.0)
c = Normal(b, 1.0)
d = Normal(c, 1.0)
e = Normal(d, 1.0)
self.assertTrue(is_independent(c, e, d))
self.assertTrue(is_independent([a, b, c], e, d))
self.assertTrue(is_independent([a, b], [d, e], c))
self.assertFalse(is_independent([a, b, e], d, c))

def test_binary_structure(self):
"""f <- c <- a -> b -> d
| |
v v
g e
"""
a = Normal(0.0, 1.0)
b = Normal(a, 1.0)
c = Normal(a, 1.0)
d = Normal(b, 1.0)
e = Normal(b, 1.0)
f = Normal(c, 1.0)
g = Normal(c, 1.0)
self.assertFalse(is_independent(b, c))
self.assertTrue(is_independent(b, c, a))
self.assertTrue(is_independent(d, [a, c, e, f, g], b))
self.assertFalse(is_independent(b, [e, d], a))
self.assertFalse(is_independent(a, [b, c, d, e, f, g]))

def test_grid_structure(self):
"""a -> b -> c
| | |
v v v
d -> e -> f
"""
a = Normal(0.0, 1.0)
b = Normal(a, 1.0)
c = Normal(b, 1.0)
d = Normal(a, 1.0)
e = Normal(b + d, 1.0)
f = Normal(e + c, 1.0)
self.assertFalse(is_independent(f, [a, b, d]))
self.assertTrue(is_independent(f, [a, b, d], [e, c]))
self.assertTrue(is_independent(e, [a, c], [b, d]))
self.assertFalse(is_independent(e, f, [b, d]))
self.assertFalse(is_independent(e, f, [a, b, c, d]))


if __name__ == '__main__':
tf.test.main()