From e9dd7258593ab52524147fcb6f90c8a2bcd658ca Mon Sep 17 00:00:00 2001 From: Devesh Sawant Date: Thu, 27 Dec 2018 00:04:08 +0530 Subject: [PATCH 1/4] Grammar and typo fixes in logic notebook --- logic.ipynb | 109 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 32 deletions(-) diff --git a/logic.ipynb b/logic.ipynb index f93e0e4c5..062ffede2 100644 --- a/logic.ipynb +++ b/logic.ipynb @@ -13,7 +13,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This Jupyter notebook acts as supporting material for topics covered in __Chapter 6 Logical Agents__, __Chapter 7 First-Order Logic__ and __Chapter 8 Inference in First-Order Logic__ of the book *[Artificial Intelligence: A Modern Approach](http://aima.cs.berkeley.edu)*. We make use the implementations in the [logic.py](https://github.com/aimacode/aima-python/blob/master/logic.py) module. See the [intro notebook](https://github.com/aimacode/aima-python/blob/master/intro.ipynb) for instructions.\n", + "This Jupyter notebook acts as supporting material for topics covered in __Chapter 6 Logical Agents__, __Chapter 7 First-Order Logic__ and __Chapter 8 Inference in First-Order Logic__ of the book *[Artificial Intelligence: A Modern Approach](http://aima.cs.berkeley.edu)*. We make use of the implementations in the [logic.py](https://github.com/aimacode/aima-python/blob/master/logic.py) module. See the [intro notebook](https://github.com/aimacode/aima-python/blob/master/intro.ipynb) for instructions.\n", "\n", "Let's first import everything from the `logic` module." ] @@ -21,7 +21,9 @@ { "cell_type": "code", "execution_count": 1, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "from utils import *\n", @@ -98,7 +100,9 @@ { "cell_type": "code", "execution_count": 3, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "(x, y, P, Q, f) = symbols('x, y, P, Q, f')" @@ -426,7 +430,9 @@ { "cell_type": "code", "execution_count": 15, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "wumpus_kb = PropKB()" @@ -444,7 +450,9 @@ { "cell_type": "code", "execution_count": 16, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "P11, P12, P21, P22, P31, B11, B21 = expr('P11, P12, P21, P22, P31, B11, B21')" @@ -461,7 +469,9 @@ { "cell_type": "code", "execution_count": 17, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "wumpus_kb.tell(~P11)" @@ -477,7 +487,9 @@ { "cell_type": "code", "execution_count": 18, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "wumpus_kb.tell(B11 | '<=>' | ((P12 | P21)))\n", @@ -494,7 +506,9 @@ { "cell_type": "code", "execution_count": 19, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "wumpus_kb.tell(~B11)\n", @@ -564,7 +578,7 @@ "
\n", "The purpose of a KB agent is to provide a level of abstraction over knowledge-base manipulation and is to be used as a base class for agents that work on a knowledge base.\n", "
\n", - "Given a percept, the KB agent adds the percept to its knowledge base, asks the knowledge base for the best action, and tells the knowledge base that it has infact taken that action.\n", + "Given a percept, the KB agent adds the percept to its knowledge base, asks the knowledge base for the best action, and tells the knowledge base that it has in fact taken that action.\n", "
\n", "Our implementation of `KB-Agent` is encapsulated in a class `KB_AgentProgram` which inherits from the `KB` class.\n", "
\n", @@ -1168,7 +1182,7 @@ "source": [ "### Proof by Resolution\n", "Recall that our goal is to check whether $\\text{KB} \\vDash \\alpha$ i.e. is $\\text{KB} \\implies \\alpha$ true in every model. Suppose we wanted to check if $P \\implies Q$ is valid. We check the satisfiability of $\\neg (P \\implies Q)$, which can be rewritten as $P \\land \\neg Q$. If $P \\land \\neg Q$ is unsatisfiable, then $P \\implies Q$ must be true in all models. This gives us the result \"$\\text{KB} \\vDash \\alpha$ if and only if $\\text{KB} \\land \\neg \\alpha$ is unsatisfiable\".
\n", - "This technique corresponds to proof by contradiction, a standard mathematical proof technique. We assume $\\alpha$ to be false and show that this leads to a contradiction with known axioms in $\\text{KB}$. We obtain a contradiction by making valid inferences using inference rules. In this proof we use a single inference rule, resolution which states $(l_1 \\lor \\dots \\lor l_k) \\land (m_1 \\lor \\dots \\lor m_n) \\land (l_i \\iff \\neg m_j) \\implies l_1 \\lor \\dots \\lor l_{i - 1} \\lor l_{i + 1} \\lor \\dots \\lor l_k \\lor m_1 \\lor \\dots \\lor m_{j - 1} \\lor m_{j + 1} \\lor \\dots \\lor m_n$. Applying the resolution yeilds us a clause which we add to the KB. We keep doing this until:\n", + "This technique corresponds to proof by contradiction, a standard mathematical proof technique. We assume $\\alpha$ to be false and show that this leads to a contradiction with known axioms in $\\text{KB}$. We obtain a contradiction by making valid inferences using inference rules. In this proof we use a single inference rule, resolution which states $(l_1 \\lor \\dots \\lor l_k) \\land (m_1 \\lor \\dots \\lor m_n) \\land (l_i \\iff \\neg m_j) \\implies l_1 \\lor \\dots \\lor l_{i - 1} \\lor l_{i + 1} \\lor \\dots \\lor l_k \\lor m_1 \\lor \\dots \\lor m_{j - 1} \\lor m_{j + 1} \\lor \\dots \\lor m_n$. Applying the resolution yields us a clause which we add to the KB. We keep doing this until:\n", "\n", "* There are no new clauses that can be added, in which case $\\text{KB} \\nvDash \\alpha$.\n", "* Two clauses resolve to yield the empty clause, in which case $\\text{KB} \\vDash \\alpha$.\n", @@ -2009,10 +2023,9 @@ "metadata": {}, "source": [ "### Forward and backward chaining\n", - "Previously, we said we will look at two algorithms to check if a sentence is entailed by the `KB`, \n", - "but here's a third one. \n", + "Previously, we said we will look at two algorithms to check if a sentence is entailed by the `KB`. Here's a third one. \n", "The difference here is that our goal now is to determine if a knowledge base of definite clauses entails a single proposition symbol *q* - the query.\n", - "There is a catch however, the knowledge base can only contain **Horn clauses**.\n", + "There is a catch however - the knowledge base can only contain **Horn clauses**.\n", "
\n", "#### Horn Clauses\n", "Horn clauses can be defined as a *disjunction* of *literals* with **at most** one positive literal. \n", @@ -2346,7 +2359,9 @@ { "cell_type": "code", "execution_count": 41, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "clauses = ['(B & F)==>E', \n", @@ -2370,7 +2385,9 @@ { "cell_type": "code", "execution_count": 42, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "definite_clauses_KB = PropDefiniteKB()\n", @@ -2800,7 +2817,9 @@ { "cell_type": "code", "execution_count": 49, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "A, B, C, D = expr('A, B, C, D')" @@ -2932,7 +2951,7 @@ "This is similar to finding a neighboring state in the `hill_climbing` algorithm.\n", "
\n", "The symbol to be flipped is decided by an evaluation function that counts the number of unsatisfied clauses.\n", - "Sometimes, symbols are also flipped randomly, to avoid local optima. A subtle balance between greediness and randomness is required. Alternatively, some versions of the algorithm restart with a completely new random assignment if no solution has been found for too long, as a way of getting out of local minima of numbers of unsatisfied clauses.\n", + "Sometimes, symbols are also flipped randomly to avoid local optima. A subtle balance between greediness and randomness is required. Alternatively, some versions of the algorithm restart with a completely new random assignment if no solution has been found for too long as a way of getting out of local minima of numbers of unsatisfied clauses.\n", "
\n", "
\n", "Let's have a look at the algorithm." @@ -3097,7 +3116,9 @@ { "cell_type": "code", "execution_count": 56, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "A, B, C, D = expr('A, B, C, D')" @@ -3173,7 +3194,9 @@ { "cell_type": "code", "execution_count": 60, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "WalkSAT([A & B, C | D, ~(D | B)], 0.5, 1000)" @@ -3198,7 +3221,9 @@ { "cell_type": "code", "execution_count": 61, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "def WalkSAT_CNF(sentence, p=0.5, max_flips=10000):\n", @@ -3248,7 +3273,9 @@ { "cell_type": "code", "execution_count": 63, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "sentence_1 = A |'<=>'| B\n", @@ -3602,7 +3629,9 @@ { "cell_type": "code", "execution_count": 69, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "clauses = []" @@ -3629,7 +3658,9 @@ { "cell_type": "code", "execution_count": 70, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "clauses.append(expr(\"(American(x) & Weapon(y) & Sells(x, y, z) & Hostile(z)) ==> Criminal(x)\"))" @@ -3648,7 +3679,9 @@ { "cell_type": "code", "execution_count": 71, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "clauses.append(expr(\"Enemy(Nono, America)\"))" @@ -3667,7 +3700,9 @@ { "cell_type": "code", "execution_count": 72, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "clauses.append(expr(\"Owns(Nono, M1)\"))\n", @@ -3689,7 +3724,9 @@ { "cell_type": "code", "execution_count": 73, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "clauses.append(expr(\"(Missile(x) & Owns(Nono, x)) ==> Sells(West, x, Nono)\"))" @@ -3708,7 +3745,9 @@ { "cell_type": "code", "execution_count": 74, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "clauses.append(expr(\"American(West)\"))" @@ -3726,7 +3765,9 @@ { "cell_type": "code", "execution_count": 75, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "clauses.append(expr(\"Missile(x) ==> Weapon(x)\"))\n", @@ -3743,7 +3784,9 @@ { "cell_type": "code", "execution_count": 76, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "crime_kb = FolKB(clauses)" @@ -4039,7 +4082,7 @@ "metadata": {}, "source": [ "### Forward Chaining Algorithm\n", - "We consider the simple forward-chaining algorithm presented in Figure 9.3. We look at each rule in the knoweldge base and see if the premises can be satisfied. This is done by finding a substitution which unifies each of the premise with a clause in the `KB`. If we are able to unify the premises, the conclusion (with the corresponding substitution) is added to the `KB`. This inferencing process is repeated until either the query can be answered or till no new sentences can be added. We test if the newly added clause unifies with the query in which case the substitution yielded by `unify` is an answer to the query. If we run out of sentences to infer, this means the query was a failure.\n", + "We consider the simple forward-chaining algorithm presented in Figure 9.3. We look at each rule in the knowledge base and see if the premises can be satisfied. This is done by finding a substitution which unifies each of the premise with a clause in the `KB`. If we are able to unify the premises, the conclusion (with the corresponding substitution) is added to the `KB`. This inferencing process is repeated until either the query can be answered or till no new sentences can be added. We test if the newly added clause unifies with the query in which case the substitution yielded by `unify` is an answer to the query. If we run out of sentences to infer, this means the query was a failure.\n", "\n", "The function `fol_fc_ask` is a generator which yields all substitutions which validate the query." ] @@ -4514,7 +4557,9 @@ { "cell_type": "code", "execution_count": 89, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "# Rebuild KB because running fol_fc_ask would add new facts to the KB\n", @@ -4951,7 +4996,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.4" + "version": "3.6.1" } }, "nbformat": 4, From f579719fd5172c1cb6b40b412ca8d2a1a6aaa9f0 Mon Sep 17 00:00:00 2001 From: devesh Date: Fri, 3 Jul 2020 00:48:11 +0530 Subject: [PATCH 2/4] Modified least-constrained heuristic with correct implementation --- csp.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/csp.py b/csp.py index 9cfdafdef..7ca59a995 100644 --- a/csp.py +++ b/csp.py @@ -83,6 +83,29 @@ def conflict(var2): return count(conflict(v) for v in self.neighbors[var]) + def countlostvalues(self, var, val, assignment): + """Returns the no. of constrained assignments on neighbours that conflict with this variable assignment""" + assign = {} + assign[var] = val + countvals = 0 + for v in self.neighbors[var]: + if v not in assignment: + for dval in self.domains[v]: + if not self.constraints(var, val, v, dval): + countvals += 1 + + for v in self.neighbors: + if v is not var: + for v2 in self.neighbors[v]: + if v2 is var and v not in assignment: + for dval in self.domains[v]: + if not self.constraints(v, dval, var, val): + countvals += 1 + + return countvals + + + def display(self, assignment): """Show a human-readable representation of the CSP.""" # Subclasses can print in a prettier way, or display with a GUI @@ -371,7 +394,7 @@ def unordered_domain_values(var, assignment, csp): def lcv(var, assignment, csp): """Least-constraining-values heuristic.""" - return sorted(csp.choices(var), key=lambda val: csp.nconflicts(var, val, assignment)) + return sorted(csp.choices(var), key=lambda val: csp.countlostvalues(var, val, assignment)) # Inference From 063139eca352db6e02f3f232a6dd0411775526c4 Mon Sep 17 00:00:00 2001 From: devesh Date: Sun, 5 Jul 2020 22:41:49 +0530 Subject: [PATCH 3/4] Added test for count_lost_values & modified existing ones --- csp.py | 2 +- tests/test_csp.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/csp.py b/csp.py index 7ca59a995..41083edf8 100644 --- a/csp.py +++ b/csp.py @@ -91,7 +91,7 @@ def countlostvalues(self, var, val, assignment): for v in self.neighbors[var]: if v not in assignment: for dval in self.domains[v]: - if not self.constraints(var, val, v, dval): + if not self.constraints(var, val, v, dval): countvals += 1 for v in self.neighbors: diff --git a/tests/test_csp.py b/tests/test_csp.py index a070cd531..9483bcaf8 100644 --- a/tests/test_csp.py +++ b/tests/test_csp.py @@ -34,6 +34,12 @@ def test_csp_nconflicts(): val = 'B' assert map_coloring_test.nconflicts(var, val, assignment) == 0 +def test_csp_countlostvalues(): + map_coloring_test = MapColoringCSP(list('RGB'), 'A: B C; B: C; C: ') + assignment = {'A': 'G'} + var = 'C' + val = 'R' + assert map_coloring_test.countlostvalues(var,val, assignment) == 2 def test_csp_actions(): map_coloring_test = MapColoringCSP(list('123'), 'A: B C; B: C; C: ') @@ -331,13 +337,13 @@ def test_lcv(): var = 'B' - assert lcv(var, assignment, csp) == [4, 0, 1, 2, 3, 5] - assignment = {'A': 1, 'C': 3} + assert lcv(var, assignment, csp) == [0, 2, 4, 1, 3, 5] + assignment = {'A': 1} constraints = lambda X, x, Y, y: (x + y) % 2 == 0 and (x + y) < 5 csp = CSP(variables=None, domains=domains, neighbors=neighbors, constraints=constraints) - assert lcv(var, assignment, csp) == [1, 3, 0, 2, 4, 5] + assert lcv(var, assignment, csp) == [0, 1, 2, 3, 4, 5] def test_forward_checking(): From 7705716d2074e625c4cf0e89b695c95af35ae975 Mon Sep 17 00:00:00 2001 From: Donato Meoli Date: Fri, 26 Jun 2026 20:05:08 +0200 Subject: [PATCH 4/4] Implement least-constraining-value heuristic correctly lcv previously sorted by nconflicts (conflicts with already-assigned neighbours), which is degenerate during a consistent search. Add CSP.count_lost_values(var, val, assignment) - the number of values ruled out in unassigned neighbours' domains - and order lcv by it, matching the AIMA definition. Clean snake_case implementation; tests updated. Closes #1089. --- csp.py | 31 ++++++++----------------------- tests/test_csp.py | 4 ++-- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/csp.py b/csp.py index 36649b122..b0645f368 100644 --- a/csp.py +++ b/csp.py @@ -83,28 +83,13 @@ def conflict(var2): return count(conflict(v) for v in self.neighbors[var]) - def countlostvalues(self, var, val, assignment): - """Returns the no. of constrained assignments on neighbours that conflict with this variable assignment""" - assign = {} - assign[var] = val - countvals = 0 - for v in self.neighbors[var]: - if v not in assignment: - for dval in self.domains[v]: - if not self.constraints(var, val, v, dval): - countvals += 1 - - for v in self.neighbors: - if v is not var: - for v2 in self.neighbors[v]: - if v2 is var and v not in assignment: - for dval in self.domains[v]: - if not self.constraints(v, dval, var, val): - countvals += 1 - - return countvals - - + def count_lost_values(self, var, val, assignment): + """Return how many values would be ruled out in the domains of the + unassigned neighbours of var if var were assigned val (the count used + by the least-constraining-value heuristic).""" + return count(not self.constraints(var, val, neighbor, dval) + for neighbor in self.neighbors[var] if neighbor not in assignment + for dval in self.domains[neighbor]) def display(self, assignment): """Show a human-readable representation of the CSP.""" @@ -394,7 +379,7 @@ def unordered_domain_values(var, assignment, csp): def lcv(var, assignment, csp): """Least-constraining-values heuristic.""" - return sorted(csp.choices(var), key=lambda val: csp.countlostvalues(var, val, assignment)) + return sorted(csp.choices(var), key=lambda val: csp.count_lost_values(var, val, assignment)) # Inference diff --git a/tests/test_csp.py b/tests/test_csp.py index c77913383..e78bb05ae 100644 --- a/tests/test_csp.py +++ b/tests/test_csp.py @@ -35,12 +35,12 @@ def test_csp_nconflicts(): val = 'B' assert map_coloring_test.nconflicts(var, val, assignment) == 0 -def test_csp_countlostvalues(): +def test_csp_count_lost_values(): map_coloring_test = MapColoringCSP(list('RGB'), 'A: B C; B: C; C: ') assignment = {'A': 'G'} var = 'C' val = 'R' - assert map_coloring_test.countlostvalues(var,val, assignment) == 2 + assert map_coloring_test.count_lost_values(var, val, assignment) == 1 def test_csp_actions(): map_coloring_test = MapColoringCSP(list('123'), 'A: B C; B: C; C: ')