In this chapter we show how to evaluate the same compiled program multiple times using updated weights and/or evidence.
In this chapter we will use the model of the smokers
example:
model = """
0.3::stress(X) :- person(X).
0.2::influences(X,Y) :- person(X), person(Y).
smokes(X) :- stress(X).
smokes(X) :- friend(X,Y), influences(Y,X), smokes(Y).
0.4::asthma(X) :- smokes(X).
person(1).
person(2).
person(3).
person(4).
friend(1,2).
friend(2,1).
friend(2,4).
friend(3,2).
friend(4,2).
evidence(smokes(2),true).
evidence(influences(4,2),false).
query(smokes(1)).
query(smokes(2)).
query(smokes(3)).
query(smokes(4)).
query(asthma(1)).
query(asthma(2)).
query(asthma(3)).
query(asthma(4)).
"""
In ProbLog “compiling the model” refers to grounding the model, breaking cycles and applying knowledge compilation to transform it into a logical form on which we can efficiently apply weighted model counting.
In ProbLog these steps are done transparently using the method
create_from
on an Evaluatable object.
from problog.program import PrologString
from problog import get_evaluatable
# Parse the Prolog string
pl_model = PrologString(model)
# Compile the Prolog model into
knowledge = get_evaluatable().create_from(pl_model)
Some important information to keep in mind:
get_evaluatable
function
specifying which one to use (e.g. ddnnf
, sdd
and bdd
).We can now evaluate the model using the parameters and evidence specified in the model.
knowledge.evaluate()
{smokes(1): 0.5087719298245615,
smokes(2): 1.0,
smokes(3): 0.44000000000000006,
smokes(4): 0.44000000000000006,
asthma(1): 0.2035087719298245,
asthma(2): 0.4000000000000001,
asthma(3): 0.17600000000000002,
asthma(4): 0.17600000000000002}
We will now evaluate the model with custom evidence. This mechanism is also used in the modes LFI and DT to avoid compiling the same theory multiple times.
Specifying custom evidence is only allowed on terms that were already
defined as evidence or query in the original model. The evidence can be
set by passing the argument evidence
to the evaluate function. This
dictionary should contain a Term-to-value mapping.
We first define the relevant terms in Python. Note that we have to wrap numbers in a Constant term.
from problog.logic import Term, Constant
# Construct the term 'smokes(2)'
smokes_2 = Term('smokes', Constant(2))
# Construct the term 'influences(4, 2)'
influences_4_2 = Term('influences', Constant(4), Constant(2))
We can now overwrite the evidence from the model, for example, by stating that person 4 does influence person 2.
knowledge.evaluate(evidence={influences_4_2: True})
{smokes(1): 0.37139999999999995,
smokes(2): 0.5393999999999999,
smokes(3): 0.375516,
smokes(4): 0.3478800000000001,
asthma(1): 0.14856,
asthma(2): 0.21575999999999992,
asthma(3): 0.1502064,
asthma(4): 0.13915200000000003}
Note that all other evidence has been discarded, that is, the
observation that person 2 smokes has been discarded. In order to keep
other evidence from the model, we can specify the option
keep_evidence=True
.
knowledge.evaluate(evidence={influences_4_2: True}, keep_evidence=True)
{smokes(1): 0.47052280311457173,
smokes(2): 1.0,
smokes(3): 0.4399999999999999,
smokes(4): 0.6449388209121248,
asthma(1): 0.18820912124582867,
asthma(2): 0.3999999999999999,
asthma(3): 0.17599999999999996,
asthma(4): 0.2579755283648499}
This is equivalent to setting smokes(2) to True explicitly.
We can also discard a specific piece of evidence by giving it the value
None
. Note that you can also use the constant none
in the
original model for evidence that you want to use later.
knowledge.evaluate(evidence={smokes_2: None}, keep_evidence=True)
{smokes(1): 0.34199999999999997,
smokes(2): 0.34199999999999997,
smokes(3): 0.34788000000000013,
smokes(4): 0.34787999999999997,
asthma(1): 0.13679999999999998,
asthma(2): 0.13679999999999998,
asthma(3): 0.139152,
asthma(4): 0.139152}
This keeps influences(4,2)
as false (because we specified
keep_evidence=True
), but it removes the observation for
smokes(2)
.
You can re-evaluate a precompiled circuit with different observations. You have to keep in mind that:
keep_evidence=True
ProbLog only uses the probabilities of the model during the evaluation phase. It is thus possible to overwrite them for that phase. There are two mechanisms that can be used: by passing a weights dictionary and using a custom semiring.
You can pass custom weights to the evaluate
function by passing a
dictionary mapping Terms on probabilities. The disadvantage of this
approach is that it requires you to know the internal names used by
ProbLog.
You can find out these names by using the get_names()
function.
knowledge.get_names()
OrderedSet([(smokes(1), 11), (smokes(2), 13), (smokes(3), 17), (smokes(4), 21), (asthma(1), 23), (asthma(2), 26), (asthma(3), 30), (asthma(4), 32), (choice(0,0,stress(1),1), 1), (choice(10,0,influences(2,1),2,1), 2), (choice(0,0,stress(2),2), 3), (choice(10,0,influences(1,2),1,2), 4), (choice(0,0,stress(4),4), 5), (choice(10,0,influences(2,4),2,4), 6), (influences(4,2), 7), (choice(10,0,influences(2,3),2,3), 14), (choice(0,0,stress(3),3), 16), (choice(31,0,asthma(1),1), 22), (choice(31,0,asthma(2),2), 25), (choice(31,0,asthma(3),3), 29), (choice(31,0,asthma(4),4), 31), (smokes(2), 24)])
You can set the weights by passing a dictionary.
choice = Term('choice')
stress = Term('stress')
c0, c1 = Constant(0), Constant(1)
term = choice(c0, c0, stress(c1), c1)
knowledge.evaluate(weights={term: 0.8})
{smokes(1): 0.883495145631068,
smokes(2): 1.0,
smokes(3): 0.43999999999999995,
smokes(4): 0.43999999999999995,
asthma(1): 0.35339805825242726,
asthma(2): 0.4000000000000001,
asthma(3): 0.17600000000000007,
asthma(4): 0.17599999999999993}
The fact that you need to know the internal names is obviously a major drawback for this approach. In practice it is therefore more useful to use the semiring-based approach.
First, we modify our model to introduce parameterized weights. In this way, we can uniquely identify each weight.
By adding arguments (e.g. p_stress(X)
) we can set the probability of
stress for each person individually. You can also opt to omit the
argument X
if you don’t need to set this parameter for each person
individually (as illustrated for p_influences
).
Note that we keep the fixed probability for asthma
.
model_sr = """
p_stress(X)::stress(X) :- person(X).
p_influences::influences(X,Y) :- person(X), person(Y).
smokes(X) :- stress(X).
smokes(X) :- friend(X,Y), influences(Y,X), smokes(Y).
0.4::asthma(X) :- smokes(X).
person(1).
person(2).
person(3).
person(4).
friend(1,2).
friend(2,1).
friend(2,4).
friend(3,2).
friend(4,2).
evidence(smokes(2),true).
evidence(influences(4,2),false).
query(smokes(1)).
query(smokes(2)).
query(smokes(3)).
query(smokes(4)).
query(asthma(1)).
query(asthma(2)).
query(asthma(3)).
query(asthma(4)).
"""
from problog.program import PrologString
from problog import get_evaluatable
# Parse the Prolog string
pl_model_sr = PrologString(model_sr)
# Compile the Prolog model into
knowledge_sr = get_evaluatable().create_from(pl_model_sr)
We can reuse most of the operations defined by the default semiring
(SemiringLogProbability). We only need to modify the value
operation
that translates a given weight to the internal value.
This function receives the ground term representing the weight (e.g.
p_stress(1)
). We override this function such that it looks up the
corresponding value in a given dictionary.
from problog.evaluator import SemiringLogProbability
class CustomSemiring(SemiringLogProbability):
def __init__(self, weights):
SemiringLogProbability.__init__(self)
self.weights = weights
def value(self, a):
# Argument 'a' contains ground term. Look up its probability in the weights dictionary.
return SemiringLogProbability.value(self, self.weights.get(a, a))
We now need to initialize all the weights in the model. To determine
which weights exist in the circuit, you can use the get_weights
function.
knowledge_sr.get_weights()
{1: p_stress(1),
2: p_influences,
3: p_stress(2),
4: p_influences,
5: p_stress(4),
6: p_influences,
7: p_influences,
14: p_influences,
16: p_stress(3),
22: 0.4,
25: 0.4,
29: 0.4,
31: 0.4}
We now initialize these weights based on the original model.
custom_weights = {}
for x in knowledge_sr.get_weights().values():
if x.functor == 'p_stress':
custom_weights[x] = 0.3
elif x.functor == 'p_influences':
custom_weights[x] = 0.2
custom_weights
{p_influences: 0.2,
p_stress(1): 0.3,
p_stress(2): 0.3,
p_stress(3): 0.3,
p_stress(4): 0.3}
When we evaluate this model using the custom semiring, we get the same result as the original model.
knowledge_sr.evaluate(semiring=CustomSemiring(custom_weights))
{asthma(1): 0.2035087719298245,
asthma(2): 0.4000000000000001,
asthma(3): 0.17600000000000002,
asthma(4): 0.17600000000000002,
smokes(4): 0.44000000000000006,
smokes(1): 0.5087719298245615,
smokes(2): 1.0,
smokes(3): 0.44000000000000006}
We can now override the parameters by changing them in the dictionary.
For example, we can set the stress probability of person 1 to 0.8.
custom_weights[Term('p_stress', Constant(1))] = 0.8
knowledge_sr.evaluate(semiring=CustomSemiring(custom_weights))
{asthma(1): 0.35339805825242726,
asthma(2): 0.4000000000000001,
asthma(3): 0.17600000000000007,
asthma(4): 0.17599999999999993,
smokes(4): 0.43999999999999995,
smokes(1): 0.883495145631068,
smokes(2): 1.0,
smokes(3): 0.43999999999999995}
In this chapter we demonstrated the “compile-once evaluate-many” strategy for two cases:
Both these cases can also be combined.