From 7377a0102a3fc0f922de8508082f8606f1b93c26 Mon Sep 17 00:00:00 2001 From: Chipe1 Date: Thu, 17 Aug 2017 19:19:44 +0530 Subject: [PATCH 1/3] Added predicate_symbols --- logic.py | 34 ++++++++++++++++++++++------------ tests/test_logic.py | 23 +++++++++++++++++++---- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/logic.py b/logic.py index 893884e51..5810e633f 100644 --- a/logic.py +++ b/logic.py @@ -196,7 +196,7 @@ def tt_entails(kb, alpha): True """ assert not variables(alpha) - symbols = prop_symbols(kb & alpha) + symbols = list(prop_symbols(kb & alpha)) return tt_check_all(kb, alpha, symbols, {}) @@ -216,23 +216,33 @@ def tt_check_all(kb, alpha, symbols, model): def prop_symbols(x): - """Return a list of all propositional symbols in x.""" + """Return the set of all propositional symbols in x.""" if not isinstance(x, Expr): - return [] + return set() elif is_prop_symbol(x.op): - return [x] + return {x} else: - return list(set(symbol for arg in x.args for symbol in prop_symbols(arg))) + return {symbol for arg in x.args for symbol in prop_symbols(arg)} def constant_symbols(x): - """Return a list of all constant symbols in x.""" + """Return the set of all constant symbols in x.""" if not isinstance(x, Expr): - return [] + return set() elif is_prop_symbol(x.op) and not x.args: - return [x] + return {x} else: - return list({symbol for arg in x.args for symbol in constant_symbols(arg)}) + return {symbol for arg in x.args for symbol in constant_symbols(arg)} + + +def predicate_symbols(x): + """Return a set of (symbol_name, arity) in x. + All symbols (even functional) with arity > 0 are considered.""" + if not isinstance(x, Expr) or not x.args: + return set() + pred_set = {(x.op, len(x.args))} if is_prop_symbol(x.op) else set() + pred_set.update({symbol for arg in x.args for symbol in predicate_symbols(arg)}) + return pred_set def tt_true(s): @@ -549,7 +559,7 @@ def dpll_satisfiable(s): function find_pure_symbol is passed a list of unknown clauses, rather than a list of all clauses and the model; this is more efficient.""" clauses = conjuncts(to_cnf(s)) - symbols = prop_symbols(s) + symbols = list(prop_symbols(s)) return dpll(clauses, symbols, {}) @@ -652,7 +662,7 @@ def WalkSAT(clauses, p=0.5, max_flips=10000): """Checks for satisfiability of all clauses by randomly flipping values of variables """ # Set of all symbols in all clauses - symbols = set(sym for clause in clauses for sym in prop_symbols(clause)) + symbols = {sym for clause in clauses for sym in prop_symbols(clause)} # model is a random assignment of true/false to the symbols in clauses model = {s: random.choice([True, False]) for s in symbols} for i in range(max_flips): @@ -663,7 +673,7 @@ def WalkSAT(clauses, p=0.5, max_flips=10000): return model clause = random.choice(unsatisfied) if probability(p): - sym = random.choice(prop_symbols(clause)) + sym = random.choice(list(prop_symbols(clause))) else: # Flip the symbol in clause that maximizes number of sat. clauses def sat_count(sym): diff --git a/tests/test_logic.py b/tests/test_logic.py index ade597609..86bcc9ed6 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -164,13 +164,28 @@ def test_tt_entails(): def test_prop_symbols(): - assert set(prop_symbols(expr('x & y & z | A'))) == {A} - assert set(prop_symbols(expr('(x & B(z)) ==> Farmer(y) | A'))) == {A, expr('Farmer(y)'), expr('B(z)')} + assert prop_symbols(expr('x & y & z | A')) == {A} + assert prop_symbols(expr('(x & B(z)) ==> Farmer(y) | A')) == {A, expr('Farmer(y)'), expr('B(z)')} def test_constant_symbols(): - assert set(constant_symbols(expr('x & y & z | A'))) == {A} - assert set(constant_symbols(expr('(x & B(z)) & Father(John) ==> Farmer(y) | A'))) == {A, expr('John')} + assert constant_symbols(expr('x & y & z | A')) == {A} + assert constant_symbols(expr('(x & B(z)) & Father(John) ==> Farmer(y) | A')) == {A, expr('John')} + + +def test_predicate_symbols(): + assert predicate_symbols(expr('x & y & z | A')) == set() + assert predicate_symbols(expr('(x & B(z)) & Father(John) ==> Farmer(y) | A')) == { + ('B', 1), + ('Father', 1), + ('Farmer', 1)} + assert predicate_symbols(expr('(x & B(x, y, z)) & F(G(x, y), x) ==> P(Q(R(x, y)), x, y, z)')) == { + ('B', 3), + ('F', 2), + ('G', 2), + ('P', 4), + ('Q', 1), + ('R', 2)} def test_eliminate_implications(): From 3615e5fb4791960e15a73b0a220bc3a32be70a12 Mon Sep 17 00:00:00 2001 From: Chipe1 Date: Fri, 18 Aug 2017 02:16:50 +0530 Subject: [PATCH 2/3] Added FOIL --- knowledge.py | 116 +++++++++++++++++++++++++- tests/test_knowledge.py | 178 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+), 1 deletion(-) diff --git a/knowledge.py b/knowledge.py index 6330923bd..6fe09acd2 100644 --- a/knowledge.py +++ b/knowledge.py @@ -1,9 +1,12 @@ """Knowledge in learning, Chapter 19""" from random import shuffle +from math import log from utils import powerset from collections import defaultdict -from itertools import combinations +from itertools import combinations, product +from logic import (FolKB, constant_symbols, predicate_symbols, standardize_variables, + variables, is_definite_clause, subst, expr, Expr) # ______________________________________________________________________________ @@ -231,6 +234,117 @@ def consistent_det(A, E): # ______________________________________________________________________________ +class FOIL_container(FolKB): + """Holds the kb and other necessary elements required by FOIL""" + + def __init__(self, clauses=[]): + self.const_syms = set() + self.pred_syms = set() + FolKB.__init__(self, clauses) + + def tell(self, sentence): + if is_definite_clause(sentence): + self.clauses.append(sentence) + self.const_syms.update(constant_symbols(sentence)) + self.pred_syms.update(predicate_symbols(sentence)) + else: + raise Exception("Not a definite clause: {}".format(sentence)) + + def foil(self, examples, target): + """Learns a list of first-order horn clauses + 'examples' is a tuple: (positive_examples, negative_examples). + positive_examples and negative_examples are both lists which contain substitutions.""" + clauses = [] + + pos_examples = examples[0] + neg_examples = examples[1] + + while pos_examples: + clause, extended_pos_examples = self.new_clause((pos_examples, neg_examples), target) + # remove positive examples covered by clause + pos_examples = self.update_examples(target, pos_examples, extended_pos_examples) + clauses.append(clause) + + return clauses + + def new_clause(self, examples, target): + """Finds a horn clause which satisfies part of the positive + examples but none of the negative examples. + The horn clause is specified as [consequent, list of antecedents] + Return value is the tuple (horn_clause, extended_positive_examples)""" + clause = [target, []] + # [positive_examples, negative_examples] + extended_examples = examples + while extended_examples[1]: + l = self.choose_literal(self.new_literals(clause), extended_examples) + clause[1].append(l) + extended_examples = [sum([list(self.extend_example(example, l)) for example in + extended_examples[i]], []) for i in range(2)] + + return (clause, extended_examples[0]) + + def extend_example(self, example, literal): + """Generates extended examples which satisfy the literal""" + # find all substitutions that satisfy literal + for s in self.ask_generator(subst(example, literal)): + s.update(example) + yield s + + def new_literals(self, clause): + """Generates new literals based on known predicate symbols. + Generated literal must share atleast one variable with clause""" + share_vars = variables(clause[0]) + for l in clause[1]: + share_vars.update(variables(l)) + + for pred, arity in self.pred_syms: + new_vars = {standardize_variables(expr('x')) for _ in range(arity - 1)} + for args in product(share_vars.union(new_vars), repeat=arity): + if any(var in share_vars for var in args): + yield Expr(pred, *[var for var in args]) + + def choose_literal(self, literals, examples): + """Chooses the best literal based on the information gain""" + def gain(l): + pre_pos = len(examples[0]) + pre_neg = len(examples[1]) + extended_examples = [sum([list(self.extend_example(example, l)) for example in + examples[i]], []) for i in range(2)] + post_pos = len(extended_examples[0]) + post_neg = len(extended_examples[1]) + if pre_pos + pre_neg == 0 or post_pos + post_neg == 0: + return -1 + + # number of positive example that are represented in extended_examples + T = 0 + for example in examples[0]: + def represents(d): + return all(d[x] == example[x] for x in example) + if any(represents(l_) for l_ in extended_examples[0]): + T += 1 + + return T * log((post_pos*(pre_pos + pre_neg) + 1e-4) / ((post_pos + post_neg)*pre_pos)) + + return max(literals, key=gain) + + def update_examples(self, target, examples, extended_examples): + """Adds to the kb those examples what are represented in extended_examples + List of omitted examples is returned""" + uncovered = [] + for example in examples: + def represents(d): + return all(d[x] == example[x] for x in example) + if any(represents(l) for l in extended_examples): + self.tell(subst(example, target)) + else: + uncovered.append(example) + + return uncovered + + +# ______________________________________________________________________________ + + def check_all_consistency(examples, h): """Check for the consistency of all examples under h""" for e in examples: diff --git a/tests/test_knowledge.py b/tests/test_knowledge.py index 764777e7d..89fe479a0 100644 --- a/tests/test_knowledge.py +++ b/tests/test_knowledge.py @@ -1,4 +1,5 @@ from knowledge import * +from utils import expr import random random.seed("aima-python") @@ -57,6 +58,135 @@ def test_minimal_consistent_det(): assert minimal_consistent_det(conductance, {'Mass', 'Temp', 'Size'}) == {'Mass', 'Temp', 'Size'} +def test_extend_example(): + assert list(test_network.extend_example({x: A, y: B}, expr('Conn(x, z)'))) == [ + {x: A, y: B, z: B}, {x: A, y: B, z: D}] + assert list(test_network.extend_example({x: G}, expr('Conn(x, y)'))) == [{x: G, y: I}] + assert list(test_network.extend_example({x: C}, expr('Conn(x, y)'))) == [] + assert len(list(test_network.extend_example({}, expr('Conn(x, y)')))) == 10 + assert len(list(small_family.extend_example({x: expr('Andrew')}, expr('Father(x, y)')))) == 2 + assert len(list(small_family.extend_example({x: expr('Andrew')}, expr('Mother(x, y)')))) == 0 + assert len(list(small_family.extend_example({x: expr('Andrew')}, expr('Female(y)')))) == 6 + + +def test_new_literals(): + assert len(list(test_network.new_literals([expr('p | q'), [expr('p')]]))) == 8 + assert len(list(test_network.new_literals([expr('p'), [expr('q'), expr('p | r')]]))) == 15 + assert len(list(small_family.new_literals([expr('p'), []]))) == 8 + assert len(list(small_family.new_literals([expr('p & q'), []]))) == 20 + + +def test_choose_literal(): + literals = [expr('Conn(p, q)'), expr('Conn(x, z)'), expr('Conn(r, s)'), expr('Conn(t, y)')] + examples_pos = [{x: A, y: B}, {x: A, y: D}] + examples_neg = [{x: A, y: C}, {x: C, y: A}, {x: C, y: B}, {x: A, y: I}] + assert test_network.choose_literal(literals, [examples_pos, examples_neg]) == expr('Conn(x, z)') + literals = [expr('Conn(x, p)'), expr('Conn(p, x)'), expr('Conn(p, q)')] + examples_pos = [{x: C}, {x: F}, {x: I}] + examples_neg = [{x: D}, {x: A}, {x: B}, {x: G}] + assert test_network.choose_literal(literals, [examples_pos, examples_neg]) == expr('Conn(p, x)') + literals = [expr('Father(x, y)'), expr('Father(y, x)'), expr('Mother(x, y)'), expr('Mother(x, y)')] + examples_pos = [{x: expr('Philip')}, {x: expr('Mark')}, {x: expr('Peter')}] + examples_neg = [{x: expr('Elizabeth')}, {x: expr('Sarah')}] + assert small_family.choose_literal(literals, [examples_pos, examples_neg]) == expr('Father(x, y)') + literals = [expr('Father(x, y)'), expr('Father(y, x)'), expr('Male(x)')] + examples_pos = [{x: expr('Philip')}, {x: expr('Mark')}, {x: expr('Andrew')}] + examples_neg = [{x: expr('Elizabeth')}, {x: expr('Sarah')}] + assert small_family.choose_literal(literals, [examples_pos, examples_neg]) == expr('Male(x)') + + +def test_new_clause(): + target = expr('Open(x, y)') + examples_pos = [{x: B}, {x: A}, {x: G}] + examples_neg = [{x: C}, {x: F}, {x: I}] + clause = test_network.new_clause([examples_pos, examples_neg], target)[0][1] + assert len(clause) == 1 and clause[0].op == 'Conn' and clause[0].args[0] == x + target = expr('Flow(x, y)') + examples_pos = [{x: B}, {x: D}, {x: E}, {x: G}] + examples_neg = [{x: A}, {x: C}, {x: F}, {x: I}, {x: H}] + clause = test_network.new_clause([examples_pos, examples_neg], target)[0][1] + assert len(clause) == 2 and \ + ((clause[0].args[0] == x and clause[1].args[1] == x) or \ + (clause[0].args[1] == x and clause[1].args[0] == x)) + + +def test_foil(): + target = expr('Reach(x, y)') + examples_pos = [{x: A, y: B}, + {x: A, y: C}, + {x: A, y: D}, + {x: A, y: E}, + {x: A, y: F}, + {x: A, y: G}, + {x: A, y: I}, + {x: B, y: C}, + {x: D, y: C}, + {x: D, y: E}, + {x: D, y: F}, + {x: D, y: G}, + {x: D, y: I}, + {x: E, y: F}, + {x: E, y: G}, + {x: E, y: I}, + {x: G, y: I}, + {x: H, y: G}, + {x: H, y: I}] + nodes = {A, B, C, D, E, F, G, H, I} + examples_neg = [example for example in [{x: a, y: b} for a in nodes for b in nodes] + if example not in examples_pos] + ## TODO: Modify FOIL to recursively check for satisfied positive examples +# clauses = test_network.foil([examples_pos, examples_neg], target) +# assert len(clauses) == 2 + target = expr('Parent(x, y)') + examples_pos = [{x: expr('Elizabeth'), y: expr('Anne')}, + {x: expr('Elizabeth'), y: expr('Andrew')}, + {x: expr('Philip'), y: expr('Anne')}, + {x: expr('Philip'), y: expr('Andrew')}, + {x: expr('Anne'), y: expr('Peter')}, + {x: expr('Anne'), y: expr('Zara')}, + {x: expr('Mark'), y: expr('Peter')}, + {x: expr('Mark'), y: expr('Zara')}, + {x: expr('Andrew'), y: expr('Beatrice')}, + {x: expr('Andrew'), y: expr('Eugenie')}, + {x: expr('Sarah'), y: expr('Beatrice')}, + {x: expr('Sarah'), y: expr('Eugenie')}] + examples_neg = [{x: expr('Anne'), y: expr('Eugenie')}, + {x: expr('Beatrice'), y: expr('Eugenie')}, + {x: expr('Mark'), y: expr('Elizabeth')}, + {x: expr('Beatrice'), y: expr('Philip')}] + clauses = small_family.foil([examples_pos, examples_neg], target) + assert len(clauses) == 2 and \ + ((clauses[0][1][0] == expr('Father(x, y)') and clauses[1][1][0] == expr('Mother(x, y)')) or \ + (clauses[1][1][0] == expr('Father(x, y)') and clauses[0][1][0] == expr('Mother(x, y)'))) + target = expr('Grandparent(x, y)') + examples_pos = [{x: expr('Elizabeth'), y: expr('Peter')}, + {x: expr('Elizabeth'), y: expr('Zara')}, + {x: expr('Elizabeth'), y: expr('Beatrice')}, + {x: expr('Elizabeth'), y: expr('Eugenie')}, + {x: expr('Philip'), y: expr('Peter')}, + {x: expr('Philip'), y: expr('Zara')}, + {x: expr('Philip'), y: expr('Beatrice')}, + {x: expr('Philip'), y: expr('Eugenie')}] + examples_neg = [{x: expr('Anne'), y: expr('Eugenie')}, + {x: expr('Beatrice'), y: expr('Eugenie')}, + {x: expr('Elizabeth'), y: expr('Andrew')}, + {x: expr('Philip'), y: expr('Anne')}, + {x: expr('Philip'), y: expr('Andrew')}, + {x: expr('Anne'), y: expr('Peter')}, + {x: expr('Anne'), y: expr('Zara')}, + {x: expr('Mark'), y: expr('Peter')}, + {x: expr('Mark'), y: expr('Zara')}, + {x: expr('Andrew'), y: expr('Beatrice')}, + {x: expr('Andrew'), y: expr('Eugenie')}, + {x: expr('Sarah'), y: expr('Beatrice')}, + {x: expr('Mark'), y: expr('Elizabeth')}, + {x: expr('Beatrice'), y: expr('Philip')}] +# clauses = small_family.foil([examples_pos, examples_neg], target) +# assert len(clauses) == 2 and \ +# ((clauses[0][1][0] == expr('Father(x, y)') and clauses[1][1][0] == expr('Mother(x, y)')) or \ +# (clauses[1][1][0] == expr('Father(x, y)') and clauses[0][1][0] == expr('Mother(x, y)'))) + + party = [ {'Pizza': 'Yes', 'Soda': 'No', 'GOAL': True}, {'Pizza': 'Yes', 'Soda': 'Yes', 'GOAL': True}, @@ -104,3 +234,51 @@ def r_example(Alt, Bar, Fri, Hun, Pat, Price, Rain, Res, Type, Est, GOAL): r_example('No', 'No', 'No', 'No', 'None', '$', 'No', 'No', 'Thai', '0-10', False), r_example('Yes', 'Yes', 'Yes', 'Yes', 'Full', '$', 'No', 'No', 'Burger', '30-60', True) ] + +""" +A H +|\ /| +| \ / | +v v v v +B D-->E-->G-->I +| / | +| / | +vv v +C F +""" +test_network = FOIL_container([expr("Conn(A, B)"), + expr("Conn(A ,D)"), + expr("Conn(B, C)"), + expr("Conn(D, C)"), + expr("Conn(D, E)"), + expr("Conn(E ,F)"), + expr("Conn(E, G)"), + expr("Conn(G, I)"), + expr("Conn(H, G)"), + expr("Conn(H, I)")]) + +small_family = FOIL_container([expr("Mother(Anne, Peter)"), + expr("Mother(Anne, Zara)"), + expr("Mother(Sarah, Beatrice)"), + expr("Mother(Sarah, Eugenie)"), + expr("Father(Mark, Peter)"), + expr("Father(Mark, Zara)"), + expr("Father(Andrew, Beatrice)"), + expr("Father(Andrew, Eugenie)"), + expr("Father(Philip, Anne)"), + expr("Father(Philip, Andrew)"), + expr("Mother(Elizabeth, Anne)"), + expr("Mother(Elizabeth, Andrew)"), + expr("Male(Philip)"), + expr("Male(Mark)"), + expr("Male(Andrew)"), + expr("Male(Peter)"), + expr("Female(Elizabeth)"), + expr("Female(Anne)"), + expr("Female(Sarah)"), + expr("Female(Zara)"), + expr("Female(Beatrice)"), + expr("Female(Eugenie)"), +]) + +A, B, C, D, E, F, G, H, I, x, y, z = map(expr, 'ABCDEFGHIxyz') From 1c39c755e55cfa5a75f2f8c1cc8922d7116d6695 Mon Sep 17 00:00:00 2001 From: Chipe1 Date: Fri, 18 Aug 2017 02:18:49 +0530 Subject: [PATCH 3/3] Updated README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3730340db..34e1e006a 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Here is a table of algorithms, the figure, name of the algorithm in the book and | 19.2 | Current-Best-Learning | `current_best_learning` | [`knowledge.py`](knowledge.py) | Done | | 19.3 | Version-Space-Learning | `version_space_learning` | [`knowledge.py`](knowledge.py) | Done | | 19.8 | Minimal-Consistent-Det | `minimal_consistent_det` | [`knowledge.py`](knowledge.py) | Done | -| 19.12 | FOIL | | | +| 19.12 | FOIL | `FOIL_container` | [`knowledge.py`](knowledge.py) | Done | | 21.2 | Passive-ADP-Agent | `PassiveADPAgent` | [`rl.py`][rl] | Done | | 21.4 | Passive-TD-Agent | `PassiveTDAgent` | [`rl.py`][rl] | Done | | 21.8 | Q-Learning-Agent | `QLearningAgent` | [`rl.py`][rl] | Done |